apipie-rails 0.2.6 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -3,7 +3,7 @@
3
3
  var disqus_shortname = "<%= Apipie.configuration.disqus_shortname %>";
4
4
  (function() {
5
5
  var dsq = document.createElement('script'); dsq.type = 'text/javascript'; dsq.async = true;
6
- dsq.src = 'http://' + disqus_shortname + '.disqus.com/embed.js';
6
+ dsq.src = '//' + disqus_shortname + '.disqus.com/embed.js';
7
7
  (document.getElementsByTagName('head')[0] || document.getElementsByTagName('body')[0]).appendChild(dsq);
8
8
  })();
9
9
  </script>
@@ -17,7 +17,7 @@
17
17
  <%= param[:description].html_safe %>
18
18
  <% unless param[:validator].blank? %>
19
19
  <br>
20
- Value: <%= param[:validator] %>
20
+ Value: <%= Apipie.markup_to_html(param[:validator]).html_safe %>
21
21
  <% end %>
22
22
 
23
23
  <% unless param[:metadata].blank? %>
@@ -10,7 +10,7 @@
10
10
  <%= param[:required] ? t('apipie.required') : t('apipie.optional') %>
11
11
  <%= param[:allow_nil] ? ', '+t('apipie.nil_allowed') : '' %>
12
12
  <% if param[:validator] %>
13
- [ <%= param[:validator] %> ]
13
+ [ <%= Apipie.markup_to_html(param[:validator]).html_safe %> ]
14
14
  <% end %>
15
15
  </small>
16
16
  <%= param[:description].html_safe %>
@@ -1,4 +1,5 @@
1
1
  require 'apipie/static_dispatcher'
2
+ require 'apipie/routes_formatter'
2
3
  require 'yaml'
3
4
  require 'digest/md5'
4
5
  require 'json'
@@ -6,7 +7,6 @@ require 'json'
6
7
  module Apipie
7
8
 
8
9
  class Application
9
-
10
10
  # we need engine just for serving static assets
11
11
  class Engine < Rails::Engine
12
12
  initializer "static assets" do |app|
@@ -29,6 +29,49 @@ module Apipie
29
29
  @controller_to_resource_id[controller] = resource_id
30
30
  end
31
31
 
32
+ def rails_routes(route_set = nil)
33
+ if route_set.nil? && @rails_routes
34
+ return @rails_routes
35
+ end
36
+ route_set ||= Rails.application.routes
37
+ # ensure routes are loaded
38
+ Rails.application.reload_routes! unless Rails.application.routes.routes.any?
39
+
40
+ flatten_routes = []
41
+
42
+ route_set.routes.each do |route|
43
+ if route.app.respond_to?(:routes) && route.app.routes.is_a?(ActionDispatch::Routing::RouteSet)
44
+ # recursively go though the moutned engines
45
+ flatten_routes.concat(rails_routes(route.app.routes))
46
+ else
47
+ flatten_routes << route
48
+ end
49
+ end
50
+
51
+ @rails_routes = flatten_routes
52
+ end
53
+
54
+ # the app might be nested when using contraints, namespaces etc.
55
+ # this method does in depth search for the route controller
56
+ def route_app_controller(app, route)
57
+ if app.respond_to?(:controller)
58
+ return app.controller(route.defaults)
59
+ elsif app.respond_to?(:app)
60
+ return route_app_controller(app.app, route)
61
+ end
62
+ rescue ActionController::RoutingError
63
+ # some errors in the routes will not stop us here: just ignoring
64
+ end
65
+
66
+ def routes_for_action(controller, method, args)
67
+ routes = rails_routes.select do |route|
68
+ controller == route_app_controller(route.app, route) &&
69
+ method.to_s == route.defaults[:action]
70
+ end
71
+
72
+ Apipie.configuration.routes_formatter.format_routes(routes, args)
73
+ end
74
+
32
75
  # create new method api description
33
76
  def define_method_description(controller, method_name, dsl_data)
34
77
  return if ignored?(controller, method_name)
@@ -4,9 +4,10 @@ module Apipie
4
4
  attr_accessor :app_name, :app_info, :copyright, :markup, :disqus_shortname,
5
5
  :api_base_url, :doc_base_url, :required_by_default, :layout,
6
6
  :default_version, :debug, :version_in_url, :namespaced_resources,
7
- :validate, :validate_value, :validate_presence, :authenticate, :doc_path,
7
+ :validate, :validate_value, :validate_presence, :validate_key, :authenticate, :doc_path,
8
8
  :show_all_examples, :process_params, :update_checksum, :checksum_path,
9
- :link_extension, :record, :languages, :translate, :locale, :default_locale
9
+ :link_extension, :record, :languages, :translate, :locale, :default_locale,
10
+ :persist_show_in_doc
10
11
 
11
12
  alias_method :validate?, :validate
12
13
  alias_method :required_by_default?, :required_by_default
@@ -27,6 +28,12 @@ module Apipie
27
28
  # Api::Engine.routes
28
29
  attr_accessor :api_routes
29
30
 
31
+ # a object responsible for transforming the routes loaded from Rails to a form
32
+ # to be used in the documentation, when using the `api!` keyword. By default,
33
+ # it's Apipie::RoutesFormatter. To customize the behaviour, one can inherit from
34
+ # from this class and override the methods as needed.
35
+ attr_accessor :routes_formatter
36
+
30
37
  def reload_controllers?
31
38
  @reload_controllers = Rails.env.development? unless defined? @reload_controllers
32
39
  return @reload_controllers && @api_controllers_matcher
@@ -42,6 +49,11 @@ module Apipie
42
49
  end
43
50
  alias_method :validate_presence?, :validate_presence
44
51
 
52
+ def validate_key
53
+ return (validate? && @validate_key)
54
+ end
55
+ alias_method :validate_key?, :validate_key
56
+
45
57
  def process_value?
46
58
  @process_params
47
59
  end
@@ -86,6 +98,10 @@ module Apipie
86
98
  @ignored.map(&:to_s)
87
99
  end
88
100
 
101
+ # Persist the show_in_doc value in the examples if true. Use this if you
102
+ # cannot set the flag in the tests themselves (no rspec for example).
103
+ attr_writer :persist_show_in_doc
104
+
89
105
  # comment to put before docs that was generated automatically. It's used to
90
106
  # determine if the description should be overwritten next recording.
91
107
  # If you want to keep the documentation (prevent from overriding), remove
@@ -124,9 +140,10 @@ module Apipie
124
140
  @app_name = "Another API"
125
141
  @app_info = HashWithIndifferentAccess.new
126
142
  @copyright = nil
127
- @validate = true
143
+ @validate = :implicitly
128
144
  @validate_value = true
129
145
  @validate_presence = true
146
+ @validate_key = false
130
147
  @required_by_default = false
131
148
  @api_base_url = HashWithIndifferentAccess.new
132
149
  @doc_base_url = "/apipie"
@@ -146,6 +163,8 @@ module Apipie
146
163
  @default_locale = 'en'
147
164
  @locale = lambda { |locale| @default_locale }
148
165
  @translate = lambda { |str, locale| str }
166
+ @persist_show_in_doc = false
167
+ @routes_formatter = RoutesFormatter.new
149
168
  end
150
169
  end
151
170
  end
@@ -20,7 +20,9 @@ module Apipie
20
20
 
21
21
  def _apipie_dsl_data_init
22
22
  @_apipie_dsl_data = {
23
+ :api => false,
23
24
  :api_args => [],
25
+ :api_from_routes => nil,
24
26
  :errors => [],
25
27
  :params => [],
26
28
  :resouce_id => nil,
@@ -72,16 +74,25 @@ module Apipie
72
74
  Apipie.add_param_group(self, name, &block)
73
75
  end
74
76
 
75
- # Declare an api.
76
77
  #
77
- # Example:
78
- # api :GET, "/resource_route", "short description",
78
+ # # load paths from routes and don't provide description
79
+ # api
79
80
  #
80
81
  def api(method, path, desc = nil, options={}) #:doc:
81
82
  return unless Apipie.active_dsl?
83
+ _apipie_dsl_data[:api] = true
82
84
  _apipie_dsl_data[:api_args] << [method, path, desc, options]
83
85
  end
84
86
 
87
+ # # load paths from routes
88
+ # api! "short description",
89
+ #
90
+ def api!(desc = nil, options={}) #:doc:
91
+ return unless Apipie.active_dsl?
92
+ _apipie_dsl_data[:api] = true
93
+ _apipie_dsl_data[:api_from_routes] = { :desc => desc, :options =>options }
94
+ end
95
+
85
96
  # Reference other similar method
86
97
  #
87
98
  # api :PUT, '/articles/:id'
@@ -189,44 +200,72 @@ module Apipie
189
200
  end
190
201
 
191
202
  def _apipie_define_validators(description)
192
- # redefine method only if validation is turned on
193
- if description && Apipie.configuration.validate == true
194
203
 
195
- old_method = instance_method(description.method)
204
+ # [re]define method only if validation is turned on
205
+ if description && (Apipie.configuration.validate == true ||
206
+ Apipie.configuration.validate == :implicitly ||
207
+ Apipie.configuration.validate == :explicitly)
196
208
 
209
+ _apipie_save_method_params(description.method, description.params)
197
210
 
198
- # @todo we should use before_filter
199
- define_method(description.method) do |*args|
211
+ unless instance_methods.include?(:apipie_validations)
212
+ define_method(:apipie_validations) do
213
+ method_params = self.class._apipie_get_method_params(action_name)
200
214
 
201
- if Apipie.configuration.validate_presence?
202
- description.params.each do |_, param|
203
- # check if required parameters are present
204
- raise ParamMissing.new(param.name) if param.required && !params.has_key?(param.name)
215
+ if Apipie.configuration.validate_presence?
216
+ method_params.each do |_, param|
217
+ # check if required parameters are present
218
+ raise ParamMissing.new(param.name) if param.required && !params.has_key?(param.name)
219
+ end
205
220
  end
206
- end
207
221
 
208
- if Apipie.configuration.validate_value?
209
- description.params.each do |_, param|
210
- # params validations
211
- param.validate(params[:"#{param.name}"]) if params.has_key?(param.name)
222
+ if Apipie.configuration.validate_value?
223
+ method_params.each do |_, param|
224
+ # params validations
225
+ param.validate(params[:"#{param.name}"]) if params.has_key?(param.name)
226
+ end
212
227
  end
213
- end
214
228
 
215
- if Apipie.configuration.process_value?
216
- @api_params = {}
229
+ # Only allow params passed in that are defined keys in the api
230
+ # Auto skip the default params (format, controller, action)
231
+ if Apipie.configuration.validate_key?
232
+ params.reject{|k,_| %w[format controller action].include?(k.to_s) }.each_key do |param|
233
+ # params allowed
234
+ raise UnknownParam.new(param) if method_params.select {|_,p| p.name.to_s == param.to_s}.empty?
235
+ end
236
+ end
217
237
 
218
- description.params.each do |_, param|
219
- # params processing
220
- @api_params[param.as] = param.process_value(params[:"#{param.name}"]) if params.has_key?(param.name)
238
+ if Apipie.configuration.process_value?
239
+ @api_params ||= {}
240
+ method_params.each do |_, param|
241
+ # params processing
242
+ @api_params[param.as] = param.process_value(params[:"#{param.name}"]) if params.has_key?(param.name)
243
+ end
221
244
  end
222
245
  end
246
+ end
247
+
248
+ if (Apipie.configuration.validate == :implicitly || Apipie.configuration.validate == true)
249
+ old_method = instance_method(description.method)
250
+
251
+ define_method(description.method) do |*args|
252
+ apipie_validations
223
253
 
224
- # run the original method code
225
- old_method.bind(self).call(*args)
254
+ # run the original method code
255
+ old_method.bind(self).call(*args)
256
+ end
226
257
  end
227
258
 
228
259
  end
260
+ end
229
261
 
262
+ def _apipie_save_method_params(method, params)
263
+ @method_params ||= {}
264
+ @method_params[method] = params
265
+ end
266
+
267
+ def _apipie_get_method_params(method)
268
+ @method_params[method]
230
269
  end
231
270
 
232
271
  end
@@ -335,22 +374,32 @@ module Apipie
335
374
  # create method api and redefine newly added method
336
375
  def method_added(method_name) #:doc:
337
376
  super
377
+ return if !Apipie.active_dsl? || !_apipie_dsl_data[:api]
338
378
 
339
- if ! Apipie.active_dsl? || _apipie_dsl_data[:api_args].blank?
340
- _apipie_dsl_data_clear
341
- return
342
- end
379
+ if _apipie_dsl_data[:api_from_routes]
380
+ desc = _apipie_dsl_data[:api_from_routes][:desc]
381
+ options = _apipie_dsl_data[:api_from_routes][:options]
343
382
 
344
- begin
345
- # remove method description if exists and create new one
346
- Apipie.remove_method_description(self, _apipie_dsl_data[:api_versions], method_name)
347
- description = Apipie.define_method_description(self, method_name, _apipie_dsl_data)
348
- ensure
349
- _apipie_dsl_data_clear
383
+ api_from_routes = Apipie.routes_for_action(self, method_name, {:desc => desc, :options => options}).map do |route_info|
384
+ [route_info[:verb],
385
+ route_info[:path],
386
+ route_info[:desc],
387
+ (route_info[:options] || {}).merge(:from_routes => true)]
388
+ end
389
+ _apipie_dsl_data[:api_args].concat(api_from_routes)
350
390
  end
351
391
 
392
+ return if _apipie_dsl_data[:api_args].blank?
393
+
394
+ # remove method description if exists and create new one
395
+ Apipie.remove_method_description(self, _apipie_dsl_data[:api_versions], method_name)
396
+ description = Apipie.define_method_description(self, method_name, _apipie_dsl_data)
397
+
398
+ _apipie_dsl_data_clear
352
399
  _apipie_define_validators(description)
353
- end # def method_added
400
+ ensure
401
+ _apipie_dsl_data_clear
402
+ end
354
403
  end
355
404
 
356
405
  module Concern
@@ -381,18 +430,12 @@ module Apipie
381
430
  def method_added(method_name) #:doc:
382
431
  super
383
432
 
384
- if ! Apipie.active_dsl? || _apipie_dsl_data[:api_args].blank?
385
- _apipie_dsl_data_clear
386
- return
387
- end
388
-
389
- begin
390
- _apipie_concern_data << [method_name, _apipie_dsl_data.merge(:from_concern => true)]
391
- ensure
392
- _apipie_dsl_data_clear
393
- end
433
+ return if ! Apipie.active_dsl? || !_apipie_dsl_data[:api]
394
434
 
395
- end # def method_added
435
+ _apipie_concern_data << [method_name, _apipie_dsl_data.merge(:from_concern => true)]
436
+ ensure
437
+ _apipie_dsl_data_clear
438
+ end
396
439
 
397
440
  end
398
441
 
data/lib/apipie/errors.rb CHANGED
@@ -21,6 +21,12 @@ module Apipie
21
21
  end
22
22
  end
23
23
 
24
+ class UnknownParam < DefinedParamError
25
+ def to_s
26
+ "Unknown parameter #{@param}"
27
+ end
28
+ end
29
+
24
30
  class ParamInvalid < DefinedParamError
25
31
  attr_accessor :value, :error
26
32
 
@@ -126,7 +126,7 @@ module Apipie
126
126
  controller = "#{controller_path}Controller"
127
127
 
128
128
  path = if /^#{Regexp.escape(@api_prefix)}(.*)$/ =~ route[:path]
129
- $1.sub!(/\(\.:format\)$/,"")
129
+ $1.sub(/\(\.:format\)$/,'')
130
130
  else
131
131
  nil
132
132
  end
@@ -1,6 +1,8 @@
1
1
  module Apipie
2
2
  module Extractor
3
3
  class Recorder
4
+ MULTIPART_BOUNDARY = 'APIPIE_RECORDER_EXAMPLE_BOUNDARY'
5
+
4
6
  def initialize
5
7
  @ignored_params = [:controller, :action]
6
8
  end
@@ -14,6 +16,8 @@ module Apipie
14
16
  if data = parse_data(env["rack.input"].read)
15
17
  @request_data = data
16
18
  env["rack.input"].rewind
19
+ elsif form_hash = env["rack.request.form_hash"]
20
+ @request_data = reformat_multipart_data(form_hash)
17
21
  end
18
22
  end
19
23
 
@@ -50,6 +54,37 @@ module Apipie
50
54
  data
51
55
  end
52
56
 
57
+ def reformat_multipart_data(form)
58
+ form.empty? and return ''
59
+ lines = ["Content-Type: multipart/form-data; boundary=#{MULTIPART_BOUNDARY}",'']
60
+ boundary = "--#{MULTIPART_BOUNDARY}"
61
+ form.each do |key, attrs|
62
+ if attrs.is_a?(String)
63
+ lines << boundary << content_disposition(key) << "Content-Length: #{attrs.size}" << '' << attrs
64
+ else
65
+ reformat_hash(boundary, attrs, lines)
66
+ end
67
+ end
68
+ lines << "#{boundary}--"
69
+ lines.join("\n")
70
+ end
71
+
72
+ def reformat_hash(boundary, attrs, lines)
73
+ if head = attrs[:head]
74
+ lines << boundary
75
+ lines.concat(head.split("\r\n"))
76
+ # To avoid large and/or binary file bodies, simply indicate the contents in the output.
77
+ lines << '' << %{... contents of "#{attrs[:name]}" ...}
78
+ else
79
+ # Look for subelements that contain a part.
80
+ attrs.each { |k,v| v.is_a?(Hash) and reformat_hash(boundary, v, lines) }
81
+ end
82
+ end
83
+
84
+ def content_disposition(name)
85
+ %{Content-Disposition: form-data; name="#{name}"}
86
+ end
87
+
53
88
  def reformat_data(data)
54
89
  parsed = parse_data(data)
55
90
  case parsed
@@ -75,7 +75,7 @@ module Apipie
75
75
  def ordered_call(call)
76
76
  call = call.stringify_keys
77
77
  ordered_call = OrderedHash.new
78
- %w[verb path versions query request_data response_data code show_in_doc recorded].each do |k|
78
+ %w[title verb path versions query request_data response_data code show_in_doc recorded].each do |k|
79
79
  next unless call.has_key?(k)
80
80
  ordered_call[k] = case call[k]
81
81
  when ActiveSupport::HashWithIndifferentAccess
@@ -97,15 +97,37 @@ module Apipie
97
97
  merged_examples = []
98
98
  (new_examples.keys + old_examples.keys).uniq.each do |key|
99
99
  if new_examples.has_key?(key)
100
- records = new_examples[key]
100
+ if old_examples.has_key?(key)
101
+ records = deep_merge_examples(new_examples[key], old_examples[key])
102
+ else
103
+ records = new_examples[key]
104
+ end
101
105
  else
102
106
  records = old_examples[key]
103
107
  end
104
- merged_examples << [key, records.map { |r| ordered_call(r) } ]
108
+ merged_examples << [key, records.map { |r| ordered_call(r) } ]
105
109
  end
106
110
  return merged_examples
107
111
  end
108
112
 
113
+ def deep_merge_examples(new_examples, old_examples)
114
+ new_examples.map do |new_example|
115
+ # Use ordered to get compareble output (mainly for the :query)
116
+ new_example_ordered = ordered_call(new_example.dup)
117
+
118
+ # Comparing verb, versions and query
119
+ if old_example = old_examples.find{ |old_example| old_example["verb"] == new_example_ordered["verb"] && old_example["versions"] == new_example_ordered["versions"] && old_example["query"] == new_example_ordered["query"]}
120
+
121
+ # Take the 'show in doc' attribute from the old example if it is present and the configuration requests the value to be persisted.
122
+ new_example[:show_in_doc] = old_example["show_in_doc"] if Apipie.configuration.persist_show_in_doc && old_example["show_in_doc"].to_i > 0
123
+
124
+ # Always take the title from the old example if it exists.
125
+ new_example[:title] ||= old_example["title"] if old_example["title"].present?
126
+ end
127
+ new_example
128
+ end
129
+ end
130
+
109
131
  def load_new_examples
110
132
  @collector.records.reduce({}) do |h, (method, calls)|
111
133
  showed_in_versions = Set.new
@@ -272,8 +294,8 @@ module Apipie
272
294
  "List #{name}"
273
295
  end
274
296
 
275
- code << "api :#{api[:method]}, \"#{api[:path]}\""
276
- code << ", \"#{desc}\"" if desc
297
+ code << "api :#{api[:method]}, '#{api[:path]}'"
298
+ code << ", '#{desc}'" if desc
277
299
  code << "\n"
278
300
  end
279
301
  return code
@@ -285,16 +307,16 @@ module Apipie
285
307
  desc[:type] = (desc[:type] && desc[:type].first) || Object
286
308
  code << "#{indent}param"
287
309
  if name =~ /\W/
288
- code << " :\"#{name}\""
310
+ code << " :'#{name}'"
289
311
  else
290
312
  code << " :#{name}"
291
313
  end
292
314
  code << ", #{desc[:type].inspect}"
293
315
  if desc[:allow_nil]
294
- code << ", :allow_nil => true"
316
+ code << ", allow_nil: true"
295
317
  end
296
318
  if desc[:required]
297
- code << ", :required => true"
319
+ code << ", required: true"
298
320
  end
299
321
  if desc[:nested]
300
322
  code << " do\n"
@@ -310,7 +332,7 @@ module Apipie
310
332
  def generate_errors_code(errors)
311
333
  code = ""
312
334
  errors.sort_by {|e| e[:code] }.each do |error|
313
- code << "error :code => #{error[:code]}\n"
335
+ code << "error code: #{error[:code]}\n"
314
336
  end
315
337
  code
316
338
  end