acts_as_api 0.3.2 → 0.3.3

Sign up to get free protection for your applications and to get access to all the features.
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