toy_adt 0.1.0 → 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1f45563ee2b678fd2d1ad8dd632d05fee56b16dbc46b5b4952082be20f48dea4
4
- data.tar.gz: 56ea2b517926e966389b211ccc30d2d9c2113d916996b514146519c2646eef52
3
+ metadata.gz: e3d42d697ccf26c13802f9052755b8a2472ed96ed4fc51b1de0559fd77fc3c6f
4
+ data.tar.gz: 3fff27aff7e04fe74fb86ab350f6f768126f9c39c31f55f4c9146b159c276760
5
5
  SHA512:
6
- metadata.gz: 90beb64a03734f4c5405a15165b939298b33cac5bf0acb1b3422083df919aa9d4ff10325c36d983f4254ce770dfbeae8792a7ea247ad6d92e8a779ed449f3f64
7
- data.tar.gz: c0a93e38be8bdfa02b24599ed4a49cd6aafda170e1488a82426497d2a7d07e0ee1540a1167e7a34bd2bdd3c15a986d47d4ce38d034c143dc129e778a6cf2ab1d
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.empty? || keys.nil?
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
@@ -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
@@ -1,6 +1,6 @@
1
- # typed: strict
1
+ # typed: false
2
2
  # frozen_string_literal: true
3
3
 
4
4
  module ToyAdt
5
- VERSION = "0.1.0"
5
+ VERSION = "0.1.1"
6
6
  end
data/lib/toy_adt.rb CHANGED
@@ -3,6 +3,7 @@
3
3
 
4
4
  require_relative "toy_adt/version"
5
5
  require_relative "toy_adt/deconstructable_sorbet_struct"
6
+ require_relative "toy_adt/matcher"
6
7
  require_relative "toy_adt/public_api"
7
8
 
8
9
  module ToyAdt
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.0
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-26 00:00:00.000000000 Z
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