strict 0.0.0 → 1.0.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/.rubocop.yml +14 -2
- data/Gemfile.lock +18 -1
- data/README.md +100 -6
- data/lib/strict/accessor/attributes.rb +15 -0
- data/lib/strict/accessor/module.rb +45 -0
- data/lib/strict/assignment_error.rb +25 -0
- data/lib/strict/attribute.rb +71 -0
- data/lib/strict/attributes/configuration.rb +46 -0
- data/lib/strict/attributes/configured.rb +13 -0
- data/lib/strict/attributes/dsl.rb +42 -0
- data/lib/strict/attributes/instance.rb +68 -0
- data/lib/strict/dsl/validatable.rb +28 -0
- data/lib/strict/initialization_error.rb +57 -0
- data/lib/strict/method.rb +52 -0
- data/lib/strict/method_call_error.rb +72 -0
- data/lib/strict/method_definition_error.rb +46 -0
- data/lib/strict/method_return_error.rb +25 -0
- data/lib/strict/methods/configuration.rb +14 -0
- data/lib/strict/methods/dsl.rb +55 -0
- data/lib/strict/methods/module.rb +26 -0
- data/lib/strict/methods/verifiable_method.rb +158 -0
- data/lib/strict/object.rb +9 -0
- data/lib/strict/parameter.rb +63 -0
- data/lib/strict/reader/attributes.rb +15 -0
- data/lib/strict/reader/module.rb +27 -0
- data/lib/strict/return.rb +28 -0
- data/lib/strict/validators/all_of.rb +24 -0
- data/lib/strict/validators/any_of.rb +24 -0
- data/lib/strict/validators/anything.rb +20 -0
- data/lib/strict/validators/array_of.rb +24 -0
- data/lib/strict/validators/boolean.rb +20 -0
- data/lib/strict/validators/hash_of.rb +25 -0
- data/lib/strict/validators/range_of.rb +24 -0
- data/lib/strict/value.rb +22 -0
- data/lib/strict/version.rb +1 -1
- data/lib/strict.rb +1 -0
- data/strict.gemspec +41 -0
- metadata +90 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e55f68c10dc39769cdc9ae894efe2bbcd1a7ff0ce368b0b9b1d8251162cf1c6e
|
4
|
+
data.tar.gz: a3454e769c6cd9578c3a5fbe4abd6edd6b0518affcd69d89249235df874875f0
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 06e647f606756054ece856881aff7de3b7c7fa50301c6b636af116b51ee8f975540b27bd6163391a18b8e8e40db911099e9dc7b1de7eacf38b239267b902287c
|
7
|
+
data.tar.gz: '09130d2f947390b821c9357c3c17a371236f14c864bb42c8165ef651fedb06943e0d6ad6021ae451549cd368e0546e2e15dfd5a12d3ed090c76091cbe178795f'
|
data/.rubocop.yml
CHANGED
@@ -1,9 +1,21 @@
|
|
1
|
+
require:
|
2
|
+
- rubocop-minitest
|
3
|
+
- rubocop-rake
|
4
|
+
|
1
5
|
AllCops:
|
2
6
|
NewCops: enable
|
3
7
|
TargetRubyVersion: 3.1.2
|
4
8
|
|
5
|
-
Style/
|
6
|
-
|
9
|
+
Style/CaseEquality:
|
10
|
+
Enabled: false
|
7
11
|
|
8
12
|
Style/Documentation:
|
9
13
|
Enabled: false
|
14
|
+
|
15
|
+
Style/StringLiterals:
|
16
|
+
EnforcedStyle: double_quotes
|
17
|
+
|
18
|
+
Metrics/BlockLength:
|
19
|
+
Exclude:
|
20
|
+
- "test/**/*"
|
21
|
+
- "*.gemspec"
|
data/Gemfile.lock
CHANGED
@@ -1,22 +1,31 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
strict (
|
4
|
+
strict (1.0.0)
|
5
5
|
zeitwerk (~> 2.6)
|
6
6
|
|
7
7
|
GEM
|
8
8
|
remote: https://rubygems.org/
|
9
9
|
specs:
|
10
10
|
ast (2.4.2)
|
11
|
+
debug (1.6.1)
|
12
|
+
irb (>= 1.3.6)
|
13
|
+
reline (>= 0.3.1)
|
11
14
|
gem-release (2.2.2)
|
15
|
+
io-console (0.5.11)
|
16
|
+
irb (1.4.1)
|
17
|
+
reline (>= 0.3.0)
|
12
18
|
json (2.6.2)
|
13
19
|
minitest (5.16.2)
|
20
|
+
minitest-spec-context (0.0.4)
|
14
21
|
parallel (1.22.1)
|
15
22
|
parser (3.1.2.1)
|
16
23
|
ast (~> 2.4.1)
|
17
24
|
rainbow (3.1.1)
|
18
25
|
rake (13.0.6)
|
19
26
|
regexp_parser (2.6.0)
|
27
|
+
reline (0.3.1)
|
28
|
+
io-console (~> 0.5)
|
20
29
|
rexml (3.2.5)
|
21
30
|
rubocop (1.36.0)
|
22
31
|
json (~> 2.3)
|
@@ -30,6 +39,10 @@ GEM
|
|
30
39
|
unicode-display_width (>= 1.4.0, < 3.0)
|
31
40
|
rubocop-ast (1.21.0)
|
32
41
|
parser (>= 3.1.1.0)
|
42
|
+
rubocop-minitest (0.22.2)
|
43
|
+
rubocop (>= 0.90, < 2.0)
|
44
|
+
rubocop-rake (0.6.0)
|
45
|
+
rubocop (~> 1.0)
|
33
46
|
ruby-progressbar (1.11.0)
|
34
47
|
unicode-display_width (2.3.0)
|
35
48
|
zeitwerk (2.6.0)
|
@@ -39,10 +52,14 @@ PLATFORMS
|
|
39
52
|
x86_64-linux
|
40
53
|
|
41
54
|
DEPENDENCIES
|
55
|
+
debug (>= 1.0.0)
|
42
56
|
gem-release (~> 2.2)
|
43
57
|
minitest (~> 5.0)
|
58
|
+
minitest-spec-context (~> 0.0.4)
|
44
59
|
rake (~> 13.0)
|
45
60
|
rubocop (~> 1.21)
|
61
|
+
rubocop-minitest (~> 0.22)
|
62
|
+
rubocop-rake (~> 0.6)
|
46
63
|
strict!
|
47
64
|
|
48
65
|
BUNDLED WITH
|
data/README.md
CHANGED
@@ -1,22 +1,112 @@
|
|
1
1
|
# Strict
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
TODO: Delete this and the text above, and describe your gem
|
3
|
+
Strict provides a means to strictly validate instantiation of values, instantiation and attribute assignment of objects, and method calls at runtime.
|
6
4
|
|
7
5
|
## Installation
|
8
6
|
|
9
7
|
Install the gem and add to the application's Gemfile by executing:
|
10
8
|
|
11
|
-
|
9
|
+
```sh
|
10
|
+
$ bundle add strict
|
11
|
+
```
|
12
12
|
|
13
13
|
If bundler is not being used to manage dependencies, install the gem by executing:
|
14
14
|
|
15
|
-
|
15
|
+
```sh
|
16
|
+
$ gem install strict
|
17
|
+
```
|
16
18
|
|
17
19
|
## Usage
|
18
20
|
|
19
|
-
|
21
|
+
### `Strict::Value`
|
22
|
+
|
23
|
+
```rb
|
24
|
+
class Money
|
25
|
+
include Strict::Value
|
26
|
+
|
27
|
+
attributes do
|
28
|
+
amount_in_cents Integer
|
29
|
+
currency AnyOf("USD", "CAD"), default: "USD"
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
Money.new(amount_in_cents: 100_00)
|
34
|
+
# => #<Money amount_in_cents=100_00 currency="USD">
|
35
|
+
|
36
|
+
Money.new(amount_in_cents: 100_00, currency: "CAD")
|
37
|
+
# => #<Money amount_in_cents=100_00 currency="CAD">
|
38
|
+
|
39
|
+
Money.new(amount_in_cents: 100.00)
|
40
|
+
# => Strict::InitializationError
|
41
|
+
|
42
|
+
Money.new(amount_in_cents: 100_00).with(amount_in_cents: 200_00)
|
43
|
+
# => #<Money amount_in_cents=200_00 currency="USD">
|
44
|
+
|
45
|
+
Money.new(amount_in_cents: 100_00).amount_in_cents = 50_00
|
46
|
+
# => NoMethodError
|
47
|
+
|
48
|
+
Money.new(amount_in_cents: 100_00) == Money.new(amount_in_cents: 100_00)
|
49
|
+
# => true
|
50
|
+
```
|
51
|
+
|
52
|
+
### `Strict::Object`
|
53
|
+
|
54
|
+
```rb
|
55
|
+
class Stateful
|
56
|
+
include Strict::Object
|
57
|
+
|
58
|
+
attributes do
|
59
|
+
some_state String
|
60
|
+
dependency Anything(), default: nil
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
Stateful.new(some_state: "123")
|
65
|
+
# => #<Stateful some_state="123" dependency=nil>
|
66
|
+
|
67
|
+
Stateful.new(some_state: "123").with(some_state: "456")
|
68
|
+
# => NoMethodError
|
69
|
+
|
70
|
+
Stateful.new(some_state: "123").some_state = "456"
|
71
|
+
# => "456"
|
72
|
+
# => #<Stateful some_state="456" dependency=nil>
|
73
|
+
|
74
|
+
Stateful.new(some_state: "123").some_state = 456
|
75
|
+
# => Strict::AssignmentError
|
76
|
+
|
77
|
+
Stateful.new(some_state: "123") == Stateful.new(some_state: "123")
|
78
|
+
# => false
|
79
|
+
```
|
80
|
+
|
81
|
+
### `Strict::Method`
|
82
|
+
|
83
|
+
```rb
|
84
|
+
class UpdateEmail
|
85
|
+
extend Strict::Method
|
86
|
+
|
87
|
+
sig do
|
88
|
+
user_id String, coerce: ->(value) { value.to_s }
|
89
|
+
email String
|
90
|
+
returns AnyOf(true, nil)
|
91
|
+
end
|
92
|
+
def call(user_id:, email:)
|
93
|
+
# contrived logic
|
94
|
+
user_id == email
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
UpdateEmail.new.call(user_id: 123, email: "123")
|
99
|
+
# => true
|
100
|
+
|
101
|
+
UpdateEmail.new.call(user_id: "123", email: "123")
|
102
|
+
# => true
|
103
|
+
|
104
|
+
UpdateEmail.new.call(user_id: "123", email: 123)
|
105
|
+
# => Strict::MethodCallError
|
106
|
+
|
107
|
+
UpdateEmail.new.call(user_id: "123", email: "456")
|
108
|
+
# => Strict::MethodReturnError
|
109
|
+
```
|
20
110
|
|
21
111
|
## Development
|
22
112
|
|
@@ -35,3 +125,7 @@ The gem is available as open source under the terms of the [MIT License](https:/
|
|
35
125
|
## Code of Conduct
|
36
126
|
|
37
127
|
Everyone interacting in the Strict project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/kylekthompson/strict/blob/main/CODE_OF_CONDUCT.md).
|
128
|
+
|
129
|
+
## Credit
|
130
|
+
|
131
|
+
I can't thank [Tom Dalling](https://github.com/tomdalling) enough for his excellent [ValueSemantics](https://github.com/tomdalling/value_semantics) gem. Strict is heavily inspired and influenced by Tom's work and has some borrowed concepts and code.
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Strict
|
4
|
+
module Accessor
|
5
|
+
module Attributes
|
6
|
+
def attributes(&block)
|
7
|
+
block ||= -> {}
|
8
|
+
configuration = Strict::Attributes::Dsl.run(&block)
|
9
|
+
include Module.new(configuration)
|
10
|
+
include Strict::Attributes::Instance
|
11
|
+
extend Strict::Attributes::Configured
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Strict
|
4
|
+
module Accessor
|
5
|
+
class Module < ::Module
|
6
|
+
attr_reader :configuration
|
7
|
+
|
8
|
+
# rubocop:disable Metrics/MethodLength
|
9
|
+
def initialize(configuration)
|
10
|
+
super()
|
11
|
+
|
12
|
+
@configuration = configuration
|
13
|
+
const_set(Strict::Attributes::Configured::CONSTANT, configuration)
|
14
|
+
configuration.attributes.each do |attribute|
|
15
|
+
module_eval(
|
16
|
+
"def #{attribute.name} = #{attribute.instance_variable}", # def name = @instance_variable
|
17
|
+
__FILE__,
|
18
|
+
__LINE__ - 2
|
19
|
+
)
|
20
|
+
|
21
|
+
module_eval(<<~RUBY, __FILE__, __LINE__ + 1)
|
22
|
+
def #{attribute.name}=(value) # def name=(value)
|
23
|
+
attribute = self.class.strict_attributes.named!(:#{attribute.name}) # attribute = self.class.strict_attributes.named!(:name)
|
24
|
+
value = attribute.coerce(value, for_class: self.class) # value = attribute.coerce(value, for_class: self.class)
|
25
|
+
if attribute.valid?(value) # if attribute.valid?(value)
|
26
|
+
#{attribute.instance_variable} = value # @instance_variable = value
|
27
|
+
else # else
|
28
|
+
raise Strict::AssignmentError.new( # raise Strict::AssignmentError.new(
|
29
|
+
assignable_class: self.class, # assignable_class: self.class,
|
30
|
+
invalid_attribute: attribute, # invalid_attribute: attribute,
|
31
|
+
value: # value:
|
32
|
+
) # )
|
33
|
+
end # end
|
34
|
+
end # end
|
35
|
+
RUBY
|
36
|
+
end
|
37
|
+
end
|
38
|
+
# rubocop:enable Metrics/MethodLength
|
39
|
+
|
40
|
+
def inspect
|
41
|
+
"#<#{self.class} (#{configuration.attributes.map(&:name).join(', ')})>"
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Strict
|
4
|
+
class AssignmentError < Error
|
5
|
+
attr_reader :invalid_attribute, :value
|
6
|
+
|
7
|
+
def initialize(assignable_class:, invalid_attribute:, value:)
|
8
|
+
super(message_from(assignable_class:, invalid_attribute:, value:))
|
9
|
+
|
10
|
+
@invalid_attribute = invalid_attribute
|
11
|
+
@value = value
|
12
|
+
end
|
13
|
+
|
14
|
+
private
|
15
|
+
|
16
|
+
def message_from(assignable_class:, invalid_attribute:, value:)
|
17
|
+
details = invalid_attribute_message_from(invalid_attribute, value)
|
18
|
+
"Assignment to #{invalid_attribute.name} of #{assignable_class} failed because:\n#{details}"
|
19
|
+
end
|
20
|
+
|
21
|
+
def invalid_attribute_message_from(invalid_attribute, value)
|
22
|
+
" - got #{value.inspect}, expected #{invalid_attribute.validator.inspect}"
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Strict
|
4
|
+
class Attribute
|
5
|
+
NOT_PROVIDED = ::Object.new.freeze
|
6
|
+
|
7
|
+
class << self
|
8
|
+
def make(name, validator = Validators::Anything.instance, coerce: false, **defaults)
|
9
|
+
unless valid_defaults?(**defaults)
|
10
|
+
raise ArgumentError, "Only one of 'default', 'default_value', or 'default_generator' can be provided"
|
11
|
+
end
|
12
|
+
|
13
|
+
new(name: name.to_sym, validator:, default_generator: make_default_generator(**defaults), coercer: coerce)
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
def valid_defaults?(default: NOT_PROVIDED, default_value: NOT_PROVIDED, default_generator: NOT_PROVIDED)
|
19
|
+
defaults_provided = [default, default_value, default_generator].count do |default_option|
|
20
|
+
!default_option.equal?(NOT_PROVIDED)
|
21
|
+
end
|
22
|
+
|
23
|
+
defaults_provided <= 1
|
24
|
+
end
|
25
|
+
|
26
|
+
def make_default_generator(default: NOT_PROVIDED, default_value: NOT_PROVIDED, default_generator: NOT_PROVIDED)
|
27
|
+
if !default.equal?(NOT_PROVIDED)
|
28
|
+
default.respond_to?(:call) ? default : -> { default }
|
29
|
+
elsif !default_value.equal?(NOT_PROVIDED)
|
30
|
+
-> { default_value }
|
31
|
+
elsif !default_generator.equal?(NOT_PROVIDED)
|
32
|
+
default_generator
|
33
|
+
else
|
34
|
+
NOT_PROVIDED
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
attr_reader :name, :validator, :default_generator, :coercer, :instance_variable
|
40
|
+
|
41
|
+
def initialize(name:, validator:, default_generator:, coercer:)
|
42
|
+
@name = name.to_sym
|
43
|
+
@validator = validator
|
44
|
+
@default_generator = default_generator
|
45
|
+
@coercer = coercer
|
46
|
+
@optional = !default_generator.equal?(NOT_PROVIDED)
|
47
|
+
@instance_variable = "@#{name.to_s.chomp('!').chomp('?')}"
|
48
|
+
end
|
49
|
+
|
50
|
+
def optional?
|
51
|
+
@optional
|
52
|
+
end
|
53
|
+
|
54
|
+
def valid?(value)
|
55
|
+
validator === value
|
56
|
+
end
|
57
|
+
|
58
|
+
def coerce(value, for_class:)
|
59
|
+
return value unless coercer
|
60
|
+
|
61
|
+
case coercer
|
62
|
+
when Symbol
|
63
|
+
for_class.public_send(coercer, value)
|
64
|
+
when true
|
65
|
+
for_class.public_send("coerce_#{name}", value)
|
66
|
+
else
|
67
|
+
coercer.call(value)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "forwardable"
|
4
|
+
|
5
|
+
module Strict
|
6
|
+
module Attributes
|
7
|
+
class Configuration
|
8
|
+
include Enumerable
|
9
|
+
extend Forwardable
|
10
|
+
|
11
|
+
class UnknownAttributeError < Error
|
12
|
+
attr_reader :attribute_name
|
13
|
+
|
14
|
+
def initialize(attribute_name:)
|
15
|
+
super(message_from(attribute_name:))
|
16
|
+
|
17
|
+
@attribute_name = attribute_name
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def message_from(attribute_name:)
|
23
|
+
"Strict tried to find an attribute named #{attribute_name} but was unable. " \
|
24
|
+
"It's likely this in an internal bug, feel free to open an issue at #{Strict::ISSUE_TRACKER} for help."
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def_delegator :attributes, :each
|
29
|
+
|
30
|
+
attr_reader :attributes
|
31
|
+
|
32
|
+
def initialize(attributes:)
|
33
|
+
@attributes = attributes
|
34
|
+
@attributes_index = attributes.to_h { |a| [a.name, a] }
|
35
|
+
end
|
36
|
+
|
37
|
+
def named!(name)
|
38
|
+
attributes_index.fetch(name) { raise UnknownAttributeError.new(attribute_name: name) }
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
attr_reader :attributes_index
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Strict
|
4
|
+
module Attributes
|
5
|
+
class Dsl < BasicObject
|
6
|
+
class << self
|
7
|
+
def run(&)
|
8
|
+
dsl = new
|
9
|
+
dsl.instance_eval(&)
|
10
|
+
::Strict::Attributes::Configuration.new(attributes: dsl.__strict_dsl_internal_attributes.values)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
include ::Strict::Dsl::Validatable
|
15
|
+
|
16
|
+
attr_reader :__strict_dsl_internal_attributes
|
17
|
+
|
18
|
+
def initialize
|
19
|
+
@__strict_dsl_internal_attributes = {}
|
20
|
+
end
|
21
|
+
|
22
|
+
def strict_attribute(*args, **kwargs)
|
23
|
+
attribute = ::Strict::Attribute.make(*args, **kwargs)
|
24
|
+
__strict_dsl_internal_attributes[attribute.name] = attribute
|
25
|
+
nil
|
26
|
+
end
|
27
|
+
|
28
|
+
def method_missing(name, *args, **kwargs)
|
29
|
+
if respond_to_missing?(name)
|
30
|
+
strict_attribute(name, *args, **kwargs)
|
31
|
+
else
|
32
|
+
super
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def respond_to_missing?(method_name, _include_private = nil)
|
37
|
+
first_letter = method_name.to_s.each_char.first
|
38
|
+
first_letter.eql?(first_letter.downcase)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Strict
|
4
|
+
module Attributes
|
5
|
+
module Instance
|
6
|
+
# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
7
|
+
def initialize(**attributes)
|
8
|
+
remaining_attributes = Set.new(attributes.keys)
|
9
|
+
invalid_attributes = nil
|
10
|
+
missing_attributes = nil
|
11
|
+
|
12
|
+
self.class.strict_attributes.each do |attribute|
|
13
|
+
if remaining_attributes.delete?(attribute.name)
|
14
|
+
value = attributes.fetch(attribute.name)
|
15
|
+
elsif attribute.optional?
|
16
|
+
value = attribute.default_generator.call
|
17
|
+
else
|
18
|
+
missing_attributes ||= []
|
19
|
+
missing_attributes << attribute.name
|
20
|
+
next
|
21
|
+
end
|
22
|
+
|
23
|
+
value = attribute.coerce(value, for_class: self.class)
|
24
|
+
if attribute.valid?(value)
|
25
|
+
instance_variable_set(attribute.instance_variable, value)
|
26
|
+
else
|
27
|
+
invalid_attributes ||= {}
|
28
|
+
invalid_attributes[attribute] = value
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
return if remaining_attributes.none? && invalid_attributes.nil? && missing_attributes.nil?
|
33
|
+
|
34
|
+
raise InitializationError.new(
|
35
|
+
initializable_class: self.class,
|
36
|
+
remaining_attributes:,
|
37
|
+
invalid_attributes:,
|
38
|
+
missing_attributes:
|
39
|
+
)
|
40
|
+
end
|
41
|
+
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
42
|
+
|
43
|
+
def to_h
|
44
|
+
self.class.strict_attributes.to_h do |attribute|
|
45
|
+
[attribute.name, public_send(attribute.name)]
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def inspect
|
50
|
+
if self.class.strict_attributes.any?
|
51
|
+
"#<#{self.class} #{to_h.map { |key, value| "#{key}=#{value.inspect}" }.join(' ')}>"
|
52
|
+
else
|
53
|
+
"#<#{self.class}>"
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def pretty_print(pp)
|
58
|
+
pp.object_group(self) do
|
59
|
+
to_h.each do |key, value|
|
60
|
+
pp.breakable
|
61
|
+
pp.text("#{key}=")
|
62
|
+
pp.pp(value)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Strict
|
4
|
+
module Dsl
|
5
|
+
module Validatable
|
6
|
+
# rubocop:disable Naming/MethodName
|
7
|
+
|
8
|
+
def AllOf(*subvalidators) = ::Strict::Validators::AllOf.new(*subvalidators)
|
9
|
+
def AnyOf(*subvalidators) = ::Strict::Validators::AnyOf.new(*subvalidators)
|
10
|
+
def Anything = ::Strict::Validators::Anything.instance
|
11
|
+
def ArrayOf(element_validator) = ::Strict::Validators::ArrayOf.new(element_validator)
|
12
|
+
def Boolean = ::Strict::Validators::Boolean.instance
|
13
|
+
|
14
|
+
def HashOf(key_validator_to_value_validator)
|
15
|
+
if key_validator_to_value_validator.size != 1
|
16
|
+
raise ArgumentError, "HashOf's usage is: HashOf(KeyValidator => ValueValidator)"
|
17
|
+
end
|
18
|
+
|
19
|
+
key_validator, value_validator = key_validator_to_value_validator.first
|
20
|
+
::Strict::Validators::HashOf.new(key_validator, value_validator)
|
21
|
+
end
|
22
|
+
|
23
|
+
def RangeOf(element_validator) = ::Strict::Validators::RangeOf.new(element_validator)
|
24
|
+
|
25
|
+
# rubocop:enable Naming/MethodName
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Strict
|
4
|
+
class InitializationError < Error
|
5
|
+
attr_reader :remaining_attributes, :invalid_attributes, :missing_attributes
|
6
|
+
|
7
|
+
def initialize(initializable_class:, remaining_attributes:, invalid_attributes:, missing_attributes:)
|
8
|
+
super(message_from(initializable_class:, remaining_attributes:, invalid_attributes:, missing_attributes:))
|
9
|
+
|
10
|
+
@remaining_attributes = remaining_attributes
|
11
|
+
@invalid_attributes = invalid_attributes
|
12
|
+
@missing_attributes = missing_attributes
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
def message_from(initializable_class:, remaining_attributes:, invalid_attributes:, missing_attributes:)
|
18
|
+
details = [
|
19
|
+
invalid_attributes_message_from(invalid_attributes),
|
20
|
+
missing_attributes_message_from(missing_attributes),
|
21
|
+
remaining_attributes_message_from(remaining_attributes)
|
22
|
+
].compact.join("\n")
|
23
|
+
|
24
|
+
"Initialization of #{initializable_class} failed because:\n#{details}"
|
25
|
+
end
|
26
|
+
|
27
|
+
def invalid_attributes_message_from(invalid_attributes)
|
28
|
+
return nil unless invalid_attributes
|
29
|
+
|
30
|
+
details = invalid_attributes.map do |attribute, value|
|
31
|
+
" - #{attribute.name}: got #{value.inspect}, expected #{attribute.validator.inspect}"
|
32
|
+
end.join("\n")
|
33
|
+
|
34
|
+
" Some attributes were invalid:\n#{details}"
|
35
|
+
end
|
36
|
+
|
37
|
+
def missing_attributes_message_from(missing_attributes)
|
38
|
+
return nil unless missing_attributes
|
39
|
+
|
40
|
+
details = missing_attributes.map do |attribute_name|
|
41
|
+
" - #{attribute_name}"
|
42
|
+
end.join("\n")
|
43
|
+
|
44
|
+
" Some attributes were missing:\n#{details}"
|
45
|
+
end
|
46
|
+
|
47
|
+
def remaining_attributes_message_from(remaining_attributes)
|
48
|
+
return nil if remaining_attributes.none?
|
49
|
+
|
50
|
+
details = remaining_attributes.map do |attribute_name|
|
51
|
+
" - #{attribute_name}"
|
52
|
+
end.join("\n")
|
53
|
+
|
54
|
+
" Some attributes were provided, but not defined:\n#{details}"
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Strict
|
4
|
+
module Method
|
5
|
+
def self.extended(mod)
|
6
|
+
return if mod.singleton_class?
|
7
|
+
|
8
|
+
mod.singleton_class.extend(self)
|
9
|
+
end
|
10
|
+
|
11
|
+
def sig(&)
|
12
|
+
instance = singleton_class? ? self : singleton_class
|
13
|
+
instance.instance_variable_set(:@__strict_method_internal_last_sig_configuration, Methods::Dsl.run(&))
|
14
|
+
end
|
15
|
+
|
16
|
+
# rubocop:disable Metrics/MethodLength
|
17
|
+
def singleton_method_added(method_name)
|
18
|
+
super
|
19
|
+
|
20
|
+
sig = singleton_class.instance_variable_get(:@__strict_method_internal_last_sig_configuration)
|
21
|
+
singleton_class.instance_variable_set(:@__strict_method_internal_last_sig_configuration, nil)
|
22
|
+
return unless sig
|
23
|
+
|
24
|
+
verifiable_method = Methods::VerifiableMethod.new(
|
25
|
+
method: singleton_class.instance_method(method_name),
|
26
|
+
parameters: sig.parameters,
|
27
|
+
returns: sig.returns,
|
28
|
+
instance: false
|
29
|
+
)
|
30
|
+
verifiable_method.verify_definition!
|
31
|
+
singleton_class.prepend(Methods::Module.new(verifiable_method))
|
32
|
+
end
|
33
|
+
|
34
|
+
def method_added(method_name)
|
35
|
+
super
|
36
|
+
|
37
|
+
sig = singleton_class.instance_variable_get(:@__strict_method_internal_last_sig_configuration)
|
38
|
+
singleton_class.instance_variable_set(:@__strict_method_internal_last_sig_configuration, nil)
|
39
|
+
return unless sig
|
40
|
+
|
41
|
+
verifiable_method = Methods::VerifiableMethod.new(
|
42
|
+
method: instance_method(method_name),
|
43
|
+
parameters: sig.parameters,
|
44
|
+
returns: sig.returns,
|
45
|
+
instance: true
|
46
|
+
)
|
47
|
+
verifiable_method.verify_definition!
|
48
|
+
prepend(Methods::Module.new(verifiable_method))
|
49
|
+
end
|
50
|
+
# rubocop:enable Metrics/MethodLength
|
51
|
+
end
|
52
|
+
end
|