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.
Files changed (81) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +82 -0
  3. data/CONTRIBUTING.md +87 -0
  4. data/LICENSE.txt +21 -0
  5. data/README.md +184 -0
  6. data/RELEASING.md +109 -0
  7. data/grape-oas.gemspec +27 -0
  8. data/lib/grape-oas.rb +3 -0
  9. data/lib/grape_oas/api_model/api.rb +42 -0
  10. data/lib/grape_oas/api_model/media_type.rb +22 -0
  11. data/lib/grape_oas/api_model/node.rb +57 -0
  12. data/lib/grape_oas/api_model/operation.rb +55 -0
  13. data/lib/grape_oas/api_model/parameter.rb +24 -0
  14. data/lib/grape_oas/api_model/path.rb +29 -0
  15. data/lib/grape_oas/api_model/request_body.rb +27 -0
  16. data/lib/grape_oas/api_model/response.rb +28 -0
  17. data/lib/grape_oas/api_model/schema.rb +60 -0
  18. data/lib/grape_oas/api_model_builder.rb +63 -0
  19. data/lib/grape_oas/api_model_builders/concerns/content_type_resolver.rb +93 -0
  20. data/lib/grape_oas/api_model_builders/concerns/oas_utilities.rb +75 -0
  21. data/lib/grape_oas/api_model_builders/concerns/type_resolver.rb +142 -0
  22. data/lib/grape_oas/api_model_builders/operation.rb +168 -0
  23. data/lib/grape_oas/api_model_builders/path.rb +122 -0
  24. data/lib/grape_oas/api_model_builders/request.rb +304 -0
  25. data/lib/grape_oas/api_model_builders/request_params.rb +128 -0
  26. data/lib/grape_oas/api_model_builders/request_params_support/nested_params_builder.rb +155 -0
  27. data/lib/grape_oas/api_model_builders/request_params_support/param_location_resolver.rb +64 -0
  28. data/lib/grape_oas/api_model_builders/request_params_support/param_schema_builder.rb +163 -0
  29. data/lib/grape_oas/api_model_builders/request_params_support/schema_enhancer.rb +111 -0
  30. data/lib/grape_oas/api_model_builders/response.rb +241 -0
  31. data/lib/grape_oas/api_model_builders/response_parsers/base.rb +56 -0
  32. data/lib/grape_oas/api_model_builders/response_parsers/default_response_parser.rb +31 -0
  33. data/lib/grape_oas/api_model_builders/response_parsers/documentation_responses_parser.rb +35 -0
  34. data/lib/grape_oas/api_model_builders/response_parsers/http_codes_parser.rb +85 -0
  35. data/lib/grape_oas/constants.rb +81 -0
  36. data/lib/grape_oas/documentation_extension.rb +124 -0
  37. data/lib/grape_oas/exporter/base/operation.rb +88 -0
  38. data/lib/grape_oas/exporter/base/paths.rb +53 -0
  39. data/lib/grape_oas/exporter/concerns/schema_indexer.rb +93 -0
  40. data/lib/grape_oas/exporter/concerns/tag_builder.rb +55 -0
  41. data/lib/grape_oas/exporter/oas2/operation.rb +31 -0
  42. data/lib/grape_oas/exporter/oas2/parameter.rb +116 -0
  43. data/lib/grape_oas/exporter/oas2/paths.rb +19 -0
  44. data/lib/grape_oas/exporter/oas2/response.rb +74 -0
  45. data/lib/grape_oas/exporter/oas2/schema.rb +125 -0
  46. data/lib/grape_oas/exporter/oas2_schema.rb +133 -0
  47. data/lib/grape_oas/exporter/oas3/operation.rb +24 -0
  48. data/lib/grape_oas/exporter/oas3/parameter.rb +27 -0
  49. data/lib/grape_oas/exporter/oas3/paths.rb +21 -0
  50. data/lib/grape_oas/exporter/oas3/request_body.rb +54 -0
  51. data/lib/grape_oas/exporter/oas3/response.rb +85 -0
  52. data/lib/grape_oas/exporter/oas3/schema.rb +249 -0
  53. data/lib/grape_oas/exporter/oas30_schema.rb +13 -0
  54. data/lib/grape_oas/exporter/oas31/schema.rb +42 -0
  55. data/lib/grape_oas/exporter/oas31_schema.rb +34 -0
  56. data/lib/grape_oas/exporter/oas3_schema.rb +130 -0
  57. data/lib/grape_oas/exporter/registry.rb +82 -0
  58. data/lib/grape_oas/exporter.rb +16 -0
  59. data/lib/grape_oas/introspectors/base.rb +44 -0
  60. data/lib/grape_oas/introspectors/dry_introspector.rb +131 -0
  61. data/lib/grape_oas/introspectors/dry_introspector_support/argument_extractor.rb +51 -0
  62. data/lib/grape_oas/introspectors/dry_introspector_support/ast_walker.rb +125 -0
  63. data/lib/grape_oas/introspectors/dry_introspector_support/constraint_applier.rb +136 -0
  64. data/lib/grape_oas/introspectors/dry_introspector_support/constraint_extractor.rb +85 -0
  65. data/lib/grape_oas/introspectors/dry_introspector_support/constraint_merger.rb +47 -0
  66. data/lib/grape_oas/introspectors/dry_introspector_support/contract_resolver.rb +60 -0
  67. data/lib/grape_oas/introspectors/dry_introspector_support/inheritance_handler.rb +87 -0
  68. data/lib/grape_oas/introspectors/dry_introspector_support/predicate_handler.rb +131 -0
  69. data/lib/grape_oas/introspectors/dry_introspector_support/type_schema_builder.rb +143 -0
  70. data/lib/grape_oas/introspectors/dry_introspector_support/type_unwrapper.rb +143 -0
  71. data/lib/grape_oas/introspectors/entity_introspector.rb +165 -0
  72. data/lib/grape_oas/introspectors/entity_introspector_support/cycle_tracker.rb +42 -0
  73. data/lib/grape_oas/introspectors/entity_introspector_support/discriminator_handler.rb +83 -0
  74. data/lib/grape_oas/introspectors/entity_introspector_support/exposure_processor.rb +261 -0
  75. data/lib/grape_oas/introspectors/entity_introspector_support/inheritance_builder.rb +112 -0
  76. data/lib/grape_oas/introspectors/entity_introspector_support/property_extractor.rb +53 -0
  77. data/lib/grape_oas/introspectors/registry.rb +136 -0
  78. data/lib/grape_oas/rake/oas_tasks.rb +127 -0
  79. data/lib/grape_oas/version.rb +5 -0
  80. data/lib/grape_oas.rb +145 -0
  81. 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