dry-types-tuple 0.0.2 → 0.1.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 +4 -4
- data/Gemfile +3 -0
- data/README.md +77 -0
- data/dry-types-tuple.gemspec +1 -1
- data/lib/dry/tuple.rb +144 -0
- data/lib/dry/types/tuple.rb +7 -2
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 1847f0ad55cbc51ed75bdf0ff5a49a89876be7c714dfea8778407aa76b65b051
|
4
|
+
data.tar.gz: b859c0d7f1c5110a529362a92b8ac22c136452d59a26867cbfd2daff0f30f239
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 6e03425bd2b5f380c11cac1c55664615bd212474f9233fffbec94aa4547f467885274a426c5db97b9974d003fa7c4f64d332e2744a236c254f4ea7a14f22cc26
|
7
|
+
data.tar.gz: 534cbf77bc3680c8ec72010c8b315bd28c109bcd4d82eb1674bf0ea6b0654933eb8a555d87fae84084d1f57b27f1ace203aad7e238272aa5b0314231e6eb44a7
|
data/Gemfile
CHANGED
data/README.md
CHANGED
@@ -58,6 +58,83 @@ Dry::Types['tuple'].of \
|
|
58
58
|
[Dry::Types['float'] | Dry::Types['integer']]
|
59
59
|
```
|
60
60
|
|
61
|
+
### Class interface mixins
|
62
|
+
|
63
|
+
The extra feature of the gem is tuple class interfaces mixins under the `Dry::Tuple` namespace. It isn't loaded by default,
|
64
|
+
so it's dependent on `require "dry/tuple"`.
|
65
|
+
|
66
|
+
#### Class interface for PORO-classes
|
67
|
+
|
68
|
+
The interfaces provide a way to use a tuple in constructors to match proper class by input.
|
69
|
+
The behaviour is similar to the one provided by `Dry::Struct::ClassInterface` and
|
70
|
+
allows to combine classes into sums but with positional input validation & coercion.
|
71
|
+
|
72
|
+
To do so, extend the class with `Dry::Tuple::ClassInterface` mixin and then assign an
|
73
|
+
explicit tuple type by calling `tuple(type_declation)` method.
|
74
|
+
|
75
|
+
With a couple of extended classes you will be able to compose a summing object, which
|
76
|
+
could be used to match incoming values by tuples. When input matches any of summed
|
77
|
+
tuples, it yields thru the type, performing coercions if need, but a top-level structure
|
78
|
+
of input keeps to be an Array. There are two abstract class methods — `coerce_tuple` and
|
79
|
+
`new_from_tuple`, — that could be redefined when, i.e., you need to splat an incoming
|
80
|
+
array into arguments due to avoid breaking the existing interface.
|
81
|
+
|
82
|
+
```ruby
|
83
|
+
class Example
|
84
|
+
extend Dry::Tuple::ClassInterface
|
85
|
+
tuple Types.Tuple(Types.Value(:example) << Types::Coercible::Symbol, Types::String)
|
86
|
+
|
87
|
+
def initializer(left, right)
|
88
|
+
@left, @right = left, right
|
89
|
+
end
|
90
|
+
|
91
|
+
# @note Used by {StructClassInterface} under the hood.
|
92
|
+
# @param input [Array] after sub types coercion
|
93
|
+
# @return [Any] args acceptable by {#initializer}.
|
94
|
+
# def self.coerce_tuple(input)
|
95
|
+
# input
|
96
|
+
# end
|
97
|
+
|
98
|
+
# @param input [Any] after {.coerce_tuple}
|
99
|
+
# @return [self] instantiated object with the given arguments
|
100
|
+
def self.new_from_tuple(input)
|
101
|
+
new(*input)
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
class OtherExample < Example
|
106
|
+
tuple Types.Tuple(Types.Value(:other_example) << Types::Coercible::Symbol, [Types::Any])
|
107
|
+
|
108
|
+
def initializer(left, right, *rest)
|
109
|
+
super(left, right)
|
110
|
+
@rest = rest
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
ExampleSum = Example | OtherExample
|
115
|
+
ExampleSum[['example', 'foo']]
|
116
|
+
# => #<Example @left = :example, @right = 'foo'>
|
117
|
+
|
118
|
+
ExampleSum[['other_example', 1, '2', {}]].class
|
119
|
+
# => #<Example @left = :other_example, @right = 1, @rest = ['2', {}]>
|
120
|
+
```
|
121
|
+
|
122
|
+
#### Class interface for Dry::Struct classes.
|
123
|
+
|
124
|
+
And, the initial target of this gem, — let `Dry::Struct` classes to take both the key-value and
|
125
|
+
the tuple inputs. Extend `Dry::Struct` classes with the `Dry::Tuple::StructClassInterface`,
|
126
|
+
ensure keys order with helper method `auto_tuple *keys` (it will auto-declare the tuple from the struct's schema)…
|
127
|
+
Profit!
|
128
|
+
|
129
|
+
```ruby
|
130
|
+
class SomeStruct < Dry::Struct
|
131
|
+
attribute :some, Types::Integer
|
132
|
+
attribute :with, Types::Hash
|
133
|
+
extend ::Dry::Tuple::StructClassInterface
|
134
|
+
auto_tuple :some, :with
|
135
|
+
end
|
136
|
+
```
|
137
|
+
|
61
138
|
## Development
|
62
139
|
|
63
140
|
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
data/dry-types-tuple.gemspec
CHANGED
data/lib/dry/tuple.rb
ADDED
@@ -0,0 +1,144 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'dry/types/tuple'
|
4
|
+
|
5
|
+
module Dry
|
6
|
+
# The namespace contains mixins for class decoration with tuple type.
|
7
|
+
# The interfaces provide a way to use a tuple in constructors to match proper class by input.
|
8
|
+
# The behaviour is similar to the one provided by `Dry::Struct::ClassInterface` and
|
9
|
+
# allows to combine classes into sums but with positional input validation & coercion.
|
10
|
+
#
|
11
|
+
# @example Splat input
|
12
|
+
# class Example
|
13
|
+
# extend Dry::Tuple::ClassInterface
|
14
|
+
# tuple Types.Tuple(Types::Integer, Types::String)
|
15
|
+
#
|
16
|
+
# def initializer(a1, a2)
|
17
|
+
# # ...
|
18
|
+
# end
|
19
|
+
#
|
20
|
+
# def self.new_from_tuple(input)
|
21
|
+
# new(*input)
|
22
|
+
# end
|
23
|
+
# end
|
24
|
+
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
|
+
case input
|
55
|
+
when self
|
56
|
+
input
|
57
|
+
when Array
|
58
|
+
resolve_tuple_safe(input, &block)
|
59
|
+
else
|
60
|
+
super
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
# @api private
|
65
|
+
def call_unsafe(input)
|
66
|
+
case input
|
67
|
+
when self
|
68
|
+
input
|
69
|
+
when Array
|
70
|
+
resolve_tuple_unsafe(input)
|
71
|
+
else
|
72
|
+
super
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
private
|
77
|
+
|
78
|
+
# @api private
|
79
|
+
def resolve_tuple_safe(input)
|
80
|
+
input = tuple.call_safe(input) do |output = input|
|
81
|
+
output = yield(output) if block_given?
|
82
|
+
return output
|
83
|
+
end
|
84
|
+
new_from_tuple(coerce_tuple(input))
|
85
|
+
end
|
86
|
+
|
87
|
+
# @api private
|
88
|
+
def resolve_tuple_unsafe(input)
|
89
|
+
input = tuple.call_unsafe(input)
|
90
|
+
new_from_tuple(coerce_tuple(input))
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
module HookExtendObject
|
95
|
+
# Makes the module's features to be prepended instead of appended to the target class when extended.
|
96
|
+
# Also defines the `tuple` class attribute.
|
97
|
+
# @api private
|
98
|
+
private def extend_object(base)
|
99
|
+
base.singleton_class.prepend(self)
|
100
|
+
base.defines :tuple
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
module ClassInterface
|
105
|
+
include Core::ClassAttributes
|
106
|
+
include Types::Type
|
107
|
+
include Types::Builder
|
108
|
+
include ClassDecorator
|
109
|
+
extend HookExtendObject
|
110
|
+
end
|
111
|
+
|
112
|
+
# Extracted due to make it possible to use this feature within {Dry::Struct} classes.
|
113
|
+
# @example extending Dry::Struct subclass
|
114
|
+
#
|
115
|
+
# class SomeStruct < Dry::Struct
|
116
|
+
# attribute :some, Types::Integer
|
117
|
+
# attribute :with, Types::Hash
|
118
|
+
# extend ::Dry::Tuple::StructClassInterface
|
119
|
+
# auto_tuple :some, :with
|
120
|
+
# end
|
121
|
+
module StructClassInterface
|
122
|
+
include ClassDecorator
|
123
|
+
extend HookExtendObject
|
124
|
+
|
125
|
+
class << self
|
126
|
+
private def extend_object(base)
|
127
|
+
super
|
128
|
+
base.defines :keys_order
|
129
|
+
base.keys_order []
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
def auto_tuple(*keys)
|
134
|
+
keys_order(keys_order | keys)
|
135
|
+
index = schema.keys.map { |t| [t.name, t.type] }.to_h
|
136
|
+
tuple Dry::Types::Tuple.build_unsplat(index.values_at(*keys_order))
|
137
|
+
end
|
138
|
+
|
139
|
+
def coerce_tuple(input)
|
140
|
+
keys_order.zip(input).to_h
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
data/lib/dry/types/tuple.rb
CHANGED
@@ -18,6 +18,11 @@ module Dry
|
|
18
18
|
# @see build_index
|
19
19
|
# @return [Dry::Types::Tuple]
|
20
20
|
def self.build(*types)
|
21
|
+
build_unsplat(types)
|
22
|
+
end
|
23
|
+
|
24
|
+
# @api private
|
25
|
+
def self.build_unsplat(types)
|
21
26
|
new(::Array, types_index: build_index(types))
|
22
27
|
end
|
23
28
|
|
@@ -41,7 +46,7 @@ module Dry
|
|
41
46
|
end
|
42
47
|
|
43
48
|
if types[-1].size > 1
|
44
|
-
raise ArgumentError, "rest_type should be an Array with single element to apply to the rest of items"
|
49
|
+
raise ArgumentError, "rest_type should be an Array with single element to apply to the rest of items: #{types[-1]}"
|
45
50
|
end
|
46
51
|
|
47
52
|
types.pop[0]
|
@@ -210,7 +215,7 @@ module Dry
|
|
210
215
|
yield "#{header}>#{opts}"
|
211
216
|
else
|
212
217
|
yield header.dup << (types.map { |pos, pos_type|
|
213
|
-
visit(pos_type) { |type| "
|
218
|
+
visit(pos_type) { |type| "#{pos}: #{type}" }
|
214
219
|
} << rest).compact.join(", ") << ">#{opts}"
|
215
220
|
end
|
216
221
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
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.1.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Anton
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2023-
|
11
|
+
date: 2023-02-06 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: dry-types
|
@@ -96,6 +96,7 @@ files:
|
|
96
96
|
- README.md
|
97
97
|
- Rakefile
|
98
98
|
- dry-types-tuple.gemspec
|
99
|
+
- lib/dry/tuple.rb
|
99
100
|
- lib/dry/types/tuple.rb
|
100
101
|
homepage: https://github.com/estum/dry-types-tuple
|
101
102
|
licenses:
|