acts_as_api 0.3.2 → 0.3.3

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/History.txt CHANGED
@@ -1,4 +1,18 @@
1
- === 0.3.0 TO BE RELEASED AS A GEM
1
+ === 0.3.3 2011-04-24
2
+
3
+ * Added support for :if and :unless options
4
+
5
+ === 0.3.2 2011-04-20
6
+
7
+ * Raise an exception if a specified api template is not found
8
+
9
+ === 0.3.1 2011-04-08
10
+
11
+ * Added the :template option to specify sub templates
12
+
13
+ * Fixed a bug concerning extended api templates
14
+
15
+ === 0.3.0 2011-02-22
2
16
 
3
17
  * Added bundler support
4
18
 
data/README.rdoc CHANGED
@@ -35,6 +35,9 @@ A nice introduction about acts_as_api with examples can be found here:
35
35
 
36
36
  http://fabrik42.github.com/acts_as_api
37
37
 
38
+ See the Wiki for a lot of usage examples and features:
39
+
40
+ https://github.com/fabrik42/acts_as_api/wiki
38
41
 
39
42
  == Features:
40
43
 
@@ -1,27 +1,133 @@
1
1
  module ActsAsApi
2
- #
2
+ # Represents an api template for a model.
3
+ # This class should not be initiated by yourself, api templates
4
+ # are created by defining them in the model by calling: +api_accessible+.
5
+ #
6
+ # The api template is configured in the block passed to +api_accessible+.
7
+ #
8
+ # Please note that +ApiTemplate+ inherits from +Hash+ so you can use all
9
+ # kind of +Hash+ and +Enumerable+ methods to manipulate the template.
3
10
  class ApiTemplate < Hash
4
11
 
5
- def options_for(key)
6
- @options[key]
7
- end
8
-
9
- def option_for(key, value)
10
- @options[key][value] if @options[key]
12
+ # The name of the api template as a Symbol.
13
+ attr_accessor :api_template
14
+
15
+ # Returns a new ApiTemplate with the api template name
16
+ # set to the passed template.
17
+ def self.create(template)
18
+ t = ApiTemplate.new
19
+ t.api_template = template
20
+ return t
11
21
  end
12
22
 
23
+ # Adds an item to the api template
24
+ #
25
+ # The value passed can be one of the following:
26
+ # * Symbol - the method with the same name will be called on the model when rendering.
27
+ # * String - must be in the form "method1.method2.method3", will call this method chain.
28
+ # * Hash - will be added as a sub hash and all its items will be resolved the way described above.
29
+ #
30
+ # Possible options to pass:
31
+ # * :template - Determine the template that should be used to render the item if it is
32
+ # +api_accessible+ itself.
13
33
  def add(val, options = {})
14
34
  item_key = (options[:as] || val).to_sym
15
-
35
+
16
36
  self[item_key] = val
17
-
37
+
18
38
  @options ||= {}
19
39
  @options[item_key] = options
20
40
  end
21
41
 
22
- def remove(key)
23
- self.delete(key)
42
+ # Removes an item from the template
43
+ def remove(item)
44
+ self.delete(item)
45
+ end
46
+
47
+ # Returns the options of an item in the api template
48
+ def options_for(item)
49
+ @options[item]
24
50
  end
25
51
 
52
+ # Returns the passed option of an item in the api template
53
+ def option_for(item, option)
54
+ @options[item][option] if @options[item]
55
+ end
56
+
57
+ # If a special template name for the passed item is specified
58
+ # it will be returned, if not the original api template.
59
+ def api_template_for(parent, item)
60
+ return api_template unless parent.is_a? ActsAsApi::ApiTemplate
61
+ parent.option_for(item, :template) || api_template
62
+ end
63
+
64
+ # Decides if the passed item should be added to
65
+ # the response.
66
+ def allowed_to_render?(parent, item, model)
67
+ return true unless parent.is_a? ActsAsApi::ApiTemplate
68
+ allowed = true
69
+ allowed = condition_fulfilled?(model, parent.option_for(item, :if)) if parent.option_for(item, :if)
70
+ allowed = !(condition_fulfilled?(model, parent.option_for(item, :unless))) if parent.option_for(item, :unless)
71
+ return allowed
72
+ end
73
+
74
+ def condition_fulfilled?(model, condition)
75
+ case condition
76
+ when Symbol
77
+ result = model.send(condition)
78
+ when Proc
79
+ result = condition.call(model)
80
+ end
81
+ !result.nil? && !result.is_a?(FalseClass)
82
+ end
83
+
84
+ # Generates a hash that represents the api response based on this
85
+ # template for the passed model instance.
86
+ def to_response_hash(model)
87
+ queue = []
88
+ api_output = {}
89
+ queue << { :parent => api_output, :item => self }
90
+
91
+ until queue.empty? do
92
+ leaf = queue.pop
93
+
94
+ leaf[:item].each do |k,v|
95
+
96
+ next unless allowed_to_render?(leaf[:item], k, model)
97
+
98
+ case v
99
+ when Symbol
100
+ if model.respond_to?(v)
101
+ out = model.send v
102
+ end
103
+
104
+ when Proc
105
+ out = v.call(model)
106
+
107
+ when String
108
+ # go up the call chain
109
+ out = model
110
+ v.split(".").each do |method|
111
+ out = out.send(method.to_sym)
112
+ end
113
+
114
+ when Hash
115
+ leaf[:parent][k] ||= {}
116
+ queue << { :parent => leaf[:parent][k], :item => v}
117
+ next
118
+ end
119
+
120
+ if out.respond_to?(:as_api_response)
121
+ sub_template = api_template_for(leaf[:item], k)
122
+ out = out.send(:as_api_response, sub_template)
123
+ end
124
+
125
+ leaf[:parent][k] = out
126
+ end
127
+ end
128
+
129
+ api_output
130
+ end
131
+
132
+ end
26
133
  end
27
- end
@@ -1,4 +1,4 @@
1
- # The standard ruby Array class is extended by one method.
1
+ # The standard ruby Array class is extended by one instance method.
2
2
  class Array
3
3
 
4
4
  # Neccessary to render an Array of models, e.g. the result of a search.
@@ -32,15 +32,9 @@ module ActsAsApi
32
32
  # *Note*: There is only whitelisting for api accessible attributes.
33
33
  # So once the model acts as api, you have to determine all attributes here that should
34
34
  # be contained in the api responses.
35
- def api_accessible_deprecated(api_templates)
36
- api_templates.each do |api_template, attributes|
37
- write_inheritable_attribute("api_accessible_#{api_template}".to_sym, Set.new(attributes) + (api_accessible_attributes(api_template) || []))
38
- end
39
- end
40
-
41
35
  def api_accessible(api_template, options = {}, &block)
42
36
 
43
- attributes = api_accessible_attributes(api_template) || ApiTemplate.new
37
+ attributes = api_accessible_attributes(api_template) || ApiTemplate.create(api_template)
44
38
 
45
39
  attributes.merge!(api_accessible_attributes(options[:extend])) if options[:extend]
46
40
 
@@ -55,75 +49,22 @@ module ActsAsApi
55
49
  def api_accessible_attributes(api_template)
56
50
  read_inheritable_attribute("api_accessible_#{api_template}".to_sym)
57
51
  end
58
-
59
52
  end
60
53
 
61
54
  module InstanceMethods
62
55
 
63
56
  # Creates the api response of the model and returns it as a Hash.
57
+ # Will raise an exception if the passed api template is not defined for the model
64
58
  def as_api_response(api_template)
65
59
  api_attributes = self.class.api_accessible_attributes(api_template)
66
-
60
+
67
61
  raise "acts_as_api template :#{api_template.to_s} was not found for model #{self.class}" if api_attributes.nil?
68
-
69
- api_output = {}
70
-
71
- return api_output if api_attributes.nil?
72
-
73
- queue = []
74
- queue << { :parent => api_output, :item => api_attributes}
75
-
76
- until queue.empty? do
77
-
78
- leaf = queue.pop
79
-
80
- leaf[:item].each do |k,v|
81
-
82
- if leaf[:item].respond_to?(:option_for)
83
- sub_template = leaf[:item].option_for(k, :template) || api_template
84
- else
85
- sub_template = api_template
86
- end
87
-
88
- case v
89
- when Symbol
90
-
91
- if self.respond_to?(v)
92
- out = send v
93
- end
94
-
95
- when Proc
96
- out = v.call(self)
97
-
98
- when String
99
- # go up the call chain
100
- out = self
101
- v.split(".").each do |method|
102
- out = out.send(method.to_sym)
103
- end
104
-
105
- when Hash
106
- leaf[:parent][k] ||= {}
107
- queue << { :parent => leaf[:parent][k], :item => v}
108
- next
109
- end
110
-
111
- if out.respond_to?(:as_api_response)
112
- out = out.send(:as_api_response, sub_template)
113
- end
114
-
115
- leaf[:parent][k] = out
116
-
117
- end
118
-
119
- end
120
-
121
- api_output
122
-
123
- end
124
-
62
+
63
+ api_attributes.to_response_hash(self)
125
64
  end
126
65
 
127
66
  end
128
-
67
+
129
68
  end
69
+
70
+ end
@@ -23,8 +23,12 @@ module ActsAsApi
23
23
  # automatically
24
24
  attr_accessor_with_default :default_root, :record
25
25
 
26
+ # If true a json response will be automatically wrapped into
27
+ # a JavaScript function call in case a :callback param is passed.
26
28
  attr_accessor_with_default :allow_jsonp_callback, false
27
29
 
30
+ # If true the jsonp function call will get the http status passed
31
+ # as a second parameter
28
32
  attr_accessor_with_default :add_http_status_to_jsonp_response, true
29
33
 
30
34
  end
@@ -1,5 +1,7 @@
1
1
  module ActsAsApi
2
- # Contains rails specific renderers used by acts_as_api
2
+ # Contains rails specific renderers used by acts_as_api to render a jsonp response
3
+ #
4
+ # See ActsAsApi::Config about the possible configurations
3
5
  module RailsRenderer
4
6
 
5
7
  def self.setup
@@ -1,4 +1,3 @@
1
1
  module ActsAsApi
2
-
3
- VERSION = "0.3.2"
2
+ VERSION = "0.3.3"
4
3
  end
data/lib/acts_as_api.rb CHANGED
@@ -1,12 +1,11 @@
1
- require 'set'
2
1
  require 'active_model'
3
2
  require 'active_support/core_ext/class'
4
3
 
5
4
  $:.unshift(File.dirname(__FILE__)) unless
6
5
  $:.include?(File.dirname(__FILE__)) || $:.include?(File.expand_path(File.dirname(__FILE__)))
7
6
 
8
- require "acts_as_api/array"
9
- require "acts_as_api/rails_renderer"
7
+ require "acts_as_api/array"
8
+ require "acts_as_api/rails_renderer"
10
9
 
11
10
  # acts_as_api is a gem that aims to make the construction of JSON and XML
12
11
  # responses in rails 3 easy and fun.
@@ -67,7 +67,7 @@ describe UsersController, :orm => :active_record do
67
67
  get :index, :format => 'json', :api_template => :name_only
68
68
  end
69
69
 
70
- it "should have a root node named users" do
70
+ it "should have a root node named users" do
71
71
  response_body_json.should have_key("users")
72
72
  end
73
73
 
@@ -93,7 +93,7 @@ describe UsersController, :orm => :active_record do
93
93
  get :show, :format => 'json', :api_template => :name_only, :id => @luke.id
94
94
  end
95
95
 
96
- it "should have a root node named user" do
96
+ it "should have a root node named user" do
97
97
  response_body_json.should have_key("user")
98
98
  end
99
99
 
@@ -163,7 +163,7 @@ describe UsersController, :orm => :active_record do
163
163
  get :index, :format => 'json', :api_template => :name_only, :callback => @callback
164
164
  end
165
165
 
166
- it "should wrap the response in the callback" do
166
+ it "should wrap the response in the callback" do
167
167
  response_body_jsonp(@callback).should_not be_nil
168
168
  end
169
169
 
@@ -175,7 +175,7 @@ describe UsersController, :orm => :active_record do
175
175
  get :show, :format => 'json', :api_template => :name_only, :id => @luke.id, :callback => @callback
176
176
  end
177
177
 
178
- it "should wrap the response in the callback" do
178
+ it "should wrap the response in the callback" do
179
179
  response_body_jsonp(@callback).should_not be_nil
180
180
  end
181
181
 
@@ -184,5 +184,4 @@ describe UsersController, :orm => :active_record do
184
184
  end
185
185
  end
186
186
 
187
-
188
187
  end
@@ -0,0 +1,284 @@
1
+ require File.dirname(__FILE__) + '/../../spec_helper.rb'
2
+
3
+ describe ActsAsApi::Base do
4
+
5
+ describe "including an association in the api template", :orm => :active_record do
6
+
7
+ before(:each) do
8
+ setup_models
9
+ end
10
+
11
+ after(:each) do
12
+ clean_up
13
+ end
14
+
15
+ describe "which doesn't acts_as_api" do
16
+
17
+ before(:each) do
18
+ @response = @luke.as_api_response(:include_tasks)
19
+ end
20
+
21
+ it "returns a hash" do
22
+ @response.should be_kind_of(Hash)
23
+ end
24
+
25
+ it "returns the correct number of fields" do
26
+ @response.should have(1).key
27
+ end
28
+
29
+ it "returns all specified fields" do
30
+ @response.keys.should include(:tasks)
31
+ end
32
+
33
+ it "returns the correct values for the specified fields" do
34
+ @response[:tasks].should be_an Array
35
+ @response[:tasks].should have(3).tasks
36
+ end
37
+
38
+ it "should contain the associated sub models" do
39
+ @response[:tasks].should include(@destroy_deathstar, @study_with_yoda, @win_rebellion)
40
+ end
41
+ end
42
+
43
+ describe "which does acts_as_api" do
44
+
45
+ context "has_many" do
46
+
47
+ before(:each) do
48
+ Task.acts_as_api
49
+ Task.api_accessible :include_tasks do |t|
50
+ t.add :heading
51
+ t.add :done
52
+ end
53
+ @response = @luke.as_api_response(:include_tasks)
54
+ end
55
+
56
+ it "returns a hash" do
57
+ @response.should be_kind_of(Hash)
58
+ end
59
+
60
+ it "returns the correct number of fields" do
61
+ @response.should have(1).key
62
+ end
63
+
64
+ it "returns all specified fields" do
65
+ @response.keys.should include(:tasks)
66
+ end
67
+
68
+ it "returns the correct values for the specified fields" do
69
+ @response[:tasks].should be_an Array
70
+ @response[:tasks].should have(3).tasks
71
+ end
72
+
73
+ it "contains the associated child models with the determined api template" do
74
+ @response[:tasks].each do |task|
75
+ task.keys.should include(:heading, :done)
76
+ task.keys.should have(2).attributes
77
+ end
78
+ end
79
+
80
+ it "contains the correct data of the child models" do
81
+ task_hash = [ @destroy_deathstar, @study_with_yoda, @win_rebellion ].collect{|t| { :done => t.done, :heading => t.heading } }
82
+ @response[:tasks].should eql task_hash
83
+ end
84
+ end
85
+
86
+ context "has_one" do
87
+
88
+ before(:each) do
89
+ Profile.acts_as_api
90
+ Profile.api_accessible :include_profile do |t|
91
+ t.add :avatar
92
+ t.add :homepage
93
+ end
94
+ @response = @luke.as_api_response(:include_profile)
95
+ end
96
+
97
+ it "returns a hash" do
98
+ @response.should be_kind_of(Hash)
99
+ end
100
+
101
+ it "returns the correct number of fields" do
102
+ @response.should have(1).key
103
+ end
104
+
105
+ it "returns all specified fields" do
106
+ @response.keys.should include(:profile)
107
+ end
108
+
109
+ it "returns the correct values for the specified fields" do
110
+ @response[:profile].should be_a Hash
111
+ @response[:profile].should have(2).attributes
112
+ end
113
+
114
+ it "contains the associated child models with the determined api template" do
115
+ @response[:profile].keys.should include(:avatar, :homepage)
116
+ end
117
+
118
+ it "contains the correct data of the child models" do
119
+ profile_hash = { :avatar => @luke.profile.avatar, :homepage => @luke.profile.homepage }
120
+ @response[:profile].should eql profile_hash
121
+ end
122
+
123
+ end
124
+ end
125
+
126
+ describe "which does acts_as_api, but with using another template name" do
127
+
128
+ before(:each) do
129
+ Task.acts_as_api
130
+ Task.api_accessible :other_template do |t|
131
+ t.add :description
132
+ t.add :time_spent
133
+ end
134
+ @response = @luke.as_api_response(:other_sub_template)
135
+ end
136
+
137
+ it "returns a hash" do
138
+ @response.should be_kind_of(Hash)
139
+ end
140
+
141
+ it "returns the correct number of fields" do
142
+ @response.should have(2).keys
143
+ end
144
+
145
+ it "returns all specified fields" do
146
+ @response.keys.should include(:first_name)
147
+ end
148
+
149
+ it "returns the correct values for the specified fields" do
150
+ @response.values.should include(@luke.first_name)
151
+ end
152
+
153
+ it "returns all specified fields" do
154
+ @response.keys.should include(:tasks)
155
+ end
156
+
157
+ it "returns the correct values for the specified fields" do
158
+ @response[:tasks].should be_an Array
159
+ @response[:tasks].should have(3).tasks
160
+ end
161
+
162
+ it "contains the associated child models with the determined api template" do
163
+ @response[:tasks].each do |task|
164
+ task.keys.should include(:description, :time_spent)
165
+ task.keys.should have(2).attributes
166
+ end
167
+ end
168
+
169
+ it "contains the correct data of the child models" do
170
+ task_hash = [ @destroy_deathstar, @study_with_yoda, @win_rebellion ].collect{|t| { :description => t.description, :time_spent => t.time_spent } }
171
+ @response[:tasks].should eql task_hash
172
+ end
173
+ end
174
+
175
+ describe "that is scoped" do
176
+
177
+ before(:each) do
178
+ # extend task model with scope
179
+ class Task < ActiveRecord::Base
180
+ scope :completed, where(:done => true)
181
+ end
182
+ Task.acts_as_api
183
+ Task.api_accessible :include_completed_tasks do |t|
184
+ t.add :heading
185
+ t.add :done
186
+ end
187
+
188
+ @response = @luke.as_api_response(:include_completed_tasks)
189
+ end
190
+
191
+ it "returns a hash" do
192
+ @response.should be_kind_of(Hash)
193
+ end
194
+
195
+ it "returns the correct number of fields" do
196
+ @response.should have(1).key
197
+ end
198
+
199
+ it "returns all specified fields" do
200
+ @response.keys.should include(:completed_tasks)
201
+ end
202
+
203
+ it "returns the correct values for the specified fields" do
204
+ @response[:completed_tasks].should be_an Array
205
+ @response[:completed_tasks].should have(2).tasks
206
+ end
207
+
208
+ it "contains the associated child models with the determined api template" do
209
+ @response[:completed_tasks].each do |task|
210
+ task.keys.should include(:heading, :done)
211
+ task.keys.should have(2).attributes
212
+ end
213
+ end
214
+
215
+ it "contains the correct data of the child models" do
216
+ task_hash = [ @destroy_deathstar, @study_with_yoda ].collect{|t| { :done => t.done, :heading => t.heading } }
217
+ @response[:completed_tasks].should eql task_hash
218
+ end
219
+ end
220
+
221
+ describe "handling nil values" do
222
+
223
+ context "has_many" do
224
+
225
+ before(:each) do
226
+ Task.acts_as_api
227
+ Task.api_accessible :include_tasks do |t|
228
+ t.add :heading
229
+ t.add :done
230
+ end
231
+ @response = @han.as_api_response(:include_tasks)
232
+ end
233
+
234
+ it "returns a hash" do
235
+ @response.should be_kind_of(Hash)
236
+ end
237
+
238
+ it "returns the correct number of fields" do
239
+ @response.should have(1).key
240
+ end
241
+
242
+ it "returns all specified fields" do
243
+ @response.keys.should include(:tasks)
244
+ end
245
+
246
+ it "returns the correct values for the specified fields" do
247
+ @response[:tasks].should be_kind_of(Array)
248
+ end
249
+
250
+ it "contains no associated child models" do
251
+ @response[:tasks].should have(0).items
252
+ end
253
+
254
+ end
255
+
256
+ context "has one" do
257
+ before(:each) do
258
+ Profile.acts_as_api
259
+ Profile.api_accessible :include_profile do |t|
260
+ t.add :avatar
261
+ t.add :homepage
262
+ end
263
+ @response = @han.as_api_response(:include_profile)
264
+ end
265
+
266
+ it "returns a hash" do
267
+ @response.should be_kind_of(Hash)
268
+ end
269
+
270
+ it "returns the correct number of fields" do
271
+ @response.should have(1).key
272
+ end
273
+
274
+ it "returns all specified fields" do
275
+ @response.keys.should include(:profile)
276
+ end
277
+
278
+ it "returns nil for the association" do
279
+ @response[:profile].should be_nil
280
+ end
281
+ end
282
+ end
283
+ end
284
+ end