mapricot 0.0.4

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,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