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 @@
1
+ require 'dry/types'
@@ -0,0 +1,115 @@
1
+ require 'bigdecimal'
2
+ require 'date'
3
+ require 'set'
4
+
5
+ require 'inflecto'
6
+ require 'thread_safe'
7
+
8
+ require 'dry-container'
9
+ require 'dry-equalizer'
10
+
11
+ require 'dry/types/version'
12
+ require 'dry/types/container'
13
+ require 'dry/types/definition'
14
+ require 'dry/types/constructor'
15
+ require 'dry/types/struct'
16
+ require 'dry/types/value'
17
+
18
+ module Dry
19
+ module Types
20
+ extend Dry::Configurable
21
+
22
+ setting :namespace, self
23
+
24
+ class SchemaError < TypeError
25
+ def initialize(key, value)
26
+ super("#{value.inspect} (#{value.class}) has invalid type for :#{key}")
27
+ end
28
+ end
29
+
30
+ class SchemaKeyError < KeyError
31
+ def initialize(key)
32
+ super(":#{key} is missing in Hash input")
33
+ end
34
+ end
35
+
36
+ StructError = Class.new(TypeError)
37
+ ConstraintError = Class.new(TypeError)
38
+
39
+ TYPE_SPEC_REGEX = %r[(.+)<(.+)>].freeze
40
+
41
+ def self.module
42
+ namespace = Module.new
43
+ define_constants(namespace, type_keys)
44
+ namespace
45
+ end
46
+
47
+ def self.finalize
48
+ warn 'Dry::Types.finalize and configuring namespace is deprecated. Just'\
49
+ ' do `include Dry::Types.module` in places where you want to have access'\
50
+ ' to built-in types'
51
+
52
+ define_constants(config.namespace, type_keys)
53
+ end
54
+
55
+ def self.container
56
+ @container ||= Container.new
57
+ end
58
+
59
+ def self.register(name, type = nil, &block)
60
+ container.register(name, type || block.call)
61
+ end
62
+
63
+ def self.register_class(klass, meth = :new)
64
+ type = Definition.new(klass).constructor(klass.method(meth))
65
+ container.register(identifier(klass), type)
66
+ end
67
+
68
+ def self.[](name)
69
+ type_map.fetch_or_store(name) do
70
+ case name
71
+ when String
72
+ result = name.match(TYPE_SPEC_REGEX)
73
+
74
+ if result
75
+ type_id, member_id = result[1..2]
76
+ container[type_id].member(self[member_id])
77
+ else
78
+ container[name]
79
+ end
80
+ when Class
81
+ self[identifier(name)]
82
+ end
83
+ end
84
+ end
85
+
86
+ def self.define_constants(namespace, identifiers)
87
+ names = identifiers.map do |id|
88
+ parts = id.split('.')
89
+ [Inflecto.camelize(parts.pop), parts.map(&Inflecto.method(:camelize))]
90
+ end
91
+
92
+ names.map do |(klass, parts)|
93
+ mod = parts.reduce(namespace) do |a, e|
94
+ a.constants.include?(e.to_sym) ? a.const_get(e) : a.const_set(e, Module.new)
95
+ end
96
+
97
+ mod.const_set(klass, self[identifier((parts + [klass]).join('::'))])
98
+ end
99
+ end
100
+
101
+ def self.identifier(klass)
102
+ Inflecto.underscore(klass).tr('/', '.')
103
+ end
104
+
105
+ def self.type_map
106
+ @type_map ||= ThreadSafe::Cache.new
107
+ end
108
+
109
+ def self.type_keys
110
+ container._container.keys
111
+ end
112
+ end
113
+ end
114
+
115
+ require 'dry/types/core' # load built-in types
@@ -0,0 +1,46 @@
1
+ module Dry
2
+ module Types
3
+ module Builder
4
+ def |(other)
5
+ Sum.new(self, other)
6
+ end
7
+
8
+ def optional
9
+ Optional.new(Types['nil'] | self)
10
+ end
11
+
12
+ def constrained(options)
13
+ Constrained.new(self, rule: Types.Rule(options))
14
+ end
15
+
16
+ def default(input = nil, &block)
17
+ value = input ? input : block
18
+
19
+ if value.is_a?(Proc) || valid?(value)
20
+ Default[value].new(self, value: value)
21
+ else
22
+ raise ConstraintError, "default value #{value.inspect} violates constraints"
23
+ end
24
+ end
25
+
26
+ def enum(*values)
27
+ Enum.new(constrained(inclusion: values), values: values)
28
+ end
29
+
30
+ def safe
31
+ Safe.new(self)
32
+ end
33
+
34
+ def constructor(constructor, options = {})
35
+ Constructor.new(with(options), fn: constructor)
36
+ end
37
+ end
38
+ end
39
+ end
40
+
41
+ require 'dry/types/default'
42
+ require 'dry/types/constrained'
43
+ require 'dry/types/enum'
44
+ require 'dry/types/optional'
45
+ require 'dry/types/safe'
46
+ require 'dry/types/sum'
@@ -0,0 +1,91 @@
1
+ require 'date'
2
+ require 'bigdecimal'
3
+ require 'bigdecimal/util'
4
+
5
+ module Dry
6
+ module Types
7
+ module Coercions
8
+ module Form
9
+ TRUE_VALUES = %w[1 on t true y yes].freeze
10
+ FALSE_VALUES = %w[0 off f false n no].freeze
11
+ BOOLEAN_MAP = Hash[TRUE_VALUES.product([true]) + FALSE_VALUES.product([false])].freeze
12
+
13
+ def self.to_nil(input)
14
+ if input.is_a?(String) && input == ''
15
+ nil
16
+ else
17
+ input
18
+ end
19
+ end
20
+
21
+ def self.to_date(input)
22
+ Date.parse(input)
23
+ rescue ArgumentError
24
+ input
25
+ end
26
+
27
+ def self.to_date_time(input)
28
+ DateTime.parse(input)
29
+ rescue ArgumentError
30
+ input
31
+ end
32
+
33
+ def self.to_time(input)
34
+ Time.parse(input)
35
+ rescue ArgumentError
36
+ input
37
+ end
38
+
39
+ def self.to_true(input)
40
+ BOOLEAN_MAP.fetch(input, input)
41
+ end
42
+
43
+ def self.to_false(input)
44
+ BOOLEAN_MAP.fetch(input, input)
45
+ end
46
+
47
+ def self.to_int(input)
48
+ if input == ''
49
+ nil
50
+ else
51
+ result = input.to_i
52
+
53
+ if result === 0 && input != '0'
54
+ input
55
+ else
56
+ result
57
+ end
58
+ end
59
+ end
60
+
61
+ def self.to_float(input)
62
+ if input == ''
63
+ nil
64
+ else
65
+ result = input.to_f
66
+
67
+ if result == 0.0 && (input != '0' || input != '0.0')
68
+ input
69
+ else
70
+ result
71
+ end
72
+ end
73
+ end
74
+
75
+ def self.to_decimal(input)
76
+ if input == ''
77
+ nil
78
+ else
79
+ result = to_float(input)
80
+
81
+ if result.is_a?(Float)
82
+ result.to_d
83
+ else
84
+ result
85
+ end
86
+ end
87
+ end
88
+ end
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,63 @@
1
+ module Dry
2
+ module Types
3
+ class Compiler
4
+ attr_reader :registry
5
+
6
+ def initialize(registry)
7
+ @registry = registry
8
+ end
9
+
10
+ def call(ast)
11
+ visit(ast)
12
+ end
13
+
14
+ def visit(node, *args)
15
+ send(:"visit_#{node[0]}", node[1], *args)
16
+ end
17
+
18
+ def visit_type(node)
19
+ type, args = node
20
+ meth = :"visit_#{type.tr('.', '_')}"
21
+
22
+ if respond_to?(meth)
23
+ send(meth, args)
24
+ else
25
+ registry[type]
26
+ end
27
+ end
28
+
29
+ def visit_sum(node)
30
+ node.map { |type| visit(type) }.reduce(:|)
31
+ end
32
+
33
+ def visit_array(node)
34
+ registry['array'].member(call(node))
35
+ end
36
+
37
+ def visit_form_array(node)
38
+ registry['form.array'].member(call(node))
39
+ end
40
+
41
+ def visit_hash(node)
42
+ constructor, schema = node
43
+ merge_with('hash', constructor, schema)
44
+ end
45
+
46
+ def visit_form_hash(node)
47
+ constructor, schema = node
48
+ merge_with('form.hash', constructor, schema)
49
+ end
50
+
51
+ def visit_key(node)
52
+ name, types = node
53
+ { name => visit(types) }
54
+ end
55
+
56
+ def merge_with(hash_id, constructor, schema)
57
+ registry[hash_id].__send__(
58
+ constructor, schema.map { |key| visit(key) }.reduce(:merge)
59
+ )
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,47 @@
1
+ require 'dry/types/decorator'
2
+ require 'dry/types/constraints'
3
+
4
+ module Dry
5
+ module Types
6
+ class Constrained
7
+ include Decorator
8
+ include Builder
9
+
10
+ attr_reader :rule
11
+
12
+ def initialize(type, options)
13
+ super
14
+ @rule = options.fetch(:rule)
15
+ end
16
+
17
+ def call(input)
18
+ result = try(input)
19
+
20
+ if valid?(result)
21
+ result
22
+ else
23
+ raise ConstraintError, "#{input.inspect} violates constraints"
24
+ end
25
+ end
26
+ alias_method :[], :call
27
+
28
+ def try(input)
29
+ type[input]
30
+ end
31
+
32
+ def valid?(input)
33
+ rule.(input).success?
34
+ end
35
+
36
+ def constrained(options)
37
+ with(rule: rule & Types.Rule(options))
38
+ end
39
+
40
+ private
41
+
42
+ def decorate?(response)
43
+ super || response.kind_of?(Constructor)
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,24 @@
1
+ require 'dry/logic/rule_compiler'
2
+ require 'dry/logic/predicates'
3
+
4
+ module Dry
5
+ module Types
6
+ module Predicates
7
+ include Logic::Predicates
8
+
9
+ predicate(:type?) do |type, value|
10
+ value.kind_of?(type)
11
+ end
12
+ end
13
+
14
+ def self.Rule(options)
15
+ rule_compiler.(
16
+ options.map { |key, val| [:val, [:predicate, [:"#{key}?", [val]]]] }
17
+ ).reduce(:and)
18
+ end
19
+
20
+ def self.rule_compiler
21
+ @rule_compiler ||= Logic::RuleCompiler.new(Types::Predicates)
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,59 @@
1
+ require 'dry/types/decorator'
2
+
3
+ module Dry
4
+ module Types
5
+ class Constructor < Definition
6
+ include Dry::Equalizer(:type)
7
+
8
+ undef_method :primitive
9
+
10
+ attr_reader :fn
11
+
12
+ attr_reader :type
13
+
14
+ def self.new(input, options = {})
15
+ type = input.is_a?(Definition) ? input : Definition.new(input)
16
+ super(type, options)
17
+ end
18
+
19
+ def initialize(type, options = {})
20
+ super
21
+ @type = type
22
+ @fn = options.fetch(:fn)
23
+ end
24
+
25
+ def primitive
26
+ type.primitive
27
+ end
28
+
29
+ def call(input)
30
+ fn[input]
31
+ end
32
+ alias_method :[], :call
33
+
34
+ def constructor(new_fn, options = {})
35
+ with(options.merge(fn: -> input { new_fn[fn[input]] }))
36
+ end
37
+
38
+ def respond_to_missing?(meth, include_private = false)
39
+ super || type.respond_to?(meth)
40
+ end
41
+
42
+ private
43
+
44
+ def method_missing(meth, *args, &block)
45
+ if type.respond_to?(meth)
46
+ response = type.__send__(meth, *args, &block)
47
+
48
+ if response.is_a?(Constructor)
49
+ constructor(response.fn, options.merge(response.options))
50
+ else
51
+ response
52
+ end
53
+ else
54
+ super
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end