lutaml-model 0.8.16 → 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 +70 -14
- data/docs/_guides/xml/namespace-semantics.adoc +2 -0
- data/docs/_guides/xml-namespace-qualification.adoc +142 -0
- 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/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 +1 -0
- 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/xml/adapter_element.rb +2 -2
- data/lib/lutaml/xml/transformation/element_builder.rb +38 -23
- 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 +2 -2
- data/spec/lutaml/model/multiple_mapping_spec.rb +4 -4
- data/spec/lutaml/model/ordered_content_spec.rb +3 -3
- 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} +15 -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 +13 -8
|
@@ -129,7 +129,7 @@ module Lutaml
|
|
|
129
129
|
next if attr_is_namespace?(attr)
|
|
130
130
|
|
|
131
131
|
ns_prefix = attr.namespace&.prefix
|
|
132
|
-
ns_prefix = nil if ns_prefix
|
|
132
|
+
ns_prefix = nil if ns_prefix && ns_prefix.empty?
|
|
133
133
|
|
|
134
134
|
attr_name = ns_prefix ? "#{ns_prefix}:#{attr.name}" : attr.name
|
|
135
135
|
|
|
@@ -150,7 +150,7 @@ module Lutaml
|
|
|
150
150
|
next if attr_is_namespace?(attr)
|
|
151
151
|
|
|
152
152
|
ns_prefix = attr.namespace&.prefix
|
|
153
|
-
ns_prefix = nil if ns_prefix
|
|
153
|
+
ns_prefix = nil if ns_prefix && ns_prefix.empty?
|
|
154
154
|
|
|
155
155
|
attr_name = ns_prefix ? "#{ns_prefix}:#{attr.name}" : attr.name
|
|
156
156
|
order << attr_name
|
|
@@ -251,18 +251,6 @@ child_transformation)
|
|
|
251
251
|
value.xml_namespace_prefix = ns_prefix
|
|
252
252
|
end
|
|
253
253
|
|
|
254
|
-
# Also set @__xml_namespace_prefix on the XmlElement for doubly-defined case.
|
|
255
|
-
# This is read by NamespaceCollector to set NamespaceUsage.used_prefix.
|
|
256
|
-
# For doubly-defined: parent has no ns, child's ns_prefix is from @__xml_ns_prefixes
|
|
257
|
-
# (not from parent's @__xml_namespace_prefix).
|
|
258
|
-
# For mixed content: parent's XmlElement already has @__xml_namespace_prefix set.
|
|
259
|
-
if parent_ns_class.nil? && ns_prefix && !ns_prefix.empty? && !child_ns_class
|
|
260
|
-
# Doubly-defined case: parent has no ns, child has ns class.
|
|
261
|
-
# Set on XmlElement so NamespaceCollector reads it.
|
|
262
|
-
# The value will be set on the model instance above (via @__xml_namespace_prefix)
|
|
263
|
-
# and we'll set it on XmlElement too so collection phase picks it up.
|
|
264
|
-
end
|
|
265
|
-
|
|
266
254
|
child_element = child_transformation.transform(value, child_options)
|
|
267
255
|
|
|
268
256
|
# Dual-namespace support: when the child model was deserialized from an element
|
|
@@ -304,15 +292,10 @@ child_transformation)
|
|
|
304
292
|
child_element.name = rule.serialized_name
|
|
305
293
|
end
|
|
306
294
|
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
if parent_element_form_default == :unqualified &&
|
|
312
|
-
parent_ns_class &&
|
|
313
|
-
child_element.namespace_class == parent_ns_class
|
|
314
|
-
child_element.namespace_class = nil
|
|
315
|
-
end
|
|
295
|
+
propagate_form(child_element, rule)
|
|
296
|
+
apply_unqualified_form_default(
|
|
297
|
+
child_element, rule, parent_ns_class, parent_element_form_default
|
|
298
|
+
)
|
|
316
299
|
|
|
317
300
|
child_element
|
|
318
301
|
end
|
|
@@ -356,7 +339,7 @@ child_transformation)
|
|
|
356
339
|
element.xml_namespace_prefix = model_ns_prefix
|
|
357
340
|
end
|
|
358
341
|
|
|
359
|
-
element
|
|
342
|
+
propagate_form(element, rule)
|
|
360
343
|
text = serialize_value(value, rule, rule.attribute_type, nil)
|
|
361
344
|
element.text_content = text if text
|
|
362
345
|
element
|
|
@@ -490,7 +473,7 @@ register_id)
|
|
|
490
473
|
element.xml_namespace_prefix = ns_prefix
|
|
491
474
|
end
|
|
492
475
|
|
|
493
|
-
element
|
|
476
|
+
propagate_form(element, rule)
|
|
494
477
|
|
|
495
478
|
# Handle Hash type - expand hash into child elements
|
|
496
479
|
if value.is_a?(::Hash) && rule.attribute_type == Lutaml::Model::Type::Hash
|
|
@@ -523,6 +506,38 @@ register_id)
|
|
|
523
506
|
end
|
|
524
507
|
end
|
|
525
508
|
|
|
509
|
+
# Copy the rule's form option onto the produced element so downstream
|
|
510
|
+
# rules (ElementFormOptionRule) can read it. Without this propagation
|
|
511
|
+
# the form option would be lost on transformed/fallback/simple paths.
|
|
512
|
+
#
|
|
513
|
+
# @param element [::Lutaml::Xml::DataModel::XmlElement] The element to update
|
|
514
|
+
# @param rule [CompiledRule] The rule carrying the form option
|
|
515
|
+
def propagate_form(element, rule)
|
|
516
|
+
element.form = rule.form if rule.form_set?
|
|
517
|
+
end
|
|
518
|
+
|
|
519
|
+
# W3C elementFormDefault: unqualified override.
|
|
520
|
+
#
|
|
521
|
+
# When the parent schema declares elementFormDefault="unqualified" and
|
|
522
|
+
# a locally-declared child lives in the same namespace as the parent,
|
|
523
|
+
# the child MUST NOT be namespace-qualified (XSD spec). An explicit
|
|
524
|
+
# form: :qualified on the rule is the user's per-element opt-out and
|
|
525
|
+
# takes precedence over the schema default.
|
|
526
|
+
#
|
|
527
|
+
# @param element [::Lutaml::Xml::DataModel::XmlElement] The element to update
|
|
528
|
+
# @param rule [CompiledRule] The rule (checked for explicit form override)
|
|
529
|
+
# @param parent_ns_class [Class, nil] Parent's namespace class
|
|
530
|
+
# @param parent_form_default [Symbol, nil] Parent's element_form_default
|
|
531
|
+
def apply_unqualified_form_default(element, rule, parent_ns_class,
|
|
532
|
+
parent_form_default)
|
|
533
|
+
return if rule.form == :qualified
|
|
534
|
+
return if parent_form_default != :unqualified
|
|
535
|
+
return unless parent_ns_class
|
|
536
|
+
return unless element.namespace_class == parent_ns_class
|
|
537
|
+
|
|
538
|
+
element.namespace_class = nil
|
|
539
|
+
end
|
|
540
|
+
|
|
526
541
|
# Apply nil marker to element if needed
|
|
527
542
|
#
|
|
528
543
|
# @param element [XmlElement] The element
|
|
@@ -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
|
|
|
@@ -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
|
|
|
@@ -23,7 +23,7 @@ RSpec.describe Lutaml::Model::UninitializedClass do
|
|
|
23
23
|
|
|
24
24
|
describe "#match?" do
|
|
25
25
|
it "returns false for any argument" do
|
|
26
|
-
expect(uninitialized).not_to
|
|
26
|
+
expect(uninitialized).not_to include("pattern")
|
|
27
27
|
expect(uninitialized).not_to match "string"
|
|
28
28
|
expect(uninitialized).not_to match nil
|
|
29
29
|
end
|
|
@@ -85,7 +85,7 @@ RSpec.describe "XSD Form Default Patterns" do
|
|
|
85
85
|
xml = instance.to_xml
|
|
86
86
|
|
|
87
87
|
# The element should be unqualified (no prefix) in instance
|
|
88
|
-
expect(xml).to
|
|
88
|
+
expect(xml).to include("<premium")
|
|
89
89
|
expect(xml).to include("<elementFormUnqualified")
|
|
90
90
|
end
|
|
91
91
|
end
|
|
@@ -281,7 +281,7 @@ RSpec.describe "XSD Form Default Patterns" do
|
|
|
281
281
|
# and the namespace is already declared on an ancestor
|
|
282
282
|
# The child element should be in the blank namespace without xmlns="" declaration
|
|
283
283
|
# because the parent uses prefix format (xmlns:ex) or the namespace is already declared.
|
|
284
|
-
expect(result).not_to
|
|
284
|
+
expect(result).not_to include('xmlns=""')
|
|
285
285
|
end
|
|
286
286
|
|
|
287
287
|
it "serializes root element with namespace declaration" do
|
|
@@ -43,7 +43,7 @@ RSpec.describe "XSD Three Pattern Architecture" do
|
|
|
43
43
|
|
|
44
44
|
expect(xsd).to include('<element name="item">')
|
|
45
45
|
expect(xsd).to include("<complexType>")
|
|
46
|
-
expect(xsd).not_to
|
|
46
|
+
expect(xsd).not_to include("<complexType name=")
|
|
47
47
|
expect(xsd).to include('<attribute name="id" type="xs:string"/>')
|
|
48
48
|
end
|
|
49
49
|
end
|
|
@@ -68,7 +68,7 @@ RSpec.describe "XSD Three Pattern Architecture" do
|
|
|
68
68
|
expect(xsd).to include('<element name="name" type="xs:string"/>')
|
|
69
69
|
expect(xsd).to include('<element name="value" type="xs:integer"/>')
|
|
70
70
|
# Should NOT generate standalone element declaration
|
|
71
|
-
expect(xsd).not_to
|
|
71
|
+
expect(xsd).not_to include('<element name="product"')
|
|
72
72
|
end
|
|
73
73
|
|
|
74
74
|
it "type-only model can be referenced by other elements" do
|
|
@@ -233,7 +233,7 @@ RSpec.describe "XSD Three Pattern Architecture" do
|
|
|
233
233
|
|
|
234
234
|
xsd = Lutaml::Model::Schema.to_xsd(klass)
|
|
235
235
|
expect(xsd).to include('<element name="simple">')
|
|
236
|
-
expect(xsd).not_to
|
|
236
|
+
expect(xsd).not_to include('<element name="simple" type=')
|
|
237
237
|
end
|
|
238
238
|
|
|
239
239
|
it "Pattern 2: type_name only -> named reusable" do
|
|
@@ -252,7 +252,7 @@ RSpec.describe "XSD Three Pattern Architecture" do
|
|
|
252
252
|
xsd = Lutaml::Model::Schema.to_xsd(klass)
|
|
253
253
|
expect(xsd).to include('<complexType name="SimpleType">')
|
|
254
254
|
# Should not have standalone element declaration (only child elements in sequence are OK)
|
|
255
|
-
expect(xsd).not_to
|
|
255
|
+
expect(xsd).not_to include('<element name="SimpleType"')
|
|
256
256
|
end
|
|
257
257
|
|
|
258
258
|
it "Pattern 3: both -> element with named type" do
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "spec_helper"
|
|
4
|
-
require "lutaml/
|
|
4
|
+
require "lutaml/rdf"
|
|
5
5
|
|
|
6
|
-
RSpec.describe Lutaml::
|
|
6
|
+
RSpec.describe Lutaml::Rdf::Context do
|
|
7
7
|
subject(:ctx) { described_class.new }
|
|
8
8
|
|
|
9
9
|
describe "empty context" do
|
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
require "spec_helper"
|
|
4
4
|
require "lutaml/jsonld"
|
|
5
|
+
require "lutaml/yamlld"
|
|
5
6
|
|
|
6
|
-
RSpec.describe Lutaml::
|
|
7
|
+
RSpec.describe Lutaml::Rdf::LinkedDataTransform do
|
|
7
8
|
before do
|
|
8
9
|
stub_const("TestSkosNs", Class.new(Lutaml::Rdf::Namespace) do
|
|
9
10
|
uri "http://www.w3.org/2004/02/skos/core#"
|
|
@@ -138,6 +139,19 @@ RSpec.describe Lutaml::JsonLd::Transform do
|
|
|
138
139
|
expect(parsed["@context"]["skos"]).to eq("http://www.w3.org/2004/02/skos/core#")
|
|
139
140
|
expect(parsed["@context"]["ex"]).to eq("http://example.org/")
|
|
140
141
|
end
|
|
142
|
+
|
|
143
|
+
it "preserves data through model → YAML-LD → model" do
|
|
144
|
+
yaml = instance.to_yamlld
|
|
145
|
+
restored = JsonLdTestModel.from_yamlld(yaml)
|
|
146
|
+
expect(restored.name).to eq("test")
|
|
147
|
+
expect(restored.description).to eq("A test concept")
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
it "produces the same data model from YAML-LD as JSON-LD" do
|
|
151
|
+
from_yamlld = YAML.safe_load(instance.to_yamlld)
|
|
152
|
+
from_jsonld = JSON.parse(instance.to_jsonld)
|
|
153
|
+
expect(from_yamlld).to eq(from_jsonld)
|
|
154
|
+
end
|
|
141
155
|
end
|
|
142
156
|
|
|
143
157
|
describe "nil attribute values" do
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "spec_helper"
|
|
4
|
-
require "lutaml/
|
|
4
|
+
require "lutaml/rdf"
|
|
5
5
|
|
|
6
|
-
RSpec.describe Lutaml::
|
|
6
|
+
RSpec.describe Lutaml::Rdf::TermDefinition do
|
|
7
7
|
it "simple term maps to name => id" do
|
|
8
8
|
td = described_class.new(name: "name", id: "http://example.org/name")
|
|
9
9
|
expect(td.to_context_hash).to eq("name" => "http://example.org/name")
|
|
@@ -119,13 +119,13 @@ RSpec.describe Lutaml::Turtle::Transform do
|
|
|
119
119
|
it "serializes integers as native Turtle literals" do
|
|
120
120
|
instance = TypedModel.new(label: "test", count: 42, active: true)
|
|
121
121
|
result = instance.to_turtle
|
|
122
|
-
expect(result).to
|
|
122
|
+
expect(result).to include("skos:notation 42")
|
|
123
123
|
end
|
|
124
124
|
|
|
125
125
|
it "serializes booleans as native Turtle literals" do
|
|
126
126
|
instance = TypedModel.new(label: "test", count: 1, active: true)
|
|
127
127
|
result = instance.to_turtle
|
|
128
|
-
expect(result).to
|
|
128
|
+
expect(result).to include("skos:note true")
|
|
129
129
|
end
|
|
130
130
|
end
|
|
131
131
|
|