Add existing code
This commit is contained in:
parent
e1f7be8e93
commit
37a9f56d10
12 changed files with 662 additions and 0 deletions
63
exe/referator
Executable file
63
exe/referator
Executable file
|
@ -0,0 +1,63 @@
|
|||
#!/usr/bin/env ruby
|
||||
# frozen_string_literal: true
|
||||
|
||||
lib = File.expand_path('../lib', __dir__).freeze
|
||||
$LOAD_PATH.unshift lib unless $LOAD_PATH.include? lib
|
||||
|
||||
require 'json'
|
||||
require 'referator'
|
||||
|
||||
$stdin.sync = true
|
||||
$stdout.sync = true
|
||||
|
||||
def parse_command(line)
|
||||
line = String(line).chomp
|
||||
command = line[0...line.index(' ')].freeze
|
||||
rest = line[command.length..].strip.freeze
|
||||
[command, rest].freeze
|
||||
end
|
||||
|
||||
workdir = ARGV.first
|
||||
workdir = Dir.pwd if String(workdir).strip.empty?
|
||||
workdir = File.expand_path(workdir).freeze
|
||||
|
||||
config = Referator::Config.new workdir
|
||||
footnotes = Referator::Footnotes.new config
|
||||
|
||||
while (line = $stdin.gets)
|
||||
command, rest = parse_command line
|
||||
case command
|
||||
when 'REGISTER_FORMAT'
|
||||
data = JSON.parse rest
|
||||
config.formats.register(
|
||||
String(data['name']).to_sym,
|
||||
**data.except('name').transform_keys(&:to_sym),
|
||||
)
|
||||
when 'REGISTER_KIND'
|
||||
data = JSON.parse rest
|
||||
config.kinds.register String(data).to_sym
|
||||
when 'ADD_REF'
|
||||
data = JSON.parse rest
|
||||
config.repo.add_ref(**data.transform_keys(&:to_sym))
|
||||
when 'ADD_NOTE'
|
||||
config.freeze
|
||||
data = JSON.parse rest
|
||||
puts footnotes.add_note(
|
||||
String(data['kind']).to_sym,
|
||||
String(data['id']),
|
||||
).to_h.to_json
|
||||
when 'REF'
|
||||
config.freeze
|
||||
data = JSON.parse rest
|
||||
puts config.repo[
|
||||
String(data['kind']).to_sym,
|
||||
String(data['id']),
|
||||
].to_h.to_json
|
||||
when 'RENDER_FOOTNOTES'
|
||||
config.freeze
|
||||
data = JSON.parse rest
|
||||
puts footnotes.render(String(data).to_sym).to_json
|
||||
else
|
||||
raise 'Invalid command'
|
||||
end
|
||||
end
|
20
lib/referator.rb
Normal file
20
lib/referator.rb
Normal file
|
@ -0,0 +1,20 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'forwardable'
|
||||
require 'json'
|
||||
require 'open3'
|
||||
require 'pathname'
|
||||
|
||||
require_relative 'referator/config'
|
||||
require_relative 'referator/config/formats'
|
||||
require_relative 'referator/config/kinds'
|
||||
require_relative 'referator/config/repo'
|
||||
require_relative 'referator/footnotes'
|
||||
require_relative 'referator/note'
|
||||
require_relative 'referator/reference'
|
||||
require_relative 'referator/script'
|
||||
|
||||
module Referator
|
||||
NAME_RE = /\A\w+(_\w+)*\z/
|
||||
SLUG_RE = /\A\w+(-\w+)*\z/
|
||||
end
|
40
lib/referator/config.rb
Normal file
40
lib/referator/config.rb
Normal file
|
@ -0,0 +1,40 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Referator
|
||||
class Config
|
||||
attr_reader :workdir
|
||||
|
||||
def initialize(workdir)
|
||||
self.workdir = workdir
|
||||
end
|
||||
|
||||
def freeze
|
||||
formats.freeze
|
||||
kinds.freeze
|
||||
repo.freeze
|
||||
super
|
||||
end
|
||||
|
||||
def formats(&)
|
||||
@formats ||= Formats.new self
|
||||
end
|
||||
|
||||
def kinds(&)
|
||||
@kinds ||= Kinds.new self
|
||||
end
|
||||
|
||||
def repo(&)
|
||||
@repo ||= Repo.new self
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def workdir=(workdir)
|
||||
workdir = Pathname.new(workdir).expand_path.freeze
|
||||
raise 'Expected absolute path' unless workdir.absolute?
|
||||
raise 'Expected existing directory' unless workdir.directory?
|
||||
|
||||
@workdir = workdir
|
||||
end
|
||||
end
|
||||
end
|
62
lib/referator/config/formats.rb
Normal file
62
lib/referator/config/formats.rb
Normal file
|
@ -0,0 +1,62 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Referator
|
||||
class Config
|
||||
class Formats
|
||||
attr_reader :config, :names, :scripts
|
||||
|
||||
def initialize(config)
|
||||
self.config = config
|
||||
@names = []
|
||||
@scripts = {}
|
||||
end
|
||||
|
||||
def freeze
|
||||
@names.freeze
|
||||
@scripts.freeze
|
||||
super
|
||||
end
|
||||
|
||||
def register(name, **kwargs)
|
||||
validate_name! name
|
||||
raise 'Already exists' if names.index(name) || scripts.key?(name)
|
||||
|
||||
script = Script.new(**kwargs.merge(workdir: config.workdir,
|
||||
vars: script_vars.keys))
|
||||
|
||||
names << name
|
||||
scripts[name] = script
|
||||
nil
|
||||
end
|
||||
|
||||
def exists!(name)
|
||||
scripts[name] or raise 'Unknown format'
|
||||
nil
|
||||
end
|
||||
|
||||
def render(name, notes)
|
||||
exists! name
|
||||
scripts[name].call(**script_vars(format: name, notes: notes.to_json))
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def config=(config)
|
||||
unless config.instance_of? Config
|
||||
raise TypeError, "Expected #{Config}, got #{config.class}"
|
||||
end
|
||||
|
||||
@config = config
|
||||
end
|
||||
|
||||
def validate_name!(name)
|
||||
unless name.instance_of? Symbol
|
||||
raise TypeError, "Expected #{Symbol}, got #{name.class}"
|
||||
end
|
||||
raise 'Invalid name' unless NAME_RE.match? name
|
||||
end
|
||||
|
||||
def script_vars(format: nil, notes: nil) = { format:, notes: }.freeze
|
||||
end
|
||||
end
|
||||
end
|
49
lib/referator/config/kinds.rb
Normal file
49
lib/referator/config/kinds.rb
Normal file
|
@ -0,0 +1,49 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Referator
|
||||
class Config
|
||||
class Kinds
|
||||
attr_reader :config, :names
|
||||
|
||||
def initialize(config)
|
||||
self.config = config
|
||||
@names = []
|
||||
end
|
||||
|
||||
def freeze
|
||||
@names.freeze
|
||||
super
|
||||
end
|
||||
|
||||
def register(name)
|
||||
validate_name! name
|
||||
raise 'Already exists' if names.index(name)
|
||||
|
||||
names << name
|
||||
nil
|
||||
end
|
||||
|
||||
def exists!(name)
|
||||
names.index name or raise 'Unknown kind'
|
||||
nil
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def config=(config)
|
||||
unless config.instance_of? Config
|
||||
raise TypeError, "Expected #{Config}, got #{config.class}"
|
||||
end
|
||||
|
||||
@config = config
|
||||
end
|
||||
|
||||
def validate_name!(name)
|
||||
unless name.instance_of? Symbol
|
||||
raise TypeError, "Expected #{Symbol}, got #{name.class}"
|
||||
end
|
||||
raise 'Invalid name' unless NAME_RE.match? name
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
46
lib/referator/config/repo.rb
Normal file
46
lib/referator/config/repo.rb
Normal file
|
@ -0,0 +1,46 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Referator
|
||||
class Config
|
||||
class Repo
|
||||
attr_reader :config
|
||||
|
||||
def initialize(config)
|
||||
self.config = config
|
||||
@references = {}
|
||||
end
|
||||
|
||||
def freeze
|
||||
@references.freeze
|
||||
super
|
||||
end
|
||||
|
||||
def add_ref(**kwargs)
|
||||
reference = Reference.new(**kwargs)
|
||||
key = "#{reference.kind}-#{reference.id}".freeze
|
||||
raise 'Reference already exists' if @references.key? key
|
||||
|
||||
@references[key] = reference
|
||||
nil
|
||||
end
|
||||
|
||||
def [](kind, id)
|
||||
config.kinds.exists! kind
|
||||
key = "#{kind}-#{id}".freeze
|
||||
@references[key].tap do |reference|
|
||||
raise 'Invalid reference' if reference.nil?
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def config=(config)
|
||||
unless config.instance_of? Config
|
||||
raise TypeError, "Expected #{Config}, got #{config.class}"
|
||||
end
|
||||
|
||||
@config = config
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
36
lib/referator/footnotes.rb
Normal file
36
lib/referator/footnotes.rb
Normal file
|
@ -0,0 +1,36 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Referator
|
||||
class Footnotes
|
||||
attr_reader :config
|
||||
|
||||
def initialize(config)
|
||||
self.config = config
|
||||
|
||||
@notes = []
|
||||
end
|
||||
|
||||
def add_note(kind, id)
|
||||
config.kinds.exists! kind
|
||||
reference = config.repo[kind, id]
|
||||
new_note = Note.new self, @notes.count + 1, reference
|
||||
@notes.each { |note| return note if note.eql? new_note }
|
||||
@notes << new_note
|
||||
new_note
|
||||
end
|
||||
|
||||
def render(format)
|
||||
config.formats.render format, @notes.map(&:to_h).freeze
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def config=(config)
|
||||
unless config.instance_of? Config
|
||||
raise TypeError, "Expected #{Config}, got #{config.class}"
|
||||
end
|
||||
|
||||
@config = config
|
||||
end
|
||||
end
|
||||
end
|
54
lib/referator/note.rb
Normal file
54
lib/referator/note.rb
Normal file
|
@ -0,0 +1,54 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Referator
|
||||
class Note
|
||||
extend Forwardable
|
||||
|
||||
attr_reader :footnotes, :index, :reference
|
||||
|
||||
def_delegators :reference, :anchor, :fragment
|
||||
|
||||
def initialize(footnotes, index, reference)
|
||||
self.footnotes = footnotes
|
||||
self.index = index
|
||||
self.reference = reference
|
||||
end
|
||||
|
||||
def to_h
|
||||
@to_h ||= reference.to_h.merge(index:).freeze
|
||||
end
|
||||
|
||||
def eql?(other)
|
||||
other.instance_of?(self.class) &&
|
||||
footnotes == other.footnotes &&
|
||||
anchor == other.anchor
|
||||
end
|
||||
|
||||
def ==(other) = eql?(other) && index == other.index
|
||||
|
||||
private
|
||||
|
||||
def footnotes=(footnotes)
|
||||
unless footnotes.instance_of? Footnotes
|
||||
raise TypeError, "Expected #{Footnotes}, got #{footnotes.class}"
|
||||
end
|
||||
|
||||
@footnotes = footnotes
|
||||
end
|
||||
|
||||
def index=(index)
|
||||
index = Integer index
|
||||
raise ArgumentError, 'Invalid index' unless index.positive?
|
||||
|
||||
@index = index
|
||||
end
|
||||
|
||||
def reference=(reference)
|
||||
unless reference.is_a? Reference
|
||||
raise TypeError, "Expected #{Reference}, got #{reference.class}"
|
||||
end
|
||||
|
||||
@reference = reference
|
||||
end
|
||||
end
|
||||
end
|
48
lib/referator/reference.rb
Normal file
48
lib/referator/reference.rb
Normal file
|
@ -0,0 +1,48 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Referator
|
||||
class Reference
|
||||
attr_reader :kind, :id, :slug, :data
|
||||
|
||||
def initialize(kind:, id:, slug:, **kwargs)
|
||||
self.kind = kind
|
||||
self.id = id
|
||||
self.slug = slug
|
||||
self.data = kwargs
|
||||
end
|
||||
|
||||
def to_h
|
||||
@to_h ||= data.merge(kind:, id:, slug:, anchor:, fragment:).freeze
|
||||
end
|
||||
|
||||
def anchor
|
||||
@anchor ||= "#{kind}-#{slug}".freeze
|
||||
end
|
||||
|
||||
def fragment
|
||||
@fragment ||= "##{anchor}".freeze
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def kind=(kind)
|
||||
@kind = String(kind).to_sym.tap do |new_kind|
|
||||
raise 'Invalid kind' unless NAME_RE.match? new_kind
|
||||
end
|
||||
end
|
||||
|
||||
def id=(id)
|
||||
@id = String(id).freeze
|
||||
end
|
||||
|
||||
def slug=(slug)
|
||||
@slug = String(slug).freeze.tap do |new_slug|
|
||||
raise 'Invalid slug' unless SLUG_RE.match? new_slug
|
||||
end
|
||||
end
|
||||
|
||||
def data=(data)
|
||||
@data = Hash(data).transform_keys { |key| String(key).to_sym }.freeze
|
||||
end
|
||||
end
|
||||
end
|
130
lib/referator/script.rb
Normal file
130
lib/referator/script.rb
Normal file
|
@ -0,0 +1,130 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Referator
|
||||
class Script
|
||||
attr_reader :workdir, :vars, :args, :env, :stdin
|
||||
|
||||
def initialize(workdir:, vars:, args: [], env: {}, stdin: [])
|
||||
self.workdir = workdir
|
||||
self.vars = vars
|
||||
|
||||
self.args = args
|
||||
self.env = env
|
||||
self.stdin = stdin
|
||||
|
||||
raise 'Invalid script' if self.args.empty?
|
||||
end
|
||||
|
||||
def call(**kwargs)
|
||||
raise 'Invalid vars' unless kwargs.keys.sort == vars
|
||||
|
||||
stdout, status = Open3.capture2(
|
||||
sub_env(**kwargs),
|
||||
*sub_args(**kwargs),
|
||||
chdir: workdir.to_s,
|
||||
stdin_data: sub_stdin(**kwargs),
|
||||
)
|
||||
raise 'Exit code' unless status.success?
|
||||
|
||||
String(stdout).freeze
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def workdir=(workdir)
|
||||
workdir = Pathname.new(workdir).expand_path.freeze
|
||||
raise 'Expected absolute path' unless workdir.absolute?
|
||||
raise 'Expected existing directory' unless workdir.directory?
|
||||
|
||||
@workdir = workdir
|
||||
end
|
||||
|
||||
def vars=(vars)
|
||||
@vars = [*vars].uniq.sort.freeze.each do |var|
|
||||
unless var.instance_of? Symbol
|
||||
raise TypeError, "Expected #{Symbol}, got #{var.class}"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def args=(args)
|
||||
@args = [*args].map { |arg| Sub.new vars, arg }.freeze
|
||||
end
|
||||
|
||||
def env=(env)
|
||||
@env = Hash(env).to_h do |key, value|
|
||||
[String(key).freeze, Sub.new(vars, value)]
|
||||
end.freeze
|
||||
end
|
||||
|
||||
def stdin=(stdin)
|
||||
@stdin = Sub.new vars, stdin
|
||||
end
|
||||
|
||||
def sub_args(**kwargs)
|
||||
args.map { |sub| sub.call(**kwargs) }.freeze
|
||||
end
|
||||
|
||||
def sub_env(**kwargs)
|
||||
env.transform_values { |sub| sub.call(**kwargs) }.freeze
|
||||
end
|
||||
|
||||
def sub_stdin(**kwargs)
|
||||
stdin.call(**kwargs)
|
||||
end
|
||||
|
||||
class Sub
|
||||
attr_reader :vars, :template
|
||||
|
||||
def initialize(vars, template)
|
||||
self.vars = vars
|
||||
|
||||
self.template =
|
||||
if template.instance_of?(String) ||
|
||||
template.instance_of?(Symbol) ||
|
||||
template.instance_of?(Hash)
|
||||
[template]
|
||||
else
|
||||
template
|
||||
end
|
||||
end
|
||||
|
||||
def call(**kwargs)
|
||||
raise 'Invalid vars' unless kwargs.keys.sort == vars
|
||||
|
||||
template.map do |part|
|
||||
if part.instance_of? String
|
||||
part
|
||||
else
|
||||
kwargs.fetch part
|
||||
end
|
||||
end.join.freeze
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def vars=(vars)
|
||||
@vars = [*vars].uniq.sort.freeze.each do |var|
|
||||
unless var.instance_of? Symbol
|
||||
raise TypeError, "Expected #{Symbol}, got #{var.class}"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def template=(template)
|
||||
raise 'Invalid format' unless template.instance_of? Array
|
||||
|
||||
@template = template.map do |part|
|
||||
part = String(part.keys.first).to_sym if part.instance_of? Hash
|
||||
|
||||
unless part.instance_of?(String) ||
|
||||
part.instance_of?(Symbol) && vars.include?(part)
|
||||
raise 'Invalid format'
|
||||
end
|
||||
|
||||
part.freeze
|
||||
end.freeze
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
72
spec/lib/referator/script/sub_spec.rb
Normal file
72
spec/lib/referator/script/sub_spec.rb
Normal file
|
@ -0,0 +1,72 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Referator::Script::Sub do
|
||||
subject(:sub) { described_class.new vars, template }
|
||||
|
||||
let(:vars) { %i[foo bar car].freeze }
|
||||
let(:template) { ['qwe', :foo, 'rty', { bar: nil }.freeze].freeze }
|
||||
|
||||
describe '#call' do
|
||||
subject(:result) { sub.call(**kwargs) }
|
||||
|
||||
let(:kwargs) { { foo: '123', bar: '456', car: '789' }.freeze }
|
||||
|
||||
it { is_expected.to be_instance_of String }
|
||||
it { is_expected.to be_frozen }
|
||||
it { is_expected.to eq 'qwe123rty456' }
|
||||
|
||||
context 'when the "vars" array is empty' do
|
||||
let(:vars) { [].freeze }
|
||||
let(:template) { %w[qwe rty].freeze }
|
||||
let(:kwargs) { {}.freeze }
|
||||
|
||||
it { is_expected.to be_instance_of String }
|
||||
it { is_expected.to be_frozen }
|
||||
it { is_expected.to eq 'qwerty' }
|
||||
end
|
||||
|
||||
context 'when template is empty' do
|
||||
let(:template) { [].freeze }
|
||||
|
||||
it { is_expected.to be_instance_of String }
|
||||
it { is_expected.to be_frozen }
|
||||
it { is_expected.to be_empty }
|
||||
end
|
||||
|
||||
context 'when the "vars" array and template are empty' do
|
||||
let(:vars) { [].freeze }
|
||||
let(:template) { [].freeze }
|
||||
let(:kwargs) { {}.freeze }
|
||||
|
||||
it { is_expected.to be_instance_of String }
|
||||
it { is_expected.to be_frozen }
|
||||
it { is_expected.to be_empty }
|
||||
end
|
||||
|
||||
context 'when template is a String' do
|
||||
let(:template) { 'qwe' }
|
||||
|
||||
it { is_expected.to be_instance_of String }
|
||||
it { is_expected.to be_frozen }
|
||||
it { is_expected.to eq 'qwe' }
|
||||
end
|
||||
|
||||
context 'when template is a Symbol' do
|
||||
let(:template) { :foo }
|
||||
|
||||
it { is_expected.to be_instance_of String }
|
||||
it { is_expected.to be_frozen }
|
||||
it { is_expected.to eq '123' }
|
||||
end
|
||||
|
||||
context 'when template is a Hash' do
|
||||
let(:template) { { foo: nil }.freeze }
|
||||
|
||||
it { is_expected.to be_instance_of String }
|
||||
it { is_expected.to be_frozen }
|
||||
it { is_expected.to eq '123' }
|
||||
end
|
||||
end
|
||||
end
|
42
spec/lib/referator/script_spec.rb
Normal file
42
spec/lib/referator/script_spec.rb
Normal file
|
@ -0,0 +1,42 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Referator::Script do
|
||||
subject(:script) { described_class.new workdir:, vars:, args:, env:, stdin: }
|
||||
|
||||
let(:workdir) { Pathname.new(__dir__).join('../../..').expand_path.freeze }
|
||||
let(:vars) { %i[foo bar car].freeze }
|
||||
|
||||
let(:args) { ['echo', :foo, { bar: nil }, :car].freeze }
|
||||
let(:env) { {}.freeze }
|
||||
let(:stdin) { [].freeze }
|
||||
|
||||
describe '#call' do
|
||||
subject(:result) { script.call(**kwargs) }
|
||||
|
||||
let(:kwargs) { { foo: '123', bar: '456', car: '789' }.freeze }
|
||||
|
||||
it { is_expected.to be_instance_of String }
|
||||
it { is_expected.to be_frozen }
|
||||
it { is_expected.to eq "123 456 789\n" }
|
||||
|
||||
context 'when it depends on env vars' do
|
||||
let(:args) { ['sh', '-c', ['echo ', :foo, ' $BAR $CAR'].freeze].freeze }
|
||||
let(:env) { { BAR: :bar, CAR: [:car].freeze }.freeze }
|
||||
|
||||
it { is_expected.to be_instance_of String }
|
||||
it { is_expected.to be_frozen }
|
||||
it { is_expected.to eq "123 456 789\n" }
|
||||
end
|
||||
|
||||
context 'when it depends on stdin' do
|
||||
let(:args) { 'cat' }
|
||||
let(:stdin) { [:foo, "\n", :bar, "\n", :car, "\n"].freeze }
|
||||
|
||||
it { is_expected.to be_instance_of String }
|
||||
it { is_expected.to be_frozen }
|
||||
it { is_expected.to eq "123\n456\n789\n" }
|
||||
end
|
||||
end
|
||||
end
|
Loading…
Reference in a new issue