lazy_resource 0.4.0 → 0.5.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.
- 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
|
|