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,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
|
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)
|