taro 1.2.0 → 1.3.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: 41f14035882a39e03a7bae9c799a42c9fa63414e4201fbbab7f876d54d8d214b
4
+ data.tar.gz: 768b505506f8ad58792d0d86b6152f1aa60da98419aef111b9fc187ee74eab42
5
5
  SHA512:
6
- metadata.gz: abb5fb481da01a4a50b19e891c3373b42372c97993788ed24c51b07146e087e25d040bc4ea077606a14013633b30f6254f6ced2e939ba9b039b341d95f918211
7
- data.tar.gz: b5dd6c2222598ad3d8ee64c64fd0fc6c40299c3f071b08317d17aa29c3f786924ae4b064f587b1e548ba102e0cc802b8a5adf7ccbadf033795974bfdaae5329a
6
+ metadata.gz: 27607c95ed9c5a7ab2c4d579dec41357657f24c694f43eac06b15677a82e491fd5986c5cad8b7bf999ee23ee456d75cc2c90debee47e93d02cdc901b4e45f498
7
+ data.tar.gz: f99bfdfe66f7a09ac1746c23e2671bcec7f20cfee23e980db76561f14e1ea082bc2fcc59e4650af8732d7e608a8b7c012473dc0f862bb15ff9d89501a641c825
data/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [1.3.0] - 2024-11-25
4
+
5
+ ### Added
6
+
7
+ - Support for string patterns (on StringType children and in export)
8
+
9
+ ### Fixed
10
+
11
+ - Fixed OpenAPI export of params with enum (inline & type-based)
12
+
3
13
  ## [1.2.0] - 2024-11-18
4
14
 
5
15
  ### 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
 
@@ -64,15 +64,22 @@ class Taro::Export::OpenAPIv3 < Taro::Export::Base # rubocop:disable Metrics/Cla
64
64
  end
65
65
 
66
66
  def export_parameter(field)
67
+ validate_path_or_query_parameter(field)
68
+
67
69
  {
68
70
  name: field.name,
69
71
  deprecated: field.deprecated,
70
72
  description: field.desc,
71
73
  required: !field.null,
72
- schema: { type: field.openapi_type },
74
+ schema: export_field(field).except(:deprecated, :description),
73
75
  }.compact
74
76
  end
75
77
 
78
+ def validate_path_or_query_parameter(field)
79
+ [:array, :object].include?(field.type.openapi_type) &&
80
+ raise("Unexpected object as path/query param #{field.name}: #{field.type}")
81
+ end
82
+
76
83
  def request_body(declaration, route)
77
84
  return unless route.can_have_request_body?
78
85
 
@@ -87,7 +94,7 @@ class Taro::Export::OpenAPIv3 < Taro::Export::Base # rubocop:disable Metrics/Cla
87
94
  body_input_type.openapi_name = params.openapi_name
88
95
 
89
96
  # For polymorphic routes (more than one for the same declaration),
90
- # we can't use refs because they request body might differ.
97
+ # we can't use refs because their request body might differ:
91
98
  # Different params might be in the path vs. in the request body.
92
99
  use_refs = !declaration.polymorphic_route?
93
100
  schema = request_body_schema(body_input_type, use_refs:)
@@ -115,13 +122,17 @@ class Taro::Export::OpenAPIv3 < Taro::Export::Base # rubocop:disable Metrics/Cla
115
122
  end
116
123
 
117
124
  def export_type(type)
118
- if type < Taro::Types::ScalarType
125
+ if type < Taro::Types::ScalarType && !custom_scalar_type?(type)
119
126
  { type: type.openapi_type }
120
127
  else
121
128
  extract_component_ref(type)
122
129
  end
123
130
  end
124
131
 
132
+ def custom_scalar_type?(type)
133
+ type < Taro::Types::ScalarType && (type.openapi_pattern || type.deprecated)
134
+ end
135
+
125
136
  def export_field(field)
126
137
  if field.type < Taro::Types::ScalarType
127
138
  export_scalar_field(field)
@@ -174,6 +185,8 @@ class Taro::Export::OpenAPIv3 < Taro::Export::Base # rubocop:disable Metrics/Cla
174
185
  enum_type_details(type)
175
186
  elsif type < Taro::Types::ListType
176
187
  list_type_details(type)
188
+ elsif custom_scalar_type?(type)
189
+ custom_scalar_type_details(type)
177
190
  else
178
191
  raise NotImplementedError, "Unsupported type: #{type}"
179
192
  end
@@ -209,6 +222,15 @@ class Taro::Export::OpenAPIv3 < Taro::Export::Base # rubocop:disable Metrics/Cla
209
222
  }.compact
210
223
  end
211
224
 
225
+ def custom_scalar_type_details(scalar)
226
+ {
227
+ type: scalar.openapi_type,
228
+ deprecated: scalar.deprecated,
229
+ description: scalar.desc,
230
+ pattern: scalar.openapi_pattern,
231
+ }.compact
232
+ end
233
+
212
234
  def assert_unique_openapi_name(type)
213
235
  @name_to_type_map ||= {}
214
236
  if (prev = @name_to_type_map[type.openapi_name]) && type != prev
@@ -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.3.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.3.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-25 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