grape-oas 1.0.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/CHANGELOG.md +82 -0
- data/CONTRIBUTING.md +87 -0
- data/LICENSE.txt +21 -0
- data/README.md +184 -0
- data/RELEASING.md +109 -0
- data/grape-oas.gemspec +27 -0
- data/lib/grape-oas.rb +3 -0
- data/lib/grape_oas/api_model/api.rb +42 -0
- data/lib/grape_oas/api_model/media_type.rb +22 -0
- data/lib/grape_oas/api_model/node.rb +57 -0
- data/lib/grape_oas/api_model/operation.rb +55 -0
- data/lib/grape_oas/api_model/parameter.rb +24 -0
- data/lib/grape_oas/api_model/path.rb +29 -0
- data/lib/grape_oas/api_model/request_body.rb +27 -0
- data/lib/grape_oas/api_model/response.rb +28 -0
- data/lib/grape_oas/api_model/schema.rb +60 -0
- data/lib/grape_oas/api_model_builder.rb +63 -0
- data/lib/grape_oas/api_model_builders/concerns/content_type_resolver.rb +93 -0
- data/lib/grape_oas/api_model_builders/concerns/oas_utilities.rb +75 -0
- data/lib/grape_oas/api_model_builders/concerns/type_resolver.rb +142 -0
- data/lib/grape_oas/api_model_builders/operation.rb +168 -0
- data/lib/grape_oas/api_model_builders/path.rb +122 -0
- data/lib/grape_oas/api_model_builders/request.rb +304 -0
- data/lib/grape_oas/api_model_builders/request_params.rb +128 -0
- data/lib/grape_oas/api_model_builders/request_params_support/nested_params_builder.rb +155 -0
- data/lib/grape_oas/api_model_builders/request_params_support/param_location_resolver.rb +64 -0
- data/lib/grape_oas/api_model_builders/request_params_support/param_schema_builder.rb +163 -0
- data/lib/grape_oas/api_model_builders/request_params_support/schema_enhancer.rb +111 -0
- data/lib/grape_oas/api_model_builders/response.rb +241 -0
- data/lib/grape_oas/api_model_builders/response_parsers/base.rb +56 -0
- data/lib/grape_oas/api_model_builders/response_parsers/default_response_parser.rb +31 -0
- data/lib/grape_oas/api_model_builders/response_parsers/documentation_responses_parser.rb +35 -0
- data/lib/grape_oas/api_model_builders/response_parsers/http_codes_parser.rb +85 -0
- data/lib/grape_oas/constants.rb +81 -0
- data/lib/grape_oas/documentation_extension.rb +124 -0
- data/lib/grape_oas/exporter/base/operation.rb +88 -0
- data/lib/grape_oas/exporter/base/paths.rb +53 -0
- data/lib/grape_oas/exporter/concerns/schema_indexer.rb +93 -0
- data/lib/grape_oas/exporter/concerns/tag_builder.rb +55 -0
- data/lib/grape_oas/exporter/oas2/operation.rb +31 -0
- data/lib/grape_oas/exporter/oas2/parameter.rb +116 -0
- data/lib/grape_oas/exporter/oas2/paths.rb +19 -0
- data/lib/grape_oas/exporter/oas2/response.rb +74 -0
- data/lib/grape_oas/exporter/oas2/schema.rb +125 -0
- data/lib/grape_oas/exporter/oas2_schema.rb +133 -0
- data/lib/grape_oas/exporter/oas3/operation.rb +24 -0
- data/lib/grape_oas/exporter/oas3/parameter.rb +27 -0
- data/lib/grape_oas/exporter/oas3/paths.rb +21 -0
- data/lib/grape_oas/exporter/oas3/request_body.rb +54 -0
- data/lib/grape_oas/exporter/oas3/response.rb +85 -0
- data/lib/grape_oas/exporter/oas3/schema.rb +249 -0
- data/lib/grape_oas/exporter/oas30_schema.rb +13 -0
- data/lib/grape_oas/exporter/oas31/schema.rb +42 -0
- data/lib/grape_oas/exporter/oas31_schema.rb +34 -0
- data/lib/grape_oas/exporter/oas3_schema.rb +130 -0
- data/lib/grape_oas/exporter/registry.rb +82 -0
- data/lib/grape_oas/exporter.rb +16 -0
- data/lib/grape_oas/introspectors/base.rb +44 -0
- data/lib/grape_oas/introspectors/dry_introspector.rb +131 -0
- data/lib/grape_oas/introspectors/dry_introspector_support/argument_extractor.rb +51 -0
- data/lib/grape_oas/introspectors/dry_introspector_support/ast_walker.rb +125 -0
- data/lib/grape_oas/introspectors/dry_introspector_support/constraint_applier.rb +136 -0
- data/lib/grape_oas/introspectors/dry_introspector_support/constraint_extractor.rb +85 -0
- data/lib/grape_oas/introspectors/dry_introspector_support/constraint_merger.rb +47 -0
- data/lib/grape_oas/introspectors/dry_introspector_support/contract_resolver.rb +60 -0
- data/lib/grape_oas/introspectors/dry_introspector_support/inheritance_handler.rb +87 -0
- data/lib/grape_oas/introspectors/dry_introspector_support/predicate_handler.rb +131 -0
- data/lib/grape_oas/introspectors/dry_introspector_support/type_schema_builder.rb +143 -0
- data/lib/grape_oas/introspectors/dry_introspector_support/type_unwrapper.rb +143 -0
- data/lib/grape_oas/introspectors/entity_introspector.rb +165 -0
- data/lib/grape_oas/introspectors/entity_introspector_support/cycle_tracker.rb +42 -0
- data/lib/grape_oas/introspectors/entity_introspector_support/discriminator_handler.rb +83 -0
- data/lib/grape_oas/introspectors/entity_introspector_support/exposure_processor.rb +261 -0
- data/lib/grape_oas/introspectors/entity_introspector_support/inheritance_builder.rb +112 -0
- data/lib/grape_oas/introspectors/entity_introspector_support/property_extractor.rb +53 -0
- data/lib/grape_oas/introspectors/registry.rb +136 -0
- data/lib/grape_oas/rake/oas_tasks.rb +127 -0
- data/lib/grape_oas/version.rb +5 -0
- data/lib/grape_oas.rb +145 -0
- metadata +152 -0
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module GrapeOAS
|
|
4
|
+
module ApiModel
|
|
5
|
+
# Represents an API operation (endpoint action) in the DTO model for OpenAPI v2/v3.
|
|
6
|
+
# Encapsulates HTTP method, parameters, request body, responses, tags, and security.
|
|
7
|
+
# Used as the core unit for both OpenAPIv2 and OpenAPIv3 operation objects.
|
|
8
|
+
#
|
|
9
|
+
# @see https://swagger.io/specification/
|
|
10
|
+
# @see GrapeOAS::ApiModel::Path
|
|
11
|
+
class Operation < Node
|
|
12
|
+
attr_accessor :http_method, :operation_id, :summary, :description,
|
|
13
|
+
:deprecated, :parameters, :request_body,
|
|
14
|
+
:responses, :tag_names, :security, :extensions,
|
|
15
|
+
:consumes, :produces, :suppress_default_error_response
|
|
16
|
+
|
|
17
|
+
def initialize(http_method:, operation_id: nil, summary: nil, description: nil,
|
|
18
|
+
deprecated: false, parameters: [], request_body: nil,
|
|
19
|
+
responses: [], tag_names: [], security: [], extensions: nil,
|
|
20
|
+
consumes: [], produces: [])
|
|
21
|
+
super()
|
|
22
|
+
@http_method = http_method.to_s.downcase
|
|
23
|
+
@operation_id = operation_id
|
|
24
|
+
@summary = summary
|
|
25
|
+
@description = description
|
|
26
|
+
@deprecated = deprecated
|
|
27
|
+
@parameters = Array(parameters)
|
|
28
|
+
@request_body = request_body
|
|
29
|
+
@responses = Array(responses)
|
|
30
|
+
@tag_names = Array(tag_names)
|
|
31
|
+
@security = Array(security)
|
|
32
|
+
@extensions = extensions
|
|
33
|
+
@consumes = Array(consumes)
|
|
34
|
+
@produces = Array(produces)
|
|
35
|
+
@suppress_default_error_response = false
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def add_parameter(parameter)
|
|
39
|
+
@parameters << parameter
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def add_parameters(*parameters)
|
|
43
|
+
@parameters.concat(parameters)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def add_response(response)
|
|
47
|
+
@responses << response
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def response(code)
|
|
51
|
+
@responses.find { |r| r.http_status == code.to_s }
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module GrapeOAS
|
|
4
|
+
module ApiModel
|
|
5
|
+
# Represents an operation parameter in the DTO model for OpenAPI v2/v3.
|
|
6
|
+
# Used for query, path, header, and cookie parameters in both OpenAPI versions.
|
|
7
|
+
#
|
|
8
|
+
# @see https://swagger.io/specification/
|
|
9
|
+
# @see GrapeOAS::ApiModel::Operation
|
|
10
|
+
class Parameter < Node
|
|
11
|
+
attr_accessor :location, :name, :required, :description, :schema, :collection_format
|
|
12
|
+
|
|
13
|
+
def initialize(location:, name:, schema:, required: false, description: nil, collection_format: nil)
|
|
14
|
+
super()
|
|
15
|
+
@location = location.to_s
|
|
16
|
+
@name = name
|
|
17
|
+
@required = required
|
|
18
|
+
@schema = schema
|
|
19
|
+
@description = description
|
|
20
|
+
@collection_format = collection_format
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module GrapeOAS
|
|
4
|
+
module ApiModel
|
|
5
|
+
# Represents an API path (endpoint) in the DTO model for OpenAPI v2/v3.
|
|
6
|
+
# Contains a list of operations (HTTP methods) for the path.
|
|
7
|
+
# Used to build the 'paths' object in both OpenAPIv2 and OpenAPIv3 documents.
|
|
8
|
+
#
|
|
9
|
+
# @see https://swagger.io/specification/
|
|
10
|
+
# @see GrapeOAS::ApiModel::Api
|
|
11
|
+
class Path < Node
|
|
12
|
+
attr_accessor :template, :operations
|
|
13
|
+
|
|
14
|
+
def initialize(template:)
|
|
15
|
+
super()
|
|
16
|
+
@template = template
|
|
17
|
+
@operations = []
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def add_operation(operation)
|
|
21
|
+
@operations << operation
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def [](method_sym)
|
|
25
|
+
@operations.find { |op| op.http_method.to_sym == method_sym.to_sym }
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module GrapeOAS
|
|
4
|
+
module ApiModel
|
|
5
|
+
# Represents a request body in the DTO model for OpenAPI v2/v3.
|
|
6
|
+
# Used to describe the payload of HTTP requests, including content type and schema.
|
|
7
|
+
#
|
|
8
|
+
# @see https://swagger.io/specification/
|
|
9
|
+
# @see GrapeOAS::ApiModel::Operation
|
|
10
|
+
class RequestBody < Node
|
|
11
|
+
attr_accessor :description, :required, :media_types, :extensions, :body_name
|
|
12
|
+
|
|
13
|
+
def initialize(description: nil, required: false, media_types: [], extensions: nil, body_name: nil)
|
|
14
|
+
super()
|
|
15
|
+
@description = description
|
|
16
|
+
@required = required
|
|
17
|
+
@media_types = Array(media_types)
|
|
18
|
+
@extensions = extensions
|
|
19
|
+
@body_name = body_name
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def add_media_type(media_type)
|
|
23
|
+
@media_types << media_type
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module GrapeOAS
|
|
4
|
+
module ApiModel
|
|
5
|
+
# Represents an HTTP response in the DTO model for OpenAPI v2/v3.
|
|
6
|
+
# Used to describe possible responses for an operation, including status, content, and headers.
|
|
7
|
+
#
|
|
8
|
+
# @see https://swagger.io/specification/
|
|
9
|
+
# @see GrapeOAS::ApiModel::Operation
|
|
10
|
+
class Response < Node
|
|
11
|
+
attr_accessor :http_status, :description, :media_types, :headers, :extensions, :examples
|
|
12
|
+
|
|
13
|
+
def initialize(http_status:, description:, media_types: [], headers: [], extensions: nil, examples: nil)
|
|
14
|
+
super()
|
|
15
|
+
@http_status = http_status.to_s
|
|
16
|
+
@description = description
|
|
17
|
+
@media_types = Array(media_types)
|
|
18
|
+
@headers = Array(headers)
|
|
19
|
+
@extensions = extensions
|
|
20
|
+
@examples = examples
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def add_media_type(media_type)
|
|
24
|
+
@media_types << media_type
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module GrapeOAS
|
|
4
|
+
module ApiModel
|
|
5
|
+
# Represents a schema object in the DTO model for OpenAPI v2/v3.
|
|
6
|
+
# Used to describe data types, properties, and structure for parameters, request bodies, and responses.
|
|
7
|
+
#
|
|
8
|
+
# @see https://swagger.io/specification/
|
|
9
|
+
# @see GrapeOAS::ApiModel::Parameter, GrapeOAS::ApiModel::RequestBody
|
|
10
|
+
class Schema < Node
|
|
11
|
+
VALID_ATTRIBUTES = %i[
|
|
12
|
+
canonical_name type format properties items description
|
|
13
|
+
required nullable enum additional_properties unevaluated_properties defs
|
|
14
|
+
examples extensions
|
|
15
|
+
min_length max_length pattern
|
|
16
|
+
minimum maximum exclusive_minimum exclusive_maximum
|
|
17
|
+
min_items max_items
|
|
18
|
+
discriminator all_of one_of any_of
|
|
19
|
+
].freeze
|
|
20
|
+
|
|
21
|
+
attr_accessor(*VALID_ATTRIBUTES)
|
|
22
|
+
|
|
23
|
+
def initialize(**attrs)
|
|
24
|
+
super()
|
|
25
|
+
|
|
26
|
+
@properties = {}
|
|
27
|
+
@required = []
|
|
28
|
+
@nullable = false
|
|
29
|
+
@enum = nil
|
|
30
|
+
@additional_properties = nil
|
|
31
|
+
@unevaluated_properties = nil
|
|
32
|
+
@defs = {}
|
|
33
|
+
@discriminator = nil
|
|
34
|
+
@all_of = nil
|
|
35
|
+
@one_of = nil
|
|
36
|
+
@any_of = nil
|
|
37
|
+
|
|
38
|
+
attrs.each do |k, v|
|
|
39
|
+
unless VALID_ATTRIBUTES.include?(k)
|
|
40
|
+
raise ArgumentError, "Unknown Schema attribute: #{k}. Valid attributes: #{VALID_ATTRIBUTES.join(", ")}"
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
public_send("#{k}=", v)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def empty?
|
|
48
|
+
return false if @all_of&.any? || @one_of&.any? || @any_of&.any?
|
|
49
|
+
|
|
50
|
+
@properties.nil? || @properties.empty?
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def add_property(name, schema, required: false)
|
|
54
|
+
@properties[name.to_s] = schema
|
|
55
|
+
@required << name.to_s if required
|
|
56
|
+
schema
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module GrapeOAS
|
|
4
|
+
class ApiModelBuilder
|
|
5
|
+
attr_reader :api
|
|
6
|
+
|
|
7
|
+
def initialize(options = {})
|
|
8
|
+
@api = GrapeOAS::ApiModel::API.new(
|
|
9
|
+
title: options.dig(:info, :title) || options[:title] || "Grape API",
|
|
10
|
+
version: options.dig(:info, :version) || options[:version] || "1",
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
@api.host = options[:host]
|
|
14
|
+
@api.base_path = normalize_base_path(options[:base_path])
|
|
15
|
+
@api.schemes = Array(options[:schemes]).compact
|
|
16
|
+
@api.security_definitions = options[:security_definitions] || {}
|
|
17
|
+
@api.security = options[:security] || []
|
|
18
|
+
@api.tag_defs.merge(Array(options[:tags])) if options[:tags]
|
|
19
|
+
@api.servers = build_servers(options)
|
|
20
|
+
@api.registered_schemas = build_registered_schemas(options[:models])
|
|
21
|
+
@api.suppress_default_error_response = options[:suppress_default_error_response] || false
|
|
22
|
+
|
|
23
|
+
@namespace_filter = options[:namespace]
|
|
24
|
+
@apis = []
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def add_app(app)
|
|
28
|
+
GrapeOAS::ApiModelBuilders::Path
|
|
29
|
+
.new(api: @api, routes: app.routes, app: app, namespace_filter: @namespace_filter)
|
|
30
|
+
.build
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def normalize_base_path(path)
|
|
36
|
+
return nil unless path
|
|
37
|
+
|
|
38
|
+
path.start_with?("/") ? path : "/#{path}"
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def build_servers(options)
|
|
42
|
+
return options[:servers] if options[:servers]
|
|
43
|
+
return [] unless options[:host]
|
|
44
|
+
|
|
45
|
+
scheme = Array(options[:schemes]).compact.first || "https"
|
|
46
|
+
url = "#{scheme}://#{options[:host]}#{normalize_base_path(options[:base_path])}"
|
|
47
|
+
[{ "url" => url }]
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Build schemas from pre-registered models (entities/contracts)
|
|
51
|
+
# This allows adding models to definitions even if not referenced by endpoints
|
|
52
|
+
def build_registered_schemas(models)
|
|
53
|
+
return [] unless models
|
|
54
|
+
|
|
55
|
+
Array(models).map do |model|
|
|
56
|
+
model = model.constantize if model.is_a?(String)
|
|
57
|
+
GrapeOAS.introspectors.build_schema(model, stack: [], registry: {})
|
|
58
|
+
rescue StandardError
|
|
59
|
+
nil
|
|
60
|
+
end.compact
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module GrapeOAS
|
|
4
|
+
module ApiModelBuilders
|
|
5
|
+
module Concerns
|
|
6
|
+
# Shared module for resolving content types from routes, apps, and APIs.
|
|
7
|
+
# Used by both Operation and Response builders to avoid code duplication.
|
|
8
|
+
module ContentTypeResolver
|
|
9
|
+
private
|
|
10
|
+
|
|
11
|
+
def resolve_content_types
|
|
12
|
+
default_format = route_default_format_from_route || default_format_from_app_or_api
|
|
13
|
+
content_types = route_content_types_from_route
|
|
14
|
+
content_types ||= content_types_from_app_or_api(default_format)
|
|
15
|
+
|
|
16
|
+
mimes = []
|
|
17
|
+
if content_types.is_a?(Hash)
|
|
18
|
+
selected = content_types.select { |k, _| k.to_s.start_with?(default_format.to_s) } if default_format
|
|
19
|
+
selected = content_types if selected.nil? || selected.empty?
|
|
20
|
+
mimes = selected.values
|
|
21
|
+
elsif content_types.respond_to?(:to_a) && !content_types.is_a?(String)
|
|
22
|
+
mimes = content_types.to_a
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
mimes << mime_for_format(default_format) if mimes.empty? && default_format
|
|
26
|
+
|
|
27
|
+
mimes = mimes.map { |m| normalize_mime(m) }.compact
|
|
28
|
+
mimes.empty? ? [Constants::MimeTypes::JSON] : mimes.uniq
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def mime_for_format(format)
|
|
32
|
+
return if format.nil?
|
|
33
|
+
return format if format.to_s.include?("/")
|
|
34
|
+
|
|
35
|
+
return unless defined?(Grape::ContentTypes::CONTENT_TYPES)
|
|
36
|
+
|
|
37
|
+
Grape::ContentTypes::CONTENT_TYPES[format.to_sym]
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def normalize_mime(mime_or_format)
|
|
41
|
+
return nil if mime_or_format.nil?
|
|
42
|
+
return mime_or_format if mime_or_format.to_s.include?("/")
|
|
43
|
+
|
|
44
|
+
mime_for_format(mime_or_format)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def route_content_types_from_route
|
|
48
|
+
return route.settings[:content_types] || route.settings[:content_type] if route.respond_to?(:settings)
|
|
49
|
+
|
|
50
|
+
route.options[:content_types] || route.options[:content_type]
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def route_default_format_from_route
|
|
54
|
+
return route.settings[:default_format] if route.respond_to?(:settings)
|
|
55
|
+
|
|
56
|
+
route.options[:format]
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def default_format_from_app_or_api
|
|
60
|
+
return api.default_format if api.respond_to?(:default_format)
|
|
61
|
+
return app.default_format if app_responds_to?(:default_format)
|
|
62
|
+
|
|
63
|
+
api.settings[:default_format] if api.respond_to?(:settings) && api.settings[:default_format]
|
|
64
|
+
rescue NoMethodError
|
|
65
|
+
nil
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def content_types_from_app_or_api(default_format)
|
|
69
|
+
source = if api.respond_to?(:content_types)
|
|
70
|
+
api.content_types
|
|
71
|
+
elsif app_responds_to?(:content_types)
|
|
72
|
+
app.content_types
|
|
73
|
+
elsif api.respond_to?(:settings)
|
|
74
|
+
api.settings[:content_types]
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
return nil unless source.is_a?(Hash)
|
|
78
|
+
|
|
79
|
+
return source unless default_format
|
|
80
|
+
|
|
81
|
+
filtered = source.select { |k, _| k.to_s.start_with?(default_format.to_s) }
|
|
82
|
+
filtered.empty? ? source : filtered
|
|
83
|
+
rescue NoMethodError
|
|
84
|
+
nil
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def app_responds_to?(method)
|
|
88
|
+
!app.nil? && app.respond_to?(method)
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module GrapeOAS
|
|
4
|
+
module ApiModelBuilders
|
|
5
|
+
module Concerns
|
|
6
|
+
# Shared utility methods for OpenAPI schema building.
|
|
7
|
+
module OasUtilities
|
|
8
|
+
# Regex pattern for valid Ruby constant names (used for entity resolution)
|
|
9
|
+
VALID_CONSTANT_PATTERN = /\A[A-Z][A-Za-z0-9_]*(::[A-Z][A-Za-z0-9_]*)*\z/
|
|
10
|
+
|
|
11
|
+
# Extracts OpenAPI extension fields (x-* prefixed keys) from a hash.
|
|
12
|
+
#
|
|
13
|
+
# @param hash [Hash] the source hash
|
|
14
|
+
# @return [Hash, nil] hash of extension fields, or nil if empty
|
|
15
|
+
def self.extract_extensions(hash)
|
|
16
|
+
return nil unless hash.is_a?(Hash)
|
|
17
|
+
|
|
18
|
+
ext = hash.select { |k, _| k.to_s.start_with?("x-") }
|
|
19
|
+
ext.empty? ? nil : ext
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Instance method version for including in classes
|
|
23
|
+
def extract_extensions(hash)
|
|
24
|
+
OasUtilities.extract_extensions(hash)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Converts a CamelCase string to snake_case.
|
|
28
|
+
#
|
|
29
|
+
# @param str [String] the string to convert
|
|
30
|
+
# @return [String] the underscored string
|
|
31
|
+
def self.underscore(str)
|
|
32
|
+
str.gsub("::", "/")
|
|
33
|
+
.gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
|
|
34
|
+
.gsub(/([a-z\d])([A-Z])/, '\1_\2')
|
|
35
|
+
.tr("-", "_")
|
|
36
|
+
.downcase
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Instance method version
|
|
40
|
+
def underscore(str)
|
|
41
|
+
OasUtilities.underscore(str)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Simple pluralization (basic English rules).
|
|
45
|
+
#
|
|
46
|
+
# @param key [String] the string to pluralize
|
|
47
|
+
# @return [String] the pluralized string
|
|
48
|
+
def self.pluralize(key)
|
|
49
|
+
return "#{key}es" if key.end_with?("s", "x", "z", "ch", "sh")
|
|
50
|
+
return "#{key[0..-2]}ies" if key.end_with?("y") && !%w[a e i o u].include?(key[-2])
|
|
51
|
+
|
|
52
|
+
"#{key}s"
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Instance method version
|
|
56
|
+
def pluralize(key)
|
|
57
|
+
OasUtilities.pluralize(key)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Checks if a string matches the valid Ruby constant pattern.
|
|
61
|
+
#
|
|
62
|
+
# @param str [String] the string to check
|
|
63
|
+
# @return [Boolean] true if valid constant name
|
|
64
|
+
def self.valid_constant_name?(str)
|
|
65
|
+
str.is_a?(String) && str.match?(VALID_CONSTANT_PATTERN)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Instance method version
|
|
69
|
+
def valid_constant_name?(str)
|
|
70
|
+
OasUtilities.valid_constant_name?(str)
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "bigdecimal"
|
|
4
|
+
|
|
5
|
+
module GrapeOAS
|
|
6
|
+
module ApiModelBuilders
|
|
7
|
+
module Concerns
|
|
8
|
+
# Centralizes Ruby type to OpenAPI schema type resolution.
|
|
9
|
+
# Used by request builders and introspectors to avoid duplicated type switching logic.
|
|
10
|
+
module TypeResolver
|
|
11
|
+
# Regex to match Grape's typed array notation like "[String]", "[Integer]"
|
|
12
|
+
TYPED_ARRAY_PATTERN = /^\[(\w+)\]$/
|
|
13
|
+
|
|
14
|
+
# Regex to match Grape's multi-type notation like "[String, Integer]", "[String, Float]"
|
|
15
|
+
MULTI_TYPE_PATTERN = /^\[(\w+(?:::\w+)*(?:,\s*\w+(?:::\w+)*)+)\]$/
|
|
16
|
+
|
|
17
|
+
# Resolves a Ruby class or type name to its OpenAPI schema type string.
|
|
18
|
+
# Handles both Ruby classes (Integer, Float) and string type names ("integer", "float").
|
|
19
|
+
# Also handles Grape's "[Type]" notation for typed arrays.
|
|
20
|
+
# Falls back to "string" for unknown types.
|
|
21
|
+
#
|
|
22
|
+
# @param type [Class, String, Symbol, nil] The type to resolve
|
|
23
|
+
# @return [String] The OpenAPI schema type
|
|
24
|
+
def resolve_schema_type(type)
|
|
25
|
+
return Constants::SchemaTypes::STRING if type.nil?
|
|
26
|
+
|
|
27
|
+
# Handle Ruby classes directly
|
|
28
|
+
if type.is_a?(Class)
|
|
29
|
+
# Check static mapping first
|
|
30
|
+
return Constants::RUBY_TYPE_MAPPING[type] if Constants::RUBY_TYPE_MAPPING.key?(type)
|
|
31
|
+
|
|
32
|
+
# Handle Grape::API::Boolean dynamically (may not be loaded at constant definition time)
|
|
33
|
+
return Constants::SchemaTypes::BOOLEAN if grape_boolean_type?(type)
|
|
34
|
+
|
|
35
|
+
return Constants::SchemaTypes::STRING
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
type_str = type.to_s
|
|
39
|
+
|
|
40
|
+
# Handle Grape's typed array notation like "[String]"
|
|
41
|
+
return Constants::SchemaTypes::ARRAY if type_str.match?(TYPED_ARRAY_PATTERN)
|
|
42
|
+
|
|
43
|
+
# Handle string/symbol type names
|
|
44
|
+
Constants::PRIMITIVE_TYPE_MAPPING.fetch(type_str.downcase, Constants::SchemaTypes::STRING)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Checks if type is Grape's Boolean class (handles dynamic loading)
|
|
48
|
+
def grape_boolean_type?(type)
|
|
49
|
+
return false unless defined?(Grape::API::Boolean)
|
|
50
|
+
|
|
51
|
+
type == Grape::API::Boolean || type.to_s == "Grape::API::Boolean"
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Extracts the member type from Grape's "[Type]" notation.
|
|
55
|
+
# Returns nil if not a typed array.
|
|
56
|
+
#
|
|
57
|
+
# @param type [String] The type string to parse
|
|
58
|
+
# @return [String, nil] The inner type or nil
|
|
59
|
+
def extract_typed_array_member(type)
|
|
60
|
+
return nil unless type.is_a?(String)
|
|
61
|
+
|
|
62
|
+
match = type.match(TYPED_ARRAY_PATTERN)
|
|
63
|
+
match ? match[1] : nil
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Checks if type is a multi-type notation like "[String, Integer]"
|
|
67
|
+
#
|
|
68
|
+
# @param type [String] The type string to check
|
|
69
|
+
# @return [Boolean] true if multi-type notation
|
|
70
|
+
def multi_type?(type)
|
|
71
|
+
return false unless type.is_a?(String)
|
|
72
|
+
|
|
73
|
+
type.match?(MULTI_TYPE_PATTERN)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Extracts individual types from Grape's multi-type notation "[String, Integer]"
|
|
77
|
+
# Returns nil if not a multi-type notation.
|
|
78
|
+
#
|
|
79
|
+
# @param type [String] The type string to parse
|
|
80
|
+
# @return [Array<String>, nil] Array of type names or nil
|
|
81
|
+
def extract_multi_types(type)
|
|
82
|
+
return nil unless type.is_a?(String)
|
|
83
|
+
|
|
84
|
+
match = type.match(MULTI_TYPE_PATTERN)
|
|
85
|
+
return nil unless match
|
|
86
|
+
|
|
87
|
+
match[1].split(/,\s*/)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Builds a basic Schema object for the given Ruby primitive type.
|
|
91
|
+
# Handles special cases like Array and Hash.
|
|
92
|
+
# Note: Uses == instead of case/when because Ruby's === doesn't work for class equality
|
|
93
|
+
# (Array === Array returns false since Array is not an instance of Array)
|
|
94
|
+
#
|
|
95
|
+
# @param primitive [Class, nil] The Ruby primitive class
|
|
96
|
+
# @param member [Object, nil] For arrays, the member type
|
|
97
|
+
# @return [ApiModel::Schema] The schema object
|
|
98
|
+
def build_schema_for_primitive(primitive, member: nil)
|
|
99
|
+
if primitive == Array
|
|
100
|
+
items_schema = build_array_items_schema(member)
|
|
101
|
+
ApiModel::Schema.new(type: Constants::SchemaTypes::ARRAY, items: items_schema)
|
|
102
|
+
elsif primitive == Hash
|
|
103
|
+
ApiModel::Schema.new(type: Constants::SchemaTypes::OBJECT)
|
|
104
|
+
else
|
|
105
|
+
ApiModel::Schema.new(type: resolve_schema_type(primitive))
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Builds schema for array items, handling nested arrays recursively.
|
|
110
|
+
#
|
|
111
|
+
# @param member [Object, nil] The member type
|
|
112
|
+
# @return [ApiModel::Schema] The items schema
|
|
113
|
+
def build_array_items_schema(member)
|
|
114
|
+
return default_string_schema unless member
|
|
115
|
+
|
|
116
|
+
member_primitive, member_member = derive_primitive_and_member(member)
|
|
117
|
+
build_schema_for_primitive(member_primitive, member: member_member)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Derives primitive type and nested member from a type.
|
|
121
|
+
# For Dry::Types, extracts the primitive and member type.
|
|
122
|
+
# For plain Ruby classes, returns the class with nil member.
|
|
123
|
+
#
|
|
124
|
+
# @param type [Object] The type to analyze
|
|
125
|
+
# @return [Array<Class, Object>] [primitive, member] tuple
|
|
126
|
+
def derive_primitive_and_member(type)
|
|
127
|
+
return [type, nil] unless type.respond_to?(:primitive)
|
|
128
|
+
|
|
129
|
+
primitive = type.primitive
|
|
130
|
+
member = type.respond_to?(:member) ? type.member : nil
|
|
131
|
+
[primitive, member]
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
private
|
|
135
|
+
|
|
136
|
+
def default_string_schema
|
|
137
|
+
ApiModel::Schema.new(type: Constants::SchemaTypes::STRING)
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
end
|