Skip to content

Commit cfc2701

Browse files
committed
Implementation of Transparent Nesting (@nest) as a term definition property.
1 parent 6e3cee4 commit cfc2701

File tree

10 files changed

+1103
-310
lines changed

10 files changed

+1103
-310
lines changed

README.md

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -298,6 +298,61 @@ The value of `@container` in a term definition can include `@id` or `@type`, in
298298
}
299299
}
300300

301+
### Transparent Nesting
302+
Many JSON APIs separate properties from their entities using an intermediate object. For example, a set of possible labels may be grouped under a common property:
303+
304+
{
305+
"@context": {
306+
"skos": "http://www.w3.org/2004/02/skos/core#",
307+
"labels": "@nest",
308+
"main_label": {"@id": "skos:prefLabel"},
309+
"other_label": {"@id": "skos:altLabel"},
310+
"homepage": {"@id":"http://schema.org/description", "@type":"@id"}
311+
},
312+
"@id":"http://example.org/myresource",
313+
"homepage": "http://example.org",
314+
"labels": {
315+
"main_label": "This is the main label for my resource",
316+
"other_label": "This is the other label"
317+
}
318+
}
319+
320+
In this case, the `labels` property is semantically meaningless. Defining it as equivalent to `@nest` causes it to be ignored when expanding, making it equivalent to the following:
321+
322+
{
323+
"@context": {
324+
"skos": "http://www.w3.org/2004/02/skos/core#",
325+
"labels": "@nest",
326+
"main_label": {"@id": "skos:prefLabel"},
327+
"other_label": {"@id": "skos:altLabel"},
328+
"homepage": {"@id":"http://schema.org/description", "@type":"@id"}
329+
},
330+
"@id":"http://example.org/myresource",
331+
"homepage": "http://example.org",
332+
"main_label": "This is the main label for my resource",
333+
"other_label": "This is the other label"
334+
}
335+
336+
Similarly, properties may be marked with "@nest": "nest-term", to cause them to be nested. Note that the `@nest` keyword can also be aliased in the context.
337+
338+
{
339+
"@context": {
340+
"skos": "http://www.w3.org/2004/02/skos/core#",
341+
"labels": "@nest",
342+
"main_label": {"@id": "skos:prefLabel", "@nest": "labels"},
343+
"other_label": {"@id": "skos:altLabel", "@nest": "labels"},
344+
"homepage": {"@id":"http://schema.org/description", "@type":"@id"}
345+
},
346+
"@id":"http://example.org/myresource",
347+
"homepage": "http://example.org",
348+
"labels": {
349+
"main_label": "This is the main label for my resource",
350+
"other_label": "This is the other label"
351+
}
352+
}
353+
354+
In this way, nesting survives round-tripping through expansion, and framed output can include nested properties.
355+
301356
### Framing Updates
302357
The [JSON-LD Framing 1.1 Specification]() improves on previous un-released versions.
303358

lib/json/ld.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ module LD
5353
@graph
5454
@language
5555
@list
56+
@nest
5657
@omitDefault
5758
@requireAll
5859
@reverse
@@ -111,6 +112,7 @@ class InvalidLanguageMapValue < JsonLdError; @code = "invalid language map value
111112
class InvalidLanguageTaggedString < JsonLdError; @code = "invalid language-tagged string"; end
112113
class InvalidLanguageTaggedValue < JsonLdError; @code = "invalid language-tagged value"; end
113114
class InvalidLocalContext < JsonLdError; @code = "invalid local context"; end
115+
class InvalidNestValue < JsonLdError; @code = "invalid @nest value"; end
114116
class InvalidRemoteContext < JsonLdError; @code = "invalid remote context"; end
115117
class InvalidReverseProperty < JsonLdError; @code = "invalid reverse property"; end
116118
class InvalidReversePropertyMap < JsonLdError; @code = "invalid reverse property map"; end

lib/json/ld/compact.rb

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ def compact(element, property: nil)
5252
end
5353

5454
inside_reverse = property == '@reverse'
55-
result = {}
55+
result, nest_result = {}, nil
5656

5757
element.each_key do |expanded_property|
5858
expanded_value = element[expanded_property]
@@ -78,6 +78,7 @@ def compact(element, property: nil)
7878
value = [value] if !value.is_a?(Array) &&
7979
(context.container(prop) == '@set' || !@options[:compactArrays])
8080
#log_debug("") {"merge #{prop} => #{value.inspect}"}
81+
8182
merge_compacted_value(result, prop, value)
8283
compacted_value.delete(prop)
8384
end
@@ -112,8 +113,14 @@ def compact(element, property: nil)
112113
reverse: inside_reverse,
113114
log_depth: @options[:log_depth])
114115

115-
iap = result[item_active_property] ||= []
116-
result[item_active_property] = [iap] unless iap.is_a?(Array)
116+
if nest_prop = context.nest(item_active_property)
117+
result[nest_prop] ||= {}
118+
iap = result[result[nest_prop]] ||= []
119+
result[nest_prop][item_active_property] = [iap] unless iap.is_a?(Array)
120+
else
121+
iap = result[item_active_property] ||= []
122+
result[item_active_property] = [iap] unless iap.is_a?(Array)
123+
end
117124
end
118125

119126
# At this point, expanded value must be an array due to the Expansion algorithm.
@@ -125,6 +132,14 @@ def compact(element, property: nil)
125132
reverse: inside_reverse,
126133
log_depth: @options[:log_depth])
127134

135+
136+
nest_result = if nest_prop = context.nest(item_active_property)
137+
# FIXME??: It's possible that nest_prop will be used both for nesting, and for values of @nest
138+
result[nest_prop] ||= {}
139+
else
140+
result
141+
end
142+
128143
container = context.container(item_active_property)
129144
value = list?(expanded_item) ? expanded_item['@list'] : expanded_item
130145
compacted_item = compact(value, property: item_active_property)
@@ -141,22 +156,20 @@ def compact(element, property: nil)
141156
end
142157
else
143158
raise JsonLdError::CompactionToListOfLists,
144-
"key cannot have more than one list value" if result.has_key?(item_active_property)
159+
"key cannot have more than one list value" if nest_result.has_key?(item_active_property)
145160
end
146161
end
147162

148163
if %w(@language @index @id @type).include?(container)
149-
map_object = result[item_active_property] ||= {}
164+
map_object = nest_result[item_active_property] ||= {}
150165
compacted_item = case container
151166
when '@id'
152167
id_prop = context.compact_iri('@id', vocab: true, quiet: true)
153168
map_key = compacted_item[id_prop]
154169
compacted_item.delete(id_prop)
155170
compacted_item
156171
when '@index'
157-
index_prop = context.compact_iri('@index', vocab: true, quiet: true)
158172
map_key = expanded_item[container]
159-
#compacted_item.delete(index_prop)
160173
compacted_item
161174
when '@language'
162175
map_key = expanded_item[container]
@@ -179,7 +192,7 @@ def compact(element, property: nil)
179192
%w(@set @list).include?(container) ||
180193
%w(@list @graph).include?(expanded_property)
181194
)
182-
merge_compacted_value(result, item_active_property, compacted_item)
195+
merge_compacted_value(nest_result, item_active_property, compacted_item)
183196
end
184197
end
185198
end

lib/json/ld/context.rb

Lines changed: 42 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,9 @@ class TermDefinition
3939
# @return ['@index', '@language', '@index', '@set', '@type', '@id'] Container mapping
4040
attr_accessor :container_mapping
4141

42+
# @return [String] Term used for nest properties
43+
attr_accessor :nest
44+
4245
# Language mapping of term, `false` is used if there is explicitly no language mapping for this term.
4346
# @return [String] Language mapping
4447
attr_accessor :language_mapping
@@ -66,6 +69,7 @@ def simple?; simple; end
6669
# @param [String] language_mapping
6770
# Language mapping of term, `false` is used if there is explicitly no language mapping for this term
6871
# @param [Boolean] reverse_property
72+
# @param [String] nest term used for nest properties
6973
# @param [Boolean] simple
7074
# This is a simple term definition, not an expanded term definition
7175
def initialize(term,
@@ -74,6 +78,7 @@ def initialize(term,
7478
container_mapping: nil,
7579
language_mapping: nil,
7680
reverse_property: false,
81+
nest: nil,
7782
simple: false,
7883
context: nil)
7984
@term = term
@@ -82,6 +87,7 @@ def initialize(term,
8287
@container_mapping = container_mapping if container_mapping
8388
@language_mapping = language_mapping if language_mapping
8489
@reverse_property = reverse_property if reverse_property
90+
@nest = nest if nest
8591
@simple = simple if simple
8692
@context = context if context
8793
end
@@ -105,7 +111,8 @@ def to_context_definition(context)
105111
container_mapping.nil? &&
106112
type_mapping.nil? &&
107113
reverse_property.nil? &&
108-
self.context.nil?
114+
self.context.nil? &&
115+
nest.nil?
109116

110117
cid.to_s unless cid == term && context.vocab
111118
else
@@ -122,6 +129,7 @@ def to_context_definition(context)
122129
# Language set as false to be output as null
123130
defn['@language'] = (language_mapping ? language_mapping : nil) unless language_mapping.nil?
124131
defn['@context'] = self.context unless self.context.nil?
132+
defn['@nest'] = selfnest unless self.nest.nil?
125133
defn
126134
end
127135
end
@@ -132,7 +140,7 @@ def to_context_definition(context)
132140
# @return [String]
133141
def to_rb
134142
defn = [%(TermDefinition.new\(#{term.inspect})]
135-
%w(id type_mapping container_mapping language_mapping reverse_property simple context).each do |acc|
143+
%w(id type_mapping container_mapping language_mapping reverse_property nest simple context).each do |acc|
136144
v = instance_variable_get("@#{acc}".to_sym)
137145
v = v.to_s if v.is_a?(RDF::Term)
138146
defn << "#{acc}: #{v.inspect}" if v
@@ -148,6 +156,7 @@ def inspect
148156
v << "container=#{container_mapping}" if container_mapping
149157
v << "lang=#{language_mapping.inspect}" unless language_mapping.nil?
150158
v << "type=#{type_mapping}" unless type_mapping.nil?
159+
v << "nest=#{nest.inspect}" unless nest.nil?
151160
v << "has-context" unless context.nil?
152161
v.join(" ") + "]"
153162
end
@@ -524,7 +533,7 @@ def create_term_definition(local_context, term, defined)
524533

525534
expected_keys = case @options[:processingMode]
526535
when "json-ld-1.0" then %w(@id @reverse @type @container @language)
527-
else %w(@id @reverse @type @container @language @context)
536+
else %w(@id @reverse @type @container @language @context @nest)
528537
end
529538

530539
extra_keys = value.keys - expected_keys
@@ -556,7 +565,7 @@ def create_term_definition(local_context, term, defined)
556565

557566
if value.has_key?('@reverse')
558567
raise JsonLdError::InvalidReverseProperty, "unexpected key in #{value.inspect} on term #{term.inspect}" if
559-
value.keys.any? {|k| %w(@id).include?(k)}
568+
value.keys.any? {|k| %w(@id @nest).include?(k)}
560569
raise JsonLdError::InvalidIRIMapping, "expected value of @reverse to be a string: #{value['@reverse'].inspect} on term #{term.inspect}" unless
561570
value['@reverse'].is_a?(String)
562571

@@ -637,6 +646,14 @@ def create_term_definition(local_context, term, defined)
637646
definition.language_mapping = language || false
638647
end
639648

649+
if value.has_key?('@nest')
650+
nest = value['@nest']
651+
raise JsonLdError::InvalidNestValue, "nest must be a string, was #{nest.inspect}} on term #{term.inspect}" unless nest.is_a?(String)
652+
raise JsonLdError::InvalidNestValue, "nest must not be a keyword other than @nest, was #{nest.inspect}} on term #{term.inspect}" if nest.start_with?('@') && nest != '@nest'
653+
#log_debug("") {"nest: #{nest.inspect}"}
654+
definition.nest = nest
655+
end
656+
640657
term_definitions[term] = definition
641658
defined[term] = true
642659
else
@@ -813,6 +830,27 @@ def content(term)
813830
term && term.content
814831
end
815832

833+
##
834+
# Retrieve nest of a term.
835+
# value of nest must be @nest or a term that resolves to @nest
836+
#
837+
# @param [Term, #to_s] term in unexpanded form
838+
# @return [String] Nesting term
839+
# @raise JsonLdError::InvalidNestValue if nesting term exists and is not a term resolving to `@nest` in the current context.
840+
def nest(term)
841+
term = find_definition(term)
842+
if term
843+
case term.nest
844+
when '@nest', nil
845+
term.nest
846+
else
847+
nest_term = find_definition(term.nest)
848+
raise JsonLdError::InvalidNestValue, "nest must a term resolving to @nest" unless nest_term && nest_term.simple? && nest_term.id == '@nest'
849+
term.nest
850+
end
851+
end
852+
end
853+
816854
##
817855
# Retrieve the language associated with a term, or the default language otherwise
818856
# @param [Term, #to_s] term in unexpanded form

0 commit comments

Comments
 (0)