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