Skip to content

Commit aa31215

Browse files
authored
Merge pull request #32 from ruby-rdf/feature/indexing
Feature/indexing
2 parents db345f2 + 9f89e86 commit aa31215

File tree

7 files changed

+403
-69
lines changed

7 files changed

+403
-69
lines changed

README.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -284,6 +284,20 @@ A term definition can include `@context`, which is applied to values of that obj
284284
"foo": "Bar"
285285
}
286286

287+
### @id and @type maps
288+
The value of `@container` in a term definition can include `@id` or `@type`, in addition to `@set`, `@list`, `@language`, and `@index`. This allows value indexing based on either the `@id` or `@type` of associated objects.
289+
290+
{
291+
"@context": {
292+
"@vocab": "http://example/",
293+
"idmap": {"@container": "@id"}
294+
},
295+
"idmap": {
296+
"http://example.org/foo": {"label": "Object with @id <foo>"},
297+
"_:bar": {"label": "Object with @id _:bar"}
298+
}
299+
}
300+
287301
### Framing Updates
288302
The [JSON-LD Framing 1.1 Specification]() improves on previous un-released versions.
289303

lib/json/ld/compact.rb

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -145,10 +145,32 @@ def compact(element, property: nil)
145145
end
146146
end
147147

148-
if %w(@language @index).include?(container)
148+
if %w(@language @index @id @type).include?(container)
149149
map_object = result[item_active_property] ||= {}
150-
compacted_item = compacted_item['@value'] if container == '@language' && value?(compacted_item)
151-
map_key = expanded_item[container]
150+
compacted_item = case container
151+
when '@id'
152+
id_prop = context.compact_iri('@id', vocab: true, quiet: true)
153+
map_key = compacted_item[id_prop]
154+
compacted_item.delete(id_prop)
155+
compacted_item
156+
when '@index'
157+
index_prop = context.compact_iri('@index', vocab: true, quiet: true)
158+
map_key = expanded_item[container]
159+
#compacted_item.delete(index_prop)
160+
compacted_item
161+
when '@language'
162+
map_key = expanded_item[container]
163+
value?(expanded_item) ? expanded_item['@value'] : compacted_item
164+
when '@type'
165+
type_prop = context.compact_iri('@type', vocab: true, quiet: true)
166+
map_key, types = Array(compacted_item[type_prop])
167+
if Array(types).empty?
168+
compacted_item.delete(type_prop)
169+
else
170+
compacted_item[type_prop] = types
171+
end
172+
compacted_item
173+
end
152174
merge_compacted_value(map_object, map_key, compacted_item)
153175
else
154176
compacted_item = [compacted_item] if

lib/json/ld/context.rb

Lines changed: 30 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ class TermDefinition
3636
# @return [String] Type mapping
3737
attr_accessor :type_mapping
3838

39-
# @return ['@set', '@list'] Container mapping
39+
# @return ['@index', '@language', '@index', '@set', '@type', '@id'] Container mapping
4040
attr_accessor :container_mapping
4141

4242
# Language mapping of term, `false` is used if there is explicitly no language mapping for this term.
@@ -62,7 +62,7 @@ def simple?; simple; end
6262
# @param [String] term
6363
# @param [String] id
6464
# @param [String] type_mapping Type mapping
65-
# @param ['@set', '@list'] container_mapping
65+
# @param ['@index', '@language', '@index', '@set', '@type', '@id'] container_mapping
6666
# @param [String] language_mapping
6767
# Language mapping of term, `false` is used if there is explicitly no language mapping for this term
6868
# @param [Boolean] reverse_property
@@ -569,12 +569,14 @@ def create_term_definition(local_context, term, defined)
569569
raise JsonLdError::InvalidIRIMapping, "non-absolute @reverse IRI: #{definition.id} on term #{term.inspect}" unless
570570
definition.id.is_a?(RDF::URI) && definition.id.absolute?
571571

572-
# If value contains an @container member, set the container mapping of definition to its value; if its value is neither @set, nor @index, nor null, an invalid reverse property error has been detected (reverse properties only support set- and index-containers) and processing is aborted.
573-
if (container = value.fetch('@container', false))
572+
# If value contains an @container member, set the container mapping of definition to its value; if its value is neither @set, @index, @type, @id, an absolute IRI nor null, an invalid reverse property error has been detected (reverse properties only support set- and index-containers) and processing is aborted.
573+
if value.has_key?('@container')
574+
container = value['@container']
575+
# FIXME: Are URIS, @id, and @type reasonable for reverse mappings?
574576
raise JsonLdError::InvalidReverseProperty,
575-
"unknown mapping for '@container' to #{container.inspect} on term #{term.inspect}" unless
576-
['@set', '@index', nil].include?(container)
577-
definition.container_mapping = container
577+
"unknown mapping for '@container' to #{container.inspect} on term #{term.inspect}" if
578+
%w(@language @list).include?(container)
579+
definition.container_mapping = check_container(container, local_context, defined, term)
578580
end
579581
definition.reverse_property = true
580582
elsif value.has_key?('@id') && value['@id'] != term
@@ -610,10 +612,8 @@ def create_term_definition(local_context, term, defined)
610612
@iri_to_term[definition.id] = term if simple_term && definition.id
611613

612614
if value.has_key?('@container')
613-
container = value['@container']
614-
raise JsonLdError::InvalidContainerMapping, "unknown mapping for '@container' to #{container.inspect} on term #{term.inspect}" unless %w(@list @set @language @index).include?(container)
615-
#log_debug("") {"container_mapping: #{container.inspect}"}
616-
definition.container_mapping = container
615+
#log_debug("") {"container_mapping: #{value['@container'].inspect}"}
616+
definition.container_mapping = check_container(value['@container'], local_context, defined, term)
617617
end
618618

619619
if value.has_key?('@context')
@@ -956,7 +956,12 @@ def compact_iri(iri, value: nil, vocab: nil, reverse: false, quiet: false, **opt
956956
default_language = self.default_language || @none
957957
containers = []
958958
tl, tl_value = "@language", "@null"
959-
containers << '@index' if index?(value)
959+
960+
# If the value is a JSON Object, then for the keywords @index, @id, and @type, if the value contains that keyword, append it to containers.
961+
%w(@index @id @type).each do |kw|
962+
containers << kw if value.has_key?(kw)
963+
end if value.is_a?(Hash)
964+
960965
if reverse
961966
tl, tl_value = "@type", "@reverse"
962967
containers << '@set'
@@ -1485,5 +1490,18 @@ def languages
14851490
memo
14861491
end
14871492
end
1493+
1494+
# Ensure @container mapping is appropriate
1495+
# The result is the original container definition. For IRI containers, this is necessary to be able to determine the @type mapping for string values
1496+
def check_container(container, local_context, defined, term)
1497+
case container
1498+
when '@set', '@list', '@language', '@index', '@type', '@id', nil
1499+
# Okay
1500+
else
1501+
raise JsonLdError::InvalidContainerMapping,
1502+
"unknown mapping for '@container' to #{container.inspect} on term #{term.inspect}"
1503+
end
1504+
container
1505+
end
14881506
end
14891507
end

lib/json/ld/expand.rb

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -249,7 +249,8 @@ def expand(input, active_property, context, ordered: true)
249249
# Use a term-specific context, if defined
250250
term_context = context.term_definitions[key].context if context.term_definitions[key]
251251
active_context = term_context ? context.parse(term_context) : context
252-
expanded_value = if active_context.container(key) == '@language' && value.is_a?(Hash)
252+
container = active_context.container(key)
253+
expanded_value = if container == '@language' && value.is_a?(Hash)
253254
# Otherwise, if key's container mapping in active context is @language and value is a JSON object then value is expanded from a language map as follows:
254255

255256
# Set multilingual array to an empty array.
@@ -272,19 +273,25 @@ def expand(input, active_property, context, ordered: true)
272273
end
273274

274275
ary
275-
elsif active_context.container(key) == '@index' && value.is_a?(Hash)
276-
# Otherwise, if key's container mapping in active context is @index and value is a JSON object then value is expanded from an index map as follows:
276+
elsif %w(@index @id @type).include?(container) && value.is_a?(Hash)
277+
# Otherwise, if key's container mapping in active context is @index, @id, @type, an IRI or Blank Node and value is a JSON object then value is expanded from an index map as follows:
277278

278279
# Set ary to an empty array.
279-
ary = []
280+
container, ary = container.to_s, []
280281

281282
# For each key-value in the object:
282283
keys = ordered ? value.keys.sort : value.keys
283284
keys.each do |k|
284285
# Initialize index value to the result of using this algorithm recursively, passing active context, key as active property, and index value as element.
285286
index_value = expand([value[k]].flatten, key, active_context, ordered: ordered)
286287
index_value.each do |item|
287-
item['@index'] ||= k
288+
case container
289+
when '@id', '@index' then item[container] ||= k
290+
# If container is @type add the key-value pair (@type-[index]) to item, appending any existing values in item
291+
when '@type' then item[container] = [k].concat(Array(item[container]))
292+
end
293+
294+
# Append item to expanded value.
288295
ary << item
289296
end
290297
end

spec/compact_spec.rb

Lines changed: 148 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -406,7 +406,46 @@
406406
end
407407
end
408408

409-
context "language maps" do
409+
context "@container: @index" do
410+
{
411+
"compact-0029" => {
412+
input: %([{
413+
"@id": "http://example.com/article",
414+
"http://example.com/vocab/author": [{
415+
"@id": "http://example.org/person/1",
416+
"@index": "regular"
417+
}, {
418+
"@id": "http://example.org/guest/cd24f329aa",
419+
"@index": "guest"
420+
}]
421+
}]),
422+
context: %({
423+
"author": {"@id": "http://example.com/vocab/author", "@container": "@index" }
424+
}),
425+
output: %({
426+
"@context": {
427+
"author": {
428+
"@id": "http://example.com/vocab/author",
429+
"@container": "@index"
430+
}
431+
},
432+
"@id": "http://example.com/article",
433+
"author": {
434+
"regular": {
435+
"@id": "http://example.org/person/1"
436+
},
437+
"guest": {
438+
"@id": "http://example.org/guest/cd24f329aa"
439+
}
440+
}
441+
})
442+
},
443+
}.each_pair do |title, params|
444+
it(title) {run_compact(params)}
445+
end
446+
end
447+
448+
context "@container: @language" do
410449
{
411450
"compact-0024" => {
412451
input: %([
@@ -440,6 +479,114 @@
440479
end
441480
end
442481

482+
context "@container: @id" do
483+
{
484+
"Indexes to object not having an @id" => {
485+
input: %([{
486+
"http://example/idmap": [
487+
{"http://example/label": [{"@value": "Object with @id _:bar"}], "@id": "_:bar"},
488+
{"http://example/label": [{"@value": "Object with @id <foo>"}], "@id": "http://example.org/foo"}
489+
]
490+
}]),
491+
context: %({
492+
"@vocab": "http://example/",
493+
"idmap": {"@container": "@id"}
494+
}),
495+
output: %({
496+
"@context": {
497+
"@vocab": "http://example/",
498+
"idmap": {"@container": "@id"}
499+
},
500+
"idmap": {
501+
"http://example.org/foo": {"label": "Object with @id <foo>"},
502+
"_:bar": {"label": "Object with @id _:bar"}
503+
}
504+
}),
505+
},
506+
"Indexes to object already having an @id" => {
507+
input: %([{
508+
"http://example/idmap": [
509+
{"@id": "_:foo", "http://example/label": [{"@value": "Object with @id _:bar"}]},
510+
{"@id": "http://example.org/bar", "http://example/label": [{"@value": "Object with @id <foo>"}]}
511+
]
512+
}]),
513+
context: %({
514+
"@vocab": "http://example/",
515+
"idmap": {"@container": "@id"}
516+
}),
517+
output: %({
518+
"@context": {
519+
"@vocab": "http://example/",
520+
"idmap": {"@container": "@id"}
521+
},
522+
"idmap": {
523+
"_:foo": {"label": "Object with @id _:bar"},
524+
"http://example.org/bar": {"label": "Object with @id <foo>"}
525+
}
526+
}),
527+
},
528+
}.each_pair do |title, params|
529+
it(title) {run_compact(params)}
530+
end
531+
end
532+
533+
context "@container: @type" do
534+
{
535+
"Indexes to object not having an @type" => {
536+
input: %([{
537+
"http://example/typemap": [
538+
{"http://example/label": [{"@value": "Object with @type _:bar"}], "@type": ["_:bar"]},
539+
{"http://example/label": [{"@value": "Object with @type <foo>"}], "@type": ["http://example.org/foo"]}
540+
]
541+
}]),
542+
context: %({
543+
"@vocab": "http://example/",
544+
"typemap": {"@container": "@type"}
545+
}),
546+
output: %({
547+
"@context": {
548+
"@vocab": "http://example/",
549+
"typemap": {"@container": "@type"}
550+
},
551+
"typemap": {
552+
"http://example.org/foo": {"label": "Object with @type <foo>"},
553+
"_:bar": {"label": "Object with @type _:bar"}
554+
}
555+
})
556+
},
557+
"Indexes to object already having an @type" => {
558+
input: %([{
559+
"http://example/typemap": [
560+
{
561+
"@type": ["_:bar", "_:foo"],
562+
"http://example/label": [{"@value": "Object with @type _:bar"}]
563+
},
564+
{
565+
"@type": ["http://example.org/foo", "http://example.org/bar"],
566+
"http://example/label": [{"@value": "Object with @type <foo>"}]
567+
}
568+
]
569+
}]),
570+
context: %({
571+
"@vocab": "http://example/",
572+
"typemap": {"@container": "@type"}
573+
}),
574+
output: %({
575+
"@context": {
576+
"@vocab": "http://example/",
577+
"typemap": {"@container": "@type"}
578+
},
579+
"typemap": {
580+
"http://example.org/foo": {"@type": "http://example.org/bar", "label": "Object with @type <foo>"},
581+
"_:bar": {"@type": "_:foo", "label": "Object with @type _:bar"}
582+
}
583+
})
584+
},
585+
}.each_pair do |title, params|
586+
it(title) {run_compact(params)}
587+
end
588+
end
589+
443590
context "@graph" do
444591
{
445592
"Uses @graph given mutliple inputs" => {

0 commit comments

Comments
 (0)