rdf-agraph 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,484 @@
1
+ module RDF::AllegroGraph
2
+ # Features shared by regular AllegroGraph repositories and by persistent
3
+ # backend sessions.
4
+ #
5
+ # Note that this class does not interoperate well with the Unix `fork`
6
+ # command if you're using blank nodes. See README.md for details.
7
+ class AbstractRepository < RDF::Repository
8
+ # This code is based on
9
+ # http://blog.datagraph.org/2010/04/rdf-repository-howto
10
+ #
11
+ # For comparison purposes, here's a list of other RDF::Repository
12
+ # implementations:
13
+ #
14
+ # https://github.com/fumi/rdf-4store/blob/master/lib/rdf/four_store/repository.rb
15
+ # https://github.com/bendiken/rdf-bert/blob/master/lib/rdf/bert/client.rb
16
+ # https://github.com/bendiken/rdf-cassandra/blob/master/lib/rdf/cassandra/repository.rb (more complete than many)
17
+ # https://github.com/bhuga/rdf-do/blob/master/lib/rdf/do.rb
18
+ # https://github.com/pius/rdf-mongo/blob/master/lib/rdf/mongo.rb
19
+ # https://github.com/njh/rdf-redstore/blob/master/lib/rdf/redstore/repository.rb
20
+ # https://github.com/bendiken/rdf-sesame/blob/master/lib/rdf/sesame/repository.rb
21
+ # https://github.com/bhuga/rdf-talis/blob/master/lib/rdf/talis/repository.rb
22
+ # https://github.com/bendiken/sparql-client/blob/master/lib/sparql/client/repository.rb
23
+ #
24
+ # We actually stack up pretty well against this list.
25
+
26
+
27
+ #--------------------------------------------------------------------
28
+ # @group RDF::Repository methods
29
+
30
+ # Create a new AllegroGraph repository adapter.
31
+ #
32
+ # @param [AllegroGraph::Resource] resource
33
+ # The underlying 'agraph'-based implementation to wrap.
34
+ # @private
35
+ def initialize(resource)
36
+ @repo = resource
37
+ @blank_nodes = []
38
+ @blank_nodes_to_generate = 8
39
+ @blank_nodes_local_to_server = {}
40
+ @blank_nodes_server_to_local = {}
41
+ end
42
+
43
+ # Returns true if `feature` is supported.
44
+ #
45
+ # @param [Symbol] feature
46
+ # @return [Boolean]
47
+ def supports?(feature)
48
+ case feature.to_sym
49
+ when :context then true
50
+ else super
51
+ end
52
+ end
53
+
54
+
55
+ #--------------------------------------------------------------------
56
+ # @group RDF::Transaction support
57
+ #
58
+ # TODO: Implement before_execute and after_execute. Note that
59
+ # RDF::Transaction can only operate on a single graph at a time. The
60
+ # RDF.rb transaction API is still pretty weak, and it is expected to
61
+ # be refined over the course of the RDF.rb 0.3.x series.
62
+ #
63
+ # Or should we implement the methods described here?
64
+ # http://blog.datagraph.org/2010/12/rdf-for-ruby It's not clear how
65
+ # we should tackle this.
66
+
67
+
68
+ #--------------------------------------------------------------------
69
+ # @group RDF::Enumerable methods
70
+
71
+ # Iterate over all statements in the repository. This is used by
72
+ # RDF::Enumerable as a fallback for handling any unimplemented
73
+ # methods.
74
+ #
75
+ # @yield [statement]
76
+ # @yieldparam [RDF::Statement] statement
77
+ # @yieldreturn [void]
78
+ # @return [void]
79
+ def each(&block)
80
+ query_pattern(RDF::Query::Pattern.new, &block)
81
+ end
82
+
83
+ # Does the repository contain the specified statement?
84
+ #
85
+ # @param [RDF::Statement] statement
86
+ # @return [Boolean]
87
+ def has_statement?(statement)
88
+ found = @repo.statements.find(statement_to_dict(statement))
89
+ !found.empty?
90
+ end
91
+
92
+ # TODO: There are lots of methods with names like 'predicates',
93
+ # 'each_predicate', etc., that we could usefully override if anybody
94
+ # needs to be able to list all the predicates in the repository
95
+ # without scanning every record. But we'll wait until somebody needs
96
+ # those before overriding the default implementations.
97
+
98
+
99
+ #--------------------------------------------------------------------
100
+ # @group RDF::Countable methods
101
+ #
102
+ # TODO: I'd love to override these methods for the sake of
103
+ # performance, but RDF.rb does not want duplicate statements to be
104
+ # counted twice, and AllegoGraph does count them.
105
+
106
+ # Is this repository empty?
107
+ #def empty?
108
+ # count == 0
109
+ #end
110
+
111
+ # How many statements are in this repository?
112
+ #def count
113
+ # @repo.request_http(:get, path(:statements),
114
+ # :headers => { 'Accept' => 'text/integer' },
115
+ # :expected_status_code => 200).chomp.to_i
116
+ #end
117
+
118
+
119
+ #--------------------------------------------------------------------
120
+ # @group RDF::Queryable methods
121
+
122
+ # Find all RDF statements matching a pattern.
123
+ #
124
+ # @overload query_pattern(pattern) {|statement| ... }
125
+ # @yield statement
126
+ # @yieldparam [RDF::Statement] statement
127
+ # @yieldreturn [void]
128
+ # @return [void]
129
+ #
130
+ # @overload query_pattern(pattern)
131
+ # @return [Enumerator]
132
+ #
133
+ # @param [RDF::Query::Pattern] pattern A simple pattern to match.
134
+ # @return [void]
135
+ def query_pattern(pattern)
136
+ if block_given?
137
+ seen = {}
138
+ dict = statement_to_dict(pattern)
139
+ dict.delete(:context) if dict[:context] == 'null'
140
+ @repo.statements.find(dict).each do |statement|
141
+ unless seen.has_key?(statement)
142
+ seen[statement] = true
143
+ s,p,o,c = statement.map {|v| unserialize(v) }
144
+ if c.nil?
145
+ yield RDF::Statement.new(s,p,o)
146
+ else
147
+ yield RDF::Statement.new(s,p,o, :context => c)
148
+ end
149
+ end
150
+ end
151
+ else
152
+ enum_for(:query_pattern, pattern)
153
+ end
154
+ end
155
+ protected :query_pattern
156
+
157
+ # TODO: Override first, first_subject, first_predicate, first_object,
158
+ # first_literal for performance.
159
+
160
+ # Run an RDF::Query on the server.
161
+ #
162
+ # @param [RDF::Query] query The query to execute.
163
+ # @yield solution
164
+ # @yieldparam [RDF::Query::Solution] solution
165
+ # @yieldreturn [void]
166
+ #
167
+ # @see RDF::Queryable#query
168
+ # @see RDF::Query#execute
169
+ def query_execute(query, &block)
170
+ if query.respond_to?(:requires_prolog?) && query.requires_prolog?
171
+ prolog_query(query.to_prolog(self), &block)
172
+ else
173
+ sparql_query(query_to_sparql(query), &block)
174
+ end
175
+ end
176
+ protected :query_execute
177
+
178
+
179
+ #--------------------------------------------------------------------
180
+ # @group AllegroGraph-specific query methods
181
+
182
+ # Run a raw SPARQL query.
183
+ #
184
+ # @overload sparql_query(query) {|solution| ... }
185
+ # @yield solution
186
+ # @yieldparam [RDF::Query::Solution] solution
187
+ # @yieldreturn [void]
188
+ # @return [void]
189
+ #
190
+ # @overload sparql_query(pattern)
191
+ # @return [Enumerator<RDF::Query::Solution>]
192
+ #
193
+ # @param [String] query The query to run.
194
+ # @return [void]
195
+ # @note This function returns a single-use Enumerator! If you want to
196
+ # to treat the results as an array, call `to_a` on it, or you will
197
+ # re-run the query against the server repeatedly. This curious
198
+ # decision is made for consistency with RDF.rb.
199
+ def sparql_query(query, &block)
200
+ raw_query(:sparql, query, &block)
201
+ end
202
+
203
+ # Run a raw Prolog query.
204
+ #
205
+ # @overload prolog_query(query) {|solution| ... }
206
+ # @yield solution
207
+ # @yieldparam [RDF::Query::Solution] solution
208
+ # @yieldreturn [void]
209
+ # @return [void]
210
+ #
211
+ # @overload prolog_query(pattern)
212
+ # @return [Enumerator<RDF::Query::Solution>]
213
+ #
214
+ # @param [String] query The query to run.
215
+ # @return [void]
216
+ # @note This function returns a single-use Enumerator! If you want to
217
+ # to treat the results as an array, call `to_a` on it, or you will
218
+ # re-run the query against the server repeatedly. This curious
219
+ # decision is made for consistency with RDF.rb.
220
+ def prolog_query(query, &block)
221
+ raw_query(:prolog, query, &block)
222
+ end
223
+
224
+ # Run a raw query in the specified language.
225
+ def raw_query(language, query, &block)
226
+ @repo.query.language = language
227
+ results = json_to_query_solutions(@repo.query.perform(query))
228
+ if block_given?
229
+ results.each {|s| yield s }
230
+ else
231
+ enum_for(:raw_query, language, query)
232
+ end
233
+ end
234
+ protected :raw_query
235
+
236
+ # Construct an AllegroGraph-specific query.
237
+ #
238
+ # @yield query
239
+ # @yieldparam [Query] The query to build. Use the Query API to add
240
+ # patterns and functors.
241
+ # @yieldreturn [void]
242
+ # @return [Query]
243
+ #
244
+ # @see Query
245
+ # @see RDF::Query
246
+ def build_query(&block)
247
+ Query.new(self, &block)
248
+ end
249
+
250
+
251
+ #--------------------------------------------------------------------
252
+ # @group RDF::Mutable methods
253
+
254
+ # Insert a single statement into the repository.
255
+ #
256
+ # @param [RDF::Statement] statement
257
+ # @return [void]
258
+ def insert_statement(statement)
259
+ insert_statements([statement])
260
+ end
261
+ protected :insert_statement
262
+
263
+ # Insert multiple statements at once.
264
+ #
265
+ # @param [Array<RDF::Statement>] statements
266
+ # @return [void]
267
+ def insert_statements(statements)
268
+ # FIXME: RDF.rb expects duplicate statements to be ignored if
269
+ # inserted into a mutable store, but AllegoGraph allows duplicate
270
+ # statements. We work around this in our other methods, but we
271
+ # need to either use transactions, find appropriate AllegroGraph
272
+ # documentation, or talk to the RDF.rb folks.
273
+ #
274
+ # A discussion of duplicate RDF statements:
275
+ # http://lists.w3.org/Archives/Public/www-rdf-interest/2004Oct/0091.html
276
+ #
277
+ # Note that specifying deleteDuplicates on repository creation doesn't
278
+ # seem to affect this.
279
+ json = statements_to_json(statements)
280
+ @repo.request_json(:post, path(:statements), :body => json,
281
+ :expected_status_code => 204)
282
+ end
283
+ protected :insert_statements
284
+
285
+ # Delete a single statement from the repository.
286
+ #
287
+ # @param [RDF::Statement] statement
288
+ # @return [void]
289
+ def delete_statement(statement)
290
+ # TODO: Do we need to handle invalid statements here by turning them
291
+ # into queries and deleting all matching statements?
292
+ delete_statements([statement])
293
+ end
294
+ protected :delete_statement
295
+
296
+ # Delete multiple statements from the repository.
297
+ #
298
+ # @param [Array<RDF::Statement>] statements
299
+ # @return [void]
300
+ def delete_statements(statements)
301
+ json = statements_to_json(statements)
302
+ @repo.request_json(:post, path('statements/delete'),
303
+ :body => json, :expected_status_code => 204)
304
+ end
305
+ protected :delete_statements
306
+
307
+ # TODO: Override delete to implement fast wildcard deletion without
308
+ # having to first query for the matching records.
309
+
310
+ # Clear all statements from the repository.
311
+ #
312
+ # @return [void]
313
+ def clear
314
+ @repo.statements.delete
315
+ end
316
+
317
+
318
+ #--------------------------------------------------------------------
319
+ # @group Serialization methods
320
+
321
+ # Serialize an RDF::Value for transmission to the server. This
322
+ # is exported for low-level libraries that need to access our
323
+ # serialization and deserialization machinery, which has special-case
324
+ # support for RDF nodes.
325
+ #
326
+ # @param [RDF::Value,RDF::Query::Variable] value
327
+ # @return [String]
328
+ # @see #serialize_prolog
329
+ def serialize(value)
330
+ case value
331
+ when RDF::Query::Variable then value.to_s
332
+ else RDF::NTriples::Writer.serialize(map_to_server(value))
333
+ end
334
+ end
335
+
336
+ # Serialize an RDF::Value for use in a Prolog expression that will
337
+ # be transmitted to the server.
338
+ #
339
+ # @param [RDF::Value,RDF::Query::Variable] value
340
+ # @return [String]
341
+ # @see #serialize
342
+ def serialize_prolog(value)
343
+ case value
344
+ when RDF::AllegroGraph::Query::PrologLiteral then value.to_s
345
+ when RDF::Query::Variable then value.to_s
346
+ else "!#{serialize(value)}"
347
+ end
348
+ end
349
+
350
+
351
+ protected
352
+
353
+ # Build a repository-relative path.
354
+ def path(relative_path)
355
+ "#{@repo.path}/#{relative_path}"
356
+ end
357
+
358
+ # Deserialize an RDF::Value received from the server, or an array of such
359
+ # values when working with Prolog queries.
360
+ #
361
+ # @param [String,Array] str_or_array
362
+ # A string, or a possibly-nested array of strings.
363
+ # @return [RDF::Value]
364
+ # @see #serialize
365
+ def unserialize(str_or_array)
366
+ case str_or_array
367
+ when Array then str_or_array.map {|v| unserialize(v) }
368
+ else map_from_server(RDF::NTriples::Reader.unserialize(str_or_array))
369
+ end
370
+ end
371
+
372
+ # Convert a list of statements to a JSON-compatible array.
373
+ def statements_to_json(statements)
374
+ statements.map do |s|
375
+ tuple = [s.subject, s.predicate, s.object]
376
+ tuple << s.context if s.context
377
+ tuple.map {|v| serialize(v) }
378
+ end
379
+ end
380
+
381
+ # Translate a RDF::Statement into a dictionary the we can pass
382
+ # directly to the 'agraph' gem.
383
+ def statement_to_dict(statement)
384
+ {
385
+ :subject => serialize(statement.subject),
386
+ :predicate => serialize(statement.predicate),
387
+ :object => serialize(statement.object),
388
+ # We have to pass the null context explicitly if we only want
389
+ # to operate a single statement. Otherwise, we will operate
390
+ # on all matching s,p,o triples regardless of context.
391
+ :context => serialize(statement.context) || 'null'
392
+ }
393
+ end
394
+
395
+ # Convert a query to SPARQL.
396
+ def query_to_sparql(query)
397
+ variables = []
398
+ patterns = []
399
+ query.patterns.each do |p|
400
+ p.variables.each {|_,v| variables << v unless variables.include?(v) }
401
+ triple = [p.subject, p.predicate, p.object]
402
+ str = triple.map {|v| serialize(v) }.join(" ")
403
+ # TODO: Wrap in graph block for context!
404
+ if p.optional?
405
+ str = "OPTIONAL { #{str} }"
406
+ end
407
+ patterns << "#{str} ."
408
+ end
409
+ "SELECT #{variables.join(" ")}\nWHERE {\n #{patterns.join("\n ")} }"
410
+ end
411
+
412
+ # Convert a JSON query solution to a list of RDF::Query::Solution
413
+ # objects.
414
+ def json_to_query_solutions(json)
415
+ names = json['names'].map {|n| n.to_sym }
416
+ json['values'].map do |match|
417
+ hash = {}
418
+ names.each_with_index do |name, i|
419
+ # TODO: I'd like to include nil values, too, but
420
+ # RDF::Query#execute does not yet do so, so we'll filter them for
421
+ # now.
422
+ hash[name] = unserialize(match[i]) unless match[i].nil?
423
+ end
424
+ RDF::Query::Solution.new(hash)
425
+ end
426
+ end
427
+
428
+ # Return true if this a blank RDF node.
429
+ def blank_node?(value)
430
+ !value.nil? && value.anonymous?
431
+ end
432
+
433
+ # Ask AllegroGraph to generate a series of blank node IDs.
434
+ def generate_blank_nodes(amount)
435
+ response = @repo.request_http(:post, path(:blankNodes),
436
+ :parameters => { :amount => amount },
437
+ :expected_status_code => 200)
438
+ response.chomp.split("\n").map {|i| i.gsub(/^_:/, '') }
439
+ end
440
+
441
+ # Allocate an "official" AllegroGraph blank node, which should
442
+ # maintain its identity across requests.
443
+ def allocate_blank_node
444
+ if @blank_nodes.empty?
445
+ @blank_nodes = generate_blank_nodes(@blank_nodes_to_generate).reverse
446
+ @blank_nodes_to_generate *= 2
447
+ end
448
+ @blank_nodes.pop
449
+ end
450
+
451
+ # Create a mapping between a local blank node ID and a server-side
452
+ # blank node ID.
453
+ def map_blank_node(local_id, server_id)
454
+ #puts "Mapping #{local_id} -> #{server_id}"
455
+ @blank_nodes_local_to_server[local_id] = server_id
456
+ @blank_nodes_server_to_local[server_id] = local_id
457
+ end
458
+
459
+ # Translate this value to a server-specific representation, taking
460
+ # care to handle blank nodes correctly.
461
+ def map_to_server(value)
462
+ return value unless blank_node?(value)
463
+ unless @blank_nodes_local_to_server.has_key?(value.id)
464
+ new_id = allocate_blank_node
465
+ map_blank_node(value.id, new_id)
466
+ end
467
+ RDF::Node.new(@blank_nodes_local_to_server[value.id])
468
+ end
469
+
470
+ # Translate this value to a client-specific representation, taking
471
+ # care to handle blank nodes correctly.
472
+ def map_from_server(value)
473
+ return value unless blank_node?(value)
474
+ if @blank_nodes_server_to_local.has_key?(value.id)
475
+ RDF::Node.new(@blank_nodes_server_to_local[value.id])
476
+ else
477
+ # We didn't generate this node ID, so we want to pass it back to
478
+ # the server unchanged.
479
+ map_blank_node(value.id, value.id)
480
+ value
481
+ end
482
+ end
483
+ end
484
+ end
@@ -0,0 +1,148 @@
1
+ module RDF::AllegroGraph::Functors
2
+
3
+ # This module contains AllegroGraph functor definitions that may be
4
+ # called when building a query. Note that these functors merely add a
5
+ # functor expression to a query. The actual functor will be called on
6
+ # the server.
7
+ #
8
+ # @see Session#generator
9
+ module SnaFunctors
10
+ # @private
11
+ PrologLiteral = RDF::AllegroGraph::Query::PrologLiteral
12
+
13
+ # @group Paths Through the Graph
14
+
15
+ # Search for paths between two nodes following the edges specified by
16
+ # generator, and using a breadth-first search strategy.
17
+ #
18
+ # @param [Symbol,RDF::Resource] from
19
+ # Input: The start node in the path.
20
+ # @param [Symbol,RDF::Resource] to
21
+ # Input: The end node in the path.
22
+ # @param [PrologLiteral] generator
23
+ # Input: The generator to use when finding links to traverse.
24
+ # @param [Symbol] to
25
+ # Output: A list of nodes in the path.
26
+ # @param [Hash] options
27
+ # @option options [Integer] :max_depth
28
+ # Input: The maxium search depth.
29
+ # @return [void]
30
+ def breadth_first_search_paths(from, to, generator, path, options={})
31
+ search_paths('breadth-first-search-paths', from, to, generator, path,
32
+ options)
33
+ end
34
+
35
+ # Search for paths between two nodes following the edges specified by
36
+ # generator, and using a depth-first search strategy.
37
+ #
38
+ # @param [Symbol,RDF::Resource] from
39
+ # Input: The start node in the path.
40
+ # @param [Symbol,RDF::Resource] to
41
+ # Input: The end node in the path.
42
+ # @param [PrologLiteral] generator
43
+ # Input: The generator to use when finding links to traverse.
44
+ # @param [Symbol] to
45
+ # Output: A list of nodes in the path.
46
+ # @param [Hash] options
47
+ # @option options [Integer] :max_depth
48
+ # Input: The maxium search depth.
49
+ # @return [void]
50
+ def depth_first_search_paths(from, to, generator, path, options={})
51
+ search_paths('depth-first-search-paths', from, to, generator, path,
52
+ options)
53
+ end
54
+
55
+ # Search for paths between two nodes following the edges specified by
56
+ # generator, and using a bidirectional search strategy.
57
+ #
58
+ # @param [Symbol,RDF::Resource] from
59
+ # Input: The start node in the path.
60
+ # @param [Symbol,RDF::Resource] to
61
+ # Input: The end node in the path.
62
+ # @param [PrologLiteral] generator
63
+ # Input: The generator to use when finding links to traverse.
64
+ # @param [Symbol] to
65
+ # Output: A list of nodes in the path.
66
+ # @param [Hash] options
67
+ # @option options [Integer] :max_depth
68
+ # Input: The maxium search depth.
69
+ # @return [void]
70
+ def bidirectional_search_paths(from, to, generator, path, options={})
71
+ search_paths('bidirectional-search-paths', from, to, generator, path,
72
+ options)
73
+ end
74
+
75
+ # @private
76
+ def search_paths(functor_name, from, to, generator, path, options={})
77
+ if options.has_key?(:max_depth)
78
+ functor(functor_name, from, to, generator,
79
+ PrologLiteral.new(options[:max_depth]), path)
80
+ else
81
+ functor(functor_name, from, to, generator, path)
82
+ end
83
+ end
84
+
85
+
86
+ # @group Nearby Nodes
87
+
88
+ # Count the neighbors of the actor.
89
+ #
90
+ # @param [Symbol,RDF::Resource] actor
91
+ # Input: The node whose neighbors we want to find.
92
+ # @param [PrologLiteral] generator
93
+ # Input: The generator to use when finding links to traverse.
94
+ # @param [Symbol] count
95
+ # Output: The number of neighbors of the actor.
96
+ # @return [void]
97
+ def neighbor_count(actor, generator, count)
98
+ functor('nodal-degree', actor, generator, count)
99
+ end
100
+ alias_method :nodal_degree, :neighbor_count
101
+
102
+ # Find all neighbors of the actor.
103
+ #
104
+ # @param [Symbol,RDF::Resource] actor
105
+ # Input: The node whose neighbors we want to find.
106
+ # @param [PrologLiteral] generator
107
+ # Input: The generator to use when finding links to traverse.
108
+ # @param [Symbol] neighbor
109
+ # Output: A neighbor of the actor.
110
+ # @return [void]
111
+ def neighbors(actor, generator, neighbor)
112
+ functor('nodal-neighbors', actor, generator, neighbor)
113
+ end
114
+ alias_method :nodal_neighbors, :neighbors
115
+
116
+ # Generate an actor's ego group.
117
+ #
118
+ # @param [Symbol,RDF::Resource] actor
119
+ # Input: The resource at the center of the graph.
120
+ # @param [Integer] depth
121
+ # Input: The maximum number of links to traverse.
122
+ # @param [PrologLiteral] generator
123
+ # Input: The generator to use when finding links to traverse.
124
+ # @param [Array<RDF::Resource>] group
125
+ # Output: Either a variable or resource.
126
+ # @return [void]
127
+ def ego_group(actor, depth, generator, group)
128
+ functor('ego-group', actor, PrologLiteral.new(depth),
129
+ generator, group)
130
+ end
131
+
132
+ # Generate all members of an actor's ego group.
133
+ #
134
+ # @param [Symbol,RDF::Resource] actor
135
+ # Input: The resource at the center of the graph.
136
+ # @param [Integer] depth
137
+ # Input: The maximum number of links to traverse.
138
+ # @param [PrologLiteral] generator
139
+ # Input: The generator to use when finding links to traverse.
140
+ # @param [Symbol,RDF::Resource] group
141
+ # Input/Output: Either a variable or resource.
142
+ # @return [void]
143
+ def ego_group_member(actor, depth, generator, member)
144
+ functor('ego-group-member', actor, PrologLiteral.new(depth),
145
+ generator, member)
146
+ end
147
+ end
148
+ end
@@ -0,0 +1,4 @@
1
+ # AllegroGraph functors for use in Prolog queries.
2
+ module RDF::AllegroGraph::Functors
3
+ autoload :SnaFunctors, 'rdf/allegro_graph/functors/sna_functors'
4
+ end
@@ -0,0 +1,56 @@
1
+ class RDF::AllegroGraph::Query
2
+
3
+ # A functor expression in a Prolog query (other than an ordinary
4
+ # pattern).
5
+ #
6
+ # @see RDF::Query::Pattern
7
+ # @see RDF::AllegroGraph::Functors
8
+ class FunctorExpression
9
+ # The name of this functor.
10
+ attr_reader :name
11
+
12
+ # The arguments passed to this functor.
13
+ attr_reader :arguments
14
+
15
+ # Construct a new functor.
16
+ #
17
+ # @param [String] name
18
+ # @param [Array<Symbol,RDF::Value,value>] arguments
19
+ # The arguments to the functor, which may be either variables,
20
+ # RDF::Value objects, or Ruby values that we can convert to literals.
21
+ def initialize(name, *arguments)
22
+ @name = name
23
+ @arguments = arguments.map do |arg|
24
+ case arg
25
+ when Symbol then RDF::Query::Variable.new(arg)
26
+ when PrologLiteral, RDF::Value then arg
27
+ else RDF::Literal.new(arg)
28
+ end
29
+ end
30
+ end
31
+
32
+ # Return a hash table of all variables used in this functor. This
33
+ # is intended to be duck-type compatible with the same method in
34
+ # RDF::Query::Pattern.
35
+ #
36
+ # @return [Hash<Symbol,RDF::Query::Variable>]
37
+ # @see RDF::Query::Pattern#variables
38
+ def variables
39
+ result = {}
40
+ @arguments.each do |arg|
41
+ result.merge!(arg.variables) if arg.is_a?(RDF::Query::Variable)
42
+ end
43
+ result
44
+ end
45
+
46
+ # Convert this functor to a Prolog Lisp expression.
47
+ #
48
+ # @param [RDF::AllegroGraph::Repository] repository
49
+ # @return [String]
50
+ # @private
51
+ def to_prolog(repository)
52
+ args = arguments.map {|a| repository.serialize_prolog(a) }
53
+ "(#{name} #{args.join(" ")})"
54
+ end
55
+ end
56
+ end