apipierails3 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (171) hide show
  1. checksums.yaml +17 -0
  2. data/.gitignore +14 -0
  3. data/.rspec +2 -0
  4. data/.travis.yml +27 -0
  5. data/APACHE-LICENSE-2.0 +202 -0
  6. data/CHANGELOG.md +469 -0
  7. data/Gemfile +1 -0
  8. data/Gemfile.rails32 +6 -0
  9. data/Gemfile.rails41 +6 -0
  10. data/Gemfile.rails42 +11 -0
  11. data/Gemfile.rails50 +6 -0
  12. data/Gemfile.rails51 +7 -0
  13. data/MIT-LICENSE +20 -0
  14. data/NOTICE +4 -0
  15. data/PROPOSAL_FOR_RESPONSE_DESCRIPTIONS.md +244 -0
  16. data/README.rst +1874 -0
  17. data/Rakefile +13 -0
  18. data/apipierails3.gemspec +27 -0
  19. data/app/controllers/apipie/apipies_controller.rb +199 -0
  20. data/app/helpers/apipie_helper.rb +10 -0
  21. data/app/public/apipie/javascripts/apipie.js +6 -0
  22. data/app/public/apipie/javascripts/bundled/bootstrap-collapse.js +138 -0
  23. data/app/public/apipie/javascripts/bundled/bootstrap.js +1726 -0
  24. data/app/public/apipie/javascripts/bundled/jquery.js +5 -0
  25. data/app/public/apipie/javascripts/bundled/prettify.js +28 -0
  26. data/app/public/apipie/stylesheets/application.css +7 -0
  27. data/app/public/apipie/stylesheets/bundled/bootstrap-responsive.min.css +12 -0
  28. data/app/public/apipie/stylesheets/bundled/bootstrap.min.css +689 -0
  29. data/app/public/apipie/stylesheets/bundled/prettify.css +30 -0
  30. data/app/views/apipie/apipies/_disqus.html.erb +13 -0
  31. data/app/views/apipie/apipies/_errors.html.erb +23 -0
  32. data/app/views/apipie/apipies/_headers.html.erb +26 -0
  33. data/app/views/apipie/apipies/_languages.erb +6 -0
  34. data/app/views/apipie/apipies/_metadata.erb +1 -0
  35. data/app/views/apipie/apipies/_method_detail.erb +61 -0
  36. data/app/views/apipie/apipies/_params.html.erb +42 -0
  37. data/app/views/apipie/apipies/_params_plain.html.erb +20 -0
  38. data/app/views/apipie/apipies/apipie_404.html.erb +17 -0
  39. data/app/views/apipie/apipies/apipie_checksum.json.erb +1 -0
  40. data/app/views/apipie/apipies/getting_started.html.erb +6 -0
  41. data/app/views/apipie/apipies/index.html.erb +56 -0
  42. data/app/views/apipie/apipies/method.html.erb +41 -0
  43. data/app/views/apipie/apipies/plain.html.erb +77 -0
  44. data/app/views/apipie/apipies/resource.html.erb +80 -0
  45. data/app/views/apipie/apipies/static.html.erb +103 -0
  46. data/app/views/layouts/apipie/apipie.html.erb +27 -0
  47. data/config/locales/de.yml +28 -0
  48. data/config/locales/en.yml +32 -0
  49. data/config/locales/es.yml +28 -0
  50. data/config/locales/fr.yml +31 -0
  51. data/config/locales/it.yml +31 -0
  52. data/config/locales/ja.yml +31 -0
  53. data/config/locales/pl.yml +28 -0
  54. data/config/locales/pt-BR.yml +28 -0
  55. data/config/locales/ru.yml +28 -0
  56. data/config/locales/tr.yml +28 -0
  57. data/config/locales/zh-CN.yml +28 -0
  58. data/config/locales/zh-TW.yml +28 -0
  59. data/images/screenshot-1.png +0 -0
  60. data/images/screenshot-2.png +0 -0
  61. data/lib/apipie/apipie_module.rb +83 -0
  62. data/lib/apipie/application.rb +462 -0
  63. data/lib/apipie/configuration.rb +186 -0
  64. data/lib/apipie/dsl_definition.rb +607 -0
  65. data/lib/apipie/error_description.rb +44 -0
  66. data/lib/apipie/errors.rb +86 -0
  67. data/lib/apipie/extractor.rb +177 -0
  68. data/lib/apipie/extractor/collector.rb +117 -0
  69. data/lib/apipie/extractor/recorder.rb +166 -0
  70. data/lib/apipie/extractor/writer.rb +454 -0
  71. data/lib/apipie/helpers.rb +73 -0
  72. data/lib/apipie/markup.rb +48 -0
  73. data/lib/apipie/method_description.rb +273 -0
  74. data/lib/apipie/middleware/checksum_in_headers.rb +35 -0
  75. data/lib/apipie/param_description.rb +280 -0
  76. data/lib/apipie/railtie.rb +9 -0
  77. data/lib/apipie/resource_description.rb +124 -0
  78. data/lib/apipie/response_description.rb +131 -0
  79. data/lib/apipie/response_description_adapter.rb +200 -0
  80. data/lib/apipie/routes_formatter.rb +33 -0
  81. data/lib/apipie/routing.rb +16 -0
  82. data/lib/apipie/rspec/response_validation_helper.rb +192 -0
  83. data/lib/apipie/see_description.rb +39 -0
  84. data/lib/apipie/static_dispatcher.rb +69 -0
  85. data/lib/apipie/swagger_generator.rb +707 -0
  86. data/lib/apipie/tag_list_description.rb +11 -0
  87. data/lib/apipie/validator.rb +526 -0
  88. data/lib/apipie/version.rb +3 -0
  89. data/lib/apipierails3.rb +25 -0
  90. data/lib/generators/apipie/install/README +6 -0
  91. data/lib/generators/apipie/install/install_generator.rb +25 -0
  92. data/lib/generators/apipie/install/templates/initializer.rb.erb +7 -0
  93. data/lib/generators/apipie/views_generator.rb +11 -0
  94. data/lib/tasks/apipie.rake +345 -0
  95. data/rel-eng/packages/.readme +3 -0
  96. data/rel-eng/packages/rubygem-apipie-rails +1 -0
  97. data/rel-eng/tito.props +5 -0
  98. data/spec/controllers/api/v1/architectures_controller_spec.rb +29 -0
  99. data/spec/controllers/api/v2/architectures_controller_spec.rb +12 -0
  100. data/spec/controllers/api/v2/nested/resources_controller_spec.rb +11 -0
  101. data/spec/controllers/apipies_controller_spec.rb +273 -0
  102. data/spec/controllers/concerns_controller_spec.rb +42 -0
  103. data/spec/controllers/extended_controller_spec.rb +11 -0
  104. data/spec/controllers/users_controller_spec.rb +740 -0
  105. data/spec/dummy/Rakefile +7 -0
  106. data/spec/dummy/app/controllers/api/base_controller.rb +4 -0
  107. data/spec/dummy/app/controllers/api/v1/architectures_controller.rb +43 -0
  108. data/spec/dummy/app/controllers/api/v1/base_controller.rb +11 -0
  109. data/spec/dummy/app/controllers/api/v2/architectures_controller.rb +30 -0
  110. data/spec/dummy/app/controllers/api/v2/base_controller.rb +11 -0
  111. data/spec/dummy/app/controllers/api/v2/nested/architectures_controller.rb +32 -0
  112. data/spec/dummy/app/controllers/api/v2/nested/resources_controller.rb +33 -0
  113. data/spec/dummy/app/controllers/application_controller.rb +18 -0
  114. data/spec/dummy/app/controllers/concerns/extending_concern.rb +11 -0
  115. data/spec/dummy/app/controllers/concerns/sample_controller.rb +41 -0
  116. data/spec/dummy/app/controllers/concerns_controller.rb +8 -0
  117. data/spec/dummy/app/controllers/extended_controller.rb +14 -0
  118. data/spec/dummy/app/controllers/files_controller.rb +5 -0
  119. data/spec/dummy/app/controllers/overridden_concerns_controller.rb +31 -0
  120. data/spec/dummy/app/controllers/pets_controller.rb +408 -0
  121. data/spec/dummy/app/controllers/pets_using_auto_views_controller.rb +73 -0
  122. data/spec/dummy/app/controllers/pets_using_self_describing_classes_controller.rb +95 -0
  123. data/spec/dummy/app/controllers/tagged_cats_controller.rb +32 -0
  124. data/spec/dummy/app/controllers/tagged_dogs_controller.rb +15 -0
  125. data/spec/dummy/app/controllers/twitter_example_controller.rb +307 -0
  126. data/spec/dummy/app/controllers/users_controller.rb +297 -0
  127. data/spec/dummy/app/views/layouts/application.html.erb +21 -0
  128. data/spec/dummy/config.ru +4 -0
  129. data/spec/dummy/config/application.rb +49 -0
  130. data/spec/dummy/config/boot.rb +10 -0
  131. data/spec/dummy/config/database.yml +21 -0
  132. data/spec/dummy/config/environment.rb +8 -0
  133. data/spec/dummy/config/environments/development.rb +28 -0
  134. data/spec/dummy/config/environments/production.rb +52 -0
  135. data/spec/dummy/config/environments/test.rb +38 -0
  136. data/spec/dummy/config/initializers/apipie.rb +110 -0
  137. data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
  138. data/spec/dummy/config/initializers/inflections.rb +10 -0
  139. data/spec/dummy/config/initializers/mime_types.rb +5 -0
  140. data/spec/dummy/config/initializers/secret_token.rb +8 -0
  141. data/spec/dummy/config/initializers/session_store.rb +8 -0
  142. data/spec/dummy/config/locales/en.yml +5 -0
  143. data/spec/dummy/config/routes.rb +51 -0
  144. data/spec/dummy/db/.gitkeep +0 -0
  145. data/spec/dummy/doc/apipie_examples.json +1 -0
  146. data/spec/dummy/doc/users/desc_from_file.md +1 -0
  147. data/spec/dummy/public/404.html +26 -0
  148. data/spec/dummy/public/422.html +26 -0
  149. data/spec/dummy/public/500.html +26 -0
  150. data/spec/dummy/public/favicon.ico +0 -0
  151. data/spec/dummy/public/stylesheets/.gitkeep +0 -0
  152. data/spec/dummy/script/rails +6 -0
  153. data/spec/lib/application_spec.rb +49 -0
  154. data/spec/lib/extractor/extractor_spec.rb +9 -0
  155. data/spec/lib/extractor/middleware_spec.rb +44 -0
  156. data/spec/lib/extractor/writer_spec.rb +110 -0
  157. data/spec/lib/file_handler_spec.rb +18 -0
  158. data/spec/lib/method_description_spec.rb +98 -0
  159. data/spec/lib/param_description_spec.rb +345 -0
  160. data/spec/lib/param_group_spec.rb +60 -0
  161. data/spec/lib/rake_spec.rb +71 -0
  162. data/spec/lib/resource_description_spec.rb +48 -0
  163. data/spec/lib/swagger/openapi_2_0_schema.json +1607 -0
  164. data/spec/lib/swagger/rake_swagger_spec.rb +139 -0
  165. data/spec/lib/swagger/response_validation_spec.rb +104 -0
  166. data/spec/lib/swagger/swagger_dsl_spec.rb +658 -0
  167. data/spec/lib/validator_spec.rb +113 -0
  168. data/spec/lib/validators/array_validator_spec.rb +85 -0
  169. data/spec/spec_helper.rb +109 -0
  170. data/spec/support/rake.rb +21 -0
  171. metadata +415 -0
@@ -0,0 +1,44 @@
1
+ module Apipie
2
+
3
+ class ErrorDescription
4
+ attr_reader :code, :description, :metadata
5
+
6
+ def self.from_dsl_data(args)
7
+ code_or_options, desc, options = args
8
+ Apipie::ErrorDescription.new(code_or_options,
9
+ desc,
10
+ options)
11
+ end
12
+
13
+ def initialize(code_or_options, desc=nil, options={})
14
+ if code_or_options.is_a? Hash
15
+ code_or_options.symbolize_keys!
16
+ @code = code_or_options[:code]
17
+ @metadata = code_or_options[:meta]
18
+ @description = code_or_options[:desc] || code_or_options[:description]
19
+ else
20
+ @code =
21
+ if code_or_options.is_a? Symbol
22
+ Rack::Utils::SYMBOL_TO_STATUS_CODE[code_or_options]
23
+ else
24
+ code_or_options
25
+ end
26
+
27
+ raise UnknownCode, code_or_options unless @code
28
+
29
+ @metadata = options[:meta]
30
+ @description = desc
31
+ end
32
+ end
33
+
34
+ def to_json
35
+ {
36
+ :code => code,
37
+ :description => description,
38
+ :metadata => metadata
39
+ }
40
+ end
41
+
42
+ end
43
+
44
+ end
@@ -0,0 +1,86 @@
1
+ module Apipie
2
+
3
+ class Error < StandardError
4
+ end
5
+
6
+ class ParamError < Error
7
+ end
8
+
9
+ class UnknownCode < Error
10
+ end
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
+
18
+ # abstract
19
+ class DefinedParamError < ParamError
20
+ attr_accessor :param
21
+
22
+ def initialize(param)
23
+ @param = param
24
+ end
25
+ end
26
+
27
+ class ParamMissing < DefinedParamError
28
+ def to_s
29
+ unless @param.options[:missing_message].nil?
30
+ if @param.options[:missing_message].kind_of?(Proc)
31
+ @param.options[:missing_message].call
32
+ else
33
+ @param.options[:missing_message].to_s
34
+ end
35
+ else
36
+ "Missing parameter #{@param.name}"
37
+ end
38
+ end
39
+ end
40
+
41
+ class UnknownParam < DefinedParamError
42
+ def to_s
43
+ "Unknown parameter #{@param}"
44
+ end
45
+ end
46
+
47
+ class ParamInvalid < DefinedParamError
48
+ attr_accessor :value, :error
49
+
50
+ def initialize(param, value, error)
51
+ super param
52
+ @value = value
53
+ @error = error
54
+ end
55
+
56
+ def to_s
57
+ "Invalid parameter '#{@param}' value #{@value.inspect}: #{@error}"
58
+ end
59
+ end
60
+
61
+ class ResponseDoesNotMatchSwaggerSchema < Error
62
+ def initialize(controller_name, method_name, response_code, error_messages, schema, returned_object)
63
+ @controller_name = controller_name
64
+ @method_name = method_name
65
+ @response_code = response_code
66
+ @error_messages = error_messages
67
+ @schema = schema
68
+ @returned_object = returned_object
69
+ end
70
+
71
+ def to_s
72
+ "Response does not match swagger schema (#{@controller_name}##{@method_name} #{@response_code}): #{@error_messages}\nSchema: #{JSON(@schema)}\nReturned object: #{@returned_object}"
73
+ end
74
+ end
75
+
76
+ class NoDocumentedMethod < Error
77
+ def initialize(controller_name, method_name)
78
+ @method_name = method_name
79
+ @controller_name = controller_name
80
+ end
81
+
82
+ def to_s
83
+ "There is no documented method #{@controller_name}##{@method_name}"
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,177 @@
1
+ require 'singleton'
2
+ require 'fileutils'
3
+ require 'set'
4
+ require 'yaml'
5
+ require 'apipie/extractor/recorder'
6
+ require 'apipie/extractor/writer'
7
+ require 'apipie/extractor/collector'
8
+
9
+ class Apipie::Railtie
10
+ initializer 'apipie.extractor' do |app|
11
+ ActiveSupport.on_load :action_controller do
12
+ before_filter do |controller|
13
+ if Apipie.configuration.record
14
+ Apipie::Extractor.call_recorder.analyse_controller(controller)
15
+ end
16
+ end
17
+ end
18
+ app.middleware.use ::Apipie::Extractor::Recorder::Middleware
19
+ end
20
+ end
21
+
22
+ module Apipie
23
+
24
+ module Extractor
25
+
26
+ class << self
27
+
28
+ def start(record)
29
+ Apipie.configuration.record = record
30
+ Apipie.configuration.force_dsl = true
31
+ end
32
+
33
+ def finish
34
+ record_params, record_examples = false, false
35
+ case Apipie.configuration.record
36
+ when "params" then record_params = true
37
+ when "examples" then record_examples = true
38
+ when "all" then record_params = true, record_examples = true
39
+ end
40
+
41
+ if record_examples
42
+ puts "Writing examples to a file"
43
+ write_examples
44
+ end
45
+ if record_params
46
+ puts "Updating auto-generated documentation"
47
+ write_docs
48
+ end
49
+ end
50
+
51
+ def logger
52
+ Rails.logger
53
+ end
54
+
55
+ def call_recorder
56
+ Thread.current[:apipie_call_recorder] ||= Recorder.new
57
+ end
58
+
59
+ def call_finished
60
+ @collector ||= Collector.new
61
+ if record = call_recorder.record
62
+ @collector.handle_record(record)
63
+ end
64
+ end
65
+
66
+ def clean_call_recorder
67
+ Thread.current[:apipie_call_recorder] = nil
68
+ end
69
+
70
+ def write_docs
71
+ Writer.new(@collector).write_docs if @collector
72
+ end
73
+
74
+ def write_examples
75
+ Writer.new(@collector).write_examples if @collector
76
+ end
77
+
78
+ def apis_from_routes
79
+ return @apis_from_routes if @apis_from_routes
80
+
81
+ @api_prefix = Apipie.api_base_url.sub(/\/$/,"")
82
+ populate_api_routes
83
+ update_api_descriptions
84
+
85
+ @apis_from_routes
86
+ end
87
+
88
+ def controller_path(name)
89
+ Apipie.api_controllers_paths.detect { |p| p.include?("#{name}_controller.rb") }
90
+ end
91
+
92
+ private
93
+
94
+ def all_api_routes
95
+ all_routes = Apipie.configuration.api_routes.routes.map do |r|
96
+ {
97
+ :verb => case r.verb
98
+ when Regexp then r.verb.source[/\w+/]
99
+ else r.verb.to_s
100
+ end,
101
+ :path => case
102
+ when r.path.respond_to?(:spec) then r.path.spec.to_s
103
+ else r.path.to_s
104
+ end,
105
+ :controller => r.requirements[:controller],
106
+ :action => r.requirements[:action]
107
+ }
108
+ end
109
+
110
+ return all_routes.find_all do |r|
111
+ r[:path].starts_with?(Apipie.api_base_url)
112
+ end
113
+ end
114
+
115
+ def populate_api_routes
116
+ @apis_from_routes = Hash.new { |h, k| h[k] = [] }
117
+
118
+ all_api_routes.each do |route|
119
+ controller_path, action = route[:controller], route[:action]
120
+ next unless controller_path && action
121
+
122
+ controller_path = controller_path.split('::').map(&:camelize).join('::')
123
+ controller = "#{controller_path}Controller"
124
+
125
+ path = if /^#{Regexp.escape(@api_prefix)}(.*)$/ =~ route[:path]
126
+ $1.sub(/\(\.:format\)$/,'')
127
+ else
128
+ nil
129
+ end
130
+
131
+ if route[:verb].present?
132
+ @apis_from_routes[[controller, action]] << {:method => route[:verb], :path => path}
133
+ end
134
+ end
135
+ end
136
+
137
+ def all_apis_from_docs
138
+ resource_descriptions = Apipie.resource_descriptions.values.map(&:values).flatten
139
+ method_descriptions = resource_descriptions.map(&:method_descriptions).flatten
140
+
141
+ return method_descriptions.reduce({}) do |h, desc|
142
+ apis = desc.method_apis_to_json.map do |api|
143
+ { :method => api[:http_method],
144
+ :path => api[:api_url],
145
+ :desc => api[:short_description] }
146
+ end
147
+ h.update(desc.id => apis)
148
+ end
149
+ end
150
+
151
+ def update_api_descriptions
152
+ apis_from_docs = all_apis_from_docs
153
+ @apis_from_routes.each do |(controller, action), new_apis|
154
+ method_key = "#{Apipie.get_resource_name(controller.safe_constantize || next)}##{action}"
155
+ old_apis = apis_from_docs[method_key] || []
156
+ new_apis.each do |new_api|
157
+ new_api[:path].sub!(/\(\.:format\)$/,"") if new_api[:path]
158
+ old_api = old_apis.find do |api|
159
+ api[:path] == "#{@api_prefix}#{new_api[:path]}"
160
+ end
161
+ if old_api
162
+ new_api[:desc] = old_api[:desc]
163
+ end
164
+ end
165
+ end
166
+ end
167
+ end
168
+ end
169
+ end
170
+
171
+ if ENV["APIPIE_RECORD"]
172
+ Apipie::Extractor.start ENV["APIPIE_RECORD"]
173
+ end
174
+
175
+ at_exit do
176
+ Apipie::Extractor.finish
177
+ end
@@ -0,0 +1,117 @@
1
+ module Apipie
2
+ module Extractor
3
+ class Collector
4
+ attr_reader :descriptions, :records
5
+
6
+ def initialize
7
+ @api_controllers_paths = Apipie.api_controllers_paths
8
+ @ignored = Apipie.configuration.ignored_by_recorder
9
+ @descriptions = Hash.new do |h, k|
10
+ h[k] = {:params => {}, :errors => Set.new}
11
+ end
12
+ @records = Hash.new { |h,k| h[k] = [] }
13
+ end
14
+
15
+ def controller_full_path(controller)
16
+ Apipie::Extractor.controller_path controller.controller_path
17
+ end
18
+
19
+ def ignore_call?(record)
20
+ return true unless record[:controller]
21
+ return true if @ignored.include?(record[:controller].name)
22
+ return true if @ignored.include?("#{Apipie.get_resource_name(record[:controller].name)}##{record[:action]}")
23
+ return true unless @api_controllers_paths.include?(controller_full_path(record[:controller]))
24
+ end
25
+
26
+ def handle_record(record)
27
+ add_to_records(record)
28
+ if ignore_call?(record)
29
+ Extractor.logger.info("REST_API: skipping #{record_to_s(record)}")
30
+ else
31
+ refine_description(record)
32
+ end
33
+ end
34
+
35
+ def add_to_records(record)
36
+ key = "#{Apipie.get_resource_name(record[:controller])}##{record[:action]}"
37
+ @records[key] << record
38
+ end
39
+
40
+ def refine_description(record)
41
+ description = @descriptions["#{record[:controller].name}##{record[:action]}"]
42
+ description[:controller] ||= record[:controller]
43
+ description[:action] ||= record[:action]
44
+
45
+ refine_errors_description(description, record)
46
+ refine_params_description(description[:params], record[:params])
47
+ end
48
+
49
+ def refine_errors_description(description, record)
50
+ if record[:code].to_i >= 300 && !description[:errors].any? { |e| e[:code].to_i == record[:code].to_i }
51
+ description[:errors] << {:code => record[:code]}
52
+ end
53
+ end
54
+
55
+ def refine_params_description(params_desc, recorded_params)
56
+ recorded_params.each do |key, value|
57
+ params_desc[key] ||= {}
58
+ param_desc = params_desc[key]
59
+
60
+ if value.nil?
61
+ param_desc[:allow_nil] = true
62
+ else
63
+ # we specify what type it might be. At the end the first type
64
+ # that left is taken as the more general one
65
+ unless param_desc[:type]
66
+ param_desc[:type] = [:bool, :boolean, :number]
67
+ param_desc[:type] << Hash if value.is_a? Hash
68
+ param_desc[:type] << :undef
69
+ end
70
+
71
+ if [:boolean, :bool].include?(param_desc[:type].first) && (! [true, false, 1, 0].include?(value))
72
+ param_desc[:type].shift
73
+ end
74
+
75
+ if param_desc[:type].first == :number && (key.to_s !~ /id$/ || !Apipie::Validator::NumberValidator.validate(value))
76
+ param_desc[:type].shift
77
+ end
78
+
79
+ if param_desc[:type].first == :decimal && (key.to_s !~ /id$/ || !Apipie::Validator::DecimalValidator.validate(value))
80
+ param_desc[:type].shift
81
+ end
82
+ end
83
+
84
+ if value.is_a? Hash
85
+ param_desc[:nested] ||= {}
86
+ refine_params_description(param_desc[:nested], value)
87
+ end
88
+ end
89
+ end
90
+
91
+ def finalize_descriptions
92
+ @descriptions.each do |method, desc|
93
+ add_routes_info(desc)
94
+ end
95
+ return @descriptions
96
+ end
97
+
98
+ def add_routes_info(desc)
99
+ api_prefix = Apipie.api_base_url.sub(/\/$/,"")
100
+ desc[:api] = Apipie::Extractor.apis_from_routes[[desc[:controller].name, desc[:action]]]
101
+ if desc[:api]
102
+ desc[:params].each do |name, param|
103
+ if desc[:api].all? { |a| a[:path].include?(":#{name}") }
104
+ param[:required] = true
105
+ end
106
+ end
107
+ end
108
+ end
109
+
110
+ def record_to_s(record)
111
+ "#{record[:controller]}##{record[:action]}"
112
+ end
113
+
114
+ end
115
+ end
116
+ end
117
+
@@ -0,0 +1,166 @@
1
+ module Apipie
2
+ module Extractor
3
+ class Recorder
4
+ MULTIPART_BOUNDARY = 'APIPIE_RECORDER_EXAMPLE_BOUNDARY'
5
+
6
+ def initialize
7
+ @ignored_params = [:controller, :action]
8
+ end
9
+
10
+ def analyse_env(env)
11
+ @verb = env["REQUEST_METHOD"].to_sym
12
+ @path = env["PATH_INFO"].sub(/^\/*/,"/")
13
+ @query = env["QUERY_STRING"] unless env["QUERY_STRING"].blank?
14
+ @params = Rack::Utils.parse_nested_query(@query)
15
+ @params.merge!(env["action_dispatch.request.request_parameters"] || {})
16
+ rack_input = env["rack.input"]
17
+ if data = parse_data(rack_input.read)
18
+ @request_data = data
19
+ elsif form_hash = env["rack.request.form_hash"]
20
+ @request_data = reformat_multipart_data(form_hash)
21
+ end
22
+ rack_input.rewind
23
+ end
24
+
25
+ def analyse_controller(controller)
26
+ @controller = controller.class
27
+ @action = controller.params[:action]
28
+ end
29
+
30
+ def analyse_response(response)
31
+ if response.last.respond_to?(:body) && data = parse_data(response.last.body)
32
+ @response_data = if response[1]['Content-Disposition'].to_s.start_with?('attachment')
33
+ '<STREAMED ATTACHMENT FILE>'
34
+ else
35
+ data
36
+ end
37
+ end
38
+ @code = response.first
39
+ end
40
+
41
+ def analyze_functional_test(test_context)
42
+ request, response = test_context.request, test_context.response
43
+ @verb = request.request_method.to_sym
44
+ @path = request.path
45
+ @params = request.request_parameters
46
+ if [:POST, :PUT, :PATCH, :DELETE].include?(@verb)
47
+ @request_data = @params
48
+ else
49
+ @query = request.query_string
50
+ end
51
+ @response_data = parse_data(response.body)
52
+ @code = response.code
53
+ end
54
+
55
+ def parse_data(data)
56
+ return nil if data.strip.blank?
57
+ JSON.parse(data)
58
+ rescue StandardError
59
+ data
60
+ end
61
+
62
+ def reformat_multipart_data(form)
63
+ form.empty? and return ''
64
+ lines = ["Content-Type: multipart/form-data; boundary=#{MULTIPART_BOUNDARY}",'']
65
+ boundary = "--#{MULTIPART_BOUNDARY}"
66
+ form.each do |key, attrs|
67
+ if attrs.is_a?(String)
68
+ lines << boundary << content_disposition(key) << "Content-Length: #{attrs.size}" << '' << attrs
69
+ else
70
+ reformat_hash(boundary, attrs, lines)
71
+ end
72
+ end
73
+ lines << "#{boundary}--"
74
+ lines.join("\n")
75
+ end
76
+
77
+ def reformat_hash(boundary, attrs, lines)
78
+ if head = attrs[:head]
79
+ lines << boundary
80
+ lines.concat(head.split("\r\n"))
81
+ # To avoid large and/or binary file bodies, simply indicate the contents in the output.
82
+ lines << '' << %{... contents of "#{attrs[:name]}" ...}
83
+ else
84
+ # Look for subelements that contain a part.
85
+ attrs.each { |k,v| v.is_a?(Hash) and reformat_hash(boundary, v, lines) }
86
+ end
87
+ end
88
+
89
+ def content_disposition(name)
90
+ %{Content-Disposition: form-data; name="#{name}"}
91
+ end
92
+
93
+ def reformat_data(data)
94
+ parsed = parse_data(data)
95
+ case parsed
96
+ when nil
97
+ nil
98
+ when String
99
+ parsed
100
+ else
101
+ JSON.pretty_generate().gsub(/: \[\s*\]/,": []").gsub(/\{\s*\}/,"{}")
102
+ end
103
+ end
104
+
105
+ def record
106
+ if @controller
107
+ {:controller => @controller,
108
+ :action => @action,
109
+ :verb => @verb,
110
+ :path => @path,
111
+ :params => @params,
112
+ :query => @query,
113
+ :request_data => @request_data,
114
+ :response_data => @response_data,
115
+ :code => @code}
116
+ else
117
+ nil
118
+ end
119
+ end
120
+
121
+ protected
122
+
123
+ def api_description
124
+ end
125
+
126
+ class Middleware
127
+ def initialize(app)
128
+ @app = app
129
+ end
130
+
131
+ def call(env)
132
+ if Apipie.configuration.record
133
+ analyze(env) do
134
+ @app.call(env)
135
+ end
136
+ else
137
+ @app.call(env)
138
+ end
139
+ end
140
+
141
+ def analyze(env, &block)
142
+ Apipie::Extractor.call_recorder.analyse_env(env)
143
+ response = block.call
144
+ Apipie::Extractor.call_recorder.analyse_response(response)
145
+ Apipie::Extractor.call_finished
146
+ response
147
+ ensure
148
+ Apipie::Extractor.clean_call_recorder
149
+ end
150
+ end
151
+
152
+ module FunctionalTestRecording
153
+ def process(*args) # action, parameters = nil, session = nil, flash = nil, http_method = 'GET')
154
+ ret = super(*args)
155
+ if Apipie.configuration.record
156
+ Apipie::Extractor.call_recorder.analyze_functional_test(self)
157
+ Apipie::Extractor.call_finished
158
+ end
159
+ ret
160
+ ensure
161
+ Apipie::Extractor.clean_call_recorder
162
+ end
163
+ end
164
+ end
165
+ end
166
+ end