toy_adt 0.1.0 → 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/lib/toy_adt/deconstructable_sorbet_struct.rb +1 -1
- data/lib/toy_adt/matcher.rb +133 -0
- data/lib/toy_adt/public_api.rb +13 -0
- data/lib/toy_adt/version.rb +2 -2
- data/lib/toy_adt.rb +1 -0
- 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: e3d42d697ccf26c13802f9052755b8a2472ed96ed4fc51b1de0559fd77fc3c6f
|
4
|
+
data.tar.gz: 3fff27aff7e04fe74fb86ab350f6f768126f9c39c31f55f4c9146b159c276760
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 40947f6a8f30381e893adaf8a6675832c53bc9a968b7d53a5c013bd815239ed12615cec0840007d37727caa08be9d8f3d92556c8411b7571d0c5703cf7b45a0a
|
7
|
+
data.tar.gz: 2c251dbe86f76e7f837969372c7078c869d9a008747206f8124d177facba235b85d073550ccd38313d9c36a8517c4b3d857d5484d891f7c904792029adba3636
|
@@ -16,7 +16,7 @@ module ToyAdt
|
|
16
16
|
).returns(T::Hash[Symbol, T.untyped])
|
17
17
|
}
|
18
18
|
def deconstruct_keys(keys)
|
19
|
-
return properties.to_h { |k| [k, send(k)] } if keys.
|
19
|
+
return properties.to_h { |k| [k, send(k)] } if keys.nil? || keys.empty?
|
20
20
|
|
21
21
|
(properties & keys).to_h { |k| [k, send(k)] }
|
22
22
|
end
|
@@ -0,0 +1,133 @@
|
|
1
|
+
# typed: false
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require "set"
|
5
|
+
|
6
|
+
module ToyAdt
|
7
|
+
class Matcher
|
8
|
+
extend T::Sig
|
9
|
+
|
10
|
+
POSITIONAL_ARGS = Set[:req, :opt, :rest]
|
11
|
+
KEYWORD_ARGS = Set[:keyreq, :key, :keyrest]
|
12
|
+
BLOCK_ARGS = Set[:block]
|
13
|
+
|
14
|
+
def initialize(from:, &fn)
|
15
|
+
@from = from
|
16
|
+
@fields = from::FIELDS
|
17
|
+
@sub_classes = @fields.to_h { [_1, from.const_get(_1.capitalize)] }
|
18
|
+
|
19
|
+
create_branch_methods
|
20
|
+
|
21
|
+
instance_eval(&fn) if block_given?
|
22
|
+
end
|
23
|
+
|
24
|
+
def else(&fn)
|
25
|
+
@else_fn = fn
|
26
|
+
end
|
27
|
+
|
28
|
+
def call(input)
|
29
|
+
# Find the subclass the input matches to
|
30
|
+
branch_name, _ = @sub_classes.find do |_, klass|
|
31
|
+
matches_type?(klass, input)
|
32
|
+
end
|
33
|
+
|
34
|
+
# If there are none either hit the else or fail hard
|
35
|
+
if branch_name.nil?
|
36
|
+
return @else_fn.call(input) if @else_fn
|
37
|
+
T.absurd(input)
|
38
|
+
end
|
39
|
+
|
40
|
+
# All values to compare against conditions
|
41
|
+
values = input.deconstruct_keys(nil)
|
42
|
+
|
43
|
+
# Try to find the first matching branch function via
|
44
|
+
# conditions:
|
45
|
+
#
|
46
|
+
# value.match do |m|
|
47
|
+
# m.some(value: 0..5) {}
|
48
|
+
# m.some {}
|
49
|
+
# m.else {}
|
50
|
+
# end
|
51
|
+
#
|
52
|
+
# In this case a "Some" type with a value matching will hit
|
53
|
+
# first. Granted less specific branches going first means
|
54
|
+
# those get hit first, so not recommended.
|
55
|
+
_, branch_fn = @branches.find do |branch_condition, _|
|
56
|
+
name, conditions = branch_condition
|
57
|
+
|
58
|
+
# First thing is the key is a tuple of subclass / branch
|
59
|
+
# name, the second part is the condition if there are any.
|
60
|
+
branch_name == name && conditions.all? { |k, condition|
|
61
|
+
matches_type?(condition, values[k])
|
62
|
+
}
|
63
|
+
end
|
64
|
+
|
65
|
+
return branch_fn.call(input) if branch_fn
|
66
|
+
return @else_fn.call(input) if @else_fn
|
67
|
+
|
68
|
+
T.absurd(input)
|
69
|
+
end
|
70
|
+
|
71
|
+
alias_method :===, :call
|
72
|
+
|
73
|
+
def to_proc
|
74
|
+
-> input { call(input) }
|
75
|
+
end
|
76
|
+
|
77
|
+
# Making this respond to both `===` and the sorbet type
|
78
|
+
# validation variants.
|
79
|
+
private def matches_type?(type, value)
|
80
|
+
type === value || sorbet_is_a?(type, value)
|
81
|
+
end
|
82
|
+
|
83
|
+
private def sorbet_is_a?(type, value)
|
84
|
+
T::Types::Base === type && type.valid?(value)
|
85
|
+
end
|
86
|
+
|
87
|
+
private def create_branch_methods
|
88
|
+
@branches = {}
|
89
|
+
|
90
|
+
@fields.each do |field|
|
91
|
+
define_singleton_method(field) do |**conditions, &fn|
|
92
|
+
param_type = get_param_type(fn)
|
93
|
+
param_names = fn.parameters.map(&:last)
|
94
|
+
|
95
|
+
# Trying to make it play nicely with pattern matching
|
96
|
+
# semantics, hitting both positional and keywords but
|
97
|
+
# also no-args.
|
98
|
+
@branches[[field, conditions]] = -> input do
|
99
|
+
case param_type
|
100
|
+
when :keyword
|
101
|
+
fn.call(**input.deconstruct_keys(param_names))
|
102
|
+
when :positional
|
103
|
+
fn.call(*input.deconstruct) # Should probably disable this
|
104
|
+
when :none
|
105
|
+
fn.call
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
private def get_param_type(fn)
|
113
|
+
param_types = fn.parameters.map(&:first)
|
114
|
+
|
115
|
+
has_positional = param_types.any?(POSITIONAL_ARGS)
|
116
|
+
has_keywords = param_types.any?(KEYWORD_ARGS)
|
117
|
+
has_block = param_types.any?(BLOCK_ARGS)
|
118
|
+
|
119
|
+
if has_block
|
120
|
+
raise ArgumentError, "Cannot use function arguments"
|
121
|
+
end
|
122
|
+
|
123
|
+
if has_positional && has_keywords
|
124
|
+
raise ArgumentError, "Cannot have both positional and keyword arguments"
|
125
|
+
end
|
126
|
+
|
127
|
+
return :keyword if has_keywords
|
128
|
+
return :positional if has_positional
|
129
|
+
|
130
|
+
:none
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
data/lib/toy_adt/public_api.rb
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
# typed: false
|
2
2
|
require "sorbet-runtime"
|
3
|
+
require "set"
|
3
4
|
|
4
5
|
module ToyAdt
|
5
6
|
module PublicApi
|
@@ -34,7 +35,12 @@ module ToyAdt
|
|
34
35
|
extend T::Helpers
|
35
36
|
sealed!
|
36
37
|
|
38
|
+
const_set(:FIELDS, types.keys)
|
39
|
+
|
37
40
|
module_eval(&fn) if block_given?
|
41
|
+
|
42
|
+
def self.match(&fn) = Matcher.new(from: self, &fn)
|
43
|
+
def self.match_call(input, &fn) = match(&fn).call(input)
|
38
44
|
end
|
39
45
|
|
40
46
|
types.each do |type, config|
|
@@ -42,12 +48,19 @@ module ToyAdt
|
|
42
48
|
include container_module
|
43
49
|
include DeconstructableSorbetStruct
|
44
50
|
|
51
|
+
const_set(:CONTAINER, container_module)
|
52
|
+
|
45
53
|
config.each do |field, type|
|
46
54
|
const field, type
|
47
55
|
end
|
56
|
+
|
57
|
+
def match(&fn) = self.class::CONTAINER.match(&fn).call(self)
|
48
58
|
end
|
49
59
|
|
50
60
|
container_module.const_set(type.capitalize, klass)
|
61
|
+
container_module.define_method(type) do
|
62
|
+
klass
|
63
|
+
end
|
51
64
|
end
|
52
65
|
|
53
66
|
container_module
|
data/lib/toy_adt/version.rb
CHANGED
data/lib/toy_adt.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: toy_adt
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Brandon Weaver
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2022-05-
|
11
|
+
date: 2022-05-28 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rspec
|
@@ -84,6 +84,7 @@ files:
|
|
84
84
|
- Rakefile
|
85
85
|
- lib/toy_adt.rb
|
86
86
|
- lib/toy_adt/deconstructable_sorbet_struct.rb
|
87
|
+
- lib/toy_adt/matcher.rb
|
87
88
|
- lib/toy_adt/public_api.rb
|
88
89
|
- lib/toy_adt/version.rb
|
89
90
|
- sorbet/config
|