lutaml-model 0.8.64 → 0.8.66
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/docs/_guides/native-engines.adoc +7 -0
- data/lib/lutaml/json/adapter/yeptris_adapter.rb +24 -1
- data/lib/lutaml/model/version.rb +1 -1
- data/lib/lutaml/xml/plan_compiler.rb +11 -0
- data/lib/lutaml/xml/plan_hydrator.rb +18 -5
- data/lib/lutaml/xml/plan_serializer.rb +32 -20
- data/lib/lutaml/yaml/adapter/yeptris_adapter.rb +34 -2
- data/spec/lutaml/json/yeptris_adapter_spec.rb +7 -2
- data/spec/lutaml/model/yeptris_error_parity_spec.rb +95 -0
- data/spec/lutaml/xml/cross_register_plan_spec.rb +75 -0
- data/spec/lutaml/yaml/yeptris_adapter_spec.rb +29 -0
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f1a4f0f42471c24f887cb31b50be356825d1bb538ca568b30d15a703928c7d0b
|
|
4
|
+
data.tar.gz: b2b5227d2f505a10921d41123caf0f04b2cafec68ab0318db49690d394d1309a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 191a88d3dc8eacc1e94c8ea2b033500dbb4ce77345feefb6e7328527c4648c67575d10db7f9cb9e4d9611fada0902210077ae5c6ec0fcc4ff457b8637f35e2ef
|
|
7
|
+
data.tar.gz: fe6e34251e26d5593ec07873e5f0320a0802e0cd3c86ec89b8492c9add49c11f919dedd4aef225f6b8b63ca6564c0d3841f88d16b614fe265c8ebf3fb27fdb00
|
|
@@ -106,6 +106,13 @@ Lutaml::Model::Config.toml_adapter_type = :teptris
|
|
|
106
106
|
raising `Psych::DisallowedClass`.
|
|
107
107
|
* **JSON**: `yeptris` parsing targets exact `JSON.parse` semantics;
|
|
108
108
|
generation stays on the `json` gem.
|
|
109
|
+
* **Error classes**: parse failures keep the standard adapters' contract
|
|
110
|
+
on every engine — the yeptris adapters re-raise the engine's syntax
|
|
111
|
+
errors as `Psych::SyntaxError` (YAML, with the engine's line, column,
|
|
112
|
+
and problem text) and `JSON::ParserError` (JSON), and the model layer
|
|
113
|
+
wraps them as `InvalidFormatError` just as it does for Psych and the
|
|
114
|
+
`json` gem. Code that rescues those classes does not change when a
|
|
115
|
+
native engine is added to the bundle.
|
|
109
116
|
* **TOML**: the teptris adapter mirrors tomlib value semantics — offset
|
|
110
117
|
and local datetimes become `Time`, dates become `Date`, local times
|
|
111
118
|
stay `String`. Parse failures raise `Teptris::ParseError` with line
|
|
@@ -12,9 +12,32 @@ module Lutaml
|
|
|
12
12
|
# load-only), so everything else inherits from the standard adapter.
|
|
13
13
|
class YeptrisAdapter < StandardAdapter
|
|
14
14
|
def self.parse(json, _options = {})
|
|
15
|
-
|
|
15
|
+
require_engine
|
|
16
16
|
::Yeptris::JSON.load(json)
|
|
17
|
+
rescue ::Yeptris::JSON::ParseError => e
|
|
18
|
+
raise parser_error(e)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# The engine require sits outside parse's rescue so a broken
|
|
22
|
+
# install still surfaces its LoadError (the detection fallback's
|
|
23
|
+
# contract) instead of a NameError from the rescue clause.
|
|
24
|
+
def self.require_engine
|
|
25
|
+
require "yeptris"
|
|
26
|
+
end
|
|
27
|
+
private_class_method :require_engine
|
|
28
|
+
|
|
29
|
+
# The parse-error contract is the standard adapter's: invalid
|
|
30
|
+
# input raises JSON::ParserError regardless of the engine, so
|
|
31
|
+
# consumers' rescue ladders (and this gem's own
|
|
32
|
+
# format_error_types conversion to InvalidFormatError) hold on
|
|
33
|
+
# every adapter. Without the json gem the engine error
|
|
34
|
+
# propagates unchanged.
|
|
35
|
+
def self.parser_error(error)
|
|
36
|
+
return error unless defined?(::JSON::ParserError)
|
|
37
|
+
|
|
38
|
+
::JSON::ParserError.new(error.message)
|
|
17
39
|
end
|
|
40
|
+
private_class_method :parser_error
|
|
18
41
|
end
|
|
19
42
|
end
|
|
20
43
|
end
|
data/lib/lutaml/model/version.rb
CHANGED
|
@@ -36,6 +36,17 @@ module Lutaml
|
|
|
36
36
|
|
|
37
37
|
class << self
|
|
38
38
|
def compile(model_class, register)
|
|
39
|
+
# A child model's declared lutaml_default_register takes
|
|
40
|
+
# precedence over the ambient (parent) register — the same
|
|
41
|
+
# contract the interpretive path applies through
|
|
42
|
+
# Register.resolve_for_child. Without this, plan compilation
|
|
43
|
+
# resolves the child's symbol attribute types in the parent
|
|
44
|
+
# context and raises UnknownTypeError for ids registered only
|
|
45
|
+
# in the child's own register (#876).
|
|
46
|
+
register = Lutaml::Model::Register.resolve_for_child(
|
|
47
|
+
model_class, register
|
|
48
|
+
)
|
|
49
|
+
|
|
39
50
|
key = [model_class, register]
|
|
40
51
|
return PLAN_CACHE[key] if PLAN_CACHE.key?(key)
|
|
41
52
|
|
|
@@ -20,11 +20,21 @@ module Lutaml
|
|
|
20
20
|
# node: the parsed source element (leptris); ordered/mixed
|
|
21
21
|
# plans rebuild element_order from it and ordered children
|
|
22
22
|
# hydrate natively against their own nodes
|
|
23
|
-
def call(model_class, plan, value, parent: nil, node: nil
|
|
23
|
+
def call(model_class, plan, value, parent: nil, node: nil,
|
|
24
|
+
register: nil)
|
|
25
|
+
# The child model's declared lutaml_default_register takes
|
|
26
|
+
# precedence over the ambient register — the same
|
|
27
|
+
# resolve_for_child contract the interpretive path applies.
|
|
28
|
+
# Without it, hydration resolves the child's symbol attribute
|
|
29
|
+
# types in the parent context and raises UnknownTypeError
|
|
30
|
+
# for ids registered only in the child's register (#876).
|
|
31
|
+
register = Lutaml::Model::Register.resolve_for_child(
|
|
32
|
+
model_class, register || Lutaml::Model::Config.default_register
|
|
33
|
+
)
|
|
24
34
|
buckets = node && plan[:needs_nodes] ? element_buckets(node) : nil
|
|
25
35
|
attr_kwargs = attributes_kwargs(plan, value, node)
|
|
26
36
|
child_kwargs, children, delegates =
|
|
27
|
-
children_kwargs(model_class, plan, value, buckets)
|
|
37
|
+
children_kwargs(model_class, plan, value, buckets, register)
|
|
28
38
|
plan[:collection_defaults].each do |name|
|
|
29
39
|
next if attr_kwargs.key?(name) || child_kwargs.key?(name)
|
|
30
40
|
|
|
@@ -80,7 +90,9 @@ module Lutaml
|
|
|
80
90
|
# — child instances come back so the caller can decorate
|
|
81
91
|
# parent/root links once the parent exists; delegate values
|
|
82
92
|
# wait for the instance (their target object must exist).
|
|
83
|
-
def children_kwargs(_model_class, plan, value, buckets = nil
|
|
93
|
+
def children_kwargs(_model_class, plan, value, buckets = nil,
|
|
94
|
+
register = nil)
|
|
95
|
+
register ||= Lutaml::Model::Config.default_register
|
|
84
96
|
grouped, tagged = group_children(value, plan[:row_tags])
|
|
85
97
|
buckets = nil unless buckets && plan[:needs_nodes]
|
|
86
98
|
kwargs = {}
|
|
@@ -126,7 +138,8 @@ module Lutaml
|
|
|
126
138
|
child_plan = PlanCompiler.compile(child_type, register)
|
|
127
139
|
items = buckets.fetch(rule.name.to_s, []).map do |n|
|
|
128
140
|
call(child_type, child_plan,
|
|
129
|
-
child_plan[:descriptor].walk(n), node: n
|
|
141
|
+
child_plan[:descriptor].walk(n), node: n,
|
|
142
|
+
register: register)
|
|
130
143
|
end
|
|
131
144
|
unless items.empty?
|
|
132
145
|
children.concat(items)
|
|
@@ -162,7 +175,7 @@ module Lutaml
|
|
|
162
175
|
items = grouped.fetch(rule.name.to_s, []).each_with_index
|
|
163
176
|
.map do |v, i|
|
|
164
177
|
call(child_type, child_plan, v,
|
|
165
|
-
node: cursor && cursor[i])
|
|
178
|
+
node: cursor && cursor[i], register: register)
|
|
166
179
|
end
|
|
167
180
|
next if items.empty?
|
|
168
181
|
|
|
@@ -25,16 +25,24 @@ module Lutaml
|
|
|
25
25
|
|
|
26
26
|
class << self
|
|
27
27
|
# plan: the compiler's entry for instance's class
|
|
28
|
-
def call(instance, plan)
|
|
28
|
+
def call(instance, plan, register = nil)
|
|
29
|
+
# The child model's declared lutaml_default_register takes
|
|
30
|
+
# precedence over the ambient register — the resolve_for_child
|
|
31
|
+
# contract (#876); without it, child plans and attribute
|
|
32
|
+
# serialization resolve in the parent context.
|
|
33
|
+
register = Lutaml::Model::Register.resolve_for_child(
|
|
34
|
+
instance.class,
|
|
35
|
+
register || Lutaml::Model::Config.default_register,
|
|
36
|
+
)
|
|
29
37
|
return nil if plan[:ordered] && instance.element_order.nil?
|
|
30
38
|
|
|
31
39
|
doc = ::Leptris::XML::Document.create
|
|
32
40
|
root = doc.create_element(plan[:tree][:name])
|
|
33
41
|
doc.root = root
|
|
34
42
|
if plan[:ordered]
|
|
35
|
-
build_ordered(root, instance, plan, doc)
|
|
43
|
+
build_ordered(root, instance, plan, doc, register)
|
|
36
44
|
else
|
|
37
|
-
build(root, instance, plan, doc)
|
|
45
|
+
build(root, instance, plan, doc, register)
|
|
38
46
|
end
|
|
39
47
|
doc.to_xml(indent: 2, no_decl: true)
|
|
40
48
|
end
|
|
@@ -54,8 +62,9 @@ module Lutaml
|
|
|
54
62
|
Lutaml::Model::Config.default_register
|
|
55
63
|
end
|
|
56
64
|
|
|
57
|
-
def build(element, instance, plan, doc)
|
|
58
|
-
|
|
65
|
+
def build(element, instance, plan, doc, reg = nil)
|
|
66
|
+
reg ||= register
|
|
67
|
+
write_attributes(element, instance, plan, reg)
|
|
59
68
|
plan[:rows].each do |rule, attr, kind, spelling, delegate|
|
|
60
69
|
value = value_of(instance, attr, delegate)
|
|
61
70
|
next unless rule.render?(value, instance)
|
|
@@ -67,24 +76,25 @@ module Lutaml
|
|
|
67
76
|
# as the interpretive writer does.
|
|
68
77
|
Array(value).each do |item|
|
|
69
78
|
add_leaf(element, row_name(rule, spelling),
|
|
70
|
-
attr.serialize(item, :xml,
|
|
79
|
+
attr.serialize(item, :xml, reg), doc,
|
|
71
80
|
attrs: rule.when_attribute)
|
|
72
81
|
end
|
|
73
82
|
when :collection_native, :collection_cb
|
|
74
83
|
Array(value).each do |item|
|
|
75
84
|
add_leaf(element, rule.name.to_s,
|
|
76
|
-
attr.serialize(item, :xml,
|
|
85
|
+
attr.serialize(item, :xml, reg), doc,
|
|
77
86
|
attrs: rule.when_attribute)
|
|
78
87
|
end
|
|
79
88
|
when :nested, :ordered_deferred
|
|
80
89
|
Array(value).each do |item|
|
|
81
|
-
child_plan = PlanCompiler.compile(item.class,
|
|
90
|
+
child_plan = PlanCompiler.compile(item.class, reg)
|
|
82
91
|
child = element.create_child(child_plan[:tree][:name])
|
|
83
92
|
if child_plan[:ordered]
|
|
84
|
-
return nil unless build_ordered(child, item, child_plan,
|
|
93
|
+
return nil unless build_ordered(child, item, child_plan,
|
|
94
|
+
doc, reg)
|
|
85
95
|
|
|
86
96
|
else
|
|
87
|
-
build(child, item, child_plan, doc)
|
|
97
|
+
build(child, item, child_plan, doc, reg)
|
|
88
98
|
end
|
|
89
99
|
end
|
|
90
100
|
when :raw
|
|
@@ -103,8 +113,9 @@ module Lutaml
|
|
|
103
113
|
# the model are reflected; element_order text is the fallback.
|
|
104
114
|
# PIs drop (interpretive parity). Returns nil when a nested
|
|
105
115
|
# ordered child lacks element_order (caller falls back).
|
|
106
|
-
def build_ordered(element, instance, plan, doc)
|
|
107
|
-
|
|
116
|
+
def build_ordered(element, instance, plan, doc, reg = nil)
|
|
117
|
+
reg ||= register
|
|
118
|
+
write_attributes(element, instance, plan, reg)
|
|
108
119
|
rows_by_name = {}
|
|
109
120
|
plan[:rows].each do |rule, attr, kind, spelling, delegate|
|
|
110
121
|
next unless rule.name
|
|
@@ -162,7 +173,7 @@ module Lutaml
|
|
|
162
173
|
|
|
163
174
|
element_indices[object.name] += 1
|
|
164
175
|
add_leaf(element, object.name,
|
|
165
|
-
attr.serialize(items[index], :xml,
|
|
176
|
+
attr.serialize(items[index], :xml, reg), doc)
|
|
166
177
|
when :nested, :ordered_deferred
|
|
167
178
|
item = if attr.collection?
|
|
168
179
|
index = element_indices[object.name]
|
|
@@ -173,18 +184,19 @@ module Lutaml
|
|
|
173
184
|
end
|
|
174
185
|
next unless item
|
|
175
186
|
|
|
176
|
-
child_plan = PlanCompiler.compile(item.class,
|
|
187
|
+
child_plan = PlanCompiler.compile(item.class, reg)
|
|
177
188
|
child = element.create_child(child_plan[:tree][:name])
|
|
178
189
|
if child_plan[:ordered]
|
|
179
|
-
return nil unless build_ordered(child, item, child_plan,
|
|
190
|
+
return nil unless build_ordered(child, item, child_plan,
|
|
191
|
+
doc, reg)
|
|
180
192
|
else
|
|
181
|
-
build(child, item, child_plan, doc)
|
|
193
|
+
build(child, item, child_plan, doc, reg)
|
|
182
194
|
end
|
|
183
195
|
when :raw
|
|
184
196
|
element.add_child(value.to_s) unless value.nil?
|
|
185
197
|
when :scalar
|
|
186
198
|
add_leaf(element, object.name,
|
|
187
|
-
attr.serialize(value, :xml,
|
|
199
|
+
attr.serialize(value, :xml, reg), doc)
|
|
188
200
|
end
|
|
189
201
|
end
|
|
190
202
|
end
|
|
@@ -193,7 +205,7 @@ module Lutaml
|
|
|
193
205
|
|
|
194
206
|
# Attributes in document order when the instance recorded it
|
|
195
207
|
# (parsed models), else plan row order.
|
|
196
|
-
def write_attributes(element, instance, plan)
|
|
208
|
+
def write_attributes(element, instance, plan, _reg = nil)
|
|
197
209
|
recorded = instance.attribute_order
|
|
198
210
|
if recorded && !recorded.empty?
|
|
199
211
|
by_name = {}
|
|
@@ -213,7 +225,7 @@ module Lutaml
|
|
|
213
225
|
end
|
|
214
226
|
end
|
|
215
227
|
|
|
216
|
-
def write_attribute(element, instance, name, entry)
|
|
228
|
+
def write_attribute(element, instance, name, entry, reg = nil)
|
|
217
229
|
return unless entry
|
|
218
230
|
|
|
219
231
|
_rule, attr, delegate = entry
|
|
@@ -221,7 +233,7 @@ module Lutaml
|
|
|
221
233
|
return if value.nil?
|
|
222
234
|
|
|
223
235
|
element.set_attribute(name,
|
|
224
|
-
attr.serialize(value, :xml, register).to_s)
|
|
236
|
+
attr.serialize(value, :xml, reg || register).to_s)
|
|
225
237
|
end
|
|
226
238
|
|
|
227
239
|
# Partition rows (#88) re-emit their discriminator: the wire
|
|
@@ -14,18 +14,50 @@ module Lutaml
|
|
|
14
14
|
# Semantic notes vs the standard adapter's Psych.safe_load:
|
|
15
15
|
# implicit typing is parity-verified (compat_11 schema); anchors
|
|
16
16
|
# and aliases always resolve; a tagged value without a core type
|
|
17
|
-
# materializes as plain data instead of raising DisallowedClass
|
|
17
|
+
# materializes as plain data instead of raising DisallowedClass;
|
|
18
|
+
# parse failures surface as Psych::SyntaxError like the standard
|
|
19
|
+
# adapter's (the engine's own ParseError is re-raised under the
|
|
20
|
+
# standard class).
|
|
18
21
|
class YeptrisAdapter < StandardAdapter
|
|
19
22
|
# yeptris is required lazily: the ruby-platform variant installs
|
|
20
23
|
# everywhere but only loads where libyeptris exists (no Windows
|
|
21
24
|
# prebuilts) — a broken install must degrade to detection
|
|
22
25
|
# fallback, never crash loads.
|
|
23
26
|
def self.parse(yaml, _options = {})
|
|
24
|
-
|
|
27
|
+
require_engine
|
|
25
28
|
require "yeptris/yaml"
|
|
26
29
|
# rubocop:disable-next Naming/VariableNumber -- upstream schema literal
|
|
27
30
|
Yeptris::YAML.load(yaml, schema: :compat_11)
|
|
31
|
+
rescue Yeptris::ParseError => e
|
|
32
|
+
raise syntax_error(e)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# The engine require sits outside parse's rescue so a broken
|
|
36
|
+
# install still surfaces its LoadError (the detection fallback's
|
|
37
|
+
# contract) instead of a NameError from the rescue clause.
|
|
38
|
+
def self.require_engine
|
|
39
|
+
require "yeptris"
|
|
40
|
+
end
|
|
41
|
+
private_class_method :require_engine
|
|
42
|
+
|
|
43
|
+
# The parse-error contract is the standard adapter's: invalid
|
|
44
|
+
# input raises Psych::SyntaxError regardless of the engine, so
|
|
45
|
+
# consumers' rescue ladders (and this gem's own
|
|
46
|
+
# format_error_types conversion to InvalidFormatError) hold on
|
|
47
|
+
# every adapter. The C parser reports the position inside the
|
|
48
|
+
# error message ("... at line L, column C"); lift it into the
|
|
49
|
+
# structured fields. Without psych (Opal) the engine error
|
|
50
|
+
# propagates unchanged.
|
|
51
|
+
def self.syntax_error(error)
|
|
52
|
+
return error unless defined?(::Psych::SyntaxError)
|
|
53
|
+
|
|
54
|
+
match = /\bline (\d+),? column (\d+)/.match(error.message)
|
|
55
|
+
line = match ? match[1].to_i : 0
|
|
56
|
+
column = match ? match[2].to_i : 0
|
|
57
|
+
problem = error.message.sub(/\s+at\s+line\s+\d+,?\s+column\s+\d+\z/, "")
|
|
58
|
+
::Psych::SyntaxError.new(nil, line, column, 0, problem, nil)
|
|
28
59
|
end
|
|
60
|
+
private_class_method :syntax_error
|
|
29
61
|
|
|
30
62
|
def to_yaml(_options = {})
|
|
31
63
|
attributes_to_serialize = if @attributes.is_a?(Lutaml::KeyValue::DataModel::Element)
|
|
@@ -25,9 +25,9 @@ RSpec.describe(YEPTRIS_AVAILABLE ? Lutaml::Json::Adapter::YeptrisAdapter : Objec
|
|
|
25
25
|
.to eq("a" => 1.5, "b" => nil, "c" => true, "d" => "1e3")
|
|
26
26
|
end
|
|
27
27
|
|
|
28
|
-
it "raises on invalid JSON" do
|
|
28
|
+
it "raises JSON::ParserError on invalid JSON, like the standard adapter" do
|
|
29
29
|
expect { described_class.parse("{nope") }
|
|
30
|
-
.to raise_error(
|
|
30
|
+
.to raise_error(JSON::ParserError)
|
|
31
31
|
end
|
|
32
32
|
end
|
|
33
33
|
|
|
@@ -59,5 +59,10 @@ RSpec.describe(YEPTRIS_AVAILABLE ? Lutaml::Json::Adapter::YeptrisAdapter : Objec
|
|
|
59
59
|
expect(model.roles).to eq(%w[admin dev])
|
|
60
60
|
expect(model_class.from_json(model.to_json).roles).to eq(%w[admin dev])
|
|
61
61
|
end
|
|
62
|
+
|
|
63
|
+
it "surfaces invalid JSON as InvalidFormatError" do
|
|
64
|
+
expect { model_class.from_json("{nope") }
|
|
65
|
+
.to raise_error(Lutaml::Model::InvalidFormatError)
|
|
66
|
+
end
|
|
62
67
|
end
|
|
63
68
|
end
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "open3"
|
|
4
|
+
require "spec_helper"
|
|
5
|
+
|
|
6
|
+
# The suite deliberately never loads the optional yeptris engine
|
|
7
|
+
# in-process (adapter_resolver_configured_spec asserts the resolver's
|
|
8
|
+
# lazy-detection guarantee), so the error-class contract is exercised
|
|
9
|
+
# in a subprocess: wherever the engine is installed it is the
|
|
10
|
+
# preferred adapter, and its parse failures must surface under the
|
|
11
|
+
# standard adapters' error classes — Psych::SyntaxError and
|
|
12
|
+
# JSON::ParserError from the adapters, Lutaml::Model::InvalidFormatError
|
|
13
|
+
# through a model — so downstream rescue ladders hold on every engine.
|
|
14
|
+
RSpec.describe "yeptris adapter error parity" do
|
|
15
|
+
# A direct ruby invocation with explicit load paths and NO shell:
|
|
16
|
+
# a nested `bundle exec` produced empty output on some CI legs, and
|
|
17
|
+
# backticks route through cmd.exe on Windows where POSIX quoting
|
|
18
|
+
# mangles the command. Open3 with array arguments is shell-free on
|
|
19
|
+
# every platform.
|
|
20
|
+
subject(:outcomes) do
|
|
21
|
+
libs = %w[lutaml-model yeptris yeptris-ruby moxml leptris].filter_map do |g|
|
|
22
|
+
"#{Gem::Specification.find_by_name(g).gem_dir}/lib"
|
|
23
|
+
rescue Gem::LoadError
|
|
24
|
+
nil
|
|
25
|
+
end
|
|
26
|
+
args = libs.flat_map { |l| ["-I", l] } + ["-e", probe]
|
|
27
|
+
Open3.capture2e(Gem.ruby, *args).first
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def yeptris_installed?
|
|
31
|
+
Gem::Specification.find_by_name("yeptris")
|
|
32
|
+
true
|
|
33
|
+
rescue Gem::LoadError
|
|
34
|
+
false
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# The probe requires the engine explicitly (the resolved adapters
|
|
38
|
+
# prove the preference), then reports one outcome line per surface.
|
|
39
|
+
let(:probe) do
|
|
40
|
+
<<~RUBY
|
|
41
|
+
require "lutaml/model"
|
|
42
|
+
|
|
43
|
+
def outcome
|
|
44
|
+
yield
|
|
45
|
+
"NO-ERROR"
|
|
46
|
+
rescue StandardError => e
|
|
47
|
+
"\#{e.class.name}: \#{e.message}"
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
puts "yaml-adapter: \#{Lutaml::Model::AdapterResolver.adapter_for(:yaml).name}"
|
|
51
|
+
puts "json-adapter: \#{Lutaml::Model::AdapterResolver.adapter_for(:json).name}"
|
|
52
|
+
|
|
53
|
+
puts "adapter-yaml: \#{outcome do
|
|
54
|
+
Lutaml::Yaml::Adapter::YeptrisAdapter.parse("name: test\\n invalid: [unclosed\\n")
|
|
55
|
+
end }"
|
|
56
|
+
puts "adapter-json: \#{outcome do
|
|
57
|
+
Lutaml::Json::Adapter::YeptrisAdapter.parse("{nope")
|
|
58
|
+
end }"
|
|
59
|
+
|
|
60
|
+
model_class = Class.new(Lutaml::Model::Serializable) do
|
|
61
|
+
attribute :name, :string
|
|
62
|
+
key_value { map "name", to: :name }
|
|
63
|
+
end
|
|
64
|
+
puts "model-yaml: \#{outcome do
|
|
65
|
+
model_class.from_yaml("name: test\\n invalid: [unclosed\\n")
|
|
66
|
+
end }"
|
|
67
|
+
puts "model-json: \#{outcome do
|
|
68
|
+
model_class.from_json("{nope")
|
|
69
|
+
end }"
|
|
70
|
+
RUBY
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
before { skip "yeptris is not installed" unless yeptris_installed? }
|
|
74
|
+
|
|
75
|
+
it "prefers the yeptris adapters when the engine is installed" do
|
|
76
|
+
expect(outcomes).to include("yaml-adapter: Lutaml::Yaml::Adapter::YeptrisAdapter")
|
|
77
|
+
expect(outcomes).to include("json-adapter: Lutaml::Json::Adapter::YeptrisAdapter")
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
it "raises Psych::SyntaxError from the yaml adapter for invalid YAML" do
|
|
81
|
+
expect(outcomes).to match(/^adapter-yaml: Psych::SyntaxError: .+ at line \d+/)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
it "raises JSON::ParserError from the json adapter for invalid JSON" do
|
|
85
|
+
expect(outcomes).to match(/^adapter-json: JSON::ParserError: /)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
it "raises InvalidFormatError through a model for invalid YAML" do
|
|
89
|
+
expect(outcomes).to match(/^model-yaml: Lutaml::Model::InvalidFormatError: /)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
it "raises InvalidFormatError through a model for invalid JSON" do
|
|
93
|
+
expect(outcomes).to match(/^model-json: Lutaml::Model::InvalidFormatError: /)
|
|
94
|
+
end
|
|
95
|
+
end
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
|
|
5
|
+
RSpec.describe "Cross-register embedding on the plan paths (#876)" do
|
|
6
|
+
before do
|
|
7
|
+
stub_const("CrossRegister876", Module.new)
|
|
8
|
+
stub_const("CrossRegister876::Part", part_class)
|
|
9
|
+
stub_const("CrossRegister876::Widget", widget_class)
|
|
10
|
+
stub_const("Host876", host_class)
|
|
11
|
+
|
|
12
|
+
ctx = Lutaml::Model::GlobalContext.create_context(
|
|
13
|
+
id: :crossregister876, registry: Lutaml::Model::TypeRegistry.new,
|
|
14
|
+
)
|
|
15
|
+
ctx.registry.register(:part, CrossRegister876::Part)
|
|
16
|
+
ctx.registry.register(:widget, CrossRegister876::Widget)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
let(:part_class) do
|
|
20
|
+
Class.new(Lutaml::Model::Serializable) do
|
|
21
|
+
def self.lutaml_default_register
|
|
22
|
+
:crossregister876
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
xml do
|
|
26
|
+
element "part"
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
let(:widget_class) do
|
|
32
|
+
klass = Class.new(Lutaml::Model::Serializable) do
|
|
33
|
+
def self.lutaml_default_register
|
|
34
|
+
:crossregister876
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
attribute :part_value, :part, collection: true
|
|
38
|
+
|
|
39
|
+
xml do
|
|
40
|
+
element "widget"
|
|
41
|
+
ordered
|
|
42
|
+
map_element "part", to: :part_value
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
klass
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
let(:host_class) do
|
|
49
|
+
Class.new(Lutaml::Model::Serializable) do
|
|
50
|
+
attribute :widget, CrossRegister876::Widget
|
|
51
|
+
|
|
52
|
+
xml do
|
|
53
|
+
element "host"
|
|
54
|
+
map_element "widget", to: :widget
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
let(:xml) { "<host><widget><part/></widget></host>" }
|
|
60
|
+
|
|
61
|
+
it "resolves the child's symbol types through the child register" do
|
|
62
|
+
model = Host876.from_xml(xml)
|
|
63
|
+
expect(model.widget.part_value.first).to be_a(CrossRegister876::Part)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
it "round-trips through both the plan serializer and back" do
|
|
67
|
+
model = Host876.from_xml(xml)
|
|
68
|
+
xml_out = model.to_xml
|
|
69
|
+
expect(xml_out).to include("<widget>")
|
|
70
|
+
expect(xml_out).to include("<part/>")
|
|
71
|
+
|
|
72
|
+
reparsed = Host876.from_xml(xml_out)
|
|
73
|
+
expect(reparsed.widget.part_value.first).to be_a(CrossRegister876::Part)
|
|
74
|
+
end
|
|
75
|
+
end
|
|
@@ -30,6 +30,30 @@ RSpec.describe(YEPTRIS_AVAILABLE ? Lutaml::Yaml::Adapter::YeptrisAdapter : Objec
|
|
|
30
30
|
expect(described_class.parse("d: 2020-01-01\n"))
|
|
31
31
|
.to eq("d" => Date.new(2020, 1, 1))
|
|
32
32
|
end
|
|
33
|
+
|
|
34
|
+
it "raises Psych::SyntaxError for invalid YAML, like the standard adapter" do
|
|
35
|
+
expect { described_class.parse("name: test\n invalid: [unclosed\n") }
|
|
36
|
+
.to raise_error(Psych::SyntaxError)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
it "raises Psych::SyntaxError for a mapping value inside a plain scalar" do
|
|
40
|
+
# The dependent-CI shape: one line, a second "key:" colon after a
|
|
41
|
+
# plain scalar run. Psych rejects it with "mapping values are not
|
|
42
|
+
# allowed in this context"; the engine must surface under the
|
|
43
|
+
# same class so downstream rescue ladders hold.
|
|
44
|
+
expect { described_class.parse('name: test\n invalid: [unclosed') }
|
|
45
|
+
.to raise_error(Psych::SyntaxError)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
it "carries the engine's position on the syntax error" do
|
|
49
|
+
expect { described_class.parse("name: test\n invalid: [unclosed\n") }
|
|
50
|
+
.to raise_error(Psych::SyntaxError) do |error|
|
|
51
|
+
expect(error.line).to be >= 1
|
|
52
|
+
expect(error.problem).to be_a(String)
|
|
53
|
+
expect(error.problem).not_to be_empty
|
|
54
|
+
expect(error.message).to include("at line #{error.line}")
|
|
55
|
+
end
|
|
56
|
+
end
|
|
33
57
|
end
|
|
34
58
|
|
|
35
59
|
describe "#to_yaml" do
|
|
@@ -59,5 +83,10 @@ RSpec.describe(YEPTRIS_AVAILABLE ? Lutaml::Yaml::Adapter::YeptrisAdapter : Objec
|
|
|
59
83
|
expect(model.roles).to eq(%w[admin dev])
|
|
60
84
|
expect(model_class.from_yaml(model.to_yaml).roles).to eq(%w[admin dev])
|
|
61
85
|
end
|
|
86
|
+
|
|
87
|
+
it "surfaces invalid YAML as InvalidFormatError" do
|
|
88
|
+
expect { model_class.from_yaml("name: test\n invalid: [unclosed\n") }
|
|
89
|
+
.to raise_error(Lutaml::Model::InvalidFormatError)
|
|
90
|
+
end
|
|
62
91
|
end
|
|
63
92
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: lutaml-model
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.8.
|
|
4
|
+
version: 0.8.66
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ribose Inc.
|
|
@@ -1880,6 +1880,7 @@ files:
|
|
|
1880
1880
|
- spec/lutaml/model/yamls_range_spec.rb
|
|
1881
1881
|
- spec/lutaml/model/yamls_sequence_spec.rb
|
|
1882
1882
|
- spec/lutaml/model/yamls_spec.rb
|
|
1883
|
+
- spec/lutaml/model/yeptris_error_parity_spec.rb
|
|
1883
1884
|
- spec/lutaml/model_spec.rb
|
|
1884
1885
|
- spec/lutaml/rdf/context_spec.rb
|
|
1885
1886
|
- spec/lutaml/rdf/graph_serialization_spec.rb
|
|
@@ -1916,6 +1917,7 @@ files:
|
|
|
1916
1917
|
- spec/lutaml/xml/conformance/xml_schema_instance_spec.rb
|
|
1917
1918
|
- spec/lutaml/xml/content_model_validation_spec.rb
|
|
1918
1919
|
- spec/lutaml/xml/cross_class_dispatch_spec.rb
|
|
1920
|
+
- spec/lutaml/xml/cross_register_plan_spec.rb
|
|
1919
1921
|
- spec/lutaml/xml/data_model_spec.rb
|
|
1920
1922
|
- spec/lutaml/xml/decisions/decision_engine_spec.rb
|
|
1921
1923
|
- spec/lutaml/xml/decisions/decision_spec.rb
|