api_resource 0.5.1 → 0.6.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.
- data/.document +5 -0
- data/.gitignore +55 -0
- data/.rspec +8 -0
- data/.travis.yml +9 -0
- data/Gemfile +3 -36
- data/Gemfile.lock +37 -57
- data/Guardfile +7 -1
- data/{LICENSE.txt → LICENSE} +4 -2
- data/README.md +29 -0
- data/README.rdoc +5 -1
- data/Rakefile +5 -36
- data/api_resource.gemspec +40 -104
- data/lib/api_resource.rb +4 -1
- data/lib/api_resource/associations.rb +30 -14
- data/lib/api_resource/associations/association_proxy.rb +97 -0
- data/lib/api_resource/associations/belongs_to_remote_object_proxy.rb +0 -1
- data/lib/api_resource/associations/has_many_remote_object_proxy.rb +0 -1
- data/lib/api_resource/associations/has_one_remote_object_proxy.rb +7 -4
- data/lib/api_resource/associations/multi_object_proxy.rb +11 -16
- data/lib/api_resource/associations/single_object_proxy.rb +8 -6
- data/lib/api_resource/attributes.rb +29 -41
- data/lib/api_resource/base.rb +31 -102
- data/lib/api_resource/conditions.rb +36 -0
- data/lib/api_resource/conditions/abstract_condition.rb +112 -0
- data/lib/api_resource/conditions/association_condition.rb +18 -0
- data/lib/api_resource/conditions/include_condition.rb +16 -0
- data/lib/api_resource/conditions/multi_object_association_condition.rb +17 -0
- data/lib/api_resource/conditions/scope_condition.rb +11 -0
- data/lib/api_resource/conditions/single_object_association_condition.rb +19 -0
- data/lib/api_resource/connection.rb +18 -12
- data/lib/api_resource/finders.rb +122 -0
- data/lib/api_resource/finders/abstract_finder.rb +89 -0
- data/lib/api_resource/finders/multi_object_association_finder.rb +39 -0
- data/lib/api_resource/finders/resource_finder.rb +33 -0
- data/lib/api_resource/finders/single_object_association_finder.rb +40 -0
- data/lib/api_resource/observing.rb +19 -3
- data/lib/api_resource/scopes.rb +3 -3
- data/lib/api_resource/typecast.rb +85 -0
- data/lib/api_resource/typecasters/array_typecaster.rb +19 -0
- data/lib/api_resource/typecasters/boolean_typecaster.rb +22 -0
- data/lib/api_resource/typecasters/date_typecaster.rb +35 -0
- data/lib/api_resource/typecasters/float_typecaster.rb +22 -0
- data/lib/api_resource/typecasters/integer_typecaster.rb +22 -0
- data/lib/api_resource/typecasters/string_typecaster.rb +19 -0
- data/lib/api_resource/typecasters/time_typecaster.rb +44 -0
- data/lib/api_resource/version.rb +3 -0
- data/spec/lib/api_resource_spec.rb +6 -0
- data/spec/lib/associations/association_scope_spec.rb +1 -1
- data/spec/lib/associations_spec.rb +17 -50
- data/spec/lib/base_spec.rb +22 -0
- data/spec/lib/conditions/abstract_conditions_spec.rb +78 -0
- data/spec/lib/connection_spec.rb +19 -0
- data/spec/lib/finders/multi_object_association_finder_spec.rb +43 -0
- data/spec/lib/finders/resource_finder_spec.rb +89 -0
- data/spec/lib/finders/single_object_association_finder_spec.rb +43 -0
- data/spec/lib/observing_spec.rb +96 -0
- data/spec/lib/typecast_spec.rb +174 -0
- data/spec/lib/typecasters/boolean_typecaster_spec.rb +33 -0
- data/spec/lib/typecasters/date_typecaster_spec.rb +62 -0
- data/spec/lib/typecasters/float_typecaster_spec.rb +39 -0
- data/spec/lib/typecasters/integer_typecaster_spec.rb +40 -0
- data/spec/lib/typecasters/string_typecaster_spec.rb +28 -0
- data/spec/lib/typecasters/time_typecaster_spec.rb +65 -0
- data/spec/spec_helper.rb +0 -1
- metadata +134 -194
- data/VERSION +0 -1
- data/coverage/assets/0.5.3/app.js +0 -88
- data/coverage/assets/0.5.3/fancybox/blank.gif +0 -0
- data/coverage/assets/0.5.3/fancybox/fancy_close.png +0 -0
- data/coverage/assets/0.5.3/fancybox/fancy_loading.png +0 -0
- data/coverage/assets/0.5.3/fancybox/fancy_nav_left.png +0 -0
- data/coverage/assets/0.5.3/fancybox/fancy_nav_right.png +0 -0
- data/coverage/assets/0.5.3/fancybox/fancy_shadow_e.png +0 -0
- data/coverage/assets/0.5.3/fancybox/fancy_shadow_n.png +0 -0
- data/coverage/assets/0.5.3/fancybox/fancy_shadow_ne.png +0 -0
- data/coverage/assets/0.5.3/fancybox/fancy_shadow_nw.png +0 -0
- data/coverage/assets/0.5.3/fancybox/fancy_shadow_s.png +0 -0
- data/coverage/assets/0.5.3/fancybox/fancy_shadow_se.png +0 -0
- data/coverage/assets/0.5.3/fancybox/fancy_shadow_sw.png +0 -0
- data/coverage/assets/0.5.3/fancybox/fancy_shadow_w.png +0 -0
- data/coverage/assets/0.5.3/fancybox/fancy_title_left.png +0 -0
- data/coverage/assets/0.5.3/fancybox/fancy_title_main.png +0 -0
- data/coverage/assets/0.5.3/fancybox/fancy_title_over.png +0 -0
- data/coverage/assets/0.5.3/fancybox/fancy_title_right.png +0 -0
- data/coverage/assets/0.5.3/fancybox/fancybox-x.png +0 -0
- data/coverage/assets/0.5.3/fancybox/fancybox-y.png +0 -0
- data/coverage/assets/0.5.3/fancybox/fancybox.png +0 -0
- data/coverage/assets/0.5.3/fancybox/jquery.fancybox-1.3.1.css +0 -363
- data/coverage/assets/0.5.3/fancybox/jquery.fancybox-1.3.1.pack.js +0 -44
- data/coverage/assets/0.5.3/favicon_green.png +0 -0
- data/coverage/assets/0.5.3/favicon_red.png +0 -0
- data/coverage/assets/0.5.3/favicon_yellow.png +0 -0
- data/coverage/assets/0.5.3/highlight.css +0 -129
- data/coverage/assets/0.5.3/highlight.pack.js +0 -1
- data/coverage/assets/0.5.3/jquery-1.6.2.min.js +0 -18
- data/coverage/assets/0.5.3/jquery.dataTables.min.js +0 -152
- data/coverage/assets/0.5.3/jquery.timeago.js +0 -141
- data/coverage/assets/0.5.3/jquery.url.js +0 -174
- data/coverage/assets/0.5.3/loading.gif +0 -0
- data/coverage/assets/0.5.3/magnify.png +0 -0
- data/coverage/assets/0.5.3/smoothness/images/ui-bg_flat_0_aaaaaa_40x100.png +0 -0
- data/coverage/assets/0.5.3/smoothness/images/ui-bg_flat_75_ffffff_40x100.png +0 -0
- data/coverage/assets/0.5.3/smoothness/images/ui-bg_glass_55_fbf9ee_1x400.png +0 -0
- data/coverage/assets/0.5.3/smoothness/images/ui-bg_glass_65_ffffff_1x400.png +0 -0
- data/coverage/assets/0.5.3/smoothness/images/ui-bg_glass_75_dadada_1x400.png +0 -0
- data/coverage/assets/0.5.3/smoothness/images/ui-bg_glass_75_e6e6e6_1x400.png +0 -0
- data/coverage/assets/0.5.3/smoothness/images/ui-bg_glass_95_fef1ec_1x400.png +0 -0
- data/coverage/assets/0.5.3/smoothness/images/ui-bg_highlight-soft_75_cccccc_1x100.png +0 -0
- data/coverage/assets/0.5.3/smoothness/images/ui-icons_222222_256x240.png +0 -0
- data/coverage/assets/0.5.3/smoothness/images/ui-icons_2e83ff_256x240.png +0 -0
- data/coverage/assets/0.5.3/smoothness/images/ui-icons_454545_256x240.png +0 -0
- data/coverage/assets/0.5.3/smoothness/images/ui-icons_888888_256x240.png +0 -0
- data/coverage/assets/0.5.3/smoothness/images/ui-icons_cd0a0a_256x240.png +0 -0
- data/coverage/assets/0.5.3/smoothness/jquery-ui-1.8.4.custom.css +0 -295
- data/coverage/assets/0.5.3/stylesheet.css +0 -383
- data/coverage/assets/0.7.1/application.css +0 -1110
- data/coverage/assets/0.7.1/application.js +0 -626
- data/coverage/assets/0.7.1/fancybox/blank.gif +0 -0
- data/coverage/assets/0.7.1/fancybox/fancy_close.png +0 -0
- data/coverage/assets/0.7.1/fancybox/fancy_loading.png +0 -0
- data/coverage/assets/0.7.1/fancybox/fancy_nav_left.png +0 -0
- data/coverage/assets/0.7.1/fancybox/fancy_nav_right.png +0 -0
- data/coverage/assets/0.7.1/fancybox/fancy_shadow_e.png +0 -0
- data/coverage/assets/0.7.1/fancybox/fancy_shadow_n.png +0 -0
- data/coverage/assets/0.7.1/fancybox/fancy_shadow_ne.png +0 -0
- data/coverage/assets/0.7.1/fancybox/fancy_shadow_nw.png +0 -0
- data/coverage/assets/0.7.1/fancybox/fancy_shadow_s.png +0 -0
- data/coverage/assets/0.7.1/fancybox/fancy_shadow_se.png +0 -0
- data/coverage/assets/0.7.1/fancybox/fancy_shadow_sw.png +0 -0
- data/coverage/assets/0.7.1/fancybox/fancy_shadow_w.png +0 -0
- data/coverage/assets/0.7.1/fancybox/fancy_title_left.png +0 -0
- data/coverage/assets/0.7.1/fancybox/fancy_title_main.png +0 -0
- data/coverage/assets/0.7.1/fancybox/fancy_title_over.png +0 -0
- data/coverage/assets/0.7.1/fancybox/fancy_title_right.png +0 -0
- data/coverage/assets/0.7.1/fancybox/fancybox-x.png +0 -0
- data/coverage/assets/0.7.1/fancybox/fancybox-y.png +0 -0
- data/coverage/assets/0.7.1/fancybox/fancybox.png +0 -0
- data/coverage/assets/0.7.1/favicon_green.png +0 -0
- data/coverage/assets/0.7.1/favicon_red.png +0 -0
- data/coverage/assets/0.7.1/favicon_yellow.png +0 -0
- data/coverage/assets/0.7.1/loading.gif +0 -0
- data/coverage/assets/0.7.1/magnify.png +0 -0
- data/coverage/assets/0.7.1/smoothness/images/ui-bg_flat_0_aaaaaa_40x100.png +0 -0
- data/coverage/assets/0.7.1/smoothness/images/ui-bg_flat_75_ffffff_40x100.png +0 -0
- data/coverage/assets/0.7.1/smoothness/images/ui-bg_glass_55_fbf9ee_1x400.png +0 -0
- data/coverage/assets/0.7.1/smoothness/images/ui-bg_glass_65_ffffff_1x400.png +0 -0
- data/coverage/assets/0.7.1/smoothness/images/ui-bg_glass_75_dadada_1x400.png +0 -0
- data/coverage/assets/0.7.1/smoothness/images/ui-bg_glass_75_e6e6e6_1x400.png +0 -0
- data/coverage/assets/0.7.1/smoothness/images/ui-bg_glass_95_fef1ec_1x400.png +0 -0
- data/coverage/assets/0.7.1/smoothness/images/ui-bg_highlight-soft_75_cccccc_1x100.png +0 -0
- data/coverage/assets/0.7.1/smoothness/images/ui-icons_222222_256x240.png +0 -0
- data/coverage/assets/0.7.1/smoothness/images/ui-icons_2e83ff_256x240.png +0 -0
- data/coverage/assets/0.7.1/smoothness/images/ui-icons_454545_256x240.png +0 -0
- data/coverage/assets/0.7.1/smoothness/images/ui-icons_888888_256x240.png +0 -0
- data/coverage/assets/0.7.1/smoothness/images/ui-icons_cd0a0a_256x240.png +0 -0
- data/coverage/index.html +0 -3564
- data/lib/api_resource/associations/abstract_scope.rb +0 -191
- data/lib/api_resource/associations/association_scope.rb +0 -47
- data/lib/api_resource/associations/resource_scope.rb +0 -21
- data/lib/api_resource/associations/scope.rb +0 -34
- data/spec/lib/associations/resource_scope_spec.rb +0 -24
- data/spec/tmp/api_resource_test_db.sqlite +0 -0
- data/tmp/rspec_guard_result +0 -1
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
module ApiResource
|
|
2
|
+
|
|
3
|
+
module Finders
|
|
4
|
+
|
|
5
|
+
extend ActiveSupport::Concern
|
|
6
|
+
extend ActiveSupport::Autoload
|
|
7
|
+
|
|
8
|
+
autoload :AbstractFinder
|
|
9
|
+
autoload :ResourceFinder
|
|
10
|
+
autoload :SingleObjectAssociationFinder
|
|
11
|
+
autoload :MultiObjectAssociationFinder
|
|
12
|
+
|
|
13
|
+
module ClassMethods
|
|
14
|
+
|
|
15
|
+
# This decides which finder method to call.
|
|
16
|
+
# It accepts arguments of the form "scope", "options={}"
|
|
17
|
+
# where options can be standard rails options or :expires_in.
|
|
18
|
+
# If :expires_in is set, it caches it for expires_in seconds.
|
|
19
|
+
def find(*arguments)
|
|
20
|
+
|
|
21
|
+
# make sure we have class data before loading
|
|
22
|
+
self.load_resource_definition
|
|
23
|
+
|
|
24
|
+
scope = arguments.slice!(0)
|
|
25
|
+
options = arguments.slice!(0) || {}
|
|
26
|
+
|
|
27
|
+
expiry = options.delete(:expires_in) || ApiResource::Base.ttl || 0
|
|
28
|
+
ApiResource.with_ttl(expiry.to_f) do
|
|
29
|
+
case scope
|
|
30
|
+
when :all then find_every(options)
|
|
31
|
+
when :first then find_every(options).first
|
|
32
|
+
when :last then find_every(options).last
|
|
33
|
+
when :one then find_one(options)
|
|
34
|
+
else find_single(scope, options)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
# A convenience wrapper for <tt>find(:first, *args)</tt>. You can pass
|
|
41
|
+
# in all the same arguments to this method as you can to
|
|
42
|
+
# <tt>find(:first)</tt>.
|
|
43
|
+
def first(*args)
|
|
44
|
+
find(:first, *args)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# A convenience wrapper for <tt>find(:last, *args)</tt>. You can pass
|
|
48
|
+
# in all the same arguments to this method as you can to
|
|
49
|
+
# <tt>find(:last)</tt>.
|
|
50
|
+
def last(*args)
|
|
51
|
+
find(:last, *args)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# This is an alias for find(:all). You can pass in all the same
|
|
55
|
+
# arguments to this method as you can to <tt>find(:all)</tt>
|
|
56
|
+
def all(*args)
|
|
57
|
+
find(:all, *args)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def instantiate_collection(collection)
|
|
61
|
+
collection.collect{|record|
|
|
62
|
+
instantiate_record(record)
|
|
63
|
+
}
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def instantiate_record(record)
|
|
67
|
+
self.load_resource_definition
|
|
68
|
+
ret = self.allocate
|
|
69
|
+
ret.instance_variable_set(
|
|
70
|
+
:@attributes, record.with_indifferent_access
|
|
71
|
+
)
|
|
72
|
+
ret.instance_variable_set(
|
|
73
|
+
:@attributes_cache, HashWithIndifferentAccess.new
|
|
74
|
+
)
|
|
75
|
+
ret
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
private
|
|
79
|
+
|
|
80
|
+
# Find every resource
|
|
81
|
+
def find_every(options)
|
|
82
|
+
begin
|
|
83
|
+
case from = options[:from]
|
|
84
|
+
when Symbol
|
|
85
|
+
instantiate_collection(get(from, options[:params]))
|
|
86
|
+
when String
|
|
87
|
+
path = "#{from}#{query_string(options[:params])}"
|
|
88
|
+
instantiate_collection(connection.get(path, headers) || [])
|
|
89
|
+
else
|
|
90
|
+
prefix_options, query_options = split_options(options[:params])
|
|
91
|
+
path = collection_path(prefix_options, query_options)
|
|
92
|
+
instantiate_collection( (connection.get(path, headers) || []))
|
|
93
|
+
end
|
|
94
|
+
rescue ApiResource::ResourceNotFound
|
|
95
|
+
# Swallowing ResourceNotFound exceptions and return nil - as per
|
|
96
|
+
# ActiveRecord.
|
|
97
|
+
nil
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Find a single resource from a one-off URL
|
|
102
|
+
def find_one(options)
|
|
103
|
+
case from = options[:from]
|
|
104
|
+
when Symbol
|
|
105
|
+
instantiate_record(get(from, options[:params]))
|
|
106
|
+
when String
|
|
107
|
+
path = "#{from}#{query_string(options[:params])}"
|
|
108
|
+
instantiate_record(connection.get(path, headers))
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Find a single resource from the default URL
|
|
113
|
+
def find_single(scope, options)
|
|
114
|
+
prefix_options, query_options = split_options(options[:params])
|
|
115
|
+
path = element_path(scope, prefix_options, query_options)
|
|
116
|
+
instantiate_record(connection.get(path, headers))
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
end
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
|
|
2
|
+
module ApiResource
|
|
3
|
+
|
|
4
|
+
module Finders
|
|
5
|
+
|
|
6
|
+
class AbstractFinder
|
|
7
|
+
|
|
8
|
+
attr_accessor :condition, :klass
|
|
9
|
+
attr_reader :found, :internal_object
|
|
10
|
+
|
|
11
|
+
# TODO: Make this list longer since there are for sure more methods to delegate
|
|
12
|
+
delegate :to_s, :inspect, :reload, :present?, :blank?, :size, :to => :internal_object
|
|
13
|
+
|
|
14
|
+
def initialize(klass, condition)
|
|
15
|
+
@klass = klass
|
|
16
|
+
@condition = condition
|
|
17
|
+
@found = false
|
|
18
|
+
@internal_object = nil
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def find
|
|
22
|
+
raise NotImplementedError("Must be defined in a subclass")
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def internal_object
|
|
26
|
+
# If we've already tried to load return what we've got
|
|
27
|
+
if instance_variable_defined?(:@internal_object)
|
|
28
|
+
return instance_variable_get(:@internal_object)
|
|
29
|
+
end
|
|
30
|
+
# If we haven't tried to load then just call find
|
|
31
|
+
self.find
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# proxy unknown methods to the internal_object
|
|
35
|
+
def method_missing(method, *args, &block)
|
|
36
|
+
self.internal_object.send(method, *args, &block)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
protected
|
|
40
|
+
|
|
41
|
+
# This returns a hash of class_names (given by the condition object)
|
|
42
|
+
# to an array of objects
|
|
43
|
+
def load_includes(id_hash)
|
|
44
|
+
# Quit early if the condition is not eager
|
|
45
|
+
return {} unless self.condition.eager_load?
|
|
46
|
+
# Otherwise go through each class_name that is included, and load the ids
|
|
47
|
+
# given in id_hash, at this point we know all these associations have their
|
|
48
|
+
# proper names
|
|
49
|
+
|
|
50
|
+
hsh = HashWithIndifferentAccess.new
|
|
51
|
+
id_hash = HashWithIndifferentAccess.new(id_hash)
|
|
52
|
+
# load each individually
|
|
53
|
+
self.condition.included_objects.inject(hsh) do |accum, assoc|
|
|
54
|
+
accum[assoc.to_sym] = self.klass.association_class(assoc).find(
|
|
55
|
+
:all,
|
|
56
|
+
:id => id_hash[assoc])
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
hsh
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def apply_includes(objects, includes)
|
|
63
|
+
Array.wrap(objects).each do |obj|
|
|
64
|
+
includes.each_pair do |assoc, vals|
|
|
65
|
+
ids_to_keep = obj.send(obj.class.association_foreign_key_field(assoc))
|
|
66
|
+
to_keep = vals.select{|elm| ids_to_keep.include?(elm.id)}
|
|
67
|
+
obj.send("#{assoc}=", to_keep)
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def build_load_path
|
|
73
|
+
raise "This is not finding an association" unless self.condition.remote_path
|
|
74
|
+
|
|
75
|
+
path = self.condition.remote_path
|
|
76
|
+
# add a format if it doesn't exist and there is no query string yet
|
|
77
|
+
path += ".#{self.klass.format.extension}" unless path =~ /\./ || path =~/\?/
|
|
78
|
+
# add the query string, allowing for other user-provided options in the remote_path if we have options
|
|
79
|
+
unless self.condition.blank_conditions?
|
|
80
|
+
path += (path =~ /\?/ ? "&" : "?") + self.condition.to_query
|
|
81
|
+
end
|
|
82
|
+
path
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
module ApiResource
|
|
2
|
+
|
|
3
|
+
module Finders
|
|
4
|
+
|
|
5
|
+
class MultiObjectAssociationFinder < AbstractFinder
|
|
6
|
+
|
|
7
|
+
def find
|
|
8
|
+
# otherwise just instantiate the record
|
|
9
|
+
unless self.condition.remote_path
|
|
10
|
+
raise "Tried to load association without a remote path"
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
data = self.klass.connection.get(self.build_load_path)
|
|
14
|
+
@loaded = true
|
|
15
|
+
return [] if data.blank?
|
|
16
|
+
|
|
17
|
+
@internal_object = self.klass.instantiate_collection(data)
|
|
18
|
+
|
|
19
|
+
id_hash = self.condition.included_objects.inject({}) do |accum, assoc|
|
|
20
|
+
accum[assoc] = @internal_object.collect do |obj|
|
|
21
|
+
obj.send(obj.class.association_foreign_key_field(assoc))
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
accum[assoc].flatten!
|
|
25
|
+
accum[assoc].uniq!
|
|
26
|
+
accum
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
included_objects = self.load_includes(id_hash)
|
|
30
|
+
|
|
31
|
+
self.apply_includes(@internal_object, included_objects)
|
|
32
|
+
|
|
33
|
+
return @internal_object
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
module ApiResource
|
|
2
|
+
|
|
3
|
+
module Finders
|
|
4
|
+
|
|
5
|
+
class ResourceFinder < AbstractFinder
|
|
6
|
+
|
|
7
|
+
# this is a little bit simpler, it's always a collection and does
|
|
8
|
+
# not require a remote path
|
|
9
|
+
def find
|
|
10
|
+
@loaded = true
|
|
11
|
+
@internal_object = self.klass.find(:all, self.condition.to_hash)
|
|
12
|
+
return [] if @internal_object.blank?
|
|
13
|
+
|
|
14
|
+
id_hash = self.condition.included_objects.inject({}) do |accum, assoc|
|
|
15
|
+
accum[assoc] = @internal_object.collect do |obj|
|
|
16
|
+
obj.send(self.klass.association_foreign_key_field(assoc))
|
|
17
|
+
end
|
|
18
|
+
accum[assoc].flatten!
|
|
19
|
+
accum[assoc].uniq!
|
|
20
|
+
accum
|
|
21
|
+
end
|
|
22
|
+
included_objects = self.load_includes(id_hash)
|
|
23
|
+
|
|
24
|
+
self.apply_includes(@internal_object, included_objects)
|
|
25
|
+
|
|
26
|
+
return @internal_object
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
module ApiResource
|
|
2
|
+
|
|
3
|
+
module Finders
|
|
4
|
+
|
|
5
|
+
class SingleObjectAssociationFinder < AbstractFinder
|
|
6
|
+
|
|
7
|
+
# since it is only a single object we can just load from
|
|
8
|
+
# the service_uri and deal with includes
|
|
9
|
+
def find
|
|
10
|
+
# otherwise just instantiate the record
|
|
11
|
+
unless self.condition.remote_path
|
|
12
|
+
raise "Tried to load association without a remote path"
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
data = self.klass.connection.get(self.build_load_path)
|
|
16
|
+
@loaded = true
|
|
17
|
+
return nil if data.blank?
|
|
18
|
+
@internal_object = self.klass.instantiate_record(data)
|
|
19
|
+
# now that the object is loaded, resolve the includes
|
|
20
|
+
id_hash = self.condition.included_objects.inject({}) do |accum, assoc|
|
|
21
|
+
accum[assoc] = Array.wrap(
|
|
22
|
+
@internal_object.send(
|
|
23
|
+
@internal_object.class.association_foreign_key_field(assoc)
|
|
24
|
+
)
|
|
25
|
+
)
|
|
26
|
+
accum
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
included_objects = self.load_includes(id_hash)
|
|
30
|
+
|
|
31
|
+
self.apply_includes(@internal_object, included_objects)
|
|
32
|
+
|
|
33
|
+
return @internal_object
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
end
|
|
@@ -7,21 +7,37 @@ module ApiResource
|
|
|
7
7
|
|
|
8
8
|
# Redefine these methods to
|
|
9
9
|
included do
|
|
10
|
+
|
|
10
11
|
%w( create save update destroy ).each do |method|
|
|
11
12
|
alias_method_chain method, :observers
|
|
13
|
+
end
|
|
12
14
|
end
|
|
13
15
|
|
|
14
16
|
%w( create save update destroy ).each do |method|
|
|
15
17
|
module_eval <<-EOE, __FILE__, __LINE__ + 1
|
|
16
18
|
def #{method}_with_observers(*args)
|
|
17
|
-
notify_observers(:before_#method)
|
|
18
|
-
|
|
19
|
-
notify_observers(:after_#{method})
|
|
19
|
+
unless notify_observers(:before_#{method})
|
|
20
|
+
return false
|
|
20
21
|
end
|
|
22
|
+
result = #{method}_without_observers(*args)
|
|
23
|
+
notify_observers(:after_#{method}) if result
|
|
21
24
|
return result
|
|
22
25
|
end
|
|
23
26
|
EOE
|
|
24
27
|
end
|
|
28
|
+
|
|
29
|
+
# also need to override notify_observers to return false if
|
|
30
|
+
# ANY of the observers return false, however it will ALWAYS run them all
|
|
31
|
+
def notify_observers(method)
|
|
32
|
+
self.class.observer_instances.inject(true) do |accum, obs|
|
|
33
|
+
obs.update(method, self) && accum
|
|
34
|
+
end
|
|
35
|
+
end
|
|
25
36
|
|
|
26
37
|
end
|
|
38
|
+
|
|
39
|
+
# Blank class here for ease of use, might need
|
|
40
|
+
# some methods some day
|
|
41
|
+
class Observer < ActiveModel::Observer
|
|
42
|
+
end
|
|
27
43
|
end
|
data/lib/api_resource/scopes.rb
CHANGED
|
@@ -4,6 +4,7 @@ module ApiResource
|
|
|
4
4
|
extend ActiveSupport::Concern
|
|
5
5
|
|
|
6
6
|
module ClassMethods
|
|
7
|
+
# TODO: calling these methods should force loading of the resource definition
|
|
7
8
|
def scopes
|
|
8
9
|
return self.related_objects[:scopes]
|
|
9
10
|
end
|
|
@@ -61,9 +62,8 @@ module ApiResource
|
|
|
61
62
|
finder_opts[scope_name] = true
|
|
62
63
|
end
|
|
63
64
|
|
|
64
|
-
ApiResource::
|
|
65
|
-
|
|
66
|
-
)
|
|
65
|
+
ApiResource::Conditions::ScopeCondition.new(finder_opts, self)
|
|
66
|
+
|
|
67
67
|
end
|
|
68
68
|
end
|
|
69
69
|
end
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
require 'api_resource/typecasters/boolean_typecaster'
|
|
2
|
+
require 'api_resource/typecasters/date_typecaster'
|
|
3
|
+
require 'api_resource/typecasters/float_typecaster'
|
|
4
|
+
require 'api_resource/typecasters/integer_typecaster'
|
|
5
|
+
require 'api_resource/typecasters/string_typecaster'
|
|
6
|
+
require 'api_resource/typecasters/time_typecaster'
|
|
7
|
+
require 'api_resource/typecasters/array_typecaster'
|
|
8
|
+
|
|
9
|
+
# Apparently need to require the active_support class_attribute
|
|
10
|
+
require 'active_support/core_ext/class/attribute'
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
module ApiResource
|
|
14
|
+
|
|
15
|
+
module Typecast
|
|
16
|
+
|
|
17
|
+
extend ActiveSupport::Concern
|
|
18
|
+
|
|
19
|
+
included do
|
|
20
|
+
class_attribute :typecasters
|
|
21
|
+
|
|
22
|
+
self.typecasters = self.default_typecasters
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
module ClassMethods
|
|
26
|
+
# Takes a typecaster name (converted to all lowercase) and
|
|
27
|
+
# either a klass (as a constant) or a block to define an anonymous module
|
|
28
|
+
# and registers a new typecaster that defines the method typecast!(value)
|
|
29
|
+
def register_typecaster(name, klass = nil, &block)
|
|
30
|
+
caster = name.to_s.downcase.to_sym
|
|
31
|
+
|
|
32
|
+
if self.typecasters[caster]
|
|
33
|
+
raise ArgumentError, "Typecaster #{name} already exists"
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
if block_given?
|
|
37
|
+
unless klass.nil?
|
|
38
|
+
raise ArgumentError, "Cannot declare a typecaster with a class and a block"
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
klass = Module.new(&block)
|
|
42
|
+
elsif klass.nil?
|
|
43
|
+
raise ArgumentError, "Must specify a typecaster with either a class or a block"
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
unless klass.respond_to?(:from_api) && klass.respond_to?(:to_api)
|
|
47
|
+
raise ArgumentError, "Typecaster must respond to from_api and to_api"
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
self.typecasters[caster] = klass
|
|
51
|
+
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Redefines a typecaster (or defines it if it doesn't exist)
|
|
55
|
+
# basically just delegates to register_typecaster
|
|
56
|
+
def redefine_typecaster!(name, klass = nil, &block)
|
|
57
|
+
caster = name.to_s.downcase.to_sym
|
|
58
|
+
# clone the original typecasters hash so that the override
|
|
59
|
+
# only applies to this class and subclasses
|
|
60
|
+
self.typecasters = self.typecasters.clone
|
|
61
|
+
self.typecasters.delete(caster)
|
|
62
|
+
self.register_typecaster(name, klass, &block)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def default_typecasters
|
|
66
|
+
@default_typecasters ||= {
|
|
67
|
+
:boolean => BooleanTypecaster,
|
|
68
|
+
:bool => BooleanTypecaster,
|
|
69
|
+
:date => DateTypecaster,
|
|
70
|
+
:decimal => FloatTypecaster,
|
|
71
|
+
:float => FloatTypecaster,
|
|
72
|
+
:integer => IntegerTypecaster,
|
|
73
|
+
:int => IntegerTypecaster,
|
|
74
|
+
:string => StringTypecaster,
|
|
75
|
+
:time => TimeTypecaster,
|
|
76
|
+
:datetime => TimeTypecaster,
|
|
77
|
+
:array => ArrayTypecaster,
|
|
78
|
+
}
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
end
|