traverse 0.0.3 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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