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.
Files changed (54) hide show
  1. checksums.yaml +4 -4
  2. data/.editorconfig +6 -2
  3. data/.rubocop.yml +11 -2
  4. data/.ruby-version +2 -0
  5. data/Gemfile.lock +91 -54
  6. data/Guardfile +20 -0
  7. data/Rakefile +32 -0
  8. data/data_model.gemspec +52 -0
  9. data/lib/data_model/boolean.rb +7 -0
  10. data/lib/data_model/builtin/array.rb +73 -0
  11. data/lib/data_model/builtin/big_decimal.rb +64 -0
  12. data/lib/data_model/builtin/boolean.rb +37 -0
  13. data/lib/data_model/builtin/date.rb +60 -0
  14. data/lib/data_model/builtin/float.rb +64 -0
  15. data/lib/data_model/builtin/hash.rb +119 -0
  16. data/lib/data_model/builtin/integer.rb +64 -0
  17. data/lib/data_model/builtin/string.rb +88 -0
  18. data/lib/data_model/builtin/symbol.rb +64 -0
  19. data/lib/data_model/builtin/time.rb +60 -0
  20. data/lib/data_model/builtin.rb +23 -0
  21. data/lib/data_model/error.rb +107 -0
  22. data/lib/data_model/errors.rb +296 -0
  23. data/lib/data_model/fixtures/array.rb +61 -0
  24. data/lib/data_model/fixtures/big_decimal.rb +55 -0
  25. data/lib/data_model/fixtures/boolean.rb +35 -0
  26. data/lib/data_model/fixtures/date.rb +53 -0
  27. data/lib/data_model/fixtures/example.rb +29 -0
  28. data/lib/data_model/fixtures/float.rb +53 -0
  29. data/lib/data_model/fixtures/hash.rb +66 -0
  30. data/lib/data_model/fixtures/integer.rb +53 -0
  31. data/lib/data_model/fixtures/string.rb +110 -0
  32. data/lib/data_model/fixtures/symbol.rb +56 -0
  33. data/lib/data_model/fixtures/time.rb +53 -0
  34. data/lib/data_model/logging.rb +23 -0
  35. data/lib/data_model/model.rb +21 -44
  36. data/lib/data_model/scanner.rb +92 -56
  37. data/lib/data_model/testing/minitest.rb +79 -0
  38. data/lib/data_model/testing.rb +6 -0
  39. data/lib/data_model/type.rb +41 -39
  40. data/lib/data_model/type_registry.rb +68 -0
  41. data/lib/data_model/version.rb +3 -1
  42. data/lib/data_model.rb +32 -16
  43. data/sorbet/config +4 -0
  44. data/sorbet/rbi/annotations/rainbow.rbi +269 -0
  45. data/sorbet/rbi/gems/minitest@5.18.0.rbi +1491 -0
  46. data/sorbet/rbi/gems/zeitwerk.rbi +196 -0
  47. data/sorbet/rbi/gems/zeitwerk@2.6.7.rbi +966 -0
  48. data/sorbet/rbi/todo.rbi +5 -0
  49. data/sorbet/tapioca/config.yml +13 -0
  50. data/sorbet/tapioca/require.rb +4 -0
  51. metadata +139 -17
  52. data/config/sus.rb +0 -2
  53. data/fixtures/schema.rb +0 -14
  54. 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