rdf 0.0.5 → 0.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/README +49 -32
- data/VERSION +1 -1
- data/lib/rdf.rb +7 -0
- data/lib/rdf/cli.rb +1 -1
- data/lib/rdf/format.rb +95 -0
- data/lib/rdf/graph.rb +8 -0
- data/lib/rdf/literal.rb +66 -1
- data/lib/rdf/node.rb +34 -4
- data/lib/rdf/ntriples.rb +12 -0
- data/lib/rdf/ntriples/format.rb +13 -0
- data/lib/rdf/ntriples/reader.rb +87 -0
- data/lib/rdf/ntriples/writer.rb +50 -0
- data/lib/rdf/query.rb +212 -0
- data/lib/rdf/query/pattern.rb +152 -0
- data/lib/rdf/query/solution.rb +139 -0
- data/lib/rdf/query/variable.rb +145 -0
- data/lib/rdf/reader.rb +7 -31
- data/lib/rdf/reader/ntriples.rb +4 -85
- data/lib/rdf/repository.rb +3 -1
- data/lib/rdf/statement.rb +54 -11
- data/lib/rdf/uri.rb +19 -0
- data/lib/rdf/value.rb +53 -2
- data/lib/rdf/version.rb +1 -1
- data/lib/rdf/vocabulary.rb +17 -0
- data/lib/rdf/writer.rb +16 -38
- data/lib/rdf/writer/ntriples.rb +4 -49
- metadata +21 -2
@@ -0,0 +1,87 @@
|
|
1
|
+
module RDF module NTriples
|
2
|
+
##
|
3
|
+
# N-Triples parser.
|
4
|
+
#
|
5
|
+
# @example Reading N-Triples data
|
6
|
+
# RDF::NTriples::Reader.open("spec/data/test.nt") do |reader|
|
7
|
+
# reader.each_statement do |statement|
|
8
|
+
# puts statement.inspect
|
9
|
+
# end
|
10
|
+
# end
|
11
|
+
#
|
12
|
+
# @see http://www.w3.org/TR/rdf-testcases/#ntriples
|
13
|
+
class Reader < RDF::Reader
|
14
|
+
format RDF::NTriples::Format
|
15
|
+
|
16
|
+
##
|
17
|
+
# @return [Array, nil]
|
18
|
+
# @see http://www.w3.org/TR/rdf-testcases/#ntrip_grammar
|
19
|
+
def read_triple
|
20
|
+
loop do
|
21
|
+
readline.strip! # EOFError thrown on end of input
|
22
|
+
|
23
|
+
unless blank? || read_comment
|
24
|
+
subject = read_uriref || read_bnode || fail_subject
|
25
|
+
predicate = read_uriref || fail_predicate
|
26
|
+
object = read_uriref || read_bnode || read_literal || fail_object
|
27
|
+
return [subject, predicate, object]
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
##
|
33
|
+
# @return [Boolean]
|
34
|
+
# @see http://www.w3.org/TR/rdf-testcases/#ntrip_grammar (comment)
|
35
|
+
def read_comment
|
36
|
+
match(/^#\s*(.*)$/)
|
37
|
+
end
|
38
|
+
|
39
|
+
##
|
40
|
+
# @return [URI, nil]
|
41
|
+
# @see http://www.w3.org/TR/rdf-testcases/#ntrip_grammar (uriref)
|
42
|
+
def read_uriref
|
43
|
+
if uri = match(/^<([^>]+)>/)
|
44
|
+
RDF::URI.parse(uri)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
##
|
49
|
+
# @return [Node, nil]
|
50
|
+
# @see http://www.w3.org/TR/rdf-testcases/#ntrip_grammar (nodeID)
|
51
|
+
def read_bnode
|
52
|
+
if node_id = match(/^_:([A-Za-z][A-Za-z0-9]*)/)
|
53
|
+
@nodes[node_id] ||= RDF::Node.new(node_id)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
##
|
58
|
+
# @return [String, Literal, nil]
|
59
|
+
# @see http://www.w3.org/TR/rdf-testcases/#ntrip_grammar (literal)
|
60
|
+
def read_literal
|
61
|
+
if literal = match(/^"((?:\\"|[^"])*)"/)
|
62
|
+
literal = unescaped(literal)
|
63
|
+
|
64
|
+
if language = match(/^@([a-z]+[\-a-z0-9]*)/)
|
65
|
+
RDF::Literal.new(literal, :language => language)
|
66
|
+
elsif datatype = match(/^(\^\^)/)
|
67
|
+
RDF::Literal.new(literal, :datatype => read_uriref || fail_object)
|
68
|
+
else
|
69
|
+
literal # plain string literal
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
##
|
75
|
+
# @param [String] string
|
76
|
+
# @return [String]
|
77
|
+
# @see http://www.w3.org/TR/rdf-testcases/#ntrip_strings
|
78
|
+
def unescaped(string)
|
79
|
+
["\t", "\n", "\r", "\"", "\\"].each do |escape|
|
80
|
+
string.gsub!(escape.inspect[1...-1], escape)
|
81
|
+
end
|
82
|
+
string.gsub!(/\\u([0-9A-Fa-f]{4,4})/u) { [$1.hex].pack('U*') }
|
83
|
+
string.gsub!(/\\U([0-9A-Fa-f]{8,8})/u) { [$1.hex].pack('U*') }
|
84
|
+
string
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
module RDF module NTriples
|
2
|
+
##
|
3
|
+
# N-Triples serializer.
|
4
|
+
#
|
5
|
+
# @see <http://www.w3.org/TR/rdf-testcases/#ntriples>
|
6
|
+
class Writer < RDF::Writer
|
7
|
+
format RDF::NTriples::Format
|
8
|
+
|
9
|
+
##
|
10
|
+
# @param [String] text
|
11
|
+
# @return [void]
|
12
|
+
def write_comment(text)
|
13
|
+
puts "# #{text}"
|
14
|
+
end
|
15
|
+
|
16
|
+
##
|
17
|
+
# @param [Resource] subject
|
18
|
+
# @param [URI] predicate
|
19
|
+
# @param [Value] object
|
20
|
+
# @return [void]
|
21
|
+
def write_triple(subject, predicate, object)
|
22
|
+
s = format_uri(subject)
|
23
|
+
p = format_uri(predicate)
|
24
|
+
o = object.kind_of?(RDF::URI) ? format_uri(object) : format_literal(object)
|
25
|
+
puts "%s %s %s ." % [s, p, o]
|
26
|
+
end
|
27
|
+
|
28
|
+
##
|
29
|
+
# @param [node] Resource
|
30
|
+
# @return [void]
|
31
|
+
def format_uri(node)
|
32
|
+
"<%s>" % uri_for(node)
|
33
|
+
end
|
34
|
+
|
35
|
+
##
|
36
|
+
# @param [String, Literal] literal
|
37
|
+
# @return [void]
|
38
|
+
def format_literal(literal)
|
39
|
+
case literal
|
40
|
+
when RDF::Literal
|
41
|
+
text = quoted(escaped(literal.value))
|
42
|
+
text << "@#{literal.language}" if literal.language
|
43
|
+
text << "^^<#{uri_for(literal.datatype)}>" if literal.datatype
|
44
|
+
text
|
45
|
+
else
|
46
|
+
quoted(escaped(literal.to_s))
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end end
|
data/lib/rdf/query.rb
ADDED
@@ -0,0 +1,212 @@
|
|
1
|
+
module RDF
|
2
|
+
##
|
3
|
+
# An RDF basic graph pattern query.
|
4
|
+
#
|
5
|
+
# @example Filtering solutions using a hash
|
6
|
+
# query.filter(:author => RDF::URI.new("http://ar.to/#self"))
|
7
|
+
# query.filter(:author => "Arto Bendiken")
|
8
|
+
# query.filter(:author => [RDF::URI.new("http://ar.to/#self"), "Arto Bendiken"])
|
9
|
+
# query.filter(:updated => RDF::Literal.new(Date.today))
|
10
|
+
#
|
11
|
+
# @example Filtering solutions using a block
|
12
|
+
# query.filter { |solution| solution.author.literal? }
|
13
|
+
# query.filter { |solution| solution.title =~ /^SPARQL/ }
|
14
|
+
# query.filter { |solution| solution.price < 30.5 }
|
15
|
+
# query.filter { |solution| solution.bound?(:date) }
|
16
|
+
# query.filter { |solution| solution.age.datatype == RDF::XSD.integer }
|
17
|
+
# query.filter { |solution| solution.name.language == :es }
|
18
|
+
#
|
19
|
+
# @example Reordering solutions based on a variable
|
20
|
+
# query.order_by(:updated)
|
21
|
+
# query.order_by(:updated, :created)
|
22
|
+
#
|
23
|
+
# @example Selecting particular variables only
|
24
|
+
# query.select(:title)
|
25
|
+
# query.select(:title, :description)
|
26
|
+
#
|
27
|
+
# @example Eliminating duplicate solutions
|
28
|
+
# query.distinct!
|
29
|
+
#
|
30
|
+
# @example Limiting the number of solutions
|
31
|
+
# query.offset(25).limit(10)
|
32
|
+
#
|
33
|
+
# @example Counting the number of solutions
|
34
|
+
# query.count
|
35
|
+
#
|
36
|
+
# @example Iterating over all found solutions
|
37
|
+
# query.each_solution { |solution| puts solution.inspect }
|
38
|
+
#
|
39
|
+
class Query
|
40
|
+
autoload :Pattern, 'rdf/query/pattern'
|
41
|
+
autoload :Solution, 'rdf/query/solution'
|
42
|
+
autoload :Variable, 'rdf/query/variable'
|
43
|
+
|
44
|
+
include Enumerable
|
45
|
+
|
46
|
+
# @return [Hash{Symbol => Variable}]
|
47
|
+
attr_reader :variables
|
48
|
+
|
49
|
+
# @return [Array<Pattern>]
|
50
|
+
attr_reader :patterns
|
51
|
+
|
52
|
+
# @return [Array<Hash{Symbol => Value}>] An unordered sequence of query solutions.
|
53
|
+
attr_accessor :solutions
|
54
|
+
|
55
|
+
##
|
56
|
+
# @param [Hash{Symbol => Object}] options
|
57
|
+
# @yield [query]
|
58
|
+
# @yieldparam [Query]
|
59
|
+
def initialize(options = {}, &block)
|
60
|
+
@variables = options.delete(:variables) || {}
|
61
|
+
@patterns = options.delete(:patterns) || []
|
62
|
+
@solutions = options.delete(:solutions) || []
|
63
|
+
@options = options
|
64
|
+
|
65
|
+
if block_given?
|
66
|
+
case block.arity
|
67
|
+
when 1 then block.call(self)
|
68
|
+
else instance_eval(&block)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
##
|
74
|
+
# Enumerates over each query solution.
|
75
|
+
#
|
76
|
+
# @yield [solution]
|
77
|
+
# @yieldparam [Solution]
|
78
|
+
# @return [Enumerable]
|
79
|
+
def each_solution(&block)
|
80
|
+
solutions.each do |bindings|
|
81
|
+
block.call(Solution.new(bindings))
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
alias_method :each, :each_solution
|
86
|
+
|
87
|
+
##
|
88
|
+
# Returns the number of query solutions.
|
89
|
+
#
|
90
|
+
# @return [Integer]
|
91
|
+
def size
|
92
|
+
solutions.size
|
93
|
+
end
|
94
|
+
|
95
|
+
alias_method :count, :size
|
96
|
+
|
97
|
+
##
|
98
|
+
# Filters the solution sequence by the given criteria.
|
99
|
+
#
|
100
|
+
# @param [Hash{Symbol => Object}] criteria
|
101
|
+
# @yield [solution]
|
102
|
+
# @yieldparam [Solution] solution
|
103
|
+
# @yieldreturn [Boolean]
|
104
|
+
# @return [Query]
|
105
|
+
def filter(criteria = {}, &block)
|
106
|
+
if block_given?
|
107
|
+
solutions.reject! do |bindings|
|
108
|
+
!block.call(Solution.new(bindings))
|
109
|
+
end
|
110
|
+
else
|
111
|
+
solutions.reject! do |bindings|
|
112
|
+
results = criteria.map do |name, value|
|
113
|
+
bindings[name] == value
|
114
|
+
end
|
115
|
+
!results.all?
|
116
|
+
end
|
117
|
+
end
|
118
|
+
self
|
119
|
+
end
|
120
|
+
|
121
|
+
alias_method :filter!, :filter
|
122
|
+
|
123
|
+
##
|
124
|
+
# Reorders the solution sequence based on `variables`.
|
125
|
+
#
|
126
|
+
# @param [Enumerable<Symbol>] variables
|
127
|
+
# @return [Query]
|
128
|
+
def order(*variables)
|
129
|
+
if variables.empty?
|
130
|
+
raise ArgumentError.new("wrong number of arguments (0 for 1)")
|
131
|
+
else
|
132
|
+
# TODO: support for descending sort, e.g. order(:s => :asc, :p => :desc)
|
133
|
+
variables.map! { |variable| variable.to_sym }
|
134
|
+
solutions.sort! do |a, b|
|
135
|
+
a = variables.map { |variable| a[variable].to_s }
|
136
|
+
b = variables.map { |variable| b[variable].to_s }
|
137
|
+
a <=> b
|
138
|
+
end
|
139
|
+
end
|
140
|
+
self
|
141
|
+
end
|
142
|
+
|
143
|
+
alias_method :order_by, :order
|
144
|
+
|
145
|
+
##
|
146
|
+
# Restricts the the solution sequence to the given `variables` only.
|
147
|
+
#
|
148
|
+
# @param [Enumerable<Symbol>] variables
|
149
|
+
# @return [Query]
|
150
|
+
def project(*variables)
|
151
|
+
unless variables.empty?
|
152
|
+
variables.map! { |variable| variable.to_sym }
|
153
|
+
solutions.each do |bindings|
|
154
|
+
bindings.delete_if { |k, v| !variables.include?(k) }
|
155
|
+
end
|
156
|
+
end
|
157
|
+
self
|
158
|
+
end
|
159
|
+
|
160
|
+
alias_method :select, :project
|
161
|
+
|
162
|
+
##
|
163
|
+
# Ensures solutions in the solution sequence are unique.
|
164
|
+
#
|
165
|
+
# @return [Query]
|
166
|
+
def distinct
|
167
|
+
solutions.uniq!
|
168
|
+
self
|
169
|
+
end
|
170
|
+
|
171
|
+
alias_method :distinct!, :distinct
|
172
|
+
alias_method :reduced, :distinct
|
173
|
+
alias_method :reduced!, :distinct
|
174
|
+
|
175
|
+
##
|
176
|
+
# Limits the solution sequence to bindings starting from the `start`
|
177
|
+
# offset in the overall solution sequence.
|
178
|
+
#
|
179
|
+
# @param [Integer] start
|
180
|
+
# @return [Query]
|
181
|
+
def offset(start)
|
182
|
+
slice(start, solutions.size - start)
|
183
|
+
end
|
184
|
+
|
185
|
+
##
|
186
|
+
# Limits the number of solutions to `length`.
|
187
|
+
#
|
188
|
+
# @param [Integer] length
|
189
|
+
# @return [Query]
|
190
|
+
def limit(length)
|
191
|
+
slice(0, length)
|
192
|
+
end
|
193
|
+
|
194
|
+
##
|
195
|
+
# Limits the solution sequence to `length` bindings starting from the
|
196
|
+
# `start` offset in the overall solution sequence.
|
197
|
+
#
|
198
|
+
# @param [Integer] start
|
199
|
+
# @param [Integer] length
|
200
|
+
# @return [Query]
|
201
|
+
def slice(start, length)
|
202
|
+
if start < solutions.size
|
203
|
+
solutions.slice!(start, length)
|
204
|
+
else
|
205
|
+
solutions = []
|
206
|
+
end
|
207
|
+
self
|
208
|
+
end
|
209
|
+
|
210
|
+
alias_method :slice!, :slice
|
211
|
+
end
|
212
|
+
end
|
@@ -0,0 +1,152 @@
|
|
1
|
+
module RDF class Query
|
2
|
+
##
|
3
|
+
# An RDF query pattern.
|
4
|
+
class Pattern < Statement
|
5
|
+
# @return [Hash{Symbol => Object}]
|
6
|
+
attr_reader :options
|
7
|
+
|
8
|
+
##
|
9
|
+
# @overload initialize(options = {})
|
10
|
+
# @param [Hash{Symbol => Object}] options
|
11
|
+
# @option options [Variable, Resource] :subject (nil)
|
12
|
+
# @option options [Variable, URI] :predicate (nil)
|
13
|
+
# @option options [Variable, Value] :object (nil)
|
14
|
+
# @option options [Boolean] :optional (false)
|
15
|
+
#
|
16
|
+
# @overload initialize(subject, predicate, object, options = {})
|
17
|
+
# @param [Variable, Resource] subject
|
18
|
+
# @param [Variable, URI] predicate
|
19
|
+
# @param [Variable, Value] object
|
20
|
+
# @param [Hash{Symbol => Object}] options
|
21
|
+
# @option options [Boolean] :optional (false)
|
22
|
+
def initialize(subject = nil, predicate = nil, object = nil, options = {})
|
23
|
+
case subject
|
24
|
+
when Hash
|
25
|
+
options = subject
|
26
|
+
subject = options.delete(:subject)
|
27
|
+
predicate = options.delete(:predicate)
|
28
|
+
object = options.delete(:object)
|
29
|
+
initialize(subject, predicate, object, options)
|
30
|
+
else
|
31
|
+
@options = options || {}
|
32
|
+
@subject = subject.is_a?(Symbol) ? Variable.new(subject) : subject
|
33
|
+
@predicate = predicate.is_a?(Symbol) ? Variable.new(predicate) : predicate
|
34
|
+
@object = object.is_a?(Symbol) ? Variable.new(object) : object
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
##
|
39
|
+
# @param [Graph, Repository] graph
|
40
|
+
# @return [Enumerator]
|
41
|
+
def execute(graph, &block)
|
42
|
+
graph.query(self) # FIXME
|
43
|
+
end
|
44
|
+
|
45
|
+
##
|
46
|
+
# Returns `true` if this pattern contains variables.
|
47
|
+
#
|
48
|
+
# @return [Boolean]
|
49
|
+
def variables?
|
50
|
+
subject.is_a?(Variable) ||
|
51
|
+
predicate.is_a?(Variable) ||
|
52
|
+
object.is_a?(Variable)
|
53
|
+
end
|
54
|
+
|
55
|
+
##
|
56
|
+
# Returns the number of variables in this pattern.
|
57
|
+
#
|
58
|
+
# @return [Integer] (0..3)
|
59
|
+
def variable_count
|
60
|
+
variables.size
|
61
|
+
end
|
62
|
+
|
63
|
+
alias_method :cardinality, :variable_count
|
64
|
+
alias_method :arity, :variable_count
|
65
|
+
|
66
|
+
##
|
67
|
+
# Returns all variables in this pattern.
|
68
|
+
#
|
69
|
+
# @return [Hash{Symbol => Variable}]
|
70
|
+
def variables
|
71
|
+
variables = {}
|
72
|
+
variables.merge!(subject.variables) if subject.is_a?(Variable)
|
73
|
+
variables.merge!(predicate.variables) if predicate.is_a?(Variable)
|
74
|
+
variables.merge!(object.variables) if object.is_a?(Variable)
|
75
|
+
variables
|
76
|
+
end
|
77
|
+
|
78
|
+
##
|
79
|
+
# Returns `true` if this pattern contains bindings.
|
80
|
+
#
|
81
|
+
# @return [Boolean]
|
82
|
+
def bindings?
|
83
|
+
!bindings.empty?
|
84
|
+
end
|
85
|
+
|
86
|
+
##
|
87
|
+
# Returns the number of bindings in this pattern.
|
88
|
+
#
|
89
|
+
# @return [Integer] (0..3)
|
90
|
+
def binding_count
|
91
|
+
bindings.size
|
92
|
+
end
|
93
|
+
|
94
|
+
##
|
95
|
+
# Returns all bindings in this pattern.
|
96
|
+
#
|
97
|
+
# @return [Hash{Symbol => Value}]
|
98
|
+
def bindings
|
99
|
+
bindings = {}
|
100
|
+
bindings.merge!(subject.bindings) if subject.is_a?(Variable)
|
101
|
+
bindings.merge!(predicate.bindings) if predicate.is_a?(Variable)
|
102
|
+
bindings.merge!(object.bindings) if object.is_a?(Variable)
|
103
|
+
bindings
|
104
|
+
end
|
105
|
+
|
106
|
+
##
|
107
|
+
# Returns `true` if all variables in this pattern are bound.
|
108
|
+
#
|
109
|
+
# @return [Boolean]
|
110
|
+
def bound?
|
111
|
+
!variables.empty? && variables.values.all?(&:bound?)
|
112
|
+
end
|
113
|
+
|
114
|
+
##
|
115
|
+
# Returns all bound variables in this pattern.
|
116
|
+
#
|
117
|
+
# @return [Hash{Symbol => Variable}]
|
118
|
+
def bound_variables
|
119
|
+
variables.reject { |name, variable| variable.unbound? }
|
120
|
+
end
|
121
|
+
|
122
|
+
##
|
123
|
+
# Returns `true` if all variables in this pattern are unbound.
|
124
|
+
#
|
125
|
+
# @return [Boolean]
|
126
|
+
def unbound?
|
127
|
+
!variables.empty? && variables.values.all?(&:unbound?)
|
128
|
+
end
|
129
|
+
|
130
|
+
##
|
131
|
+
# Returns all unbound variables in this pattern.
|
132
|
+
#
|
133
|
+
# @return [Hash{Symbol => Variable}]
|
134
|
+
def unbound_variables
|
135
|
+
variables.reject { |name, variable| variable.bound? }
|
136
|
+
end
|
137
|
+
|
138
|
+
##
|
139
|
+
# Returns the string representation of this pattern.
|
140
|
+
#
|
141
|
+
# @return [String]
|
142
|
+
def to_s
|
143
|
+
require 'stringio' unless defined?(StringIO)
|
144
|
+
StringIO.open do |buffer| # FIXME in RDF::Statement
|
145
|
+
buffer << (subject.is_a?(Variable) ? subject.to_s : "<#{subject}>") << ' '
|
146
|
+
buffer << (predicate.is_a?(Variable) ? predicate.to_s : "<#{predicate}>") << ' '
|
147
|
+
buffer << (object.is_a?(Variable) ? object.to_s : "<#{object}>") << ' .'
|
148
|
+
buffer.string
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end end
|