lutaml-model 0.8.14 → 0.8.15
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/Gemfile +3 -5
- data/lib/lutaml/jsonld/transform.rb +44 -13
- data/lib/lutaml/model/liquefiable.rb +9 -0
- data/lib/lutaml/model/liquid/indexed_access.rb +33 -0
- data/lib/lutaml/model/liquid.rb +1 -0
- data/lib/lutaml/model/version.rb +1 -1
- data/lib/lutaml/rdf/mapping.rb +10 -1
- data/lib/lutaml/rdf/member_rule.rb +29 -4
- data/lib/lutaml/turtle/transform.rb +54 -35
- data/lib/lutaml/xml/adapter/xml_serializer.rb +1 -1
- data/lutaml-model.gemspec +1 -1
- data/spec/lutaml/jsonld/transform_spec.rb +147 -0
- data/spec/lutaml/model/liquid/indexed_access_spec.rb +135 -0
- data/spec/lutaml/rdf/mapping_spec.rb +70 -6
- data/spec/lutaml/rdf/member_rule_spec.rb +103 -1
- data/spec/lutaml/turtle/transform_spec.rb +144 -0
- metadata +8 -6
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8d6ce7467101e1a845bcd0bfb98b0e878d55d440065a12481576a8aa5c050c98
|
|
4
|
+
data.tar.gz: e534138d7ca3848eaa33681e286584ea385386734617f37c5fa89a7c3f752f79
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 8e872c659c1e147598fffe00ea015ed07dbd16b795c254f4aed6df30d2c6f2c91ab51740f5112b93e67d113d5b7d7f4edef755a2d6fa848053ab885533ac1336
|
|
7
|
+
data.tar.gz: 22a484ca450a64ff4b3b0d426f1982e0e17d90eb422534158cf02ca65b96cc5293f401fec9cbbaa39a0ccaa06c0ef987b9a7f0f02312bc6fd55233b1666191ad
|
data/Gemfile
CHANGED
|
@@ -5,14 +5,12 @@ source "https://rubygems.org"
|
|
|
5
5
|
# Specify your gem's dependencies in lutaml-model.gemspec
|
|
6
6
|
gemspec
|
|
7
7
|
|
|
8
|
-
gem "moxml", git: "https://github.com/lutaml/moxml", branch: "main"
|
|
9
|
-
|
|
10
8
|
# needed for liquid with ruby 3.4
|
|
11
9
|
gem "base64"
|
|
12
10
|
gem "benchmark-ips"
|
|
13
11
|
gem "bigdecimal"
|
|
14
12
|
gem "canon" # , path: "../canon"
|
|
15
|
-
gem "json-ld"
|
|
13
|
+
gem "json-ld"
|
|
16
14
|
gem "liquid", "~> 5"
|
|
17
15
|
gem "multi_json"
|
|
18
16
|
gem "nokogiri"
|
|
@@ -21,7 +19,7 @@ gem "oj"
|
|
|
21
19
|
gem "openssl", "~> 3.0"
|
|
22
20
|
gem "ox"
|
|
23
21
|
gem "rake"
|
|
24
|
-
gem "rdf-turtle"
|
|
22
|
+
gem "rdf-turtle"
|
|
25
23
|
gem "rexml"
|
|
26
24
|
gem "rspec"
|
|
27
25
|
gem "rubocop"
|
|
@@ -33,4 +31,4 @@ gem "toml-rb"
|
|
|
33
31
|
|
|
34
32
|
# ruby-prof works on all platforms including Windows (unlike stackprof)
|
|
35
33
|
# Provides both CPU and memory profiling
|
|
36
|
-
gem "ruby-prof",
|
|
34
|
+
gem "ruby-prof", group: :development
|
|
@@ -45,6 +45,12 @@ module Lutaml
|
|
|
45
45
|
build_instance(attrs, options)
|
|
46
46
|
end
|
|
47
47
|
|
|
48
|
+
protected
|
|
49
|
+
|
|
50
|
+
def additional_resource_data(_instance, _mapping)
|
|
51
|
+
{}
|
|
52
|
+
end
|
|
53
|
+
|
|
48
54
|
private
|
|
49
55
|
|
|
50
56
|
def extract_mapping(options)
|
|
@@ -52,28 +58,32 @@ module Lutaml
|
|
|
52
58
|
end
|
|
53
59
|
|
|
54
60
|
def build_graph_document(mapping, instance)
|
|
55
|
-
context =
|
|
61
|
+
context = build_merged_context_recursive(mapping, instance)
|
|
62
|
+
graph = collect_resources(mapping, instance)
|
|
63
|
+
|
|
64
|
+
{ "@context" => context, "@graph" => graph }
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def collect_resources(mapping, instance)
|
|
56
68
|
graph = []
|
|
57
69
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
graph << resource unless resource.empty?
|
|
61
|
-
end
|
|
70
|
+
resource = build_resource_data(mapping, instance)
|
|
71
|
+
graph << resource unless resource.empty?
|
|
62
72
|
|
|
63
73
|
mapping.rdf_members.each do |member_rule|
|
|
64
74
|
each_member(instance, member_rule) do |member|
|
|
65
75
|
member_mapping = member_mapping_for(member, :jsonld)
|
|
66
76
|
next unless member_mapping
|
|
67
77
|
|
|
68
|
-
|
|
69
|
-
graph
|
|
78
|
+
child_resources = collect_resources(member_mapping, member)
|
|
79
|
+
graph.concat(child_resources)
|
|
70
80
|
end
|
|
71
81
|
end
|
|
72
82
|
|
|
73
|
-
|
|
83
|
+
graph
|
|
74
84
|
end
|
|
75
85
|
|
|
76
|
-
def
|
|
86
|
+
def build_merged_context_recursive(mapping, instance)
|
|
77
87
|
context_hash = build_context_from_mapping(mapping).to_hash
|
|
78
88
|
|
|
79
89
|
mapping.rdf_members.each do |member_rule|
|
|
@@ -82,6 +92,9 @@ module Lutaml
|
|
|
82
92
|
next unless member_mapping
|
|
83
93
|
|
|
84
94
|
context_hash.merge!(build_context_from_mapping(member_mapping).to_hash)
|
|
95
|
+
|
|
96
|
+
child_ctx = build_merged_context_recursive(member_mapping, member)
|
|
97
|
+
context_hash.merge!(child_ctx)
|
|
85
98
|
end
|
|
86
99
|
end
|
|
87
100
|
|
|
@@ -100,9 +113,14 @@ module Lutaml
|
|
|
100
113
|
mapping.rdf_members.each do |member_rule|
|
|
101
114
|
next unless member_rule.linked?
|
|
102
115
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
116
|
+
predicate_uri = if member_rule.predicate_name
|
|
117
|
+
member_rule.linked_predicate_uri
|
|
118
|
+
end
|
|
119
|
+
if predicate_uri
|
|
120
|
+
context.term(member_rule.predicate_name.to_s,
|
|
121
|
+
id: predicate_uri,
|
|
122
|
+
type: "@id")
|
|
123
|
+
end
|
|
106
124
|
end
|
|
107
125
|
|
|
108
126
|
context
|
|
@@ -151,9 +169,12 @@ module Lutaml
|
|
|
151
169
|
member_refs = collect_member_references(instance, member_rule)
|
|
152
170
|
next if member_refs.empty?
|
|
153
171
|
|
|
154
|
-
|
|
172
|
+
key = jsonld_member_key(member_rule)
|
|
173
|
+
result[key] = member_refs
|
|
155
174
|
end
|
|
156
175
|
|
|
176
|
+
result.merge!(additional_resource_data(instance, mapping))
|
|
177
|
+
|
|
157
178
|
result
|
|
158
179
|
end
|
|
159
180
|
|
|
@@ -168,6 +189,16 @@ module Lutaml
|
|
|
168
189
|
refs
|
|
169
190
|
end
|
|
170
191
|
|
|
192
|
+
def jsonld_member_key(member_rule)
|
|
193
|
+
if member_rule.predicate_name
|
|
194
|
+
member_rule.predicate_name.to_s
|
|
195
|
+
elsif member_rule.link.is_a?(String)
|
|
196
|
+
member_rule.link.split(":").last
|
|
197
|
+
else
|
|
198
|
+
member_rule.attr_name.to_s
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
|
|
171
202
|
def serialize_value(value, rule)
|
|
172
203
|
case rule.kind
|
|
173
204
|
when :uri_reference then serialize_uri_reference(value)
|
|
@@ -54,6 +54,15 @@ module Lutaml
|
|
|
54
54
|
value.to_liquid
|
|
55
55
|
end
|
|
56
56
|
end
|
|
57
|
+
|
|
58
|
+
def liquid_method_missing(method)
|
|
59
|
+
if @object.is_a?(::Lutaml::Model::Liquid::IndexedAccess)
|
|
60
|
+
result = @object.liquid_fetch(method)
|
|
61
|
+
result.nil? ? super : liquefy_value(result)
|
|
62
|
+
else
|
|
63
|
+
super
|
|
64
|
+
end
|
|
65
|
+
end
|
|
57
66
|
end)
|
|
58
67
|
end
|
|
59
68
|
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Lutaml
|
|
4
|
+
module Model
|
|
5
|
+
module Liquid
|
|
6
|
+
# Module for Lutaml::Model objects that support bracket-based
|
|
7
|
+
# lookup in Liquid templates (e.g., collections with index or key access).
|
|
8
|
+
#
|
|
9
|
+
# Include this module in any Serializable subclass that supports
|
|
10
|
+
# +self[key]+ so that its auto-generated Liquid drop can delegate
|
|
11
|
+
# +drop[key]+ through to the underlying object.
|
|
12
|
+
#
|
|
13
|
+
# Example:
|
|
14
|
+
# class Glossarist::Collections::LocalizationCollection
|
|
15
|
+
# include Lutaml::Model::Liquid::IndexedAccess
|
|
16
|
+
# # ...
|
|
17
|
+
# end
|
|
18
|
+
#
|
|
19
|
+
# drop['eng'] #=> calls drop.liquid_method_missing('eng')
|
|
20
|
+
# #=> calls @object.liquid_fetch('eng')
|
|
21
|
+
# #=> calls @object['eng']
|
|
22
|
+
# #=> returns the localized concept drop
|
|
23
|
+
module IndexedAccess
|
|
24
|
+
# Called by the auto-generated Liquid drop via +liquid_method_missing+.
|
|
25
|
+
# Delegates to +self[key]+ by default. Override in specific classes
|
|
26
|
+
# for custom lookup behavior.
|
|
27
|
+
def liquid_fetch(key)
|
|
28
|
+
self[key]
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
data/lib/lutaml/model/liquid.rb
CHANGED
data/lib/lutaml/model/version.rb
CHANGED
data/lib/lutaml/rdf/mapping.rb
CHANGED
|
@@ -27,6 +27,14 @@ module Lutaml
|
|
|
27
27
|
@rdf_type = Array(value)
|
|
28
28
|
end
|
|
29
29
|
|
|
30
|
+
def types(*values)
|
|
31
|
+
@rdf_type = values.flatten
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def has_types_or_predicates?
|
|
35
|
+
@rdf_type.any? || @rdf_predicates.any?
|
|
36
|
+
end
|
|
37
|
+
|
|
30
38
|
def predicate(name, namespace:, to:, lang_tagged: false,
|
|
31
39
|
uri_reference: false)
|
|
32
40
|
@rdf_predicates << Lutaml::Rdf::MappingRule.new(
|
|
@@ -38,11 +46,12 @@ module Lutaml
|
|
|
38
46
|
)
|
|
39
47
|
end
|
|
40
48
|
|
|
41
|
-
def members(attr_name, predicate_name: nil, namespace: nil)
|
|
49
|
+
def members(attr_name, predicate_name: nil, namespace: nil, link: nil)
|
|
42
50
|
@rdf_members << Lutaml::Rdf::MemberRule.new(
|
|
43
51
|
attr_name,
|
|
44
52
|
predicate_name: predicate_name,
|
|
45
53
|
namespace: namespace,
|
|
54
|
+
link: link,
|
|
46
55
|
)
|
|
47
56
|
end
|
|
48
57
|
|
|
@@ -3,28 +3,53 @@
|
|
|
3
3
|
module Lutaml
|
|
4
4
|
module Rdf
|
|
5
5
|
class MemberRule
|
|
6
|
-
attr_reader :attr_name, :predicate_name, :namespace
|
|
6
|
+
attr_reader :attr_name, :predicate_name, :namespace, :link
|
|
7
7
|
|
|
8
|
-
def initialize(attr_name, predicate_name: nil, namespace: nil)
|
|
8
|
+
def initialize(attr_name, predicate_name: nil, namespace: nil, link: nil)
|
|
9
9
|
if predicate_name && !namespace
|
|
10
10
|
raise ArgumentError,
|
|
11
11
|
"namespace is required when predicate_name is provided"
|
|
12
12
|
end
|
|
13
13
|
|
|
14
|
+
if predicate_name && link
|
|
15
|
+
raise ArgumentError,
|
|
16
|
+
"predicate_name and link are mutually exclusive"
|
|
17
|
+
end
|
|
18
|
+
|
|
14
19
|
@attr_name = attr_name.to_sym
|
|
15
20
|
@predicate_name = predicate_name
|
|
16
21
|
@namespace = namespace
|
|
22
|
+
@link = link
|
|
17
23
|
end
|
|
18
24
|
|
|
19
25
|
def linked?
|
|
20
|
-
|
|
26
|
+
!!(@predicate_name || @link)
|
|
21
27
|
end
|
|
22
28
|
|
|
23
29
|
def linked_predicate_uri
|
|
24
|
-
return nil unless
|
|
30
|
+
return nil unless @predicate_name
|
|
25
31
|
|
|
26
32
|
@namespace[@predicate_name]
|
|
27
33
|
end
|
|
34
|
+
|
|
35
|
+
def link_predicate_for(member, resolver)
|
|
36
|
+
return nil unless @link
|
|
37
|
+
|
|
38
|
+
case @link
|
|
39
|
+
when String
|
|
40
|
+
resolver.call(@link)
|
|
41
|
+
when Proc
|
|
42
|
+
resolver.call(@link.call(member))
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def resolve_link_uri(member, resolver)
|
|
47
|
+
if @predicate_name
|
|
48
|
+
linked_predicate_uri
|
|
49
|
+
elsif @link
|
|
50
|
+
link_predicate_for(member, resolver)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
28
53
|
end
|
|
29
54
|
end
|
|
30
55
|
end
|
|
@@ -10,7 +10,7 @@ module Lutaml
|
|
|
10
10
|
mapping = extract_turtle_mapping(options)
|
|
11
11
|
return "" unless mapping
|
|
12
12
|
|
|
13
|
-
if !mapping.rdf_subject && mapping.
|
|
13
|
+
if !mapping.rdf_subject && mapping.has_types_or_predicates? && mapping.rdf_members.empty?
|
|
14
14
|
raise MissingSubjectError,
|
|
15
15
|
"Turtle mapping requires a subject block"
|
|
16
16
|
end
|
|
@@ -18,7 +18,7 @@ module Lutaml
|
|
|
18
18
|
graph = build_graph(mapping, instance)
|
|
19
19
|
return "" if graph.empty?
|
|
20
20
|
|
|
21
|
-
prefixes =
|
|
21
|
+
prefixes = collect_all_prefixes(mapping, instance)
|
|
22
22
|
RDF::Turtle::Writer.buffer(prefixes: prefixes) do |writer|
|
|
23
23
|
graph.each_statement { |stmt| writer << stmt }
|
|
24
24
|
end.strip
|
|
@@ -37,6 +37,12 @@ module Lutaml
|
|
|
37
37
|
build_instance(attrs, options)
|
|
38
38
|
end
|
|
39
39
|
|
|
40
|
+
protected
|
|
41
|
+
|
|
42
|
+
def additional_resource_triples(_instance, _subject_uri, _mapping)
|
|
43
|
+
[]
|
|
44
|
+
end
|
|
45
|
+
|
|
40
46
|
private
|
|
41
47
|
|
|
42
48
|
def extract_turtle_mapping(options)
|
|
@@ -47,63 +53,69 @@ module Lutaml
|
|
|
47
53
|
graph = RDF::Graph.new
|
|
48
54
|
|
|
49
55
|
has_resource_data =
|
|
50
|
-
mapping.
|
|
51
|
-
mapping.rdf_predicates.any? ||
|
|
56
|
+
mapping.has_types_or_predicates? ||
|
|
52
57
|
mapping.rdf_members.any?(&:linked?)
|
|
53
58
|
|
|
54
59
|
if has_resource_data
|
|
55
|
-
subject_uri =
|
|
56
|
-
|
|
60
|
+
subject_uri = if mapping.rdf_subject
|
|
61
|
+
RDF::URI(resolve_subject_uri(mapping, instance))
|
|
62
|
+
else
|
|
63
|
+
RDF::Node.new
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
emit_type_statements(graph, subject_uri, mapping)
|
|
67
|
+
emit_predicate_statements(graph, subject_uri, instance, mapping)
|
|
68
|
+
emit_member_link_statements(graph, subject_uri, instance, mapping)
|
|
69
|
+
additional_resource_triples(instance, subject_uri, mapping).each do |stmt|
|
|
70
|
+
graph << stmt
|
|
71
|
+
end
|
|
57
72
|
end
|
|
58
73
|
|
|
59
|
-
|
|
74
|
+
emit_child_resources(graph, instance, mapping)
|
|
60
75
|
|
|
61
76
|
graph
|
|
62
77
|
end
|
|
63
78
|
|
|
64
|
-
def
|
|
65
|
-
if mapping.rdf_subject
|
|
66
|
-
RDF::URI(resolve_subject_uri(mapping, instance))
|
|
67
|
-
else
|
|
68
|
-
RDF::Node.new
|
|
69
|
-
end
|
|
70
|
-
end
|
|
71
|
-
|
|
72
|
-
def build_resource_triples(graph, mapping, instance, subject_uri)
|
|
79
|
+
def emit_type_statements(graph, subject_uri, mapping)
|
|
73
80
|
mapping.rdf_type.each do |type_value|
|
|
74
81
|
type_uri = RDF::URI(resolve_single_type_uri(mapping, type_value))
|
|
75
82
|
graph << RDF::Statement.new(subject_uri, RDF.type, type_uri)
|
|
76
83
|
end
|
|
84
|
+
end
|
|
77
85
|
|
|
86
|
+
def emit_predicate_statements(graph, subject_uri, instance, mapping)
|
|
78
87
|
mapping.rdf_predicates.each do |rule|
|
|
79
88
|
value = instance.public_send(rule.to)
|
|
80
89
|
next if value.nil?
|
|
81
90
|
|
|
82
91
|
Array(value).each do |v|
|
|
92
|
+
next if v.is_a?(String) && v.empty?
|
|
93
|
+
|
|
83
94
|
object = build_rdf_object(v, rule, mapping.namespace_set)
|
|
84
95
|
graph << RDF::Statement.new(subject_uri, RDF::URI(rule.uri), object)
|
|
85
96
|
end
|
|
86
97
|
end
|
|
98
|
+
end
|
|
87
99
|
|
|
100
|
+
def emit_member_link_statements(graph, subject_uri, instance, mapping)
|
|
88
101
|
mapping.rdf_members.each do |member_rule|
|
|
89
102
|
next unless member_rule.linked?
|
|
90
103
|
|
|
91
104
|
each_member(instance, member_rule) do |member|
|
|
92
105
|
member_mapping = member_mapping_for(member, :turtle)
|
|
93
|
-
next unless member_mapping
|
|
106
|
+
next unless member_mapping&.rdf_subject
|
|
107
|
+
|
|
108
|
+
child_uri = RDF::URI(resolve_subject_uri(member_mapping, member))
|
|
109
|
+
resolver = mapping.namespace_set.method(:resolve_compact_iri)
|
|
110
|
+
link_uri = RDF::URI(member_rule.resolve_link_uri(member, resolver))
|
|
111
|
+
next unless link_uri
|
|
94
112
|
|
|
95
|
-
|
|
96
|
-
member))
|
|
97
|
-
graph << RDF::Statement.new(
|
|
98
|
-
subject_uri,
|
|
99
|
-
RDF::URI(member_rule.linked_predicate_uri),
|
|
100
|
-
member_subject,
|
|
101
|
-
)
|
|
113
|
+
graph << RDF::Statement.new(subject_uri, link_uri, child_uri)
|
|
102
114
|
end
|
|
103
115
|
end
|
|
104
116
|
end
|
|
105
117
|
|
|
106
|
-
def
|
|
118
|
+
def emit_child_resources(graph, instance, mapping)
|
|
107
119
|
mapping.rdf_members.each do |member_rule|
|
|
108
120
|
each_member(instance, member_rule) do |member|
|
|
109
121
|
member_mapping = member_mapping_for(member, :turtle)
|
|
@@ -144,7 +156,14 @@ module Lutaml
|
|
|
144
156
|
end
|
|
145
157
|
end
|
|
146
158
|
|
|
147
|
-
def
|
|
159
|
+
def collect_all_prefixes(mapping, instance)
|
|
160
|
+
ns_set = collect_namespaces_recursive(mapping, instance)
|
|
161
|
+
ns_set.each.with_object({}) do |ns, h|
|
|
162
|
+
h[ns.prefix.to_sym] = ns.uri if ns.prefix && ns.uri
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def collect_namespaces_recursive(mapping, instance)
|
|
148
167
|
ns_set = mapping.namespace_set
|
|
149
168
|
|
|
150
169
|
mapping.rdf_members.each do |member_rule|
|
|
@@ -153,12 +172,12 @@ module Lutaml
|
|
|
153
172
|
next unless member_mapping
|
|
154
173
|
|
|
155
174
|
ns_set = ns_set.merge(member_mapping.namespace_set)
|
|
175
|
+
child_ns = collect_namespaces_recursive(member_mapping, member)
|
|
176
|
+
ns_set = ns_set.merge(child_ns)
|
|
156
177
|
end
|
|
157
178
|
end
|
|
158
179
|
|
|
159
|
-
ns_set
|
|
160
|
-
h[ns.prefix.to_sym] = ns.uri if ns.prefix && ns.uri
|
|
161
|
-
end
|
|
180
|
+
ns_set
|
|
162
181
|
end
|
|
163
182
|
|
|
164
183
|
def extract_attributes(graph, mapping)
|
|
@@ -175,6 +194,12 @@ module Lutaml
|
|
|
175
194
|
attrs
|
|
176
195
|
end
|
|
177
196
|
|
|
197
|
+
def find_subjects_by_types(graph, type_uris)
|
|
198
|
+
type_uris.flat_map do |type_uri|
|
|
199
|
+
graph.query([nil, RDF.type, RDF::URI(type_uri)]).map(&:subject).uniq
|
|
200
|
+
end.uniq
|
|
201
|
+
end
|
|
202
|
+
|
|
178
203
|
def extract_predicate_attributes(graph, subject, mapping, attrs)
|
|
179
204
|
mapping.rdf_predicates.each do |rule|
|
|
180
205
|
stmts = graph.query([subject, RDF::URI(rule.uri), nil])
|
|
@@ -187,12 +212,6 @@ module Lutaml
|
|
|
187
212
|
end
|
|
188
213
|
end
|
|
189
214
|
|
|
190
|
-
def find_subjects_by_types(graph, type_uris)
|
|
191
|
-
type_uris.flat_map do |type_uri|
|
|
192
|
-
graph.query([nil, RDF.type, RDF::URI(type_uri)]).map(&:subject).uniq
|
|
193
|
-
end.uniq
|
|
194
|
-
end
|
|
195
|
-
|
|
196
215
|
def literal_to_ruby(rdf_object, rule, namespace_set)
|
|
197
216
|
case rdf_object
|
|
198
217
|
when RDF::URI
|
data/lutaml-model.gemspec
CHANGED
|
@@ -36,7 +36,7 @@ Gem::Specification.new do |spec|
|
|
|
36
36
|
spec.add_dependency "canon"
|
|
37
37
|
spec.add_dependency "concurrent-ruby"
|
|
38
38
|
spec.add_dependency "liquid", ">= 4.0", "< 6.0"
|
|
39
|
-
spec.add_dependency "moxml", "
|
|
39
|
+
spec.add_dependency "moxml", "~> 0.1.23"
|
|
40
40
|
spec.add_dependency "ostruct"
|
|
41
41
|
spec.add_dependency "rubyzip", "~> 2.3"
|
|
42
42
|
spec.add_dependency "thor"
|
|
@@ -447,4 +447,151 @@ RSpec.describe Lutaml::JsonLd::Transform do
|
|
|
447
447
|
expect(parsed).not_to have_key("@type")
|
|
448
448
|
end
|
|
449
449
|
end
|
|
450
|
+
|
|
451
|
+
describe "dynamic link predicates (String)" do
|
|
452
|
+
before do
|
|
453
|
+
stub_const("JsonLdDynChild", Class.new(Lutaml::Model::Serializable) do
|
|
454
|
+
attribute :cid, :string
|
|
455
|
+
attribute :label, :string
|
|
456
|
+
|
|
457
|
+
rdf do
|
|
458
|
+
namespace TestSkosNs, TestExNs
|
|
459
|
+
|
|
460
|
+
subject { |m| "http://example.org/item/#{m.cid}" } # rubocop:disable RSpec/NamedSubject, RSpec/MultipleSubjects
|
|
461
|
+
|
|
462
|
+
type "skos:Concept"
|
|
463
|
+
|
|
464
|
+
predicate :prefLabel, namespace: TestSkosNs, to: :label
|
|
465
|
+
end
|
|
466
|
+
end)
|
|
467
|
+
|
|
468
|
+
stub_const("JsonLdDynParent", Class.new(Lutaml::Model::Serializable) do
|
|
469
|
+
attribute :name, :string
|
|
470
|
+
attribute :children, JsonLdDynChild, collection: true
|
|
471
|
+
|
|
472
|
+
rdf do
|
|
473
|
+
namespace TestSkosNs, TestExNs
|
|
474
|
+
|
|
475
|
+
subject { |m| "http://example.org/group/#{m.name}" } # rubocop:disable RSpec/NamedSubject, RSpec/MultipleSubjects
|
|
476
|
+
|
|
477
|
+
type "skos:Collection"
|
|
478
|
+
|
|
479
|
+
predicate :prefLabel, namespace: TestSkosNs, to: :name
|
|
480
|
+
|
|
481
|
+
members :children, link: "skos:member"
|
|
482
|
+
end
|
|
483
|
+
end)
|
|
484
|
+
end
|
|
485
|
+
|
|
486
|
+
it "generates @id references for linked members" do
|
|
487
|
+
parent = JsonLdDynParent.new(
|
|
488
|
+
name: "grp1",
|
|
489
|
+
children: [
|
|
490
|
+
JsonLdDynChild.new(cid: "a", label: "Alpha"),
|
|
491
|
+
JsonLdDynChild.new(cid: "b", label: "Beta"),
|
|
492
|
+
],
|
|
493
|
+
)
|
|
494
|
+
parsed = JSON.parse(parent.to_jsonld)
|
|
495
|
+
parent_resource = parsed["@graph"].find { |r| r["@type"] == "skos:Collection" }
|
|
496
|
+
expect(parent_resource["member"]).to eq([
|
|
497
|
+
{ "@id" => "http://example.org/item/a" },
|
|
498
|
+
{ "@id" => "http://example.org/item/b" },
|
|
499
|
+
])
|
|
500
|
+
end
|
|
501
|
+
|
|
502
|
+
it "includes member resources in @graph" do
|
|
503
|
+
parent = JsonLdDynParent.new(
|
|
504
|
+
name: "grp1",
|
|
505
|
+
children: [JsonLdDynChild.new(cid: "a", label: "Alpha")],
|
|
506
|
+
)
|
|
507
|
+
parsed = JSON.parse(parent.to_jsonld)
|
|
508
|
+
member = parsed["@graph"].find { |r| r["prefLabel"] == "Alpha" }
|
|
509
|
+
expect(member).not_to be_nil
|
|
510
|
+
end
|
|
511
|
+
end
|
|
512
|
+
|
|
513
|
+
describe "recursive context and resource collection" do
|
|
514
|
+
before do
|
|
515
|
+
stub_const("JsonLdLeaf", Class.new(Lutaml::Model::Serializable) do
|
|
516
|
+
attribute :value, :string
|
|
517
|
+
attribute :lid, :string
|
|
518
|
+
|
|
519
|
+
rdf do
|
|
520
|
+
namespace TestExNs
|
|
521
|
+
|
|
522
|
+
subject { |m| "http://example.org/leaf/#{m.lid}" } # rubocop:disable RSpec/NamedSubject, RSpec/MultipleSubjects
|
|
523
|
+
|
|
524
|
+
type "ex:Leaf"
|
|
525
|
+
|
|
526
|
+
predicate :name, namespace: TestExNs, to: :value
|
|
527
|
+
end
|
|
528
|
+
end)
|
|
529
|
+
|
|
530
|
+
stub_const("JsonLdMid", Class.new(Lutaml::Model::Serializable) do
|
|
531
|
+
attribute :label, :string
|
|
532
|
+
attribute :mid, :string
|
|
533
|
+
attribute :leaves, JsonLdLeaf, collection: true
|
|
534
|
+
|
|
535
|
+
rdf do
|
|
536
|
+
namespace TestSkosNs, TestExNs
|
|
537
|
+
|
|
538
|
+
subject { |m| "http://example.org/mid/#{m.mid}" } # rubocop:disable RSpec/NamedSubject, RSpec/MultipleSubjects
|
|
539
|
+
|
|
540
|
+
type "skos:Concept"
|
|
541
|
+
|
|
542
|
+
predicate :prefLabel, namespace: TestSkosNs, to: :label
|
|
543
|
+
|
|
544
|
+
members :leaves, link: "skos:member"
|
|
545
|
+
end
|
|
546
|
+
end)
|
|
547
|
+
|
|
548
|
+
stub_const("JsonLdRoot", Class.new(Lutaml::Model::Serializable) do
|
|
549
|
+
attribute :title, :string
|
|
550
|
+
attribute :mids, JsonLdMid, collection: true
|
|
551
|
+
|
|
552
|
+
rdf do
|
|
553
|
+
namespace TestSkosNs
|
|
554
|
+
|
|
555
|
+
subject { |m| "http://example.org/root/#{m.title}" } # rubocop:disable RSpec/NamedSubject, RSpec/MultipleSubjects
|
|
556
|
+
|
|
557
|
+
type "skos:Collection"
|
|
558
|
+
|
|
559
|
+
predicate :prefLabel, namespace: TestSkosNs, to: :title
|
|
560
|
+
|
|
561
|
+
members :mids, link: "skos:member"
|
|
562
|
+
end
|
|
563
|
+
end)
|
|
564
|
+
end
|
|
565
|
+
|
|
566
|
+
it "collects @context from all nesting levels" do
|
|
567
|
+
root = JsonLdRoot.new(
|
|
568
|
+
title: "r1",
|
|
569
|
+
mids: [JsonLdMid.new(
|
|
570
|
+
label: "m1",
|
|
571
|
+
mid: "a",
|
|
572
|
+
leaves: [JsonLdLeaf.new(value: "l1", lid: "x")],
|
|
573
|
+
)],
|
|
574
|
+
)
|
|
575
|
+
parsed = JSON.parse(root.to_jsonld)
|
|
576
|
+
expect(parsed["@context"]["skos"]).to eq("http://www.w3.org/2004/02/skos/core#")
|
|
577
|
+
expect(parsed["@context"]["ex"]).to eq("http://example.org/")
|
|
578
|
+
end
|
|
579
|
+
|
|
580
|
+
it "includes resources from all nesting levels in @graph" do
|
|
581
|
+
root = JsonLdRoot.new(
|
|
582
|
+
title: "r1",
|
|
583
|
+
mids: [JsonLdMid.new(
|
|
584
|
+
label: "m1",
|
|
585
|
+
mid: "a",
|
|
586
|
+
leaves: [JsonLdLeaf.new(value: "l1", lid: "x")],
|
|
587
|
+
)],
|
|
588
|
+
)
|
|
589
|
+
parsed = JSON.parse(root.to_jsonld)
|
|
590
|
+
graph = parsed["@graph"]
|
|
591
|
+
types = graph.map { |r| r["@type"] }
|
|
592
|
+
expect(types).to include("skos:Collection")
|
|
593
|
+
expect(types).to include("skos:Concept")
|
|
594
|
+
expect(types).to include("ex:Leaf")
|
|
595
|
+
end
|
|
596
|
+
end
|
|
450
597
|
end
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
require "liquid"
|
|
5
|
+
|
|
6
|
+
RSpec.describe Lutaml::Model::Liquid::IndexedAccess do
|
|
7
|
+
describe "integration with auto-generated drops" do
|
|
8
|
+
before do
|
|
9
|
+
stub_const("IndexedSpec::Item", Class.new(Lutaml::Model::Serializable) do
|
|
10
|
+
attribute :name, :string
|
|
11
|
+
attribute :value, :string
|
|
12
|
+
end)
|
|
13
|
+
|
|
14
|
+
stub_const("IndexedSpec::ItemCollection", Class.new(Lutaml::Model::Serializable) do
|
|
15
|
+
include Lutaml::Model::Liquid::IndexedAccess
|
|
16
|
+
|
|
17
|
+
attribute :items, IndexedSpec::Item, collection: true
|
|
18
|
+
|
|
19
|
+
def [](key)
|
|
20
|
+
case key
|
|
21
|
+
when Integer then items[key]
|
|
22
|
+
when String then items.find { |i| i.name == key }
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
let(:collection) do
|
|
29
|
+
IndexedSpec::ItemCollection.new(
|
|
30
|
+
items: [
|
|
31
|
+
IndexedSpec::Item.new(name: "alpha", value: "A"),
|
|
32
|
+
IndexedSpec::Item.new(name: "beta", value: "B"),
|
|
33
|
+
IndexedSpec::Item.new(name: "gamma", value: "C"),
|
|
34
|
+
],
|
|
35
|
+
)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
let(:drop) { collection.to_liquid }
|
|
39
|
+
|
|
40
|
+
describe "#liquid_method_missing" do
|
|
41
|
+
it "resolves string key via liquid_fetch" do
|
|
42
|
+
result = drop["alpha"]
|
|
43
|
+
expect(result).to be_a(Liquid::Drop)
|
|
44
|
+
expect(result.name).to eq("alpha")
|
|
45
|
+
expect(result.value).to eq("A")
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
it "resolves integer index via liquid_fetch" do
|
|
49
|
+
result = drop[0]
|
|
50
|
+
expect(result).to be_a(Liquid::Drop)
|
|
51
|
+
expect(result.name).to eq("alpha")
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
it "returns nil for unknown key" do
|
|
55
|
+
result = drop["nonexistent"]
|
|
56
|
+
expect(result).to be_nil
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
it "returns nil for out-of-bounds index" do
|
|
60
|
+
result = drop[99]
|
|
61
|
+
expect(result).to be_nil
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
describe "Liquid template rendering" do
|
|
66
|
+
it "resolves bracket access in templates" do
|
|
67
|
+
template = Liquid::Template.parse("{{ collection['beta'].value }}")
|
|
68
|
+
result = template.render("collection" => drop)
|
|
69
|
+
expect(result).to eq("B")
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
it "resolves integer bracket access in templates" do
|
|
73
|
+
template = Liquid::Template.parse("{{ collection[2].name }}")
|
|
74
|
+
result = template.render("collection" => drop)
|
|
75
|
+
expect(result).to eq("gamma")
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
it "renders empty string for unknown key" do
|
|
79
|
+
template = Liquid::Template.parse("{{ collection['missing'].name }}")
|
|
80
|
+
result = template.render("collection" => drop)
|
|
81
|
+
expect(result).to eq("")
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
describe "coexistence with declared attribute methods" do
|
|
86
|
+
it "still exposes declared attributes normally" do
|
|
87
|
+
expect(drop.items).to be_a(Array)
|
|
88
|
+
expect(drop.items.size).to eq(3)
|
|
89
|
+
expect(drop.items[0].name).to eq("alpha")
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
it "prefers declared methods over indexed access" do
|
|
93
|
+
# 'items' is a declared attribute, so invoke_drop('items') calls
|
|
94
|
+
# the generated method, not liquid_fetch
|
|
95
|
+
result = drop.items
|
|
96
|
+
expect(result).to be_a(Array)
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
describe "objects without IndexedAccess" do
|
|
102
|
+
before do
|
|
103
|
+
stub_const("PlainSpec::Model", Class.new(Lutaml::Model::Serializable) do
|
|
104
|
+
attribute :name, :string
|
|
105
|
+
end)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
let(:instance) { PlainSpec::Model.new(name: "test") }
|
|
109
|
+
let(:drop) { instance.to_liquid }
|
|
110
|
+
|
|
111
|
+
it "does not attempt bracket access on non-indexed objects" do
|
|
112
|
+
result = drop["anything"]
|
|
113
|
+
expect(result).to be_nil
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
it "still exposes declared attributes" do
|
|
117
|
+
expect(drop.name).to eq("test")
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
describe "IndexedAccess module" do
|
|
122
|
+
it "provides liquid_fetch that delegates to []" do
|
|
123
|
+
klass = Class.new do
|
|
124
|
+
include Lutaml::Model::Liquid::IndexedAccess
|
|
125
|
+
|
|
126
|
+
def [](key)
|
|
127
|
+
"value_for_#{key}"
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
instance = klass.new
|
|
132
|
+
expect(instance.liquid_fetch("test")).to eq("value_for_test")
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|
|
@@ -46,6 +46,53 @@ RSpec.describe Lutaml::Rdf::Mapping do
|
|
|
46
46
|
end
|
|
47
47
|
end
|
|
48
48
|
|
|
49
|
+
describe "#types" do
|
|
50
|
+
it "stores multiple types from splat arguments" do
|
|
51
|
+
mapping.types("skos:Concept", "dcterms:Agent")
|
|
52
|
+
expect(mapping.rdf_type).to eq(["skos:Concept", "dcterms:Agent"])
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
it "stores single type" do
|
|
56
|
+
mapping.types("skos:Concept")
|
|
57
|
+
expect(mapping.rdf_type).to eq(["skos:Concept"])
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
it "flattens nested arrays" do
|
|
61
|
+
mapping.types(["skos:Concept", "owl:Thing"], "foaf:Person")
|
|
62
|
+
expect(mapping.rdf_type).to eq(["skos:Concept", "owl:Thing", "foaf:Person"])
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
it "overwrites previous types on subsequent call" do
|
|
66
|
+
mapping.types("skos:Concept")
|
|
67
|
+
mapping.types("dcterms:Agent")
|
|
68
|
+
expect(mapping.rdf_type).to eq(["dcterms:Agent"])
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
describe "#has_types_or_predicates?" do
|
|
73
|
+
it "returns false when no types or predicates" do
|
|
74
|
+
expect(mapping.has_types_or_predicates?).to be(false)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
it "returns true when types are present" do
|
|
78
|
+
mapping.type("skos:Concept")
|
|
79
|
+
expect(mapping.has_types_or_predicates?).to be(true)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
it "returns true when predicates are present" do
|
|
83
|
+
mapping.predicate(:prefLabel,
|
|
84
|
+
namespace: Lutaml::Rdf::Namespaces::SkosNamespace, to: :name)
|
|
85
|
+
expect(mapping.has_types_or_predicates?).to be(true)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
it "returns true when both types and predicates present" do
|
|
89
|
+
mapping.type("skos:Concept")
|
|
90
|
+
mapping.predicate(:prefLabel,
|
|
91
|
+
namespace: Lutaml::Rdf::Namespaces::SkosNamespace, to: :name)
|
|
92
|
+
expect(mapping.has_types_or_predicates?).to be(true)
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
49
96
|
describe "#predicate" do
|
|
50
97
|
it "creates MappingRule with namespace reference" do
|
|
51
98
|
mapping.predicate(
|
|
@@ -128,13 +175,13 @@ RSpec.describe Lutaml::Rdf::Mapping do
|
|
|
128
175
|
end
|
|
129
176
|
|
|
130
177
|
describe "#members" do
|
|
131
|
-
it "creates MemberRule" do
|
|
178
|
+
it "creates MemberRule without linking predicate" do
|
|
132
179
|
mapping.members(:items)
|
|
133
180
|
expect(mapping.rdf_members.length).to eq(1)
|
|
134
181
|
expect(mapping.rdf_members.first.attr_name).to eq(:items)
|
|
135
182
|
end
|
|
136
183
|
|
|
137
|
-
it "creates MemberRule with linking predicate" do
|
|
184
|
+
it "creates MemberRule with static linking predicate" do
|
|
138
185
|
mapping.members(:items,
|
|
139
186
|
predicate_name: :member,
|
|
140
187
|
namespace: Lutaml::Rdf::Namespaces::SkosNamespace)
|
|
@@ -143,11 +190,19 @@ RSpec.describe Lutaml::Rdf::Mapping do
|
|
|
143
190
|
expect(rule.linked_predicate_uri).to eq("http://www.w3.org/2004/02/skos/core#member")
|
|
144
191
|
end
|
|
145
192
|
|
|
146
|
-
it "creates MemberRule
|
|
147
|
-
mapping.members(:items)
|
|
193
|
+
it "creates MemberRule with link as String" do
|
|
194
|
+
mapping.members(:items, link: "skos:member")
|
|
148
195
|
rule = mapping.rdf_members.first
|
|
149
|
-
expect(rule.linked?).to be(
|
|
150
|
-
expect(rule.
|
|
196
|
+
expect(rule.linked?).to be(true)
|
|
197
|
+
expect(rule.link).to eq("skos:member")
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
it "creates MemberRule with link as Proc" do
|
|
201
|
+
resolver = ->(item) { "skos:#{item.type}" }
|
|
202
|
+
mapping.members(:items, link: resolver)
|
|
203
|
+
rule = mapping.rdf_members.first
|
|
204
|
+
expect(rule.linked?).to be(true)
|
|
205
|
+
expect(rule.link).to eq(resolver)
|
|
151
206
|
end
|
|
152
207
|
|
|
153
208
|
it "raises when predicate_name given without namespace" do
|
|
@@ -155,6 +210,15 @@ RSpec.describe Lutaml::Rdf::Mapping do
|
|
|
155
210
|
mapping.members(:items, predicate_name: :member)
|
|
156
211
|
end.to raise_error(ArgumentError, /namespace is required/)
|
|
157
212
|
end
|
|
213
|
+
|
|
214
|
+
it "raises when predicate_name and link both given" do
|
|
215
|
+
expect do
|
|
216
|
+
mapping.members(:items,
|
|
217
|
+
predicate_name: :member,
|
|
218
|
+
namespace: Lutaml::Rdf::Namespaces::SkosNamespace,
|
|
219
|
+
link: "skos:member")
|
|
220
|
+
end.to raise_error(ArgumentError, /mutually exclusive/)
|
|
221
|
+
end
|
|
158
222
|
end
|
|
159
223
|
|
|
160
224
|
describe "#mappings" do
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "spec_helper"
|
|
4
|
+
require_relative "../../../lib/lutaml/rdf"
|
|
4
5
|
|
|
5
6
|
RSpec.describe Lutaml::Rdf::MemberRule do
|
|
6
7
|
describe ".new" do
|
|
@@ -26,10 +27,19 @@ RSpec.describe Lutaml::Rdf::MemberRule do
|
|
|
26
27
|
namespace: Lutaml::Rdf::Namespaces::SkosNamespace)
|
|
27
28
|
expect(rule.predicate_name).to eq(:member)
|
|
28
29
|
end
|
|
30
|
+
|
|
31
|
+
it "raises ArgumentError when predicate_name and link both given" do
|
|
32
|
+
expect do
|
|
33
|
+
described_class.new(:items,
|
|
34
|
+
predicate_name: :member,
|
|
35
|
+
namespace: Lutaml::Rdf::Namespaces::SkosNamespace,
|
|
36
|
+
link: "skos:member")
|
|
37
|
+
end.to raise_error(ArgumentError, /mutually exclusive/)
|
|
38
|
+
end
|
|
29
39
|
end
|
|
30
40
|
|
|
31
41
|
describe "#linked?" do
|
|
32
|
-
it "returns false when no
|
|
42
|
+
it "returns false when no linking option" do
|
|
33
43
|
rule = described_class.new(:items)
|
|
34
44
|
expect(rule.linked?).to be(false)
|
|
35
45
|
end
|
|
@@ -40,6 +50,16 @@ RSpec.describe Lutaml::Rdf::MemberRule do
|
|
|
40
50
|
namespace: Lutaml::Rdf::Namespaces::SkosNamespace)
|
|
41
51
|
expect(rule.linked?).to be(true)
|
|
42
52
|
end
|
|
53
|
+
|
|
54
|
+
it "returns true when link is a String" do
|
|
55
|
+
rule = described_class.new(:items, link: "skos:member")
|
|
56
|
+
expect(rule.linked?).to be(true)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
it "returns true when link is a Proc" do
|
|
60
|
+
rule = described_class.new(:items, link: ->(m) { "skos:#{m}" })
|
|
61
|
+
expect(rule.linked?).to be(true)
|
|
62
|
+
end
|
|
43
63
|
end
|
|
44
64
|
|
|
45
65
|
describe "#linked_predicate_uri" do
|
|
@@ -54,5 +74,87 @@ RSpec.describe Lutaml::Rdf::MemberRule do
|
|
|
54
74
|
namespace: Lutaml::Rdf::Namespaces::SkosNamespace)
|
|
55
75
|
expect(rule.linked_predicate_uri).to eq("http://www.w3.org/2004/02/skos/core#member")
|
|
56
76
|
end
|
|
77
|
+
|
|
78
|
+
it "returns nil when link is a String (not static)" do
|
|
79
|
+
rule = described_class.new(:items, link: "skos:member")
|
|
80
|
+
expect(rule.linked_predicate_uri).to be_nil
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
describe "#link_predicate_for" do
|
|
85
|
+
let(:mapping) do
|
|
86
|
+
instance = Lutaml::Rdf::Mapping.new
|
|
87
|
+
instance.namespace(
|
|
88
|
+
Lutaml::Rdf::Namespaces::SkosNamespace,
|
|
89
|
+
Lutaml::Rdf::Namespaces::DctermsNamespace,
|
|
90
|
+
)
|
|
91
|
+
instance
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
let(:resolver) { mapping.namespace_set.method(:resolve_compact_iri) }
|
|
95
|
+
|
|
96
|
+
it "resolves String link via resolver" do
|
|
97
|
+
rule = described_class.new(:items, link: "skos:member")
|
|
98
|
+
expect(rule.link_predicate_for(nil, resolver))
|
|
99
|
+
.to eq("http://www.w3.org/2004/02/skos/core#member")
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
it "resolves Proc link by calling with member" do
|
|
103
|
+
rule = described_class.new(:items,
|
|
104
|
+
link: ->(m) { "skos:#{m.type}" })
|
|
105
|
+
member = Struct.new(:type).new("Concept")
|
|
106
|
+
expect(rule.link_predicate_for(member, resolver))
|
|
107
|
+
.to eq("http://www.w3.org/2004/02/skos/core#Concept")
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
it "returns URI as-is from Proc when prefix not found" do
|
|
111
|
+
rule = described_class.new(:items,
|
|
112
|
+
link: ->(m) { "http://example.org/#{m.id}" })
|
|
113
|
+
member = Struct.new(:id).new("42")
|
|
114
|
+
expect(rule.link_predicate_for(member, resolver))
|
|
115
|
+
.to eq("http://example.org/42")
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
it "returns nil when no link" do
|
|
119
|
+
rule = described_class.new(:items)
|
|
120
|
+
expect(rule.link_predicate_for(nil, resolver)).to be_nil
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
describe "#resolve_link_uri" do
|
|
125
|
+
let(:mapping) do
|
|
126
|
+
instance = Lutaml::Rdf::Mapping.new
|
|
127
|
+
instance.namespace(Lutaml::Rdf::Namespaces::SkosNamespace)
|
|
128
|
+
instance
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
let(:resolver) { mapping.namespace_set.method(:resolve_compact_iri) }
|
|
132
|
+
|
|
133
|
+
it "uses linked_predicate_uri for static links" do
|
|
134
|
+
rule = described_class.new(:items,
|
|
135
|
+
predicate_name: :member,
|
|
136
|
+
namespace: Lutaml::Rdf::Namespaces::SkosNamespace)
|
|
137
|
+
expect(rule.resolve_link_uri(nil, resolver))
|
|
138
|
+
.to eq("http://www.w3.org/2004/02/skos/core#member")
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
it "uses link_predicate_for for String links" do
|
|
142
|
+
rule = described_class.new(:items, link: "skos:member")
|
|
143
|
+
expect(rule.resolve_link_uri(nil, resolver))
|
|
144
|
+
.to eq("http://www.w3.org/2004/02/skos/core#member")
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
it "uses link_predicate_for for Proc links" do
|
|
148
|
+
rule = described_class.new(:items,
|
|
149
|
+
link: ->(m) { "skos:#{m.type}" })
|
|
150
|
+
member = Struct.new(:type).new("Concept")
|
|
151
|
+
expect(rule.resolve_link_uri(member, resolver))
|
|
152
|
+
.to eq("http://www.w3.org/2004/02/skos/core#Concept")
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
it "returns nil when not linked" do
|
|
156
|
+
rule = described_class.new(:items)
|
|
157
|
+
expect(rule.resolve_link_uri(nil, resolver)).to be_nil
|
|
158
|
+
end
|
|
57
159
|
end
|
|
58
160
|
end
|
|
@@ -585,4 +585,148 @@ RSpec.describe Lutaml::Turtle::Transform do
|
|
|
585
585
|
skip "Heterogeneous collection requires union-typed attribute (not yet supported)"
|
|
586
586
|
end
|
|
587
587
|
end
|
|
588
|
+
|
|
589
|
+
describe "dynamic link predicates" do
|
|
590
|
+
before do
|
|
591
|
+
stub_const("DynChild", Class.new(Lutaml::Model::Serializable) do
|
|
592
|
+
attribute :label, :string
|
|
593
|
+
attribute :cid, :string
|
|
594
|
+
|
|
595
|
+
turtle do
|
|
596
|
+
namespace Lutaml::Rdf::Namespaces::SkosNamespace
|
|
597
|
+
|
|
598
|
+
subject { |m| "http://example.org/child/#{m.cid}" } # rubocop:disable RSpec/NamedSubject, RSpec/MultipleSubjects
|
|
599
|
+
|
|
600
|
+
type "skos:Concept"
|
|
601
|
+
|
|
602
|
+
predicate :prefLabel,
|
|
603
|
+
namespace: Lutaml::Rdf::Namespaces::SkosNamespace,
|
|
604
|
+
to: :label
|
|
605
|
+
end
|
|
606
|
+
end)
|
|
607
|
+
|
|
608
|
+
stub_const("DynParent", Class.new(Lutaml::Model::Serializable) do
|
|
609
|
+
attribute :name, :string
|
|
610
|
+
attribute :children, DynChild, collection: true
|
|
611
|
+
|
|
612
|
+
turtle do
|
|
613
|
+
namespace Lutaml::Rdf::Namespaces::SkosNamespace
|
|
614
|
+
|
|
615
|
+
subject { |m| "http://example.org/parent/#{m.name}" }
|
|
616
|
+
|
|
617
|
+
type "skos:Collection"
|
|
618
|
+
|
|
619
|
+
predicate :prefLabel,
|
|
620
|
+
namespace: Lutaml::Rdf::Namespaces::SkosNamespace,
|
|
621
|
+
to: :name
|
|
622
|
+
|
|
623
|
+
members :children, link: "skos:member"
|
|
624
|
+
end
|
|
625
|
+
end)
|
|
626
|
+
end
|
|
627
|
+
|
|
628
|
+
it "generates linking triples from String link" do
|
|
629
|
+
parent = DynParent.new(
|
|
630
|
+
name: "p1",
|
|
631
|
+
children: [DynChild.new(label: "c1", cid: "a")],
|
|
632
|
+
)
|
|
633
|
+
result = parent.to_turtle
|
|
634
|
+
expect(result).to include("skos:member <http://example.org/child/a>")
|
|
635
|
+
end
|
|
636
|
+
|
|
637
|
+
it "includes child subgraph data" do
|
|
638
|
+
parent = DynParent.new(
|
|
639
|
+
name: "p1",
|
|
640
|
+
children: [DynChild.new(label: "c1", cid: "a")],
|
|
641
|
+
)
|
|
642
|
+
result = parent.to_turtle
|
|
643
|
+
expect(result).to include("skos:prefLabel \"c1\"")
|
|
644
|
+
end
|
|
645
|
+
end
|
|
646
|
+
|
|
647
|
+
describe "recursive prefix collection" do
|
|
648
|
+
before do
|
|
649
|
+
stub_const("SkosNs", Lutaml::Rdf::Namespaces::SkosNamespace)
|
|
650
|
+
stub_const("DctermsNs", Lutaml::Rdf::Namespaces::DctermsNamespace)
|
|
651
|
+
|
|
652
|
+
stub_const("LeafModel", Class.new(Lutaml::Model::Serializable) do
|
|
653
|
+
attribute :value, :string
|
|
654
|
+
attribute :lid, :string
|
|
655
|
+
|
|
656
|
+
turtle do
|
|
657
|
+
namespace DctermsNs
|
|
658
|
+
|
|
659
|
+
subject { |m| "http://example.org/leaf/#{m.lid}" } # rubocop:disable RSpec/NamedSubject, RSpec/MultipleSubjects
|
|
660
|
+
|
|
661
|
+
type "dcterms:Agent"
|
|
662
|
+
|
|
663
|
+
predicate :title, namespace: DctermsNs, to: :value
|
|
664
|
+
end
|
|
665
|
+
end)
|
|
666
|
+
|
|
667
|
+
stub_const("MidModel", Class.new(Lutaml::Model::Serializable) do
|
|
668
|
+
attribute :name, :string
|
|
669
|
+
attribute :mid, :string
|
|
670
|
+
attribute :leaves, LeafModel, collection: true
|
|
671
|
+
|
|
672
|
+
turtle do
|
|
673
|
+
namespace SkosNs, DctermsNs
|
|
674
|
+
|
|
675
|
+
subject { |m| "http://example.org/mid/#{m.mid}" } # rubocop:disable RSpec/NamedSubject, RSpec/MultipleSubjects
|
|
676
|
+
|
|
677
|
+
type "skos:Concept"
|
|
678
|
+
|
|
679
|
+
predicate :prefLabel, namespace: SkosNs, to: :name
|
|
680
|
+
|
|
681
|
+
members :leaves, link: "skos:member"
|
|
682
|
+
end
|
|
683
|
+
end)
|
|
684
|
+
|
|
685
|
+
stub_const("RootModel", Class.new(Lutaml::Model::Serializable) do
|
|
686
|
+
attribute :title, :string
|
|
687
|
+
attribute :mids, MidModel, collection: true
|
|
688
|
+
|
|
689
|
+
turtle do
|
|
690
|
+
namespace SkosNs
|
|
691
|
+
|
|
692
|
+
subject { |m| "http://example.org/root/#{m.title}" }
|
|
693
|
+
|
|
694
|
+
type "skos:Collection"
|
|
695
|
+
|
|
696
|
+
predicate :prefLabel, namespace: SkosNs, to: :title
|
|
697
|
+
|
|
698
|
+
members :mids, link: "skos:member"
|
|
699
|
+
end
|
|
700
|
+
end)
|
|
701
|
+
end
|
|
702
|
+
|
|
703
|
+
it "collects prefixes from all nesting levels" do
|
|
704
|
+
root = RootModel.new(
|
|
705
|
+
title: "r1",
|
|
706
|
+
mids: [MidModel.new(
|
|
707
|
+
name: "m1",
|
|
708
|
+
mid: "a",
|
|
709
|
+
leaves: [LeafModel.new(value: "l1", lid: "x")],
|
|
710
|
+
)],
|
|
711
|
+
)
|
|
712
|
+
result = root.to_turtle
|
|
713
|
+
expect(result).to include("@prefix skos:")
|
|
714
|
+
expect(result).to include("@prefix dcterms:")
|
|
715
|
+
end
|
|
716
|
+
|
|
717
|
+
it "emits triples from all nesting levels" do
|
|
718
|
+
root = RootModel.new(
|
|
719
|
+
title: "r1",
|
|
720
|
+
mids: [MidModel.new(
|
|
721
|
+
name: "m1",
|
|
722
|
+
mid: "a",
|
|
723
|
+
leaves: [LeafModel.new(value: "l1", lid: "x")],
|
|
724
|
+
)],
|
|
725
|
+
)
|
|
726
|
+
result = root.to_turtle
|
|
727
|
+
expect(result).to include("skos:prefLabel \"r1\"")
|
|
728
|
+
expect(result).to include("skos:prefLabel \"m1\"")
|
|
729
|
+
expect(result).to include("dcterms:title \"l1\"")
|
|
730
|
+
end
|
|
731
|
+
end
|
|
588
732
|
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: lutaml-model
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.8.
|
|
4
|
+
version: 0.8.15
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ribose Inc.
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-06-01 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: base64
|
|
@@ -90,16 +90,16 @@ dependencies:
|
|
|
90
90
|
name: moxml
|
|
91
91
|
requirement: !ruby/object:Gem::Requirement
|
|
92
92
|
requirements:
|
|
93
|
-
- - "
|
|
93
|
+
- - "~>"
|
|
94
94
|
- !ruby/object:Gem::Version
|
|
95
|
-
version: 0.1.
|
|
95
|
+
version: 0.1.23
|
|
96
96
|
type: :runtime
|
|
97
97
|
prerelease: false
|
|
98
98
|
version_requirements: !ruby/object:Gem::Requirement
|
|
99
99
|
requirements:
|
|
100
|
-
- - "
|
|
100
|
+
- - "~>"
|
|
101
101
|
- !ruby/object:Gem::Version
|
|
102
|
-
version: 0.1.
|
|
102
|
+
version: 0.1.23
|
|
103
103
|
- !ruby/object:Gem::Dependency
|
|
104
104
|
name: ostruct
|
|
105
105
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -476,6 +476,7 @@ files:
|
|
|
476
476
|
- lib/lutaml/model/jsonl.rb
|
|
477
477
|
- lib/lutaml/model/liquefiable.rb
|
|
478
478
|
- lib/lutaml/model/liquid.rb
|
|
479
|
+
- lib/lutaml/model/liquid/indexed_access.rb
|
|
479
480
|
- lib/lutaml/model/liquid/mapping.rb
|
|
480
481
|
- lib/lutaml/model/mapping/listener.rb
|
|
481
482
|
- lib/lutaml/model/mapping/mapping.rb
|
|
@@ -1611,6 +1612,7 @@ files:
|
|
|
1611
1612
|
- spec/lutaml/model/key_value_mapping_spec.rb
|
|
1612
1613
|
- spec/lutaml/model/lazy_collection_spec.rb
|
|
1613
1614
|
- spec/lutaml/model/liquefiable_spec.rb
|
|
1615
|
+
- spec/lutaml/model/liquid/indexed_access_spec.rb
|
|
1614
1616
|
- spec/lutaml/model/liquid_compatibility_spec.rb
|
|
1615
1617
|
- spec/lutaml/model/logger_spec.rb
|
|
1616
1618
|
- spec/lutaml/model/map_all_spec.rb
|