apipie-rails 0.2.6 → 0.3.0

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