strict 0.0.0 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +14 -2
  3. data/Gemfile.lock +18 -1
  4. data/README.md +100 -6
  5. data/lib/strict/accessor/attributes.rb +15 -0
  6. data/lib/strict/accessor/module.rb +45 -0
  7. data/lib/strict/assignment_error.rb +25 -0
  8. data/lib/strict/attribute.rb +71 -0
  9. data/lib/strict/attributes/configuration.rb +46 -0
  10. data/lib/strict/attributes/configured.rb +13 -0
  11. data/lib/strict/attributes/dsl.rb +42 -0
  12. data/lib/strict/attributes/instance.rb +68 -0
  13. data/lib/strict/dsl/validatable.rb +28 -0
  14. data/lib/strict/initialization_error.rb +57 -0
  15. data/lib/strict/method.rb +52 -0
  16. data/lib/strict/method_call_error.rb +72 -0
  17. data/lib/strict/method_definition_error.rb +46 -0
  18. data/lib/strict/method_return_error.rb +25 -0
  19. data/lib/strict/methods/configuration.rb +14 -0
  20. data/lib/strict/methods/dsl.rb +55 -0
  21. data/lib/strict/methods/module.rb +26 -0
  22. data/lib/strict/methods/verifiable_method.rb +158 -0
  23. data/lib/strict/object.rb +9 -0
  24. data/lib/strict/parameter.rb +63 -0
  25. data/lib/strict/reader/attributes.rb +15 -0
  26. data/lib/strict/reader/module.rb +27 -0
  27. data/lib/strict/return.rb +28 -0
  28. data/lib/strict/validators/all_of.rb +24 -0
  29. data/lib/strict/validators/any_of.rb +24 -0
  30. data/lib/strict/validators/anything.rb +20 -0
  31. data/lib/strict/validators/array_of.rb +24 -0
  32. data/lib/strict/validators/boolean.rb +20 -0
  33. data/lib/strict/validators/hash_of.rb +25 -0
  34. data/lib/strict/validators/range_of.rb +24 -0
  35. data/lib/strict/value.rb +22 -0
  36. data/lib/strict/version.rb +1 -1
  37. data/lib/strict.rb +1 -0
  38. data/strict.gemspec +41 -0
  39. metadata +90 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b23b160a9d38c3450223118fcf7c11f7833f1bd25c7f384c966e553e52430e52
4
- data.tar.gz: 8588a01fe2783f8ad7955089af408d969e11713309e97db7eaaa38e344d8b070
3
+ metadata.gz: e55f68c10dc39769cdc9ae894efe2bbcd1a7ff0ce368b0b9b1d8251162cf1c6e
4
+ data.tar.gz: a3454e769c6cd9578c3a5fbe4abd6edd6b0518affcd69d89249235df874875f0
5
5
  SHA512:
6
- metadata.gz: 284acd977a15b2d81a05d91b3dd2bfa3a0cfc9cad3e029cf2c10880020fbbbe23f6f7bfaf3b50d09b23ebc58e78cd65983ea156c7bc1cdd4bbd38337da6efcb6
7
- data.tar.gz: f9c133d678e635b1d14dbb46acbcf71538aa9fc7508cfa3d75d47ae21015fef61f097956f6f3e89998b805e2ea16d4777f7c36802fc59e82783645b80525506c
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/StringLiterals:
6
- EnforcedStyle: double_quotes
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 (0.0.0)
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
- Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/strict`. To experiment with that code, run `bin/console` for an interactive prompt.
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
- $ bundle add strict
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
- $ gem install strict
15
+ ```sh
16
+ $ gem install strict
17
+ ```
16
18
 
17
19
  ## Usage
18
20
 
19
- TODO: Write usage instructions here
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,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Strict
4
+ module Attributes
5
+ module Configured
6
+ CONSTANT = :STRICT_INTERNAL_ATTRIBUTES_CONFIGURATION__
7
+
8
+ def strict_attributes
9
+ self::STRICT_INTERNAL_ATTRIBUTES_CONFIGURATION__
10
+ end
11
+ end
12
+ end
13
+ 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