data_model 0.0.1 → 0.2.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 +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
|