cc-os/plugins/os-adr/lib/adr/record.rb

83 lines
2.4 KiB
Ruby

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