shex 0.4.0 → 0.6.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -19,7 +19,7 @@ module ShEx::Algebra
19
19
  # Creates an operator instance from a parsed ShExJ representation
20
20
  # @param (see Operator#from_shexj)
21
21
  # @return [Operator]
22
- def self.from_shexj(operator, options = {})
22
+ def self.from_shexj(operator, **options)
23
23
  raise ArgumentError unless operator.is_a?(Hash) && operator['type'] == 'ShapeAnd'
24
24
  raise ArgumentError, "missing shapeExprs in #{operator.inspect}" unless operator.has_key?('shapeExprs')
25
25
  super
@@ -7,7 +7,7 @@ module ShEx::Algebra
7
7
  # Creates an operator instance from a parsed ShExJ representation
8
8
  # @param (see Operator#from_shexj)
9
9
  # @return [Operator]
10
- def self.from_shexj(operator, options = {})
10
+ def self.from_shexj(operator, **options)
11
11
  raise ArgumentError unless operator.is_a?(Hash) && operator['type'] == "Annotation"
12
12
  raise ArgumentError, "missing predicate in #{operator.inspect}" unless operator.has_key?('predicate')
13
13
  raise ArgumentError, "missing object in #{operator.inspect}" unless operator.has_key?('object')
@@ -8,7 +8,7 @@ module ShEx::Algebra
8
8
  # Creates an operator instance from a parsed ShExJ representation
9
9
  # @param (see Operator#from_shexj)
10
10
  # @return [Operator]
11
- def self.from_shexj(operator, options = {})
11
+ def self.from_shexj(operator, **options)
12
12
  raise ArgumentError unless operator.is_a?(Hash) && operator['type'] == 'EachOf'
13
13
  raise ArgumentError, "missing expressions in #{operator.inspect}" unless operator.has_key?('expressions')
14
14
  super
@@ -0,0 +1,22 @@
1
+ module ShEx::Algebra
2
+ ##
3
+ class Language < Operator::Unary
4
+ NAME = :language
5
+
6
+ ##
7
+ # matches any literal having a language tag that matches value
8
+ def match?(value, depth: 0)
9
+ status "", depth: depth
10
+ if case expr = operands.first
11
+ when RDF::Literal then value.language == expr.to_s.to_sym
12
+ else false
13
+ end
14
+ status "matched #{value}", depth: depth
15
+ true
16
+ else
17
+ status "not matched #{value}", depth: depth
18
+ false
19
+ end
20
+ end
21
+ end
22
+ end
@@ -1,3 +1,4 @@
1
+ # -*- encoding: utf-8 -*-
1
2
  module ShEx::Algebra
2
3
  ##
3
4
  class NodeConstraint < Operator
@@ -8,7 +9,7 @@ module ShEx::Algebra
8
9
  # Creates an operator instance from a parsed ShExJ representation
9
10
  # @param (see Operator#from_shexj)
10
11
  # @return [Operator]
11
- def self.from_shexj(operator, options = {})
12
+ def self.from_shexj(operator, **options)
12
13
  raise ArgumentError unless operator.is_a?(Hash) && operator['type'] == 'NodeConstraint'
13
14
  super
14
15
  end
@@ -58,7 +59,7 @@ module ShEx::Algebra
58
59
  return true unless dt
59
60
 
60
61
  not_satisfied "Node was #{value.inspect}, expected datatype #{dt}", depth: depth unless
61
- value.is_a?(RDF::Literal) && value.datatype == RDF::URI(dt)
62
+ value.is_a?(RDF::Literal) && value.datatype == RDF::URI(dt) && value.valid?
62
63
  status "right datatype: #{value}: #{dt}", depth: depth
63
64
  true
64
65
  end
@@ -68,11 +69,18 @@ module ShEx::Algebra
68
69
  # Checks all length/minlength/maxlength/pattern facets against the string representation of the value.
69
70
  # @return [Boolean] `true` if satisfied, `false` if it does not apply
70
71
  # @raise [ShEx::NotSatisfied] if not satisfied
72
+ # @todo using the XPath regexp engine supports additional flags "s" and "q"
71
73
  def satisfies_string_facet?(value, depth: 0)
72
74
  length = op_fetch(:length)
73
75
  minlength = op_fetch(:minlength)
74
76
  maxlength = op_fetch(:maxlength)
75
- pattern = op_fetch(:pattern)
77
+ pat = (operands.detect {|op| op.is_a?(Array) && op[0] == :pattern} || [])
78
+ pattern = pat[1]
79
+
80
+ flags = 0
81
+ flags |= Regexp::EXTENDED if pat[2].to_s.include?("x")
82
+ flags |= Regexp::IGNORECASE if pat[2].to_s.include?("i")
83
+ flags |= Regexp::MULTILINE if pat[2].to_s.include?("m")
76
84
 
77
85
  return true if (length || minlength || maxlength || pattern).nil?
78
86
 
@@ -80,6 +88,7 @@ module ShEx::Algebra
80
88
  when RDF::Node then value.id
81
89
  else value.to_s
82
90
  end
91
+
83
92
  not_satisfied "Node #{v_s.inspect} length not #{length}", depth: depth if
84
93
  length && v_s.length != length.to_i
85
94
  not_satisfied"Node #{v_s.inspect} length < #{minlength}", depth: depth if
@@ -87,7 +96,7 @@ module ShEx::Algebra
87
96
  not_satisfied "Node #{v_s.inspect} length > #{maxlength}", depth: depth if
88
97
  maxlength && v_s.length > maxlength.to_i
89
98
  not_satisfied "Node #{v_s.inspect} does not match #{pattern}", depth: depth if
90
- pattern && !Regexp.new(pattern).match(v_s)
99
+ pattern && !Regexp.new(pattern, flags).match(v_s)
91
100
  status "right string facet: #{value}", depth: depth
92
101
  true
93
102
  end
@@ -8,7 +8,7 @@ module ShEx::Algebra
8
8
  # Creates an operator instance from a parsed ShExJ representation
9
9
  # @param (see Operator#from_shexj)
10
10
  # @return [Operator]
11
- def self.from_shexj(operator, options = {})
11
+ def self.from_shexj(operator, **options)
12
12
  raise ArgumentError unless operator.is_a?(Hash) && operator['type'] == 'ShapeNot'
13
13
  raise ArgumentError, "missing shapeExpr in #{operator.inspect}" unless operator.has_key?('shapeExpr')
14
14
  super
@@ -19,7 +19,7 @@ module ShEx::Algebra
19
19
  # @param (see ShapeExpression#satisfies?)
20
20
  # @return (see ShapeExpression#satisfies?)
21
21
  # @raise (see ShapeExpression#satisfies?)
22
- # @see [https://shexspec.github.io/spec/#shape-expression-semantics]
22
+ # @see [http://shex.io/shex-semantics/#shape-expression-semantics]
23
23
  def satisfies?(focus, depth: 0)
24
24
  status ""
25
25
  op = expressions.last
@@ -8,7 +8,7 @@ module ShEx::Algebra
8
8
  # Creates an operator instance from a parsed ShExJ representation
9
9
  # @param (see Operator#from_shexj)
10
10
  # @return [Operator]
11
- def self.from_shexj(operator, options = {})
11
+ def self.from_shexj(operator, **options)
12
12
  raise ArgumentError unless operator.is_a?(Hash) && operator['type'] == 'OneOf'
13
13
  raise ArgumentError, "missing expressions in #{operator.inspect}" unless operator.has_key?('expressions')
14
14
  super
@@ -26,7 +26,7 @@ module ShEx::Algebra
26
26
  # @overload initialize(*operands)
27
27
  # @param [Array<RDF::Term>] operands
28
28
  #
29
- # @overload initialize(*operands, options)
29
+ # @overload initialize(*operands, **options)
30
30
  # @param [Array<RDF::Term>] operands
31
31
  # @param [Hash{Symbol => Object}] options
32
32
  # any additional options
@@ -244,7 +244,7 @@ module ShEx::Algebra
244
244
  # @return [String]
245
245
  def to_sxp
246
246
  begin
247
- require 'sxp' # @see http://rubygems.org/gems/sxp
247
+ require 'sxp' # @see https://rubygems.org/gems/sxp
248
248
  rescue LoadError
249
249
  abort "SPARQL::Algebra::Operator#to_sxp requires the SXP gem (hint: `gem install sxp')."
250
250
  end
@@ -260,24 +260,29 @@ module ShEx::Algebra
260
260
  # @option options [RDF::URI] :base
261
261
  # @option options [Hash{String => RDF::URI}] :prefixes
262
262
  # @return [Operator]
263
- def self.from_shexj(operator, options = {})
263
+ def self.from_shexj(operator, **options)
264
264
  options[:context] ||= JSON::LD::Context.parse(ShEx::CONTEXT)
265
265
  operands = []
266
266
  id = nil
267
267
 
268
268
  operator.each do |k, v|
269
269
  case k
270
- when /length|pattern|clusive|digits/ then operands << [k.to_sym, RDF::Literal(v)]
271
- when 'id' then id = iri(v, options)
272
- when 'min', 'max' then operands << [k.to_sym, v]
270
+ when /length|clusive|digits/ then operands << [k.to_sym, RDF::Literal(v)]
271
+ when 'id' then id = iri(v, **options)
272
+ when 'flags' then ; # consumed in pattern below
273
+ when 'min', 'max' then operands << [k.to_sym, (v == -1 ? '*' : v)]
273
274
  when 'inverse', 'closed' then operands << k.to_sym
274
275
  when 'nodeKind' then operands << v.to_sym
275
- when 'object' then operands << value(v, options)
276
+ when 'object' then operands << value(v, **options)
277
+ when 'languageTag' then operands << v
278
+ when 'pattern'
279
+ # Include flags as well
280
+ operands << [:pattern, RDF::Literal(v), operator['flags']].compact
276
281
  when 'start'
277
282
  if v.is_a?(String)
278
- operands << Start.new(iri(v, options))
283
+ operands << Start.new(iri(v, **options))
279
284
  else
280
- operands << Start.new(ShEx::Algebra.from_shexj(v, options))
285
+ operands << Start.new(ShEx::Algebra.from_shexj(v, **options))
281
286
  end
282
287
  when '@context' then
283
288
  options[:context] = JSON::LD::Context.parse(v)
@@ -285,39 +290,47 @@ module ShEx::Algebra
285
290
  when 'shapes'
286
291
  operands << case v
287
292
  when Array
288
- [:shapes] + v.map {|vv| ShEx::Algebra.from_shexj(vv, options)}
293
+ [:shapes] + v.map {|vv| ShEx::Algebra.from_shexj(vv, **options)}
289
294
  else
290
295
  raise "Expected value of shapes #{v.inspect}"
291
296
  end
292
297
  when 'stem', 'name'
293
298
  # Value may be :wildcard for stem
294
- operands << (v.is_a?(Symbol) ? v : iri(v, options))
295
- when 'predicate' then operands << [:predicate, iri(v, options)]
299
+ if [IriStem, IriStemRange, SemAct].include?(self)
300
+ operands << (v.is_a?(Symbol) ? v : value(v, **options))
301
+ else
302
+ operands << v
303
+ end
304
+ when 'predicate' then operands << [:predicate, iri(v, **options)]
296
305
  when 'extra', 'datatype'
297
306
  v = [v] unless v.is_a?(Array)
298
- operands << (v.map {|op| iri(op, options)}).unshift(k.to_sym)
307
+ operands << (v.map {|op| iri(op, **options)}).unshift(k.to_sym)
299
308
  when 'exclusions'
300
309
  v = [v] unless v.is_a?(Array)
301
310
  operands << v.map do |op|
302
- op.is_a?(Hash) ?
303
- ShEx::Algebra.from_shexj(op, options) :
304
- value(op, options)
311
+ if op.is_a?(Hash) && op.has_key?('type')
312
+ ShEx::Algebra.from_shexj(op, **options)
313
+ elsif [IriStem, IriStemRange].include?(self)
314
+ value(op, **options)
315
+ else
316
+ RDF::Literal(op)
317
+ end
305
318
  end.unshift(:exclusions)
306
319
  when 'semActs', 'startActs', 'annotations'
307
320
  v = [v] unless v.is_a?(Array)
308
- operands += v.map {|op| ShEx::Algebra.from_shexj(op, options)}
321
+ operands += v.map {|op| ShEx::Algebra.from_shexj(op, **options)}
309
322
  when 'expression', 'expressions', 'shapeExpr', 'shapeExprs', 'valueExpr'
310
323
  v = [v] unless v.is_a?(Array)
311
324
  operands += v.map do |op|
312
325
  # It's a URI reference to a Shape
313
- op.is_a?(String) ? iri(op, options) : ShEx::Algebra.from_shexj(op, options)
326
+ op.is_a?(String) ? iri(op, **options) : ShEx::Algebra.from_shexj(op, **options)
314
327
  end
315
328
  when 'code'
316
329
  operands << v
317
330
  when 'values'
318
331
  v = [v] unless v.is_a?(Array)
319
332
  operands += v.map do |op|
320
- Value.new(value(op, options))
333
+ Value.new(value(op, **options))
321
334
  end
322
335
  end
323
336
  end
@@ -345,11 +358,18 @@ module ShEx::Algebra
345
358
  when Array
346
359
  # First element should be a symbol
347
360
  case sym = op.first
348
- when :datatype,
349
- :pattern then obj[op.first.to_s] = op.last.to_s
350
- when :exclusions then obj['exclusions'] = Array(op[1..-1]).map {|v| serialize_value(v)}
361
+ when :datatype then obj['datatype'] = op.last.to_s
362
+ when :exclusions
363
+ obj['exclusions'] = Array(op[1..-1]).map do |v|
364
+ case v
365
+ when Operator then v.to_h
366
+ else v.to_s
367
+ end
368
+ end
351
369
  when :extra then (obj['extra'] ||= []).concat Array(op[1..-1]).map(&:to_s)
352
- # FIXME Shapes should be an array, not a hash
370
+ when :pattern
371
+ obj['pattern'] = op[1]
372
+ obj['flags'] = op[2] if op[2]
353
373
  when :shapes then obj['shapes'] = Array(op[1..-1]).map {|v| v.to_h}
354
374
  when :minlength,
355
375
  :maxlength,
@@ -360,7 +380,7 @@ module ShEx::Algebra
360
380
  :maxexclusive,
361
381
  :totaldigits,
362
382
  :fractiondigits then obj[op.first.to_s] = op.last.object
363
- when :min, :max then obj[op.first.to_s] = op.last
383
+ when :min, :max then obj[op.first.to_s] = op.last == '*' ? -1 : op.last
364
384
  when :predicate then obj[op.first.to_s] = op.last.to_s
365
385
  when :base, :prefix
366
386
  # Ignore base and prefix
@@ -378,13 +398,18 @@ module ShEx::Algebra
378
398
  end
379
399
  when RDF::Value
380
400
  case self
381
- when Stem, StemRange then obj['stem'] = op.to_s
401
+ when Stem, StemRange
402
+ obj['stem'] = case op
403
+ when Operator then op.to_h
404
+ else op.to_s
405
+ end
382
406
  when SemAct then obj[op.is_a?(RDF::URI) ? 'name' : 'code'] = op.to_s
383
407
  when TripleConstraint then obj['valueExpr'] = op.to_s
384
408
  when Shape then obj['expression'] = op.to_s
385
409
  when EachOf, OneOf then (obj['expressions'] ||= []) << op.to_s
386
410
  when And, Or then (obj['shapeExprs'] ||= []) << op.to_s
387
411
  when Not then obj['shapeExpr'] = op.to_s
412
+ when Language then obj['languageTag'] = op.to_s
388
413
  else
389
414
  raise "How to serialize Value #{op.inspect} to json for #{self}"
390
415
  end
@@ -451,14 +476,14 @@ module ShEx::Algebra
451
476
  # @option options [JSON::LD::Context] :context
452
477
  # @return [RDF::Value]
453
478
  def iri(value, options = @options)
454
- self.class.iri(value, options)
479
+ self.class.iri(value, **options)
455
480
  end
456
481
 
457
482
  # Create URIs
458
483
  # @param (see #iri)
459
484
  # @option (see #iri)
460
485
  # @return (see #iri)
461
- def self.iri(value, options)
486
+ def self.iri(value, **options)
462
487
  # If we have a base URI, use that when constructing a new URI
463
488
  base_uri = options[:base_uri]
464
489
 
@@ -467,7 +492,7 @@ module ShEx::Algebra
467
492
  # A JSON-LD node reference
468
493
  v = options[:context].expand_value(value)
469
494
  raise "Expected #{value.inspect} to be a JSON-LD Node Reference" unless JSON::LD::Utils.node_reference?(v)
470
- self.iri(v['@id'], options)
495
+ self.iri(v['@id'], **options)
471
496
  when RDF::URI
472
497
  if base_uri && value.relative?
473
498
  base_uri.join(value)
@@ -505,26 +530,26 @@ module ShEx::Algebra
505
530
  # @option options [Hash{String => RDF::URI}] :prefixes
506
531
  # @return [RDF::Value]
507
532
  def value(value, options = @options)
508
- self.class.value(value, options)
533
+ self.class.value(value, **options)
509
534
  end
510
535
 
511
536
  # Create Values, with "clever" matching to see if it might be a value, IRI or BNode.
512
537
  # @param (see #value)
513
538
  # @option (see #value)
514
539
  # @return (see #value)
515
- def self.value(value, options)
540
+ def self.value(value, **options)
516
541
  # If we have a base URI, use that when constructing a new URI
517
542
  case value
518
543
  when Hash
519
544
  # Either a value object or a node reference
520
- if value['uri']
521
- iri(value['uri'], options)
522
- elsif value['value']
523
- RDF::Literal(value['value'], datatype: value['type'], language: value['language'])
545
+ if value['uri'] || value['@id']
546
+ iri(value['uri'] || value['@id'], **options)
547
+ elsif value['value'] || value['@value']
548
+ RDF::Literal(value['value'] || value['@value'], datatype: value['type'] || value['@type'], language: value['language'] || value['@language'])
524
549
  else
525
- ShEx::Algebra.from_shexj(value, options)
550
+ ShEx::Algebra.from_shexj(value, **options)
526
551
  end
527
- else iri(value, options)
552
+ else iri(value, **options)
528
553
  end
529
554
  end
530
555
 
@@ -541,6 +566,8 @@ module ShEx::Algebra
541
566
  merge(value.has_language? ? {'language' => value.language.to_s} : {})
542
567
  when RDF::Resource
543
568
  value.to_s
569
+ when String
570
+ {'value' => value}
544
571
  else value.to_h
545
572
  end
546
573
  end
@@ -566,7 +593,6 @@ module ShEx::Algebra
566
593
 
567
594
  ##
568
595
  # Enumerate via depth-first recursive descent over operands, yielding each operator
569
- # @param [Integer] depth incrementeded for each depth of operator, and provided to block if Arity is 2
570
596
  # @yield operator
571
597
  # @yieldparam [Object] operator
572
598
  # @return [Enumerator]
@@ -634,12 +660,12 @@ module ShEx::Algebra
634
660
  self
635
661
  end
636
662
 
637
- protected
638
- def dup
639
- operands = @operands.map {|o| o.dup rescue o}
640
- self.class.new(*operands, id: @id)
641
- end
663
+ def dup
664
+ operands = @operands.map {|o| o.dup rescue o}
665
+ self.class.new(*operands, id: @id)
666
+ end
642
667
 
668
+ protected
643
669
  ##
644
670
  # A unary operator.
645
671
  #
@@ -654,7 +680,7 @@ module ShEx::Algebra
654
680
  # the first operand
655
681
  # @param [Hash{Symbol => Object}] options
656
682
  # any additional options (see {Operator#initialize})
657
- def initialize(arg1, options = {})
683
+ def initialize(arg1, **options)
658
684
  raise ArgumentError, "wrong number of arguments (given 2, expected 1)" unless options.is_a?(Hash)
659
685
  super
660
686
  end
@@ -676,7 +702,7 @@ module ShEx::Algebra
676
702
  # the second operand
677
703
  # @param [Hash{Symbol => Object}] options
678
704
  # any additional options (see {Operator#initialize})
679
- def initialize(arg1, arg2, options = {})
705
+ def initialize(arg1, arg2, **options)
680
706
  raise ArgumentError, "wrong number of arguments (given 3, expected 2)" unless options.is_a?(Hash)
681
707
  super
682
708
  end
@@ -19,7 +19,7 @@ module ShEx::Algebra
19
19
  # Creates an operator instance from a parsed ShExJ representation
20
20
  # @param (see Operator#from_shexj)
21
21
  # @return [Operator]
22
- def self.from_shexj(operator, options = {})
22
+ def self.from_shexj(operator, **options)
23
23
  raise ArgumentError unless operator.is_a?(Hash) && operator['type'] == 'ShapeOr'
24
24
  raise ArgumentError, "missing shapeExprs in #{operator.inspect}" unless operator.is_a?(Hash) && operator.has_key?('shapeExprs')
25
25
  super
@@ -19,7 +19,7 @@ module ShEx::Algebra
19
19
  # Creates an operator instance from a parsed ShExJ representation
20
20
  # @param (see Operator#from_shexj)
21
21
  # @return [Operator]
22
- def self.from_shexj(operator, options = {})
22
+ def self.from_shexj(operator, **options)
23
23
  raise ArgumentError unless operator.is_a?(Hash) && operator['type'] == "Schema"
24
24
  super
25
25
  end
@@ -36,19 +36,22 @@ module ShEx::Algebra
36
36
  ##
37
37
  # Match on schema. Finds appropriate shape for node, and matches that shape.
38
38
  #
39
- # @param [RDF::Term] focus
40
39
  # @param [RDF::Queryable] graph
41
- # @param [Hash{RDF::Resource => RDF::Resource}] map
40
+ # @param [Hash{RDF::Term => <RDF::Resource>}, Array<Array(RDF::Term, RDF::Resource)>] map
41
+ # A set of (`term`, `resource`) pairs where `term` is a node within `graph`, and `resource` identifies a shape
42
+ # @param [Array<RDF::Term>] focus ([])
43
+ # One or more nodes within `graph` for which to run the start expression.
42
44
  # @param [Array<Schema, String>] shapeExterns ([])
43
45
  # One or more schemas, or paths to ShEx schema resources used for finding external shapes.
44
- # @return [Operand] Returns operand graph annotated with satisfied and unsatisfied operations.
46
+ # @return [Hash{RDF::Term => Array<ShapeResult>}] Returns _ShapeResults_, a hash of graph nodes to the results of their associated shapes
45
47
  # @param [Hash{Symbol => Object}] options
46
48
  # @option options [String] :base_uri (for resolving focus)
47
- # @raise [ShEx::NotSatisfied] along with operand graph described for return
48
- def execute(focus, graph, map, shapeExterns: [], depth: 0, **options)
49
- @graph, @shapes_entered = graph, {}
49
+ # @raise [ShEx::NotSatisfied] along with individual shape results
50
+ def execute(graph, map, focus: [], shapeExterns: [], depth: 0, **options)
51
+ @graph, @shapes_entered, results = graph, {}, {}
50
52
  @external_schemas = shapeExterns
51
- focus = value(focus, options)
53
+ @extensions = {}
54
+ focus = Array(focus).map {|f| value(f, **options)}
52
55
 
53
56
  logger = options[:logger] || @options[:logger]
54
57
  each_descendant do |op|
@@ -57,7 +60,6 @@ module ShEx::Algebra
57
60
  end
58
61
 
59
62
  # Initialize Extensions
60
- @extensions = {}
61
63
  each_descendant do |op|
62
64
  next unless op.is_a?(SemAct)
63
65
  name = op.operands.first.to_s
@@ -67,36 +69,74 @@ module ShEx::Algebra
67
69
  end
68
70
 
69
71
  # If `n` is a Blank Node, we won't find it through normal matching, find an equivalent node in the graph having the same id
70
- graph_focus = graph.enum_term.detect {|t| t.node? && t.id == focus.id} if focus.is_a?(RDF::Node)
71
- graph_focus ||= focus
72
-
73
- # Make sure they're URIs
74
- @map = (map || {}).inject({}) {|memo, (k,v)| memo.merge(value(k) => iri(v))}
72
+ @map = case map
73
+ when Hash
74
+ map.inject({}) do |memo, (node, shapes)|
75
+ gnode = graph.enum_term.detect {|t| t.node? && t.id == node.id} if node.is_a?(RDF::Node)
76
+ node = gnode if gnode
77
+ memo.merge(node => Array(shapes))
78
+ end
79
+ when Array
80
+ map.inject({}) do |memo, (node, shape)|
81
+ gnode = graph.enum_term.detect {|t| t.node? && t.id == node.id} if node.is_a?(RDF::Node)
82
+ node = gnode if gnode
83
+ (memo[node] ||= []).concat(Array(shape))
84
+ memo
85
+ end
86
+ when nil then {}
87
+ else
88
+ structure_error "Unrecognized shape map: #{map.inspect}"
89
+ end
75
90
 
76
91
  # First, evaluate semantic acts
77
92
  semantic_actions.all? do |op|
78
93
  op.satisfies?([], depth: depth + 1)
79
94
  end
80
95
 
81
- # Keep a new Schema, specifically for recording actions
82
- satisfied_schema = Schema.new
83
96
  # Next run any start expression
84
- if start
85
- satisfied_schema.operands << start.satisfies?(focus, depth: depth + 1)
97
+ if !focus.empty?
98
+ if start
99
+ focus.each do |node|
100
+ node = graph.enum_term.detect {|t| t.node? && t.id == node.id} if node.is_a?(RDF::Node)
101
+ sr = ShapeResult.new(RDF::URI("http://www.w3.org/ns/shex#Start"))
102
+ (results[node] ||= []) << sr
103
+ begin
104
+ sr.expression = start.satisfies?(node, depth: depth + 1)
105
+ sr.result = true
106
+ rescue ShEx::NotSatisfied => e
107
+ sr.expression = e.expression
108
+ sr.result = false
109
+ end
110
+ end
111
+ else
112
+ structure_error "Focus nodes with no start"
113
+ end
86
114
  end
87
115
 
88
- # Add shape result(s)
89
- satisfied_shapes = {}
90
- satisfied_schema.operands << [:shapes, satisfied_shapes] unless shapes.empty?
91
-
92
116
  # Match against all shapes associated with the ids for focus
93
- Array(@map[focus]).each do |id|
94
- enter_shape(id, focus) do |shape|
95
- satisfied_shapes[id] = shape.satisfies?(graph_focus, depth: depth + 1)
117
+ @map.each do |node, shapes|
118
+ results[node] ||= []
119
+ shapes.each do |id|
120
+ enter_shape(id, node) do |shape|
121
+ sr = ShapeResult.new(id)
122
+ results[node] << sr
123
+ begin
124
+ sr.expression = shape.satisfies?(node, depth: depth + 1)
125
+ sr.result = true
126
+ rescue ShEx::NotSatisfied => e
127
+ sr.expression = e.expression
128
+ sr.result = false
129
+ end
130
+ end
96
131
  end
97
132
  end
98
- status "schema satisfied", depth: depth
99
- satisfied_schema
133
+
134
+ if results.values.flatten.all? {|sr| sr.result}
135
+ status "schema satisfied", depth: depth
136
+ results
137
+ else
138
+ raise ShEx::NotSatisfied.new("Graph does not conform to schema", expression: results)
139
+ end
100
140
  ensure
101
141
  # Close Semantic Action extensions
102
142
  @extensions.values.each {|ext| ext.close(schema: self, depth: depth, **options)}
@@ -105,16 +145,12 @@ module ShEx::Algebra
105
145
  ##
106
146
  # Match on schema. Finds appropriate shape for node, and matches that shape.
107
147
  #
108
- # @param [RDF::Resource] focus
109
- # @param [RDF::Queryable] graph
110
- # @param [Hash{RDF::Resource => RDF::Resource}] map
111
- # @param [Array<Schema, String>] shapeExterns ([])
112
- # One or more schemas, or paths to ShEx schema resources used for finding external shapes.
148
+ # @param (see ShEx::Algebra::Schema#execute)
113
149
  # @param [Hash{Symbol => Object}] options
114
150
  # @option options [String] :base_uri
115
151
  # @return [Boolean]
116
- def satisfies?(focus, graph, map, shapeExterns: [], **options)
117
- execute(focus, graph, map, options.merge(shapeExterns: shapeExterns))
152
+ def satisfies?(graph, map, **options)
153
+ execute(graph, map, **options)
118
154
  rescue ShEx::NotSatisfied
119
155
  false
120
156
  end
@@ -198,4 +234,41 @@ module ShEx::Algebra
198
234
  super
199
235
  end
200
236
  end
237
+
238
+ # A shape result
239
+ class ShapeResult
240
+ # The label of the shape within the schema, or a URI indicating a start shape
241
+ # @return [RDF::Resource]
242
+ attr_reader :shape
243
+
244
+ # Does the node conform to the shape
245
+ # @return [Boolean]
246
+ attr_accessor :result
247
+
248
+ # The annotated {Operator} indicating processing results
249
+ # @return [ShEx::Algebra::Operator]
250
+ attr_accessor :expression
251
+
252
+ # Holds the result of processing a shape
253
+ # @param [RDF::Resource] shape
254
+ # @return [ShapeResult]
255
+ def initialize(shape)
256
+ @shape = shape
257
+ end
258
+
259
+ # The SXP of {#expression}
260
+ # @return [String]
261
+ def reason
262
+ SXP::Generator.string(expression.to_sxp_bin)
263
+ end
264
+
265
+ ##
266
+ # Returns the binary S-Expression (SXP) representation of this result.
267
+ #
268
+ # @return [Array]
269
+ # @see https://en.wikipedia.org/wiki/S-expression
270
+ def to_sxp_bin
271
+ [:ShapeResult, shape, result, expression].map(&:to_sxp_bin)
272
+ end
273
+ end
201
274
  end