taro 1.2.0 → 1.3.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 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