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.
@@ -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