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
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
|