lazy_resource 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,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