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 +4 -4
- data/.rubocop.yml +1 -1
- data/CHANGELOG.md +8 -0
- data/README.md +4 -4
- data/dry-types-tuple.gemspec +7 -3
- data/lib/dry/tuple/class_decorator.rb +68 -0
- data/lib/dry/tuple/class_interface.rb +44 -0
- data/lib/dry/tuple/struct.rb +75 -0
- data/lib/dry/tuple/type_coercer.rb +36 -0
- data/lib/dry/tuple/version.rb +7 -0
- data/lib/dry/tuple.rb +27 -121
- data/lib/dry/types/tuple.rb +131 -110
- data/lib/dry-types-tuple.rb +3 -0
- metadata +39 -8
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 37df5ee0299ce0e65a395a458f0284fa540f1f4c3cbc5f09ca7424a9b5ca1ca3
|
4
|
+
data.tar.gz: 0d83445514c90cf9381c49fb805b98d49ef2c9c6a00c776fc921ba39fda6a5d0
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e1c4fc70ed670e992613b22444d68fef3a4fcddb6459405657272099dc54caec158f3a77e20ed00df352de869e922188dcac6ba2241aecaec36d5995ae2dd234
|
7
|
+
data.tar.gz: 1a077702745b571a3c0c5d8ec8dd00aa1885971b054dc1b96e23a026b1b24798756e496f653eebdaad611b034ab3e0607934e4c5b75440a930e394eebeb7214c
|
data/.rubocop.yml
CHANGED
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
|
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
|
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', {}]]
|
119
|
-
# => #<
|
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.
|
data/dry-types-tuple.gemspec
CHANGED
@@ -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 =
|
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 = ">=
|
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
|
data/lib/dry/tuple.rb
CHANGED
@@ -1,6 +1,8 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require
|
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
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
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
|
-
|
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
|
-
|
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
|
data/lib/dry/types/tuple.rb
CHANGED
@@ -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 <
|
14
|
+
class Tuple < Nominal
|
15
|
+
# VERSION ||= Dry::Tuple::VERSION
|
16
|
+
|
13
17
|
# Build a tuple type.
|
14
18
|
#
|
15
|
-
# @overload
|
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
|
-
|
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
|
-
# @
|
31
|
-
# @
|
32
|
-
# @return [
|
29
|
+
# @see self.extract_rest
|
30
|
+
# @param types [Array<Type>]
|
31
|
+
# @return [Hash { Integer => Type }]
|
33
32
|
def self.build_index(types)
|
34
|
-
|
35
|
-
|
36
|
-
|
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
|
41
|
-
#
|
42
|
-
# @
|
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
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
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
|
-
|
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
|
-
|
56
|
-
|
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
|
-
|
132
|
-
|
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 {
|
142
|
-
structure << [rest_type.to_ast(meta:
|
143
|
-
structure << meta ?
|
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? ||
|
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
|
-
|
152
|
+
item = types_index[index]&.try(value)
|
169
153
|
|
170
|
-
if
|
154
|
+
if item.nil?
|
171
155
|
failures << CoercionError.new("#{value.inspect} not fits to the fixed-size tuple")
|
172
156
|
break
|
173
|
-
elsif
|
174
|
-
failures <<
|
157
|
+
elsif item.failure?
|
158
|
+
failures << item.error
|
175
159
|
break
|
176
160
|
else
|
177
|
-
output <<
|
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
|
176
|
+
# @see Tuple.build
|
193
177
|
# @overload Tuple(*fixed_types, rest_type)
|
194
|
-
# @param [Array<
|
195
|
-
# @param [Array(
|
196
|
-
# @return [
|
197
|
-
def Tuple(
|
198
|
-
Tuple.build(
|
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) {
|
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 << (
|
220
|
-
|
221
|
-
|
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
|
-
|
215
|
+
nominal_tuple = Tuple.build([self['any']])
|
216
|
+
|
217
|
+
register "nominal.tuple", nominal_tuple
|
228
218
|
|
229
|
-
|
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
|
-
|
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
|
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.
|
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:
|
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:
|
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.
|
125
|
-
signing_key:
|
156
|
+
rubygems_version: 3.6.9
|
126
157
|
specification_version: 4
|
127
158
|
summary: Dry::Types::Tuple
|
128
159
|
test_files: []
|