require "yaml" require "date" module Adr # One ADR file: frontmatter + titled body sections. Parses and serializes; # knows nothing about the filesystem. class Record ParseError = Class.new(StandardError) FRONTMATTER_ORDER = %w[ id date status supersedes superseded-by affected-paths affected-components migration_confidence ].freeze QUOTED_FIELDS = %w[id supersedes superseded-by].freeze SECTION_ORDER = ["Context", "Decision", "Consequences", "Alternatives rejected"].freeze attr_reader :frontmatter, :title, :sections def self.parse(text) match = text.match(/\A---\n(.*?)\n---\n(.*)\z/m) raise ParseError, "no frontmatter block found" unless match frontmatter = YAML.safe_load(match[1], permitted_classes: [Date]) || {} body = match[2] heading = body[/^# +(.+)$/, 1].to_s.strip title = heading.sub(/\A\S+\s+—\s+/, "") sections = {} body.scan(/^## ([^\n]+)\n(.*?)(?=^## |\z)/m) do |name, content| sections[name.strip] = content.strip end new(frontmatter: frontmatter, title: title, sections: sections) end def initialize(frontmatter:, title:, sections:) @frontmatter = frontmatter @title = title @sections = sections end def id = frontmatter["id"]&.to_s def status = frontmatter["status"] def date = frontmatter["date"] def supersede_with(new_id) frontmatter["superseded-by"] = new_id frontmatter["status"] = "Superseded" end def to_markdown lines = ["---"] FRONTMATTER_ORDER.each do |field| next unless frontmatter.key?(field) lines << "#{field}: #{format_value(field, frontmatter[field])}".rstrip end (frontmatter.keys - FRONTMATTER_ORDER).each do |field| lines << "#{field}: #{format_value(field, frontmatter[field])}".rstrip end lines << "---" << "" << "# #{id} — #{title}" sections.each do |name, content| lines << "" << "## #{name}" << "" << content end lines.join("\n") + "\n" end private def format_value(field, value) case value when nil then "" when Array then "[#{value.join(', ')}]" when Date then value.iso8601 else text = value.to_s if QUOTED_FIELDS.include?(field) || text.match?(/[:#]/) text.inspect else text end end end end end