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,106 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+ require "lutaml/turtle"
5
+ require "lutaml/jsonld"
6
+
7
+ RSpec.describe "Multi-format model" do
8
+ before do
9
+ stub_const("TestSkosNs", Class.new(Lutaml::Rdf::Namespace) do
10
+ uri "http://www.w3.org/2004/02/skos/core#"
11
+ prefix "skos"
12
+ end)
13
+
14
+ stub_const("MultiFormatModel", Class.new(Lutaml::Model::Serializable) do
15
+ attribute :name, :string
16
+ attribute :description, :string
17
+ attribute :code, :string
18
+
19
+ json do
20
+ map "name", to: :name
21
+ map "description", to: :description
22
+ map "code", to: :code
23
+ end
24
+
25
+ rdf do
26
+ namespace TestSkosNs
27
+
28
+ subject { |m| "http://example.org/concept/#{m.code}" } # rubocop:disable RSpec/NamedSubject
29
+
30
+ type "skos:Concept"
31
+
32
+ predicate :prefLabel, namespace: TestSkosNs, to: :name
33
+ predicate :definition, namespace: TestSkosNs, to: :description
34
+ predicate :notation, namespace: TestSkosNs, to: :code
35
+ end
36
+ end)
37
+ end
38
+
39
+ let(:instance) do
40
+ MultiFormatModel.new(name: "test", description: "desc", code: "42")
41
+ end
42
+
43
+ describe "JSON format" do
44
+ it "serializes without @context" do
45
+ json = instance.to_json
46
+ parsed = JSON.parse(json)
47
+ expect(parsed).not_to have_key("@context")
48
+ expect(parsed["name"]).to eq("test")
49
+ expect(parsed["code"]).to eq("42")
50
+ end
51
+
52
+ it "round-trips" do
53
+ restored = MultiFormatModel.from_json(instance.to_json)
54
+ expect(restored.name).to eq("test")
55
+ expect(restored.code).to eq("42")
56
+ end
57
+ end
58
+
59
+ describe "JSON-LD format" do
60
+ it "serializes with @type and @id" do
61
+ jsonld = instance.to_jsonld
62
+ parsed = JSON.parse(jsonld)
63
+ expect(parsed["@type"]).to eq("skos:Concept")
64
+ expect(parsed["@id"]).to eq("http://example.org/concept/42")
65
+ expect(parsed["prefLabel"]).to eq("test")
66
+ end
67
+
68
+ it "round-trips" do
69
+ restored = MultiFormatModel.from_jsonld(instance.to_jsonld)
70
+ expect(restored.name).to eq("test")
71
+ expect(restored.code).to eq("42")
72
+ end
73
+ end
74
+
75
+ describe "Turtle format" do
76
+ it "serializes with prefixes and type" do
77
+ turtle = instance.to_turtle
78
+ expect(turtle).to include("@prefix skos:")
79
+ expect(turtle).to include("a skos:Concept")
80
+ expect(turtle).to include("<http://example.org/concept/42>")
81
+ expect(turtle).to include("skos:prefLabel \"test\"")
82
+ end
83
+
84
+ it "round-trips" do
85
+ restored = MultiFormatModel.from_turtle(instance.to_turtle)
86
+ expect(restored.name).to eq("test")
87
+ expect(restored.code).to eq("42")
88
+ end
89
+ end
90
+
91
+ describe "cross-format independence" do
92
+ it "JSON serialization does not affect JSON-LD" do
93
+ json_parsed = JSON.parse(instance.to_json)
94
+ jsonld_parsed = JSON.parse(instance.to_jsonld)
95
+ expect(json_parsed).not_to have_key("@type")
96
+ expect(jsonld_parsed).to have_key("@type")
97
+ end
98
+
99
+ it "JSON-LD serialization does not affect Turtle" do
100
+ instance.to_jsonld
101
+ turtle = instance.to_turtle
102
+ expect(turtle).not_to include("@context")
103
+ expect(turtle).to include("@prefix")
104
+ end
105
+ end
106
+ end
@@ -0,0 +1,170 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+ require "lutaml/turtle"
5
+ require "lutaml/jsonld"
6
+
7
+ RSpec.describe "Round-trip fidelity" do
8
+ before do
9
+ stub_const("TestSkosNs", Class.new(Lutaml::Rdf::Namespace) do
10
+ uri "http://www.w3.org/2004/02/skos/core#"
11
+ prefix "skos"
12
+ end)
13
+
14
+ stub_const("TestExNs", Class.new(Lutaml::Rdf::Namespace) do
15
+ uri "http://example.org/"
16
+ prefix "ex"
17
+ end)
18
+ end
19
+
20
+ describe "Turtle round-trip" do
21
+ before do
22
+ stub_const("RtTurtleModel", Class.new(Lutaml::Model::Serializable) do
23
+ attribute :label, :string
24
+ attribute :note, :string
25
+ attribute :code, :integer
26
+
27
+ turtle do
28
+ namespace TestSkosNs
29
+
30
+ subject { |m| "http://example.org/item/#{m.code}" } # rubocop:disable RSpec/NamedSubject
31
+
32
+ type "skos:Concept"
33
+ predicate :prefLabel,
34
+ namespace: TestSkosNs,
35
+ to: :label
36
+ predicate :note,
37
+ namespace: TestSkosNs,
38
+ to: :note
39
+ predicate :notation,
40
+ namespace: TestSkosNs,
41
+ to: :code
42
+ end
43
+ end)
44
+ end
45
+
46
+ it "preserves string and integer attributes" do
47
+ original = RtTurtleModel.new(label: "hello", note: "world", code: 99)
48
+ turtle = original.to_turtle
49
+ restored = RtTurtleModel.from_turtle(turtle)
50
+ expect(restored.label).to eq("hello")
51
+ expect(restored.note).to eq("world")
52
+ expect(restored.code).to eq(99)
53
+ end
54
+
55
+ it "handles special characters in string values" do
56
+ original = RtTurtleModel.new(label: 'say "hi"', note: "line1\nline2",
57
+ code: 1)
58
+ turtle = original.to_turtle
59
+ restored = RtTurtleModel.from_turtle(turtle)
60
+ expect(restored.label).to eq('say "hi"')
61
+ expect(restored.note).to eq("line1\nline2")
62
+ end
63
+
64
+ it "handles nil optional attributes" do
65
+ original = RtTurtleModel.new(label: "test", note: nil, code: 1)
66
+ turtle = original.to_turtle
67
+ restored = RtTurtleModel.from_turtle(turtle)
68
+ expect(restored.label).to eq("test")
69
+ expect(restored.note).to be_nil
70
+ expect(restored.code).to eq(1)
71
+ end
72
+ end
73
+
74
+ describe "JSON-LD round-trip" do
75
+ before do
76
+ stub_const("RtJsonLdModel", Class.new(Lutaml::Model::Serializable) do
77
+ attribute :title, :string
78
+ attribute :body, :string
79
+ attribute :priority, :integer
80
+
81
+ rdf do
82
+ namespace TestExNs
83
+
84
+ subject { |m| "http://example.org/articles/#{m.priority}" } # rubocop:disable RSpec/NamedSubject
85
+
86
+ type "http://example.org/Article"
87
+
88
+ predicate :title, namespace: TestExNs, to: :title
89
+ predicate :body, namespace: TestExNs, to: :body
90
+ predicate :priority, namespace: TestExNs, to: :priority
91
+ end
92
+ end)
93
+ end
94
+
95
+ it "preserves all attributes through serialize → deserialize" do
96
+ original = RtJsonLdModel.new(title: "Test", body: "Content", priority: 5)
97
+ json = original.to_jsonld
98
+ restored = RtJsonLdModel.from_jsonld(json)
99
+ expect(restored.title).to eq("Test")
100
+ expect(restored.body).to eq("Content")
101
+ expect(restored.priority).to eq(5)
102
+ end
103
+
104
+ it "preserves @context structure across round-trip" do
105
+ original = RtJsonLdModel.new(title: "Test", body: "Content", priority: 1)
106
+ json1 = original.to_jsonld
107
+ restored = RtJsonLdModel.from_jsonld(json1)
108
+ json2 = restored.to_jsonld
109
+
110
+ ctx1 = JSON.parse(json1)["@context"]
111
+ ctx2 = JSON.parse(json2)["@context"]
112
+ expect(ctx1).to eq(ctx2)
113
+ end
114
+
115
+ it "handles nil optional attributes" do
116
+ original = RtJsonLdModel.new(title: "Test", body: nil, priority: 1)
117
+ json = original.to_jsonld
118
+ restored = RtJsonLdModel.from_jsonld(json)
119
+ expect(restored.title).to eq("Test")
120
+ expect(restored.body).to be_nil
121
+ end
122
+ end
123
+
124
+ describe "Error handling" do
125
+ it "Turtle raises MissingSubjectError without subject" do
126
+ stub_const("NoSubjModel", Class.new(Lutaml::Model::Serializable) do
127
+ attribute :name, :string
128
+
129
+ turtle do
130
+ namespace TestSkosNs
131
+ type "skos:Concept"
132
+ predicate :prefLabel,
133
+ namespace: TestSkosNs,
134
+ to: :name
135
+ end
136
+ end)
137
+
138
+ expect { NoSubjModel.new(name: "test").to_turtle }
139
+ .to raise_error(Lutaml::Turtle::MissingSubjectError, /subject/)
140
+ end
141
+
142
+ it "JSON-LD handles invalid JSON gracefully" do
143
+ stub_const("SimpleJsonLdModel", Class.new(Lutaml::Model::Serializable) do
144
+ attribute :name, :string
145
+
146
+ rdf do
147
+ namespace TestExNs
148
+ predicate :name, namespace: TestExNs, to: :name
149
+ end
150
+ end)
151
+
152
+ expect { SimpleJsonLdModel.from_jsonld("not valid json!!!") }
153
+ .to raise_error(Lutaml::Model::InvalidFormatError)
154
+ end
155
+
156
+ it "Rdf::MappingRule validates namespace type" do
157
+ mapping = Lutaml::Rdf::Mapping.new
158
+ expect do
159
+ mapping.predicate(:foo, namespace: String, to: :bar)
160
+ end.to raise_error(ArgumentError, /Rdf::Namespace/)
161
+ end
162
+
163
+ it "Rdf::MappingRule requires :to parameter" do
164
+ mapping = Lutaml::Rdf::Mapping.new
165
+ expect do
166
+ mapping.predicate(:foo, namespace: TestSkosNs, to: nil)
167
+ end.to raise_error(ArgumentError, /required/)
168
+ end
169
+ end
170
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+ require "lutaml/jsonld"
5
+
6
+ RSpec.describe Lutaml::JsonLd::Adapter do
7
+ let(:jsonld_hash) do
8
+ {
9
+ "@context" => { "name" => "http://example.org/name" },
10
+ "@type" => "Thing",
11
+ "name" => "test",
12
+ }
13
+ end
14
+
15
+ describe ".parse" do
16
+ it "parses valid JSON-LD string to hash" do
17
+ json = JSON.generate(jsonld_hash)
18
+ result = described_class.parse(json)
19
+ expect(result).to eq(jsonld_hash.transform_keys(&:to_s))
20
+ end
21
+ end
22
+
23
+ describe "#to_jsonld" do
24
+ it "generates JSON-LD string from hash" do
25
+ adapter = described_class.new(jsonld_hash)
26
+ result = adapter.to_jsonld
27
+ parsed = JSON.parse(result)
28
+ expect(parsed["@context"]).to eq({ "name" => "http://example.org/name" })
29
+ end
30
+
31
+ it "supports pretty generation" do
32
+ adapter = described_class.new(jsonld_hash)
33
+ result = adapter.to_jsonld(pretty: true)
34
+ expect(result).to include("\n")
35
+ end
36
+ end
37
+
38
+ it "round-trips parse → generate" do
39
+ json = JSON.generate(jsonld_hash)
40
+ parsed = described_class.parse(json)
41
+ adapter = described_class.new(parsed)
42
+ result = adapter.to_jsonld
43
+ round_tripped = JSON.parse(result)
44
+ expect(round_tripped).to eq(jsonld_hash.transform_keys(&:to_s))
45
+ end
46
+ end
@@ -0,0 +1,114 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+ require "lutaml/jsonld"
5
+
6
+ RSpec.describe Lutaml::JsonLd::Context do
7
+ subject(:ctx) { described_class.new }
8
+
9
+ describe "empty context" do
10
+ it "serializes to empty hash" do
11
+ expect(ctx.to_hash).to eq({})
12
+ end
13
+ end
14
+
15
+ describe "#prefix" do
16
+ it "adds namespace prefix from Rdf::Namespace class" do
17
+ ctx.prefix(Lutaml::Rdf::Namespaces::SkosNamespace)
18
+ expect(ctx.to_hash).to include("skos" => "http://www.w3.org/2004/02/skos/core#")
19
+ end
20
+
21
+ it "adds multiple prefixes" do
22
+ ctx.prefix(Lutaml::Rdf::Namespaces::SkosNamespace)
23
+ ctx.prefix(Lutaml::Rdf::Namespaces::DctermsNamespace)
24
+ hash = ctx.to_hash
25
+ expect(hash).to include("skos" => "http://www.w3.org/2004/02/skos/core#")
26
+ expect(hash).to include("dcterms" => "http://purl.org/dc/terms/")
27
+ end
28
+ end
29
+
30
+ describe "#vocab" do
31
+ it "sets @vocab" do
32
+ ctx.vocab("http://example.org/ns/")
33
+ expect(ctx.to_hash).to include("@vocab" => "http://example.org/ns/")
34
+ end
35
+ end
36
+
37
+ describe "#language" do
38
+ it "sets @language" do
39
+ ctx.language("en")
40
+ expect(ctx.to_hash).to include("@language" => "en")
41
+ end
42
+ end
43
+
44
+ describe "#base" do
45
+ it "sets @base" do
46
+ ctx.base("http://example.org/")
47
+ expect(ctx.to_hash).to include("@base" => "http://example.org/")
48
+ end
49
+ end
50
+
51
+ describe "#term" do
52
+ it "adds simple term as name => id" do
53
+ ctx.term("name", id: "http://example.org/name")
54
+ expect(ctx.to_hash).to include("name" => "http://example.org/name")
55
+ end
56
+
57
+ it "adds term with type" do
58
+ ctx.term("date", id: "http://example.org/date", type: "xsd:date")
59
+ expect(ctx.to_hash).to include("date" => {
60
+ "@id" => "http://example.org/date", "@type" => "xsd:date"
61
+ })
62
+ end
63
+
64
+ it "adds term with container" do
65
+ ctx.term("labels", id: "http://example.org/labels", container: :language)
66
+ expect(ctx.to_hash).to include("labels" => {
67
+ "@id" => "http://example.org/labels", "@container" => "@language"
68
+ })
69
+ end
70
+
71
+ it "adds term with reverse" do
72
+ ctx.term("parent", id: "http://example.org/parent", reverse: true)
73
+ expect(ctx.to_hash).to include("parent" => {
74
+ "@id" => "http://example.org/parent", "@reverse" => true
75
+ })
76
+ end
77
+ end
78
+
79
+ describe "#to_hash" do
80
+ it "serializes complete context" do
81
+ ctx.prefix(Lutaml::Rdf::Namespaces::SkosNamespace)
82
+ ctx.vocab("http://example.org/ns/")
83
+ ctx.term("name", id: "http://example.org/name")
84
+ hash = ctx.to_hash
85
+ expect(hash["@vocab"]).to eq("http://example.org/ns/")
86
+ expect(hash["skos"]).to eq("http://www.w3.org/2004/02/skos/core#")
87
+ expect(hash["name"]).to eq("http://example.org/name")
88
+ end
89
+ end
90
+
91
+ describe "#resolve" do
92
+ before do
93
+ ctx.prefix(Lutaml::Rdf::Namespaces::SkosNamespace)
94
+ ctx.vocab("http://example.org/ns/")
95
+ ctx.term("status", id: "http://example.org/status")
96
+ end
97
+
98
+ it "resolves compact IRI via prefixes" do
99
+ expect(ctx.resolve("skos:prefLabel")).to eq("http://www.w3.org/2004/02/skos/core#prefLabel")
100
+ end
101
+
102
+ it "resolves via term definitions" do
103
+ expect(ctx.resolve("status")).to eq("http://example.org/status")
104
+ end
105
+
106
+ it "resolves unprefixed name via @vocab" do
107
+ expect(ctx.resolve("unknown")).to eq("http://example.org/ns/unknown")
108
+ end
109
+
110
+ it "returns nil for unknown prefix" do
111
+ expect(ctx.resolve("unknown:something")).to be_nil
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+ require "lutaml/jsonld"
5
+
6
+ RSpec.describe Lutaml::JsonLd::TermDefinition do
7
+ it "simple term maps to name => id" do
8
+ td = described_class.new(name: "name", id: "http://example.org/name")
9
+ expect(td.to_context_hash).to eq("name" => "http://example.org/name")
10
+ end
11
+
12
+ it "term with type includes @type" do
13
+ td = described_class.new(name: "date", id: "http://example.org/date",
14
+ type: "xsd:date")
15
+ expect(td.to_context_hash).to eq("date" => {
16
+ "@id" => "http://example.org/date", "@type" => "xsd:date"
17
+ })
18
+ end
19
+
20
+ it "term with container includes @container" do
21
+ td = described_class.new(name: "labels", id: "http://example.org/labels",
22
+ container: :language)
23
+ expect(td.to_context_hash).to eq("labels" => {
24
+ "@id" => "http://example.org/labels", "@container" => "@language"
25
+ })
26
+ end
27
+
28
+ it "term with language includes @language" do
29
+ td = described_class.new(name: "title", id: "http://example.org/title",
30
+ language: "en")
31
+ expect(td.to_context_hash).to eq("title" => {
32
+ "@id" => "http://example.org/title", "@language" => "en"
33
+ })
34
+ end
35
+
36
+ it "term with reverse includes @reverse" do
37
+ td = described_class.new(name: "parent", id: "http://example.org/parent",
38
+ reverse: true)
39
+ expect(td.to_context_hash).to eq("parent" => {
40
+ "@id" => "http://example.org/parent", "@reverse" => true
41
+ })
42
+ end
43
+
44
+ it "complex term includes all fields" do
45
+ td = described_class.new(
46
+ name: "date",
47
+ id: "http://example.org/date",
48
+ type: "xsd:date",
49
+ container: :set,
50
+ )
51
+ hash = td.to_context_hash
52
+ expect(hash["date"]).to eq({ "@id" => "http://example.org/date",
53
+ "@type" => "xsd:date", "@container" => "@set" })
54
+ end
55
+ end