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.
- checksums.yaml +4 -4
- data/.github/workflows/dependent-tests.yml +3 -1
- data/.rubocop.yml +18 -0
- data/.rubocop_todo.yml +16 -22
- data/Gemfile +2 -0
- data/README.adoc +327 -3
- data/docs/_guides/document-validation.adoc +303 -0
- data/docs/_guides/index.adoc +19 -0
- data/docs/_guides/jsonld-serialization.adoc +217 -0
- data/docs/_guides/rdf-serialization.adoc +344 -0
- data/docs/_guides/turtle-serialization.adoc +224 -0
- data/docs/_guides/xml-mapping.adoc +9 -1
- data/docs/_guides/xml_mappings/07_best_practices.adoc +36 -0
- data/docs/_guides/xml_mappings/08_troubleshooting.adoc +89 -0
- data/docs/_pages/serialization_adapters.adoc +31 -0
- data/docs/_references/index.adoc +1 -0
- data/docs/_references/rdf-namespaces.adoc +243 -0
- data/docs/_tutorials/lutaml-xml-architecture.adoc +6 -1
- data/docs/index.adoc +3 -2
- data/lib/lutaml/jsonld/adapter.rb +23 -0
- data/lib/lutaml/jsonld/context.rb +69 -0
- data/lib/lutaml/jsonld/term_definition.rb +39 -0
- data/lib/lutaml/jsonld/transform.rb +174 -0
- data/lib/lutaml/jsonld.rb +23 -0
- data/lib/lutaml/model/attribute.rb +19 -1
- data/lib/lutaml/model/error/liquid_drop_already_registered_error.rb +11 -0
- data/lib/lutaml/model/error/ordered_content_mapping_error.rb +17 -0
- data/lib/lutaml/model/format_registry.rb +10 -1
- data/lib/lutaml/model/global_context.rb +1 -0
- data/lib/lutaml/model/liquefiable.rb +12 -15
- data/lib/lutaml/model/mapping/mapping_rule.rb +10 -2
- data/lib/lutaml/model/mapping_hash.rb +1 -1
- data/lib/lutaml/model/serialize/format_conversion.rb +17 -1
- data/lib/lutaml/model/services/transformer.rb +67 -32
- data/lib/lutaml/model/transform.rb +41 -4
- data/lib/lutaml/model/uninitialized_class.rb +11 -5
- data/lib/lutaml/model/validation/concerns/has_issues.rb +27 -0
- data/lib/lutaml/model/validation/context.rb +36 -0
- data/lib/lutaml/model/validation/issue.rb +62 -0
- data/lib/lutaml/model/validation/layer_result.rb +34 -0
- data/lib/lutaml/model/validation/profile.rb +66 -0
- data/lib/lutaml/model/validation/registry.rb +60 -0
- data/lib/lutaml/model/validation/remediation.rb +33 -0
- data/lib/lutaml/model/validation/remediation_result.rb +20 -0
- data/lib/lutaml/model/validation/report.rb +39 -0
- data/lib/lutaml/model/validation/rule.rb +59 -0
- data/lib/lutaml/model/validation.rb +2 -1
- data/lib/lutaml/model/validation_framework.rb +77 -0
- data/lib/lutaml/model/version.rb +1 -1
- data/lib/lutaml/model.rb +10 -0
- data/lib/lutaml/rdf/error.rb +7 -0
- data/lib/lutaml/rdf/iri.rb +44 -0
- data/lib/lutaml/rdf/language_tagged.rb +11 -0
- data/lib/lutaml/rdf/literal.rb +62 -0
- data/lib/lutaml/rdf/mapping.rb +71 -0
- data/lib/lutaml/rdf/mapping_rule.rb +35 -0
- data/lib/lutaml/rdf/member_rule.rb +13 -0
- data/lib/lutaml/rdf/namespace.rb +58 -0
- data/lib/lutaml/rdf/namespace_set.rb +69 -0
- data/lib/lutaml/rdf/namespaces/dcterms_namespace.rb +12 -0
- data/lib/lutaml/rdf/namespaces/owl_namespace.rb +12 -0
- data/lib/lutaml/rdf/namespaces/rdf_namespace.rb +14 -0
- data/lib/lutaml/rdf/namespaces/rdfs_namespace.rb +12 -0
- data/lib/lutaml/rdf/namespaces/skos_namespace.rb +12 -0
- data/lib/lutaml/rdf/namespaces/xsd_namespace.rb +12 -0
- data/lib/lutaml/rdf/namespaces.rb +14 -0
- data/lib/lutaml/rdf/transform.rb +36 -0
- data/lib/lutaml/rdf.rb +19 -0
- data/lib/lutaml/turtle/adapter.rb +35 -0
- data/lib/lutaml/turtle/mapping.rb +7 -0
- data/lib/lutaml/turtle/transform.rb +158 -0
- data/lib/lutaml/turtle.rb +22 -0
- data/lib/lutaml/xml/adapter/nokogiri_adapter.rb +9 -2
- data/lib/lutaml/xml/adapter/oga_adapter.rb +11 -3
- data/lib/lutaml/xml/adapter/ox_adapter.rb +5 -2
- data/lib/lutaml/xml/adapter/rexml_adapter.rb +10 -3
- data/lib/lutaml/xml/adapter_element.rb +26 -2
- data/lib/lutaml/xml/data_model.rb +14 -0
- data/lib/lutaml/xml/document.rb +3 -0
- data/lib/lutaml/xml/element.rb +8 -2
- data/lib/lutaml/xml/mapping.rb +9 -0
- data/lib/lutaml/xml/model_transform.rb +42 -0
- data/lib/lutaml/xml/schema/xsd/base.rb +4 -1
- data/lib/lutaml/xml/serialization/instance_methods.rb +3 -1
- data/lib/lutaml/xml/transformation/ordered_applier.rb +46 -2
- data/lib/lutaml/xml/transformation.rb +40 -1
- data/lib/lutaml/xml/xml_element.rb +8 -7
- data/lutaml-model.gemspec +1 -1
- data/spec/lutaml/integration/edge_cases_spec.rb +109 -0
- data/spec/lutaml/integration/multi_format_spec.rb +106 -0
- data/spec/lutaml/integration/round_trip_spec.rb +170 -0
- data/spec/lutaml/jsonld/adapter_spec.rb +46 -0
- data/spec/lutaml/jsonld/context_spec.rb +114 -0
- data/spec/lutaml/jsonld/term_definition_spec.rb +55 -0
- data/spec/lutaml/jsonld/transform_spec.rb +211 -0
- data/spec/lutaml/model/attribute_default_cache_spec.rb +58 -0
- data/spec/lutaml/model/liquefiable_spec.rb +22 -6
- data/spec/lutaml/model/liquid_compatibility_spec.rb +442 -0
- data/spec/lutaml/model/ordered_content_spec.rb +5 -5
- data/spec/lutaml/model/services/transformer_spec.rb +43 -0
- data/spec/lutaml/model/transform_cache_spec.rb +62 -0
- data/spec/lutaml/model/transform_dynamic_attributes_spec.rb +41 -0
- data/spec/lutaml/model/uninitialized_class_deep_dup_spec.rb +39 -0
- data/spec/lutaml/model/uninitialized_class_spec.rb +14 -2
- data/spec/lutaml/model/validation/concerns/has_issues_spec.rb +76 -0
- data/spec/lutaml/model/validation/context_spec.rb +60 -0
- data/spec/lutaml/model/validation/issue_spec.rb +77 -0
- data/spec/lutaml/model/validation/layer_result_spec.rb +66 -0
- data/spec/lutaml/model/validation/profile_spec.rb +134 -0
- data/spec/lutaml/model/validation/registry_spec.rb +94 -0
- data/spec/lutaml/model/validation/remediation_result_spec.rb +23 -0
- data/spec/lutaml/model/validation/remediation_spec.rb +72 -0
- data/spec/lutaml/model/validation/report_spec.rb +58 -0
- data/spec/lutaml/model/validation/rule_spec.rb +134 -0
- data/spec/lutaml/model/validation/uninitialized_class_validate_spec.rb +29 -0
- data/spec/lutaml/model/validation/validation_error_spec.rb +29 -0
- data/spec/lutaml/model/validation/validation_framework_spec.rb +110 -0
- data/spec/lutaml/rdf/graph_serialization_spec.rb +137 -0
- data/spec/lutaml/rdf/iri_spec.rb +73 -0
- data/spec/lutaml/rdf/literal_spec.rb +98 -0
- data/spec/lutaml/rdf/mapping_spec.rb +164 -0
- data/spec/lutaml/rdf/member_rule_spec.rb +17 -0
- data/spec/lutaml/rdf/namespace_set_spec.rb +115 -0
- data/spec/lutaml/rdf/namespace_spec.rb +241 -0
- data/spec/lutaml/rdf/rdf_transform_spec.rb +82 -0
- data/spec/lutaml/turtle/adapter_spec.rb +47 -0
- data/spec/lutaml/turtle/mapping_spec.rb +123 -0
- data/spec/lutaml/turtle/transform_spec.rb +273 -0
- data/spec/lutaml/xml/content_model_validation_spec.rb +157 -0
- data/spec/lutaml/xml/mapping_spec.rb +12 -7
- metadata +95 -7
- data/spec/fixtures/liquid_templates/_ceramics.liquid +0 -3
|
@@ -0,0 +1,442 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
require "liquid"
|
|
5
|
+
require "tmpdir"
|
|
6
|
+
require "fileutils"
|
|
7
|
+
|
|
8
|
+
# These specs verify the Liquid API surface that Lutaml::Model::Liquefiable
|
|
9
|
+
# depends on. They are behavior-based, not version-gated — if these pass,
|
|
10
|
+
# the installed Liquid version provides everything we need.
|
|
11
|
+
#
|
|
12
|
+
# Liquid features exercised here:
|
|
13
|
+
# - Liquid::Drop (base class for generated drop objects)
|
|
14
|
+
# - Liquid::Template.parse / #render (template compilation and execution)
|
|
15
|
+
# - Liquid::LocalFileSystem (partial resolution for {% include %})
|
|
16
|
+
# - to_liquid protocol (automatic drop conversion in render contexts)
|
|
17
|
+
# - Template tags: {{ var }}, {% for %}, {% if %}, {% assign %}
|
|
18
|
+
RSpec.describe "Liquid compatibility for Lutaml::Model::Liquefiable" do
|
|
19
|
+
# ── Core API availability ──────────────────────────────────────────────
|
|
20
|
+
|
|
21
|
+
describe "required Liquid classes and methods" do
|
|
22
|
+
it "provides Liquid::Drop as a base class" do
|
|
23
|
+
expect(defined?(Liquid::Drop)).to eq("constant")
|
|
24
|
+
drop = Class.new(Liquid::Drop).new
|
|
25
|
+
expect(drop).to be_a(Liquid::Drop)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
it "provides Liquid::Template.parse" do
|
|
29
|
+
expect(Liquid::Template).to respond_to(:parse)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
it "provides template#render with a hash of variables" do
|
|
33
|
+
template = Liquid::Template.parse("{{ msg }}")
|
|
34
|
+
expect(template.render("msg" => "hello")).to eq("hello")
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
it "provides Liquid::LocalFileSystem" do
|
|
38
|
+
expect(defined?(Liquid::LocalFileSystem)).to eq("constant")
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
it "supports the to_liquid protocol on strings and integers" do
|
|
42
|
+
expect("hello".to_liquid).to eq("hello")
|
|
43
|
+
expect(42.to_liquid).to eq(42)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# ── Drop generation from Serializable models ───────────────────────────
|
|
48
|
+
|
|
49
|
+
describe "drop generation for Serializable models" do
|
|
50
|
+
let(:model_class) do
|
|
51
|
+
Class.new(Lutaml::Model::Serializable) do
|
|
52
|
+
def self.name
|
|
53
|
+
"Widget"
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
attribute :label, :string
|
|
57
|
+
attribute :count, :integer
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
let(:instance) { model_class.new(label: "Sprocket", count: 3) }
|
|
62
|
+
|
|
63
|
+
it "generates a Drop subclass inheriting from Liquid::Drop" do
|
|
64
|
+
drop = instance.to_liquid
|
|
65
|
+
expect(drop).to be_a(Liquid::Drop)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
it "exposes attributes as drop methods" do
|
|
69
|
+
drop = instance.to_liquid
|
|
70
|
+
expect(drop.label).to eq("Sprocket")
|
|
71
|
+
expect(drop.count).to eq(3)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
it "returns the same Drop class for all instances" do
|
|
75
|
+
drop_class = instance.to_liquid.class
|
|
76
|
+
other = model_class.new(label: "Gear", count: 1)
|
|
77
|
+
expect(other.to_liquid.class).to eq(drop_class)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
it "does not re-register methods on subsequent calls" do
|
|
81
|
+
instance.to_liquid
|
|
82
|
+
expect do
|
|
83
|
+
instance.to_liquid
|
|
84
|
+
end.not_to(change do
|
|
85
|
+
instance.to_liquid.class.instance_methods(false).sort
|
|
86
|
+
end)
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# ── Nested model drops ─────────────────────────────────────────────────
|
|
91
|
+
|
|
92
|
+
describe "nested model composition" do
|
|
93
|
+
let(:inner_class) do
|
|
94
|
+
Class.new(Lutaml::Model::Serializable) do
|
|
95
|
+
def self.name
|
|
96
|
+
"Detail"
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
attribute :color, :string
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
let(:outer_class) do
|
|
104
|
+
inner = inner_class
|
|
105
|
+
|
|
106
|
+
Class.new(Lutaml::Model::Serializable) do
|
|
107
|
+
define_method(:inner_class) { inner }
|
|
108
|
+
|
|
109
|
+
def self.name
|
|
110
|
+
"Container"
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
attribute :title, :string
|
|
114
|
+
attribute :detail, inner
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
let(:instance) do
|
|
119
|
+
outer_class.new(title: "Box", detail: inner_class.new(color: "blue"))
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
it "converts nested models to drops via to_liquid" do
|
|
123
|
+
drop = instance.to_liquid
|
|
124
|
+
expect(drop.detail).to be_a(Liquid::Drop)
|
|
125
|
+
expect(drop.detail.color).to eq("blue")
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
it "resolves nested access in templates" do
|
|
129
|
+
template = Liquid::Template.parse("{{ instance.detail.color }}")
|
|
130
|
+
result = template.render("instance" => instance)
|
|
131
|
+
expect(result).to eq("blue")
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# ── Collections ────────────────────────────────────────────────────────
|
|
136
|
+
|
|
137
|
+
describe "collection handling" do
|
|
138
|
+
let(:item_class) do
|
|
139
|
+
Class.new(Lutaml::Model::Serializable) do
|
|
140
|
+
def self.name
|
|
141
|
+
"Item"
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
attribute :name, :string
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
let(:container_class) do
|
|
149
|
+
item_klass = item_class
|
|
150
|
+
|
|
151
|
+
Class.new(Lutaml::Model::Serializable) do
|
|
152
|
+
define_method(:item_class) { item_klass }
|
|
153
|
+
|
|
154
|
+
def self.name
|
|
155
|
+
"ItemContainer"
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
attribute :items, item_klass, collection: true
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
let(:instance) do
|
|
163
|
+
container_class.new(
|
|
164
|
+
items: [
|
|
165
|
+
item_class.new(name: "Alpha"),
|
|
166
|
+
item_class.new(name: "Beta"),
|
|
167
|
+
],
|
|
168
|
+
)
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
it "converts array of models to array of drops" do
|
|
172
|
+
drops = instance.to_liquid.items
|
|
173
|
+
expect(drops).to all(be_a(Liquid::Drop))
|
|
174
|
+
expect(drops.map(&:name)).to eq(["Alpha", "Beta"])
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
it "iterates collections in {% for %} loops" do
|
|
178
|
+
template = Liquid::Template.parse(<<~LIQUID)
|
|
179
|
+
{% for item in container.items %}{{ item.name }},{% endfor %}
|
|
180
|
+
LIQUID
|
|
181
|
+
result = template.render("container" => instance)
|
|
182
|
+
expect(result.strip).to eq("Alpha,Beta,")
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
# ── Custom liquid mappings ─────────────────────────────────────────────
|
|
187
|
+
|
|
188
|
+
describe "custom liquid mappings via liquid block" do
|
|
189
|
+
let(:model_class) do
|
|
190
|
+
Class.new(Lutaml::Model::Serializable) do
|
|
191
|
+
def self.name
|
|
192
|
+
"MappedWidget"
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
attribute :path, :string
|
|
196
|
+
attribute :source, :string
|
|
197
|
+
|
|
198
|
+
liquid do
|
|
199
|
+
map "full_path", to: :computed_path
|
|
200
|
+
map "summary", to: :formatted_summary
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def computed_path
|
|
204
|
+
"/app/#{path}"
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def formatted_summary
|
|
208
|
+
"#{source} (#{path})"
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
let(:instance) { model_class.new(path: "index.xml", source: "Hello") }
|
|
214
|
+
|
|
215
|
+
it "maps custom keys to instance methods" do
|
|
216
|
+
drop = instance.to_liquid
|
|
217
|
+
expect(drop.full_path).to eq("/app/index.xml")
|
|
218
|
+
expect(drop.summary).to eq("Hello (index.xml)")
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
it "renders custom mappings in templates" do
|
|
222
|
+
template = Liquid::Template.parse("{{ w.full_path }} | {{ w.summary }}")
|
|
223
|
+
result = template.render("w" => instance)
|
|
224
|
+
expect(result).to eq("/app/index.xml | Hello (index.xml)")
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
it "still exposes original attributes alongside custom mappings" do
|
|
228
|
+
drop = instance.to_liquid
|
|
229
|
+
expect(drop.path).to eq("index.xml")
|
|
230
|
+
expect(drop.source).to eq("Hello")
|
|
231
|
+
end
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
# ── Conditional and control-flow templates ─────────────────────────────
|
|
235
|
+
|
|
236
|
+
describe "template control flow with drops" do
|
|
237
|
+
let(:model_class) do
|
|
238
|
+
Class.new(Lutaml::Model::Serializable) do
|
|
239
|
+
def self.name
|
|
240
|
+
"ConditionalModel"
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
attribute :title, :string
|
|
244
|
+
attribute :score, :integer
|
|
245
|
+
end
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
it "handles {% if %} with drop attributes" do
|
|
249
|
+
instance = model_class.new(title: "present", score: 10)
|
|
250
|
+
template = Liquid::Template.parse(<<~LIQUID)
|
|
251
|
+
{% if m.title %}HAS_TITLE{% else %}NO_TITLE{% endif %}
|
|
252
|
+
LIQUID
|
|
253
|
+
expect(template.render("m" => instance).strip).to eq("HAS_TITLE")
|
|
254
|
+
|
|
255
|
+
empty = model_class.new(title: nil, score: 0)
|
|
256
|
+
expect(template.render("m" => empty).strip).to eq("NO_TITLE")
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
it "handles {% assign %} and expressions with drop values" do
|
|
260
|
+
instance = model_class.new(title: "demo", score: 42)
|
|
261
|
+
template = Liquid::Template.parse(<<~LIQUID)
|
|
262
|
+
{% assign threshold = 10 %}{% if m.score > threshold %}HIGH{% else %}LOW{% endif %}
|
|
263
|
+
LIQUID
|
|
264
|
+
expect(template.render("m" => instance).strip).to eq("HIGH")
|
|
265
|
+
end
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
# ── Inheritance ────────────────────────────────────────────────────────
|
|
269
|
+
|
|
270
|
+
describe "drop inheritance across class hierarchy" do
|
|
271
|
+
let(:parent_class) do
|
|
272
|
+
Class.new(Lutaml::Model::Serializable) do
|
|
273
|
+
def self.name
|
|
274
|
+
"Parent"
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
attribute :name, :string
|
|
278
|
+
end
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
let(:child_class) do
|
|
282
|
+
Class.new(parent_class) do
|
|
283
|
+
def self.name
|
|
284
|
+
"Child"
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
attribute :age, :integer
|
|
288
|
+
end
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
it "inherits attribute drops from the parent" do
|
|
292
|
+
child = child_class.new(name: "Alice", age: 5)
|
|
293
|
+
drop = child.to_liquid
|
|
294
|
+
expect(drop.name).to eq("Alice")
|
|
295
|
+
expect(drop.age).to eq(5)
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
it "parent and child have distinct drop classes" do
|
|
299
|
+
parent = parent_class.new(name: "Bob")
|
|
300
|
+
child = child_class.new(name: "Alice", age: 5)
|
|
301
|
+
expect(parent.to_liquid.class).not_to eq(child.to_liquid.class)
|
|
302
|
+
end
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
# ── Non-Serializable Liquefiable ───────────────────────────────────────
|
|
306
|
+
|
|
307
|
+
describe "Liquefiable without Serializable" do
|
|
308
|
+
let(:plain_class) do
|
|
309
|
+
Class.new do
|
|
310
|
+
include Lutaml::Model::Liquefiable
|
|
311
|
+
|
|
312
|
+
def self.name
|
|
313
|
+
"PlainObject"
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
def initialize(label)
|
|
317
|
+
@label = label
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
def label
|
|
321
|
+
@label
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
liquid do
|
|
325
|
+
map "label", to: :label
|
|
326
|
+
end
|
|
327
|
+
end
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
it "creates a drop class inheriting from Liquid::Drop" do
|
|
331
|
+
instance = plain_class.new("test")
|
|
332
|
+
expect(instance.to_liquid).to be_a(Liquid::Drop)
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
it "exposes mapped methods on the drop" do
|
|
336
|
+
instance = plain_class.new("hello")
|
|
337
|
+
expect(instance.to_liquid.label).to eq("hello")
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
it "renders in templates" do
|
|
341
|
+
instance = plain_class.new("world")
|
|
342
|
+
template = Liquid::Template.parse("{{ obj.label }}")
|
|
343
|
+
expect(template.render("obj" => instance)).to eq("world")
|
|
344
|
+
end
|
|
345
|
+
end
|
|
346
|
+
|
|
347
|
+
# ── Error handling ─────────────────────────────────────────────────────
|
|
348
|
+
|
|
349
|
+
describe "error handling" do
|
|
350
|
+
it "raises LiquidNotEnabledError when Liquid is not loaded" do
|
|
351
|
+
allow(Object).to receive(:const_defined?).with(:Liquid).and_return(false)
|
|
352
|
+
klass = Class.new do
|
|
353
|
+
include Lutaml::Model::Liquefiable
|
|
354
|
+
end
|
|
355
|
+
instance = klass.new
|
|
356
|
+
expect { instance.to_liquid }.to raise_error(
|
|
357
|
+
Lutaml::Model::LiquidNotEnabledError,
|
|
358
|
+
)
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
it "raises LiquidDropAlreadyRegisteredError on duplicate registration" do
|
|
362
|
+
allow(Object).to receive(:const_defined?).with(:Liquid).and_call_original
|
|
363
|
+
klass = Class.new(Lutaml::Model::Serializable) do
|
|
364
|
+
def self.name
|
|
365
|
+
"DupTestModel"
|
|
366
|
+
end
|
|
367
|
+
|
|
368
|
+
attribute :x, :string
|
|
369
|
+
end
|
|
370
|
+
# Drop already registered during class definition
|
|
371
|
+
expect do
|
|
372
|
+
klass.register_liquid_drop_class
|
|
373
|
+
end.to raise_error(Lutaml::Model::LiquidDropAlreadyRegisteredError)
|
|
374
|
+
end
|
|
375
|
+
|
|
376
|
+
it "raises LiquidClassNotFoundError for missing custom drop class" do
|
|
377
|
+
klass = Class.new(Lutaml::Model::Serializable) do
|
|
378
|
+
def self.name
|
|
379
|
+
"MissingDropModel"
|
|
380
|
+
end
|
|
381
|
+
|
|
382
|
+
attribute :x, :string
|
|
383
|
+
|
|
384
|
+
liquid_class "NonexistentDrop"
|
|
385
|
+
end
|
|
386
|
+
instance = klass.new(x: "test")
|
|
387
|
+
expect { instance.to_liquid }.to raise_error(
|
|
388
|
+
Lutaml::Model::LiquidClassNotFoundError,
|
|
389
|
+
/NonexistentDrop/,
|
|
390
|
+
)
|
|
391
|
+
end
|
|
392
|
+
|
|
393
|
+
it "raises NoAttributesDefinedLiquidError for attribute-less Serializable" do
|
|
394
|
+
klass = Class.new(Lutaml::Model::Serializable) do
|
|
395
|
+
def self.name
|
|
396
|
+
"NoAttrsModel"
|
|
397
|
+
end
|
|
398
|
+
end
|
|
399
|
+
instance = klass.new
|
|
400
|
+
expect { instance.to_liquid }.to raise_error(
|
|
401
|
+
Lutaml::Model::NoAttributesDefinedLiquidError,
|
|
402
|
+
/NoAttrsModel/,
|
|
403
|
+
)
|
|
404
|
+
end
|
|
405
|
+
end
|
|
406
|
+
|
|
407
|
+
# ── LocalFileSystem (partials via {% include %}) ───────────────────────
|
|
408
|
+
|
|
409
|
+
describe "partial rendering with Liquid::LocalFileSystem" do
|
|
410
|
+
let(:template_dir) do
|
|
411
|
+
Dir.mktmpdir
|
|
412
|
+
end
|
|
413
|
+
|
|
414
|
+
after do
|
|
415
|
+
FileUtils.remove_entry(template_dir)
|
|
416
|
+
end
|
|
417
|
+
|
|
418
|
+
it "resolves {% include %} partials via LocalFileSystem" do
|
|
419
|
+
File.write(File.join(template_dir, "_item.liquid"), <<~LIQUID)
|
|
420
|
+
[{{ item.name }}]
|
|
421
|
+
LIQUID
|
|
422
|
+
|
|
423
|
+
item_class = Class.new(Lutaml::Model::Serializable) do
|
|
424
|
+
def self.name
|
|
425
|
+
"FsItem"
|
|
426
|
+
end
|
|
427
|
+
|
|
428
|
+
attribute :name, :string
|
|
429
|
+
end
|
|
430
|
+
|
|
431
|
+
item = item_class.new(name: "Alpha")
|
|
432
|
+
template = Liquid::Template.new
|
|
433
|
+
template.registers[:file_system] = Liquid::LocalFileSystem.new(template_dir)
|
|
434
|
+
template.parse(<<~LIQUID)
|
|
435
|
+
{% include 'item' item: item %}
|
|
436
|
+
LIQUID
|
|
437
|
+
|
|
438
|
+
result = template.render("item" => item)
|
|
439
|
+
expect(result.strip).to eq("[Alpha]")
|
|
440
|
+
end
|
|
441
|
+
end
|
|
442
|
+
end
|
|
@@ -15,11 +15,11 @@ module OrderedContentSpec
|
|
|
15
15
|
attribute :bold, :string, collection: true
|
|
16
16
|
attribute :italic, :string, collection: true
|
|
17
17
|
attribute :underline, :string
|
|
18
|
-
attribute :content, :string
|
|
18
|
+
attribute :content, :string, collection: true
|
|
19
19
|
|
|
20
20
|
xml do
|
|
21
21
|
element "RootOrderedContent"
|
|
22
|
-
|
|
22
|
+
mixed_content
|
|
23
23
|
|
|
24
24
|
map_attribute :id, to: :id
|
|
25
25
|
map_element :bold, to: :bold
|
|
@@ -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.to_s).to match(/The Earth's Moon rings like a/)
|
|
106
|
-
expect(obj.content.
|
|
105
|
+
expect(obj.content.first.to_s).to match(/The Earth's Moon rings like a/)
|
|
106
|
+
expect(obj.content.join).to match(/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.to_s).to match(/The Earth's Moon rings like a/)
|
|
115
|
+
expect(round_trip.content.first.to_s).to match(/The Earth's Moon rings like a/)
|
|
116
116
|
end
|
|
117
117
|
end
|
|
118
118
|
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
|
|
5
|
+
RSpec.describe Lutaml::Model::Transformer do
|
|
6
|
+
describe "open/closed principle" do
|
|
7
|
+
it "defaults to import direction" do
|
|
8
|
+
transformer = described_class.new(nil, nil)
|
|
9
|
+
expect(transformer.export_direction?).to be false
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
it "ImportTransformer returns false for export_direction?" do
|
|
13
|
+
transformer = Lutaml::Model::ImportTransformer.new(nil, nil)
|
|
14
|
+
expect(transformer.export_direction?).to be false
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
it "ExportTransformer returns true for export_direction?" do
|
|
18
|
+
transformer = Lutaml::Model::ExportTransformer.new(nil, nil)
|
|
19
|
+
expect(transformer.export_direction?).to be true
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
it "allows custom subclass to define direction" do
|
|
23
|
+
custom = Class.new(described_class) do
|
|
24
|
+
def export_direction?
|
|
25
|
+
true
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
expect(custom.new(nil, nil).export_direction?).to be true
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
describe "class-level .call" do
|
|
33
|
+
it "ImportTransformer.call returns value unchanged without transforms" do
|
|
34
|
+
result = Lutaml::Model::ImportTransformer.call("hello", nil, nil)
|
|
35
|
+
expect(result).to eq("hello")
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
it "ExportTransformer.call returns value unchanged without transforms" do
|
|
39
|
+
result = Lutaml::Model::ExportTransformer.call("hello", nil, nil)
|
|
40
|
+
expect(result).to eq("hello")
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
|
|
5
|
+
RSpec.describe "Transform caching" do
|
|
6
|
+
after { Lutaml::Model::Transform.clear_cache! }
|
|
7
|
+
|
|
8
|
+
describe ".cached_transform" do
|
|
9
|
+
it "returns same instance for same context and register" do
|
|
10
|
+
klass = Class.new(Lutaml::Model::Serializable) do
|
|
11
|
+
attribute :name, :string
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
t1 = Lutaml::Model::Transform.cached_transform(klass, :default)
|
|
15
|
+
t2 = Lutaml::Model::Transform.cached_transform(klass, :default)
|
|
16
|
+
expect(t1).to equal(t2)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
it "returns different instances for different contexts" do
|
|
20
|
+
klass_a = Class.new(Lutaml::Model::Serializable) do
|
|
21
|
+
attribute :name, :string
|
|
22
|
+
end
|
|
23
|
+
klass_b = Class.new(Lutaml::Model::Serializable) do
|
|
24
|
+
attribute :title, :string
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
t1 = Lutaml::Model::Transform.cached_transform(klass_a, :default)
|
|
28
|
+
t2 = Lutaml::Model::Transform.cached_transform(klass_b, :default)
|
|
29
|
+
expect(t1).not_to equal(t2)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
describe ".clear_cache!" do
|
|
34
|
+
it "clears the cache" do
|
|
35
|
+
klass = Class.new(Lutaml::Model::Serializable) do
|
|
36
|
+
attribute :name, :string
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
Lutaml::Model::Transform.cached_transform(klass, :default)
|
|
40
|
+
expect(Lutaml::Model::Transform.cache_size).to be > 0
|
|
41
|
+
|
|
42
|
+
Lutaml::Model::Transform.clear_cache!
|
|
43
|
+
expect(Lutaml::Model::Transform.cache_size).to eq(0)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
describe "cache eviction" do
|
|
48
|
+
it "evicts entries when exceeding MAX_CACHE_SIZE" do
|
|
49
|
+
stub_const("Lutaml::Model::Transform::MAX_CACHE_SIZE", 4)
|
|
50
|
+
|
|
51
|
+
classes = Array.new(6) do |i|
|
|
52
|
+
Class.new(Lutaml::Model::Serializable) do
|
|
53
|
+
attribute :"attr_#{i}", :string
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
classes.each { |k| Lutaml::Model::Transform.cached_transform(k, :default) }
|
|
58
|
+
|
|
59
|
+
expect(Lutaml::Model::Transform.cache_size).to be <= 4
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
|
|
5
|
+
RSpec.describe "Transform with dynamically added attributes" do
|
|
6
|
+
before do
|
|
7
|
+
Lutaml::Model::GlobalContext.clear_caches
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
it "picks up attributes added after initial class definition" do
|
|
11
|
+
base_class = Class.new(Lutaml::Model::Serializable) do
|
|
12
|
+
attribute :name, :string
|
|
13
|
+
|
|
14
|
+
xml do
|
|
15
|
+
root "test"
|
|
16
|
+
map_element "name", to: :name
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def self.name
|
|
20
|
+
"DynamicAttributeTestClass"
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Parse once to populate Transform cache
|
|
25
|
+
base_class.from_xml("<test><name>initial</name></test>")
|
|
26
|
+
|
|
27
|
+
# Dynamically add a new attribute and mapping (like xmi EaRoot.load_extension)
|
|
28
|
+
base_class.class_eval do
|
|
29
|
+
attribute :extra, :string
|
|
30
|
+
|
|
31
|
+
xml do
|
|
32
|
+
map_element "extra", to: :extra
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# The Transform must see the newly added attribute
|
|
37
|
+
result = base_class.from_xml("<test><name>hello</name><extra>world</extra></test>")
|
|
38
|
+
expect(result.name).to eq("hello")
|
|
39
|
+
expect(result.extra).to eq("world")
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
|
|
5
|
+
RSpec.describe "UninitializedClass deep_dup compatibility" do
|
|
6
|
+
let(:uninitialized) { Lutaml::Model::UninitializedClass.instance }
|
|
7
|
+
|
|
8
|
+
# Replicates the rng gem's ExternalRefResolver#deep_dup pattern
|
|
9
|
+
def deep_dup(obj)
|
|
10
|
+
case obj
|
|
11
|
+
when Array
|
|
12
|
+
obj.map { |o| deep_dup(o) }
|
|
13
|
+
when Hash
|
|
14
|
+
obj.each_with_object({}) { |(k, v), h| h[deep_dup(k)] = deep_dup(v) }
|
|
15
|
+
when NilClass, Symbol, Numeric, TrueClass, FalseClass
|
|
16
|
+
obj
|
|
17
|
+
else
|
|
18
|
+
obj.dup
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
it "does not raise TypeError when deep_dup encounters UninitializedClass in a hash value" do
|
|
23
|
+
data = { "key" => "value", "missing" => uninitialized }
|
|
24
|
+
result = deep_dup(data)
|
|
25
|
+
expect(result["missing"]).to equal(uninitialized)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
it "does not raise TypeError when deep_dup encounters UninitializedClass in an array" do
|
|
29
|
+
data = ["hello", uninitialized, "world"]
|
|
30
|
+
result = deep_dup(data)
|
|
31
|
+
expect(result[1]).to equal(uninitialized)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
it "does not raise TypeError when deep_dup encounters UninitializedClass as hash key" do
|
|
35
|
+
data = { uninitialized => "value" }
|
|
36
|
+
result = deep_dup(data)
|
|
37
|
+
expect(result.keys.first).to equal(uninitialized)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -77,8 +77,8 @@ RSpec.describe Lutaml::Model::UninitializedClass do
|
|
|
77
77
|
end
|
|
78
78
|
|
|
79
79
|
context "when method doesn't end with '?'" do
|
|
80
|
-
it "
|
|
81
|
-
expect
|
|
80
|
+
it "returns nil" do
|
|
81
|
+
expect(uninitialized.unknown_method).to be_nil
|
|
82
82
|
end
|
|
83
83
|
end
|
|
84
84
|
end
|
|
@@ -93,4 +93,16 @@ RSpec.describe Lutaml::Model::UninitializedClass do
|
|
|
93
93
|
expect(uninitialized.respond_to?(:unknown_method)).to be false
|
|
94
94
|
end
|
|
95
95
|
end
|
|
96
|
+
|
|
97
|
+
describe "#dup" do
|
|
98
|
+
it "returns self" do
|
|
99
|
+
expect(uninitialized.dup).to equal(uninitialized)
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
describe "#clone" do
|
|
104
|
+
it "returns self" do
|
|
105
|
+
expect(uninitialized.clone).to equal(uninitialized)
|
|
106
|
+
end
|
|
107
|
+
end
|
|
96
108
|
end
|