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.
- checksums.yaml +17 -0
- data/.gitignore +14 -0
- data/.rspec +2 -0
- data/.travis.yml +27 -0
- data/APACHE-LICENSE-2.0 +202 -0
- data/CHANGELOG.md +469 -0
- data/Gemfile +1 -0
- data/Gemfile.rails32 +6 -0
- data/Gemfile.rails41 +6 -0
- data/Gemfile.rails42 +11 -0
- data/Gemfile.rails50 +6 -0
- data/Gemfile.rails51 +7 -0
- data/MIT-LICENSE +20 -0
- data/NOTICE +4 -0
- data/PROPOSAL_FOR_RESPONSE_DESCRIPTIONS.md +244 -0
- data/README.rst +1874 -0
- data/Rakefile +13 -0
- data/apipierails3.gemspec +27 -0
- data/app/controllers/apipie/apipies_controller.rb +199 -0
- data/app/helpers/apipie_helper.rb +10 -0
- data/app/public/apipie/javascripts/apipie.js +6 -0
- data/app/public/apipie/javascripts/bundled/bootstrap-collapse.js +138 -0
- data/app/public/apipie/javascripts/bundled/bootstrap.js +1726 -0
- data/app/public/apipie/javascripts/bundled/jquery.js +5 -0
- data/app/public/apipie/javascripts/bundled/prettify.js +28 -0
- data/app/public/apipie/stylesheets/application.css +7 -0
- data/app/public/apipie/stylesheets/bundled/bootstrap-responsive.min.css +12 -0
- data/app/public/apipie/stylesheets/bundled/bootstrap.min.css +689 -0
- data/app/public/apipie/stylesheets/bundled/prettify.css +30 -0
- data/app/views/apipie/apipies/_disqus.html.erb +13 -0
- data/app/views/apipie/apipies/_errors.html.erb +23 -0
- data/app/views/apipie/apipies/_headers.html.erb +26 -0
- data/app/views/apipie/apipies/_languages.erb +6 -0
- data/app/views/apipie/apipies/_metadata.erb +1 -0
- data/app/views/apipie/apipies/_method_detail.erb +61 -0
- data/app/views/apipie/apipies/_params.html.erb +42 -0
- data/app/views/apipie/apipies/_params_plain.html.erb +20 -0
- data/app/views/apipie/apipies/apipie_404.html.erb +17 -0
- data/app/views/apipie/apipies/apipie_checksum.json.erb +1 -0
- data/app/views/apipie/apipies/getting_started.html.erb +6 -0
- data/app/views/apipie/apipies/index.html.erb +56 -0
- data/app/views/apipie/apipies/method.html.erb +41 -0
- data/app/views/apipie/apipies/plain.html.erb +77 -0
- data/app/views/apipie/apipies/resource.html.erb +80 -0
- data/app/views/apipie/apipies/static.html.erb +103 -0
- data/app/views/layouts/apipie/apipie.html.erb +27 -0
- data/config/locales/de.yml +28 -0
- data/config/locales/en.yml +32 -0
- data/config/locales/es.yml +28 -0
- data/config/locales/fr.yml +31 -0
- data/config/locales/it.yml +31 -0
- data/config/locales/ja.yml +31 -0
- data/config/locales/pl.yml +28 -0
- data/config/locales/pt-BR.yml +28 -0
- data/config/locales/ru.yml +28 -0
- data/config/locales/tr.yml +28 -0
- data/config/locales/zh-CN.yml +28 -0
- data/config/locales/zh-TW.yml +28 -0
- data/images/screenshot-1.png +0 -0
- data/images/screenshot-2.png +0 -0
- data/lib/apipie/apipie_module.rb +83 -0
- data/lib/apipie/application.rb +462 -0
- data/lib/apipie/configuration.rb +186 -0
- data/lib/apipie/dsl_definition.rb +607 -0
- data/lib/apipie/error_description.rb +44 -0
- data/lib/apipie/errors.rb +86 -0
- data/lib/apipie/extractor.rb +177 -0
- data/lib/apipie/extractor/collector.rb +117 -0
- data/lib/apipie/extractor/recorder.rb +166 -0
- data/lib/apipie/extractor/writer.rb +454 -0
- data/lib/apipie/helpers.rb +73 -0
- data/lib/apipie/markup.rb +48 -0
- data/lib/apipie/method_description.rb +273 -0
- data/lib/apipie/middleware/checksum_in_headers.rb +35 -0
- data/lib/apipie/param_description.rb +280 -0
- data/lib/apipie/railtie.rb +9 -0
- data/lib/apipie/resource_description.rb +124 -0
- data/lib/apipie/response_description.rb +131 -0
- data/lib/apipie/response_description_adapter.rb +200 -0
- data/lib/apipie/routes_formatter.rb +33 -0
- data/lib/apipie/routing.rb +16 -0
- data/lib/apipie/rspec/response_validation_helper.rb +192 -0
- data/lib/apipie/see_description.rb +39 -0
- data/lib/apipie/static_dispatcher.rb +69 -0
- data/lib/apipie/swagger_generator.rb +707 -0
- data/lib/apipie/tag_list_description.rb +11 -0
- data/lib/apipie/validator.rb +526 -0
- data/lib/apipie/version.rb +3 -0
- data/lib/apipierails3.rb +25 -0
- data/lib/generators/apipie/install/README +6 -0
- data/lib/generators/apipie/install/install_generator.rb +25 -0
- data/lib/generators/apipie/install/templates/initializer.rb.erb +7 -0
- data/lib/generators/apipie/views_generator.rb +11 -0
- data/lib/tasks/apipie.rake +345 -0
- data/rel-eng/packages/.readme +3 -0
- data/rel-eng/packages/rubygem-apipie-rails +1 -0
- data/rel-eng/tito.props +5 -0
- data/spec/controllers/api/v1/architectures_controller_spec.rb +29 -0
- data/spec/controllers/api/v2/architectures_controller_spec.rb +12 -0
- data/spec/controllers/api/v2/nested/resources_controller_spec.rb +11 -0
- data/spec/controllers/apipies_controller_spec.rb +273 -0
- data/spec/controllers/concerns_controller_spec.rb +42 -0
- data/spec/controllers/extended_controller_spec.rb +11 -0
- data/spec/controllers/users_controller_spec.rb +740 -0
- data/spec/dummy/Rakefile +7 -0
- data/spec/dummy/app/controllers/api/base_controller.rb +4 -0
- data/spec/dummy/app/controllers/api/v1/architectures_controller.rb +43 -0
- data/spec/dummy/app/controllers/api/v1/base_controller.rb +11 -0
- data/spec/dummy/app/controllers/api/v2/architectures_controller.rb +30 -0
- data/spec/dummy/app/controllers/api/v2/base_controller.rb +11 -0
- data/spec/dummy/app/controllers/api/v2/nested/architectures_controller.rb +32 -0
- data/spec/dummy/app/controllers/api/v2/nested/resources_controller.rb +33 -0
- data/spec/dummy/app/controllers/application_controller.rb +18 -0
- data/spec/dummy/app/controllers/concerns/extending_concern.rb +11 -0
- data/spec/dummy/app/controllers/concerns/sample_controller.rb +41 -0
- data/spec/dummy/app/controllers/concerns_controller.rb +8 -0
- data/spec/dummy/app/controllers/extended_controller.rb +14 -0
- data/spec/dummy/app/controllers/files_controller.rb +5 -0
- data/spec/dummy/app/controllers/overridden_concerns_controller.rb +31 -0
- data/spec/dummy/app/controllers/pets_controller.rb +408 -0
- data/spec/dummy/app/controllers/pets_using_auto_views_controller.rb +73 -0
- data/spec/dummy/app/controllers/pets_using_self_describing_classes_controller.rb +95 -0
- data/spec/dummy/app/controllers/tagged_cats_controller.rb +32 -0
- data/spec/dummy/app/controllers/tagged_dogs_controller.rb +15 -0
- data/spec/dummy/app/controllers/twitter_example_controller.rb +307 -0
- data/spec/dummy/app/controllers/users_controller.rb +297 -0
- data/spec/dummy/app/views/layouts/application.html.erb +21 -0
- data/spec/dummy/config.ru +4 -0
- data/spec/dummy/config/application.rb +49 -0
- data/spec/dummy/config/boot.rb +10 -0
- data/spec/dummy/config/database.yml +21 -0
- data/spec/dummy/config/environment.rb +8 -0
- data/spec/dummy/config/environments/development.rb +28 -0
- data/spec/dummy/config/environments/production.rb +52 -0
- data/spec/dummy/config/environments/test.rb +38 -0
- data/spec/dummy/config/initializers/apipie.rb +110 -0
- data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
- data/spec/dummy/config/initializers/inflections.rb +10 -0
- data/spec/dummy/config/initializers/mime_types.rb +5 -0
- data/spec/dummy/config/initializers/secret_token.rb +8 -0
- data/spec/dummy/config/initializers/session_store.rb +8 -0
- data/spec/dummy/config/locales/en.yml +5 -0
- data/spec/dummy/config/routes.rb +51 -0
- data/spec/dummy/db/.gitkeep +0 -0
- data/spec/dummy/doc/apipie_examples.json +1 -0
- data/spec/dummy/doc/users/desc_from_file.md +1 -0
- data/spec/dummy/public/404.html +26 -0
- data/spec/dummy/public/422.html +26 -0
- data/spec/dummy/public/500.html +26 -0
- data/spec/dummy/public/favicon.ico +0 -0
- data/spec/dummy/public/stylesheets/.gitkeep +0 -0
- data/spec/dummy/script/rails +6 -0
- data/spec/lib/application_spec.rb +49 -0
- data/spec/lib/extractor/extractor_spec.rb +9 -0
- data/spec/lib/extractor/middleware_spec.rb +44 -0
- data/spec/lib/extractor/writer_spec.rb +110 -0
- data/spec/lib/file_handler_spec.rb +18 -0
- data/spec/lib/method_description_spec.rb +98 -0
- data/spec/lib/param_description_spec.rb +345 -0
- data/spec/lib/param_group_spec.rb +60 -0
- data/spec/lib/rake_spec.rb +71 -0
- data/spec/lib/resource_description_spec.rb +48 -0
- data/spec/lib/swagger/openapi_2_0_schema.json +1607 -0
- data/spec/lib/swagger/rake_swagger_spec.rb +139 -0
- data/spec/lib/swagger/response_validation_spec.rb +104 -0
- data/spec/lib/swagger/swagger_dsl_spec.rb +658 -0
- data/spec/lib/validator_spec.rb +113 -0
- data/spec/lib/validators/array_validator_spec.rb +85 -0
- data/spec/spec_helper.rb +109 -0
- data/spec/support/rake.rb +21 -0
- 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
|