dry-types 0.6.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|