mapricot 0.0.4

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,119 @@
1
+ require 'open-uri'
2
+ require 'hpricot'
3
+ require 'libxml'
4
+ require 'nokogiri'
5
+
6
+
7
+ module Mapricot
8
+ @parser = :hpricot
9
+ class << self; attr_accessor :parser; end
10
+
11
+ # AbstractDoc should be able to find tags, get inner tag content. Find all tags (return an array)
12
+ # I think I will also need AbstractNode
13
+
14
+ class AbstractDoc
15
+
16
+ def self.from_url(url)
17
+ adoc = new
18
+ adoc.url = url
19
+ adoc
20
+ end
21
+
22
+ def self.from_string(string)
23
+ adoc = new
24
+ adoc.string = string
25
+ adoc
26
+ end
27
+
28
+ def url=(url)
29
+ if Mapricot.parser == :libxml
30
+ @udoc = LibXML::XML::Parser.file(url).parse
31
+ elsif Mapricot.parser == :hpricot
32
+ @udoc = Hpricot::XML(open(url))
33
+ elsif Mapricot.parser == :nokogiri
34
+ @udoc = Nokogiri::HTML(open(url))
35
+ end
36
+ end
37
+
38
+ def string=(string)
39
+ if Mapricot.parser == :libxml
40
+ @udoc = LibXML::XML::Parser.string(string).parse
41
+ elsif Mapricot.parser == :hpricot
42
+ @udoc = Hpricot::XML(string)
43
+ elsif Mapricot.parser == :nokogiri
44
+ @udoc = Nokogiri::XML(string)
45
+ end
46
+ end
47
+
48
+
49
+ def find(tagname)
50
+ if Mapricot.parser == :libxml
51
+ AbstractNodeList.new(@udoc.find("//#{tagname}")) # hmm...
52
+ elsif Mapricot.parser == :hpricot
53
+ # AbstractNodeList.new(@udoc/tagname)
54
+ AbstractNodeList.new(@udoc/"//#{tagname}")
55
+ elsif Mapricot.parser == :nokogiri
56
+ AbstractNodeList.new(@udoc.search(tagname))
57
+ # AbstractNodeList.new(@udoc.xpath("//#{tagname}"))
58
+ end
59
+ end
60
+
61
+ end
62
+
63
+
64
+
65
+ class AbstractNodeList
66
+ include Enumerable
67
+
68
+ def initialize(node_list)
69
+ @unode_list = node_list
70
+ end
71
+
72
+ def each(&block)
73
+ @unode_list.each {|unode| yield(AbstractNode.new(unode))}
74
+ end
75
+
76
+ def [](i)
77
+ AbstractNode.new(@unode_list[i])
78
+ end
79
+
80
+ def first
81
+ AbstractNode.new(@unode_list.first)
82
+ end
83
+
84
+ def empty?
85
+ @unode_list.empty?
86
+ end
87
+ end
88
+
89
+
90
+ class AbstractNode
91
+ attr_reader :unode
92
+
93
+ def initialize(unode)
94
+ @unode = unode # unresolved node
95
+ end
96
+
97
+ def to_s
98
+ @unode.to_s
99
+ end
100
+
101
+ def attributes
102
+ if Mapricot.parser != :nokogiri
103
+ @unode.attributes
104
+ else
105
+ atts = {}
106
+ @unode.attributes.each {|k,v| atts[k] = v.value}
107
+ atts
108
+ end
109
+ end
110
+
111
+ def contents
112
+ if Mapricot.parser == :libxml || Mapricot.parser == :nokogiri
113
+ @unode.content
114
+ else
115
+ @unode.inner_html
116
+ end
117
+ end
118
+ end
119
+ end
@@ -0,0 +1,100 @@
1
+ module Mapricot
2
+ # Abstract class; used to subclass HasOneAssociation and HasManyAssociation
3
+ class Association
4
+ VALID_TYPES = [:integer, :time, :xml, :string]
5
+
6
+ attr_accessor :name, :type, :value
7
+ attr_accessor :namespace
8
+
9
+ def initialize(name, type, opts = {})
10
+ raise "Don't instantiate me" if abstract_class?
11
+ @name, @type, @opts = name, type, opts
12
+ @namespace = nil
13
+ end
14
+
15
+ def tag_name
16
+ @opts[:tag_name] || singular_name
17
+ end
18
+
19
+ private
20
+ def typecast
21
+ raise "association type is invalid" unless VALID_TYPES.include?(@type)
22
+ if [:integer, :time].include?(@type)
23
+ @value = self.send("typecast_#{@type}")
24
+ end
25
+ end
26
+
27
+ def typecast_integer
28
+ @value.is_a?(Array) ? @value.collect {|v| v.to_i} : @value.to_i
29
+ end
30
+
31
+ # oh, forgot about this, need to add to readme
32
+ def typecast_time
33
+ if @value.is_a?(Array)
34
+ @value.collect {|v| Time.parse(v) }
35
+ else
36
+ Time.parse(@value)
37
+ end
38
+ end
39
+
40
+ def abstract_class?
41
+ self.class == Association
42
+ end
43
+
44
+ def singular_name
45
+ @name.to_s
46
+ end
47
+
48
+ def class_from_name
49
+ # ok, first we have to find how the class that inherited from Mapricot::Base is namespaced
50
+ # the class will an @associations class instance var, that will hold an instance of
51
+ if @namespace
52
+ "#{@namespace}::#{singular_name.classify}".constantize
53
+ else
54
+ singular_name.classify.constantize
55
+ end
56
+ end
57
+
58
+ def set_value_from_node_list(node_list)
59
+ end
60
+ end
61
+
62
+
63
+ class HasOneAssociation < Association
64
+
65
+ # pass a node list, depending on the type of association
66
+ def set_value_from_node_list(node_list)
67
+ if node_list.empty?
68
+ @value = nil
69
+ else
70
+ if @type == :xml
71
+ @value = class_from_name.new(:xml => node_list.first.to_s)
72
+ else
73
+ @value = node_list.first.contents
74
+ typecast
75
+ end
76
+ end
77
+ end
78
+ end
79
+
80
+
81
+ class HasManyAssociation < Association
82
+
83
+ def singular_name
84
+ "#{@name}".singularize
85
+ end
86
+
87
+ def set_value_from_node_list(node_list)
88
+ @value = []
89
+ node_list.each do |node|
90
+ if @type == :xml
91
+ @value << class_from_name.new(:xml => node.to_s)
92
+ else
93
+ @value << node.contents
94
+ end
95
+ end
96
+ typecast
97
+ end
98
+ end
99
+
100
+ end
@@ -0,0 +1,22 @@
1
+ module Mapricot
2
+ class Attribute
3
+ attr_accessor :name, :type, :value
4
+
5
+ def initialize(name, type)
6
+ @name = name
7
+ @type = type
8
+ end
9
+
10
+ def set_value_from_node(node)
11
+ @value = node.attributes[name.to_s]
12
+ typecast
13
+ end
14
+
15
+ private
16
+ def typecast
17
+ if !@value.nil? && !@value.empty? && @type == :integer
18
+ @value = @value.to_i
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,81 @@
1
+ module Mapricot
2
+ # Inherit from base, e.g. class Animal < Mapricot::Base
3
+ # Use either a string of xml or a url to initialize
4
+ class Base
5
+
6
+ class << self
7
+ # @associations is used to initialize instance variables
8
+ # creates a new HasOneAssociation and appends it to the @association_list
9
+ def has_one(name, type = :string, opts = {})
10
+ association = HasOneAssociation.new(name, type, opts)
11
+ if self.name.match(/::/)
12
+ association.namespace = self.name.match(/(.*)::[^:]+$/)[1]
13
+ end
14
+ association_list << association
15
+ class_eval "attr_reader :#{name}", __FILE__, __LINE__
16
+ end
17
+
18
+ # creates a new HasManyAssociation and appends it to the @association_list
19
+ def has_many(name, type = :string, opts = {})
20
+ association = HasManyAssociation.new(name, type, opts)
21
+ if self.name.match(/::/)
22
+ association.namespace = self.name.match(/(.*)::[^:]+$/)[1]
23
+ end
24
+ association_list << association
25
+ class_eval "attr_reader :#{name}", __FILE__, __LINE__
26
+ end
27
+
28
+ def has_attribute(name, type = :string)
29
+ attribute_list << Attribute.new(name, type)
30
+ class_eval "attr_reader :#{name}", __FILE__, __LINE__
31
+ end
32
+
33
+ def association_list
34
+ @association_list ||= []
35
+ end
36
+
37
+ def attribute_list
38
+ @attribute_list ||= []
39
+ end
40
+ end
41
+
42
+ # class SomeClass < Mapricot::Base; end;
43
+ # SomeClass.new :url => "http://some_url"
44
+ # SomeClass.new :xml => %(<hi></hi>)
45
+ # the class instance variable @association_list is duplicated in every instance of Feed, as the instance variable @associations.
46
+ # i.e. Feed.association_list is the template for feed.associations
47
+ def initialize(opts)
48
+ @doc = AbstractDoc.from_url(opts[:url]) if opts[:url]
49
+ @doc = AbstractDoc.from_string(opts[:xml]) if opts[:xml]
50
+ dup_associations_and_attributes
51
+ map_associations
52
+ map_attributes
53
+ end
54
+
55
+ def dup_associations_and_attributes
56
+ @associations = self.class.association_list.collect {|x| x.dup} # do not do this: self.class.association_list.dup
57
+ @attributes = self.class.attribute_list.collect {|x| x.dup}
58
+ end
59
+
60
+ def map_associations
61
+ @associations.each do |association|
62
+ node_list = @doc.find(association.tag_name)
63
+ association.set_value_from_node_list(node_list)
64
+ instance_variable_set("@#{association.name}", association.value)
65
+ end
66
+ end
67
+
68
+ def map_attributes
69
+ @attributes.each do |attribute|
70
+ node = @doc.find(tag_name).first
71
+ attribute.set_value_from_node(node)
72
+ instance_variable_set("@#{attribute.name}", attribute.value)
73
+ end
74
+ end
75
+
76
+ # associations and base classes both have tag_name method
77
+ def tag_name
78
+ self.class.name.downcase.match(/[^:]+$/)[0]
79
+ end
80
+ end
81
+ end
data/lib/mapricot.rb ADDED
@@ -0,0 +1,12 @@
1
+ require 'active_support/inflector'
2
+
3
+ module Mapricot
4
+ VERSION = "0.0.4"
5
+ end
6
+
7
+ path = File.expand_path(File.join(File.dirname(__FILE__), 'mapricot'))
8
+
9
+ require File.join(path, 'base')
10
+ require File.join(path, 'abstract_doc')
11
+ require File.join(path, 'associations')
12
+ require File.join(path, 'attribute')
data/mapricot.gemspec ADDED
@@ -0,0 +1,40 @@
1
+ Gem::Specification.new do |s|
2
+ s.name = "mapricot"
3
+ s.version = "0.0.4"
4
+ s.summary = "XML to object mapper"
5
+ s.email = "lzell11@gmail.com"
6
+ s.homepage = "http://github.com/lzell/mapricot"
7
+ s.description = "XML to object mapper with an interface similar to ActiveRecord associations."
8
+ s.has_rdoc = true
9
+ s.authors = ["Lou Zell"]
10
+ s.files = ["README.rdoc",
11
+ "History.txt",
12
+ "License.txt",
13
+ "mapricot.gemspec",
14
+ "examples/facebook_api.rb",
15
+ "examples/lastfm_api.rb",
16
+ "examples/lastfm_api_no_request.rb",
17
+ "examples/readme_examples.rb",
18
+ "examples/xml_with_attributes.rb",
19
+ "test/suite.rb",
20
+ "test/test_abstract_doc.rb",
21
+ "test/test_mapricot.rb",
22
+ "test/test_mapricot_readme.rb",
23
+ "test/has_attribute/test_has_attribute.rb",
24
+ "test/has_many/test_has_many_ids.rb",
25
+ "test/has_many/test_has_many_nested.rb",
26
+ "test/has_one/test_has_one_id.rb",
27
+ "test/has_one/test_has_one_nested.rb",
28
+ "lib/mapricot.rb",
29
+ "lib/mapricot/abstract_doc.rb",
30
+ "lib/mapricot/associations.rb",
31
+ "lib/mapricot/attribute.rb",
32
+ "lib/mapricot/base.rb",
33
+ "benchmark/benchmarks.rb"]
34
+ s.require_paths = ["lib"]
35
+ s.rdoc_options = ["--main", "README.rdoc", "--title", "Mapricot"]
36
+ s.extra_rdoc_files = ["README.rdoc"]
37
+ s.add_dependency("hpricot")
38
+ s.add_dependency("nokogiri")
39
+ s.add_dependency("libxml-ruby")
40
+ end
@@ -0,0 +1,42 @@
1
+ require 'test/unit'
2
+ require File.expand_path(File.join(File.dirname(__FILE__), '..', '..', 'lib', 'mapricot'))
3
+
4
+
5
+ class Response < Mapricot::Base
6
+ has_one :location, :xml
7
+ end
8
+
9
+ class Location < Mapricot::Base
10
+ has_one :city
11
+ has_one :state
12
+ has_attribute :code
13
+ has_attribute :id, :integer
14
+ end
15
+
16
+ class TestResponse < Test::Unit::TestCase
17
+
18
+ def setup
19
+ @parsers = [:hpricot, :nokogiri, :libxml]
20
+ @xml = %(
21
+ <response>
22
+ <location code='nyc' id='100'>
23
+ <city>New York</city>
24
+ <state>NY</state>
25
+ </location>
26
+ </response>
27
+ )
28
+ end
29
+
30
+ def test_response
31
+ @parsers.each do |parser|
32
+ Mapricot.parser = parser
33
+ response = Response.new(:xml => @xml)
34
+ assert_equal "New York", response.location.city
35
+ assert_equal "NY", response.location.state
36
+ assert_equal "nyc", response.location.code
37
+ assert_equal 100, response.location.id
38
+ end
39
+ end
40
+
41
+ end
42
+
@@ -0,0 +1,55 @@
1
+ require 'test/unit'
2
+ require File.expand_path(File.join(File.dirname(__FILE__), '..', '..', 'lib', 'mapricot'))
3
+
4
+
5
+ class ResponseWithManyIds < Mapricot::Base
6
+ has_many :ids, :integer
7
+ end
8
+
9
+
10
+ class TestResponseWithManyIds < Test::Unit::TestCase
11
+
12
+ def setup
13
+ @parsers = [:hpricot, :nokogiri, :libxml]
14
+ @xml = %(
15
+ <response>
16
+ <id>10</id>
17
+ <id>20</id>
18
+ <id>30</id>
19
+ </response>
20
+ )
21
+ end
22
+
23
+ def test_response
24
+ @parsers.each do |parser|
25
+ Mapricot.parser = parser
26
+ response = ResponseWithManyIds.new(:xml => @xml)
27
+ assert_equal 10, response.ids[0]
28
+ assert_equal 20, response.ids[1]
29
+ assert_equal 30, response.ids[2]
30
+ end
31
+ end
32
+
33
+ # ---------------- stop reading here ---------------- #
34
+
35
+
36
+
37
+
38
+
39
+
40
+
41
+ def test_internals
42
+ response = ResponseWithManyIds.new(:xml => @xml)
43
+ template = response.class.association_list.first
44
+ assert_equal :ids, template.name
45
+ assert_equal :integer, template.type
46
+ assert_nil template.namespace
47
+ assert_nil template.value
48
+
49
+ ass = response.instance_variable_get(:@associations).first
50
+ assert_equal :ids, ass.name
51
+ assert_equal :integer, ass.type
52
+ assert_nil ass.namespace
53
+ assert_equal [10,20,30], ass.value
54
+ end
55
+ end
@@ -0,0 +1,81 @@
1
+ require 'test/unit'
2
+ require File.expand_path(File.join(File.dirname(__FILE__), '..', '..', 'lib', 'mapricot'))
3
+
4
+ class ResponseWithNesting < Mapricot::Base
5
+ has_many :users, :xml
6
+ end
7
+
8
+ class User < Mapricot::Base
9
+ has_one :id, :integer
10
+ has_one :name
11
+ end
12
+
13
+ class TestResponseWithNesting < Test::Unit::TestCase
14
+
15
+ def setup
16
+ @parsers = [:hpricot, :nokogiri, :libxml]
17
+ @xml = %(
18
+ <response>
19
+ <user>
20
+ <id>10</id>
21
+ <name>bob</name>
22
+ </user>
23
+ <user>
24
+ <id>20</id>
25
+ <name>sally</name>
26
+ </user>
27
+ </response>
28
+ )
29
+ end
30
+
31
+
32
+ def test_response
33
+ @parsers.each do |parser|
34
+ Mapricot.parser = parser
35
+ response = ResponseWithNesting.new(:xml => @xml)
36
+ assert_equal 10, response.users[0].id
37
+ assert_equal "bob", response.users[0].name
38
+ assert_equal 20, response.users[1].id
39
+ assert_equal "sally", response.users[1].name
40
+ end
41
+ end
42
+
43
+ # ---------------- stop reading here ---------------- #
44
+
45
+
46
+
47
+
48
+
49
+
50
+ def test_response_internals
51
+ response = ResponseWithNesting.new(:xml => @xml)
52
+ template = response.class.association_list.first
53
+
54
+ assert_equal :users, template.name
55
+ assert_equal :xml, template.type
56
+ assert_nil template.namespace
57
+ assert_nil template.value
58
+
59
+ ass = response.instance_variable_get(:@associations).first
60
+ assert_equal :users, ass.name
61
+ assert_equal :xml, ass.type
62
+ assert_nil ass.namespace
63
+ assert_not_nil ass.value
64
+ end
65
+
66
+ def test_response_users_internals
67
+ response = ResponseWithNesting.new(:xml => @xml)
68
+ template = response.users.first.class.association_list.first
69
+ assert_equal :id, template.name
70
+ assert_equal :integer, template.type
71
+ assert_nil template.namespace
72
+ assert_nil template.value
73
+
74
+ ass = response.users.first.instance_variable_get(:@associations).first
75
+ assert_equal :id, ass.name
76
+ assert_equal :integer, ass.type
77
+ assert_nil ass.namespace
78
+ assert_equal 10, ass.value
79
+ end
80
+
81
+ end
@@ -0,0 +1,25 @@
1
+ require 'test/unit'
2
+ require File.expand_path(File.join(File.dirname(__FILE__), '..', '..', 'lib', 'mapricot'))
3
+
4
+ class ResponseWithOneId < Mapricot::Base
5
+ has_one :id, :integer
6
+ end
7
+
8
+
9
+ class TestReponseWithOneId < Test::Unit::TestCase
10
+
11
+ def setup
12
+ @parsers = [:hpricot, :nokogiri, :libxml]
13
+ @xml = %(
14
+ <id>10</id>
15
+ )
16
+ end
17
+
18
+ def test_response
19
+ @parsers.each do |parser|
20
+ Mapricot.parser = parser
21
+ response = ResponseWithOneId.new(:xml => @xml)
22
+ assert_equal 10, response.id
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,31 @@
1
+ require 'test/unit'
2
+ require File.expand_path(File.join(File.dirname(__FILE__), '..', '..', 'lib', 'mapricot'))
3
+
4
+ class ResponseWithNesting < Mapricot::Base
5
+ has_one :user, :xml
6
+ end
7
+
8
+ class User < Mapricot::Base
9
+ has_one :name
10
+ end
11
+
12
+
13
+ class TestReponseWithNesting < Test::Unit::TestCase
14
+
15
+ def setup
16
+ @parsers = [:hpricot, :nokogiri, :libxml]
17
+ @xml = %(
18
+ <user>
19
+ <name>bob</name>
20
+ </user>
21
+ )
22
+ end
23
+
24
+ def test_response
25
+ @parsers.each do |parser|
26
+ Mapricot.parser = parser
27
+ response = ResponseWithNesting.new(:xml => @xml)
28
+ assert_equal "bob", response.user.name
29
+ end
30
+ end
31
+ end
data/test/suite.rb ADDED
@@ -0,0 +1,20 @@
1
+ # Usage:
2
+ # ruby suite.rb
3
+ # OR
4
+ # ruby suite.rb rg
5
+ # ^^^ that one is for color, and requires the redgreen gem
6
+
7
+ path = File.expand_path(File.dirname(__FILE__))
8
+
9
+ test_glob = File.join(path, "**", "test_*.rb")
10
+ tests = Dir.glob(test_glob)
11
+
12
+ if ARGV[0] != 'rg'
13
+ tests.each {|test| system("ruby #{test}")}
14
+ else
15
+ tests.each {|test| system("rg #{test}")}
16
+ end
17
+
18
+ # spec_glob = File.join(path, "**", "*_spec.rb")
19
+ # specs = Dir.glob(spec_glob)
20
+ # specs.each {|spec| system("spec --format progress --color #{spec}")}
@@ -0,0 +1,41 @@
1
+ require 'test/unit'
2
+ require File.expand_path(File.join(File.dirname(__FILE__), '..', 'lib', 'mapricot'))
3
+
4
+
5
+ class TestAbstractDoc < Test::Unit::TestCase
6
+
7
+ def test_initialize_from_string_or_url
8
+ assert_respond_to Mapricot::AbstractDoc, :from_string
9
+ assert_respond_to Mapricot::AbstractDoc, :from_url
10
+ end
11
+
12
+ def test_getting_node_contents
13
+ [:libxml, :nokogiri, :hpricot].each do |parser|
14
+ Mapricot.parser = parser
15
+ @doc = Mapricot::AbstractDoc.from_string('<user>bob</user>')
16
+ @node = @doc.find(:user).first
17
+ assert_equal "bob", @node.contents
18
+ assert_equal "<user>bob</user>", @node.to_s
19
+ end
20
+ end
21
+
22
+ def test_node_list
23
+ [:libxml, :nokogiri, :hpricot].each do |parser|
24
+ Mapricot.parser = parser
25
+ @doc = Mapricot::AbstractDoc.from_string('<response><user>sally</user><user>bob</user></response>')
26
+ @node_list = @doc.find(:user)
27
+ assert_instance_of Mapricot::AbstractNodeList, @node_list
28
+ assert_respond_to @node_list, :each
29
+ assert_respond_to @node_list, :first
30
+ assert_respond_to @node_list, :[]
31
+
32
+ @node_list.each do |node|
33
+ assert_instance_of Mapricot::AbstractNode, node
34
+ end
35
+
36
+ assert_equal "sally", @node_list[0].contents
37
+ assert_equal "bob", @node_list[1].contents
38
+ end
39
+ end
40
+
41
+ end