rpath 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,15 @@
1
+ ---
2
+ !binary "U0hBMQ==":
3
+ metadata.gz: !binary |-
4
+ NDRkNDdiZDc2YTVmOTBlNmY4MzMzN2VmNDU2NThlOTk1OTY4YzBjZQ==
5
+ data.tar.gz: !binary |-
6
+ MmE5NzkxYzg2MzdhNjJiYzNjN2U5NDhiMTAwNDBlYjBmYjg1YjZlNw==
7
+ SHA512:
8
+ metadata.gz: !binary |-
9
+ ODIxNmI2NWM4MTUyYzc0MDA1ZTlhNjlmNzc5YWY1YjkyNWE1Nzk1OTc0Y2Ex
10
+ NWY0ZDQ3ODI4ODQ4ODVkZmU4MzFmOTQ2YTNjMzMwNWNiNjMyYjk0OTRmNmE1
11
+ YTM0ZmQ1NGQ4ODJiZWE4MWI0NjIwNWE0ZGRhNTZiMjFmMmZkNGE=
12
+ data.tar.gz: !binary |-
13
+ ZTk2ODMyMTc4NTI1OWU1MWI0NjY5YmZkZDI4NDljNTIxNWJiN2Y4OGY0MTZj
14
+ MTI1MDA2MzhjNjU4MjdmMjJiYjMzYjc3MWFmMWY5NWUzYTdmZWI2YWIzZTVl
15
+ MDQ5NmVjZWM4YjI5OTNlYzNiMWI5MmEyZGUzMzRhMGQ3MTA0NWI=
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2015 Jonah Burke
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,263 @@
1
+ # RPath
2
+
3
+ "Don't do this." —[flavorjones](https://github.com/flavorjones) [[1]](http://www.nokogiri.org/tutorials/searching_a_xml_html_document.html)
4
+
5
+ ## Overview
6
+
7
+ RPath lets you traverse graphs, such as XML documents, with just Ruby.
8
+
9
+ RPath can operate on [Nokogiri](http://www.nokogiri.org) documents, [REXML](http://www.germane-software.com/software/rexml/) documents, and the filesystem. Building adapters for other graphs is simple.
10
+
11
+ Leading members of the Ruby community have [warned against](http://www.nokogiri.org/tutorials/searching_a_xml_html_document.html) RPath's approach. They're probably right! RPath is as much an experiment as a useful tool.
12
+
13
+ ## Documentation
14
+
15
+ This README provides an overview of RPath. Full documentation is available at [rubydoc.info](http://www.rubydoc.info/gems/rpath).
16
+
17
+ ## Installation
18
+
19
+ ```bash
20
+ gem install rpath
21
+ ```
22
+
23
+ ## Example
24
+
25
+ Suppose we want the value of the `name` attribute in the following XML document:
26
+
27
+ ```ruby
28
+ xml = Nokogiri::XML <<end
29
+ <places>
30
+ <place name="Green-Wood"/>
31
+ </places>
32
+ end
33
+ ```
34
+
35
+ First we tell RPath we'll be using Nokgiri:
36
+
37
+ ```ruby
38
+ RPath.use :nokogiri
39
+ ```
40
+
41
+ Then we create an RPath expression ...
42
+
43
+ ```ruby
44
+ exp = RPath { places.place[:name] }
45
+ ```
46
+
47
+ ... and evaluate it on the document:
48
+
49
+ ```ruby
50
+ exp.eval(xml) # => "Green-Wood"
51
+ ```
52
+
53
+ Or, if we only plan to use the expression once, we can combine the two lines above, passing the graph to `RPath`. RPath evaluates the expression and returns the result:
54
+
55
+ ```ruby
56
+ RPath(xml) { places.place[:name] } # => "Green-Wood"
57
+ ```
58
+
59
+ For even more succinct syntax, RPath adds to Nokogiri the `#rpath` method:
60
+
61
+ ```ruby
62
+ xml.rpath { places.place[:name] } # => "Green-Wood"
63
+ ```
64
+
65
+ ## The Graph Model
66
+
67
+ In an RPath [graph](http://en.wikipedia.org/wiki/Graph_(mathematics)),
68
+
69
+ * There is an initial vertex (a "root"),
70
+ * Each vertex has a name,
71
+ * Each vertex has zero or more adjacent vertices,
72
+ * Each vertex has zero or more named attributes, and
73
+ * Each vertex may have associated data called "content."
74
+
75
+ RPath expressions assume only this abstract model. They can be applied to any graph for which there is an adapter.
76
+
77
+ ## Expressions
78
+
79
+ An RPath expression, given a graph, produces a value—a vertex, a vertex array, the value of an attribute, or a vertex's content. RPath expressions are constructed by chaining methods inside the block passed to `RPath`.
80
+
81
+ ### Selecting Vertices
82
+
83
+ All vertices named "foo" adjacent to the root:
84
+
85
+ ```ruby
86
+ RPath { foo }
87
+ ```
88
+
89
+ The first "foo" adjacent to the root:
90
+
91
+ ```ruby
92
+ RPath { foo[0] }
93
+ ```
94
+
95
+ All vertices named "bar" adjacent to the first "foo":
96
+
97
+ ```ruby
98
+ RPath { foo[0].bar }
99
+ ```
100
+
101
+ Or, more succinctly (the first "foo" is assumed if the indexer is omitted):
102
+
103
+ ```ruby
104
+ RPath { foo.bar }
105
+ ```
106
+
107
+ _All_ vertices adjacent to the first "foo":
108
+
109
+ ```ruby
110
+ RPath { foo.adjacent }
111
+ ```
112
+
113
+ All vertices adjacent to the first "foo" named "adjacent" (`#named` lets us avoid collisions with built-in methods):
114
+
115
+ ```ruby
116
+ RPath { foo.adjacent.named("adjacent") }
117
+ ```
118
+
119
+ All "foos" with attribute "baz" equal to "qux":
120
+
121
+ ```ruby
122
+ RPath { foo.where(baz: 'qux') }
123
+ ```
124
+
125
+ Or simply:
126
+
127
+ ```ruby
128
+ RPath { foo[baz: 'qux'] }
129
+ ```
130
+
131
+ And finally, all "foos" meeting arbitrary criteria:
132
+
133
+ ```ruby
134
+ RPath { foo.where { |vertex| some_predicate?(vertex) } }
135
+ ```
136
+
137
+ ### Selecting Attributes
138
+
139
+ Attribute values are selected by passing a string to `#[]`:
140
+
141
+ ```ruby
142
+ # The "baz" attribute of the first vertex named "foo" adjacent to the root
143
+ RPath { foo['baz'] }
144
+ ```
145
+
146
+ ### Selecting Content
147
+
148
+ A vertex's content is selected with `#content`:
149
+
150
+ ```ruby
151
+ # The content of the first vertex named "foo" adjacent to the root
152
+ RPath { foo.content }
153
+ ```
154
+
155
+ ## Adapters
156
+
157
+ ### Nokogiri
158
+
159
+ The Nokogiri adapter exposes XML elements as vertices and child elements as adjacent vertices:
160
+
161
+ ```ruby
162
+ RPath.use :nokogiri
163
+
164
+ xml = Nokogiri::XML <<end
165
+ <foo>
166
+ <bar baz="qux">Hello, RPath</bar>
167
+ </foo>
168
+ end
169
+
170
+ RPath(xml) { foo.bar[0] } # => #<Nokogiri::XML::Element ... >
171
+ ```
172
+
173
+ XML attributes become RPath attributes:
174
+
175
+ ```ruby
176
+ RPath(xml) { foo.bar['baz'] } # => "qux"
177
+ ```
178
+
179
+ And text content is accessible with `#content`:
180
+
181
+ ```ruby
182
+ RPath(xml) { foo.bar.content } # => "Hello, RPath"
183
+ ```
184
+
185
+ An expression may be evaluated not just on an XML document but any `Nokogiri::XML::Node`. Non-element nodes such as processing instructions, alas, are not accessible.
186
+
187
+ Finally, the convenience method `#rpath`, added to `Nokogiri::XML::Node`, allows for more compact syntax:
188
+
189
+ ```ruby
190
+ xml.rpath { foo.bar.content } # => "Hello, RPath"
191
+ ```
192
+
193
+ ### REXML
194
+
195
+ The REXML adapter is similar to the Nokogiri one. Expressions may be evaluated on any `REXML::Element`.
196
+
197
+ ```ruby
198
+ RPath.use :rexml
199
+ xml = REXML::Document.new('<foo bar="baz"/>')
200
+ xml.rpath { foo['bar'] } # => "baz"
201
+ ```
202
+
203
+ ### Filesystem
204
+
205
+ The filesystem adapter exposes files and directories as vertices. Directory entries are adjacent to their directory. Expressions may be evaluated on any directory:
206
+
207
+ ```ruby
208
+ RPath.use :filesystem
209
+
210
+ # Note that we must specify the adapter because RPath can't infer it from '~'
211
+ RPath('~', :filesystem) { where { |f| f =~ /bash/ } } # => ["~/.bash_history", "~/.bash_profile"]
212
+ ```
213
+
214
+ Many file properties become RPath attributes:
215
+
216
+ ```ruby
217
+ RPath('/', :filesystem) { etc.hostname[:mtime] } # => 2014-12-17 14:43:24 -0500
218
+ ```
219
+
220
+ And file contents are accessible with `#content`:
221
+
222
+ ```ruby
223
+ RPath('/', :filesystem) { etc.hostname.content } # => "jbook"
224
+ ```
225
+
226
+ ## Custom Adapters
227
+
228
+ Custom adapters are subclasses of `RPath::Adapter`. They implement three abstract methods: `#adjacent`, `#attribute`, and `#content`. See the implementations in `RPath::Adapters` for examples.
229
+
230
+ Register a custom adapter by passing an instance to `RPath.use`:
231
+
232
+ ```ruby
233
+ RPath.use MapsAdapter.new
234
+ ```
235
+
236
+ To use the adapter, pass the underscored, symbolized class name as the second argument to `RPath` or `RPath::Expression#eval`:
237
+
238
+ ```ruby
239
+ RPath(map, :maps_adapter) { ... }
240
+ ```
241
+
242
+ You can eliminate the need to specify the adapter by implementing `RPath::Adapter#adapts?`:
243
+
244
+ ```ruby
245
+ class MapsAdapter < RPath::Adapter
246
+ def adapts?(graph)
247
+ graph.is_a? Map
248
+ end
249
+ ...
250
+ end
251
+ ```
252
+
253
+ Now RPath will select `MapsAdapter` when an expression is evaluated on a `Map`:
254
+
255
+ ```ruby
256
+ RPath.use MapsAdapter.new
257
+ RPath(Map.new) { ... }
258
+ ```
259
+
260
+ ## Contributing
261
+
262
+ Please submit issues and pull requests to [jonahb/rpath](http://github.com/jonahb/rpath) on GitHub.
263
+
@@ -0,0 +1,101 @@
1
+ %w{
2
+ adapter
3
+ adapters
4
+ expressions
5
+ registry
6
+ util
7
+ version
8
+ }.each do |file|
9
+ require "rpath/#{file}"
10
+ end
11
+
12
+ module RPath
13
+ class << self
14
+ # Registers an adapter. Once an adapter is registered, RPath calls its
15
+ # {Adapter#adapts?} when trying to infer the adapter for an evaluation,
16
+ # and its id may be given to {#RPath}.
17
+ # @example Built-in adapter
18
+ # RPath.use :nokogiri
19
+ # @example Custom adapter
20
+ # RPath.use CustomAdapter.new
21
+ # RPath(graph, :custom_adapter) { foo.bar }
22
+ # @example Custom adapter with custom ID
23
+ # RPath.use CustomAdapter.new, :custom
24
+ # RPath(graph, :custom) { foo.bar }
25
+ # @param [Symbol, Adapter] adapter
26
+ # For built-in adapters, the underscored, symbolized class name (e.g.
27
+ # +:nokogiri+). For custom adapters, an instance of the adapter class.
28
+ # @param [Symbol, nil] id
29
+ # The identifier to be used in calls to {#RPath}. If +nil+, the
30
+ # underscored, symbolized name of the adapter class is assumed.
31
+ # @return [void]
32
+ #
33
+ def use(adapter, id = nil)
34
+ if adapter.is_a?(Symbol)
35
+ class_names = [Util.camelcase(adapter.to_s), adapter.to_s.upcase]
36
+ class_ = Util.first_defined_const(RPath::Adapters, *class_names)
37
+
38
+ unless class_
39
+ raise "No adapter in RPath::Adapters with class name in #{class_names}"
40
+ end
41
+
42
+ adapter = class_.new
43
+ end
44
+
45
+ Registry.register adapter, id
46
+ end
47
+ alias_method :register, :use
48
+ end
49
+ end
50
+
51
+ # Constructs an RPath expression and optionally evaluates it on a graph.
52
+ #
53
+ # @overload RPath
54
+ # Constructs an RPath expression
55
+ # @example Construct an expression
56
+ # exp = RPath { foo.bar }
57
+ # @example Construct an expression beginning with an uppercase letter
58
+ # exp = RPath { |root| root.Users.alice }
59
+ # @yieldparam [RPath::Root] root
60
+ # The {RPath::Root} of the RPath expression. You should almost
61
+ # always omit this yield paramter. Use it only to avoid an exception if the
62
+ # first letter of your expression is uppercase. See the example above.
63
+ # @return [RPath::Expression]
64
+ # @see file:README.md
65
+ #
66
+ # @overload RPath(graph, adapter = nil)
67
+ # Constructs an RPath expression, evaluates it, and returns the result
68
+ # @example Construct and expression and evaluate it on an XML document
69
+ # RPath.use :nokogiri
70
+ # xml = Nokogiri::XML('<foo bar="baz"/>')
71
+ # RPath(xml) { foo['bar'] } # => "baz"
72
+ # @example Construct an expression and evaluate it with a custom adapter
73
+ # RPath(graph, CustomAdapter.new) { foo.bar }
74
+ # @example Construct an expression and evaluate it with a custom adapter that has been registered
75
+ # RPath(graph, :custom) { foo.bar }
76
+ # @example Construct an expression and evaluate it, letting RPath infer the adapter
77
+ # RPath(graph) { foo.bar }
78
+ # @param [Object] graph
79
+ # The graph on which to evaluate the expression.
80
+ # @param [RPath::Adapter, Symbol, nil] adapter
81
+ # The adapter with which to evaluate the expression. If the adapter has been
82
+ # registered with {RPath.use}, its id (a symbol) may be given as a
83
+ # shortcut. If +nil+, RPath attempts to infer the adapter by calling
84
+ # {RPath::Adapter#adapts?} on registered adapters.
85
+ # @yieldparam [RPath::Root] root
86
+ # The {RPath::Root} of the RPath expression. You should almost
87
+ # always omit this yield parameter. Use it only to avoid an exception if the
88
+ # first letter of your expression is uppercase. See the example above.
89
+ # @return [Object]
90
+ # @see file:README.md
91
+ # @see RPath.use
92
+ #
93
+ def RPath(graph = nil, adapter = nil, &block)
94
+ exp = RPath::Root.new
95
+
96
+ if block_given?
97
+ exp = block.arity > 0 ? block.call(exp) : exp.instance_eval(&block)
98
+ end
99
+
100
+ graph ? exp.eval(graph, adapter) : exp
101
+ end
@@ -0,0 +1,64 @@
1
+ module RPath
2
+
3
+ # An RPath adapter makes it possible to evaluate RPath expressions on some
4
+ # type of graph. There are built-in adapters for Nokogiri, REXML, and the
5
+ # filesystem. To build an adapter for another graph type, inherit from
6
+ # {Adapter} and implement the abstract methods.
7
+ # @abstract
8
+ #
9
+ class Adapter
10
+ # Used to infer the adapter when {#RPath} is called without an explicit
11
+ # adapter. The first registered adapter whose {#adapts?} returns +true+
12
+ # is chosen. The default implementation returns +false+.
13
+ # @param [Object] graph
14
+ # @return [Boolean]
15
+ # @see #RPath
16
+ # @see RPath.use
17
+ def adapts?(graph)
18
+ false
19
+ end
20
+
21
+ # Returns the root of the given graph, the vertex where evaluation
22
+ # begins. The default implementation returns the given graph.
23
+ # the given graph.
24
+ # @param [Object] graph
25
+ # @return [Object]
26
+ def root(graph)
27
+ graph
28
+ end
29
+
30
+ # Returns the name of the given vertex
31
+ # @abstract
32
+ # @param [Object] vertex
33
+ # @return [String]
34
+ def name(vertex)
35
+ raise NotImplementedError
36
+ end
37
+
38
+ # Returns the vertices adjacent to the given vertex.
39
+ # @abstract
40
+ # @param [Object] vertex
41
+ # @return [Array]
42
+ def adjacent(vertex)
43
+ raise NotImplementedError
44
+ end
45
+
46
+ # Returns the value of attribute +name+ of +vertex+ or +nil+ if no such
47
+ # attribute exists.
48
+ # @abstract
49
+ # @param [Object] vertex
50
+ # @param [String, Symbol] name
51
+ # @return [Object, nil]
52
+ def attribute(vertex, name)
53
+ raise NotImplementedError
54
+ end
55
+
56
+ # Returns the content of +vertex+ or nil if no content exists.
57
+ # @abstract
58
+ # @param [Object] vertex
59
+ # @return [Object, nil]
60
+ def content(vertex)
61
+ raise NotImplementedError
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,7 @@
1
+ module RPath
2
+ module Adapters
3
+ autoload :Filesystem, 'rpath/adapters/filesystem'
4
+ autoload :Nokogiri, 'rpath/adapters/nokogiri'
5
+ autoload :REXML, 'rpath/adapters/rexml'
6
+ end
7
+ end
@@ -0,0 +1,108 @@
1
+ require 'pathname'
2
+
3
+ module RPath
4
+ module Adapters
5
+
6
+ class Filesystem < RPath::Adapter
7
+
8
+ # Always false. The filesystem adapter must be specified in calls to
9
+ # {#RPath}.
10
+ # @param [Object] graph
11
+ # @return [Boolean]
12
+ def adapts?(graph)
13
+ false
14
+ end
15
+
16
+ # @param [String] vertex
17
+ # A filesystem path
18
+ # @return [String]
19
+ # Returns the basename
20
+ def name(vertex)
21
+ File.basename vertex
22
+ end
23
+
24
+ # @param [String] vertex
25
+ # A filesystem path
26
+ # @return [Array<String>]
27
+ # Returns the expanded paths of the directory entries. An empty array
28
+ # if +vertex+ is a file.
29
+ def adjacent(vertex)
30
+ begin
31
+ entries = Dir.entries(File.expand_path(vertex))
32
+ rescue SystemCallError
33
+ return []
34
+ end
35
+
36
+ entries.collect { |entry| File.join(vertex, entry) }
37
+ end
38
+
39
+ # @param [String] vertex
40
+ # A filesystem path
41
+ # @param [String, Symbol] name
42
+ # An attribute in {ATTRIBUTES}
43
+ # @return [Object, nil]
44
+ # Returns the value of the attribute; +nil+ if the attribute is
45
+ # invalid.
46
+ def attribute(vertex, name)
47
+ if ATTRIBUTES.include?(name.to_s)
48
+ begin
49
+ Pathname(File.expand_path(vertex)).send(name)
50
+ rescue SystemCallError
51
+ nil
52
+ end
53
+ else
54
+ nil
55
+ end
56
+ end
57
+
58
+ # @param [String] vertex
59
+ # A filesystem path
60
+ # @return [String, nil]
61
+ # Returns the contents if +vertex+ is a file; otherwise +nil+.
62
+ def content(vertex)
63
+ begin
64
+ File.read File.expand_path(vertex)
65
+ rescue SystemCallError
66
+ nil
67
+ end
68
+ end
69
+
70
+ # Attributes that may be passed as names to {#attribute}
71
+ ATTRIBUTES = %w{
72
+ blockdev?
73
+ chardev?
74
+ directory?
75
+ executable?
76
+ executable_real?
77
+ file?
78
+ grpowned?
79
+ owned?
80
+ pipe?
81
+ readable?
82
+ world_readable?
83
+ readable_real?
84
+ setgid?
85
+ setuid?
86
+ size
87
+ socket?
88
+ sticky?
89
+ symlink?
90
+ writable?
91
+ world_writable?
92
+ writable_real?
93
+ zero?
94
+ atime
95
+ birthtime
96
+ ctime
97
+ mtime
98
+ ftype
99
+ readlink
100
+ stat
101
+ lstat
102
+ dirname
103
+ extname
104
+ split }
105
+ end
106
+
107
+ end
108
+ end
@@ -0,0 +1,60 @@
1
+ require 'nokogiri'
2
+
3
+ module RPath
4
+ module Adapters
5
+
6
+ class Nokogiri < RPath::Adapter
7
+
8
+ # Returns +true+ iff +graph+ is a +Nokogiri::XML::Node+.
9
+ # @param [Object] graph
10
+ # @return [Boolean]
11
+ def adapts?(graph)
12
+ graph.is_a? ::Nokogiri::XML::Node
13
+ end
14
+
15
+ # Returns the name of the given node
16
+ # @param [Nokogiri::XML::Node] vertex
17
+ # @return [String]
18
+ def name(vertex)
19
+ vertex.name
20
+ end
21
+
22
+ # Returns the child elements of the given node
23
+ # @param [Nokogiri::XML::Node] vertex
24
+ # @return [Array<Nokogiri::XML::Node>]
25
+ def adjacent(vertex)
26
+ vertex.children.to_a
27
+ end
28
+
29
+ # Returns the value of the named attribute on the given node.
30
+ # @param [Nokogiri::XML::Node] vertex
31
+ # @param [String, Symbol] name
32
+ # @return [String, nil]
33
+ def attribute(vertex, name)
34
+ vertex[name.to_s]
35
+ end
36
+
37
+ # Returns the text content of the given node.
38
+ # @param [Nokogiri::XML::Node] vertex
39
+ # @return [String, nil]
40
+ def content(vertex)
41
+ vertex.text
42
+ end
43
+ end
44
+
45
+ end
46
+ end
47
+
48
+ class Nokogiri::XML::Node
49
+ # Evaluates an expression on the element
50
+ # @example
51
+ # RPath.use :nokogiri
52
+ # xml = Nokogiri::XML('<foo bar="baz"/>')
53
+ # xml.rpath { foo['bar'] } # => "baz"
54
+ # @see #RPath
55
+ # @return [Object]
56
+ #
57
+ def rpath(&block)
58
+ RPath self, :nokogiri, &block
59
+ end
60
+ end
@@ -0,0 +1,60 @@
1
+ require 'rexml/document'
2
+
3
+ module RPath
4
+ module Adapters
5
+
6
+ class REXML < RPath::Adapter
7
+
8
+ # Returns +true+ iff +graph+ is an +REXML::Element+.
9
+ # @param [Object] graph
10
+ # @return [Boolean]
11
+ def adapts?(graph)
12
+ graph.is_a? ::REXML::Element
13
+ end
14
+
15
+ # Returns the name of the given element
16
+ # @param [REXML::Element] vertex
17
+ # @return [String]
18
+ def name(vertex)
19
+ vertex.name
20
+ end
21
+
22
+ # Returns the child elements of the given element
23
+ # @param [REXML::Element] vertex
24
+ # @return [Array<REXML::Element>]
25
+ def adjacent(vertex)
26
+ vertex.elements.to_a
27
+ end
28
+
29
+ # Returns the value of the named attribute on the given element.
30
+ # @param [REXML::Element] vertex
31
+ # @param [String, Symbol] name
32
+ # @return [String, nil]
33
+ def attribute(vertex, name)
34
+ vertex.attributes[name.to_s]
35
+ end
36
+
37
+ # Returns the text content of the given element.
38
+ # @param [REXML::Element] vertex
39
+ # @return [String, nil]
40
+ def content(vertex)
41
+ vertex.text
42
+ end
43
+ end
44
+
45
+ end
46
+ end
47
+
48
+ class REXML::Element
49
+ # Evaluates an expression on the element
50
+ # @example
51
+ # RPath.use :rexml
52
+ # xml = REXML::Document.new('<foo bar="baz"/>')
53
+ # xml.rpath { foo['bar'] } # => "baz"
54
+ # @see #RPath
55
+ # @return [Object]
56
+ #
57
+ def rpath(&block)
58
+ RPath self, :rexml, &block
59
+ end
60
+ end
@@ -0,0 +1,332 @@
1
+ module RPath
2
+
3
+ # An RPath expression, given a graph, produces a value: a vertex, a vertex
4
+ # array, an attribute value, or a vertex's content.
5
+ # @abstract
6
+ class Expression
7
+
8
+ # Evaluates the expression on a graph
9
+ # @param [Object] graph
10
+ # @param [RPath::Adapter, Symbol, nil] adapter
11
+ # An {Adapter} instance, the id symbol given when the adapter was
12
+ # registered with {RPath.use}, or +nil+ if the adapter should be
13
+ # inferred.
14
+ # @return [Object]
15
+ # @raise [RuntimeError]
16
+ # The adapter can't be determined
17
+ # @ raise [ArgumentError]
18
+ # +adapter+ is not an {Adapter}, Symbol, or nil
19
+ # @see #RPath
20
+ # @see RPath.use
21
+ #
22
+ def eval(graph, adapter = nil)
23
+ adapter = case adapter
24
+ when RPath::Adapter
25
+ adapter
26
+ when Symbol
27
+ Registry.find adapter.to_sym
28
+ when nil
29
+ Registry.infer graph
30
+ else
31
+ raise ArgumentError, "Adapter must be an RPath::Adapter, Symbol, or nil"
32
+ end
33
+
34
+ unless adapter
35
+ raise "Can't determine adapter"
36
+ end
37
+
38
+ do_eval graph, adapter
39
+ end
40
+
41
+ private
42
+
43
+ def do_eval(graph, adapter)
44
+ raise NotImplementedError
45
+ end
46
+ end
47
+
48
+
49
+ # An expression that evaluates to a vertex V
50
+ # @abstract
51
+ class VertexExpression < Expression
52
+ # Returns an expression that evaluates to V's adjacent vertices.
53
+ # @return [Adjacent]
54
+ def adjacent
55
+ Adjacent.new self
56
+ end
57
+
58
+ # Returns an expression that evaluates to V's content.
59
+ # @return [Content]
60
+ def content
61
+ Content.new self
62
+ end
63
+
64
+ # Returns an expression that evaluates to the value of an attribute of V
65
+ # @return [Attribute]
66
+ # @raise [ArgumentError]
67
+ # +subscript+ is not a String or Symbol
68
+ def [](subscript)
69
+ unless subscript.is_a?(String) || subscript.is_a?(Symbol)
70
+ raise ArgumentError, "Subscript for expression producing a vertex must by a String or Symbol"
71
+ end
72
+ Attribute.new self, subscript
73
+ end
74
+
75
+ # Returns an expression that evaluates to V's adjacent vertices named
76
+ # +name+. Enables the basic RPath expression +RPath { foo }+.
77
+ # @return [Named]
78
+ def method_missing(name, *args, &block)
79
+ Named.new adjacent, name.to_s
80
+ end
81
+ end
82
+
83
+
84
+ # An expression that evaluates to a vertex array A
85
+ # @abstract
86
+ class VertexArrayExpression < Expression
87
+ # Returns an expression that evaluates to the vertices in A meeting certain
88
+ # conditions.
89
+ # @return [Where]
90
+ # @see Where#initialize
91
+ def where(*args, &block)
92
+ Where.new self, *args, &block
93
+ end
94
+
95
+ # Returns an expression that evaluates to the vertices in A named +name+.
96
+ # @param [String] name
97
+ # @return [Named]
98
+ def named(name)
99
+ Named.new self, name
100
+ end
101
+
102
+ # @overload [](index)
103
+ # Returns an expression that evaluates to the vertex at index +index+ in
104
+ # A.
105
+ # @param [Integer] index
106
+ # @return [At]
107
+ # @overload [](conditions)
108
+ # Returns an expression that evaluates to the vertices in A meeting
109
+ # certain conditions.
110
+ # @param [Hash] conditions
111
+ # @return [Where]
112
+ # @see Where#initialize
113
+ # @overload [](attribute)
114
+ # Returns an expression that evaluates to the value of an attribute of
115
+ # the first vertex in A. Enables omitting the indexer in
116
+ # +RPath { foo['bar'] }+
117
+ # @param [String, Symbol] attribute
118
+ # @return [Attribute]
119
+ # @raise [ArgumentError]
120
+ # +subscript+ is not an Integer, Hash, String, or Symbol
121
+ def [](subscript)
122
+ case subscript
123
+ when Integer
124
+ At.new self, subscript
125
+ when Hash
126
+ Where.new self, subscript
127
+ when String, Symbol
128
+ self[0][subscript]
129
+ else
130
+ raise ArgumentError, "Subscript for expression producing a vertex must be an Integer, Hash, String, or Symbol"
131
+ end
132
+ end
133
+
134
+ # Constructs an {At} that evaluates to the first vertex in A;
135
+ # forwards the method invocation to this {At}. Enables omitting
136
+ # the indexer in expressions like +RPath { foo.bar }+.
137
+ def method_missing(name, *args, &block)
138
+ self[0].send name, *args, &block
139
+ end
140
+ end
141
+
142
+
143
+ # Evaluates to the root of the graph.
144
+ class Root < VertexExpression
145
+ # @return [String]
146
+ def to_s
147
+ 'root'
148
+ end
149
+
150
+ private
151
+
152
+ def do_eval(graph, adapter)
153
+ adapter.root graph
154
+ end
155
+ end
156
+
157
+
158
+ # Given a prior expression producing vertex V, evaluates to an array
159
+ # containing V's adjacent vertices.
160
+ class Adjacent < VertexArrayExpression
161
+ # @param [Expression] prior
162
+ # An expression that evaluates to a vertex
163
+ def initialize(prior)
164
+ super()
165
+ @prior = prior
166
+ end
167
+
168
+ # @return [String]
169
+ def to_s
170
+ "#{@prior}."
171
+ end
172
+
173
+ private
174
+
175
+ def do_eval(graph, adapter)
176
+ vertex = @prior.eval(graph, adapter)
177
+ vertex && adapter.adjacent(vertex)
178
+ end
179
+ end
180
+
181
+
182
+ # Given a prior expression producing vertex array A, evaluates to an array
183
+ # containing the vertices in A with a certain name.
184
+ class Named < VertexArrayExpression
185
+ # @param [Expression] prior
186
+ # An expression that evaluates to a vertex array
187
+ # @param [String] name
188
+ def initialize(prior, name)
189
+ super()
190
+ @prior = prior
191
+ @name = name
192
+ end
193
+
194
+ # @return [String]
195
+ def to_s
196
+ "#{@prior}#{@name}"
197
+ end
198
+
199
+ private
200
+
201
+ def do_eval(graph, adapter)
202
+ vertices = @prior.eval(graph, adapter)
203
+ vertices && vertices.select { |vertex| @name == adapter.name(vertex) }
204
+ end
205
+ end
206
+
207
+
208
+ # Given a prior expression producing vertex array A, evaluates to an array
209
+ # containing the vertices in A that match certain conditions.
210
+ class Where < VertexArrayExpression
211
+ # @overload initialize(prior, conditions)
212
+ # @param [Expression] prior
213
+ # An expression that evaluates to a vertex array
214
+ # @param [Hash{Symbol => Object}] conditions
215
+ # A map of attribute keys to values.
216
+ # @overload initialize(prior)
217
+ # @param [Expression] prior
218
+ # An expression that evaluates to a vertex array
219
+ # @yieldparam vertex [Object]
220
+ # @yieldreturn [Boolean]
221
+ # Whether the vertex should be selected
222
+ def initialize(prior, conditions = {}, &selector)
223
+ super()
224
+ @prior = prior
225
+ @selector = block_given? ? selector : nil
226
+ @conditions = block_given? ? nil : conditions
227
+ end
228
+
229
+ # @return [String]
230
+ def to_s
231
+ conditions = @selector ?
232
+ 'selector' :
233
+ @conditions.map { |k, v| "#{k}: #{v}" }.join(', ')
234
+
235
+ "#{@prior}[#{conditions}]"
236
+ end
237
+
238
+ private
239
+
240
+ def do_eval(graph, adapter)
241
+ vertices = @prior.eval(graph, adapter)
242
+ return nil unless vertices
243
+
244
+ if @selector
245
+ vertices.select(&@selector)
246
+ else
247
+ vertices.select do |vertex|
248
+ @conditions.all? do |name, value|
249
+ adapter.attribute(vertex, name) == value
250
+ end
251
+ end
252
+ end
253
+ end
254
+ end
255
+
256
+
257
+ # Given a prior expression producing vertex array A, evaluates to the vertex
258
+ # in A at a given index.
259
+ class At < VertexExpression
260
+ # @param [Expression] prior
261
+ # An expression that evaluates to a vertex array
262
+ # @param [Integer] index
263
+ # The index of the vertex to produce
264
+ def initialize(prior, index)
265
+ super()
266
+ @prior = prior
267
+ @index = index
268
+ end
269
+
270
+ # @return [String]
271
+ def to_s
272
+ "#{@prior}[#{@index}]"
273
+ end
274
+
275
+ private
276
+
277
+ def do_eval(graph, adapter)
278
+ vertices = @prior.eval(graph, adapter)
279
+ vertices && vertices[@index]
280
+ end
281
+ end
282
+
283
+
284
+ # Given a prior expression producing a vertex V, evaluates to the value of
285
+ # the attribute of V with the given name.
286
+ class Attribute < Expression
287
+ # @param [Expression] prior
288
+ # An expression that evaluates to a vertex
289
+ # @param [String] name
290
+ # The name of the attribute
291
+ def initialize(prior, name)
292
+ super()
293
+ @prior = prior
294
+ @name = name
295
+ end
296
+
297
+ # @return [String]
298
+ def to_s
299
+ "#{@prior}[#{@name}]"
300
+ end
301
+
302
+ private
303
+
304
+ def do_eval(graph, adapter)
305
+ vertex = @prior.eval(graph, adapter)
306
+ vertex && adapter.attribute(vertex, @name)
307
+ end
308
+ end
309
+
310
+
311
+ # Given a prior expression producing vertex V, evaluates to V's content.
312
+ class Content < Expression
313
+ # @param [Expression] prior
314
+ # An expression producing a vertex
315
+ def initialize(prior)
316
+ @prior = prior
317
+ end
318
+
319
+ # @return [String]
320
+ def to_s
321
+ "#{@prior}:content"
322
+ end
323
+
324
+ private
325
+
326
+ def do_eval(graph, adapter)
327
+ vertex = @prior.eval(graph, adapter)
328
+ vertex && adapter.content(vertex)
329
+ end
330
+ end
331
+
332
+ end
@@ -0,0 +1,52 @@
1
+ module RPath
2
+
3
+ # @private
4
+ class Registry
5
+ class << self
6
+ # Registers an adapter. Once an adapter is registered, RPath calls its
7
+ # {#adapts?} when trying to infer the adapter for an evaluation, and its
8
+ # id, as opposed to an instance, may be given to {#RPath}.
9
+ # @param [Adapter] adapter
10
+ # @param [Symbol, nil] id
11
+ # An id that can later be passed to {#RPath}. If omitted, the
12
+ # symbolized, underscored name of the adapter class is assumed.
13
+ # @return [void]
14
+ def register(adapter, id = nil)
15
+ id ||= default_id(adapter)
16
+ id_to_adapter[id] = adapter
17
+ end
18
+ alias_method :use, :register
19
+
20
+ # Infers the adapter for a given graph. The first adapter whose
21
+ # {#adapts?} returns +true+ is chosen.
22
+ # @param [Object] graph
23
+ # @return [Adapter, nil]
24
+ def infer(graph)
25
+ id_to_adapter.each_value.find { |adapter| adapter.adapts?(graph) }
26
+ end
27
+
28
+ # Finds a registered adapter by id.
29
+ # @param [Symbol] id
30
+ # @return [Adapter, nil]
31
+ def find(id)
32
+ id_to_adapter[id]
33
+ end
34
+
35
+ # Unregisters all adapters
36
+ def clear
37
+ id_to_adapter.clear
38
+ end
39
+
40
+ private
41
+
42
+ def id_to_adapter
43
+ @id_to_adapter ||= {}
44
+ end
45
+
46
+ def default_id(adapter)
47
+ Util.underscore(adapter.class.name.split('::').last).to_sym
48
+ end
49
+ end
50
+ end
51
+
52
+ end
@@ -0,0 +1,21 @@
1
+ module RPath
2
+
3
+ # @private
4
+ module Util
5
+ class << self
6
+ def underscore(string)
7
+ string.gsub(/([^A-Z])([A-Z])/, '\1_\2').downcase
8
+ end
9
+
10
+ def camelcase(string)
11
+ string.gsub(/(?:^|_)([a-z])/) { $1.upcase }
12
+ end
13
+
14
+ def first_defined_const(module_, *consts)
15
+ const = consts.find { |c| module_.const_defined?(c) }
16
+ const && module_.const_get(const)
17
+ end
18
+ end
19
+ end
20
+
21
+ end
@@ -0,0 +1,3 @@
1
+ module RPath
2
+ VERSION = '1.0.0'
3
+ end
metadata ADDED
@@ -0,0 +1,113 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rpath
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Jonah Burke
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2015-02-03 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: nokogiri
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ~>
18
+ - !ruby/object:Gem::Version
19
+ version: 1.6.0
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ~>
25
+ - !ruby/object:Gem::Version
26
+ version: 1.6.0
27
+ - !ruby/object:Gem::Dependency
28
+ name: bundler
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ~>
32
+ - !ruby/object:Gem::Version
33
+ version: '1.7'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ~>
39
+ - !ruby/object:Gem::Version
40
+ version: '1.7'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rake
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ~>
46
+ - !ruby/object:Gem::Version
47
+ version: '10.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ~>
53
+ - !ruby/object:Gem::Version
54
+ version: '10.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: yard
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ~>
60
+ - !ruby/object:Gem::Version
61
+ version: 0.8.7
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ~>
67
+ - !ruby/object:Gem::Version
68
+ version: 0.8.7
69
+ description:
70
+ email:
71
+ - jonah@jonahb.com
72
+ executables: []
73
+ extensions: []
74
+ extra_rdoc_files: []
75
+ files:
76
+ - LICENSE.txt
77
+ - README.md
78
+ - lib/rpath.rb
79
+ - lib/rpath/adapter.rb
80
+ - lib/rpath/adapters.rb
81
+ - lib/rpath/adapters/filesystem.rb
82
+ - lib/rpath/adapters/nokogiri.rb
83
+ - lib/rpath/adapters/rexml.rb
84
+ - lib/rpath/expressions.rb
85
+ - lib/rpath/registry.rb
86
+ - lib/rpath/util.rb
87
+ - lib/rpath/version.rb
88
+ homepage: http://github.com/jonahb/rpath
89
+ licenses:
90
+ - MIT
91
+ metadata: {}
92
+ post_install_message:
93
+ rdoc_options: []
94
+ require_paths:
95
+ - lib
96
+ required_ruby_version: !ruby/object:Gem::Requirement
97
+ requirements:
98
+ - - ! '>='
99
+ - !ruby/object:Gem::Version
100
+ version: 1.9.3
101
+ required_rubygems_version: !ruby/object:Gem::Requirement
102
+ requirements:
103
+ - - ! '>='
104
+ - !ruby/object:Gem::Version
105
+ version: '0'
106
+ requirements: []
107
+ rubyforge_project:
108
+ rubygems_version: 2.4.5
109
+ signing_key:
110
+ specification_version: 4
111
+ summary: TBD
112
+ test_files: []
113
+ has_rdoc: