grape-oas 1.2.0 → 1.4.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 +4 -4
- data/CHANGELOG.md +42 -0
- data/README.md +6 -0
- data/lib/grape_oas/api_model/api.rb +4 -0
- data/lib/grape_oas/api_model/schema.rb +18 -3
- data/lib/grape_oas/api_model_builders/concerns/content_type_resolver.rb +21 -0
- data/lib/grape_oas/api_model_builders/concerns/type_resolver.rb +3 -6
- data/lib/grape_oas/api_model_builders/request.rb +21 -12
- data/lib/grape_oas/api_model_builders/request_params_support/param_location_resolver.rb +7 -2
- data/lib/grape_oas/api_model_builders/request_params_support/param_schema_builder.rb +15 -1
- data/lib/grape_oas/api_model_builders/request_params_support/schema_enhancer.rb +34 -50
- data/lib/grape_oas/constants.rb +13 -0
- data/lib/grape_oas/doc_key_normalizer.rb +14 -0
- data/lib/grape_oas/exporter/oas2/parameter.rb +1 -0
- data/lib/grape_oas/exporter/oas2/schema.rb +53 -19
- data/lib/grape_oas/exporter/oas3/parameter.rb +5 -2
- data/lib/grape_oas/exporter/oas3/schema.rb +81 -46
- data/lib/grape_oas/introspectors/dry_introspector_support/argument_extractor.rb +2 -31
- data/lib/grape_oas/introspectors/dry_introspector_support/constraint_extractor.rb +10 -19
- data/lib/grape_oas/introspectors/dry_introspector_support/predicate_handler.rb +2 -10
- data/lib/grape_oas/introspectors/entity_introspector.rb +5 -1
- data/lib/grape_oas/introspectors/entity_introspector_support/discriminator_handler.rb +2 -25
- data/lib/grape_oas/introspectors/entity_introspector_support/exposure_processor.rb +139 -136
- data/lib/grape_oas/introspectors/entity_introspector_support/inheritance_builder.rb +6 -30
- data/lib/grape_oas/introspectors/entity_introspector_support/nesting_merger.rb +104 -0
- data/lib/grape_oas/introspectors/entity_introspector_support/property_extractor.rb +2 -1
- data/lib/grape_oas/introspectors/entity_introspector_support/type_schema_resolver.rb +139 -0
- data/lib/grape_oas/introspectors/entity_introspector_support.rb +57 -0
- data/lib/grape_oas/range_utils.rb +87 -0
- data/lib/grape_oas/schema_constraints.rb +36 -0
- data/lib/grape_oas/type_resolvers/array_resolver.rb +3 -5
- data/lib/grape_oas/values_normalizer.rb +47 -0
- data/lib/grape_oas/version.rb +1 -1
- data/lib/grape_oas.rb +27 -0
- metadata +9 -2
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module GrapeOAS
|
|
4
|
+
module Introspectors
|
|
5
|
+
# Shared helpers used by multiple classes within EntityIntrospectorSupport.
|
|
6
|
+
# These are module-level methods to avoid copy-pasting the same logic across
|
|
7
|
+
# ExposureProcessor, DiscriminatorHandler, and InheritanceBuilder.
|
|
8
|
+
module EntityIntrospectorSupport
|
|
9
|
+
# Returns the raw exposure list for an entity class.
|
|
10
|
+
# Reads root_exposures via internal Grape::Entity ivars, which is
|
|
11
|
+
# unavoidable given Grape does not expose a stable public API for this.
|
|
12
|
+
def self.exposures(entity_class)
|
|
13
|
+
return [] unless entity_class.respond_to?(:root_exposures)
|
|
14
|
+
|
|
15
|
+
root = entity_class.root_exposures
|
|
16
|
+
list = root.instance_variable_get(:@exposures) || []
|
|
17
|
+
Array(list)
|
|
18
|
+
rescue NoMethodError
|
|
19
|
+
[]
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Resolves the canonical name for an entity class, preferring entity_name
|
|
23
|
+
# when defined on the class itself (via def self. or extend) and non-blank,
|
|
24
|
+
# falling back to the Ruby class name. Inherited entity_name is ignored to
|
|
25
|
+
# avoid collisions between parent and child schemas.
|
|
26
|
+
def self.resolve_canonical_name(entity_class)
|
|
27
|
+
if defines_own_entity_name?(entity_class)
|
|
28
|
+
name = entity_class.entity_name
|
|
29
|
+
name.is_a?(String) && !name.strip.empty? ? name : entity_class.name
|
|
30
|
+
else
|
|
31
|
+
entity_class.name
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def self.defines_own_entity_name?(entity_class)
|
|
36
|
+
return false unless entity_class.respond_to?(:entity_name)
|
|
37
|
+
|
|
38
|
+
parent = find_parent_entity(entity_class)
|
|
39
|
+
return true if parent.nil? || !parent.respond_to?(:entity_name)
|
|
40
|
+
|
|
41
|
+
entity_class.method(:entity_name).owner !=
|
|
42
|
+
parent.method(:entity_name).owner
|
|
43
|
+
end
|
|
44
|
+
private_class_method :defines_own_entity_name?
|
|
45
|
+
|
|
46
|
+
# Finds the parent entity class if one exists in the Grape::Entity hierarchy.
|
|
47
|
+
def self.find_parent_entity(entity_class)
|
|
48
|
+
return nil unless defined?(Grape::Entity)
|
|
49
|
+
|
|
50
|
+
parent = entity_class.superclass
|
|
51
|
+
return nil unless parent && parent < Grape::Entity && parent != Grape::Entity
|
|
52
|
+
|
|
53
|
+
parent
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module GrapeOAS
|
|
4
|
+
# Converts Range values into OpenAPI-compatible representations.
|
|
5
|
+
class RangeUtils
|
|
6
|
+
NUMERIC_TYPES = [Constants::SchemaTypes::INTEGER, Constants::SchemaTypes::NUMBER].freeze
|
|
7
|
+
|
|
8
|
+
class << self
|
|
9
|
+
# Expands a non-numeric bounded Range to an enum array.
|
|
10
|
+
# Returns nil for numeric, unbounded, empty, or oversized ranges.
|
|
11
|
+
def expand_range_to_enum(range)
|
|
12
|
+
return nil if range.begin.nil? || range.end.nil?
|
|
13
|
+
return nil if range.begin.is_a?(Numeric) || range.end.is_a?(Numeric)
|
|
14
|
+
|
|
15
|
+
begin
|
|
16
|
+
array = range.first(Constants::MAX_ENUM_RANGE_SIZE + 1)
|
|
17
|
+
rescue TypeError
|
|
18
|
+
return nil
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
return nil if array.empty? || array.size > Constants::MAX_ENUM_RANGE_SIZE
|
|
22
|
+
|
|
23
|
+
array
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Writes numeric range constraints directly to any object with
|
|
27
|
+
# minimum=/maximum=/exclusive_maximum= setters (Schema, ConstraintSet, etc).
|
|
28
|
+
# Skips descending and infinite bounds.
|
|
29
|
+
def apply_numeric_range(target, range)
|
|
30
|
+
return unless range
|
|
31
|
+
|
|
32
|
+
first_val = range.begin
|
|
33
|
+
last_val = range.end
|
|
34
|
+
|
|
35
|
+
return if descending?(first_val, last_val)
|
|
36
|
+
|
|
37
|
+
target.minimum = first_val if finite_numeric?(first_val) && target.respond_to?(:minimum=)
|
|
38
|
+
return unless finite_numeric?(last_val)
|
|
39
|
+
|
|
40
|
+
target.maximum = last_val if target.respond_to?(:maximum=)
|
|
41
|
+
target.exclusive_maximum = range.exclude_end? if target.respond_to?(:exclusive_maximum=)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Returns true when all non-nil bounds are Numeric (pure numeric range).
|
|
45
|
+
def numeric_range?(range)
|
|
46
|
+
bounds = [range.begin, range.end].compact
|
|
47
|
+
bounds.any? && bounds.all?(Numeric)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Applies a Range to a schema as min/max or enum.
|
|
51
|
+
# @param schema [ApiModel::Schema]
|
|
52
|
+
def apply_to_schema(schema, range)
|
|
53
|
+
bounds = [range.begin, range.end].compact
|
|
54
|
+
return if bounds.empty?
|
|
55
|
+
|
|
56
|
+
all_numeric = numeric_range?(range)
|
|
57
|
+
any_numeric = bounds.any?(Numeric)
|
|
58
|
+
mixed_numeric = any_numeric && !all_numeric
|
|
59
|
+
numeric_range = all_numeric
|
|
60
|
+
numeric_type = NUMERIC_TYPES.include?(schema.type)
|
|
61
|
+
|
|
62
|
+
if mixed_numeric
|
|
63
|
+
GrapeOAS.logger.warn("Mixed-type range #{range} ignored; endpoints must both be numeric or both non-numeric")
|
|
64
|
+
elsif numeric_range && numeric_type
|
|
65
|
+
apply_numeric_range(schema, range)
|
|
66
|
+
elsif numeric_range
|
|
67
|
+
GrapeOAS.logger.warn("Numeric range #{range} ignored on non-numeric schema type '#{schema.type}'")
|
|
68
|
+
elsif !numeric_type
|
|
69
|
+
expanded = expand_range_to_enum(range)
|
|
70
|
+
schema.enum = expanded if expanded
|
|
71
|
+
else
|
|
72
|
+
GrapeOAS.logger.warn("Non-numeric range #{range} ignored on numeric schema type '#{schema.type}'")
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
private
|
|
77
|
+
|
|
78
|
+
def finite_numeric?(val)
|
|
79
|
+
val.is_a?(Numeric) && val.finite?
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def descending?(first_val, last_val)
|
|
83
|
+
first_val.is_a?(Numeric) && last_val.is_a?(Numeric) && first_val > last_val
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module GrapeOAS
|
|
4
|
+
# Applies numeric and string constraints from documentation to a schema.
|
|
5
|
+
#
|
|
6
|
+
# Accepts both symbol-keyed and string-keyed doc hashes; keys are normalized
|
|
7
|
+
# internally so callers need not pre-convert them.
|
|
8
|
+
module SchemaConstraints
|
|
9
|
+
def self.apply(schema, doc)
|
|
10
|
+
doc = doc.transform_keys(&:to_sym) unless doc.empty?
|
|
11
|
+
if doc.key?(:minimum)
|
|
12
|
+
schema.minimum = doc[:minimum] if schema.respond_to?(:minimum=)
|
|
13
|
+
# Clear stale exclusive_minimum; re-set below if also provided
|
|
14
|
+
schema.exclusive_minimum = nil if schema.respond_to?(:exclusive_minimum=) && !doc.key?(:exclusive_minimum)
|
|
15
|
+
end
|
|
16
|
+
if doc.key?(:maximum)
|
|
17
|
+
schema.maximum = doc[:maximum] if schema.respond_to?(:maximum=)
|
|
18
|
+
# Clear stale exclusive_maximum; re-set below if also provided
|
|
19
|
+
schema.exclusive_maximum = nil if schema.respond_to?(:exclusive_maximum=) && !doc.key?(:exclusive_maximum)
|
|
20
|
+
end
|
|
21
|
+
set_if_present(schema, :exclusive_minimum=, doc, :exclusive_minimum)
|
|
22
|
+
set_if_present(schema, :exclusive_maximum=, doc, :exclusive_maximum)
|
|
23
|
+
set_if_present(schema, :min_length=, doc, :min_length)
|
|
24
|
+
set_if_present(schema, :max_length=, doc, :max_length)
|
|
25
|
+
set_if_present(schema, :pattern=, doc, :pattern)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def self.set_if_present(schema, setter, doc, key)
|
|
29
|
+
return unless doc.key?(key) && schema.respond_to?(setter)
|
|
30
|
+
|
|
31
|
+
schema.public_send(setter, doc[key])
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private_class_method :set_if_present
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -20,15 +20,13 @@ module GrapeOAS
|
|
|
20
20
|
class ArrayResolver
|
|
21
21
|
extend Base
|
|
22
22
|
|
|
23
|
-
|
|
24
|
-
# Intentionally narrow: avoid treating multi-type strings like "[String, Integer]" as arrays.
|
|
25
|
-
ARRAY_PATTERN = /\A\[(?<inner>(?:::)?[A-Z]\w*(?:::[A-Z]\w*)*)\]\z/
|
|
23
|
+
TYPED_ARRAY_PATTERN = Constants::TypePatterns::TYPED_ARRAY
|
|
26
24
|
|
|
27
25
|
class << self
|
|
28
26
|
def handles?(type)
|
|
29
27
|
return false unless type.is_a?(String)
|
|
30
28
|
|
|
31
|
-
type.match?(
|
|
29
|
+
type.match?(TYPED_ARRAY_PATTERN)
|
|
32
30
|
end
|
|
33
31
|
|
|
34
32
|
def build_schema(type)
|
|
@@ -53,7 +51,7 @@ module GrapeOAS
|
|
|
53
51
|
private
|
|
54
52
|
|
|
55
53
|
def extract_inner_type(type)
|
|
56
|
-
match = type.match(
|
|
54
|
+
match = type.match(TYPED_ARRAY_PATTERN)
|
|
57
55
|
match[:inner] if match
|
|
58
56
|
end
|
|
59
57
|
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module GrapeOAS
|
|
4
|
+
# Normalizes values from Grape parameter or entity documentation into
|
|
5
|
+
# Array, Range, or nil for OpenAPI schema generation.
|
|
6
|
+
module ValuesNormalizer
|
|
7
|
+
# @param values [Object] raw values from spec or documentation
|
|
8
|
+
# @param context [String] description for warning messages
|
|
9
|
+
# @return [Array, Range, nil]
|
|
10
|
+
def self.normalize(values, context: "values")
|
|
11
|
+
return nil unless values
|
|
12
|
+
|
|
13
|
+
return nil if values.is_a?(Hash) && !values.key?(:value) && !values.key?("value")
|
|
14
|
+
|
|
15
|
+
values = values.key?(:value) ? values[:value] : values["value"] if values.is_a?(Hash)
|
|
16
|
+
return nil unless values
|
|
17
|
+
|
|
18
|
+
if values.respond_to?(:call)
|
|
19
|
+
# Two-stage defense for callable values:
|
|
20
|
+
# 1) Arity check filters out validators (arity > 0) and objects without arity.
|
|
21
|
+
# This is a heuristic — optional-arg procs (proc { |v = nil| ... }) report arity 0.
|
|
22
|
+
# 2) Post-call type check catches those false positives by verifying the return
|
|
23
|
+
# value is a collection type. Both guards are load-bearing; do not remove either.
|
|
24
|
+
return nil unless values.respond_to?(:arity) && values.arity.zero?
|
|
25
|
+
|
|
26
|
+
begin
|
|
27
|
+
values = values.call
|
|
28
|
+
rescue StandardError => e
|
|
29
|
+
GrapeOAS.logger.warn("Proc evaluation failed for #{context} (#{e.class}): #{e.message}")
|
|
30
|
+
return nil
|
|
31
|
+
end
|
|
32
|
+
return nil unless values.is_a?(Array) || values.is_a?(Range) || set_instance?(values)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
values = values.to_a if set_instance?(values)
|
|
36
|
+
return nil unless values.is_a?(Array) || values.is_a?(Range)
|
|
37
|
+
return nil if values.is_a?(Array) && values.empty?
|
|
38
|
+
|
|
39
|
+
values
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def self.set_instance?(value)
|
|
43
|
+
value.is_a?(Set)
|
|
44
|
+
end
|
|
45
|
+
private_class_method :set_instance?
|
|
46
|
+
end
|
|
47
|
+
end
|
data/lib/grape_oas/version.rb
CHANGED
data/lib/grape_oas.rb
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "grape"
|
|
4
|
+
require "logger"
|
|
4
5
|
require "zeitwerk"
|
|
5
6
|
|
|
6
7
|
loader = Zeitwerk::Loader.for_gem
|
|
@@ -41,6 +42,32 @@ module GrapeOAS
|
|
|
41
42
|
end
|
|
42
43
|
module_function :version
|
|
43
44
|
|
|
45
|
+
# Formatter that prepends [grape-oas] and suppresses Logger metadata.
|
|
46
|
+
# Used by the default logger and the test capture helper.
|
|
47
|
+
LOG_FORMATTER = proc { |_severity, _datetime, _progname, msg| "[grape-oas] #{msg}\n" }
|
|
48
|
+
|
|
49
|
+
# Configurable logger for schema generation warnings.
|
|
50
|
+
# Defaults to Logger on $stderr. Set to Rails.logger or Logger.new(File::NULL).
|
|
51
|
+
#
|
|
52
|
+
# @return [#warn]
|
|
53
|
+
def logger
|
|
54
|
+
@logger ||= begin
|
|
55
|
+
l = Logger.new($stderr, progname: "grape-oas", level: Logger::WARN)
|
|
56
|
+
l.formatter = LOG_FORMATTER
|
|
57
|
+
l
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# @param value [#warn, nil] a logger-compatible object, or nil to reset
|
|
62
|
+
# to the default $stderr logger
|
|
63
|
+
def logger=(value)
|
|
64
|
+
raise ArgumentError, "logger must respond to :warn (got #{value.class})" if value && !value.respond_to?(:warn)
|
|
65
|
+
|
|
66
|
+
@logger = value
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
module_function :logger, :logger=
|
|
70
|
+
|
|
44
71
|
# Returns the global introspector registry.
|
|
45
72
|
#
|
|
46
73
|
# The registry manages introspectors that build schemas from various sources
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: grape-oas
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Andrei Subbota
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-04-23 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: grape
|
|
@@ -81,6 +81,7 @@ files:
|
|
|
81
81
|
- lib/grape_oas/api_model_builders/response_parsers/documentation_responses_parser.rb
|
|
82
82
|
- lib/grape_oas/api_model_builders/response_parsers/http_codes_parser.rb
|
|
83
83
|
- lib/grape_oas/constants.rb
|
|
84
|
+
- lib/grape_oas/doc_key_normalizer.rb
|
|
84
85
|
- lib/grape_oas/documentation_extension.rb
|
|
85
86
|
- lib/grape_oas/exporter.rb
|
|
86
87
|
- lib/grape_oas/exporter/base/operation.rb
|
|
@@ -118,18 +119,24 @@ files:
|
|
|
118
119
|
- lib/grape_oas/introspectors/dry_introspector_support/type_schema_builder.rb
|
|
119
120
|
- lib/grape_oas/introspectors/dry_introspector_support/type_unwrapper.rb
|
|
120
121
|
- lib/grape_oas/introspectors/entity_introspector.rb
|
|
122
|
+
- lib/grape_oas/introspectors/entity_introspector_support.rb
|
|
121
123
|
- lib/grape_oas/introspectors/entity_introspector_support/cycle_tracker.rb
|
|
122
124
|
- lib/grape_oas/introspectors/entity_introspector_support/discriminator_handler.rb
|
|
123
125
|
- lib/grape_oas/introspectors/entity_introspector_support/exposure_processor.rb
|
|
124
126
|
- lib/grape_oas/introspectors/entity_introspector_support/inheritance_builder.rb
|
|
127
|
+
- lib/grape_oas/introspectors/entity_introspector_support/nesting_merger.rb
|
|
125
128
|
- lib/grape_oas/introspectors/entity_introspector_support/property_extractor.rb
|
|
129
|
+
- lib/grape_oas/introspectors/entity_introspector_support/type_schema_resolver.rb
|
|
126
130
|
- lib/grape_oas/introspectors/registry.rb
|
|
127
131
|
- lib/grape_oas/rake/oas_tasks.rb
|
|
132
|
+
- lib/grape_oas/range_utils.rb
|
|
133
|
+
- lib/grape_oas/schema_constraints.rb
|
|
128
134
|
- lib/grape_oas/type_resolvers/array_resolver.rb
|
|
129
135
|
- lib/grape_oas/type_resolvers/base.rb
|
|
130
136
|
- lib/grape_oas/type_resolvers/dry_type_resolver.rb
|
|
131
137
|
- lib/grape_oas/type_resolvers/primitive_resolver.rb
|
|
132
138
|
- lib/grape_oas/type_resolvers/registry.rb
|
|
139
|
+
- lib/grape_oas/values_normalizer.rb
|
|
133
140
|
- lib/grape_oas/version.rb
|
|
134
141
|
homepage: https://github.com/numbata/grape-oas
|
|
135
142
|
licenses:
|