apipie-rails 0.5.7 → 0.5.8

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