taro 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 +21 -0
- data/README.md +1 -2
- data/lib/taro/export/open_api_v3.rb +39 -6
- data/lib/taro/rails/declaration.rb +1 -21
- data/lib/taro/rails/normalized_route.rb +13 -1
- data/lib/taro/types/scalar/iso8601_date_type.rb +2 -3
- data/lib/taro/types/scalar/iso8601_datetime_type.rb +2 -3
- data/lib/taro/types/scalar/string_type.rb +17 -6
- data/lib/taro/types/scalar/uuid_v4_type.rb +2 -20
- data/lib/taro/types/scalar_type.rb +1 -0
- data/lib/taro/types/shared/custom_field_resolvers.rb +2 -2
- data/lib/taro/types/shared/pattern.rb +69 -0
- data/lib/taro/version.rb +1 -1
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d30f413c950e02e52f99ed96d234acdba122c92ec4e39dd2d35e87c899ed8a13
|
4
|
+
data.tar.gz: 5ed455623397ce6fbcb19412710bf98bc7ca2b6898db193a2cf858104a149327
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ba99529dd914238371708a80b5f3f6497a4884a042a0e11ec7e9b557107379d0bfa2232b6a71137623658651e5d920f8ad47ab683119ae5ce105261fa627b980
|
7
|
+
data.tar.gz: 26b2652184c485aa7970707373cfe4ebfdecd3141fc7e995066060b5a0a5452b1e749d6e16fdc3d99e34a199434ed9edc7400d89fe8f8ed32df442d764d06175
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,26 @@
|
|
1
1
|
## [Unreleased]
|
2
2
|
|
3
|
+
## [1.4.0] - 2024-11-27
|
4
|
+
|
5
|
+
### Added
|
6
|
+
|
7
|
+
- added operationId to OpenAPI export
|
8
|
+
|
9
|
+
### Fixed
|
10
|
+
|
11
|
+
- fixed potential ref name clashes in OpenAPI export
|
12
|
+
- e.g. `FooBar::BazController` & `Foo::BarBazController`
|
13
|
+
|
14
|
+
## [1.3.0] - 2024-11-25
|
15
|
+
|
16
|
+
### Added
|
17
|
+
|
18
|
+
- Support for string patterns (on StringType children and in export)
|
19
|
+
|
20
|
+
### Fixed
|
21
|
+
|
22
|
+
- Fixed OpenAPI export of params with enum (inline & type-based)
|
23
|
+
|
3
24
|
## [1.2.0] - 2024-11-18
|
4
25
|
|
5
26
|
### Added
|
data/README.md
CHANGED
@@ -305,8 +305,7 @@ end
|
|
305
305
|
- mixed enums
|
306
306
|
- nullable enums
|
307
307
|
- string format specifications (e.g. binary, int64, password ...)
|
308
|
-
- string pattern
|
309
|
-
- string minLength and maxLength
|
308
|
+
- string minLength and maxLength (substitute: `self.pattern = /\A.{3,5}\z/`)
|
310
309
|
- number minimum, exclusiveMinimum, maximum, multipleOf
|
311
310
|
- readOnly, writeOnly
|
312
311
|
|
@@ -31,9 +31,10 @@ class Taro::Export::OpenAPIv3 < Taro::Export::Base # rubocop:disable Metrics/Cla
|
|
31
31
|
description: declaration.desc,
|
32
32
|
summary: declaration.summary,
|
33
33
|
tags: declaration.tags,
|
34
|
+
operationId: route.openapi_operation_id,
|
34
35
|
parameters: route_parameters(declaration, route),
|
35
36
|
requestBody: request_body(declaration, route),
|
36
|
-
responses: responses(declaration),
|
37
|
+
responses: responses(declaration, route),
|
37
38
|
}.compact,
|
38
39
|
}
|
39
40
|
end
|
@@ -64,15 +65,22 @@ class Taro::Export::OpenAPIv3 < Taro::Export::Base # rubocop:disable Metrics/Cla
|
|
64
65
|
end
|
65
66
|
|
66
67
|
def export_parameter(field)
|
68
|
+
validate_path_or_query_parameter(field)
|
69
|
+
|
67
70
|
{
|
68
71
|
name: field.name,
|
69
72
|
deprecated: field.deprecated,
|
70
73
|
description: field.desc,
|
71
74
|
required: !field.null,
|
72
|
-
schema:
|
75
|
+
schema: export_field(field).except(:deprecated, :description),
|
73
76
|
}.compact
|
74
77
|
end
|
75
78
|
|
79
|
+
def validate_path_or_query_parameter(field)
|
80
|
+
[:array, :object].include?(field.type.openapi_type) &&
|
81
|
+
raise("Unexpected object as path/query param #{field.name}: #{field.type}")
|
82
|
+
end
|
83
|
+
|
76
84
|
def request_body(declaration, route)
|
77
85
|
return unless route.can_have_request_body?
|
78
86
|
|
@@ -84,10 +92,10 @@ class Taro::Export::OpenAPIv3 < Taro::Export::Base # rubocop:disable Metrics/Cla
|
|
84
92
|
|
85
93
|
body_input_type = Class.new(params)
|
86
94
|
body_input_type.fields.replace(body_param_fields)
|
87
|
-
body_input_type.openapi_name =
|
95
|
+
body_input_type.openapi_name = "#{route.openapi_operation_id}_Input"
|
88
96
|
|
89
97
|
# For polymorphic routes (more than one for the same declaration),
|
90
|
-
# we can't use refs because
|
98
|
+
# we can't use refs because their request body might differ:
|
91
99
|
# Different params might be in the path vs. in the request body.
|
92
100
|
use_refs = !declaration.polymorphic_route?
|
93
101
|
schema = request_body_schema(body_input_type, use_refs:)
|
@@ -102,7 +110,9 @@ class Taro::Export::OpenAPIv3 < Taro::Export::Base # rubocop:disable Metrics/Cla
|
|
102
110
|
end
|
103
111
|
end
|
104
112
|
|
105
|
-
def responses(declaration)
|
113
|
+
def responses(declaration, route)
|
114
|
+
name_anonymous_return_types(declaration, route)
|
115
|
+
|
106
116
|
declaration.returns.sort.to_h do |code, type|
|
107
117
|
[
|
108
118
|
code.to_s,
|
@@ -114,14 +124,26 @@ class Taro::Export::OpenAPIv3 < Taro::Export::Base # rubocop:disable Metrics/Cla
|
|
114
124
|
end
|
115
125
|
end
|
116
126
|
|
127
|
+
def name_anonymous_return_types(declaration, route)
|
128
|
+
declaration.returns.each do |code, type|
|
129
|
+
next if type.openapi_name?
|
130
|
+
|
131
|
+
type.openapi_name = "#{route.openapi_operation_id}_#{code}_Response"
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
117
135
|
def export_type(type)
|
118
|
-
if type < Taro::Types::ScalarType
|
136
|
+
if type < Taro::Types::ScalarType && !custom_scalar_type?(type)
|
119
137
|
{ type: type.openapi_type }
|
120
138
|
else
|
121
139
|
extract_component_ref(type)
|
122
140
|
end
|
123
141
|
end
|
124
142
|
|
143
|
+
def custom_scalar_type?(type)
|
144
|
+
type < Taro::Types::ScalarType && (type.openapi_pattern || type.deprecated)
|
145
|
+
end
|
146
|
+
|
125
147
|
def export_field(field)
|
126
148
|
if field.type < Taro::Types::ScalarType
|
127
149
|
export_scalar_field(field)
|
@@ -174,6 +196,8 @@ class Taro::Export::OpenAPIv3 < Taro::Export::Base # rubocop:disable Metrics/Cla
|
|
174
196
|
enum_type_details(type)
|
175
197
|
elsif type < Taro::Types::ListType
|
176
198
|
list_type_details(type)
|
199
|
+
elsif custom_scalar_type?(type)
|
200
|
+
custom_scalar_type_details(type)
|
177
201
|
else
|
178
202
|
raise NotImplementedError, "Unsupported type: #{type}"
|
179
203
|
end
|
@@ -209,6 +233,15 @@ class Taro::Export::OpenAPIv3 < Taro::Export::Base # rubocop:disable Metrics/Cla
|
|
209
233
|
}.compact
|
210
234
|
end
|
211
235
|
|
236
|
+
def custom_scalar_type_details(scalar)
|
237
|
+
{
|
238
|
+
type: scalar.openapi_type,
|
239
|
+
deprecated: scalar.deprecated,
|
240
|
+
description: scalar.desc,
|
241
|
+
pattern: scalar.openapi_pattern,
|
242
|
+
}.compact
|
243
|
+
end
|
244
|
+
|
212
245
|
def assert_unique_openapi_name(type)
|
213
246
|
@name_to_type_map ||= {}
|
214
247
|
if (prev = @name_to_type_map[type.openapi_name]) && type != prev
|
@@ -53,11 +53,6 @@ class Taro::Rails::Declaration
|
|
53
53
|
end
|
54
54
|
|
55
55
|
def finalize(controller_class:, action_name:)
|
56
|
-
add_routes(controller_class:, action_name:)
|
57
|
-
add_openapi_names(controller_class:, action_name:)
|
58
|
-
end
|
59
|
-
|
60
|
-
def add_routes(controller_class:, action_name:)
|
61
56
|
routes = Taro::Rails::RouteFinder.call(controller_class:, action_name:)
|
62
57
|
routes.any? || raise_missing_route(controller_class, action_name)
|
63
58
|
self.routes = routes
|
@@ -72,21 +67,6 @@ class Taro::Rails::Declaration
|
|
72
67
|
routes.size > 1
|
73
68
|
end
|
74
69
|
|
75
|
-
# TODO: these change when the controller class is renamed.
|
76
|
-
# We might need a way to set `base`. Perhaps as a kwarg to `::api`?
|
77
|
-
def add_openapi_names(controller_class:, action_name:)
|
78
|
-
base = "#{controller_class.name.chomp('Controller').gsub('::', '_')}_#{action_name}"
|
79
|
-
params.openapi_name = "#{base}_Input"
|
80
|
-
params.define_singleton_method(:name) { openapi_name }
|
81
|
-
|
82
|
-
returns.each do |status, return_type|
|
83
|
-
next if return_type.openapi_name? # only set for ad-hoc / nested return types
|
84
|
-
|
85
|
-
return_type.openapi_name = "#{base}_#{status}_Response"
|
86
|
-
return_type.define_singleton_method(:name) { openapi_name }
|
87
|
-
end
|
88
|
-
end
|
89
|
-
|
90
70
|
require 'rack'
|
91
71
|
def self.coerce_status_to_int(status)
|
92
72
|
# support using http status numbers directly
|
@@ -137,6 +117,6 @@ class Taro::Rails::Declaration
|
|
137
117
|
end
|
138
118
|
|
139
119
|
def <=>(other)
|
140
|
-
|
120
|
+
routes.first.openapi_operation_id <=> other.routes.first.openapi_operation_id
|
141
121
|
end
|
142
122
|
end
|
@@ -18,15 +18,26 @@ Taro::Rails::NormalizedRoute = Data.define(:rails_route) do
|
|
18
18
|
rails_route.path.spec.to_s.gsub('(.:format)', '').gsub(/:(\w+)/, '{\1}')
|
19
19
|
end
|
20
20
|
|
21
|
+
def openapi_operation_id
|
22
|
+
"#{verb}_#{action}_#{controller.gsub('/', '__')}"
|
23
|
+
end
|
24
|
+
|
21
25
|
def path_params
|
22
26
|
openapi_path.scan(/{(\w+)}/).flatten.map(&:to_sym)
|
23
27
|
end
|
24
28
|
|
25
29
|
def endpoint
|
26
|
-
controller, action = rails_route.requirements.values_at(:controller, :action)
|
27
30
|
"#{controller}##{action}"
|
28
31
|
end
|
29
32
|
|
33
|
+
def action
|
34
|
+
rails_route.requirements[:action]
|
35
|
+
end
|
36
|
+
|
37
|
+
def controller
|
38
|
+
rails_route.requirements[:controller]
|
39
|
+
end
|
40
|
+
|
30
41
|
def can_have_request_body?
|
31
42
|
%w[patch post put].include?(verb)
|
32
43
|
end
|
@@ -34,4 +45,5 @@ Taro::Rails::NormalizedRoute = Data.define(:rails_route) do
|
|
34
45
|
def inspect
|
35
46
|
%(#<#{self.class} "#{verb} #{openapi_path}">)
|
36
47
|
end
|
48
|
+
alias to_s inspect
|
37
49
|
end
|
@@ -2,11 +2,10 @@ class Taro::Types::Scalar::ISO8601DateType < Taro::Types::ScalarType
|
|
2
2
|
self.desc = 'Represents a time as Date in ISO8601 format.'
|
3
3
|
self.openapi_name = 'ISO8601Date'
|
4
4
|
self.openapi_type = :string
|
5
|
-
|
6
|
-
PATTERN = /\A\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])\z/
|
5
|
+
self.pattern = /\A\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])\z/
|
7
6
|
|
8
7
|
def coerce_input
|
9
|
-
if object.instance_of?(String) && object.match?(
|
8
|
+
if object.instance_of?(String) && object.match?(pattern)
|
10
9
|
Date.parse(object)
|
11
10
|
else
|
12
11
|
input_error("must be a ISO8601 formatted string")
|
@@ -2,11 +2,10 @@ class Taro::Types::Scalar::ISO8601DateTimeType < Taro::Types::ScalarType
|
|
2
2
|
self.desc = 'Represents a time as DateTime in ISO8601 format.'
|
3
3
|
self.openapi_name = 'ISO8601DateTime'
|
4
4
|
self.openapi_type = :string
|
5
|
-
|
6
|
-
PATTERN = /\A\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])T([01]\d|2[0-3]):[0-5]\d:[0-5]\d(Z|[+-](0[0-9]|1[0-4]):[0-5]\d)\z/
|
5
|
+
self.pattern = /\A\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])T([01]\d|2[0-3]):[0-5]\d:[0-5]\d(Z|[+-](0[0-9]|1[0-4]):[0-5]\d)\z/
|
7
6
|
|
8
7
|
def coerce_input
|
9
|
-
if object.instance_of?(String) && object.match?(
|
8
|
+
if object.instance_of?(String) && object.match?(pattern)
|
10
9
|
DateTime.iso8601(object)
|
11
10
|
else
|
12
11
|
input_error("must be a ISO8601 formatted string")
|
@@ -2,14 +2,25 @@ class Taro::Types::Scalar::StringType < Taro::Types::ScalarType
|
|
2
2
|
self.openapi_type = :string
|
3
3
|
|
4
4
|
def coerce_input
|
5
|
-
object.instance_of?(String)
|
5
|
+
object.instance_of?(String) || input_error('must be a String')
|
6
|
+
|
7
|
+
pattern.nil? || pattern.match?(object) ||
|
8
|
+
input_error("must match pattern #{pattern.inspect}")
|
9
|
+
|
10
|
+
object
|
6
11
|
end
|
7
12
|
|
8
13
|
def coerce_response
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
+
str =
|
15
|
+
case object
|
16
|
+
when String then object
|
17
|
+
when Symbol then object.to_s
|
18
|
+
else response_error('must be a String or Symbol')
|
19
|
+
end
|
20
|
+
|
21
|
+
pattern.nil? || pattern.match?(str) ||
|
22
|
+
response_error("must match pattern #{pattern.inspect}")
|
23
|
+
|
24
|
+
str
|
14
25
|
end
|
15
26
|
end
|
@@ -1,23 +1,5 @@
|
|
1
|
-
class Taro::Types::Scalar::UUIDv4Type < Taro::Types::
|
1
|
+
class Taro::Types::Scalar::UUIDv4Type < Taro::Types::Scalar::StringType
|
2
2
|
self.desc = "A UUID v4 string"
|
3
3
|
self.openapi_name = 'UUIDv4'
|
4
|
-
self.
|
5
|
-
|
6
|
-
PATTERN = /\A\h{8}-?(?:\h{4}-?){3}\h{12}\z/
|
7
|
-
|
8
|
-
def coerce_input
|
9
|
-
if object.is_a?(String) && object.match?(PATTERN)
|
10
|
-
object
|
11
|
-
else
|
12
|
-
input_error("must be a UUID v4 string")
|
13
|
-
end
|
14
|
-
end
|
15
|
-
|
16
|
-
def coerce_response
|
17
|
-
if object.is_a?(String) && object.match?(PATTERN)
|
18
|
-
object
|
19
|
-
else
|
20
|
-
response_error("must be a UUID v4 string")
|
21
|
-
end
|
22
|
-
end
|
4
|
+
self.pattern = /\A\h{8}-?(?:\h{4}-?){3}\h{12}\z/
|
23
5
|
end
|
@@ -15,8 +15,8 @@ module Taro::Types::Shared::CustomFieldResolvers
|
|
15
15
|
end
|
16
16
|
|
17
17
|
def method_added(name)
|
18
|
-
if
|
19
|
-
raise(Taro::ArgumentError,
|
18
|
+
if [:object, :pattern].include?(name)
|
19
|
+
raise(Taro::ArgumentError, "##{name} is a reserved, internally used method name")
|
20
20
|
elsif ![:coerce_input, :coerce_response].include?(name) &&
|
21
21
|
!self.name.to_s.start_with?('Taro::Types::')
|
22
22
|
custom_resolvers[name] = true
|
@@ -0,0 +1,69 @@
|
|
1
|
+
module Taro::Types::Shared::Pattern
|
2
|
+
def pattern
|
3
|
+
self.class.pattern
|
4
|
+
end
|
5
|
+
|
6
|
+
def self.included(base)
|
7
|
+
base.extend(ClassMethods)
|
8
|
+
end
|
9
|
+
|
10
|
+
module ClassMethods
|
11
|
+
attr_reader :pattern, :openapi_pattern
|
12
|
+
|
13
|
+
def pattern=(regexp)
|
14
|
+
openapi_type == :string ||
|
15
|
+
raise(Taro::RuntimeError, 'pattern requires openapi_type :string')
|
16
|
+
|
17
|
+
@pattern = regexp
|
18
|
+
@openapi_pattern = Taro::Types::Shared::Pattern.to_es262(regexp)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.to_es262(regexp)
|
23
|
+
validate(regexp).source.gsub(
|
24
|
+
/#{NOT_ESCAPED}\\[Ahz]/,
|
25
|
+
{ '\\A' => '^', '\\h' => '[0-9a-fA-F]', '\\z' => '$' }
|
26
|
+
)
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.validate(regexp)
|
30
|
+
validate_no_flags(regexp)
|
31
|
+
validate_not_empty(regexp)
|
32
|
+
validate_no_advanced_syntax(regexp)
|
33
|
+
regexp
|
34
|
+
end
|
35
|
+
|
36
|
+
def self.validate_no_flags(regexp)
|
37
|
+
(flags = regexp.inspect[%r{/\w+\z}]) &&
|
38
|
+
raise(Taro::ArgumentError, "pattern flags (#{flags}) are not supported")
|
39
|
+
end
|
40
|
+
|
41
|
+
def self.validate_not_empty(regexp)
|
42
|
+
regexp.source.empty? &&
|
43
|
+
raise(Taro::ArgumentError, 'pattern cannot be empty')
|
44
|
+
end
|
45
|
+
|
46
|
+
def self.validate_no_advanced_syntax(regexp)
|
47
|
+
return unless (match = regexp.source.match(ADVANCED_RUBY_REGEXP_SYNTAX_REGEXP))
|
48
|
+
|
49
|
+
feature = match.named_captures.find { |k, v| break k if v }
|
50
|
+
raise Taro::ArgumentError, <<~MSG
|
51
|
+
pattern uses non-JS syntax #{match} (#{feature}) at index #{match.begin(0)}
|
52
|
+
MSG
|
53
|
+
end
|
54
|
+
|
55
|
+
NOT_ESCAPED = /(?<!\\)(?:\\\\)*\K/
|
56
|
+
|
57
|
+
# This is not 100% accurate, e.g. /[?+]/ is a false positive, but it should be
|
58
|
+
# good enough so we don't need regexp_parser or js_regex as a dependency.
|
59
|
+
ADVANCED_RUBY_REGEXP_SYNTAX_REGEXP = /
|
60
|
+
#{NOT_ESCAPED}
|
61
|
+
(?:
|
62
|
+
(?<a special group or lookaround> \(\?[^:] )
|
63
|
+
| (?<a Ruby-specific escape> \\[a-zA-Z&&[^bBdDsSwWAzfhnrv]] )
|
64
|
+
| (?<an advanced quantifier> [?*+}][?+] )
|
65
|
+
| (?<a nested set> \[[^\]]*(?<!\\)\[ )
|
66
|
+
| (?<a set intersection> && )
|
67
|
+
)
|
68
|
+
/x
|
69
|
+
end
|
data/lib/taro/version.rb
CHANGED
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: taro
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.4.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Janosch Müller
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: exe
|
11
11
|
cert_chain: []
|
12
|
-
date: 2024-11-
|
12
|
+
date: 2024-11-27 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: rack
|
@@ -99,6 +99,7 @@ files:
|
|
99
99
|
- lib/taro/types/shared/object_coercion.rb
|
100
100
|
- lib/taro/types/shared/openapi_name.rb
|
101
101
|
- lib/taro/types/shared/openapi_type.rb
|
102
|
+
- lib/taro/types/shared/pattern.rb
|
102
103
|
- lib/taro/types/shared/rendering.rb
|
103
104
|
- lib/taro/version.rb
|
104
105
|
- tasks/benchmark.rake
|