dry-types 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ module Dry
2
+ module Types
3
+ class Container
4
+ include Dry::Container::Mixin
5
+ end
6
+ end
7
+ end
@@ -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