bm-typed 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/lib/typed/builder.rb +237 -0
- data/lib/typed/struct.rb +157 -0
- data/lib/typed/version.rb +5 -0
- data/lib/typed.rb +106 -0
- metadata +134 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 28fd1838d9a1659a003182955c8bb7d72e15cd231a0d68ab536373b188d200dc
|
4
|
+
data.tar.gz: f40f3685b87a1271f2d516ca9b0718f5543da5422f106bf27d81f42404bf6dfc
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: d9d8e1a676753700e5301b84ebc14d0c63593958b6ff71e997e08d9053049956a34700bd566441b96e0048918bd6ad95bae7fae723be1d9c031edc36c59dee50
|
7
|
+
data.tar.gz: e28ef39291fb44dd1f75e72773288c12d61c77aeab0d0d6be97aa4567e342bc0491dbbb731410a406113f23fbe191b7654f97730502f9dd41b8d951eaa3e1e22
|
@@ -0,0 +1,237 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'dry-logic'
|
4
|
+
require 'dry/logic/rule_compiler'
|
5
|
+
require 'dry/logic/predicates'
|
6
|
+
|
7
|
+
module Typed
|
8
|
+
module Builder
|
9
|
+
# Entrypoint
|
10
|
+
def self.any
|
11
|
+
AnyHandler.instance
|
12
|
+
end
|
13
|
+
|
14
|
+
Result = ::Struct.new(:ok, :value, :message)
|
15
|
+
class Result
|
16
|
+
attr_reader :ok, :value
|
17
|
+
|
18
|
+
def initialize(ok, value, message)
|
19
|
+
@ok = ok
|
20
|
+
@value = value
|
21
|
+
@failure_block = message
|
22
|
+
end
|
23
|
+
|
24
|
+
def message
|
25
|
+
@message ||= @failure_block.call
|
26
|
+
end
|
27
|
+
|
28
|
+
class << self
|
29
|
+
def success(value)
|
30
|
+
new(true, value, nil)
|
31
|
+
end
|
32
|
+
|
33
|
+
def failure(&failure_block)
|
34
|
+
new(false, nil, failure_block)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
module BaseType
|
40
|
+
def nullable
|
41
|
+
Typed.null | self
|
42
|
+
end
|
43
|
+
|
44
|
+
def missable
|
45
|
+
Typed.value(Undefined) | self
|
46
|
+
end
|
47
|
+
|
48
|
+
def default(new_value = Typed::Undefined, &block)
|
49
|
+
call(new_value) unless block
|
50
|
+
block ||= -> { new_value }
|
51
|
+
DefaultType.new(self) { call(block.call) }
|
52
|
+
end
|
53
|
+
|
54
|
+
def instance(expected_class)
|
55
|
+
dry_constrained(type: expected_class)
|
56
|
+
end
|
57
|
+
|
58
|
+
def enum(*values)
|
59
|
+
dry_constrained(included_in: values.map { |value| call(value) })
|
60
|
+
end
|
61
|
+
|
62
|
+
def |(other)
|
63
|
+
expected_type other
|
64
|
+
|
65
|
+
SumType.new(self, other)
|
66
|
+
end
|
67
|
+
|
68
|
+
def constructor(input: Typed.any, swallow: [], &block)
|
69
|
+
expected_type(input)
|
70
|
+
return self unless block_given?
|
71
|
+
|
72
|
+
CoerceType.new(input, self, swallow, &block)
|
73
|
+
end
|
74
|
+
|
75
|
+
def dry_constrained(**options)
|
76
|
+
predicate = ::Dry::Logic::RuleCompiler.new(::Dry::Logic::Predicates).call(
|
77
|
+
options.map { |key, val|
|
78
|
+
::Dry::Logic::Rule::Predicate.new(
|
79
|
+
::Dry::Logic::Predicates[:"#{key}?"]
|
80
|
+
).curry(val).to_ast
|
81
|
+
}
|
82
|
+
).reduce(:and)
|
83
|
+
|
84
|
+
constrained do |value|
|
85
|
+
"#{value.inspect} violates #{predicate}" unless predicate.call(value).success?
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def constrained(&constraint)
|
90
|
+
return self unless constraint
|
91
|
+
|
92
|
+
ConstrainedType.new(self, &constraint)
|
93
|
+
end
|
94
|
+
|
95
|
+
def call(*args)
|
96
|
+
result = process((args + [Typed::Undefined]).first)
|
97
|
+
return result.value if result.ok
|
98
|
+
|
99
|
+
raise InvalidValue, result.message
|
100
|
+
end
|
101
|
+
|
102
|
+
def process(value)
|
103
|
+
Typed::Builder::Result.success(value)
|
104
|
+
end
|
105
|
+
|
106
|
+
private
|
107
|
+
|
108
|
+
def expected_type(type)
|
109
|
+
raise InvalidType, "Not a Typed type: #{type.inspect}" unless type.is_a?(BaseType)
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
class ArrayType
|
114
|
+
include BaseType
|
115
|
+
|
116
|
+
def initialize(element_type)
|
117
|
+
@element_type = element_type
|
118
|
+
end
|
119
|
+
|
120
|
+
def process(value)
|
121
|
+
return Result.failure { "Invalid collection: #{value.inspect}" } unless value.respond_to?(:each)
|
122
|
+
|
123
|
+
new_value = []
|
124
|
+
|
125
|
+
value.each do |element|
|
126
|
+
element_result = element_type.process(element)
|
127
|
+
return element_result unless element_result.ok
|
128
|
+
|
129
|
+
new_value << element_result.value
|
130
|
+
end
|
131
|
+
|
132
|
+
Result.success(new_value)
|
133
|
+
end
|
134
|
+
|
135
|
+
private
|
136
|
+
|
137
|
+
attr_reader :base_type, :element_type
|
138
|
+
end
|
139
|
+
|
140
|
+
class ConstrainedType
|
141
|
+
include BaseType
|
142
|
+
|
143
|
+
def initialize(base_type, &constraint)
|
144
|
+
@base_type = base_type
|
145
|
+
@constraint = constraint
|
146
|
+
end
|
147
|
+
|
148
|
+
def process(value)
|
149
|
+
result = base_type.process(value)
|
150
|
+
return result unless result.ok
|
151
|
+
|
152
|
+
error = constraint.call(result.value)
|
153
|
+
return result unless error
|
154
|
+
|
155
|
+
Result.failure { error }
|
156
|
+
end
|
157
|
+
|
158
|
+
private
|
159
|
+
|
160
|
+
attr_reader :base_type, :constraint
|
161
|
+
end
|
162
|
+
|
163
|
+
class DefaultType
|
164
|
+
include BaseType
|
165
|
+
|
166
|
+
def initialize(base_type, &default_value)
|
167
|
+
@base_type = base_type
|
168
|
+
@default_value = default_value
|
169
|
+
end
|
170
|
+
|
171
|
+
def process(value)
|
172
|
+
new_value = Typed::Undefined.equal?(value) ? default_value.call : value
|
173
|
+
base_type.process(new_value)
|
174
|
+
end
|
175
|
+
|
176
|
+
private
|
177
|
+
|
178
|
+
attr_reader :default_value, :base_type
|
179
|
+
end
|
180
|
+
|
181
|
+
class SumType
|
182
|
+
include BaseType
|
183
|
+
|
184
|
+
def initialize(type_a, type_b)
|
185
|
+
@type_a = type_a
|
186
|
+
@type_b = type_b
|
187
|
+
end
|
188
|
+
|
189
|
+
def process(value)
|
190
|
+
result = type_a.process(value)
|
191
|
+
return result if result.ok
|
192
|
+
|
193
|
+
type_b.process(value)
|
194
|
+
end
|
195
|
+
|
196
|
+
private
|
197
|
+
|
198
|
+
attr_reader :type_a, :type_b
|
199
|
+
end
|
200
|
+
|
201
|
+
class CoerceType
|
202
|
+
include BaseType
|
203
|
+
|
204
|
+
def initialize(input_type, return_type, swallow, &coercion)
|
205
|
+
@input_type = input_type
|
206
|
+
@return_type = return_type
|
207
|
+
@coercion = coercion
|
208
|
+
@swallow = swallow
|
209
|
+
end
|
210
|
+
|
211
|
+
def process(value)
|
212
|
+
# No coercion needed
|
213
|
+
passthrough_result = return_type.process(value)
|
214
|
+
return passthrough_result if passthrough_result.ok
|
215
|
+
|
216
|
+
# Check input_type enables this coercion
|
217
|
+
input_result = input_type.process(value)
|
218
|
+
|
219
|
+
if input_result.ok
|
220
|
+
coerced_value =
|
221
|
+
begin
|
222
|
+
coercion.call(input_result.value)
|
223
|
+
rescue *swallow
|
224
|
+
input_result.value
|
225
|
+
end
|
226
|
+
return return_type.process(coerced_value)
|
227
|
+
end
|
228
|
+
|
229
|
+
passthrough_result
|
230
|
+
end
|
231
|
+
|
232
|
+
private
|
233
|
+
|
234
|
+
attr_reader :input_type, :return_type, :coercion, :swallow
|
235
|
+
end
|
236
|
+
end
|
237
|
+
end
|
data/lib/typed/struct.rb
ADDED
@@ -0,0 +1,157 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Typed
|
4
|
+
class Struct
|
5
|
+
# TODO: This has nothing to do in this gem, should be moved to application
|
6
|
+
class Updater
|
7
|
+
attr_reader :params, :target
|
8
|
+
|
9
|
+
def initialize(target, params)
|
10
|
+
@target = target
|
11
|
+
@params = params
|
12
|
+
end
|
13
|
+
|
14
|
+
def assign(from, to: from, &value_builder)
|
15
|
+
check_from(from)
|
16
|
+
return unless params.key?(from)
|
17
|
+
|
18
|
+
input_value = params[from]
|
19
|
+
default_getter = proc { input_value }
|
20
|
+
processed_value = (value_builder || default_getter).call(input_value)
|
21
|
+
target.send("#{to}=", processed_value)
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def check_from(from)
|
27
|
+
return if params.class.schema.key?(from)
|
28
|
+
|
29
|
+
raise "Key #{from.inspect} does not exist on #{params.class}"
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
class << self
|
34
|
+
include Builder::BaseType
|
35
|
+
|
36
|
+
def attribute(name, type = Typed.any)
|
37
|
+
expected_type(type)
|
38
|
+
|
39
|
+
name = name.to_sym
|
40
|
+
|
41
|
+
raise Typed::InvalidType, "Property already defined: #{name}" if typed_attributes.key?(name)
|
42
|
+
|
43
|
+
typed_attributes[name] = type
|
44
|
+
define_method(name) { @_data.fetch(name) { Typed::Undefined } }
|
45
|
+
end
|
46
|
+
|
47
|
+
def schema
|
48
|
+
@schema ||= ancestors.select { |a| Typed::Struct > a }.reverse.reduce({}) { |acc, clazz|
|
49
|
+
acc.merge(clazz.typed_attributes)
|
50
|
+
}.freeze
|
51
|
+
end
|
52
|
+
|
53
|
+
def allow_extra_keys(new_flag)
|
54
|
+
define_singleton_method(:allow_extra_keys?) { new_flag }
|
55
|
+
end
|
56
|
+
|
57
|
+
def allow_extra_keys?
|
58
|
+
true
|
59
|
+
end
|
60
|
+
|
61
|
+
def typed_attributes
|
62
|
+
@typed_attributes ||= {}
|
63
|
+
end
|
64
|
+
|
65
|
+
def process(data)
|
66
|
+
result = parse_as_hash(data)
|
67
|
+
result.ok ? Typed::Builder::Result.success(new(result)) : result
|
68
|
+
end
|
69
|
+
|
70
|
+
def parse_as_hash(input_data)
|
71
|
+
return Typed::Builder::Result.success(input_data.to_h) if input_data.is_a?(self)
|
72
|
+
|
73
|
+
# TODO: remove this hack
|
74
|
+
unless input_data.is_a?(::Hash) || input_data.class.name == 'ActionController::Parameters'
|
75
|
+
return Typed::Builder::Result.failure { "Expected Hash, got #{input_data.inspect}" }
|
76
|
+
end
|
77
|
+
|
78
|
+
# Start by creating a new "clean" hash from input
|
79
|
+
# This way, we can easily handle some variants (ActionController::Parameters, ...)
|
80
|
+
clean_data = Hash.new { ::Typed::Undefined }
|
81
|
+
input_data.each { |key, value| clean_data[key.to_sym] = value }
|
82
|
+
|
83
|
+
# Check presence of extra keys
|
84
|
+
extra_property = (clean_data.keys - schema.keys).first
|
85
|
+
if extra_property && !allow_extra_keys?
|
86
|
+
return Typed::Builder::Result
|
87
|
+
.failure("Unknown property '#{extra_property}' of #{inspect}")
|
88
|
+
end
|
89
|
+
|
90
|
+
# Construct the final hash which will be stored internally to represent
|
91
|
+
# Struct's data.
|
92
|
+
output = schema.each_with_object({}) { |(name, type), acc|
|
93
|
+
result = type.process(clean_data[name])
|
94
|
+
|
95
|
+
unless result.ok
|
96
|
+
return Typed::Builder::Result.failure {
|
97
|
+
"Invalid property '#{name}' of #{inspect}: #{result.message}"
|
98
|
+
}
|
99
|
+
end
|
100
|
+
|
101
|
+
acc[name] = result.value unless Typed::Undefined.equal?(result.value)
|
102
|
+
}.freeze
|
103
|
+
|
104
|
+
Typed::Builder::Result.success(output)
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
def updater(target)
|
109
|
+
Updater.new(target, self)
|
110
|
+
end
|
111
|
+
|
112
|
+
def inspect
|
113
|
+
attrs = self.class.schema.keys.map { |key| " #{key}=#{@_data[key].inspect}" }.join
|
114
|
+
"#<#{self.class.name || self.class.inspect}#{attrs}>"
|
115
|
+
end
|
116
|
+
|
117
|
+
def to_h
|
118
|
+
@_data
|
119
|
+
end
|
120
|
+
|
121
|
+
def [](key)
|
122
|
+
raise Typed::InvalidType, "Unknown property: #{key.inspect}" unless self.class.schema.key?(key)
|
123
|
+
|
124
|
+
@_data.fetch(key) { Typed::Undefined }
|
125
|
+
end
|
126
|
+
|
127
|
+
def ==(other)
|
128
|
+
return true if other.equal?(self)
|
129
|
+
return false unless other.instance_of?(self.class)
|
130
|
+
|
131
|
+
@_data == other.instance_variable_get(:@_data)
|
132
|
+
end
|
133
|
+
|
134
|
+
def hash
|
135
|
+
@_data.hash
|
136
|
+
end
|
137
|
+
|
138
|
+
def key?(key)
|
139
|
+
@_data.key?(key)
|
140
|
+
end
|
141
|
+
|
142
|
+
def initialize(input_data = {})
|
143
|
+
case input_data
|
144
|
+
when Typed::Builder::Result then initialize_from_result(input_data)
|
145
|
+
else initialize_from_result(self.class.parse_as_hash(input_data))
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
private
|
150
|
+
|
151
|
+
def initialize_from_result(result)
|
152
|
+
raise Typed::InvalidValue, result.message unless result.ok
|
153
|
+
|
154
|
+
@_data = result.value
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
data/lib/typed.rb
ADDED
@@ -0,0 +1,106 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'typed/builder'
|
4
|
+
require 'typed/struct'
|
5
|
+
require 'typed/version'
|
6
|
+
require 'uri'
|
7
|
+
require 'active_support/time'
|
8
|
+
|
9
|
+
module Typed
|
10
|
+
class InvalidValue < TypeError; end
|
11
|
+
class InvalidType < TypeError; end
|
12
|
+
|
13
|
+
class << self
|
14
|
+
include Typed::Builder::BaseType
|
15
|
+
|
16
|
+
def array(element_type = Typed.any)
|
17
|
+
expected_type(element_type)
|
18
|
+
|
19
|
+
Typed::Builder::ArrayType.new(element_type)
|
20
|
+
end
|
21
|
+
|
22
|
+
def any
|
23
|
+
self
|
24
|
+
end
|
25
|
+
|
26
|
+
def null
|
27
|
+
value(nil)
|
28
|
+
end
|
29
|
+
|
30
|
+
def value(expected_value)
|
31
|
+
dry_constrained(eql: call(expected_value))
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
# Undefined is both:
|
36
|
+
# - A placeholder used to represent an undefined value.
|
37
|
+
# - The type used to represent this placeholder.
|
38
|
+
module Undefined
|
39
|
+
class << self
|
40
|
+
include Typed::Builder::BaseType
|
41
|
+
|
42
|
+
def process(value)
|
43
|
+
if Undefined.equal?(value)
|
44
|
+
Typed::Builder::Result.success(value)
|
45
|
+
else
|
46
|
+
Typed::Builder::Result.failure { 'Expected value undefined' }
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
module Strict
|
53
|
+
String = Typed.instance(::String)
|
54
|
+
Symbol = Typed.instance(::Symbol)
|
55
|
+
Int = Typed.instance(::Integer)
|
56
|
+
Float = Typed.instance(::Float)
|
57
|
+
Date = Typed.instance(::Date)
|
58
|
+
True = Typed.value(true)
|
59
|
+
False = Typed.value(false)
|
60
|
+
Boolean = True | False
|
61
|
+
Time = Typed.instance(::Time)
|
62
|
+
DateTime = Typed.instance(::DateTime)
|
63
|
+
end
|
64
|
+
|
65
|
+
String = Strict::String.constructor(input: Strict::Int | Strict::Float | Strict::Symbol, &:to_s)
|
66
|
+
|
67
|
+
Float = Strict::Float.constructor(
|
68
|
+
input: Strict::String | Strict::Int,
|
69
|
+
swallow: [TypeError, ArgumentError]
|
70
|
+
) { |value| Float(value) }
|
71
|
+
|
72
|
+
Int = Strict::Int
|
73
|
+
.constructor(
|
74
|
+
input: Strict::String,
|
75
|
+
swallow: [TypeError, ArgumentError]
|
76
|
+
) { |value| Integer(value) }
|
77
|
+
.constructor(
|
78
|
+
input: Float,
|
79
|
+
swallow: [TypeError, ArgumentError]
|
80
|
+
) { |value|
|
81
|
+
parsed = Integer(value)
|
82
|
+
parsed == value ? parsed : value
|
83
|
+
}
|
84
|
+
|
85
|
+
Date = Strict::Date
|
86
|
+
.constructor(
|
87
|
+
input: String,
|
88
|
+
swallow: [TypeError, ArgumentError, RangeError]
|
89
|
+
) { |value| ::Date.parse(value) }
|
90
|
+
.constructor(input: Typed.instance(::Time), &:to_date)
|
91
|
+
|
92
|
+
Boolean = Strict::Boolean.constructor(input: String) { |value|
|
93
|
+
{ 'true' => true, 'false' => false }.fetch(value) { value }
|
94
|
+
}
|
95
|
+
|
96
|
+
Time = (Strict::DateTime | Strict::Time)
|
97
|
+
.constructor(input: String, swallow: [TypeError, ArgumentError]) { |value|
|
98
|
+
::ActiveSupport::TimeZone['UTC'].parse(value)
|
99
|
+
}
|
100
|
+
.constructor(input: Int | Float, swallow: [TypeError, ArgumentError]) { |value| ::Time.at(value) }
|
101
|
+
|
102
|
+
UUID = String.dry_constrained(format: /\A[a-f\d]{8}(-[a-f\d]{4}){3}-[a-f\d]{12}\z/)
|
103
|
+
.constructor(input: String, &:downcase)
|
104
|
+
|
105
|
+
URL = String.dry_constrained(format: URI::DEFAULT_PARSER.make_regexp(%w[http https]))
|
106
|
+
end
|
metadata
ADDED
@@ -0,0 +1,134 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: bm-typed
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Frederic Terrazzoni
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2018-10-31 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: coveralls
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0.8'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0.8'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: pry
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rspec
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '3'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '3'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rubocop
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - '='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: 0.59.2
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - '='
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: 0.59.2
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: activesupport
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '5.2'
|
76
|
+
type: :runtime
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '5.2'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: dry-logic
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - "~>"
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: 0.4.2
|
90
|
+
type: :runtime
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - "~>"
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: 0.4.2
|
97
|
+
description: A dry-types/dry-struct alternative making the difference between undefined
|
98
|
+
and nil
|
99
|
+
email:
|
100
|
+
- frederic.terrazzoni@gmail.com
|
101
|
+
executables: []
|
102
|
+
extensions: []
|
103
|
+
extra_rdoc_files: []
|
104
|
+
files:
|
105
|
+
- lib/typed.rb
|
106
|
+
- lib/typed/builder.rb
|
107
|
+
- lib/typed/struct.rb
|
108
|
+
- lib/typed/version.rb
|
109
|
+
homepage: https://github.com/getbannerman/typed
|
110
|
+
licenses:
|
111
|
+
- MIT
|
112
|
+
metadata: {}
|
113
|
+
post_install_message:
|
114
|
+
rdoc_options: []
|
115
|
+
require_paths:
|
116
|
+
- lib
|
117
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
118
|
+
requirements:
|
119
|
+
- - ">="
|
120
|
+
- !ruby/object:Gem::Version
|
121
|
+
version: '0'
|
122
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
123
|
+
requirements:
|
124
|
+
- - ">="
|
125
|
+
- !ruby/object:Gem::Version
|
126
|
+
version: '0'
|
127
|
+
requirements: []
|
128
|
+
rubyforge_project:
|
129
|
+
rubygems_version: 2.7.6
|
130
|
+
signing_key:
|
131
|
+
specification_version: 4
|
132
|
+
summary: A dry-types/dry-struct alternative making the difference between undefined
|
133
|
+
and nil
|
134
|
+
test_files: []
|