lazy_resource 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +18 -0
- data/.rspec +3 -0
- data/.rvmrc +1 -0
- data/Gemfile +14 -0
- data/Guardfile +5 -0
- data/LICENSE +22 -0
- data/README.md +127 -0
- data/Rakefile +6 -0
- data/examples/github.rb +64 -0
- data/lazy_resource.gemspec +22 -0
- data/lib/lazy_resource.rb +39 -0
- data/lib/lazy_resource/attributes.rb +131 -0
- data/lib/lazy_resource/errors.rb +72 -0
- data/lib/lazy_resource/http_mock.rb +23 -0
- data/lib/lazy_resource/mapping.rb +73 -0
- data/lib/lazy_resource/relation.rb +132 -0
- data/lib/lazy_resource/request.rb +63 -0
- data/lib/lazy_resource/resource.rb +197 -0
- data/lib/lazy_resource/resource_queue.rb +40 -0
- data/lib/lazy_resource/types.rb +53 -0
- data/lib/lazy_resource/url_generation.rb +98 -0
- data/lib/lazy_resource/version.rb +3 -0
- data/spec/fixtures/comment.rb +6 -0
- data/spec/fixtures/post.rb +6 -0
- data/spec/fixtures/user.rb +9 -0
- data/spec/lazy_resource/attributes_spec.rb +157 -0
- data/spec/lazy_resource/errors_spec.rb +48 -0
- data/spec/lazy_resource/mapping_spec.rb +237 -0
- data/spec/lazy_resource/relation_spec.rb +169 -0
- data/spec/lazy_resource/request_spec.rb +143 -0
- data/spec/lazy_resource/resource_queue_spec.rb +66 -0
- data/spec/lazy_resource/resource_spec.rb +463 -0
- data/spec/lazy_resource/types_spec.rb +109 -0
- data/spec/lazy_resource/url_generation_spec.rb +155 -0
- data/spec/spec_helper.rb +25 -0
- metadata +165 -0
@@ -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
|