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