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