dcc 0.2.0 → 0.2.2
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 +4 -4
- data/.github/workflows/ci.yml +19 -0
- data/.github/workflows/release.yml +28 -0
- data/.gitignore +4 -0
- data/.rubocop_todo.yml +560 -10
- data/CHANGELOG.adoc +34 -1
- data/CONTRIBUTING.adoc +8 -1
- data/Gemfile +5 -7
- data/README.adoc +162 -7
- data/dcc.gemspec +9 -2
- data/docs/schema-versions.adoc +20 -7
- data/examples/build_minimal_dcc.rb +0 -0
- data/examples/convert_all_formats.rb +2 -1
- data/examples/parse_and_inspect.rb +0 -0
- data/examples/quantity_math_demo.rb +7 -7
- data/examples/validate_full.rb +1 -1
- data/lib/dcc/base/calibration_laboratory.rb +2 -1
- data/lib/dcc/base/condition.rb +5 -2
- data/lib/dcc/base/core_data.rb +8 -3
- data/lib/dcc/base/formula.rb +3 -3
- data/lib/dcc/base/identification.rb +6 -3
- data/lib/dcc/base/mathml.rb +33 -0
- data/lib/dcc/base/quantity.rb +2 -1
- data/lib/dcc/base/resp_person.rb +4 -2
- data/lib/dcc/base/text.rb +1 -1
- data/lib/dcc/base.rb +2 -1
- data/lib/dcc/builder/dsl.rb +1 -1
- data/lib/dcc/builder/session.rb +16 -14
- data/lib/dcc/builder.rb +3 -3
- data/lib/dcc/cli/cli.rb +152 -26
- data/lib/dcc/cli/formatters.rb +35 -7
- data/lib/dcc/cli.rb +1 -1
- data/lib/dcc/context_configuration.rb +2 -1
- data/lib/dcc/context_options.rb +4 -2
- data/lib/dcc/convert/csv.rb +27 -9
- data/lib/dcc/convert/html.rb +26 -110
- data/lib/dcc/convert/json.rb +10 -7
- data/lib/dcc/convert/liquid_filters.rb +58 -0
- data/lib/dcc/convert/result.rb +1 -1
- data/lib/dcc/convert/templates/report.liquid +250 -0
- data/lib/dcc/convert/yaml.rb +1 -1
- data/lib/dcc/convert.rb +1 -0
- data/lib/dcc/diff/change.rb +1 -1
- data/lib/dcc/diff/result.rb +8 -4
- data/lib/dcc/diff.rb +43 -11
- data/lib/dcc/error.rb +7 -0
- data/lib/dcc/extract/file.rb +3 -4
- data/lib/dcc/extract/formula/ast.rb +137 -0
- data/lib/dcc/extract/formula/evaluator.rb +357 -0
- data/lib/dcc/extract/formula/parser.rb +305 -0
- data/lib/dcc/extract/formula/quantity.rb +89 -0
- data/lib/dcc/extract/formula.rb +110 -0
- data/lib/dcc/extract/ring.rb +1 -1
- data/lib/dcc/extract.rb +1 -1
- data/lib/dcc/i18n/text_lookup.rb +8 -4
- data/lib/dcc/i18n.rb +1 -1
- data/lib/dcc/inspect/report.rb +51 -14
- data/lib/dcc/inspect.rb +1 -1
- data/lib/dcc/migrate/route.rb +44 -0
- data/lib/dcc/migrate/v2_to_v3.rb +445 -0
- data/lib/dcc/migrate.rb +130 -26
- data/lib/dcc/plugin/base.rb +64 -0
- data/lib/dcc/plugin/registry.rb +7 -2
- data/lib/dcc/plugin.rb +6 -2
- data/lib/dcc/quantity_format/formatter.rb +16 -10
- data/lib/dcc/quantity_format.rb +1 -1
- data/lib/dcc/quantity_math/complex.rb +252 -0
- data/lib/dcc/quantity_math/matrix.rb +217 -0
- data/lib/dcc/quantity_math/quantity.rb +1 -1
- data/lib/dcc/quantity_math/real.rb +6 -6
- data/lib/dcc/quantity_math.rb +31 -3
- data/lib/dcc/schema/version.rb +2 -2
- data/lib/dcc/server/app.html +282 -0
- data/lib/dcc/server/app.rb +171 -87
- data/lib/dcc/server/demo_cert.xml +244 -0
- data/lib/dcc/server/storage/entry.rb +1 -1
- data/lib/dcc/server/storage/memory.rb +3 -2
- data/lib/dcc/server/storage.rb +1 -1
- data/lib/dcc/server.rb +1 -1
- data/lib/dcc/si/base/complex.rb +1 -1
- data/lib/dcc/si/base/complex_list_xml_list.rb +1 -1
- data/lib/dcc/si/base/constant.rb +1 -1
- data/lib/dcc/si/base/coverage_interval.rb +36 -0
- data/lib/dcc/si/base/coverage_interval_mu.rb +1 -1
- data/lib/dcc/si/base/expanded_mu.rb +1 -1
- data/lib/dcc/si/base/expanded_unc.rb +34 -0
- data/lib/dcc/si/base/expanded_unc_xml_list.rb +3 -2
- data/lib/dcc/si/base/hybrid.rb +3 -2
- data/lib/dcc/si/base/measurement_uncertainty_univariate.rb +1 -1
- data/lib/dcc/si/base/real.rb +13 -3
- data/lib/dcc/si/base/real_list_xml_list.rb +5 -3
- data/lib/dcc/si/base/si_list.rb +3 -2
- data/lib/dcc/si/base/standard_mu.rb +1 -1
- data/lib/dcc/si/base.rb +5 -2
- data/lib/dcc/si/v1/coverage_interval.rb +8 -0
- data/lib/dcc/si/v1/expanded_unc.rb +1 -16
- data/lib/dcc/si/v1/measurement_uncertainty_univariate.rb +2 -1
- data/lib/dcc/si/v1.rb +5 -2
- data/lib/dcc/si/v2/coverage_interval.rb +8 -0
- data/lib/dcc/si/v2/coverage_interval_mu_xml_list.rb +4 -2
- data/lib/dcc/si/v2/expanded_mu_xml_list.rb +2 -1
- data/lib/dcc/si/v2/expanded_unc.rb +8 -0
- data/lib/dcc/si/v2/measurement_uncertainty_univariate.rb +2 -1
- data/lib/dcc/si/v2.rb +8 -3
- data/lib/dcc/signature/result.rb +2 -2
- data/lib/dcc/signature/signer.rb +31 -18
- data/lib/dcc/signature/verifier.rb +23 -21
- data/lib/dcc/signature.rb +1 -1
- data/lib/dcc/streaming/reader.rb +246 -0
- data/lib/dcc/streaming.rb +79 -0
- data/lib/dcc/transform/result.rb +1 -1
- data/lib/dcc/transform/xslt.rb +22 -12
- data/lib/dcc/transform.rb +5 -4
- data/lib/dcc/type/base64_binary.rb +5 -3
- data/lib/dcc/type/decimal_xml_list.rb +99 -13
- data/lib/dcc/type/iso_country_code.rb +3 -2
- data/lib/dcc/type/iso_language_code.rb +3 -2
- data/lib/dcc/type/schema_version.rb +3 -2
- data/lib/dcc/type/si_unit.rb +3 -2
- data/lib/dcc/type_guards.rb +3 -3
- data/lib/dcc/v2/calibration_laboratory.rb +2 -1
- data/lib/dcc/v2/condition.rb +22 -1
- data/lib/dcc/v2/configuration.rb +1 -1
- data/lib/dcc/v2/digital_calibration_certificate.rb +2 -1
- data/lib/dcc/v2/identification.rb +14 -1
- data/lib/dcc/v2/mathml.rb +8 -0
- data/lib/dcc/v2/measurement_meta_data_list.rb +2 -1
- data/lib/dcc/v2/quantity.rb +4 -2
- data/lib/dcc/v2.rb +4 -2
- data/lib/dcc/v3/calibration_laboratory.rb +2 -1
- data/lib/dcc/v3/configuration.rb +1 -1
- data/lib/dcc/v3/digital_calibration_certificate.rb +2 -1
- data/lib/dcc/v3/mathml.rb +8 -0
- data/lib/dcc/v3/measurement_meta_data_list.rb +2 -1
- data/lib/dcc/v3/quantity.rb +4 -2
- data/lib/dcc/v3.rb +12 -2
- data/lib/dcc/validate/business_rules.rb +23 -9
- data/lib/dcc/validate/issue.rb +2 -1
- data/lib/dcc/validate/result.rb +6 -4
- data/lib/dcc/validate/schematron/profile.rb +73 -18
- data/lib/dcc/validate/schematron/rule.rb +13 -3
- data/lib/dcc/validate/schematron/rules/administrative_data_completeness.rb +11 -4
- data/lib/dcc/validate/schematron/rules/base.rb +1 -2
- data/lib/dcc/validate/schematron/rules/date_range_check.rb +11 -5
- data/lib/dcc/validate/schematron/rules/id_refid_linking.rb +0 -3
- data/lib/dcc/validate/schematron/rules/influence_conditions_placement.rb +22 -12
- data/lib/dcc/validate/schematron/rules/iso_code_validation.rb +31 -14
- data/lib/dcc/validate/schematron/rules/language_code_dedup.rb +12 -5
- data/lib/dcc/validate/schematron/rules/non_si_declaration.rb +7 -4
- data/lib/dcc/validate/schematron/rules/release_format_check.rb +7 -4
- data/lib/dcc/validate/schematron/rules/schema_version_check.rb +13 -9
- data/lib/dcc/validate/schematron/rules/uncertainty_consistency.rb +39 -10
- data/lib/dcc/validate/schematron/rules/unit_format_check.rb +2 -3
- data/lib/dcc/validate/schematron/rules/used_methods_placement.rb +15 -10
- data/lib/dcc/validate/schematron/rules/used_software_placement.rb +15 -10
- data/lib/dcc/validate/schematron/rules/xml_list_spacing.rb +4 -3
- data/lib/dcc/validate/schematron/rules.rb +28 -14
- data/lib/dcc/validate/schematron.rb +1 -1
- data/lib/dcc/validate/xsd.rb +9 -3
- data/lib/dcc/validate.rb +1 -1
- data/lib/dcc/version.rb +1 -1
- data/lib/dcc/versioned_parser.rb +1 -1
- data/lib/dcc.rb +68 -10
- data/sample_dcc.xml +140 -0
- data/web/index.html +12 -0
- data/web/package-lock.json +1599 -0
- data/web/package.json +22 -0
- data/web/src/App.vue +349 -0
- data/web/src/lib/builder.ts +171 -0
- data/web/src/lib/dcc.ts +338 -0
- data/web/src/lib/units.json +5581 -0
- data/web/src/main.ts +5 -0
- data/web/src/style.css +27 -0
- data/web/tsconfig.json +15 -0
- data/web/vite.config.ts +14 -0
- metadata +148 -9
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "moxml"
|
|
4
|
+
|
|
5
|
+
module Dcc
|
|
6
|
+
module Streaming
|
|
7
|
+
# SAX handler that reconstructs each matched subtree as a standalone XML
|
|
8
|
+
# fragment, parses it into the version-appropriate model, yields it, and
|
|
9
|
+
# drops it. Only one subtree is buffered at a time, so peak memory tracks
|
|
10
|
+
# the largest single subtree rather than the document.
|
|
11
|
+
#
|
|
12
|
+
# The fragment reproduces the XML infoset, not the original bytes.
|
|
13
|
+
# Entity references arrive expanded, CDATA becomes text, processing
|
|
14
|
+
# instruction spacing is normalised, and every namespace in scope is
|
|
15
|
+
# re-declared on the fragment root so prefixes still resolve once the
|
|
16
|
+
# subtree is detached.
|
|
17
|
+
class Reader < ::Moxml::SAX::Handler
|
|
18
|
+
# Stream `io`, yielding one model per matched subtree.
|
|
19
|
+
#
|
|
20
|
+
# @param io [IO, StringIO] readable XML stream.
|
|
21
|
+
# @param model_id [Symbol] registered model id, `:item` or `:quantity`.
|
|
22
|
+
# @param version [Integer, nil] major DCC version. When nil it is read
|
|
23
|
+
# from the first `schemaVersion` attribute seen outside a matched
|
|
24
|
+
# subtree before the first model is yielded, and falls back to 3.
|
|
25
|
+
# `Dcc.detect_version` instead scans the whole document lexically,
|
|
26
|
+
# because it is not restricted to a single pass.
|
|
27
|
+
# @param context [Symbol, String, nil] substitution context.
|
|
28
|
+
# @return [void]
|
|
29
|
+
# @raise [Dcc::ParseError] if the stream is not well-formed XML. A
|
|
30
|
+
# failing IO also surfaces this way, with the original IO message
|
|
31
|
+
# replaced by the parser's — libxml2 does not pass it through.
|
|
32
|
+
def self.call(io, model_id:, version:, context:, &)
|
|
33
|
+
handler = new(model_id:, version:, context:, &)
|
|
34
|
+
adapter = ::Lutaml::Model::Config.xml_adapter_type
|
|
35
|
+
::Moxml.new(adapter).sax_parse(io, handler)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# A handler carries one document's worth of state, so `.call` builds a
|
|
39
|
+
# fresh one per stream and construction stays closed.
|
|
40
|
+
private_class_method :new
|
|
41
|
+
|
|
42
|
+
def initialize(model_id:, version:, context:, &block)
|
|
43
|
+
super()
|
|
44
|
+
@model_id = model_id
|
|
45
|
+
@major = version
|
|
46
|
+
@context = context
|
|
47
|
+
@block = block
|
|
48
|
+
@scopes = [{}]
|
|
49
|
+
@subtree = nil
|
|
50
|
+
@depth = 0
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Declarations are merged and snapshotted before the element's own
|
|
54
|
+
# prefix is resolved, so an element that rebinds its prefix matches on
|
|
55
|
+
# its new binding rather than the inherited one.
|
|
56
|
+
def on_start_element(name, attributes = {}, namespaces = {})
|
|
57
|
+
# Almost no element declares a namespace, so merging every time would
|
|
58
|
+
# allocate a copy of the parent scope per element. Scopes are only
|
|
59
|
+
# ever read, so the unchanged ones can share one hash.
|
|
60
|
+
@scopes.push(
|
|
61
|
+
namespaces.empty? ? @scopes.last : @scopes.last.merge(namespaces),
|
|
62
|
+
)
|
|
63
|
+
return begin_subtree(name, attributes) unless @subtree
|
|
64
|
+
|
|
65
|
+
@depth += 1
|
|
66
|
+
@subtree.start_element(name, attributes, namespaces)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def on_end_element(name)
|
|
70
|
+
finish_element(name) if @subtree
|
|
71
|
+
@scopes.pop
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def on_characters(text)
|
|
75
|
+
@subtree&.characters(text)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def on_cdata(text)
|
|
79
|
+
@subtree&.characters(text)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def on_comment(text)
|
|
83
|
+
@subtree&.comment(text)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def on_processing_instruction(target, data)
|
|
87
|
+
@subtree&.processing_instruction(target, data)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Translated here rather than around the whole parse, so an exception
|
|
91
|
+
# raised by the consumer block travels untouched instead of being
|
|
92
|
+
# reported as malformed input.
|
|
93
|
+
def on_error(error)
|
|
94
|
+
raise ::Dcc::ParseError, error.message
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
private
|
|
98
|
+
|
|
99
|
+
# Only elements outside a matched subtree get a say in the version, so a
|
|
100
|
+
# `schemaVersion` on a descendant — or inside an opaque `dcc:xml` blob —
|
|
101
|
+
# cannot choose the parser for the whole stream.
|
|
102
|
+
def begin_subtree(name, attributes)
|
|
103
|
+
@major ||= schema_version_major(name, attributes)
|
|
104
|
+
return unless match?(name)
|
|
105
|
+
|
|
106
|
+
@depth = 0
|
|
107
|
+
@subtree = Subtree.new(name, attributes, @scopes.last)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def finish_element(name)
|
|
111
|
+
@subtree.end_element(name)
|
|
112
|
+
if @depth.zero?
|
|
113
|
+
emit(@subtree.to_xml)
|
|
114
|
+
@subtree = nil
|
|
115
|
+
else
|
|
116
|
+
@depth -= 1
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def emit(xml)
|
|
121
|
+
@block.call(model_class.from_xml(xml, register: context_id))
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def match?(name)
|
|
125
|
+
split(name).last == @model_id.to_s && dcc_element?(name)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def dcc_element?(name)
|
|
129
|
+
dcc_namespace?(@scopes.last[split(name).first])
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# `Dcc::Namespace::Dcc` also accepts the `.xsd` alias that older PTB
|
|
133
|
+
# documents bind, so streaming matches whatever `Dcc.parse` accepts.
|
|
134
|
+
def dcc_namespace?(uri)
|
|
135
|
+
namespace = ::Dcc::Namespace::Dcc
|
|
136
|
+
uri == namespace.uri || namespace.uri_aliases.include?(uri)
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def split(name)
|
|
140
|
+
prefix, local = name.split(":", 2)
|
|
141
|
+
local ? [prefix, local] : [nil, prefix]
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# Resolved through the registry rather than referenced directly, so a
|
|
145
|
+
# configured custom model substitutes the root class too.
|
|
146
|
+
def model_class
|
|
147
|
+
@model_class ||=
|
|
148
|
+
::Lutaml::Model::GlobalContext.resolve_type(@model_id, context_id)
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def context_id
|
|
152
|
+
@context_id ||= ::Dcc::ContextOptions.normalize_context_option(
|
|
153
|
+
context: @context,
|
|
154
|
+
register: nil,
|
|
155
|
+
default_context: configuration.default_context_id,
|
|
156
|
+
warning_source: "Dcc::Streaming",
|
|
157
|
+
)
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def configuration
|
|
161
|
+
::Dcc.parser_for(major)::Configuration
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# `schemaVersion` sits on the DCC root, which is not always the document
|
|
165
|
+
# root — a certificate can arrive wrapped in an envelope. Only a DCC
|
|
166
|
+
# element settles the version, so a wrapper carrying a `schemaVersion`
|
|
167
|
+
# of its own does not retype the certificate nested inside it.
|
|
168
|
+
def schema_version_major(name, attributes)
|
|
169
|
+
version = attributes["schemaVersion"]
|
|
170
|
+
return unless version && dcc_element?(name)
|
|
171
|
+
|
|
172
|
+
::Dcc.major_version_from(version)
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# A document that declares `schemaVersion` nowhere falls back the way an
|
|
176
|
+
# unusable one does, so `Dcc` keeps deciding that rather than a literal
|
|
177
|
+
# repeated here. The answer is written back rather than defaulted on
|
|
178
|
+
# each read: the model class and the context are memoised off this at
|
|
179
|
+
# the first yield, so a `schemaVersion` appearing later must not move
|
|
180
|
+
# the version out from under them.
|
|
181
|
+
def major
|
|
182
|
+
@major ||= ::Dcc.major_version_from(nil)
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
# Accumulates the raw XML of a single matched subtree.
|
|
186
|
+
#
|
|
187
|
+
# SAX hands over decoded characters, so control whitespace has to go
|
|
188
|
+
# back as character references. Written literally it would be
|
|
189
|
+
# normalised a second time on reparse — a carriage return in text
|
|
190
|
+
# would arrive as a newline, and a tab or newline in an attribute
|
|
191
|
+
# would collapse to a space.
|
|
192
|
+
#
|
|
193
|
+
# C14N mandates exactly that set of character references, so moxml's
|
|
194
|
+
# encoder is reused rather than restating the table here.
|
|
195
|
+
class Subtree
|
|
196
|
+
def initialize(name, attributes, scope)
|
|
197
|
+
@buffer = +""
|
|
198
|
+
write_start(name, attributes, scope)
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def start_element(name, attributes, namespaces)
|
|
202
|
+
write_start(name, attributes, namespaces)
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def end_element(name)
|
|
206
|
+
@buffer << "</#{name}>"
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def characters(text)
|
|
210
|
+
@buffer << ::Moxml::C14n.escape_text(text)
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def comment(text)
|
|
214
|
+
@buffer << "<!--#{text}-->"
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
def processing_instruction(target, data)
|
|
218
|
+
@buffer << "<?#{target} #{data}?>"
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def to_xml
|
|
222
|
+
@buffer
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
private
|
|
226
|
+
|
|
227
|
+
def write_start(name, attributes, namespaces)
|
|
228
|
+
@buffer << "<#{name}"
|
|
229
|
+
namespaces.each do |prefix, href|
|
|
230
|
+
write_attribute(prefix ? "xmlns:#{prefix}" : "xmlns", href)
|
|
231
|
+
end
|
|
232
|
+
attributes.each do |key, value|
|
|
233
|
+
write_attribute(key, value)
|
|
234
|
+
end
|
|
235
|
+
@buffer << ">"
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
def write_attribute(name, value)
|
|
239
|
+
escaped = ::Moxml::C14n.escape_attribute(value)
|
|
240
|
+
@buffer << %( #{name}="#{escaped}")
|
|
241
|
+
end
|
|
242
|
+
end
|
|
243
|
+
private_constant :Subtree
|
|
244
|
+
end
|
|
245
|
+
end
|
|
246
|
+
end
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# `Dcc::Streaming` walks a DCC document one subtree at a time, so a batch
|
|
4
|
+
# pipeline can process a multi-MB certificate without holding the whole
|
|
5
|
+
# object tree in memory. Each matched subtree is reconstructed on its own,
|
|
6
|
+
# parsed into the version-appropriate model, yielded, and dropped.
|
|
7
|
+
#
|
|
8
|
+
# @example Walk every calibrated item
|
|
9
|
+
# File.open("certificate.xml") do |io|
|
|
10
|
+
# Dcc::Streaming.each_item(io) { |item| puts item.id }
|
|
11
|
+
# end
|
|
12
|
+
#
|
|
13
|
+
# @example Take the first two quantities without reading the whole document
|
|
14
|
+
# Dcc::Streaming.each_quantity(io).first(2)
|
|
15
|
+
#
|
|
16
|
+
# Version detection differs from `Dcc.parse`. Streaming reads `schemaVersion`
|
|
17
|
+
# off a real DCC element, so a certificate wrapped in an envelope is detected
|
|
18
|
+
# from its own root even when the envelope carries a `schemaVersion` of its
|
|
19
|
+
# own. `Dcc.detect_version` scans the document lexically instead, so it also
|
|
20
|
+
# matches a `schemaVersion=` inside a comment, inside text or CDATA, on a
|
|
21
|
+
# prefixed attribute such as `f:schemaVersion`, or on a foreign wrapper
|
|
22
|
+
# element. The two can disagree on those documents, and streaming is the one
|
|
23
|
+
# reading the schema's actual attribute. Pass `version:` to settle it.
|
|
24
|
+
module Dcc
|
|
25
|
+
module Streaming
|
|
26
|
+
autoload :Reader, "dcc/streaming/reader"
|
|
27
|
+
|
|
28
|
+
class << self
|
|
29
|
+
# Yield each `dcc:item` in document order.
|
|
30
|
+
#
|
|
31
|
+
# @param io [IO, StringIO] readable XML stream.
|
|
32
|
+
# @param version [Integer, nil] major DCC version (2 or 3). When nil it
|
|
33
|
+
# is detected from the first `schemaVersion` attribute seen outside a
|
|
34
|
+
# matched subtree before the first item is yielded, and falls back to
|
|
35
|
+
# 3. See the note on version detection above.
|
|
36
|
+
# @param context [Symbol, String, nil] substitution context. Defaults
|
|
37
|
+
# to the version's configured context.
|
|
38
|
+
# @yieldparam [Dcc::V2::Item, Dcc::V3::Item]
|
|
39
|
+
# @return [Enumerator] when no block is given.
|
|
40
|
+
# @raise [Dcc::ParseError] if the stream is not well-formed XML.
|
|
41
|
+
def each_item(io, version: nil,
|
|
42
|
+
context: ::Dcc::UNSPECIFIED_CONTEXT, &block)
|
|
43
|
+
return enum_for(:each_item, io, version:, context:) unless block
|
|
44
|
+
|
|
45
|
+
stream(io, :item, version, context, &block)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Yield each outermost `dcc:quantity` in document order.
|
|
49
|
+
#
|
|
50
|
+
# A quantity nested inside another quantity's `influenceConditions` is
|
|
51
|
+
# not yielded on its own — it is reachable from the outer object.
|
|
52
|
+
#
|
|
53
|
+
# @param io [IO, StringIO] readable XML stream.
|
|
54
|
+
# @param version [Integer, nil] major DCC version (2 or 3). Detected the
|
|
55
|
+
# same way `each_item` detects it when nil.
|
|
56
|
+
# @param context [Symbol, String, nil] substitution context.
|
|
57
|
+
# @yieldparam [Dcc::V2::Quantity, Dcc::V3::Quantity]
|
|
58
|
+
# @return [Enumerator] when no block is given.
|
|
59
|
+
# @raise [Dcc::ParseError] if the stream is not well-formed XML.
|
|
60
|
+
def each_quantity(io, version: nil,
|
|
61
|
+
context: ::Dcc::UNSPECIFIED_CONTEXT, &block)
|
|
62
|
+
return enum_for(:each_quantity, io, version:, context:) unless block
|
|
63
|
+
|
|
64
|
+
stream(io, :quantity, version, context, &block)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
private
|
|
68
|
+
|
|
69
|
+
# `version` is validated here rather than on first match, so an
|
|
70
|
+
# unsupported value raises the same way regardless of what the
|
|
71
|
+
# document happens to contain.
|
|
72
|
+
def stream(io, model_id, version, context, &)
|
|
73
|
+
::Dcc.parser_for(version) if version
|
|
74
|
+
::Dcc.load_all!
|
|
75
|
+
Reader.call(io, model_id:, version:, context:, &)
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
data/lib/dcc/transform/result.rb
CHANGED
data/lib/dcc/transform/xslt.rb
CHANGED
|
@@ -6,9 +6,9 @@ module Dcc
|
|
|
6
6
|
module Transform
|
|
7
7
|
# `Dcc::Transform::Xslt` applies an XSLT stylesheet to a DCC XML document.
|
|
8
8
|
#
|
|
9
|
-
# Nokogiri is
|
|
10
|
-
# the system `xsltproc` (libxslt) for XSLT 1.0
|
|
11
|
-
# 2.0/3.0, set `ENV["SAXON_JAR"]` to
|
|
9
|
+
# Nokogiri is banned outside XSD validation and the v2-to-v3 migration,
|
|
10
|
+
# so we shell out to the system `xsltproc` (libxslt) for XSLT 1.0
|
|
11
|
+
# transforms. For XSLT 2.0/3.0, set `ENV["SAXON_JAR"]` to saxon-he.jar.
|
|
12
12
|
module Xslt
|
|
13
13
|
class << self
|
|
14
14
|
# @param xml [String, Lutaml::Model::Serializable]
|
|
@@ -21,7 +21,8 @@ module Dcc
|
|
|
21
21
|
when :xsltproc then run_xsltproc(xml_string, stylesheet)
|
|
22
22
|
when :saxon then run_saxon(xml_string, stylesheet)
|
|
23
23
|
else
|
|
24
|
-
raise ::ArgumentError,
|
|
24
|
+
raise ::ArgumentError,
|
|
25
|
+
"unknown XSLT engine: #{engine.inspect}"
|
|
25
26
|
end
|
|
26
27
|
::Dcc::Transform::Result.new(payload: output, engine: engine)
|
|
27
28
|
end
|
|
@@ -31,22 +32,31 @@ module Dcc
|
|
|
31
32
|
def run_xsltproc(xml, stylesheet)
|
|
32
33
|
ensure_tool!("xsltproc")
|
|
33
34
|
xsl_path = stylesheet_path(stylesheet)
|
|
34
|
-
out, status = ::Open3.capture2("xsltproc", xsl_path, "-",
|
|
35
|
-
|
|
35
|
+
out, status = ::Open3.capture2("xsltproc", xsl_path, "-",
|
|
36
|
+
stdin_data: xml)
|
|
37
|
+
unless status.success?
|
|
38
|
+
raise ::Dcc::TransformError,
|
|
39
|
+
"xsltproc failed: #{status}"
|
|
40
|
+
end
|
|
36
41
|
|
|
37
42
|
out
|
|
38
43
|
end
|
|
39
44
|
|
|
40
45
|
def run_saxon(xml, stylesheet)
|
|
41
|
-
jar = ENV
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
46
|
+
jar = ENV.fetch("SAXON_JAR", nil)
|
|
47
|
+
unless jar
|
|
48
|
+
raise ::Dcc::MissingDependencyError.new(gem_name: "saxon-he",
|
|
49
|
+
feature: "XSLT 2.0/3.0"),
|
|
50
|
+
"SAXON_JAR env var is required for the :saxon engine"
|
|
51
|
+
end
|
|
45
52
|
|
|
46
53
|
xsl_path = stylesheet_path(stylesheet)
|
|
47
54
|
cmd = ["java", "-jar", jar, "-s:-", "-xsl:#{xsl_path}"]
|
|
48
55
|
out, status = ::Open3.capture2(*cmd, stdin_data: xml)
|
|
49
|
-
|
|
56
|
+
unless status.success?
|
|
57
|
+
raise ::Dcc::TransformError,
|
|
58
|
+
"saxon failed: #{status}"
|
|
59
|
+
end
|
|
50
60
|
|
|
51
61
|
out
|
|
52
62
|
end
|
|
@@ -69,4 +79,4 @@ module Dcc
|
|
|
69
79
|
end
|
|
70
80
|
end
|
|
71
81
|
end
|
|
72
|
-
end
|
|
82
|
+
end
|
data/lib/dcc/transform.rb
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
# `Dcc::Transform` provides XSLT transformations. We cannot use Nokogiri
|
|
4
|
-
# for this per project rule (only validation
|
|
5
|
-
# we shell out to the system `xsltproc` CLI for
|
|
6
|
-
# `java -jar saxon-he.jar` for XSLT 2.0/3.0 when SAXON_JAR
|
|
4
|
+
# for this per project rule (only XSD validation and the v2-to-v3 migration
|
|
5
|
+
# may touch Nokogiri); instead we shell out to the system `xsltproc` CLI for
|
|
6
|
+
# XSLT 1.0, or to `java -jar saxon-he.jar` for XSLT 2.0/3.0 when SAXON_JAR
|
|
7
|
+
# is set.
|
|
7
8
|
module Dcc
|
|
8
9
|
module Transform
|
|
9
10
|
autoload :Xslt, "dcc/transform/xslt"
|
|
10
11
|
autoload :Result, "dcc/transform/result"
|
|
11
12
|
end
|
|
12
|
-
end
|
|
13
|
+
end
|
|
@@ -15,7 +15,8 @@ module Dcc
|
|
|
15
15
|
def self.cast(value)
|
|
16
16
|
return nil if value.nil? || value == ""
|
|
17
17
|
unless value.is_a?(::String)
|
|
18
|
-
raise Lutaml::Model::Type::InvalidValueError.new(value,
|
|
18
|
+
raise Lutaml::Model::Type::InvalidValueError.new(value,
|
|
19
|
+
"expected binary String")
|
|
19
20
|
end
|
|
20
21
|
|
|
21
22
|
value
|
|
@@ -25,7 +26,8 @@ module Dcc
|
|
|
25
26
|
return nil if value.nil?
|
|
26
27
|
|
|
27
28
|
unless value.is_a?(::String)
|
|
28
|
-
raise Lutaml::Model::Type::InvalidValueError.new(value,
|
|
29
|
+
raise Lutaml::Model::Type::InvalidValueError.new(value,
|
|
30
|
+
"expected binary String")
|
|
29
31
|
end
|
|
30
32
|
|
|
31
33
|
value
|
|
@@ -52,4 +54,4 @@ module Dcc
|
|
|
52
54
|
end
|
|
53
55
|
end
|
|
54
56
|
end
|
|
55
|
-
end
|
|
57
|
+
end
|
|
@@ -1,19 +1,86 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "bigdecimal"
|
|
4
|
+
require "forwardable"
|
|
4
5
|
require "lutaml/model"
|
|
5
6
|
|
|
6
7
|
module Dcc
|
|
7
8
|
module Type
|
|
8
9
|
# Space-separated list of decimals, as used by D-SI `*XMLList` elements
|
|
9
10
|
# (e.g. `<si:valueXMLList>306 373 448</si:valueXMLList>`). Parses to/from
|
|
10
|
-
#
|
|
11
|
+
# a `Values` list of BigDecimal.
|
|
12
|
+
#
|
|
13
|
+
# `cast` deliberately does not return a plain Array: lutaml-model treats an
|
|
14
|
+
# Array-valued attribute as a collection and would emit one element per
|
|
15
|
+
# decimal instead of a single space-separated list.
|
|
11
16
|
class DecimalXmlList < Lutaml::Model::Type::Value
|
|
17
|
+
# An ordered list of BigDecimals that renders as space-separated text and
|
|
18
|
+
# compares equal to the equivalent Array.
|
|
19
|
+
class Values
|
|
20
|
+
include Enumerable
|
|
21
|
+
extend Forwardable
|
|
22
|
+
|
|
23
|
+
def_delegators :@decimals, :size, :length, :empty?, :last, :[]
|
|
24
|
+
|
|
25
|
+
def initialize(decimals)
|
|
26
|
+
@decimals = decimals
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def each(&)
|
|
30
|
+
return to_enum(:each) unless block_given?
|
|
31
|
+
|
|
32
|
+
@decimals.each(&)
|
|
33
|
+
self
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Implicit conversion, so `Array#==` delegates back here instead of
|
|
37
|
+
# returning false and making equality asymmetric.
|
|
38
|
+
def to_ary
|
|
39
|
+
@decimals.dup
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Joined over the serialized decimals, not the raw BigDecimals, so it
|
|
43
|
+
# cannot disagree with `to_s` by emitting "0.72e-1" for "0.072".
|
|
44
|
+
def join(separator = " ")
|
|
45
|
+
map { |decimal| DecimalXmlList.serialize_one(decimal) }.join(separator)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def to_s
|
|
49
|
+
DecimalXmlList.serialize(self)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Dumped as its wire form, so YAML matches JSON rather than leaking a
|
|
53
|
+
# `!ruby/object:` tag that `safe_load` refuses to read back.
|
|
54
|
+
def encode_with(coder)
|
|
55
|
+
coder.represent_scalar(nil, to_s)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def ==(other)
|
|
59
|
+
case other
|
|
60
|
+
when Values then to_a == other.to_a
|
|
61
|
+
when ::Array then to_a == other
|
|
62
|
+
else false
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Stricter than `==`, which accepts a plain Array. Hash lookup pairs
|
|
67
|
+
# `eql?` with `hash`, and an Array keyed alongside us would hash the
|
|
68
|
+
# same while comparing equal in only one direction.
|
|
69
|
+
def eql?(other)
|
|
70
|
+
other.is_a?(Values) && to_a == other.to_a
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def hash
|
|
74
|
+
to_a.hash
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
12
78
|
def self.cast(value)
|
|
13
|
-
return
|
|
14
|
-
return
|
|
79
|
+
return value if value.is_a?(Values)
|
|
80
|
+
return Values.new([]) if value.nil?
|
|
15
81
|
|
|
16
|
-
value.
|
|
82
|
+
tokens = value.is_a?(::Array) ? value : value.to_s.split
|
|
83
|
+
Values.new(tokens.map { |token| cast_one(token) })
|
|
17
84
|
end
|
|
18
85
|
|
|
19
86
|
def self.serialize(value)
|
|
@@ -22,24 +89,43 @@ module Dcc
|
|
|
22
89
|
Array(value).map { |v| serialize_one(v) }.join(" ")
|
|
23
90
|
end
|
|
24
91
|
|
|
92
|
+
# One decimal in the notation D-SI writes, e.g. 0.072 rather than
|
|
93
|
+
# BigDecimal's own "0.72e-1". `to_s("F")` renders every BigDecimal,
|
|
94
|
+
# finite or not, and `sub` only trims a trailing ".0". That is what
|
|
95
|
+
# lets `NaN` through, which D-SI admits in every decimal type
|
|
96
|
+
# (SI_Format.xsd:1195).
|
|
97
|
+
def self.serialize_one(value)
|
|
98
|
+
reject_infinite!(value)
|
|
99
|
+
|
|
100
|
+
case value
|
|
101
|
+
when ::BigDecimal then value.to_s("F").sub(/\.0\z/, "")
|
|
102
|
+
when ::Float then format("%.15g", value)
|
|
103
|
+
else value.to_s
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
25
107
|
class << self
|
|
26
108
|
private
|
|
27
109
|
|
|
28
110
|
def cast_one(token)
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
111
|
+
decimal = token.is_a?(::BigDecimal) ? token : BigDecimal(token.to_s)
|
|
112
|
+
reject_infinite!(decimal)
|
|
113
|
+
decimal
|
|
32
114
|
rescue ::ArgumentError => e
|
|
33
115
|
raise Lutaml::Model::Type::InvalidValueError.new(token,
|
|
34
116
|
"invalid decimal in XML list: #{e.message}")
|
|
35
117
|
end
|
|
36
118
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
119
|
+
# Called from `cast_one` and from the public `serialize_one`, since
|
|
120
|
+
# `serialize` takes a raw BigDecimal and casting is not the only way in.
|
|
121
|
+
def reject_infinite!(value)
|
|
122
|
+
return unless value.is_a?(::BigDecimal) && value.infinite?
|
|
123
|
+
|
|
124
|
+
raise Lutaml::Model::Type::InvalidValueError.new(
|
|
125
|
+
value,
|
|
126
|
+
"infinity is not a D-SI decimal: SI_Format.xsd:1195 admits " \
|
|
127
|
+
"numbers and NaN only",
|
|
128
|
+
)
|
|
43
129
|
end
|
|
44
130
|
end
|
|
45
131
|
end
|
|
@@ -7,13 +7,14 @@ module Dcc
|
|
|
7
7
|
# ISO 3166-1 alpha-2 country code (two uppercase letters, e.g. `"DE"`).
|
|
8
8
|
# Used by `dcc:countryCodeISO3166_1`.
|
|
9
9
|
class IsoCountryCode < Lutaml::Model::Type::String
|
|
10
|
-
PATTERN = /\A[A-Z]{2}\z
|
|
10
|
+
PATTERN = /\A[A-Z]{2}\z/
|
|
11
11
|
|
|
12
12
|
def self.cast(value)
|
|
13
13
|
return nil unless value.is_a?(::String) && value != ""
|
|
14
14
|
|
|
15
15
|
unless PATTERN.match?(value)
|
|
16
|
-
raise Lutaml::Model::Type::InvalidValueError.new(value,
|
|
16
|
+
raise Lutaml::Model::Type::InvalidValueError.new(value,
|
|
17
|
+
"invalid ISO 3166-1 alpha-2 country code: #{value.inspect}")
|
|
17
18
|
end
|
|
18
19
|
|
|
19
20
|
value
|
|
@@ -7,13 +7,14 @@ module Dcc
|
|
|
7
7
|
# ISO 639-1 language code (two lowercase letters, e.g. `"en"`).
|
|
8
8
|
# Used by `dcc:usedLangCodeISO639_1` and `dcc:mandatoryLangCodeISO639_1`.
|
|
9
9
|
class IsoLanguageCode < Lutaml::Model::Type::String
|
|
10
|
-
PATTERN = /\A[a-z]{2}\z
|
|
10
|
+
PATTERN = /\A[a-z]{2}\z/
|
|
11
11
|
|
|
12
12
|
def self.cast(value)
|
|
13
13
|
return nil unless value.is_a?(::String) && value != ""
|
|
14
14
|
|
|
15
15
|
unless PATTERN.match?(value)
|
|
16
|
-
raise Lutaml::Model::Type::InvalidValueError.new(value,
|
|
16
|
+
raise Lutaml::Model::Type::InvalidValueError.new(value,
|
|
17
|
+
"invalid ISO 639-1 language code: #{value.inspect}")
|
|
17
18
|
end
|
|
18
19
|
|
|
19
20
|
value
|
|
@@ -12,13 +12,14 @@ module Dcc
|
|
|
12
12
|
(?<major>\d+)\.(?<minor>\d+)\.(?<patch>\d+)
|
|
13
13
|
(?:-(?<pre>[0-9A-Za-z.-]+))?
|
|
14
14
|
(?:\+(?<build>[0-9A-Za-z.-]+))?
|
|
15
|
-
\z/x
|
|
15
|
+
\z/x
|
|
16
16
|
|
|
17
17
|
def self.cast(value)
|
|
18
18
|
return nil unless value.is_a?(::String) && value != ""
|
|
19
19
|
|
|
20
20
|
unless PATTERN.match?(value)
|
|
21
|
-
raise Lutaml::Model::Type::InvalidValueError.new(value,
|
|
21
|
+
raise Lutaml::Model::Type::InvalidValueError.new(value,
|
|
22
|
+
"invalid schemaVersion")
|
|
22
23
|
end
|
|
23
24
|
|
|
24
25
|
value
|
data/lib/dcc/type/si_unit.rb
CHANGED
|
@@ -10,13 +10,14 @@ module Dcc
|
|
|
10
10
|
# check: the string must not contain whitespace (Schematron rule), and the
|
|
11
11
|
# non-SI declaration prefix (`|unit`) is allowed.
|
|
12
12
|
class SiUnit < Lutaml::Model::Type::String
|
|
13
|
-
NO_WHITESPACE_PATTERN = /\A\S+\z
|
|
13
|
+
NO_WHITESPACE_PATTERN = /\A\S+\z/
|
|
14
14
|
|
|
15
15
|
def self.cast(value)
|
|
16
16
|
return nil unless value.is_a?(::String) && value != ""
|
|
17
17
|
|
|
18
18
|
unless NO_WHITESPACE_PATTERN.match?(value)
|
|
19
|
-
raise Lutaml::Model::Type::InvalidValueError.new(value,
|
|
19
|
+
raise Lutaml::Model::Type::InvalidValueError.new(value,
|
|
20
|
+
"invalid SI unit expression (whitespace not allowed): #{value.inspect}")
|
|
20
21
|
end
|
|
21
22
|
|
|
22
23
|
value
|
data/lib/dcc/type_guards.rb
CHANGED
|
@@ -130,11 +130,11 @@ module Dcc
|
|
|
130
130
|
end
|
|
131
131
|
|
|
132
132
|
# Safe method call: invokes the method if the node's class declares it.
|
|
133
|
-
def call_if_present(node, method_name, *
|
|
133
|
+
def call_if_present(node, method_name, *)
|
|
134
134
|
return nil unless node.is_a?(::Lutaml::Model::Serializable)
|
|
135
135
|
return nil unless node.class.attributes.key?(method_name)
|
|
136
136
|
|
|
137
|
-
node.public_send(method_name, *
|
|
137
|
+
node.public_send(method_name, *)
|
|
138
138
|
end
|
|
139
139
|
end
|
|
140
|
-
end
|
|
140
|
+
end
|