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.
Files changed (163) hide show
  1. data/.document +5 -0
  2. data/.gitignore +55 -0
  3. data/.rspec +8 -0
  4. data/.travis.yml +9 -0
  5. data/Gemfile +3 -36
  6. data/Gemfile.lock +37 -57
  7. data/Guardfile +7 -1
  8. data/{LICENSE.txt → LICENSE} +4 -2
  9. data/README.md +29 -0
  10. data/README.rdoc +5 -1
  11. data/Rakefile +5 -36
  12. data/api_resource.gemspec +40 -104
  13. data/lib/api_resource.rb +4 -1
  14. data/lib/api_resource/associations.rb +30 -14
  15. data/lib/api_resource/associations/association_proxy.rb +97 -0
  16. data/lib/api_resource/associations/belongs_to_remote_object_proxy.rb +0 -1
  17. data/lib/api_resource/associations/has_many_remote_object_proxy.rb +0 -1
  18. data/lib/api_resource/associations/has_one_remote_object_proxy.rb +7 -4
  19. data/lib/api_resource/associations/multi_object_proxy.rb +11 -16
  20. data/lib/api_resource/associations/single_object_proxy.rb +8 -6
  21. data/lib/api_resource/attributes.rb +29 -41
  22. data/lib/api_resource/base.rb +31 -102
  23. data/lib/api_resource/conditions.rb +36 -0
  24. data/lib/api_resource/conditions/abstract_condition.rb +112 -0
  25. data/lib/api_resource/conditions/association_condition.rb +18 -0
  26. data/lib/api_resource/conditions/include_condition.rb +16 -0
  27. data/lib/api_resource/conditions/multi_object_association_condition.rb +17 -0
  28. data/lib/api_resource/conditions/scope_condition.rb +11 -0
  29. data/lib/api_resource/conditions/single_object_association_condition.rb +19 -0
  30. data/lib/api_resource/connection.rb +18 -12
  31. data/lib/api_resource/finders.rb +122 -0
  32. data/lib/api_resource/finders/abstract_finder.rb +89 -0
  33. data/lib/api_resource/finders/multi_object_association_finder.rb +39 -0
  34. data/lib/api_resource/finders/resource_finder.rb +33 -0
  35. data/lib/api_resource/finders/single_object_association_finder.rb +40 -0
  36. data/lib/api_resource/observing.rb +19 -3
  37. data/lib/api_resource/scopes.rb +3 -3
  38. data/lib/api_resource/typecast.rb +85 -0
  39. data/lib/api_resource/typecasters/array_typecaster.rb +19 -0
  40. data/lib/api_resource/typecasters/boolean_typecaster.rb +22 -0
  41. data/lib/api_resource/typecasters/date_typecaster.rb +35 -0
  42. data/lib/api_resource/typecasters/float_typecaster.rb +22 -0
  43. data/lib/api_resource/typecasters/integer_typecaster.rb +22 -0
  44. data/lib/api_resource/typecasters/string_typecaster.rb +19 -0
  45. data/lib/api_resource/typecasters/time_typecaster.rb +44 -0
  46. data/lib/api_resource/version.rb +3 -0
  47. data/spec/lib/api_resource_spec.rb +6 -0
  48. data/spec/lib/associations/association_scope_spec.rb +1 -1
  49. data/spec/lib/associations_spec.rb +17 -50
  50. data/spec/lib/base_spec.rb +22 -0
  51. data/spec/lib/conditions/abstract_conditions_spec.rb +78 -0
  52. data/spec/lib/connection_spec.rb +19 -0
  53. data/spec/lib/finders/multi_object_association_finder_spec.rb +43 -0
  54. data/spec/lib/finders/resource_finder_spec.rb +89 -0
  55. data/spec/lib/finders/single_object_association_finder_spec.rb +43 -0
  56. data/spec/lib/observing_spec.rb +96 -0
  57. data/spec/lib/typecast_spec.rb +174 -0
  58. data/spec/lib/typecasters/boolean_typecaster_spec.rb +33 -0
  59. data/spec/lib/typecasters/date_typecaster_spec.rb +62 -0
  60. data/spec/lib/typecasters/float_typecaster_spec.rb +39 -0
  61. data/spec/lib/typecasters/integer_typecaster_spec.rb +40 -0
  62. data/spec/lib/typecasters/string_typecaster_spec.rb +28 -0
  63. data/spec/lib/typecasters/time_typecaster_spec.rb +65 -0
  64. data/spec/spec_helper.rb +0 -1
  65. metadata +134 -194
  66. data/VERSION +0 -1
  67. data/coverage/assets/0.5.3/app.js +0 -88
  68. data/coverage/assets/0.5.3/fancybox/blank.gif +0 -0
  69. data/coverage/assets/0.5.3/fancybox/fancy_close.png +0 -0
  70. data/coverage/assets/0.5.3/fancybox/fancy_loading.png +0 -0
  71. data/coverage/assets/0.5.3/fancybox/fancy_nav_left.png +0 -0
  72. data/coverage/assets/0.5.3/fancybox/fancy_nav_right.png +0 -0
  73. data/coverage/assets/0.5.3/fancybox/fancy_shadow_e.png +0 -0
  74. data/coverage/assets/0.5.3/fancybox/fancy_shadow_n.png +0 -0
  75. data/coverage/assets/0.5.3/fancybox/fancy_shadow_ne.png +0 -0
  76. data/coverage/assets/0.5.3/fancybox/fancy_shadow_nw.png +0 -0
  77. data/coverage/assets/0.5.3/fancybox/fancy_shadow_s.png +0 -0
  78. data/coverage/assets/0.5.3/fancybox/fancy_shadow_se.png +0 -0
  79. data/coverage/assets/0.5.3/fancybox/fancy_shadow_sw.png +0 -0
  80. data/coverage/assets/0.5.3/fancybox/fancy_shadow_w.png +0 -0
  81. data/coverage/assets/0.5.3/fancybox/fancy_title_left.png +0 -0
  82. data/coverage/assets/0.5.3/fancybox/fancy_title_main.png +0 -0
  83. data/coverage/assets/0.5.3/fancybox/fancy_title_over.png +0 -0
  84. data/coverage/assets/0.5.3/fancybox/fancy_title_right.png +0 -0
  85. data/coverage/assets/0.5.3/fancybox/fancybox-x.png +0 -0
  86. data/coverage/assets/0.5.3/fancybox/fancybox-y.png +0 -0
  87. data/coverage/assets/0.5.3/fancybox/fancybox.png +0 -0
  88. data/coverage/assets/0.5.3/fancybox/jquery.fancybox-1.3.1.css +0 -363
  89. data/coverage/assets/0.5.3/fancybox/jquery.fancybox-1.3.1.pack.js +0 -44
  90. data/coverage/assets/0.5.3/favicon_green.png +0 -0
  91. data/coverage/assets/0.5.3/favicon_red.png +0 -0
  92. data/coverage/assets/0.5.3/favicon_yellow.png +0 -0
  93. data/coverage/assets/0.5.3/highlight.css +0 -129
  94. data/coverage/assets/0.5.3/highlight.pack.js +0 -1
  95. data/coverage/assets/0.5.3/jquery-1.6.2.min.js +0 -18
  96. data/coverage/assets/0.5.3/jquery.dataTables.min.js +0 -152
  97. data/coverage/assets/0.5.3/jquery.timeago.js +0 -141
  98. data/coverage/assets/0.5.3/jquery.url.js +0 -174
  99. data/coverage/assets/0.5.3/loading.gif +0 -0
  100. data/coverage/assets/0.5.3/magnify.png +0 -0
  101. data/coverage/assets/0.5.3/smoothness/images/ui-bg_flat_0_aaaaaa_40x100.png +0 -0
  102. data/coverage/assets/0.5.3/smoothness/images/ui-bg_flat_75_ffffff_40x100.png +0 -0
  103. data/coverage/assets/0.5.3/smoothness/images/ui-bg_glass_55_fbf9ee_1x400.png +0 -0
  104. data/coverage/assets/0.5.3/smoothness/images/ui-bg_glass_65_ffffff_1x400.png +0 -0
  105. data/coverage/assets/0.5.3/smoothness/images/ui-bg_glass_75_dadada_1x400.png +0 -0
  106. data/coverage/assets/0.5.3/smoothness/images/ui-bg_glass_75_e6e6e6_1x400.png +0 -0
  107. data/coverage/assets/0.5.3/smoothness/images/ui-bg_glass_95_fef1ec_1x400.png +0 -0
  108. data/coverage/assets/0.5.3/smoothness/images/ui-bg_highlight-soft_75_cccccc_1x100.png +0 -0
  109. data/coverage/assets/0.5.3/smoothness/images/ui-icons_222222_256x240.png +0 -0
  110. data/coverage/assets/0.5.3/smoothness/images/ui-icons_2e83ff_256x240.png +0 -0
  111. data/coverage/assets/0.5.3/smoothness/images/ui-icons_454545_256x240.png +0 -0
  112. data/coverage/assets/0.5.3/smoothness/images/ui-icons_888888_256x240.png +0 -0
  113. data/coverage/assets/0.5.3/smoothness/images/ui-icons_cd0a0a_256x240.png +0 -0
  114. data/coverage/assets/0.5.3/smoothness/jquery-ui-1.8.4.custom.css +0 -295
  115. data/coverage/assets/0.5.3/stylesheet.css +0 -383
  116. data/coverage/assets/0.7.1/application.css +0 -1110
  117. data/coverage/assets/0.7.1/application.js +0 -626
  118. data/coverage/assets/0.7.1/fancybox/blank.gif +0 -0
  119. data/coverage/assets/0.7.1/fancybox/fancy_close.png +0 -0
  120. data/coverage/assets/0.7.1/fancybox/fancy_loading.png +0 -0
  121. data/coverage/assets/0.7.1/fancybox/fancy_nav_left.png +0 -0
  122. data/coverage/assets/0.7.1/fancybox/fancy_nav_right.png +0 -0
  123. data/coverage/assets/0.7.1/fancybox/fancy_shadow_e.png +0 -0
  124. data/coverage/assets/0.7.1/fancybox/fancy_shadow_n.png +0 -0
  125. data/coverage/assets/0.7.1/fancybox/fancy_shadow_ne.png +0 -0
  126. data/coverage/assets/0.7.1/fancybox/fancy_shadow_nw.png +0 -0
  127. data/coverage/assets/0.7.1/fancybox/fancy_shadow_s.png +0 -0
  128. data/coverage/assets/0.7.1/fancybox/fancy_shadow_se.png +0 -0
  129. data/coverage/assets/0.7.1/fancybox/fancy_shadow_sw.png +0 -0
  130. data/coverage/assets/0.7.1/fancybox/fancy_shadow_w.png +0 -0
  131. data/coverage/assets/0.7.1/fancybox/fancy_title_left.png +0 -0
  132. data/coverage/assets/0.7.1/fancybox/fancy_title_main.png +0 -0
  133. data/coverage/assets/0.7.1/fancybox/fancy_title_over.png +0 -0
  134. data/coverage/assets/0.7.1/fancybox/fancy_title_right.png +0 -0
  135. data/coverage/assets/0.7.1/fancybox/fancybox-x.png +0 -0
  136. data/coverage/assets/0.7.1/fancybox/fancybox-y.png +0 -0
  137. data/coverage/assets/0.7.1/fancybox/fancybox.png +0 -0
  138. data/coverage/assets/0.7.1/favicon_green.png +0 -0
  139. data/coverage/assets/0.7.1/favicon_red.png +0 -0
  140. data/coverage/assets/0.7.1/favicon_yellow.png +0 -0
  141. data/coverage/assets/0.7.1/loading.gif +0 -0
  142. data/coverage/assets/0.7.1/magnify.png +0 -0
  143. data/coverage/assets/0.7.1/smoothness/images/ui-bg_flat_0_aaaaaa_40x100.png +0 -0
  144. data/coverage/assets/0.7.1/smoothness/images/ui-bg_flat_75_ffffff_40x100.png +0 -0
  145. data/coverage/assets/0.7.1/smoothness/images/ui-bg_glass_55_fbf9ee_1x400.png +0 -0
  146. data/coverage/assets/0.7.1/smoothness/images/ui-bg_glass_65_ffffff_1x400.png +0 -0
  147. data/coverage/assets/0.7.1/smoothness/images/ui-bg_glass_75_dadada_1x400.png +0 -0
  148. data/coverage/assets/0.7.1/smoothness/images/ui-bg_glass_75_e6e6e6_1x400.png +0 -0
  149. data/coverage/assets/0.7.1/smoothness/images/ui-bg_glass_95_fef1ec_1x400.png +0 -0
  150. data/coverage/assets/0.7.1/smoothness/images/ui-bg_highlight-soft_75_cccccc_1x100.png +0 -0
  151. data/coverage/assets/0.7.1/smoothness/images/ui-icons_222222_256x240.png +0 -0
  152. data/coverage/assets/0.7.1/smoothness/images/ui-icons_2e83ff_256x240.png +0 -0
  153. data/coverage/assets/0.7.1/smoothness/images/ui-icons_454545_256x240.png +0 -0
  154. data/coverage/assets/0.7.1/smoothness/images/ui-icons_888888_256x240.png +0 -0
  155. data/coverage/assets/0.7.1/smoothness/images/ui-icons_cd0a0a_256x240.png +0 -0
  156. data/coverage/index.html +0 -3564
  157. data/lib/api_resource/associations/abstract_scope.rb +0 -191
  158. data/lib/api_resource/associations/association_scope.rb +0 -47
  159. data/lib/api_resource/associations/resource_scope.rb +0 -21
  160. data/lib/api_resource/associations/scope.rb +0 -34
  161. data/spec/lib/associations/resource_scope_spec.rb +0 -24
  162. data/spec/tmp/api_resource_test_db.sqlite +0 -0
  163. 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
- if result = #{method}_without_observers(*args)
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
@@ -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::Associations::ResourceScope.new(
65
- self, finder_opts
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