muffins 0.0.5 → 0.0.6
Sign up to get free protection for your applications and to get access to all the features.
- data/.rvmrc +1 -0
- data/README.md +15 -22
- data/Rakefile +1 -1
- data/lib/muffins.rb +13 -35
- data/lib/muffins/class_methods.rb +60 -0
- data/lib/muffins/document.rb +38 -0
- data/lib/muffins/mapping.rb +24 -59
- data/lib/muffins/mapping_parent.rb +27 -0
- data/lib/muffins/version.rb +1 -1
- data/muffins.gemspec +15 -17
- data/spec/fixtures/books.html +23 -0
- data/spec/fixtures/books.xml +11 -0
- data/spec/muffins/document_spec.rb +46 -0
- data/spec/muffins/mapping_parent_spec.rb +33 -0
- data/spec/muffins/mapping_spec.rb +29 -266
- data/spec/muffins_spec.rb +43 -103
- data/spec/spec_helper.rb +1 -4
- metadata +30 -59
- data/features/basic_parsing.feature +0 -33
- data/features/nested_parsing.feature +0 -37
- data/features/non_primitive_typing.feature +0 -36
- data/features/specified_pathnames.feature +0 -33
- data/features/specified_typing.feature +0 -33
- data/features/step_definitions/application_steps.rb +0 -52
- data/features/support/env.rb +0 -16
- data/lib/muffins/mapping_decorator.rb +0 -22
- data/spec/muffins/mapping_decorator_spec.rb +0 -46
data/.rvmrc
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
rvm use --create 1.9.2@muffins
|
data/README.md
CHANGED
@@ -1,6 +1,8 @@
|
|
1
1
|
Muffins
|
2
2
|
========
|
3
3
|
|
4
|
+
[![travis](https://secure.travis-ci.org/rclosner/muffins.png)](http://travis-ci.org/rclosner/muffins)
|
5
|
+
|
4
6
|
A Nokogiri-based Object to XML/HTML mapping library.
|
5
7
|
|
6
8
|
Installation
|
@@ -38,44 +40,35 @@ Examples
|
|
38
40
|
|
39
41
|
Primary class:
|
40
42
|
|
43
|
+
```ruby
|
41
44
|
class Book
|
42
45
|
|
43
46
|
include Muffins
|
44
47
|
|
45
|
-
|
48
|
+
path 'book'
|
46
49
|
|
47
|
-
map :asin
|
50
|
+
map :asin, String
|
48
51
|
|
49
52
|
within :item_attributes do |attributes|
|
50
|
-
attributes.map :title
|
51
|
-
attributes.map :author
|
52
|
-
attributes.map :ean, :to => 'EAN'
|
53
|
-
attributes.map :release_date,
|
53
|
+
attributes.map :title, String
|
54
|
+
attributes.map :author, String, :collection => true
|
55
|
+
attributes.map :ean, String, :to => 'EAN'
|
56
|
+
attributes.map :release_date, Date
|
54
57
|
end
|
55
58
|
|
56
|
-
map :editorial_review, :within => :reviews
|
57
|
-
|
58
|
-
map :similar_products, :to => 'SimilarProducts', :type => Product
|
59
|
-
|
60
|
-
end
|
61
|
-
|
62
|
-
Associated class:
|
63
|
-
|
64
|
-
class Product
|
65
|
-
|
66
|
-
include Muffins
|
67
|
-
|
68
|
-
base_path 'similar_product'
|
69
|
-
|
70
|
-
map :title
|
71
|
-
map :ean, :to => 'EAN'
|
59
|
+
map :editorial_review, String, :within => :reviews
|
72
60
|
|
73
61
|
end
|
62
|
+
```
|
74
63
|
|
75
64
|
##Usage
|
76
65
|
|
66
|
+
```ruby
|
77
67
|
Book.parse(xml)
|
68
|
+
```
|
78
69
|
|
79
70
|
returns:
|
80
71
|
|
72
|
+
```ruby
|
81
73
|
[#<Book:0x233ebe8 @asin='1400079985', @title="War and Peace (Vintage Classics)", @author="Leo Tolstoy", @ean="9781400079988", @release_date=Tue, 02 Dec 2008, @editorial_review='ZOMG.', :similar_products=[#<Product:0x234ebe9 @title='Anna Karenina', @ean='9780143035008'>]>]
|
74
|
+
```
|
data/Rakefile
CHANGED
@@ -1 +1 @@
|
|
1
|
-
require
|
1
|
+
require "bundler/gem_tasks"
|
data/lib/muffins.rb
CHANGED
@@ -1,44 +1,22 @@
|
|
1
|
-
require
|
2
|
-
require
|
3
|
-
require "muffins/version"
|
1
|
+
require 'nokogiri'
|
2
|
+
require 'virtus'
|
4
3
|
|
5
4
|
module Muffins
|
6
5
|
|
6
|
+
# @api private
|
7
7
|
def self.included(base)
|
8
|
+
base.send(:include, Virtus)
|
9
|
+
base.extend(Virtus::Options)
|
10
|
+
base.accept_options(:path)
|
8
11
|
base.extend(ClassMethods)
|
9
12
|
end
|
10
13
|
|
11
|
-
|
12
|
-
|
13
|
-
def base_path(bp = nil)
|
14
|
-
@base_path = bp if bp
|
15
|
-
@base_path
|
16
|
-
end
|
17
|
-
|
18
|
-
def parse(document, options = {})
|
19
|
-
document.css(base_path).collect do |node|
|
20
|
-
(new).tap do |object|
|
21
|
-
|
22
|
-
mappings.each do |mapping|
|
23
|
-
object.send("#{mapping.name}=", mapping.map(node))
|
24
|
-
end
|
25
|
-
|
26
|
-
end
|
27
|
-
end
|
28
|
-
end
|
29
|
-
|
30
|
-
def within(path, &block)
|
31
|
-
yield MappingDecorator.new(path, self)
|
32
|
-
end
|
14
|
+
private_class_method :included
|
33
15
|
|
34
|
-
def map(symbol, options = {})
|
35
|
-
attr_accessor symbol unless new.respond_to?(symbol)
|
36
|
-
|
37
|
-
mappings << Mapping.new(symbol, options)
|
38
|
-
end
|
39
|
-
|
40
|
-
def mappings
|
41
|
-
@mappings ||= []
|
42
|
-
end
|
43
|
-
end
|
44
16
|
end
|
17
|
+
|
18
|
+
require "muffins/version"
|
19
|
+
require "muffins/class_methods"
|
20
|
+
require "muffins/mapping"
|
21
|
+
require "muffins/mapping_parent"
|
22
|
+
require "muffins/document"
|
@@ -0,0 +1,60 @@
|
|
1
|
+
module Muffins
|
2
|
+
module ClassMethods
|
3
|
+
|
4
|
+
# @api public
|
5
|
+
def map(name, type, options = {})
|
6
|
+
options[:name] = name
|
7
|
+
options[:type] = type
|
8
|
+
|
9
|
+
mapping = Mapping.new(options)
|
10
|
+
|
11
|
+
add_mapping(mapping)
|
12
|
+
add_attribute(name, type)
|
13
|
+
|
14
|
+
self
|
15
|
+
end
|
16
|
+
|
17
|
+
# @api public
|
18
|
+
def within(path, &block)
|
19
|
+
Muffins::MappingParent.new(:path => path, :klass => self.class).tap { |parent| yield parent }
|
20
|
+
end
|
21
|
+
|
22
|
+
# @api public
|
23
|
+
def parse(doc, options = {})
|
24
|
+
document = Muffins::Document.new(:body => doc)
|
25
|
+
|
26
|
+
if options[:single]
|
27
|
+
new_from_node(document.first(path))
|
28
|
+
else
|
29
|
+
document.map(path) { |node| new_from_node(node) }
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
# @api private
|
36
|
+
def mappings
|
37
|
+
@mappins ||= []
|
38
|
+
end
|
39
|
+
|
40
|
+
# @api private
|
41
|
+
def add_mapping(mapping)
|
42
|
+
mappings << mapping
|
43
|
+
end
|
44
|
+
|
45
|
+
# @api private
|
46
|
+
def add_attribute(name, type)
|
47
|
+
attribute(name, type)
|
48
|
+
end
|
49
|
+
|
50
|
+
# @api private
|
51
|
+
def new_from_node(node)
|
52
|
+
new.tap do |obj|
|
53
|
+
mappings.each do |mapping|
|
54
|
+
obj.send("#{mapping.name}=", mapping.parse(node))
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module Muffins
|
2
|
+
class Document
|
3
|
+
|
4
|
+
include Virtus
|
5
|
+
|
6
|
+
attribute :body, Class, :reader => :protected
|
7
|
+
|
8
|
+
# @api public
|
9
|
+
def first(path)
|
10
|
+
node = body.at_css(path)
|
11
|
+
node.text if node
|
12
|
+
end
|
13
|
+
|
14
|
+
# @api public
|
15
|
+
def all(path)
|
16
|
+
nodes = body.css(path)
|
17
|
+
nodes.map(&:text)
|
18
|
+
end
|
19
|
+
|
20
|
+
# @api public
|
21
|
+
def map(path, &block)
|
22
|
+
all(path).map { |node| yield node }
|
23
|
+
end
|
24
|
+
|
25
|
+
# @api public
|
26
|
+
def each(path, &block)
|
27
|
+
all(path).each { |node| yield node }
|
28
|
+
end
|
29
|
+
|
30
|
+
protected
|
31
|
+
|
32
|
+
# @api private
|
33
|
+
def body=(body)
|
34
|
+
@body = Nokogiri::HTML(body)
|
35
|
+
end
|
36
|
+
|
37
|
+
end
|
38
|
+
end
|
data/lib/muffins/mapping.rb
CHANGED
@@ -1,79 +1,44 @@
|
|
1
|
-
class Boolean; end;
|
2
|
-
|
3
1
|
module Muffins
|
4
2
|
class Mapping
|
5
3
|
|
6
|
-
|
4
|
+
include Virtus
|
7
5
|
|
8
|
-
|
6
|
+
attribute :name, Symbol
|
7
|
+
attribute :type, Class
|
8
|
+
attribute :to, String
|
9
|
+
attribute :within, String
|
10
|
+
attribute :collection, Boolean
|
9
11
|
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
end
|
12
|
+
# @api public
|
13
|
+
def parse(doc)
|
14
|
+
document = Muffins::Document.new(:body => doc)
|
14
15
|
|
15
|
-
def map(document)
|
16
16
|
if collection?
|
17
|
-
(
|
18
|
-
document.css(path).each do |node|
|
19
|
-
items << typecast(node)
|
20
|
-
end
|
21
|
-
end
|
17
|
+
document.map(absolute_path) { |node| coerce(node) }
|
22
18
|
else
|
23
|
-
|
19
|
+
coerce(document.first(absolute_path))
|
24
20
|
end
|
25
21
|
end
|
26
22
|
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
unless options[:within].blank?
|
31
|
-
path = "#{options[:within]} > #{path}"
|
32
|
-
end
|
33
|
-
|
34
|
-
@path ||= path
|
23
|
+
# @api public
|
24
|
+
def coerce(value)
|
25
|
+
Virtus::Coercion[value.class].send(coercion_method, value)
|
35
26
|
end
|
36
27
|
|
37
|
-
|
38
|
-
|
28
|
+
# @api public
|
29
|
+
def coercion_method
|
30
|
+
Virtus::Attribute.determine_type(type).coercion_method
|
39
31
|
end
|
40
32
|
|
41
|
-
|
42
|
-
|
33
|
+
# @api public
|
34
|
+
def absolute_path
|
35
|
+
within ? [ within, relative_path ].compact.join(" > ") : relative_path
|
43
36
|
end
|
44
37
|
|
45
|
-
|
46
|
-
|
38
|
+
# @api public
|
39
|
+
def relative_path
|
40
|
+
to || name
|
47
41
|
end
|
48
42
|
|
49
|
-
def typecast(node)
|
50
|
-
value = node.try(:text)
|
51
|
-
|
52
|
-
return value if value.blank? || constant.blank?
|
53
|
-
|
54
|
-
if primitive?
|
55
|
-
case constant
|
56
|
-
when 'String'
|
57
|
-
value.to_s
|
58
|
-
when 'Float'
|
59
|
-
value[/[-+]?[0-9]*\.?[0-9]+/].to_f
|
60
|
-
when 'Integer'
|
61
|
-
value[/[\d]+/].to_i
|
62
|
-
when 'Time'
|
63
|
-
Time.parse(value) rescue Time.at(value.to_i)
|
64
|
-
when 'Date'
|
65
|
-
Date.parse(value)
|
66
|
-
when 'DateTime'
|
67
|
-
DateTime.parse(value)
|
68
|
-
when 'Boolean'
|
69
|
-
['true', 't', '1'].include?(value.to_s.downcase)
|
70
|
-
else
|
71
|
-
value
|
72
|
-
end
|
73
|
-
|
74
|
-
else
|
75
|
-
constant.constantize.parse(node)
|
76
|
-
end
|
77
|
-
end
|
78
43
|
end
|
79
|
-
end
|
44
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module Muffins
|
2
|
+
class MappingParent
|
3
|
+
|
4
|
+
include Virtus
|
5
|
+
|
6
|
+
attribute :path, String
|
7
|
+
attribute :klass, Object
|
8
|
+
|
9
|
+
# @api public
|
10
|
+
def map(name, type, options = {})
|
11
|
+
set_options(options).tap do |options|
|
12
|
+
klass.map(name, type, options)
|
13
|
+
end
|
14
|
+
|
15
|
+
self
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
# @api private
|
21
|
+
def set_options(options)
|
22
|
+
options[:within] = [ path, options[:within] ].compact.join(" > ")
|
23
|
+
options
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
27
|
+
end
|
data/lib/muffins/version.rb
CHANGED
data/muffins.gemspec
CHANGED
@@ -6,31 +6,29 @@ Gem::Specification.new do |s|
|
|
6
6
|
s.name = "muffins"
|
7
7
|
s.version = Muffins::VERSION
|
8
8
|
s.authors = ["Ryan Closner"]
|
9
|
-
s.email = ["ryan
|
10
|
-
s.homepage = "
|
11
|
-
s.summary = %q{An Object to XML/HTML mapping library
|
12
|
-
s.description = %q{An Object to XML/HTML mapping library using Nokogiri}
|
9
|
+
s.email = ["ryan@ryanclosner.com"]
|
10
|
+
s.homepage = "http://rubygems.org/gems/muffins"
|
11
|
+
s.summary = %q{An Object to XML/HTML mapping library.}
|
12
|
+
s.description = %q{An Object to XML/HTML mapping library using Virtus and Nokogiri}
|
13
13
|
|
14
14
|
s.rubyforge_project = "muffins"
|
15
15
|
|
16
|
-
{
|
17
|
-
'
|
18
|
-
'
|
19
|
-
}
|
20
|
-
s.add_runtime_dependency lib, version
|
21
|
-
end
|
16
|
+
runtime_dependencies = {
|
17
|
+
'nokogiri' => '~> 1.5.0',
|
18
|
+
'virtus' => '~> 0.0.9'
|
19
|
+
}
|
22
20
|
|
21
|
+
runtime_dependencies.each {|lib, version| s.add_runtime_dependency(lib, version) }
|
23
22
|
|
24
|
-
{
|
25
|
-
'
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
s.add_development_dependency lib, version
|
30
|
-
end
|
23
|
+
development_dependencies = {
|
24
|
+
'rspec' => '~> 2.7.0'
|
25
|
+
}
|
26
|
+
|
27
|
+
development_dependencies.each {|lib, version| s.add_development_dependency(lib, version) }
|
31
28
|
|
32
29
|
s.files = `git ls-files`.split("\n")
|
33
30
|
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
34
31
|
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
35
32
|
s.require_paths = ["lib"]
|
33
|
+
|
36
34
|
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN"
|
2
|
+
"http://www.w3.org/TR/html4/strict.dtd">
|
3
|
+
|
4
|
+
<html lang="en">
|
5
|
+
<head>
|
6
|
+
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
|
7
|
+
</head>
|
8
|
+
<body>
|
9
|
+
<div id="books">
|
10
|
+
|
11
|
+
<div class="book">
|
12
|
+
<span class="isbn">1400079985</span>
|
13
|
+
<span class="title">War and Peace (Vintage Classics)</span>
|
14
|
+
</div>
|
15
|
+
|
16
|
+
<div class="book">
|
17
|
+
<span class="isbn">1143035008</span>
|
18
|
+
<span class="title">Anna Karenina (Oprah's Book Club)</span>
|
19
|
+
</div>
|
20
|
+
|
21
|
+
</div>
|
22
|
+
</body>
|
23
|
+
</html>
|