bm-typed 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 +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: []
|