rpath 1.0.0

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.
@@ -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: