83 lines
2.4 KiB
Ruby
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
|