dry-types-tuple 0.1.4 → 0.3.2

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: fd3228e18cf0642b5270041045dcd053ab2ae71b46246420e1a58f0c3fb1dda7
4
- data.tar.gz: 58e0adf146063babf2b35beeabcfda9f531f2c00b1075fa7f5e017d6c5cd00ae
3
+ metadata.gz: 37df5ee0299ce0e65a395a458f0284fa540f1f4c3cbc5f09ca7424a9b5ca1ca3
4
+ data.tar.gz: 0d83445514c90cf9381c49fb805b98d49ef2c9c6a00c776fc921ba39fda6a5d0
5
5
  SHA512:
6
- metadata.gz: 29847469e31c10ead9a61e7afedf3ddc98b1b2dd9cea975237d0a72642778b93dbe4bca20230c28ef511809d67471d86ead90165468da1744df510ab2e11fe5f
7
- data.tar.gz: 96b53068b51630699c1c6e5c943c9671da97b0b15d09c8286929a51105a24e0e6ec2c2f89a89c2cba88239e9d37cb1bb12b91ac7dd471f4546e705a3b7f68e61
6
+ metadata.gz: e1c4fc70ed670e992613b22444d68fef3a4fcddb6459405657272099dc54caec158f3a77e20ed00df352de869e922188dcac6ba2241aecaec36d5995ae2dd234
7
+ data.tar.gz: 1a077702745b571a3c0c5d8ec8dd00aa1885971b054dc1b96e23a026b1b24798756e496f653eebdaad611b034ab3e0607934e4c5b75440a930e394eebeb7214c
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.4'
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 TypeCoercer.call(array)
9
+ # @param array [Array<Mixed>]
10
+ # @overload TypeCoercer.call(input)
11
+ # @param tuple [Dry::Types::Tuple]
12
+ # @overload TypeCoercer.call(type)
13
+ # @param type [Dry::Types::Constrained]
14
+ # @example Usage
15
+ # Dry::Tuple::TypeCoercer.([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::Tuple::TypeCoercer[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.2'
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,128 +24,32 @@ module Dry
22
24
  # end
23
25
  # end
24
26
  module Tuple
25
- # Pure class type decorator mixin.
26
- module ClassDecorator
27
- # @param input [Array] the result of {#coerce_tuple}
28
- # @return [self]
29
- # @abstract
30
- # It is designed to be redefined for example to splat arguments on constructor.
31
- # @note
32
- # Will be invoked on input that was passed thru tuple validation & coercion.
33
- def new_from_tuple(input)
34
- defined?(super) ? super(input) : new(input)
35
- end
36
-
37
- # @param input [Array] the result of tuple#call
38
- # @return [Array] coerced input
39
- # @note
40
- # Will be invoked on input that was passed thru tuple validation & coercion.
41
- def coerce_tuple(input)
42
- defined?(super) ? super(input) : input
43
- end
44
-
45
- # @api private
46
- def call_safe(input, &block)
47
- if input.is_a?(self)
48
- input
49
- elsif input.is_a?(Array)
50
- resolve_tuple_safe(input, &block)
51
- else
52
- super
53
- end
54
- end
55
-
56
- # @api private
57
- def call_unsafe(input)
58
- if input.is_a?(self)
59
- input
60
- elsif input.is_a?(Array)
61
- resolve_tuple_unsafe(input)
62
- else
63
- super
64
- end
65
- end
66
-
67
- private
68
-
69
- # @api private
70
- def resolve_tuple_safe(input)
71
- input = tuple.call_safe(input) do |output = input|
72
- output = yield(output) if block_given?
73
- 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
+ loader.tag = "dry-types-tuple"
37
+ loader.inflector = ::Zeitwerk::GemInflector.new("#{root}/dry-types-tuple.rb")
38
+ loader.push_dir root
39
+ loader.ignore \
40
+ "#{root}/dry-types-tuple.rb",
41
+ "#{root}/dry/types",
42
+ "#{root}/dry/tuple/{struct,version}.rb"
43
+
44
+ if defined?(Pry)
45
+ loader.log!
46
+ loader.enable_reloading
47
+ end
74
48
  end
75
- new_from_tuple(coerce_tuple(input))
76
- end
77
-
78
- # @api private
79
- def resolve_tuple_unsafe(input)
80
- input = tuple.call_unsafe(input)
81
- new_from_tuple(coerce_tuple(input))
82
- end
83
- end
84
-
85
- module HookExtendObject
86
- # Makes the module's features to be prepended instead of appended to the target class when extended.
87
- # Also defines the `tuple` class attribute.
88
- # @api private
89
- private def extend_object(base)
90
- base.singleton_class.prepend(self)
91
- base.defines :tuple
92
- end
93
49
  end
94
50
 
95
- module ClassInterface
96
- include Core::ClassAttributes
97
- include Types::Type
98
- include Types::Builder
99
- include Types::Decorator
100
- include ClassDecorator
101
- extend HookExtendObject
51
+ # rubocop:enable Metrics/MethodLength
102
52
 
103
- # @return [@tuple]
104
- def type
105
- @tuple
106
- end
107
- end
108
-
109
- # Extracted due to make it possible to use this feature within {Dry::Struct} classes.
110
- # @example extending Dry::Struct subclass
111
- #
112
- # class SomeStruct < Dry::Struct
113
- # attribute :some, Types::Integer
114
- # attribute :with, Types::Hash
115
- # extend ::Dry::Tuple::StructClassInterface
116
- # auto_tuple :some, :with
117
- # end
118
- module StructClassInterface
119
- include ClassDecorator
120
- extend HookExtendObject
121
-
122
- class << self
123
- private def extend_object(base)
124
- super
125
- base.defines :keys_order
126
- base.keys_order []
127
- end
128
- end
129
-
130
- def try(input, &block)
131
- if input.is_a?(::Array)
132
- @tuple.try(input, &block)
133
- else
134
- super(input, &block)
135
- end
136
- end
137
-
138
- def auto_tuple(*keys)
139
- keys_order(keys_order | keys)
140
- index = schema.keys.map { |t| [t.name, t.type] }.to_h
141
- tuple Dry::Types::Tuple.build_unsplat(index.values_at(*keys_order))
142
- end
143
-
144
- def coerce_tuple(input)
145
- keys_order.zip(input).to_h
146
- end
147
- end
53
+ loader.setup
148
54
  end
149
- end
55
+ end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "dry/tuple"
4
+
3
5
  module Dry
4
6
  module Types
5
7
  # @example
@@ -9,53 +11,70 @@ module Dry
9
11
  # )
10
12
  # Types::ServiceArgs[['thumb', '300', '300', 'sample']]
11
13
  # # => [:thumb, 300, 300, "sample"]
12
- class Tuple < Array
14
+ class Tuple < Nominal
15
+ # VERSION ||= Dry::Tuple::VERSION
16
+
13
17
  # Build a tuple type.
14
18
  #
15
- # @overload Tuple.build(*fixed_types, rest_type)
19
+ # @overload self.build(*fixed_types, rest_type)
16
20
  # @param [Array<Dry::Types::Type>] fixed_types
17
21
  # @param [Array(Dry::Types::Type)] rest_type
18
- # @see build_index
22
+ # @see self.build_index
19
23
  # @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))
24
+ def self.build(*types, **opts)
25
+ coerce(types, **opts)
27
26
  end
28
27
 
29
28
  # 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]
29
+ # @see self.extract_rest
30
+ # @param types [Array<Type>]
31
+ # @return [Hash { Integer => Type }]
33
32
  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)
33
+ types_index = {}
34
+
35
+ Undefined.map(extract_rest(types)) do |rest_type|
36
+ types_index.default = rest_type
37
+ end
38
+
39
+ types.each_with_index do |type, index|
40
+ types_index[index] = type
41
+ end
42
+
37
43
  types_index
38
44
  end
39
45
 
40
- # Extracts and unwraps the rest type
41
- # @param [Array<Dry::Types::Type>] types
42
- # @return [Dry::Core::Constants::Undefined, Dry::Types::Type]
46
+ # Extracts the rest type or types.
47
+ # More than one types in list will be composed to {Sum}.
48
+ # @note
49
+ # Destructive on input arrays.
50
+ # @param types [Array<Type, Sum>]
51
+ # @return [Undefined, Type, Sum]
43
52
  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]}"
53
+ case types
54
+ in *head, ::Array => rest if rest.size > 0
55
+ types.replace(head)
56
+ rest.reduce(:|)
57
+ else
58
+ Undefined
50
59
  end
60
+ end
51
61
 
52
- types.pop[0]
62
+ # @api private
63
+ def self.coerce(types, **opts)
64
+ types_index = build_index(types)
65
+ new(::Array, **opts, types_index:)
53
66
  end
54
67
 
55
- def initialize(_primitive, types_index: EMPTY_HASH, meta: EMPTY_HASH)
56
- super(_primitive, types_index: types_index, meta: meta)
68
+ singleton_class.alias_method :build_unsplat, :coerce
69
+
70
+ # @param primitive [Class]
71
+ # @return [self]
72
+ def initialize(primitive, types_index: EMPTY_HASH, meta: EMPTY_HASH, **opts)
73
+ super(primitive, **opts, types_index:, meta:)
57
74
  end
58
75
 
76
+ # @api public
77
+
59
78
  # @see Tuple.build
60
79
  # @return [Dry::Types::Tuple]
61
80
  def of(*types)
@@ -63,92 +82,59 @@ module Dry
63
82
  end
64
83
 
65
84
  # @return [Hash]
66
- #
67
- # @api public
68
- def types_index
69
- options[:types_index]
70
- end
85
+ def types_index = options[:types_index]
71
86
 
72
87
  # @return [Array<Type>]
73
- #
74
- # @api public
75
- def fixed_types
76
- options[:types_index].values
77
- end
88
+ def fixed_types = types_index.values
78
89
 
79
90
  # @return [Type]
80
- #
81
- # @api public
82
- def rest_type
83
- options[:types_index].default
84
- end
91
+ def rest_type = types_index.default
85
92
 
86
93
  # @return [String]
87
- #
88
- # @api public
89
- def name
90
- "Tuple"
91
- end
94
+ def name = 'Tuple'
92
95
 
93
96
  # @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
112
-
113
- # @param [Array] tuple
114
- #
115
97
  # @return [Result]
116
- #
117
- # @api public
118
98
  def try(tuple)
119
99
  result = coerce(tuple)
120
100
  return result if result.success? || !block_given?
121
-
122
101
  yield(result)
123
102
  end
124
103
 
125
104
  # Build a lax type
126
- #
127
105
  # @return [Lax]
128
- #
129
- # @api public
130
106
  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))
107
+ types_index = types_index().transform_values(&:lax)
108
+ types_index.default = rest_type.lax if rest_type
109
+ Lax.new(Tuple.new(primitive, **options, types_index:, meta:))
133
110
  end
134
111
 
135
112
  # @param meta [Boolean] Whether to dump the meta to the AST
136
- #
137
113
  # @return [Array] An AST representation
138
- #
139
- # @api public
140
114
  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
115
+ structure = [*fixed_types.map { _1.to_ast(meta:) }]
116
+ structure << [rest_type.to_ast(meta:)] unless rest_type.nil?
117
+ structure << meta ? meta() : EMPTY_HASH
144
118
  [:tuple, structure]
145
119
  end
146
120
 
147
121
  # @return [Boolean]
148
- #
149
- # @api public
150
122
  def constrained?
151
- rest_type&.constrained? || options[:types_index].each_value.any?(&:constrained?)
123
+ rest_type&.constrained? || types_index.each_value.any?(&:constrained?)
124
+ end
125
+
126
+ # @param tuple [Array]
127
+ # @return [Array]
128
+ # @api private
129
+ def call_unsafe(tuple)
130
+ try(tuple) { raise MapError, _1.error.message }.input
131
+ end
132
+
133
+ # @param tuple [Array]
134
+ # @return [Array]
135
+ # @api private
136
+ def call_safe(tuple)
137
+ try(tuple) { return yield }.input
152
138
  end
153
139
 
154
140
  private
@@ -156,25 +142,23 @@ module Dry
156
142
  # @api private
157
143
  def coerce(input)
158
144
  unless primitive?(input)
159
- return failure(
160
- input, CoercionError.new("#{input.inspect} must be an instance of #{primitive}")
161
- )
145
+ return failure(input, CoercionError.new("#{input.inspect} must be an instance of #{primitive}"))
162
146
  end
163
147
 
164
148
  output = []
165
149
  failures = []
166
150
 
167
151
  input.each_with_index do |value, index|
168
- res_i = types_index[index]&.try(value)
152
+ item = types_index[index]&.try(value)
169
153
 
170
- if res_i.nil?
154
+ if item.nil?
171
155
  failures << CoercionError.new("#{value.inspect} not fits to the fixed-size tuple")
172
156
  break
173
- elsif res_i.failure?
174
- failures << res_i.error
157
+ elsif item.failure?
158
+ failures << item.error
175
159
  break
176
160
  else
177
- output << res_i.input
161
+ output << item.input
178
162
  end
179
163
  end
180
164
 
@@ -189,13 +173,13 @@ module Dry
189
173
  module BuilderMethods
190
174
  # Build a tuple type.
191
175
  #
192
- # @see Dry::Types::Tuple#build
176
+ # @see Tuple.build
193
177
  # @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)
178
+ # @param [Array<Type>] fixed_types
179
+ # @param [Array(Type)] rest_type
180
+ # @return [Tuple]
181
+ def Tuple(...)
182
+ Tuple.build(...)
199
183
  end
200
184
  end
201
185
 
@@ -211,26 +195,63 @@ module Dry
211
195
 
212
196
  visit_options(options, tuple.meta) do |opts|
213
197
  header = "Tuple<"
214
- rest = visit(types.default) { |type| "*: #{type}" } if types.default
198
+ rest = visit(types.default) { "*: #{_1}" } if types.default
215
199
 
216
200
  if size.zero?
217
201
  yield "#{header}>#{opts}"
218
202
  else
219
- yield header.dup << (types.map { |pos, pos_type|
220
- visit(pos_type) { |type| "#{pos}: #{type}" }
221
- } << rest).compact.join(", ") << ">#{opts}"
203
+ yield header.dup << (
204
+ types.flat_map do |pos, pos_types|
205
+ Kernel.Array(pos_types).map do |pos_type|
206
+ visit(pos_type) { "#{pos}: #{_1}" }
207
+ end
208
+ end << rest
209
+ ).compact.join(", ") << ">#{opts}"
222
210
  end
223
211
  end
224
212
  end
225
213
  end
226
214
 
227
- register "nominal.tuple", Types::Tuple.build([self['any']])
215
+ nominal_tuple = Tuple.build([self['any']])
216
+
217
+ register "nominal.tuple", nominal_tuple
228
218
 
229
- type = self["nominal.tuple"].constrained(type: ::Array)
219
+ strict_tuple = nominal_tuple.constrained(type: ::Array)
220
+
221
+ register "tuple", strict_tuple
222
+ register "strict.tuple", strict_tuple
223
+ register "coercible.tuple", nominal_tuple.constructor(Kernel.method(:Array))
224
+ register "params.tuple", nominal_tuple.constructor(Coercions::Params.method(:to_ary))
225
+
226
+ TUPLE_PREFIX_REGEX = /(?:(?:coercible|nominal|params|strict)\.)?tuple(?=\<)/
227
+ TUPLE_MEMBERS_REGEX = /(?<=tuple<).+(?=>)/
228
+ TUPLE_MEMBERS_SCAN_REGEX = /(tuple\<(?:\g<1>\,)*\g<1>\>|\[(\g<1>)\](?=$)|[^,]+)(?=,|$)/
229
+ SUM_MATCH_REGEX = /((?:(?:\A|\|)(?:[^\|]+))*)\|([^\|]+)\z/
230
+
231
+ # @api private
232
+ module ReferenceHook
233
+ def [](name)
234
+ case name
235
+ when TUPLE_PREFIX_REGEX
236
+ type_map.fetch_or_store(name) do
237
+ key = Regexp.last_match[0]
238
+ types =
239
+ name[TUPLE_MEMBERS_REGEX].
240
+ scan(TUPLE_MEMBERS_SCAN_REGEX).
241
+ map { |(type, rest)| rest.nil? ? self[type] : [self[rest]] }
242
+ super(key).of(*types)
243
+ end
244
+ when SUM_MATCH_REGEX
245
+ type_map.fetch_or_store(name) do
246
+ left, right = Regexp.last_match.captures
247
+ self[left] | super(right)
248
+ end
249
+ else
250
+ super(name)
251
+ end
252
+ end
253
+ end
230
254
 
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))
255
+ singleton_class.prepend ReferenceHook
235
256
  end
236
257
  end
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ 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.4
4
+ version: 0.3.2
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: []