lutaml-model 0.8.95 → 0.8.96
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/grammar-backed-formats.adoc +69 -0
- data/docs/_guides/index.adoc +2 -0
- data/lib/lutaml/model/serialize/attribute_definition.rb +133 -7
- data/lib/lutaml/model/serialize/format_conversion.rb +6 -1
- data/lib/lutaml/model/serialize/model_import.rb +5 -0
- data/lib/lutaml/model/version.rb +1 -1
- data/lib/lutaml/model.rb +7 -0
- data/lib/lutaml/xml/format.rb +8 -10
- data/lib/lutaml/xml/leptris/plan_compiler.rb +36 -18
- data/lib/lutaml/xml/leptris/plan_hydrator.rb +34 -1
- data/lib/lutaml/xml/leptris/plan_serializer.rb +114 -18
- data/lib/lutaml/xml/leptris.rb +26 -0
- data/spec/lutaml/model/opal_smoke_spec.rb +17 -0
- data/spec/lutaml/model/serialize/attribute_definition_block_form_spec.rb +244 -0
- data/spec/lutaml/xml/opal_xml_spec.rb +47 -0
- data/spec/lutaml/xml/plan_namespaced_serialize_spec.rb +157 -0
- data/spec/lutaml/xml/plan_type_ns_attribute_spec.rb +2 -1
- data/spec/lutaml/xml/plan_wildcard_any_spec.rb +105 -0
- metadata +5 -1
|
@@ -38,16 +38,84 @@ module Lutaml
|
|
|
38
38
|
return nil if plan[:ordered] && instance.element_order.nil?
|
|
39
39
|
|
|
40
40
|
doc = ::Leptris::XML::Document.create
|
|
41
|
-
|
|
41
|
+
ns_ctx = Namespaces.new(plan)
|
|
42
|
+
return nil if ns_ctx.unspellable?
|
|
43
|
+
|
|
44
|
+
root = doc.create_element(
|
|
45
|
+
ns_ctx.spell(plan[:tree][:name], plan[:tree][:ns]),
|
|
46
|
+
)
|
|
42
47
|
doc.root = root
|
|
48
|
+
ns_ctx.declare_on(root)
|
|
43
49
|
if plan[:ordered]
|
|
44
50
|
build_ordered(root, instance, plan, doc, register)
|
|
45
51
|
else
|
|
46
|
-
build(root, instance, plan, doc, register)
|
|
52
|
+
build(root, instance, plan, doc, register, ns_ctx)
|
|
47
53
|
end
|
|
48
54
|
doc.to_xml(indent: 2, no_decl: true)
|
|
49
55
|
end
|
|
50
56
|
|
|
57
|
+
# Namespace spelling for namespaced plans (#847): every ns
|
|
58
|
+
# form resolves to a wire spelling (prefix:local, or local
|
|
59
|
+
# under a default xmlns), and every distinct (prefix, uri)
|
|
60
|
+
# pair is declared exactly once on the output root in
|
|
61
|
+
# first-encounter order — the #1558 contract on the DOM
|
|
62
|
+
# path. A nested plan's own tree ns overrides the inherited
|
|
63
|
+
# one; rows without an ns form inherit.
|
|
64
|
+
class Namespaces
|
|
65
|
+
attr_reader :unspellable
|
|
66
|
+
|
|
67
|
+
def initialize(plan)
|
|
68
|
+
@forms = {}
|
|
69
|
+
@declared = {}
|
|
70
|
+
@unspellable = false
|
|
71
|
+
collect(plan[:tree])
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def unspellable?
|
|
75
|
+
@unspellable
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def collect(tree)
|
|
79
|
+
register(tree[:ns])
|
|
80
|
+
(tree[:attributes] || []).each { |attr| register(attr[:ns]) }
|
|
81
|
+
(tree[:children] || []).each do |row|
|
|
82
|
+
register(row[:ns]) if row[:ns].is_a?(Hash)
|
|
83
|
+
collect(row[:plan]) if row[:kind] == :nested && row[:plan]
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def register(ns_form)
|
|
88
|
+
return if ns_form.nil? || ns_form == :none || ns_form == :any
|
|
89
|
+
|
|
90
|
+
form = { prefix: ns_form[:prefix], uri: ns_form[:exact] }
|
|
91
|
+
@forms[ns_form[:exact]] ||= form
|
|
92
|
+
@unspellable = true if ns_form[:exact] && !ns_form.key?(:prefix)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# The wire spelling for a local name under this form.
|
|
96
|
+
def spell(local, ns_form = nil)
|
|
97
|
+
form = ns_form.is_a?(Hash) ? ns_form : @forms[ns_form]
|
|
98
|
+
prefix = form && form[:prefix]
|
|
99
|
+
prefix ? "#{prefix}:#{local}" : local
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def form_for(ns_form)
|
|
103
|
+
return nil if ns_form.nil? || ns_form == :none
|
|
104
|
+
|
|
105
|
+
ns_form.is_a?(Hash) ? ns_form : @forms[ns_form]
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def declare_on(root)
|
|
109
|
+
@forms.each_value do |form|
|
|
110
|
+
key = form[:uri]
|
|
111
|
+
next if key.nil? || @declared[key]
|
|
112
|
+
|
|
113
|
+
@declared[key] = true
|
|
114
|
+
root.add_namespace_definition(form[:prefix], form[:uri])
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
51
119
|
# Whether the compiled plan is serialize-shaped. Ordered/mixed
|
|
52
120
|
# models serialize through build_ordered from element_order;
|
|
53
121
|
# plain models through plan rows.
|
|
@@ -63,9 +131,10 @@ module Lutaml
|
|
|
63
131
|
Lutaml::Model::Config.default_register
|
|
64
132
|
end
|
|
65
133
|
|
|
66
|
-
def build(element, instance, plan, doc, reg = nil
|
|
134
|
+
def build(element, instance, plan, doc, reg = nil, ns_ctx = nil,
|
|
135
|
+
inherited_ns = nil)
|
|
67
136
|
reg ||= register
|
|
68
|
-
write_attributes(element, instance, plan, reg)
|
|
137
|
+
write_attributes(element, instance, plan, reg, ns_ctx)
|
|
69
138
|
plan[:rows].each do |rule, attr, kind, spelling, delegate|
|
|
70
139
|
value = value_of(instance, attr, delegate)
|
|
71
140
|
next unless rule.render?(value, instance)
|
|
@@ -76,33 +145,46 @@ module Lutaml
|
|
|
76
145
|
# several occurrences serializes one element per item,
|
|
77
146
|
# as the interpretive writer does. A plain scalar skips
|
|
78
147
|
# the Array() wrap — the common case.
|
|
148
|
+
row_ns = plan[:tree][:children]&.find do |r|
|
|
149
|
+
r[:name] == rule.name.to_s
|
|
150
|
+
end&.[](:ns) || inherited_ns
|
|
79
151
|
if value.is_a?(::Array)
|
|
80
152
|
value.each do |item|
|
|
81
|
-
add_leaf(element,
|
|
153
|
+
add_leaf(element,
|
|
154
|
+
ns_ctx ? ns_ctx.spell(row_name(rule, spelling), row_ns) : row_name(rule, spelling),
|
|
82
155
|
attr.serialize(item, :xml, reg), doc,
|
|
83
156
|
attrs: rule.when_attribute)
|
|
84
157
|
end
|
|
85
158
|
else
|
|
86
|
-
add_leaf(element,
|
|
159
|
+
add_leaf(element,
|
|
160
|
+
ns_ctx ? ns_ctx.spell(row_name(rule, spelling), row_ns) : row_name(rule, spelling),
|
|
87
161
|
attr.serialize(value, :xml, reg), doc,
|
|
88
162
|
attrs: rule.when_attribute)
|
|
89
163
|
end
|
|
90
164
|
when :collection_native, :collection_cb
|
|
165
|
+
row_ns = plan[:tree][:children]&.find do |r|
|
|
166
|
+
r[:name] == rule.name.to_s
|
|
167
|
+
end&.[](:ns) || inherited_ns
|
|
91
168
|
Array(value).each do |item|
|
|
92
|
-
add_leaf(element,
|
|
169
|
+
add_leaf(element,
|
|
170
|
+
ns_ctx ? ns_ctx.spell(rule.name.to_s, row_ns) : rule.name.to_s,
|
|
93
171
|
attr.serialize(item, :xml, reg), doc,
|
|
94
172
|
attrs: rule.when_attribute)
|
|
95
173
|
end
|
|
96
174
|
when :nested, :ordered_deferred
|
|
97
175
|
Array(value).each do |item|
|
|
98
176
|
child_plan = PlanCompiler.compile(item.class, reg)
|
|
99
|
-
|
|
177
|
+
child_ns = child_plan[:tree][:ns] || inherited_ns
|
|
178
|
+
child = element.create_child(
|
|
179
|
+
ns_ctx ? ns_ctx.spell(child_plan[:tree][:name], child_ns) : child_plan[:tree][:name],
|
|
180
|
+
)
|
|
181
|
+
ns_ctx&.collect(child_plan[:tree])
|
|
100
182
|
if child_plan[:ordered]
|
|
101
|
-
|
|
102
|
-
|
|
183
|
+
next unless build_ordered(child, item, child_plan,
|
|
184
|
+
doc, reg, ns_ctx, child_ns)
|
|
103
185
|
|
|
104
186
|
else
|
|
105
|
-
build(child, item, child_plan, doc, reg)
|
|
187
|
+
build(child, item, child_plan, doc, reg, ns_ctx, child_ns)
|
|
106
188
|
end
|
|
107
189
|
end
|
|
108
190
|
when :raw
|
|
@@ -121,9 +203,11 @@ module Lutaml
|
|
|
121
203
|
# the model are reflected; element_order text is the fallback.
|
|
122
204
|
# PIs drop (interpretive parity). Returns nil when a nested
|
|
123
205
|
# ordered child lacks element_order (caller falls back).
|
|
124
|
-
def build_ordered(element, instance, plan, doc, reg = nil
|
|
206
|
+
def build_ordered(element, instance, plan, doc, reg = nil,
|
|
207
|
+
ns_ctx = nil, _inherited_ns = nil)
|
|
125
208
|
reg ||= register
|
|
126
|
-
|
|
209
|
+
ns_ctx&.collect(plan[:tree])
|
|
210
|
+
write_attributes(element, instance, plan, reg, ns_ctx)
|
|
127
211
|
rows_by_name = {}
|
|
128
212
|
plan[:rows].each do |rule, attr, kind, spelling, delegate|
|
|
129
213
|
next unless rule.name
|
|
@@ -213,8 +297,16 @@ module Lutaml
|
|
|
213
297
|
|
|
214
298
|
# Attributes in document order when the instance recorded it
|
|
215
299
|
# (parsed models), else plan row order.
|
|
216
|
-
def write_attributes(element, instance, plan,
|
|
300
|
+
def write_attributes(element, instance, plan, reg = nil,
|
|
301
|
+
ns_ctx = nil)
|
|
217
302
|
recorded = instance.attribute_order
|
|
303
|
+
attr_prefixes = {}
|
|
304
|
+
if ns_ctx
|
|
305
|
+
(plan[:tree][:attributes] || []).each do |a|
|
|
306
|
+
form = a[:ns].is_a?(Hash) ? a[:ns] : nil
|
|
307
|
+
attr_prefixes[a[:name]] = form && form[:prefix]
|
|
308
|
+
end
|
|
309
|
+
end
|
|
218
310
|
if recorded && !recorded.empty?
|
|
219
311
|
by_name = {}
|
|
220
312
|
plan[:attr_rows].each do |rule, attr, _sp, delegate|
|
|
@@ -223,24 +315,28 @@ module Lutaml
|
|
|
223
315
|
names = recorded |
|
|
224
316
|
plan[:attr_rows].map { |rule, _a, _s, _d| rule.name.to_s }
|
|
225
317
|
names.each do |name|
|
|
226
|
-
write_attribute(element, instance, name, by_name[name]
|
|
318
|
+
write_attribute(element, instance, name, by_name[name],
|
|
319
|
+
reg, attr_prefixes[name])
|
|
227
320
|
end
|
|
228
321
|
else
|
|
229
322
|
plan[:attr_rows].each do |rule, attr, _sp, delegate|
|
|
230
323
|
write_attribute(element, instance, rule.name.to_s,
|
|
231
|
-
[rule, attr, delegate]
|
|
324
|
+
[rule, attr, delegate], reg,
|
|
325
|
+
attr_prefixes[rule.name.to_s])
|
|
232
326
|
end
|
|
233
327
|
end
|
|
234
328
|
end
|
|
235
329
|
|
|
236
|
-
def write_attribute(element, instance, name, entry, reg = nil
|
|
330
|
+
def write_attribute(element, instance, name, entry, reg = nil,
|
|
331
|
+
prefix = nil)
|
|
237
332
|
return unless entry
|
|
238
333
|
|
|
239
334
|
_rule, attr, delegate = entry
|
|
240
335
|
value = value_of(instance, attr, delegate)
|
|
241
336
|
return if value.nil?
|
|
242
337
|
|
|
243
|
-
|
|
338
|
+
spelled = prefix ? "#{prefix}:#{name}" : name
|
|
339
|
+
element.set_attribute(spelled,
|
|
244
340
|
attr.serialize(value, :xml, reg || register).to_s)
|
|
245
341
|
end
|
|
246
342
|
|
data/lib/lutaml/xml/leptris.rb
CHANGED
|
@@ -21,6 +21,18 @@ module Lutaml
|
|
|
21
21
|
# type-namespaced attributes on the interpretive path.
|
|
22
22
|
MIN_LEPTRIS_ATTR_NS_VERSION = "1.9.289.0"
|
|
23
23
|
|
|
24
|
+
# Nested child plans capture their declared attribute rows
|
|
25
|
+
# correctly again as of 1.9.313.0 (leptris#1563: the walk dropped
|
|
26
|
+
# children's attribute capture when the parent plan carried an
|
|
27
|
+
# exact-URI attribute row). Older gems keep those models
|
|
28
|
+
# interpretive.
|
|
29
|
+
MIN_LEPTRIS_NESTED_ATTR_VERSION = "1.9.313.0"
|
|
30
|
+
|
|
31
|
+
# Wildcard child rows (leptris#1552: named-rows-take-precedence
|
|
32
|
+
# two-pass walk, ns-form aware, type_tag echo) — the plan path's
|
|
33
|
+
# map_any_element catch-all. Ships with the 1.9.313 binding.
|
|
34
|
+
MIN_LEPTRIS_WILDCARD_VERSION = "1.9.313.0"
|
|
35
|
+
|
|
24
36
|
def self.plan_path_compatible?
|
|
25
37
|
return false unless defined?(Gem)
|
|
26
38
|
|
|
@@ -35,6 +47,20 @@ module Lutaml
|
|
|
35
47
|
spec && spec.version >= Gem::Version.new(MIN_LEPTRIS_ATTR_NS_VERSION)
|
|
36
48
|
end
|
|
37
49
|
|
|
50
|
+
def self.nested_attr_capture_compatible?
|
|
51
|
+
return false unless defined?(Gem)
|
|
52
|
+
|
|
53
|
+
spec = Gem.loaded_specs["leptris"]
|
|
54
|
+
spec && spec.version >= Gem::Version.new(MIN_LEPTRIS_NESTED_ATTR_VERSION)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def self.wildcard_rows_compatible?
|
|
58
|
+
return false unless defined?(Gem)
|
|
59
|
+
|
|
60
|
+
spec = Gem.loaded_specs["leptris"]
|
|
61
|
+
spec && spec.version >= Gem::Version.new(MIN_LEPTRIS_WILDCARD_VERSION)
|
|
62
|
+
end
|
|
63
|
+
|
|
38
64
|
autoload :PlanCompiler, "lutaml/xml/leptris/plan_compiler"
|
|
39
65
|
autoload :PlanHydrator, "lutaml/xml/leptris/plan_hydrator"
|
|
40
66
|
autoload :PlanOrder, "lutaml/xml/leptris/plan_order"
|
|
@@ -147,6 +147,23 @@ RSpec.describe "Opal compatibility", if: RUBY_ENGINE == "opal" do
|
|
|
147
147
|
expect(instance.port).to eq(8080)
|
|
148
148
|
end
|
|
149
149
|
|
|
150
|
+
# Opal has no TracePoint; a symbol import is resolved on first use.
|
|
151
|
+
it "resolves a deferred symbol import on first use" do
|
|
152
|
+
register = Lutaml::Model::GlobalRegister
|
|
153
|
+
.lookup(Lutaml::Model::Config.default_register)
|
|
154
|
+
importer = Class.new(Lutaml::Model::Serializable) do
|
|
155
|
+
import_model :opal_deferred_import
|
|
156
|
+
end
|
|
157
|
+
imported = Class.new(Lutaml::Model::Serializable) do
|
|
158
|
+
attribute :title, :string
|
|
159
|
+
end
|
|
160
|
+
register.register_model(imported, id: :opal_deferred_import)
|
|
161
|
+
|
|
162
|
+
instance = importer.from_json('{"title":"Deferred"}')
|
|
163
|
+
expect(instance.title).to eq("Deferred")
|
|
164
|
+
expect(instance.to_hash).to eq({ "title" => "Deferred" })
|
|
165
|
+
end
|
|
166
|
+
|
|
150
167
|
it "RuntimeCompatibility detects Opal" do
|
|
151
168
|
expect(Lutaml::Model::RuntimeCompatibility.opal?).to be true
|
|
152
169
|
end
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
require_relative "../../../../lib/lutaml/model"
|
|
5
|
+
require "lutaml/xml/adapter/nokogiri_adapter"
|
|
6
|
+
|
|
7
|
+
# Runtimes without a source compiler (Opal) define the attribute
|
|
8
|
+
# accessors and the state-defaults seed from blocks instead of a String
|
|
9
|
+
# class_eval. These specs force that block form on MRI and check it
|
|
10
|
+
# behaves exactly like the compiled form.
|
|
11
|
+
module AttributeDefinitionBlockFormSpec
|
|
12
|
+
module_function
|
|
13
|
+
|
|
14
|
+
def define_model(klass)
|
|
15
|
+
klass.class_eval do
|
|
16
|
+
attribute :name, :string
|
|
17
|
+
attribute :count, :integer
|
|
18
|
+
attribute :tags, :string, collection: true
|
|
19
|
+
# "char" is an enum shorthand, so the second :char definition
|
|
20
|
+
# redefines the setter but keeps the first Attribute's accessor.
|
|
21
|
+
attribute :align, :string, values: %w[char left]
|
|
22
|
+
attribute :char, :integer
|
|
23
|
+
attribute :char, :string
|
|
24
|
+
# String names, mutated once defined: the accessors must keep
|
|
25
|
+
# reading the ivar named at definition time.
|
|
26
|
+
label = +"label"
|
|
27
|
+
notes = +"notes"
|
|
28
|
+
attribute label, :integer
|
|
29
|
+
attribute notes, :string, collection: true
|
|
30
|
+
label << "x"
|
|
31
|
+
notes << "x"
|
|
32
|
+
|
|
33
|
+
# A model may define its own #send; the setters must not dispatch
|
|
34
|
+
# through it.
|
|
35
|
+
define_method(:send) { |*| "domain-method" }
|
|
36
|
+
|
|
37
|
+
xml do
|
|
38
|
+
element "item"
|
|
39
|
+
ordered
|
|
40
|
+
map_attribute "name", to: :name
|
|
41
|
+
map_element "count", to: :count
|
|
42
|
+
map_element "tag", to: :tags
|
|
43
|
+
map_element "char", to: :char
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
key_value do
|
|
47
|
+
map "name", to: :name
|
|
48
|
+
map "count", to: :count
|
|
49
|
+
map "tags", to: :tags
|
|
50
|
+
map "char", to: :char
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
klass
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# A model overriding the core methods the block form might dispatch
|
|
57
|
+
# through: the accessors must behave as the compiled ones do.
|
|
58
|
+
def override_model(klass)
|
|
59
|
+
klass.class_eval do
|
|
60
|
+
attribute :count, :integer
|
|
61
|
+
attribute :tags, :string, collection: true
|
|
62
|
+
%i[__send__ instance_variable_get instance_variable_set].each do |m|
|
|
63
|
+
define_method(m) { |*| :overridden }
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
m = klass.new
|
|
67
|
+
m.count = "5"
|
|
68
|
+
m.tags = %w[a]
|
|
69
|
+
m.tags("b")
|
|
70
|
+
# The collection getter itself reads through the overridden method,
|
|
71
|
+
# so check what was stored.
|
|
72
|
+
stored = ::Kernel.instance_method(:instance_variable_get)
|
|
73
|
+
[stored.bind_call(m, :@count), stored.bind_call(m, :@tags)]
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# A model whose scope defines its own Array: appends must resolve the
|
|
77
|
+
# constant where the compiled getter does.
|
|
78
|
+
# Checked on the defining class and on a subclass that shadows Array,
|
|
79
|
+
# with the defining class overriding const_get.
|
|
80
|
+
def array_scope_model(klass)
|
|
81
|
+
klass.const_set(:Array, Class.new(::Array))
|
|
82
|
+
klass.class_eval { attribute :tags, :string, collection: true }
|
|
83
|
+
klass.define_singleton_method(:const_get) { |*| raise "const_get called" }
|
|
84
|
+
sub = Class.new(klass)
|
|
85
|
+
sub.const_set(:Array, ::Array)
|
|
86
|
+
[klass, sub].map do |k|
|
|
87
|
+
m = k.new
|
|
88
|
+
m.tags = %w[a]
|
|
89
|
+
m.tags("b")
|
|
90
|
+
::Kernel.instance_method(:instance_variable_get).bind_call(m, :@tags)
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Raw-state readers after the state defaults compiled for a smaller
|
|
95
|
+
# attribute set: an attribute imported later, and one declared later,
|
|
96
|
+
# must both get a reader (#933).
|
|
97
|
+
def late_raw_readers(klass)
|
|
98
|
+
klass.class_eval { attribute :name, :string }
|
|
99
|
+
klass.new
|
|
100
|
+
imported = Class.new(Lutaml::Model::Serializable) do
|
|
101
|
+
attribute :br, :string
|
|
102
|
+
end
|
|
103
|
+
klass.import_model_attributes(imported)
|
|
104
|
+
klass.class_eval { attribute :late, :string, collection: true }
|
|
105
|
+
m = klass.new
|
|
106
|
+
m.br = "b"
|
|
107
|
+
readers = %i[name br late].map do |a|
|
|
108
|
+
raw = m.public_send(:"__lutaml_raw_#{a}")
|
|
109
|
+
[a, Lutaml::Model::Utils.uninitialized?(raw), raw.equal?(Lutaml::Model::Serialize::LAZY_EMPTY_COLLECTION) || raw]
|
|
110
|
+
end
|
|
111
|
+
readers << register_choice_raw_reader
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# A choice import into a non-default register merges attribute records
|
|
115
|
+
# without define_attribute_methods: only the state-defaults
|
|
116
|
+
# compilation gives the imported name its raw reader.
|
|
117
|
+
def register_choice_raw_reader
|
|
118
|
+
register = Lutaml::Model::Register.new(:raw_reader_block_form_spec)
|
|
119
|
+
Lutaml::Model::GlobalRegister.register(register)
|
|
120
|
+
imported = Class.new(Lutaml::Model::Serializable) do
|
|
121
|
+
attribute :br, :string, collection: true
|
|
122
|
+
end
|
|
123
|
+
klass = Class.new(Lutaml::Model::Serializable) do
|
|
124
|
+
attribute :name, :string
|
|
125
|
+
choice { import_model_attributes(imported, :raw_reader_block_form_spec) }
|
|
126
|
+
end
|
|
127
|
+
klass.allocate_for_deserialization(:raw_reader_block_form_spec).__lutaml_raw_br
|
|
128
|
+
.equal?(Lutaml::Model::Serialize::LAZY_EMPTY_COLLECTION)
|
|
129
|
+
ensure
|
|
130
|
+
Lutaml::Model::GlobalRegister.unregister(:raw_reader_block_form_spec)
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def build_model
|
|
134
|
+
define_model(Class.new(Lutaml::Model::Serializable))
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def element_order(model)
|
|
138
|
+
model.element_order.map { |e| [e.type, e.name, e.text_content] }
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def arity_error(model, name)
|
|
142
|
+
model.public_send(name, "1", "2")
|
|
143
|
+
nil
|
|
144
|
+
rescue ArgumentError => e
|
|
145
|
+
e.message
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def observe(klass)
|
|
149
|
+
fresh = klass.new
|
|
150
|
+
set = klass.new
|
|
151
|
+
set.name = "a"
|
|
152
|
+
set.count = "5"
|
|
153
|
+
set.tags("x")
|
|
154
|
+
set.tags("y")
|
|
155
|
+
set.char = "7"
|
|
156
|
+
# A parsed instance with no tags still holds the lazy sentinel.
|
|
157
|
+
emptied = klass.from_xml('<item name="z"/>')
|
|
158
|
+
sentinel_before_nil = emptied.instance_variable_get(:@tags)
|
|
159
|
+
.equal?(Lutaml::Model::Serialize::LAZY_EMPTY_COLLECTION)
|
|
160
|
+
emptied.tags = nil
|
|
161
|
+
xml = set.to_xml
|
|
162
|
+
parsed = klass.from_xml(xml)
|
|
163
|
+
|
|
164
|
+
seed = klass.__send__(:compiled_state_defaults_name!, :default)
|
|
165
|
+
seeded = klass.new.__send__(seed)
|
|
166
|
+
|
|
167
|
+
{
|
|
168
|
+
seed_return: [seeded.class, seeded.equal?(Lutaml::Model::Serialize::LAZY_EMPTY_COLLECTION)],
|
|
169
|
+
fresh_scalar_uninitialized: Lutaml::Model::Utils.uninitialized?(
|
|
170
|
+
fresh.instance_variable_get(:@name),
|
|
171
|
+
),
|
|
172
|
+
fresh_collection_sentinel: fresh.instance_variable_get(:@tags)
|
|
173
|
+
.equal?(Lutaml::Model::Serialize::LAZY_EMPTY_COLLECTION),
|
|
174
|
+
fresh_readers: [fresh.name, fresh.count, fresh.tags],
|
|
175
|
+
set_readers: [set.name, set.count, set.tags, set.char],
|
|
176
|
+
set_using_default: %i[name count tags].map { |a| set.using_default?(a) },
|
|
177
|
+
sentinel_before_nil: sentinel_before_nil,
|
|
178
|
+
nil_keeps_sentinel: emptied.instance_variable_get(:@tags)
|
|
179
|
+
.equal?(Lutaml::Model::Serialize::LAZY_EMPTY_COLLECTION),
|
|
180
|
+
getter_with_arg: klass.new.tap { |m| m.count("3") }.count,
|
|
181
|
+
assign_parsed_skips_cast: klass.new.tap { |m| m.__assign_parsed_count = "3" }.count,
|
|
182
|
+
built_order: element_order(klass.new do |m|
|
|
183
|
+
m.count = "1"
|
|
184
|
+
m.tags = %w[p q]
|
|
185
|
+
m.tags("r")
|
|
186
|
+
end),
|
|
187
|
+
string_names: klass.new.tap do |m|
|
|
188
|
+
m.label = "4"
|
|
189
|
+
m.notes("n")
|
|
190
|
+
end.then { |m| [m.label, m.notes, m.instance_variables.grep(/label|notes/).sort] },
|
|
191
|
+
two_args: %i[count tags label notes].map { |a| arity_error(klass.new, a) },
|
|
192
|
+
xml: xml,
|
|
193
|
+
json: set.to_json,
|
|
194
|
+
parsed: [parsed.name, parsed.count, parsed.tags, parsed.char],
|
|
195
|
+
parsed_equal: parsed == set,
|
|
196
|
+
}
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
RSpec.describe Lutaml::Model::Serialize::AttributeDefinition do
|
|
201
|
+
let(:compiled) { AttributeDefinitionBlockFormSpec.observe(AttributeDefinitionBlockFormSpec.build_model) }
|
|
202
|
+
|
|
203
|
+
it "compiles accessors from source by default on MRI" do
|
|
204
|
+
expect(described_class.source_compilation?).to be(true)
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
it "defines the block form without String class_eval and matches the compiled form" do
|
|
208
|
+
expected = compiled
|
|
209
|
+
allow(described_class).to receive(:source_compilation?).and_return(false)
|
|
210
|
+
klass = Class.new(Lutaml::Model::Serializable)
|
|
211
|
+
allow(klass).to receive(:class_eval).and_wrap_original do |original, *args, &block|
|
|
212
|
+
raise "String class_eval used: #{args.first}" if args.first.is_a?(String)
|
|
213
|
+
|
|
214
|
+
original.call(*args, &block)
|
|
215
|
+
end
|
|
216
|
+
AttributeDefinitionBlockFormSpec.define_model(klass)
|
|
217
|
+
|
|
218
|
+
expect(AttributeDefinitionBlockFormSpec.observe(klass)).to eq(expected)
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
it "does not dispatch through a model's own core methods" do
|
|
222
|
+
expected = AttributeDefinitionBlockFormSpec.override_model(Class.new(Lutaml::Model::Serializable))
|
|
223
|
+
allow(described_class).to receive(:source_compilation?).and_return(false)
|
|
224
|
+
actual = AttributeDefinitionBlockFormSpec.override_model(Class.new(Lutaml::Model::Serializable))
|
|
225
|
+
|
|
226
|
+
expect(actual).to eq(expected)
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
it "resolves Array in the model's scope when appending" do
|
|
230
|
+
expected = AttributeDefinitionBlockFormSpec.array_scope_model(Class.new(Lutaml::Model::Serializable))
|
|
231
|
+
allow(described_class).to receive(:source_compilation?).and_return(false)
|
|
232
|
+
actual = AttributeDefinitionBlockFormSpec.array_scope_model(Class.new(Lutaml::Model::Serializable))
|
|
233
|
+
|
|
234
|
+
expect(actual).to eq(expected)
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
it "defines raw-state readers for imported and late attributes as the compiled form does" do
|
|
238
|
+
expected = AttributeDefinitionBlockFormSpec.late_raw_readers(Class.new(Lutaml::Model::Serializable))
|
|
239
|
+
allow(described_class).to receive(:source_compilation?).and_return(false)
|
|
240
|
+
actual = AttributeDefinitionBlockFormSpec.late_raw_readers(Class.new(Lutaml::Model::Serializable))
|
|
241
|
+
|
|
242
|
+
expect(actual).to eq(expected)
|
|
243
|
+
end
|
|
244
|
+
end
|
|
@@ -144,4 +144,51 @@ RSpec.describe "XML adapters under Opal", if: RUBY_ENGINE == "opal" do
|
|
|
144
144
|
expect(Lutaml::Model::Config.xml_adapter_type).to eq(:rexml)
|
|
145
145
|
end
|
|
146
146
|
end
|
|
147
|
+
|
|
148
|
+
# Opal does not carry modules prepended into Serialize over to
|
|
149
|
+
# Serializable, which included it at boot; lib/lutaml/model.rb and
|
|
150
|
+
# lib/lutaml/xml/format.rb prepend onto Serializable directly.
|
|
151
|
+
describe "Serializable subclasses" do
|
|
152
|
+
before { Lutaml::Model::Config.xml_adapter_type = :oga }
|
|
153
|
+
|
|
154
|
+
it "builds an ordered model from a builder block" do
|
|
155
|
+
klass = Class.new(Lutaml::Model::Serializable) do
|
|
156
|
+
attribute :first, :string
|
|
157
|
+
attribute :second, :string
|
|
158
|
+
xml do
|
|
159
|
+
element "pair"
|
|
160
|
+
ordered
|
|
161
|
+
map_element "first", to: :first
|
|
162
|
+
map_element "second", to: :second
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
instance = klass.new do
|
|
167
|
+
second "b"
|
|
168
|
+
first "a"
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
expect([instance.first, instance.second]).to eq(%w[a b])
|
|
172
|
+
expect(instance.element_order.map(&:name)).to eq(%w[second first])
|
|
173
|
+
expect(instance.to_xml)
|
|
174
|
+
.to include("<pair><second>b</second><first>a</first></pair>")
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
it "installs the XML accessors on a plain model class" do
|
|
178
|
+
plain = Class.new { attr_accessor :name }
|
|
179
|
+
mapper = Class.new(Lutaml::Model::Serializable) do
|
|
180
|
+
model plain
|
|
181
|
+
attribute :name, :string
|
|
182
|
+
xml do
|
|
183
|
+
element "person"
|
|
184
|
+
map_element "name", to: :name
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
expect(plain.method_defined?(:encoding=)).to be true
|
|
189
|
+
parsed = mapper.from_xml("<person><name>Alice</name></person>")
|
|
190
|
+
expect(parsed).to be_a(plain)
|
|
191
|
+
expect(parsed.name).to eq("Alice")
|
|
192
|
+
end
|
|
193
|
+
end
|
|
147
194
|
end
|