grape_ape_rails 0.5.0
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 +7 -0
- data/.gitignore +18 -0
- data/.rspec +2 -0
- data/.ruby-gemset +1 -0
- data/.ruby-version +1 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +29 -0
- data/Rakefile +1 -0
- data/grape_ape_rails.gemspec +46 -0
- data/lib/generators/setup_generator.rb +59 -0
- data/lib/generators/templates/initializer.rb +22 -0
- data/lib/grape_ape_rails/api.rb +88 -0
- data/lib/grape_ape_rails/base.rb +27 -0
- data/lib/grape_ape_rails/handlers/formatters.rb +134 -0
- data/lib/grape_ape_rails/handlers/header_versioning.rb +27 -0
- data/lib/grape_ape_rails/handlers/locale.rb +24 -0
- data/lib/grape_ape_rails/handlers/rails_logging.rb +33 -0
- data/lib/grape_ape_rails/handlers/responses.rb +32 -0
- data/lib/grape_ape_rails/handlers.rb +22 -0
- data/lib/grape_ape_rails/railtie.rb +9 -0
- data/lib/grape_ape_rails/version.rb +3 -0
- data/lib/grape_ape_rails.rb +55 -0
- data/lib/swagger/grape_swagger_modified.rb +470 -0
- data/lib/tasks/routes.rake +13 -0
- data/spec/dummy/.rspec +1 -0
- data/spec/dummy/README.rdoc +28 -0
- data/spec/dummy/Rakefile +15 -0
- data/spec/dummy/app/assets/images/.keep +0 -0
- data/spec/dummy/app/assets/javascripts/application.js +13 -0
- data/spec/dummy/app/assets/stylesheets/application.css +13 -0
- data/spec/dummy/app/controllers/api/base.rb +18 -0
- data/spec/dummy/app/controllers/api/v1/monkeys.rb +17 -0
- data/spec/dummy/app/controllers/api/v1/widgets.rb +57 -0
- data/spec/dummy/app/controllers/api/v2/widgets.rb +18 -0
- data/spec/dummy/app/controllers/application_controller.rb +5 -0
- data/spec/dummy/app/controllers/concerns/.keep +0 -0
- data/spec/dummy/app/helpers/application_helper.rb +2 -0
- data/spec/dummy/app/mailers/.keep +0 -0
- data/spec/dummy/app/models/.keep +0 -0
- data/spec/dummy/app/models/concerns/.keep +0 -0
- data/spec/dummy/app/models/monkey.rb +3 -0
- data/spec/dummy/app/models/monkey_serializer.rb +7 -0
- data/spec/dummy/app/models/widget.rb +6 -0
- data/spec/dummy/app/views/api/v1/base_widget.rabl +1 -0
- data/spec/dummy/app/views/api/v1/widget.rabl +2 -0
- data/spec/dummy/app/views/api/v1/widgets.rabl +2 -0
- data/spec/dummy/app/views/api/v2/base_widget.rabl +2 -0
- data/spec/dummy/app/views/api/v2/widget.rabl +2 -0
- data/spec/dummy/app/views/layouts/application.html.erb +14 -0
- data/spec/dummy/bin/bundle +3 -0
- data/spec/dummy/bin/rails +4 -0
- data/spec/dummy/bin/rake +4 -0
- data/spec/dummy/config/application.rb +28 -0
- data/spec/dummy/config/boot.rb +5 -0
- data/spec/dummy/config/database.yml +25 -0
- data/spec/dummy/config/environment.rb +5 -0
- data/spec/dummy/config/environments/development.rb +29 -0
- data/spec/dummy/config/environments/production.rb +80 -0
- data/spec/dummy/config/environments/test.rb +36 -0
- data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
- data/spec/dummy/config/initializers/filter_parameter_logging.rb +4 -0
- data/spec/dummy/config/initializers/grape_ape_rails.rb +23 -0
- data/spec/dummy/config/initializers/inflections.rb +16 -0
- data/spec/dummy/config/initializers/mime_types.rb +5 -0
- data/spec/dummy/config/initializers/secret_token.rb +12 -0
- data/spec/dummy/config/initializers/session_store.rb +3 -0
- data/spec/dummy/config/initializers/wrap_parameters.rb +14 -0
- data/spec/dummy/config/locales/en.yml +23 -0
- data/spec/dummy/config/routes.rb +5 -0
- data/spec/dummy/config.ru +4 -0
- data/spec/dummy/db/development.sqlite3 +0 -0
- data/spec/dummy/db/migrate/20140714154355_create_widgets.rb +10 -0
- data/spec/dummy/db/migrate/20140804173544_create_monkeys.rb +9 -0
- data/spec/dummy/db/schema.rb +30 -0
- data/spec/dummy/db/test.sqlite3 +0 -0
- data/spec/dummy/lib/assets/.keep +0 -0
- data/spec/dummy/public/404.html +58 -0
- data/spec/dummy/public/422.html +58 -0
- data/spec/dummy/public/500.html +57 -0
- data/spec/dummy/public/favicon.ico +0 -0
- data/spec/requests/v1/monkeys_spec.rb +22 -0
- data/spec/requests/v1/widgets_spec.rb +125 -0
- data/spec/requests/v2/widgets_spec.rb +41 -0
- data/spec/spec_helper.rb +102 -0
- metadata +524 -0
@@ -0,0 +1,27 @@
|
|
1
|
+
module GrapeApeRails
|
2
|
+
module Handlers
|
3
|
+
module HeaderVersioning
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
included do
|
7
|
+
gar_resource = self.name.split('::').last.underscore
|
8
|
+
gar_version = self.name.split('::')[-2].underscore.gsub('_','.')
|
9
|
+
cascades = GrapeApeRails::API.api_version_cascades_map
|
10
|
+
gar_version = cascades[gar_version] if cascades[gar_version].present?
|
11
|
+
gar_appname = GrapeApeRails.configuration.app_name
|
12
|
+
gar_organization = GrapeApeRails.configuration.organization_name
|
13
|
+
version gar_version, using: :header, vendor: "#{gar_organization}.#{gar_appname}", strict: true
|
14
|
+
|
15
|
+
before do
|
16
|
+
req = Rack::Request.new(env)
|
17
|
+
api_key = version ? GrapeApeRails::API.api_keys_map[version] : nil
|
18
|
+
if api_key.nil?
|
19
|
+
msg = "Cannot determine API version from header info."
|
20
|
+
error!({ code: "UNAUTHORIZED", message: msg}, 401)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module GrapeApeRails
|
2
|
+
module Handlers
|
3
|
+
module Locale
|
4
|
+
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
|
7
|
+
included do
|
8
|
+
I18n.config.enforce_available_locales = true
|
9
|
+
I18n.config.load_path += Dir['./config/locales/*.yml']
|
10
|
+
I18n.config.available_locales = GrapeApeRails.configuration.available_locales
|
11
|
+
I18n.config.default_locale = :en
|
12
|
+
|
13
|
+
before do
|
14
|
+
# set the locale based on Accept-Language or params[:locale]
|
15
|
+
req = Rack::Request.new(env)
|
16
|
+
params_locale = (req.params['locale'] && I18n.available_locales.include?(req.params['locale'].downcase.to_sym)) ? req.params['locale'] : nil
|
17
|
+
I18n.locale = env.http_accept_language.compatible_language_from(I18n.available_locales) || params_locale || :en
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module GrapeApeRails
|
2
|
+
module Handlers
|
3
|
+
class RailsLogging
|
4
|
+
def initialize(app)
|
5
|
+
@app = app
|
6
|
+
end
|
7
|
+
|
8
|
+
def call(env)
|
9
|
+
api_version = env['rack.routing_args'][:route_info].route_version rescue nil
|
10
|
+
payload = {
|
11
|
+
remote_addr: env['REMOTE_ADDR'],
|
12
|
+
request_method: env['REQUEST_METHOD'],
|
13
|
+
request_path: env['PATH_INFO'],
|
14
|
+
request_query: env['QUERY_STRING'],
|
15
|
+
api_version: api_version
|
16
|
+
}
|
17
|
+
req = Rack::Request.new(env)
|
18
|
+
payload[:params] = req.params
|
19
|
+
ActiveSupport::Notifications.instrument "grape.request", payload do
|
20
|
+
@app.call(env).tap do |response|
|
21
|
+
status, headers, body = *response
|
22
|
+
payload[:params].merge!(env["api.endpoint"].params.to_hash)
|
23
|
+
payload[:params].delete("route_info")
|
24
|
+
payload[:params].delete("format")
|
25
|
+
payload[:response_status] = status
|
26
|
+
end
|
27
|
+
end
|
28
|
+
rescue
|
29
|
+
@app.call(env)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module GrapeApeRails
|
2
|
+
module Handlers
|
3
|
+
module Responses
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
included do
|
7
|
+
format :json
|
8
|
+
formatter :json, Grape::Formatter::GarRabl
|
9
|
+
error_formatter :json, Grape::Formatter::GarError
|
10
|
+
|
11
|
+
before do
|
12
|
+
# ...
|
13
|
+
end
|
14
|
+
|
15
|
+
rescue_from Grape::Exceptions::ValidationErrors do |e|
|
16
|
+
status = 422
|
17
|
+
hash_err = { error: {
|
18
|
+
code: 'RESOURCE_FAILED_VALIDATION',
|
19
|
+
message: '[RESOURCE_FAILED_VALIDATION] Resource failed validation per API parameter requirements' }
|
20
|
+
}
|
21
|
+
log_msg = hash_err[:error].clone
|
22
|
+
hash_err[:error].merge!({ data: e.errors }) if e.errors.present?
|
23
|
+
if defined?(::Rails) && Rails.respond_to?(:logger)
|
24
|
+
api_version = "[#{env['rack.routing_args'][:route_info].route_version}]" rescue nil
|
25
|
+
Rails.logger.warn "[API]#{api_version} Responding with #{status} #{log_msg}"
|
26
|
+
end
|
27
|
+
Rack::Response.new(MultiJson.dump(hash_err), status)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module GrapeApeRails
|
2
|
+
module Handlers
|
3
|
+
module All
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
included do
|
7
|
+
# middleware for every API
|
8
|
+
use ::HttpAcceptLanguage::Middleware
|
9
|
+
use ::GrapeApeRails::Handlers::RailsLogging
|
10
|
+
|
11
|
+
# third-party Grape tools
|
12
|
+
include Grape::Kaminari
|
13
|
+
include Grape::Rails::Cache
|
14
|
+
|
15
|
+
# Gar-specific handlers
|
16
|
+
include GrapeApeRails::Handlers::HeaderVersioning
|
17
|
+
include GrapeApeRails::Handlers::Responses
|
18
|
+
include GrapeApeRails::Handlers::Locale
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
require "grape_ape_rails/version"
|
2
|
+
|
3
|
+
require 'active_support'
|
4
|
+
require 'grape'
|
5
|
+
require 'swagger/grape_swagger_modified'
|
6
|
+
require 'grape_ape_rails/railtie' if defined?(Rails)
|
7
|
+
require 'grape_ape_rails/api'
|
8
|
+
require 'grape_ape_rails/base'
|
9
|
+
require 'grape_ape_rails/handlers/header_versioning'
|
10
|
+
require 'grape_ape_rails/handlers/responses'
|
11
|
+
require 'grape_ape_rails/handlers/formatters'
|
12
|
+
require 'grape_ape_rails/handlers/locale'
|
13
|
+
require 'grape_ape_rails/handlers'
|
14
|
+
|
15
|
+
|
16
|
+
module GrapeApeRails
|
17
|
+
|
18
|
+
class << self
|
19
|
+
attr_accessor :configuration
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.configure
|
23
|
+
self.configuration ||= Configuration.new
|
24
|
+
yield(configuration)
|
25
|
+
raise_configuration_errors!
|
26
|
+
self
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.raise_configuration_errors!
|
30
|
+
if self.configuration.api_header_security_enabled
|
31
|
+
%i[ app_name organization_name api_secret_key ].each do |setting|
|
32
|
+
if self.configuration.send(setting).nil?
|
33
|
+
raise "You must set #{setting} in a configuration block in your initializer!"
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
class Configuration
|
40
|
+
attr_accessor :app_name, :organization_name,
|
41
|
+
:api_secret_key, :api_header_security_enabled,
|
42
|
+
:available_locales, :security_envelope_debug
|
43
|
+
|
44
|
+
def initialize
|
45
|
+
@app_name = nil
|
46
|
+
@organization_name = nil
|
47
|
+
@api_secret_key = nil
|
48
|
+
@api_header_security_enabled = true
|
49
|
+
@api_security_algorithm = "sha256"
|
50
|
+
@available_locales = [ :en ]
|
51
|
+
# ...
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
end
|
@@ -0,0 +1,470 @@
|
|
1
|
+
require 'kramdown'
|
2
|
+
module Grape
|
3
|
+
class API
|
4
|
+
class << self
|
5
|
+
attr_reader :combined_routes, :combined_namespaces
|
6
|
+
|
7
|
+
def add_swagger_documentation(options = {})
|
8
|
+
documentation_class = create_documentation_class
|
9
|
+
documentation_class.setup({ target_class: self }.merge(options))
|
10
|
+
mount(documentation_class)
|
11
|
+
|
12
|
+
@combined_routes = {}
|
13
|
+
routes.each do |route|
|
14
|
+
route_match = route.route_path.split(route.route_prefix).last.match('\/([\w|-]*?)[\.\/\(]')
|
15
|
+
next if route_match.nil?
|
16
|
+
resource = route_match.captures.first
|
17
|
+
next if resource.empty?
|
18
|
+
resource.downcase!
|
19
|
+
@combined_routes[resource] ||= []
|
20
|
+
next if @@hide_documentation_path && route.route_path.include?(@@mount_path)
|
21
|
+
@combined_routes[resource] << route
|
22
|
+
end
|
23
|
+
|
24
|
+
@combined_namespaces = {}
|
25
|
+
combine_namespaces(self)
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def combine_namespaces(app)
|
31
|
+
app.endpoints.each do |endpoint|
|
32
|
+
ns = endpoint.settings.stack.last[:namespace]
|
33
|
+
@combined_namespaces[ns.space] = ns if ns
|
34
|
+
|
35
|
+
combine_namespaces(endpoint.options[:app]) if endpoint.options[:app]
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def create_documentation_class
|
40
|
+
Class.new(Grape::API) do
|
41
|
+
class << self
|
42
|
+
def name
|
43
|
+
@@class_name
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def self.setup(options)
|
48
|
+
defaults = {
|
49
|
+
target_class: nil,
|
50
|
+
mount_path: '/swagger_doc',
|
51
|
+
mount_with_version: false,
|
52
|
+
base_path: nil,
|
53
|
+
api_version: '0.1',
|
54
|
+
markdown: false,
|
55
|
+
hide_documentation_path: false,
|
56
|
+
hide_format: false,
|
57
|
+
format: nil,
|
58
|
+
models: [],
|
59
|
+
info: {},
|
60
|
+
authorizations: nil,
|
61
|
+
root_base_path: true,
|
62
|
+
api_documentation: { desc: 'Swagger compatible API description' },
|
63
|
+
specific_api_documentation: { desc: 'Swagger compatible API description for specific API' }
|
64
|
+
}
|
65
|
+
|
66
|
+
options = defaults.merge(options)
|
67
|
+
|
68
|
+
target_class = options[:target_class]
|
69
|
+
api_version = options[:api_version]
|
70
|
+
@@mount_path = options[:mount_path] + ((options[:mount_with_version] and api_version.present?) ? "/#{api_version}" : '')
|
71
|
+
@@class_name = options[:class_name] || options[:mount_path].gsub('/', '')
|
72
|
+
@@markdown = options[:markdown]
|
73
|
+
@@hide_format = options[:hide_format]
|
74
|
+
base_path = options[:base_path]
|
75
|
+
authorizations = options[:authorizations]
|
76
|
+
root_base_path = options[:root_base_path]
|
77
|
+
extra_info = options[:info]
|
78
|
+
api_doc = options[:api_documentation].dup
|
79
|
+
specific_api_doc = options[:specific_api_documentation].dup
|
80
|
+
@@models = options[:models] || []
|
81
|
+
@@hide_documentation_path = options[:hide_documentation_path]
|
82
|
+
|
83
|
+
if options[:format]
|
84
|
+
[:format, :default_format, :default_error_formatter].each do |method|
|
85
|
+
send(method, options[:format])
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
desc api_doc.delete(:desc), params: api_doc.delete(:params)
|
90
|
+
@last_description.merge!(api_doc)
|
91
|
+
get @@mount_path do
|
92
|
+
header['Access-Control-Allow-Origin'] = '*'
|
93
|
+
header['Access-Control-Request-Method'] = '*'
|
94
|
+
|
95
|
+
routes = target_class.combined_routes
|
96
|
+
namespaces = target_class.combined_namespaces
|
97
|
+
|
98
|
+
if @@hide_documentation_path
|
99
|
+
routes.reject! { |route, _value| "/#{route}/".index(parse_path(@@mount_path, nil) << '/') == 0 }
|
100
|
+
end
|
101
|
+
|
102
|
+
routes_array = routes.keys.map do |local_route|
|
103
|
+
next if routes[local_route].all?(&:route_hidden)
|
104
|
+
|
105
|
+
url_format = '.{format}' unless @@hide_format
|
106
|
+
|
107
|
+
description = namespaces[local_route] && namespaces[local_route].options[:desc]
|
108
|
+
description ||= "Operations about #{local_route.pluralize}"
|
109
|
+
|
110
|
+
{
|
111
|
+
path: "/#{local_route}#{url_format}",
|
112
|
+
description: description
|
113
|
+
}
|
114
|
+
end.compact
|
115
|
+
|
116
|
+
output = {
|
117
|
+
apiVersion: api_version,
|
118
|
+
swaggerVersion: '1.2',
|
119
|
+
produces: content_types_for(target_class),
|
120
|
+
apis: routes_array,
|
121
|
+
info: parse_info(extra_info)
|
122
|
+
}
|
123
|
+
|
124
|
+
output[:authorizations] = authorizations unless authorizations.nil? || authorizations.empty?
|
125
|
+
|
126
|
+
output
|
127
|
+
end
|
128
|
+
|
129
|
+
desc specific_api_doc.delete(:desc), params: {
|
130
|
+
'name' => {
|
131
|
+
desc: 'Resource name of mounted API',
|
132
|
+
type: 'string',
|
133
|
+
required: true
|
134
|
+
}
|
135
|
+
}.merge(specific_api_doc.delete(:params) || {})
|
136
|
+
@last_description.merge!(specific_api_doc)
|
137
|
+
get "#{@@mount_path}/:name" do
|
138
|
+
header['Access-Control-Allow-Origin'] = '*'
|
139
|
+
header['Access-Control-Request-Method'] = '*'
|
140
|
+
|
141
|
+
models = []
|
142
|
+
routes = target_class.combined_routes[params[:name]]
|
143
|
+
error!('Not Found', 404) unless routes
|
144
|
+
|
145
|
+
ops = routes.reject(&:route_hidden).group_by do |route|
|
146
|
+
parse_path(route.route_path, api_version)
|
147
|
+
end
|
148
|
+
|
149
|
+
error!('Not Found', 404) unless ops.any?
|
150
|
+
|
151
|
+
apis = []
|
152
|
+
|
153
|
+
ops.each do |path, op_routes|
|
154
|
+
operations = op_routes.map do |route|
|
155
|
+
notes = as_markdown(route.route_notes)
|
156
|
+
|
157
|
+
http_codes = parse_http_codes(route.route_http_codes, models)
|
158
|
+
|
159
|
+
models << @@models if @@models.present?
|
160
|
+
|
161
|
+
models << route.route_entity if route.route_entity.present?
|
162
|
+
|
163
|
+
models = models_with_included_presenters(models.flatten.compact)
|
164
|
+
|
165
|
+
operation = {
|
166
|
+
notes: notes.to_s,
|
167
|
+
summary: route.route_description || '',
|
168
|
+
nickname: route.route_nickname || (route.route_method + route.route_path.gsub(/[\/:\(\)\.]/, '-')),
|
169
|
+
method: route.route_method,
|
170
|
+
parameters: parse_header_params(route.route_headers) + parse_params(route.route_params, route.route_path, route.route_method),
|
171
|
+
type: 'void'
|
172
|
+
}
|
173
|
+
operation[:authorizations] = route.route_authorizations unless route.route_authorizations.nil? || route.route_authorizations.empty?
|
174
|
+
if operation[:parameters].any? { | param | param[:type] == 'File' }
|
175
|
+
operation.merge!(consumes: ['multipart/form-data'])
|
176
|
+
end
|
177
|
+
operation.merge!(responseMessages: http_codes) unless http_codes.empty?
|
178
|
+
|
179
|
+
if route.route_entity
|
180
|
+
type = parse_entity_name(route.route_entity)
|
181
|
+
if route.instance_variable_get(:@options)[:is_array]
|
182
|
+
operation.merge!(
|
183
|
+
'type' => 'array',
|
184
|
+
'items' => generate_typeref(type)
|
185
|
+
)
|
186
|
+
else
|
187
|
+
operation.merge!('type' => type)
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
operation[:nickname] = route.route_nickname if route.route_nickname
|
192
|
+
operation
|
193
|
+
end.compact
|
194
|
+
apis << {
|
195
|
+
path: path,
|
196
|
+
operations: operations
|
197
|
+
}
|
198
|
+
end
|
199
|
+
|
200
|
+
api_description = {
|
201
|
+
apiVersion: api_version,
|
202
|
+
swaggerVersion: '1.2',
|
203
|
+
resourcePath: "/#{params[:name]}",
|
204
|
+
produces: content_types_for(target_class),
|
205
|
+
apis: apis
|
206
|
+
}
|
207
|
+
|
208
|
+
base_path = parse_base_path(base_path, request)
|
209
|
+
api_description[:basePath] = base_path if base_path && base_path.size > 0 && root_base_path != false
|
210
|
+
api_description[:models] = parse_entity_models(models) unless models.empty?
|
211
|
+
api_description[:authorizations] = authorizations if authorizations
|
212
|
+
|
213
|
+
api_description
|
214
|
+
end
|
215
|
+
end
|
216
|
+
|
217
|
+
helpers do
|
218
|
+
|
219
|
+
def as_markdown(description)
|
220
|
+
description && @@markdown ? Kramdown::Document.new(strip_heredoc(description), input: 'GFM', enable_coderay: false).to_html : description
|
221
|
+
end
|
222
|
+
|
223
|
+
def parse_params(params, path, method)
|
224
|
+
params ||= []
|
225
|
+
params.map do |param, value|
|
226
|
+
value[:type] = 'File' if value.is_a?(Hash) && value[:type] == 'Rack::Multipart::UploadedFile'
|
227
|
+
items = {}
|
228
|
+
|
229
|
+
raw_data_type = value.is_a?(Hash) ? (value[:type] || 'string').to_s : 'string'
|
230
|
+
data_type = case raw_data_type
|
231
|
+
when 'Boolean', 'Date', 'Integer', 'String'
|
232
|
+
raw_data_type.downcase
|
233
|
+
when 'BigDecimal'
|
234
|
+
'long'
|
235
|
+
when 'DateTime'
|
236
|
+
'dateTime'
|
237
|
+
when 'Numeric'
|
238
|
+
'double'
|
239
|
+
else
|
240
|
+
parse_entity_name(raw_data_type)
|
241
|
+
end
|
242
|
+
description = value.is_a?(Hash) ? value[:desc] || value[:description] : ''
|
243
|
+
required = value.is_a?(Hash) ? !!value[:required] : false
|
244
|
+
default_value = value.is_a?(Hash) ? value[:default] : nil
|
245
|
+
is_array = value.is_a?(Hash) ? (value[:is_array] || false) : false
|
246
|
+
enum_values = value.is_a?(Hash) ? value[:values] : nil
|
247
|
+
enum_values = enum_values.call if enum_values && enum_values.is_a?(Proc)
|
248
|
+
|
249
|
+
if value.is_a?(Hash) && value.key?(:param_type)
|
250
|
+
param_type = value[:param_type]
|
251
|
+
if is_array
|
252
|
+
items = { '$ref' => data_type }
|
253
|
+
data_type = 'array'
|
254
|
+
end
|
255
|
+
else
|
256
|
+
param_type = case
|
257
|
+
when path.include?(":#{param}")
|
258
|
+
'path'
|
259
|
+
when %w(POST PUT PATCH).include?(method)
|
260
|
+
if is_primitive?(data_type)
|
261
|
+
'form'
|
262
|
+
else
|
263
|
+
'body'
|
264
|
+
end
|
265
|
+
else
|
266
|
+
'query'
|
267
|
+
end
|
268
|
+
end
|
269
|
+
name = (value.is_a?(Hash) && value[:full_name]) || param
|
270
|
+
|
271
|
+
parsed_params = {
|
272
|
+
paramType: param_type,
|
273
|
+
name: name,
|
274
|
+
description: as_markdown(description),
|
275
|
+
type: data_type,
|
276
|
+
required: required,
|
277
|
+
allowMultiple: is_array
|
278
|
+
}
|
279
|
+
parsed_params.merge!(format: 'int32') if data_type == 'integer'
|
280
|
+
parsed_params.merge!(format: 'int64') if data_type == 'long'
|
281
|
+
parsed_params.merge!(items: items) if items.present?
|
282
|
+
parsed_params.merge!(defaultValue: default_value) if default_value
|
283
|
+
parsed_params.merge!(enum: enum_values) if enum_values
|
284
|
+
parsed_params
|
285
|
+
end
|
286
|
+
end
|
287
|
+
|
288
|
+
def content_types_for(target_class)
|
289
|
+
content_types = (target_class.settings[:content_types] || {}).values
|
290
|
+
|
291
|
+
if content_types.empty?
|
292
|
+
formats = [target_class.settings[:format], target_class.settings[:default_format]].compact.uniq
|
293
|
+
formats = Grape::Formatter::Base.formatters({}).keys if formats.empty?
|
294
|
+
content_types = Grape::ContentTypes::CONTENT_TYPES.select { |content_type, _mime_type| formats.include? content_type }.values
|
295
|
+
end
|
296
|
+
|
297
|
+
content_types.uniq
|
298
|
+
end
|
299
|
+
|
300
|
+
def parse_info(info)
|
301
|
+
{
|
302
|
+
contact: info[:contact],
|
303
|
+
description: as_markdown(info[:description]),
|
304
|
+
license: info[:license],
|
305
|
+
licenseUrl: info[:license_url],
|
306
|
+
termsOfServiceUrl: info[:terms_of_service_url],
|
307
|
+
title: info[:title]
|
308
|
+
}.delete_if { |_, value| value.blank? }
|
309
|
+
end
|
310
|
+
|
311
|
+
def parse_header_params(params)
|
312
|
+
params ||= []
|
313
|
+
|
314
|
+
params.map do |param, value|
|
315
|
+
data_type = 'String'
|
316
|
+
description = value.is_a?(Hash) ? value[:description] : ''
|
317
|
+
required = value.is_a?(Hash) ? !!value[:required] : false
|
318
|
+
default_value = value.is_a?(Hash) ? value[:default] : nil
|
319
|
+
param_type = 'header'
|
320
|
+
|
321
|
+
parsed_params = {
|
322
|
+
paramType: param_type,
|
323
|
+
name: param,
|
324
|
+
description: as_markdown(description),
|
325
|
+
type: data_type,
|
326
|
+
required: required
|
327
|
+
}
|
328
|
+
|
329
|
+
parsed_params.merge!(defaultValue: default_value) if default_value
|
330
|
+
|
331
|
+
parsed_params
|
332
|
+
end
|
333
|
+
end
|
334
|
+
|
335
|
+
def parse_path(path, version)
|
336
|
+
# adapt format to swagger format
|
337
|
+
parsed_path = path.gsub('(.:format)', @@hide_format ? '' : '.{format}')
|
338
|
+
# This is attempting to emulate the behavior of
|
339
|
+
# Rack::Mount::Strexp. We cannot use Strexp directly because
|
340
|
+
# all it does is generate regular expressions for parsing URLs.
|
341
|
+
# TODO: Implement a Racc tokenizer to properly generate the
|
342
|
+
# parsed path.
|
343
|
+
parsed_path = parsed_path.gsub(/:([a-zA-Z_]\w*)/, '{\1}')
|
344
|
+
# add the version
|
345
|
+
version ? parsed_path.gsub('{version}', version) : parsed_path
|
346
|
+
end
|
347
|
+
|
348
|
+
def parse_entity_name(model)
|
349
|
+
if model.respond_to?(:entity_name)
|
350
|
+
model.entity_name
|
351
|
+
else
|
352
|
+
name = model.to_s
|
353
|
+
entity_parts = name.split('::')
|
354
|
+
entity_parts.reject! { |p| p == 'Entity' || p == 'Entities' }
|
355
|
+
entity_parts.join('::')
|
356
|
+
end
|
357
|
+
end
|
358
|
+
|
359
|
+
def parse_entity_models(models)
|
360
|
+
result = {}
|
361
|
+
models.each do |model|
|
362
|
+
name = parse_entity_name(model)
|
363
|
+
properties = {}
|
364
|
+
required = []
|
365
|
+
|
366
|
+
model.documentation.each do |property_name, property_info|
|
367
|
+
p = property_info.dup
|
368
|
+
|
369
|
+
required << property_name.to_s if p.delete(:required)
|
370
|
+
|
371
|
+
if p.delete(:is_array)
|
372
|
+
p[:items] = generate_typeref(p[:type])
|
373
|
+
p[:type] = 'array'
|
374
|
+
else
|
375
|
+
p.merge! generate_typeref(p.delete(:type))
|
376
|
+
end
|
377
|
+
|
378
|
+
# rename Grape Entity's "desc" to "description"
|
379
|
+
property_description = p.delete(:desc)
|
380
|
+
p[:description] = property_description if property_description
|
381
|
+
|
382
|
+
# rename Grape's 'values' to 'enum'
|
383
|
+
select_values = p.delete(:values)
|
384
|
+
if select_values
|
385
|
+
select_values = select_values.call if select_values.is_a?(Proc)
|
386
|
+
p[:enum] = select_values
|
387
|
+
end
|
388
|
+
|
389
|
+
properties[property_name] = p
|
390
|
+
|
391
|
+
end
|
392
|
+
|
393
|
+
result[name] = {
|
394
|
+
id: model.instance_variable_get(:@root) || name,
|
395
|
+
properties: properties
|
396
|
+
}
|
397
|
+
result[name].merge!(required: required) unless required.empty?
|
398
|
+
end
|
399
|
+
|
400
|
+
result
|
401
|
+
end
|
402
|
+
|
403
|
+
def models_with_included_presenters(models)
|
404
|
+
all_models = models
|
405
|
+
|
406
|
+
models.each do |model|
|
407
|
+
# get model references from exposures with a documentation
|
408
|
+
additional_models = model.exposures.map do |_, config|
|
409
|
+
config[:using] if config.key?(:documentation)
|
410
|
+
end.compact
|
411
|
+
|
412
|
+
all_models += additional_models
|
413
|
+
end
|
414
|
+
|
415
|
+
all_models
|
416
|
+
end
|
417
|
+
|
418
|
+
def is_primitive?(type)
|
419
|
+
%w(integer long float double string byte boolean date dateTime).include? type
|
420
|
+
end
|
421
|
+
|
422
|
+
def generate_typeref(type)
|
423
|
+
if is_primitive? type
|
424
|
+
{ 'type' => type }
|
425
|
+
else
|
426
|
+
{ '$ref' => type }
|
427
|
+
end
|
428
|
+
end
|
429
|
+
|
430
|
+
def parse_http_codes(codes, models)
|
431
|
+
codes ||= {}
|
432
|
+
codes.map do |k, v, m|
|
433
|
+
models << m if m
|
434
|
+
http_code_hash = {
|
435
|
+
code: k,
|
436
|
+
message: v
|
437
|
+
}
|
438
|
+
http_code_hash[:responseModel] = parse_entity_name(m) if m
|
439
|
+
http_code_hash
|
440
|
+
end
|
441
|
+
end
|
442
|
+
|
443
|
+
def try(*args, &block)
|
444
|
+
if args.empty? && block_given?
|
445
|
+
yield self
|
446
|
+
elsif respond_to?(args.first)
|
447
|
+
public_send(*args, &block)
|
448
|
+
end
|
449
|
+
end
|
450
|
+
|
451
|
+
def strip_heredoc(string)
|
452
|
+
indent = string.scan(/^[ \t]*(?=\S)/).min.try(:size) || 0
|
453
|
+
string.gsub(/^[ \t]{#{indent}}/, '')
|
454
|
+
end
|
455
|
+
|
456
|
+
def parse_base_path(base_path, request)
|
457
|
+
if base_path.is_a?(Proc)
|
458
|
+
base_path.call(request)
|
459
|
+
elsif base_path.is_a?(String)
|
460
|
+
URI(base_path).relative? ? URI.join(request.base_url, base_path).to_s : base_path
|
461
|
+
else
|
462
|
+
request.base_url
|
463
|
+
end
|
464
|
+
end
|
465
|
+
end
|
466
|
+
end
|
467
|
+
end
|
468
|
+
end
|
469
|
+
end
|
470
|
+
end
|