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,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
|