taro 1.2.0 → 1.4.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5007f07dcb3230a1a45011138c19cc0f233e0a0e0fa81d64d554ee3d5cc4a82e
4
- data.tar.gz: 99dde57ec71bb2724a29005e644a0bb23d642df11fe4555bd9203340169814df
3
+ metadata.gz: d30f413c950e02e52f99ed96d234acdba122c92ec4e39dd2d35e87c899ed8a13
4
+ data.tar.gz: 5ed455623397ce6fbcb19412710bf98bc7ca2b6898db193a2cf858104a149327
5
5
  SHA512:
6
- metadata.gz: abb5fb481da01a4a50b19e891c3373b42372c97993788ed24c51b07146e087e25d040bc4ea077606a14013633b30f6254f6ced2e939ba9b039b341d95f918211
7
- data.tar.gz: b5dd6c2222598ad3d8ee64c64fd0fc6c40299c3f071b08317d17aa29c3f786924ae4b064f587b1e548ba102e0cc802b8a5adf7ccbadf033795974bfdaae5329a
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 specifications
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: { type: field.openapi_type },
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 = params.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 they request body might differ.
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
- params.openapi_name <=> other.params.openapi_name
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?(PATTERN)
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?(PATTERN)
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) ? object : input_error('must be a 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
- case object
10
- when String then object
11
- when Symbol then object.to_s
12
- else response_error('must be a String or Symbol')
13
- end
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::ScalarType
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.openapi_type = :string
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
@@ -1,5 +1,6 @@
1
1
  # Abstract base class for scalar types, i.e. types without fields.
2
2
  class Taro::Types::ScalarType < Taro::Types::BaseType
3
+ include Taro::Types::Shared::Pattern
3
4
  end
4
5
 
5
6
  module Taro::Types::Scalar
@@ -15,8 +15,8 @@ module Taro::Types::Shared::CustomFieldResolvers
15
15
  end
16
16
 
17
17
  def method_added(name)
18
- if name == :object
19
- raise(Taro::ArgumentError, '#object is a reserved, internally used method name')
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
@@ -1,4 +1,4 @@
1
1
  # :nocov:
2
2
  module Taro
3
- VERSION = "1.2.0"
3
+ VERSION = "1.4.0"
4
4
  end
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.2.0
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-18 00:00:00.000000000 Z
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