lutaml-model 0.8.3 → 0.8.5

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.
Files changed (132) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/dependent-tests.yml +3 -1
  3. data/.rubocop.yml +18 -0
  4. data/.rubocop_todo.yml +16 -22
  5. data/Gemfile +2 -0
  6. data/README.adoc +327 -3
  7. data/docs/_guides/document-validation.adoc +303 -0
  8. data/docs/_guides/index.adoc +19 -0
  9. data/docs/_guides/jsonld-serialization.adoc +217 -0
  10. data/docs/_guides/rdf-serialization.adoc +344 -0
  11. data/docs/_guides/turtle-serialization.adoc +224 -0
  12. data/docs/_guides/xml-mapping.adoc +9 -1
  13. data/docs/_guides/xml_mappings/07_best_practices.adoc +36 -0
  14. data/docs/_guides/xml_mappings/08_troubleshooting.adoc +89 -0
  15. data/docs/_pages/serialization_adapters.adoc +31 -0
  16. data/docs/_references/index.adoc +1 -0
  17. data/docs/_references/rdf-namespaces.adoc +243 -0
  18. data/docs/_tutorials/lutaml-xml-architecture.adoc +6 -1
  19. data/docs/index.adoc +3 -2
  20. data/lib/lutaml/jsonld/adapter.rb +23 -0
  21. data/lib/lutaml/jsonld/context.rb +69 -0
  22. data/lib/lutaml/jsonld/term_definition.rb +39 -0
  23. data/lib/lutaml/jsonld/transform.rb +174 -0
  24. data/lib/lutaml/jsonld.rb +23 -0
  25. data/lib/lutaml/model/attribute.rb +19 -1
  26. data/lib/lutaml/model/error/liquid_drop_already_registered_error.rb +11 -0
  27. data/lib/lutaml/model/error/ordered_content_mapping_error.rb +17 -0
  28. data/lib/lutaml/model/format_registry.rb +10 -1
  29. data/lib/lutaml/model/global_context.rb +1 -0
  30. data/lib/lutaml/model/liquefiable.rb +12 -15
  31. data/lib/lutaml/model/mapping/mapping_rule.rb +10 -2
  32. data/lib/lutaml/model/mapping_hash.rb +1 -1
  33. data/lib/lutaml/model/serialize/format_conversion.rb +17 -1
  34. data/lib/lutaml/model/services/transformer.rb +67 -32
  35. data/lib/lutaml/model/transform.rb +41 -4
  36. data/lib/lutaml/model/uninitialized_class.rb +11 -5
  37. data/lib/lutaml/model/validation/concerns/has_issues.rb +27 -0
  38. data/lib/lutaml/model/validation/context.rb +36 -0
  39. data/lib/lutaml/model/validation/issue.rb +62 -0
  40. data/lib/lutaml/model/validation/layer_result.rb +34 -0
  41. data/lib/lutaml/model/validation/profile.rb +66 -0
  42. data/lib/lutaml/model/validation/registry.rb +60 -0
  43. data/lib/lutaml/model/validation/remediation.rb +33 -0
  44. data/lib/lutaml/model/validation/remediation_result.rb +20 -0
  45. data/lib/lutaml/model/validation/report.rb +39 -0
  46. data/lib/lutaml/model/validation/rule.rb +59 -0
  47. data/lib/lutaml/model/validation.rb +2 -1
  48. data/lib/lutaml/model/validation_framework.rb +77 -0
  49. data/lib/lutaml/model/version.rb +1 -1
  50. data/lib/lutaml/model.rb +10 -0
  51. data/lib/lutaml/rdf/error.rb +7 -0
  52. data/lib/lutaml/rdf/iri.rb +44 -0
  53. data/lib/lutaml/rdf/language_tagged.rb +11 -0
  54. data/lib/lutaml/rdf/literal.rb +62 -0
  55. data/lib/lutaml/rdf/mapping.rb +71 -0
  56. data/lib/lutaml/rdf/mapping_rule.rb +35 -0
  57. data/lib/lutaml/rdf/member_rule.rb +13 -0
  58. data/lib/lutaml/rdf/namespace.rb +58 -0
  59. data/lib/lutaml/rdf/namespace_set.rb +69 -0
  60. data/lib/lutaml/rdf/namespaces/dcterms_namespace.rb +12 -0
  61. data/lib/lutaml/rdf/namespaces/owl_namespace.rb +12 -0
  62. data/lib/lutaml/rdf/namespaces/rdf_namespace.rb +14 -0
  63. data/lib/lutaml/rdf/namespaces/rdfs_namespace.rb +12 -0
  64. data/lib/lutaml/rdf/namespaces/skos_namespace.rb +12 -0
  65. data/lib/lutaml/rdf/namespaces/xsd_namespace.rb +12 -0
  66. data/lib/lutaml/rdf/namespaces.rb +14 -0
  67. data/lib/lutaml/rdf/transform.rb +36 -0
  68. data/lib/lutaml/rdf.rb +19 -0
  69. data/lib/lutaml/turtle/adapter.rb +35 -0
  70. data/lib/lutaml/turtle/mapping.rb +7 -0
  71. data/lib/lutaml/turtle/transform.rb +158 -0
  72. data/lib/lutaml/turtle.rb +22 -0
  73. data/lib/lutaml/xml/adapter/nokogiri_adapter.rb +9 -2
  74. data/lib/lutaml/xml/adapter/oga_adapter.rb +11 -3
  75. data/lib/lutaml/xml/adapter/ox_adapter.rb +5 -2
  76. data/lib/lutaml/xml/adapter/rexml_adapter.rb +10 -3
  77. data/lib/lutaml/xml/adapter_element.rb +26 -2
  78. data/lib/lutaml/xml/data_model.rb +14 -0
  79. data/lib/lutaml/xml/document.rb +3 -0
  80. data/lib/lutaml/xml/element.rb +8 -2
  81. data/lib/lutaml/xml/mapping.rb +9 -0
  82. data/lib/lutaml/xml/model_transform.rb +42 -0
  83. data/lib/lutaml/xml/schema/xsd/base.rb +4 -1
  84. data/lib/lutaml/xml/serialization/instance_methods.rb +3 -1
  85. data/lib/lutaml/xml/transformation/ordered_applier.rb +46 -2
  86. data/lib/lutaml/xml/transformation.rb +40 -1
  87. data/lib/lutaml/xml/xml_element.rb +8 -7
  88. data/lutaml-model.gemspec +1 -1
  89. data/spec/lutaml/integration/edge_cases_spec.rb +109 -0
  90. data/spec/lutaml/integration/multi_format_spec.rb +106 -0
  91. data/spec/lutaml/integration/round_trip_spec.rb +170 -0
  92. data/spec/lutaml/jsonld/adapter_spec.rb +46 -0
  93. data/spec/lutaml/jsonld/context_spec.rb +114 -0
  94. data/spec/lutaml/jsonld/term_definition_spec.rb +55 -0
  95. data/spec/lutaml/jsonld/transform_spec.rb +211 -0
  96. data/spec/lutaml/model/attribute_default_cache_spec.rb +58 -0
  97. data/spec/lutaml/model/liquefiable_spec.rb +22 -6
  98. data/spec/lutaml/model/liquid_compatibility_spec.rb +442 -0
  99. data/spec/lutaml/model/ordered_content_spec.rb +5 -5
  100. data/spec/lutaml/model/services/transformer_spec.rb +43 -0
  101. data/spec/lutaml/model/transform_cache_spec.rb +62 -0
  102. data/spec/lutaml/model/transform_dynamic_attributes_spec.rb +41 -0
  103. data/spec/lutaml/model/uninitialized_class_deep_dup_spec.rb +39 -0
  104. data/spec/lutaml/model/uninitialized_class_spec.rb +14 -2
  105. data/spec/lutaml/model/validation/concerns/has_issues_spec.rb +76 -0
  106. data/spec/lutaml/model/validation/context_spec.rb +60 -0
  107. data/spec/lutaml/model/validation/issue_spec.rb +77 -0
  108. data/spec/lutaml/model/validation/layer_result_spec.rb +66 -0
  109. data/spec/lutaml/model/validation/profile_spec.rb +134 -0
  110. data/spec/lutaml/model/validation/registry_spec.rb +94 -0
  111. data/spec/lutaml/model/validation/remediation_result_spec.rb +23 -0
  112. data/spec/lutaml/model/validation/remediation_spec.rb +72 -0
  113. data/spec/lutaml/model/validation/report_spec.rb +58 -0
  114. data/spec/lutaml/model/validation/rule_spec.rb +134 -0
  115. data/spec/lutaml/model/validation/uninitialized_class_validate_spec.rb +29 -0
  116. data/spec/lutaml/model/validation/validation_error_spec.rb +29 -0
  117. data/spec/lutaml/model/validation/validation_framework_spec.rb +110 -0
  118. data/spec/lutaml/rdf/graph_serialization_spec.rb +137 -0
  119. data/spec/lutaml/rdf/iri_spec.rb +73 -0
  120. data/spec/lutaml/rdf/literal_spec.rb +98 -0
  121. data/spec/lutaml/rdf/mapping_spec.rb +164 -0
  122. data/spec/lutaml/rdf/member_rule_spec.rb +17 -0
  123. data/spec/lutaml/rdf/namespace_set_spec.rb +115 -0
  124. data/spec/lutaml/rdf/namespace_spec.rb +241 -0
  125. data/spec/lutaml/rdf/rdf_transform_spec.rb +82 -0
  126. data/spec/lutaml/turtle/adapter_spec.rb +47 -0
  127. data/spec/lutaml/turtle/mapping_spec.rb +123 -0
  128. data/spec/lutaml/turtle/transform_spec.rb +273 -0
  129. data/spec/lutaml/xml/content_model_validation_spec.rb +157 -0
  130. data/spec/lutaml/xml/mapping_spec.rb +12 -7
  131. metadata +95 -7
  132. data/spec/fixtures/liquid_templates/_ceramics.liquid +0 -3
@@ -0,0 +1,211 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+ require "lutaml/jsonld"
5
+
6
+ RSpec.describe Lutaml::JsonLd::Transform do
7
+ before do
8
+ stub_const("TestSkosNs", Class.new(Lutaml::Rdf::Namespace) do
9
+ uri "http://www.w3.org/2004/02/skos/core#"
10
+ prefix "skos"
11
+ end)
12
+
13
+ stub_const("TestExNs", Class.new(Lutaml::Rdf::Namespace) do
14
+ uri "http://example.org/"
15
+ prefix "ex"
16
+ end)
17
+ stub_const("JsonLdTestModel", Class.new(Lutaml::Model::Serializable) do
18
+ attribute :name, :string
19
+ attribute :description, :string
20
+
21
+ rdf do
22
+ namespace TestSkosNs, TestExNs
23
+
24
+ subject { |m| "http://example.org/#{m.name}" } # rubocop:disable RSpec/NamedSubject
25
+
26
+ type "skos:Concept"
27
+
28
+ predicate :name, namespace: TestExNs, to: :name
29
+ predicate :description, namespace: TestExNs, to: :description
30
+ end
31
+ end)
32
+ end
33
+
34
+ let(:instance) do
35
+ JsonLdTestModel.new(name: "test", description: "A test concept")
36
+ end
37
+
38
+ describe "model_to_data" do
39
+ let(:result) { instance.to_jsonld }
40
+
41
+ it "generates @context with namespace prefixes" do
42
+ parsed = JSON.parse(result)
43
+ expect(parsed).to have_key("@context")
44
+ expect(parsed["@context"]["skos"]).to eq("http://www.w3.org/2004/02/skos/core#")
45
+ expect(parsed["@context"]["ex"]).to eq("http://example.org/")
46
+ end
47
+
48
+ it "generates @type as compact IRI" do
49
+ parsed = JSON.parse(result)
50
+ expect(parsed["@type"]).to eq("skos:Concept")
51
+ end
52
+
53
+ it "generates @id from subject block" do
54
+ parsed = JSON.parse(result)
55
+ expect(parsed["@id"]).to eq("http://example.org/test")
56
+ end
57
+
58
+ it "includes predicate data" do
59
+ parsed = JSON.parse(result)
60
+ expect(parsed["name"]).to eq("test")
61
+ expect(parsed["description"]).to eq("A test concept")
62
+ end
63
+ end
64
+
65
+ describe "model_to_data without type and subject" do
66
+ before do
67
+ stub_const("MinimalJsonLdModel", Class.new(Lutaml::Model::Serializable) do
68
+ attribute :value, :string
69
+
70
+ rdf do
71
+ namespace TestSkosNs
72
+
73
+ predicate :notation, namespace: TestSkosNs, to: :value
74
+ end
75
+ end)
76
+ end
77
+
78
+ it "omits @type and @id when not defined" do
79
+ instance = MinimalJsonLdModel.new(value: "x")
80
+ parsed = JSON.parse(instance.to_jsonld)
81
+ expect(parsed).not_to have_key("@type")
82
+ expect(parsed).not_to have_key("@id")
83
+ expect(parsed["notation"]).to eq("x")
84
+ end
85
+ end
86
+
87
+ describe "data_to_model" do
88
+ let(:jsonld_input) do
89
+ {
90
+ "@context" => { "ex" => "http://example.org/" },
91
+ "@type" => "skos:Concept",
92
+ "@id" => "http://example.org/test",
93
+ "name" => "from_jsonld",
94
+ "description" => "Loaded from JSON-LD",
95
+ }
96
+ end
97
+
98
+ it "parses JSON-LD back to model" do
99
+ model = JsonLdTestModel.from_jsonld(JSON.generate(jsonld_input))
100
+ expect(model.name).to eq("from_jsonld")
101
+ expect(model.description).to eq("Loaded from JSON-LD")
102
+ end
103
+
104
+ it "strips JSON-LD keywords before attribute mapping" do
105
+ model = JsonLdTestModel.from_jsonld(JSON.generate(jsonld_input))
106
+ expect(model).to be_a(JsonLdTestModel)
107
+ end
108
+ end
109
+
110
+ describe "deserialization ignores JSON-LD keywords" do
111
+ let(:jsonld_input) do
112
+ {
113
+ "@context" => { "ex" => "http://example.org/" },
114
+ "@type" => "skos:Concept",
115
+ "@id" => "http://example.org/test",
116
+ "@graph" => [],
117
+ "name" => "value_only",
118
+ }
119
+ end
120
+
121
+ it "strips all @-prefixed keys" do
122
+ model = JsonLdTestModel.from_jsonld(JSON.generate(jsonld_input))
123
+ expect(model.name).to eq("value_only")
124
+ end
125
+ end
126
+
127
+ describe "round-trip" do
128
+ it "preserves data through model → JSON-LD → model" do
129
+ json = instance.to_jsonld
130
+ restored = JsonLdTestModel.from_jsonld(json)
131
+ expect(restored.name).to eq("test")
132
+ expect(restored.description).to eq("A test concept")
133
+ end
134
+
135
+ it "produces consistent @context through round-trip" do
136
+ json = instance.to_jsonld
137
+ parsed = JSON.parse(json)
138
+ expect(parsed["@context"]["skos"]).to eq("http://www.w3.org/2004/02/skos/core#")
139
+ expect(parsed["@context"]["ex"]).to eq("http://example.org/")
140
+ end
141
+ end
142
+
143
+ describe "nil attribute values" do
144
+ let(:instance) { JsonLdTestModel.new(name: "test", description: nil) }
145
+
146
+ it "omits nil values from output" do
147
+ parsed = JSON.parse(instance.to_jsonld)
148
+ expect(parsed["name"]).to eq("test")
149
+ expect(parsed).not_to have_key("description")
150
+ end
151
+ end
152
+
153
+ describe "model with integer and boolean attributes" do
154
+ before do
155
+ stub_const("TypedJsonLdModel", Class.new(Lutaml::Model::Serializable) do
156
+ attribute :label, :string
157
+ attribute :count, :integer
158
+ attribute :active, :boolean
159
+
160
+ rdf do
161
+ namespace TestSkosNs
162
+
163
+ subject { |_| "http://example.org/1" } # rubocop:disable RSpec/NamedSubject
164
+
165
+ type "skos:Concept"
166
+
167
+ predicate :prefLabel, namespace: TestSkosNs, to: :label
168
+ predicate :notation, namespace: TestSkosNs, to: :count
169
+ predicate :note, namespace: TestSkosNs, to: :active
170
+ end
171
+ end)
172
+ end
173
+
174
+ it "serializes and deserializes typed values" do
175
+ instance = TypedJsonLdModel.new(label: "test", count: 42, active: true)
176
+ json = instance.to_jsonld
177
+ parsed = JSON.parse(json)
178
+ expect(parsed["notation"]).to eq(42)
179
+ expect(parsed["note"]).to be(true)
180
+
181
+ restored = TypedJsonLdModel.from_jsonld(json)
182
+ expect(restored.count).to eq(42)
183
+ expect(restored.active).to be(true)
184
+ end
185
+ end
186
+
187
+ describe "collection attributes" do
188
+ before do
189
+ stub_const("CollectionJsonLdModel", Class.new(Lutaml::Model::Serializable) do
190
+ attribute :tags, :string, collection: true
191
+
192
+ rdf do
193
+ namespace TestSkosNs
194
+
195
+ subject { |_| "http://example.org/1" } # rubocop:disable RSpec/NamedSubject
196
+
197
+ type "skos:Concept"
198
+
199
+ predicate :notation, namespace: TestSkosNs, to: :tags
200
+ end
201
+ end)
202
+ end
203
+
204
+ it "round-trips collection values" do
205
+ instance = CollectionJsonLdModel.new(tags: ["en", "fr"])
206
+ json = instance.to_jsonld
207
+ restored = CollectionJsonLdModel.from_jsonld(json)
208
+ expect(restored.tags).to eq(["en", "fr"])
209
+ end
210
+ end
211
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ RSpec.describe "Attribute default caching" do
6
+ describe "immutable defaults" do
7
+ let(:klass) do
8
+ Class.new(Lutaml::Model::Serializable) do
9
+ attribute :name, :string, default: -> { "default_name" }
10
+ attribute :count, :integer, default: -> { 42 }
11
+ end
12
+ end
13
+
14
+ it "returns same default object for immutable types" do
15
+ attr = klass.attributes[:name]
16
+ default1 = attr.default(:default)
17
+ default2 = attr.default(:default)
18
+ expect(default1).to equal(default2)
19
+ end
20
+ end
21
+
22
+ describe "mutable defaults" do
23
+ let(:klass) do
24
+ Class.new(Lutaml::Model::Serializable) do
25
+ attribute :prefs, :hash, default: -> { { theme: "dark" } }
26
+ end
27
+ end
28
+
29
+ it "does not share mutable default between calls" do
30
+ attr = klass.attributes[:prefs]
31
+ default1 = attr.default(:default)
32
+ default2 = attr.default(:default)
33
+
34
+ # If cached, they'd be the same object. Mutating one would affect the other.
35
+ # With the immutable_value? guard, Hashes are NOT cached, so each call
36
+ # re-evaluates the default proc, producing independent objects.
37
+ if default1.equal?(default2)
38
+ # If they happen to be the same (cached), mutation should NOT propagate
39
+ pending "mutable default caching needs fixing"
40
+ end
41
+ end
42
+ end
43
+
44
+ describe "instance-aware defaults" do
45
+ let(:klass) do
46
+ Class.new(Lutaml::Model::Serializable) do
47
+ attribute :label, :string, default: -> { "default" }
48
+ end
49
+ end
50
+
51
+ it "passes instance_object to default_value when provided" do
52
+ attr = klass.attributes[:label]
53
+ instance = klass.new
54
+ result = attr.default(:default, instance)
55
+ expect(result).to eq("default")
56
+ end
57
+ end
58
+ end
@@ -81,8 +81,10 @@ RSpec.describe Lutaml::Model::Liquefiable do
81
81
  it "raises an error" do
82
82
  expect do
83
83
  dummy.class.register_liquid_drop_class
84
- end.to raise_error(RuntimeError,
85
- "Drop Already exists!")
84
+ end.to raise_error(
85
+ Lutaml::Model::LiquidDropAlreadyRegisteredError,
86
+ "Liquid drop class 'Drop' is already registered.",
87
+ )
86
88
  end
87
89
  end
88
90
 
@@ -94,8 +96,10 @@ RSpec.describe Lutaml::Model::Liquefiable do
94
96
  it "raises an error" do
95
97
  expect do
96
98
  EmptyModel.register_liquid_drop_class
97
- end.to raise_error(RuntimeError,
98
- "Drop Already exists!")
99
+ end.to raise_error(
100
+ Lutaml::Model::LiquidDropAlreadyRegisteredError,
101
+ "Liquid drop class 'Drop' is already registered.",
102
+ )
99
103
  end
100
104
  end
101
105
  end
@@ -254,14 +258,26 @@ RSpec.describe Lutaml::Model::Liquefiable do
254
258
  end
255
259
 
256
260
  it "renders" do
261
+ # Probe which partial tag the installed Liquid supports (render ≥5, include ≤4)
262
+ partial_tag = begin
263
+ Liquid::Template.parse("{% render 'probe' %}")
264
+ "render"
265
+ rescue Liquid::SyntaxError
266
+ "include"
267
+ end
268
+
257
269
  template = Liquid::Template.new
258
270
  file_system = Liquid::LocalFileSystem.new(liquid_template_dir)
259
271
  template.registers[:file_system] = file_system
260
- template.parse(file_system.read_template_file("ceramics"))
272
+ parent_template = <<~LIQUID
273
+ {% for ceramic in ceramic_collection.ceramics %}
274
+ {% #{partial_tag} 'ceramic' ceramic: ceramic %}
275
+ {%- endfor %}
276
+ LIQUID
277
+ template.parse(parent_template)
261
278
 
262
279
  ceramic_collection = LiquefiableSpec::CeramicCollection.from_yaml(yaml)
263
280
  output = template.render("ceramic_collection" => ceramic_collection)
264
- # puts output
265
281
 
266
282
  expected_output = <<~OUTPUT
267
283
  * Name: "Celadon Bowl"