grape_ape_rails 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|