grumpymapper 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +4 -0
- data/.travis.yml +5 -0
- data/Gemfile +4 -0
- data/LICENSE +0 -0
- data/README.md +39 -0
- data/Rakefile +10 -0
- data/grumpymapper.gemspec +23 -0
- data/lib/grumpymapper/version.rb +3 -0
- data/lib/grumpymapper.rb +90 -0
- data/test/fixtures/lastfm.xml +89 -0
- data/test/lastfm_test.rb +94 -0
- metadata +76 -0
data/.gitignore
ADDED
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/LICENSE
ADDED
File without changes
|
data/README.md
ADDED
@@ -0,0 +1,39 @@
|
|
1
|
+
Introduction to GrumpyMapper [![Build Status](https://secure.travis-ci.org/marcinwyszynski/grumpymapper.png?branch=master)](http://travis-ci.org/marcinwyszynski/grumpymapper)
|
2
|
+
------------------------
|
3
|
+
|
4
|
+
GrumpyMapper is a simple alternative to the excellent [HappyMapper](https://github.com/jnunemaker/happymapper/) gem. Unlike HappyMapper it only works one way - parsing XML into a Ruby object. It is also more difficult to use as it assumes some knowledge of XPath. I did create the gem for myself but as a reference for my future self and perhaps a lost visitor to this repository below is some documentation.
|
5
|
+
|
6
|
+
Using GrumpyMapper
|
7
|
+
------------------
|
8
|
+
|
9
|
+
GrumpyMapper is a module which you include in your classes. Once included, it gives access to three class macros ("tag", "has_one" and "has_many") as well as one class method ("parse"). Here is how you'd use it:
|
10
|
+
|
11
|
+
```ruby
|
12
|
+
class Venue
|
13
|
+
include GrumpyMapper
|
14
|
+
tag "//venue"
|
15
|
+
has_one :name, ".//name/text()", String
|
16
|
+
has_one :latitude, ".//lat/text()", Float
|
17
|
+
has_one :longitude, ".//long/text()", Float
|
18
|
+
end
|
19
|
+
|
20
|
+
|
21
|
+
class Event
|
22
|
+
include GrumpyMapper
|
23
|
+
tag "//event"
|
24
|
+
has_one :name, ".//title/text()", String
|
25
|
+
has_one :cancelled, ".//cancelled/text()", Boolean
|
26
|
+
has_one :venue, ".//venue", Venue
|
27
|
+
has_one :start_date, ".//startDate/text()", DateTime
|
28
|
+
has_many :artists, ".//artists/artist/text()", String
|
29
|
+
end
|
30
|
+
|
31
|
+
events = Event::parse(some_xml_content) # ... events is an Array of Event objects
|
32
|
+
````
|
33
|
+
|
34
|
+
For a piece of XML this is supposed to parse, please see fixtures/lastfm.xml or the unit test for this library. In any case, the "tag" class macro defines what is the parent tag for your mapped object. Both attributes and children are then defined using XPath expressions in "has_one" and "has_many" class macros. Each of these macros takes at least three arguments - the attribute to map to the value to, the XPath expression (can be an attribute as well as an element) to search by and the expected class of the element found. A number of simple classes is supported - that is String, Integer, Float, Date and DateTime. A Boolean class is defined inside the GrumpyMapper as Ruby does not have one built-in. If you specify any other class, it should be one that responds to the "parse" class method - preferably one that also includes the GrumpyMapper module. One slight difference between "has_one" and "has_many" class macros is that the first allows you to specify a default value as the fourth argument. It will be used when the XPath expression finds no matches. Some expected classes like String, Integer and Float have a default default - "", 0 and 0.0 respectively. For others it is a Ruby native nil value. The "has_many" class macro always has an empty Array as it's default.
|
35
|
+
|
36
|
+
Parting words
|
37
|
+
-------------
|
38
|
+
|
39
|
+
For more inspiration on how to use the library use the unit test, source code and make sure to have your favorite XPath tutorial close at hand. Enjoy!
|
data/Rakefile
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "grumpymapper/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "grumpymapper"
|
7
|
+
s.version = GrumpyMapper::VERSION
|
8
|
+
s.authors = ["Marcin Wyszynski"]
|
9
|
+
s.email = ["marcin.pixie@gmail.com"]
|
10
|
+
s.homepage = "http://github.com/marcinwyszynski/grumpymapper"
|
11
|
+
s.summary = %q{More versatile, XPath-based one-way version of HappyMapper}
|
12
|
+
s.description = %q{More versatile, XPath-based one-way version of HappyMapper}
|
13
|
+
|
14
|
+
s.rubyforge_project = "grumpymapper"
|
15
|
+
|
16
|
+
s.files = `git ls-files`.split("\n")
|
17
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
18
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
19
|
+
s.require_paths = ["lib"]
|
20
|
+
|
21
|
+
s.add_dependency("nokogiri")
|
22
|
+
|
23
|
+
end
|
data/lib/grumpymapper.rb
ADDED
@@ -0,0 +1,90 @@
|
|
1
|
+
require "date"
|
2
|
+
require "nokogiri"
|
3
|
+
require "grumpymapper/version"
|
4
|
+
|
5
|
+
module GrumpyMapper
|
6
|
+
|
7
|
+
class Boolean; end
|
8
|
+
|
9
|
+
Property = Struct::new(:xpath, :klass, :multi, :default)
|
10
|
+
|
11
|
+
def self.included(base)
|
12
|
+
base.instance_variable_set("@tag", nil)
|
13
|
+
base.instance_variable_set("@attributes", {})
|
14
|
+
base.extend ClassMethods
|
15
|
+
end
|
16
|
+
|
17
|
+
module ClassMethods
|
18
|
+
|
19
|
+
def tag(xpath)
|
20
|
+
@tag = xpath
|
21
|
+
end
|
22
|
+
|
23
|
+
def has_one(name, xpath, klass, default=nil)
|
24
|
+
attr_accessor name
|
25
|
+
if default.nil?
|
26
|
+
default = if klass.eql?(String)
|
27
|
+
""
|
28
|
+
elsif klass.eql?(Integer)
|
29
|
+
0
|
30
|
+
elsif klass.eql?(Float)
|
31
|
+
0.0
|
32
|
+
else
|
33
|
+
nil
|
34
|
+
end
|
35
|
+
end
|
36
|
+
@attributes[name] = Property::new(xpath, klass, false, default)
|
37
|
+
end
|
38
|
+
|
39
|
+
def has_many(name, xpath, klass)
|
40
|
+
attr_accessor name
|
41
|
+
@attributes[name] = Property::new(xpath, klass, true, [])
|
42
|
+
end
|
43
|
+
|
44
|
+
def parse(xml)
|
45
|
+
result = []
|
46
|
+
Nokogiri::XML(xml).xpath(@tag).each do |tag|
|
47
|
+
entity = new
|
48
|
+
@attributes.each do |key,property|
|
49
|
+
matches = tag.xpath(property.xpath)
|
50
|
+
if matches.size > 0
|
51
|
+
matches = if property.klass.eql?(Integer)
|
52
|
+
matches.map(&:text).map(&:to_i)
|
53
|
+
elsif property.klass.eql?(Float)
|
54
|
+
matches.map(&:text).map(&:to_f)
|
55
|
+
elsif property.klass.eql?(Boolean)
|
56
|
+
matches.map(&:text).map do |m|
|
57
|
+
%w(true t 1).include?(m) ? true : false
|
58
|
+
end
|
59
|
+
elsif property.klass.eql?(Date)
|
60
|
+
matches.map(&:text).map do |m|
|
61
|
+
Date::parse(m)
|
62
|
+
end
|
63
|
+
elsif property.klass.eql?(DateTime)
|
64
|
+
matches.map(&:text).map do |m|
|
65
|
+
DateTime::parse(m)
|
66
|
+
end
|
67
|
+
elsif property.klass.eql?(String)
|
68
|
+
matches.map(&:text)
|
69
|
+
else
|
70
|
+
matches.map do |m|
|
71
|
+
property.klass.parse(m.to_s).first
|
72
|
+
end
|
73
|
+
end
|
74
|
+
if property.multi
|
75
|
+
entity.send(:"#{key}=", matches)
|
76
|
+
else
|
77
|
+
entity.send(:"#{key}=", matches.first)
|
78
|
+
end
|
79
|
+
else
|
80
|
+
entity.send(:"#{key}=", property.default)
|
81
|
+
end # if matches.size > 0
|
82
|
+
end # @attributes.each
|
83
|
+
result << entity
|
84
|
+
end # each tag
|
85
|
+
return result
|
86
|
+
end
|
87
|
+
|
88
|
+
end
|
89
|
+
|
90
|
+
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
<?xml version="1.0" encoding="utf-8"?>
|
2
|
+
<lfm status="ok">
|
3
|
+
<events artist="Radiohead" festivalsonly="0" xmlns:geo="http://www.w3.org/2003/01/geo/wgs84_pos#" page="1" perPage="50" totalPages="1" total="24">
|
4
|
+
<event xmlns:geo="http://www.w3.org/2003/01/geo/wgs84_pos#" >
|
5
|
+
<id>3109094</id>
|
6
|
+
<title>Fuji Rock Festival 2012</title>
|
7
|
+
<artists>
|
8
|
+
<artist>Radiohead</artist>
|
9
|
+
<artist>ケンイシイ</artist>
|
10
|
+
<headliner>Radiohead</headliner>
|
11
|
+
</artists>
|
12
|
+
<venue>
|
13
|
+
<id>8802452</id>
|
14
|
+
<name>Naeba Ski Resort</name>
|
15
|
+
<location>
|
16
|
+
<city>Yuzawa</city>
|
17
|
+
<country>Japan</country>
|
18
|
+
<street></street>
|
19
|
+
<postalcode></postalcode>
|
20
|
+
<geo:point>
|
21
|
+
<geo:lat>36.9333333</geo:lat>
|
22
|
+
<geo:long>138.8166667</geo:long>
|
23
|
+
</geo:point>
|
24
|
+
</location>
|
25
|
+
<url>http://www.last.fm/venue/8802452+Naeba+Ski+Resort</url>
|
26
|
+
<website></website>
|
27
|
+
<phonenumber></phonenumber>
|
28
|
+
</venue>
|
29
|
+
<startDate>Fri, 27 Jul 2012 18:28:01</startDate>
|
30
|
+
<endDate>Sun, 29 Jul 2012 18:28:01</endDate>
|
31
|
+
<description>A detailed description in Japanese.</description>
|
32
|
+
<image size="small">http://userserve-ak.last.fm/serve/34/76106242.jpg</image>
|
33
|
+
<image size="medium">http://userserve-ak.last.fm/serve/64/76106242.jpg</image>
|
34
|
+
<image size="large">http://userserve-ak.last.fm/serve/126/76106242.jpg</image>
|
35
|
+
<image size="extralarge">http://userserve-ak.last.fm/serve/252/76106242.jpg</image>
|
36
|
+
<attendance>557</attendance>
|
37
|
+
<reviews>0</reviews>
|
38
|
+
<tag>lastfm:event=3109094</tag>
|
39
|
+
<url>http://www.last.fm/festival/3109094+Fuji+Rock+Festival+2012</url>
|
40
|
+
<website>http://www.fujirockfestival.com/</website>
|
41
|
+
<tickets> </tickets>
|
42
|
+
<cancelled>0</cancelled>
|
43
|
+
</event>
|
44
|
+
<event xmlns:geo="http://www.w3.org/2003/01/geo/wgs84_pos#" >
|
45
|
+
<id>3191725</id>
|
46
|
+
<title>Jisan Valley Rock Festival</title>
|
47
|
+
<artists>
|
48
|
+
<artist>Radiohead</artist>
|
49
|
+
<artist>Owl City</artist>
|
50
|
+
<headliner>Radiohead</headliner>
|
51
|
+
</artists>
|
52
|
+
<venue>
|
53
|
+
<id>10240273</id>
|
54
|
+
<name>Jisan Valley Ski Resort</name>
|
55
|
+
<location>
|
56
|
+
<city>Icheon</city>
|
57
|
+
<country>Korea, Republic of</country>
|
58
|
+
<street></street>
|
59
|
+
<postalcode></postalcode>
|
60
|
+
<geo:point>
|
61
|
+
<geo:lat></geo:lat>
|
62
|
+
<geo:long></geo:long>
|
63
|
+
</geo:point>
|
64
|
+
</location>
|
65
|
+
<url>http://www.last.fm/venue/10240273+Jisan+Valley+Ski+Resort</url>
|
66
|
+
<website></website>
|
67
|
+
<phonenumber></phonenumber>
|
68
|
+
<image size="small"></image>
|
69
|
+
</venue>
|
70
|
+
<startDate>Fri, 27 Jul 2012 19:21:01</startDate>
|
71
|
+
<endDate>Sun, 29 Jul 2012 19:21:01</endDate>
|
72
|
+
<description>Something in Korean.</description>
|
73
|
+
<image size="small">http://userserve-ak.last.fm/serve/34/79922351.jpg</image>
|
74
|
+
<image size="medium">http://userserve-ak.last.fm/serve/64/79922351.jpg</image>
|
75
|
+
<image size="large">http://userserve-ak.last.fm/serve/126/79922351.jpg</image>
|
76
|
+
<image size="extralarge">http://userserve-ak.last.fm/serve/252/79922351.jpg</image>
|
77
|
+
<attendance>103</attendance>
|
78
|
+
<reviews>0</reviews>
|
79
|
+
<tag>lastfm:event=3191725</tag>
|
80
|
+
<url>http://www.last.fm/festival/3191725+Jisan+Valley+Rock+Festival</url>
|
81
|
+
<website>http://valleyrockfestival.mnet.com/2012/index.asp</website>
|
82
|
+
<tickets></tickets>
|
83
|
+
<cancelled>1</cancelled>
|
84
|
+
<tags>
|
85
|
+
<tag>rock</tag>
|
86
|
+
</tags>
|
87
|
+
</event>
|
88
|
+
</events>
|
89
|
+
</lfm>
|
data/test/lastfm_test.rb
ADDED
@@ -0,0 +1,94 @@
|
|
1
|
+
require "test/unit"
|
2
|
+
require "grumpymapper"
|
3
|
+
require "pry"
|
4
|
+
|
5
|
+
|
6
|
+
class Venue
|
7
|
+
include GrumpyMapper
|
8
|
+
tag "//venue"
|
9
|
+
has_one :name, ".//name/text()", String
|
10
|
+
has_one :lastfm_id, ".//id/text()", Integer
|
11
|
+
has_one :latitude, ".//lat/text()", Float
|
12
|
+
has_one :longitude, ".//long/text()", Float
|
13
|
+
has_one :city, ".//city/text()", String
|
14
|
+
end
|
15
|
+
|
16
|
+
|
17
|
+
class Event
|
18
|
+
include GrumpyMapper
|
19
|
+
tag "//event"
|
20
|
+
has_one :name, ".//title/text()", String
|
21
|
+
has_one :lastfm_id, ".//id/text()", Integer
|
22
|
+
has_one :cancelled, ".//cancelled/text()", Boolean
|
23
|
+
has_one :description, ".//description/text()", String
|
24
|
+
has_one :venue, ".//venue", Venue
|
25
|
+
has_one :start_date, ".//startDate/text()", DateTime
|
26
|
+
has_many :artists, ".//artists/artist/text()", String
|
27
|
+
end
|
28
|
+
|
29
|
+
|
30
|
+
class LastFMTest < Test::Unit::TestCase
|
31
|
+
|
32
|
+
def setup
|
33
|
+
@content = File::open(File::join(File::dirname(__FILE__),
|
34
|
+
"fixtures", "lastfm.xml")).read()
|
35
|
+
end
|
36
|
+
|
37
|
+
def test_parsing_correctness
|
38
|
+
# Check for parsing correctness
|
39
|
+
events = nil
|
40
|
+
assert_nothing_raised do
|
41
|
+
events = Event::parse(@content)
|
42
|
+
end
|
43
|
+
assert_equal 2, events.size, "Should have parsed two events"
|
44
|
+
end
|
45
|
+
|
46
|
+
def test_boolean_parsing
|
47
|
+
events = Event::parse(@content)
|
48
|
+
assert !events.first.cancelled, "First event should not be cancelled"
|
49
|
+
assert events.last.cancelled, "Second event should be cancelled"
|
50
|
+
end
|
51
|
+
|
52
|
+
def test_integer_parsing
|
53
|
+
events = Event::parse(@content)
|
54
|
+
assert events.first.lastfm_id.is_a?(Integer), "LastFM ID is not an integer"
|
55
|
+
assert_equal 3109094, events.first.lastfm_id, "Wrong LastFM ID"
|
56
|
+
end
|
57
|
+
|
58
|
+
def test_datetime_parsing
|
59
|
+
events = Event::parse(@content)
|
60
|
+
assert events.first.start_date.is_a?(DateTime), "Start date not a DateTime object"
|
61
|
+
assert_equal 2012, events.first.start_date.year, "Wrong year parsed"
|
62
|
+
assert_equal 7, events.first.start_date.month, "Wrong month parsed"
|
63
|
+
assert_equal 27, events.first.start_date.day, "Wrong day parsed"
|
64
|
+
assert events.first.start_date < events.last.start_date
|
65
|
+
end
|
66
|
+
|
67
|
+
def test_single_string_parsing
|
68
|
+
events = Event::parse(@content)
|
69
|
+
assert events.first.name.is_a?(String), "Event name not a String"
|
70
|
+
assert_equal "Fuji Rock Festival 2012", events.first.name, "Wrong name parsed"
|
71
|
+
end
|
72
|
+
|
73
|
+
def test_multiple_string_parsing
|
74
|
+
events = Event::parse(@content)
|
75
|
+
assert events.first.artists.is_a?(Array), "Artist list not an Array"
|
76
|
+
assert events.first.artists.first.is_a?(String), "First artist name not a String"
|
77
|
+
assert_equal "Radiohead", events.first.artists.first, "Wrong name parsed"
|
78
|
+
end
|
79
|
+
|
80
|
+
def test_single_object_parsing
|
81
|
+
events = Event::parse(@content)
|
82
|
+
venue = events.first.venue
|
83
|
+
assert venue.is_a?(Venue), "Not a Venue object"
|
84
|
+
assert_equal "Naeba Ski Resort", venue.name, "Wrong Venue name"
|
85
|
+
end
|
86
|
+
|
87
|
+
def test_float_parsing
|
88
|
+
venue = Event::parse(@content).first.venue
|
89
|
+
assert venue.latitude.is_a?(Float), "Venue latitude not a Float"
|
90
|
+
assert venue.longitude.is_a?(Float), "Venue longitude not a Float"
|
91
|
+
assert_equal 36.9333333, venue.latitude, "Wrong latitude parsed"
|
92
|
+
end
|
93
|
+
|
94
|
+
end
|
metadata
ADDED
@@ -0,0 +1,76 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: grumpymapper
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
prerelease:
|
5
|
+
version: 0.0.2
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Marcin Wyszynski
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
|
13
|
+
date: 2012-07-28 00:00:00 Z
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: nokogiri
|
17
|
+
prerelease: false
|
18
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
19
|
+
none: false
|
20
|
+
requirements:
|
21
|
+
- - ">="
|
22
|
+
- !ruby/object:Gem::Version
|
23
|
+
version: "0"
|
24
|
+
type: :runtime
|
25
|
+
version_requirements: *id001
|
26
|
+
description: More versatile, XPath-based one-way version of HappyMapper
|
27
|
+
email:
|
28
|
+
- marcin.pixie@gmail.com
|
29
|
+
executables: []
|
30
|
+
|
31
|
+
extensions: []
|
32
|
+
|
33
|
+
extra_rdoc_files: []
|
34
|
+
|
35
|
+
files:
|
36
|
+
- .gitignore
|
37
|
+
- .travis.yml
|
38
|
+
- Gemfile
|
39
|
+
- LICENSE
|
40
|
+
- README.md
|
41
|
+
- Rakefile
|
42
|
+
- grumpymapper.gemspec
|
43
|
+
- lib/grumpymapper.rb
|
44
|
+
- lib/grumpymapper/version.rb
|
45
|
+
- test/fixtures/lastfm.xml
|
46
|
+
- test/lastfm_test.rb
|
47
|
+
homepage: http://github.com/marcinwyszynski/grumpymapper
|
48
|
+
licenses: []
|
49
|
+
|
50
|
+
post_install_message:
|
51
|
+
rdoc_options: []
|
52
|
+
|
53
|
+
require_paths:
|
54
|
+
- lib
|
55
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
56
|
+
none: false
|
57
|
+
requirements:
|
58
|
+
- - ">="
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
version: "0"
|
61
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
62
|
+
none: false
|
63
|
+
requirements:
|
64
|
+
- - ">="
|
65
|
+
- !ruby/object:Gem::Version
|
66
|
+
version: "0"
|
67
|
+
requirements: []
|
68
|
+
|
69
|
+
rubyforge_project: grumpymapper
|
70
|
+
rubygems_version: 1.8.15
|
71
|
+
signing_key:
|
72
|
+
specification_version: 3
|
73
|
+
summary: More versatile, XPath-based one-way version of HappyMapper
|
74
|
+
test_files:
|
75
|
+
- test/fixtures/lastfm.xml
|
76
|
+
- test/lastfm_test.rb
|