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,134 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+ require "lutaml/model/validation_framework"
5
+
6
+ RSpec.describe Lutaml::Model::Validation::Rule do
7
+ subject(:rule) { described_class.new }
8
+
9
+ describe "defaults" do
10
+ it "returns nil code" do
11
+ expect(rule.code).to be_nil
12
+ end
13
+
14
+ it "returns :general category" do
15
+ expect(rule.category).to eq(:general)
16
+ end
17
+
18
+ it "returns error severity" do
19
+ expect(rule.severity).to eq("error")
20
+ end
21
+
22
+ it "is always applicable" do
23
+ expect(rule.applicable?(nil)).to be true
24
+ expect(rule.applicable?({})).to be true
25
+ end
26
+
27
+ it "returns empty issues from check" do
28
+ expect(rule.check(nil)).to eq([])
29
+ end
30
+
31
+ it "is not deferred" do
32
+ expect(rule.needs_deferred?).to be false
33
+ end
34
+
35
+ it "returns nil from collect" do
36
+ expect(rule.collect(:element, nil)).to be_nil
37
+ end
38
+
39
+ it "returns empty array from complete" do
40
+ expect(rule.complete(nil)).to eq([])
41
+ end
42
+ end
43
+
44
+ describe "custom subclass" do
45
+ let(:custom_rule_class) do
46
+ Class.new(described_class) do
47
+ def code = "CUSTOM-001"
48
+ def category = :custom
49
+ def severity = "warning"
50
+
51
+ def applicable?(context)
52
+ context&.dig(:enabled) != false
53
+ end
54
+
55
+ def check(_context)
56
+ [issue("Found a problem")]
57
+ end
58
+ end
59
+ end
60
+
61
+ it "overrides defaults" do
62
+ rule = custom_rule_class.new
63
+ expect(rule.code).to eq("CUSTOM-001")
64
+ expect(rule.category).to eq(:custom)
65
+ expect(rule.severity).to eq("warning")
66
+ end
67
+
68
+ it "produces issues via helper" do
69
+ rule = custom_rule_class.new
70
+ issues = rule.check(nil)
71
+ expect(issues.length).to eq(1)
72
+ expect(issues.first.code).to eq("CUSTOM-001")
73
+ expect(issues.first.severity).to eq("warning")
74
+ expect(issues.first.message).to eq("Found a problem")
75
+ end
76
+
77
+ it "respects applicable?" do
78
+ rule = custom_rule_class.new
79
+ expect(rule.applicable?({ enabled: true })).to be true
80
+ expect(rule.applicable?({ enabled: false })).to be false
81
+ end
82
+
83
+ it "allows issue helper to override severity and code" do
84
+ klass = Class.new(described_class) do
85
+ def code = "OVERRIDE"
86
+ def severity = "error"
87
+
88
+ def check(_context)
89
+ [issue("msg", severity: "info", code: "OTHER")]
90
+ end
91
+ end
92
+ issues = klass.new.check(nil)
93
+ expect(issues.first.severity).to eq("info")
94
+ expect(issues.first.code).to eq("OTHER")
95
+ end
96
+ end
97
+
98
+ describe "streaming subclass" do
99
+ let(:streaming_rule_class) do
100
+ Class.new(described_class) do
101
+ def code = "STREAM-001"
102
+ def needs_deferred? = true
103
+
104
+ def collect(element, context)
105
+ state = context.rule_state(code)
106
+ state[:items] ||= []
107
+ state[:items] << element
108
+ end
109
+
110
+ def complete(context)
111
+ state = context.rule_state(code)
112
+ return [] unless state[:items]&.any?
113
+
114
+ [issue("Collected #{state[:items].length} items")]
115
+ end
116
+ end
117
+ end
118
+
119
+ it "signals deferred collection" do
120
+ rule = streaming_rule_class.new
121
+ expect(rule.needs_deferred?).to be true
122
+ end
123
+
124
+ it "collects elements and reports in complete" do
125
+ ctx = Lutaml::Model::Validation::Context.new
126
+ rule = streaming_rule_class.new
127
+ rule.collect("item_a", ctx)
128
+ rule.collect("item_b", ctx)
129
+ issues = rule.complete(ctx)
130
+ expect(issues.length).to eq(1)
131
+ expect(issues.first.message).to eq("Collected 2 items")
132
+ end
133
+ end
134
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ RSpec.describe "Lutaml::Model::Validation with UninitializedClass" do
6
+ before do
7
+ stub_const("ValidateWithUninitializedModel", Class.new(Lutaml::Model::Serializable) do
8
+ attribute :name, :string
9
+ attribute :role, :string, values: %w[admin guest], default: -> { "guest" }
10
+ end)
11
+ end
12
+
13
+ it "does not crash when validating with uninitialized attributes" do
14
+ model = ValidateWithUninitializedModel.new
15
+ # name is uninitialized, but validate should not crash
16
+ expect { model.validate }.not_to raise_error
17
+ end
18
+
19
+ it "still catches value constraint violations" do
20
+ model = ValidateWithUninitializedModel.new(role: "hacker")
21
+ errors = model.validate
22
+ expect(errors).not_to be_empty
23
+ end
24
+
25
+ it "returns empty errors for valid instance" do
26
+ model = ValidateWithUninitializedModel.new(name: "ok", role: "admin")
27
+ expect(model.validate).to be_empty
28
+ end
29
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+ require "lutaml/model/validation_framework"
5
+
6
+ RSpec.describe Lutaml::Model::Validation::ValidationError do
7
+ it "is a StandardError" do
8
+ expect(described_class).to be < StandardError
9
+ end
10
+
11
+ it "stores message" do
12
+ error = described_class.new("Something broke")
13
+ expect(error.message).to eq("Something broke")
14
+ end
15
+
16
+ it "stores issues" do
17
+ issue = Lutaml::Model::Validation::Issue.new(
18
+ severity: "error", code: "E-001", message: "bad",
19
+ )
20
+ error = described_class.new("Failed", issues: [issue])
21
+ expect(error.issues.length).to eq(1)
22
+ expect(error.issues.first.code).to eq("E-001")
23
+ end
24
+
25
+ it "defaults issues to empty array" do
26
+ error = described_class.new("Failed")
27
+ expect(error.issues).to eq([])
28
+ end
29
+ end
@@ -0,0 +1,110 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+ require "lutaml/model/validation_framework"
5
+
6
+ RSpec.describe Lutaml::Model::Validation, ".validate / .validate!" do
7
+ let(:registry) { described_class.new_registry }
8
+
9
+ let(:rule_a_class) do
10
+ Class.new(Lutaml::Model::Validation::Rule) do
11
+ def code = "E2E-001"
12
+ def severity = "error"
13
+
14
+ def check(context)
15
+ context[:items].empty? ? [issue("No items found")] : []
16
+ end
17
+ end
18
+ end
19
+
20
+ let(:rule_b_class) do
21
+ Class.new(Lutaml::Model::Validation::Rule) do
22
+ def code = "E2E-002"
23
+ def severity = "warning"
24
+
25
+ def check(context)
26
+ context[:items].length > 100 ? [issue("Too many items")] : []
27
+ end
28
+ end
29
+ end
30
+
31
+ before do
32
+ registry.register(rule_a_class)
33
+ registry.register(rule_b_class)
34
+ end
35
+
36
+ describe ".validate" do
37
+ it "finds no issues with valid data" do
38
+ issues = described_class.validate({ items: (1..50).to_a }, registry)
39
+ expect(issues).to be_empty
40
+ end
41
+
42
+ it "finds errors for empty items" do
43
+ issues = described_class.validate({ items: [] }, registry)
44
+ expect(issues.length).to eq(1)
45
+ expect(issues.first.code).to eq("E2E-001")
46
+ expect(issues.first).to be_error
47
+ end
48
+
49
+ it "finds warnings for too many items" do
50
+ issues = described_class.validate({ items: (1..101).to_a }, registry)
51
+ expect(issues.length).to eq(1)
52
+ expect(issues.first.code).to eq("E2E-002")
53
+ expect(issues.first).to be_warning
54
+ end
55
+
56
+ it "skips inapplicable rules" do
57
+ skip_rule = Class.new(Lutaml::Model::Validation::Rule) do
58
+ def code = "SKIP"
59
+ def applicable?(_ctx) = false
60
+ def check(_ctx) = [issue("should not appear")]
61
+ end
62
+ registry.register(skip_rule)
63
+ issues = described_class.validate({ items: [1] }, registry)
64
+ expect(issues).to be_empty
65
+ end
66
+
67
+ it "does not crash with contexts that support add_error" do
68
+ Lutaml::Model::Validation::Context.new
69
+ # Use plain hash for data — rules expect [:items]
70
+ issues = described_class.validate({ items: [] }, registry)
71
+ expect(issues.length).to eq(1)
72
+ # Plain hash doesn't accumulate, but validate still works
73
+ end
74
+ end
75
+
76
+ describe ".validate!" do
77
+ it "raises on errors" do
78
+ expect do
79
+ described_class.validate!({ items: [] }, registry)
80
+ end.to raise_error(Lutaml::Model::Validation::ValidationError, /E2E-001/)
81
+ end
82
+
83
+ it "exposes issues on ValidationError" do
84
+ described_class.validate!({ items: [] }, registry)
85
+ rescue Lutaml::Model::Validation::ValidationError => e
86
+ expect(e.issues.length).to eq(1)
87
+ expect(e.issues.first.code).to eq("E2E-001")
88
+ end
89
+
90
+ it "does not raise when only warnings" do
91
+ expect do
92
+ described_class.validate!({ items: (1..101).to_a }, registry)
93
+ end.not_to raise_error
94
+ end
95
+
96
+ it "does not raise when no issues" do
97
+ expect do
98
+ described_class.validate!({ items: (1..50).to_a }, registry)
99
+ end.not_to raise_error
100
+ end
101
+ end
102
+
103
+ describe ".new_registry" do
104
+ it "returns a fresh Registry instance" do
105
+ reg = described_class.new_registry
106
+ expect(reg).to be_a(Lutaml::Model::Validation::Registry)
107
+ expect(reg.size).to eq(0)
108
+ end
109
+ end
110
+ end
@@ -0,0 +1,137 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+ require "lutaml/turtle"
5
+ require "lutaml/jsonld"
6
+
7
+ RSpec.describe "RDF graph-aware serialization" do
8
+ before do
9
+ stub_const("GraphTestNs", Class.new(Lutaml::Rdf::Namespace) do
10
+ uri "http://example.org/"
11
+ prefix "ex"
12
+ end)
13
+
14
+ stub_const("GraphMemberModel", Class.new(Lutaml::Model::Serializable) do
15
+ attribute :code, :string
16
+ attribute :name, :string
17
+
18
+ rdf do
19
+ namespace GraphTestNs
20
+
21
+ type "ex:Item"
22
+ predicate :notation, namespace: GraphTestNs, to: :code
23
+ predicate :prefLabel, namespace: GraphTestNs, to: :name
24
+ end
25
+ end)
26
+
27
+ stub_const("GraphContainerModel", Class.new(Lutaml::Model::Serializable) do
28
+ attribute :id, :string
29
+ attribute :items, GraphMemberModel, collection: true
30
+
31
+ rdf do
32
+ namespace GraphTestNs
33
+ subject { |m| "http://example.org/container/#{m.id}" } # rubocop:disable RSpec/NamedSubject
34
+
35
+ type "ex:Container"
36
+ predicate :prefLabel, namespace: GraphTestNs, to: :id
37
+ members :items
38
+ end
39
+ end)
40
+ end
41
+
42
+ let(:first_item) { GraphMemberModel.new(code: "1", name: "First") }
43
+ let(:second_item) { GraphMemberModel.new(code: "2", name: "Second") }
44
+ let(:container) do
45
+ GraphContainerModel.new(id: "test", items: [first_item, second_item])
46
+ end
47
+
48
+ describe "Turtle serialization with members" do
49
+ subject(:turtle) { container.to_turtle }
50
+
51
+ it "includes container triples" do
52
+ expect(turtle).to include("a ex:Container")
53
+ expect(turtle).to include("ex:container")
54
+ end
55
+
56
+ it "includes container predicates" do
57
+ expect(turtle).to include('ex:prefLabel "test"')
58
+ end
59
+
60
+ it "includes all member blank nodes" do
61
+ expect(turtle.scan("a ex:Item").length).to be >= 2
62
+ end
63
+
64
+ it "includes member types" do
65
+ expect(turtle.scan("a ex:Item").length).to eq(2)
66
+ end
67
+
68
+ it "includes member predicates" do
69
+ expect(turtle).to include('ex:notation "1"')
70
+ expect(turtle).to include('ex:notation "2"')
71
+ expect(turtle).to include('ex:prefLabel "First"')
72
+ expect(turtle).to include('ex:prefLabel "Second"')
73
+ end
74
+
75
+ it "shares prefix declarations" do
76
+ prefix_count = turtle.scan(/@prefix ex:/).length
77
+ expect(prefix_count).to eq(1)
78
+ end
79
+ end
80
+
81
+ describe "JSON-LD serialization with members" do
82
+ subject(:jsonld) { JSON.parse(container.to_jsonld) }
83
+
84
+ it "includes @context" do
85
+ expect(jsonld["@context"]).to include("ex")
86
+ end
87
+
88
+ it "includes @graph array" do
89
+ expect(jsonld["@graph"]).to be_an(Array)
90
+ expect(jsonld["@graph"].length).to eq(3) # container + 2 items
91
+ end
92
+
93
+ it "includes container in @graph" do
94
+ container_data = jsonld["@graph"].find do |r|
95
+ r["@type"] == "ex:Container"
96
+ end
97
+ expect(container_data).not_to be_nil
98
+ expect(container_data["@id"]).to eq("http://example.org/container/test")
99
+ expect(container_data["prefLabel"]).to eq("test")
100
+ end
101
+
102
+ it "includes all members in @graph" do
103
+ items = jsonld["@graph"].select { |r| r["@type"] == "ex:Item" }
104
+ expect(items.length).to eq(2)
105
+ codes = items.map { |i| i["notation"] }
106
+ expect(codes).to contain_exactly("1", "2")
107
+ end
108
+ end
109
+
110
+ describe "container without subject (member-only)" do
111
+ before do
112
+ stub_const("MemberOnlyModel", Class.new(Lutaml::Model::Serializable) do
113
+ attribute :items, GraphMemberModel, collection: true
114
+
115
+ rdf do
116
+ namespace GraphTestNs
117
+ members :items
118
+ end
119
+ end)
120
+ end
121
+
122
+ let(:model) { MemberOnlyModel.new(items: [first_item, second_item]) }
123
+
124
+ it "serializes only member triples to turtle" do
125
+ turtle = model.to_turtle
126
+ expect(turtle).to include("a ex:Item")
127
+ expect(turtle).not_to include("ex:Container")
128
+ end
129
+
130
+ it "serializes only members to jsonld @graph" do
131
+ jsonld = JSON.parse(model.to_jsonld)
132
+ expect(jsonld["@graph"].length).to eq(2)
133
+ items = jsonld["@graph"].select { |r| r["@type"] == "ex:Item" }
134
+ expect(items.length).to eq(2)
135
+ end
136
+ end
137
+ end
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+ require "lutaml/rdf"
5
+
6
+ RSpec.describe Lutaml::Rdf::Iri do
7
+ subject(:iri) { described_class.new("http://example.org/ns/Concept") }
8
+
9
+ it "stores frozen value" do
10
+ expect(iri.value).to eq("http://example.org/ns/Concept")
11
+ expect(iri.value).to be_frozen
12
+ end
13
+
14
+ it "coerces to string" do
15
+ expect(iri.to_s).to eq("http://example.org/ns/Concept")
16
+ end
17
+
18
+ describe "equality" do
19
+ it "equals Iri with same value" do
20
+ other = described_class.new("http://example.org/ns/Concept")
21
+ expect(iri).to eq(other)
22
+ end
23
+
24
+ it "does not equal Iri with different value" do
25
+ other = described_class.new("http://other.org/Thing")
26
+ expect(iri).not_to eq(other)
27
+ end
28
+
29
+ it "has consistent hash" do
30
+ other = described_class.new("http://example.org/ns/Concept")
31
+ expect(iri.hash).to eq(other.hash)
32
+ end
33
+ end
34
+
35
+ describe "comparable" do
36
+ it "compares by value" do
37
+ a = described_class.new("http://a.org/")
38
+ b = described_class.new("http://b.org/")
39
+ expect(a < b).to be true
40
+ end
41
+ end
42
+
43
+ describe "#expand" do
44
+ let(:ns_set) do
45
+ Lutaml::Rdf::NamespaceSet.new(
46
+ Lutaml::Rdf::Namespaces::SkosNamespace,
47
+ )
48
+ end
49
+
50
+ it "expands compact IRI via namespace set" do
51
+ iri = described_class.new("skos:Concept")
52
+ expect(iri.expand(ns_set)).to eq("http://www.w3.org/2004/02/skos/core#Concept")
53
+ end
54
+ end
55
+
56
+ describe "#compact" do
57
+ let(:ns_set) do
58
+ Lutaml::Rdf::NamespaceSet.new(
59
+ Lutaml::Rdf::Namespaces::SkosNamespace,
60
+ )
61
+ end
62
+
63
+ it "compacts full URI to prefixed form" do
64
+ iri = described_class.new("http://www.w3.org/2004/02/skos/core#Concept")
65
+ expect(iri.compact(ns_set)).to eq("skos:Concept")
66
+ end
67
+
68
+ it "returns nil for unknown URI" do
69
+ iri = described_class.new("http://unknown.org/Thing")
70
+ expect(iri.compact(ns_set)).to be_nil
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+ require "lutaml/rdf"
5
+
6
+ RSpec.describe Lutaml::Rdf::Literal do
7
+ describe "plain literal" do
8
+ subject(:lit) { described_class.new("hello") }
9
+
10
+ it "stores value" do
11
+ expect(lit.value).to eq("hello")
12
+ end
13
+
14
+ it "has no datatype" do
15
+ expect(lit.datatype).to be_nil
16
+ end
17
+
18
+ it "has no language" do
19
+ expect(lit.language).to be_nil
20
+ end
21
+
22
+ it "serializes to Turtle" do
23
+ expect(lit.to_turtle).to eq('"hello"')
24
+ end
25
+
26
+ it "serializes to JSON-LD as plain value" do
27
+ expect(lit.to_jsonld_term).to eq("hello")
28
+ end
29
+ end
30
+
31
+ describe "language-tagged literal" do
32
+ subject(:lit) { described_class.new("hello", language: "en") }
33
+
34
+ it "serializes to Turtle with language tag" do
35
+ expect(lit.to_turtle).to eq('"hello"@en')
36
+ end
37
+
38
+ it "serializes to JSON-LD with @language" do
39
+ expect(lit.to_jsonld_term).to eq({ "@value" => "hello",
40
+ "@language" => "en" })
41
+ end
42
+ end
43
+
44
+ describe "typed literal" do
45
+ subject(:lit) { described_class.new("2024-01-01", datatype: "http://www.w3.org/2001/XMLSchema#date") }
46
+
47
+ it "serializes to Turtle with datatype" do
48
+ expect(lit.to_turtle).to eq('"2024-01-01"^^<http://www.w3.org/2001/XMLSchema#date>')
49
+ end
50
+
51
+ it "serializes to JSON-LD with @type" do
52
+ expect(lit.to_jsonld_term).to eq({ "@value" => "2024-01-01",
53
+ "@type" => "http://www.w3.org/2001/XMLSchema#date" })
54
+ end
55
+ end
56
+
57
+ describe "special character escaping" do
58
+ it "escapes quotes" do
59
+ lit = described_class.new('has "quotes"')
60
+ expect(lit.to_turtle).to eq('"has \\"quotes\\""')
61
+ end
62
+
63
+ it "escapes newlines" do
64
+ lit = described_class.new("line1\nline2")
65
+ expect(lit.to_turtle).to eq('"line1\\nline2"')
66
+ end
67
+
68
+ it "escapes backslashes" do
69
+ lit = described_class.new("back\\slash")
70
+ expect(lit.to_turtle).to eq('"back\\\\slash"')
71
+ end
72
+
73
+ it "escapes tabs" do
74
+ lit = described_class.new("tab\there")
75
+ expect(lit.to_turtle).to eq('"tab\\there"')
76
+ end
77
+ end
78
+
79
+ describe "equality" do
80
+ it "equals literal with same value, datatype, and language" do
81
+ a = described_class.new("hello", language: "en")
82
+ b = described_class.new("hello", language: "en")
83
+ expect(a).to eq(b)
84
+ end
85
+
86
+ it "does not equal with different language" do
87
+ a = described_class.new("hello", language: "en")
88
+ b = described_class.new("hello", language: "fr")
89
+ expect(a).not_to eq(b)
90
+ end
91
+
92
+ it "does not equal with different datatype" do
93
+ a = described_class.new("1", datatype: "xsd:integer")
94
+ b = described_class.new("1", datatype: "xsd:string")
95
+ expect(a).not_to eq(b)
96
+ end
97
+ end
98
+ end