apipie-rails 0.0.17 → 0.0.18

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore CHANGED
@@ -5,3 +5,5 @@ spec/dummy/db/*.sqlite3
5
5
  spec/dummy/log/*.log
6
6
  spec/dummy/tmp/
7
7
  .idea
8
+ Gemfile.lock
9
+ .rvmrc
data/CHANGELOG CHANGED
@@ -2,6 +2,12 @@
2
2
  Changelog
3
3
  ===========
4
4
 
5
+ v0.0.18
6
+ -------
7
+
8
+ * ``param_group`` and ``def_param_group`` keywords
9
+ * ``:action_aware`` options for reusing param groups for create/update actions
10
+
5
11
  v0.0.17
6
12
  -------
7
13
 
data/README.rst CHANGED
@@ -266,6 +266,111 @@ Example:
266
266
  #...
267
267
  end
268
268
 
269
+ DRY with param_group
270
+ --------------------
271
+
272
+ Often, params occur together in more actions. Typically, most of the
273
+ params for ``create`` and ``update`` actions are common for both of
274
+ them.
275
+
276
+ This params can be extracted with ``def_param_group`` and
277
+ ``param_group`` keywords.
278
+
279
+ The definition is looked up in the scope of the controller. If the
280
+ group is defined in a different controller, it might be referenced by
281
+ specifying the second argument.
282
+
283
+ Example:
284
+ ~~~~~~~~
285
+
286
+ .. code:: ruby
287
+
288
+ # v1/users_controller.rb
289
+ def_param_group :address do
290
+ param :street, String
291
+ param :number, Integer
292
+ param :zip, String
293
+ end
294
+
295
+ def_param_group :user do
296
+ param :user, Hash do
297
+ param :name, String, "Name of the user"
298
+ param_group :address
299
+ end
300
+ end
301
+
302
+ api :POST, "/users", "Create an user"
303
+ param_group :user
304
+ def create
305
+ # ...
306
+ end
307
+
308
+ api :PUT, "/users/:id", "Update an user"
309
+ param_group :user
310
+ def update
311
+ # ...
312
+ end
313
+
314
+ # v2/users_controller.rb
315
+ api :POST, "/users", "Create an user"
316
+ param_group :user, V1::UsersController
317
+ def create
318
+ # ...
319
+ end
320
+
321
+ Action Aware params
322
+ -------------------
323
+
324
+ In CRUD operations, this pattern occurs quite often: params that need
325
+ to be set are:
326
+
327
+ * for create action: ``required => true`` and ``allow_nil => false``
328
+ * for update action: ``required => false`` and ``allow_nil => false``
329
+
330
+ This makes it hard to share the param definitions across theses
331
+ actions. Therefore, you can make the description a bit smarter by
332
+ setting ``:action_aware => true``.
333
+
334
+ You can specify explicitly how the param group should be evaluated
335
+ with ``:as`` option (either :create or :update)
336
+
337
+ Example
338
+ ~~~~~~~
339
+
340
+ .. code:: ruby
341
+
342
+ def_param_group :user do
343
+ param :user, Hash, :action_aware => true do
344
+ param :name, String, :required => true
345
+ param :description, :String
346
+ end
347
+ end
348
+
349
+ api :POST, "/users", "Create an user"
350
+ param_group :user
351
+ def create
352
+ # ...
353
+ end
354
+
355
+ api :PUT, "/users/admin", "Create an admin"
356
+ param_group :user, :as => :create
357
+ def create_admin
358
+ # ...
359
+ end
360
+
361
+ api :PUT, "/users/:id", "Update an user"
362
+ param_group :user
363
+ def update
364
+ # ...
365
+ end
366
+
367
+ In this case, ``user[name]`` will be not be allowed nil for all
368
+ actions and required only for ``create`` and ``create_admin``. Params
369
+ with ``allow_nil`` set explicitly don't have this value changed.
370
+
371
+ Action awareness is being inherited from ancestors (in terms of
372
+ nested params).
373
+
269
374
 
270
375
  =========================
271
376
  Configuration Reference
@@ -564,7 +669,7 @@ Textile
564
669
  Use Apipie::Markup::Textile.new. You need RedCloth gem.
565
670
 
566
671
  Or provide you own object with ``to_html(text)`` method.
567
- For inspiration this is how Textile look like:
672
+ For inspiration this is how Textile markup usage looks like:
568
673
 
569
674
  .. code:: ruby
570
675
 
@@ -1,6 +1,7 @@
1
1
  require "apipie/routing"
2
2
  require "apipie/markup"
3
3
  require "apipie/apipie_module"
4
+ require "apipie/dsl_definition"
4
5
  require "apipie/configuration"
5
6
  require "apipie/method_description"
6
7
  require "apipie/resource_description"
@@ -9,7 +10,6 @@ require "apipie/errors"
9
10
  require "apipie/error_description"
10
11
  require "apipie/see_description"
11
12
  require "apipie/validator"
12
- require "apipie/dsl_definition"
13
13
  require "apipie/railtie"
14
14
  require 'apipie/extractor'
15
15
  require "apipie/version"
@@ -12,14 +12,11 @@ module Apipie
12
12
  end
13
13
  end
14
14
 
15
- attr_accessor :last_dsl_data
16
-
17
15
  attr_reader :resource_descriptions
18
16
 
19
17
  def initialize
20
18
  super
21
19
  init_env
22
- clear_last
23
20
  end
24
21
 
25
22
  def available_versions
@@ -31,10 +28,11 @@ module Apipie
31
28
  end
32
29
 
33
30
  # create new method api description
34
- def define_method_description(controller, method_name, versions = [])
31
+ def define_method_description(controller, method_name, dsl_data)
35
32
  return if ignored?(controller, method_name)
36
33
  ret_method_description = nil
37
34
 
35
+ versions = dsl_data[:api_versions] || []
38
36
  versions = controller_versions(controller) if versions.empty?
39
37
 
40
38
  versions.each do |version|
@@ -45,7 +43,7 @@ module Apipie
45
43
  resource_description = define_resource_description(controller, version)
46
44
  end
47
45
 
48
- method_description = Apipie::MethodDescription.new(method_name, resource_description, self)
46
+ method_description = Apipie::MethodDescription.new(method_name, resource_description, dsl_data)
49
47
 
50
48
  # we create separate method description for each version in
51
49
  # case the method belongs to more versions. We return just one
@@ -95,17 +93,18 @@ module Apipie
95
93
  @controller_versions[controller] = versions
96
94
  end
97
95
 
98
- def add_method_description_args(method, path, desc)
99
- @last_dsl_data[:api_args] << MethodDescription::Api.new(method, path, desc)
100
- end
101
-
102
- def add_example(example)
103
- @last_dsl_data[:examples] << example.strip_heredoc
96
+ def add_param_group(controller, name, &block)
97
+ key = "#{controller.controller_path}##{name}"
98
+ @param_groups[key] = block
104
99
  end
105
100
 
106
- # check if there is some saved description
107
- def apipie_provided?
108
- true unless last_dsl_data[:api_args].blank?
101
+ def get_param_group(controller, name)
102
+ key = "#{controller.controller_path}##{name}"
103
+ if @param_groups.has_key?(key)
104
+ return @param_groups[key]
105
+ else
106
+ raise "param group #{key} not defined"
107
+ end
109
108
  end
110
109
 
111
110
  # get api for given method
@@ -198,25 +197,11 @@ module Apipie
198
197
  def init_env
199
198
  @resource_descriptions = HashWithIndifferentAccess.new { |h, version| h[version] = {} }
200
199
  @controller_to_resource_id = {}
200
+ @param_groups = {}
201
201
 
202
202
  # what versions does the controller belong in (specified by resource_description)?
203
203
  @controller_versions = Hash.new { |h, controller| h[controller] = [] }
204
204
  end
205
- # clear all saved data
206
- def clear_last
207
- @last_dsl_data = {
208
- :api_args => [],
209
- :errors => [],
210
- :params => [],
211
- :resouce_id => nil,
212
- :short_description => nil,
213
- :description => nil,
214
- :examples => [],
215
- :see => [],
216
- :formats => nil,
217
- :api_versions => []
218
- }
219
- end
220
205
 
221
206
  def recorded_examples
222
207
  return @recorded_examples if @recorded_examples
@@ -5,45 +5,36 @@ module Apipie
5
5
  # DSL is a module that provides #api, #error, #param, #error.
6
6
  module DSL
7
7
 
8
- attr_reader :apipie_resource_descriptions
9
-
10
- private
11
-
12
- # Describe whole resource
13
- #
14
- # Example:
15
- # api :desc => "Show user profile", :path => "/users/", :version => '1.0 - 3.4.2012'
16
- # param :id, Fixnum, :desc => "User ID", :required => true
17
- # desc <<-EOS
18
- # Long description...
19
- # EOS
20
- def resource_description(options = {}, &block) #:doc:
21
- return unless Apipie.active_dsl?
22
- raise ArgumentError, "Block expected" unless block_given?
23
-
24
- dsl_data = ResourceDescriptionDsl.eval_dsl(self, &block)
25
- versions = dsl_data[:api_versions]
26
- @apipie_resource_descriptions = versions.map do |version|
27
- Apipie.define_resource_description(self, version, dsl_data)
28
- end
29
- Apipie.set_controller_versions(self, versions)
30
- end
8
+ module Base
9
+ attr_reader :apipie_resource_descriptions
31
10
 
32
- class ResourceDescriptionDsl
33
- include Apipie::DSL
11
+ private
34
12
 
35
- NO_RESOURCE_KEYWORDS = %w[api see example]
13
+ def _apipie_dsl_data
14
+ @_apipie_dsl_data ||= _apipie_dsl_data_init
15
+ end
36
16
 
37
- NO_RESOURCE_KEYWORDS.each do |method|
38
- define_method method do
39
- raise "#{method} is not defined for resource description"
40
- end
17
+ def _apipie_dsl_data_clear
18
+ @_apipie_dsl_data = nil
41
19
  end
42
20
 
43
- def initialize(controller)
44
- @controller = controller
21
+ def _apipie_dsl_data_init
22
+ @_apipie_dsl_data = {
23
+ :api_args => [],
24
+ :errors => [],
25
+ :params => [],
26
+ :resouce_id => nil,
27
+ :short_description => nil,
28
+ :description => nil,
29
+ :examples => [],
30
+ :see => [],
31
+ :formats => nil,
32
+ :api_versions => []
33
+ }
45
34
  end
35
+ end
46
36
 
37
+ module Resource
47
38
  # by default, the resource id is derived from controller_name
48
39
  # it can be overwritten with.
49
40
  #
@@ -53,171 +44,248 @@ module Apipie
53
44
  end
54
45
 
55
46
  def name(name)
56
- Apipie.last_dsl_data[:resource_name] = name
47
+ _apipie_dsl_data[:resource_name] = name
57
48
  end
58
49
 
59
50
  def api_base_url(url)
60
- Apipie.last_dsl_data[:api_base_url] = url
51
+ _apipie_dsl_data[:api_base_url] = url
61
52
  end
62
53
 
63
54
  def short(short)
64
- Apipie.last_dsl_data[:short_description] = short
55
+ _apipie_dsl_data[:short_description] = short
65
56
  end
66
57
  alias :short_description :short
67
58
 
68
59
  def path(path)
69
- Apipie.last_dsl_data[:path] = path
60
+ _apipie_dsl_data[:path] = path
70
61
  end
71
62
 
72
63
  def app_info(app_info)
73
- Apipie.last_dsl_data[:app_info] = app_info
64
+ _apipie_dsl_data[:app_info] = app_info
74
65
  end
66
+ end
75
67
 
76
- # evaluates resource description DSL and returns results
77
- def self.eval_dsl(controller, &block)
78
- self.new(controller).instance_eval(&block)
79
- dsl_data = Apipie.last_dsl_data
80
- if dsl_data[:api_versions].empty?
81
- dsl_data[:api_versions] = Apipie.controller_versions(controller)
68
+ module Action
69
+
70
+ def def_param_group(name, &block)
71
+ Apipie.add_param_group(self, name, &block)
72
+ end
73
+
74
+ # Declare an api.
75
+ #
76
+ # Example:
77
+ # api :GET, "/resource_route", "short description",
78
+ #
79
+ def api(method, path, desc = nil) #:doc:
80
+ return unless Apipie.active_dsl?
81
+ _apipie_dsl_data[:api_args] << [method, path, desc]
82
+ end
83
+
84
+ # Reference other similar method
85
+ #
86
+ # api :PUT, '/articles/:id'
87
+ # see "articles#create"
88
+ # def update; end
89
+ def see(*args)
90
+ return unless Apipie.active_dsl?
91
+ _apipie_dsl_data[:see] << args
92
+ end
93
+
94
+ # Show some example of what does the described
95
+ # method return.
96
+ def example(example) #:doc:
97
+ return unless Apipie.active_dsl?
98
+ _apipie_dsl_data[:examples] << example.strip_heredoc
99
+ end
100
+
101
+ # Describe whole resource
102
+ #
103
+ # Example:
104
+ # api :desc => "Show user profile", :path => "/users/", :version => '1.0 - 3.4.2012'
105
+ # param :id, Fixnum, :desc => "User ID", :required => true
106
+ # desc <<-EOS
107
+ # Long description...
108
+ # EOS
109
+ def resource_description(options = {}, &block) #:doc:
110
+ return unless Apipie.active_dsl?
111
+ raise ArgumentError, "Block expected" unless block_given?
112
+
113
+ dsl_data = ResourceDescriptionDsl.eval_dsl(self, &block)
114
+ versions = dsl_data[:api_versions]
115
+ @apipie_resource_descriptions = versions.map do |version|
116
+ Apipie.define_resource_description(self, version, dsl_data)
82
117
  end
83
- dsl_data
84
- ensure
85
- Apipie.clear_last
118
+ Apipie.set_controller_versions(self, versions)
86
119
  end
87
- end
88
120
 
121
+ # create method api and redefine newly added method
122
+ def method_added(method_name) #:doc:
123
+ super
89
124
 
90
- # Declare an api.
91
- #
92
- # Example:
93
- # api :GET, "/resource_route", "short description",
94
- #
95
- def api(method, path, desc = nil) #:doc:
96
- return unless Apipie.active_dsl?
97
- Apipie.add_method_description_args(method, path, desc)
98
- end
125
+ if ! Apipie.active_dsl? || _apipie_dsl_data[:api_args].blank?
126
+ _apipie_dsl_data_clear
127
+ return
128
+ end
99
129
 
100
- def api_versions(*versions)
101
- Apipie.last_dsl_data[:api_versions].concat(versions)
102
- end
103
- alias :api_version :api_versions
104
-
105
- # Describe the next method.
106
- #
107
- # Example:
108
- # desc "print hello world"
109
- # def hello_world
110
- # puts "hello world"
111
- # end
112
- #
113
- def desc(description) #:doc:
114
- return unless Apipie.active_dsl?
115
- if Apipie.last_dsl_data[:description]
116
- raise "Double method description."
117
- end
118
- Apipie.last_dsl_data[:description] = description
119
- end
120
- alias :description :desc
121
- alias :full_description :desc
122
-
123
- # Reference other similar method
124
- #
125
- # api :PUT, '/articles/:id'
126
- # see "articles#create"
127
- # def update; end
128
- def see(*args)
129
- return unless Apipie.active_dsl?
130
- Apipie.last_dsl_data[:see] << Apipie::SeeDescription.new(args)
131
- end
130
+ begin
131
+ # remove method description if exists and create new one
132
+ Apipie.remove_method_description(self, _apipie_dsl_data[:api_versions], method_name)
133
+ description = Apipie.define_method_description(self, method_name, _apipie_dsl_data)
134
+ ensure
135
+ _apipie_dsl_data_clear
136
+ end
132
137
 
133
- # Show some example of what does the described
134
- # method return.
135
- def example(example) #:doc:
136
- return unless Apipie.active_dsl?
137
- Apipie.add_example(example)
138
- end
138
+ # redefine method only if validation is turned on
139
+ if description && Apipie.configuration.validate == true
139
140
 
140
- # Describe available request/response formats
141
- #
142
- # formats ['json', 'jsonp', 'xml']
143
- def formats(formats) #:doc:
144
- return unless Apipie.active_dsl?
145
- Apipie.last_dsl_data[:formats] = formats
146
- end
141
+ old_method = instance_method(method_name)
147
142
 
148
- # Describe possible errors
149
- #
150
- # Example:
151
- # error :desc => "speaker is sleeping", :code => 500
152
- # error 500, "speaker is sleeping"
153
- # def hello_world
154
- # return 500 if self.speaker.sleeping?
155
- # puts "hello world"
156
- # end
157
- #
158
- def error(*args) #:doc:
159
- return unless Apipie.active_dsl?
160
- Apipie.last_dsl_data[:errors] << Apipie::ErrorDescription.new(args)
161
- end
143
+ define_method(method_name) do |*args|
162
144
 
163
- # Describe method's parameter
164
- #
165
- # Example:
166
- # param :greeting, String, :desc => "arbitrary text", :required => true
167
- # def hello_world(greeting)
168
- # puts greeting
169
- # end
170
- #
171
- def param(param_name, validator, desc_or_options = nil, options = {}, &block) #:doc:
172
- return unless Apipie.active_dsl?
173
- Apipie.last_dsl_data[:params] << Apipie::ParamDescription.new(param_name, validator, desc_or_options, options, &block)
145
+ if Apipie.configuration.validate == true
146
+ description.params.each do |_, param|
147
+
148
+ # check if required parameters are present
149
+ if param.required && !params.has_key?(param.name)
150
+ raise ParamMissing.new(param.name)
151
+ end
152
+
153
+ # params validations
154
+ if params.has_key?(param.name)
155
+ param.validate(params[:"#{param.name}"])
156
+ end
157
+
158
+ end
159
+ end
160
+
161
+ # run the original method code
162
+ old_method.bind(self).call(*args)
163
+ end
164
+
165
+ end
166
+ end # def method_added
174
167
  end
175
168
 
176
- # create method api and redefine newly added method
177
- def method_added(method_name) #:doc:
178
- super
169
+ module Common
170
+ def api_versions(*versions)
171
+ _apipie_dsl_data[:api_versions].concat(versions)
172
+ end
173
+ alias :api_version :api_versions
179
174
 
180
- if ! Apipie.active_dsl? || ! Apipie.apipie_provided?
181
- Apipie.clear_last
182
- return
175
+ # Describe the next method.
176
+ #
177
+ # Example:
178
+ # desc "print hello world"
179
+ # def hello_world
180
+ # puts "hello world"
181
+ # end
182
+ #
183
+ def desc(description) #:doc:
184
+ return unless Apipie.active_dsl?
185
+ if _apipie_dsl_data[:description]
186
+ raise "Double method description."
187
+ end
188
+ _apipie_dsl_data[:description] = description
183
189
  end
190
+ alias :description :desc
191
+ alias :full_description :desc
184
192
 
185
- begin
186
- # remove method description if exists and create new one
187
- Apipie.remove_method_description(self, Apipie.last_dsl_data[:api_versions], method_name)
188
- description = Apipie.define_method_description(self, method_name, Apipie.last_dsl_data[:api_versions])
189
- ensure
190
- Apipie.clear_last
193
+ # Describe available request/response formats
194
+ #
195
+ # formats ['json', 'jsonp', 'xml']
196
+ def formats(formats) #:doc:
197
+ return unless Apipie.active_dsl?
198
+ _apipie_dsl_data[:formats] = formats
191
199
  end
192
200
 
193
- # redefine method only if validation is turned on
194
- if description && Apipie.configuration.validate == true
201
+ # Describe possible errors
202
+ #
203
+ # Example:
204
+ # error :desc => "speaker is sleeping", :code => 500
205
+ # error 500, "speaker is sleeping"
206
+ # def hello_world
207
+ # return 500 if self.speaker.sleeping?
208
+ # puts "hello world"
209
+ # end
210
+ #
211
+ def error(*args) #:doc:
212
+ return unless Apipie.active_dsl?
213
+ _apipie_dsl_data[:errors] << args
214
+ end
195
215
 
196
- old_method = instance_method(method_name)
216
+ end
197
217
 
198
- define_method(method_name) do |*args|
218
+ # this describes the params, it's in separate module because it's
219
+ # used in Validators as well
220
+ module Param
221
+ # Describe method's parameter
222
+ #
223
+ # Example:
224
+ # param :greeting, String, :desc => "arbitrary text", :required => true
225
+ # def hello_world(greeting)
226
+ # puts greeting
227
+ # end
228
+ #
229
+ def param(param_name, validator, desc_or_options = nil, options = {}, &block) #:doc:
230
+ return unless Apipie.active_dsl?
231
+ _apipie_dsl_data[:params] << [param_name,
232
+ validator,
233
+ desc_or_options,
234
+ options.merge(:param_group => @_current_param_group),
235
+ block]
236
+ end
199
237
 
200
- if Apipie.configuration.validate == true
201
- description.params.each do |_, param|
238
+ # Reuses param group for this method. The definition is looked up
239
+ # in scope of this controller. If the group was defined in
240
+ # different controller, the second param can be used to specify it.
241
+ # when using action_aware parmas, you can specify :as =>
242
+ # :create or :update to explicitly say how it should behave
243
+ def param_group(name, scope_or_options = nil, options = {})
244
+ if scope_or_options.is_a? Hash
245
+ options.merge!(scope_or_options)
246
+ scope = options[:scope]
247
+ else
248
+ scope = scope_or_options
249
+ end
250
+ scope ||= _default_param_group_scope
251
+ @_current_param_group = {:scope => scope, :name => name, :options => options}
252
+ self.instance_exec(&Apipie.get_param_group(scope, name))
253
+ ensure
254
+ @_current_param_group = nil
255
+ end
202
256
 
203
- # check if required parameters are present
204
- if param.required && !params.has_key?(param.name)
205
- raise ParamMissing.new(param.name)
206
- end
257
+ # where the group definition should be looked up when no scope
258
+ # given. This is expected to return a controller.
259
+ def _default_param_group_scope
260
+ self
261
+ end
262
+ end
207
263
 
208
- # params validations
209
- if params.has_key?(param.name)
210
- param.validate(params[:"#{param.name}"])
211
- end
264
+ class ResourceDescriptionDsl
265
+ include Apipie::DSL::Base
266
+ include Apipie::DSL::Common
267
+ include Apipie::DSL::Resource
268
+ include Apipie::DSL::Param
212
269
 
213
- end
214
- end
270
+ def initialize(controller)
271
+ @controller = controller
272
+ end
215
273
 
216
- # run the original method code
217
- old_method.bind(self).call(*args)
218
- end
219
274
 
275
+ def _eval_dsl(&block)
276
+ instance_eval(&block)
277
+ return _apipie_dsl_data
220
278
  end
221
- end # def method_added
279
+
280
+ # evaluates resource description DSL and returns results
281
+ def self.eval_dsl(controller, &block)
282
+ dsl_data = self.new(controller)._eval_dsl(&block)
283
+ if dsl_data[:api_versions].empty?
284
+ dsl_data[:api_versions] = Apipie.controller_versions(controller)
285
+ end
286
+ dsl_data
287
+ end
288
+ end
289
+
222
290
  end # module DSL
223
291
  end # module Apipie