lutaml-model 0.8.15 → 0.8.17
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/release.yml +0 -3
- data/.rubocop_todo.yml +49 -52
- data/README.adoc +188 -25
- data/docs/_guides/xml/namespace-semantics.adoc +2 -0
- data/docs/_guides/xml-mapping.adoc +178 -24
- data/docs/_guides/xml-namespace-qualification.adoc +142 -0
- data/docs/_pages/importable_models.adoc +7 -1
- data/docs/_tutorials/xml-element-attribute-namespace-guide.adoc +2 -0
- data/docs/_tutorials/xml-schema-primer-style-guide.adoc +2 -0
- data/docs/namespace-management.adoc +2 -0
- data/docs/xml-schema-qualification.md +6 -0
- data/lib/lutaml/jsonld.rb +1 -4
- data/lib/lutaml/model/attribute.rb +4 -0
- data/lib/lutaml/model/choice.rb +34 -0
- data/lib/lutaml/model/compiled_rule.rb +7 -0
- data/lib/lutaml/model/version.rb +1 -1
- data/lib/lutaml/model.rb +3 -1
- data/lib/lutaml/{jsonld → rdf}/context.rb +1 -1
- data/lib/lutaml/{jsonld/transform.rb → rdf/linked_data_transform.rb} +33 -35
- data/lib/lutaml/{jsonld → rdf}/term_definition.rb +1 -1
- data/lib/lutaml/rdf.rb +3 -0
- data/lib/lutaml/turtle/transform.rb +2 -1
- data/lib/lutaml/xml/adapter/plan_based_builder.rb +3 -1
- data/lib/lutaml/xml/adapter/xml_serializer.rb +14 -4
- data/lib/lutaml/xml/adapter.rb +2 -1
- data/lib/lutaml/xml/adapter_element.rb +2 -2
- data/lib/lutaml/xml/builder/base.rb +2 -1
- data/lib/lutaml/xml/data_model.rb +19 -3
- data/lib/lutaml/xml/mapping.rb +3 -1
- data/lib/lutaml/xml/mapping_rule.rb +28 -2
- data/lib/lutaml/xml/model_transform.rb +9 -1
- data/lib/lutaml/xml/serialization/instance_methods.rb +16 -9
- data/lib/lutaml/xml/transformation/element_builder.rb +39 -26
- data/lib/lutaml/xml/transformation/rule_applier.rb +21 -0
- data/lib/lutaml/xml/transformation/rule_compiler.rb +12 -3
- data/lib/lutaml/yamlld/adapter.rb +25 -0
- data/lib/lutaml/yamlld.rb +25 -0
- data/spec/lutaml/integration/multi_format_spec.rb +23 -0
- data/spec/lutaml/model/attribute_spec.rb +8 -1
- data/spec/lutaml/model/choice_restrict_spec.rb +225 -0
- data/spec/lutaml/model/custom_model_spec.rb +4 -4
- data/spec/lutaml/model/mixed_content_spec.rb +50 -9
- data/spec/lutaml/model/multiple_mapping_spec.rb +4 -4
- data/spec/lutaml/model/ordered_content_spec.rb +3 -3
- data/spec/lutaml/model/raw_element_spec.rb +533 -0
- data/spec/lutaml/model/uninitialized_class_spec.rb +1 -1
- data/spec/lutaml/model/xsd_form_default_patterns_spec.rb +2 -2
- data/spec/lutaml/model/xsd_patterns_spec.rb +4 -4
- data/spec/lutaml/{jsonld → rdf}/context_spec.rb +2 -2
- data/spec/lutaml/{jsonld/transform_spec.rb → rdf/linked_data_transform_spec.rb} +18 -2
- data/spec/lutaml/rdf/mapping_spec.rb +2 -1
- data/spec/lutaml/{jsonld → rdf}/term_definition_spec.rb +2 -2
- data/spec/lutaml/turtle/transform_spec.rb +2 -2
- data/spec/lutaml/xml/enhanced_mapping_spec.rb +230 -1
- data/spec/lutaml/xml/mapping_spec.rb +4 -4
- data/spec/lutaml/xml/namespace_placement_spec.rb +11 -8
- data/spec/lutaml/xml/namespace_three_phase_spec.rb +1 -1
- data/spec/lutaml/xml/prefix_control_spec.rb +4 -4
- data/spec/lutaml/xml/serializable_namespace_spec.rb +6 -6
- data/spec/lutaml/xml/type_namespace_examples_spec.rb +1 -1
- data/spec/lutaml/xml/type_namespace_integration_spec.rb +3 -3
- data/spec/lutaml/yamlld/adapter_spec.rb +56 -0
- data/spec/lutaml/yamlld/registration_spec.rb +33 -0
- metadata +14 -8
|
@@ -253,6 +253,15 @@ _register)
|
|
|
253
253
|
|
|
254
254
|
private
|
|
255
255
|
|
|
256
|
+
# Resolve the unified raw mode from mapping rule and attribute
|
|
257
|
+
#
|
|
258
|
+
# @param mapping_rule [Xml::MappingRule] The mapping rule
|
|
259
|
+
# @param attr [Attribute, nil] The attribute (nil for custom methods)
|
|
260
|
+
# @return [Symbol, nil] :element, :content, or nil
|
|
261
|
+
def resolve_raw_mode(mapping_rule, attr)
|
|
262
|
+
mapping_rule.raw || (attr&.raw? ? :content : nil)
|
|
263
|
+
end
|
|
264
|
+
|
|
256
265
|
# Infer attribute name from mapping rule or custom methods
|
|
257
266
|
#
|
|
258
267
|
# @param mapping_rule [Xml::MappingRule] The mapping rule
|
|
@@ -321,7 +330,7 @@ register_id, register, attr_name, custom_methods_value)
|
|
|
321
330
|
mapping_type: :element,
|
|
322
331
|
cdata: mapping_rule.cdata,
|
|
323
332
|
mixed_content: mapping_rule.mixed_content?,
|
|
324
|
-
raw: attr
|
|
333
|
+
raw: resolve_raw_mode(mapping_rule, attr),
|
|
325
334
|
render_default: mapping_rule.render_default,
|
|
326
335
|
value_map: value_map,
|
|
327
336
|
custom_methods: custom_methods_value,
|
|
@@ -350,7 +359,7 @@ custom_methods_value)
|
|
|
350
359
|
mapping_type: :element,
|
|
351
360
|
cdata: mapping_rule.cdata,
|
|
352
361
|
mixed_content: mapping_rule.mixed_content?,
|
|
353
|
-
raw:
|
|
362
|
+
raw: mapping_rule.raw,
|
|
354
363
|
render_default: mapping_rule.render_default,
|
|
355
364
|
value_map: value_map,
|
|
356
365
|
custom_methods: custom_methods_value,
|
|
@@ -401,7 +410,7 @@ register_id, register, custom_methods_value)
|
|
|
401
410
|
mapping_type: :element,
|
|
402
411
|
cdata: mapping_rule.cdata,
|
|
403
412
|
mixed_content: mapping_rule.mixed_content?,
|
|
404
|
-
raw: attr
|
|
413
|
+
raw: resolve_raw_mode(mapping_rule, attr),
|
|
405
414
|
render_default: mapping_rule.render_default,
|
|
406
415
|
value_map: value_map,
|
|
407
416
|
custom_methods: custom_methods_value,
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "yaml"
|
|
4
|
+
|
|
5
|
+
module Lutaml
|
|
6
|
+
module YamlLd
|
|
7
|
+
class Adapter < Lutaml::KeyValue::Document
|
|
8
|
+
PERMITTED_CLASSES = Lutaml::Yaml::Adapter::StandardAdapter::PERMITTED_CLASSES
|
|
9
|
+
|
|
10
|
+
def self.parse(yaml_string, _options = {})
|
|
11
|
+
YAML.safe_load(yaml_string, permitted_classes: PERMITTED_CLASSES)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def to_yamlld(options = {})
|
|
15
|
+
attributes_to_serialize =
|
|
16
|
+
if @attributes.is_a?(Lutaml::KeyValue::DataModel::Element)
|
|
17
|
+
@attributes.to_hash["__root__"]
|
|
18
|
+
else
|
|
19
|
+
@attributes
|
|
20
|
+
end
|
|
21
|
+
YAML.dump(attributes_to_serialize, options)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "model"
|
|
4
|
+
require_relative "rdf"
|
|
5
|
+
require_relative "yaml"
|
|
6
|
+
|
|
7
|
+
module Lutaml
|
|
8
|
+
module YamlLd
|
|
9
|
+
autoload :Adapter, "#{__dir__}/yamlld/adapter"
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# Known limitations (v1):
|
|
14
|
+
# - Single YAML document only; multi-document streams not interpreted as @graph.
|
|
15
|
+
# - YAML anchors/aliases not used to deduplicate @id references.
|
|
16
|
+
# - External @context loading not performed (matches :jsonld behavior).
|
|
17
|
+
Lutaml::Model::FormatRegistry.register(
|
|
18
|
+
:yamlld,
|
|
19
|
+
mapping_class: Lutaml::Rdf::Mapping,
|
|
20
|
+
adapter_class: Lutaml::YamlLd::Adapter,
|
|
21
|
+
transformer: Lutaml::Rdf::LinkedDataTransform,
|
|
22
|
+
key_value: false,
|
|
23
|
+
rdf: true,
|
|
24
|
+
error_types: ["Psych::SyntaxError"],
|
|
25
|
+
)
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require "spec_helper"
|
|
4
4
|
require "lutaml/turtle"
|
|
5
5
|
require "lutaml/jsonld"
|
|
6
|
+
require "lutaml/yamlld"
|
|
6
7
|
|
|
7
8
|
RSpec.describe "Multi-format model" do
|
|
8
9
|
before do
|
|
@@ -103,4 +104,26 @@ RSpec.describe "Multi-format model" do
|
|
|
103
104
|
expect(turtle).to include("@prefix")
|
|
104
105
|
end
|
|
105
106
|
end
|
|
107
|
+
|
|
108
|
+
describe "YAML-LD format" do
|
|
109
|
+
it "serializes with @type and @id" do
|
|
110
|
+
yamlld = instance.to_yamlld
|
|
111
|
+
parsed = YAML.safe_load(yamlld)
|
|
112
|
+
expect(parsed["@type"]).to eq("skos:Concept")
|
|
113
|
+
expect(parsed["@id"]).to eq("http://example.org/concept/42")
|
|
114
|
+
expect(parsed["prefLabel"]).to eq("test")
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
it "round-trips" do
|
|
118
|
+
restored = MultiFormatModel.from_yamlld(instance.to_yamlld)
|
|
119
|
+
expect(restored.name).to eq("test")
|
|
120
|
+
expect(restored.code).to eq("42")
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
it "produces the same data model as JSON-LD" do
|
|
124
|
+
from_yamlld = YAML.safe_load(instance.to_yamlld)
|
|
125
|
+
from_jsonld = JSON.parse(instance.to_jsonld)
|
|
126
|
+
expect(from_yamlld).to eq(from_jsonld)
|
|
127
|
+
end
|
|
128
|
+
end
|
|
106
129
|
end
|
|
@@ -191,7 +191,14 @@ RSpec.describe Lutaml::Model::Attribute do
|
|
|
191
191
|
|
|
192
192
|
Lutaml::Model::Attribute::ALLOWED_OPTIONS.each do |option|
|
|
193
193
|
it "return true if option is `#{option}`" do
|
|
194
|
-
|
|
194
|
+
if option == :xsd_type
|
|
195
|
+
expect do
|
|
196
|
+
result = validate_options.call({ option => "value" })
|
|
197
|
+
expect(result).to be(true)
|
|
198
|
+
end.to output.to_stderr
|
|
199
|
+
else
|
|
200
|
+
expect(validate_options.call({ option => "value" })).to be(true)
|
|
201
|
+
end
|
|
195
202
|
end
|
|
196
203
|
end
|
|
197
204
|
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
|
|
5
|
+
RSpec.describe Lutaml::Model::Choice, "#restrict and #remove_attribute" do
|
|
6
|
+
before do
|
|
7
|
+
stub_const("ChoiceRestrictParent", Class.new(Lutaml::Model::Serializable) do
|
|
8
|
+
attribute :name, :string
|
|
9
|
+
attribute :age, :integer
|
|
10
|
+
attribute :email, :string
|
|
11
|
+
end)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
describe "#restrict" do
|
|
15
|
+
it "restricts a predefined attribute inside a choice block" do
|
|
16
|
+
stub_const("ChoiceRestrictModel", Class.new(Lutaml::Model::Serializable) do
|
|
17
|
+
attribute :foo, :string, collection: 1..10
|
|
18
|
+
|
|
19
|
+
choice min: 1, max: 1 do
|
|
20
|
+
restrict :foo, collection: 2..5
|
|
21
|
+
end
|
|
22
|
+
end)
|
|
23
|
+
|
|
24
|
+
attr = ChoiceRestrictModel.attributes[:foo]
|
|
25
|
+
expect(attr.options[:collection]).to eq(2..5)
|
|
26
|
+
choice = ChoiceRestrictModel.choice_attributes.first
|
|
27
|
+
expect(choice.attributes).to include(attr)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
it "raises UndefinedAttributeError for non-existent attribute" do
|
|
31
|
+
expect do
|
|
32
|
+
stub_const("ChoiceRestrictBadModel", Class.new(Lutaml::Model::Serializable) do
|
|
33
|
+
choice min: 1, max: 1 do
|
|
34
|
+
restrict :nonexistent, collection: 1..2
|
|
35
|
+
end
|
|
36
|
+
end)
|
|
37
|
+
end.to raise_error(Lutaml::Model::UndefinedAttributeError)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
it "restricts an imported model attribute inside a choice block" do
|
|
41
|
+
stub_const("ChoiceImportModel", Class.new(Lutaml::Model::Serializable) do
|
|
42
|
+
attribute :name, :string
|
|
43
|
+
attribute :age, :integer
|
|
44
|
+
|
|
45
|
+
choice min: 1, max: 1 do
|
|
46
|
+
import_model_attributes ChoiceRestrictParent
|
|
47
|
+
restrict :age, values: [18, 21, 30]
|
|
48
|
+
end
|
|
49
|
+
end)
|
|
50
|
+
|
|
51
|
+
attr = ChoiceImportModel.attributes[:age]
|
|
52
|
+
expect(attr.options[:values]).to eq([18, 21, 30])
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
it "marks the attribute as belonging to the choice" do
|
|
56
|
+
stub_const("ChoiceRestrictOwnedModel", Class.new(Lutaml::Model::Serializable) do
|
|
57
|
+
attribute :tag, :string
|
|
58
|
+
|
|
59
|
+
choice min: 0, max: 1 do
|
|
60
|
+
restrict :tag, values: %w[a b c]
|
|
61
|
+
end
|
|
62
|
+
end)
|
|
63
|
+
|
|
64
|
+
attr = ChoiceRestrictOwnedModel.attributes[:tag]
|
|
65
|
+
expect(attr.options[:choice]).to be_a(described_class)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
it "does not duplicate the attribute if restrict is called twice" do
|
|
69
|
+
stub_const("ChoiceRestrictDupModel", Class.new(Lutaml::Model::Serializable) do
|
|
70
|
+
attribute :val, :integer
|
|
71
|
+
|
|
72
|
+
choice min: 0, max: 1 do
|
|
73
|
+
restrict :val, values: [1, 2]
|
|
74
|
+
restrict :val, values: [1, 2, 3]
|
|
75
|
+
end
|
|
76
|
+
end)
|
|
77
|
+
|
|
78
|
+
choice = ChoiceRestrictDupModel.choice_attributes.first
|
|
79
|
+
count = choice.attributes.count do |a|
|
|
80
|
+
!a.is_a?(described_class) && a.name == :val
|
|
81
|
+
end
|
|
82
|
+
expect(count).to eq(1)
|
|
83
|
+
expect(ChoiceRestrictDupModel.attributes[:val].options[:values]).to eq([
|
|
84
|
+
1, 2, 3
|
|
85
|
+
])
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
it "restricts an inherited attribute in a child class choice" do
|
|
89
|
+
stub_const("ChoiceRestrictParent", Class.new(Lutaml::Model::Serializable) do
|
|
90
|
+
attribute :tag, :string
|
|
91
|
+
attribute :label, :string
|
|
92
|
+
end)
|
|
93
|
+
|
|
94
|
+
child = Class.new(ChoiceRestrictParent) do
|
|
95
|
+
include Lutaml::Model::Serialize
|
|
96
|
+
|
|
97
|
+
choice min: 1, max: 1 do
|
|
98
|
+
restrict :tag, values: %w[a b c]
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
attr = child.attributes[:tag]
|
|
103
|
+
expect(attr.options[:values]).to eq(%w[a b c])
|
|
104
|
+
choice = child.choice_attributes.first
|
|
105
|
+
expect(choice.flat_attributes.any? { |a| a.name == :tag }).to be(true)
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
describe "#remove_attribute" do
|
|
110
|
+
it "removes an attribute from the choice block" do
|
|
111
|
+
stub_const("ChoiceRemoveModel", Class.new(Lutaml::Model::Serializable) do
|
|
112
|
+
attribute :keep, :string
|
|
113
|
+
attribute :drop, :string
|
|
114
|
+
|
|
115
|
+
choice min: 0, max: 1 do
|
|
116
|
+
attribute :keep, :string
|
|
117
|
+
attribute :drop, :string
|
|
118
|
+
remove_attribute :drop
|
|
119
|
+
end
|
|
120
|
+
end)
|
|
121
|
+
|
|
122
|
+
choice = ChoiceRemoveModel.choice_attributes.first
|
|
123
|
+
attr_names = choice.attributes.filter_map do |a|
|
|
124
|
+
a.is_a?(described_class) ? nil : a.name
|
|
125
|
+
end
|
|
126
|
+
expect(attr_names).to eq([:keep])
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
it "clears the choice option from the removed attribute" do
|
|
130
|
+
stub_const("ChoiceRemoveOptModel", Class.new(Lutaml::Model::Serializable) do
|
|
131
|
+
attribute :item, :string
|
|
132
|
+
|
|
133
|
+
choice min: 0, max: 1 do
|
|
134
|
+
attribute :item, :string
|
|
135
|
+
remove_attribute :item
|
|
136
|
+
end
|
|
137
|
+
end)
|
|
138
|
+
|
|
139
|
+
attr = ChoiceRemoveOptModel.attributes[:item]
|
|
140
|
+
# The top-level :item attribute should still exist without :choice
|
|
141
|
+
# since the one in the choice was a different instance
|
|
142
|
+
expect(attr.options[:choice]).to be_nil
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
it "returns nil if the attribute is not in the choice" do
|
|
146
|
+
result = nil
|
|
147
|
+
stub_const("ChoiceRemoveNotFoundModel", Class.new(Lutaml::Model::Serializable) do
|
|
148
|
+
attribute :x, :string
|
|
149
|
+
|
|
150
|
+
choice min: 0, max: 1 do
|
|
151
|
+
result = remove_attribute(:nonexistent)
|
|
152
|
+
end
|
|
153
|
+
end)
|
|
154
|
+
|
|
155
|
+
expect(result).to be_nil
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
it "invalidates the flat_attributes cache" do
|
|
159
|
+
stub_const("ChoiceRemoveCacheModel", Class.new(Lutaml::Model::Serializable) do
|
|
160
|
+
attribute :a, :string
|
|
161
|
+
attribute :b, :string
|
|
162
|
+
|
|
163
|
+
choice min: 0, max: 1 do
|
|
164
|
+
attribute :a, :string
|
|
165
|
+
attribute :b, :string
|
|
166
|
+
end
|
|
167
|
+
end)
|
|
168
|
+
|
|
169
|
+
choice = ChoiceRemoveCacheModel.choice_attributes.first
|
|
170
|
+
expect(choice.flat_attributes.map(&:name)).to contain_exactly(:a, :b)
|
|
171
|
+
choice.remove_attribute(:b)
|
|
172
|
+
expect(choice.flat_attributes.map(&:name)).to contain_exactly(:a)
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
it "removes an imported attribute from the choice block" do
|
|
176
|
+
source = Class.new(Lutaml::Model::Serializable) do
|
|
177
|
+
attribute :name, :string
|
|
178
|
+
attribute :email, :string
|
|
179
|
+
attribute :phone, :string
|
|
180
|
+
end
|
|
181
|
+
stub_const("ChoiceRemoveImportSource", source)
|
|
182
|
+
|
|
183
|
+
stub_const("ChoiceRemoveImportModel", Class.new(Lutaml::Model::Serializable) do
|
|
184
|
+
choice min: 1, max: 1 do
|
|
185
|
+
import_model_attributes ChoiceRemoveImportSource
|
|
186
|
+
remove_attribute :phone
|
|
187
|
+
end
|
|
188
|
+
end)
|
|
189
|
+
|
|
190
|
+
choice = ChoiceRemoveImportModel.choice_attributes.first
|
|
191
|
+
names = choice.flat_attributes.map(&:name)
|
|
192
|
+
expect(names).to contain_exactly(:name, :email)
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
it "removes a restrict-added attribute from the choice" do
|
|
196
|
+
stub_const("ChoiceRemoveRestrictModel", Class.new(Lutaml::Model::Serializable) do
|
|
197
|
+
attribute :tag, :string
|
|
198
|
+
|
|
199
|
+
choice min: 0, max: 1 do
|
|
200
|
+
restrict :tag, values: %w[a b c]
|
|
201
|
+
remove_attribute :tag
|
|
202
|
+
end
|
|
203
|
+
end)
|
|
204
|
+
|
|
205
|
+
choice = ChoiceRemoveRestrictModel.choice_attributes.first
|
|
206
|
+
expect(choice.flat_attributes).to be_empty
|
|
207
|
+
expect(ChoiceRemoveRestrictModel.attributes[:tag].options[:choice]).to be_nil
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
it "returns the removed attribute" do
|
|
211
|
+
stub_const("ChoiceRemoveReturnModel", Class.new(Lutaml::Model::Serializable) do
|
|
212
|
+
attribute :item, :string
|
|
213
|
+
|
|
214
|
+
choice min: 0, max: 1 do
|
|
215
|
+
attribute :item, :string
|
|
216
|
+
end
|
|
217
|
+
end)
|
|
218
|
+
|
|
219
|
+
choice = ChoiceRemoveReturnModel.choice_attributes.first
|
|
220
|
+
removed = choice.remove_attribute(:item)
|
|
221
|
+
expect(removed).to be_a(Lutaml::Model::Attribute)
|
|
222
|
+
expect(removed.name).to eq(:item)
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
end
|
|
@@ -153,16 +153,16 @@ module CustomModelSpecs
|
|
|
153
153
|
end
|
|
154
154
|
|
|
155
155
|
class MixedWithNestedContent < Lutaml::Model::Serializable
|
|
156
|
-
attribute :street, :string
|
|
157
|
-
attribute :city, :string
|
|
156
|
+
attribute :street, :string
|
|
157
|
+
attribute :city, :string
|
|
158
158
|
attribute :bibdata, Bibdata
|
|
159
159
|
|
|
160
160
|
xml do
|
|
161
161
|
element "MixedWithNestedContent"
|
|
162
162
|
mixed_content
|
|
163
163
|
|
|
164
|
-
map_element "street", to: :street
|
|
165
|
-
map_element "city", to: :city
|
|
164
|
+
map_element "street", to: :street, raw: :content
|
|
165
|
+
map_element "city", to: :city, raw: :content
|
|
166
166
|
map_element "bibdata",
|
|
167
167
|
to: :bibdata,
|
|
168
168
|
with: { from: :bibdata_from_xml, to: :bibdata_to_xml }
|
|
@@ -107,12 +107,12 @@ module MixedContentSpec
|
|
|
107
107
|
end
|
|
108
108
|
|
|
109
109
|
class SpecialCharContentWithRawAndMixedOption < Lutaml::Model::Serializable
|
|
110
|
-
attribute :special, :string
|
|
110
|
+
attribute :special, :string
|
|
111
111
|
|
|
112
112
|
xml do
|
|
113
113
|
element "SpecialCharContentWithRawOptionAndMixedOption"
|
|
114
114
|
mixed_content
|
|
115
|
-
map_element :special, to: :special
|
|
115
|
+
map_element :special, to: :special, raw: :content
|
|
116
116
|
end
|
|
117
117
|
end
|
|
118
118
|
|
|
@@ -1111,19 +1111,22 @@ RSpec.describe "MixedContent" do
|
|
|
1111
1111
|
expect(enum).to be_a(Enumerator)
|
|
1112
1112
|
end
|
|
1113
1113
|
|
|
1114
|
-
it "
|
|
1114
|
+
it "yields whitespace text nodes in mixed content" do
|
|
1115
1115
|
parsed = MixedContentSpec::RootMixedContent.from_xml(xml)
|
|
1116
1116
|
|
|
1117
1117
|
results = []
|
|
1118
1118
|
parsed.each_mixed_content do |node|
|
|
1119
|
-
results << node if node.is_a?(String)
|
|
1119
|
+
results << node if node.is_a?(String)
|
|
1120
1120
|
end
|
|
1121
1121
|
|
|
1122
|
-
expect(results).
|
|
1122
|
+
expect(results).not_to be_empty
|
|
1123
|
+
text_joined = results.join
|
|
1124
|
+
expect(text_joined).to include("Hello")
|
|
1125
|
+
expect(text_joined).to include("and")
|
|
1126
|
+
expect(text_joined).to include("!")
|
|
1123
1127
|
end
|
|
1124
1128
|
|
|
1125
1129
|
context "with ordered-only content (no mixed)" do
|
|
1126
|
-
# Test that ordered content (elements only, no text) works
|
|
1127
1130
|
let(:xml) do
|
|
1128
1131
|
<<~XML
|
|
1129
1132
|
<RootMixedContentNested id="outer">
|
|
@@ -1134,15 +1137,17 @@ RSpec.describe "MixedContent" do
|
|
|
1134
1137
|
XML
|
|
1135
1138
|
end
|
|
1136
1139
|
|
|
1137
|
-
it "yields element
|
|
1140
|
+
it "yields element values in document order" do
|
|
1138
1141
|
parsed = MixedContentSpec::RootMixedContentNested.from_xml(xml)
|
|
1139
1142
|
|
|
1140
1143
|
results = []
|
|
1141
1144
|
parsed.content.each_mixed_content { |node| results << node }
|
|
1142
1145
|
|
|
1143
|
-
#
|
|
1146
|
+
# RootMixedContent has mixed_content, so whitespace text nodes
|
|
1147
|
+
# ARE yielded alongside element values
|
|
1144
1148
|
string_results = results.grep(String)
|
|
1145
|
-
expect(string_results
|
|
1149
|
+
expect(string_results).to include("first")
|
|
1150
|
+
expect(string_results).to include("second")
|
|
1146
1151
|
end
|
|
1147
1152
|
end
|
|
1148
1153
|
|
|
@@ -1152,6 +1157,42 @@ RSpec.describe "MixedContent" do
|
|
|
1152
1157
|
expect(parsed.each_mixed_content.to_a).to eq([])
|
|
1153
1158
|
end
|
|
1154
1159
|
end
|
|
1160
|
+
|
|
1161
|
+
context "with ordered-only model (no mixed_content)" do
|
|
1162
|
+
before do
|
|
1163
|
+
stub_const("OrderedOnlyContainer", Class.new(Lutaml::Model::Serializable) do
|
|
1164
|
+
attribute :items, :string, collection: true
|
|
1165
|
+
|
|
1166
|
+
xml do
|
|
1167
|
+
element "container"
|
|
1168
|
+
ordered
|
|
1169
|
+
map_element "item", to: :items
|
|
1170
|
+
end
|
|
1171
|
+
end)
|
|
1172
|
+
end
|
|
1173
|
+
|
|
1174
|
+
it "skips whitespace-only text nodes between elements" do
|
|
1175
|
+
xml = <<~XML
|
|
1176
|
+
<container>
|
|
1177
|
+
<item>first</item>
|
|
1178
|
+
<item>second</item>
|
|
1179
|
+
</container>
|
|
1180
|
+
XML
|
|
1181
|
+
|
|
1182
|
+
parsed = OrderedOnlyContainer.from_xml(xml)
|
|
1183
|
+
results = []
|
|
1184
|
+
parsed.each_mixed_content { |node| results << node }
|
|
1185
|
+
|
|
1186
|
+
# Ordered-only: whitespace between elements is formatting noise, not content
|
|
1187
|
+
whitespace_only = results.select do |n|
|
|
1188
|
+
n.is_a?(String) && n.strip.empty?
|
|
1189
|
+
end
|
|
1190
|
+
expect(whitespace_only).to eq([])
|
|
1191
|
+
|
|
1192
|
+
string_results = results.grep(String)
|
|
1193
|
+
expect(string_results).to eq(%w[first second])
|
|
1194
|
+
end
|
|
1195
|
+
end
|
|
1155
1196
|
end
|
|
1156
1197
|
|
|
1157
1198
|
# Issue #630: Mutation after deserialization should update serialization output
|
|
@@ -240,12 +240,12 @@ RSpec.describe MultipleMapping do
|
|
|
240
240
|
expect(product1.name).to eq("Coffee Maker")
|
|
241
241
|
expect(product1.description).to eq("Premium coffee maker")
|
|
242
242
|
expect(product1.status).to eq("active")
|
|
243
|
-
expect(product1.content.to_s).to
|
|
243
|
+
expect(product1.content.to_s).to include("Some content here")
|
|
244
244
|
|
|
245
245
|
expect(product2.name).to eq("Coffee Maker")
|
|
246
246
|
expect(product2.description).to eq("Premium coffee maker")
|
|
247
247
|
expect(product2.status).to eq("in-stock")
|
|
248
|
-
expect(product2.content.to_s).to
|
|
248
|
+
expect(product2.content.to_s).to include("Different content")
|
|
249
249
|
|
|
250
250
|
# Test round-trip: serialize and deserialize should preserve data
|
|
251
251
|
# (Note: exact XML format differs between adapters due to mixed content whitespace handling)
|
|
@@ -253,13 +253,13 @@ RSpec.describe MultipleMapping do
|
|
|
253
253
|
expect(round_trip1.name).to eq(product1.name)
|
|
254
254
|
expect(round_trip1.description).to eq(product1.description)
|
|
255
255
|
expect(round_trip1.status).to eq(product1.status)
|
|
256
|
-
expect(round_trip1.content.to_s).to
|
|
256
|
+
expect(round_trip1.content.to_s).to include("Some content here")
|
|
257
257
|
|
|
258
258
|
round_trip2 = MultipleMapping::Product.from_xml(product2.to_xml)
|
|
259
259
|
expect(round_trip2.name).to eq(product2.name)
|
|
260
260
|
expect(round_trip2.description).to eq(product2.description)
|
|
261
261
|
expect(round_trip2.status).to eq(product2.status)
|
|
262
|
-
expect(round_trip2.content.to_s).to
|
|
262
|
+
expect(round_trip2.content.to_s).to include("Different content")
|
|
263
263
|
end
|
|
264
264
|
end
|
|
265
265
|
|
|
@@ -102,8 +102,8 @@ RSpec.describe "OrderedContent" do
|
|
|
102
102
|
expect(obj.bold).to eq(["bell", "cool"])
|
|
103
103
|
expect(obj.italic).to eq(["384,400 km"])
|
|
104
104
|
expect(obj.underline).to eq("craters")
|
|
105
|
-
expect(obj.content.first.to_s).to
|
|
106
|
-
expect(obj.content.join).to
|
|
105
|
+
expect(obj.content.first.to_s).to include("The Earth's Moon rings like a")
|
|
106
|
+
expect(obj.content.join).to include("Ain't that")
|
|
107
107
|
|
|
108
108
|
# Verify round-trip preserves data
|
|
109
109
|
# (Note: exact XML format differs between adapters in ordered mode)
|
|
@@ -112,7 +112,7 @@ RSpec.describe "OrderedContent" do
|
|
|
112
112
|
expect(round_trip.bold).to eq(obj.bold)
|
|
113
113
|
expect(round_trip.italic).to eq(obj.italic)
|
|
114
114
|
expect(round_trip.underline).to eq(obj.underline)
|
|
115
|
-
expect(round_trip.content.first.to_s).to
|
|
115
|
+
expect(round_trip.content.first.to_s).to include("The Earth's Moon rings like a")
|
|
116
116
|
end
|
|
117
117
|
end
|
|
118
118
|
|