dry-types-tuple 0.0.2 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6e6edbbb2c8dc709f36d3cfcdd63bab007411ad71106549f9a25986588d1d0af
4
- data.tar.gz: 0fde381de860f1aa66dd3e35df16f611ea28cafbab83e45c75eae19b5452f0a4
3
+ metadata.gz: 7583440f28dfcf7be606ffade7d58fe0fac29e783c4b79eeb15467ff6b63c654
4
+ data.tar.gz: 27da3385c06793c20d7ece1872d740ebc9cc431dc795c9a3141de4a5d64314ee
5
5
  SHA512:
6
- metadata.gz: 11393c3bd16005e8fca81b0b9e6483a8708f2bd7274f4f01eb70c0761ff2480492efd65bc332963b6c5c4c25698c9d9533ba2030407778e7ecc83279afe3185f
7
- data.tar.gz: 233646bf141f872a84a011090fd3802678b88439662ec78e536fa08ead8c637aa203b22022f3f7e400fe7dc19c44a0565da5d40fe29a1381cfd22811ab45b8e0
6
+ metadata.gz: 1886d2dabab95dc8d07ba7285991b73e70bd6083d81280c0e2ec6dd3238e20889d4a186edc012d8e381523906b6d730f2782e285112ce17385c0db33e5003594
7
+ data.tar.gz: 4e6f489907102ca633736d9109725a96bcae2d677b25feca5daead2d3f69e32368410006325c3776c6e6196ea5dd0e9ff7b3126d5fd3776dc5f07e3d747a4051
data/Gemfile CHANGED
@@ -13,6 +13,9 @@ group :test do
13
13
  gem "rspec"
14
14
  gem "warning"
15
15
  gem "rspec-instafail", require: false
16
+ gem "dry-initializer", require: false
17
+ gem "dry-inflector"
18
+ gem "dry-struct"
16
19
  end
17
20
 
18
21
  group :tools do
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.
@@ -2,7 +2,7 @@
2
2
 
3
3
  Gem::Specification.new do |spec|
4
4
  spec.name = "dry-types-tuple"
5
- spec.version = '0.0.2'
5
+ spec.version = '0.1.0'
6
6
  spec.authors = ["Anton"]
7
7
  spec.email = ["anton.estum@gmail.com"]
8
8
 
data/lib/dry/tuple.rb ADDED
@@ -0,0 +1,142 @@
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
+ if defined?(super)
55
+ resolve_tuple_safe(input) { |output| super(output) }
56
+ elsif input.is_a?(self)
57
+ input
58
+ else
59
+ resolve_tuple_safe(input, &block)
60
+ end
61
+ end
62
+
63
+ # @api private
64
+ def call_unsafe(input)
65
+ if defined?(super)
66
+ resolve_tuple_safe(input) { |output| super(output) }
67
+ elsif input.is_a?(self)
68
+ input
69
+ else
70
+ resolve_tuple_unsafe(input)
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
81
+ 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
+ end
109
+
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
122
+
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
141
+ end
142
+ end
@@ -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]
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.0.2
4
+ version: 0.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Anton
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2023-01-26 00:00:00.000000000 Z
11
+ date: 2023-01-31 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: