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
data/lib/dry-types.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require 'dry/types'
|
data/lib/dry/types.rb
ADDED
@@ -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
|