apipie-rails 0.0.17 → 0.0.18

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/.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