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
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Strict
4
+ class MethodCallError < Error
5
+ attr_reader :verifiable_method, :remaining_args, :remaining_kwargs, :invalid_parameters, :missing_parameters
6
+
7
+ def initialize(verifiable_method:, remaining_args:, remaining_kwargs:, invalid_parameters:, missing_parameters:)
8
+ super(
9
+ message_from(verifiable_method:, remaining_args:, remaining_kwargs:, invalid_parameters:, missing_parameters:)
10
+ )
11
+
12
+ @verifiable_method = verifiable_method
13
+ @remaining_args = remaining_args
14
+ @remaining_kwargs = remaining_kwargs
15
+ @invalid_parameters = invalid_parameters
16
+ @missing_parameters = missing_parameters
17
+ end
18
+
19
+ private
20
+
21
+ def message_from(verifiable_method:, remaining_args:, remaining_kwargs:, invalid_parameters:, missing_parameters:)
22
+ details = [
23
+ invalid_parameters_message_from(invalid_parameters),
24
+ missing_parameters_message_from(missing_parameters),
25
+ remaining_args_message_from(remaining_args),
26
+ remaining_kwargs_message_from(remaining_kwargs)
27
+ ].compact.join("\n")
28
+
29
+ "Calling #{verifiable_method} failed because:\n#{details}"
30
+ end
31
+
32
+ def invalid_parameters_message_from(invalid_parameters)
33
+ return nil unless invalid_parameters
34
+
35
+ details = invalid_parameters.map do |parameter, value|
36
+ " - #{parameter.name}: got #{value.inspect}, expected #{parameter.validator.inspect}"
37
+ end.join("\n")
38
+
39
+ " Some arguments were invalid:\n#{details}"
40
+ end
41
+
42
+ def missing_parameters_message_from(missing_parameters)
43
+ return nil unless missing_parameters
44
+
45
+ details = missing_parameters.map do |parameter_name|
46
+ " - #{parameter_name}"
47
+ end.join("\n")
48
+
49
+ " Some arguments were missing:\n#{details}"
50
+ end
51
+
52
+ def remaining_args_message_from(remaining_args)
53
+ return nil if remaining_args.none?
54
+
55
+ details = remaining_args.map do |arg|
56
+ " - #{arg.inspect}"
57
+ end.join("\n")
58
+
59
+ " Additional positional arguments were provided, but not defined:\n#{details}"
60
+ end
61
+
62
+ def remaining_kwargs_message_from(remaining_kwargs)
63
+ return nil if remaining_kwargs.none?
64
+
65
+ details = remaining_kwargs.map do |key, value|
66
+ " - #{key}: #{value.inspect}"
67
+ end.join("\n")
68
+
69
+ " Additional keyword arguments were provided, but not defined:\n#{details}"
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Strict
4
+ class MethodDefinitionError < Error
5
+ attr_reader :verifiable_method, :missing_parameters, :additional_parameters
6
+
7
+ def initialize(verifiable_method:, missing_parameters:, additional_parameters:)
8
+ super(message_from(verifiable_method:, missing_parameters:, additional_parameters:))
9
+
10
+ @verifiable_method = verifiable_method
11
+ @missing_parameters = missing_parameters
12
+ @additional_parameters = additional_parameters
13
+ end
14
+
15
+ private
16
+
17
+ def message_from(verifiable_method:, missing_parameters:, additional_parameters:)
18
+ details = [
19
+ missing_parameters_message_from(missing_parameters),
20
+ additional_parameters_message_from(additional_parameters)
21
+ ].compact.join("\n")
22
+
23
+ "Defining #{verifiable_method} failed because:\n#{details}"
24
+ end
25
+
26
+ def missing_parameters_message_from(missing_parameters)
27
+ return nil unless missing_parameters.any?
28
+
29
+ details = missing_parameters.map do |parameter_name|
30
+ " - #{parameter_name}"
31
+ end.join("\n")
32
+
33
+ " Some parameters were in the `sig`, but were not in the parameter list:\n#{details}"
34
+ end
35
+
36
+ def additional_parameters_message_from(additional_parameters)
37
+ return nil unless additional_parameters.any?
38
+
39
+ details = additional_parameters.map do |parameter_name|
40
+ " - #{parameter_name}"
41
+ end.join("\n")
42
+
43
+ " Some parameters were not in the `sig`, but were in the parameter list:\n#{details}"
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Strict
4
+ class MethodReturnError < Error
5
+ attr_reader :verifiable_method, :value
6
+
7
+ def initialize(verifiable_method:, value:)
8
+ super(message_from(verifiable_method:, value:))
9
+
10
+ @verifiable_method = verifiable_method
11
+ @value = value
12
+ end
13
+
14
+ private
15
+
16
+ def message_from(verifiable_method:, value:)
17
+ details = invalid_returns_message_from(verifiable_method, value)
18
+ "#{verifiable_method}'s return value was invalid because:\n#{details}"
19
+ end
20
+
21
+ def invalid_returns_message_from(verifiable_method, value)
22
+ " - got #{value.inspect}, expected #{verifiable_method.returns.validator.inspect}"
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Strict
4
+ module Methods
5
+ class Configuration
6
+ attr_reader :parameters, :returns
7
+
8
+ def initialize(parameters:, returns:)
9
+ @parameters = parameters
10
+ @returns = returns
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Strict
4
+ module Methods
5
+ class Dsl < BasicObject
6
+ class << self
7
+ def run(&)
8
+ dsl = new
9
+ dsl.instance_eval(&)
10
+ ::Strict::Methods::Configuration.new(
11
+ parameters: dsl.__strict_dsl_internal_parameters.values,
12
+ returns: dsl.__strict_dsl_internal_returns
13
+ )
14
+ end
15
+ end
16
+
17
+ include ::Strict::Dsl::Validatable
18
+
19
+ attr_reader :__strict_dsl_internal_parameters, :__strict_dsl_internal_returns
20
+
21
+ def initialize
22
+ @__strict_dsl_internal_parameters = {}
23
+ @__strict_dsl_internal_returns = ::Strict::Return.make
24
+ end
25
+
26
+ def returns(*args, **kwargs)
27
+ self.__strict_dsl_internal_returns = ::Strict::Return.make(*args, **kwargs)
28
+ nil
29
+ end
30
+
31
+ def strict_parameter(*args, **kwargs)
32
+ parameter = ::Strict::Parameter.make(*args, **kwargs)
33
+ __strict_dsl_internal_parameters[parameter.name] = parameter
34
+ nil
35
+ end
36
+
37
+ def method_missing(name, *args, **kwargs)
38
+ if respond_to_missing?(name)
39
+ strict_parameter(name, *args, **kwargs)
40
+ else
41
+ super
42
+ end
43
+ end
44
+
45
+ def respond_to_missing?(method_name, _include_private = nil)
46
+ first_letter = method_name.to_s.each_char.first
47
+ first_letter.eql?(first_letter.downcase)
48
+ end
49
+
50
+ private
51
+
52
+ attr_writer :__strict_dsl_internal_returns
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Strict
4
+ module Methods
5
+ class Module < ::Module
6
+ attr_reader :verifiable_method
7
+
8
+ def initialize(verifiable_method)
9
+ super()
10
+
11
+ @verifiable_method = verifiable_method
12
+ define_method verifiable_method.name do |*args, **kwargs, &block|
13
+ args, kwargs = verifiable_method.verify_parameters!(*args, **kwargs)
14
+
15
+ super(*args, **kwargs, &block).tap do |value|
16
+ verifiable_method.verify_returns!(value)
17
+ end
18
+ end
19
+ end
20
+
21
+ def inspect
22
+ "#<#{self.class} (#{verifiable_method.name})>"
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,158 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "forwardable"
4
+
5
+ module Strict
6
+ module Methods
7
+ class VerifiableMethod # rubocop:disable Metrics/ClassLength
8
+ extend Forwardable
9
+
10
+ class UnknownParameterError < Error
11
+ attr_reader :parameter_name
12
+
13
+ def initialize(parameter_name:)
14
+ super(message_from(parameter_name:))
15
+
16
+ @parameter_name = parameter_name
17
+ end
18
+
19
+ private
20
+
21
+ def message_from(parameter_name:)
22
+ "Strict tried to find a parameter named #{parameter_name} but was unable. " \
23
+ "It's likely this in an internal bug, feel free to open an issue at #{Strict::ISSUE_TRACKER} for help."
24
+ end
25
+ end
26
+
27
+ def_delegator :method, :name
28
+
29
+ attr_reader :parameters, :returns
30
+
31
+ def initialize(method:, parameters:, returns:, instance:)
32
+ @method = method
33
+ @parameters = parameters
34
+ @parameters_index = parameters.to_h { |p| [p.name, p] }
35
+ @returns = returns
36
+ @instance = instance
37
+ end
38
+
39
+ def to_s
40
+ "#{method.owner}#{separator}#{name}"
41
+ end
42
+
43
+ def verify_definition!
44
+ expected_parameters = Set.new(parameters.map(&:name))
45
+ defined_parameters = Set.new(method.parameters.filter_map { |kind, name| name unless kind == :block })
46
+ return if expected_parameters == defined_parameters
47
+
48
+ missing_parameters = expected_parameters - defined_parameters
49
+ additional_parameters = defined_parameters - expected_parameters
50
+ raise Strict::MethodDefinitionError.new(verifiable_method: self, missing_parameters:, additional_parameters:)
51
+ end
52
+
53
+ # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity, Metrics/BlockLength
54
+ # TODO(kkt): clean this up- it's late, though, and the tests are passing
55
+ def verify_parameters!(*args, **kwargs)
56
+ invalid_parameters = nil
57
+ missing_parameters = nil
58
+
59
+ positional_arguments = []
60
+ keyword_arguments = {}
61
+
62
+ # TODO(kkt): doesn't handle oddly sorted optional positional parameters like def foo(opt = nil, req)
63
+ method.parameters.each do |kind, name|
64
+ case kind
65
+ when POSITIONAL
66
+ parameter_kind = :positional
67
+ value = args.any? ? args.shift : NOT_PROVIDED
68
+ when REST
69
+ parameter_kind = :rest
70
+ value = [*args]
71
+ args.clear
72
+ when KEYWORD
73
+ parameter_kind = :keyword
74
+ value = kwargs.key?(name) ? kwargs.delete(name) : NOT_PROVIDED
75
+ when KEYREST
76
+ parameter_kind = :keyrest
77
+ value = { **kwargs }
78
+ kwargs.clear
79
+ end
80
+ next unless parameter_kind
81
+
82
+ parameter = parameter_named!(name)
83
+ if value.equal?(NOT_PROVIDED) && parameter.optional?
84
+ value = parameter.default_generator.call
85
+ elsif value.equal?(NOT_PROVIDED)
86
+ missing_parameters ||= []
87
+ missing_parameters << parameter.name
88
+ next
89
+ end
90
+
91
+ value = parameter.coerce(value)
92
+ if parameter.valid?(value)
93
+ case parameter_kind
94
+ when :positional
95
+ positional_arguments << value
96
+ when :rest
97
+ positional_arguments.concat(value)
98
+ when :keyword
99
+ keyword_arguments[name] = value
100
+ when :keyrest
101
+ keyword_arguments.merge!(value)
102
+ end
103
+ else
104
+ invalid_parameters ||= {}
105
+ invalid_parameters[parameter] = value
106
+ end
107
+ end
108
+
109
+ if args.empty? && kwargs.empty? && invalid_parameters.nil? && missing_parameters.nil?
110
+ [positional_arguments, keyword_arguments]
111
+ else
112
+ raise Strict::MethodCallError.new(
113
+ verifiable_method: self,
114
+ remaining_args: args,
115
+ remaining_kwargs: kwargs,
116
+ invalid_parameters:,
117
+ missing_parameters:
118
+ )
119
+ end
120
+ end
121
+ # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity, Metrics/BlockLength
122
+
123
+ def verify_returns!(value)
124
+ value = returns.coerce(value)
125
+ return if returns.valid?(value)
126
+
127
+ raise Strict::MethodReturnError.new(verifiable_method: self, value:)
128
+ end
129
+
130
+ private
131
+
132
+ POSITIONAL = Set.new(%i[req opt])
133
+ private_constant :POSITIONAL
134
+ REST = :rest
135
+ private_constant :REST
136
+ KEYWORD = Set.new(%i[keyreq key])
137
+ private_constant :KEYWORD
138
+ KEYREST = :keyrest
139
+ private_constant :KEYREST
140
+ NOT_PROVIDED = ::Object.new.freeze
141
+ private_constant :NOT_PROVIDED
142
+
143
+ attr_reader :method, :parameters_index
144
+
145
+ def instance?
146
+ @instance
147
+ end
148
+
149
+ def separator
150
+ instance? ? "#" : "."
151
+ end
152
+
153
+ def parameter_named!(name)
154
+ parameters_index.fetch(name) { raise UnknownParameterError.new(parameter_name: name) }
155
+ end
156
+ end
157
+ end
158
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Strict
4
+ module Object
5
+ def self.included(mod)
6
+ mod.extend(Accessor::Attributes)
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Strict
4
+ class Parameter
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
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
+ end
48
+
49
+ def optional?
50
+ @optional
51
+ end
52
+
53
+ def valid?(value)
54
+ validator === value
55
+ end
56
+
57
+ def coerce(value)
58
+ return value unless coercer
59
+
60
+ coercer.call(value)
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Strict
4
+ module Reader
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,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Strict
4
+ module Reader
5
+ class Module < ::Module
6
+ attr_reader :configuration
7
+
8
+ def initialize(configuration)
9
+ super()
10
+
11
+ @configuration = configuration
12
+ const_set(Strict::Attributes::Configured::CONSTANT, configuration)
13
+ configuration.attributes.each do |attribute|
14
+ module_eval(
15
+ "def #{attribute.name} = #{attribute.instance_variable}", # def name = @instance_variable
16
+ __FILE__,
17
+ __LINE__ - 2
18
+ )
19
+ end
20
+ end
21
+
22
+ def inspect
23
+ "#<#{self.class} (#{configuration.attributes.map(&:name).join(', ')})>"
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Strict
4
+ class Return
5
+ class << self
6
+ def make(validator = Validators::Anything.instance, coerce: false)
7
+ new(validator:, coercer: coerce)
8
+ end
9
+ end
10
+
11
+ attr_reader :validator, :coercer
12
+
13
+ def initialize(validator:, coercer:)
14
+ @validator = validator
15
+ @coercer = coercer
16
+ end
17
+
18
+ def valid?(value)
19
+ validator === value
20
+ end
21
+
22
+ def coerce(value)
23
+ return value unless coercer
24
+
25
+ coercer.call(value)
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Strict
4
+ module Validators
5
+ class AllOf
6
+ attr_reader :subvalidators
7
+
8
+ def initialize(*subvalidators)
9
+ @subvalidators = subvalidators
10
+ end
11
+
12
+ def ===(value)
13
+ subvalidators.all? do |subvalidator|
14
+ subvalidator === value
15
+ end
16
+ end
17
+
18
+ def inspect
19
+ "AllOf(#{subvalidators.map(&:inspect).join(', ')})"
20
+ end
21
+ alias to_s inspect
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Strict
4
+ module Validators
5
+ class AnyOf
6
+ attr_reader :subvalidators
7
+
8
+ def initialize(*subvalidators)
9
+ @subvalidators = subvalidators
10
+ end
11
+
12
+ def ===(value)
13
+ subvalidators.any? do |subvalidator|
14
+ subvalidator === value
15
+ end
16
+ end
17
+
18
+ def inspect
19
+ "AnyOf(#{subvalidators.map(&:inspect).join(', ')})"
20
+ end
21
+ alias to_s inspect
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "singleton"
4
+
5
+ module Strict
6
+ module Validators
7
+ class Anything
8
+ include Singleton
9
+
10
+ def ===(_value)
11
+ true
12
+ end
13
+
14
+ def inspect
15
+ "Anything()"
16
+ end
17
+ alias to_s inspect
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Strict
4
+ module Validators
5
+ class ArrayOf
6
+ attr_reader :element_validator
7
+
8
+ def initialize(element_validator)
9
+ @element_validator = element_validator
10
+ end
11
+
12
+ def ===(value)
13
+ Array === value && value.all? do |v|
14
+ element_validator === v
15
+ end
16
+ end
17
+
18
+ def inspect
19
+ "ArrayOf(#{element_validator.inspect})"
20
+ end
21
+ alias to_s inspect
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "singleton"
4
+
5
+ module Strict
6
+ module Validators
7
+ class Boolean
8
+ include Singleton
9
+
10
+ def ===(value)
11
+ value.equal?(true) || value.equal?(false)
12
+ end
13
+
14
+ def inspect
15
+ "Boolean()"
16
+ end
17
+ alias to_s inspect
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Strict
4
+ module Validators
5
+ class HashOf
6
+ attr_reader :key_validator, :value_validator
7
+
8
+ def initialize(key_validator, value_validator)
9
+ @key_validator = key_validator
10
+ @value_validator = value_validator
11
+ end
12
+
13
+ def ===(value)
14
+ Hash === value && value.all? do |k, v|
15
+ key_validator === k && value_validator === v
16
+ end
17
+ end
18
+
19
+ def inspect
20
+ "HashOf(#{key_validator.inspect} => #{value_validator.inspect})"
21
+ end
22
+ alias to_s inspect
23
+ end
24
+ end
25
+ end