strict 1.0.0 → 1.1.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e55f68c10dc39769cdc9ae894efe2bbcd1a7ff0ce368b0b9b1d8251162cf1c6e
4
- data.tar.gz: a3454e769c6cd9578c3a5fbe4abd6edd6b0518affcd69d89249235df874875f0
3
+ metadata.gz: 2bf58678470f8cc840c19af0f64ce47ea55abe7124779d1b37ebe10707708d41
4
+ data.tar.gz: 02fb60b1183ab1d5eb90c8438a2271512db588438e75f1f89958e67325788360
5
5
  SHA512:
6
- metadata.gz: 06e647f606756054ece856881aff7de3b7c7fa50301c6b636af116b51ee8f975540b27bd6163391a18b8e8e40db911099e9dc7b1de7eacf38b239267b902287c
7
- data.tar.gz: '09130d2f947390b821c9357c3c17a371236f14c864bb42c8165ef651fedb06943e0d6ad6021ae451549cd368e0546e2e15dfd5a12d3ed090c76091cbe178795f'
6
+ metadata.gz: e333d0836e79b87dfe89fbdc88c915506609a815b1bfce94b4432f40cdf87696c94da52294d3410d11faa48de6c2197194f8be1216cc50ccce2fc9871ab543fc
7
+ data.tar.gz: 14c429b220b1a1c890f164daf50dbda22b86d85659abd1c4a8980abd90785d6c52efc7840af85a75740f2dd928c45861d1244061eb703f5e1461b4cabbe8c2f9
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- strict (1.0.0)
4
+ strict (1.1.0)
5
5
  zeitwerk (~> 2.6)
6
6
 
7
7
  GEM
@@ -45,7 +45,7 @@ GEM
45
45
  rubocop (~> 1.0)
46
46
  ruby-progressbar (1.11.0)
47
47
  unicode-display_width (2.3.0)
48
- zeitwerk (2.6.0)
48
+ zeitwerk (2.6.1)
49
49
 
50
50
  PLATFORMS
51
51
  arm64-darwin-21
data/README.md CHANGED
@@ -108,6 +108,71 @@ UpdateEmail.new.call(user_id: "123", email: "456")
108
108
  # => Strict::MethodReturnError
109
109
  ```
110
110
 
111
+ ### `Strict::Interface`
112
+
113
+ ```rb
114
+ class Storage
115
+ extend Strict::Interface
116
+
117
+ expose(:write) do
118
+ key String
119
+ contents String
120
+ returns Boolean()
121
+ end
122
+
123
+ expose(:read) do
124
+ key String
125
+ returns AnyOf(String, nil)
126
+ end
127
+ end
128
+
129
+ module Storages
130
+ class Memory
131
+ def initialize
132
+ @storage = {}
133
+ end
134
+
135
+ def write(key:, contents:)
136
+ storage[key] = contents
137
+ true
138
+ end
139
+
140
+ def read(key:)
141
+ storage[key]
142
+ end
143
+
144
+ private
145
+
146
+ attr_reader :storage
147
+ end
148
+ end
149
+
150
+ storage = Storage.new(Storages::Memory.new)
151
+ # => #<Storage implementation=#<Storages::Memory>>
152
+
153
+ storage.write(key: "some/path/to/file.rb", contents: "Hello")
154
+ # => true
155
+
156
+ storage.write(key: "some/path/to/file.rb", contents: {})
157
+ # => Strict::MethodCallError
158
+
159
+ storage.read(key: "some/path/to/file.rb")
160
+ # => "Hello"
161
+
162
+ storage.read(key: "some/path/to/other.rb")
163
+ # => nil
164
+
165
+ module Storages
166
+ class Wat
167
+ def write(key:)
168
+ end
169
+ end
170
+ end
171
+
172
+ storage = Storage.new(Storages::Wat.new)
173
+ # => Strict::ImplementationDoesNotConformError
174
+ ```
175
+
111
176
  ## Development
112
177
 
113
178
  After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
@@ -8,7 +8,7 @@ module Strict
8
8
  configuration = Strict::Attributes::Dsl.run(&block)
9
9
  include Module.new(configuration)
10
10
  include Strict::Attributes::Instance
11
- extend Strict::Attributes::Configured
11
+ extend Strict::Attributes::Class
12
12
  end
13
13
  end
14
14
  end
@@ -10,7 +10,7 @@ module Strict
10
10
  super()
11
11
 
12
12
  @configuration = configuration
13
- const_set(Strict::Attributes::Configured::CONSTANT, configuration)
13
+ const_set(Strict::Attributes::Class::CONSTANT, configuration)
14
14
  configuration.attributes.each do |attribute|
15
15
  module_eval(
16
16
  "def #{attribute.name} = #{attribute.instance_variable}", # def name = @instance_variable
@@ -2,12 +2,16 @@
2
2
 
3
3
  module Strict
4
4
  module Attributes
5
- module Configured
5
+ module Class
6
6
  CONSTANT = :STRICT_INTERNAL_ATTRIBUTES_CONFIGURATION__
7
7
 
8
8
  def strict_attributes
9
9
  self::STRICT_INTERNAL_ATTRIBUTES_CONFIGURATION__
10
10
  end
11
+
12
+ def coercer
13
+ Coercer.new(self)
14
+ end
11
15
  end
12
16
  end
13
17
  end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Strict
4
+ module Attributes
5
+ class Coercer
6
+ attr_reader :attributes_class
7
+
8
+ def initialize(attributes_class)
9
+ @attributes_class = attributes_class
10
+ end
11
+
12
+ def call(value)
13
+ return value if value.nil? || !value.respond_to?(:to_h)
14
+
15
+ coerce(value.to_h)
16
+ end
17
+
18
+ private
19
+
20
+ NOT_PROVIDED = ::Object.new.freeze
21
+
22
+ def coerce(hash)
23
+ attributes_class.new(
24
+ **attributes_class.strict_attributes.each_with_object({}) do |attribute, attributes|
25
+ value = hash.fetch(attribute.name) { hash.fetch(attribute.name.to_s, NOT_PROVIDED) }
26
+ attributes[attribute.name] = value unless value.equal?(NOT_PROVIDED)
27
+ end
28
+ )
29
+ end
30
+ end
31
+ end
32
+ end
@@ -11,6 +11,7 @@ module Strict
11
11
  end
12
12
  end
13
13
 
14
+ include ::Strict::Dsl::Coercible
14
15
  include ::Strict::Dsl::Validatable
15
16
 
16
17
  attr_reader :__strict_dsl_internal_attributes
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Strict
4
+ module Coercers
5
+ class Array
6
+ attr_reader :element_coercer
7
+
8
+ def initialize(element_coercer = nil)
9
+ @element_coercer = element_coercer
10
+ end
11
+
12
+ def call(value)
13
+ return value if value.nil? || !value.respond_to?(:to_a)
14
+
15
+ array = value.to_a
16
+ return array unless element_coercer
17
+
18
+ array.map { |element| element_coercer.call(element) }
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Strict
4
+ module Coercers
5
+ class Hash
6
+ attr_reader :key_coercer, :value_coercer
7
+
8
+ def initialize(key_coercer = nil, value_coercer = nil)
9
+ @key_coercer = key_coercer
10
+ @value_coercer = value_coercer
11
+ end
12
+
13
+ def call(value)
14
+ return value if value.nil? || !value.respond_to?(:to_h)
15
+
16
+ if key_coercer && value_coercer
17
+ coerce_keys_and_values(value.to_h)
18
+ elsif key_coercer
19
+ coerce_keys(value.to_h)
20
+ elsif value_coercer
21
+ coerce_values(value.to_h)
22
+ else
23
+ value.to_h
24
+ end
25
+ end
26
+
27
+ private
28
+
29
+ def coerce_keys_and_values(hash) = hash.to_h { |key, value| [key_coercer.call(key), value_coercer.call(value)] }
30
+ def coerce_keys(hash) = hash.transform_keys { |key| key_coercer.call(key) }
31
+ def coerce_values(hash) = hash.transform_values { |value| value_coercer.call(value) }
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Strict
4
+ module Dsl
5
+ module Coercible
6
+ # rubocop:disable Naming/MethodName
7
+
8
+ def ToArray(with: nil) = ::Strict::Coercers::Array.new(with)
9
+ def ToHash(with_keys: nil, with_values: nil) = ::Strict::Coercers::Hash.new(with_keys, with_values)
10
+
11
+ # rubocop:enable Naming/MethodName
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Strict
4
+ class ImplementationDoesNotConformError < Error
5
+ attr_reader :interface, :receiver, :missing_methods, :invalid_method_definitions
6
+
7
+ def initialize(interface:, receiver:, missing_methods:, invalid_method_definitions:)
8
+ super(message_from(interface:, receiver:, missing_methods:, invalid_method_definitions:))
9
+
10
+ @interface = interface
11
+ @receiver = receiver
12
+ @missing_methods = missing_methods
13
+ @invalid_method_definitions = invalid_method_definitions
14
+ end
15
+
16
+ private
17
+
18
+ def message_from(interface:, receiver:, missing_methods:, invalid_method_definitions:)
19
+ details = [
20
+ missing_methods_message_from(missing_methods),
21
+ invalid_method_definitions_message_from(invalid_method_definitions)
22
+ ].compact.join("\n")
23
+
24
+ case receiver
25
+ when ::Class, ::Module
26
+ "#{receiver}'s implementation of #{interface} does not conform:\n#{details}"
27
+ else
28
+ "#{receiver.class}'s implementation of #{interface} does not conform:\n#{details}"
29
+ end
30
+ end
31
+
32
+ def missing_methods_message_from(missing_methods)
33
+ return nil unless missing_methods
34
+
35
+ details = missing_methods.map do |method_name|
36
+ " - #{method_name}"
37
+ end.join("\n")
38
+
39
+ " Some methods exposed in the interface were not defined in the implementation:\n#{details}"
40
+ end
41
+
42
+ def invalid_method_definitions_message_from(invalid_method_definitions)
43
+ return nil if invalid_method_definitions.empty?
44
+
45
+ methods_details = invalid_method_definitions.map do |name, invalid_method_definition|
46
+ method_details = [
47
+ missing_parameters_message_from(invalid_method_definition.fetch(:missing_parameters)),
48
+ additional_parameters_message_from(invalid_method_definition.fetch(:additional_parameters)),
49
+ non_keyword_parameters_message_from(invalid_method_definition.fetch(:non_keyword_parameters))
50
+ ].compact.join("\n")
51
+
52
+ " #{name}:\n#{method_details}"
53
+ end.join("\n")
54
+
55
+ " Some methods defined in the implementation did not conform to their interface:\n#{methods_details}"
56
+ end
57
+
58
+ def missing_parameters_message_from(missing_parameters)
59
+ return nil unless missing_parameters.any?
60
+
61
+ details = missing_parameters.map do |parameter_name|
62
+ " - #{parameter_name}"
63
+ end.join("\n")
64
+
65
+ " Some parameters were expected, but were not in the parameter list:\n#{details}"
66
+ end
67
+
68
+ def additional_parameters_message_from(additional_parameters)
69
+ return nil unless additional_parameters.any?
70
+
71
+ details = additional_parameters.map do |parameter_name|
72
+ " - #{parameter_name}"
73
+ end.join("\n")
74
+
75
+ " Some parameters were not expected, but were in the parameter list:\n#{details}"
76
+ end
77
+
78
+ def non_keyword_parameters_message_from(non_keyword_parameters)
79
+ return nil unless non_keyword_parameters.any?
80
+
81
+ details = non_keyword_parameters.map do |parameter_name|
82
+ " - #{parameter_name}"
83
+ end.join("\n")
84
+
85
+ " Some parameters were not keywords, but only keywords are supported:\n#{details}"
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Strict
4
+ module Interface
5
+ def self.extended(mod)
6
+ mod.extend(Strict::Method)
7
+ mod.include(Interfaces::Instance)
8
+ end
9
+
10
+ def expose(method_name, &)
11
+ sig = sig(&)
12
+ parameter_list = sig.parameters.map { |parameter| "#{parameter.name}:" }.join(", ")
13
+
14
+ module_eval(<<~RUBY, __FILE__, __LINE__ + 1)
15
+ def #{method_name}(#{parameter_list}, &block) # def method_name(one:, two:, three:, &block)
16
+ implementation.#{method_name}(#{parameter_list}, &block) # implementation.method_name(one:, two:, three:, &block)
17
+ end # end
18
+ RUBY
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Strict
4
+ module Interfaces
5
+ module Instance
6
+ attr_reader :implementation
7
+
8
+ # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
9
+ def initialize(implementation)
10
+ missing_methods = nil
11
+ invalid_method_definitions = Hash.new do |h, k|
12
+ h[k] = { additional_parameters: [], missing_parameters: [], non_keyword_parameters: [] }
13
+ end
14
+
15
+ self.class.strict_instance_methods.each do |method_name, strict_method|
16
+ unless implementation.respond_to?(method_name)
17
+ missing_methods ||= []
18
+ missing_methods << method_name
19
+ next
20
+ end
21
+
22
+ expected_parameters = Set.new(strict_method.parameters.map(&:name))
23
+ defined_parameters = Set.new
24
+
25
+ implementation.method(method_name).parameters.each do |kind, parameter_name|
26
+ next if kind == :block
27
+
28
+ if expected_parameters.include?(parameter_name)
29
+ defined_parameters.add(parameter_name)
30
+ invalid_method_definitions[method_name][:non_keyword_parameters] << parameter_name if kind != :keyreq
31
+ else
32
+ invalid_method_definitions[method_name][:additional_parameters] << parameter_name
33
+ end
34
+ end
35
+
36
+ missing_parameters = expected_parameters - defined_parameters
37
+ invalid_method_definitions[method_name][:missing_parameters] = missing_parameters if missing_parameters.any?
38
+ end
39
+
40
+ if missing_methods || !invalid_method_definitions.empty?
41
+ raise Strict::ImplementationDoesNotConformError.new(
42
+ interface: self.class,
43
+ receiver: implementation,
44
+ missing_methods:,
45
+ invalid_method_definitions:
46
+ )
47
+ end
48
+
49
+ @implementation = implementation
50
+ end
51
+ # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
52
+ end
53
+ end
54
+ end
data/lib/strict/method.rb CHANGED
@@ -13,6 +13,24 @@ module Strict
13
13
  instance.instance_variable_set(:@__strict_method_internal_last_sig_configuration, Methods::Dsl.run(&))
14
14
  end
15
15
 
16
+ def strict_class_methods
17
+ instance = singleton_class? ? self : singleton_class
18
+ if instance.instance_variable_defined?(:@__strict_method_internal_class_methods)
19
+ instance.instance_variable_get(:@__strict_method_internal_class_methods)
20
+ else
21
+ instance.instance_variable_set(:@__strict_method_internal_class_methods, {})
22
+ end
23
+ end
24
+
25
+ def strict_instance_methods
26
+ instance = singleton_class? ? self : singleton_class
27
+ if instance.instance_variable_defined?(:@__strict_method_internal_instance_methods)
28
+ instance.instance_variable_get(:@__strict_method_internal_instance_methods)
29
+ else
30
+ instance.instance_variable_set(:@__strict_method_internal_instance_methods, {})
31
+ end
32
+ end
33
+
16
34
  # rubocop:disable Metrics/MethodLength
17
35
  def singleton_method_added(method_name)
18
36
  super
@@ -28,6 +46,7 @@ module Strict
28
46
  instance: false
29
47
  )
30
48
  verifiable_method.verify_definition!
49
+ strict_class_methods[method_name] = verifiable_method
31
50
  singleton_class.prepend(Methods::Module.new(verifiable_method))
32
51
  end
33
52
 
@@ -45,6 +64,7 @@ module Strict
45
64
  instance: true
46
65
  )
47
66
  verifiable_method.verify_definition!
67
+ strict_instance_methods[method_name] = verifiable_method
48
68
  prepend(Methods::Module.new(verifiable_method))
49
69
  end
50
70
  # rubocop:enable Metrics/MethodLength
@@ -30,7 +30,7 @@ module Strict
30
30
  " - #{parameter_name}"
31
31
  end.join("\n")
32
32
 
33
- " Some parameters were in the `sig`, but were not in the parameter list:\n#{details}"
33
+ " Some parameters were in the sig, but were not in the parameter list:\n#{details}"
34
34
  end
35
35
 
36
36
  def additional_parameters_message_from(additional_parameters)
@@ -40,7 +40,7 @@ module Strict
40
40
  " - #{parameter_name}"
41
41
  end.join("\n")
42
42
 
43
- " Some parameters were not in the `sig`, but were in the parameter list:\n#{details}"
43
+ " Some parameters were not in the sig, but were in the parameter list:\n#{details}"
44
44
  end
45
45
  end
46
46
  end
@@ -14,6 +14,7 @@ module Strict
14
14
  end
15
15
  end
16
16
 
17
+ include ::Strict::Dsl::Coercible
17
18
  include ::Strict::Dsl::Validatable
18
19
 
19
20
  attr_reader :__strict_dsl_internal_parameters, :__strict_dsl_internal_returns
@@ -8,7 +8,7 @@ module Strict
8
8
  configuration = Strict::Attributes::Dsl.run(&block)
9
9
  include Module.new(configuration)
10
10
  include Strict::Attributes::Instance
11
- extend Strict::Attributes::Configured
11
+ extend Strict::Attributes::Class
12
12
  end
13
13
  end
14
14
  end
@@ -9,7 +9,7 @@ module Strict
9
9
  super()
10
10
 
11
11
  @configuration = configuration
12
- const_set(Strict::Attributes::Configured::CONSTANT, configuration)
12
+ const_set(Strict::Attributes::Class::CONSTANT, configuration)
13
13
  configuration.attributes.each do |attribute|
14
14
  module_eval(
15
15
  "def #{attribute.name} = #{attribute.instance_variable}", # def name = @instance_variable
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Strict
4
- VERSION = "1.0.0"
4
+ VERSION = "1.1.0"
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: strict
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 1.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kyle Thompson
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2022-10-12 00:00:00.000000000 Z
11
+ date: 2022-10-14 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: zeitwerk
@@ -156,13 +156,20 @@ files:
156
156
  - lib/strict/accessor/module.rb
157
157
  - lib/strict/assignment_error.rb
158
158
  - lib/strict/attribute.rb
159
+ - lib/strict/attributes/class.rb
160
+ - lib/strict/attributes/coercer.rb
159
161
  - lib/strict/attributes/configuration.rb
160
- - lib/strict/attributes/configured.rb
161
162
  - lib/strict/attributes/dsl.rb
162
163
  - lib/strict/attributes/instance.rb
164
+ - lib/strict/coercers/array.rb
165
+ - lib/strict/coercers/hash.rb
166
+ - lib/strict/dsl/coercible.rb
163
167
  - lib/strict/dsl/validatable.rb
164
168
  - lib/strict/error.rb
169
+ - lib/strict/implementation_does_not_conform_error.rb
165
170
  - lib/strict/initialization_error.rb
171
+ - lib/strict/interface.rb
172
+ - lib/strict/interfaces/instance.rb
166
173
  - lib/strict/method.rb
167
174
  - lib/strict/method_call_error.rb
168
175
  - lib/strict/method_definition_error.rb