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.
@@ -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: attribute plan rows are local-name keyed — a
68
- # type-namespaced attribute (xmi:type vs type) must take the
69
- # interpretive matcher, or the namespaced value is silently lost.
70
- it "opts models with type-namespaced attributes out to the interpretive path" do
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