shacl 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,65 @@
1
+ module SHACL::Algebra
2
+ ##
3
+ class Xone < Operator
4
+ NAME = :xone
5
+
6
+ ##
7
+ # Specifies the condition that each value node conforms to exactly one of the provided shapes.
8
+ #
9
+ # @example
10
+ # ex:XoneConstraintExampleShape
11
+ # a sh:NodeShape ;
12
+ # sh:targetClass ex:Person ;
13
+ # sh:xone (
14
+ # [
15
+ # sh:property [
16
+ # sh:path ex:fullName ;
17
+ # sh:minCount 1 ;
18
+ # ]
19
+ # ]
20
+ # [
21
+ # sh:property [
22
+ # sh:path ex:firstName ;
23
+ # sh:minCount 1 ;
24
+ # ] ;
25
+ # sh:property [
26
+ # sh:path ex:lastName ;
27
+ # sh:minCount 1 ;
28
+ # ]
29
+ # ]
30
+ # ) .
31
+ #
32
+ # @param [RDF::Term] node
33
+ # @param [Hash{Symbol => Object}] options
34
+ # @return [Array<SHACL::ValidationResult>]
35
+ def conforms(node, path: nil, depth: 0, **options)
36
+ log_debug(NAME, depth: depth) {SXP::Generator.string({node: node}.to_sxp_bin)}
37
+ num_conform = operands.inject(0) do |memo, op|
38
+ results = op.conforms(node, depth: depth + 1, **options)
39
+ memo += (results.all?(&:conform?) ? 1 : 0)
40
+ end
41
+ case num_conform
42
+ when 0
43
+ not_satisfied(focus: node, path: path,
44
+ value: node,
45
+ message: "node does not conform to any shape",
46
+ resultSeverity: options.fetch(:severity),
47
+ component: RDF::Vocab::SHACL.XoneConstraintComponent,
48
+ depth: depth, **options)
49
+ when 1
50
+ satisfy(focus: node, path: path,
51
+ value: node,
52
+ message: "node conforms to a single shape",
53
+ component: RDF::Vocab::SHACL.XoneConstraintComponent,
54
+ depth: depth, **options)
55
+ else
56
+ not_satisfied(focus: node, path: path,
57
+ value: node,
58
+ message: "node conforms to #{num_conform} shapes",
59
+ resultSeverity: options.fetch(:severity),
60
+ component: RDF::Vocab::SHACL.XoneConstraintComponent,
61
+ depth: depth, **options)
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,41 @@
1
+ # -*- encoding: utf-8 -*-
2
+ # frozen_string_literal: true
3
+ # This file generated automatically from http://github.com/ruby-rdf/shacl/
4
+ require 'json/ld'
5
+ class JSON::LD::Context
6
+ add_preloaded("http://github.com/ruby-rdf/shacl/") do
7
+ new(vocab: "http://www.w3.org/ns/shacl#", processingMode: "json-ld-1.1", term_definitions: {
8
+ "and" => TermDefinition.new("and", id: "http://www.w3.org/ns/shacl#and", type_mapping: "@id", container_mapping: "@list"),
9
+ "annotationProperty" => TermDefinition.new("annotationProperty", id: "http://www.w3.org/ns/shacl#annotationProperty", type_mapping: "@id"),
10
+ "class" => TermDefinition.new("class", id: "http://www.w3.org/ns/shacl#class", type_mapping: "@id"),
11
+ "comment" => TermDefinition.new("comment", id: "http://www.w3.org/2000/01/rdf-schema#comment", simple: true),
12
+ "condition" => TermDefinition.new("condition", id: "http://www.w3.org/ns/shacl#condition", type_mapping: "@id"),
13
+ "datatype" => TermDefinition.new("datatype", id: "http://www.w3.org/ns/shacl#datatype", type_mapping: "@vocab"),
14
+ "declare" => TermDefinition.new("declare", id: "http://www.w3.org/ns/shacl#declare", type_mapping: "@id"),
15
+ "disjoint" => TermDefinition.new("disjoint", id: "http://www.w3.org/ns/shacl#disjoint", type_mapping: "@id"),
16
+ "entailment" => TermDefinition.new("entailment", id: "http://www.w3.org/ns/shacl#entailment", type_mapping: "@id"),
17
+ "equals" => TermDefinition.new("equals", id: "http://www.w3.org/ns/shacl#equals", type_mapping: "@id"),
18
+ "id" => TermDefinition.new("id", id: "@id", simple: true),
19
+ "ignoredProperties" => TermDefinition.new("ignoredProperties", id: "http://www.w3.org/ns/shacl#ignoredProperties", type_mapping: "@id", container_mapping: "@list"),
20
+ "in" => TermDefinition.new("in", id: "http://www.w3.org/ns/shacl#in", type_mapping: "@none", container_mapping: "@list"),
21
+ "inversePath" => TermDefinition.new("inversePath", id: "http://www.w3.org/ns/shacl#inversePath", type_mapping: "@id"),
22
+ "label" => TermDefinition.new("label", id: "http://www.w3.org/2000/01/rdf-schema#label", simple: true),
23
+ "languageIn" => TermDefinition.new("languageIn", id: "http://www.w3.org/ns/shacl#languageIn", container_mapping: "@list"),
24
+ "lessThan" => TermDefinition.new("lessThan", id: "http://www.w3.org/ns/shacl#lessThan", type_mapping: "@id"),
25
+ "lessThanOrEquals" => TermDefinition.new("lessThanOrEquals", id: "http://www.w3.org/ns/shacl#lessThanOrEquals", type_mapping: "@id"),
26
+ "nodeKind" => TermDefinition.new("nodeKind", id: "http://www.w3.org/ns/shacl#nodeKind", type_mapping: "@vocab"),
27
+ "or" => TermDefinition.new("or", id: "http://www.w3.org/ns/shacl#or", type_mapping: "@id", container_mapping: "@list"),
28
+ "path" => TermDefinition.new("path", id: "http://www.w3.org/ns/shacl#path", type_mapping: "@none"),
29
+ "property" => TermDefinition.new("property", id: "http://www.w3.org/ns/shacl#property", type_mapping: "@id"),
30
+ "rdfs" => TermDefinition.new("rdfs", id: "http://www.w3.org/2000/01/rdf-schema#", simple: true, prefix: true),
31
+ "severity" => TermDefinition.new("severity", id: "http://www.w3.org/ns/shacl#severity", type_mapping: "@vocab"),
32
+ "sh" => TermDefinition.new("sh", id: "http://www.w3.org/ns/shacl#", simple: true, prefix: true),
33
+ "shacl" => TermDefinition.new("shacl", id: "http://www.w3.org/ns/shacl#", simple: true, prefix: true),
34
+ "targetClass" => TermDefinition.new("targetClass", id: "http://www.w3.org/ns/shacl#targetClass", type_mapping: "@id"),
35
+ "targetNode" => TermDefinition.new("targetNode", id: "http://www.w3.org/ns/shacl#targetNode", type_mapping: "@none"),
36
+ "type" => TermDefinition.new("type", id: "@type", container_mapping: "@set"),
37
+ "xone" => TermDefinition.new("xone", id: "http://www.w3.org/ns/shacl#xone", type_mapping: "@id", container_mapping: "@list"),
38
+ "xsd" => TermDefinition.new("xsd", id: "http://www.w3.org/2001/XMLSchema#", simple: true, prefix: true)
39
+ })
40
+ end
41
+ end
@@ -0,0 +1,88 @@
1
+ require 'rdf/format'
2
+
3
+ module SHACL
4
+ ##
5
+ # SHACL format specification. Note that this format does not define any readers or writers.
6
+ #
7
+ # @example Obtaining an ShEx format class
8
+ # RDF::Format.for(:shacl) #=> ShEx::Format
9
+ class Format < RDF::Format
10
+ ##
11
+ # Hash of CLI commands appropriate for this format
12
+ # @return [Hash{Symbol => Lambda(Array, Hash)}]
13
+ def self.cli_commands
14
+ {
15
+ shacl: {
16
+ description: "Validate repository given shape",
17
+ help: %(shacl [--shape URI] [--focus Resource] [--replace]
18
+
19
+ Evaluates the repository according the the specified shapes.
20
+ If no shape file is specified, it will look for one or more
21
+ shapes graphs using the sh:shapesGraph property found within
22
+ the repository.
23
+ ).gsub(/^\s+/, ''),
24
+ parse: true,
25
+ lambda: -> (argv, **options) do
26
+ shacl = case options[:shape]
27
+ when IO, StringIO
28
+ SHACL.get_shapes(RDF::Reader.new(options[:shape]), **options)
29
+ when nil
30
+ SHACL.from_queryable(RDF::CLI.repository, **options)
31
+ else SHACL.open(options[:shape], **options)
32
+ end
33
+
34
+ if options[:to_sxp]
35
+ options[:messages][:shacl] = {}
36
+ options[:messages][:shacl].merge!({"S-Expression": [SXP::Generator.string(shacl.to_sxp_bin)]})
37
+ else
38
+ start = Time.now
39
+ report = shacl.execute(RDF::CLI.repository, **options)
40
+ secs = Time.new - start
41
+ options[:logger].info "SHACL resulted in #{report.conform? ? 'success' : 'failure'} including #{report.count} results."
42
+ options[:logger].info "Validated in #{secs} seconds."
43
+ options[:messages][:shacl] = {result: report.conform? ? "Satisfied shape" : "Did not satisfy shape"}
44
+ if report.conform?
45
+ options[:messages][:shacl] = {result: ["Satisfied shape"]}
46
+ else
47
+ RDF::CLI.repository << report
48
+ options[:messages][:shacl] = {result: ["Did not satisfy shape: #{report.count} results"]}
49
+ options[:messages].merge!(report.linter_messages)
50
+ end
51
+ end
52
+ RDF::CLI.repository
53
+ end,
54
+ options: [
55
+ RDF::CLI::Option.new(
56
+ symbol: :focus,
57
+ datatype: String,
58
+ control: :text,
59
+ on: ["--focus Resource"],
60
+ description: "Focus node within repository"
61
+ ) {|v| RDF::URI(v)},
62
+ RDF::CLI::Option.new(
63
+ symbol: :replace,
64
+ datatype: TrueClass,
65
+ control: :checkbox,
66
+ on: ["--replace"],
67
+ description: "Replaces the data graph with the validation report"
68
+ ) {|v| RDF::URI(v)},
69
+ RDF::CLI::Option.new(
70
+ symbol: :shape,
71
+ datatype: String,
72
+ control: :url2,
73
+ on: ["--shape URI"],
74
+ description: "SHACL shapes graph location"
75
+ ) {|v| RDF::URI(v)},
76
+ RDF::CLI::Option.new(
77
+ symbol: :to_sxp,
78
+ datatype: String,
79
+ control: :checkbox,
80
+ on: ["--to-sxp"],
81
+ description: "Display parsed shapes as an S-Expression"
82
+ ),
83
+ ]
84
+ }
85
+ }
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,198 @@
1
+ # Localized refinements to externally defined classes
2
+ module SHACL::Refinements
3
+ using SHACL::Refinements
4
+
5
+ refine Hash do
6
+ # @!parse
7
+ # # Refinements on Hash
8
+ # class Hash
9
+ # ##
10
+ # # Deep merge two hashes folding array values together.
11
+ # #
12
+ # # @param [Hash] second
13
+ # # @return [Hash]
14
+ # def deep_merge(second); end
15
+ # end
16
+ def deep_merge(second)
17
+ merger = ->(_, v1, v2) {Hash === v1 && Hash === v2 ? v1.merge(v2, &merger) : Array === v1 && Array === v2 ? v1 | v2 : v2.nil? ? v1 : v2 }
18
+ merge(second.to_h, &merger)
19
+ end
20
+ end
21
+
22
+ refine SPARQL::Algebra::Operator::Alt do
23
+ # @!parse
24
+ # # Refinements on SPARQL::Algebra::Operator::Alt
25
+ # class SPARQL::Algebra::Operator::Alt
26
+ # ##
27
+ # # Retrieve the possibly newly assigned blank node subject to use for representing this operator.
28
+ # # @return [RDF::Node]
29
+ # attr_accessor :subject
30
+ #
31
+ # ##
32
+ # # Generate the SHACL representation of this operator
33
+ # # @return [RDF::Node]
34
+ # def each_statement(&block); end.
35
+ # end
36
+ attr_accessor :subject
37
+ def each_statement(&block)
38
+ @subject = RDF::Node.new
39
+ elements = operands.map do |op|
40
+ if op.respond_to?(:each_statement)
41
+ op.each_statement(&block)
42
+ op.subject
43
+ else
44
+ op
45
+ end
46
+ end
47
+ list = RDF::List(*elements)
48
+ list.each_statement(&block)
49
+ block.call(RDF::Statement(@subject, RDF::Vocab::SHACL.alternativePath, list.subject))
50
+ end
51
+ end
52
+
53
+ refine SPARQL::Algebra::Operator::PathOpt do
54
+ # @!parse
55
+ # # Refinements on SPARQL::Algebra::Operator::PathOpt
56
+ # class SPARQL::Algebra::Operator::Alt
57
+ # ##
58
+ # # Retrieve the possibly newly assigned blank node subject to use for representing this operator.
59
+ # # @return [RDF::Node]
60
+ # attr_accessor :subject
61
+ #
62
+ # ##
63
+ # # Generate the SHACL representation of this operator
64
+ # # @return [RDF::Node]
65
+ # def each_statement(&block); end.
66
+ # end
67
+ attr_accessor :subject
68
+ def each_statement(&block)
69
+ @subject = RDF::Node.new
70
+ operands.each do |op|
71
+ obj = if op.respond_to?(:each_statement)
72
+ op.each_statement(&block)
73
+ op.subject
74
+ else
75
+ op
76
+ end
77
+ block.call(RDF::Statement(@subject, RDF::Vocab::SHACL.zeroOrOnePath, obj))
78
+ end
79
+ end
80
+ end
81
+
82
+ refine SPARQL::Algebra::Operator::PathPlus do
83
+ # @!parse
84
+ # # Refinements on SPARQL::Algebra::Operator::PathPlus
85
+ # class SPARQL::Algebra::Operator::Alt
86
+ # ##
87
+ # # Retrieve the possibly newly assigned blank node subject to use for representing this operator.
88
+ # # @return [RDF::Node]
89
+ # attr_accessor :subject
90
+ #
91
+ # ##
92
+ # # Generate the SHACL representation of this operator
93
+ # # @return [RDF::Node]
94
+ # def each_statement(&block); end.
95
+ # end
96
+ attr_accessor :subject
97
+ def each_statement(&block)
98
+ @subject = RDF::Node.new
99
+ operands.each do |op|
100
+ obj = if op.respond_to?(:each_statement)
101
+ op.each_statement(&block)
102
+ op.subject
103
+ else
104
+ op
105
+ end
106
+ block.call(RDF::Statement(@subject, RDF::Vocab::SHACL.oneOrMorePath, obj))
107
+ end
108
+ end
109
+ end
110
+
111
+ refine SPARQL::Algebra::Operator::PathStar do
112
+ # @!parse
113
+ # # Refinements on SPARQL::Algebra::Operator::PathPlus
114
+ # class SPARQL::Algebra::Operator::Alt
115
+ # ##
116
+ # # Retrieve the possibly newly assigned blank node subject to use for representing this operator.
117
+ # # @return [RDF::Node]
118
+ # attr_accessor :subject
119
+ #
120
+ # ##
121
+ # # Generate the SHACL representation of this operator
122
+ # # @return [RDF::Node]
123
+ # def each_statement(&block); end.
124
+ # end
125
+ attr_accessor :subject
126
+ def each_statement(&block)
127
+ @subject = RDF::Node.new
128
+ operands.each do |op|
129
+ obj = if op.respond_to?(:each_statement)
130
+ op.each_statement(&block)
131
+ op.subject
132
+ else
133
+ op
134
+ end
135
+ block.call(RDF::Statement(@subject, RDF::Vocab::SHACL.zeroOrMorePath, obj))
136
+ end
137
+ end
138
+ end
139
+
140
+ refine SPARQL::Algebra::Operator::Reverse do
141
+ # @!parse
142
+ # # Refinements on SPARQL::Algebra::Operator::Reverse
143
+ # class SPARQL::Algebra::Operator::Alt
144
+ # ##
145
+ # # Retrieve the possibly newly assigned blank node subject to use for representing this operator.
146
+ # # @return [RDF::Node]
147
+ # attr_accessor :subject
148
+ #
149
+ # ##
150
+ # # Generate the SHACL representation of this operator
151
+ # # @return [RDF::Node]
152
+ # def each_statement(&block); end.
153
+ # end
154
+ attr_accessor :subject
155
+ def each_statement(&block)
156
+ @subject = RDF::Node.new
157
+ operands.each do |op|
158
+ obj = if op.respond_to?(:each_statement)
159
+ op.each_statement(&block)
160
+ op.subject
161
+ else
162
+ op
163
+ end
164
+ block.call(RDF::Statement(@subject, RDF::Vocab::SHACL.inversePath, obj))
165
+ end
166
+ end
167
+ end
168
+
169
+ refine SPARQL::Algebra::Operator::Seq do
170
+ # @!parse
171
+ # # Refinements on SPARQL::Algebra::Operator::Seq
172
+ # class SPARQL::Algebra::Operator::Alt
173
+ # ##
174
+ # # Retrieve the possibly newly assigned blank node subject to use for representing this operator.
175
+ # # @return [RDF::Node]
176
+ # attr_accessor :subject
177
+ #
178
+ # ##
179
+ # # Generate the SHACL representation of this operator
180
+ # # @return [RDF::Node]
181
+ # def each_statement(&block); end.
182
+ # end
183
+ attr_accessor :subject
184
+ def each_statement(&block)
185
+ elements = operands.map do |op|
186
+ if op.respond_to?(:each_statement)
187
+ op.each_statement(&block)
188
+ op.subject
189
+ else
190
+ op
191
+ end
192
+ end
193
+ list = RDF::List(*elements)
194
+ list.each_statement(&block)
195
+ @subject = list.subject
196
+ end
197
+ end
198
+ end
@@ -0,0 +1,160 @@
1
+ require_relative 'algebra'
2
+ require_relative 'validation_report'
3
+ require_relative 'context'
4
+ require 'json/ld'
5
+
6
+ module SHACL
7
+ ##
8
+ # The set of shapes loaded from a graph.
9
+ class Shapes < Array
10
+ include RDF::Util::Logger
11
+
12
+ # The graphs which have been loaded as shapes
13
+ #
14
+ # @return [Array<RDF::URI>]
15
+ attr_reader :loaded_graphs
16
+
17
+ # The JSON used to instantiate shapes
18
+ #
19
+ # @return [Array<Hash>]
20
+ attr_reader :shape_json
21
+
22
+ ##
23
+ # Initializes the shapes from `graph`loading `owl:imports` until all references are loaded.
24
+ #
25
+ # The shapes come from the following:
26
+ # * Instances of `sh:NodeShape` or `sh:PropertyShape`
27
+ # * resources that have any of the properties `sh:targetClass`, `sh:targetNode`, `sh:targetObjectsOf`, or `sh:targetSubjectsOf`.
28
+ #
29
+ # @param [RDF::Graph] graph
30
+ # @param [Array<RDF::URI>] loaded_graphs = []
31
+ # @param [Hash{Symbol => Object}] options
32
+ # @return [Shapes]
33
+ # @raise [SHACL::Error]
34
+ def self.from_graph(graph, loaded_graphs: [], **options)
35
+ @loded_graphs = loaded_graphs
36
+
37
+ import_count = 0
38
+ while (imports = graph.query(predicate: RDF::OWL.imports).map(&:object)).count > import_count
39
+ # Load each imported graph
40
+ imports.each do |ref|
41
+ graph.load(imports)
42
+ loaded_graphs << ref
43
+ import_count += 1
44
+ end
45
+ end
46
+
47
+ # Serialize the graph as framed JSON-LD and initialize patterns, recursively.
48
+ shape_json = JSON::LD::API.fromRdf(graph, useNativeTypes: true) do |expanded|
49
+ JSON::LD::API.frame(expanded, SHAPES_FRAME, omitGraph: false, embed: '@always', expanded: true)
50
+ end['@graph']
51
+
52
+ # Create an array of the framed shapes
53
+ shapes = self.new(shape_json.map {|o| Algebra.from_json(o, **options)})
54
+ shapes.instance_variable_set(:@shape_json, shape_json)
55
+ shapes
56
+ end
57
+
58
+ ##
59
+ # Retrieve shapes from a sh:shapesGraph reference within the queryable
60
+ #
61
+ # @param [RDF::Queryable] queryable
62
+ # The data graph which may contain references to the shapes graph
63
+ # @param [Hash{Symbol => Object}] options
64
+ # @return [Shapes]
65
+ # @raise [SHACL::Error]
66
+ def self.from_queryable(queryable, **options)
67
+ # Query queryable to find one ore more shapes graphs
68
+ graphs = queryable.query(predicate: RDF::Vocab::SHACL.shapesGraph).objects
69
+ graph = RDF::Graph.new do |g|
70
+ graphs.each {|iri| g.load(iri)}
71
+ end
72
+ from_graph(graph, loaded_graphs: graphs, **options)
73
+ end
74
+
75
+ ##
76
+ # Match on schema. Finds appropriate shape for node, and matches that shape.
77
+ #
78
+ # @param [RDF::Queryable] graph
79
+ # @return [Hash{RDF::Term => Array<ValidationResult>}] Returns _ValidationResults_, a hash of focus nodes to the results of their associated shapes
80
+ # @param [Hash{Symbol => Object}] options
81
+ # @option options [RDF::Term] :focus
82
+ # An explicit focus node, overriding any defined on the top-level shaps.
83
+ # @return [SHACL::ValidationReport]
84
+ def execute(graph, depth: 0, **options)
85
+ self.each do |shape|
86
+ shape.graph = graph
87
+ shape.each_descendant do |op|
88
+ op.graph = graph
89
+ end
90
+ end
91
+
92
+ # Execute all shapes against their target nodes
93
+ ValidationReport.new(self.map do |shape|
94
+ nodes = Array(options.fetch(:focus, shape.targetNodes))
95
+ nodes.map do |node|
96
+ shape.conforms(node, depth: depth + 1)
97
+ end
98
+ end.flatten)
99
+ end
100
+
101
+ def to_sxp_bin
102
+ [:shapes, super]
103
+ end
104
+
105
+ def to_sxp
106
+ to_sxp_bin.to_sxp
107
+ end
108
+
109
+ SHAPES_FRAME = JSON.parse(%({
110
+ "@context": {
111
+ "id": "@id",
112
+ "type": {"@id": "@type", "@container": "@set"},
113
+ "@vocab": "http://www.w3.org/ns/shacl#",
114
+ "rdfs": "http://www.w3.org/2000/01/rdf-schema#",
115
+ "shacl": "http://www.w3.org/ns/shacl#",
116
+ "sh": "http://www.w3.org/ns/shacl#",
117
+ "xsd": "http://www.w3.org/2001/XMLSchema#",
118
+ "and": {"@type": "@id", "@container": "@list"},
119
+ "annotationProperty": {"@type": "@id"},
120
+ "class": {"@type": "@id"},
121
+ "comment": "http://www.w3.org/2000/01/rdf-schema#comment",
122
+ "condition": {"@type": "@id"},
123
+ "datatype": {"@type": "@vocab"},
124
+ "declare": {"@type": "@id"},
125
+ "disjoint": {"@type": "@id"},
126
+ "entailment": {"@type": "@id"},
127
+ "equals": {"@type": "@id"},
128
+ "ignoredProperties": {"@type": "@id", "@container": "@list"},
129
+ "in": {"@type": "@none", "@container": "@list"},
130
+ "inversePath": {"@type": "@id"},
131
+ "label": "http://www.w3.org/2000/01/rdf-schema#label",
132
+ "languageIn": {"@container": "@list"},
133
+ "lessThan": {"@type": "@id"},
134
+ "lessThanOrEquals": {"@type": "@id"},
135
+ "nodeKind": {"@type": "@vocab"},
136
+ "or": {"@type": "@id", "@container": "@list"},
137
+ "path": {"@type": "@none"},
138
+ "property": {"@type": "@id"},
139
+ "severity": {"@type": "@vocab"},
140
+ "targetClass": {"@type": "@id"},
141
+ "targetNode": {"@type": "@none"},
142
+ "xone": {"@type": "@id", "@container": "@list"}
143
+ },
144
+ "and": {},
145
+ "class": {},
146
+ "datatype": {},
147
+ "in": {},
148
+ "node": {},
149
+ "nodeKind": {},
150
+ "not": {},
151
+ "or": {},
152
+ "property": {},
153
+ "targetClass": {},
154
+ "targetNode": {},
155
+ "targetObjectsOf": {},
156
+ "xone": {},
157
+ "targetSubjectsOf": {}
158
+ })).freeze
159
+ end
160
+ end