json-ld 2.1.7 → 2.2.0

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: e6faa8c0a2b65b20cfeaa22ac3cba60cf98ae835
4
- data.tar.gz: 9e116d70b8081c39e5b725e9053a7b40b1ecfd57
3
+ metadata.gz: 93516bb49f7999bcda524b639c817eb197acc412
4
+ data.tar.gz: 3f3a0ce771608fb40714dd05a45c9fcdec5cf21a
5
5
  SHA512:
6
- metadata.gz: 4990f799a5dd3898ce3ea7b746a96f2bf84baab2fd7b90c7f08b360cd72bb8a7863bd31e56048ddfd68ca53b4a4c7510e090c9ce49aa637d4f11298020488881
7
- data.tar.gz: 6c551095ca9137161f52c8f978c650a144cae1c070df62501a5dbb0323b3ffda0f13a00221cd257babbc09bd00a0c21d99056eece1895d955c64326b6ea935a5
6
+ metadata.gz: 80be800ae35b9906c8f289eff34b7eaa84a4d637e883f1c8d1452a4516f98daa7a63b77bddacc8791e9caf4131018ea0994f4a023557e9e9cc0587234ad85d3e
7
+ data.tar.gz: 2234dbcec6f4701da4adb9fd3b90bbf6cb3c2918c9be1394756a2ddfd10c28931d44a911e6f1a5a2fbbb7082bf9631e0da100119cccd7a1af8c3463bf9c5be60
data/README.md CHANGED
@@ -298,6 +298,86 @@ The value of `@container` in a term definition can include `@id` or `@type`, in
298
298
  }
299
299
  }
300
300
 
301
+ ### @graph containers and maps
302
+ A term can have `@container` set to include `@graph` optionally including `@id` or `@index` and `@set`. In the first form, with `@container` set to `@graph`, the value of a property is treated as a _simple graph object_, meaning that values treated as if they were contained in an object with `@graph`, creating _named graph_ with an anonymous name.
303
+
304
+ {
305
+ "@context": {
306
+ "@vocab": "http://example.org/",
307
+ "input": {"@container": "@graph"}
308
+ },
309
+ "input": {
310
+ "value": "x"
311
+ }
312
+ }
313
+
314
+ which expands to the following:
315
+
316
+ [{
317
+ "http://example.org/input": [{
318
+ "@graph": [{
319
+ "http://example.org/value": [{"@value": "x"}]
320
+ }]
321
+ }]
322
+ }]
323
+
324
+ Compaction reverses this process, optionally ensuring that a single value is contained within an array of `@container` also includes `@set`:
325
+
326
+ {
327
+ "@context": {
328
+ "@vocab": "http://example.org/",
329
+ "input": {"@container": ["@graph", "@set"]}
330
+ }
331
+ }
332
+
333
+ A graph map uses the map form already existing for `@index`, `@language`, `@type`, and `@id` where the index is either an index value or an id.
334
+
335
+ {
336
+ "@context": {
337
+ "@vocab": "http://example.org/",
338
+ "input": {"@container": ["@graph", "@index"]}
339
+ },
340
+ "input": {
341
+ "g1": {"value": "x"}
342
+ }
343
+ }
344
+
345
+ treats "g1" as an index, and expands to the following:
346
+
347
+ [{
348
+ "http://example.org/input": [{
349
+ "@index": "g1",
350
+ "@graph": [{
351
+ "http://example.org/value": [{"@value": "x"}]
352
+ }]
353
+ }]
354
+ }])
355
+
356
+ This can also include `@set` to ensure that, when compacting, a single value of an index will be in array form.
357
+
358
+ The _id_ version is similar:
359
+
360
+ {
361
+ "@context": {
362
+ "@vocab": "http://example.org/",
363
+ "input": {"@container": ["@graph", "@id"]}
364
+ },
365
+ "input": {
366
+ "http://example.com/g1": {"value": "x"}
367
+ }
368
+ }
369
+
370
+ which expands to:
371
+
372
+ [{
373
+ "http://example.org/input": [{
374
+ "@id": "http://example.com/g1",
375
+ "@graph": [{
376
+ "http://example.org/value": [{"@value": "x"}]
377
+ }]
378
+ }]
379
+ }])
380
+
301
381
  ### Transparent Nesting
302
382
  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
383
 
data/VERSION CHANGED
@@ -1 +1 @@
1
- 2.1.7
1
+ 2.2.0
@@ -82,6 +82,7 @@ module JSON::LD
82
82
  if expanded_property == '@reverse'
83
83
  compacted_value = compact(expanded_value, property: '@reverse')
84
84
  #log_debug("@reverse") {"compacted_value: #{compacted_value.inspect}"}
85
+ # handle double-reversed properties
85
86
  compacted_value.each do |prop, value|
86
87
  if context.reverse?(prop)
87
88
  value = [value] if !value.is_a?(Array) &&
@@ -112,7 +113,7 @@ module JSON::LD
112
113
  next
113
114
  end
114
115
 
115
- if expanded_property == '@index' && context.container(property) == '@index'
116
+ if expanded_property == '@index' && context.container(property) == %w(@index)
116
117
  #log_debug("@index") {"drop @index"}
117
118
  next
118
119
  end
@@ -162,13 +163,20 @@ module JSON::LD
162
163
 
163
164
  container = context.container(item_active_property)
164
165
  as_array = context.as_array?(item_active_property)
165
- value = list?(expanded_item) ? expanded_item['@list'] : expanded_item
166
+
167
+ value = case
168
+ when list?(expanded_item) then expanded_item['@list']
169
+ when graph?(expanded_item) then expanded_item['@graph']
170
+ else expanded_item
171
+ end
172
+
166
173
  compacted_item = compact(value, property: item_active_property)
167
174
  #log_debug("") {" => compacted key: #{item_active_property.inspect} for #{compacted_item.inspect}"}
168
175
 
176
+ # handle @list
169
177
  if list?(expanded_item)
170
178
  compacted_item = [compacted_item] unless compacted_item.is_a?(Array)
171
- unless container == '@list'
179
+ unless container == %w(@list)
172
180
  al = context.compact_iri('@list', vocab: true, quiet: true)
173
181
  compacted_item = {al => compacted_item}
174
182
  if expanded_item.has_key?('@index')
@@ -178,25 +186,63 @@ module JSON::LD
178
186
  else
179
187
  raise JsonLdError::CompactionToListOfLists,
180
188
  "key cannot have more than one list value" if nest_result.has_key?(item_active_property)
189
+ # Falls through to add list value below
181
190
  end
182
191
  end
183
192
 
184
- if container == '@language' || container == '@index' || container == '@id' || container == '@type'
193
+ # Graph object compaction cases:
194
+ if graph?(expanded_item)
195
+ if container.include?('@graph') && container.include?('@id')
196
+ # container includes @graph and @id
197
+ map_object = nest_result[item_active_property] ||= {}
198
+ map_key = expanded_item['@id']
199
+ # If there is no @id, create a blank node identifier to use as an index
200
+ map_key = map_key ? context.compact_iri(map_key, quiet: true) : namer.get_name
201
+ merge_compacted_value(map_object, map_key, compacted_item)
202
+ elsif container.include?('@graph') && container.include?('@index') && simple_graph?(expanded_item)
203
+ # container includes @graph and @index and value is a simple graph object
204
+ map_object = nest_result[item_active_property] ||= {}
205
+ # If there is no @index, use @none
206
+ map_key = expanded_item['@index'] || '@none'
207
+ merge_compacted_value(map_object, map_key, compacted_item)
208
+ elsif container.include?('@graph') && simple_graph?(expanded_item)
209
+ # container includes @graph but not @id or @index and value is a simple graph object
210
+ # Drop through, where compacted_value will be added
211
+ compacted_item = [compacted_item] if
212
+ !compacted_item.is_a?(Array) && (!@options[:compactArrays] || as_array)
213
+ merge_compacted_value(nest_result, item_active_property, compacted_item)
214
+ else
215
+ # container does not include @graph or otherwise does not match one of the previous cases, redo compacted_item
216
+ compacted_item = [compacted_item]
217
+ al = context.compact_iri('@graph', vocab: true, quiet: true)
218
+ compacted_item = {al => compacted_item}
219
+ if expanded_item['@id']
220
+ al = context.compact_iri('@id', vocab: true, quiet: true)
221
+ compacted_item[al] = context.compact_iri(expanded_item['@id'], vocab: false, quiet: true).to_s
222
+ end
223
+ if expanded_item.has_key?('@index')
224
+ key = context.compact_iri('@index', vocab: true, quiet: true)
225
+ compacted_item[key] = expanded_item['@index']
226
+ end
227
+ compacted_item = [compacted_item] if !@options[:compactArrays] || as_array
228
+ merge_compacted_value(nest_result, item_active_property, compacted_item)
229
+ end
230
+ elsif !(container & %w(@language @index @id @type)).empty? && !container.include?('@graph')
185
231
  map_object = nest_result[item_active_property] ||= {}
186
232
  compacted_item = case container
187
- when '@id'
233
+ when %w(@id)
188
234
  id_prop = context.compact_iri('@id', vocab: true, quiet: true)
189
235
  map_key = compacted_item[id_prop]
190
236
  map_key = context.compact_iri(map_key, quiet: true)
191
237
  compacted_item.delete(id_prop)
192
238
  compacted_item
193
- when '@index'
194
- map_key = expanded_item[container]
239
+ when %w(@index)
240
+ map_key = expanded_item['@index']
195
241
  compacted_item
196
- when '@language'
197
- map_key = expanded_item[container]
242
+ when %w(@language)
243
+ map_key = expanded_item['@language']
198
244
  value?(expanded_item) ? expanded_item['@value'] : compacted_item
199
- when '@type'
245
+ when %w(@type)
200
246
  type_prop = context.compact_iri('@type', vocab: true, quiet: true)
201
247
  map_key, *types = Array(compacted_item[type_prop])
202
248
  map_key = context.compact_iri(map_key, vocab: true, quiet: true)
@@ -38,13 +38,9 @@ module JSON::LD
38
38
  attr_accessor :type_mapping
39
39
 
40
40
  # Base container mapping, without @set
41
- # @return ['@index', '@language', '@index', '@type', '@id'] Container mapping
41
+ # @return Array<'@index', '@language', '@index', '@set', '@type', '@id', '@graph'> Container mapping
42
42
  attr_reader :container_mapping
43
43
 
44
- # If container mapping was defined along with @set
45
- # @return [Boolean]
46
- attr_reader :as_set
47
-
48
44
  # @return [String] Term used for nest properties
49
45
  attr_accessor :nest
50
46
 
@@ -78,7 +74,7 @@ module JSON::LD
78
74
  # @param [String] term
79
75
  # @param [String] id
80
76
  # @param [String] type_mapping Type mapping
81
- # @param ['@index', '@language', '@index', '@set', '@type', '@id'] container_mapping
77
+ # @param [Array<'@index', '@language', '@index', '@set', '@type', '@id', '@graph'>] container_mapping
82
78
  # @param [String] language_mapping
83
79
  # Language mapping of term, `false` is used if there is explicitly no language mapping for this term
84
80
  # @param [Boolean] reverse_property
@@ -100,7 +96,7 @@ module JSON::LD
100
96
  @term = term
101
97
  @id = id.to_s unless id.nil?
102
98
  @type_mapping = type_mapping.to_s unless type_mapping.nil?
103
- self.container_mapping = container_mapping unless container_mapping.nil?
99
+ self.container_mapping = container_mapping
104
100
  @language_mapping = language_mapping unless language_mapping.nil?
105
101
  @reverse_property = reverse_property
106
102
  @nest = nest unless nest.nil?
@@ -116,7 +112,7 @@ module JSON::LD
116
112
  mapping = mapping.dup
117
113
  mapping.delete('@set')
118
114
  end
119
- @container_mapping = mapping.first
115
+ @container_mapping = mapping.sort
120
116
  end
121
117
 
122
118
  ##
@@ -147,7 +143,7 @@ module JSON::LD
147
143
  end
148
144
  end
149
145
 
150
- cm = [container_mapping, ('@set' if as_set)].compact
146
+ cm = (Array(container_mapping) + (as_set? ? %w(@set) : [])).compact
151
147
  cm = cm.first if cm.length == 1
152
148
  defn['@container'] = cm unless cm.empty?
153
149
  # Language set as false to be output as null
@@ -168,21 +164,26 @@ module JSON::LD
168
164
  %w(id type_mapping container_mapping language_mapping reverse_property nest simple prefix context).each do |acc|
169
165
  v = instance_variable_get("@#{acc}".to_sym)
170
166
  v = v.to_s if v.is_a?(RDF::Term)
171
- if acc == 'container_mapping' && as_set
172
- v = v ? [v, '@set'] : '@set'
167
+ if acc == 'container_mapping'
168
+ v.concat(%w(@set)) if as_set?
169
+ v = v.first if v.length <= 1
173
170
  end
174
171
  defn << "#{acc}: #{v.inspect}" if v
175
172
  end
176
173
  defn.join(', ') + ")"
177
174
  end
178
175
 
176
+ # If container mapping was defined along with @set
177
+ # @return [Boolean]
178
+ def as_set?; @as_set || false; end
179
+
179
180
  def inspect
180
181
  v = %w([TD)
181
182
  v << "id=#{@id}"
182
183
  v << "term=#{@term}"
183
184
  v << "rev" if reverse_property
184
185
  v << "container=#{container_mapping}" if container_mapping
185
- v << "as_set=#{as_set.inspect}"
186
+ v << "as_set=#{as_set?.inspect}"
186
187
  v << "lang=#{language_mapping.inspect}" unless language_mapping.nil?
187
188
  v << "type=#{type_mapping}" unless type_mapping.nil?
188
189
  v << "nest=#{nest.inspect}" unless nest.nil?
@@ -874,23 +875,22 @@ module JSON::LD
874
875
  # Retrieve container mapping, add it if `value` is provided
875
876
  #
876
877
  # @param [Term, #to_s] term in unexpanded form
877
- # @return [String]
878
+ # @return [Array<'@index', '@language', '@index', '@set', '@type', '@id', '@graph'>]
878
879
  def container(term)
879
- return '@set' if term == '@graph'
880
- return term if KEYWORDS.include?(term)
880
+ return [term] if KEYWORDS.include?(term)
881
881
  term = find_definition(term)
882
- term && term.container_mapping
882
+ term ? term.container_mapping : []
883
883
  end
884
884
 
885
885
  ##
886
- # Should values be represented as a set?
886
+ # Should values be represented using an array?
887
887
  #
888
888
  # @param [Term, #to_s] term in unexpanded form
889
889
  # @return [Boolean]
890
890
  def as_array?(term)
891
- return true if term == '@graph' || term == '@list'
891
+ return true if CONTEXT_CONTAINER_ARRAY_TERMS.include?(term)
892
892
  term = find_definition(term)
893
- term && (term.as_set || term.container_mapping == '@list')
893
+ term && (term.as_set? || term.container_mapping.include?('@list'))
894
894
  end
895
895
 
896
896
  ##
@@ -1122,6 +1122,11 @@ module JSON::LD
1122
1122
  tl_value = common_language
1123
1123
  end
1124
1124
  #log_debug("") {"list: containers: #{containers.inspect}, type/language: #{tl.inspect}, type/language value: #{tl_value.inspect}"} unless quiet
1125
+ elsif graph?(value)
1126
+ # TODO: support `@graphId`?
1127
+ # TODO: "@graph@set"?
1128
+ containers << '@graph'
1129
+ containers << '@set'
1125
1130
  else
1126
1131
  if value?(value)
1127
1132
  if value.has_key?('@language') && !index?(value)
@@ -1228,14 +1233,16 @@ module JSON::LD
1228
1233
  def expand_value(property, value, useNativeTypes: false, **options)
1229
1234
  #log_debug("expand_value") {"property: #{property.inspect}, value: #{value.inspect}"}
1230
1235
 
1236
+ td = term_definitions.fetch(property, TermDefinition.new(property))
1237
+
1231
1238
  # If the active property has a type mapping in active context that is @id, return a new JSON object containing a single key-value pair where the key is @id and the value is the result of using the IRI Expansion algorithm, passing active context, value, and true for document relative.
1232
- if (td = term_definitions.fetch(property, TermDefinition.new(property))) && td.type_mapping == '@id'
1239
+ if value.is_a?(String) && td.type_mapping == '@id'
1233
1240
  #log_debug("") {"as relative IRI: #{value.inspect}"}
1234
1241
  return {'@id' => expand_iri(value, documentRelative: true).to_s}
1235
1242
  end
1236
1243
 
1237
1244
  # If active property has a type mapping in active context that is @vocab, return a new JSON object containing a single key-value pair where the key is @id and the value is the result of using the IRI Expansion algorithm, passing active context, value, true for vocab, and true for document relative.
1238
- if td.type_mapping == '@vocab'
1245
+ if value.is_a?(String) && td.type_mapping == '@vocab'
1239
1246
  #log_debug("") {"as vocab IRI: #{value.inspect}"}
1240
1247
  return {'@id' => expand_iri(value, vocab: true, documentRelative: true).to_s}
1241
1248
  end
@@ -1271,15 +1278,14 @@ module JSON::LD
1271
1278
  # Otherwise, initialize result to a JSON object with an @value member whose value is set to value.
1272
1279
  res = {'@value' => value}
1273
1280
 
1274
- if td.type_mapping
1281
+ if td.type_mapping && !%w(@id @vocab).include?(td.type_mapping.to_s)
1275
1282
  res['@type'] = td.type_mapping.to_s
1276
- elsif value.is_a?(String)
1277
- if td.language_mapping
1278
- res['@language'] = td.language_mapping
1279
- elsif default_language && td.language_mapping.nil?
1280
- res['@language'] = default_language
1281
- end
1283
+ elsif value.is_a?(String) && td.language_mapping
1284
+ res['@language'] = td.language_mapping
1285
+ elsif value.is_a?(String) && default_language && td.language_mapping.nil?
1286
+ res['@language'] = default_language
1282
1287
  end
1288
+
1283
1289
  res
1284
1290
  end
1285
1291
 
@@ -1305,7 +1311,7 @@ module JSON::LD
1305
1311
 
1306
1312
  num_members = value.length
1307
1313
 
1308
- num_members -= 1 if index?(value) && container(property) == '@index'
1314
+ num_members -= 1 if index?(value) && container(property).include?('@index')
1309
1315
  if num_members > 2
1310
1316
  #log_debug("") {"can't compact value with # members > 2"}
1311
1317
  return value
@@ -1442,6 +1448,10 @@ module JSON::LD
1442
1448
 
1443
1449
  private
1444
1450
 
1451
+ CONTEXT_CONTAINER_ARRAY_TERMS = %w(@set @list @graph).freeze
1452
+ CONTEXT_CONTAINER_ID_GRAPH = %w(@id @graph).freeze
1453
+ CONTEXT_CONTAINER_INDEX_GRAPH = %w(@index @graph).freeze
1454
+
1445
1455
  def uri(value)
1446
1456
  case value.to_s
1447
1457
  when /^_:(.*)$/
@@ -1482,7 +1492,26 @@ module JSON::LD
1482
1492
  #
1483
1493
  # To make use of an inverse context, a list of preferred container mappings and the type mapping or language mapping are gathered for a particular value associated with an IRI. These parameters are then fed to the Term Selection algorithm, which will find the term that most appropriately matches the value's mappings.
1484
1494
  #
1495
+ # @example Basic structure of resulting inverse context
1496
+ # {
1497
+ # "http://example.com/term": {
1498
+ # "@language": {
1499
+ # "@null": "term",
1500
+ # "@none": "term",
1501
+ # "en": "term"
1502
+ # },
1503
+ # "@type": {
1504
+ # "@reverse": "term",
1505
+ # "@none": "term",
1506
+ # "http://datatype": "term"
1507
+ # },
1508
+ # "@any": {
1509
+ # "@none": "term",
1510
+ # }
1511
+ # }
1512
+ # }
1485
1513
  # @return [Hash{String => Hash{String => String}}]
1514
+ # @todo May want to include @set along with container to allow selecting terms using @set over those without @set. May require adding some notion of value cardinality to compact_iri
1486
1515
  def inverse_context
1487
1516
  @inverse_context ||= begin
1488
1517
  result = {}
@@ -1491,7 +1520,15 @@ module JSON::LD
1491
1520
  a.length == b.length ? (a <=> b) : (a.length <=> b.length)
1492
1521
  end.each do |term|
1493
1522
  next unless td = term_definitions[term]
1494
- container = td.container_mapping || (td.as_set ? '@set' : '@none')
1523
+
1524
+ container = Array(td.container_mapping).sort.first
1525
+ container ||= td.as_set? ? %(@set) : %(@none)
1526
+ # FIXME: Alternative to consider
1527
+ ## Creates "@language", "@language@set", "@set", or "@none"
1528
+ ## for each of "@language", "@index", "@type", "@id", "@list", and "@graph"
1529
+ #container = td.container_mapping.to_s
1530
+ #container += '@set' if td.as_set?
1531
+ #container = '@none' if container.empty?
1495
1532
  container_map = result[td.id.to_s] ||= {}
1496
1533
  tl_map = container_map[container] ||= {'@language' => {}, '@type' => {}, '@any' => {}}
1497
1534
  type_map = tl_map['@type']
@@ -1620,20 +1657,45 @@ module JSON::LD
1620
1657
  val = Array(container).dup
1621
1658
  val.delete('@set') if has_set = val.include?('@set')
1622
1659
 
1623
- raise JsonLdError::InvalidContainerMapping,
1624
- "'@container' has more than one value other than @set" if val.length > 1
1625
-
1626
- case val.first
1627
- when '@list'
1660
+ if val.include?('@list')
1628
1661
  raise JsonLdError::InvalidContainerMapping,
1629
- "'@container' on term #{term.inspect} cannot be both @list and @set" if has_set
1662
+ "'@container' on term #{term.inspect} using @list cannot have any other values" unless
1663
+ !has_set && val.length == 1
1630
1664
  # Okay
1631
- when '@language', '@index', nil
1665
+ elsif val.include?('@language')
1666
+ raise JsonLdError::InvalidContainerMapping,
1667
+ "unknown mapping for '@container' to #{container.inspect} on term #{term.inspect}" if
1668
+ has_set && (processingMode || 'json-ld-1.0') < 'json-ld-1.1'
1669
+ raise JsonLdError::InvalidContainerMapping,
1670
+ "'@container' on term #{term.inspect} using @language cannot have any values other than @set, found #{container.inspect}" unless
1671
+ val.length == 1
1672
+ # Okay
1673
+ elsif val.include?('@index')
1674
+ raise JsonLdError::InvalidContainerMapping,
1675
+ "unknown mapping for '@container' to #{container.inspect} on term #{term.inspect}" if
1676
+ has_set && (processingMode || 'json-ld-1.0') < 'json-ld-1.1'
1677
+ raise JsonLdError::InvalidContainerMapping,
1678
+ "'@container' on term #{term.inspect} using @index cannot have any values other than @set and/or @graph, found #{container.inspect}" unless
1679
+ (val - CONTEXT_CONTAINER_INDEX_GRAPH).empty?
1680
+ # Okay
1681
+ elsif val.include?('@id')
1682
+ raise JsonLdError::InvalidContainerMapping,
1683
+ "unknown mapping for '@container' to #{container.inspect} on term #{term.inspect}" if
1684
+ (processingMode || 'json-ld-1.0') < 'json-ld-1.1'
1685
+ raise JsonLdError::InvalidContainerMapping,
1686
+ "'@container' on term #{term.inspect} using @id cannot have any values other than @set and/or @graph, found #{container.inspect}" unless
1687
+ (val - CONTEXT_CONTAINER_ID_GRAPH).empty?
1632
1688
  # Okay
1633
- when '@type', '@id', nil
1689
+ elsif val.include?('@type') || val.include?('@graph')
1634
1690
  raise JsonLdError::InvalidContainerMapping,
1635
1691
  "unknown mapping for '@container' to #{container.inspect} on term #{term.inspect}" if
1636
1692
  (processingMode || 'json-ld-1.0') < 'json-ld-1.1'
1693
+ raise JsonLdError::InvalidContainerMapping,
1694
+ "'@container' on term #{term.inspect} using @language cannot have any values other than @set, found #{container.inspect}" unless
1695
+ val.length == 1
1696
+ # Okay
1697
+ elsif val.empty?
1698
+ # Okay
1637
1699
  else
1638
1700
  raise JsonLdError::InvalidContainerMapping,
1639
1701
  "unknown mapping for '@container' to #{container.inspect} on term #{term.inspect}"