craft 0.0.2 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,5 @@
1
+ language: ruby
2
+ rvm:
3
+ - 1.9.3
4
+ - rbx-19mode
5
+ - jruby-19mode
@@ -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
@@ -2,3 +2,12 @@ source 'https://rubygems.org'
2
2
 
3
3
  # Specify your gem's dependencies in craft.gemspec
4
4
  gemspec
5
+
6
+ group :ci do
7
+ gem 'rake'
8
+ end
9
+
10
+ platforms :jruby do
11
+ gem 'minitest'
12
+ end
13
+ gem 'pry'
data/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # Craft
1
+ # Craft [![Build Status](https://secure.travis-ci.org/papercavalier/craft.png)](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
@@ -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
- # We alias call to new so that crafted objects may nest themselves or other
23
- # crafted objects as transformations.
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 a method that extracts a collection of values from a parsed
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 method.
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 an Array.
36
+ # Returns nothing.
36
37
  def many(name, *paths)
37
- transform = pop_transformation paths
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| transform.call node }
42
+ @node.search(*paths).map { |node| instance_exec node, &transform }
41
43
  end
42
44
  end
43
45
 
44
- # Define a method that extracts a single value from a parsed document.
46
+ # Define an attribute that extracts a single value from a parsed document.
45
47
  #
46
- # name - The Symbol name of the method.
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 an Object.
54
+ # Returns nothing.
53
55
  def one(name, *paths)
54
- transform = pop_transformation paths
56
+ transform = pop_transform_from_paths paths
57
+ @attribute_names << name
55
58
 
56
59
  define_method name do
57
- transform.call @node.at(*paths)
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 pop_transformation(array)
73
- if array.last.respond_to? :call
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
- Module.new do
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
@@ -1,3 +1,3 @@
1
1
  class Craft
2
- VERSION = '0.0.2'
2
+ VERSION = '0.1.0'
3
3
  end
@@ -3,45 +3,89 @@ require 'minitest/autorun'
3
3
  require 'craft'
4
4
 
5
5
  describe Craft do
6
- let :html do
7
- '<html><ul><li>1</li><li>2</li>'
8
- end
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
- let :klass do
11
- Class.new Craft
12
- end
10
+ describe '.attribute_names' do
11
+ it 'is empty by default' do
12
+ klass.attribute_names.must_equal []
13
+ end
13
14
 
14
- let :instance do
15
- klass.parse html
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 'foo', 'li'
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 'foo', 'li', ->(node) { node.text.to_i }
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 'foo', 'li'
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 'foo', 'li', ->(node) { node.text.to_i }
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 do
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
- it 'nests' do
53
- nest = Class.new Craft
54
- nest.many 'foo', 'li'
55
- klass.one 'foo', 'ul', nest
56
- instance.foo.foo.must_equal %w(1 2)
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.2
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-18 00:00:00.000000000 Z
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: -3848600058118697198
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: -3848600058118697198
72
+ hash: 2821744877133646519
71
73
  requirements: []
72
74
  rubyforge_project:
73
75
  rubygems_version: 1.8.23