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,112 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GrapeOAS
4
+ module Introspectors
5
+ module EntityIntrospectorSupport
6
+ # Handles entity inheritance and builds allOf schemas for parent-child entity relationships.
7
+ class InheritanceBuilder
8
+ def initialize(entity_class, stack:, registry:)
9
+ @entity_class = entity_class
10
+ @stack = stack
11
+ @registry = registry
12
+ end
13
+
14
+ # Finds the parent entity class if one exists.
15
+ #
16
+ # @param entity_class [Class] the entity class to check
17
+ # @return [Class, nil] the parent entity class or nil
18
+ def self.find_parent_entity(entity_class)
19
+ return nil unless defined?(Grape::Entity)
20
+
21
+ parent = entity_class.superclass
22
+ return nil unless parent && parent < Grape::Entity && parent != Grape::Entity
23
+
24
+ parent
25
+ end
26
+
27
+ # Checks if an entity inherits from a parent that uses discriminator.
28
+ #
29
+ # @param entity_class [Class] the entity class to check
30
+ # @return [Boolean] true if parent has a discriminator field
31
+ def self.inherits_with_discriminator?(entity_class)
32
+ parent = find_parent_entity(entity_class)
33
+ parent && DiscriminatorHandler.new(parent).discriminator?
34
+ end
35
+
36
+ # Builds an inherited schema using allOf composition.
37
+ #
38
+ # @param parent_entity [Class] the parent entity class
39
+ # @return [ApiModel::Schema] the composed schema
40
+ def build_inherited_schema(parent_entity)
41
+ # First, ensure parent schema is built
42
+ parent_schema = GrapeOAS.introspectors.build_schema(parent_entity, stack: @stack, registry: @registry)
43
+
44
+ # Build child-specific properties (excluding inherited ones)
45
+ child_schema = build_child_only_schema(parent_entity)
46
+
47
+ # Create allOf schema with ref to parent + child properties
48
+ schema = ApiModel::Schema.new(
49
+ canonical_name: @entity_class.name,
50
+ all_of: [parent_schema, child_schema],
51
+ )
52
+
53
+ @registry[@entity_class] = schema
54
+ schema
55
+ end
56
+
57
+ private
58
+
59
+ def build_child_only_schema(parent_entity)
60
+ child_schema = ApiModel::Schema.new(type: Constants::SchemaTypes::OBJECT)
61
+ processor = ExposureProcessor.new(@entity_class, stack: @stack, registry: @registry)
62
+
63
+ # Get parent's exposure keys to exclude
64
+ parent_keys = processor.parent_exposures(parent_entity).map { |e| e.key.to_s }
65
+
66
+ processor.exposures.each do |exposure|
67
+ next unless processor.exposed?(exposure)
68
+
69
+ name = exposure.key.to_s
70
+ # Skip if this is an inherited property
71
+ next if parent_keys.include?(name)
72
+
73
+ add_child_property(child_schema, exposure, processor)
74
+ end
75
+
76
+ child_schema
77
+ end
78
+
79
+ def add_child_property(child_schema, exposure, processor)
80
+ doc = exposure.documentation || {}
81
+ opts = exposure.instance_variable_get(:@options) || {}
82
+
83
+ return if processor.merge_exposure?(exposure, doc, opts)
84
+
85
+ prop_schema = processor.schema_for_exposure(exposure, doc)
86
+ required = determine_required(doc, exposure, processor)
87
+ prop_schema = wrap_in_array_if_needed(prop_schema, doc)
88
+
89
+ child_schema.add_property(exposure.key.to_s, prop_schema, required: required)
90
+ end
91
+
92
+ def determine_required(doc, exposure, processor)
93
+ # If explicitly set in documentation, use that value
94
+ return doc[:required] unless doc[:required].nil?
95
+
96
+ # Conditional exposures are not required (may be absent from output)
97
+ return false if processor.conditional?(exposure)
98
+
99
+ # Unconditional exposures are required by default (always present in output)
100
+ true
101
+ end
102
+
103
+ def wrap_in_array_if_needed(prop_schema, doc)
104
+ is_array = doc[:is_array] || doc["is_array"]
105
+ return prop_schema unless is_array
106
+
107
+ ApiModel::Schema.new(type: Constants::SchemaTypes::ARRAY, items: prop_schema)
108
+ end
109
+ end
110
+ end
111
+ end
112
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GrapeOAS
4
+ module Introspectors
5
+ module EntityIntrospectorSupport
6
+ # Utility class for extracting properties from entity documentation hashes.
7
+ # All methods are stateless and can be called directly on the class.
8
+ class PropertyExtractor
9
+ class << self
10
+ # Extracts description from a documentation hash.
11
+ #
12
+ # @param hash [Hash] the documentation hash
13
+ # @return [String, nil] the description value
14
+ def extract_description(hash)
15
+ hash[:description] || hash[:desc]
16
+ end
17
+
18
+ # Extracts nullable flag from a documentation hash.
19
+ #
20
+ # @param doc [Hash] the documentation hash
21
+ # @return [Boolean] true if nullable
22
+ def extract_nullable(doc)
23
+ doc[:nullable] || doc["nullable"] || false
24
+ end
25
+
26
+ # Extracts merge flag from exposure options and documentation.
27
+ #
28
+ # @param exposure the entity exposure
29
+ # @param doc [Hash] the documentation hash
30
+ # @param opts [Hash] the options hash
31
+ # @return [Boolean, nil] true if this is a merge exposure
32
+ def extract_merge_flag(exposure, doc, opts)
33
+ opts[:merge] || doc[:merge] || (exposure.respond_to?(:for_merge) && exposure.for_merge)
34
+ end
35
+
36
+ # Applies entity-level properties to a schema.
37
+ #
38
+ # @param schema [ApiModel::Schema] the schema to modify
39
+ # @param doc [Hash] the entity documentation hash
40
+ def apply_entity_level_properties(schema, doc)
41
+ schema.additional_properties = doc[:additional_properties] if doc.key?(:additional_properties)
42
+ schema.unevaluated_properties = doc[:unevaluated_properties] if doc.key?(:unevaluated_properties)
43
+
44
+ defs = doc[:defs] || doc[:$defs]
45
+ schema.defs = defs if defs.is_a?(Hash)
46
+ rescue NoMethodError
47
+ # Silently handle errors when schema doesn't respond to setters
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,136 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GrapeOAS
4
+ module Introspectors
5
+ # Registry for managing introspectors that can build schemas from various sources.
6
+ # Allows third-party gems to register custom introspectors for new schema formats.
7
+ #
8
+ # @example Registering a custom introspector
9
+ # GrapeOAS.introspectors.register(MyCustomIntrospector)
10
+ #
11
+ # @example Inserting before an existing introspector
12
+ # GrapeOAS.introspectors.register(HighPriorityIntrospector, before: EntityIntrospector)
13
+ #
14
+ class Registry
15
+ include Enumerable
16
+
17
+ def initialize
18
+ @introspectors = []
19
+ end
20
+
21
+ # Registers an introspector class.
22
+ #
23
+ # @param introspector [Class] Class that extends GrapeOAS::Introspectors::Base
24
+ # @param before [Class, nil] Insert before this introspector
25
+ # @param after [Class, nil] Insert after this introspector
26
+ # @return [self]
27
+ def register(introspector, before: nil, after: nil)
28
+ validate_introspector!(introspector)
29
+
30
+ if before
31
+ insert_before(introspector, before)
32
+ elsif after
33
+ insert_after(introspector, after)
34
+ else
35
+ @introspectors << introspector unless @introspectors.include?(introspector)
36
+ end
37
+
38
+ self
39
+ end
40
+
41
+ # Unregisters an introspector class.
42
+ #
43
+ # @param introspector [Class] The introspector to remove
44
+ # @return [self]
45
+ def unregister(introspector)
46
+ @introspectors.delete(introspector)
47
+ self
48
+ end
49
+
50
+ # Finds the first introspector that can handle the given subject.
51
+ #
52
+ # @param subject [Object] The object to introspect
53
+ # @return [Class, nil] The introspector class, or nil if none found
54
+ def find(subject)
55
+ @introspectors.find { |introspector| introspector.handles?(subject) }
56
+ end
57
+
58
+ # Builds a schema using the appropriate introspector.
59
+ #
60
+ # @param subject [Object] The object to introspect
61
+ # @param stack [Array] Recursion stack for cycle detection
62
+ # @param registry [Hash] Schema registry for caching
63
+ # @return [ApiModel::Schema, nil] The built schema, or nil if no handler found
64
+ def build_schema(subject, stack: [], registry: {})
65
+ introspector = find(subject)
66
+ return nil unless introspector
67
+
68
+ introspector.build_schema(subject, stack: stack, registry: registry)
69
+ end
70
+
71
+ # Checks if any introspector can handle the given subject.
72
+ #
73
+ # @param subject [Object] The object to check
74
+ # @return [Boolean]
75
+ def handles?(subject)
76
+ @introspectors.any? { |introspector| introspector.handles?(subject) }
77
+ end
78
+
79
+ # Iterates over all registered introspectors.
80
+ #
81
+ # @yield [introspector] Each registered introspector
82
+ def each(&)
83
+ @introspectors.each(&)
84
+ end
85
+
86
+ # Returns the number of registered introspectors.
87
+ #
88
+ # @return [Integer]
89
+ def size
90
+ @introspectors.size
91
+ end
92
+
93
+ # Clears all registered introspectors.
94
+ #
95
+ # @return [self]
96
+ def clear
97
+ @introspectors.clear
98
+ self
99
+ end
100
+
101
+ # Returns a list of registered introspectors.
102
+ #
103
+ # @return [Array<Class>]
104
+ def to_a
105
+ @introspectors.dup
106
+ end
107
+
108
+ private
109
+
110
+ def validate_introspector!(introspector)
111
+ return if introspector.respond_to?(:handles?) && introspector.respond_to?(:build_schema)
112
+
113
+ raise ArgumentError,
114
+ "Introspector must respond to .handles?(subject) and .build_schema(subject, stack:, registry:)"
115
+ end
116
+
117
+ def insert_before(introspector, target)
118
+ index = @introspectors.index(target)
119
+ if index
120
+ @introspectors.insert(index, introspector) unless @introspectors.include?(introspector)
121
+ else
122
+ @introspectors << introspector unless @introspectors.include?(introspector)
123
+ end
124
+ end
125
+
126
+ def insert_after(introspector, target)
127
+ index = @introspectors.index(target)
128
+ if index
129
+ @introspectors.insert(index + 1, introspector) unless @introspectors.include?(introspector)
130
+ else
131
+ @introspectors << introspector unless @introspectors.include?(introspector)
132
+ end
133
+ end
134
+ end
135
+ end
136
+ end
@@ -0,0 +1,127 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rake"
4
+ require "rake/tasklib"
5
+ require "json"
6
+
7
+ module GrapeOAS
8
+ module Rake
9
+ # Rake tasks for generating and validating OpenAPI documentation.
10
+ #
11
+ # @example Usage in Rakefile
12
+ # require 'grape_oas/rake/oas_tasks'
13
+ # GrapeOAS::Rake::OasTasks.new(MyAPI)
14
+ #
15
+ # @example With options
16
+ # GrapeOAS::Rake::OasTasks.new(MyAPI, schema_type: :oas31, title: "My API")
17
+ #
18
+ class OasTasks < ::Rake::TaskLib
19
+ attr_reader :api_class, :options
20
+
21
+ # @param api_class [Class, String] The Grape API class or its name as a string
22
+ # @param options [Hash] Options passed to GrapeOAS.generate
23
+ def initialize(api_class, **options)
24
+ super()
25
+
26
+ if api_class.is_a?(String)
27
+ @api_class_name = api_class
28
+ else
29
+ @api_class = api_class
30
+ end
31
+
32
+ @options = options
33
+ define_tasks
34
+ end
35
+
36
+ private
37
+
38
+ def resolved_api_class
39
+ @resolved_api_class ||= @api_class || @api_class_name.constantize
40
+ end
41
+
42
+ # Returns :environment if the task exists, otherwise an empty array
43
+ # This allows the tasks to work both in Rails (with :environment) and standalone
44
+ def environment_task
45
+ ::Rake::Task.task_defined?(:environment) ? :environment : []
46
+ end
47
+
48
+ def define_tasks
49
+ namespace :oas do
50
+ define_generate_task
51
+ define_validate_task
52
+ end
53
+ end
54
+
55
+ def define_generate_task
56
+ desc <<~DESC
57
+ Generate OpenAPI documentation
58
+ Params (usage: KEY=value):
59
+ output - Output file path (default: stdout)
60
+ format - Output format: json or yaml (default: json)
61
+ version - OpenAPI version: oas2, oas3, oas31 (default: from options or oas3)
62
+ DESC
63
+ task generate: environment_task do
64
+ schema = generate_schema
65
+ output = format_output(schema)
66
+
67
+ if output_file
68
+ File.write(output_file, output)
69
+ $stdout.puts "OpenAPI spec written to #{output_file}"
70
+ else
71
+ $stdout.puts output
72
+ end
73
+ end
74
+ end
75
+
76
+ def define_validate_task
77
+ desc <<~DESC
78
+ Validate OpenAPI documentation using swagger-cli
79
+ Params (usage: KEY=value):
80
+ version - OpenAPI version: oas2, oas3, oas31 (default: from options or oas3)
81
+ DESC
82
+ task validate: environment_task do
83
+ require "tempfile"
84
+
85
+ schema = generate_schema
86
+ output = JSON.pretty_generate(schema)
87
+
88
+ Tempfile.create(["openapi", ".json"]) do |f|
89
+ f.write(output)
90
+ f.flush
91
+
92
+ if system("which swagger-cli > /dev/null 2>&1")
93
+ success = system("swagger-cli validate #{f.path}")
94
+ exit(1) unless success
95
+ else
96
+ warn "swagger-cli not found. Install with: npm install -g @apidevtools/swagger-cli"
97
+ exit(1)
98
+ end
99
+ end
100
+ end
101
+ end
102
+
103
+ def generate_schema
104
+ schema_type = ENV.fetch("version", nil)&.to_sym || options[:schema_type] || :oas3
105
+ GrapeOAS.generate(app: resolved_api_class, schema_type: schema_type, **options)
106
+ end
107
+
108
+ def format_output(schema)
109
+ case output_format
110
+ when "yaml"
111
+ require "yaml"
112
+ schema.to_yaml
113
+ else
114
+ JSON.pretty_generate(schema)
115
+ end
116
+ end
117
+
118
+ def output_file
119
+ ENV.fetch("output", nil)
120
+ end
121
+
122
+ def output_format
123
+ ENV.fetch("format", "json")
124
+ end
125
+ end
126
+ end
127
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GrapeOAS
4
+ VERSION = "1.0.0"
5
+ end
data/lib/grape_oas.rb ADDED
@@ -0,0 +1,145 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "grape"
4
+ require "zeitwerk"
5
+
6
+ loader = Zeitwerk::Loader.for_gem
7
+ loader.inflector.inflect(
8
+ "api" => "API",
9
+ "grape-oas" => "GrapeOAS",
10
+ "grape_oas" => "GrapeOAS",
11
+ "oas2" => "OAS2",
12
+ "oas2_schema" => "OAS2Schema",
13
+ "oas3" => "OAS3",
14
+ "oas3_schema" => "OAS3Schema",
15
+ "oas30" => "OAS30",
16
+ "oas30_schema" => "OAS30Schema",
17
+ "oas31" => "OAS31",
18
+ "oas31_schema" => "OAS31Schema",
19
+ )
20
+ loader.ignore("#{__dir__}/grape-oas.rb")
21
+ loader.setup
22
+
23
+ # GrapeOAS generates OpenAPI specifications from Grape APIs.
24
+ #
25
+ # @example Basic usage
26
+ # schema = GrapeOAS.generate(app: MyAPI)
27
+ # puts JSON.pretty_generate(schema)
28
+ #
29
+ # @example Generate OpenAPI 2.0 (Swagger)
30
+ # schema = GrapeOAS.generate(app: MyAPI, schema_type: :oas2)
31
+ #
32
+ # @example Generate OpenAPI 3.1
33
+ # schema = GrapeOAS.generate(app: MyAPI, schema_type: :oas31)
34
+ #
35
+ module GrapeOAS
36
+ # Returns the version of the GrapeOAS gem.
37
+ #
38
+ # @return [String] the semantic version string
39
+ def version
40
+ OAS::VERSION
41
+ end
42
+ module_function :version
43
+
44
+ # Returns the global introspector registry.
45
+ #
46
+ # The registry manages introspectors that build schemas from various sources
47
+ # (e.g., Grape::Entity, Dry contracts). Third-party gems can register custom
48
+ # introspectors to support new schema definition formats.
49
+ #
50
+ # @return [Introspectors::Registry] the global introspector registry
51
+ #
52
+ # @example Registering a custom introspector
53
+ # GrapeOAS.introspectors.register(MyCustomIntrospector)
54
+ #
55
+ # @example Inserting before an existing introspector
56
+ # GrapeOAS.introspectors.register(
57
+ # HighPriorityIntrospector,
58
+ # before: GrapeOAS::Introspectors::EntityIntrospector
59
+ # )
60
+ #
61
+ def introspectors
62
+ @introspectors ||= begin
63
+ registry = Introspectors::Registry.new
64
+ # Register built-in introspectors in order of precedence
65
+ registry.register(Introspectors::EntityIntrospector)
66
+ registry.register(Introspectors::DryIntrospector)
67
+ registry
68
+ end
69
+ end
70
+ module_function :introspectors
71
+
72
+ # Returns the global exporter registry.
73
+ #
74
+ # The registry manages exporters that generate OpenAPI specifications
75
+ # in different versions (OAS 2.0, 3.0, 3.1). Third-party gems can register
76
+ # custom exporters for new output formats.
77
+ #
78
+ # @return [Exporter::Registry] the global exporter registry
79
+ #
80
+ # @example Registering a custom exporter
81
+ # GrapeOAS.exporters.register(:custom, MyCustomExporter)
82
+ #
83
+ # @example Using a custom exporter
84
+ # schema = GrapeOAS.generate(app: MyAPI, schema_type: :custom)
85
+ #
86
+ def exporters
87
+ @exporters ||= begin
88
+ registry = Exporter::Registry.new
89
+ # Register built-in exporters
90
+ registry.register(Exporter::OAS2Schema, as: :oas2)
91
+ registry.register(Exporter::OAS30Schema, as: %i[oas3 oas30])
92
+ registry.register(Exporter::OAS31Schema, as: :oas31)
93
+ registry
94
+ end
95
+ end
96
+ module_function :exporters
97
+
98
+ # Generates an OpenAPI specification from a Grape API application.
99
+ #
100
+ # Introspects the Grape API routes, parameters, entities, and contracts
101
+ # to produce a complete OpenAPI specification document.
102
+ #
103
+ # @param app [Class<Grape::API>] The Grape API class to document
104
+ # @param schema_type [Symbol] The OpenAPI version to generate
105
+ # - `:oas2` - OpenAPI 2.0 (Swagger)
106
+ # - `:oas3` - OpenAPI 3.0 (default)
107
+ # - `:oas31` - OpenAPI 3.1
108
+ # @param options [Hash] Additional options passed to the API model builder
109
+ # @option options [String] :title API title for the info section
110
+ # @option options [String] :version API version string
111
+ # @option options [Array<String>] :servers Server URLs (OAS3 only)
112
+ # @option options [Hash] :license License information
113
+ # @option options [Hash] :security_definitions Security scheme definitions
114
+ # @option options [String] :namespace Filter routes to only include paths
115
+ # starting with this namespace (e.g., "users" includes /users and /users/{id})
116
+ #
117
+ # @return [Hash] The OpenAPI specification as a Hash (JSON-serializable)
118
+ #
119
+ # @example Basic generation
120
+ # schema = GrapeOAS.generate(app: MyAPI)
121
+ #
122
+ # @example With custom metadata
123
+ # schema = GrapeOAS.generate(
124
+ # app: MyAPI,
125
+ # schema_type: :oas3,
126
+ # title: "My API",
127
+ # version: "1.0.0"
128
+ # )
129
+ #
130
+ # @example Filter by namespace
131
+ # schema = GrapeOAS.generate(app: MyAPI, namespace: "users")
132
+ # # Only includes paths like /users, /users/{id}, etc.
133
+ #
134
+ def generate(app:, schema_type: :oas3, **options)
135
+ api_model = GrapeOAS::ApiModelBuilder.new(options)
136
+ api_model.add_app(app)
137
+
138
+ GrapeOAS::Exporter.for(schema_type)
139
+ .new(api_model: api_model.api)
140
+ .generate
141
+ end
142
+ module_function :generate
143
+ end
144
+
145
+ Grape::API::Instance.extend(GrapeOAS::DocumentationExtension)