lazy_resource 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,72 @@
1
+ module LazyResource
2
+ class ConnectionError < StandardError # :nodoc:
3
+ attr_reader :response
4
+
5
+ def initialize(response, message = nil)
6
+ @response = response
7
+ @message = message
8
+ end
9
+
10
+ def to_s
11
+ message = "Failed."
12
+ message << " Response code = #{response.code}." if response.respond_to?(:code)
13
+ message << " Response message = #{response.body}." if response.respond_to?(:body)
14
+ message
15
+ end
16
+ end
17
+
18
+ # Raised when a Timeout::Error occurs.
19
+ class TimeoutError < ConnectionError
20
+ def initialize(message)
21
+ @message = message
22
+ end
23
+ def to_s; @message ;end
24
+ end
25
+
26
+ # Raised when a OpenSSL::SSL::SSLError occurs.
27
+ class SSLError < ConnectionError
28
+ def initialize(message)
29
+ @message = message
30
+ end
31
+ def to_s; @message ;end
32
+ end
33
+
34
+ # 3xx Redirection
35
+ class Redirection < ConnectionError # :nodoc:
36
+ def to_s; (response.headers['Location'] || response.headers[:Location]) ? "#{super} => #{response.headers['Location'] || response.headers[:Location]}" : super; end
37
+ end
38
+
39
+ # 4xx Client Error
40
+ class ClientError < ConnectionError; end # :nodoc:
41
+
42
+ # 400 Bad Request
43
+ class BadRequest < ClientError; end # :nodoc
44
+
45
+ # 401 Unauthorized
46
+ class UnauthorizedAccess < ClientError; end # :nodoc
47
+
48
+ # 403 Forbidden
49
+ class ForbiddenAccess < ClientError; end # :nodoc
50
+
51
+ # 404 Not Found
52
+ class ResourceNotFound < ClientError; end # :nodoc:
53
+
54
+ # 409 Conflict
55
+ class ResourceConflict < ClientError; end # :nodoc:
56
+
57
+ # 410 Gone
58
+ class ResourceGone < ClientError; end # :nodoc:
59
+
60
+ # 422 Unprocessable Entity
61
+ class UnprocessableEntity < ClientError; end # :nodoc:
62
+
63
+ # 5xx Server Error
64
+ class ServerError < ConnectionError; end # :nodoc:
65
+
66
+ # 405 Method Not Allowed
67
+ class MethodNotAllowed < ClientError # :nodoc:
68
+ def allowed_methods
69
+ @response.headers['Allow'].split(',').map { |verb| verb.strip.downcase.to_sym }
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,23 @@
1
+ module LazyResource
2
+ class HttpMock
3
+ class Responder
4
+ [:post, :put, :get, :delete].each do |method|
5
+ module_eval <<-RUBY, __FILE__, __LINE__ + 1
6
+ def #{method}(path, body='', status=200, response_headers={})
7
+ request_queue.stub(:#{method}, path).and_return(Typhoeus::Response.new(:code => status, :headers => response_headers, :body => body, :time => 0.3))
8
+ end
9
+ RUBY
10
+ end
11
+
12
+ def request_queue
13
+ Thread.current[:request_queue] ||= Typhoeus::Hydra.new
14
+ end
15
+ end
16
+
17
+ class << self
18
+ def respond_to(*args)
19
+ yield Responder.new
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,73 @@
1
+ module LazyResource
2
+ module Mapping
3
+ extend ActiveSupport::Concern
4
+
5
+ attr_accessor :fetched, :persisted
6
+
7
+ def fetched?
8
+ @fetched
9
+ end
10
+
11
+ def self.root_node_name=(node)
12
+ @root_node_name = node
13
+ end
14
+
15
+ def self.root_node_name
16
+ @root_node_name
17
+ end
18
+
19
+ module ClassMethods
20
+ def root_node_name=(node)
21
+ @root_node_name = node
22
+ end
23
+
24
+ def root_node_name
25
+ @root_node_name || LazyResource::Mapping.root_node_name
26
+ end
27
+
28
+ def load(objects)
29
+ if objects.is_a?(Array)
30
+ objects.map do |object|
31
+ self.new.load(object)
32
+ end
33
+ else
34
+ if self.root_node_name && objects.key?(self.root_node_name.to_s)
35
+ self.load(objects[self.root_node_name.to_s])
36
+ else
37
+ self.new.load(objects)
38
+ end
39
+ end
40
+ end
41
+ end
42
+
43
+ def load(hash, persisted=true)
44
+ hash.fetched = true and return hash if hash.kind_of?(LazyResource::Mapping)
45
+
46
+ self.tap do |resource|
47
+ resource.persisted = persisted
48
+ resource.fetched = false
49
+
50
+ hash = hash[resource.class.root_node_name.to_s] if resource.class.root_node_name && hash.key?(resource.class.root_node_name.to_s)
51
+ hash.each do |name, value|
52
+ attribute = self.class.attributes[name.to_sym]
53
+ next if attribute.nil?
54
+
55
+ type = attribute[:type]
56
+ if type.is_a?(::Array)
57
+ if type.first.include?(LazyResource::Mapping)
58
+ resource.send(:"#{name}=", type.first.load(value))
59
+ else
60
+ resource.send(:"#{name}=", value.map { |object| type.first.parse(object) })
61
+ end
62
+ elsif type.include?(LazyResource::Mapping)
63
+ resource.send(:"#{name}=", type.load(value))
64
+ else
65
+ resource.send(:"#{name}=", type.parse(value)) rescue StandardError
66
+ end
67
+ end
68
+
69
+ resource.fetched = true
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,132 @@
1
+ require 'active_support/core_ext/hash/slice'
2
+
3
+ module LazyResource
4
+ class Relation
5
+ class << self
6
+ def resource_queue
7
+ Thread.current[:resource_queue] ||= ResourceQueue.new
8
+ end
9
+ end
10
+
11
+ attr_accessor :fetched, :klass, :values, :from, :site
12
+
13
+ def initialize(klass, options = {})
14
+ @klass = klass
15
+ @values = options.slice(:where_values, :order_value, :limit_value, :offset_value, :page_value)
16
+ @fetched = options[:fetched] || false
17
+ unless fetched?
18
+ resource_queue.queue(self)
19
+ end
20
+
21
+ self
22
+ end
23
+
24
+ def from
25
+ @from || self.klass.collection_name
26
+ end
27
+
28
+ def collection_name
29
+ from
30
+ end
31
+
32
+ def to_params
33
+ params = {}
34
+ params.merge!(where_values) unless where_values.nil?
35
+ params.merge!(:order => order_value) unless order_value.nil?
36
+ params.merge!(:limit => limit_value) unless limit_value.nil?
37
+ params.merge!(:offset => offset_value) unless offset_value.nil?
38
+ params.merge!(:page => page_value) unless page_value.nil?
39
+ params
40
+ end
41
+
42
+ def load(objects)
43
+ @fetched = true
44
+ @result = @klass.load(objects)
45
+ end
46
+
47
+ def resource_queue
48
+ self.class.resource_queue
49
+ end
50
+
51
+ def where(where_values)
52
+ if @values[:where_values].nil?
53
+ @values[:where_values] = where_values
54
+ else
55
+ @values[:where_values].merge!(where_values)
56
+ end
57
+
58
+ self
59
+ end
60
+
61
+ def order(order_value)
62
+ @values[:order_value] = order_value
63
+ self
64
+ end
65
+
66
+ def limit(limit_value)
67
+ @values[:limit_value] = limit_value
68
+ self
69
+ end
70
+
71
+ def offset(offset_value)
72
+ @values[:offset_value] = offset_value
73
+ self
74
+ end
75
+
76
+ def page(page_value)
77
+ @values[:page_value] = page_value
78
+ self
79
+ end
80
+
81
+ def where_values
82
+ @values[:where_values]
83
+ end
84
+
85
+ def order_value
86
+ @values[:order_value]
87
+ end
88
+
89
+ def limit_value
90
+ @values[:limit_value]
91
+ end
92
+
93
+ def offset_value
94
+ @values[:offset_value]
95
+ end
96
+
97
+ def page_value
98
+ @values[:page_value]
99
+ end
100
+
101
+ def fetched?
102
+ @fetched
103
+ end
104
+
105
+ def to_a
106
+ resource_queue.run if !fetched?
107
+ result
108
+ end
109
+
110
+ def result
111
+ @result ||= []
112
+ end
113
+
114
+ def respond_to?(method, include_private = false)
115
+ super || result.respond_to?(method, include_private)
116
+ end
117
+
118
+ def as_json(options = {})
119
+ to_a.map do |record|
120
+ record.as_json
121
+ end
122
+ end
123
+
124
+ def method_missing(name, *args, &block)
125
+ if result.respond_to?(name)
126
+ self.to_a.send(name, *args, &block)
127
+ else
128
+ super
129
+ end
130
+ end
131
+ end
132
+ end
@@ -0,0 +1,63 @@
1
+ module LazyResource
2
+ class Request < Typhoeus::Request
3
+ SUCCESS_STATUSES = [200, 201]
4
+
5
+ attr_accessor :resource, :response
6
+
7
+ def initialize(url, resource, options={})
8
+ options = options.dup
9
+ options[:headers] ||= {}
10
+ options[:headers][:Accept] ||= 'application/json'
11
+ options[:headers].merge!(Thread.current[:default_headers]) unless Thread.current[:default_headers].nil?
12
+ options[:method] ||= :get
13
+
14
+ super(url, options)
15
+
16
+ @resource = resource
17
+ self.on_complete = on_complete_proc
18
+
19
+ self
20
+ end
21
+
22
+ def on_complete_proc
23
+ Proc.new do |response|
24
+ @response = response
25
+ handle_errors unless SUCCESS_STATUSES.include?(@response.code)
26
+ parse
27
+ end
28
+ end
29
+
30
+ def parse
31
+ unless self.response.body.nil? || self.response.body == ''
32
+ @resource.load(JSON.parse(self.response.body))
33
+ end
34
+ end
35
+
36
+ def handle_errors
37
+ case @response.code
38
+ when 300...400
39
+ raise Redirection.new(@response)
40
+ when 400
41
+ raise BadRequest.new(@response)
42
+ when 401
43
+ raise UnauthorizedAccess.new(@response)
44
+ when 403
45
+ raise ForbiddenAccess.new(@response)
46
+ when 404
47
+ raise ResourceNotFound.new(@response)
48
+ when 405
49
+ raise MethodNotAllowed.new(@response)
50
+ when 409
51
+ raise ResourceConflict.new(@response)
52
+ when 410
53
+ raise ResourceGone.new(@response)
54
+ when 422
55
+ raise UnprocessableEntity.new(@response)
56
+ when 400...500
57
+ raise ClientError.new(@response)
58
+ when 500...600
59
+ raise ServerError.new(@response)
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,197 @@
1
+ module LazyResource
2
+ module Resource
3
+ extend ActiveSupport::Concern
4
+ include ActiveModel::Conversion
5
+ include Attributes, Mapping, Types, UrlGeneration
6
+
7
+ included do
8
+ extend ActiveModel::Callbacks
9
+ define_model_callbacks :create, :update, :save, :destroy
10
+
11
+ include ActiveModel::Validations
12
+ end
13
+
14
+ def self.site=(site)
15
+ @site = site
16
+ end
17
+
18
+ def self.site
19
+ @site
20
+ end
21
+
22
+ def self.root_node_name=(node_name)
23
+ LazyResource::Mapping.root_node_name = node_name
24
+ end
25
+
26
+ module ClassMethods
27
+ # Gets the URI of the REST resources to map for this class. The site variable is required for
28
+ # Active Async's mapping to work.
29
+ def site
30
+ if defined?(@site)
31
+ @site
32
+ else
33
+ LazyResource::Resource.site
34
+ end
35
+ end
36
+
37
+ # Sets the URI of the REST resources to map for this class to the value in the +site+ argument.
38
+ # The site variable is required for Active Async's mapping to work.
39
+ def site=(site)
40
+ @site = site
41
+ end
42
+
43
+ def request_queue
44
+ Thread.current[:request_queue] ||= Typhoeus::Hydra.new
45
+ end
46
+
47
+ def find(id, params={}, options={})
48
+ self.new(self.primary_key_name => id).tap do |resource|
49
+ resource.fetched = false
50
+ resource.persisted = true
51
+ request = Request.new(resource.element_url(params), resource, options)
52
+ request_queue.queue(request)
53
+ end
54
+ end
55
+
56
+ def where(where_values)
57
+ Relation.new(self, :where_values => where_values)
58
+ end
59
+
60
+ def order(order_value)
61
+ Relation.new(self, :order_value => order_value)
62
+ end
63
+
64
+ def limit(limit_value)
65
+ Relation.new(self, :limit_value => limit_value)
66
+ end
67
+
68
+ def offset(offset_value)
69
+ Relation.new(self, :offset_value => offset_value)
70
+ end
71
+
72
+ def page(page_value)
73
+ Relation.new(self, :page_value => page_value)
74
+ end
75
+
76
+ def all
77
+ Relation.new(self)
78
+ end
79
+
80
+ def create(attributes={})
81
+ new(attributes).tap do |resource|
82
+ resource.create
83
+ end
84
+ end
85
+ end
86
+
87
+ def initialize(attributes={})
88
+ self.tap do |resource|
89
+ resource.load(attributes, false)
90
+ end
91
+ end
92
+
93
+ # Tests for equality. Returns true iff +other+ is the same object or
94
+ # other is an instance of the same class and has the same attributes.
95
+ def ==(other)
96
+ return true if other.equal?(self)
97
+ return false unless other.instance_of?(self.class)
98
+
99
+ self.class.attributes.inject(true) do |memo, attribute|
100
+ attribute_name = attribute.first
101
+ attribute_type = attribute.last[:type]
102
+
103
+ # Skip associations
104
+ if attribute_type.include?(LazyResource::Resource) || (attribute_type.is_a?(::Array) && attribute_type.first.include?(LazyResource::Resource))
105
+ memo
106
+ else
107
+ memo && self.send(:"#{attribute_name}") == other.send(:"#{attribute_name}")
108
+ end
109
+ end
110
+ end
111
+
112
+ def eql?(other)
113
+ self == other
114
+ end
115
+
116
+ def persisted?
117
+ @persisted
118
+ end
119
+
120
+ def new_record?
121
+ !persisted?
122
+ end
123
+
124
+ alias :new? :new_record?
125
+
126
+ def save
127
+ return true if !changed?
128
+ run_callbacks :save do
129
+ new_record? ? create : update
130
+ self.persisted = true
131
+ end
132
+ end
133
+
134
+ def create
135
+ run_callbacks :create do
136
+ request = Request.new(self.collection_url, self, { :method => :post, :params => attribute_params })
137
+ self.class.request_queue.queue(request)
138
+ self.class.fetch_all
139
+ self.changed_attributes.clear
140
+ end
141
+ end
142
+
143
+ def update
144
+ run_callbacks :update do
145
+ request = Request.new(self.element_url, self, { :method => :put, :params => attribute_params })
146
+ self.class.request_queue.queue(request)
147
+ self.class.fetch_all
148
+ self.changed_attributes.clear
149
+ end
150
+ end
151
+
152
+ def destroy
153
+ run_callbacks :destroy do
154
+ request = Request.new(self.element_url, self, { :method => :delete })
155
+ self.class.request_queue.queue(request)
156
+ self.class.fetch_all
157
+ end
158
+ end
159
+
160
+ def update_attributes(attributes={})
161
+ attributes.each do |name, value|
162
+ self.send("#{name}=", value)
163
+ end
164
+ self.update
165
+ end
166
+
167
+ def attribute_params
168
+ { self.class.element_name.to_sym => changed_attributes.inject({}) do |hash, changed_attribute|
169
+ hash.tap do |hash|
170
+ hash[changed_attribute.first] = self.send(changed_attribute.first)
171
+ end
172
+ end }
173
+ end
174
+
175
+ def as_json(options={})
176
+ self.class.attributes.inject({}) do |hash, (attribute_name, attribute_options)|
177
+ attribute_type = attribute_options[:type]
178
+
179
+ # Skip nil attributes (need to use instance_variable_get to avoid the stub relations that get added for associations."
180
+ unless self.instance_variable_get("@#{attribute_name}").nil?
181
+ value = self.send(:"#{attribute_name}")
182
+
183
+ if (attribute_type.is_a?(::Array) && attribute_type.first.include?(LazyResource::Resource))
184
+ value = value.map { |v| v.as_json }
185
+ elsif attribute_type.include?(LazyResource::Resource)
186
+ value = value.as_json
187
+ elsif attribute_type == DateTime
188
+ value = value.to_s
189
+ end
190
+
191
+ hash[attribute_name.to_sym] = value
192
+ end
193
+ hash
194
+ end
195
+ end
196
+ end
197
+ end