traverse 0.0.3 → 0.1.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.
data/README.md CHANGED
@@ -1,12 +1,20 @@
1
1
  # Traverse
2
2
 
3
- Traverse is a simple tool that makes it easy to traverse XML.
3
+ ```bash
4
+ gem install traverse
5
+ ```
6
+
7
+ ## Introduction
8
+
9
+ Traverse is a simple tool that makes it easy to traverse XML and JSON.
4
10
 
5
- Let's say you're messing around with Twitter's
11
+ Let's say you're messing around with Twitter's XML
6
12
  [public timeline](http://api.twitter.com/statuses/public_timeline.xml).
7
13
  Traverse let's you do things like this:
8
14
 
9
15
  ```ruby
16
+ require 'open-uri'
17
+
10
18
  timeline = Traverse::Document.new(open "http://api.twitter.com/statuses/public_timeline.xml")
11
19
 
12
20
  timeline.statuses.each do |status|
@@ -14,6 +22,18 @@ timeline.statuses.each do |status|
14
22
  end
15
23
  ```
16
24
 
25
+ Or, let's say you're foolin' with Spotify's JSON [Search
26
+ API](http://ws.spotify.com/search/1/track.json?q=like+a+virgin).
27
+
28
+ ```ruby
29
+ search = Traverse::Document.new(open "http://ws.spotify.com/search/1/track.json?q=like+a+virgin")
30
+
31
+ search.tracks.each do |track|
32
+ puts "Track: #{track.name}"
33
+ puts "Album: #{track.album.name}"
34
+ end
35
+ ```
36
+
17
37
  For a slightly more complicated example, take a look at a
18
38
  [boxscore](http://gd2.mlb.com/components/game/mlb/year_2011/month_03/day_31/gid_2011_03_31_detmlb_nyamlb_1/boxscore.xml)
19
39
  pulled from Major League Baseball's public API.
@@ -22,8 +42,6 @@ pulled from Major League Baseball's public API.
22
42
  url = "http://gd2.mlb.com/components/game/mlb/year_2011/month_03/day_31/gid_2011_03_31_detmlb_nyamlb_1/boxscore.xml"
23
43
  boxscore = Traverse::Document.new(open url)
24
44
 
25
- # let's start traversing!
26
-
27
45
  boxscore.game_id # => '2011/03/31/detmlb-nyamlb-1'
28
46
 
29
47
  boxscore.battings[0].batters[1].name_display_first_last # => 'Derek Jeter'
@@ -36,3 +54,158 @@ boxscore.pitchings.find do |pitching|
36
54
  pitching.team_flag == 'away'
37
55
  end.out # => '24'
38
56
  ```
57
+
58
+ ## Traverse's XML API
59
+
60
+ When in doubt, check the spec file.
61
+
62
+ ### Children
63
+
64
+ Let's say you're working with the following node of XML:
65
+
66
+ ```xml
67
+ <foo>
68
+ <bar ...>
69
+ ...
70
+ </bar>
71
+ </foo>
72
+ ```
73
+
74
+ Assuming you've wrapped the XML in a `Traverse::Document` named `foo`, you can
75
+ traverse to the `bar` node with a simple method call:
76
+
77
+ ```ruby
78
+ foo.bar # => returns a representation of the bar node
79
+ ```
80
+
81
+ If there are in fact many `bar`s inside `foo`, Traverse will transparently
82
+ collect them in an array. You can access that array by pluralizing the name of
83
+ the individual nodes:
84
+
85
+ ```ruby
86
+ foo.bars # => an array containing all of the bars
87
+ foo.bars.first # => grab the first bar
88
+ ```
89
+
90
+ Traverse will also do its best to transparently collect singularly-named nodes
91
+ inside of pluralized parents. Example:
92
+
93
+ ```xml
94
+ <foo>
95
+ <bars>
96
+ <bar ...>
97
+ ...
98
+ </bar>
99
+ ...
100
+ <bar ...>
101
+ ...
102
+ </bar>
103
+ </bars>
104
+ </foo>
105
+ ```
106
+ ```ruby
107
+ foo.bars # => an array of all of the bars
108
+ foo.bars.first # => the first bar
109
+ ```
110
+
111
+ This won't work if the pluralized parent node has attributes or if its children
112
+ aren't all singularized versions of itself! Twitter's timeline is an example;
113
+ the parent node's name is `statuses`, and its children are all named
114
+ `status`, but the `statuses` node has an attribute.
115
+
116
+ ### Attributes
117
+
118
+ If your XML node has an attribute named `foo`, you can access that attribute
119
+ with a simple method call:
120
+
121
+ ```ruby
122
+ my_node.foo # => return the attribute's value
123
+ ```
124
+
125
+ Notice that there might be a conflict if your node has both an attribute named
126
+ `foo` _and_ a child `foo`. Traverse puts children first, so the attribute `foo`
127
+ will get clobbered.
128
+
129
+ To get around this, Traverse provides a backdoor for when you really want the
130
+ attribute:
131
+
132
+ ```ruby
133
+ my_node.foo # => grabs the child named foo
134
+ my_node['foo'] # => grabs the attribute named foo
135
+ ```
136
+
137
+ ### Text
138
+
139
+ If you traverse to a node that has no attributes and contains only text, you
140
+ can grab that text with a simple method call. Let's suppose you have the
141
+ following XML:
142
+
143
+ ```xml
144
+ <foo>
145
+ <bar>
146
+ This bar rocks!
147
+ </bar>
148
+ </foo>
149
+ ```
150
+
151
+ You can grab the text like this:
152
+
153
+ ```ruby
154
+ foo.bar # => "This bar rocks!"
155
+ ```
156
+
157
+ What if `bar` does have attributes? Then you'll need to use the `text` method:
158
+
159
+ ```xml
160
+ <foo>
161
+ <bar baz="quux">
162
+ This bar rocks!
163
+ </bar>
164
+ </foo>
165
+ ```
166
+ ```ruby
167
+ foo.bar # => grabs a representation of the node, not the text!
168
+ foo.bar.text # => "This bar rocks!"
169
+ ```
170
+
171
+ I know what you're thinking: what the hell do I do if my node has text inside
172
+ it but I want to grab its attribute named `text`???
173
+
174
+ ```ruby
175
+ foo.bar['text'] # => grab bar's attribute named 'text'
176
+ foo.bar.text # => grab bar's text contents
177
+ ```
178
+
179
+ ## Traverse's JSON API
180
+
181
+ Again, when in doubt, check the spec file.
182
+
183
+ Traverse doesn't really do anything magical with JSON data; under the hood, it
184
+ uses `YAJL` to parse JSON into a `Hash`, and hashes are alredy pretty
185
+ traversable in Ruby. But, to maintain consistency with the XML API, you can
186
+ happily traverse JSON using method calls instead of querying a hash.
187
+
188
+ ## Helper methods
189
+ Traverse provides a few helper methods to help with traversal.
190
+
191
+ If you're traversing some JSON, the ```_keys_``` method will be available. It
192
+ returns an array containing the names of the JSON object's keys.
193
+
194
+ ```ruby
195
+ search._keys_.count
196
+ # => 2
197
+
198
+ search.first._keys_
199
+ # => ["user","favorited","source","id","text","created_at"]
200
+ ```
201
+
202
+ If you're looking at XML, the ```_children_``` and ```_attributes_``` methods
203
+ will be available. The ```_children_``` method gives you an array of
204
+ traversable child nodes, and ```_attributes_``` gives you a hash of
205
+ attribute-name/attribute-value pairs.
206
+
207
+ ## Contributors!
208
+
209
+ Traverse wouldn't be possible without help from friends. Thanks!
210
+
211
+ - [muffs](https://github.com/muffs)
data/Rakefile CHANGED
@@ -2,5 +2,7 @@ require 'bundler/gem_tasks'
2
2
 
3
3
  desc 'run the spec'
4
4
  task "spec" do
5
- load './spec/spec.rb'
5
+ Dir.glob('spec/*.rb') do |spec|
6
+ load spec
7
+ end
6
8
  end
@@ -1,10 +1,53 @@
1
1
  require "traverse/version"
2
2
  require 'nokogiri'
3
- require 'open-uri'
3
+ require 'yajl'
4
4
  require 'active_support/inflector'
5
5
 
6
6
  module Traverse
7
7
  class Document
8
+ def initialize document
9
+ if xml? document
10
+ @proxy = XML.new document
11
+ elsif json? document
12
+ @proxy = JSON.new document
13
+ end
14
+ end
15
+
16
+ private
17
+ def method_missing m, *args, &block
18
+ @proxy.send m, *args, &block
19
+ end
20
+
21
+ def xml? document
22
+ begin
23
+ Nokogiri::XML(document) do |config|
24
+ config.options = Nokogiri::XML::ParseOptions::STRICT
25
+ end
26
+ true
27
+ rescue Nokogiri::XML::SyntaxError
28
+ false
29
+ ensure
30
+ document.rewind if document.respond_to? :read
31
+ end
32
+ end
33
+
34
+ def json? document
35
+ begin
36
+ Yajl::Parser.new.parse(document)
37
+ true
38
+ rescue Yajl::ParseError
39
+ false
40
+ ensure
41
+ document.rewind if document.respond_to? :read
42
+ end
43
+ end
44
+
45
+ def to_s
46
+ "<Traverse::Document...>"
47
+ end
48
+ end
49
+
50
+ class XML
8
51
  def initialize document
9
52
  setup_underlying_document document
10
53
 
@@ -23,7 +66,7 @@ module Traverse
23
66
  end
24
67
  else
25
68
  define_singleton_method name do
26
- Document.new child
69
+ XML.new child
27
70
  end
28
71
  end
29
72
  else
@@ -32,7 +75,7 @@ module Traverse
32
75
  if text_only_node? child
33
76
  child.content.strip
34
77
  else
35
- Document.new child
78
+ XML.new child
36
79
  end
37
80
  end
38
81
  end
@@ -43,7 +86,7 @@ module Traverse
43
86
  define_singleton_method pluralized_child.name do
44
87
  pluralized_child.children.reject do |baby|
45
88
  baby.class == Nokogiri::XML::Text
46
- end.map { |child| Document.new child }
89
+ end.map { |child| XML.new child }
47
90
  end
48
91
  end
49
92
 
@@ -53,15 +96,15 @@ module Traverse
53
96
  @document.get_attribute attr
54
97
  end
55
98
 
56
- def attributes
99
+ def _attributes_
57
100
  name_value_pairs = @document.attributes.map do |name, attribute|
58
101
  [name, attribute.value]
59
102
  end
60
103
  Hash[ name_value_pairs ]
61
104
  end
62
105
 
63
- def children
64
- real_children.map { |child| Document.new child }
106
+ def _children_
107
+ real_children.map { |child| XML.new child }
65
108
  end
66
109
 
67
110
  private
@@ -145,4 +188,61 @@ module Traverse
145
188
  "<Traversable... >"
146
189
  end
147
190
  end
191
+
192
+ class JSON
193
+
194
+ def initialize json
195
+
196
+ setup_underlying_json json
197
+
198
+ if @json.is_a? Array
199
+ @proxy = @json.map do |item|
200
+ JSON.new item
201
+ end
202
+ elsif @json.is_a? Hash
203
+ @json.each_pair do |k,v|
204
+ define_singleton_method k do
205
+ if v.is_a? Hash
206
+ JSON.new(v)
207
+ elsif v.is_a? Array
208
+ v.map { |i| JSON.new(i) }
209
+ else
210
+ v
211
+ end
212
+ end
213
+ define_singleton_method "_keys_" do
214
+ @json.keys
215
+ end
216
+ end
217
+ elsif @json.is_a? Array
218
+ @json.map! { |i| JSON.new i }
219
+ end
220
+ end
221
+
222
+ private
223
+ def method_missing m, *args, &block
224
+ if @proxy
225
+ @proxy.send m, *args, &block
226
+ else
227
+ super
228
+ end
229
+ end
230
+
231
+ def setup_underlying_json document
232
+ if document.is_a? String
233
+ @json = Yajl::Parser.new.parse document
234
+ elsif document.respond_to? :read # Tempfile / StringIO
235
+ begin
236
+ parser = Yajl::Parser.new
237
+ @json = parser.parse(document)
238
+ rescue
239
+ nil
240
+ ensure
241
+ document.rewind
242
+ end
243
+ elsif document.is_a? Hash
244
+ @json = document
245
+ end
246
+ end
247
+ end
148
248
  end
@@ -1,3 +1,3 @@
1
1
  module Traverse
2
- VERSION = "0.0.3"
2
+ VERSION = "0.1.0"
3
3
  end
@@ -0,0 +1,43 @@
1
+ require 'traverse'
2
+
3
+ gem 'minitest'
4
+ require 'minitest/autorun'
5
+ require 'minitest/pride'
6
+
7
+ json = %{
8
+ {
9
+ "menu": {
10
+ "id": "file",
11
+ "value": "File",
12
+ "popup": {
13
+ "menuitem": [
14
+ {"value": "New", "onclick": "CreateNewDoc()"},
15
+ {"value": "Open", "onclick": "OpenDoc()"},
16
+ {"value": "Close", "onclick": "CloseDoc()"}
17
+ ]
18
+ }
19
+ }
20
+ }
21
+ }
22
+
23
+ describe Traverse::Document do
24
+ before do
25
+ @doc = Traverse::Document.new json
26
+ end
27
+
28
+ describe "Grabbing simple attributes" do
29
+ it "helps you access attributes" do
30
+ @doc.menu.id.must_equal "file"
31
+ end
32
+ end
33
+
34
+ describe "Grabbing attributes that are arrays" do
35
+ it "knows how to handle json arrays" do
36
+ @doc.menu.popup.menuitem.count.must_equal 3
37
+ end
38
+
39
+ it "traversifies the elements of json arrays" do
40
+ @doc.menu.popup.menuitem.last.value.must_equal "Close"
41
+ end
42
+ end
43
+ end
@@ -1,4 +1,3 @@
1
- $:.push '../'
2
1
  require 'traverse'
3
2
 
4
3
  gem 'minitest'
@@ -38,7 +37,7 @@ xml = %{
38
37
  expert. Not a genius, exactly, more like an idiot savant with X-ray
39
38
  vision.
40
39
  </quotation>
41
- </quotes>
40
+ </quotations>
42
41
  <review reviewer="Salman Rushdie">
43
42
  What is interesting is to have before us, at the end of the Greed
44
43
  Decade, that rarest of birds: a major political novel about what
@@ -59,66 +58,70 @@ describe Traverse::Document do
59
58
  @book = Traverse::Document.new xml
60
59
  end
61
60
 
62
- it "helps you access attributes" do
63
- @book.title.must_equal "Vineland"
64
- end
61
+ describe "Grabbing attributes" do
62
+ it "helps you access attributes" do
63
+ @book.title.must_equal "Vineland"
64
+ end
65
65
 
66
- it "also helps you access attributes shadowed by children" do
67
- @book.author.wont_equal "Thomas Pynchon"
68
- @book['author'].must_equal "Thomas Pynchon"
69
- @book.author.name.must_equal "Thomas Pynchon"
70
- end
66
+ it "also lets you access attributes shadowed by children" do
67
+ @book.author.wont_equal "Thomas Pynchon"
68
+ @book.author.class.wont_equal String
71
69
 
72
- describe "support for enumerable" do
70
+ @book['author'].must_equal "Thomas Pynchon"
71
+ end
72
+ end
73
73
 
74
- it "gives you access to the current node's attributes" do
75
- @book.attributes.any? do |name, value|
76
- value == "Vineland"
77
- end.must_equal true
74
+ describe "Traversing to children" do
75
+ it "helps you traverse to child nodes" do
76
+ @book.review.reviewer.must_equal "Salman Rushdie"
77
+ @book.epigraph.author.must_equal "Johnny Copeland"
78
78
  end
79
79
 
80
- it "gives you access to the current node's children as traversable documents" do
81
- assert @book.children.any? do |child|
82
- child.attributes.any? do |name, value|
83
- name == "reviewer" and value == "Salman Rushdie"
84
- end
80
+ it "knows to collect children with the same name" do
81
+ @book.author.books.class.must_equal Array
82
+ @book.author.books.count.must_equal 8
83
+ assert @book.author.books.all? do |book|
84
+ book.is_a? String
85
85
  end
86
86
  end
87
87
 
88
+ it "knows to collect singularized children of a pluralized parent" do
89
+ @book.quotations.count.must_equal 2
90
+ @book.quotations.last.text.must_match(/more like an idiot savant/)
91
+ end
88
92
  end
89
93
 
90
- it "helps you traverse to child nodes" do
91
- @book.review.reviewer.must_equal "Salman Rushdie"
92
- @book.epigraph.author.must_equal "Johnny Copeland"
93
- end
94
+ describe "Dealing with text nodes" do
95
+ it "handles annoying text nodes transparently" do
96
+ @book.epigraph.text.must_match(/Every dog has his day/)
97
+ @book.review.text.must_match(/that rarest of birds/)
98
+ end
94
99
 
95
- it "knows when a node contains only text" do
96
- assert @book.epigraph.send(:text_node?)
97
- end
100
+ it "nevertheless handles attributes named 'text'" do
101
+ @book.text['text'].must_match(/seriously/)
102
+ @book.text.text.must_match(/rilly/)
103
+ end
98
104
 
99
- it "handles annoying text nodes transparently" do
100
- @book.epigraph.text.must_match(/Every dog has his day/)
101
- @book.review.text.must_match(/that rarest of birds/)
105
+ it "handles nodes with only text and no attributes" do
106
+ @book.pagecount.must_equal "385"
107
+ end
102
108
  end
103
109
 
104
- it "nevertheless handles attributes named 'text'" do
105
- @book.text['text'].must_match(/seriously/)
106
- @book.text.text.must_match(/rilly/)
107
- end
108
110
 
109
- it "knows when a node has only text and no attributes" do
110
- @book.pagecount.must_equal "385"
111
- end
111
+ describe "Support for enumerable" do
112
+ it "gives you access to the current node's attributes" do
113
+ @book._attributes_.any? do |name, value|
114
+ value == "Vineland"
115
+ end.must_equal true
116
+ end
112
117
 
113
- it "knows to collect children with the same name" do
114
- @book.author.books.count.must_equal 8
115
- assert @book.author.books.all? do |book|
116
- book.is_a? String
118
+ it "gives you access to the current node's children as traversable documents" do
119
+ assert @book._children_.any? do |child|
120
+ child.attributes.any? do |name, value|
121
+ name == "reviewer" and value == "Salman Rushdie"
122
+ end
123
+ end
117
124
  end
118
125
  end
119
126
 
120
- it "knows to collect children of a pluralized parent" do
121
- @book.quotations.count.must_equal 2
122
- @book.quotations.last.text.must_match(/more like an idiot savant/)
123
- end
124
127
  end
@@ -5,11 +5,11 @@ require "traverse/version"
5
5
  Gem::Specification.new do |s|
6
6
  s.name = "traverse"
7
7
  s.version = Traverse::VERSION
8
- s.authors = ["happy4crazy"]
8
+ s.authors = ["happy4crazy", "muffs"]
9
9
  s.email = ["alan.m.odonnell@gmail.com"]
10
10
  s.homepage = "https://github.com/happy4crazy/traverse"
11
- s.summary = %q{Easily traverse an XML document.}
12
- s.description = %q{Easily traverse an XML document.}
11
+ s.summary = %q{Easily traverse XML and JSON.}
12
+ s.description = %q{Easily traverse XML and JSON.}
13
13
 
14
14
  s.rubyforge_project = "traverse"
15
15
 
@@ -18,5 +18,9 @@ Gem::Specification.new do |s|
18
18
  s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
19
19
  s.require_paths = ["lib"]
20
20
 
21
+ s.add_development_dependency 'minitest'
21
22
  s.add_dependency 'nokogiri', '~> 1.5'
23
+ s.add_dependency 'active_support'
24
+ s.add_dependency 'i18n'
25
+ s.add_dependency 'yajl-ruby'
22
26
  end
metadata CHANGED
@@ -1,20 +1,32 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: traverse
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.3
4
+ version: 0.1.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
8
8
  - happy4crazy
9
+ - muffs
9
10
  autorequire:
10
11
  bindir: bin
11
12
  cert_chain: []
12
- date: 2011-08-05 00:00:00.000000000 -04:00
13
+ date: 2011-08-18 00:00:00.000000000 -04:00
13
14
  default_executable:
14
15
  dependencies:
16
+ - !ruby/object:Gem::Dependency
17
+ name: minitest
18
+ requirement: &2157763120 !ruby/object:Gem::Requirement
19
+ none: false
20
+ requirements:
21
+ - - ! '>='
22
+ - !ruby/object:Gem::Version
23
+ version: '0'
24
+ type: :development
25
+ prerelease: false
26
+ version_requirements: *2157763120
15
27
  - !ruby/object:Gem::Dependency
16
28
  name: nokogiri
17
- requirement: &2152908620 !ruby/object:Gem::Requirement
29
+ requirement: &2157762620 !ruby/object:Gem::Requirement
18
30
  none: false
19
31
  requirements:
20
32
  - - ~>
@@ -22,8 +34,41 @@ dependencies:
22
34
  version: '1.5'
23
35
  type: :runtime
24
36
  prerelease: false
25
- version_requirements: *2152908620
26
- description: Easily traverse an XML document.
37
+ version_requirements: *2157762620
38
+ - !ruby/object:Gem::Dependency
39
+ name: active_support
40
+ requirement: &2157762200 !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ! '>='
44
+ - !ruby/object:Gem::Version
45
+ version: '0'
46
+ type: :runtime
47
+ prerelease: false
48
+ version_requirements: *2157762200
49
+ - !ruby/object:Gem::Dependency
50
+ name: i18n
51
+ requirement: &2152011700 !ruby/object:Gem::Requirement
52
+ none: false
53
+ requirements:
54
+ - - ! '>='
55
+ - !ruby/object:Gem::Version
56
+ version: '0'
57
+ type: :runtime
58
+ prerelease: false
59
+ version_requirements: *2152011700
60
+ - !ruby/object:Gem::Dependency
61
+ name: yajl-ruby
62
+ requirement: &2152011280 !ruby/object:Gem::Requirement
63
+ none: false
64
+ requirements:
65
+ - - ! '>='
66
+ - !ruby/object:Gem::Version
67
+ version: '0'
68
+ type: :runtime
69
+ prerelease: false
70
+ version_requirements: *2152011280
71
+ description: Easily traverse XML and JSON.
27
72
  email:
28
73
  - alan.m.odonnell@gmail.com
29
74
  executables: []
@@ -36,7 +81,8 @@ files:
36
81
  - Rakefile
37
82
  - lib/traverse.rb
38
83
  - lib/traverse/version.rb
39
- - spec/spec.rb
84
+ - spec/json.rb
85
+ - spec/xml.rb
40
86
  - traverse.gemspec
41
87
  has_rdoc: true
42
88
  homepage: https://github.com/happy4crazy/traverse
@@ -62,6 +108,7 @@ rubyforge_project: traverse
62
108
  rubygems_version: 1.6.2
63
109
  signing_key:
64
110
  specification_version: 3
65
- summary: Easily traverse an XML document.
111
+ summary: Easily traverse XML and JSON.
66
112
  test_files:
67
- - spec/spec.rb
113
+ - spec/json.rb
114
+ - spec/xml.rb