apipie-rails 0.5.7 → 0.5.8

Sign up to get free protection for your applications and to get access to all the features.
@@ -12,6 +12,8 @@ require "apipie/resource_description"
12
12
  require "apipie/param_description"
13
13
  require "apipie/errors"
14
14
  require "apipie/error_description"
15
+ require "apipie/response_description"
16
+ require "apipie/response_description_adapter"
15
17
  require "apipie/see_description"
16
18
  require "apipie/validator"
17
19
  require "apipie/railtie"
@@ -55,7 +55,7 @@ module Apipie
55
55
  # this method does in depth search for the route controller
56
56
  def route_app_controller(app, route, visited_apps = [])
57
57
  if route.defaults[:controller]
58
- controller_name = (route.defaults[:controller] + 'Controller').camelize
58
+ controller_name = "#{route.defaults[:controller]}_controller".camelize
59
59
  controller_name.safe_constantize
60
60
  end
61
61
  end
@@ -1,7 +1,8 @@
1
1
  module Apipie
2
2
  class Configuration
3
3
 
4
- attr_accessor :app_name, :app_info, :copyright, :markup, :disqus_shortname,
4
+ attr_accessor :app_name, :app_info, :copyright, :compress_examples,
5
+ :markup, :disqus_shortname,
5
6
  :api_base_url, :doc_base_url, :required_by_default, :layout,
6
7
  :default_version, :debug, :version_in_url, :namespaced_resources,
7
8
  :validate, :validate_value, :validate_presence, :validate_key, :authenticate, :doc_path,
@@ -9,7 +10,8 @@ module Apipie
9
10
  :link_extension, :record, :languages, :translate, :locale, :default_locale,
10
11
  :persist_show_in_doc, :authorize,
11
12
  :swagger_include_warning_tags, :swagger_content_type_input, :swagger_json_input_uses_refs,
12
- :swagger_suppress_warnings, :swagger_api_host, :swagger_generate_x_computed_id_field
13
+ :swagger_suppress_warnings, :swagger_api_host, :swagger_generate_x_computed_id_field,
14
+ :swagger_allow_additional_properties_in_response
13
15
 
14
16
  alias_method :validate?, :validate
15
17
  alias_method :required_by_default?, :required_by_default
@@ -176,6 +178,7 @@ module Apipie
176
178
  @swagger_suppress_warnings = false #[105,100,102]
177
179
  @swagger_api_host = "localhost:3000"
178
180
  @swagger_generate_x_computed_id_field = false
181
+ @swagger_allow_additional_properties_in_response = false
179
182
  end
180
183
  end
181
184
  end
@@ -2,7 +2,7 @@
2
2
 
3
3
  module Apipie
4
4
 
5
- # DSL is a module that provides #api, #error, #param, #error.
5
+ # DSL is a module that provides #api, #error, #param, #returns.
6
6
  module DSL
7
7
 
8
8
  module Base
@@ -32,6 +32,7 @@ module Apipie
32
32
  :api_args => [],
33
33
  :api_from_routes => nil,
34
34
  :errors => [],
35
+ :returns => {},
35
36
  :params => [],
36
37
  :headers => [],
37
38
  :resource_id => nil,
@@ -309,6 +310,7 @@ module Apipie
309
310
  end
310
311
  end
311
312
 
313
+
312
314
  # this describes the params, it's in separate module because it's
313
315
  # used in Validators as well
314
316
  module Param
@@ -329,6 +331,13 @@ module Apipie
329
331
  block]
330
332
  end
331
333
 
334
+ def property(param_name, validator, desc_or_options = nil, options = {}, &block) #:doc:
335
+ return unless Apipie.active_dsl?
336
+ options[:only_in] ||= :response
337
+ options[:required] = true if options[:required].nil?
338
+ param(param_name, validator, desc_or_options, options, &block)
339
+ end
340
+
332
341
  # Reuses param group for this method. The definition is looked up
333
342
  # in scope of this controller. If the group was defined in
334
343
  # different controller, the second param can be used to specify it.
@@ -354,6 +363,65 @@ module Apipie
354
363
  @_current_param_group = nil
355
364
  end
356
365
 
366
+ # Describe possible responses
367
+ #
368
+ # Example:
369
+ # def_param_group :user do
370
+ # param :user, Hash do
371
+ # param :name, String
372
+ # end
373
+ # end
374
+ #
375
+ # returns :user, "the speaker"
376
+ # returns "the speaker" do
377
+ # param_group: :user
378
+ # end
379
+ # returns :param_group => :user, "the speaker"
380
+ # returns :user, :code => 201, :desc => "the created speaker record"
381
+ # returns :array_of => :user, "many speakers"
382
+ # def hello_world
383
+ # render json: {user: {name: "Alfred"}}
384
+ # end
385
+ #
386
+ def returns(pgroup_or_options, desc_or_options=nil, options={}, &block) #:doc:
387
+ return unless Apipie.active_dsl?
388
+
389
+
390
+ if desc_or_options.is_a? Hash
391
+ options.merge!(desc_or_options)
392
+ elsif !desc_or_options.nil?
393
+ options[:desc] = desc_or_options
394
+ end
395
+
396
+ if pgroup_or_options.is_a? Hash
397
+ options.merge!(pgroup_or_options)
398
+ else
399
+ options[:param_group] = pgroup_or_options
400
+ end
401
+
402
+ code = options[:code] || 200
403
+ scope = options[:scope] || _default_param_group_scope
404
+ descriptor = options[:param_group] || options[:array_of]
405
+
406
+ if block.nil?
407
+ if descriptor.is_a? ResponseDescriptionAdapter
408
+ adapter = descriptor
409
+ elsif descriptor.respond_to? :describe_own_properties
410
+ adapter = ResponseDescriptionAdapter.from_self_describing_class(descriptor)
411
+ else
412
+ begin
413
+ block = Apipie.get_param_group(scope, descriptor) if descriptor
414
+ rescue
415
+ raise "No param_group or self-describing class named #{descriptor}"
416
+ end
417
+ end
418
+ elsif descriptor
419
+ raise "cannot specify both block and param_group"
420
+ end
421
+
422
+ _apipie_dsl_data[:returns][code] = [options, scope, block, adapter]
423
+ end
424
+
357
425
  # where the group definition should be looked up when no scope
358
426
  # given. This is expected to return a controller.
359
427
  def _default_param_group_scope
@@ -9,6 +9,12 @@ module Apipie
9
9
  class UnknownCode < Error
10
10
  end
11
11
 
12
+ class ReturnsMultipleDefinitionError < Error
13
+ def to_s
14
+ "a 'returns' statement cannot indicate both array_of and type"
15
+ end
16
+ end
17
+
12
18
  # abstract
13
19
  class DefinedParamError < ParamError
14
20
  attr_accessor :param
@@ -51,5 +57,4 @@ module Apipie
51
57
  "Invalid parameter '#{@param}' value #{@value.inspect}: #{@error}"
52
58
  end
53
59
  end
54
-
55
60
  end
@@ -3,11 +3,76 @@ require 'set'
3
3
  module Apipie
4
4
  module Extractor
5
5
  class Writer
6
+ class << self
7
+ def compressed
8
+ Apipie.configuration.compress_examples
9
+ end
10
+
11
+ def update_action_description(controller, action)
12
+ updater = ActionDescriptionUpdater.new(controller, action)
13
+ yield updater
14
+ updater.write!
15
+ rescue ActionDescriptionUpdater::ControllerNotFound
16
+ logger.warn("REST_API: Couldn't find controller file for #{controller}")
17
+ rescue ActionDescriptionUpdater::ActionNotFound
18
+ logger.warn("REST_API: Couldn't find action #{action} in #{controller}")
19
+ end
20
+
21
+ def write_recorded_examples(examples)
22
+ FileUtils.mkdir_p(File.dirname(examples_file))
23
+ content = serialize_examples(examples)
24
+ content = Zlib::Deflate.deflate(content).force_encoding('utf-8') if compressed
25
+ File.open(examples_file, 'w') { |f| f << content }
26
+ end
27
+
28
+ def load_recorded_examples
29
+ return {} unless File.exist?(examples_file)
30
+ load_json_examples
31
+ end
32
+
33
+ def examples_file
34
+ pure_path = Rails.root.join(
35
+ Apipie.configuration.doc_path, 'apipie_examples.json'
36
+ )
37
+ zipped_path = pure_path.to_s + '.gz'
38
+ return zipped_path if compressed
39
+ pure_path.to_s
40
+ end
41
+
42
+ protected
43
+
44
+ def serialize_examples(examples)
45
+ JSON.pretty_generate(
46
+ OrderedHash[*examples.sort_by(&:first).flatten(1)]
47
+ )
48
+ end
49
+
50
+ def deserialize_examples(examples_string)
51
+ examples = JSON.parse(examples_string)
52
+ return {} if examples.nil?
53
+ examples.each_value do |records|
54
+ records.each do |record|
55
+ record['verb'] = record['verb'].to_sym if record['verb']
56
+ end
57
+ end
58
+ end
59
+
60
+ def load_json_examples
61
+ raw = IO.read(examples_file)
62
+ raw = Zlib::Inflate.inflate(raw).force_encoding('utf-8') if compressed
63
+ deserialize_examples(raw)
64
+ end
65
+
66
+ def logger
67
+ Extractor.logger
68
+ end
69
+ end
6
70
 
7
71
  def initialize(collector)
8
72
  @collector = collector
9
73
  end
10
74
 
75
+
11
76
  def write_examples
12
77
  merged_examples = merge_old_new_examples
13
78
  self.class.write_recorded_examples(merged_examples)
@@ -26,47 +91,9 @@ module Apipie
26
91
  end
27
92
  end
28
93
 
29
- def self.update_action_description(controller, action)
30
- updater = ActionDescriptionUpdater.new(controller, action)
31
- yield updater
32
- updater.write!
33
- rescue ActionDescriptionUpdater::ControllerNotFound
34
- logger.warn("REST_API: Couldn't find controller file for #{controller}")
35
- rescue ActionDescriptionUpdater::ActionNotFound
36
- logger.warn("REST_API: Couldn't find action #{action} in #{controller}")
37
- end
38
-
39
- def self.write_recorded_examples(examples)
40
- examples_file = self.examples_file
41
- FileUtils.mkdir_p(File.dirname(examples_file))
42
- File.open(examples_file, "w") do |f|
43
- f << JSON.pretty_generate(OrderedHash[*examples.sort_by(&:first).flatten(1)])
44
- end
45
- end
46
-
47
- def self.load_recorded_examples
48
- examples_file = self.examples_file
49
- if File.exists?(examples_file)
50
- return load_json_examples
51
- end
52
- return {}
53
- end
54
-
55
- def self.examples_file
56
- File.join(Rails.root,Apipie.configuration.doc_path,"apipie_examples.json")
57
- end
58
94
 
59
95
  protected
60
96
 
61
- def self.load_json_examples
62
- examples = JSON.load(IO.read(examples_file))
63
- return {} if examples.nil?
64
- examples.each do |method, records|
65
- records.each do |record|
66
- record["verb"] = record["verb"].to_sym if record["verb"]
67
- end
68
- end
69
- end
70
97
 
71
98
  def desc_to_s(description)
72
99
  "#{description[:controller].name}##{description[:action]}"
@@ -177,10 +204,6 @@ module Apipie
177
204
  self.class.logger
178
205
  end
179
206
 
180
- def self.logger
181
- Extractor.logger
182
- end
183
-
184
207
  def showable_in_doc?(call)
185
208
  # we don't want to mess documentation with too large examples
186
209
  if hash_nodes_count(call["request_data"]) + hash_nodes_count(call["response_data"]) > 100
@@ -5,7 +5,7 @@ module Apipie
5
5
 
6
6
  class Api
7
7
 
8
- attr_accessor :short_description, :path, :http_method, :from_routes, :options
8
+ attr_accessor :short_description, :path, :http_method, :from_routes, :options, :returns
9
9
 
10
10
  def initialize(method, path, desc, options)
11
11
  @http_method = method.to_s
@@ -34,6 +34,10 @@ module Apipie
34
34
  Apipie::ErrorDescription.from_dsl_data(args)
35
35
  end
36
36
 
37
+ @returns = dsl_data[:returns].map do |code,args|
38
+ Apipie::ResponseDescription.from_dsl_data(self, code, args)
39
+ end
40
+
37
41
  @see = dsl_data[:see].map do |args|
38
42
  Apipie::SeeDescription.new(args)
39
43
  end
@@ -46,7 +50,8 @@ module Apipie
46
50
 
47
51
  @params_ordered = dsl_data[:params].map do |args|
48
52
  Apipie::ParamDescription.from_dsl_data(self, args)
49
- end
53
+ end.reject{|p| p.response_only? }
54
+
50
55
  @params_ordered = ParamDescription.unify(@params_ordered)
51
56
  @headers = dsl_data[:headers]
52
57
 
@@ -85,6 +90,25 @@ module Apipie
85
90
  all_params.find_all(&:validator)
86
91
  end
87
92
 
93
+ def returns_self
94
+ @returns
95
+ end
96
+
97
+ def returns
98
+ all_returns = []
99
+ parent = Apipie.get_resource_description(@resource.controller.superclass)
100
+
101
+ # get response descriptions from parent resource description
102
+ [parent, @resource].compact.each do |resource|
103
+ resource_returns = resource._returns_args.map do |code, args|
104
+ Apipie::ResponseDescription.from_dsl_data(self, code, args)
105
+ end
106
+ merge_returns(all_returns, resource_returns)
107
+ end
108
+
109
+ merge_returns(all_returns, @returns)
110
+ end
111
+
88
112
  def errors
89
113
  return @merged_errors if @merged_errors
90
114
  @merged_errors = []
@@ -189,6 +213,12 @@ module Apipie
189
213
  params.concat(new_params)
190
214
  end
191
215
 
216
+ def merge_returns(returns, new_returns)
217
+ new_return_codes = Set.new(new_returns.map(&:code))
218
+ returns.delete_if { |p| new_return_codes.include?(p.code) }
219
+ returns.concat(new_returns)
220
+ end
221
+
192
222
  def load_recorded_examples
193
223
  (Apipie.recorded_examples[id] || []).
194
224
  find_all { |ex| ex["show_in_doc"].to_i > 0 }.
@@ -8,9 +8,14 @@ module Apipie
8
8
  # validator - Validator::BaseValidator subclass
9
9
  class ParamDescription
10
10
 
11
- attr_reader :method_description, :name, :desc, :allow_nil, :allow_blank, :validator, :options, :metadata, :show, :as, :validations
11
+ attr_reader :method_description, :name, :desc, :allow_nil, :allow_blank, :validator, :options, :metadata, :show, :as, :validations, :response_only, :request_only
12
+ attr_reader :additional_properties, :is_array
12
13
  attr_accessor :parent, :required
13
14
 
15
+ alias_method :response_only?, :response_only
16
+ alias_method :request_only?, :request_only
17
+ alias_method :is_array?, :is_array
18
+
14
19
  def self.from_dsl_data(method_description, args)
15
20
  param_name, validator, desc_or_options, options, block = args
16
21
  Apipie::ParamDescription.new(method_description,
@@ -62,6 +67,10 @@ module Apipie
62
67
 
63
68
  @required = is_required?
64
69
 
70
+ @response_only = (@options[:only_in] == :response)
71
+ @request_only = (@options[:only_in] == :request)
72
+ raise ArgumentError.new("'#{@options[:only_in]}' is not a valid value for :only_in") if (!@response_only && !@request_only) && @options[:only_in].present?
73
+
65
74
  @show = if @options.has_key? :show
66
75
  @options[:show]
67
76
  else
@@ -74,11 +83,20 @@ module Apipie
74
83
  action_awareness
75
84
 
76
85
  if validator
86
+ if (validator != Hash) && (validator.is_a? Hash) && (validator[:array_of])
87
+ @is_array = true
88
+ rest_of_options = validator
89
+ validator = validator[:array_of]
90
+ options.merge!(rest_of_options.select{|k,v| k != :array_of })
91
+ raise "an ':array_of =>' validator is allowed exclusively on response-only fields" unless @response_only
92
+ end
77
93
  @validator = Validator::BaseValidator.find(self, validator, @options, block)
78
94
  raise "Validator for #{validator} not found." unless @validator
79
95
  end
80
96
 
81
97
  @validations = Array(options[:validations]).map {|v| concern_subst(Apipie.markup_to_html(v)) }
98
+
99
+ @additional_properties = @options[:additional_properties]
82
100
  end
83
101
 
84
102
  def from_concern?
@@ -14,7 +14,7 @@ module Apipie
14
14
  class ResourceDescription
15
15
 
16
16
  attr_reader :controller, :_short_description, :_full_description, :_methods, :_id,
17
- :_path, :_name, :_params_args, :_errors_args, :_formats, :_parent, :_metadata,
17
+ :_path, :_name, :_params_args, :_returns_args, :_errors_args, :_formats, :_parent, :_metadata,
18
18
  :_headers, :_deprecated
19
19
 
20
20
  def initialize(controller, resource_name, dsl_data = nil, version = nil, &block)
@@ -22,6 +22,7 @@ module Apipie
22
22
  @_methods = ActiveSupport::OrderedHash.new
23
23
  @_params_args = []
24
24
  @_errors_args = []
25
+ @_returns_args = []
25
26
 
26
27
  @controller = controller
27
28
  @_id = resource_name
@@ -40,6 +41,7 @@ module Apipie
40
41
  @_formats = dsl_data[:formats]
41
42
  @_errors_args = dsl_data[:errors]
42
43
  @_params_args = dsl_data[:params]
44
+ @_returns_args = dsl_data[:returns]
43
45
  @_metadata = dsl_data[:meta]
44
46
  @_api_base_url = dsl_data[:api_base_url]
45
47
  @_headers = dsl_data[:headers]
@@ -0,0 +1,125 @@
1
+ module Apipie
2
+
3
+ class ResponseDescription
4
+ class ResponseObject
5
+ include Apipie::DSL::Base
6
+ include Apipie::DSL::Param
7
+
8
+ attr_accessor :additional_properties
9
+
10
+ def initialize(method_description, scope, block)
11
+ @method_description = method_description
12
+ @scope = scope
13
+ @param_group = {scope: scope}
14
+ @additional_properties = false
15
+
16
+ self.instance_exec(&block) if block
17
+
18
+ prepare_hash_params
19
+ end
20
+
21
+ # this routine overrides Param#_default_param_group_scope and is called if Param#param_group is
22
+ # invoked during the instance_exec call in ResponseObject#initialize
23
+ def _default_param_group_scope
24
+ @scope
25
+ end
26
+
27
+ def name
28
+ "response #{@code} for #{@method_description.method}"
29
+ end
30
+
31
+ def params_ordered
32
+ @params_ordered ||= _apipie_dsl_data[:params].map do |args|
33
+ options = args.find { |arg| arg.is_a? Hash }
34
+ options[:param_group] = @param_group
35
+ Apipie::ParamDescription.from_dsl_data(@method_description, args) unless options[:only_in] == :request
36
+ end.compact
37
+ end
38
+
39
+ def prepare_hash_params
40
+ @hash_params = params_ordered.reduce({}) do |h, param|
41
+ h.update(param.name.to_sym => param)
42
+ end
43
+ end
44
+
45
+ end
46
+ end
47
+
48
+
49
+ class ResponseDescription
50
+ include Apipie::DSL::Base
51
+ include Apipie::DSL::Param
52
+
53
+ attr_reader :code, :description, :scope, :type_ref, :hash_validator, :is_array_of
54
+
55
+ def self.from_dsl_data(method_description, code, args)
56
+ options, scope, block, adapter = args
57
+
58
+ Apipie::ResponseDescription.new(method_description,
59
+ code,
60
+ options,
61
+ scope,
62
+ block,
63
+ adapter)
64
+ end
65
+
66
+ def is_array?
67
+ @is_array_of != false
68
+ end
69
+
70
+ def initialize(method_description, code, options, scope, block, adapter)
71
+
72
+ @type_ref = options[:param_group]
73
+ @is_array_of = options[:array_of] || false
74
+ raise ReturnsMultipleDefinitionError, options if @is_array_of && @type_ref
75
+
76
+ @type_ref ||= @is_array_of
77
+
78
+ @method_description = method_description
79
+
80
+ if code.is_a? Symbol
81
+ @code = Rack::Utils::SYMBOL_TO_STATUS_CODE[code]
82
+ else
83
+ @code = code
84
+ end
85
+
86
+ @description = options[:desc]
87
+ if @description.nil?
88
+ @description = Rack::Utils::HTTP_STATUS_CODES[@code]
89
+ raise "Cannot infer description from status code #{@code}" if @description.nil?
90
+ end
91
+ @scope = scope
92
+
93
+ if adapter
94
+ @response_object = adapter
95
+ else
96
+ @response_object = ResponseObject.new(method_description, scope, block)
97
+ end
98
+
99
+ @response_object.additional_properties ||= options[:additional_properties]
100
+ end
101
+
102
+ def param_description
103
+ nil
104
+ end
105
+
106
+ def params_ordered
107
+ @response_object.params_ordered
108
+ end
109
+
110
+ def additional_properties
111
+ !!@response_object.additional_properties
112
+ end
113
+ alias :allow_additional_properties :additional_properties
114
+
115
+ def to_json(lang=nil)
116
+ {
117
+ :code => code,
118
+ :description => description,
119
+ :is_array => is_array?,
120
+ :returns_object => params_ordered.map{ |param| param.to_json(lang).tap{|h| h.delete(:validations) }}.flatten,
121
+ :additional_properties => additional_properties,
122
+ }
123
+ end
124
+ end
125
+ end