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.
@@ -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