ruby-marc-spec 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/build.yml +18 -0
  3. data/.gitignore +388 -0
  4. data/.gitmodules +3 -0
  5. data/.idea/codeStyles/codeStyleConfig.xml +5 -0
  6. data/.idea/go.imports.xml +6 -0
  7. data/.idea/inspectionProfiles/Project_Default.xml +23 -0
  8. data/.idea/marc_spec.iml +102 -0
  9. data/.idea/misc.xml +6 -0
  10. data/.idea/modules.xml +8 -0
  11. data/.idea/templateLanguages.xml +6 -0
  12. data/.idea/vcs.xml +7 -0
  13. data/.rubocop.yml +269 -0
  14. data/.ruby-version +1 -0
  15. data/.simplecov +8 -0
  16. data/CHANGES.md +3 -0
  17. data/Gemfile +6 -0
  18. data/LICENSE.md +21 -0
  19. data/README.md +172 -0
  20. data/Rakefile +20 -0
  21. data/lib/.rubocop.yml +5 -0
  22. data/lib/marc/spec/module_info.rb +14 -0
  23. data/lib/marc/spec/parsing/closed_int_range.rb +28 -0
  24. data/lib/marc/spec/parsing/closed_lc_alpha_range.rb +28 -0
  25. data/lib/marc/spec/parsing/parser.rb +213 -0
  26. data/lib/marc/spec/parsing.rb +1 -0
  27. data/lib/marc/spec/queries/al_num_range.rb +105 -0
  28. data/lib/marc/spec/queries/applicable.rb +18 -0
  29. data/lib/marc/spec/queries/character_spec.rb +81 -0
  30. data/lib/marc/spec/queries/comparison_string.rb +45 -0
  31. data/lib/marc/spec/queries/condition.rb +133 -0
  32. data/lib/marc/spec/queries/condition_context.rb +49 -0
  33. data/lib/marc/spec/queries/dsl.rb +80 -0
  34. data/lib/marc/spec/queries/indicator_value.rb +77 -0
  35. data/lib/marc/spec/queries/operator.rb +129 -0
  36. data/lib/marc/spec/queries/part.rb +63 -0
  37. data/lib/marc/spec/queries/position.rb +59 -0
  38. data/lib/marc/spec/queries/position_or_range.rb +27 -0
  39. data/lib/marc/spec/queries/query.rb +94 -0
  40. data/lib/marc/spec/queries/query_executor.rb +52 -0
  41. data/lib/marc/spec/queries/selector.rb +12 -0
  42. data/lib/marc/spec/queries/subfield.rb +88 -0
  43. data/lib/marc/spec/queries/subfield_value.rb +63 -0
  44. data/lib/marc/spec/queries/tag.rb +107 -0
  45. data/lib/marc/spec/queries/transform.rb +154 -0
  46. data/lib/marc/spec/queries.rb +1 -0
  47. data/lib/marc/spec.rb +32 -0
  48. data/rakelib/.rubocop.yml +19 -0
  49. data/rakelib/bundle.rake +8 -0
  50. data/rakelib/coverage.rake +11 -0
  51. data/rakelib/gem.rake +54 -0
  52. data/rakelib/parser_specs/formatter.rb +31 -0
  53. data/rakelib/parser_specs/parser_specs.rb.txt.erb +35 -0
  54. data/rakelib/parser_specs/rule.rb +95 -0
  55. data/rakelib/parser_specs/suite.rb +91 -0
  56. data/rakelib/parser_specs/test.rb +97 -0
  57. data/rakelib/parser_specs.rb +1 -0
  58. data/rakelib/rubocop.rake +18 -0
  59. data/rakelib/spec.rake +27 -0
  60. data/ruby-marc-spec.gemspec +42 -0
  61. data/spec/.rubocop.yml +46 -0
  62. data/spec/README.md +16 -0
  63. data/spec/data/b23161018-sru.xml +182 -0
  64. data/spec/data/sandburg.xml +82 -0
  65. data/spec/generated/char_indicator_spec.rb +174 -0
  66. data/spec/generated/char_spec.rb +113 -0
  67. data/spec/generated/comparison_string_spec.rb +74 -0
  68. data/spec/generated/field_tag_spec.rb +156 -0
  69. data/spec/generated/index_char_spec.rb +669 -0
  70. data/spec/generated/index_indicator_spec.rb +174 -0
  71. data/spec/generated/index_spec.rb +113 -0
  72. data/spec/generated/index_sub_spec_spec.rb +1087 -0
  73. data/spec/generated/indicators_spec.rb +75 -0
  74. data/spec/generated/position_or_range_spec.rb +110 -0
  75. data/spec/generated/sub_spec_spec.rb +208 -0
  76. data/spec/generated/sub_spec_sub_spec_spec.rb +1829 -0
  77. data/spec/generated/subfield_char_spec.rb +405 -0
  78. data/spec/generated/subfield_range_range_spec.rb +48 -0
  79. data/spec/generated/subfield_range_spec.rb +87 -0
  80. data/spec/generated/subfield_range_sub_spec_spec.rb +214 -0
  81. data/spec/generated/subfield_tag_range_spec.rb +477 -0
  82. data/spec/generated/subfield_tag_sub_spec_spec.rb +3216 -0
  83. data/spec/generated/subfield_tag_tag_spec.rb +5592 -0
  84. data/spec/marc/spec/parsing/closed_int_range_spec.rb +49 -0
  85. data/spec/marc/spec/parsing/closed_lc_alpha_range_spec.rb +49 -0
  86. data/spec/marc/spec/parsing/parser_spec.rb +545 -0
  87. data/spec/marc/spec/queries/al_num_range_spec.rb +114 -0
  88. data/spec/marc/spec/queries/character_spec_spec.rb +28 -0
  89. data/spec/marc/spec/queries/comparison_string_spec.rb +28 -0
  90. data/spec/marc/spec/queries/indicator_value_spec.rb +28 -0
  91. data/spec/marc/spec/queries/query_spec.rb +200 -0
  92. data/spec/marc/spec/queries/subfield_spec.rb +92 -0
  93. data/spec/marc/spec/queries/subfield_value_spec.rb +31 -0
  94. data/spec/marc/spec/queries/tag_spec.rb +144 -0
  95. data/spec/marc/spec/queries/transform_spec.rb +459 -0
  96. data/spec/marc_spec_spec.rb +247 -0
  97. data/spec/scratch_spec.rb +112 -0
  98. data/spec/spec_helper.rb +23 -0
  99. metadata +341 -0
@@ -0,0 +1,63 @@
1
+ require 'stringio'
2
+ require 'marc/spec/queries/selector'
3
+ require 'marc/spec/queries/subfield'
4
+
5
+ module MARC
6
+ module Spec
7
+ module Queries
8
+ class SubfieldValue
9
+ include Selector
10
+
11
+ # ------------------------------------------------------------
12
+ # Attributes
13
+
14
+ attr_reader :subfield, :character_spec
15
+
16
+ # ------------------------------------------------------------
17
+ # Initializer
18
+
19
+ def initialize(subfield, character_spec = nil)
20
+ @subfield = ensure_type(subfield, Subfield)
21
+ @character_spec = ensure_type(character_spec, CharacterSpec, allow_nil: true)
22
+ end
23
+
24
+ # ------------------------------------------------------------
25
+ # Object overrides
26
+
27
+ def to_s
28
+ StringIO.new.tap do |out|
29
+ out << subfield
30
+ out << "/#{character_spec}" if character_spec
31
+ end.string
32
+ end
33
+
34
+ # ------------------------------------------------------------
35
+ # Protected
36
+
37
+ protected
38
+
39
+ def can_apply?(marc_obj)
40
+ subfield.send(:can_apply?, marc_obj)
41
+ end
42
+
43
+ def do_apply(data_field)
44
+ subfields = subfield.apply(data_field)
45
+ return subfields.map(&:value) unless character_spec
46
+
47
+ subfields.flat_map { |sf| character_spec.apply(sf.value) }
48
+ end
49
+
50
+ def to_s_inspect
51
+ StringIO.new.tap do |out|
52
+ out << subfield.inspect
53
+ out << "/#{character_spec.inspect}" if character_spec
54
+ end.string
55
+ end
56
+
57
+ def equality_attrs
58
+ %i[subfield character_spec]
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,107 @@
1
+ require 'marc'
2
+ require 'marc/spec/queries/applicable'
3
+
4
+ module MARC
5
+ module Spec
6
+ module Queries
7
+ class Tag
8
+ include Applicable
9
+
10
+ # ------------------------------------------------------------
11
+ # Constants
12
+
13
+ LDR = 'LDR'.freeze
14
+
15
+ # ------------------------------------------------------------
16
+ # Attributes
17
+
18
+ attr_reader :index, :tag_re, :tag_exact
19
+
20
+ # ------------------------------------------------------------
21
+ # Initializer
22
+
23
+ def initialize(tag, index = nil)
24
+ raise ArgumentError, 'Tag cannot be nil' unless tag
25
+
26
+ @tag_exact = tag.to_s unless (@tag_re = tag_re_from(tag))
27
+ @index = ensure_type(index, PositionOrRange, allow_nil: true)
28
+ end
29
+
30
+ # ------------------------------------------------------------
31
+ # Public methods
32
+
33
+ def leader?
34
+ tag_exact == LDR
35
+ end
36
+
37
+ # ------------------------------------------------------------
38
+ # Object overrides
39
+
40
+ def to_s
41
+ StringIO.new.tap do |out|
42
+ out << tag_str
43
+ out << "[#{index}]" if index
44
+ end.string
45
+ end
46
+
47
+ # ------------------------------------------------------------
48
+ # Protected methods
49
+
50
+ protected
51
+
52
+ # ------------------------------
53
+ # Applicable
54
+
55
+ def can_apply?(marc_obj)
56
+ marc_obj.is_a?(MARC::Record)
57
+ end
58
+
59
+ def do_apply(marc_record)
60
+ return [marc_record.leader] if leader?
61
+
62
+ all_fields = all_fields(marc_record)
63
+ index ? index.select_from(all_fields) : all_fields
64
+ end
65
+
66
+ # ------------------------------
67
+ # Predicate
68
+
69
+ def to_s_inspect
70
+ StringIO.new.tap do |out|
71
+ out << (tag_re ? tag_re.inspect : tag_exact)
72
+ out << "[#{index.inspect}]" if index
73
+ end.string
74
+ end
75
+
76
+ def equality_attrs
77
+ %i[tag_str index]
78
+ end
79
+
80
+ # ------------------------------------------------------------
81
+ # Private methods
82
+
83
+ private
84
+
85
+ def all_fields(marc_record)
86
+ return marc_record.fields(tag_exact) if tag_exact
87
+
88
+ [].tap do |ff|
89
+ ff << marc_record.leader if LDR =~ tag_re
90
+ marc_record.each { |field| ff << field if field.tag =~ tag_re }
91
+ end
92
+ end
93
+
94
+ def tag_re_from(tag)
95
+ return tag if tag.is_a?(Regexp)
96
+
97
+ tag_s = tag.to_s
98
+ Regexp.compile("^#{tag_s}$") if tag_s.include?('.')
99
+ end
100
+
101
+ def tag_str
102
+ @tag_str ||= tag_exact || tag_re.source.gsub(/^\^(.*)\$$/, '\\1')
103
+ end
104
+ end
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,154 @@
1
+ require 'parslet'
2
+
3
+ module MARC
4
+ module Spec
5
+ module Queries
6
+ class Transform < Parslet::Transform
7
+
8
+ # ----------------------------------------
9
+ # Misc. atoms
10
+
11
+ # { pos: }
12
+ rule(pos: simple(:pos)) { Position.new(pos) }
13
+
14
+ # { from:, to: }
15
+ rule(from: simple(:from), to: simple(:to)) { AlNumRange.new(from, to) }
16
+
17
+ # { comparison_string: }
18
+ rule(comparison_string: simple(:string)) { ComparisonString.new(string) }
19
+
20
+ # { character_spec: }
21
+ rule(character_spec: simple(:character_spec)) do
22
+ # noinspection RubyArgCount
23
+ CharacterSpec.new(character_spec)
24
+ end
25
+
26
+ # ----------------------------------------
27
+ # subTermSet
28
+
29
+ # { left:, operator:, right: }
30
+ rule(left: simple(:left), operator: simple(:operator), right: simple(:right)) do
31
+ Condition.new(operator, left: left, right: right)
32
+ end
33
+
34
+ # { operator:, right: }
35
+ rule(operator: simple(:operator), right: simple(:right)) do
36
+ Condition.new(operator, right: right)
37
+ end
38
+
39
+ # { right: }
40
+ rule(right: simple(:right)) do
41
+ Condition.new(right: right)
42
+ end
43
+
44
+ # { any_condition: }
45
+ rule(any_condition: sequence(:conditions)) do
46
+ Condition.any_of(*conditions)
47
+ end
48
+
49
+ # { all_conditions: }
50
+ rule(all_conditions: sequence(:conditions)) do
51
+ Condition.all_of(*conditions)
52
+ end
53
+
54
+ # ----------------------------------------
55
+ # fieldSpec
56
+
57
+ # { tag: }
58
+ rule(tag: simple(:tag)) do
59
+ Tag.new(tag)
60
+ end
61
+
62
+ # { tag:, index: }
63
+ rule(tag: simple(:tag), index: simple(:index)) do
64
+ Tag.new(tag, index)
65
+ end
66
+
67
+ # ----------------------------------------
68
+ # abrSubfieldSpec
69
+
70
+ # { code: }
71
+ rule(code: simple(:code)) do
72
+ Subfield.new(code)
73
+ end
74
+
75
+ # { code:, index: }
76
+ rule(code: simple(:code), index: simple(:index)) do
77
+ Subfield.new(code, index: index)
78
+ end
79
+
80
+ # { code:, sf_chars: }
81
+ rule(code: simple(:code), sf_chars: simple(:sf_chars)) do
82
+ SubfieldValue.new(Subfield.new(code), sf_chars)
83
+ end
84
+
85
+ # { code:, index:, sf_chars: }
86
+ rule(code: simple(:code), index: simple(:index), sf_chars: simple(:sf_chars)) do
87
+ SubfieldValue.new(Subfield.new(code, index: index), sf_chars)
88
+ end
89
+
90
+ # ----------------------------------------
91
+ # indicatorSpec
92
+
93
+ # { ind: }
94
+ rule(ind: simple(:ind)) do
95
+ IndicatorValue.new(ind)
96
+ end
97
+
98
+ # ----------------------------------------
99
+ # subSpec
100
+
101
+ # TODO: separate Subquery type?
102
+
103
+ # { selector: }
104
+ rule(selector: simple(:selector)) do
105
+ Query.new(selector: selector)
106
+ end
107
+
108
+ # { selector:, condition: }
109
+ rule(selector: simple(:selector), condition: simple(:condition)) do
110
+ Query.new(selector: selector, condition: condition)
111
+ end
112
+
113
+ # ----------------------------------------
114
+ # MARCSpec
115
+
116
+ # { tag:, selector: }
117
+ rule(tag: simple(:tag), selector: simple(:selector)) do
118
+ Query.new(tag: Tag.new(tag), selector: selector)
119
+ end
120
+
121
+ # { tag:, index:, selector: }
122
+ rule(tag: simple(:tag), index: simple(:index), selector: simple(:selector)) do
123
+ Query.new(tag: Tag.new(tag, index), selector: selector)
124
+ end
125
+
126
+ # { tag:, condition: }
127
+ rule(tag: simple(:tag), condition: simple(:condition)) do
128
+ Query.new(tag: Tag.new(tag), condition: condition)
129
+ end
130
+
131
+ # { tag:, index:, condition: }
132
+ rule(tag: simple(:tag), index: simple(:index), condition: simple(:condition)) do
133
+ Query.new(tag: Tag.new(tag, index), condition: condition)
134
+ end
135
+
136
+ # { tag:, subqueries: }
137
+ rule(tag: simple(:tag), subqueries: sequence(:subqueries)) do
138
+ Query.new(tag: Tag.new(tag), subqueries: subqueries)
139
+ end
140
+
141
+ # { tag:, selector:, condition: }
142
+ rule(tag: simple(:tag), selector: simple(:selector), condition: simple(:condition)) do
143
+ Query.new(tag: Tag.new(tag), selector: selector, condition: condition)
144
+ end
145
+
146
+ # { tag:, index:, selector:, condition: }
147
+ rule(tag: simple(:tag), index: simple(:index), selector: simple(:selector), condition: simple(:condition)) do
148
+ Query.new(tag: Tag.new(tag, index), selector: selector, condition: condition)
149
+ end
150
+
151
+ end
152
+ end
153
+ end
154
+ end
@@ -0,0 +1 @@
1
+ Dir.glob(File.expand_path('queries/*.rb', __dir__)).sort.each(&method(:require))
data/lib/marc/spec.rb ADDED
@@ -0,0 +1,32 @@
1
+ Dir.glob(File.expand_path('spec/*.rb', __dir__)).sort.each(&method(:require))
2
+
3
+ module MARC
4
+ module Spec
5
+ class << self
6
+ def find(query_string, marc_record)
7
+ root = parse_query(query_string)
8
+ executor = Queries::QueryExecutor.new(marc_record, root)
9
+ executor.execute
10
+ end
11
+
12
+ def parse_query(query_string)
13
+ parse_tree = parser.parse(query_string, reporter: reporter)
14
+ xform.apply(parse_tree)
15
+ end
16
+
17
+ private
18
+
19
+ def parser
20
+ @parser ||= Parsing::Parser.new
21
+ end
22
+
23
+ def xform
24
+ @xform ||= Queries::Transform.new
25
+ end
26
+
27
+ def reporter
28
+ @reporter ||= Parslet::ErrorReporter::Contextual.new
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,19 @@
1
+ inherit_from: ../.rubocop.yml
2
+
3
+ Style/ClassAndModuleChildren:
4
+ Enabled: false
5
+
6
+ Layout/LineLength:
7
+ Enabled: false
8
+
9
+ Metrics/BlockLength:
10
+ Enabled: false
11
+
12
+ Metrics/ClassLength:
13
+ Enabled: false
14
+
15
+ Metrics/ModuleLength:
16
+ Enabled: false
17
+
18
+ Metrics/MethodLength:
19
+ Enabled: false
@@ -0,0 +1,8 @@
1
+ namespace :bundle do
2
+ desc 'Updates the ruby-advisory-db then runs bundle-audit'
3
+ task :audit do
4
+ require 'bundler/audit/cli'
5
+ Bundler::Audit::CLI.start ['update']
6
+ Bundler::Audit::CLI.start %w[check --ignore CVE-2015-9284]
7
+ end
8
+ end
@@ -0,0 +1,11 @@
1
+ require 'ci/reporter/rake/rspec'
2
+
3
+ # Configure CI::Reporter report generation
4
+ ENV['GENERATE_REPORTS'] ||= 'true'
5
+ ENV['CI_REPORTS'] = 'artifacts/rspec'
6
+
7
+ desc 'Run all specs in spec directory, with coverage'
8
+ task coverage: ['ci:setup:rspec'] do
9
+ ENV['COVERAGE'] ||= 'true'
10
+ Rake::Task[:spec].invoke
11
+ end
data/rakelib/gem.rake ADDED
@@ -0,0 +1,54 @@
1
+ require 'rubygems/gem_runner'
2
+ require 'marc/spec/module_info'
3
+
4
+ module MARC::Spec
5
+ class << self
6
+ def project_root
7
+ @project_root ||= File.expand_path('..', __dir__)
8
+ end
9
+
10
+ def artifacts_dir
11
+ return project_root unless ENV['CI']
12
+
13
+ @artifacts_dir ||= File.join(project_root, 'artifacts')
14
+ end
15
+
16
+ def gemspec_file
17
+ @gemspec_file ||= begin
18
+ gemspec_files = Dir.glob(File.expand_path('*.gemspec', project_root))
19
+ raise ArgumentError, "Too many .gemspecs: #{gemspec_files.join(', ')}" if gemspec_files.size > 1
20
+ raise ArgumentError, 'No .gemspec file found' if gemspec_files.empty?
21
+
22
+ gemspec_files[0]
23
+ end
24
+ end
25
+
26
+ def gemspec_basename
27
+ File.basename(gemspec_file)
28
+ end
29
+
30
+ def output_file
31
+ @output_file ||= begin
32
+ gem_name = File.basename(gemspec_file, '.*')
33
+ version = MARC::Spec::ModuleInfo::VERSION
34
+ basename = "#{gem_name}-#{version}.gem"
35
+ File.join(artifacts_dir, basename)
36
+ end
37
+ end
38
+
39
+ def output_file_relative
40
+ return File.basename(output_file) unless ENV['CI']
41
+
42
+ @output_file_relative ||= begin
43
+ artifacts_dir_relative = File.basename(artifacts_dir)
44
+ File.join(artifacts_dir_relative, File.basename(output_file))
45
+ end
46
+ end
47
+ end
48
+ end
49
+
50
+ desc "Build #{MARC::Spec.gemspec_basename} as #{MARC::Spec.output_file_relative}"
51
+ task :gem do
52
+ args = ['build', MARC::Spec.gemspec_file, "--output=#{MARC::Spec.output_file}"]
53
+ Gem::GemRunner.new.run(args)
54
+ end
@@ -0,0 +1,31 @@
1
+ require 'rubocop'
2
+
3
+ module ParserSpecs
4
+ module Formatter
5
+ include RuboCop::Cop::Util
6
+
7
+ def validity(v)
8
+ v ? 'valid' : 'invalid'
9
+ end
10
+
11
+ def quote(s)
12
+ to_string_literal(String.new(s))
13
+ end
14
+
15
+ def decamelize(str)
16
+ return unless str
17
+
18
+ str.gsub(/(?<!^)[A-Z]/) { "_#{$&}" }.downcase
19
+ end
20
+
21
+ def indent(str, indent, omit_first: true)
22
+ return str.gsub(/^/, indent) unless omit_first
23
+
24
+ str.gsub(/(\n)/, "\\1#{indent}")
25
+ end
26
+
27
+ class << self
28
+ include Formatter
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,35 @@
1
+ require 'spec_helper'
2
+ require 'parslet/rig/rspec'
3
+
4
+ module MARC
5
+ module Spec
6
+ module Parsing
7
+ context 'suite' do
8
+ describe <%= ":#{name}" %> do
9
+ let(:parser) { Parser.new }
10
+ let(:reporter) { Parslet::ErrorReporter::Deepest.new }
11
+ <% suites.sort.each do |suite| %>
12
+ describe <%= quote(suite.description) %> do
13
+ # <%= suite.json_path %><% (suite.tests_by_group[nil] || []).sort.each do |test| %>
14
+ it <%= quote("#{test.description} -> #{validity(test.valid)}") %> do
15
+ # <%= test.json_path %><% test.rspec_assertions.each do |asrt| %>
16
+ <%= asrt %><% end %>
17
+ end
18
+ <% end %>
19
+ <% suite.tests_by_group.each do |group, tests| %>
20
+ <% next unless group %>
21
+ describe <%= quote(group) %> do
22
+ <% tests.sort.each do |test| %>
23
+ it <%= quote("#{test.detail} -> #{validity(test.valid)}") %> do
24
+ # <%= test.json_path %><% test.rspec_assertions.each do |asrt| %>
25
+ <%= asrt %><% end %>
26
+ end
27
+ <% end %>
28
+ end
29
+ <% end %>
30
+ end<% end %>
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,95 @@
1
+ require 'json'
2
+ require 'ostruct'
3
+ require_relative 'formatter'
4
+ require_relative 'suite'
5
+
6
+ module ParserSpecs
7
+ class Rule
8
+ include Formatter
9
+
10
+ RULE_RE = %r{/(?<wild>wildCombination_)?(?<valid>valid|invalid)(?<rule>[A-Z][[:alpha:]]+)\.json$}.freeze
11
+
12
+ TEMPLATE_PATH = File.expand_path('parser_specs.rb.txt.erb', __dir__)
13
+
14
+ attr_reader :name
15
+
16
+ def initialize(name, suites = [])
17
+ @name = name
18
+ suites.each { |s| add_suite(s) }
19
+ end
20
+
21
+ def to_s
22
+ name
23
+ end
24
+
25
+ def suites
26
+ @suites ||= []
27
+ end
28
+
29
+ def add_suite(s)
30
+ return (suites << s) unless (existing = suites.find { |s1| s1.merge?(s) })
31
+
32
+ existing.merge(s)
33
+ end
34
+
35
+ def write_rspec_to(dir)
36
+ basename = "#{name}_spec.rb"
37
+ spec_path = File.join(dir, basename)
38
+
39
+ # ideally we could just run the ERB as-is, but it's hard to write
40
+ # a legible template that doesn't introduce some unwanted whitespace
41
+ spec_src = Rule.template.result(binding)
42
+ .gsub(/ +$/, '')
43
+ .gsub(/\n\n+/, "\n\n")
44
+
45
+ puts "writing #{basename}"
46
+ File.write(spec_path, spec_src)
47
+ end
48
+
49
+ class << self
50
+ include Formatter
51
+
52
+ def all_from_json(json_root)
53
+ Dir.glob(File.join(json_root, '*valid/*.json')).sort.each_with_object([]) do |json_path, rules|
54
+ rule_name, wild = extract_rule_metadata(json_path)
55
+
56
+ suite_data = JSON.parse(File.read(json_path), object_class: OpenStruct, symbolize_names: true)
57
+ suite = Suite.from_ostruct(suite_data, rule_name, wild, json_path.sub(json_root, ''))
58
+
59
+ add_suite(rules, rule_name, suite)
60
+ end
61
+ end
62
+
63
+ def template
64
+ @template ||= begin
65
+ template_src = File.read(TEMPLATE_PATH)
66
+ ERB.new(template_src, trim_mode: '-')
67
+ end
68
+ end
69
+
70
+ private
71
+
72
+ def extract_rule_metadata(json_path)
73
+ raise ArgumentError, "#{json_path} does not match #{RULE_RE.source}" unless (match_data = RULE_RE.match(json_path))
74
+
75
+ [normalize_rule_name(match_data[:rule]), !match_data[:wild].nil?]
76
+ end
77
+
78
+ def add_suite(rules, rule_name, suite)
79
+ if (existing = rules.find { |r| r.name == rule_name })
80
+ existing.add_suite(suite)
81
+ else
82
+ rules << Rule.new(rule_name, [suite])
83
+ end
84
+ end
85
+
86
+ def normalize_rule_name(name)
87
+ return unless name
88
+
89
+ decamelize(name).tap do |n|
90
+ return 'subfield_char' if n == 'subfield_tag' # suite doesn't match spec
91
+ end
92
+ end
93
+ end
94
+ end
95
+ end