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