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