lazy_resource 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,40 @@
1
+ module LazyResource
2
+ class ResourceQueue
3
+ include LazyResource::UrlGeneration
4
+
5
+ def initialize
6
+ @queue = []
7
+ end
8
+
9
+ def queue(relation)
10
+ @queue.push(relation)
11
+ end
12
+
13
+ def flush!
14
+ @queue = []
15
+ end
16
+
17
+ def request_queue
18
+ Thread.current[:request_queue] ||= Typhoeus::Hydra.new
19
+ end
20
+
21
+ def run
22
+ send_to_request_queue!
23
+ request_queue.run
24
+ end
25
+
26
+ def send_to_request_queue!
27
+ while(relation = @queue.pop)
28
+ request = Request.new(url_for(relation), relation)
29
+ request_queue.queue(request)
30
+ end
31
+ end
32
+
33
+ def url_for(relation)
34
+ url = ''
35
+ url << relation.klass.site
36
+ url << self.class.collection_path(relation.to_params, nil, relation.from)
37
+ url
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,53 @@
1
+ module LazyResource
2
+ module Types
3
+ class Array < ::Array
4
+ def self.parse(o)
5
+ o.to_a
6
+ end
7
+ end
8
+
9
+ class String < ::String
10
+ def self.parse(o)
11
+ o.to_s
12
+ end
13
+ end
14
+
15
+ class Fixnum < ::Fixnum
16
+ def self.parse(o)
17
+ o.to_i
18
+ end
19
+ end
20
+
21
+ class Boolean
22
+ def self.parse(o)
23
+ if [true, '1', 'true'].include? o
24
+ true
25
+ else
26
+ false
27
+ end
28
+ end
29
+ end
30
+
31
+ class Float < ::Float
32
+ def self.parse(o)
33
+ o.to_f
34
+ end
35
+ end
36
+
37
+ class Hash < ::Hash
38
+ def self.parse(o)
39
+ o
40
+ end
41
+ end
42
+
43
+ class DateTime < ::DateTime
44
+ def self.parse(o)
45
+ if o.is_a?(::DateTime)
46
+ o
47
+ else
48
+ super(o)
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,98 @@
1
+ module LazyResource
2
+ module UrlGeneration
3
+ extend ActiveSupport::Concern
4
+
5
+ def element_path(options = nil)
6
+ self.class.element_path(self.primary_key, options)
7
+ end
8
+
9
+ def element_url(options = nil)
10
+ url = self.class.site.to_s.gsub(/\/$/, '')
11
+ url << self.element_path(options)
12
+ end
13
+
14
+ def new_element_path
15
+ self.class.new_element_path
16
+ end
17
+
18
+ def collection_path(options = nil)
19
+ self.class.collection_path(options)
20
+ end
21
+
22
+ def collection_url(options = nil)
23
+ url = self.class.site.to_s.gsub(/\/$/, '')
24
+ url << self.collection_path(options)
25
+ end
26
+
27
+ def split_options(options = {})
28
+ self.class.split_options(options)
29
+ end
30
+
31
+ module ClassMethods
32
+ # Gets the \prefix for a resource's nested URL (e.g., <tt>prefix/collectionname/1</tt>)
33
+ def prefix(options={})
34
+ path = '/'
35
+ options = options.to_a.uniq
36
+ path = options.inject(path) do |uri, option|
37
+ key, value = option[0].to_s, option[1]
38
+ uri << ActiveSupport::Inflector.pluralize(key.gsub("_id", ''))
39
+ uri << "/#{value}/"
40
+ end
41
+ end
42
+
43
+
44
+ # Gets the element path for the given ID in +id+. If the +query_options+ parameter is omitted, Rails
45
+ # will split from the \prefix options.
46
+ #
47
+ # ==== Options
48
+ # +prefix_options+ - A \hash to add a \prefix to the request for nested URLs (e.g., <tt>:account_id => 19</tt>
49
+ # would yield a URL like <tt>/accounts/19/purchases.json</tt>).
50
+ #
51
+ # +query_options+ - A \hash to add items to the query string for the request.
52
+ def element_path(id, prefix_options = {}, query_options = nil, from = nil)
53
+ prefix_options, query_options = split_options(prefix_options) if query_options.nil?
54
+ "#{prefix(prefix_options)}#{from || collection_name}/#{URI.escape id.to_s}#{query_string(query_options)}"
55
+ end
56
+
57
+ # Gets the new element path for REST resources.
58
+ #
59
+ # ==== Options
60
+ # * +prefix_options+ - A hash to add a prefix to the request for nested URLs (e.g., <tt>:account_id => 19</tt>
61
+ # would yield a URL like <tt>/accounts/19/purchases/new.json</tt>).
62
+ def new_element_path(prefix_options = {}, from = nil)
63
+ "#{prefix(prefix_options)}#{from || collection_name}/new"
64
+ end
65
+
66
+ # Gets the collection path for the REST resources. If the +query_options+ parameter is omitted, Rails
67
+ # will split from the +prefix_options+.
68
+ #
69
+ # ==== Options
70
+ # * +prefix_options+ - A hash to add a prefix to the request for nested URLs (e.g., <tt>:account_id => 19</tt>
71
+ # would yield a URL like <tt>/accounts/19/purchases.json</tt>).
72
+ # * +query_options+ - A hash to add items to the query string for the request.
73
+ def collection_path(prefix_options = {}, query_options = nil, from = nil)
74
+ prefix_options, query_options = split_options(prefix_options) if query_options.nil?
75
+ "#{prefix(prefix_options)}#{from || collection_name}#{query_string(query_options)}"
76
+ end
77
+
78
+ # Builds the query string for the request.
79
+ def query_string(options)
80
+ "?#{options.to_query}" unless options.nil? || options.empty?
81
+ end
82
+
83
+ # split an option hash into two hashes, one containing the prefix options,
84
+ # and the other containing the leftovers.
85
+ def split_options(options = {})
86
+ prefix_options, query_options = {}, {}
87
+
88
+ (options || {}).each do |key, value|
89
+ next if key.blank?
90
+ (key =~ /\w*_id/ ? prefix_options : query_options)[key.to_sym] = value
91
+ end
92
+
93
+ [prefix_options, query_options]
94
+ end
95
+ end
96
+ end
97
+ end
98
+
@@ -0,0 +1,3 @@
1
+ module LazyResource
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,6 @@
1
+ class Comment
2
+ include LazyResource::Resource
3
+
4
+ attribute :id, Fixnum
5
+ attribute :body, String
6
+ end
@@ -0,0 +1,6 @@
1
+ class Post
2
+ include LazyResource::Resource
3
+
4
+ attribute :id, Fixnum
5
+ attribute :title, String
6
+ end
@@ -0,0 +1,9 @@
1
+ class User
2
+ include LazyResource::Resource
3
+
4
+ attribute :id, Fixnum
5
+ attribute :name, String
6
+ attribute :created_at, DateTime
7
+ attribute :post, Post
8
+ attribute :comments, [Comment]
9
+ end
@@ -0,0 +1,157 @@
1
+ require 'spec_helper'
2
+
3
+ class AttributeObject
4
+ include LazyResource::Attributes
5
+
6
+ attr_accessor :fetched
7
+
8
+ def self.resource_queue
9
+ @resource_queue ||= LazyResource::ResourceQueue.new
10
+ end
11
+
12
+ def self.request_queue
13
+ @request_queue ||= Typhoeus::Hydra.new
14
+ end
15
+
16
+ def fetched?
17
+ @fetched
18
+ end
19
+
20
+ def element_name
21
+ "attribute_object"
22
+ end
23
+
24
+ def id
25
+ 1
26
+ end
27
+ end
28
+
29
+ describe LazyResource::Attributes do
30
+ before :each do
31
+ AttributeObject.attribute(:name, String)
32
+ @foo = AttributeObject.new
33
+ end
34
+
35
+ describe '.attribute' do
36
+ it 'adds the attribute to the attributes hash' do
37
+ AttributeObject.attributes[:name].should == { :type => String, :options => {} }
38
+ end
39
+
40
+ it 'creates a getter method' do
41
+ @foo.respond_to?(:name).should == true
42
+ end
43
+
44
+ it 'creates a setter method' do
45
+ @foo.respond_to?(:name=).should == true
46
+ end
47
+
48
+ it 'creates a question method' do
49
+ @foo.respond_to?(:name?).should == true
50
+ end
51
+
52
+ describe 'getter' do
53
+ it 'runs fetch_all if the current object is not fetched' do
54
+ @foo.fetched = false
55
+ AttributeObject.should_receive(:fetch_all)
56
+ @foo.name
57
+ end
58
+
59
+ it 'does not run fetch_all if the current object is fetched' do
60
+ @foo.fetched = true
61
+ AttributeObject.should_not_receive(:fetch_all)
62
+ @foo.name
63
+ end
64
+
65
+ describe 'associations' do
66
+ before :each do
67
+ AttributeObject.attribute(:posts, [Post])
68
+ AttributeObject.attribute(:user, User)
69
+ end
70
+
71
+ it 'returns a relation with the specified where values' do
72
+ Post.should_receive(:where).with(:attribute_object_id => 1)
73
+ @foo.fetched = false
74
+ @foo.posts
75
+ end
76
+
77
+ it 'finds a singular resource' do
78
+ User.should_receive(:where).with(:attribute_object_id => 1)
79
+ @foo.fetched = false
80
+ @foo.user
81
+ end
82
+
83
+ describe ':using' do
84
+ before :each do
85
+ AttributeObject.attribute(:posts_url, String)
86
+ AttributeObject.attribute(:user_url, String)
87
+ AttributeObject.attribute(:posts, [Post], :using => :posts_url)
88
+ AttributeObject.attribute(:user, User, :using => :user_url)
89
+ @foo.send(:instance_variable_set, "@posts_url", 'http://example.com/path/to/posts')
90
+ @foo.send(:instance_variable_set, "@user_url", 'http://example.com/path/to/user')
91
+ end
92
+
93
+ it 'finds a collection using the specified url' do
94
+ relation = LazyResource::Relation.new(Post)
95
+ request = LazyResource::Request.new(@foo.posts_url, relation)
96
+ LazyResource::Request.should_receive(:new).with(@foo.posts_url, relation).and_return(request)
97
+ LazyResource::Relation.should_receive(:new).with(Post, :fetched => true).and_return(relation)
98
+ @foo.class.request_queue.should_receive(:queue).with(request)
99
+ @foo.fetched = false
100
+ @foo.posts
101
+ end
102
+
103
+ it 'finds a singular resource with the specified url' do
104
+ resource = User.load({})
105
+ request = LazyResource::Request.new(@foo.user_url, resource)
106
+ LazyResource::Request.should_receive(:new).with(@foo.user_url, resource).and_return(request)
107
+ @foo.class.request_queue.should_receive(:queue).with(request)
108
+ @foo.fetched = false
109
+ @foo.user
110
+ end
111
+ end
112
+ end
113
+ end
114
+ end
115
+
116
+ describe '.primary_key_name' do
117
+ after :all do
118
+ AttributeObject.primary_key_name = 'id'
119
+ end
120
+
121
+ it 'defaults to id' do
122
+ AttributeObject.primary_key_name.should == 'id'
123
+ end
124
+
125
+ it 'returns the primary_key_name instance variable' do
126
+ AttributeObject.primary_key_name.should == AttributeObject.instance_variable_get("@primary_key_name")
127
+ end
128
+ end
129
+
130
+ describe '.primary_key_name=' do
131
+ after :all do
132
+ AttributeObject.primary_key_name = 'id'
133
+ end
134
+
135
+ it 'sets the primary_key_name' do
136
+ AttributeObject.primary_key_name = 'name'
137
+ AttributeObject.instance_variable_get("@primary_key_name").should == 'name'
138
+ end
139
+ end
140
+
141
+ describe '.attributes' do
142
+ it 'returns a hash of the defined attributes' do
143
+ AttributeObject.attributes.should == { :name => { :type => String, :options => {} },
144
+ :posts => { :type => [Post], :options => { :using => :posts_url } },
145
+ :user => { :type => User, :options => { :using => :user_url } },
146
+ :posts_url => { :type => String, :options => {} },
147
+ :user_url => { :type => String, :options => {} } }
148
+ end
149
+ end
150
+
151
+ describe '#primary_key' do
152
+ it 'returns the value at the primary_key_name' do
153
+ obj = AttributeObject.new
154
+ obj.primary_key.should == obj.send(AttributeObject.primary_key_name)
155
+ end
156
+ end
157
+ end
@@ -0,0 +1,48 @@
1
+ require 'spec_helper'
2
+
3
+ describe LazyResource::ConnectionError do
4
+ describe '#to_s' do
5
+ it 'prints a message with the response code and message' do
6
+ error = LazyResource::ConnectionError.new(Typhoeus::Response.new(:code => 300, :body => 'redirect'))
7
+ error.to_s.should match(/300/)
8
+ error.to_s.should match(/redirect/)
9
+ end
10
+ end
11
+ end
12
+
13
+ describe LazyResource::Redirection do
14
+ describe '#to_s' do
15
+ it 'prints the response\'s redirection location' do
16
+ error = LazyResource::Redirection.new(Typhoeus::Response.new(:code => 300, :body => 'redirect', :headers => { :Location => 'http://example.com' }))
17
+ error.to_s.should match(/example\.com/)
18
+ end
19
+ end
20
+ end
21
+
22
+ describe LazyResource::MethodNotAllowed do
23
+ describe '#allowed_methods' do
24
+ it 'prints the allowed methods' do
25
+ error = LazyResource::MethodNotAllowed.new(Typhoeus::Response.new(:code => 405, :headers => { 'Allow' => 'put' }))
26
+ error.allowed_methods.should == [:put]
27
+ end
28
+ end
29
+ end
30
+
31
+ describe LazyResource::TimeoutError do
32
+ describe '#new' do
33
+ it 'only accepts a message' do
34
+ error = LazyResource::TimeoutError.new('timed out')
35
+ error.to_s.should == 'timed out'
36
+ end
37
+ end
38
+ end
39
+
40
+ describe LazyResource::SSLError do
41
+ describe '#new' do
42
+ it 'only accepts a message' do
43
+ error = LazyResource::SSLError.new('timed out')
44
+ error.to_s.should == 'timed out'
45
+ end
46
+ end
47
+ end
48
+
@@ -0,0 +1,237 @@
1
+ require 'spec_helper'
2
+
3
+ class Foo
4
+ include LazyResource::Mapping
5
+ include LazyResource::Types
6
+
7
+ attr_accessor :id, :name, :created_at, :post, :comments, :comments_text
8
+
9
+ def self.attributes
10
+ @attributes ||= {
11
+ :id => { :type => Fixnum },
12
+ :name => { :type => String },
13
+ :created_at => { :type => DateTime },
14
+ :post => { :type => Bar },
15
+ :comments => { :type => [Buzz] },
16
+ :comments_text => { :type => [String] }
17
+ }
18
+ end
19
+ end
20
+
21
+ class Bar
22
+ include LazyResource::Mapping
23
+ include LazyResource::Types
24
+
25
+ attr_accessor :title
26
+
27
+ def self.attributes
28
+ @attributes ||= {
29
+ :title => { :type => String }
30
+ }
31
+ end
32
+ end
33
+
34
+ class Buzz
35
+ include LazyResource::Mapping
36
+ include LazyResource::Types
37
+
38
+ attr_accessor :body
39
+
40
+ def self.attributes
41
+ @attributes ||= {
42
+ :body => { :type => String }
43
+ }
44
+ end
45
+ end
46
+
47
+ describe LazyResource::Mapping do
48
+ describe '.load' do
49
+ before :each do
50
+ @now = DateTime.now
51
+ @now_as_sting = @now.to_s
52
+ @post = Bar.new
53
+ @post.title = "Lorem Ipsum"
54
+ @comments = []
55
+ 4.times do
56
+ comment = Buzz.new
57
+ comment.body = "Lorem Ipsum"
58
+ @comments << comment
59
+ end
60
+ end
61
+
62
+ it 'loads single objects' do
63
+ user = Foo.load({
64
+ :id => 123,
65
+ :name => 'Andrew',
66
+ :created_at => @now,
67
+ :post => @post,
68
+ :comments => @comments
69
+ })
70
+
71
+ user.id.should == 123
72
+ user.name.should == 'Andrew'
73
+ user.created_at.should == @now
74
+ user.post.should == @post
75
+ user.comments.should == @comments
76
+ end
77
+
78
+ it 'loads an array of objects' do
79
+ users = Foo.load([
80
+ {
81
+ :id => 123
82
+ },
83
+ {
84
+ :id => 123
85
+ }
86
+ ])
87
+
88
+ users.each do |user|
89
+ user.id.should == 123
90
+ end
91
+ end
92
+ end
93
+
94
+ describe '.root_node_name' do
95
+ after :each do
96
+ LazyResource::Mapping.root_node_name = nil
97
+ Foo.root_node_name = nil
98
+ end
99
+
100
+ it 'defaults to nil' do
101
+ Foo.root_node_name.should == nil
102
+ end
103
+
104
+ it 'specifies the root node name' do
105
+ Foo.root_node_name = :data
106
+ Foo.root_node_name.should == :data
107
+ end
108
+
109
+ it 'maps the object at that value' do
110
+ Foo.root_node_name = :data
111
+ user = Foo.load({ 'data' => { :id => 123 } })
112
+ user.id.should == 123
113
+ end
114
+
115
+ it 'maps objects without root node names, even if a root node name is defined' do
116
+ Foo.root_node_name = :data
117
+ user = Foo.load({ :id => 123 })
118
+ user.id.should == 123
119
+ end
120
+
121
+ it 'maps collections at the root node name' do
122
+ Foo.root_node_name = :data
123
+ users = Foo.load({ 'data' => [{ :id => 123 }, { :id => 124 }]})
124
+ users.map(&:id).should == [123,124]
125
+ end
126
+
127
+ it 'maps collections without root node names, even if a root node name is defined' do
128
+ Foo.root_node_name = :data
129
+ users = Foo.load([{ :id => 123 }, { :id => 124 }])
130
+ users.map(&:id).should == [123,124]
131
+ end
132
+
133
+ it 'looks in the module for the root node name' do
134
+ LazyResource::Mapping.root_node_name = :data
135
+ Foo.root_node_name.should == :data
136
+ end
137
+
138
+ it 'handles root node names that are strings or symbols' do
139
+ Foo.root_node_name = :data
140
+ user = Foo.load('data' => { :id => 123 })
141
+ user.id.should == 123
142
+ Foo.root_node_name = 'data'
143
+ user = Foo.load('data' => { :id => 123 })
144
+ user.id.should == 123
145
+ end
146
+ end
147
+
148
+ describe '#load' do
149
+ before :each do
150
+ @now = DateTime.now
151
+ @now_as_string = @now.to_s
152
+ @post = Bar.new
153
+ @post.title = "Lorem Ipsum"
154
+ @comments = []
155
+ 4.times do
156
+ comment = Buzz.new
157
+ comment.body = "Lorem Ipsum"
158
+ @comments << comment
159
+ end
160
+ end
161
+
162
+ it 'loads attributes' do
163
+ user = Foo.new
164
+ user.load({ :name => 'Bob' })
165
+ user.name.should == 'Bob'
166
+ end
167
+
168
+ it 'overwrites existing attributes' do
169
+ user = Foo.new
170
+ user.name = 'Andrew'
171
+ user.load({ :name => 'Bob' })
172
+ user.name.should == 'Bob'
173
+ end
174
+
175
+ it 'loads objects based on their type in the attributes method' do
176
+ user = Foo.new
177
+ user.load({
178
+ :id => "123",
179
+ :name => "Andrew",
180
+ :created_at => @now_as_string
181
+ })
182
+
183
+ user.id.should == 123
184
+ user.name.should == 'Andrew'
185
+ user.created_at.to_s.should == @now_as_string
186
+ end
187
+
188
+ it 'loads associations' do
189
+ user = Foo.new
190
+ user.load({
191
+ :post => {
192
+ :title => 'Lorem Ipsum'
193
+ }
194
+ })
195
+
196
+ user.post.title.should == 'Lorem Ipsum'
197
+ end
198
+
199
+ it 'loads association arrays' do
200
+ user = Foo.new
201
+ user.load({
202
+ :comments => [
203
+ { :body => 'Lorem Ipsum' },
204
+ { :body => 'Lorem Ipsum' }
205
+ ]
206
+ })
207
+
208
+ user.comments.each do |comment|
209
+ comment.body.should == 'Lorem Ipsum'
210
+ end
211
+ end
212
+
213
+ it 'loads arrays' do
214
+ user = Foo.new
215
+ user.load({
216
+ :comments_text => [
217
+ 'Lorem Ipsum',
218
+ 'Lorem Ipsum'
219
+ ]
220
+ })
221
+
222
+ user.comments_text.each do |comment|
223
+ comment.should == 'Lorem Ipsum'
224
+ end
225
+ end
226
+
227
+ it 'skips unknown attributes' do
228
+ user = Foo.new
229
+ user.load({
230
+ :fizz => '',
231
+ :name => 'Andrew'
232
+ })
233
+
234
+ user.name.should == 'Andrew'
235
+ end
236
+ end
237
+ end