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