lutaml-model 0.8.94 → 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/.github/workflows/downstream-performance.yml +17 -6
- data/bench/bench_merge_attempts.rb +49 -0
- data/docs/_guides/grammar-backed-formats.adoc +69 -0
- data/docs/_guides/index.adoc +2 -0
- data/lib/lutaml/model/serialize/attribute_definition.rb +145 -16
- data/lib/lutaml/model/serialize/format_conversion.rb +6 -1
- data/lib/lutaml/model/serialize/initialization.rb +13 -1
- data/lib/lutaml/model/serialize/model_import.rb +30 -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 +110 -21
- data/lib/lutaml/xml/leptris/plan_hydrator.rb +124 -10
- data/lib/lutaml/xml/leptris/plan_serializer.rb +114 -18
- data/lib/lutaml/xml/leptris.rb +38 -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/model/writer_assigned_attribute_spec.rb +2 -2
- data/spec/lutaml/xml/opal_xml_spec.rb +47 -0
- data/spec/lutaml/xml/plan_fast_path_spec.rb +6 -4
- data/spec/lutaml/xml/plan_namespaced_serialize_spec.rb +157 -0
- data/spec/lutaml/xml/plan_ns_less_document_spec.rb +86 -0
- data/spec/lutaml/xml/plan_type_ns_attribute_spec.rb +142 -0
- data/spec/lutaml/xml/plan_wildcard_any_spec.rb +105 -0
- data/spec/person_spec.rb +8 -8
- metadata +9 -2
|
@@ -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
|
|
@@ -43,9 +43,9 @@ end
|
|
|
43
43
|
# unmatched-rule replay must never overwrite a value a writer assigned.
|
|
44
44
|
RSpec.describe "attributes assigned inside another attribute's writer" do
|
|
45
45
|
%i[Plain Defaulted].each do |klass_name|
|
|
46
|
-
klass = WriterAssigned.const_get(klass_name)
|
|
47
|
-
|
|
48
46
|
describe "with #{klass_name.to_s.downcase} text attribute" do
|
|
47
|
+
let(:klass) { WriterAssigned.const_get(klass_name) }
|
|
48
|
+
|
|
49
49
|
it "keeps the derived value through from_xml" do
|
|
50
50
|
instance = klass.from_xml("<probe><code>67.060</code></probe>")
|
|
51
51
|
|
|
@@ -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
|
|
@@ -64,10 +64,12 @@ RSpec.describe "XML plan fast path" do
|
|
|
64
64
|
expect(parsed).to be_a(node)
|
|
65
65
|
end
|
|
66
66
|
|
|
67
|
-
# lutaml-model#744:
|
|
68
|
-
# type-namespaced attribute
|
|
69
|
-
#
|
|
70
|
-
|
|
67
|
+
# lutaml-model#744/#758: a local name claimed by BOTH a
|
|
68
|
+
# type-namespaced attribute row and a plain row stays interpretive —
|
|
69
|
+
# the walk's ns form :none matches any qualification, so the plain
|
|
70
|
+
# row would capture the qualified spelling too. The interpretive
|
|
71
|
+
# matcher dispatches the spellings correctly.
|
|
72
|
+
it "opts shared-local-name attribute claims out to the interpretive path" do
|
|
71
73
|
xmi_ns = Class.new(Lutaml::Xml::Namespace) do
|
|
72
74
|
uri "http://www.omg.org/spec/XMI/20131001"
|
|
73
75
|
prefix_default "xmi"
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
|
|
5
|
+
RSpec.describe "XML plan serializer: namespaced models" do
|
|
6
|
+
# lutaml-model#847: the plan path used to refuse namespaced models
|
|
7
|
+
# outright (the xmlns dropped). The DOM emission now spells prefixed
|
|
8
|
+
# names, hoists every distinct (prefix, uri) declaration exactly once
|
|
9
|
+
# onto the output root, and prefixes type-namespaced attribute rows —
|
|
10
|
+
# byte-equal with the interpretive writer.
|
|
11
|
+
|
|
12
|
+
let(:wml_ns) do
|
|
13
|
+
Class.new(Lutaml::Xml::Namespace) do
|
|
14
|
+
uri "http://example.com/gate2/wml"
|
|
15
|
+
prefix_default "w"
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
let(:val_type) do
|
|
20
|
+
stub_const("Gate2Ns::WmlNs", wml_ns)
|
|
21
|
+
type = Class.new(Lutaml::Model::Type::String) do
|
|
22
|
+
include Lutaml::Xml::Type::Configurable
|
|
23
|
+
|
|
24
|
+
xml do
|
|
25
|
+
namespace Gate2Ns::WmlNs
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
stub_const("Gate2Ns::ValType", type)
|
|
29
|
+
type
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
let(:doc_class) do
|
|
33
|
+
val_type
|
|
34
|
+
body_class
|
|
35
|
+
model = Class.new(Lutaml::Model::Serializable) do
|
|
36
|
+
attribute :name, Gate2Ns::ValType
|
|
37
|
+
attribute :body, Gate2Ns::Body
|
|
38
|
+
|
|
39
|
+
xml do
|
|
40
|
+
element "document"
|
|
41
|
+
namespace Gate2Ns::WmlNs
|
|
42
|
+
map_attribute "name", to: :name
|
|
43
|
+
map_element "body", to: :body
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
stub_const("Gate2Ns::Document", model)
|
|
47
|
+
model
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
let(:body_class) do
|
|
51
|
+
val_type
|
|
52
|
+
model = Class.new(Lutaml::Model::Serializable) do
|
|
53
|
+
attribute :text, Gate2Ns::ValType, collection: true
|
|
54
|
+
|
|
55
|
+
xml do
|
|
56
|
+
element "body"
|
|
57
|
+
namespace Gate2Ns::WmlNs
|
|
58
|
+
map_element "t", to: :text
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
stub_const("Gate2Ns::Body", model)
|
|
62
|
+
model
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
let(:default_ns_model) do
|
|
66
|
+
ns = Class.new(Lutaml::Xml::Namespace) do
|
|
67
|
+
uri "http://example.com/gate2/default"
|
|
68
|
+
end
|
|
69
|
+
stub_const("Gate2Ns::DefaultNs", ns)
|
|
70
|
+
model = Class.new(Lutaml::Model::Serializable) do
|
|
71
|
+
attribute :name, :string
|
|
72
|
+
|
|
73
|
+
xml do
|
|
74
|
+
element "root"
|
|
75
|
+
namespace Gate2Ns::DefaultNs
|
|
76
|
+
map_attribute "name", to: :name
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
stub_const("Gate2Ns::DefaultDoc", model)
|
|
80
|
+
model
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
around do |example|
|
|
84
|
+
old = Lutaml::Model::Config.instance.xml_plan_fast_path
|
|
85
|
+
Lutaml::Model::Config.instance.xml_plan_fast_path = true
|
|
86
|
+
Lutaml::Model::Config.with_adapter(xml: :leptris) { example.run }
|
|
87
|
+
ensure
|
|
88
|
+
Lutaml::Model::Config.instance.xml_plan_fast_path = old
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def plan_to_xml(klass, instance)
|
|
92
|
+
plan = Lutaml::Xml::Leptris::PlanCompiler.compile(
|
|
93
|
+
klass, Lutaml::Model::Config.default_register
|
|
94
|
+
)
|
|
95
|
+
Lutaml::Xml::Leptris::PlanSerializer.call(
|
|
96
|
+
instance, plan, Lutaml::Model::Config.default_register
|
|
97
|
+
)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
it "emits prefixed names with the declaration exactly once" do
|
|
101
|
+
instance = doc_class.new(
|
|
102
|
+
name: "d1",
|
|
103
|
+
body: body_class.new(text: ["hello"]),
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
out = plan_to_xml(doc_class, instance)
|
|
107
|
+
expect(out).to include(%(xmlns:w="http://example.com/gate2/wml"))
|
|
108
|
+
expect(out.scan("xmlns:w=").count).to eq(1)
|
|
109
|
+
expect(out).to include("<w:document")
|
|
110
|
+
expect(out).to include("w:name=\"d1\"")
|
|
111
|
+
expect(out).to include("<w:body>")
|
|
112
|
+
expect(out).to include("<w:t>hello</w:t>")
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# The plan path spells the type-namespaced attribute w:name=; the
|
|
116
|
+
# interpretive writer emits name= unprefixed (a long-standing
|
|
117
|
+
# interpretive serialize gap — the unprefixed form does not rebind
|
|
118
|
+
# to the (URI, local) rule on reparse). Element tree, declarations,
|
|
119
|
+
# and values are compared; the attribute spelling divergence is
|
|
120
|
+
# pinned by the round-trip example below.
|
|
121
|
+
it "matches the interpretive element tree and declarations" do
|
|
122
|
+
instance = doc_class.new(
|
|
123
|
+
name: "d1",
|
|
124
|
+
body: body_class.new(text: ["hello", "world"]),
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
interpretive = Lutaml::Model::Config.with_adapter(xml: :nokogiri) do
|
|
128
|
+
instance.to_xml
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
plan_out = plan_to_xml(doc_class, instance)
|
|
132
|
+
expect(plan_out.sub(" w:name=\"d1\"", "")).to eq(
|
|
133
|
+
interpretive.sub(" name=\"d1\"", "").strip,
|
|
134
|
+
)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
it "round-trips through the plan parse path" do
|
|
138
|
+
instance = doc_class.new(
|
|
139
|
+
name: "d1",
|
|
140
|
+
body: body_class.new(text: ["hello"]),
|
|
141
|
+
)
|
|
142
|
+
xml = plan_to_xml(doc_class, instance)
|
|
143
|
+
|
|
144
|
+
reparsed = doc_class.from_xml(xml)
|
|
145
|
+
expect(reparsed.name).to eq("d1")
|
|
146
|
+
expect(reparsed.body.text).to eq(["hello"])
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
it "spells default-namespace models unprefixed with xmlns on the root" do
|
|
150
|
+
instance = default_ns_model.new(name: "plain")
|
|
151
|
+
|
|
152
|
+
out = plan_to_xml(default_ns_model, instance)
|
|
153
|
+
expect(out).to include(%(xmlns="http://example.com/gate2/default"))
|
|
154
|
+
expect(out).to include("<root")
|
|
155
|
+
expect(out).not_to include("xmlns:xmlns")
|
|
156
|
+
end
|
|
157
|
+
end
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
|
|
5
|
+
RSpec.describe "XML plan fast path: namespace-less models on namespaced documents" do
|
|
6
|
+
# lutaml-model#932: elements (unlike attributes) inherit the default
|
|
7
|
+
# xmlns, so the walk's strict no-URI default dropped every child of a
|
|
8
|
+
# namespaced document for models that declare no namespace. The
|
|
9
|
+
# ns_lenient flag restores the interpretive matcher's behavior:
|
|
10
|
+
# children match by local name under any document qualification.
|
|
11
|
+
|
|
12
|
+
let(:child_class) do
|
|
13
|
+
klass = Class.new(Lutaml::Model::Serializable) do
|
|
14
|
+
attribute :fileref, :string
|
|
15
|
+
|
|
16
|
+
xml do
|
|
17
|
+
element "imagedata"
|
|
18
|
+
map_attribute "fileref", to: :fileref
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
stub_const("NsLessDoc::Imagedata", klass)
|
|
22
|
+
klass
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def define_parent
|
|
26
|
+
child_class
|
|
27
|
+
Class.new(Lutaml::Model::Serializable) do
|
|
28
|
+
attribute :imagedata, NsLessDoc::Imagedata
|
|
29
|
+
attribute :content, :string, collection: true
|
|
30
|
+
|
|
31
|
+
xml do
|
|
32
|
+
element "imageobject"
|
|
33
|
+
map_element "imagedata", to: :imagedata
|
|
34
|
+
map_content to: :content
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
around do |example|
|
|
41
|
+
old = Lutaml::Model::Config.instance.xml_plan_fast_path
|
|
42
|
+
Lutaml::Model::Config.instance.xml_plan_fast_path = true
|
|
43
|
+
Lutaml::Model::Config.with_adapter(xml: :leptris) { example.run }
|
|
44
|
+
ensure
|
|
45
|
+
Lutaml::Model::Config.instance.xml_plan_fast_path = old
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
it "binds nested children under a default namespace" do
|
|
49
|
+
parent = define_parent
|
|
50
|
+
doc = '<imageobject xmlns="http://docbook.org/ns/docbook">' \
|
|
51
|
+
'<imagedata fileref="x.png"/></imageobject>'
|
|
52
|
+
|
|
53
|
+
expect(parent.from_xml(doc).imagedata.fileref).to eq("x.png")
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
it "binds nested children under a prefixed namespace" do
|
|
57
|
+
parent = define_parent
|
|
58
|
+
doc = '<db:imageobject xmlns:db="http://docbook.org/ns/docbook">' \
|
|
59
|
+
'<db:imagedata fileref="x.png"/></db:imageobject>'
|
|
60
|
+
|
|
61
|
+
expect(parent.from_xml(doc).imagedata.fileref).to eq("x.png")
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
it "keeps binding on namespace-less documents" do
|
|
65
|
+
parent = define_parent
|
|
66
|
+
doc = %(<imageobject><imagedata fileref="x.png"/></imageobject>)
|
|
67
|
+
|
|
68
|
+
expect(parent.from_xml(doc).imagedata.fileref).to eq("x.png")
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
it "binds nested children of mixed-content models under a default namespace" do
|
|
72
|
+
parent = define_parent
|
|
73
|
+
parent.xml do
|
|
74
|
+
element "imageobject"
|
|
75
|
+
mixed_content
|
|
76
|
+
map_content to: :content
|
|
77
|
+
map_element "imagedata", to: :imagedata
|
|
78
|
+
end
|
|
79
|
+
doc = '<imageobject xmlns="http://docbook.org/ns/docbook">' \
|
|
80
|
+
'text<imagedata fileref="x.png"/>tail</imageobject>'
|
|
81
|
+
|
|
82
|
+
parsed = parent.from_xml(doc)
|
|
83
|
+
expect(parsed.imagedata.fileref).to eq("x.png")
|
|
84
|
+
expect(parsed.content).to include("text")
|
|
85
|
+
end
|
|
86
|
+
end
|