shex 0.2.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -6,19 +6,22 @@ module ShEx::Algebra
6
6
 
7
7
  #
8
8
  # S is a ShapeRef and the Schema's shapes maps reference to a shape expression se2 and satisfies(n, se2, G, m).
9
- def satisfies?(focus)
9
+ def satisfies?(focus, depth: 0)
10
10
  extern_shape = nil
11
11
 
12
12
  # Find the label for this external
13
- label = schema.shapes.key(self)
14
- not_satisfied("Can't find label for this extern") unless label
13
+ not_satisfied("Can't find label for this extern", depth: depth) unless label
15
14
 
16
15
  schema.external_schemas.each do |schema|
17
- extern_shape ||= schema.shapes[label]
16
+ extern_shape ||= schema.shapes.detect {|s| s.label == label}
18
17
  end
19
18
 
20
- not_satisfied("External not configured for this shape") unless extern_shape
21
- extern_shape.satisfies?(focus)
19
+ not_satisfied("External not configured for this shape", depth: depth) unless extern_shape
20
+ extern_shape.satisfies?(focus, depth: depth + 1)
21
+ end
22
+
23
+ def json_type
24
+ "ShapeExternal"
22
25
  end
23
26
  end
24
27
  end
@@ -4,6 +4,16 @@ module ShEx::Algebra
4
4
  include TripleExpression
5
5
  NAME = :inclusion
6
6
 
7
+ ##
8
+ # Creates an operator instance from a parsed ShExJ representation
9
+ # @param (see Operator#from_shexj)
10
+ # @return [Operator]
11
+ def self.from_shexj(operator, options = {})
12
+ raise ArgumentError unless operator.is_a?(Hash) && operator['type'] == "Inclusion"
13
+ raise ArgumentError, "missing include in #{operator.inspect}" unless operator.has_key?('include')
14
+ super
15
+ end
16
+
7
17
  def initialize(arg, **options)
8
18
  raise ArgumentError, "Shape inclusion must be an IRI or BNode: #{arg}" unless arg.is_a?(RDF::Resource)
9
19
  super
@@ -12,17 +22,17 @@ module ShEx::Algebra
12
22
  ##
13
23
  # In this case, we accept an array of statements, and match based on cardinality.
14
24
  #
15
- # @param [Array<RDF::Statement>] statements
16
- # @return [Array<RDF::Statement>]
17
- # @raise [ShEx::NotMatched]
18
- def matches(statements)
25
+ # @param (see TripleExpression#matches)
26
+ # @return (see TripleExpression#matches)
27
+ # @raise (see TripleExpression#matches)
28
+ def matches(arcs_in, arcs_out, depth: 0)
19
29
  status "referenced_shape: #{operands.first}"
20
- expression = referenced_shape.triple_expressions.first
30
+ expression = referenced_shape.expression
21
31
  max = maximum
22
- matched_expression = expression.matches(statements)
23
- satisfy matched: matched_expression.matched
32
+ matched_expression = expression.matches(arcs_in, arcs_out, depth: depth + 1)
33
+ satisfy matched: matched_expression.matched, depth: depth
24
34
  rescue ShEx::NotMatched => e
25
- not_matched e.message, unsatisfied: e.expression
35
+ not_matched e.message, unsatisfied: e.expression, depth: depth
26
36
  end
27
37
 
28
38
  ##
@@ -30,7 +40,7 @@ module ShEx::Algebra
30
40
  #
31
41
  # @return [Operand]
32
42
  def referenced_shape
33
- schema.shapes[operands.first.to_s]
43
+ @referenced_shape ||= schema.shapes.detect {|s| s.label == operands.first}
34
44
  end
35
45
 
36
46
  ##
@@ -41,16 +51,17 @@ module ShEx::Algebra
41
51
  def validate!
42
52
  structure_error("Missing included shape: #{operands.first}") if referenced_shape.nil?
43
53
  structure_error("Self included shape: #{operands.first}") if referenced_shape == first_ancestor(Shape)
44
-
45
- triple_expressions = referenced_shape.triple_expressions
46
- case triple_expressions.length
47
- when 0
48
- structure_error("Includes shape with no triple expressions")
49
- when 1
50
- else
51
- structure_error("Includes shape with multiple triple expressions")
52
- end
54
+ structure_error("Referenced shape must be a Shape: #{operands.first}") unless referenced_shape.is_a?(Shape)
53
55
  super
54
56
  end
57
+
58
+ ##
59
+ # Returns the binary S-Expression (SXP) representation of this operator.
60
+ #
61
+ # @return [Array]
62
+ # @see https://en.wikipedia.org/wiki/S-expression
63
+ def to_sxp_bin
64
+ ([:inclusion, ([:label, @label] if @label)].compact + operands).to_sxp_bin
65
+ end
55
66
  end
56
67
  end
@@ -4,19 +4,28 @@ module ShEx::Algebra
4
4
  include Satisfiable
5
5
  NAME = :nodeConstraint
6
6
 
7
+ ##
8
+ # Creates an operator instance from a parsed ShExJ representation
9
+ # @param (see Operator#from_shexj)
10
+ # @return [Operator]
11
+ def self.from_shexj(operator, options = {})
12
+ raise ArgumentError unless operator.is_a?(Hash) && operator['type'] == 'NodeConstraint'
13
+ super
14
+ end
15
+
7
16
  #
8
17
  # S is a NodeConstraint and satisfies2(focus, se) as described below in Node Constraints. Note that testing if a node satisfies a node constraint does not require a graph or shapeMap.
9
18
  # @param (see Satisfiable#satisfies?)
10
19
  # @return (see Satisfiable#satisfies?)
11
20
  # @raise (see Satisfiable#satisfies?)
12
- def satisfies?(focus)
13
- status ""
14
- satisfies_node_kind?(focus) &&
15
- satisfies_datatype?(focus) &&
16
- satisfies_string_facet?(focus) &&
17
- satisfies_numeric_facet?(focus) &&
18
- satisfies_values?(focus) &&
19
- satisfy
21
+ def satisfies?(focus, depth: 0)
22
+ status "", depth: depth
23
+ satisfies_node_kind?(focus, depth: depth + 1) &&
24
+ satisfies_datatype?(focus, depth: depth + 1) &&
25
+ satisfies_string_facet?(focus, depth: depth + 1) &&
26
+ satisfies_numeric_facet?(focus, depth: depth + 1) &&
27
+ satisfies_values?(focus, depth: depth + 1) &&
28
+ satisfy(depth: depth)
20
29
  end
21
30
 
22
31
  private
@@ -25,8 +34,8 @@ module ShEx::Algebra
25
34
  # Satisfies Node Kind Constraint
26
35
  # @return [Boolean] `true` if satisfied, `false` if it does not apply
27
36
  # @raise [ShEx::NotSatisfied] if not satisfied
28
- def satisfies_node_kind?(value)
29
- kind = case operands.first
37
+ def satisfies_node_kind?(value, depth: 0)
38
+ kind = case operands.detect {|o| o.is_a?(Symbol)}
30
39
  when :iri then RDF::URI
31
40
  when :bnode then RDF::Node
32
41
  when :literal then RDF::Literal
@@ -34,9 +43,9 @@ module ShEx::Algebra
34
43
  else return true
35
44
  end
36
45
 
37
- not_satisfied "Node was #{value.inspect} expected kind #{kind}" unless
46
+ not_satisfied "Node was #{value.inspect} expected kind #{kind}", depth: depth unless
38
47
  value.is_a?(kind)
39
- status "right kind: #{value}: #{kind}"
48
+ status "right kind: #{value}: #{kind}", depth: depth
40
49
  true
41
50
  end
42
51
 
@@ -44,13 +53,13 @@ module ShEx::Algebra
44
53
  # Datatype Constraint
45
54
  # @return [Boolean] `true` if satisfied, `false` if it does not apply
46
55
  # @raise [ShEx::NotSatisfied] if not satisfied
47
- def satisfies_datatype?(value)
48
- dt = operands[1] if operands.first == :datatype
56
+ def satisfies_datatype?(value, depth: 0)
57
+ dt = op_fetch(:datatype)
49
58
  return true unless dt
50
59
 
51
- not_satisfied "Node was #{value.inspect}, expected datatype #{dt}" unless
60
+ not_satisfied "Node was #{value.inspect}, expected datatype #{dt}", depth: depth unless
52
61
  value.is_a?(RDF::Literal) && value.datatype == RDF::URI(dt)
53
- status "right datatype: #{value}: #{dt}"
62
+ status "right datatype: #{value}: #{dt}", depth: depth
54
63
  true
55
64
  end
56
65
 
@@ -59,7 +68,7 @@ module ShEx::Algebra
59
68
  # Checks all length/minlength/maxlength/pattern facets against the string representation of the value.
60
69
  # @return [Boolean] `true` if satisfied, `false` if it does not apply
61
70
  # @raise [ShEx::NotSatisfied] if not satisfied
62
- def satisfies_string_facet?(value)
71
+ def satisfies_string_facet?(value, depth: 0)
63
72
  length = op_fetch(:length)
64
73
  minlength = op_fetch(:minlength)
65
74
  maxlength = op_fetch(:maxlength)
@@ -71,15 +80,15 @@ module ShEx::Algebra
71
80
  when RDF::Node then value.id
72
81
  else value.to_s
73
82
  end
74
- not_satisfied "Node #{v_s.inspect} length not #{length}" if
83
+ not_satisfied "Node #{v_s.inspect} length not #{length}", depth: depth if
75
84
  length && v_s.length != length.to_i
76
- not_satisfied"Node #{v_s.inspect} length < #{minlength}" if
85
+ not_satisfied"Node #{v_s.inspect} length < #{minlength}", depth: depth if
77
86
  minlength && v_s.length < minlength.to_i
78
- not_satisfied "Node #{v_s.inspect} length > #{maxlength}" if
87
+ not_satisfied "Node #{v_s.inspect} length > #{maxlength}", depth: depth if
79
88
  maxlength && v_s.length > maxlength.to_i
80
- not_satisfied "Node #{v_s.inspect} does not match #{pattern}" if
89
+ not_satisfied "Node #{v_s.inspect} does not match #{pattern}", depth: depth if
81
90
  pattern && !Regexp.new(pattern).match(v_s)
82
- status "right string facet: #{value}"
91
+ status "right string facet: #{value}", depth: depth
83
92
  true
84
93
  end
85
94
 
@@ -88,7 +97,7 @@ module ShEx::Algebra
88
97
  # Checks all numeric facets against the value.
89
98
  # @return [Boolean] `true` if satisfied, `false` if it does not apply
90
99
  # @raise [ShEx::NotSatisfied] if not satisfied
91
- def satisfies_numeric_facet?(value)
100
+ def satisfies_numeric_facet?(value, depth: 0)
92
101
  mininclusive = op_fetch(:mininclusive)
93
102
  minexclusive = op_fetch(:minexclusive)
94
103
  maxinclusive = op_fetch(:maxinclusive)
@@ -98,32 +107,32 @@ module ShEx::Algebra
98
107
 
99
108
  return true if (mininclusive || minexclusive || maxinclusive || maxexclusive || totaldigits || fractiondigits).nil?
100
109
 
101
- not_satisfied "Node #{value.inspect} not numeric" unless
110
+ not_satisfied "Node #{value.inspect} not numeric", depth: depth unless
102
111
  value.is_a?(RDF::Literal::Numeric)
103
112
 
104
- not_satisfied "Node #{value.inspect} not decimal" if
113
+ not_satisfied "Node #{value.inspect} not decimal", depth: depth if
105
114
  (totaldigits || fractiondigits) && (!value.is_a?(RDF::Literal::Decimal) || value.invalid?)
106
115
 
107
116
  numeric_value = value.object
108
117
  case
109
- when !mininclusive.nil? && numeric_value < mininclusive.object then not_satisfied("Node #{value.inspect} < #{mininclusive.object}")
110
- when !minexclusive.nil? && numeric_value <= minexclusive.object then not_satisfied("Node #{value.inspect} not <= #{minexclusive.object}")
111
- when !maxinclusive.nil? && numeric_value > maxinclusive.object then not_satisfied("Node #{value.inspect} > #{maxinclusive.object}")
112
- when !maxexclusive.nil? && numeric_value >= maxexclusive.object then not_satisfied("Node #{value.inspect} >= #{maxexclusive.object}")
118
+ when !mininclusive.nil? && numeric_value < mininclusive.object then not_satisfied("Node #{value.inspect} < #{mininclusive.object}", depth: depth)
119
+ when !minexclusive.nil? && numeric_value <= minexclusive.object then not_satisfied("Node #{value.inspect} not <= #{minexclusive.object}", depth: depth)
120
+ when !maxinclusive.nil? && numeric_value > maxinclusive.object then not_satisfied("Node #{value.inspect} > #{maxinclusive.object}", depth: depth)
121
+ when !maxexclusive.nil? && numeric_value >= maxexclusive.object then not_satisfied("Node #{value.inspect} >= #{maxexclusive.object}", depth: depth)
113
122
  when !totaldigits.nil?
114
123
  md = value.canonicalize.to_s.match(/([1-9]\d*|0)?(?:\.(\d+)(?!0))?/)
115
124
  digits = md ? (md[1].to_s + md[2].to_s) : ""
116
125
  if digits.length > totaldigits.to_i
117
- not_satisfied "Node #{value.inspect} total digits != #{totaldigits}"
126
+ not_satisfied "Node #{value.inspect} total digits != #{totaldigits}", depth: depth
118
127
  end
119
128
  when !fractiondigits.nil?
120
129
  md = value.canonicalize.to_s.match(/\.(\d+)(?!0)?/)
121
130
  num = md ? md[1].to_s : ""
122
131
  if num.length > fractiondigits.to_i
123
- not_satisfied "Node #{value.inspect} fractional digits != #{fractiondigits}"
132
+ not_satisfied "Node #{value.inspect} fractional digits != #{fractiondigits}", depth: depth
124
133
  end
125
134
  end
126
- status "right numeric facet: #{value}"
135
+ status "right numeric facet: #{value}", depth: depth
127
136
  true
128
137
  end
129
138
 
@@ -132,12 +141,12 @@ module ShEx::Algebra
132
141
  # Checks all numeric facets against the value.
133
142
  # @return [Boolean] `true` if satisfied, `false` if it does not apply
134
143
  # @raise [ShEx::NotSatisfied] if not satisfied
135
- def satisfies_values?(value)
144
+ def satisfies_values?(value, depth: 0)
136
145
  values = operands.select {|op| op.is_a?(Value)}
137
146
  return true if values.empty?
138
- matched_value = values.detect {|v| v.match?(value)}
139
- not_satisfied "Node #{value.inspect} not expected" unless matched_value
140
- status "right value: #{value}"
147
+ matched_value = values.detect {|v| v.match?(value, depth: depth + 1)}
148
+ not_satisfied "Value #{value.to_sxp} not expected, wanted #{values.to_sxp}", depth: depth unless matched_value
149
+ status "right value: #{value}", depth: depth
141
150
  true
142
151
  end
143
152
 
@@ -4,20 +4,35 @@ module ShEx::Algebra
4
4
  include Satisfiable
5
5
  NAME = :not
6
6
 
7
+ ##
8
+ # Creates an operator instance from a parsed ShExJ representation
9
+ # @param (see Operator#from_shexj)
10
+ # @return [Operator]
11
+ def self.from_shexj(operator, options = {})
12
+ raise ArgumentError unless operator.is_a?(Hash) && operator['type'] == 'ShapeNot'
13
+ raise ArgumentError, "missing shapeExpr in #{operator.inspect}" unless operator.has_key?('shapeExpr')
14
+ super
15
+ end
16
+
7
17
  #
8
18
  # S is a ShapeNot and for the shape expression se2 at shapeExpr, notSatisfies(n, se2, G, m).
9
19
  # @param (see Satisfiable#satisfies?)
10
20
  # @return (see Satisfiable#satisfies?)
11
21
  # @raise (see Satisfiable#satisfies?)
12
22
  # @see [https://shexspec.github.io/spec/#shape-expression-semantics]
13
- def satisfies?(focus)
23
+ def satisfies?(focus, depth: 0)
14
24
  status ""
15
25
  satisfied_op = begin
16
- operands.first.satisfies?(focus)
26
+ operands.last.satisfies?(focus, depth: depth + 1)
17
27
  rescue ShEx::NotSatisfied => e
18
- return satisfy satisfied: e.expression.unsatisfied
28
+ return satisfy focus: focus, satisfied: e.expression.unsatisfied, depth: depth
19
29
  end
20
- not_satisfied "Expression should not have matched", unsatisfied: satisfied_op
30
+ not_satisfied "Expression should not have matched",
31
+ focus: focus, unsatisfied: satisfied_op, depth: depth
32
+ end
33
+
34
+ def json_type
35
+ "ShapeNot"
21
36
  end
22
37
  end
23
38
  end
@@ -4,12 +4,23 @@ module ShEx::Algebra
4
4
  include TripleExpression
5
5
  NAME = :oneOf
6
6
 
7
+ ##
8
+ # Creates an operator instance from a parsed ShExJ representation
9
+ # @param (see Operator#from_shexj)
10
+ # @return [Operator]
11
+ def self.from_shexj(operator, options = {})
12
+ raise ArgumentError unless operator.is_a?(Hash) && operator['type'] == 'OneOf'
13
+ raise ArgumentError, "missing expressions in #{operator.inspect}" unless operator.has_key?('expressions')
14
+ super
15
+ end
16
+
7
17
  ##
8
18
  # `expr` is a OneOf and there is some shape expression `se2` in shapeExprs such that a `matches(T, se2, m)`...
9
19
  #
10
- # @param [Array<RDF::Statement>] statements
11
- # @return [Array<RDF::Statement]
12
- def matches(statements)
20
+ # @param (see TripleExpression#matches)
21
+ # @return (see TripleExpression#matches)
22
+ # @raise (see TripleExpression#matches)
23
+ def matches(arcs_in, arcs_out, depth: 0)
13
24
  results, satisfied, unsatisfied = [], [], []
14
25
  num_iters, max = 0, maximum
15
26
 
@@ -18,22 +29,21 @@ module ShEx::Algebra
18
29
  while num_iters < max
19
30
  matched_something = operands.select {|o| o.is_a?(TripleExpression)}.any? do |op|
20
31
  begin
21
- matched_op = op.matches(statements)
32
+ matched_op = op.matches(arcs_in, arcs_out, depth: depth + 1)
22
33
  satisfied << matched_op
23
34
  results += matched_op.matched
24
- statements -= matched_op.matched
25
- status "matched #{matched_op.matched.to_sxp}"
35
+ arcs_in -= matched_op.matched
36
+ arcs_out -= matched_op.matched
37
+ status "matched #{matched_op.matched.to_sxp}", depth: depth
26
38
  rescue ShEx::NotMatched => e
27
- status "not matched: #{e.message}"
28
- op = op.dup
29
- op.unmatched = statements - results
30
- unsatisfied << op
39
+ status "not matched: #{e.message}", depth: depth
40
+ unsatisfied << e.expression
31
41
  false
32
42
  end
33
43
  end
34
44
  break unless matched_something
35
45
  num_iters += 1
36
- status "matched #{results.length} statements after #{num_iters} iterations"
46
+ status "matched #{results.length} statements after #{num_iters} iterations", depth: depth
37
47
  end
38
48
 
39
49
  # Max violations handled in Shape
@@ -42,15 +52,15 @@ module ShEx::Algebra
42
52
  end
43
53
 
44
54
  # Last, evaluate semantic acts
45
- semantic_actions.all? do |op|
46
- op.satisfies?(results)
55
+ semantic_actions.each do |op|
56
+ op.satisfies?(matched: results, depth: depth + 1)
47
57
  end unless results.empty?
48
58
 
49
- satisfy matched: results, satisfied: satisfied, unsatisfied: unsatisfied
59
+ satisfy matched: results, satisfied: satisfied, depth: depth
50
60
  rescue ShEx::NotMatched, ShEx::NotSatisfied => e
51
61
  not_matched e.message,
52
- matched: results, unmatched: (statements - results),
53
- satisfied: satisfied, unsatisfied: unsatisfied
62
+ matched: results, unmatched: ((arcs_in + arcs_out).uniq - results),
63
+ satisfied: satisfied, unsatisfied: unsatisfied, depth: depth
54
64
  end
55
65
  end
56
66
  end
@@ -1,4 +1,6 @@
1
1
  require 'sparql/algebra'
2
+ require 'json/ld/preloaded'
3
+ require 'shex/shex_context'
2
4
 
3
5
  module ShEx::Algebra
4
6
 
@@ -30,6 +32,8 @@ module ShEx::Algebra
30
32
  # any additional options
31
33
  # @option options [Boolean] :memoize (false)
32
34
  # whether to memoize results for particular operands
35
+ # @option options [RDF::Resource] :label
36
+ # Identifier of the operator
33
37
  # @raise [TypeError] if any operand is invalid
34
38
  def initialize(*operands)
35
39
  @options = operands.last.is_a?(Hash) ? operands.pop.dup : {}
@@ -51,15 +55,7 @@ module ShEx::Algebra
51
55
  end
52
56
  end
53
57
 
54
- if options[:logger]
55
- options[:depth] = 0
56
- each_descendant(1) do |depth, operand|
57
- if operand.respond_to?(:options)
58
- operand.options[:logger] = options[:logger]
59
- operand.options[:depth] = depth
60
- end
61
- end
62
- end
58
+ @label = options[:label]
63
59
  end
64
60
 
65
61
  ##
@@ -85,6 +81,16 @@ module ShEx::Algebra
85
81
  # Does this operator a SemAct?
86
82
  def semact?; false; end
87
83
 
84
+ ##
85
+ # On a result instance, the focus of the expression
86
+ def focus
87
+ Array(operands.detect {|op| op.is_a?(Array) && op[0] == :focus} || [:focus])[1]
88
+ end
89
+ def focus=(node)
90
+ operands.delete_if {|op| op.is_a?(Array) && op[0] == :focus}
91
+ operands << [:focus, node]
92
+ end
93
+
88
94
  ##
89
95
  # On a result instance, the statements that matched this expression.
90
96
  # @return [Array<Statement>]
@@ -129,13 +135,26 @@ module ShEx::Algebra
129
135
  operands << ops.unshift(:unsatisfied) unless (ops || []).empty?
130
136
  end
131
137
 
138
+ ##
139
+ # On a result instance, the failure message. (failure only).
140
+ # @return [String]
141
+ def message
142
+ (operands.detect {|op| op.is_a?(Array) && op[0] == :message} || [:message])[1..-1]
143
+ end
144
+ def message=(str)
145
+ operands.delete_if {|op| op.is_a?(Array) && op[0] == :message}
146
+ operands << [:message, str]
147
+ end
148
+
132
149
  ##
133
150
  # Duplication this operand, and add `matched`, `unmatched`, `satisfied`, and `unsatisfied` operands for accessing downstream.
134
151
  #
135
152
  # @return [Operand]
136
- def satisfy(matched: nil, unmatched: nil, satisfied: nil, unsatisfied: nil)
137
- log_debug(self.class.const_get(:NAME), "satisfied", depth: options.fetch(:depth, 0))
153
+ def satisfy(focus: nil, matched: nil, unmatched: nil, satisfied: nil, unsatisfied: nil, message: nil, **opts)
154
+ log_debug(self.class.const_get(:NAME), "satisfied", **opts) unless message
138
155
  expression = self.dup
156
+ expression.message = message if message
157
+ expression.focus = focus if focus
139
158
  expression.matched = Array(matched) if matched
140
159
  expression.unmatched = Array(unmatched) if unmatched
141
160
  expression.satisfied = Array(satisfied) if satisfied
@@ -145,25 +164,17 @@ module ShEx::Algebra
145
164
 
146
165
  ##
147
166
  # Exception handling
148
- def not_matched(message, matched: nil, unmatched: nil, satisfied: nil, unsatisfied: nil, **opts, &block)
149
- expression = opts.fetch(:expression, self).satisfy(
150
- matched: matched,
151
- unmatched: unmatched,
152
- satisfied: satisfied,
153
- unsatisfied: unsatisfied)
167
+ def not_matched(message, **opts, &block)
168
+ expression = opts.fetch(:expression, self).satisfy(message: message, **opts)
154
169
  exception = opts.fetch(:exception, ShEx::NotMatched)
155
- status(message) {(block_given? ? block.call : "") + "expression: #{expression.to_sxp}"}
170
+ status(message, **opts) {(block_given? ? block.call : "") + "expression: #{expression.to_sxp}"}
156
171
  raise exception.new(message, expression: expression)
157
172
  end
158
173
 
159
- def not_satisfied(message, matched: nil, unmatched: nil, satisfied: nil, unsatisfied: nil, **opts)
160
- expression = opts.fetch(:expression, self).satisfy(
161
- matched: matched,
162
- unmatched: unmatched,
163
- satisfied: satisfied,
164
- unsatisfied: unsatisfied)
174
+ def not_satisfied(message, **opts)
175
+ expression = opts.fetch(:expression, self).satisfy(message: message, **opts)
165
176
  exception = opts.fetch(:exception, ShEx::NotSatisfied)
166
- status(message) {(block_given? ? block.call : "") + "expression: #{expression.to_sxp}"}
177
+ status(message, **opts) {(block_given? ? block.call : "") + "expression: #{expression.to_sxp}"}
167
178
  raise exception.new(message, expression: expression)
168
179
  end
169
180
 
@@ -173,8 +184,8 @@ module ShEx::Algebra
173
184
  log_error(message, depth: options.fetch(:depth, 0), exception: exception) {"expression: #{expression.to_sxp}"}
174
185
  end
175
186
 
176
- def status(message, &block)
177
- log_debug(self.class.const_get(:NAME), message, depth: options.fetch(:depth, 0), &block)
187
+ def status(message, **opts, &block)
188
+ log_debug(self.class.const_get(:NAME).to_s + (@label ? "(#{@label})" : ""), message, **opts, &block)
178
189
  true
179
190
  end
180
191
 
@@ -184,6 +195,16 @@ module ShEx::Algebra
184
195
  # @return [Array]
185
196
  attr_reader :operands
186
197
 
198
+ ##
199
+ # The label (or subject) of this operand
200
+ # @return [RDF::Resource]
201
+ attr_accessor :label
202
+
203
+ ##
204
+ # Logging support (reader is in RDF::Util::Logger)
205
+ # @return [Logger]
206
+ attr_writer :logger
207
+
187
208
  ##
188
209
  # Returns the operand at the given `index`.
189
210
  #
@@ -200,8 +221,9 @@ module ShEx::Algebra
200
221
  # @return [Array]
201
222
  # @see https://en.wikipedia.org/wiki/S-expression
202
223
  def to_sxp_bin
203
- operator = [self.class.const_get(:NAME)].flatten.first
204
- [operator, *(operands || []).map(&:to_sxp_bin)]
224
+ [self.class.const_get(:NAME)] +
225
+ (label ? [[:label, label]] : []) +
226
+ (operands || []).map(&:to_sxp_bin)
205
227
  end
206
228
 
207
229
  ##
@@ -219,6 +241,286 @@ module ShEx::Algebra
219
241
  to_sxp_bin.to_sxp
220
242
  end
221
243
 
244
+ ##
245
+ # Creates an operator instance from a parsed ShExJ representation
246
+ # @param [Hash] operator
247
+ # @param [Hash] options ({})
248
+ # @option options [RDF::URI] :base
249
+ # @option options [Hash{String => RDF::URI}] :prefixes
250
+ # @return [Operator]
251
+ def self.from_shexj(operator, options = {})
252
+ options[:context] ||= JSON::LD::Context.parse(ShEx::CONTEXT)
253
+ operands = []
254
+ label = nil
255
+
256
+ operator.each do |k, v|
257
+ case k
258
+ when /length|pattern|clusive/ then operands << [k.to_sym, v]
259
+ when 'label' then label = iri(v, options)
260
+ when 'min', 'max', 'inverse', 'closed' then operands << [k.to_sym, v]
261
+ when 'nodeKind' then operands << v.to_sym
262
+ when 'object' then operands << value(v, options)
263
+ when 'start' then operands << Start.new(ShEx::Algebra.from_shexj(v, options))
264
+ when '@context' then
265
+ options[:context] = JSON::LD::Context.parse(v)
266
+ options[:base_uri] = options[:context].base
267
+ when 'shapes'
268
+ operands << case v
269
+ when Array
270
+ [:shapes] + v.map {|vv| ShEx::Algebra.from_shexj(vv, options)}
271
+ else
272
+ raise "Expected value of shapes #{v.inspect}"
273
+ end
274
+ when 'reference', 'include', 'stem', 'name'
275
+ # Value may be :wildcard for stem
276
+ operands << (v.is_a?(Symbol) ? v : iri(v, options))
277
+ when 'predicate' then operands << iri(v, options)
278
+ when 'extra', 'datatype'
279
+ v = [v] unless v.is_a?(Array)
280
+ operands << (v.map {|op| iri(op, options)}).unshift(k.to_sym)
281
+ when 'exclusions'
282
+ v = [v] unless v.is_a?(Array)
283
+ operands << v.map do |op|
284
+ op.is_a?(Hash) ?
285
+ ShEx::Algebra.from_shexj(op, options) :
286
+ value(op, options)
287
+ end.unshift(:exclusions)
288
+ when 'min', 'max', 'inverse', 'closed', 'valueExpr', 'semActs',
289
+ 'shapeExpr', 'shapeExprs', 'startActs', 'expression',
290
+ 'expressions', 'annotations'
291
+ v = [v] unless v.is_a?(Array)
292
+ operands += v.map {|op| ShEx::Algebra.from_shexj(op, options)}
293
+ when 'code'
294
+ operands << v
295
+ when 'values'
296
+ v = [v] unless v.is_a?(Array)
297
+ operands += v.map do |op|
298
+ Value.new(value(op, options))
299
+ end
300
+ end
301
+ end
302
+
303
+ new(*operands, label: label)
304
+ end
305
+
306
+ def json_type
307
+ self.class.name.split('::').last
308
+ end
309
+
310
+ def to_json(options = nil)
311
+ self.to_h.to_json(options)
312
+ end
313
+
314
+ ##
315
+ # Create a hash version of the operator, suitable for turning into JSON.
316
+ # @return [Hash]
317
+ def to_h
318
+ obj = json_type == 'Schema' ?
319
+ {'@context' => ShEx::CONTEXT, 'type' => json_type} :
320
+ {'type' => json_type}
321
+ obj['label'] = label.to_s if label
322
+ operands.each do |op|
323
+ case op
324
+ when Array
325
+ # First element should be a symbol
326
+ case sym = op.first
327
+ when :datatype,
328
+ :pattern then obj[op.first.to_s] = op.last.to_s
329
+ when :exclusions then obj['exclusions'] = Array(op[1..-1]).map {|v| serialize_value(v)}
330
+ when :extra then (obj['extra'] ||= []).concat Array(op[1..-1]).map(&:to_s)
331
+ # FIXME Shapes should be an array, not a hash
332
+ when :shapes then obj['shapes'] = Array(op[1..-1]).map {|v| v.to_h}
333
+ when :minlength,
334
+ :maxlength,
335
+ :length,
336
+ :mininclusive,
337
+ :maxinclusive,
338
+ :minexclusive,
339
+ :maxexclusive,
340
+ :totaldigits,
341
+ :fractiondigits then obj[op.first.to_s] = op.last.object
342
+ when :min, :max then obj[op.first.to_s] = op.last
343
+ when :base, :prefix
344
+ # Ignore base and prefix
345
+ when Symbol then obj[sym.to_s] = Array(op[1..-1]).map(&:to_h)
346
+ else
347
+ raise "Expected array to start with a symbol for #{self}"
348
+ end
349
+ when :wildcard then obj['stem'] = {'type' => 'Wildcard'}
350
+ when Annotation then (obj['annotations'] ||= []) << op.to_h
351
+ when SemAct then (obj[is_a?(Schema) ? 'startActs' : 'semActs'] ||= []) << op.to_h
352
+ when Start then obj['start'] = op.operands.first.to_h
353
+ when RDF::Value
354
+ case self
355
+ when TripleConstraint then obj['predicate'] = op.to_s
356
+ when Stem, StemRange then obj['stem'] = op.to_s
357
+ when Inclusion then obj['include'] = op.to_s
358
+ when ShapeRef then obj['reference'] = op.to_s
359
+ when SemAct then obj[op.is_a?(RDF::URI) ? 'name' : 'code'] = op.to_s
360
+ else
361
+ raise "How to serialize Value #{op.inspect} to json for #{self}"
362
+ end
363
+ when Symbol
364
+ case self
365
+ when NodeConstraint then obj['nodeKind'] = op.to_s
366
+ when Shape then obj['closed'] = true
367
+ when TripleConstraint then obj['inverse'] = true
368
+ else
369
+ raise "How to serialize Symbol #{op.inspect} to json for #{self}"
370
+ end
371
+ when TripleConstraint, EachOf, OneOf, Inclusion
372
+ case self
373
+ when EachOf, OneOf
374
+ (obj['expressions'] ||= []) << op.to_h
375
+ else
376
+ obj['expression'] = op.to_h
377
+ end
378
+ when NodeConstraint
379
+ case self
380
+ when And, Or
381
+ (obj['shapeExprs'] ||= []) << op.to_h
382
+ else
383
+ obj['valueExpr'] = op.to_h
384
+ end
385
+ when And, Or, Shape, Not, ShapeRef
386
+ case self
387
+ when And, Or
388
+ (obj['shapeExprs'] ||= []) << op.to_h
389
+ when TripleConstraint
390
+ obj['valueExpr'] = op.to_h
391
+ else
392
+ obj['shapeExpr'] = op.to_h
393
+ end
394
+ when Value
395
+ obj['values'] ||= []
396
+ Array(op).map {|o| o.operands}.flatten.each do |oo|
397
+ obj['values'] << serialize_value(oo)
398
+ end
399
+ else
400
+ raise "How to serialize #{op.inspect} to json for #{self}"
401
+ end
402
+ end
403
+ obj
404
+ end
405
+
406
+ ##
407
+ # Returns the Base URI defined for the parser,
408
+ # as specified or when parsing a BASE prologue element.
409
+ #
410
+ # @example
411
+ # base #=> RDF::URI('http://example.com/')
412
+ #
413
+ # @return [HRDF::URI]
414
+ def base_uri
415
+ @options[:base_uri]
416
+ end
417
+
418
+ # Create URIs
419
+ # @param [RDF::Value, String] value
420
+ # @param [Hash{Symbol => Object}] options
421
+ # @option options [RDF::URI] :base_uri
422
+ # @option options [Hash{String => RDF::URI}] :prefixes
423
+ # @option options [JSON::LD::Context] :context
424
+ # @return [RDF::Value]
425
+ def iri(value, options = @options)
426
+ self.class.iri(value, options)
427
+ end
428
+
429
+ # Create URIs
430
+ # @param (see #iri)
431
+ # @option (see #iri)
432
+ # @return (see #iri)
433
+ def self.iri(value, options)
434
+ # If we have a base URI, use that when constructing a new URI
435
+ base_uri = options[:base_uri]
436
+
437
+ case value
438
+ when Hash
439
+ # A JSON-LD node reference
440
+ v = options[:context].expand_value(value)
441
+ raise "Expected #{value.inspect} to be a JSON-LD Node Reference" unless JSON::LD::Utils.node_reference?(v)
442
+ self.iri(v['@id'], options)
443
+ when RDF::URI
444
+ if base_uri && value.relative?
445
+ base_uri.join(value)
446
+ else
447
+ value
448
+ end
449
+ when RDF::Value then value
450
+ when /^_:/ then
451
+ id = value[2..-1].to_s
452
+ RDF::Node.intern(id)
453
+ when /^(\w+):(\S+)$/
454
+ prefixes = options.fetch(:prefixes, {})
455
+ if prefixes.has_key?($1)
456
+ prefixes[$1].join($2)
457
+ elsif RDF.type == value
458
+ a = RDF.type.dup; a.lexical = 'a'; a
459
+ elsif options[:context]
460
+ options[:context].expand_iri(value, vocab: true)
461
+ else
462
+ RDF::URI(value)
463
+ end
464
+ else
465
+ if options[:context]
466
+ options[:context].expand_iri(value, document: true)
467
+ elsif base_uri
468
+ base_uri.join(value)
469
+ elsif base_uri
470
+ base_uri.join(value)
471
+ else
472
+ RDF::URI(value)
473
+ end
474
+ end
475
+ end
476
+
477
+ # Create Values, with "clever" matching to see if it might be a value, IRI or BNode.
478
+ # @param [RDF::Value, String] value
479
+ # @param [Hash{Symbol => Object}] options
480
+ # @option options [RDF::URI] :base_uri
481
+ # @option options [Hash{String => RDF::URI}] :prefixes
482
+ # @return [RDF::Value]
483
+ def value(value, options = @options)
484
+ self.class.value(value, options)
485
+ end
486
+
487
+ # Create Values, with "clever" matching to see if it might be a value, IRI or BNode.
488
+ # @param (see #value)
489
+ # @option (see #value)
490
+ # @return (see #value)
491
+ def self.value(value, options)
492
+ # If we have a base URI, use that when constructing a new URI
493
+ case value
494
+ when Hash
495
+ # Either a value object or a node reference
496
+ if value['uri']
497
+ iri(value['uri'], options)
498
+ elsif value['value']
499
+ RDF::Literal(value['value'], datatype: value['type'], language: value['language'])
500
+ else
501
+ ShEx::Algebra.from_shexj(value, options)
502
+ end
503
+ else iri(value, options)
504
+ end
505
+ end
506
+
507
+ ##
508
+ # Serialize a value, either as JSON, or as modififed N-Triples
509
+ #
510
+ # @param [RDF::Value, Operator] value
511
+ # @return [String]
512
+ def serialize_value(value)
513
+ case value
514
+ when RDF::Literal
515
+ {'value' => value.to_s}.
516
+ merge(value.has_datatype? ? {'type' => value.datatype.to_s} : {}).
517
+ merge(value.has_language? ? {'language' => value.language.to_s} : {})
518
+ when RDF::Resource
519
+ value.to_s
520
+ else value.to_h
521
+ end
522
+ end
523
+
222
524
  ##
223
525
  # Returns a developer-friendly representation of this operator.
224
526
  #
@@ -243,6 +545,12 @@ module ShEx::Algebra
243
545
  # @return [Enumerator]
244
546
  def each_descendant(depth = 0, &block)
245
547
  if block_given?
548
+
549
+ case block.arity
550
+ when 1 then block.call(self)
551
+ else block.call(depth, self)
552
+ end
553
+
246
554
  operands.each do |operand|
247
555
  case operand
248
556
  when Array
@@ -252,11 +560,6 @@ module ShEx::Algebra
252
560
  else
253
561
  operand.each_descendant(depth + 1, &block) if operand.respond_to?(:each_descendant)
254
562
  end
255
-
256
- case block.arity
257
- when 1 then block.call(operand)
258
- else block.call(depth, operand)
259
- end
260
563
  end
261
564
  end
262
565
  enum_for(:each_descendant)
@@ -296,6 +599,19 @@ module ShEx::Algebra
296
599
  self
297
600
  end
298
601
 
602
+ protected
603
+ def dup
604
+ operands = @operands.map {|o| o.dup rescue o}
605
+ self.class.new(*operands, label: @label)
606
+ end
607
+
608
+ ##
609
+ # Implement `to_hash` only if accessed; otherwise, it becomes an _Implicit Accessor_ which will cause problems with splat arguments, which causes the last to be turned into a hash for extracting keyword aruments.
610
+ def method_missing(method, *args)
611
+ return to_h(*args) if method == :hash
612
+ super
613
+ end
614
+
299
615
  ##
300
616
  # A unary operator.
301
617
  #