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,88 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module GrapeOAS
|
|
4
|
+
module Exporter
|
|
5
|
+
module Base
|
|
6
|
+
# Base class for Operation exporters
|
|
7
|
+
# Contains common logic shared between OAS2 and OAS3
|
|
8
|
+
class Operation
|
|
9
|
+
def initialize(operation, ref_tracker = nil, **options)
|
|
10
|
+
@op = operation
|
|
11
|
+
@ref_tracker = ref_tracker
|
|
12
|
+
@options = options
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def build
|
|
16
|
+
data = build_common_fields
|
|
17
|
+
|
|
18
|
+
# Add version-specific fields
|
|
19
|
+
data.merge!(build_version_specific_fields)
|
|
20
|
+
|
|
21
|
+
# Add security if present
|
|
22
|
+
data["security"] = @op.security unless @op.security.nil? || @op.security.empty?
|
|
23
|
+
|
|
24
|
+
# Guarantee a 4xx response for lint rules
|
|
25
|
+
ensure_default_error_response(data)
|
|
26
|
+
|
|
27
|
+
# Merge extensions
|
|
28
|
+
data.merge!(@op.extensions) if @op.extensions&.any?
|
|
29
|
+
|
|
30
|
+
data.compact
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
# Common fields present in both OAS2 and OAS3
|
|
36
|
+
def build_common_fields
|
|
37
|
+
summary = @op.summary
|
|
38
|
+
summary ||= @op.description&.split(/\.\s/)&.first&.strip
|
|
39
|
+
# Fallback: generate a readable summary from the operationId to satisfy lint rules
|
|
40
|
+
if summary.nil? && @op.operation_id
|
|
41
|
+
summary = @op.operation_id
|
|
42
|
+
.to_s
|
|
43
|
+
.tr("_", " ")
|
|
44
|
+
.split
|
|
45
|
+
.map(&:capitalize)
|
|
46
|
+
.join(" ")
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
{
|
|
50
|
+
"operationId" => @op.operation_id,
|
|
51
|
+
"summary" => summary,
|
|
52
|
+
"description" => @op.description,
|
|
53
|
+
"deprecated" => @op.deprecated,
|
|
54
|
+
"tags" => @op.tag_names
|
|
55
|
+
}
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Template method - subclasses must implement version-specific fields
|
|
59
|
+
# @return [Hash] Version-specific fields (e.g., consumes/produces for OAS2, requestBody for OAS3)
|
|
60
|
+
def build_version_specific_fields
|
|
61
|
+
raise NotImplementedError, "#{self.class} must implement #build_version_specific_fields"
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Ensure there is at least one 4xx response when any responses exist
|
|
65
|
+
# Can be suppressed per-operation via suppress_default_error_response flag
|
|
66
|
+
def ensure_default_error_response(data)
|
|
67
|
+
# Check operation-level suppression
|
|
68
|
+
return data if @op.respond_to?(:suppress_default_error_response) && @op.suppress_default_error_response
|
|
69
|
+
|
|
70
|
+
# Check global suppression
|
|
71
|
+
return data if @options[:suppress_default_error_response]
|
|
72
|
+
|
|
73
|
+
responses = data["responses"]
|
|
74
|
+
return data unless responses && !responses.empty?
|
|
75
|
+
|
|
76
|
+
has_4xx = responses.keys.any? { |code| code.to_s.start_with?("4") }
|
|
77
|
+
return data if has_4xx
|
|
78
|
+
|
|
79
|
+
responses["400"] = {
|
|
80
|
+
"description" => "Bad Request"
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
data
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module GrapeOAS
|
|
4
|
+
module Exporter
|
|
5
|
+
module Base
|
|
6
|
+
# Base class for Paths exporters
|
|
7
|
+
# Contains common logic shared between OAS2 and OAS3
|
|
8
|
+
class Paths
|
|
9
|
+
def initialize(source, ref_tracker = nil, **options)
|
|
10
|
+
@source = source
|
|
11
|
+
@ref_tracker = ref_tracker
|
|
12
|
+
@options = options
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def build
|
|
16
|
+
if api?(@source)
|
|
17
|
+
build_paths(@source)
|
|
18
|
+
else
|
|
19
|
+
build_path_item(@source)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def api?(obj)
|
|
26
|
+
obj.respond_to?(:paths)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def build_paths(api)
|
|
30
|
+
paths = {}
|
|
31
|
+
api.paths.each do |path|
|
|
32
|
+
paths[path.template] = build_path_item(path)
|
|
33
|
+
end
|
|
34
|
+
paths
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def build_path_item(path)
|
|
38
|
+
item = {}
|
|
39
|
+
path.operations.each do |operation|
|
|
40
|
+
item[operation.http_method] = build_operation(operation)
|
|
41
|
+
end
|
|
42
|
+
item
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Template method - subclasses must implement
|
|
46
|
+
# Returns the version-specific Operation instance
|
|
47
|
+
def build_operation(operation)
|
|
48
|
+
raise NotImplementedError, "#{self.class} must implement #build_operation"
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module GrapeOAS
|
|
4
|
+
module Exporter
|
|
5
|
+
module Concerns
|
|
6
|
+
# Shared schema indexing and reference collection logic for OAS2 and OAS3 schema exporters.
|
|
7
|
+
# Handles building schema indexes from operations and collecting nested schema references.
|
|
8
|
+
module SchemaIndexer
|
|
9
|
+
def find_schema_by_canonical_name(canonical_name)
|
|
10
|
+
@ref_schemas[canonical_name] || schema_index[canonical_name]
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def schema_index
|
|
14
|
+
@schema_index ||= build_schema_index
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def build_schema_index
|
|
18
|
+
index = {}
|
|
19
|
+
# Index schemas from operations
|
|
20
|
+
@api.paths.each do |path|
|
|
21
|
+
path.operations.each do |op|
|
|
22
|
+
collect_schemas_from_operation(op, index)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
# Index pre-registered models
|
|
26
|
+
Array(@api.registered_schemas).each do |schema|
|
|
27
|
+
index_schema(schema, index)
|
|
28
|
+
end
|
|
29
|
+
index
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def collect_schemas_from_operation(operation, index)
|
|
33
|
+
Array(operation.parameters).each do |param|
|
|
34
|
+
index_schema(param.schema, index)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
if operation.request_body
|
|
38
|
+
Array(operation.request_body.media_types).each do |media_type|
|
|
39
|
+
index_schema(media_type.schema, index)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
Array(operation.responses).each do |resp|
|
|
44
|
+
Array(resp.media_types).each do |media_type|
|
|
45
|
+
index_schema(media_type.schema, index)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def index_schema(schema, index)
|
|
51
|
+
return unless schema.respond_to?(:canonical_name) && schema.canonical_name
|
|
52
|
+
|
|
53
|
+
index[schema.canonical_name] ||= schema
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def collect_refs(schema, pending, seen = Set.new)
|
|
57
|
+
return unless schema
|
|
58
|
+
|
|
59
|
+
if schema.respond_to?(:canonical_name) && schema.canonical_name
|
|
60
|
+
return if seen.include?(schema.canonical_name)
|
|
61
|
+
|
|
62
|
+
seen << schema.canonical_name
|
|
63
|
+
@ref_schemas[schema.canonical_name] ||= schema
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
if schema.respond_to?(:properties) && schema.properties
|
|
67
|
+
schema.properties.each_value do |prop|
|
|
68
|
+
if prop.respond_to?(:canonical_name) && prop.canonical_name
|
|
69
|
+
pending << prop.canonical_name
|
|
70
|
+
@ref_schemas[prop.canonical_name] ||= prop
|
|
71
|
+
end
|
|
72
|
+
collect_refs(prop, pending, seen)
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
collect_refs(schema.items, pending, seen) if schema.respond_to?(:items) && schema.items
|
|
76
|
+
|
|
77
|
+
# Handle allOf/oneOf/anyOf composition (for inheritance/polymorphism)
|
|
78
|
+
%i[all_of one_of any_of].each do |composition_type|
|
|
79
|
+
next unless schema.respond_to?(composition_type) && schema.send(composition_type)
|
|
80
|
+
|
|
81
|
+
schema.send(composition_type).each do |sub_schema|
|
|
82
|
+
if sub_schema.respond_to?(:canonical_name) && sub_schema.canonical_name
|
|
83
|
+
pending << sub_schema.canonical_name
|
|
84
|
+
@ref_schemas[sub_schema.canonical_name] ||= sub_schema
|
|
85
|
+
end
|
|
86
|
+
collect_refs(sub_schema, pending, seen)
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module GrapeOAS
|
|
4
|
+
module Exporter
|
|
5
|
+
module Concerns
|
|
6
|
+
# Shared tag-building logic for OAS2 and OAS3 schema exporters.
|
|
7
|
+
# Extracts tag definitions from operations and normalizes tag formats.
|
|
8
|
+
module TagBuilder
|
|
9
|
+
def build_tags
|
|
10
|
+
used_tag_names = collect_used_tag_names
|
|
11
|
+
seen_names = Set.new
|
|
12
|
+
tags = Array(@api.tag_defs).filter_map do |tag|
|
|
13
|
+
normalized = normalize_tag(tag)
|
|
14
|
+
tag_name = normalized["name"]
|
|
15
|
+
# Only include tags that are actually used by operations and not already seen
|
|
16
|
+
next if seen_names.include?(tag_name) || !used_tag_names.include?(tag_name)
|
|
17
|
+
|
|
18
|
+
seen_names << tag_name
|
|
19
|
+
normalized
|
|
20
|
+
end
|
|
21
|
+
tags.empty? ? nil : tags
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def normalize_tag(tag)
|
|
25
|
+
if tag.is_a?(Hash)
|
|
26
|
+
# Convert symbol keys to string keys
|
|
27
|
+
tag.transform_keys(&:to_s)
|
|
28
|
+
elsif tag.respond_to?(:name)
|
|
29
|
+
h = { "name" => tag.name.to_s }
|
|
30
|
+
h["description"] = tag.description if tag.respond_to?(:description)
|
|
31
|
+
h
|
|
32
|
+
else
|
|
33
|
+
name = tag.to_s
|
|
34
|
+
desc = if defined?(ActiveSupport::Inflector)
|
|
35
|
+
"Operations about #{ActiveSupport::Inflector.pluralize(name)}"
|
|
36
|
+
else
|
|
37
|
+
"Operations about #{name}s"
|
|
38
|
+
end
|
|
39
|
+
{ "name" => name, "description" => desc }
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def collect_used_tag_names
|
|
44
|
+
used_tags = Set.new
|
|
45
|
+
@api.paths.each do |path|
|
|
46
|
+
path.operations.each do |op|
|
|
47
|
+
Array(op.tag_names).each { |t| used_tags << t }
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
used_tags
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module GrapeOAS
|
|
4
|
+
module Exporter
|
|
5
|
+
module OAS2
|
|
6
|
+
# OAS2-specific Operation exporter
|
|
7
|
+
# Inherits common operation logic from Base::Operation
|
|
8
|
+
class Operation < Base::Operation
|
|
9
|
+
private
|
|
10
|
+
|
|
11
|
+
# OAS2-specific fields: consumes, produces, parameters (including body)
|
|
12
|
+
def build_version_specific_fields
|
|
13
|
+
{
|
|
14
|
+
"consumes" => consumes,
|
|
15
|
+
"produces" => produces,
|
|
16
|
+
"parameters" => Parameter.new(@op, @ref_tracker).build,
|
|
17
|
+
"responses" => Response.new(@op.responses, @ref_tracker).build
|
|
18
|
+
}
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def consumes
|
|
22
|
+
Array(@op.consumes.presence || [Constants::MimeTypes::JSON])
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def produces
|
|
26
|
+
Array(@op.produces.presence || [Constants::MimeTypes::JSON])
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module GrapeOAS
|
|
4
|
+
module Exporter
|
|
5
|
+
module OAS2
|
|
6
|
+
class Parameter
|
|
7
|
+
PRIMITIVE_MAPPINGS = {
|
|
8
|
+
Constants::SchemaTypes::INTEGER => { type: Constants::SchemaTypes::INTEGER, format: "int32" },
|
|
9
|
+
"long" => { type: Constants::SchemaTypes::INTEGER, format: "int64" },
|
|
10
|
+
"float" => { type: Constants::SchemaTypes::NUMBER, format: "float" },
|
|
11
|
+
"double" => { type: Constants::SchemaTypes::NUMBER, format: "double" },
|
|
12
|
+
"byte" => { type: Constants::SchemaTypes::STRING, format: "byte" },
|
|
13
|
+
"date" => { type: Constants::SchemaTypes::STRING, format: "date" },
|
|
14
|
+
"dateTime" => { type: Constants::SchemaTypes::STRING, format: "date-time" },
|
|
15
|
+
"binary" => { type: Constants::SchemaTypes::STRING, format: "binary" },
|
|
16
|
+
"password" => { type: Constants::SchemaTypes::STRING, format: "password" },
|
|
17
|
+
"email" => { type: Constants::SchemaTypes::STRING, format: "email" },
|
|
18
|
+
"uuid" => { type: Constants::SchemaTypes::STRING, format: "uuid" }
|
|
19
|
+
}.freeze
|
|
20
|
+
|
|
21
|
+
def initialize(operation, ref_tracker = nil)
|
|
22
|
+
@op = operation
|
|
23
|
+
@ref_tracker = ref_tracker
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def build
|
|
27
|
+
params = Array(@op.parameters).map { |param| build_parameter(param) }
|
|
28
|
+
params << build_body_parameter(@op.request_body) if @op.request_body
|
|
29
|
+
params
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
def build_parameter(param)
|
|
35
|
+
type = param.schema&.type
|
|
36
|
+
format = param.schema&.format
|
|
37
|
+
primitive_types = PRIMITIVE_MAPPINGS.keys + %w[object string boolean file json array]
|
|
38
|
+
is_primitive = type && primitive_types.include?(type)
|
|
39
|
+
|
|
40
|
+
if is_primitive && param.location != "body"
|
|
41
|
+
mapping = PRIMITIVE_MAPPINGS[type]
|
|
42
|
+
result = {
|
|
43
|
+
"name" => param.name,
|
|
44
|
+
"in" => param.location,
|
|
45
|
+
"required" => param.required,
|
|
46
|
+
"description" => param.description,
|
|
47
|
+
"type" => mapping ? mapping[:type] : type,
|
|
48
|
+
"format" => format || (mapping ? mapping[:format] : nil)
|
|
49
|
+
}
|
|
50
|
+
apply_collection_format(result, param, type)
|
|
51
|
+
result.compact
|
|
52
|
+
else
|
|
53
|
+
{
|
|
54
|
+
"name" => param.name,
|
|
55
|
+
"in" => param.location,
|
|
56
|
+
"required" => param.required,
|
|
57
|
+
"description" => param.description,
|
|
58
|
+
"schema" => build_schema_or_ref(param.schema)
|
|
59
|
+
}.tap do |h|
|
|
60
|
+
h["type"] = type if type
|
|
61
|
+
h["format"] = format if format
|
|
62
|
+
end.compact
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def apply_collection_format(result, param, type)
|
|
67
|
+
return unless type == Constants::SchemaTypes::ARRAY
|
|
68
|
+
return unless param.collection_format
|
|
69
|
+
|
|
70
|
+
valid_formats = %w[csv ssv tsv pipes multi brackets]
|
|
71
|
+
result["collectionFormat"] = param.collection_format if valid_formats.include?(param.collection_format)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def build_body_parameter(request_body)
|
|
75
|
+
schema = build_body_schema(request_body)
|
|
76
|
+
name = derive_body_name(request_body)
|
|
77
|
+
{
|
|
78
|
+
"name" => name,
|
|
79
|
+
"in" => "body",
|
|
80
|
+
"required" => request_body.required,
|
|
81
|
+
"description" => request_body.description,
|
|
82
|
+
"schema" => schema
|
|
83
|
+
}.compact
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def derive_body_name(request_body)
|
|
87
|
+
# Use explicit body_name if provided
|
|
88
|
+
return request_body.body_name if request_body.respond_to?(:body_name) && request_body.body_name
|
|
89
|
+
|
|
90
|
+
# Fall back to canonical name from schema
|
|
91
|
+
canonical = begin
|
|
92
|
+
request_body&.media_types&.first&.schema&.canonical_name
|
|
93
|
+
rescue NoMethodError
|
|
94
|
+
nil
|
|
95
|
+
end
|
|
96
|
+
canonical ? canonical.gsub("::", "_") : "body"
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def build_body_schema(request_body)
|
|
100
|
+
mt = Array(request_body.media_types).first
|
|
101
|
+
mt ? build_schema_or_ref(mt.schema) : nil
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def build_schema_or_ref(schema)
|
|
105
|
+
if schema.respond_to?(:canonical_name) && schema.canonical_name
|
|
106
|
+
@ref_tracker << schema.canonical_name if @ref_tracker
|
|
107
|
+
ref_name = schema.canonical_name.gsub("::", "_")
|
|
108
|
+
{ "$ref" => "#/definitions/#{ref_name}" }
|
|
109
|
+
else
|
|
110
|
+
Schema.new(schema, @ref_tracker).build
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module GrapeOAS
|
|
4
|
+
module Exporter
|
|
5
|
+
module OAS2
|
|
6
|
+
# OAS2-specific Paths exporter
|
|
7
|
+
# Inherits common path building logic from Base::Paths
|
|
8
|
+
class Paths < Base::Paths
|
|
9
|
+
private
|
|
10
|
+
|
|
11
|
+
# Build OAS2-specific operation
|
|
12
|
+
def build_operation(operation)
|
|
13
|
+
Operation.new(operation, @ref_tracker,
|
|
14
|
+
suppress_default_error_response: @options[:suppress_default_error_response],).build
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module GrapeOAS
|
|
4
|
+
module Exporter
|
|
5
|
+
module OAS2
|
|
6
|
+
class Response
|
|
7
|
+
def initialize(responses, ref_tracker = nil)
|
|
8
|
+
@responses = responses
|
|
9
|
+
@ref_tracker = ref_tracker
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def build
|
|
13
|
+
res = {}
|
|
14
|
+
Array(@responses).each do |resp|
|
|
15
|
+
res[resp.http_status] = {
|
|
16
|
+
"description" => resp.description,
|
|
17
|
+
"schema" => build_response_schema(resp),
|
|
18
|
+
"headers" => build_headers(resp.headers),
|
|
19
|
+
"examples" => build_examples(resp.media_types, resp.examples)
|
|
20
|
+
}.compact
|
|
21
|
+
res[resp.http_status].merge!(resp.extensions) if resp.extensions
|
|
22
|
+
end
|
|
23
|
+
res
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def build_response_schema(resp)
|
|
29
|
+
mt = Array(resp.media_types).first
|
|
30
|
+
mt ? build_schema_or_ref(mt.schema) : nil
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def build_schema_or_ref(schema)
|
|
34
|
+
if schema.respond_to?(:canonical_name) && schema.canonical_name
|
|
35
|
+
@ref_tracker << schema.canonical_name if @ref_tracker
|
|
36
|
+
ref_name = schema.canonical_name.gsub("::", "_")
|
|
37
|
+
{ "$ref" => "#/definitions/#{ref_name}" }
|
|
38
|
+
else
|
|
39
|
+
Schema.new(schema, @ref_tracker).build
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def build_headers(headers)
|
|
44
|
+
return nil unless headers && !headers.empty?
|
|
45
|
+
|
|
46
|
+
headers.each_with_object({}) do |hdr, h|
|
|
47
|
+
name = hdr[:name] || hdr["name"] || hdr[:key] || hdr["key"]
|
|
48
|
+
next unless name
|
|
49
|
+
|
|
50
|
+
# OAS2 headers have type at top level (not wrapped in schema)
|
|
51
|
+
schema_value = hdr[:schema] || hdr["schema"] || {}
|
|
52
|
+
schema_type = schema_value["type"] || schema_value[:type] || Constants::SchemaTypes::STRING
|
|
53
|
+
description = hdr[:description] || hdr["description"] || schema_value["description"]
|
|
54
|
+
|
|
55
|
+
header_obj = { "type" => schema_type }
|
|
56
|
+
header_obj["description"] = description if description
|
|
57
|
+
h[name] = header_obj
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def build_examples(media_types, response_examples = nil)
|
|
62
|
+
return nil unless media_types
|
|
63
|
+
|
|
64
|
+
mt = Array(media_types).first
|
|
65
|
+
# Media type examples take precedence over response-level examples
|
|
66
|
+
examples = mt&.examples || response_examples
|
|
67
|
+
return nil unless examples
|
|
68
|
+
|
|
69
|
+
examples.is_a?(Hash) ? examples : { mt&.mime_type || "application/json" => examples }
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module GrapeOAS
|
|
4
|
+
module Exporter
|
|
5
|
+
module OAS2
|
|
6
|
+
class Schema
|
|
7
|
+
def initialize(schema, ref_tracker = nil)
|
|
8
|
+
@schema = schema
|
|
9
|
+
@ref_tracker = ref_tracker
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def build
|
|
13
|
+
return {} unless @schema
|
|
14
|
+
|
|
15
|
+
# Handle allOf composition (for inheritance)
|
|
16
|
+
return build_all_of_schema if @schema.all_of && !@schema.all_of.empty?
|
|
17
|
+
|
|
18
|
+
# Handle oneOf/anyOf by using first type (OAS2 doesn't support oneOf/anyOf)
|
|
19
|
+
# Skip if schema has explicit type - use type with extensions instead
|
|
20
|
+
return build_first_of_schema(:one_of) if @schema.one_of && !@schema.one_of.empty? && !@schema.type
|
|
21
|
+
return build_first_of_schema(:any_of) if @schema.any_of && !@schema.any_of.empty? && !@schema.type
|
|
22
|
+
|
|
23
|
+
schema_hash = build_base_hash
|
|
24
|
+
apply_constraints(schema_hash)
|
|
25
|
+
apply_extensions(schema_hash)
|
|
26
|
+
schema_hash.compact
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def build_base_hash
|
|
30
|
+
schema_hash = {
|
|
31
|
+
"type" => @schema.type,
|
|
32
|
+
"format" => @schema.format,
|
|
33
|
+
"description" => @schema.description&.to_s,
|
|
34
|
+
"properties" => build_properties(@schema.properties),
|
|
35
|
+
"items" => (@schema.items ? build_schema_or_ref(@schema.items) : nil),
|
|
36
|
+
"enum" => normalize_enum(@schema.enum, @schema.type)
|
|
37
|
+
}
|
|
38
|
+
if schema_hash["properties"].nil? || schema_hash["properties"].empty? || @schema.type != Constants::SchemaTypes::OBJECT
|
|
39
|
+
schema_hash.delete("properties")
|
|
40
|
+
end
|
|
41
|
+
schema_hash["example"] = @schema.examples if @schema.examples
|
|
42
|
+
schema_hash["required"] = @schema.required if @schema.required && !@schema.required.empty?
|
|
43
|
+
schema_hash["discriminator"] = @schema.discriminator if @schema.discriminator
|
|
44
|
+
schema_hash
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def apply_constraints(schema_hash)
|
|
48
|
+
schema_hash["minLength"] = @schema.min_length if @schema.min_length
|
|
49
|
+
schema_hash["maxLength"] = @schema.max_length if @schema.max_length
|
|
50
|
+
schema_hash["pattern"] = @schema.pattern if @schema.pattern
|
|
51
|
+
schema_hash["minimum"] = @schema.minimum if @schema.minimum
|
|
52
|
+
schema_hash["maximum"] = @schema.maximum if @schema.maximum
|
|
53
|
+
schema_hash["exclusiveMinimum"] = @schema.exclusive_minimum if @schema.exclusive_minimum
|
|
54
|
+
schema_hash["exclusiveMaximum"] = @schema.exclusive_maximum if @schema.exclusive_maximum
|
|
55
|
+
schema_hash["minItems"] = @schema.min_items if @schema.min_items
|
|
56
|
+
schema_hash["maxItems"] = @schema.max_items if @schema.max_items
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def apply_extensions(schema_hash)
|
|
60
|
+
schema_hash.merge!(@schema.extensions) if @schema.extensions
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
private
|
|
64
|
+
|
|
65
|
+
# Build schema from oneOf/anyOf by using first type (OAS2 doesn't support these)
|
|
66
|
+
# Extensions are merged to allow x-anyOf/x-oneOf for consumers that support them
|
|
67
|
+
def build_first_of_schema(composition_type)
|
|
68
|
+
schemas = @schema.send(composition_type)
|
|
69
|
+
first_schema = schemas.first
|
|
70
|
+
return {} unless first_schema
|
|
71
|
+
|
|
72
|
+
# Build the first schema as the fallback
|
|
73
|
+
result = build_schema_or_ref(first_schema)
|
|
74
|
+
result["description"] = @schema.description.to_s if @schema.description
|
|
75
|
+
apply_extensions(result)
|
|
76
|
+
result
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Build allOf schema for inheritance
|
|
80
|
+
def build_all_of_schema
|
|
81
|
+
all_of_items = @schema.all_of.map do |item|
|
|
82
|
+
build_schema_or_ref(item)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
result = { "allOf" => all_of_items }
|
|
86
|
+
result["description"] = @schema.description.to_s if @schema.description
|
|
87
|
+
result
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def build_properties(properties)
|
|
91
|
+
return nil unless properties
|
|
92
|
+
return nil if properties.empty?
|
|
93
|
+
|
|
94
|
+
properties.transform_values do |prop_schema|
|
|
95
|
+
build_schema_or_ref(prop_schema)
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def build_schema_or_ref(schema)
|
|
100
|
+
if schema.respond_to?(:canonical_name) && schema.canonical_name
|
|
101
|
+
@ref_tracker << schema.canonical_name if @ref_tracker
|
|
102
|
+
ref_name = schema.canonical_name.gsub("::", "_")
|
|
103
|
+
{ "$ref" => "#/definitions/#{ref_name}" }
|
|
104
|
+
else
|
|
105
|
+
Schema.new(schema, @ref_tracker).build
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def normalize_enum(enum_vals, type)
|
|
110
|
+
return nil unless enum_vals.is_a?(Array)
|
|
111
|
+
|
|
112
|
+
coerced = enum_vals.map do |v|
|
|
113
|
+
case type
|
|
114
|
+
when Constants::SchemaTypes::INTEGER then v.to_i if v.respond_to?(:to_i)
|
|
115
|
+
when Constants::SchemaTypes::NUMBER then v.to_f if v.respond_to?(:to_f)
|
|
116
|
+
else v
|
|
117
|
+
end
|
|
118
|
+
end.compact
|
|
119
|
+
|
|
120
|
+
coerced.uniq
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|