definition 0.7.1 → 0.8.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +1 -0
- data/Changelog.md +11 -0
- data/Gemfile.lock +21 -19
- data/README.md +134 -54
- data/UpgradeNotes.md +27 -0
- data/benchmark/initializer.rb +91 -0
- data/benchmark/model.rb +70 -0
- data/benchmark/validation_only.rb +11 -13
- data/config/locales/en.yml +2 -2
- data/lib/definition/dsl/comparators.rb +29 -19
- data/lib/definition/dsl/nil.rb +1 -1
- data/lib/definition/dsl.rb +15 -15
- data/lib/definition/initializer.rb +52 -0
- data/lib/definition/model.rb +80 -0
- data/lib/definition/types/keys.rb +49 -67
- data/lib/definition/types/lambda.rb +7 -1
- data/lib/definition/v1_deprecator.rb +7 -0
- data/lib/definition/version.rb +1 -1
- data/lib/definition.rb +2 -0
- metadata +9 -4
- data/tags +0 -155
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 9e79beba1853328e9b03847f3b8f9b3a088d381965cba2fd1ccedd283b57bd0c
|
4
|
+
data.tar.gz: b0835b29633b6887e39bbb6b6d34146c6870a13ab82415492e70050a9727831c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ca5aae1db840f6ee3b45fd59cf8698a66a83a5d40d7f5f06be1413c0ce40ee979932382e4e3c866423faffda6a113f6f926053b84a76bd5986ba68eb37cda2f8
|
7
|
+
data.tar.gz: 86c2aad58a186b58f71f43f4e5a137ced0e2f472d1ee5bcd970045b76e252fa6ef2b258e005d9ffa169b0cbe7dea028d6e4ec3cda21dd38d4e23dc337e3ad330
|
data/.gitignore
CHANGED
data/Changelog.md
CHANGED
@@ -6,6 +6,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
6
6
|
|
7
7
|
## Unreleased
|
8
8
|
|
9
|
+
## [0.8.0] - 2022-10-05
|
10
|
+
### Added
|
11
|
+
- A Definition::Initializer mixin that can be used to validate keyword arguments of a class constructor
|
12
|
+
|
13
|
+
### Changes
|
14
|
+
- Include more information in the internal non-translated error messages. E.g. the max size of a LessThenEqual definition
|
15
|
+
- Renamed GreaterThen, GreaterThenEqual, LessThen and LessThenEqual to fix typo (Then VS Than) Backwards compatibility is ensured
|
16
|
+
|
17
|
+
### Breaking changes
|
18
|
+
- The Definition::ValueObject was removed and replaced by Definition:Model which has a nicer DSL and only works with hash data structures. See [upgrade notes](./UpgradeNotes.md)
|
19
|
+
|
9
20
|
## [0.7.1] - 2022-03-04
|
10
21
|
### Fixed
|
11
22
|
- Float coercion: check for nil before coercion
|
data/Gemfile.lock
CHANGED
@@ -1,26 +1,27 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
definition (0.
|
4
|
+
definition (0.8.0)
|
5
5
|
activesupport
|
6
6
|
i18n
|
7
7
|
|
8
8
|
GEM
|
9
9
|
remote: https://rubygems.org/
|
10
10
|
specs:
|
11
|
-
activesupport (7.0.
|
11
|
+
activesupport (7.0.4)
|
12
12
|
concurrent-ruby (~> 1.0, >= 1.0.2)
|
13
13
|
i18n (>= 1.6, < 2)
|
14
14
|
minitest (>= 5.1)
|
15
15
|
tzinfo (~> 2.0)
|
16
|
-
approvals (0.0.
|
16
|
+
approvals (0.0.26)
|
17
|
+
json (~> 2.0)
|
17
18
|
nokogiri (~> 1.8)
|
18
19
|
thor (~> 1.0)
|
19
20
|
ast (2.4.2)
|
20
21
|
awesome_print (1.9.2)
|
21
22
|
benchmark-ips (2.10.0)
|
22
23
|
coderay (1.1.3)
|
23
|
-
concurrent-ruby (1.1.
|
24
|
+
concurrent-ruby (1.1.10)
|
24
25
|
diff-lcs (1.5.0)
|
25
26
|
ffi (1.15.5)
|
26
27
|
formatador (1.1.0)
|
@@ -41,37 +42,38 @@ GEM
|
|
41
42
|
guard (~> 2.1)
|
42
43
|
guard-compat (~> 1.1)
|
43
44
|
rspec (>= 2.99.0, < 4.0)
|
44
|
-
i18n (1.
|
45
|
+
i18n (1.12.0)
|
45
46
|
concurrent-ruby (~> 1.0)
|
46
47
|
jaro_winkler (1.5.4)
|
48
|
+
json (2.6.2)
|
47
49
|
listen (3.7.1)
|
48
50
|
rb-fsevent (~> 0.10, >= 0.10.3)
|
49
51
|
rb-inotify (~> 0.9, >= 0.9.10)
|
50
52
|
lumberjack (1.2.8)
|
51
53
|
method_source (1.0.0)
|
52
54
|
mini_portile2 (2.8.0)
|
53
|
-
minitest (5.
|
55
|
+
minitest (5.16.3)
|
54
56
|
nenv (0.3.0)
|
55
|
-
nokogiri (1.13.
|
57
|
+
nokogiri (1.13.8)
|
56
58
|
mini_portile2 (~> 2.8.0)
|
57
59
|
racc (~> 1.4)
|
58
|
-
nokogiri (1.13.
|
60
|
+
nokogiri (1.13.8-x86_64-linux)
|
59
61
|
racc (~> 1.4)
|
60
62
|
notiffany (0.1.3)
|
61
63
|
nenv (~> 0.1)
|
62
64
|
shellany (~> 0.0)
|
63
|
-
parallel (1.
|
64
|
-
parser (3.1.
|
65
|
+
parallel (1.22.1)
|
66
|
+
parser (3.1.2.1)
|
65
67
|
ast (~> 2.4.1)
|
66
68
|
pry (0.14.1)
|
67
69
|
coderay (~> 1.1)
|
68
70
|
method_source (~> 1.0)
|
69
|
-
psych (4.0.
|
71
|
+
psych (4.0.6)
|
70
72
|
stringio
|
71
73
|
racc (1.6.0)
|
72
74
|
rainbow (3.1.1)
|
73
75
|
rake (13.0.6)
|
74
|
-
rb-fsevent (0.11.
|
76
|
+
rb-fsevent (0.11.2)
|
75
77
|
rb-inotify (0.10.1)
|
76
78
|
ffi (~> 1.0)
|
77
79
|
rspec (3.11.0)
|
@@ -80,17 +82,17 @@ GEM
|
|
80
82
|
rspec-mocks (~> 3.11.0)
|
81
83
|
rspec-core (3.11.0)
|
82
84
|
rspec-support (~> 3.11.0)
|
83
|
-
rspec-expectations (3.11.
|
85
|
+
rspec-expectations (3.11.1)
|
84
86
|
diff-lcs (>= 1.2.0, < 2.0)
|
85
87
|
rspec-support (~> 3.11.0)
|
86
88
|
rspec-its (1.3.0)
|
87
89
|
rspec-core (>= 3.0.0)
|
88
90
|
rspec-expectations (>= 3.0.0)
|
89
|
-
rspec-mocks (3.11.
|
91
|
+
rspec-mocks (3.11.1)
|
90
92
|
diff-lcs (>= 1.2.0, < 2.0)
|
91
93
|
rspec-support (~> 3.11.0)
|
92
|
-
rspec-support (3.11.
|
93
|
-
rspec_junit_formatter (0.
|
94
|
+
rspec-support (3.11.1)
|
95
|
+
rspec_junit_formatter (0.6.0)
|
94
96
|
rspec-core (>= 2, < 4, != 2.12.0)
|
95
97
|
rubocop (0.66.0)
|
96
98
|
jaro_winkler (~> 1.5.1)
|
@@ -105,10 +107,10 @@ GEM
|
|
105
107
|
rubocop_runner (2.2.0)
|
106
108
|
ruby-progressbar (1.11.0)
|
107
109
|
shellany (0.0.1)
|
108
|
-
stringio (3.0.
|
110
|
+
stringio (3.0.2)
|
109
111
|
thor (1.2.1)
|
110
|
-
timecop (0.9.
|
111
|
-
tzinfo (2.0.
|
112
|
+
timecop (0.9.5)
|
113
|
+
tzinfo (2.0.5)
|
112
114
|
concurrent-ruby (~> 1.0)
|
113
115
|
unicode-display_width (1.5.0)
|
114
116
|
|
data/README.md
CHANGED
@@ -42,7 +42,7 @@ conform_result.error_hash # =>
|
|
42
42
|
# {
|
43
43
|
# :birthday => [
|
44
44
|
# [0] <Definition::ConformError
|
45
|
-
#
|
45
|
+
# description: "hash fails validation for key birthday: { Is of type String instead of Date }",
|
46
46
|
# json_pointer: "/birthday">
|
47
47
|
# ]
|
48
48
|
# }
|
@@ -71,66 +71,60 @@ conform_result.value # => {title: "My first blog post", body: "Shortest one ever
|
|
71
71
|
Because definitions do not only validate input but also transform input, we use
|
72
72
|
the term `conform` which stands for validation and coercion.
|
73
73
|
|
74
|
-
###
|
74
|
+
### Handling errors
|
75
75
|
|
76
|
-
|
77
|
-
|
78
|
-
definition(Definition.Keys do
|
79
|
-
required :username, Definition.Type(String)
|
80
|
-
required :password, Definition.Type(String)
|
81
|
-
end)
|
82
|
-
end
|
83
|
-
|
84
|
-
user = User.new(username: "johndoe", password: "zg(2ds8x2/")
|
85
|
-
user.username # => "johndoe"
|
86
|
-
user[:username] # => "johndoe"
|
87
|
-
user.username = "Alice" # => NoMethodError (ValueObjects are immutable)
|
88
|
-
user[:username] = "Alice" # => FrozenError (ValueObjects are immutable)
|
89
|
-
|
90
|
-
User.new(username: "johndoe") # => Definition::InvalidValueObjectError: hash does not include :password
|
91
|
-
```
|
92
|
-
|
93
|
-
Value objects delegate all calls to the output value of the defined definition,
|
94
|
-
so in this example you can use all methods that are defined on `Hash` also on the
|
95
|
-
user object. If you use a `Keys` definition, the value object additionally defines
|
96
|
-
convenient accessor methods for all attributes.
|
97
|
-
|
98
|
-
Value Objects can also be used for all other data structures that can be validated
|
99
|
-
by a definition, for example arrays:
|
76
|
+
#### I18n translated errors
|
77
|
+
For end users you best use the translated errors that you get from definition:
|
100
78
|
|
101
79
|
```ruby
|
102
|
-
|
103
|
-
|
80
|
+
schema = Definition.Keys do
|
81
|
+
required :title, Definition.NonEmptyString
|
82
|
+
required :body, Definition::And(
|
83
|
+
Definition.Type(String),
|
84
|
+
Definition.MinSize(100)
|
85
|
+
)
|
104
86
|
end
|
105
87
|
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
88
|
+
conform_result = schema.conform({title: "", body: "this is not long enough"})
|
89
|
+
conform_result.errors # => returns an array of Definition::ConformError
|
90
|
+
conform_result.errors.each do |error|
|
91
|
+
puts "----"
|
92
|
+
puts error.json_pointer # provides a path to the invalid value, also works with nested objects and arrays
|
93
|
+
puts error.translated_error
|
94
|
+
end
|
95
|
+
# =>
|
96
|
+
# ----
|
97
|
+
# /title
|
98
|
+
# Value is shorter than 1
|
99
|
+
# ----
|
100
|
+
# /body
|
101
|
+
# Value is shorter than 100
|
110
102
|
```
|
111
103
|
|
112
|
-
|
104
|
+
The error messages are only translated into English for now, but you can add or change translations by adding a yaml file like [this](./config/locales/en.yml) to your I18n load path.
|
113
105
|
|
114
|
-
####
|
106
|
+
#### Other ways of accessing errors
|
115
107
|
|
116
|
-
|
117
|
-
or by using the `CoercibleValueObject` Definition. The latter would convert input
|
118
|
-
hashes that conform with the value objects schema to an instance of the value object.
|
108
|
+
To get a quick error summary during debugging, you can also use `conform_result.error_message`
|
119
109
|
|
120
|
-
|
121
|
-
class IntegerArray < Definition::ValueObject
|
122
|
-
definition(Definition.Each(Definition.Type(Integer)))
|
123
|
-
end
|
110
|
+
Instead of getting a flat array of all errors via `conform_result.errors`, you can also get a hierarchical representation:
|
124
111
|
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
112
|
+
```ruby
|
113
|
+
conform_result.error_hash
|
114
|
+
# =>
|
115
|
+
# {
|
116
|
+
# :title => [
|
117
|
+
# [0] <Definition::ConformError
|
118
|
+
# message: "hash fails validation for key title: { Not all definitions are valid for 'non_empty_string': { Did not pass test for min_size (1) } }",
|
119
|
+
# json_pointer: "/title">
|
120
|
+
# ],
|
121
|
+
# :body => [
|
122
|
+
# [0] <Definition::ConformError
|
123
|
+
# message: "hash fails validation for key body: { Not all definitions are valid for 'and': { Did not pass test for min_size (100) } }",
|
124
|
+
# json_pointer: "/body">
|
125
|
+
# ]
|
126
|
+
# }
|
131
127
|
|
132
|
-
object = User.new(username: "John", scores: [1,2,3])
|
133
|
-
object.scores.class.name # => IntegerArray
|
134
128
|
```
|
135
129
|
|
136
130
|
### Conforming Hashes
|
@@ -353,10 +347,10 @@ Definition.Regex(/^\d*$/).conform("123") # => pass
|
|
353
347
|
#### Numerics
|
354
348
|
|
355
349
|
```ruby
|
356
|
-
Definition.
|
357
|
-
Definition.
|
358
|
-
Definition.
|
359
|
-
Definition.
|
350
|
+
Definition.GreaterThan(5).conform(5.1) # => pass
|
351
|
+
Definition.GreaterThanEqual(5).conform(5) # => pass
|
352
|
+
Definition.LessThan(5).conform(4) # => pass
|
353
|
+
Definition.LessThanEqual(5).conform(5) # => pass
|
360
354
|
```
|
361
355
|
|
362
356
|
#### Strings, Array, Hashes
|
@@ -382,7 +376,7 @@ Definition.Nil.conform(nil) # => pass
|
|
382
376
|
#### Boolean
|
383
377
|
|
384
378
|
```ruby
|
385
|
-
Definition.Boolean.conform(
|
379
|
+
Definition.Boolean.conform(true) # => pass
|
386
380
|
```
|
387
381
|
|
388
382
|
#### All types
|
@@ -431,6 +425,92 @@ end
|
|
431
425
|
schema.conform(input_hash).errors.first.translated_error # => Value is of wrong type, needs to be a String"
|
432
426
|
```
|
433
427
|
|
428
|
+
# Helpers / useful tools
|
429
|
+
|
430
|
+
### Value Objects / Models
|
431
|
+
|
432
|
+
Provides simple immutable objects that can validate and hold your data so that it can be safely passed around in your application.
|
433
|
+
|
434
|
+
```ruby
|
435
|
+
class User < Definition::Model
|
436
|
+
required :username, Definition.Type(String)
|
437
|
+
required :password, Definition.Type(String)
|
438
|
+
optional :age, Definition.Type(Integer)
|
439
|
+
end
|
440
|
+
|
441
|
+
user = User.new(username: "johndoe", password: "zg(2ds8x2/")
|
442
|
+
user.username # => "johndoe"
|
443
|
+
user.age # => nil
|
444
|
+
user.to_h # => { username: "johndoe", password: "zg(2ds8x2/" }
|
445
|
+
user.new(age: 21) # => new model instance with username and password from before plus the age set to 21
|
446
|
+
|
447
|
+
user.username = "Alice" # => raises NoMethodError (Models are immutable)
|
448
|
+
|
449
|
+
User.new(username: "johndoe") # => raises a Definition::InvalidModelError: hash is missing :password
|
450
|
+
|
451
|
+
```
|
452
|
+
|
453
|
+
You can access the conform result of InvalidModel errors via their `conform_result` method.
|
454
|
+
|
455
|
+
#### Nesting Models
|
456
|
+
|
457
|
+
Models can be nested by either using the model object itself as type definition,
|
458
|
+
or by using the `CoercibleModel` Definition. The latter is less strict and will
|
459
|
+
convert input hashes that conform with the model schema to an instance of the model.
|
460
|
+
|
461
|
+
```ruby
|
462
|
+
class Address < Definition::Model
|
463
|
+
required :street, Definition.Type(String)
|
464
|
+
required :postal_code, Definition.Type(String)
|
465
|
+
end
|
466
|
+
|
467
|
+
class User < Definition::Model
|
468
|
+
required :username, Definition.Type(String)
|
469
|
+
required :address, Definition.CoercibleModel(Address)
|
470
|
+
end
|
471
|
+
|
472
|
+
# Address is converted into an Address model automatically:
|
473
|
+
user = User.new(username: "John", address: { street: "123 Fakestreet", postal_code: "2dfx4" })
|
474
|
+
user.address.street # => "123 Fakestreet"
|
475
|
+
|
476
|
+
class UserNotCoercibleAddress < Definition::Model
|
477
|
+
required :username, Definition.Type(String)
|
478
|
+
required :address, Address
|
479
|
+
end
|
480
|
+
|
481
|
+
# Address is not converted automatically, instead it needs to be of type Address already:
|
482
|
+
UserNotCoercibleAddress.new(username: "John", address: { street: "123 Fakestreet", postal_code: "2dfx4" }) # => raises a Definition::InvalidModelError
|
483
|
+
UserNotCoercibleAddress.new(username: "John", address: Address.new(street: "123 Fakestreet", postal_code: "2dfx4")).address.street # => "123 Fakestreet"
|
484
|
+
```
|
485
|
+
|
486
|
+
### Intialization argument validation
|
487
|
+
|
488
|
+
Definition provides a mixin that allows you to validate keyword arguments of class initialization methods. This is meant to be used with classes that provide business logic whereas the models are meant to be used to pass data around.
|
489
|
+
|
490
|
+
The major differences to a Definition::Model are:
|
491
|
+
* The values are not frozen and can be changed by the classes internal business logic
|
492
|
+
* None of the getters for the attributes are public
|
493
|
+
|
494
|
+
```ruby
|
495
|
+
class User
|
496
|
+
include Definition::Initializer
|
497
|
+
|
498
|
+
required :id, Definition.Type(Integer)
|
499
|
+
required :name, Definition.Type(String)
|
500
|
+
optional :phone, Definition.Type(String), default: nil
|
501
|
+
|
502
|
+
def hello
|
503
|
+
puts "Hello, I'm #{name}"
|
504
|
+
end
|
505
|
+
end
|
506
|
+
|
507
|
+
user = User.new(id: 1, name: "Joe")
|
508
|
+
user.hello # => "Hello, I'm Joe"
|
509
|
+
user.name # => raises NoMethodError
|
510
|
+
|
511
|
+
User.new(id: "1", name: "Joe") # => raises a Definition::Initializer::InvalidArgumentError
|
512
|
+
```
|
513
|
+
|
434
514
|
## Development
|
435
515
|
|
436
516
|
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
data/UpgradeNotes.md
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
# Upgrade Notes
|
2
|
+
|
3
|
+
## From version 0.7.1 and prior to version 0.8
|
4
|
+
##### The Definition::ValueObject was removed and replaced by Definition:Model
|
5
|
+
If you used ValueObjects then you need to migrate them to Models. This will be very easy if you used the ValueObject with a `Definition.Keys` definition, which is most likely the case.
|
6
|
+
|
7
|
+
Before:
|
8
|
+
```ruby
|
9
|
+
class User < Definition::ValueObject
|
10
|
+
definition(Definition.Keys do
|
11
|
+
required :username, Definition.Type(String)
|
12
|
+
required :password, Definition.Type(String)
|
13
|
+
end)
|
14
|
+
end
|
15
|
+
```
|
16
|
+
|
17
|
+
After:
|
18
|
+
```ruby
|
19
|
+
class User < Definition::Model
|
20
|
+
required :username, Definition.Type(String)
|
21
|
+
required :password, Definition.Type(String)
|
22
|
+
end
|
23
|
+
```
|
24
|
+
|
25
|
+
If you use the `Definition.CoercibleValueObject` definition in your models, you just need to replace those with a `Definition.CoercibleModel` definition.
|
26
|
+
|
27
|
+
If you use ValueObjects that do not use a `Keys` definition then there is currently no built in replacement available.
|
@@ -0,0 +1,91 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "bundler/inline"
|
4
|
+
|
5
|
+
gemfile do
|
6
|
+
source "https://rubygems.org"
|
7
|
+
gem "dry-initializer", "~> 3.1"
|
8
|
+
gem "dry-types", "~> 1.5"
|
9
|
+
gem "awesome_print"
|
10
|
+
gem "benchmark-ips"
|
11
|
+
gem "pry"
|
12
|
+
gem "ruby-prof"
|
13
|
+
gem "sorbet"
|
14
|
+
gem "sorbet-runtime"
|
15
|
+
gem "definition", path: File.expand_path("../.", __dir__)
|
16
|
+
end
|
17
|
+
|
18
|
+
class SorbetUseCase
|
19
|
+
extend T::Sig
|
20
|
+
|
21
|
+
sig { params(phone: String, admin: T::Boolean).void }
|
22
|
+
def initialize(phone:, admin: false)
|
23
|
+
self.phone = phone
|
24
|
+
self.admin = admin
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
attr_accessor :phone, :admin
|
30
|
+
end
|
31
|
+
|
32
|
+
class DryUseCase
|
33
|
+
extend Dry::Initializer
|
34
|
+
|
35
|
+
option :phone, Dry::Types["strict.string"]
|
36
|
+
option :admin, Dry::Types["strict.bool"], default: false, optional: true
|
37
|
+
end
|
38
|
+
|
39
|
+
class DefinitionUseCase
|
40
|
+
include Definition::Initializer
|
41
|
+
|
42
|
+
required :phone, Definition.Type(String)
|
43
|
+
optional :admin, Definition.Boolean, default: false
|
44
|
+
end
|
45
|
+
|
46
|
+
puts "Benchmark with valid input data:"
|
47
|
+
valid_data = { phone: "+49 3424 234234" }
|
48
|
+
|
49
|
+
Benchmark.ips do |x|
|
50
|
+
x.config(time: 5, warmup: 2)
|
51
|
+
|
52
|
+
x.report("sorbet") do
|
53
|
+
SorbetUseCase.new(**valid_data)
|
54
|
+
end
|
55
|
+
|
56
|
+
x.report("definition") do
|
57
|
+
DefinitionUseCase.new(**valid_data)
|
58
|
+
end
|
59
|
+
|
60
|
+
x.report("dry-struct") do
|
61
|
+
DryUseCase.new(**valid_data)
|
62
|
+
end
|
63
|
+
|
64
|
+
x.compare!
|
65
|
+
end
|
66
|
+
|
67
|
+
puts "Benchmark with invalid input data:"
|
68
|
+
invalid_data = { phone: "+49 3424 234234", admin: "yes" }
|
69
|
+
Benchmark.ips do |x|
|
70
|
+
x.config(time: 5, warmup: 2)
|
71
|
+
|
72
|
+
x.report("sorbet") do
|
73
|
+
SorbetUseCase.new(**invalid_data)
|
74
|
+
rescue TypeError
|
75
|
+
nil
|
76
|
+
end
|
77
|
+
|
78
|
+
x.report("definition") do
|
79
|
+
DefinitionUseCase.new(**invalid_data)
|
80
|
+
rescue Definition::Initializer::InvalidArgumentError
|
81
|
+
nil
|
82
|
+
end
|
83
|
+
|
84
|
+
x.report("dry-struct") do
|
85
|
+
DryUseCase.new(**invalid_data)
|
86
|
+
rescue Dry::Types::ConstraintError
|
87
|
+
nil
|
88
|
+
end
|
89
|
+
|
90
|
+
x.compare!
|
91
|
+
end
|
data/benchmark/model.rb
ADDED
@@ -0,0 +1,70 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "bundler/inline"
|
4
|
+
|
5
|
+
gemfile do
|
6
|
+
source "https://rubygems.org"
|
7
|
+
gem "dry-struct", "~> 1.4"
|
8
|
+
gem "awesome_print"
|
9
|
+
gem "benchmark-ips"
|
10
|
+
gem "pry"
|
11
|
+
gem "ruby-prof"
|
12
|
+
gem "definition", path: File.expand_path("../.", __dir__)
|
13
|
+
end
|
14
|
+
|
15
|
+
class DryStructModel < Dry::Struct
|
16
|
+
schema schema.strict
|
17
|
+
|
18
|
+
attribute :id, Dry::Types["strict.integer"]
|
19
|
+
attribute :app_key, Dry::Types["strict.string"]
|
20
|
+
attribute :app_version, Dry::Types["strict.string"]
|
21
|
+
attribute :app_name, Dry::Types["strict.string"].optional.default(nil)
|
22
|
+
attribute :app_branch, Dry::Types["strict.string"].optional.default(nil)
|
23
|
+
attribute :platform, Dry::Types["strict.string"].optional.default(nil)
|
24
|
+
end
|
25
|
+
|
26
|
+
class DefinitionModel < Definition::Model
|
27
|
+
required :id, Definition.Type(Integer)
|
28
|
+
required :app_key, Definition.Type(String)
|
29
|
+
required :app_version, Definition.Type(String)
|
30
|
+
optional :app_name, Definition.Type(String)
|
31
|
+
optional :app_branch, Definition.Type(String)
|
32
|
+
optional :platform, Definition.Type(String)
|
33
|
+
end
|
34
|
+
|
35
|
+
puts "Benchmark with valid input data:"
|
36
|
+
valid_data = { id: 1, app_key: "com.test", app_version: "1.0.0", app_name: "testapp" }
|
37
|
+
|
38
|
+
Benchmark.ips do |x|
|
39
|
+
x.config(time: 5, warmup: 2)
|
40
|
+
|
41
|
+
x.report("definition") do
|
42
|
+
DefinitionModel.new(**valid_data)
|
43
|
+
end
|
44
|
+
|
45
|
+
x.report("dry-struct") do
|
46
|
+
DryStructModel.new(**valid_data)
|
47
|
+
end
|
48
|
+
|
49
|
+
x.compare!
|
50
|
+
end
|
51
|
+
|
52
|
+
puts "Benchmark with invalid input data:"
|
53
|
+
invalid_data = { id: "abc", app_key: "com.test", app_name: "testapp" }
|
54
|
+
Benchmark.ips do |x|
|
55
|
+
x.config(time: 5, warmup: 2)
|
56
|
+
|
57
|
+
x.report("definition") do
|
58
|
+
DefinitionModel.new(**invalid_data)
|
59
|
+
rescue Definition::InvalidModelError
|
60
|
+
nil
|
61
|
+
end
|
62
|
+
|
63
|
+
x.report("dry-struct") do
|
64
|
+
DryStructModel.new(**invalid_data)
|
65
|
+
rescue Dry::Struct::Error
|
66
|
+
nil
|
67
|
+
end
|
68
|
+
|
69
|
+
x.compare!
|
70
|
+
end
|
@@ -4,22 +4,20 @@ require "bundler/inline"
|
|
4
4
|
|
5
5
|
gemfile do
|
6
6
|
source "https://rubygems.org"
|
7
|
-
|
8
|
-
gem "dry-
|
9
|
-
gem "dry-types", "< 0.15"
|
7
|
+
gem "dry-validation", "~> 1.8"
|
8
|
+
gem "dry-types", "~> 1.5"
|
10
9
|
gem "awesome_print"
|
11
10
|
gem "benchmark-ips"
|
12
11
|
gem "definition", path: File.expand_path("../.", __dir__)
|
13
12
|
end
|
14
13
|
|
15
|
-
|
16
|
-
|
17
|
-
|
14
|
+
class DryContract < Dry::Validation::Contract
|
15
|
+
params do
|
16
|
+
required(:name).filled(:string)
|
17
|
+
required(:time).filled(:time)
|
18
18
|
end
|
19
|
-
|
20
|
-
required(:name, :string).value(type?: String)
|
21
|
-
required(:time, :time).value(type?: String)
|
22
19
|
end
|
20
|
+
DryContractInstance = DryContract.new
|
23
21
|
|
24
22
|
DefinitionSchema = Definition.Keys do
|
25
23
|
required(:name, Definition.Type(String))
|
@@ -29,7 +27,7 @@ end
|
|
29
27
|
puts "Benchmark with valid input data:"
|
30
28
|
valid_data = { name: "test", time: Time.now }
|
31
29
|
ap DefinitionSchema.conform(valid_data).value
|
32
|
-
ap
|
30
|
+
ap DryContractInstance.call(valid_data)
|
33
31
|
Benchmark.ips do |x|
|
34
32
|
x.config(time: 5, warmup: 2)
|
35
33
|
|
@@ -38,7 +36,7 @@ Benchmark.ips do |x|
|
|
38
36
|
end
|
39
37
|
|
40
38
|
x.report("dry-validation") do
|
41
|
-
|
39
|
+
DryContractInstance.call(valid_data)
|
42
40
|
end
|
43
41
|
|
44
42
|
x.compare!
|
@@ -47,7 +45,7 @@ end
|
|
47
45
|
puts "Benchmark with invalid input data:"
|
48
46
|
invalid_data = { name: 1, time: Time.now.to_s }
|
49
47
|
ap DefinitionSchema.conform(invalid_data).error_message
|
50
|
-
ap
|
48
|
+
ap DryContractInstance.call(invalid_data).errors
|
51
49
|
Benchmark.ips do |x|
|
52
50
|
x.config(time: 5, warmup: 2)
|
53
51
|
|
@@ -56,7 +54,7 @@ Benchmark.ips do |x|
|
|
56
54
|
end
|
57
55
|
|
58
56
|
x.report("dry-validation") do
|
59
|
-
|
57
|
+
DryContractInstance.call(invalid_data)
|
60
58
|
end
|
61
59
|
|
62
60
|
x.compare!
|
data/config/locales/en.yml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
en:
|
2
2
|
definition:
|
3
|
-
max_size: "Value is
|
4
|
-
min_size: "Value is
|
3
|
+
max_size: "Value is longer than %{max_size}"
|
4
|
+
min_size: "Value is shorter than %{min_size}"
|
5
5
|
greater_then: "Value must be greater than %{min_value}"
|
6
6
|
greater_then_equal: "Value must be greater or equal to %{min_value}"
|
7
7
|
less_then: "Value must be less than %{max_value}"
|