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.
@@ -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 = LazyResource.debug && LazyResource.logger && @multi.items_queued_but_not_running?
5
- if log
6
- LazyResource.logger.info "Processing requests:"
7
- start_time = Time.now
8
+ if log?
9
+ log { run_without_logging }
10
+ else
11
+ run_without_logging
8
12
  end
13
+ end
9
14
 
10
- run_without_logging
15
+ private
16
+ def log?
17
+ LazyResource.debug && LazyResource.logger && items_queued? && !running?
18
+ end
11
19
 
12
- if log
13
- LazyResource.logger.info "Requests processed in #{((Time.now - start_time) * 1000).ceil}ms"
14
- end
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 items_queued_but_not_running?
25
- easy_handles.size > 0 && running_count <= 0
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)
@@ -2,7 +2,7 @@ module LazyResource
2
2
  module Mapping
3
3
  extend ActiveSupport::Concern
4
4
 
5
- attr_accessor :fetched, :persisted, :other_attributes
5
+ attr_accessor :fetched, :persisted, :other_attributes, :request_error
6
6
 
7
7
  def fetched?
8
8
  @fetched
@@ -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 = [200, 201]
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
- LazyResource.logger.info "\t[#{response.code}](#{((response.time || 0) * 1000).ceil}ms): #{self.url}"
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 handle_errors
57
+ def error
38
58
  case @response.code
39
59
  when 300...400
40
- raise Redirection.new(@response)
60
+ Redirection.new(@response)
41
61
  when 400
42
- raise BadRequest.new(@response)
62
+ BadRequest.new(@response)
43
63
  when 401
44
- raise UnauthorizedAccess.new(@response)
64
+ UnauthorizedAccess.new(@response)
45
65
  when 403
46
- raise ForbiddenAccess.new(@response)
66
+ ForbiddenAccess.new(@response)
47
67
  when 404
48
- raise ResourceNotFound.new(@response)
68
+ ResourceNotFound.new(@response)
49
69
  when 405
50
- raise MethodNotAllowed.new(@response)
70
+ MethodNotAllowed.new(@response)
51
71
  when 409
52
- raise ResourceConflict.new(@response)
72
+ ResourceConflict.new(@response)
53
73
  when 410
54
- raise ResourceGone.new(@response)
74
+ ResourceGone.new(@response)
55
75
  when 422
56
- raise UnprocessableEntity.new(@response)
76
+ UnprocessableEntity.new(@response)
57
77
  when 400...500
58
- raise ClientError.new(@response)
78
+ ClientError.new(@response)
59
79
  when 500...600
60
- raise ServerError.new(@response)
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(self.primary_key_name => id).tap do |resource|
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, :params => attribute_params })
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, :params => attribute_params })
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
- request = Request.new(url_for(relation), relation, :headers => relation.headers)
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
- url = ''
35
- url << relation.klass.site
36
- url << self.class.collection_path(relation.to_params, nil, relation.from)
37
- url
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.primary_key, options)
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}#{query_string(query_options)}"
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/ ? prefix_options : query_options)[key.to_sym] = value
95
+ (key =~ /\w*_id$/ ? prefix_options : query_options)[key.to_sym] = value
94
96
  end
95
97
 
96
98
  [prefix_options, query_options]
@@ -1,3 +1,3 @@
1
1
  module LazyResource
2
- VERSION = "0.4.0"
2
+ VERSION = "0.5.0"
3
3
  end
@@ -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
- describe ':using' do
91
+ context ':route' do
84
92
  before :each do
85
93
  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')
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
- relation = LazyResource::Relation.new(Post, :headers => {})
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
- 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
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 => { :using => :posts_url } },
145
- :user => { :type => User, :options => { :using => :user_url } },
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