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,296 @@
1
+ # typed: strict
2
+
3
+ module DataModel
4
+ # Provide Error building functionality as a mixin
5
+ module Errors
6
+ include Kernel
7
+ extend T::Sig
8
+
9
+ TTemporal = T.type_alias { T.any(::Date, ::Time, ::DateTime) }
10
+
11
+ ## Constructors
12
+
13
+ # Type error applies when a value is not of the expected type
14
+ sig { params(cls: T.class_of(Object), value: Object).returns(TError) }
15
+ def type_error(cls, value)
16
+ [:type, [cls, value]]
17
+ end
18
+
19
+ # Coerce error applies when a value cannot be coerced to the expected type
20
+ sig { params(cls: T.class_of(Object), value: Object).returns(TError) }
21
+ def coerce_error(cls, value)
22
+ [:coerce, [cls, value]]
23
+ end
24
+
25
+ # Missing error applies when a value is missing
26
+ sig { params(cls: T.class_of(Object)).returns(TError) }
27
+ def missing_error(cls)
28
+ [:missing, cls]
29
+ end
30
+
31
+ # Inclusion error applies when a value is not in a set of allowed values
32
+ sig { params(set: T::Array[T.any(Symbol, String)]).returns(TError) }
33
+ def inclusion_error(set)
34
+ [:inclusion, set]
35
+ end
36
+
37
+ # Exclusive error applies when a value is in a set of disallowed values
38
+ sig { params(set: T::Array[T.any(Symbol, String)]).returns(TError) }
39
+ def exclusion_error(set)
40
+ [:exclusion, set]
41
+ end
42
+
43
+ # Blank error applies when a value is blank
44
+ sig { returns(TError) }
45
+ def blank_error
46
+ [:blank, nil]
47
+ end
48
+
49
+ # Extra keys error applies when a hash has extra keys
50
+ sig { params(keys: T::Array[Symbol]).returns(TError) }
51
+ def extra_keys_error(keys)
52
+ [:extra_keys, keys]
53
+ end
54
+
55
+ # Min applies when value is less then the minimum
56
+ sig { params(min: Numeric, val: Numeric).returns(TError) }
57
+ def min_error(min, val)
58
+ [:min, [min, val]]
59
+ end
60
+
61
+ # Max applies when value is less then the minimum
62
+ sig { params(min: Numeric, val: Numeric).returns(TError) }
63
+ def max_error(min, val)
64
+ [:max, [min, val]]
65
+ end
66
+
67
+ # Earliest applies when value is earlier then earliest
68
+ sig { params(earliest: TTemporal, val: TTemporal).returns(TError) }
69
+ def earliest_error(earliest, val)
70
+ [:earliest, [earliest, val]]
71
+ end
72
+
73
+ # Latest applies when value is earlier then earliest
74
+ sig { params(latest: TTemporal, val: TTemporal).returns(TError) }
75
+ def latest_error(latest, val)
76
+ [:latest, [latest, val]]
77
+ end
78
+
79
+ # Format applies when value does not match a format
80
+ sig { params(format: Object, val: String).returns(TError) }
81
+ def format_error(format, val)
82
+ [:format, [format, val]]
83
+ end
84
+
85
+ ## Messages
86
+
87
+ # Generate a message for a type error
88
+ sig { params(cls: T.class_of(Object), value: Object).returns(String) }
89
+ def type_error_message(cls, value)
90
+ "#{value.inspect} is not a #{cls.name}, it is a #{value.class.name}"
91
+ end
92
+
93
+ # Generate a message for a coerce error
94
+ sig { params(cls: T.class_of(Object), value: Object).returns(String) }
95
+ def coerce_error_message(cls, value)
96
+ "cannot be coerced to #{cls.name}, it is a #{value.class.name}"
97
+ end
98
+
99
+ # Generate a message for a missing error
100
+ sig { params(cls: T.class_of(Object)).returns(String) }
101
+ def missing_error_message(cls)
102
+ "missing value, expected a #{cls.name}"
103
+ end
104
+
105
+ # Generate a message for an inclusion error
106
+ sig { params(set: T::Array[Symbol]).returns(String) }
107
+ def inclusion_error_message(set)
108
+ "must be one of #{set.join(', ')}"
109
+ end
110
+
111
+ # Generate a message for an exclusion error
112
+ sig { params(set: T::Array[Symbol]).returns(String) }
113
+ def exclusion_error_message(set)
114
+ "must not be one of #{set.join(', ')}"
115
+ end
116
+
117
+ # Generate a message for a blank error
118
+ sig { returns(String) }
119
+ def blank_error_message
120
+ "cannot be blank"
121
+ end
122
+
123
+ # Generate a message for an extra keys error
124
+ sig { params(keys: T::Array[Symbol]).returns(String) }
125
+ def extra_keys_error_message(keys)
126
+ "more elements found in closed hash then specified children: #{keys.join(', ')}"
127
+ end
128
+
129
+ # Generate a message for a min error
130
+ sig { params(min: Numeric, val: Numeric).returns(String) }
131
+ def min_error_message(min, val)
132
+ "value is less than the minimum of #{min}, it is #{val}"
133
+ end
134
+
135
+ # Generate a message for a min error
136
+ sig { params(max: Numeric, val: Numeric).returns(String) }
137
+ def max_error_message(max, val)
138
+ "value is more than the maximum of #{max}, it is #{val}"
139
+ end
140
+
141
+ # Generate a message for a value that occurs earlier then the specified earliest point
142
+ sig { params(earliest: TTemporal, val: TTemporal).returns(String) }
143
+ def early_error_message(earliest, val)
144
+ "value #{val} is before #{earliest}"
145
+ end
146
+
147
+ # Generate a message for a value that occurs later then the specified latest point
148
+ sig { params(latest: TTemporal, val: TTemporal).returns(String) }
149
+ def late_error_message(latest, val)
150
+ "value #{val} is after #{latest}"
151
+ end
152
+
153
+ # Generate a message for a value that does not match the format
154
+ sig { params(format: Object, val: String).returns(String) }
155
+ def format_error_message(format, val)
156
+ "value #{val} does not match format #{format}"
157
+ end
158
+
159
+ ## API
160
+ # TODO: split this file
161
+
162
+ TErrorMessageBuilder = T.type_alias { T.proc.params(ctx: T.untyped).returns(String) }
163
+
164
+ # Register a custom error message for use with custom errors
165
+ sig { params(type: Symbol, block: TErrorMessageBuilder).void }
166
+ def register_error_message(type, &block)
167
+ error_message_builders[type] = block
168
+ end
169
+
170
+ TErrorMessages = T.type_alias { T::Hash[Symbol, TErrorMessageBuilder] }
171
+ TClassValueCtx = T.type_alias { [T.class_of(Object), Object] }
172
+ TClassCtx = T.type_alias { T.class_of(Object) }
173
+ TSetCtx = T.type_alias { T::Array[Symbol] }
174
+ TWithinCtx = T.type_alias { [Numeric, Numeric] }
175
+ TWithinTemporalCtx = T.type_alias { [TTemporal, TTemporal] }
176
+ TFormatCtx = T.type_alias { [Object, String] }
177
+
178
+ # Get the error message builders
179
+ sig { returns(TErrorMessages) }
180
+ def error_message_builders
181
+ if @error_messages.nil?
182
+ @error_messages ||= T.let({}, T.nilable(TErrorMessages))
183
+
184
+ # wire up defaults
185
+
186
+ register_error_message(:type) do |ctx|
187
+ cls, val = T.let(ctx, TClassValueCtx)
188
+ type_error_message(cls, val)
189
+ end
190
+
191
+ register_error_message(:coerce) do |ctx|
192
+ cls, val = T.let(ctx, TClassValueCtx)
193
+ coerce_error_message(cls, val)
194
+ end
195
+
196
+ register_error_message(:missing) do |ctx|
197
+ cls = T.let(ctx, TClassCtx)
198
+ missing_error_message(cls)
199
+ end
200
+
201
+ register_error_message(:inclusion) do |ctx|
202
+ set = T.let(ctx, TSetCtx)
203
+ inclusion_error_message(set)
204
+ end
205
+
206
+ register_error_message(:exclusion) do |ctx|
207
+ set = T.let(ctx, TSetCtx)
208
+ exclusion_error_message(set)
209
+ end
210
+
211
+ register_error_message(:extra_keys) do |ctx|
212
+ set = T.let(ctx, TSetCtx)
213
+ extra_keys_error_message(set)
214
+ end
215
+
216
+ register_error_message(:min) do |ctx|
217
+ min, val = T.let(ctx, TWithinCtx)
218
+ min_error_message(min, val)
219
+ end
220
+
221
+ register_error_message(:max) do |ctx|
222
+ max, val = T.let(ctx, TWithinCtx)
223
+ max_error_message(max, val)
224
+ end
225
+
226
+ register_error_message(:earliest) do |ctx|
227
+ earliest, val = T.let(ctx, TWithinTemporalCtx)
228
+ early_error_message(earliest, val)
229
+ end
230
+
231
+ register_error_message(:latest) do |ctx|
232
+ latest, val = T.let(ctx, TWithinTemporalCtx)
233
+ late_error_message(latest, val)
234
+ end
235
+
236
+ register_error_message(:blank) do
237
+ blank_error_message
238
+ end
239
+
240
+ register_error_message(:format) do |ctx|
241
+ format, val = T.let(ctx, TFormatCtx)
242
+ format_error_message(format, val)
243
+ end
244
+ end
245
+
246
+ @error_messages
247
+ end
248
+
249
+ # Build the error message for a given error
250
+ sig { params(error: TError).returns(String) }
251
+ def error_message(error)
252
+ type = T.let(error[0], Symbol)
253
+ ctx = T.let(error[1], T.untyped)
254
+
255
+ builder = error_message_builders[type]
256
+
257
+ if builder.nil?
258
+ raise "no error message builder for #{type}"
259
+ end
260
+
261
+ builder.call(ctx)
262
+ end
263
+
264
+ # TODO: separate builders from other use cases for this mixin
265
+ # Build error messages from error object
266
+ sig { params(error: Error).returns(T::Hash[Symbol, T::Array[String]]) }
267
+ def error_messages(error)
268
+ error.to_h.transform_values do |error_list|
269
+ error_list.map { |e| error_message(e) }
270
+ end
271
+ end
272
+
273
+ sig { params(error: Error, from: T.class_of(Object), to: T.class_of(Object)).void }
274
+ def set_error_class(error, from, to)
275
+ error.transform_context do |ctx, type|
276
+ case type
277
+ when :type, :coerce
278
+ cls, val = T.cast(ctx, TClassValueCtx)
279
+ if cls == from
280
+ [to, val]
281
+ else
282
+ [cls, val]
283
+ end
284
+ when :missing
285
+ if ctx == from
286
+ [to, val]
287
+ else
288
+ [cls, val]
289
+ end
290
+ else
291
+ [cls, val]
292
+ end
293
+ end
294
+ end
295
+ end
296
+ end
@@ -0,0 +1,61 @@
1
+ # typed: strict
2
+
3
+ module DataModel
4
+ module Fixtures::Array
5
+ extend self
6
+ extend T::Sig
7
+ include Fixtures
8
+
9
+ sig { returns(Example) }
10
+ def string_array
11
+ Example.new(
12
+ [:array, :string],
13
+ variants: {
14
+ strings: ["a", "b", "c"],
15
+ string: ["a", ["a"]],
16
+ number: [1, ["1"]],
17
+ missing: nil,
18
+ numbers: [[1, 2, 3], ["1", "2", "3"]],
19
+ other_type: Object.new
20
+ },
21
+ )
22
+ end
23
+
24
+ sig { returns(Example) }
25
+ def wrapping_string_array
26
+ Example.new(
27
+ [:array, { wrap_single_value: true }, :string],
28
+ variants: {
29
+ strings: ["a", "b", "c"],
30
+ string: ["a", ["a"]],
31
+ number: [1, ["1"]],
32
+ missing: nil,
33
+ numbers: [[1, 2, 3], ["1", "2", "3"]],
34
+ other_type: Object.new
35
+ },
36
+ )
37
+ end
38
+
39
+ sig { returns(Example) }
40
+ def optional_string_array
41
+ Example.new(
42
+ [:array, { optional: true }, :string],
43
+ variants: {
44
+ strings: ["a", "b", "c"],
45
+ missing: nil
46
+ },
47
+ )
48
+ end
49
+
50
+ sig { returns(Example) }
51
+ def array_optional_string
52
+ Example.new(
53
+ [:array, [:string, { optional: true }]],
54
+ variants: {
55
+ optional_strings: ["a", nil, "c"],
56
+ numbers: [1, nil, 3]
57
+ },
58
+ )
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,55 @@
1
+ # typed: strict
2
+
3
+ require "bigdecimal/util"
4
+
5
+ module DataModel
6
+ module Fixtures::BigDecimal
7
+ include Fixtures
8
+ extend T::Sig
9
+ extend self
10
+
11
+ sig { returns(Example) }
12
+ def simple
13
+ Example.new(
14
+ [:decimal],
15
+ variants: {
16
+ valid: 5.to_d,
17
+ missing: nil,
18
+ string: ["5", 5.to_d]
19
+ },
20
+ )
21
+ end
22
+
23
+ sig { returns(Example) }
24
+ def optional
25
+ Example.new(
26
+ [:decimal, { optional: true }],
27
+ variants: {
28
+ missing: nil
29
+ },
30
+ )
31
+ end
32
+
33
+ sig { returns(Example) }
34
+ def min
35
+ Example.new(
36
+ [:decimal, { min: 5 }],
37
+ variants: {
38
+ bigger: 6.to_d,
39
+ smaller: 4.to_d
40
+ },
41
+ )
42
+ end
43
+
44
+ sig { returns(Example) }
45
+ def max
46
+ Example.new(
47
+ [:decimal, { max: 5 }],
48
+ variants: {
49
+ bigger: 6.to_d,
50
+ smaller: 4.to_d
51
+ },
52
+ )
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,35 @@
1
+ # typed: strict
2
+
3
+ module DataModel
4
+ module Fixtures::Boolean
5
+ extend T::Sig
6
+ extend self
7
+ include Fixtures
8
+
9
+ sig { returns(Example) }
10
+ def simple
11
+ Example.new(
12
+ [:boolean],
13
+ variants: {
14
+ true: true,
15
+ false: false,
16
+ string: ["true", true],
17
+ missing: nil
18
+ },
19
+ )
20
+ end
21
+
22
+ sig { returns(Example) }
23
+ def optional
24
+ Example.new(
25
+ [:boolean, { optional: true }],
26
+ variants: {
27
+ true: true,
28
+ false: false,
29
+ string: "true",
30
+ missing: nil
31
+ },
32
+ )
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,53 @@
1
+ # typed: strict
2
+
3
+ module DataModel
4
+ module Fixtures::Date
5
+ extend T::Sig
6
+ extend self
7
+ include Fixtures
8
+
9
+ sig { returns(::Date) }
10
+ def earliest_date
11
+ return ::Date.today - 1
12
+ end
13
+
14
+ sig { returns(::Date) }
15
+ def latest_date
16
+ return ::Date.today + 1
17
+ end
18
+
19
+ sig { returns(T::Hash[Symbol, Object]) }
20
+ def variants
21
+ today = ::Date.today
22
+
23
+ {
24
+ date: today,
25
+ string: [today.to_s, today],
26
+ invalid: "invalid",
27
+ early: earliest_date - 1,
28
+ late: latest_date + 1,
29
+ missing: nil
30
+ }
31
+ end
32
+
33
+ sig { returns(Example) }
34
+ def simple
35
+ Example.new([:date], variants:)
36
+ end
37
+
38
+ sig { returns(Example) }
39
+ def optional
40
+ Example.new([:date, { optional: true }], variants:)
41
+ end
42
+
43
+ sig { returns(Example) }
44
+ def earliest
45
+ Example.new([:date, { earliest: earliest_date }], variants:)
46
+ end
47
+
48
+ sig { returns(Example) }
49
+ def latest
50
+ Example.new([:date, { latest: latest_date }], variants:)
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,29 @@
1
+ # typed: strict
2
+
3
+ module DataModel
4
+ class Fixtures::Example
5
+ extend T::Sig
6
+
7
+ sig { params(schema: TSchema, variants: T::Hash[::Symbol, Object]).void }
8
+ def initialize(schema, variants:)
9
+ @schema = schema
10
+ @variants = variants
11
+ end
12
+
13
+ sig { returns(Model) }
14
+ def model
15
+ DataModel.define(@schema)
16
+ end
17
+
18
+ sig { params(type: Symbol).returns([Model, Object]) }
19
+ def [](type)
20
+ if !@variants.key?(type)
21
+ raise "#{type} is not a defined variant: #{@variants}"
22
+ end
23
+
24
+ result = @variants.fetch(type)
25
+
26
+ return [model, result]
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,53 @@
1
+ # typed: strict
2
+
3
+ module DataModel
4
+ module Fixtures::Float
5
+ include Fixtures
6
+ extend T::Sig
7
+ extend self
8
+
9
+ sig { returns(Example) }
10
+ def simple
11
+ Example.new(
12
+ [:float],
13
+ variants: {
14
+ valid: 5.0,
15
+ missing: nil,
16
+ string: ["5", 5.0]
17
+ },
18
+ )
19
+ end
20
+
21
+ sig { returns(Example) }
22
+ def optional
23
+ Example.new(
24
+ [:float, { optional: true }],
25
+ variants: {
26
+ missing: nil
27
+ },
28
+ )
29
+ end
30
+
31
+ sig { returns(Example) }
32
+ def min
33
+ Example.new(
34
+ [:float, { min: 5 }],
35
+ variants: {
36
+ bigger: 6.0,
37
+ smaller: 4.0
38
+ },
39
+ )
40
+ end
41
+
42
+ sig { returns(Example) }
43
+ def max
44
+ Example.new(
45
+ [:float, { max: 5.0 }],
46
+ variants: {
47
+ bigger: 6.0,
48
+ smaller: 4.0
49
+ },
50
+ )
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,66 @@
1
+ # typed: strict
2
+
3
+ module DataModel
4
+ module Fixtures::Hash
5
+ include Fixtures
6
+ extend T::Sig
7
+ extend self
8
+
9
+ TContact = T.type_alias { T::Hash[Symbol, T.untyped] }
10
+
11
+ sig { returns(TContact) }
12
+ def example_contact
13
+ {
14
+ first_name: "foo",
15
+ last_name: "bar",
16
+ email: "foo@bar.com"
17
+ }
18
+ end
19
+
20
+ sig { returns(Example) }
21
+ def contact
22
+ Example.new(
23
+ [:hash,
24
+ [:first_name, :string],
25
+ [:last_name, :string, { optional: true }],
26
+ [:email, :string]],
27
+ variants: {
28
+ valid: example_contact,
29
+ missing: nil,
30
+ coercible: example_contact.to_a,
31
+ missing_email: example_contact.tap { |h| T.cast(h, TContact).delete(:email) },
32
+ invalid_field: example_contact.merge(email: 123),
33
+ other_type: []
34
+ },
35
+ )
36
+ end
37
+
38
+ sig { returns(Example) }
39
+ def optional_contact
40
+ Example.new(
41
+ [:hash, { optional: true },
42
+ [:first_name, :string],
43
+ [:last_name, :string, { optional: true }],
44
+ [:email, :string]],
45
+ variants: {
46
+ valid: example_contact,
47
+ missing: nil
48
+ },
49
+ )
50
+ end
51
+
52
+ sig { returns(Example) }
53
+ def closed_contact
54
+ Example.new(
55
+ [:hash, { open: false },
56
+ [:first_name, :string],
57
+ [:last_name, :string, { optional: true }],
58
+ [:email, :string]],
59
+ variants: {
60
+ valid: example_contact,
61
+ extra_keys: example_contact.merge(extra: "keys")
62
+ },
63
+ )
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,53 @@
1
+ # typed: strict
2
+
3
+ module DataModel
4
+ module Fixtures::Integer
5
+ include Fixtures
6
+ extend T::Sig
7
+ extend self
8
+
9
+ sig { returns(Example) }
10
+ def simple
11
+ Example.new(
12
+ [:integer],
13
+ variants: {
14
+ valid: 5,
15
+ missing: nil,
16
+ string: ["5", 5]
17
+ },
18
+ )
19
+ end
20
+
21
+ sig { returns(Example) }
22
+ def optional
23
+ Example.new(
24
+ [:integer, { optional: true }],
25
+ variants: {
26
+ missing: nil
27
+ },
28
+ )
29
+ end
30
+
31
+ sig { returns(Example) }
32
+ def min
33
+ Example.new(
34
+ [:integer, { min: 5 }],
35
+ variants: {
36
+ bigger: 6,
37
+ smaller: 4
38
+ },
39
+ )
40
+ end
41
+
42
+ sig { returns(Example) }
43
+ def max
44
+ Example.new(
45
+ [:integer, { max: 5 }],
46
+ variants: {
47
+ bigger: 6,
48
+ smaller: 4
49
+ },
50
+ )
51
+ end
52
+ end
53
+ end