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,213 @@
1
+ require 'parslet'
2
+ require 'marc/spec/parsing/closed_int_range'
3
+ require 'marc/spec/parsing/closed_lc_alpha_range'
4
+
5
+ module MARC
6
+ module Spec
7
+ module Parsing
8
+ # rubocop:disable Style/BlockDelimiters
9
+ # noinspection RubyResolve
10
+ class Parser < Parslet::Parser
11
+
12
+ # ------------------------------------------------------------
13
+ # DSL extensions
14
+
15
+ def closed_int_range
16
+ ClosedIntRange.new
17
+ end
18
+
19
+ def closed_lc_alpha_range
20
+ ClosedLcAlphaRange.new
21
+ end
22
+
23
+ # ------------------------------------------------------------
24
+ # Parsing rules
25
+
26
+ # alphaupper = %x41-5A
27
+ # ; A-Z
28
+ rule(:alpha_upper) { match['A-Z'] }
29
+
30
+ # alphalower = %x61-7A
31
+ # ; a-z
32
+ rule(:alpha_lower) { match['a-z'] }
33
+
34
+ # DIGIT = %x30-39
35
+ # ; 0-9
36
+ rule(:digit) { match['0-9'] }
37
+
38
+ # VCHAR = %x21-7E
39
+ # ; visible (printing) characters
40
+ rule(:vchar) { match['\u0021-\u007e'] }
41
+
42
+ # positiveDigit = %x31-39
43
+ # ; "1" / "2" / "3" / "4" / "5" / "6" / "7" / "8" / "9"
44
+ rule(:positive_digit) { match['1-9'] }
45
+
46
+ # positiveInteger = "0" / positiveDigit [1*DIGIT]
47
+ #
48
+ # NOTE: yes, this is a misnomer
49
+ rule(:positive_integer) { str('0') | (positive_digit >> digit.repeat) }
50
+
51
+ # fieldTag = 3(alphalower / DIGIT / ".") / 3(alphaupper / DIGIT / ".")
52
+ rule(:field_tag) {
53
+ (alpha_lower | digit | str('.')).repeat(3, 3) | (alpha_upper | digit | str('.')).repeat(3, 3)
54
+ }
55
+
56
+ # position = positiveInteger / "#"
57
+ rule(:position) { positive_integer | str('#') }
58
+
59
+ # Extracted from range, below
60
+ #
61
+ # NOTE: #-n means from (last index - n) to end of string
62
+ rule(:left_open_range) { str('#').ignore.as(:from) >> str('-') >> (positive_integer | str('#').ignore).as(:to) }
63
+
64
+ # Extracted from range, below
65
+ #
66
+ # NOTE: n-# means from position n to end of string
67
+ rule(:right_open_range) { positive_integer.as(:from) >> str('-') >> str('#').ignore.as(:to) }
68
+
69
+ # range = position "-" position
70
+ #
71
+ # NOTE: n-# means from position n to end of string;
72
+ # #-n means from (last index - n) to end of string
73
+ # rule(:range) { position.as(:from) >> str('-') >> position.as(:to) }
74
+ rule(:range) { left_open_range | right_open_range | closed_int_range }
75
+
76
+ # positionOrRange = range / position
77
+ rule(:position_or_range) { range | position.as(:pos) }
78
+
79
+ # characterSpec = "/" positionOrRange
80
+ rule(:character_spec) { str('/') >> position_or_range.as(:character_spec) }
81
+
82
+ # index = "[" positionOrRange "]"
83
+ rule(:index) { (str('[') >> position_or_range >> str(']')).as(:index) }
84
+
85
+ # fieldSpec = fieldTag [index] [characterSpec]
86
+ rule(:field_spec) { field_tag.as(:tag) >> index.maybe >> character_spec.as(:selector).maybe }
87
+
88
+ # abrFieldSpec = index [characterSpec] / characterSpec
89
+ rule(:abr_field_spec) do
90
+ (index >> character_spec.as(:selector).maybe) | character_spec.as(:selector)
91
+ end
92
+
93
+ # subfieldChar = %x21-3F / %x5B-7B / %x7D-7E
94
+ # ; ! " # $ % & ' ( ) * + , - . / 0-9 : ; < = > ? [ \ ] ^ _ \` a-z { } ~
95
+ # NOTE: Not just alphanumeric; see https://github.com/MARCspec/MARCspec/issues/31
96
+ rule(:subfield_char) { match['\u0021-\u003f'] | match['\u005b-\u007b'] | match['\u007d-\u007e'] }
97
+
98
+ # subfieldCode = "$" subfieldChar
99
+ rule(:subfield_code) { str('$').ignore >> subfield_char }
100
+
101
+ # UNDOCUMENTED -- see spec/suite/valid/validSubfieldRange.json, https://github.com/MARCspec/MARCspec-Test-Suite/issues/1
102
+ rule(:subfield_range) { (closed_lc_alpha_range | closed_int_range) }
103
+
104
+ # subfieldCodeRange = "$" ( (alphalower "-" alphalower) / (DIGIT "-" DIGIT) )
105
+ # ; [a-z]-[a-z] / [0-9]-[0-9]
106
+ #
107
+ # NOTE: docs don't insist the range be valid (start <= end), but tests enforce it
108
+ rule(:subfield_code_range) { str('$').ignore >> subfield_range }
109
+
110
+ # abrSubfieldSpec = (subfieldCode / subfieldCodeRange) [index] [characterSpec]
111
+ rule(:abr_subfield_spec) do
112
+ ((subfield_code_range | subfield_code).as(:code) >> index.maybe >> character_spec.as(:sf_chars).maybe).as(:selector)
113
+ end
114
+
115
+ # subfieldSpec = fieldTag [index] abrSubfieldSpec
116
+ rule(:subfield_spec) { field_tag.as(:tag) >> index.maybe >> abr_subfield_spec }
117
+
118
+ # UNDOCUMENTED -- see spec/suite/valid/validIndicators.json, https://github.com/MARCspec/MARCspec-Test-Suite/issues/1
119
+ rule(:indicators) { str('1') | str('2') }
120
+
121
+ # abrIndicatorSpec = [index] "^" ("1" / "2")
122
+ rule(:abr_indicator_spec) { index.maybe >> str('^') >> indicators.as(:ind).as(:selector) }
123
+
124
+ # indicatorSpec = fieldTag abrIndicatorSpec
125
+ rule(:indicator_spec) { field_tag.as(:tag) >> abr_indicator_spec }
126
+
127
+ # Extracted from comparisonString (some VCHARs need to be escaped,
128
+ # and literal \ needs special handling)
129
+ rule(:vchar_cs_plain) { match['\u0021-\u007e&&[^!$=?{|}~]'] }
130
+
131
+ # Extracted from comparisonString (some VCHARs need to be escaped)
132
+ rule(:vchar_cs_special) { match['!$=?{|}~'] }
133
+
134
+ # Extracted from comparisonString (escaped)
135
+ rule(:vchar_cs_esc) { (str('\\') >> vchar_cs_special) }
136
+
137
+ # Extracted from comparisonString to simplify generated tests,
138
+ # which don't take leading \ into account
139
+ rule(:comparison_string) {
140
+ # escape is optional in position 1, apparently
141
+ head = (vchar_cs_special | vchar_cs_esc) | vchar_cs_plain
142
+ tail = (vchar_cs_esc | vchar_cs_plain).repeat
143
+ head >> tail
144
+ }
145
+
146
+ # comparisonString = "\" *VCHAR
147
+ #
148
+ # NOTE: generated tests only handle the body of the string, not the
149
+ # leading \, so we give the full rule a separate name
150
+ rule(:_comparison_string) { ((str('\\s') | str('\\').ignore) >> comparison_string).as(:comparison_string) }
151
+
152
+ # operator = "=" / "!=" / "~" / "!~" / "!" / "?"
153
+ # ; equal / unequal / includes / not includes / not exists / exists
154
+ rule(:operator) { (str('=') | str('!=') | str('~') | str('!~') | str('!') | str('?')) }
155
+
156
+ # abbreviation = abrFieldSpec / abrSubfieldSpec / abrIndicatorSpec
157
+ rule(:abbreviation) { (abr_subfield_spec | abr_indicator_spec | abr_field_spec) }
158
+
159
+ # subTerm = fieldSpec / subfieldSpec / indicatorSpec / comparisonString / abbreviation
160
+ rule(:sub_term) { subfield_spec | indicator_spec | field_spec | _comparison_string | abbreviation }
161
+
162
+ # subTermSet = [ [subTerm] operator ] subTerm
163
+ rule(:sub_term_set) { (sub_term.as(:left).maybe >> operator.as(:operator)).maybe >> sub_term.as(:right) }
164
+
165
+ # Extracted from subSpec for clarity
166
+ rule(:_chained_sub_term_sets) { (sub_term_set >> (str('|') >> sub_term_set).repeat(1)).as(:any_condition) }
167
+
168
+ # NOTE: generated tests are properly for subSpec*, so we give the
169
+ # single one a separate name
170
+ #
171
+ # subSpec = "{" subTermSet *( "|" subTermSet ) "}"
172
+ rule(:_sub_spec) { str('{') >> (_chained_sub_term_sets | sub_term_set) >> str('}') }
173
+
174
+ # Extracted from SubSpec for clarity
175
+ rule(:_repeated_sub_specs) { _sub_spec.repeat(2).as(:all_conditions) }
176
+
177
+ # Repeated to satisfy generated tests
178
+ rule(:sub_spec) { _repeated_sub_specs | _sub_spec }
179
+
180
+ # Rewritten from MARCspec for clarity
181
+ # (subfieldSpec *subSpec *(abrSubfieldSpec *subSpec))
182
+ # -> (fieldTag [index] *(abrSubfieldSpec *subSpec))
183
+ rule(:_multiple_subfield_spec) {
184
+ (field_tag.as(:tag) >> index.maybe) >>
185
+ (abr_subfield_spec >> sub_spec.as(:condition).maybe).repeat(2).as(:subqueries)
186
+ }
187
+
188
+ # Extracted from MARCspec for clarity:
189
+ # (subfieldSpec *subSpec *(abrSubfieldSpec *subSpec))
190
+ # Rewritten for ease of parsing:
191
+ # (fieldTag [index] *(abrSubfieldSpec *subSpec))
192
+ rule(:_varfield_marc_spec) {
193
+ _multiple_subfield_spec | (subfield_spec >> sub_spec.as(:condition).maybe)
194
+ }
195
+
196
+ # Extracted from MARCspec for clarity
197
+ # indicatorSpec *subSpec
198
+ rule(:_indicator_marc_spec) { (indicator_spec >> sub_spec.as(:condition).maybe) }
199
+
200
+ # Extracted from MARCspec for clarity
201
+ # fieldSpec *subSpec
202
+ rule(:_fixedfield_marc_spec) { (field_spec >> sub_spec.as(:condition).maybe) }
203
+
204
+ # MARCspec = fieldSpec *subSpec / (subfieldSpec *subSpec *(abrSubfieldSpec *subSpec)) / indicatorSpec *subSpec
205
+ rule(:marc_spec) { _varfield_marc_spec | _indicator_marc_spec | _fixedfield_marc_spec }
206
+
207
+ root(:marc_spec)
208
+ end
209
+
210
+ # rubocop:enable Style/BlockDelimiters
211
+ end
212
+ end
213
+ end
@@ -0,0 +1 @@
1
+ Dir.glob(File.expand_path('parsing/*.rb', __dir__)).sort.each(&method(:require))
@@ -0,0 +1,105 @@
1
+ require 'marc/spec/queries/position_or_range'
2
+
3
+ module MARC
4
+ module Spec
5
+ module Queries
6
+ class AlNumRange
7
+ include PositionOrRange
8
+
9
+ # ------------------------------------------------------------
10
+ # Attributes
11
+
12
+ attr_reader :from, :to
13
+
14
+ # ------------------------------------------------------------
15
+ # Initializer
16
+
17
+ def initialize(from, to)
18
+ @from, @to = parse_endpoints(from, to)
19
+ end
20
+
21
+ # ------------------------------------------------------------
22
+ # Instance methods
23
+
24
+ def select_from(seq)
25
+ raw_result = select_raw_from(seq)
26
+ seq.is_a?(String) ? wrap_string_result(raw_result) : raw_result
27
+ end
28
+
29
+ def include?(v)
30
+ return false if empty?
31
+ return (v < 0 && v > reverse_endpoint) if from.nil?
32
+ return false if v < from
33
+
34
+ to.nil? ? true : v <= to
35
+ end
36
+
37
+ def alphabetic?
38
+ lc_alpha?(from) || lc_alpha?(to)
39
+ end
40
+
41
+ def empty?
42
+ from.nil? && to.nil?
43
+ end
44
+
45
+ def index_range
46
+ @index_range ||= to_range
47
+ end
48
+
49
+ # ------------------------------------------------------------
50
+ # Object overrides
51
+
52
+ def to_s
53
+ "#{from || '#'}-#{to || '#'}"
54
+ end
55
+
56
+ # ------------------------------------------------------------
57
+ # Part
58
+
59
+ protected
60
+
61
+ def equality_attrs
62
+ %i[from to]
63
+ end
64
+
65
+ private
66
+
67
+ def select_raw_from(seq)
68
+ return seq if from == 0 && to.nil?
69
+ return seq[index_range] unless alphabetic?
70
+ return select_raw_from(seq.chars).join if seq.respond_to?(:chars)
71
+ raise ArgumentError, "Can't select from non-sequence #{seq.inspect}" unless seq.respond_to?(:select)
72
+
73
+ seq.select { |x| include?(x) }
74
+ end
75
+
76
+ def reverse_endpoint
77
+ -(1 + to)
78
+ end
79
+
80
+ def to_range
81
+ return (0..-1) if empty?
82
+ return (reverse_endpoint..) if from.nil?
83
+
84
+ (from..to) # OK for to to be nil here
85
+ end
86
+
87
+ def parse_endpoints(from, to)
88
+ original_values = [from, to]
89
+ if original_values.all? { |p| lc_alpha?(p) }
90
+ original_values.map(&:to_s)
91
+ else
92
+ original_values.map { |p| int_or_nil(p) }
93
+ end
94
+ end
95
+
96
+ def lc_alpha?(endpoint)
97
+ endpoint_str = endpoint.to_s
98
+ endpoint_str.size == 1 &&
99
+ endpoint_str.ord >= 'a'.ord &&
100
+ endpoint_str.ord <= 'z'.ord
101
+ end
102
+ end
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,18 @@
1
+ require 'marc/spec/queries/part'
2
+
3
+ module MARC
4
+ module Spec
5
+ module Queries
6
+ # Supermodule of query objects that can return a result
7
+ module Applicable
8
+ include Part
9
+
10
+ def apply(marc_obj)
11
+ return [] unless can_apply?(marc_obj)
12
+
13
+ do_apply(marc_obj)
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,81 @@
1
+ require 'stringio'
2
+ require 'marc/spec/queries/selector'
3
+
4
+ module MARC
5
+ module Spec
6
+ module Queries
7
+ class CharacterSpec
8
+ include Selector
9
+
10
+ # ------------------------------------------------------------
11
+ # Attributes
12
+
13
+ attr_reader :character_spec
14
+
15
+ # ------------------------------------------------------------
16
+ # Initializer
17
+
18
+ def initialize(character_spec = AlNumRange.new(0, nil))
19
+ @character_spec = ensure_type(character_spec, PositionOrRange, allow_nil: false)
20
+ end
21
+
22
+ # ------------------------------------------------------------
23
+ # Object overrides
24
+
25
+ def to_s
26
+ "/#{character_spec}"
27
+ end
28
+
29
+ # ------------------------------
30
+ # Applicable
31
+
32
+ def can_apply?(marc_obj)
33
+ # MARC leader is ControlField-like but is returned as string
34
+ [String, MARC::ControlField, MARC::Subfield].any? { |t| marc_obj.is_a?(t) }
35
+ end
36
+
37
+ # ------------------------------------------------------------
38
+ # Protected methods
39
+
40
+ protected
41
+
42
+ # ------------------------------
43
+ # Applicable
44
+
45
+ def do_apply(control_field)
46
+ field_value = field_value_for(control_field)
47
+ field_value ? [field_value] : []
48
+ end
49
+
50
+ # ------------------------------
51
+ # Part
52
+
53
+ def equality_attrs
54
+ %i[character_spec]
55
+ end
56
+
57
+ def to_s_inspect
58
+ "/#{character_spec.inspect}"
59
+ end
60
+
61
+ # ------------------------------------------------------------
62
+ # Private methods
63
+
64
+ private
65
+
66
+ def field_value_for(control_field)
67
+ value_str = string_value_from(control_field)
68
+ return value_str unless character_spec
69
+
70
+ character_spec.select_from(value_str)
71
+ end
72
+
73
+ def string_value_from(tag_result)
74
+ return tag_result if tag_result.is_a?(String)
75
+ return tag_result.value if tag_result.respond_to?(:value)
76
+ end
77
+
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,45 @@
1
+ require 'marc/spec/queries/part'
2
+
3
+ module MARC
4
+ module Spec
5
+ module Queries
6
+ class ComparisonString
7
+ include Part
8
+
9
+ # ------------------------------------------------------------
10
+ # Accessors
11
+
12
+ attr_reader :str_raw, :str_exact
13
+
14
+ # ------------------------------------------------------------
15
+ # Initializer
16
+
17
+ def initialize(str_raw)
18
+ @str_raw = str_raw.to_s
19
+ @str_exact = unescape(@str_raw)
20
+ end
21
+
22
+ # ------------------------------------------------------------
23
+ # Object overrides
24
+
25
+ def to_s
26
+ "\\#{str_raw}"
27
+ end
28
+
29
+ # ------------------------------------------------------------
30
+ # Protected methods
31
+
32
+ protected
33
+
34
+ def equality_attrs
35
+ [:str_raw]
36
+ end
37
+
38
+ def unescape(str_raw)
39
+ str_raw.gsub(/\\(?=[${}!=~?|])/, '').gsub(/\\s/, ' ')
40
+ end
41
+
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,133 @@
1
+ require 'marc/spec/queries/condition_context'
2
+ require 'marc/spec/queries/part'
3
+ require 'marc/spec/queries/operator'
4
+
5
+ module MARC
6
+ module Spec
7
+ module Queries
8
+ class Condition
9
+ include Part
10
+
11
+ # ------------------------------------------------------------
12
+ # Attributes
13
+
14
+ attr_reader :left, :operator, :right
15
+
16
+ # ------------------------------------------------------------
17
+ # Initializer
18
+
19
+ # rubocop:disable Style/KeywordParametersOrder
20
+ def initialize(operator = '?', left: nil, right:)
21
+ @operator = Operator.from_str(operator)
22
+ # TODO: verify left semantics for unary operators
23
+ # see: https://marcspec.github.io/MARCspec/marc-spec.html#general
24
+ # https://marcspec.github.io/MARCspec/marc-spec.html#subspec-interpretation
25
+
26
+ @left = left_operand(left) if binary?
27
+ @right = right_operand(right)
28
+ end
29
+ # rubocop:enable Style/KeywordParametersOrder
30
+
31
+ # ------------------------------------------------------------
32
+ # Static factory methods
33
+
34
+ class << self
35
+ def any_of(*conditions)
36
+ conditions.inject do |cc, c|
37
+ cc.or(c)
38
+ end
39
+ end
40
+
41
+ def all_of(*conditions)
42
+ conditions.inject { |cc, c| cc.and(c) }
43
+ end
44
+ end
45
+
46
+ # ------------------------------------------------------------
47
+ # Instance methods
48
+
49
+ def met?(condition_context)
50
+ # puts self
51
+
52
+ right_val = condition_context.operand_value(right)
53
+ # puts "\t#{right.inspect} -> #{right_val.inspect}"
54
+ return unary_apply(right_val) unless binary?
55
+
56
+ left_val = condition_context.operand_value(left, implicit: true)
57
+ # puts "\t#{left.inspect} -> #{left_val.inspect}"
58
+ binary_apply(left_val, right_val)
59
+ end
60
+
61
+ def and(other_condition)
62
+ return self if other_condition == self || other_condition.nil?
63
+
64
+ Condition.new('&&', left: self, right: other_condition)
65
+ end
66
+
67
+ def or(other_condition)
68
+ return self if other_condition == self || other_condition.nil?
69
+
70
+ Condition.new('||', left: self, right: other_condition)
71
+ end
72
+
73
+ # ------------------------------------------------------------
74
+ # Object overrides
75
+
76
+ def to_s
77
+ operator.to_expression(left, right)
78
+ end
79
+
80
+ # ------------------------------------------------------------
81
+ # Protected methods
82
+
83
+ protected
84
+
85
+ def to_s_inspect
86
+ StringIO.new.tap do |out|
87
+ out << left.inspect if left
88
+ out << operator
89
+ out << right.inspect
90
+ end.string
91
+ end
92
+
93
+ def equality_attrs
94
+ %i[left operator right]
95
+ end
96
+
97
+ private
98
+
99
+ def binary_apply(left_val, right_val)
100
+ operator.apply(left_val, right_val).tap do |_result|
101
+ # puts "\t#{left_val} #{operator} #{right_val} => #{_result}"
102
+ end
103
+ end
104
+
105
+ def unary_apply(right_val)
106
+ operator.apply(right_val).tap do |_result|
107
+ # puts "\t#{operator} #{right_val} => #{_result}"
108
+ end
109
+ end
110
+
111
+ def right_operand(right)
112
+ return right if right.is_a?(ComparisonString)
113
+
114
+ operand(right)
115
+ end
116
+
117
+ def left_operand(left)
118
+ operand(left) if left
119
+ end
120
+
121
+ # TODO: superinterface?
122
+ def operand(operand)
123
+ return operand if operand.is_a?(Condition) || operand.is_a?(Query)
124
+ return Query.new(tag: operand) if operand.is_a?(Tag)
125
+ end
126
+
127
+ def binary?
128
+ operator.binary?
129
+ end
130
+ end
131
+ end
132
+ end
133
+ end
@@ -0,0 +1,49 @@
1
+ require 'marc/spec/queries/comparison_string'
2
+ require 'marc/spec/queries/condition'
3
+ require 'marc/spec/queries/query'
4
+
5
+ module MARC
6
+ module Spec
7
+ module Queries
8
+ class ConditionContext
9
+ attr_reader :context_field, :context_result, :executor
10
+
11
+ def initialize(context_field, context_result, executor)
12
+ @context_field = context_field
13
+ @context_result = context_result
14
+ @executor = executor
15
+ end
16
+
17
+ def operand_value(operand, implicit: false)
18
+ return context_result if implicit && operand.nil?
19
+
20
+ raw_value = operand_value_raw(operand)
21
+ is_boolean = [true, false].include?(raw_value)
22
+ is_boolean ? raw_value : as_string(raw_value)
23
+ end
24
+
25
+ private
26
+
27
+ def operand_value_raw(operand)
28
+ return unless operand
29
+
30
+ case operand
31
+ when ComparisonString
32
+ operand.str_exact
33
+ when Condition
34
+ operand.met?(self)
35
+ when Query
36
+ operand.execute(executor, [context_field], context_result)
37
+ end
38
+ end
39
+
40
+ def as_string(op_val)
41
+ return unless op_val
42
+ return op_val if op_val.is_a?(String)
43
+ return op_val.value if op_val.respond_to?(:value) && !op_val.is_a?(MARC::DataField)
44
+ return op_val.map { |v| as_string(v) } if op_val.is_a?(Array)
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end