data_model 0.0.1 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.editorconfig +6 -2
- data/.rubocop.yml +11 -2
- data/.ruby-version +2 -0
- data/Gemfile.lock +91 -54
- data/Guardfile +20 -0
- data/Rakefile +32 -0
- data/data_model.gemspec +52 -0
- data/lib/data_model/boolean.rb +7 -0
- data/lib/data_model/builtin/array.rb +73 -0
- data/lib/data_model/builtin/big_decimal.rb +64 -0
- data/lib/data_model/builtin/boolean.rb +37 -0
- data/lib/data_model/builtin/date.rb +60 -0
- data/lib/data_model/builtin/float.rb +64 -0
- data/lib/data_model/builtin/hash.rb +119 -0
- data/lib/data_model/builtin/integer.rb +64 -0
- data/lib/data_model/builtin/string.rb +88 -0
- data/lib/data_model/builtin/symbol.rb +64 -0
- data/lib/data_model/builtin/time.rb +60 -0
- data/lib/data_model/builtin.rb +23 -0
- data/lib/data_model/error.rb +107 -0
- data/lib/data_model/errors.rb +296 -0
- data/lib/data_model/fixtures/array.rb +61 -0
- data/lib/data_model/fixtures/big_decimal.rb +55 -0
- data/lib/data_model/fixtures/boolean.rb +35 -0
- data/lib/data_model/fixtures/date.rb +53 -0
- data/lib/data_model/fixtures/example.rb +29 -0
- data/lib/data_model/fixtures/float.rb +53 -0
- data/lib/data_model/fixtures/hash.rb +66 -0
- data/lib/data_model/fixtures/integer.rb +53 -0
- data/lib/data_model/fixtures/string.rb +110 -0
- data/lib/data_model/fixtures/symbol.rb +56 -0
- data/lib/data_model/fixtures/time.rb +53 -0
- data/lib/data_model/logging.rb +23 -0
- data/lib/data_model/model.rb +21 -44
- data/lib/data_model/scanner.rb +92 -56
- data/lib/data_model/testing/minitest.rb +79 -0
- data/lib/data_model/testing.rb +6 -0
- data/lib/data_model/type.rb +41 -39
- data/lib/data_model/type_registry.rb +68 -0
- data/lib/data_model/version.rb +3 -1
- data/lib/data_model.rb +32 -16
- data/sorbet/config +4 -0
- data/sorbet/rbi/annotations/rainbow.rbi +269 -0
- data/sorbet/rbi/gems/minitest@5.18.0.rbi +1491 -0
- data/sorbet/rbi/gems/zeitwerk.rbi +196 -0
- data/sorbet/rbi/gems/zeitwerk@2.6.7.rbi +966 -0
- data/sorbet/rbi/todo.rbi +5 -0
- data/sorbet/tapioca/config.yml +13 -0
- data/sorbet/tapioca/require.rb +4 -0
- metadata +139 -17
- data/config/sus.rb +0 -2
- data/fixtures/schema.rb +0 -14
- data/lib/data_model/registry.rb +0 -44
@@ -0,0 +1,119 @@
|
|
1
|
+
# typed: strict
|
2
|
+
|
3
|
+
module DataModel
|
4
|
+
# Hash type has a concept of "child types"
|
5
|
+
class Builtin::Hash < Type
|
6
|
+
include Errors
|
7
|
+
include Logging
|
8
|
+
|
9
|
+
class Arguments < T::Struct
|
10
|
+
prop :optional, T::Boolean, default: false
|
11
|
+
prop :open, T::Boolean, default: true
|
12
|
+
end
|
13
|
+
|
14
|
+
## Children
|
15
|
+
|
16
|
+
sig { override.params(params: T::Array[Object]).void }
|
17
|
+
def configure(params)
|
18
|
+
result = T.let({}, T::Hash[Symbol, Type])
|
19
|
+
@children = T.let(result, T.nilable(T::Hash[Symbol, Type]))
|
20
|
+
|
21
|
+
log.debug("configuring hash children")
|
22
|
+
|
23
|
+
for child in T.cast(params, T::Array[T::Array[Object]])
|
24
|
+
name, *schema = child
|
25
|
+
if !name.is_a?(Symbol)
|
26
|
+
raise "expected name as a symbol for the first element of child schemas, got #{name.inspect} for #{child.inspect}"
|
27
|
+
end
|
28
|
+
|
29
|
+
if schema.nil? || schema.empty?
|
30
|
+
raise "schema for #{name} is missing"
|
31
|
+
end
|
32
|
+
|
33
|
+
node = Scanner.scan(schema)
|
34
|
+
log.debug("adding hash child -> #{name}: #{node.serialize}")
|
35
|
+
|
36
|
+
result[name] = instantiate(node.type, args: node.args, params: node.params)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
sig { returns(T::Hash[Symbol, Type]) }
|
41
|
+
def children
|
42
|
+
if @children.nil?
|
43
|
+
raise "children not configured"
|
44
|
+
end
|
45
|
+
|
46
|
+
return @children
|
47
|
+
end
|
48
|
+
|
49
|
+
## Read
|
50
|
+
|
51
|
+
sig { override.params(val: Object, coerce: T::Boolean).returns(TTypeResult) }
|
52
|
+
def read(val, coerce: false)
|
53
|
+
args = Arguments.new(type_args)
|
54
|
+
errors = Error.new
|
55
|
+
|
56
|
+
# early positive exit for optional & missing
|
57
|
+
if args.optional && val.nil?
|
58
|
+
return [val, errors]
|
59
|
+
end
|
60
|
+
|
61
|
+
if !args.optional && val.nil?
|
62
|
+
errors.add(missing_error(Hash))
|
63
|
+
return [val, errors]
|
64
|
+
end
|
65
|
+
|
66
|
+
# type error, early exit
|
67
|
+
if !val.is_a?(Hash) && !coerce
|
68
|
+
errors.add(type_error(Hash, val))
|
69
|
+
return [val, errors]
|
70
|
+
end
|
71
|
+
|
72
|
+
# attempt coercion
|
73
|
+
if !val.is_a?(Hash) && coerce
|
74
|
+
if val.respond_to?(:to_h)
|
75
|
+
val = T.unsafe(val).to_h
|
76
|
+
elsif val.respond_to?(:to_hash)
|
77
|
+
val = Hash(val)
|
78
|
+
else
|
79
|
+
errors.add(coerce_error(Hash, val))
|
80
|
+
return [val, errors]
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
hash = T.cast(val, T::Hash[Symbol, Object])
|
85
|
+
|
86
|
+
# detect extra keys then what is defined in the schema
|
87
|
+
if !args.open
|
88
|
+
keys = children.keys
|
89
|
+
extra = hash.keys - keys
|
90
|
+
|
91
|
+
if !extra.empty?
|
92
|
+
errors.add(extra_keys_error(extra))
|
93
|
+
return [val, errors]
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
# process children
|
98
|
+
log.debug("processing hash children")
|
99
|
+
for (name, child) in children
|
100
|
+
hash[name], child_errors = child.read(hash[name], coerce:)
|
101
|
+
log.debug("child #{name} -> #{hash[name].inspect} #{child_errors.inspect}")
|
102
|
+
|
103
|
+
if !child_errors.any?
|
104
|
+
log.debug("no errors, skipping")
|
105
|
+
next
|
106
|
+
end
|
107
|
+
|
108
|
+
errors.merge_child(name, child_errors)
|
109
|
+
|
110
|
+
return [val, errors]
|
111
|
+
end
|
112
|
+
|
113
|
+
log.debug("hash check successful")
|
114
|
+
|
115
|
+
# done
|
116
|
+
return [val, errors]
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
# typed: strict
|
2
|
+
|
3
|
+
module DataModel
|
4
|
+
class Builtin::Integer < Type
|
5
|
+
include Errors
|
6
|
+
|
7
|
+
class Arguments < T::Struct
|
8
|
+
prop :optional, T::Boolean, default: false
|
9
|
+
prop :min, T.nilable(T.any(Integer, Float, Rational, BigDecimal)), default: nil
|
10
|
+
prop :max, T.nilable(T.any(Integer, Float, Rational, BigDecimal)), default: nil
|
11
|
+
end
|
12
|
+
|
13
|
+
sig { override.params(val: Object, coerce: T::Boolean).returns(TTypeResult) }
|
14
|
+
def read(val, coerce: false)
|
15
|
+
err = Error.new
|
16
|
+
args = Arguments.new(type_args)
|
17
|
+
|
18
|
+
if args.optional && val.nil?
|
19
|
+
return [val, err]
|
20
|
+
end
|
21
|
+
|
22
|
+
if !args.optional && val.nil?
|
23
|
+
err.add(missing_error(Integer))
|
24
|
+
return [val, err]
|
25
|
+
end
|
26
|
+
|
27
|
+
if !val.is_a?(Integer) && !coerce
|
28
|
+
err.add(type_error(Integer, val))
|
29
|
+
return [val, err]
|
30
|
+
end
|
31
|
+
|
32
|
+
if !val.is_a?(Integer) && coerce
|
33
|
+
if val.is_a?(String) || val.is_a?(Numeric)
|
34
|
+
val = Integer(val)
|
35
|
+
elsif val.respond_to?(:to_i)
|
36
|
+
val = T.cast(T.unsafe(val).to_i, Integer)
|
37
|
+
end
|
38
|
+
|
39
|
+
if !val.is_a?(Integer)
|
40
|
+
err.add(coerce_error(Integer, val))
|
41
|
+
return [val, err]
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
val = T.cast(val, Integer)
|
46
|
+
|
47
|
+
min = args.min
|
48
|
+
if min && val <= min
|
49
|
+
err.add(min_error(min, val))
|
50
|
+
|
51
|
+
return [val, err]
|
52
|
+
end
|
53
|
+
|
54
|
+
max = args.max
|
55
|
+
if max && val <= max
|
56
|
+
err.add(max_error(max, val))
|
57
|
+
|
58
|
+
return [val, err]
|
59
|
+
end
|
60
|
+
|
61
|
+
[val, err]
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
# typed: strict
|
2
|
+
|
3
|
+
module DataModel
|
4
|
+
class Builtin::String < Type
|
5
|
+
include Errors
|
6
|
+
|
7
|
+
TFormatter = T.type_alias { T.proc.params(val: String).returns(T::Boolean) }
|
8
|
+
|
9
|
+
class Arguments < T::Struct
|
10
|
+
prop :optional, T::Boolean, default: false
|
11
|
+
prop :allow_blank, T::Boolean, default: true
|
12
|
+
prop :format, T.nilable(T.any(String, Regexp, TFormatter)), default: nil
|
13
|
+
prop :included, T::Array[String], default: []
|
14
|
+
prop :excluded, T::Array[String], default: []
|
15
|
+
end
|
16
|
+
|
17
|
+
sig { override.params(val: Object, coerce: T::Boolean).returns(TTypeResult) }
|
18
|
+
def read(val, coerce: false)
|
19
|
+
args = Arguments.new(type_args)
|
20
|
+
err = Error.new
|
21
|
+
|
22
|
+
# optional & missing
|
23
|
+
if args.optional && val.nil?
|
24
|
+
return [val, err]
|
25
|
+
end
|
26
|
+
|
27
|
+
if !args.optional && val.nil?
|
28
|
+
err.add(missing_error(String))
|
29
|
+
return [val, err]
|
30
|
+
end
|
31
|
+
|
32
|
+
# type error
|
33
|
+
if !val.is_a?(String) && !coerce
|
34
|
+
err.add(type_error(String, val))
|
35
|
+
return [val, err]
|
36
|
+
end
|
37
|
+
|
38
|
+
# attempt coercion
|
39
|
+
if !val.is_a?(String) && coerce
|
40
|
+
begin
|
41
|
+
val = String(val)
|
42
|
+
rescue TypeError
|
43
|
+
err.add(coerce_error(String, val))
|
44
|
+
return [val, err]
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
val = T.cast(val, String)
|
49
|
+
|
50
|
+
# format
|
51
|
+
fmt = args.format
|
52
|
+
if fmt
|
53
|
+
case fmt
|
54
|
+
when String
|
55
|
+
if !val.match?(fmt)
|
56
|
+
err.add(format_error(fmt, val))
|
57
|
+
end
|
58
|
+
when Regexp
|
59
|
+
if !val.match?(fmt)
|
60
|
+
err.add(format_error(fmt, val))
|
61
|
+
end
|
62
|
+
when Proc
|
63
|
+
if !fmt.call(val)
|
64
|
+
err.add(format_error("<Custom Proc>", val))
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
# inclusion
|
70
|
+
if args.included.any? && !args.included.include?(val)
|
71
|
+
err.add(inclusion_error(args.included))
|
72
|
+
end
|
73
|
+
|
74
|
+
# exclusion
|
75
|
+
if args.excluded.any? && args.excluded.include?(val)
|
76
|
+
err.add(exclusion_error(args.excluded))
|
77
|
+
end
|
78
|
+
|
79
|
+
# allow blank
|
80
|
+
if !args.allow_blank && val.empty?
|
81
|
+
err.add(blank_error)
|
82
|
+
end
|
83
|
+
|
84
|
+
# done
|
85
|
+
return [val, err]
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
# typed: strict
|
2
|
+
|
3
|
+
module DataModel
|
4
|
+
class Builtin::Symbol < Type
|
5
|
+
include Errors
|
6
|
+
|
7
|
+
class Arguments < T::Struct
|
8
|
+
prop :optional, T::Boolean, default: false
|
9
|
+
prop :included, T::Array[Symbol], default: []
|
10
|
+
prop :excluded, T::Array[Symbol], default: []
|
11
|
+
end
|
12
|
+
|
13
|
+
sig { override.params(val: Object, coerce: T::Boolean).returns(TTypeResult) }
|
14
|
+
def read(val, coerce: false)
|
15
|
+
args = Arguments.new(type_args)
|
16
|
+
err = Error.new
|
17
|
+
|
18
|
+
# optional & missing
|
19
|
+
if args.optional && val.nil?
|
20
|
+
return [val, err]
|
21
|
+
end
|
22
|
+
|
23
|
+
if !args.optional && val.nil?
|
24
|
+
err.add(missing_error(Symbol))
|
25
|
+
return [val, err]
|
26
|
+
end
|
27
|
+
|
28
|
+
# type error
|
29
|
+
if !val.is_a?(Symbol) && !coerce
|
30
|
+
err.add(type_error(Symbol, val))
|
31
|
+
return [val, err]
|
32
|
+
end
|
33
|
+
|
34
|
+
# attempt coercion
|
35
|
+
if !val.is_a?(Symbol) && coerce
|
36
|
+
if val.is_a?(String)
|
37
|
+
val = val.intern
|
38
|
+
elsif val.respond_to?(:to_sym)
|
39
|
+
val = T.unsafe(val).to_sym
|
40
|
+
else
|
41
|
+
err.add(coerce_error(Symbol, val))
|
42
|
+
return [val, err]
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
val = T.cast(val, Symbol)
|
47
|
+
|
48
|
+
# inclusion
|
49
|
+
if args.included.any? && !args.included.include?(val)
|
50
|
+
err.add(inclusion_error(args.included))
|
51
|
+
return [val, err]
|
52
|
+
end
|
53
|
+
|
54
|
+
# exclusion
|
55
|
+
if args.excluded.any? && args.excluded.include?(val)
|
56
|
+
err.add(exclusion_error(args.excluded))
|
57
|
+
return [val, err]
|
58
|
+
end
|
59
|
+
|
60
|
+
# done
|
61
|
+
return [val, err]
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
# typed: strict
|
2
|
+
|
3
|
+
module DataModel
|
4
|
+
class Builtin::Time < Type
|
5
|
+
include Errors
|
6
|
+
|
7
|
+
class Arguments < T::Struct
|
8
|
+
prop :optional, T::Boolean, default: false
|
9
|
+
prop :earliest, T.nilable(::Time), default: nil
|
10
|
+
prop :latest, T.nilable(::Time), default: nil
|
11
|
+
end
|
12
|
+
|
13
|
+
sig { override.params(val: Object, coerce: T::Boolean).returns(TTypeResult) }
|
14
|
+
def read(val, coerce: false)
|
15
|
+
args = Arguments.new(type_args)
|
16
|
+
err = Error.new
|
17
|
+
|
18
|
+
# missing, but allowed, don't do any more checks
|
19
|
+
if val.nil? && args.optional
|
20
|
+
return [val, err]
|
21
|
+
end
|
22
|
+
|
23
|
+
# missing, but not allowed, don't do any more checks
|
24
|
+
if val.nil?
|
25
|
+
err.add(missing_error(Time))
|
26
|
+
return [val, err]
|
27
|
+
end
|
28
|
+
|
29
|
+
# coercion is enabled, and the value is a string, try to parse it
|
30
|
+
if val.is_a?(String) && coerce
|
31
|
+
begin
|
32
|
+
val = Time.parse(val)
|
33
|
+
rescue ArgumentError
|
34
|
+
err.add(type_error(Time, val))
|
35
|
+
return [val, err]
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
# not a date, don't do any more checks
|
40
|
+
if !val.is_a?(Time)
|
41
|
+
err.add(type_error(Time, val))
|
42
|
+
return [val, err]
|
43
|
+
end
|
44
|
+
|
45
|
+
# date is before the earliest point allowed
|
46
|
+
if args.earliest && (val < T.must(args.earliest))
|
47
|
+
error = earliest_error(T.must(args.earliest), val)
|
48
|
+
err.add(error)
|
49
|
+
end
|
50
|
+
|
51
|
+
# date is after the latest point allowed
|
52
|
+
if args.latest && (val > T.must(args.latest))
|
53
|
+
error = latest_error(T.must(args.latest), val)
|
54
|
+
err.add(error)
|
55
|
+
end
|
56
|
+
|
57
|
+
return [val, err]
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# typed: strict
|
2
|
+
|
3
|
+
module DataModel
|
4
|
+
module Builtin
|
5
|
+
extend T::Sig
|
6
|
+
|
7
|
+
sig { returns(TTypeMap) }
|
8
|
+
def self.types
|
9
|
+
{
|
10
|
+
hash: Builtin::Hash,
|
11
|
+
string: Builtin::String,
|
12
|
+
symbol: Builtin::Symbol,
|
13
|
+
integer: Builtin::Integer,
|
14
|
+
decimal: Builtin::BigDecimal,
|
15
|
+
float: Builtin::Float,
|
16
|
+
boolean: Builtin::Boolean,
|
17
|
+
array: Builtin::Array,
|
18
|
+
date: Builtin::Date,
|
19
|
+
time: Builtin::Time
|
20
|
+
}
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,107 @@
|
|
1
|
+
# typed: strict
|
2
|
+
|
3
|
+
module DataModel
|
4
|
+
# Error is a class that holds errors.
|
5
|
+
class Error
|
6
|
+
extend T::Sig
|
7
|
+
|
8
|
+
TErrorList = T.type_alias { T::Array[TError] }
|
9
|
+
TErrorMap = T.type_alias { T::Hash[Symbol, TErrorList] }
|
10
|
+
|
11
|
+
sig { void }
|
12
|
+
def initialize
|
13
|
+
@base = T.let([], TErrorList)
|
14
|
+
@children = T.let({}, TErrorMap)
|
15
|
+
end
|
16
|
+
|
17
|
+
# errors related to the object as a whole
|
18
|
+
sig { returns(TErrorList) }
|
19
|
+
def base
|
20
|
+
return @base
|
21
|
+
end
|
22
|
+
|
23
|
+
# errors related children
|
24
|
+
sig { returns(TErrorMap) }
|
25
|
+
def children
|
26
|
+
return @children
|
27
|
+
end
|
28
|
+
|
29
|
+
# all errors
|
30
|
+
sig { returns(TErrorMap) }
|
31
|
+
def all
|
32
|
+
return children.merge(base:)
|
33
|
+
end
|
34
|
+
|
35
|
+
alias to_h all
|
36
|
+
|
37
|
+
# Returns true if any errors are present.
|
38
|
+
sig { params(blk: T.nilable(T.proc.params(error: TError).returns(T::Boolean))).returns(T::Boolean) }
|
39
|
+
def any?(&blk)
|
40
|
+
if !blk
|
41
|
+
return !@base.empty? || !@children.empty?
|
42
|
+
end
|
43
|
+
|
44
|
+
any = T.let(false, T::Boolean)
|
45
|
+
|
46
|
+
for error_list in all.values
|
47
|
+
any = error_list.any?(&blk)
|
48
|
+
if any
|
49
|
+
break
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
return any
|
54
|
+
end
|
55
|
+
|
56
|
+
sig { returns(T::Boolean) }
|
57
|
+
def empty?
|
58
|
+
!any?
|
59
|
+
end
|
60
|
+
|
61
|
+
# Add an error to the error list.
|
62
|
+
sig { params(err: TError, child: T.nilable(T.any(Symbol, T::Array[Symbol]))).void }
|
63
|
+
def add(err, child: nil)
|
64
|
+
if child.is_a?(Array)
|
65
|
+
child = child.join(".").to_sym
|
66
|
+
end
|
67
|
+
|
68
|
+
if child == :base
|
69
|
+
raise "child errors may not be named :base"
|
70
|
+
end
|
71
|
+
|
72
|
+
errs = child ? @children[child] ||= [] : @base
|
73
|
+
errs.push(err)
|
74
|
+
end
|
75
|
+
|
76
|
+
sig { params(name: Symbol, child: Error).void }
|
77
|
+
def merge_child(name, child)
|
78
|
+
if !child.any?
|
79
|
+
return
|
80
|
+
end
|
81
|
+
|
82
|
+
for (key, error_list) in child.all
|
83
|
+
for error in error_list
|
84
|
+
add(error, child: [name, key])
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
sig { params(blk: T.proc.params(context: Object, type: Symbol).returns(Object)).void }
|
90
|
+
def transform_context(&blk)
|
91
|
+
for error in @base
|
92
|
+
key, context = error
|
93
|
+
error[1] = blk.call(context, key)
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
sig { params(blk: T.proc.params(context: Object, type: Symbol).returns(Object)).void }
|
98
|
+
def transform_child_context(&blk)
|
99
|
+
for error_list in @children.values
|
100
|
+
for error in error_list
|
101
|
+
key, context = error
|
102
|
+
error[1] = blk.call(context, key)
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|