ruby-marc-spec 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.github/workflows/build.yml +18 -0
- data/.gitignore +388 -0
- data/.gitmodules +3 -0
- data/.idea/codeStyles/codeStyleConfig.xml +5 -0
- data/.idea/go.imports.xml +6 -0
- data/.idea/inspectionProfiles/Project_Default.xml +23 -0
- data/.idea/marc_spec.iml +102 -0
- data/.idea/misc.xml +6 -0
- data/.idea/modules.xml +8 -0
- data/.idea/templateLanguages.xml +6 -0
- data/.idea/vcs.xml +7 -0
- data/.rubocop.yml +269 -0
- data/.ruby-version +1 -0
- data/.simplecov +8 -0
- data/CHANGES.md +3 -0
- data/Gemfile +6 -0
- data/LICENSE.md +21 -0
- data/README.md +172 -0
- data/Rakefile +20 -0
- data/lib/.rubocop.yml +5 -0
- data/lib/marc/spec/module_info.rb +14 -0
- data/lib/marc/spec/parsing/closed_int_range.rb +28 -0
- data/lib/marc/spec/parsing/closed_lc_alpha_range.rb +28 -0
- data/lib/marc/spec/parsing/parser.rb +213 -0
- data/lib/marc/spec/parsing.rb +1 -0
- data/lib/marc/spec/queries/al_num_range.rb +105 -0
- data/lib/marc/spec/queries/applicable.rb +18 -0
- data/lib/marc/spec/queries/character_spec.rb +81 -0
- data/lib/marc/spec/queries/comparison_string.rb +45 -0
- data/lib/marc/spec/queries/condition.rb +133 -0
- data/lib/marc/spec/queries/condition_context.rb +49 -0
- data/lib/marc/spec/queries/dsl.rb +80 -0
- data/lib/marc/spec/queries/indicator_value.rb +77 -0
- data/lib/marc/spec/queries/operator.rb +129 -0
- data/lib/marc/spec/queries/part.rb +63 -0
- data/lib/marc/spec/queries/position.rb +59 -0
- data/lib/marc/spec/queries/position_or_range.rb +27 -0
- data/lib/marc/spec/queries/query.rb +94 -0
- data/lib/marc/spec/queries/query_executor.rb +52 -0
- data/lib/marc/spec/queries/selector.rb +12 -0
- data/lib/marc/spec/queries/subfield.rb +88 -0
- data/lib/marc/spec/queries/subfield_value.rb +63 -0
- data/lib/marc/spec/queries/tag.rb +107 -0
- data/lib/marc/spec/queries/transform.rb +154 -0
- data/lib/marc/spec/queries.rb +1 -0
- data/lib/marc/spec.rb +32 -0
- data/rakelib/.rubocop.yml +19 -0
- data/rakelib/bundle.rake +8 -0
- data/rakelib/coverage.rake +11 -0
- data/rakelib/gem.rake +54 -0
- data/rakelib/parser_specs/formatter.rb +31 -0
- data/rakelib/parser_specs/parser_specs.rb.txt.erb +35 -0
- data/rakelib/parser_specs/rule.rb +95 -0
- data/rakelib/parser_specs/suite.rb +91 -0
- data/rakelib/parser_specs/test.rb +97 -0
- data/rakelib/parser_specs.rb +1 -0
- data/rakelib/rubocop.rake +18 -0
- data/rakelib/spec.rake +27 -0
- data/ruby-marc-spec.gemspec +42 -0
- data/spec/.rubocop.yml +46 -0
- data/spec/README.md +16 -0
- data/spec/data/b23161018-sru.xml +182 -0
- data/spec/data/sandburg.xml +82 -0
- data/spec/generated/char_indicator_spec.rb +174 -0
- data/spec/generated/char_spec.rb +113 -0
- data/spec/generated/comparison_string_spec.rb +74 -0
- data/spec/generated/field_tag_spec.rb +156 -0
- data/spec/generated/index_char_spec.rb +669 -0
- data/spec/generated/index_indicator_spec.rb +174 -0
- data/spec/generated/index_spec.rb +113 -0
- data/spec/generated/index_sub_spec_spec.rb +1087 -0
- data/spec/generated/indicators_spec.rb +75 -0
- data/spec/generated/position_or_range_spec.rb +110 -0
- data/spec/generated/sub_spec_spec.rb +208 -0
- data/spec/generated/sub_spec_sub_spec_spec.rb +1829 -0
- data/spec/generated/subfield_char_spec.rb +405 -0
- data/spec/generated/subfield_range_range_spec.rb +48 -0
- data/spec/generated/subfield_range_spec.rb +87 -0
- data/spec/generated/subfield_range_sub_spec_spec.rb +214 -0
- data/spec/generated/subfield_tag_range_spec.rb +477 -0
- data/spec/generated/subfield_tag_sub_spec_spec.rb +3216 -0
- data/spec/generated/subfield_tag_tag_spec.rb +5592 -0
- data/spec/marc/spec/parsing/closed_int_range_spec.rb +49 -0
- data/spec/marc/spec/parsing/closed_lc_alpha_range_spec.rb +49 -0
- data/spec/marc/spec/parsing/parser_spec.rb +545 -0
- data/spec/marc/spec/queries/al_num_range_spec.rb +114 -0
- data/spec/marc/spec/queries/character_spec_spec.rb +28 -0
- data/spec/marc/spec/queries/comparison_string_spec.rb +28 -0
- data/spec/marc/spec/queries/indicator_value_spec.rb +28 -0
- data/spec/marc/spec/queries/query_spec.rb +200 -0
- data/spec/marc/spec/queries/subfield_spec.rb +92 -0
- data/spec/marc/spec/queries/subfield_value_spec.rb +31 -0
- data/spec/marc/spec/queries/tag_spec.rb +144 -0
- data/spec/marc/spec/queries/transform_spec.rb +459 -0
- data/spec/marc_spec_spec.rb +247 -0
- data/spec/scratch_spec.rb +112 -0
- data/spec/spec_helper.rb +23 -0
- 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
|