lazy_resource 0.4.0 → 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +15 -0
- data/.rspec +0 -1
- data/.rvmrc +1 -1
- data/.travis.yml +6 -0
- data/Gemfile +1 -1
- data/NOTES.md +36 -0
- data/README.md +115 -60
- data/examples/github.rb +8 -0
- data/lazy_resource.gemspec +3 -3
- data/lib/lazy_resource.rb +17 -0
- data/lib/lazy_resource/attributes.rb +24 -15
- data/lib/lazy_resource/ext/typhoeus.rb +43 -10
- data/lib/lazy_resource/log_subscriber.rb +17 -0
- data/lib/lazy_resource/mapping.rb +1 -1
- data/lib/lazy_resource/relation.rb +11 -1
- data/lib/lazy_resource/request.rb +36 -16
- data/lib/lazy_resource/resource.rb +6 -5
- data/lib/lazy_resource/resource_queue.rb +33 -7
- data/lib/lazy_resource/url_generation.rb +6 -4
- data/lib/lazy_resource/version.rb +1 -1
- data/spec/lazy_resource/attributes_spec.rb +44 -23
- data/spec/lazy_resource/ext/typhoeus_spec.rb +44 -13
- data/spec/lazy_resource/lazy_resource_spec.rb +26 -1
- data/spec/lazy_resource/log_subscriber_spec.rb +46 -0
- data/spec/lazy_resource/relation_spec.rb +25 -0
- data/spec/lazy_resource/request_spec.rb +11 -4
- data/spec/lazy_resource/resource_queue_spec.rb +47 -0
- data/spec/lazy_resource/resource_spec.rb +20 -6
- data/spec/lazy_resource/url_generation_spec.rb +10 -4
- data/spec/spec_helper.rb +1 -1
- metadata +20 -31
@@ -1,17 +1,33 @@
|
|
1
1
|
module Typhoeus
|
2
2
|
class Hydra
|
3
|
+
def items_queued?
|
4
|
+
@multi.items_queued? || self.queued_requests.size > 0
|
5
|
+
end
|
6
|
+
|
3
7
|
def run_with_logging
|
4
|
-
log
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
+
if log?
|
9
|
+
log { run_without_logging }
|
10
|
+
else
|
11
|
+
run_without_logging
|
8
12
|
end
|
13
|
+
end
|
9
14
|
|
10
|
-
|
15
|
+
private
|
16
|
+
def log?
|
17
|
+
LazyResource.debug && LazyResource.logger && items_queued? && !running?
|
18
|
+
end
|
11
19
|
|
12
|
-
|
13
|
-
|
14
|
-
|
20
|
+
def running?
|
21
|
+
@running || @multi.running?
|
22
|
+
end
|
23
|
+
|
24
|
+
def log(&block)
|
25
|
+
start_time = Time.now
|
26
|
+
ActiveSupport::Notifications.instrument('request_group_started.lazy_resource', start_time: start_time)
|
27
|
+
|
28
|
+
yield
|
29
|
+
|
30
|
+
ActiveSupport::Notifications.instrument('request_group_finished.lazy_resource', start_time: start_time, end_time: Time.now)
|
15
31
|
end
|
16
32
|
|
17
33
|
alias_method :run_without_logging, :run
|
@@ -21,8 +37,25 @@ end
|
|
21
37
|
|
22
38
|
module Ethon
|
23
39
|
class Multi
|
24
|
-
def
|
25
|
-
|
40
|
+
def running?
|
41
|
+
running_count > 0
|
42
|
+
end
|
43
|
+
|
44
|
+
def running_count
|
45
|
+
@running_count ||= 0
|
46
|
+
end
|
47
|
+
|
48
|
+
def items_queued?
|
49
|
+
easy_handles.size > 0
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def self.logger
|
54
|
+
@logger ||= DevNull.new
|
55
|
+
end
|
56
|
+
|
57
|
+
class DevNull
|
58
|
+
def method_missing(*args, &block)
|
26
59
|
end
|
27
60
|
end
|
28
61
|
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module LazyResource
|
2
|
+
class LogSubscriber < ActiveSupport::LogSubscriber
|
3
|
+
def request(event)
|
4
|
+
info "\s\s\s\s[#{event.payload[:code]}](#{((event.payload[:time] || 0) * 1000).ceil}ms) #{event.payload[:url]}"
|
5
|
+
end
|
6
|
+
|
7
|
+
def request_group_started(event)
|
8
|
+
info "Processing requests:"
|
9
|
+
end
|
10
|
+
|
11
|
+
def request_group_finished(event)
|
12
|
+
info "Requests finished in #{((event.payload[:end_time] - event.payload[:start_time]) * 1000).ceil}ms"
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
LazyResource::LogSubscriber.attach_to(:lazy_resource)
|
@@ -8,12 +8,17 @@ module LazyResource
|
|
8
8
|
end
|
9
9
|
end
|
10
10
|
|
11
|
-
attr_accessor :fetched, :klass, :values, :from, :site, :other_attributes
|
11
|
+
attr_accessor :fetched, :klass, :values, :from, :site, :other_attributes, :request_error
|
12
|
+
attr_writer :method
|
13
|
+
attr_reader :route
|
12
14
|
|
13
15
|
def initialize(klass, options = {})
|
14
16
|
@klass = klass
|
17
|
+
@route = options.fetch(:where_values, {}).delete(:_route)
|
15
18
|
@values = options.slice(:where_values, :order_value, :limit_value, :offset_value, :page_value)
|
16
19
|
@fetched = options[:fetched] || false
|
20
|
+
@method = options[:method]
|
21
|
+
|
17
22
|
unless fetched?
|
18
23
|
resource_queue.queue(self)
|
19
24
|
end
|
@@ -29,6 +34,10 @@ module LazyResource
|
|
29
34
|
from
|
30
35
|
end
|
31
36
|
|
37
|
+
def method
|
38
|
+
@method ? @method.downcase.to_sym : nil
|
39
|
+
end
|
40
|
+
|
32
41
|
def to_params
|
33
42
|
params = {}
|
34
43
|
params.merge!(where_values) unless where_values.nil?
|
@@ -117,6 +126,7 @@ module LazyResource
|
|
117
126
|
|
118
127
|
def to_a
|
119
128
|
resource_queue.run if !fetched?
|
129
|
+
raise self.request_error if self.request_error.present?
|
120
130
|
result
|
121
131
|
end
|
122
132
|
|
@@ -1,23 +1,39 @@
|
|
1
1
|
module LazyResource
|
2
2
|
class Request < Typhoeus::Request
|
3
|
-
SUCCESS_STATUSES =
|
3
|
+
SUCCESS_STATUSES = 200...300
|
4
4
|
|
5
5
|
attr_accessor :resource, :response
|
6
6
|
|
7
7
|
def initialize(url, resource, options={})
|
8
8
|
options = options.dup
|
9
|
-
options[:headers]
|
9
|
+
options[:headers] = (options[:headers] || {}).dup
|
10
10
|
options[:headers][:Accept] ||= 'application/json'
|
11
11
|
options[:headers].merge!(Thread.current[:default_headers]) unless Thread.current[:default_headers].nil?
|
12
|
+
|
13
|
+
params = (URI.parse(url).query || '')
|
14
|
+
.split('&')
|
15
|
+
.map { |param| param.split('=') }
|
16
|
+
.inject({}) { |memo, (k,v)| memo[URI.unescape(k)] = v.nil? ? v : URI.unescape(v); memo }
|
17
|
+
|
18
|
+
url.gsub!(/\?.*/, '')
|
19
|
+
|
20
|
+
options[:params] ||= {}
|
21
|
+
options[:params].merge!(params)
|
22
|
+
options[:params].merge!(Thread.current[:default_params]) unless Thread.current[:default_params].nil?
|
23
|
+
|
12
24
|
options[:method] ||= :get
|
13
25
|
|
26
|
+
if [:post, :put].include?(options[:method])
|
27
|
+
options[:headers]['Content-Type'] = 'application/json'
|
28
|
+
end
|
29
|
+
|
14
30
|
super(url, options)
|
15
31
|
|
16
32
|
@resource = resource
|
33
|
+
|
17
34
|
self.on_complete do
|
18
35
|
log_response(response) if LazyResource.debug && LazyResource.logger
|
19
36
|
@response = response
|
20
|
-
handle_errors unless SUCCESS_STATUSES.include?(@response.code)
|
21
37
|
parse
|
22
38
|
end
|
23
39
|
|
@@ -25,39 +41,43 @@ module LazyResource
|
|
25
41
|
end
|
26
42
|
|
27
43
|
def log_response(response)
|
28
|
-
|
44
|
+
ActiveSupport::Notifications.instrument('request.lazy_resource', code: response.code, time: response.time, url: url)
|
29
45
|
end
|
30
46
|
|
31
47
|
def parse
|
48
|
+
unless SUCCESS_STATUSES.include?(@response.code)
|
49
|
+
@resource.request_error = error
|
50
|
+
end
|
51
|
+
|
32
52
|
unless self.response.body.nil? || self.response.body == ''
|
33
53
|
@resource.load(JSON.parse(self.response.body))
|
34
54
|
end
|
35
55
|
end
|
36
56
|
|
37
|
-
def
|
57
|
+
def error
|
38
58
|
case @response.code
|
39
59
|
when 300...400
|
40
|
-
|
60
|
+
Redirection.new(@response)
|
41
61
|
when 400
|
42
|
-
|
62
|
+
BadRequest.new(@response)
|
43
63
|
when 401
|
44
|
-
|
64
|
+
UnauthorizedAccess.new(@response)
|
45
65
|
when 403
|
46
|
-
|
66
|
+
ForbiddenAccess.new(@response)
|
47
67
|
when 404
|
48
|
-
|
68
|
+
ResourceNotFound.new(@response)
|
49
69
|
when 405
|
50
|
-
|
70
|
+
MethodNotAllowed.new(@response)
|
51
71
|
when 409
|
52
|
-
|
72
|
+
ResourceConflict.new(@response)
|
53
73
|
when 410
|
54
|
-
|
74
|
+
ResourceGone.new(@response)
|
55
75
|
when 422
|
56
|
-
|
76
|
+
UnprocessableEntity.new(@response)
|
57
77
|
when 400...500
|
58
|
-
|
78
|
+
ClientError.new(@response)
|
59
79
|
when 500...600
|
60
|
-
|
80
|
+
ServerError.new(@response)
|
61
81
|
end
|
62
82
|
end
|
63
83
|
end
|
@@ -69,11 +69,12 @@ module LazyResource
|
|
69
69
|
end
|
70
70
|
|
71
71
|
def request_queue
|
72
|
-
Thread.current[:request_queue] ||= Typhoeus::Hydra.new
|
72
|
+
Thread.current[:request_queue] ||= Typhoeus::Hydra.new(:max_concurrency => LazyResource.max_concurrency)
|
73
73
|
end
|
74
74
|
|
75
75
|
def find(id, params={}, options={})
|
76
|
-
self.new
|
76
|
+
self.new.tap do |resource|
|
77
|
+
resource.instance_variable_set("@#{self.primary_key_name}", id)
|
77
78
|
resource.fetched = false
|
78
79
|
resource.persisted = true
|
79
80
|
options[:headers] ||= {}
|
@@ -98,7 +99,7 @@ module LazyResource
|
|
98
99
|
def offset(offset_value)
|
99
100
|
Relation.new(self, :offset_value => offset_value)
|
100
101
|
end
|
101
|
-
|
102
|
+
|
102
103
|
def page(page_value)
|
103
104
|
Relation.new(self, :page_value => page_value)
|
104
105
|
end
|
@@ -163,7 +164,7 @@ module LazyResource
|
|
163
164
|
|
164
165
|
def create
|
165
166
|
run_callbacks :create do
|
166
|
-
request = Request.new(self.collection_url, self, { :method => :post, :
|
167
|
+
request = Request.new(self.collection_url, self, { :method => :post, :body => attribute_params.to_json })
|
167
168
|
self.class.request_queue.queue(request)
|
168
169
|
self.class.fetch_all
|
169
170
|
self.changed_attributes.clear
|
@@ -172,7 +173,7 @@ module LazyResource
|
|
172
173
|
|
173
174
|
def update
|
174
175
|
run_callbacks :update do
|
175
|
-
request = Request.new(self.element_url, self, { :method => :put, :
|
176
|
+
request = Request.new(self.element_url, self, { :method => :put, :body => attribute_params.to_json })
|
176
177
|
self.class.request_queue.queue(request)
|
177
178
|
self.class.fetch_all
|
178
179
|
self.changed_attributes.clear
|
@@ -15,26 +15,52 @@ module LazyResource
|
|
15
15
|
end
|
16
16
|
|
17
17
|
def request_queue
|
18
|
-
Thread.current[:request_queue] ||= Typhoeus::Hydra.new
|
18
|
+
Thread.current[:request_queue] ||= Typhoeus::Hydra.new(:max_concurrency => LazyResource.max_concurrency)
|
19
19
|
end
|
20
20
|
|
21
21
|
def run
|
22
22
|
send_to_request_queue!
|
23
|
-
request_queue.run
|
23
|
+
request_queue.run if request_queue.items_queued?
|
24
24
|
end
|
25
25
|
|
26
26
|
def send_to_request_queue!
|
27
27
|
while(relation = @queue.pop)
|
28
|
-
|
28
|
+
options = { :headers => relation.headers, :method => relation.method }
|
29
|
+
|
30
|
+
if [:post, :put].include?(relation.method)
|
31
|
+
options[:body] = relation.to_params.to_json
|
32
|
+
end
|
33
|
+
|
34
|
+
request = Request.new(url_for(relation), relation, options)
|
29
35
|
request_queue.queue(request)
|
30
36
|
end
|
31
37
|
end
|
32
38
|
|
33
39
|
def url_for(relation)
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
40
|
+
if relation.route.nil?
|
41
|
+
include_query = ![:post, :put].include?(relation.method)
|
42
|
+
|
43
|
+
url = ''
|
44
|
+
url << relation.klass.site
|
45
|
+
url << relation.klass.collection_path(relation.to_params, nil, relation.from, include_query)
|
46
|
+
url
|
47
|
+
else
|
48
|
+
url = relation.route
|
49
|
+
url.gsub!(/:\w*/) do |match|
|
50
|
+
attr = match[1..-1].to_sym
|
51
|
+
if relation.where_values.has_key?(attr)
|
52
|
+
relation.where_values[attr]
|
53
|
+
else
|
54
|
+
match
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
if url =~ /http/
|
59
|
+
url
|
60
|
+
else
|
61
|
+
relation.klass.site + url
|
62
|
+
end
|
63
|
+
end
|
38
64
|
end
|
39
65
|
end
|
40
66
|
end
|
@@ -3,7 +3,7 @@ module LazyResource
|
|
3
3
|
extend ActiveSupport::Concern
|
4
4
|
|
5
5
|
def element_path(options = nil)
|
6
|
-
self.class.element_path(self.
|
6
|
+
self.class.element_path(self.instance_variable_get("@#{self.class.primary_key_name}"), options)
|
7
7
|
end
|
8
8
|
|
9
9
|
def element_url(options = nil)
|
@@ -72,10 +72,12 @@ module LazyResource
|
|
72
72
|
# * +prefix_options+ - A hash to add a prefix to the request for nested URLs (e.g., <tt>:account_id => 19</tt>
|
73
73
|
# would yield a URL like <tt>/accounts/19/purchases.json</tt>).
|
74
74
|
# * +query_options+ - A hash to add items to the query string for the request.
|
75
|
-
def collection_path(prefix_options = {}, query_options = nil, from = nil)
|
75
|
+
def collection_path(prefix_options = {}, query_options = nil, from = nil, include_query = true)
|
76
76
|
prefix_options, query_options = split_options(prefix_options) if query_options.nil?
|
77
77
|
from = self.from if from.nil? && respond_to?(:from)
|
78
|
-
"#{prefix(prefix_options)}#{from || collection_name}
|
78
|
+
path = "#{prefix(prefix_options)}#{from || collection_name}"
|
79
|
+
path += "#{query_string(query_options)}" if include_query
|
80
|
+
path
|
79
81
|
end
|
80
82
|
|
81
83
|
# Builds the query string for the request.
|
@@ -90,7 +92,7 @@ module LazyResource
|
|
90
92
|
|
91
93
|
(options || {}).each do |key, value|
|
92
94
|
next if key.blank?
|
93
|
-
(key =~ /\w*_id
|
95
|
+
(key =~ /\w*_id$/ ? prefix_options : query_options)[key.to_sym] = value
|
94
96
|
end
|
95
97
|
|
96
98
|
[prefix_options, query_options]
|
@@ -1,9 +1,12 @@
|
|
1
1
|
require 'spec_helper'
|
2
2
|
|
3
|
+
# We need a reference to this exact proc in two spots
|
4
|
+
LAMBDA_ROUTE = lambda { "/path/to/#{name}" }
|
5
|
+
|
3
6
|
class AttributeObject
|
4
7
|
include LazyResource::Attributes
|
5
8
|
|
6
|
-
attr_accessor :fetched
|
9
|
+
attr_accessor :fetched, :request_error
|
7
10
|
|
8
11
|
def self.resource_queue
|
9
12
|
@resource_queue ||= LazyResource::ResourceQueue.new
|
@@ -62,6 +65,11 @@ describe LazyResource::Attributes do
|
|
62
65
|
@foo.name
|
63
66
|
end
|
64
67
|
|
68
|
+
it 'raises the error at request_error if it exists' do
|
69
|
+
@foo.request_error = StandardError.new
|
70
|
+
lambda { @foo.name }.should raise_error(StandardError)
|
71
|
+
end
|
72
|
+
|
65
73
|
describe 'associations' do
|
66
74
|
before :each do
|
67
75
|
AttributeObject.attribute(:posts, [Post])
|
@@ -80,33 +88,45 @@ describe LazyResource::Attributes do
|
|
80
88
|
@foo.user
|
81
89
|
end
|
82
90
|
|
83
|
-
|
91
|
+
context ':route' do
|
84
92
|
before :each do
|
85
93
|
AttributeObject.attribute(:posts_url, String)
|
86
|
-
AttributeObject.attribute(:
|
87
|
-
AttributeObject.attribute(:
|
88
|
-
|
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')
|
94
|
+
AttributeObject.attribute(:posts, [Post], :route => :posts_url)
|
95
|
+
AttributeObject.attribute(:user, User, :route => "/path/to/user")
|
96
|
+
@foo.send(:instance_variable_set, "@posts_url", '/path/to/posts')
|
91
97
|
end
|
92
98
|
|
93
99
|
it 'finds a collection using the specified url' do
|
94
|
-
|
95
|
-
request = LazyResource::Request.new(@foo.posts_url, relation)
|
96
|
-
LazyResource::Request.should_receive(:new).with(@foo.posts_url, relation, :headers => relation.headers).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
|
100
|
+
@foo.posts.route.should == '/path/to/posts'
|
101
101
|
end
|
102
102
|
|
103
103
|
it 'finds a singular resource with the specified url' do
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
104
|
+
@foo.user.route.should == '/path/to/user'
|
105
|
+
end
|
106
|
+
|
107
|
+
context 'as a proc' do
|
108
|
+
before :each do
|
109
|
+
AttributeObject.any_instance.stub(:name).and_return("foobar")
|
110
|
+
AttributeObject.attribute(:comments, [Post], :route => LAMBDA_ROUTE)
|
111
|
+
end
|
112
|
+
|
113
|
+
it 'evaluates the proc to generate the url' do
|
114
|
+
@foo.comments.route.should == '/path/to/foobar'
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
context ':using' do
|
120
|
+
after :all do
|
121
|
+
AttributeObject.attribute(:posts_url, String)
|
122
|
+
AttributeObject.attribute(:user_url, String)
|
123
|
+
AttributeObject.attribute(:posts, [Post], :route => :posts_url)
|
124
|
+
AttributeObject.attribute(:user, User, :route => :user_url)
|
125
|
+
end
|
126
|
+
|
127
|
+
it 'generates a deprecation warning when using :using' do
|
128
|
+
LazyResource.should_receive(:deprecate).with("Attribute option :using is deprecated. Please use :route instead.", anything, anything)
|
129
|
+
AttributeObject.attribute(:posts, [Post], :using => :posts_url)
|
110
130
|
end
|
111
131
|
end
|
112
132
|
end
|
@@ -141,10 +161,11 @@ describe LazyResource::Attributes do
|
|
141
161
|
describe '.attributes' do
|
142
162
|
it 'returns a hash of the defined attributes' do
|
143
163
|
AttributeObject.attributes.should == { :name => { :type => String, :options => {} },
|
144
|
-
:posts => { :type => [Post], :options => { :
|
145
|
-
:user => { :type => User, :options => { :
|
164
|
+
:posts => { :type => [Post], :options => { :route => :posts_url } },
|
165
|
+
:user => { :type => User, :options => { :route => :user_url } },
|
146
166
|
:posts_url => { :type => String, :options => {} },
|
147
|
-
:user_url => { :type => String, :options => {} }
|
167
|
+
:user_url => { :type => String, :options => {} },
|
168
|
+
:comments => { :type => [Post], :options => { :route => LAMBDA_ROUTE } } }
|
148
169
|
end
|
149
170
|
end
|
150
171
|
|