apipierails3 0.0.1

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