lzell-mapricot 0.0.1 → 0.0.2.1

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/lib/mapricot.rb CHANGED
@@ -1,97 +1,110 @@
1
- require 'open-uri'
2
- begin
3
- require 'hpricot'
4
- rescue LoadError
5
- require 'rubygems'
6
- require 'hpricot'
7
- end
8
- # singularize, constantize, camelize, classify; doc here: http://api.rubyonrails.com/classes/Inflector.html
1
+ require 'rubygems'
9
2
  require 'active_support/inflector'
3
+ require File.expand_path(File.dirname(__FILE__) + "/abstract_doc")
10
4
 
11
5
  module Mapricot
12
-
6
+
7
+
13
8
  # Inherit from base, e.g. class Animal < Mapricot::Base
14
9
  # Use either a string of xml or a url to initialize
15
10
  class Base
11
+
16
12
  class << self
17
13
  # @associations is used to initialize instance variables
18
- # creates a new HasOneAssociation and appends it to the @associations list
14
+ # creates a new HasOneAssociation and appends it to the @association_list
19
15
  def has_one(name, type = :string, opts = {})
20
- ass = HasOneAssociation.new(name, type, opts)
21
- self.name.match(/::/) && ass.namespace = self.name.match(/(.*)::[^:]+$/)[1]
22
- associations << ass
16
+ association = HasOneAssociation.new(name, type, opts)
17
+ if self.name.match(/::/)
18
+ association.namespace = self.name.match(/(.*)::[^:]+$/)[1]
19
+ end
20
+ association_list << association
23
21
  class_eval "attr_reader :#{name}", __FILE__, __LINE__
24
22
  end
25
- # creates a new HasManyAssociation and appends it to the @associations list
23
+
24
+ # creates a new HasManyAssociation and appends it to the @association_list
26
25
  def has_many(name, type = :string, opts = {})
27
- ass = HasManyAssociation.new(name, type, opts)
28
- self.name.match(/::/) && ass.namespace = self.name.match(/(.*)::[^:]+$/)[1]
29
- associations << ass
26
+ association = HasManyAssociation.new(name, type, opts)
27
+ if self.name.match(/::/)
28
+ association.namespace = self.name.match(/(.*)::[^:]+$/)[1]
29
+ end
30
+ association_list << association
30
31
  class_eval "attr_reader :#{name}", __FILE__, __LINE__
31
32
  end
32
- def has_attribute(name)
33
- attributes << name
33
+
34
+ def has_attribute(name, type = :string)
35
+ attribute_list << Attribute.new(name, type)
34
36
  class_eval "attr_reader :#{name}", __FILE__, __LINE__
35
37
  end
36
- def associations
37
- @associations ||= []
38
+
39
+ def association_list
40
+ @association_list ||= []
38
41
  end
39
- def attributes
40
- @attributes ||= []
42
+
43
+ def attribute_list
44
+ @attribute_list ||= []
41
45
  end
42
46
  end
43
47
 
44
48
  # class SomeClass < Mapricot::Base; end;
45
49
  # SomeClass.new :url => "http://some_url"
46
50
  # SomeClass.new :xml => %(<hi></hi>)
51
+ # the class instance variable @association_list is duplicated in every instance of Feed, as the instance variable @associations.
52
+ # i.e. Feed.association_list is the template for feed.associations
47
53
  def initialize(opts)
48
- @xml = Hpricot::XML(open(opts[:url])) if opts[:url]
49
- @xml = Hpricot::XML(opts[:xml]) if opts[:xml]
50
- load_associations
51
- load_attributes
54
+ @doc = AbstractDoc.from_url(opts[:url]) if opts[:url]
55
+ @doc = AbstractDoc.from_string(opts[:xml]) if opts[:xml]
56
+ dup_associations_and_attributes
57
+ map_associations
58
+ map_attributes
52
59
  end
53
60
 
54
- # searches xml for a tag with association.name, sets association.value to the inner html of this tag and typecasts it
55
- def load_has_one(has_one_association)
56
- has_one_association.search(@xml)
61
+ def dup_associations_and_attributes
62
+ @associations = self.class.association_list.collect {|x| x.dup} # do not do this: self.class.association_list.dup
63
+ @attributes = self.class.attribute_list.collect {|x| x.dup}
57
64
  end
58
65
 
59
- def load_has_many(has_many_association)
60
- has_many_association.search(@xml)
61
- end
62
-
63
- def load_associations
64
- # loop through the class's instance variable @attributes, which holds all of our associations
65
- association_list = self.class.instance_variable_get(:@associations)
66
- association_list && association_list.each do |ass|
67
- load_has_one(ass) if ass.is_a?(HasOneAssociation)
68
- load_has_many(ass) if ass.is_a?(HasManyAssociation)
69
- # set instance variables and create accessors for each
70
- instance_variable_set("@#{ass.name}", ass.value)
66
+ def map_associations
67
+ @associations.each do |association|
68
+ node_list = @doc.find(association.tag_name)
69
+ association.set_value_from_node_list(node_list)
70
+ instance_variable_set("@#{association.name}", association.value)
71
71
  end
72
72
  end
73
73
 
74
- def load_attributes
75
- attr_list = self.class.instance_variable_get(:@attributes)
76
- attr_list && attr_list.each do |att|
77
- val = (@xml/self.class.name.downcase.match(/[^:]+$/)[0]).first.attributes[att.to_s]
78
- instance_variable_set("@#{att}", val)
74
+ def map_attributes
75
+ @attributes.each do |attribute|
76
+ node = @doc.find(tag_name).first
77
+ attribute.set_value_from_node(node)
78
+ instance_variable_set("@#{attribute.name}", attribute.value)
79
79
  end
80
80
  end
81
+
82
+ # associations and base classes both have tag_name method
83
+ def tag_name
84
+ self.class.name.downcase.match(/[^:]+$/)[0]
85
+ end
81
86
  end
82
87
 
88
+
89
+
83
90
  # Abstract class; used to subclass HasOneAssociation and HasManyAssociation
84
91
  class Association
85
92
  VALID_TYPES = [:integer, :time, :xml, :string]
86
93
 
87
94
  attr_accessor :name, :type, :value
88
95
  attr_accessor :namespace
96
+
89
97
  def initialize(name, type, opts = {})
90
98
  raise "Don't instantiate me" if abstract_class?
91
99
  @name, @type, @opts = name, type, opts
92
100
  @namespace = nil
93
101
  end
102
+
103
+ def tag_name
104
+ @opts[:tag_name] || singular_name
105
+ end
94
106
 
107
+ private
95
108
  def typecast
96
109
  raise "association type is invalid" unless VALID_TYPES.include?(@type)
97
110
  if [:integer, :time].include?(@type)
@@ -103,6 +116,7 @@ module Mapricot
103
116
  @value.is_a?(Array) ? @value.collect {|v| v.to_i} : @value.to_i
104
117
  end
105
118
 
119
+ # oh, forgot about this, need to add to readme
106
120
  def typecast_time
107
121
  if @value.is_a?(Array)
108
122
  @value.collect {|v| Time.parse(v) }
@@ -110,8 +124,7 @@ module Mapricot
110
124
  Time.parse(@value)
111
125
  end
112
126
  end
113
-
114
- private
127
+
115
128
  def abstract_class?
116
129
  self.class == Association
117
130
  end
@@ -129,46 +142,69 @@ module Mapricot
129
142
  singular_name.classify.constantize
130
143
  end
131
144
  end
145
+
146
+ def set_value_from_node_list(node_list)
147
+ end
148
+
132
149
  end
133
150
 
134
151
 
135
152
  class HasOneAssociation < Association
136
- # searches xml for tag name, sets the inner xml as association value and typecasts it
137
- def search(xml)
138
- # if tag_name option was passed, use it:
139
- element = (xml/"#{ @opts[:tag_name] || @name }").first # class Hpricot::Elements
140
- if element
153
+
154
+ # pass a node list, depending on the type of association
155
+ def set_value_from_node_list(node_list)
156
+ if node_list.empty?
157
+ @value = nil
158
+ else
141
159
  if @type == :xml
142
- @value = class_from_name.new(:xml => element.to_s) # we want to include the tag, not just the inner_html
160
+ @value = class_from_name.new(:xml => node_list.first.to_s)
143
161
  else
144
- @value = element.inner_html
162
+ @value = node_list.first.contents
163
+ typecast
145
164
  end
146
- self.typecast
147
165
  end
148
166
  end
149
167
  end
150
168
 
151
-
169
+
152
170
  class HasManyAssociation < Association
153
171
 
154
172
  def singular_name
155
- # @name.to_s[0..-2]
156
173
  "#{@name}".singularize
157
174
  end
158
175
 
159
- # searches xml for all occurrences of self.singular_name, the inner xml from each tag is stored in an array and set as the association value
160
- # finally, each element in the array is typecast
161
- def search(xml)
176
+ def set_value_from_node_list(node_list)
162
177
  @value = []
163
- (xml/"#{@opts[:tag_name] || self.singular_name}").each do |tag|
178
+ node_list.each do |node|
164
179
  if @type == :xml
165
- @value << class_from_name.new(:xml => tag.to_s) # a bit of recursion if the inner xml is more xml
180
+ @value << class_from_name.new(:xml => node.to_s)
166
181
  else
167
- @value << tag.inner_html # in the case of a string, integer, etc.
182
+ @value << node.contents
168
183
  end
169
184
  end
185
+ typecast
170
186
  end
171
-
172
187
  end
188
+
189
+
190
+ class Attribute
191
+ attr_accessor :name, :type, :value
173
192
 
193
+ def initialize(name, type)
194
+ @name = name
195
+ @type = type
196
+ end
197
+
198
+ def set_value_from_node(node)
199
+ @value = node.attributes[name.to_s]
200
+ typecast
201
+ end
202
+
203
+ private
204
+ def typecast
205
+ if !@value.nil? && !@value.empty? && @type == :integer
206
+ @value = @value.to_i
207
+ end
208
+ end
209
+ end
174
210
  end
data/mapricot.gemspec CHANGED
@@ -1,7 +1,7 @@
1
1
  Gem::Specification.new do |s|
2
2
  s.name = "mapricot"
3
- s.version = "0.0.1"
4
- s.date = "2009-03-06"
3
+ s.version = "0.0.2.1"
4
+ s.date = "2009-03-17"
5
5
  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
6
6
  s.summary = "XML to object mapper"
7
7
  s.email = "lzell11@gmail.com"
@@ -15,14 +15,27 @@ Gem::Specification.new do |s|
15
15
  "mapricot.gemspec",
16
16
  "examples/lastfm_api.rb",
17
17
  "examples/lastfm_api_no_request.rb",
18
+ "examples/facebook_api.rb",
18
19
  "examples/natural_inputs.rb",
19
20
  "examples/readme_examples.rb",
20
- "test/mapricot_tests.rb",
21
- "test/mapricot_spec.rb",
22
- "lib/mapricot.rb"]
21
+ "examples/xml_with_attributes.rb",
22
+ "test/suite.rb",
23
+ "test/abstract_doc_spec.rb",
24
+ "test/test_mapricot.rb",
25
+ "test/test_mapricot_readme.rb",
26
+ "test/has_attribute/has_attribute_spec.rb",
27
+ "test/has_many/has_many_ids_spec.rb",
28
+ "test/has_many/has_many_nested_spec.rb",
29
+ "test/has_one/has_one_id_spec.rb",
30
+ "test/has_one/has_one_user_spec.rb",
31
+ "test/has_one/has_one_nested_spec.rb",
32
+ "lib/mapricot.rb",
33
+ "lib/abstract_doc.rb",
34
+ "benchmark/benchmarks.rb"]
23
35
  s.require_paths = ["lib"]
24
36
  s.rdoc_options = ["--main", "README.rdoc", "--inline-source", "--title", "Mapricot"]
25
37
  s.extra_rdoc_files = ["README.rdoc"]
26
38
  s.add_dependency("hpricot")
39
+ s.add_dependency("nokogiri")
40
+ s.add_dependency("libxml-ruby")
27
41
  end
28
-
@@ -0,0 +1,79 @@
1
+ require 'rubygems'
2
+ require 'spec'
3
+ require File.expand_path(File.dirname(__FILE__) + "/../lib/abstract_doc")
4
+
5
+
6
+ include Mapricot
7
+
8
+
9
+ share_examples_for "an abstract xml parser" do
10
+
11
+ it "should initialize from a url or a string" do
12
+ AbstractDoc.should respond_to(:from_string)
13
+ AbstractDoc.should respond_to(:from_url)
14
+ end
15
+
16
+ it "should be using libxml or hpricot or nokogiri" do
17
+ puts "using #{Mapricot.parser}..."
18
+ [:libxml, :hpricot, :nokogiri].include?(Mapricot.parser).should be_true
19
+ end
20
+
21
+ describe "creating a document from a string of xml" do
22
+ before(:all) do
23
+ @doc = AbstractDoc.from_string %(<user>bob</user>)
24
+ end
25
+
26
+ it "should be able to find all the <user> nodes" do
27
+ @doc.find(:user).should_not be_nil
28
+ end
29
+
30
+ describe "the list of user nodes" do
31
+
32
+ before(:all) do
33
+ @user_nodes = @doc.find(:user)
34
+ end
35
+
36
+ it "should be an abstract node list" do
37
+ @user_nodes.should be_a(AbstractNodeList)
38
+ end
39
+
40
+ it "should be able to iterate over" do
41
+ @user_nodes.should respond_to(:each)
42
+ @user_nodes.should respond_to(:first)
43
+ @user_nodes.should respond_to(:[])
44
+ end
45
+
46
+ describe "the first user node" do
47
+
48
+ before(:all) do
49
+ @user = @user_nodes.first
50
+ end
51
+
52
+ it "should be a abstract node" do
53
+ @user.should be_a(AbstractNode)
54
+ end
55
+
56
+ it "should be able to get the node contents" do
57
+ @user.contents.should == "bob"
58
+ @user.to_s.should == "<user>bob</user>"
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
64
+
65
+ describe "AbstractDoc using libxml" do
66
+ before(:all) { Mapricot.parser = :libxml }
67
+ it_should_behave_like "an abstract xml parser"
68
+ end
69
+
70
+ describe "AbstractDoc useing hpricot" do
71
+ before(:all) { Mapricot.parser = :hpricot }
72
+ it_should_behave_like "an abstract xml parser"
73
+ end
74
+
75
+ describe "AbstractDoc useing hpricot" do
76
+ before(:all) { Mapricot.parser = :nokogiri }
77
+ it_should_behave_like "an abstract xml parser"
78
+ end
79
+
@@ -0,0 +1,57 @@
1
+ require 'rubygems'
2
+ require 'spec'
3
+ require File.expand_path(File.dirname(__FILE__) + "/../../lib/mapricot")
4
+
5
+ include Mapricot
6
+
7
+ class Response < Mapricot::Base
8
+ has_one :location, :xml
9
+ end
10
+
11
+ class Location < Mapricot::Base
12
+ has_one :city
13
+ has_one :state
14
+ has_attribute :code
15
+ has_attribute :id, :integer
16
+ end
17
+
18
+
19
+ share_as :HasAttribute do
20
+
21
+ describe "; response with location city, state, and code" do
22
+
23
+ before(:all) do
24
+ @response = Response.new(:xml => %(
25
+ <response>
26
+ <location code='nyc' id='100'>
27
+ <city>New York</city>
28
+ <state>NY</state>
29
+ </location>
30
+ </response>
31
+ ))
32
+ end
33
+
34
+ it "should have location code 'nyc'" do
35
+ @response.location.city.should == "New York"
36
+ @response.location.state.should == "NY"
37
+ @response.location.code.should == 'nyc'
38
+ @response.location.id.should == 100
39
+ end
40
+ end
41
+ end
42
+
43
+
44
+ describe "has attribute, parsing with hpricot" do
45
+ before(:all) { Mapricot.parser = :hpricot }
46
+ it_should_behave_like HasAttribute
47
+ end
48
+
49
+ describe "has many id, parsing with libxml" do
50
+ before(:all) { Mapricot.parser = :libxml }
51
+ it_should_behave_like HasAttribute
52
+ end
53
+
54
+ describe "has many id, parsing with nokogiri" do
55
+ before(:all) { Mapricot.parser = :nokogiri }
56
+ it_should_behave_like HasAttribute
57
+ end
@@ -0,0 +1,73 @@
1
+ require 'rubygems'
2
+ require 'spec'
3
+ require File.expand_path(File.dirname(__FILE__) + "/../../lib/mapricot")
4
+
5
+ include Mapricot
6
+
7
+ class ResponseWithManyIds < Mapricot::Base
8
+ has_many :ids, :integer
9
+ end
10
+
11
+
12
+ share_as :HasManyIds do
13
+
14
+ before(:all) do
15
+ @response = ResponseWithManyIds.new(:xml => %(
16
+ <response>
17
+ <id>10</id>
18
+ <id>20</id>
19
+ <id>30</id>
20
+ </response>
21
+ ))
22
+ end
23
+
24
+ describe "Interface" do
25
+
26
+ it "should have an array of ids" do
27
+ @response.ids.should be_a(Array)
28
+ @response.ids[0].should == 10
29
+ @response.ids[1].should == 20
30
+ @response.ids[2].should == 30
31
+ end
32
+ end
33
+
34
+
35
+ describe "Internals" do
36
+
37
+ it "should have an accessor on ids" do
38
+ @response.should respond_to(:ids)
39
+ end
40
+
41
+ it "should have a class instance variable @association_list" do
42
+ ass_template = @response.class.association_list.first
43
+ ass_template.name.should == :ids
44
+ ass_template.type.should == :integer
45
+ ass_template.namespace.should be_nil
46
+ ass_template.value.should be_nil
47
+ end
48
+
49
+ it "should have an instance variable @associations, duplicated from class ivar @association_list, with value now set" do
50
+ ass = @response.instance_variable_get(:@associations).first
51
+ ass.name.should == :ids
52
+ ass.type.should == :integer
53
+ ass.namespace.should be_nil
54
+ ass.value.should == [10,20,30]
55
+ end
56
+ end
57
+ end
58
+
59
+
60
+ describe "has many ids, parsing with hpricot" do
61
+ before(:all) { Mapricot.parser = :hpricot }
62
+ it_should_behave_like HasManyIds
63
+ end
64
+
65
+ describe "has many id, parsing with libxml" do
66
+ before(:all) { Mapricot.parser = :libxml }
67
+ it_should_behave_like HasManyIds
68
+ end
69
+
70
+ describe "has many id, parsing with nokogiri" do
71
+ before(:all) { Mapricot.parser = :nokogiri }
72
+ it_should_behave_like HasManyIds
73
+ end
@@ -0,0 +1,116 @@
1
+ require 'rubygems'
2
+ require 'spec'
3
+ require File.expand_path(File.dirname(__FILE__) + "/../../lib/mapricot")
4
+
5
+ include Mapricot
6
+
7
+ class ResponseWithNesting < Mapricot::Base
8
+ has_many :users, :xml
9
+ end
10
+
11
+ class User < Mapricot::Base
12
+ has_one :id, :integer
13
+ has_one :name
14
+ end
15
+
16
+
17
+ share_as :HasManyNested do
18
+
19
+ before(:all) do
20
+ @response = ResponseWithNesting.new(:xml => %(
21
+ <response>
22
+ <user>
23
+ <id>10</id>
24
+ <name>bob</name>
25
+ </user>
26
+ <user>
27
+ <id>20</id>
28
+ <name>sally</name>
29
+ </user>
30
+ </response>
31
+ ))
32
+ end
33
+
34
+ describe "Interface" do
35
+
36
+ it "should have bob as the first user" do
37
+ @response.users.first.id.should == 10
38
+ @response.users.first.name.should == "bob"
39
+ end
40
+
41
+ it "should have sally as the second user" do
42
+ @response.users.last.id.should == 20
43
+ @response.users.last.name.should == "sally"
44
+ end
45
+ end
46
+
47
+
48
+ describe "Internals" do
49
+
50
+ it "should have an accessor on users" do
51
+ @response.should respond_to(:users)
52
+ end
53
+
54
+ it "should have a class instance variable @association_list" do
55
+ ass_template = @response.class.association_list.first
56
+ ass_template.name.should == :users
57
+ ass_template.type.should == :xml
58
+ ass_template.namespace.should be_nil
59
+ ass_template.value.should be_nil
60
+ end
61
+
62
+ it "should have an instance variable @associations, duplicated from class ivar @association_list, with value now set" do
63
+ ass = @response.instance_variable_get(:@associations).first
64
+ ass.name.should == :users
65
+ ass.type.should == :xml
66
+ ass.namespace.should be_nil
67
+ ass.value.should_not be_nil # should not
68
+ end
69
+
70
+ describe "its users" do
71
+
72
+ before(:all) do
73
+ @users = @response.users
74
+ end
75
+
76
+ it "should be a an array of User objects, a subclass of Mapricot::Base" do
77
+ @users.should be_a(Array)
78
+ @users.first.should be_a(User)
79
+ @users.first.should be_a(Mapricot::Base)
80
+ end
81
+
82
+ it "should have a class instance variable @association_list, an array of HasMany and HasOne *instances*" do
83
+ ass_template = @users.first.class.association_list.first
84
+ ass_template.should be_a(HasOneAssociation)
85
+ ass_template.name.should == :id
86
+ ass_template.type.should == :integer
87
+ ass_template.namespace.should be_nil
88
+ ass_template.value.should be_nil
89
+ end
90
+
91
+ it "should have an instance variable @associations, duplicated from class ivar @association_list, with value now set" do
92
+ ass = @users.first.instance_variable_get(:@associations).first
93
+ ass.name.should == :id
94
+ ass.type.should == :integer
95
+ ass.namespace.should be_nil
96
+ ass.value.should == 10
97
+ end
98
+ end
99
+ end
100
+ end
101
+
102
+
103
+ describe "has many ids, parsing with hpricot" do
104
+ before(:all) { Mapricot.parser = :hpricot }
105
+ it_should_behave_like HasManyNested
106
+ end
107
+
108
+ describe "has many id, parsing with libxml" do
109
+ before(:all) { Mapricot.parser = :libxml }
110
+ it_should_behave_like HasManyNested
111
+ end
112
+
113
+ describe "has many id, parsing with nokogiri" do
114
+ before(:all) { Mapricot.parser = :nokogiri }
115
+ it_should_behave_like HasManyNested
116
+ end
@@ -0,0 +1,64 @@
1
+ require 'rubygems'
2
+ require 'spec'
3
+ require File.expand_path(File.dirname(__FILE__) + "/../../lib/mapricot")
4
+
5
+ include Mapricot
6
+
7
+ class FeedWithOneId < Mapricot::Base
8
+ has_one :id, :integer
9
+ end
10
+
11
+
12
+ share_as :HasOneId do
13
+
14
+ before(:all) do
15
+ @feed = FeedWithOneId.new(:xml => "<id>10</id>")
16
+ end
17
+
18
+ describe "Interface" do
19
+
20
+ it "should have an id of 10" do
21
+ @feed.id.should == 10
22
+ end
23
+ end
24
+
25
+
26
+ describe "Internals" do
27
+
28
+ it "should have an accessor on id" do
29
+ @feed.should respond_to(:id)
30
+ end
31
+
32
+ it "should have a class instance variable @association_list" do
33
+ ass_template = @feed.class.association_list.first
34
+ ass_template.name.should == :id
35
+ ass_template.type.should == :integer
36
+ ass_template.namespace.should be_nil
37
+ ass_template.value.should be_nil
38
+ end
39
+
40
+ it "should have an instance variable @associations, duplicated from class ivar @association_list, with value now set" do
41
+ ass = @feed.instance_variable_get(:@associations).first
42
+ ass.name.should == :id
43
+ ass.type.should == :integer
44
+ ass.namespace.should be_nil
45
+ ass.value.should == 10
46
+ end
47
+ end
48
+ end
49
+
50
+
51
+ describe "has one id, parsing with hpricot" do
52
+ before(:all) { Mapricot.parser = :hpricot }
53
+ it_should_behave_like HasOneId
54
+ end
55
+
56
+ describe "has one id, parsing with libxml" do
57
+ before(:all) { Mapricot.parser = :libxml }
58
+ it_should_behave_like HasOneId
59
+ end
60
+
61
+ describe "has one id, parsing with nokogiri" do
62
+ before(:all) { Mapricot.parser = :nokogiri }
63
+ it_should_behave_like HasOneId
64
+ end