dry-types 0.6.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/.gitignore +9 -0
- data/.rspec +2 -0
- data/.travis.yml +30 -0
- data/CHANGELOG.md +169 -0
- data/Gemfile +20 -0
- data/LICENSE +20 -0
- data/README.md +29 -0
- data/Rakefile +6 -0
- data/benchmarks/basic.rb +57 -0
- data/benchmarks/constrained.rb +37 -0
- data/bin/console +18 -0
- data/bin/setup +7 -0
- data/dry-types.gemspec +41 -0
- data/lib/dry-types.rb +1 -0
- data/lib/dry/types.rb +115 -0
- data/lib/dry/types/builder.rb +46 -0
- data/lib/dry/types/coercions/form.rb +91 -0
- data/lib/dry/types/compiler.rb +63 -0
- data/lib/dry/types/constrained.rb +47 -0
- data/lib/dry/types/constraints.rb +24 -0
- data/lib/dry/types/constructor.rb +59 -0
- data/lib/dry/types/container.rb +7 -0
- data/lib/dry/types/core.rb +57 -0
- data/lib/dry/types/decorator.rb +48 -0
- data/lib/dry/types/default.rb +43 -0
- data/lib/dry/types/definition.rb +53 -0
- data/lib/dry/types/definition/array.rb +24 -0
- data/lib/dry/types/definition/hash.rb +68 -0
- data/lib/dry/types/enum.rb +30 -0
- data/lib/dry/types/form.rb +53 -0
- data/lib/dry/types/optional.rb +16 -0
- data/lib/dry/types/safe.rb +25 -0
- data/lib/dry/types/struct.rb +64 -0
- data/lib/dry/types/sum.rb +34 -0
- data/lib/dry/types/value.rb +22 -0
- data/lib/dry/types/version.rb +5 -0
- metadata +236 -0
@@ -0,0 +1,57 @@
|
|
1
|
+
module Dry
|
2
|
+
module Types
|
3
|
+
COERCIBLE = {
|
4
|
+
string: String,
|
5
|
+
int: Integer,
|
6
|
+
float: Float,
|
7
|
+
decimal: BigDecimal,
|
8
|
+
array: Array,
|
9
|
+
hash: Hash
|
10
|
+
}.freeze
|
11
|
+
|
12
|
+
NON_COERCIBLE = {
|
13
|
+
nil: NilClass,
|
14
|
+
symbol: Symbol,
|
15
|
+
class: Class,
|
16
|
+
true: TrueClass,
|
17
|
+
false: FalseClass,
|
18
|
+
date: Date,
|
19
|
+
date_time: DateTime,
|
20
|
+
time: Time
|
21
|
+
}.freeze
|
22
|
+
|
23
|
+
ALL_PRIMITIVES = COERCIBLE.merge(NON_COERCIBLE).freeze
|
24
|
+
|
25
|
+
# Register built-in types that are non-coercible through kernel methods
|
26
|
+
ALL_PRIMITIVES.each do |name, primitive|
|
27
|
+
register(name.to_s, Definition[primitive].new(primitive))
|
28
|
+
end
|
29
|
+
|
30
|
+
# Register strict built-in types that are non-coercible through kernel methods
|
31
|
+
ALL_PRIMITIVES.each do |name, primitive|
|
32
|
+
register("strict.#{name}", self[name.to_s].constrained(type: primitive))
|
33
|
+
end
|
34
|
+
|
35
|
+
# Register built-in primitive types with kernel coercion methods
|
36
|
+
COERCIBLE.each do |name, primitive|
|
37
|
+
register("coercible.#{name}", self[name.to_s].constructor(Kernel.method(primitive.name)))
|
38
|
+
end
|
39
|
+
|
40
|
+
# Register non-coercible maybe types
|
41
|
+
ALL_PRIMITIVES.each_key do |name|
|
42
|
+
next if name == :nil
|
43
|
+
register("maybe.strict.#{name}", self["strict.#{name}"].optional)
|
44
|
+
end
|
45
|
+
|
46
|
+
# Register coercible maybe types
|
47
|
+
COERCIBLE.each_key do |name|
|
48
|
+
register("maybe.coercible.#{name}", self["coercible.#{name}"].optional)
|
49
|
+
end
|
50
|
+
|
51
|
+
# Register :bool since it's common and not a built-in Ruby type :(
|
52
|
+
register("bool", self["true"] | self["false"])
|
53
|
+
register("strict.bool", self["strict.true"] | self["strict.false"])
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
require 'dry/types/form'
|
@@ -0,0 +1,48 @@
|
|
1
|
+
module Dry
|
2
|
+
module Types
|
3
|
+
module Decorator
|
4
|
+
attr_reader :type, :options
|
5
|
+
|
6
|
+
def initialize(type, options = {})
|
7
|
+
@type = type
|
8
|
+
@options = options
|
9
|
+
end
|
10
|
+
|
11
|
+
def constructor
|
12
|
+
type.constructor
|
13
|
+
end
|
14
|
+
|
15
|
+
def valid?(input)
|
16
|
+
type.valid?(input)
|
17
|
+
end
|
18
|
+
|
19
|
+
def respond_to_missing?(meth, include_private = false)
|
20
|
+
super || type.respond_to?(meth)
|
21
|
+
end
|
22
|
+
|
23
|
+
def with(new_options)
|
24
|
+
self.class.new(type, options.merge(new_options))
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def decorate?(response)
|
30
|
+
response.kind_of?(type.class)
|
31
|
+
end
|
32
|
+
|
33
|
+
def method_missing(meth, *args, &block)
|
34
|
+
if type.respond_to?(meth)
|
35
|
+
response = type.__send__(meth, *args, &block)
|
36
|
+
|
37
|
+
if decorate?(response)
|
38
|
+
self.class.new(response, options)
|
39
|
+
else
|
40
|
+
response
|
41
|
+
end
|
42
|
+
else
|
43
|
+
super
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
require 'dry/types/decorator'
|
2
|
+
|
3
|
+
module Dry
|
4
|
+
module Types
|
5
|
+
class Default
|
6
|
+
include Decorator
|
7
|
+
include Builder
|
8
|
+
|
9
|
+
class Callable < Default
|
10
|
+
def evaluate
|
11
|
+
value.call
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
attr_reader :value
|
16
|
+
|
17
|
+
alias_method :evaluate, :value
|
18
|
+
|
19
|
+
def self.[](value)
|
20
|
+
if value.respond_to?(:call)
|
21
|
+
Callable
|
22
|
+
else
|
23
|
+
self
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def initialize(type, options)
|
28
|
+
super
|
29
|
+
@value = options.fetch(:value)
|
30
|
+
end
|
31
|
+
|
32
|
+
def call(input)
|
33
|
+
if input.nil?
|
34
|
+
evaluate
|
35
|
+
else
|
36
|
+
output = type[input]
|
37
|
+
output.nil? ? evaluate : output
|
38
|
+
end
|
39
|
+
end
|
40
|
+
alias_method :[], :call
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
require 'dry/types/builder'
|
2
|
+
|
3
|
+
module Dry
|
4
|
+
module Types
|
5
|
+
class Definition
|
6
|
+
include Dry::Equalizer(:primitive, :options)
|
7
|
+
include Builder
|
8
|
+
|
9
|
+
attr_reader :options
|
10
|
+
|
11
|
+
attr_reader :primitive
|
12
|
+
|
13
|
+
def self.[](primitive)
|
14
|
+
if primitive == ::Array
|
15
|
+
Definition::Array
|
16
|
+
elsif primitive == ::Hash
|
17
|
+
Definition::Hash
|
18
|
+
else
|
19
|
+
self
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def initialize(primitive, options = {})
|
24
|
+
@primitive = primitive
|
25
|
+
@options = options
|
26
|
+
end
|
27
|
+
|
28
|
+
def with(new_options)
|
29
|
+
self.class.new(primitive, options.merge(new_options))
|
30
|
+
end
|
31
|
+
|
32
|
+
def name
|
33
|
+
primitive.name
|
34
|
+
end
|
35
|
+
|
36
|
+
def call(input)
|
37
|
+
input
|
38
|
+
end
|
39
|
+
alias_method :[], :call
|
40
|
+
|
41
|
+
def try(input)
|
42
|
+
call(input)
|
43
|
+
end
|
44
|
+
|
45
|
+
def valid?(input)
|
46
|
+
input.is_a?(primitive)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
require 'dry/types/definition/array'
|
53
|
+
require 'dry/types/definition/hash'
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module Dry
|
2
|
+
module Types
|
3
|
+
class Definition
|
4
|
+
class Array < Definition
|
5
|
+
def self.constructor(member_constructor, array)
|
6
|
+
array.map { |value| member_constructor[value] }
|
7
|
+
end
|
8
|
+
|
9
|
+
def member(type)
|
10
|
+
member_constructor =
|
11
|
+
case type
|
12
|
+
when String, Class then Types[type]
|
13
|
+
else type
|
14
|
+
end
|
15
|
+
|
16
|
+
array_constructor = self.class
|
17
|
+
.method(:constructor).to_proc.curry.(member_constructor)
|
18
|
+
|
19
|
+
constructor(array_constructor, member: member_constructor)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
module Dry
|
2
|
+
module Types
|
3
|
+
class Definition
|
4
|
+
class Hash < Definition
|
5
|
+
def self.safe_constructor(types, hash)
|
6
|
+
types.each_with_object({}) do |(key, type), result|
|
7
|
+
if hash.key?(key)
|
8
|
+
result[key] = type[hash[key]]
|
9
|
+
elsif type.is_a?(Default)
|
10
|
+
result[key] = type.evaluate
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.symbolized_constructor(types, hash)
|
16
|
+
types.each_with_object({}) do |(key, type), result|
|
17
|
+
if hash.key?(key)
|
18
|
+
result[key] = type[hash[key]]
|
19
|
+
else
|
20
|
+
key_name = key.to_s
|
21
|
+
|
22
|
+
if hash.key?(key_name)
|
23
|
+
result[key] = type[hash[key_name]]
|
24
|
+
elsif type.is_a?(Default)
|
25
|
+
result[key] = type.evaluate
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def self.strict_constructor(types, hash)
|
32
|
+
types.each_with_object({}) do |(key, type), result|
|
33
|
+
begin
|
34
|
+
value = hash.fetch(key)
|
35
|
+
result[key] = type[value]
|
36
|
+
rescue TypeError
|
37
|
+
raise SchemaError.new(key, value)
|
38
|
+
rescue KeyError
|
39
|
+
raise SchemaKeyError.new(key)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def strict(type_map)
|
45
|
+
schema(type_map, :strict_constructor)
|
46
|
+
end
|
47
|
+
|
48
|
+
def symbolized(type_map)
|
49
|
+
schema(type_map, :symbolized_constructor)
|
50
|
+
end
|
51
|
+
|
52
|
+
def schema(type_map, meth = :safe_constructor)
|
53
|
+
types = type_map.each_with_object({}) { |(name, type), result|
|
54
|
+
result[name] =
|
55
|
+
case type
|
56
|
+
when String, Class then Types[type]
|
57
|
+
else type
|
58
|
+
end
|
59
|
+
}
|
60
|
+
|
61
|
+
fn = self.class.method(meth).to_proc.curry.(types)
|
62
|
+
|
63
|
+
constructor(fn, schema: types)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require 'dry/types/decorator'
|
2
|
+
|
3
|
+
module Dry
|
4
|
+
module Types
|
5
|
+
class Enum
|
6
|
+
include Decorator
|
7
|
+
|
8
|
+
attr_reader :values, :mapping
|
9
|
+
|
10
|
+
def initialize(type, options)
|
11
|
+
super
|
12
|
+
@values = options.fetch(:values).freeze
|
13
|
+
@values.each(&:freeze)
|
14
|
+
@mapping = values.each_with_object({}) { |v, h| h[values.index(v)] = v }.freeze
|
15
|
+
end
|
16
|
+
|
17
|
+
def call(input)
|
18
|
+
value =
|
19
|
+
if values.include?(input)
|
20
|
+
input
|
21
|
+
elsif mapping.key?(input)
|
22
|
+
mapping[input]
|
23
|
+
end
|
24
|
+
|
25
|
+
type[value || input]
|
26
|
+
end
|
27
|
+
alias_method :[], :call
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
require 'dry/types/coercions/form'
|
2
|
+
|
3
|
+
module Dry
|
4
|
+
module Types
|
5
|
+
register('form.nil') do
|
6
|
+
self['nil'].constructor(Coercions::Form.method(:to_nil))
|
7
|
+
end
|
8
|
+
|
9
|
+
register('form.date') do
|
10
|
+
self['date'].constructor(Coercions::Form.method(:to_date))
|
11
|
+
end
|
12
|
+
|
13
|
+
register('form.date_time') do
|
14
|
+
self['date_time'].constructor(Coercions::Form.method(:to_date_time))
|
15
|
+
end
|
16
|
+
|
17
|
+
register('form.time') do
|
18
|
+
self['time'].constructor(Coercions::Form.method(:to_time))
|
19
|
+
end
|
20
|
+
|
21
|
+
register('form.true') do
|
22
|
+
self['true'].constructor(Coercions::Form.method(:to_true))
|
23
|
+
end
|
24
|
+
|
25
|
+
register('form.false') do
|
26
|
+
self['false'].constructor(Coercions::Form.method(:to_false))
|
27
|
+
end
|
28
|
+
|
29
|
+
register('form.bool') do
|
30
|
+
self['form.true'] | self['form.false']
|
31
|
+
end
|
32
|
+
|
33
|
+
register('form.int') do
|
34
|
+
self['int'].constructor(Coercions::Form.method(:to_int))
|
35
|
+
end
|
36
|
+
|
37
|
+
register('form.float') do
|
38
|
+
self['float'].constructor(Coercions::Form.method(:to_float))
|
39
|
+
end
|
40
|
+
|
41
|
+
register('form.decimal') do
|
42
|
+
self['decimal'].constructor(Coercions::Form.method(:to_decimal))
|
43
|
+
end
|
44
|
+
|
45
|
+
register('form.array') do
|
46
|
+
self['array'].safe
|
47
|
+
end
|
48
|
+
|
49
|
+
register('form.hash') do
|
50
|
+
self['hash'].safe
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'kleisli/maybe'
|
2
|
+
require 'dry/types/decorator'
|
3
|
+
|
4
|
+
module Dry
|
5
|
+
module Types
|
6
|
+
class Optional
|
7
|
+
include Decorator
|
8
|
+
include Builder
|
9
|
+
|
10
|
+
def call(input)
|
11
|
+
input.is_a?(Kleisli::Maybe) ? input : Maybe(type[input])
|
12
|
+
end
|
13
|
+
alias_method :[], :call
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
require 'dry/types/decorator'
|
2
|
+
|
3
|
+
module Dry
|
4
|
+
module Types
|
5
|
+
class Safe
|
6
|
+
include Decorator
|
7
|
+
include Builder
|
8
|
+
|
9
|
+
def call(input)
|
10
|
+
if input.is_a?(primitive)
|
11
|
+
type.call(input)
|
12
|
+
else
|
13
|
+
input
|
14
|
+
end
|
15
|
+
end
|
16
|
+
alias_method :[], :call
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def decorate?(response)
|
21
|
+
super || response.kind_of?(Constructor)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
module Dry
|
2
|
+
module Types
|
3
|
+
class Struct
|
4
|
+
class << self
|
5
|
+
attr_reader :constructor
|
6
|
+
end
|
7
|
+
|
8
|
+
def self.inherited(klass)
|
9
|
+
super
|
10
|
+
Types.register_class(klass) unless klass == Value
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.attribute(name, type)
|
14
|
+
attributes(name => type)
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.attributes(new_schema)
|
18
|
+
prev_schema = schema
|
19
|
+
|
20
|
+
@schema = prev_schema.merge(new_schema)
|
21
|
+
@constructor = Types['coercible.hash'].public_send(constructor_type, schema)
|
22
|
+
|
23
|
+
attr_reader(*(new_schema.keys - prev_schema.keys))
|
24
|
+
|
25
|
+
self
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.constructor_type(type = :strict)
|
29
|
+
@constructor_type ||= type
|
30
|
+
end
|
31
|
+
|
32
|
+
def self.schema
|
33
|
+
super_schema = superclass.respond_to?(:schema) ? superclass.schema : {}
|
34
|
+
super_schema.merge(@schema || {})
|
35
|
+
end
|
36
|
+
|
37
|
+
def self.new(attributes = {})
|
38
|
+
if attributes.is_a?(self)
|
39
|
+
attributes
|
40
|
+
else
|
41
|
+
super(constructor[attributes])
|
42
|
+
end
|
43
|
+
rescue SchemaError, SchemaKeyError => e
|
44
|
+
raise StructError, "[#{self}.new] #{e.message}"
|
45
|
+
end
|
46
|
+
|
47
|
+
def initialize(attributes)
|
48
|
+
attributes.each { |key, value| instance_variable_set("@#{key}", value) }
|
49
|
+
end
|
50
|
+
|
51
|
+
def [](name)
|
52
|
+
public_send(name)
|
53
|
+
end
|
54
|
+
|
55
|
+
def to_hash
|
56
|
+
self.class.schema.keys.each_with_object({}) { |key, result|
|
57
|
+
value = self[key]
|
58
|
+
result[key] = value.respond_to?(:to_hash) ? value.to_hash : value
|
59
|
+
}
|
60
|
+
end
|
61
|
+
alias_method :to_h, :to_hash
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|