craft 0.0.2 → 0.1.0
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/.travis.yml +5 -0
- data/CHANGES.md +8 -0
- data/Gemfile +9 -0
- data/README.md +6 -1
- data/lib/craft.rb +57 -21
- data/lib/craft/version.rb +1 -1
- data/spec/craft_spec.rb +93 -20
- metadata +6 -4
data/.travis.yml
ADDED
data/CHANGES.md
ADDED
@@ -0,0 +1,8 @@
|
|
1
|
+
v0.1.0
|
2
|
+
-----------
|
3
|
+
|
4
|
+
- When nesting, make parent accessible to child.
|
5
|
+
- Craft#attributes returns the attributes.
|
6
|
+
- Craft.stub stubs a static or dynamic value that doesn't need to be parsed in
|
7
|
+
the document.
|
8
|
+
- Transforms are executed in the context of the crafted object.
|
data/Gemfile
CHANGED
data/README.md
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
# Craft
|
1
|
+
# Craft [](http://travis-ci.org/papercavalier/craft)
|
2
2
|
|
3
3
|
Craft XML and HTML into objects.
|
4
4
|
|
@@ -16,6 +16,9 @@ class Page < Craft
|
|
16
16
|
|
17
17
|
# Perform transforms on returned nodes
|
18
18
|
many :images, 'img', lambda { |img| img.attr('src').upcase }
|
19
|
+
|
20
|
+
# Stub attributes that don't need to be parsed
|
21
|
+
stub :spidered_at, lambda { Time.now }
|
19
22
|
end
|
20
23
|
|
21
24
|
page = Page.parse open('http://www.google.com')
|
@@ -24,6 +27,8 @@ page.title #=> 'Google'
|
|
24
27
|
page.links #=> ['http://www.google.com/imghp?hl=en&tab=wi', ...]
|
25
28
|
page.images #=> ['/LOGOS/2012/MOBY_DICK12-HP.JPG']
|
26
29
|
|
30
|
+
page.attributes #=> { :title => 'Google', :links => ... }
|
31
|
+
|
27
32
|
class Script < Craft
|
28
33
|
one :body, 'text()'
|
29
34
|
end
|
data/lib/craft.rb
CHANGED
@@ -7,6 +7,7 @@ require 'nokogiri'
|
|
7
7
|
#
|
8
8
|
# module Transformations
|
9
9
|
# IntegerTransform = lambda { |n| Integer n.text }
|
10
|
+
# Timestamp = lambda { Time.now }
|
10
11
|
# end
|
11
12
|
#
|
12
13
|
# class Person < Craft
|
@@ -15,46 +16,48 @@ require 'nokogiri'
|
|
15
16
|
# one :name, 'div.name'
|
16
17
|
# one :age, 'div.age', IntegerTransform
|
17
18
|
# many :friends, 'li.friend', Person
|
19
|
+
# stub :created_at, Timestamp
|
18
20
|
# end
|
19
21
|
#
|
20
22
|
class Craft
|
21
23
|
class << self
|
22
|
-
#
|
23
|
-
|
24
|
-
alias call new
|
24
|
+
# Returns an Array of names for the attributes defined in the class.
|
25
|
+
attr :attribute_names
|
25
26
|
|
26
|
-
# Define
|
27
|
+
# Define an attribute that extracts a collection of values from a parsed
|
27
28
|
# document.
|
28
29
|
#
|
29
|
-
# name - The Symbol name of the
|
30
|
+
# name - The Symbol name of the attribute.
|
30
31
|
# paths - One or more String XPath of CSS queries. An optional Proc
|
31
32
|
# transformation on the extracted value may be appended. If none is
|
32
33
|
# appended, the default transformation returns the stripped String
|
33
34
|
# value of the node.
|
34
35
|
#
|
35
|
-
# Returns
|
36
|
+
# Returns nothing.
|
36
37
|
def many(name, *paths)
|
37
|
-
transform =
|
38
|
+
transform = pop_transform_from_paths paths
|
39
|
+
@attribute_names << name
|
38
40
|
|
39
41
|
define_method name do
|
40
|
-
@node.search(*paths).map { |node|
|
42
|
+
@node.search(*paths).map { |node| instance_exec node, &transform }
|
41
43
|
end
|
42
44
|
end
|
43
45
|
|
44
|
-
# Define
|
46
|
+
# Define an attribute that extracts a single value from a parsed document.
|
45
47
|
#
|
46
|
-
# name - The Symbol name of the
|
48
|
+
# name - The Symbol name of the attribute.
|
47
49
|
# paths - One or more String XPath of CSS queries. An optional Proc
|
48
50
|
# transformation on the extracted value may be appended. If none is
|
49
51
|
# appended, the default transformation returns the stripped String
|
50
52
|
# value of the node.
|
51
53
|
#
|
52
|
-
# Returns
|
54
|
+
# Returns nothing.
|
53
55
|
def one(name, *paths)
|
54
|
-
transform =
|
56
|
+
transform = pop_transform_from_paths paths
|
57
|
+
@attribute_names << name
|
55
58
|
|
56
59
|
define_method name do
|
57
|
-
|
60
|
+
instance_exec @node.at(*paths), &transform
|
58
61
|
end
|
59
62
|
end
|
60
63
|
|
@@ -67,25 +70,58 @@ class Craft
|
|
67
70
|
new Nokogiri body
|
68
71
|
end
|
69
72
|
|
73
|
+
# Define an attribute that returns a value without parsing the document.
|
74
|
+
#
|
75
|
+
# name - The Symbol name of the attribute.
|
76
|
+
# value - Some value the attribute should return. If given a Proc, the
|
77
|
+
# value will be generated dynamically (default: nil).
|
78
|
+
#
|
79
|
+
# Returns nothing.
|
80
|
+
def stub(name, value = nil)
|
81
|
+
@attribute_names << name
|
82
|
+
|
83
|
+
define_method name do
|
84
|
+
value.respond_to?(:call) ? instance_exec(&value) : value
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def to_proc
|
89
|
+
klass = self
|
90
|
+
->(node) { klass.new node, self }
|
91
|
+
end
|
92
|
+
|
70
93
|
private
|
71
94
|
|
72
|
-
def
|
73
|
-
|
95
|
+
def inherited(subclass)
|
96
|
+
subclass.instance_variable_set :@attribute_names, []
|
97
|
+
end
|
98
|
+
|
99
|
+
def pop_transform_from_paths(array)
|
100
|
+
if array.last.respond_to? :to_proc
|
74
101
|
array.pop
|
75
102
|
else
|
76
|
-
|
77
|
-
def self.call(node)
|
78
|
-
node.text.strip if node
|
79
|
-
end
|
80
|
-
end
|
103
|
+
->(node) { node.text.strip if node }
|
81
104
|
end
|
82
105
|
end
|
83
106
|
end
|
84
107
|
|
108
|
+
attr :parent
|
109
|
+
|
85
110
|
# Craft a new object.
|
86
111
|
#
|
87
112
|
# node - A Nokogiri::XML::Node.
|
88
|
-
def initialize(node)
|
113
|
+
def initialize(node, parent = nil)
|
89
114
|
@node = node
|
115
|
+
@parent = parent
|
116
|
+
end
|
117
|
+
|
118
|
+
# Returns the Hash attributes.
|
119
|
+
def attributes
|
120
|
+
Hash[attribute_names.map { |key| [key, self.send(key)] }]
|
121
|
+
end
|
122
|
+
|
123
|
+
# Returns an Array of names for the attributes on this object.
|
124
|
+
def attribute_names
|
125
|
+
self.class.attribute_names
|
90
126
|
end
|
91
127
|
end
|
data/lib/craft/version.rb
CHANGED
data/spec/craft_spec.rb
CHANGED
@@ -3,45 +3,89 @@ require 'minitest/autorun'
|
|
3
3
|
require 'craft'
|
4
4
|
|
5
5
|
describe Craft do
|
6
|
-
let
|
7
|
-
|
8
|
-
|
6
|
+
let(:html) { '<html><ul><li>1</li><li>2</li>' }
|
7
|
+
let(:klass) { Class.new Craft }
|
8
|
+
let(:instance) { klass.parse html }
|
9
9
|
|
10
|
-
|
11
|
-
|
12
|
-
|
10
|
+
describe '.attribute_names' do
|
11
|
+
it 'is empty by default' do
|
12
|
+
klass.attribute_names.must_equal []
|
13
|
+
end
|
13
14
|
|
14
|
-
|
15
|
-
|
15
|
+
it 'does not reference other attribute names' do
|
16
|
+
klass.stub :foo
|
17
|
+
other = Class.new(Craft) { stub :bar }
|
18
|
+
klass.attribute_names.wont_equal other.attribute_names
|
19
|
+
end
|
16
20
|
end
|
17
21
|
|
18
22
|
describe '.many' do
|
19
23
|
it 'extracts nodes' do
|
20
|
-
klass.many
|
24
|
+
klass.many :foo, 'li'
|
21
25
|
instance.foo.must_equal %w(1 2)
|
22
26
|
end
|
23
27
|
|
24
28
|
it 'transforms' do
|
25
|
-
klass.many
|
29
|
+
klass.many :foo, 'li', ->(node) { node.text.to_i }
|
26
30
|
instance.foo.must_equal [1, 2]
|
27
31
|
end
|
32
|
+
|
33
|
+
it 'transforms in scope' do
|
34
|
+
klass.many :foo, 'li', ->(node) { bar }
|
35
|
+
klass.send(:define_method, :bar) { 'bar' }
|
36
|
+
instance.foo.must_equal ['bar', 'bar']
|
37
|
+
end
|
38
|
+
|
39
|
+
it 'stores attribute name' do
|
40
|
+
klass.many :foo, 'li'
|
41
|
+
klass.attribute_names.must_include :foo
|
42
|
+
end
|
43
|
+
|
44
|
+
it 'nests' do
|
45
|
+
klass.many :foo, 'ul', Class.new(Craft)
|
46
|
+
instance.foo.each { |attr| attr.must_be_kind_of Craft }
|
47
|
+
end
|
48
|
+
|
49
|
+
it 'has a parent when nested' do
|
50
|
+
klass.many :foo, 'li', Class.new(Craft)
|
51
|
+
instance.foo.each { |attr| attr.parent.must_equal instance }
|
52
|
+
end
|
28
53
|
end
|
29
54
|
|
30
55
|
describe '.one' do
|
31
56
|
it 'extracts a node' do
|
32
|
-
klass.one
|
57
|
+
klass.one :foo, 'li'
|
33
58
|
instance.foo.must_equal '1'
|
34
59
|
end
|
35
60
|
|
36
61
|
it 'transforms' do
|
37
|
-
klass.one
|
62
|
+
klass.one :foo, 'li', ->(node) { node.text.to_i }
|
38
63
|
instance.foo.must_equal 1
|
39
64
|
end
|
40
65
|
|
66
|
+
it 'transforms in scope' do
|
67
|
+
klass.one :foo, 'li', ->(node) { bar }
|
68
|
+
klass.send(:define_method, :bar) { 'bar' }
|
69
|
+
instance.foo.must_equal 'bar'
|
70
|
+
end
|
71
|
+
|
72
|
+
it 'stores attribute name' do
|
73
|
+
klass.one :foo, 'li'
|
74
|
+
klass.attribute_names.must_include :foo
|
75
|
+
end
|
76
|
+
|
77
|
+
it 'nests' do
|
78
|
+
klass.one :foo, 'ul', Class.new(Craft)
|
79
|
+
instance.foo.must_be_kind_of Craft
|
80
|
+
end
|
81
|
+
|
82
|
+
it 'has a parent when nested' do
|
83
|
+
klass.one :foo, 'li', Class.new(Craft)
|
84
|
+
instance.foo.parent.must_equal instance
|
85
|
+
end
|
86
|
+
|
41
87
|
describe 'given no matches' do
|
42
|
-
before
|
43
|
-
klass.one 'foo', 'foo'
|
44
|
-
end
|
88
|
+
before { klass.one :foo, 'bar' }
|
45
89
|
|
46
90
|
it 'returns nil' do
|
47
91
|
instance.foo.must_be_nil
|
@@ -49,10 +93,39 @@ describe Craft do
|
|
49
93
|
end
|
50
94
|
end
|
51
95
|
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
96
|
+
describe '.stub' do
|
97
|
+
it 'returns nil by default' do
|
98
|
+
klass.stub :foo
|
99
|
+
instance.foo.must_be_nil
|
100
|
+
end
|
101
|
+
|
102
|
+
it 'returns a static value' do
|
103
|
+
klass.stub :foo, 1
|
104
|
+
instance.foo.must_equal 1
|
105
|
+
end
|
106
|
+
|
107
|
+
it 'returns a dynamic value' do
|
108
|
+
klass.stub :foo, -> { Time.now }
|
109
|
+
instance.foo.must_be_instance_of Time
|
110
|
+
end
|
111
|
+
|
112
|
+
it 'transforms in scope' do
|
113
|
+
klass.stub :foo, -> { bar }
|
114
|
+
klass.send(:define_method, :bar) { 'bar' }
|
115
|
+
instance.foo.must_equal 'bar'
|
116
|
+
end
|
117
|
+
|
118
|
+
it 'stores attribute name' do
|
119
|
+
klass.stub :foo
|
120
|
+
klass.attribute_names.must_include :foo
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
describe '#attributes' do
|
125
|
+
it 'returns attributes' do
|
126
|
+
klass.stub :foo
|
127
|
+
klass.one :bar, 'li'
|
128
|
+
instance.attributes.must_equal({ foo: nil, bar: '1' })
|
129
|
+
end
|
57
130
|
end
|
58
131
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: craft
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0
|
4
|
+
version: 0.1.0
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -10,7 +10,7 @@ authors:
|
|
10
10
|
autorequire:
|
11
11
|
bindir: bin
|
12
12
|
cert_chain: []
|
13
|
-
date: 2012-10-
|
13
|
+
date: 2012-10-25 00:00:00.000000000 Z
|
14
14
|
dependencies:
|
15
15
|
- !ruby/object:Gem::Dependency
|
16
16
|
name: nokogiri
|
@@ -36,6 +36,8 @@ extensions: []
|
|
36
36
|
extra_rdoc_files: []
|
37
37
|
files:
|
38
38
|
- .gitignore
|
39
|
+
- .travis.yml
|
40
|
+
- CHANGES.md
|
39
41
|
- Gemfile
|
40
42
|
- LICENSE.txt
|
41
43
|
- README.md
|
@@ -58,7 +60,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
58
60
|
version: '0'
|
59
61
|
segments:
|
60
62
|
- 0
|
61
|
-
hash:
|
63
|
+
hash: 2821744877133646519
|
62
64
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
63
65
|
none: false
|
64
66
|
requirements:
|
@@ -67,7 +69,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
67
69
|
version: '0'
|
68
70
|
segments:
|
69
71
|
- 0
|
70
|
-
hash:
|
72
|
+
hash: 2821744877133646519
|
71
73
|
requirements: []
|
72
74
|
rubyforge_project:
|
73
75
|
rubygems_version: 1.8.23
|