Skip to content

Commit c8811c7

Browse files
committed
Change compact-iri generation to allow any term mapping to a string ending in a gen-delim in 1.0 mode, or which does that and is a simple term in 1.1, or has a @prefix: true mapping in an expanded term definition in 1.1.
1 parent 9930e0a commit c8811c7

File tree

6 files changed

+109
-106
lines changed

6 files changed

+109
-106
lines changed

lib/json/ld.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,7 @@ class InvalidLanguageTaggedString < JsonLdError; @code = "invalid language-tagge
115115
class InvalidLanguageTaggedValue < JsonLdError; @code = "invalid language-tagged value"; end
116116
class InvalidLocalContext < JsonLdError; @code = "invalid local context"; end
117117
class InvalidNestValue < JsonLdError; @code = "invalid @nest value"; end
118+
class InvalidPrefixValue < JsonLdError; @code = "invalid @prefix value"; end
118119
class InvalidRemoteContext < JsonLdError; @code = "invalid remote context"; end
119120
class InvalidReverseProperty < JsonLdError; @code = "invalid reverse property"; end
120121
class InvalidReversePropertyMap < JsonLdError; @code = "invalid reverse property map"; end

lib/json/ld/api.rb

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -224,7 +224,6 @@ def self.compact(input, context, options = {})
224224
result
225225
end
226226

227-
#require 'byebug'; byebug
228227
API.new(expanded_input, context, options.merge(no_default_base: true)) do
229228
log_debug(".compact") {"expanded input: #{expanded_input.to_json(JSON_STATE) rescue 'malformed json'}"}
230229
result = compact(value)

lib/json/ld/context.rb

Lines changed: 59 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,9 @@ class TermDefinition
5858
# @return [Boolean]
5959
attr_accessor :simple
6060

61+
# Indicate that term may be used as a prefix
62+
attr_writer :prefix
63+
6164
# Term-specific context
6265
# @return [Hash{String => Object}]
6366
attr_accessor :context
@@ -66,6 +69,10 @@ class TermDefinition
6669
# @return [Boolean] simple
6770
def simple?; simple; end
6871

72+
# This is an appropriate term to use as the prefix of a compact IRI
73+
# @return [Boolean] simple
74+
def prefix?; @prefix; end
75+
6976
# Create a new Term Mapping with an ID
7077
# @param [String] term
7178
# @param [String] id
@@ -77,6 +84,8 @@ def simple?; simple; end
7784
# @param [String] nest term used for nest properties
7885
# @param [Boolean] simple
7986
# This is a simple term definition, not an expanded term definition
87+
# @param [Boolean] prefix
88+
# Term may be used as a prefix
8089
def initialize(term,
8190
id: nil,
8291
type_mapping: nil,
@@ -85,16 +94,18 @@ def initialize(term,
8594
reverse_property: false,
8695
nest: nil,
8796
simple: false,
97+
prefix: nil,
8898
context: nil)
8999
@term = term
90-
@id = id.to_s if id
91-
@type_mapping = type_mapping.to_s if type_mapping
92-
self.container_mapping = container_mapping if container_mapping
93-
@language_mapping = language_mapping if language_mapping
94-
@reverse_property = reverse_property if reverse_property
95-
@nest = nest if nest
96-
@simple = simple if simple
97-
@context = context if context
100+
@id = id.to_s unless id.nil?
101+
@type_mapping = type_mapping.to_s unless type_mapping.nil?
102+
self.container_mapping = container_mapping unless container_mapping.nil?
103+
@language_mapping = language_mapping unless language_mapping.nil?
104+
@reverse_property = reverse_property
105+
@nest = nest unless nest.nil?
106+
@simple = simple
107+
@prefix = prefix unless prefix.nil?
108+
@context = context unless context.nil?
98109
end
99110

100111
# Set container mapping, from an array which may include @set
@@ -121,14 +132,7 @@ def to_context_definition(context)
121132
iri && iri != id ? "#{prefix}:#{id.to_s[iri.length..-1]}" : id
122133
end
123134

124-
if language_mapping.nil? &&
125-
container_mapping.nil? &&
126-
!as_set &&
127-
type_mapping.nil? &&
128-
reverse_property.nil? &&
129-
self.context.nil? &&
130-
nest.nil?
131-
135+
if simple?
132136
cid.to_s unless cid == term && context.vocab
133137
else
134138
defn = {}
@@ -145,9 +149,10 @@ def to_context_definition(context)
145149
cm = cm.first if cm.length == 1
146150
defn['@container'] = cm unless cm.empty?
147151
# Language set as false to be output as null
148-
defn['@language'] = (language_mapping ? language_mapping : nil) unless language_mapping.nil?
149-
defn['@context'] = self.context unless self.context.nil?
150-
defn['@nest'] = selfnest unless self.nest.nil?
152+
defn['@language'] = (@language_mapping ? @language_mapping : nil) unless @language_mapping.nil?
153+
defn['@context'] = @context if @context
154+
defn['@nest'] = @nest if @nest
155+
defn['@prefix'] = @prefix unless @prefix.nil? || (context.processingMode || 'json-ld-1.0') == 'json-ld-1.0'
151156
defn
152157
end
153158
end
@@ -158,7 +163,7 @@ def to_context_definition(context)
158163
# @return [String]
159164
def to_rb
160165
defn = [%(TermDefinition.new\(#{term.inspect})]
161-
%w(id type_mapping container_mapping language_mapping reverse_property nest simple context).each do |acc|
166+
%w(id type_mapping container_mapping language_mapping reverse_property nest simple prefix context).each do |acc|
162167
v = instance_variable_get("@#{acc}".to_sym)
163168
v = v.to_s if v.is_a?(RDF::Term)
164169
if acc == 'container_mapping' && as_set
@@ -179,6 +184,8 @@ def inspect
179184
v << "lang=#{language_mapping.inspect}" unless language_mapping.nil?
180185
v << "type=#{type_mapping}" unless type_mapping.nil?
181186
v << "nest=#{nest.inspect}" unless nest.nil?
187+
v << "simple=true" if @simple
188+
v << "prefix=#{@prefix.inspect}" unless @prefix.nil?
182189
v << "has-context" unless context.nil?
183190
v.join(" ") + "]"
184191
end
@@ -279,8 +286,7 @@ def initialize(**options)
279286
(options[:prefixes] || {}).each_pair do |k, v|
280287
next if k.nil?
281288
@iri_to_term[v.to_s] = k
282-
@term_definitions[k.to_s] = TermDefinition.new(k, id: v.to_s)
283-
@term_definitions[k.to_s].simple = true
289+
@term_definitions[k.to_s] = TermDefinition.new(k, id: v.to_s, simple: true, prefix: true)
284290
end
285291

286292
self.vocab = options[:vocab] if options[:vocab]
@@ -439,7 +445,7 @@ def parse(local_context, remote_contexts = [])
439445
end
440446
raise JsonLdError::InvalidRemoteContext, "#{context}" unless jo.is_a?(Hash) && jo.has_key?('@context')
441447
context = jo['@context']
442-
if processingMode && processingMode <= "json-ld-1.1"
448+
if (processingMode || 'json-ld-1.0') <= "json-ld-1.1"
443449
context_no_base.provided_context = context.dup
444450
end
445451
end
@@ -561,7 +567,7 @@ def create_term_definition(local_context, term, defined)
561567
# Initialize value to a the value associated with the key term in local context.
562568
value = local_context.fetch(term, false)
563569
simple_term = value.is_a?(String)
564-
value = {'@id' => value} if value.is_a?(String)
570+
value = {'@id' => value} if simple_term
565571

566572
case value
567573
when nil, {'@id' => nil}
@@ -575,9 +581,9 @@ def create_term_definition(local_context, term, defined)
575581
definition = TermDefinition.new(term)
576582
definition.simple = simple_term
577583

578-
expected_keys = case @options[:processingMode]
579-
when "json-ld-1.0" then %w(@id @reverse @type @container @language)
580-
else %w(@id @reverse @type @container @language @context @nest)
584+
expected_keys = case processingMode
585+
when "json-ld-1.0", nil then %w(@container @id @language @reverse @type)
586+
else %w(@container @context @id @language @nest @prefix @reverse @type)
581587
end
582588

583589
extra_keys = value.keys - expected_keys
@@ -641,6 +647,12 @@ def create_term_definition(local_context, term, defined)
641647
defined: defined)
642648
raise JsonLdError::InvalidKeywordAlias, "expected value of @id to not be @context on term #{term.inspect}" if
643649
definition.id == '@context'
650+
651+
# If id ends with a gen-delim, it may be used as a prefix
652+
if definition.id.to_s.end_with?(*%w(: / ? # [ ] @))
653+
definition.prefix = true if simple_term || ((processingMode || 'json-ld-1.0') == 'json-ld-1.0')
654+
end
655+
value['@prefix'] = value['@id'].end_with?(*%w(: / ? # [ ] @)) if simple_term && (processingMode || 'json-ld-1.0') >= 'json-ld-1.1'
644656
elsif term.include?(':')
645657
# If term is a compact IRI with a prefix that is a key in local context then a dependency has been found. Use this algorithm recursively passing active context, local context, the prefix as term, and defined.
646658
prefix, suffix = term.split(':')
@@ -669,9 +681,6 @@ def create_term_definition(local_context, term, defined)
669681
end
670682

671683
if value.has_key?('@context')
672-
# Not supported in JSON-LD 1.0
673-
raise JsonLdError::InvalidTermDefinition, '@context not valid in term definition for JSON-LD 1.0 on term #{term.inspect}, set processing mode using @version' if processingMode && processingMode < 'json-ld-1.1'
674-
675684
begin
676685
self.parse(value['@context'])
677686
definition.context = value['@context']
@@ -689,19 +698,22 @@ def create_term_definition(local_context, term, defined)
689698
end
690699

691700
if value.has_key?('@nest')
692-
# Not supported in JSON-LD 1.0
693-
raise JsonLdError::InvalidTermDefinition, '@nest not valid in term definition for JSON-LD 1.0 on term #{term.inspect}, set processing mode using @version' if processingMode && processingMode < 'json-ld-1.1'
694-
695-
# Not supported in JSON-LD 1.0
696-
raise JsonLdError::InvalidTermDefinition, '@nest not valid in term definition for JSON-LD 1.0 on term #{term.inspect}, set processing mode using @version' if processingMode && processingMode < 'json-ld-1.1'
697-
698701
nest = value['@nest']
699702
raise JsonLdError::InvalidNestValue, "nest must be a string, was #{nest.inspect}} on term #{term.inspect}" unless nest.is_a?(String)
700703
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'
701704
#log_debug("") {"nest: #{nest.inspect}"}
702705
definition.nest = nest
703706
end
704707

708+
if value.has_key?('@prefix')
709+
case pfx = value['@prefix']
710+
when TrueClass, FalseClass
711+
definition.prefix = pfx
712+
else
713+
raise JsonLdError::InvalidPrefixValue, "unknown value for '@prefix': #{pfx.inspect} on term #{term.inspect}"
714+
end
715+
end
716+
705717
term_definitions[term] = definition
706718
defined[term] = true
707719
else
@@ -837,7 +849,7 @@ def from_vocabulary(graph)
837849
def set_mapping(term, value)
838850
#log_debug("") {"map #{term.inspect} to #{value.inspect}"}
839851
term = term.to_s
840-
term_definitions[term] = TermDefinition.new(term, id: value)
852+
term_definitions[term] = TermDefinition.new(term, id: value, simple: true, prefix: (value.to_s.end_with?(*%w(: / ? # [ ] @))))
841853
term_definitions[term].simple = true
842854

843855
term_sym = term.empty? ? "" : term.to_sym
@@ -993,19 +1005,13 @@ def expand_iri(value, documentRelative: false, vocab: false, local_context: nil,
9931005
return RDF::Node.new(namer.get_sym(suffix)) if prefix == '_'
9941006
return RDF::URI(value) if suffix[0,2] == '//'
9951007

996-
# If local context is not null, it contains a key that equals prefix, or prefix followed by a ':', and the value associated with the key that equals prefix in defined is not true, invoke the Create Term Definition algorithm, passing active context, local context, prefix (possibly with an added ':') as term, and defined. This will ensure that a term definition is created for prefix in active context during Context Processing.
997-
if local_context
998-
if local_context.has_key?(prefix + ':') && !defined[prefix + ':']
999-
create_term_definition(local_context, prefix + ':', defined)
1000-
elsif local_context.has_key?(prefix) && !defined[prefix]
1001-
create_term_definition(local_context, prefix, defined)
1002-
end
1008+
# If local context is not null, it contains a key that equals prefix, and the value associated with the key that equals prefix in defined is not true, invoke the Create Term Definition algorithm, passing active context, local context, prefix as term, and defined. This will ensure that a term definition is created for prefix in active context during Context Processing.
1009+
if local_context && local_context.has_key?(prefix) && !defined[prefix]
1010+
create_term_definition(local_context, prefix, defined)
10031011
end
10041012

1005-
# If active context contains a term definition for prefix followed by ':', or prefix, return the result of concatenating the IRI mapping associated with prefix and suffix.
1006-
result = if (td = term_definitions[prefix + ':'])
1007-
result = td.id + suffix
1008-
elsif (td = term_definitions[prefix])
1013+
# If active context contains a term definition for prefix, return the result of concatenating the IRI mapping associated with prefix and suffix.
1014+
result = if (td = term_definitions[prefix])
10091015
result = td.id + suffix
10101016
else
10111017
# (Otherwise) Return value as it is already an absolute IRI.
@@ -1160,32 +1166,16 @@ def compact_iri(iri, value: nil, vocab: nil, reverse: false, quiet: false, **opt
11601166
# The iri could not be compacted using the active context's vocabulary mapping. Try to create a compact IRI, starting by initializing compact IRI to null. This variable will be used to tore the created compact IRI, if any.
11611167
candidates = []
11621168

1163-
# For 1.1, favor simple terms that end in a ':'
1164-
term_definitions.each do |term, td|
1165-
# Skip things that don't end with a ':' or contain a ':'
1166-
next unless term.end_with?(':')
1167-
next if td.nil? || td.id.nil? || td.id == iri || !iri.start_with?(td.id)
1168-
1169-
# Also skip term if it was not a simple term with an id ends in a gen-delim, or if the term ends with ':'
1170-
next unless td.simple?
1171-
1172-
suffix = iri[td.id.length..-1]
1173-
ciri = "#{term}#{suffix}"
1174-
candidates << ciri unless value && term_definitions.has_key?(ciri)
1175-
end
1176-
11771169
term_definitions.each do |term, td|
1178-
next if term =~ /:\w+/ # Skip things that already look like compact IRIs
11791170
next if td.nil? || td.id.nil? || td.id == iri || !iri.start_with?(td.id)
11801171

1181-
# Also skip term if it was not a simple term with an id ends in a gen-delim, or if the term ends with ':'
1182-
next unless td.simple?
1183-
next unless td.id.to_s.end_with?(*%w(: / ? # [ ] @))
1172+
# Skip term if `@prefix` is not true in term definition
1173+
next unless td.prefix?
11841174

11851175
suffix = iri[td.id.length..-1]
11861176
ciri = "#{term}:#{suffix}"
11871177
candidates << ciri unless value && term_definitions.has_key?(ciri)
1188-
end if candidates.empty?
1178+
end
11891179

11901180
return candidates.term_sort.first if !candidates.empty?
11911181

@@ -1614,7 +1604,7 @@ def languages
16141604
# Ensure @container mapping is appropriate
16151605
# The result is the original container definition. For IRI containers, this is necessary to be able to determine the @type mapping for string values
16161606
def check_container(container, local_context, defined, term)
1617-
if container.is_a?(Array) && processingMode < 'json-ld-1.1'
1607+
if container.is_a?(Array) && (processingMode || 'json-ld-1.0') < 'json-ld-1.1'
16181608
raise JsonLdError::InvalidContainerMapping,
16191609
"'@container' on term #{term.inspect} must be a string: #{container.inspect}"
16201610
end
@@ -1635,7 +1625,7 @@ def check_container(container, local_context, defined, term)
16351625
when '@type', '@id', nil
16361626
raise JsonLdError::InvalidContainerMapping,
16371627
"unknown mapping for '@container' to #{container.inspect} on term #{term.inspect}" if
1638-
processingMode < 'json-ld-1.1'
1628+
(processingMode || 'json-ld-1.0') < 'json-ld-1.1'
16391629
else
16401630
raise JsonLdError::InvalidContainerMapping,
16411631
"unknown mapping for '@container' to #{container.inspect} on term #{term.inspect}"

spec/compact_spec.rb

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -331,16 +331,15 @@
331331
},
332332
"Expands but does not compact to document base in 1.1 with compactToRelative false" => {
333333
input: %({
334-
"@id": "a",
335-
"http://example.com/b": {"@id": "c"}
334+
"@id": "http://example.org/a",
335+
"http://example.com/b": {"@id": "http://example.org/c"}
336336
}),
337337
context: %({"b": "http://example.com/b"}),
338338
output: %({
339339
"@context": {"b": "http://example.com/b"},
340340
"@id": "http://example.org/a",
341341
"b": {"@id": "http://example.org/c"}
342342
}),
343-
base: "http://example.org/",
344343
compactToRelative: false,
345344
processingMode: 'json-ld-1.1'
346345
},
@@ -1148,6 +1147,7 @@
11481147
"foo": {"@context": {"bar": "http://example.org/bar"}}
11491148
}),
11501149
processingMode: nil,
1150+
validate: true,
11511151
exception: JSON::LD::JsonLdError::InvalidTermDefinition
11521152
},
11531153
}.each_pair do |title, params|
@@ -1308,6 +1308,7 @@
13081308
"Foo": {"@context": {"bar": "http://example.org/bar"}}
13091309
}),
13101310
processingMode: nil,
1311+
validate: true,
13111312
exception: JSON::LD::JsonLdError::InvalidTermDefinition
13121313
},
13131314
}.each_pair do |title, params|
@@ -1339,13 +1340,23 @@
13391340

13401341
context "compact IRI selection" do
13411342
{
1342-
"does not compact using expanded term" => {
1343+
"compacts using expanded term in 1.0" => {
1344+
input: %({"http://example.org/foo": "term"}),
1345+
context: %({"ex": {"@id": "http://example.org/"}}),
1346+
output: %({
1347+
"@context": {"ex": {"@id": "http://example.org/"}},
1348+
"ex:foo": "term"
1349+
}),
1350+
processingMode: "json-ld-1.0"
1351+
},
1352+
"does not compact using expanded term in 1.1" => {
13431353
input: %({"http://example.org/foo": "term"}),
13441354
context: %({"ex": {"@id": "http://example.org/"}}),
13451355
output: %({
13461356
"@context": {"ex": {"@id": "http://example.org/"}},
13471357
"http://example.org/foo": "term"
1348-
})
1358+
}),
1359+
processingMode: "json-ld-1.1"
13491360
},
13501361
"does not compact using simple term not ending in gen-delim" => {
13511362
input: %({"http://example.org/foo": "term"}),
@@ -1411,15 +1422,6 @@
14111422
"ex:bar": "term"
14121423
})
14131424
},
1414-
"prefers compacting using simple term ending ':' in 1.1" => {
1415-
input: %({"http://example.org/foo/bar": "term"}),
1416-
context: %({"ex": "http://example.org/foo/", "xe:": "http://example.org/foo/"}),
1417-
output: %({
1418-
"@context": {"ex": "http://example.org/foo/", "xe:": "http://example.org/foo/"},
1419-
"xe:bar": "term"
1420-
}),
1421-
processingMode: 'json-ld-1.1'
1422-
},
14231425
}.each do |title, params|
14241426
it(title) {run_compact(params)}
14251427
end

0 commit comments

Comments
 (0)