ruby-marc-spec 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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