Add existing code

This commit is contained in:
Alex Kotov 2023-09-29 22:13:00 +04:00
parent e1f7be8e93
commit 37a9f56d10
Signed by: kotovalexarian
GPG key ID: 553C0EBBEB5D5F08
12 changed files with 662 additions and 0 deletions

63
exe/referator Executable file
View 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
View 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
View 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

View 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

View 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

View 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

View 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
View 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

View 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
View 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

View 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

View 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