grape-oas 1.0.2 → 1.0.3
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 +8 -1
- data/lib/grape_oas/api_model_builders/request.rb +0 -1
- data/lib/grape_oas/api_model_builders/request_params.rb +0 -5
- data/lib/grape_oas/exporter/oas2_schema.rb +0 -3
- data/lib/grape_oas/exporter/oas3_schema.rb +0 -3
- data/lib/grape_oas/introspectors/dry_introspector.rb +19 -10
- data/lib/grape_oas/introspectors/dry_introspector_support/argument_extractor.rb +1 -0
- data/lib/grape_oas/introspectors/dry_introspector_support/inheritance_handler.rb +17 -5
- data/lib/grape_oas/introspectors/dry_introspector_support/predicate_handler.rb +11 -4
- data/lib/grape_oas/introspectors/dry_introspector_support/rule_index.rb +196 -0
- data/lib/grape_oas/introspectors/dry_introspector_support/type_schema_builder.rb +89 -17
- data/lib/grape_oas/introspectors/entity_introspector.rb +0 -8
- data/lib/grape_oas/version.rb +1 -1
- metadata +4 -6
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 683920ecd4af815a8dd0a58b8e36b5feb4be21030566842474f33a1c804fa008
|
|
4
|
+
data.tar.gz: c5c3035f9038878338b836cb09b096164723e012af2066b120b63535f062f5ed
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 820d58e88ffe10735ef72a72c41b4feeaedbcfb6d7a5cb3a3f5f5a50d8c91c6d7db5203fce40289511d9adc57ca605ec1ca3a2f6158ce043e58baf89c30a9d3a
|
|
7
|
+
data.tar.gz: ac0862ed30858730c19d7afc1592a53ca2aef1f14c789c407e535ab482f4ea497ab342b2967730aed4ffdf542497f445766def91d22e19c8810c0dded172b80d
|
data/CHANGELOG.md
CHANGED
|
@@ -5,7 +5,14 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
## [1.0.3] - 2025-12-23
|
|
9
|
+
|
|
10
|
+
* Your contribution here
|
|
11
|
+
- [#21](https://github.com/numbata/grape-oas/pull/21): Remove unnecessary require_relative in favor of Zeitwerk autoloadin [@numbata](https://github.com/numbata)
|
|
12
|
+
- [#17](https://github.com/numbata/grape-oas/pull/17): Support for nested rules and predicates in dry-schema introspection [@slbug](https://github.com/slbug)
|
|
13
|
+
- [#20](https://github.com/numbata/grape-oas/pull/20): Use annotation for coverage report [@numbata](https://github.com/numbata)
|
|
14
|
+
- [#18](https://github.com/numbata/grape-oas/pull/18): Support for range in size? predicate `required(:tags).value(:array, size?: 1..10).each(:string)` [@slbug](https://github.com/slbug)
|
|
15
|
+
- [#19](https://github.com/numbata/grape-oas/pull/19): Temporary disable memory profiler workflow for PRs [@numbata](https://github.com/numbata).
|
|
9
16
|
|
|
10
17
|
## [1.0.2] - 2025-12-15
|
|
11
18
|
|
|
@@ -1,10 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative "request_params_support/param_location_resolver"
|
|
4
|
-
require_relative "request_params_support/param_schema_builder"
|
|
5
|
-
require_relative "request_params_support/schema_enhancer"
|
|
6
|
-
require_relative "request_params_support/nested_params_builder"
|
|
7
|
-
|
|
8
3
|
module GrapeOAS
|
|
9
4
|
module ApiModelBuilders
|
|
10
5
|
class RequestParams
|
|
@@ -1,10 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative "base"
|
|
4
|
-
require_relative "dry_introspector_support/contract_resolver"
|
|
5
|
-
require_relative "dry_introspector_support/inheritance_handler"
|
|
6
|
-
require_relative "dry_introspector_support/type_schema_builder"
|
|
7
|
-
|
|
8
3
|
module GrapeOAS
|
|
9
4
|
module Introspectors
|
|
10
5
|
# Introspector for Dry::Validation contracts and Dry::Schema.
|
|
@@ -92,16 +87,30 @@ module GrapeOAS
|
|
|
92
87
|
end
|
|
93
88
|
|
|
94
89
|
def build_flat_schema
|
|
95
|
-
|
|
90
|
+
contract_schema = contract_resolver.contract_schema
|
|
91
|
+
|
|
92
|
+
constraints_by_path, required_by_object_path =
|
|
93
|
+
DryIntrospectorSupport::RuleIndex.build(contract_schema)
|
|
94
|
+
|
|
95
|
+
type_schema_builder.configure_path_aware_mode(constraints_by_path, required_by_object_path)
|
|
96
|
+
|
|
96
97
|
schema = ApiModel::Schema.new(
|
|
97
98
|
type: Constants::SchemaTypes::OBJECT,
|
|
98
99
|
canonical_name: contract_resolver.canonical_name,
|
|
99
100
|
)
|
|
100
101
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
102
|
+
root_required = required_by_object_path.fetch("", [])
|
|
103
|
+
|
|
104
|
+
contract_schema.types.each do |name, dry_type|
|
|
105
|
+
name_s = name.to_s
|
|
106
|
+
prop_schema = nil
|
|
107
|
+
|
|
108
|
+
type_schema_builder.with_path(name_s) do
|
|
109
|
+
prop_schema = type_schema_builder.build_schema_for_type(dry_type,
|
|
110
|
+
type_schema_builder.constraints_for_current_path,)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
schema.add_property(name, prop_schema, required: root_required.include?(name_s))
|
|
105
114
|
end
|
|
106
115
|
|
|
107
116
|
# Use canonical_name as registry key for schema objects (they don't have unique classes),
|
|
@@ -68,15 +68,27 @@ module GrapeOAS
|
|
|
68
68
|
def build_child_only_schema(parent_contract, type_schema_builder)
|
|
69
69
|
child_schema = ApiModel::Schema.new(type: Constants::SchemaTypes::OBJECT)
|
|
70
70
|
parent_keys = parent_contract_types(parent_contract)
|
|
71
|
-
|
|
71
|
+
contract_schema = @contract_resolver.contract_schema
|
|
72
72
|
|
|
73
|
-
|
|
73
|
+
constraints_by_path, required_by_object_path =
|
|
74
|
+
RuleIndex.build(contract_schema)
|
|
75
|
+
|
|
76
|
+
type_schema_builder.configure_path_aware_mode(constraints_by_path, required_by_object_path)
|
|
77
|
+
root_required = required_by_object_path.fetch("", [])
|
|
78
|
+
|
|
79
|
+
contract_schema.types.each do |name, dry_type|
|
|
74
80
|
# Skip inherited properties
|
|
75
81
|
next if parent_keys.include?(name.to_s)
|
|
76
82
|
|
|
77
|
-
|
|
78
|
-
prop_schema =
|
|
79
|
-
|
|
83
|
+
name_s = name.to_s
|
|
84
|
+
prop_schema = nil
|
|
85
|
+
|
|
86
|
+
type_schema_builder.with_path(name_s) do
|
|
87
|
+
prop_schema = type_schema_builder.build_schema_for_type(dry_type,
|
|
88
|
+
type_schema_builder.constraints_for_current_path,)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
child_schema.add_property(name, prop_schema, required: root_required.include?(name_s))
|
|
80
92
|
end
|
|
81
93
|
|
|
82
94
|
child_schema
|
|
@@ -97,10 +97,17 @@ module GrapeOAS
|
|
|
97
97
|
end
|
|
98
98
|
|
|
99
99
|
def handle_size(name, args)
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
100
|
+
rng = ArgumentExtractor.extract_range(args.first)
|
|
101
|
+
|
|
102
|
+
if rng
|
|
103
|
+
constraints.min_size = rng.begin if rng.begin
|
|
104
|
+
constraints.max_size = rng.max if rng.end
|
|
105
|
+
else
|
|
106
|
+
min_val = ArgumentExtractor.extract_numeric(args[0])
|
|
107
|
+
max_val = ArgumentExtractor.extract_numeric(args[1]) if name == :size?
|
|
108
|
+
constraints.min_size = min_val if min_val
|
|
109
|
+
constraints.max_size = max_val if max_val
|
|
110
|
+
end
|
|
104
111
|
end
|
|
105
112
|
|
|
106
113
|
def handle_range(args)
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "ast_walker"
|
|
4
|
+
require_relative "constraint_extractor"
|
|
5
|
+
require_relative "constraint_merger"
|
|
6
|
+
|
|
7
|
+
module GrapeOAS
|
|
8
|
+
module Introspectors
|
|
9
|
+
module DryIntrospectorSupport
|
|
10
|
+
# Builds path-aware constraint and required field indexes from dry-schema AST
|
|
11
|
+
class RuleIndex
|
|
12
|
+
def initialize(contract_schema)
|
|
13
|
+
@walker = AstWalker.new(ConstraintExtractor::ConstraintSet)
|
|
14
|
+
@merger = ConstraintMerger
|
|
15
|
+
@constraints_by_path = {}
|
|
16
|
+
@required_by_object_path = Hash.new { |h, k| h[k] = {} }
|
|
17
|
+
|
|
18
|
+
build_indexes(contract_schema)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def self.build(contract_schema)
|
|
22
|
+
new(contract_schema).to_a
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def to_a
|
|
26
|
+
[@constraints_by_path, @required_by_object_path.transform_values(&:keys)]
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def build_indexes(contract_schema)
|
|
32
|
+
rules = contract_schema.respond_to?(:rules) ? contract_schema.rules : {}
|
|
33
|
+
rules.each_value do |rule|
|
|
34
|
+
ast = rule.respond_to?(:to_ast) ? rule.to_ast : rule
|
|
35
|
+
collect_constraints(ast, [])
|
|
36
|
+
collect_required(ast, [], in_implication_condition: false)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def collect_constraints(ast, path)
|
|
41
|
+
return unless ast.is_a?(Array)
|
|
42
|
+
|
|
43
|
+
case ast[0]
|
|
44
|
+
when :key
|
|
45
|
+
key_name, value_ast = parse_key_node(ast)
|
|
46
|
+
return unless key_name && value_ast.is_a?(Array)
|
|
47
|
+
|
|
48
|
+
new_path = path + [key_name]
|
|
49
|
+
apply_node_constraints(value_ast, new_path)
|
|
50
|
+
collect_constraints(value_ast, new_path)
|
|
51
|
+
|
|
52
|
+
when :each
|
|
53
|
+
child = ast[1]
|
|
54
|
+
return unless child.is_a?(Array)
|
|
55
|
+
|
|
56
|
+
item_path = path + ["[]"]
|
|
57
|
+
|
|
58
|
+
# Index constraints that apply to the item schema itself
|
|
59
|
+
apply_node_constraints(child, item_path)
|
|
60
|
+
|
|
61
|
+
# Recurse so nested keys inside the item get their own paths
|
|
62
|
+
collect_constraints(child, item_path)
|
|
63
|
+
else
|
|
64
|
+
ast.each { |child| collect_constraints(child, path) if child.is_a?(Array) }
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def collect_required(ast, object_path, in_implication_condition:)
|
|
69
|
+
return unless ast.is_a?(Array)
|
|
70
|
+
|
|
71
|
+
case ast[0]
|
|
72
|
+
when :implication
|
|
73
|
+
left, right = ast[1].is_a?(Array) ? ast[1] : [nil, nil]
|
|
74
|
+
collect_required(left, object_path, in_implication_condition: true) if left
|
|
75
|
+
collect_required(right, object_path, in_implication_condition: false) if right
|
|
76
|
+
|
|
77
|
+
when :predicate
|
|
78
|
+
mark_required_if_key_predicate(ast[1], object_path) unless in_implication_condition
|
|
79
|
+
|
|
80
|
+
when :key, :each
|
|
81
|
+
if ast[0] == :key
|
|
82
|
+
key_name, value_ast = parse_key_node(ast)
|
|
83
|
+
if key_name && value_ast.is_a?(Array)
|
|
84
|
+
collect_required(value_ast, object_path + [key_name],
|
|
85
|
+
in_implication_condition: in_implication_condition,)
|
|
86
|
+
end
|
|
87
|
+
elsif ast[1] # :each
|
|
88
|
+
collect_required(ast[1], object_path + ["[]"],
|
|
89
|
+
in_implication_condition: in_implication_condition,)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
else
|
|
93
|
+
ast.each do |child|
|
|
94
|
+
collect_required(child, object_path, in_implication_condition: in_implication_condition) if child.is_a?(Array)
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def parse_key_node(ast)
|
|
100
|
+
info = ast[1]
|
|
101
|
+
return [nil, nil] unless info.is_a?(Array) && info.any?
|
|
102
|
+
|
|
103
|
+
key_name = info[0]
|
|
104
|
+
value_ast = info[1] || info[-1]
|
|
105
|
+
[key_name&.to_s, value_ast]
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def apply_node_constraints(value_ast, path)
|
|
109
|
+
pruned = prune_nested_validations(value_ast)
|
|
110
|
+
return unless pruned
|
|
111
|
+
|
|
112
|
+
constraints = @walker.walk(pruned)
|
|
113
|
+
constraints.required = nil if constraints.respond_to?(:required=)
|
|
114
|
+
|
|
115
|
+
path_key = path.join("/")
|
|
116
|
+
if @constraints_by_path.key?(path_key)
|
|
117
|
+
@merger.merge(@constraints_by_path[path_key], constraints)
|
|
118
|
+
else
|
|
119
|
+
@constraints_by_path[path_key] = constraints
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def prune_nested_validations(ast)
|
|
124
|
+
return ast unless ast.is_a?(Array)
|
|
125
|
+
|
|
126
|
+
tag = ast[0]
|
|
127
|
+
return ast unless tag.is_a?(Symbol)
|
|
128
|
+
|
|
129
|
+
case tag
|
|
130
|
+
when :each, :key
|
|
131
|
+
nil
|
|
132
|
+
|
|
133
|
+
when :set
|
|
134
|
+
children, wrapped = extract_children(ast)
|
|
135
|
+
pruned = children.filter_map { |c| c.is_a?(Array) ? prune_nested_validations(c) : c }
|
|
136
|
+
return nil if pruned.empty?
|
|
137
|
+
|
|
138
|
+
# rewrite set -> and, preserve wrapper style
|
|
139
|
+
wrapped ? [:and, pruned] : [:and, *pruned]
|
|
140
|
+
|
|
141
|
+
when :and, :or, :rule
|
|
142
|
+
children, wrapped = extract_children(ast)
|
|
143
|
+
pruned = children.filter_map { |c| c.is_a?(Array) ? prune_nested_validations(c) : c }
|
|
144
|
+
return nil if pruned.empty?
|
|
145
|
+
|
|
146
|
+
wrapped ? [tag, pruned] : [tag, *pruned]
|
|
147
|
+
|
|
148
|
+
when :implication
|
|
149
|
+
pair = ast[1]
|
|
150
|
+
return ast unless pair.is_a?(Array) && pair.size >= 2
|
|
151
|
+
|
|
152
|
+
left = pair[0].is_a?(Array) ? prune_nested_validations(pair[0]) : pair[0]
|
|
153
|
+
right = pair[1].is_a?(Array) ? prune_nested_validations(pair[1]) : pair[1]
|
|
154
|
+
[:implication, [left, right]]
|
|
155
|
+
|
|
156
|
+
when :not
|
|
157
|
+
child = ast[1]
|
|
158
|
+
child = prune_nested_validations(child) if child.is_a?(Array)
|
|
159
|
+
[:not, child]
|
|
160
|
+
|
|
161
|
+
else
|
|
162
|
+
ast
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def extract_children(ast)
|
|
167
|
+
# handles both shapes:
|
|
168
|
+
# [:and, [node1, node2]]
|
|
169
|
+
# [:and, node1, node2]
|
|
170
|
+
payload = ast[1]
|
|
171
|
+
|
|
172
|
+
if payload.is_a?(Array) && !payload.empty? && payload.all? { |x| x.is_a?(Array) && x[0].is_a?(Symbol) }
|
|
173
|
+
[payload, true] # wrapped list
|
|
174
|
+
else
|
|
175
|
+
[ast[1..], false] # splatted
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def mark_required_if_key_predicate(pred, object_path)
|
|
180
|
+
return unless pred.is_a?(Array) && pred[0] == :key?
|
|
181
|
+
|
|
182
|
+
name = extract_key_name(pred)
|
|
183
|
+
@required_by_object_path[object_path.join("/")][name] = true if name
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def extract_key_name(pred_node)
|
|
187
|
+
args = pred_node[1]
|
|
188
|
+
return nil unless args.is_a?(Array)
|
|
189
|
+
|
|
190
|
+
name_pair = args.find { |x| x.is_a?(Array) && x[0] == :name }
|
|
191
|
+
name_pair&.dig(1)&.to_s
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
end
|
|
@@ -11,7 +11,39 @@ module GrapeOAS
|
|
|
11
11
|
ConstraintSet = ConstraintExtractor::ConstraintSet
|
|
12
12
|
|
|
13
13
|
def initialize
|
|
14
|
-
|
|
14
|
+
@path_stack = []
|
|
15
|
+
@constraints_by_path = nil
|
|
16
|
+
@required_by_object_path = nil
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def configure_path_aware_mode(constraints_by_path, required_by_object_path)
|
|
20
|
+
@path_stack = []
|
|
21
|
+
@constraints_by_path = constraints_by_path
|
|
22
|
+
@required_by_object_path = required_by_object_path
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def with_path(part)
|
|
26
|
+
@path_stack << part
|
|
27
|
+
yield
|
|
28
|
+
ensure
|
|
29
|
+
@path_stack.pop
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def current_object_path
|
|
33
|
+
@path_stack.join("/")
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def constraints_for_current_path
|
|
37
|
+
return nil unless @constraints_by_path
|
|
38
|
+
|
|
39
|
+
@constraints_by_path[current_object_path]
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def required_keys_for_current_object
|
|
43
|
+
return nil unless @required_by_object_path
|
|
44
|
+
|
|
45
|
+
# In path-aware mode we rely entirely on rule-index requiredness
|
|
46
|
+
@required_by_object_path[current_object_path] || []
|
|
15
47
|
end
|
|
16
48
|
|
|
17
49
|
# Builds a schema for a Dry type.
|
|
@@ -20,7 +52,7 @@ module GrapeOAS
|
|
|
20
52
|
# @param constraints [ConstraintSet, nil] extracted constraints
|
|
21
53
|
# @return [ApiModel::Schema] the built schema
|
|
22
54
|
def build_schema_for_type(dry_type, constraints = nil)
|
|
23
|
-
constraints ||= ConstraintSet.new(unhandled_predicates: [])
|
|
55
|
+
constraints ||= constraints_for_current_path || ConstraintSet.new(unhandled_predicates: [])
|
|
24
56
|
meta = dry_type.respond_to?(:meta) ? dry_type.meta : {}
|
|
25
57
|
|
|
26
58
|
# Check for Sum type first (TypeA | TypeB) -> anyOf
|
|
@@ -29,6 +61,10 @@ module GrapeOAS
|
|
|
29
61
|
# Check for Hash schema type (nested schemas like .hash(SomeSchema))
|
|
30
62
|
return build_hash_schema(dry_type) if hash_schema_type?(dry_type)
|
|
31
63
|
|
|
64
|
+
# Check for object schema (unwrapped hash with keys)
|
|
65
|
+
unwrapped = TypeUnwrapper.unwrap(dry_type)
|
|
66
|
+
return build_object_schema(unwrapped) if unwrapped.respond_to?(:keys)
|
|
67
|
+
|
|
32
68
|
primitive, member = TypeUnwrapper.derive_primitive_and_member(dry_type)
|
|
33
69
|
enum_vals = extract_enum_from_type(dry_type)
|
|
34
70
|
|
|
@@ -59,6 +95,36 @@ module GrapeOAS
|
|
|
59
95
|
|
|
60
96
|
private
|
|
61
97
|
|
|
98
|
+
def build_object_schema(unwrapped_schema_type)
|
|
99
|
+
schema = ApiModel::Schema.new(type: Constants::SchemaTypes::OBJECT)
|
|
100
|
+
required_keys = required_keys_for_current_object
|
|
101
|
+
|
|
102
|
+
# Dry::Types::Schema does not have each_key, so we disable the cop here
|
|
103
|
+
unwrapped_schema_type.keys.each do |key| # rubocop:disable Style/HashEachMethods
|
|
104
|
+
key_name = key.respond_to?(:name) ? key.name.to_s : key.to_s
|
|
105
|
+
key_type = key.respond_to?(:type) ? key.type : nil
|
|
106
|
+
|
|
107
|
+
prop_schema = nil
|
|
108
|
+
with_path(key_name) do
|
|
109
|
+
prop_schema = if key_type
|
|
110
|
+
build_schema_for_type(key_type, constraints_for_current_path)
|
|
111
|
+
else
|
|
112
|
+
default_string_schema
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
is_required = if required_keys
|
|
117
|
+
required_keys.include?(key_name)
|
|
118
|
+
else
|
|
119
|
+
key.respond_to?(:required?) ? key.required? : false
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
schema.add_property(key_name, prop_schema, required: is_required)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
schema
|
|
126
|
+
end
|
|
127
|
+
|
|
62
128
|
def build_any_of_schema(sum_type)
|
|
63
129
|
types = TypeUnwrapper.extract_sum_types(sum_type)
|
|
64
130
|
|
|
@@ -86,29 +152,35 @@ module GrapeOAS
|
|
|
86
152
|
end
|
|
87
153
|
|
|
88
154
|
def build_hash_schema(dry_type)
|
|
89
|
-
schema = ApiModel::Schema.new(type: Constants::SchemaTypes::OBJECT)
|
|
90
155
|
unwrapped = TypeUnwrapper.unwrap(dry_type)
|
|
91
156
|
|
|
92
|
-
|
|
157
|
+
# Delegate to the same path-aware logic as regular object schemas.
|
|
158
|
+
# This ensures nested rule constraints (e.g. max_size?, gteq?, format?) are applied
|
|
159
|
+
# to properties inside `.hash do ... end` blocks.
|
|
160
|
+
return ApiModel::Schema.new(type: Constants::SchemaTypes::OBJECT) unless unwrapped.respond_to?(:keys)
|
|
93
161
|
|
|
94
|
-
|
|
95
|
-
schema_keys = unwrapped.keys
|
|
96
|
-
schema_keys.each do |key|
|
|
97
|
-
key_name = key.respond_to?(:name) ? key.name.to_s : key.to_s
|
|
98
|
-
key_type = key.respond_to?(:type) ? key.type : nil
|
|
99
|
-
|
|
100
|
-
prop_schema = key_type ? build_schema_for_type(key_type) : default_string_schema
|
|
101
|
-
req = key.respond_to?(:required?) ? key.required? : true
|
|
102
|
-
schema.add_property(key_name, prop_schema, required: req)
|
|
103
|
-
end
|
|
104
|
-
|
|
105
|
-
schema
|
|
162
|
+
build_object_schema(unwrapped)
|
|
106
163
|
end
|
|
107
164
|
|
|
108
165
|
def build_base_schema(primitive, member)
|
|
109
166
|
if primitive == Array
|
|
110
|
-
items_schema =
|
|
167
|
+
items_schema = nil
|
|
168
|
+
|
|
169
|
+
with_path("[]") do
|
|
170
|
+
if member
|
|
171
|
+
unwrapped = TypeUnwrapper.unwrap(member)
|
|
172
|
+
items_schema = if unwrapped.respond_to?(:keys)
|
|
173
|
+
build_object_schema(unwrapped)
|
|
174
|
+
else
|
|
175
|
+
build_schema_for_type(member, constraints_for_current_path)
|
|
176
|
+
end
|
|
177
|
+
else
|
|
178
|
+
items_schema = default_string_schema
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
|
|
111
182
|
ApiModel::Schema.new(type: Constants::SchemaTypes::ARRAY, items: items_schema)
|
|
183
|
+
|
|
112
184
|
else
|
|
113
185
|
build_schema_for_primitive(primitive)
|
|
114
186
|
end
|
|
@@ -1,13 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative "base"
|
|
4
|
-
require_relative "../api_model_builders/concerns/type_resolver"
|
|
5
|
-
require_relative "entity_introspector_support/cycle_tracker"
|
|
6
|
-
require_relative "entity_introspector_support/discriminator_handler"
|
|
7
|
-
require_relative "entity_introspector_support/inheritance_builder"
|
|
8
|
-
require_relative "entity_introspector_support/property_extractor"
|
|
9
|
-
require_relative "entity_introspector_support/exposure_processor"
|
|
10
|
-
|
|
11
3
|
module GrapeOAS
|
|
12
4
|
module Introspectors
|
|
13
5
|
# Introspector for Grape::Entity classes.
|
data/lib/grape_oas/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: grape-oas
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.0.
|
|
4
|
+
version: 1.0.3
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Andrei Subbota
|
|
8
|
-
autorequire:
|
|
9
8
|
bindir: bin
|
|
10
9
|
cert_chain: []
|
|
11
|
-
date:
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
12
11
|
dependencies:
|
|
13
12
|
- !ruby/object:Gem::Dependency
|
|
14
13
|
name: grape
|
|
@@ -114,6 +113,7 @@ files:
|
|
|
114
113
|
- lib/grape_oas/introspectors/dry_introspector_support/contract_resolver.rb
|
|
115
114
|
- lib/grape_oas/introspectors/dry_introspector_support/inheritance_handler.rb
|
|
116
115
|
- lib/grape_oas/introspectors/dry_introspector_support/predicate_handler.rb
|
|
116
|
+
- lib/grape_oas/introspectors/dry_introspector_support/rule_index.rb
|
|
117
117
|
- lib/grape_oas/introspectors/dry_introspector_support/type_schema_builder.rb
|
|
118
118
|
- lib/grape_oas/introspectors/dry_introspector_support/type_unwrapper.rb
|
|
119
119
|
- lib/grape_oas/introspectors/entity_introspector.rb
|
|
@@ -136,7 +136,6 @@ metadata:
|
|
|
136
136
|
documentation_uri: https://github.com/numbata/grape-oas#readme
|
|
137
137
|
bug_tracker_uri: https://github.com/numbata/grape-oas/issues
|
|
138
138
|
rubygems_mfa_required: 'true'
|
|
139
|
-
post_install_message:
|
|
140
139
|
rdoc_options: []
|
|
141
140
|
require_paths:
|
|
142
141
|
- lib
|
|
@@ -151,8 +150,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
151
150
|
- !ruby/object:Gem::Version
|
|
152
151
|
version: '0'
|
|
153
152
|
requirements: []
|
|
154
|
-
rubygems_version: 3.
|
|
155
|
-
signing_key:
|
|
153
|
+
rubygems_version: 3.6.8
|
|
156
154
|
specification_version: 4
|
|
157
155
|
summary: OpenAPI (Swagger) v2 and v3 documentation for Grape APIs
|
|
158
156
|
test_files: []
|