dry-types-tuple 0.1.2 → 0.3.1

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: 3ff4a6208f9f1d2b30e3956b6493cc13e20f095d86c5be454f201fbdea0316dc
4
- data.tar.gz: d4ec021dc5a952a6d238af76a05df9d573643169d1ada1e6ad4823fb63cfcd0b
3
+ metadata.gz: d045e436a8d045af55c65dd3278b67ef1c07fd90971ceea8a3fa2bf7e39279d9
4
+ data.tar.gz: 769f6744fe426662e65f4f6c8314f4d62f41f1f73b700ab4a39d992f6faf298a
5
5
  SHA512:
6
- metadata.gz: e50016534bb0db3324c8cdd2640a11595fb990c58b43228368e22dc87ca981557cc0c2d286c68aacbb73b2bc55e0869531e3ec88eab91551930c89aaf719cff4
7
- data.tar.gz: 62c1d224167979f49826e960100699e944afecd0cc1a31479c0aef31f12efe9834e9bb8d6baaa0d6fde11745ed1b426743dc4442153439e883c816a7d80ca188
6
+ metadata.gz: 7ca8466bb3dfbae6b6d7265bd1f84d7c930747748476c0b1c3c7d1ee6894e08e5f753ac35dc5f6902657ad438d135b39fd31b78433983e73cb57abafb6fdf58a
7
+ data.tar.gz: 1a558eebab32a111fbcb8899b524cfc74a9fba7b901c98dcb4c4084cdff76847b6692fe7e21b0f31a62b01834e934ea4c1150c52a749552e05a90f923e2c7e1b
data/.rubocop.yml CHANGED
@@ -1,5 +1,5 @@
1
1
  AllCops:
2
- TargetRubyVersion: 2.6
2
+ TargetRubyVersion: 3.0
3
3
 
4
4
  Style/StringLiterals:
5
5
  Enabled: true
data/CHANGELOG.md CHANGED
@@ -1,3 +1,11 @@
1
+ ## 0.3.1 2025-06-26
2
+
3
+ - Change minimal ruby version to 3.1
4
+ - Use zeitwerk
5
+ - Inherit `Dry::Tuple::Struct` from `Dry::Struct`, optionally provide `Dry::Tuple::Struct::ClassInterface` mixin.
6
+ - Implement Dry::Types[] inferrer syntax
7
+ - Some refactorings, docs & examples
8
+
1
9
  ## 0.0.1 2023-01-26
2
10
 
3
11
  First public release
data/README.md CHANGED
@@ -84,7 +84,7 @@ class Example
84
84
  extend Dry::Tuple::ClassInterface
85
85
  tuple Types.Tuple(Types.Value(:example) << Types::Coercible::Symbol, Types::String)
86
86
 
87
- def initializer(left, right)
87
+ def initialize(left, right)
88
88
  @left, @right = left, right
89
89
  end
90
90
 
@@ -105,7 +105,7 @@ end
105
105
  class OtherExample < Example
106
106
  tuple Types.Tuple(Types.Value(:other_example) << Types::Coercible::Symbol, [Types::Any])
107
107
 
108
- def initializer(left, right, *rest)
108
+ def initialize(left, right, *rest)
109
109
  super(left, right)
110
110
  @rest = rest
111
111
  end
@@ -115,8 +115,8 @@ ExampleSum = Example | OtherExample
115
115
  ExampleSum[['example', 'foo']]
116
116
  # => #<Example @left = :example, @right = 'foo'>
117
117
 
118
- ExampleSum[['other_example', 1, '2', {}]].class
119
- # => #<Example @left = :other_example, @right = 1, @rest = ['2', {}]>
118
+ ExampleSum[['other_example', 1, '2', {}]]
119
+ # => #<OtherExample @left = :other_example, @right = 1, @rest = ['2', {}]>
120
120
  ```
121
121
 
122
122
  #### Class interface for Dry::Struct classes.
@@ -1,16 +1,18 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "lib/dry/tuple/version.rb"
4
+
3
5
  Gem::Specification.new do |spec|
4
6
  spec.name = "dry-types-tuple"
5
- spec.version = '0.1.2'
6
- spec.authors = ["Anton"]
7
+ spec.version = Dry::Tuple::VERSION
8
+ spec.authors = ["Anton Semenov"]
7
9
  spec.email = ["anton.estum@gmail.com"]
8
10
 
9
11
  spec.summary = "Dry::Types::Tuple"
10
12
  spec.description = "The Tuple type implementation for Dry::Types."
11
13
  spec.homepage = "https://github.com/estum/dry-types-tuple"
12
14
  spec.license = "MIT"
13
- spec.required_ruby_version = ">= 2.6.0"
15
+ spec.required_ruby_version = ">= 3.1.0"
14
16
 
15
17
  spec.metadata["allowed_push_host"] = "https://rubygems.org"
16
18
  spec.metadata["homepage_uri"] = spec.homepage
@@ -28,7 +30,9 @@ Gem::Specification.new do |spec|
28
30
  spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
29
31
  spec.require_paths = ["lib"]
30
32
 
33
+ spec.add_dependency "zeitwerk"
31
34
  spec.add_dependency "dry-types"
35
+ spec.add_dependency "dry-core"
32
36
 
33
37
  spec.add_development_dependency "bundler"
34
38
  spec.add_development_dependency "rake"
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dry
4
+ module Tuple
5
+ # Pure class type decorator mixin.
6
+ module ClassDecorator
7
+ # @param input [Array]
8
+ # the result of {#coerce_tuple}
9
+ # @return [self]
10
+ # @abstract
11
+ # Redefine to handle input transformations. For example to splat input array.
12
+ # @note
13
+ # Will be invoked on input that was passed thru tuple validation & coercion.
14
+ def new_from_tuple(input)
15
+ defined?(super) ? super(input) : new(input)
16
+ end
17
+
18
+ # @param input [Array<Mixed>]
19
+ # the result of {Types::Tuple#call}
20
+ # @return [Array]
21
+ # coerced input
22
+ # @note
23
+ # Will be invoked on input that was passed thru tuple validation & coercion.
24
+ def coerce_tuple(input)
25
+ defined?(super) ? super(input) : input
26
+ end
27
+
28
+ # @api private
29
+ def call_safe(input, &block)
30
+ if input.is_a?(self)
31
+ input
32
+ elsif input.is_a?(Array)
33
+ resolve_tuple_safe(input, &block)
34
+ else
35
+ super(input, &block)
36
+ end
37
+ end
38
+
39
+ # @api private
40
+ def call_unsafe(input)
41
+ if input.is_a?(self)
42
+ input
43
+ elsif input.is_a?(Array)
44
+ resolve_tuple_unsafe(input)
45
+ else
46
+ super
47
+ end
48
+ end
49
+
50
+ private
51
+
52
+ # @api private
53
+ def resolve_tuple_safe(input)
54
+ input = tuple.call_safe(input) do |output = input|
55
+ output = yield(output) if block_given?
56
+ return output
57
+ end
58
+ new_from_tuple(coerce_tuple(input))
59
+ end
60
+
61
+ # @api private
62
+ def resolve_tuple_unsafe(input)
63
+ input = tuple.call_unsafe(input)
64
+ new_from_tuple(coerce_tuple(input))
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/core/class_attributes"
4
+ require "dry/types/type"
5
+ require "dry/types/builder"
6
+ require "dry/types/decorator"
7
+
8
+ module Dry
9
+ module Tuple
10
+ # Universal class interface
11
+ # @example Usage
12
+ # class SomeClass
13
+ # extend Dry::Tuple::ClassInterface
14
+ # tuple [Dry::Types['coercible.integer'], Dry::Types['string']]
15
+ #
16
+ # def initialize(a, b)
17
+ # @a, @b = a, b
18
+ # end
19
+ # end
20
+ module ClassInterface
21
+ include Dry::Core::ClassAttributes
22
+ include Dry::Types::Type
23
+ include Dry::Types::Builder
24
+ include Dry::Types::Decorator
25
+ include ClassDecorator
26
+
27
+ # @return [Dry::Types::Tuple]
28
+ def type = @tuple
29
+
30
+ # @!method tuple()
31
+ # @overload tuple(input)
32
+ # @param input [Mixed]
33
+ # @see TypeCoercer#call
34
+ # @overload tuple()
35
+ # @return [Dry::Types::Tuple]
36
+
37
+ # @api private
38
+ def self.extended(base)
39
+ base.defines :tuple, coerce: TypeCoercer
40
+ super
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry-struct"
4
+
5
+ module Dry
6
+ module Tuple
7
+ # {Dry::Struct} abstract subclass extended with {Dry::Types::Struct::ClassInterface}
8
+ #
9
+ # @example Usage
10
+ # class Ambivalent < Dry::Tuple::Struct
11
+ # attribute :left, Dry::Types['coercible.string']
12
+ # attribute :right, Dry::Types['coercible.string']
13
+ #
14
+ # # set key order
15
+ # auto_tuple :left, :right
16
+ # end
17
+ #
18
+ # class AmbivalentButPrefer < Ambivalent
19
+ # attribute :prefer, Dry::Types['coercible.symbol'].enum(:left, :right)
20
+ # auto_tuple :prefer
21
+ # end
22
+ class Struct < Dry::Struct
23
+ abstract
24
+
25
+ # Extracted due to make it possible to use this feature within {Dry::Struct} classes.
26
+ # @example extending Dry::Struct subclass
27
+ #
28
+ # class SomeStruct < Dry::Struct
29
+ # attribute :some, Types::Integer
30
+ # attribute :with, Types::Hash
31
+ #
32
+ # extend ::Dry::Tuple::Struct::ClassInterface
33
+ # auto_tuple :some, :with
34
+ # end
35
+ module ClassInterface
36
+ include ClassDecorator
37
+
38
+ # Merges the given keys into the #{keys_order} and redefines the +tuple+ of class.
39
+ # @param keys [Array<Symbol>]
40
+ # @return [void]
41
+ def auto_tuple(*keys)
42
+ keys_order(keys_order | keys)
43
+ index = schema.keys.map { |t| [t.name, t.type] }.to_h
44
+ tuple Dry::Types::Tuple.coerce(index.values_at(*keys_order))
45
+ end
46
+
47
+ # Constructs a hash of struct attributes from the given array by zipping within {#keys_order}.
48
+ # @param input [Array<Any>]
49
+ # @return [Hash]
50
+ def coerce_tuple(input)
51
+ keys_order.zip(input).to_h
52
+ end
53
+
54
+ # @return [Dry::Types::Result]
55
+ def try(input, &block)
56
+ if input.is_a?(::Array)
57
+ tuple.try(input, &block)
58
+ else
59
+ super(input, &block)
60
+ end
61
+ end
62
+
63
+ # @api private
64
+ def self.extended(base)
65
+ base.defines :tuple, coerce: TypeCoercer
66
+ base.defines :keys_order, type: Dry::Types['array<symbol>']
67
+ base.keys_order EMPTY_ARRAY
68
+ super
69
+ end
70
+ end
71
+
72
+ extend ClassInterface
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dry
4
+ module Tuple
5
+ module TypeCoercer
6
+ module_function
7
+
8
+ # @overload Coercer.call(array)
9
+ # @param array [Array<Mixed>]
10
+ # @overload Coercer.call(input)
11
+ # @param tuple [Dry::Types::Tuple]
12
+ # @overload Coercer.call(type)
13
+ # @param type [Dry::Types::Constrained]
14
+ # @example Usage
15
+ # Dry::Types::Tuple::Coercer.([Dry::Types['any'], Dry::Types['string']])
16
+ def call(input, returns: Undefined)
17
+ case input when Array
18
+ Dry::Types::Tuple.coerce(input)
19
+ when Dry::Types::Tuple
20
+ Undefined.default(returns, input)
21
+ when Dry::Types::Constrained
22
+ call(input.type, returns: input)
23
+ when NilClass
24
+ Dry::Types['nominal.tuple']
25
+ end
26
+ end
27
+
28
+ # @see Tuple.coerce
29
+ # @example Usage
30
+ # Dry::Types::Tuple::Coercer[Dry::Types['any'], Dry::Types['string']]
31
+ def [](*input, **opts)
32
+ call(input, **opts)
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dry
4
+ module Tuple
5
+ VERSION = '0.3.1'
6
+ end
7
+ end
data/lib/dry/tuple.rb CHANGED
@@ -1,6 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'dry/types/tuple'
3
+ require "dry/tuple/version"
4
+ require "zeitwerk"
5
+ require "dry/core"
4
6
 
5
7
  module Dry
6
8
  # The namespace contains mixins for class decoration with tuple type.
@@ -22,121 +24,34 @@ module Dry
22
24
  # end
23
25
  # end
24
26
  module Tuple
25
- # Pure class type decorator mixin.
26
- module ClassDecorator
27
- include Types::Decorator
28
-
29
- # @return [@tuple]
30
- def type
31
- @tuple
32
- end
33
-
34
- # @param input [Array] the result of {#coerce_tuple}
35
- # @return [self]
36
- # @abstract
37
- # It is designed to be redefined for example to splat arguments on constructor.
38
- # @note
39
- # Will be invoked on input that was passed thru tuple validation & coercion.
40
- def new_from_tuple(input)
41
- defined?(super) ? super(input) : new(input)
42
- end
43
-
44
- # @param input [Array] the result of tuple#call
45
- # @return [Array] coerced input
46
- # @note
47
- # Will be invoked on input that was passed thru tuple validation & coercion.
48
- def coerce_tuple(input)
49
- defined?(super) ? super(input) : input
50
- end
51
-
52
- # @api private
53
- def call_safe(input, &block)
54
- if input.is_a?(self)
55
- input
56
- elsif input.is_a?(Array)
57
- resolve_tuple_safe(input, &block)
58
- else
59
- super
60
- end
61
- end
62
-
63
- # @api private
64
- def call_unsafe(input)
65
- if input.is_a?(self)
66
- input
67
- elsif input.is_a?(Array)
68
- resolve_tuple_unsafe(input)
69
- else
70
- super
71
- end
72
- end
73
-
74
- private
75
-
76
- # @api private
77
- def resolve_tuple_safe(input)
78
- input = tuple.call_safe(input) do |output = input|
79
- output = yield(output) if block_given?
80
- return output
27
+ include Dry::Core::Constants
28
+
29
+ # rubocop:disable Metrics/MethodLength
30
+
31
+ # @api private
32
+ def self.loader
33
+ @loader ||=
34
+ ::Zeitwerk::Loader.new.tap do |loader|
35
+ root = ::File.expand_path("..", __dir__)
36
+ warn root
37
+ loader.tag = "dry-types-tuple"
38
+ loader.inflector = ::Zeitwerk::GemInflector.new("#{root}/dry-types-tuple.rb")
39
+ loader.push_dir root
40
+ loader.ignore \
41
+ "#{root}/dry-types-tuple.rb",
42
+ "#{root}/dry/types",
43
+ # "#{root}/dry/tuple.rb",
44
+ "#{root}/dry/tuple/{struct,version}.rb"
45
+
46
+ if defined?(Pry)
47
+ loader.log!
48
+ loader.enable_reloading
49
+ end
81
50
  end
82
- new_from_tuple(coerce_tuple(input))
83
- end
84
-
85
- # @api private
86
- def resolve_tuple_unsafe(input)
87
- input = tuple.call_unsafe(input)
88
- new_from_tuple(coerce_tuple(input))
89
- end
90
- end
91
-
92
- module HookExtendObject
93
- # Makes the module's features to be prepended instead of appended to the target class when extended.
94
- # Also defines the `tuple` class attribute.
95
- # @api private
96
- private def extend_object(base)
97
- base.singleton_class.prepend(self)
98
- base.defines :tuple
99
- end
100
- end
101
-
102
- module ClassInterface
103
- include Core::ClassAttributes
104
- include Types::Type
105
- include Types::Builder
106
- include ClassDecorator
107
- extend HookExtendObject
108
51
  end
109
52
 
110
- # Extracted due to make it possible to use this feature within {Dry::Struct} classes.
111
- # @example extending Dry::Struct subclass
112
- #
113
- # class SomeStruct < Dry::Struct
114
- # attribute :some, Types::Integer
115
- # attribute :with, Types::Hash
116
- # extend ::Dry::Tuple::StructClassInterface
117
- # auto_tuple :some, :with
118
- # end
119
- module StructClassInterface
120
- include ClassDecorator
121
- extend HookExtendObject
53
+ # rubocop:enable Metrics/MethodLength
122
54
 
123
- class << self
124
- private def extend_object(base)
125
- super
126
- base.defines :keys_order
127
- base.keys_order []
128
- end
129
- end
130
-
131
- def auto_tuple(*keys)
132
- keys_order(keys_order | keys)
133
- index = schema.keys.map { |t| [t.name, t.type] }.to_h
134
- tuple Dry::Types::Tuple.build_unsplat(index.values_at(*keys_order))
135
- end
136
-
137
- def coerce_tuple(input)
138
- keys_order.zip(input).to_h
139
- end
140
- end
55
+ loader.setup
141
56
  end
142
- end
57
+ end
@@ -9,53 +9,70 @@ module Dry
9
9
  # )
10
10
  # Types::ServiceArgs[['thumb', '300', '300', 'sample']]
11
11
  # # => [:thumb, 300, 300, "sample"]
12
- class Tuple < Array
12
+ class Tuple < Nominal
13
+ # VERSION ||= Dry::Tuple::VERSION
14
+
13
15
  # Build a tuple type.
14
16
  #
15
- # @overload Tuple.build(*fixed_types, rest_type)
17
+ # @overload self.build(*fixed_types, rest_type)
16
18
  # @param [Array<Dry::Types::Type>] fixed_types
17
19
  # @param [Array(Dry::Types::Type)] rest_type
18
- # @see build_index
20
+ # @see self.build_index
19
21
  # @return [Dry::Types::Tuple]
20
- def self.build(*types)
21
- build_unsplat(types)
22
- end
23
-
24
- # @api private
25
- def self.build_unsplat(types)
26
- new(::Array, types_index: build_index(types))
22
+ def self.build(*types, **opts)
23
+ coerce(types, **opts)
27
24
  end
28
25
 
29
26
  # Prepares types index for the Tuple
30
- # @param [Array<Dry::Types::Type>] types
31
- # @see extract_rest
32
- # @return [Dry::Core::Constants::Undefined, Dry::Types::Type]
27
+ # @see self.extract_rest
28
+ # @param types [Array<Type>]
29
+ # @return [Hash { Integer => Type }]
33
30
  def self.build_index(types)
34
- rest_type = extract_rest(types)
35
- types_index = ::Hash[types.size.times.zip(types)]
36
- types_index.default = Undefined.default(rest_type, nil)
31
+ types_index = {}
32
+
33
+ Undefined.map(extract_rest(types)) do |rest_type|
34
+ types_index.default = rest_type
35
+ end
36
+
37
+ types.each_with_index do |type, index|
38
+ types_index[index] = type
39
+ end
40
+
37
41
  types_index
38
42
  end
39
43
 
40
- # Extracts and unwraps the rest type
41
- # @param [Array<Dry::Types::Type>] types
42
- # @return [Dry::Core::Constants::Undefined, Dry::Types::Type]
44
+ # Extracts the rest type or types.
45
+ # More than one types in list will be composed to {Sum}.
46
+ # @note
47
+ # Destructive on input arrays.
48
+ # @param types [Array<Type, Sum>]
49
+ # @return [Undefined, Type, Sum]
43
50
  def self.extract_rest(types)
44
- if !types[-1].is_a?(::Array)
45
- return Undefined
46
- end
47
-
48
- if types[-1].size > 1
49
- raise ArgumentError, "rest_type should be an Array with single element to apply to the rest of items: #{types[-1]}"
51
+ case types
52
+ in *head, ::Array => rest if rest.size > 0
53
+ types.replace(head)
54
+ rest.reduce(:|)
55
+ else
56
+ Undefined
50
57
  end
58
+ end
51
59
 
52
- types.pop[0]
60
+ # @api private
61
+ def self.coerce(types, **opts)
62
+ types_index = build_index(types)
63
+ new(::Array, **opts, types_index:)
53
64
  end
54
65
 
55
- def initialize(_primitive, types_index: EMPTY_HASH, meta: EMPTY_HASH)
56
- super(_primitive, types_index: types_index, meta: meta)
66
+ singleton_class.alias_method :build_unsplat, :coerce
67
+
68
+ # @param primitive [Class]
69
+ # @return [self]
70
+ def initialize(primitive, types_index: EMPTY_HASH, meta: EMPTY_HASH, **opts)
71
+ super(primitive, **opts, types_index:, meta:)
57
72
  end
58
73
 
74
+ # @api public
75
+
59
76
  # @see Tuple.build
60
77
  # @return [Dry::Types::Tuple]
61
78
  def of(*types)
@@ -63,92 +80,59 @@ module Dry
63
80
  end
64
81
 
65
82
  # @return [Hash]
66
- #
67
- # @api public
68
- def types_index
69
- options[:types_index]
70
- end
83
+ def types_index = options[:types_index]
71
84
 
72
85
  # @return [Array<Type>]
73
- #
74
- # @api public
75
- def fixed_types
76
- options[:types_index].values
77
- end
86
+ def fixed_types = types_index.values
78
87
 
79
88
  # @return [Type]
80
- #
81
- # @api public
82
- def rest_type
83
- options[:types_index].default
84
- end
89
+ def rest_type = types_index.default
85
90
 
86
91
  # @return [String]
87
- #
88
- # @api public
89
- def name
90
- "Tuple"
91
- end
92
-
93
- # @param [Array] tuple
94
- #
95
- # @return [Array]
96
- #
97
- # @api private
98
- def call_unsafe(tuple)
99
- try(tuple) { |failure|
100
- raise MapError, failure.error.message
101
- }.input
102
- end
103
-
104
- # @param [Array] tuple
105
- #
106
- # @return [Array]
107
- #
108
- # @api private
109
- def call_safe(tuple)
110
- try(tuple) { return yield }.input
111
- end
92
+ def name = 'Tuple'
112
93
 
113
94
  # @param [Array] tuple
114
- #
115
95
  # @return [Result]
116
- #
117
- # @api public
118
96
  def try(tuple)
119
97
  result = coerce(tuple)
120
98
  return result if result.success? || !block_given?
121
-
122
99
  yield(result)
123
100
  end
124
101
 
125
102
  # Build a lax type
126
- #
127
103
  # @return [Lax]
128
- #
129
- # @api public
130
104
  def lax
131
- lax_types_index = types_index.transform_values(&:lax)
132
- Lax.new(Tuple.new(primitive, **options, types_index: lax_types_index, meta: meta))
105
+ types_index = types_index().transform_values(&:lax)
106
+ types_index.default = rest_type.lax if rest_type
107
+ Lax.new(Tuple.new(primitive, **options, types_index:, meta:))
133
108
  end
134
109
 
135
110
  # @param meta [Boolean] Whether to dump the meta to the AST
136
- #
137
111
  # @return [Array] An AST representation
138
- #
139
- # @api public
140
112
  def to_ast(meta: true)
141
- structure = [*fixed_types.map { |type| type.to_ast(meta: meta) }]
142
- structure << [rest_type.to_ast(meta: meta)] unless rest_type.nil?
143
- structure << meta ? self.meta : EMPTY_HASH
113
+ structure = [*fixed_types.map { _1.to_ast(meta:) }]
114
+ structure << [rest_type.to_ast(meta:)] unless rest_type.nil?
115
+ structure << meta ? meta() : EMPTY_HASH
144
116
  [:tuple, structure]
145
117
  end
146
118
 
147
119
  # @return [Boolean]
148
- #
149
- # @api public
150
120
  def constrained?
151
- rest_type&.constrained? || options[:types_index].each_value.any?(&:constrained?)
121
+ rest_type&.constrained? || types_index.each_value.any?(&:constrained?)
122
+ end
123
+
124
+ # @param tuple [Array]
125
+ # @return [Array]
126
+ # @api private
127
+ def call_unsafe(tuple)
128
+ try(tuple) { raise MapError, _1.error.message }.input
129
+ end
130
+
131
+ # @param tuple [Array]
132
+ # @return [Array]
133
+ # @api private
134
+ def call_safe(tuple)
135
+ try(tuple) { return yield }.input
152
136
  end
153
137
 
154
138
  private
@@ -156,25 +140,23 @@ module Dry
156
140
  # @api private
157
141
  def coerce(input)
158
142
  unless primitive?(input)
159
- return failure(
160
- input, CoercionError.new("#{input.inspect} must be an instance of #{primitive}")
161
- )
143
+ return failure(input, CoercionError.new("#{input.inspect} must be an instance of #{primitive}"))
162
144
  end
163
145
 
164
146
  output = []
165
147
  failures = []
166
148
 
167
149
  input.each_with_index do |value, index|
168
- res_i = types_index[index]&.try(value)
150
+ item = types_index[index]&.try(value)
169
151
 
170
- if res_i.nil?
152
+ if item.nil?
171
153
  failures << CoercionError.new("#{value.inspect} not fits to the fixed-size tuple")
172
154
  break
173
- elsif res_i.failure?
174
- failures << res_i.error
155
+ elsif item.failure?
156
+ failures << item.error
175
157
  break
176
158
  else
177
- output << res_i.input
159
+ output << item.input
178
160
  end
179
161
  end
180
162
 
@@ -189,13 +171,13 @@ module Dry
189
171
  module BuilderMethods
190
172
  # Build a tuple type.
191
173
  #
192
- # @see Dry::Types::Tuple#build
174
+ # @see Tuple.build
193
175
  # @overload Tuple(*fixed_types, rest_type)
194
- # @param [Array<Dry::Types::Type>] fixed_types
195
- # @param [Array(Dry::Types::Type)] rest_type
196
- # @return [Dry::Types::Tuple]
197
- def Tuple(*types)
198
- Tuple.build(*types)
176
+ # @param [Array<Type>] fixed_types
177
+ # @param [Array(Type)] rest_type
178
+ # @return [Tuple]
179
+ def Tuple(...)
180
+ Tuple.build(...)
199
181
  end
200
182
  end
201
183
 
@@ -211,26 +193,63 @@ module Dry
211
193
 
212
194
  visit_options(options, tuple.meta) do |opts|
213
195
  header = "Tuple<"
214
- rest = visit(types.default) { |type| "*: #{type}" } if types.default
196
+ rest = visit(types.default) { "*: #{_1}" } if types.default
215
197
 
216
198
  if size.zero?
217
199
  yield "#{header}>#{opts}"
218
200
  else
219
- yield header.dup << (types.map { |pos, pos_type|
220
- visit(pos_type) { |type| "#{pos}: #{type}" }
221
- } << rest).compact.join(", ") << ">#{opts}"
201
+ yield header.dup << (
202
+ types.flat_map do |pos, pos_types|
203
+ Kernel.Array(pos_types).map do |pos_type|
204
+ visit(pos_type) { "#{pos}: #{_1}" }
205
+ end
206
+ end << rest
207
+ ).compact.join(", ") << ">#{opts}"
222
208
  end
223
209
  end
224
210
  end
225
211
  end
226
212
 
227
- register "nominal.tuple", Types::Tuple.build([self['any']])
213
+ nominal_tuple = Tuple.build([self['any']])
214
+
215
+ register "nominal.tuple", nominal_tuple
228
216
 
229
- type = self["nominal.tuple"].constrained(type: ::Array)
217
+ strict_tuple = nominal_tuple.constrained(type: ::Array)
218
+
219
+ register "tuple", strict_tuple
220
+ register "strict.tuple", strict_tuple
221
+ register "coercible.tuple", nominal_tuple.constructor(Kernel.method(:Array))
222
+ register "params.tuple", nominal_tuple.constructor(Coercions::Params.method(:to_ary))
223
+
224
+ TUPLE_PREFIX_REGEX = /(?:(?:coercible|nominal|params|strict)\.)?tuple(?=\<)/
225
+ TUPLE_MEMBERS_REGEX = /(?<=tuple<).+(?=>)/
226
+ TUPLE_MEMBERS_SCAN_REGEX = /(tuple\<(?:\g<1>\,)*\g<1>\>|\[(\g<1>)\](?=$)|[^,]+)(?=,|$)/
227
+ SUM_MATCH_REGEX = /((?:(?:\A|\|)(?:[^\|]+))*)\|([^\|]+)\z/
228
+
229
+ # @api private
230
+ module ReferenceHook
231
+ def [](name)
232
+ case name
233
+ when TUPLE_PREFIX_REGEX
234
+ type_map.fetch_or_store(name) do
235
+ key = Regexp.last_match[0]
236
+ types =
237
+ name[TUPLE_MEMBERS_REGEX].
238
+ scan(TUPLE_MEMBERS_SCAN_REGEX).
239
+ map { |(type, rest)| rest.nil? ? self[type] : [self[rest]] }
240
+ super(key).of(*types)
241
+ end
242
+ when SUM_MATCH_REGEX
243
+ type_map.fetch_or_store(name) do
244
+ left, right = Regexp.last_match.captures
245
+ self[left] | super(right)
246
+ end
247
+ else
248
+ super(name)
249
+ end
250
+ end
251
+ end
230
252
 
231
- register "tuple", type
232
- register "strict.tuple", type
233
- register "coercible.tuple", self["nominal.tuple"].constructor(Kernel.method(:Array))
234
- register "params.tuple", self["nominal.tuple"].constructor(Coercions::Params.method(:to_ary))
253
+ singleton_class.prepend ReferenceHook
235
254
  end
236
255
  end
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/tuple"
4
+ require "dry/types/tuple"
metadata CHANGED
@@ -1,15 +1,28 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dry-types-tuple
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.2
4
+ version: 0.3.1
5
5
  platform: ruby
6
6
  authors:
7
- - Anton
8
- autorequire:
7
+ - Anton Semenov
9
8
  bindir: exe
10
9
  cert_chain: []
11
- date: 2023-02-07 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: zeitwerk
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '0'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: '0'
13
26
  - !ruby/object:Gem::Dependency
14
27
  name: dry-types
15
28
  requirement: !ruby/object:Gem::Requirement
@@ -24,6 +37,20 @@ dependencies:
24
37
  - - ">="
25
38
  - !ruby/object:Gem::Version
26
39
  version: '0'
40
+ - !ruby/object:Gem::Dependency
41
+ name: dry-core
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: '0'
47
+ type: :runtime
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
27
54
  - !ruby/object:Gem::Dependency
28
55
  name: bundler
29
56
  requirement: !ruby/object:Gem::Requirement
@@ -96,7 +123,13 @@ files:
96
123
  - README.md
97
124
  - Rakefile
98
125
  - dry-types-tuple.gemspec
126
+ - lib/dry-types-tuple.rb
99
127
  - lib/dry/tuple.rb
128
+ - lib/dry/tuple/class_decorator.rb
129
+ - lib/dry/tuple/class_interface.rb
130
+ - lib/dry/tuple/struct.rb
131
+ - lib/dry/tuple/type_coercer.rb
132
+ - lib/dry/tuple/version.rb
100
133
  - lib/dry/types/tuple.rb
101
134
  homepage: https://github.com/estum/dry-types-tuple
102
135
  licenses:
@@ -106,7 +139,6 @@ metadata:
106
139
  homepage_uri: https://github.com/estum/dry-types-tuple
107
140
  source_code_uri: https://github.com/estum/dry-types-tuple
108
141
  changelog_uri: https://github.com/estum/dry-types-tuple/blob/main/CHANGELOG.md
109
- post_install_message:
110
142
  rdoc_options: []
111
143
  require_paths:
112
144
  - lib
@@ -114,15 +146,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
114
146
  requirements:
115
147
  - - ">="
116
148
  - !ruby/object:Gem::Version
117
- version: 2.6.0
149
+ version: 3.1.0
118
150
  required_rubygems_version: !ruby/object:Gem::Requirement
119
151
  requirements:
120
152
  - - ">="
121
153
  - !ruby/object:Gem::Version
122
154
  version: '0'
123
155
  requirements: []
124
- rubygems_version: 3.3.7
125
- signing_key:
156
+ rubygems_version: 3.6.9
126
157
  specification_version: 4
127
158
  summary: Dry::Types::Tuple
128
159
  test_files: []