typed_cache 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.
Files changed (52) hide show
  1. checksums.yaml +7 -0
  2. checksums.yaml.gz.sig +0 -0
  3. data/LICENSE +201 -0
  4. data/README.md +168 -0
  5. data/examples.md +190 -0
  6. data/lib/typed_cache/backend.rb +16 -0
  7. data/lib/typed_cache/backends/active_support.rb +113 -0
  8. data/lib/typed_cache/backends/memory.rb +166 -0
  9. data/lib/typed_cache/backends.rb +34 -0
  10. data/lib/typed_cache/cache_builder.rb +77 -0
  11. data/lib/typed_cache/cache_key.rb +45 -0
  12. data/lib/typed_cache/cache_ref.rb +155 -0
  13. data/lib/typed_cache/clock.rb +23 -0
  14. data/lib/typed_cache/decorator.rb +12 -0
  15. data/lib/typed_cache/decorators.rb +35 -0
  16. data/lib/typed_cache/either.rb +121 -0
  17. data/lib/typed_cache/errors.rb +64 -0
  18. data/lib/typed_cache/instrumentation.rb +112 -0
  19. data/lib/typed_cache/maybe.rb +92 -0
  20. data/lib/typed_cache/namespace.rb +162 -0
  21. data/lib/typed_cache/registry.rb +55 -0
  22. data/lib/typed_cache/snapshot.rb +72 -0
  23. data/lib/typed_cache/store/instrumented.rb +83 -0
  24. data/lib/typed_cache/store.rb +152 -0
  25. data/lib/typed_cache/version.rb +5 -0
  26. data/lib/typed_cache.rb +58 -0
  27. data/sig/generated/typed_cache/backend.rbs +17 -0
  28. data/sig/generated/typed_cache/backends/active_support.rbs +56 -0
  29. data/sig/generated/typed_cache/backends/memory.rbs +95 -0
  30. data/sig/generated/typed_cache/backends.rbs +21 -0
  31. data/sig/generated/typed_cache/cache_builder.rbs +37 -0
  32. data/sig/generated/typed_cache/cache_key.rbs +33 -0
  33. data/sig/generated/typed_cache/cache_ref.rbs +91 -0
  34. data/sig/generated/typed_cache/clock.rbs +15 -0
  35. data/sig/generated/typed_cache/decorator.rbs +14 -0
  36. data/sig/generated/typed_cache/decorators.rbs +25 -0
  37. data/sig/generated/typed_cache/either.rbs +106 -0
  38. data/sig/generated/typed_cache/errors.rbs +51 -0
  39. data/sig/generated/typed_cache/instrumentation.rbs +30 -0
  40. data/sig/generated/typed_cache/maybe.rbs +85 -0
  41. data/sig/generated/typed_cache/namespace.rbs +130 -0
  42. data/sig/generated/typed_cache/registry.rbs +25 -0
  43. data/sig/generated/typed_cache/snapshot.rbs +50 -0
  44. data/sig/generated/typed_cache/store/instrumented.rbs +37 -0
  45. data/sig/generated/typed_cache/store.rbs +104 -0
  46. data/sig/generated/typed_cache/version.rbs +5 -0
  47. data/sig/generated/typed_cache.rbs +34 -0
  48. data/sig/handwritten/gems/zeitwerk/2.7/zeitwerk.rbs +9 -0
  49. data/typed_cache.gemspec +42 -0
  50. data.tar.gz.sig +0 -0
  51. metadata +228 -0
  52. metadata.gz.sig +0 -0
@@ -0,0 +1,166 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'singleton'
4
+ require 'forwardable'
5
+
6
+ module TypedCache
7
+ class MemoryStoreRegistry
8
+ include Singleton
9
+ extend Forwardable
10
+
11
+ def_delegators :@backing_store, :[], :[]=, :delete, :key?, :keys
12
+
13
+ #: -> void
14
+ def initialize
15
+ @backing_store = Concurrent::Map.new
16
+ end
17
+ end
18
+
19
+ module Backends
20
+ # A type-safe memory store implementation with built-in namespacing
21
+ # @rbs generic V
22
+ class Memory
23
+ include Backend #[V]
24
+
25
+ # @rbs!
26
+ # interface _HashLike[K, V]
27
+ # def []: (K) -> V?
28
+ # def []=: (K, V) -> V
29
+ # def delete: (K) -> V?
30
+ # def key?: (K) -> bool
31
+ # def keys: () -> Array[K]
32
+ # end
33
+ #
34
+ # type hash_like[K, V] = _HashLike[K, V]
35
+
36
+ # @private
37
+ # @rbs generic V
38
+ class Entry < Dry::Struct
39
+ attribute :value, Dry.Types::Any
40
+ attribute :expires_at, Dry.Types::Time
41
+
42
+ # @rbs! attr_accessor expires_at: Time
43
+ # @rbs! attr_reader value: V
44
+
45
+ # @rbs (value: V, expires_in: Integer) -> Entry[V]
46
+ def self.expiring(value:, expires_in:)
47
+ new(value: value, expires_at: Clock.moment + expires_in)
48
+ end
49
+
50
+ # @rbs () -> bool
51
+ def expired?
52
+ Clock.moment >= expires_at
53
+ end
54
+ end
55
+ private_constant :Entry
56
+
57
+ attr_reader :namespace, :ttl #: Namespace, Integer
58
+ attr_reader :backing_store #: hash_like[CacheKey, Entry[V]]
59
+
60
+ #: (Namespace, shared: bool, ttl: Integer) -> void
61
+ def initialize(namespace, shared: false, ttl: 600)
62
+ @namespace = namespace
63
+ @ttl = ttl
64
+ @backing_store = shared ? MemoryStoreRegistry.instance : {}
65
+ end
66
+
67
+ # @rbs override
68
+ #: (cache_key) -> either[Error, Snapshot[V]]
69
+ def get(key)
70
+ key = namespaced_key(key)
71
+ return Either.left(CacheMissError.new(key)) unless backing_store.key?(key)
72
+
73
+ entry = backing_store[key]
74
+
75
+ if entry.expired?
76
+ backing_store.delete(key)
77
+ return Either.left(CacheMissError.new(key))
78
+ end
79
+
80
+ Either.right(Snapshot.new(entry.value, source: :cache))
81
+ end
82
+
83
+ # @rbs override
84
+ #: (cache_key, V) -> either[Error, Snapshot[V]]
85
+ def set(key, value)
86
+ key = namespaced_key(key)
87
+ expires_at = Clock.moment + @ttl
88
+ entry = Entry.new(value: value, expires_at: expires_at)
89
+ backing_store[key] = entry
90
+ Either.right(Snapshot.new(value, source: :cache))
91
+ rescue => e
92
+ Either.left(StoreError.new(
93
+ :set,
94
+ key,
95
+ "Failed to store value for key '#{key}': #{e.message}",
96
+ e,
97
+ ))
98
+ end
99
+
100
+ # @rbs override
101
+ #: (cache_key) -> either[Error, Snapshot[V]]
102
+ def delete(key)
103
+ key = namespaced_key(key)
104
+ entry = backing_store.delete(key)
105
+ if entry.nil?
106
+ Either.left(CacheMissError.new(key))
107
+ else
108
+ Either.right(Snapshot.new(entry.value, source: :cache))
109
+ end
110
+ end
111
+
112
+ # @rbs override
113
+ #: (cache_key) -> bool
114
+ def key?(key)
115
+ key = namespaced_key(key)
116
+ return false unless backing_store.key?(key) && key.belongs_to?(namespace)
117
+
118
+ entry = backing_store[key]
119
+ !entry.expired?
120
+ end
121
+
122
+ # @rbs override
123
+ #: -> maybe[Error]
124
+ def clear
125
+ keys_to_delete = backing_store.keys.select { |k| k.belongs_to?(namespace) }
126
+ keys_to_delete.each { |key| backing_store.delete(key) }
127
+ Maybe.none
128
+ rescue => e
129
+ Maybe.some(e)
130
+ end
131
+
132
+ # @rbs override
133
+ #: -> String
134
+ def store_type
135
+ 'memory'
136
+ end
137
+
138
+ #: -> Integer
139
+ def size
140
+ purge_expired_keys
141
+ backing_store.keys.count { |k| k.belongs_to?(namespace) }
142
+ end
143
+
144
+ #: -> Array[CacheKey]
145
+ def keys
146
+ purge_expired_keys
147
+ backing_store.keys
148
+ .select { |k| k.belongs_to?(namespace) }
149
+ end
150
+
151
+ private
152
+
153
+ #: -> Hash[CacheKey, Entry[V]]
154
+ def namespaced_entries
155
+ backing_store.select { |key, _entry| key.belongs_to?(namespace) }
156
+ end
157
+
158
+ #: -> void
159
+ def purge_expired_keys
160
+ namespaced_entries.each do |key, entry|
161
+ backing_store.delete(key) if entry.expired?
162
+ end
163
+ end
164
+ end
165
+ end
166
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'dry/struct'
4
+ require 'dry/types'
5
+
6
+ require_relative 'backends/memory'
7
+ require_relative 'backends/active_support'
8
+
9
+ module TypedCache
10
+ module Backends
11
+ # Backend registry using composition
12
+ REGISTRY = Registry.new('backend', {
13
+ memory: Memory,
14
+ active_support: ActiveSupport,
15
+ }).freeze
16
+
17
+ private_constant :REGISTRY
18
+
19
+ class << self
20
+ extend Forwardable
21
+ delegate [:resolve, :register, :available, :registered?] => :registry
22
+
23
+ #: -> Registry
24
+ def registry = REGISTRY
25
+
26
+ # Convenience method delegating to registry
27
+ # @rbs!
28
+ # def resolve: (Symbol, *untyped, **untyped) -> either[Error, Store[untyped]]
29
+ # def available: -> Array[Symbol]
30
+ # def register: (Symbol, Class) -> either[Error, void]
31
+ # def registered?: (Symbol) -> bool
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TypedCache
4
+ class CacheBuilder
5
+ # @rbs! type config = TypedCache::typed_cache_config
6
+
7
+ # @rbs (config, Registry[backend[untyped]], Registry[decorator[untyped]]) -> void
8
+ def initialize(config, backend_registry = Backends, decorator_registry = Decorators)
9
+ @config = config
10
+ @backend_registry = backend_registry
11
+ @decorator_registry = decorator_registry
12
+
13
+ @backend_name = nil
14
+ @backend_args = []
15
+ @backend_options = {}
16
+ @decorators = []
17
+ end
18
+
19
+ # Builds the cache - the only method that can fail
20
+ # @rbs (?Namespace) -> either[Error, Store[V]]
21
+ def build(namespace = Namespace.at(@config.default_namespace))
22
+ validate_and_build(namespace)
23
+ end
24
+
25
+ # Familiar Ruby fluent interface - always succeeds
26
+ # Invalid configurations are caught during build()
27
+ # @rbs (Symbol, *untyped, **untyped) -> self
28
+ def with_backend(name, *args, **options)
29
+ @backend_name = name
30
+ @backend_args = args
31
+ @backend_options = options
32
+ self
33
+ end
34
+
35
+ # Adds an arbitrary decorator by registry key
36
+ # @rbs (Symbol) -> self
37
+ def with_decorator(name)
38
+ @decorators << name
39
+ self
40
+ end
41
+
42
+ # @rbs () -> self
43
+ def with_instrumentation
44
+ with_decorator(:instrumented)
45
+ end
46
+
47
+ private
48
+
49
+ # @rbs (Namespace) -> either[Error, Store[V]]
50
+ def validate_and_build(namespace)
51
+ create_store(namespace).bind do |store|
52
+ apply_decorators(store)
53
+ end
54
+ end
55
+
56
+ # @rbs (Namespace) -> either[Error, Store[V]]
57
+ def create_store(namespace)
58
+ return Either.left(ArgumentError.new('Backend not configured')) unless @backend_name
59
+
60
+ # Prepend namespace to the arguments for the backend constructor
61
+ @backend_registry.resolve(@backend_name, namespace, *@backend_args, **@backend_options)
62
+ end
63
+
64
+ # @rbs (Store[V]) -> either[Error, Store[V]]
65
+ def apply_decorators(store)
66
+ return Either.right(store) if @decorators.empty?
67
+
68
+ @decorators.reduce(Either.right(store)) do |result, decorator_name|
69
+ result.bind do |current_store|
70
+ @decorator_registry.resolve(decorator_name, current_store)
71
+ end
72
+ end
73
+ rescue => e
74
+ Either.left(StoreError.new(:decorator_application, 'decorator', "Failed to apply decorator: #{e.message}", e))
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TypedCache
4
+ class CacheKey
5
+ extend Forwardable
6
+
7
+ attr_reader :namespace #: Namespace
8
+ attr_reader :key #: String
9
+
10
+ # @rbs (Namespace, String) -> void
11
+ def initialize(namespace, key)
12
+ @namespace = namespace
13
+ @key = key
14
+ end
15
+
16
+ # @rbs (Namespace) -> bool
17
+ def belongs_to?(namespace)
18
+ @namespace.to_s.start_with?(namespace.to_s)
19
+ end
20
+
21
+ # @rbs () -> String
22
+ def to_s
23
+ "#{@namespace}:#{@key}"
24
+ end
25
+
26
+ alias cache_key to_s
27
+
28
+ # @rbs () -> String
29
+ def inspect
30
+ "#<#{self.class} namespace=#{@namespace} key=#{@key}>"
31
+ end
32
+
33
+ # @rbs () -> Integer
34
+ def hash
35
+ [@namespace, @key].hash
36
+ end
37
+
38
+ # @rbs (Object) -> bool
39
+ def ==(other)
40
+ other.is_a?(self.class) && other.namespace == @namespace && other.key == @key
41
+ end
42
+
43
+ alias eql? ==
44
+ end
45
+ end
@@ -0,0 +1,155 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TypedCache
4
+ # A monadic wrapper for cached values that provides safe access with rich error context.
5
+ # All operations return Either[Error, Snapshot[V]] to provide detailed information about
6
+ # cache operations and the source of values.
7
+ #
8
+ # @rbs generic V
9
+ class CacheRef
10
+ attr_reader :store #: Store[V]
11
+ attr_reader :key #: CacheKey
12
+
13
+ #: (Store[V], CacheKey) -> void
14
+ def initialize(store, key)
15
+ @store = store
16
+ @key = key
17
+ end
18
+
19
+ # Gets a value from the cache as a snapshot
20
+ #: -> either[Error, Snapshot[V]]
21
+ def get
22
+ store.get(key)
23
+ end
24
+
25
+ # Sets a value in the cache and returns it as an updated snapshot
26
+ #: (V) -> either[Error, Snapshot[V]]
27
+ def set(value)
28
+ store.set(key, value)
29
+ end
30
+
31
+ # Deletes the value from the cache and returns the deleted value as a snapshot
32
+ #: -> either[Error, Snapshot[V]]
33
+ def delete
34
+ store.delete(key)
35
+ end
36
+
37
+ # Fetches a value from cache, computing and storing it if not found
38
+ # The snapshot indicates whether the value came from cache or was computed
39
+ #: () { -> V } -> either[Error, Snapshot[V]]
40
+ def fetch(&block)
41
+ store.fetch(key, &block)
42
+ end
43
+
44
+ # Checks if the cache contains a value for this key
45
+ #: -> bool
46
+ def present?
47
+ store.get(key).right?
48
+ end
49
+
50
+ # Checks if the cache is empty for this key
51
+ #: -> bool
52
+ def empty?
53
+ store.get(key).left?
54
+ end
55
+
56
+ # Maps over the cached value if it exists, preserving snapshot metadata
57
+ #: [R] () { (V) -> R } -> either[Error, Snapshot[R]]
58
+ def map(&block)
59
+ get.map { |snapshot| snapshot.map(&block) }
60
+ end
61
+
62
+ # Binds over the cached value, allowing for monadic composition with snapshots
63
+ #: [R] () { (V) -> either[Error, R] } -> either[Error, Snapshot[R]]
64
+ def bind(&block)
65
+ get.bind { |snapshot| snapshot.bind(&block) }
66
+ end
67
+
68
+ alias flat_map bind
69
+
70
+ # Updates the cached value using the provided block
71
+ # Returns the updated value as a snapshot with source=:updated
72
+ #: () { (V) -> V } -> either[Error, Snapshot[V]]
73
+ def update(&block)
74
+ get.bind do |snapshot|
75
+ new_value = yield(snapshot.value)
76
+ set(new_value)
77
+ rescue => e
78
+ Either.left(StoreError.new(
79
+ :update,
80
+ key,
81
+ "Failed to update value: #{e.message}",
82
+ e,
83
+ ))
84
+ end
85
+ end
86
+
87
+ # Returns the cached value or a default if the cache is empty/errored
88
+ #: (V) -> V
89
+ def value_or(default)
90
+ get.fold(
91
+ ->(_error) { default },
92
+ ->(snapshot) { snapshot.value },
93
+ )
94
+ end
95
+
96
+ # Returns a Maybe containing the cached value, or None if not present
97
+ # This provides a more functional approach than value_or
98
+ #: -> maybe[V]
99
+ def value_maybe
100
+ get.fold(
101
+ ->(_error) { Maybe.none },
102
+ ->(snapshot) { Maybe.some(snapshot.value) },
103
+ )
104
+ end
105
+
106
+ # Computes and caches a value if the cache is currently empty
107
+ # Returns existing snapshot if present, computed snapshot if cache miss, error otherwise
108
+ #: () { -> V } -> either[Error, Snapshot[V]]
109
+ def compute_if_absent(&block)
110
+ fetch(&block).fold(
111
+ ->(error) {
112
+ Either.left(StoreError.new(
113
+ :compute_if_absent,
114
+ key,
115
+ "Failed to compute value: #{error.message}",
116
+ error,
117
+ ))
118
+ },
119
+ ->(snapshot) { Either.right(snapshot) },
120
+ )
121
+ end
122
+
123
+ # Creates a new CacheRef with the same store but different key
124
+ #: [R] (String) -> CacheRef[R]
125
+ def with_key(new_key)
126
+ CacheRef.new(store, store.namespace.key(new_key))
127
+ end
128
+
129
+ # Creates a scoped CacheRef by appending to the current key path
130
+ #: [R] (String) -> CacheRef[R]
131
+ def scope(scope_key)
132
+ new_namespace = store.namespace.nested(key.key)
133
+ new_store = store.with_namespace(new_namespace)
134
+ CacheRef.new(new_store, new_namespace.key(scope_key))
135
+ end
136
+
137
+ # Pattern matching support for Either[Error, Snapshot[V]] results
138
+ #: [R] () { (Error) -> R } () { (Snapshot[V]) -> R } -> R
139
+ def fold(left_fn, right_fn)
140
+ get.fold(left_fn, right_fn)
141
+ end
142
+
143
+ # Convenience method to work with the snapshot directly
144
+ #: [R] () { (Snapshot[V]) -> R } -> either[Error, R]
145
+ def with_snapshot(&block)
146
+ get.map(&block)
147
+ end
148
+
149
+ # Convenience method to work with just the value (losing snapshot context)
150
+ #: [R] () { (V) -> R } -> either[Error, R]
151
+ def with(&block)
152
+ get.map { |snapshot| yield(snapshot.value) }
153
+ end
154
+ end
155
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TypedCache
4
+ # A simple, testable wrapper around Time to provide a consistent way of
5
+ # getting the current time, respecting ActiveSupport's time zone when available.
6
+ class Clock
7
+ class << self
8
+ # Retrieves the current time. If ActiveSupport's `Time.current` is
9
+ # available, it will be used to respect the configured timezone. Otherwise,
10
+ # it falls back to the system's `Time.now`.
11
+ #
12
+ # @return [Time] The current time.
13
+ # @rbs () -> Time
14
+ def moment
15
+ if Time.respond_to?(:current)
16
+ Time.current
17
+ else
18
+ Time.now
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TypedCache
4
+ # Marker mixin for cache store decorators. A decorator behaves exactly like a
5
+ # Store but must accept another Store instance in its constructor.
6
+ # @rbs generic V
7
+ module Decorator
8
+ include Store #[V]
9
+ # @rbs! include Store::_Store[V]
10
+ # @rbs! include Store::_Decorator[V]
11
+ end
12
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TypedCache
4
+ # Holds store decorators (e.g., instrumentation wrappers) that can be composed
5
+ # by the CacheBuilder. Decorators must conform to the same API as the wrapped
6
+ # store (see `TypedCache::Store`) and accept the store instance as their first
7
+ # constructor argument.
8
+ #
9
+ # Example:
10
+ # Decorators.register(:my_decorator, MyDecorator)
11
+ # cache = TypedCache.builder
12
+ # .with_backend { |reg, ns| reg.resolve(:memory, ns).value }
13
+ # .with_decorator(:my_decorator)
14
+ # .build.value
15
+ #
16
+ module Decorators
17
+ # Default decorator set – starts with instrumentation only, but this registry
18
+ # lets end-users register their own via `Decorators.register`.
19
+ REGISTRY = Registry.new('decorator', {
20
+ instrumented: Store::Instrumented,
21
+ }).freeze
22
+
23
+ private_constant :REGISTRY
24
+
25
+ class << self
26
+ extend Forwardable
27
+
28
+ # Delegate common registry helpers
29
+ delegate [:resolve, :register, :available, :registered?] => :registry
30
+
31
+ # @rbs () -> Registry[Store[untyped]]
32
+ def registry = REGISTRY
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,121 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TypedCache
4
+ # @rbs! type either[out E, out R] = (Left[E] | Right[R]) & _Either[E, R]
5
+
6
+ module Either
7
+ include Kernel
8
+
9
+ class << self
10
+ #: [E] (E) -> either[E, bot]
11
+ def left(error) = Left.new(error)
12
+ #: [R] (R) -> either[bot, R]
13
+ def right(value) = Right.new(value)
14
+
15
+ #: [E, R] (E | R) -> either[E, R]
16
+ def wrap(value, error_class = StandardError)
17
+ case value
18
+ when Left, Right then value
19
+ when error_class then Left.new(value)
20
+ else
21
+ Right.new(value)
22
+ end
23
+ end
24
+ end
25
+ end
26
+
27
+ # @rbs! interface _Either[out E, out R]
28
+ # def left?: -> bool
29
+ # def right?: -> bool
30
+ # def map: [T] () { (R) -> T } -> either[E, T]
31
+ # def bind: [E2, R2] () { (R) -> either[E2, R2] } -> either[E | E2, R2]
32
+ # def map_left: [F] () { (E) -> F } -> either[F, R]
33
+ # def fold: [T, E, R] (^(E) -> T, ^(R) -> T) -> T
34
+ # end
35
+
36
+ # @rbs generic out E
37
+ class Left
38
+ # @rbs! include _Either[E, bot]
39
+
40
+ attr_reader :error #: E
41
+
42
+ #: (E) -> void
43
+ def initialize(error)
44
+ @error = error
45
+ end
46
+
47
+ # @rbs override
48
+ #: -> true
49
+ def left? = true
50
+
51
+ # @rbs override
52
+ #: -> false
53
+ def right? = false
54
+
55
+ # @rbs override
56
+ #: [T] () { (R) -> T } -> either[E, T]
57
+ def map(&) = self
58
+
59
+ # @rbs override
60
+ #: [E2, R2] () { (R) -> either[E2, R2] } -> either[E | E2, R2]
61
+ def bind(&) = self
62
+
63
+ # @rbs override
64
+ #: [F] () { (E) -> F } -> either[F, R]
65
+ def map_left(&) = Left.new(yield(error))
66
+
67
+ # @rbs override
68
+ #: [T, E, R] (^(E) -> T, ^(R) -> T) -> T
69
+ def fold(left_fn, right_fn)
70
+ left_fn.call(error)
71
+ end
72
+
73
+ #: (Array[top]) -> ({ error: E })
74
+ def deconstruct_keys(keys)
75
+ { error: }
76
+ end
77
+ end
78
+
79
+ # @rbs generic out R
80
+ class Right
81
+ # @rbs! include _Either[bot, R]
82
+
83
+ attr_reader :value #: R
84
+
85
+ #: (R) -> void
86
+ def initialize(value)
87
+ @value = value
88
+ end
89
+
90
+ # @rbs override
91
+ #: -> false
92
+ def left? = false
93
+
94
+ # @rbs override
95
+ #: -> true
96
+ def right? = true
97
+
98
+ # @rbs override
99
+ #: [T] () { (R) -> T } -> either[E, T]
100
+ def map(&) = Right.new(yield(value))
101
+
102
+ # @rbs override
103
+ #: [E2, R2] () { (R) -> either[E2, R2] } -> either[E | E2, R2]
104
+ def bind(&) = yield(value)
105
+
106
+ # @rbs override
107
+ #: [F] () { (E) -> F } -> either[F, R]
108
+ def map_left(&) = self
109
+
110
+ # @rbs override
111
+ #: [T, E, R] (^(E) -> T, ^(R) -> T) -> T
112
+ def fold(left_fn, right_fn)
113
+ right_fn.call(value)
114
+ end
115
+
116
+ #: (Array[top]) -> ({ value: R })
117
+ def deconstruct_keys(keys)
118
+ { value: }
119
+ end
120
+ end
121
+ end