design_by_contract 0.1.0 → 0.2.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
  SHA1:
3
- metadata.gz: 2848a2d974ba0719cfde67f1869c159823ef7a5c
4
- data.tar.gz: b09db1b0e27ffaa7fad9fa0367af71b7563cb69f
3
+ metadata.gz: 1e4752cf5928c7732b9687ce87e0bfa87fa6ac8d
4
+ data.tar.gz: 5a40db6dfc4a45d6e2128061acdf57f403d07d05
5
5
  SHA512:
6
- metadata.gz: ae579964b09e3f8e096924837277a4c54a40c1896fabce5187402a70a7931ac04560b6f0e575354af5b892eb0c73a9ed8b70f05ea5ca3f597756071ed6cd4b04
7
- data.tar.gz: 31ba66b25fda22e573e3358bd384174d7415428128b7e32a64c9c686b65f3617652097a2d44512b17eef472dcfbe8ce88b4ee9a28d8ad976bd35ae351618e12c
6
+ metadata.gz: ba4dd8a67eecfc5e034402091cc29fe502f363e9a00cd7f724227f34d3dd4460e9ade3e58105f1d17e90fff61f99d36ed00948b9b7d6ec185e2e9bc9be4a6cfa
7
+ data.tar.gz: a0ab745b6ac09a0aafb17c2c208f1cbf2ec3b1e0f7a6acc956b10e3675195f156dbe821423455f8d4630e2316cf0a6c29877194f243a45087cf7956fb7e77dde
data/README.md CHANGED
@@ -20,23 +20,100 @@ Or install it yourself as:
20
20
 
21
21
  ## Usage
22
22
 
23
- ### Interface
23
+ ### Design pattern fulfilling methods
24
24
 
25
- The most basic simple usecase for interface is to use it for simply validate a class
25
+ This are created to increase productivity and, make test fail fast in order to eliminate unwanted hiccup
26
26
 
27
- ```ruby
27
+ #### Dependency Injection Pattern
28
+
29
+ interfaces in dependency injection contract placed as last element in the argument description array.
30
+ For key or keyreq arguments, it is required to specify the keyword as second parameter for argument description element.
28
31
 
29
- i = DesignByContract::Interface.new test: [:req, :opt, %i[keyreq keyword] ]
32
+ ```ruby
30
33
 
34
+ require 'logger'
31
35
  class T
36
+
37
+ StoreInterface = DesignByContract::Interface.new(create: [:req, :req], read: [:req])
38
+
39
+ def initialize(store, logger: Logger.new(STDOUT))
40
+ end
41
+ end
42
+
43
+ DesignByContract.as_dependency_injection_for T, [
44
+ [:req, StoreInterface], # pass as predefined interfaces
45
+ [:key, :logger, {info: [:req, :block]}] # or as raw hash based signature
46
+ ]
47
+
48
+ ```
49
+
50
+ ### Under the Hood components
51
+
52
+ #### Signature
53
+
54
+ Signatures are the fingerprint of a function.
55
+ It can describe how the method should look.
56
+ This normally just part of the convention methods under the hood.
57
+
58
+
59
+ ```ruby
60
+
61
+ s = DesignByContract::Signature.new [:req, :opt, %i[keyreq keyword] ]
62
+
63
+ def test(value, value_with_default="def", keyword:)
64
+ end
65
+
66
+ s.match?(method(:test)) #=> true
67
+
68
+ ```
69
+
70
+ #### Interface
71
+
72
+ The most basic simple use case for interface is to use it for simply validate a class.
73
+ Other than that it's also just only the part of the convention methods under the hood.
74
+
75
+ ```ruby
76
+
77
+ i = DesignByContract::Interface.new test: [:req, %i[keyreq keyword] ]
78
+
79
+ class Good1
32
80
  def test(value, value_with_default="def", keyword:)
33
81
  end
34
82
  end
35
83
 
36
- i.implemented_by?(T) #=> true
37
- i.implemented_by?(Class) #=> false
84
+ class Good2
85
+ def test(value, keyword:)
86
+ end
87
+ end
88
+
89
+ class Good3
90
+ def test(value="with_def_still_ok_for_req", keyword:)
91
+ end
92
+ end
93
+
94
+ class Bad
95
+ def test(value1, value2, keyword:)
96
+ end
97
+ end
98
+
99
+ i.implemented_by?(Good1) #=> true
100
+ i.fulfilled_by?(Good1.new) #=> true
101
+ i.match?(Good1.new.method(:test)) #=> true
102
+
103
+ i.implemented_by?(Good2) #=> true
104
+ i.fulfilled_by?(Good2.new) #=> true
105
+ i.match?(Good2.new.method(:test)) #=> true
106
+
107
+ i.implemented_by?(Good3) #=> true
108
+ i.fulfilled_by?(Good3.new) #=> true
109
+ i.match?(Good3.new.method(:test)) #=> true
110
+
111
+ i.implemented_by?(Bad) #=> false
112
+ i.fulfilled_by?(Bad.new) #=> false
113
+ i.match?(Bad.new.method(:test)) #=> false
38
114
 
39
115
  ```
116
+
40
117
  ## Contributing
41
118
 
42
119
  Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/design_by_contract. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.1.0
1
+ 0.2.0
@@ -1,3 +1,41 @@
1
1
  module DesignByContract
2
+ extend(self)
3
+
2
4
  autoload :Interface, 'design_by_contract/interface'
5
+ autoload :Pattern, 'design_by_contract/pattern'
6
+ autoload :Signature, 'design_by_contract/signature'
7
+
8
+ def forget_contract_specifications!
9
+ contracts.keys.each(&:down)
10
+ contracts.clear
11
+ nil
12
+ end
13
+
14
+ def enable_defensive_contract
15
+ @defensive_contract = true
16
+ fulfill_contracts!
17
+ end
18
+
19
+ def as_dependency_injection_for(klass, initialize_signature_spec)
20
+ register_contract DesignByContract::Pattern::DependencyInjection.new(klass, initialize_signature_spec)
21
+ end
22
+
23
+ private
24
+
25
+ def contracts
26
+ @__contracts__ ||= {}
27
+ end
28
+
29
+ def register_contract(contract)
30
+ contracts[contract] = :inactive
31
+ fulfill_contracts! if @defensive_contract
32
+ end
33
+
34
+ def fulfill_contracts!
35
+ contracts.each do |contract, state|
36
+ next if state == :active
37
+ contract.up
38
+ contracts[contract] = :active
39
+ end
40
+ end
3
41
  end
@@ -1,80 +1,49 @@
1
1
  class DesignByContract::Interface
2
- REQUIRED_ARGUMENT = :req
3
- OPTIONAL_ARGUMENT = :opt
4
- REST_OF_THE_ARGUMENTS = :rest
5
-
6
- REQUIRED_KEYWORD = :keyreq
7
- OPTIONAL_KEYWORD = :key
8
- AFTER_KEYWORD_ARGUMENTS = :keyrest
9
-
10
2
  def initialize(method_specifications)
11
- @method_specifications = method_specifications
3
+ @method_specifications = method_specifications.reduce({}) do |ms, (name, raw_signature)|
4
+ ms.merge(name => DesignByContract::Signature.new(raw_signature))
5
+ end
12
6
  end
13
7
 
14
8
  def implemented_by?(implementator_class)
15
9
  @method_specifications.each do |name, signature|
16
10
  return false unless implementator_class.method_defined?(name)
17
- return false unless signature_match?(implementator_class, name, signature)
11
+ return false unless signature.match?(implementator_class.instance_method(name))
18
12
  end
19
13
  true
20
14
  end
21
15
 
22
- private
23
-
24
- def signature_match?(klass, name, signature)
25
- parameters = klass.instance_method(name).parameters
26
-
27
- return false unless req_match?(parameters, signature)
28
- return false unless opt_match?(parameters, signature)
29
- return false unless rest_match?(parameters, signature)
30
- return false unless keyreq_match?(parameters, signature)
31
- return false unless key_match?(parameters, signature)
32
- return false unless keyrest_match?(parameters, signature)
33
-
16
+ def fulfilled_by?(object)
17
+ @method_specifications.each do |name, signature|
18
+ return false unless object.respond_to?(name)
19
+ return false unless signature.match?(object.method(name))
20
+ end
34
21
  true
35
22
  end
36
23
 
37
- def keyrest_match?(parameters, signature)
38
- return true unless signature.include?(:keyrest)
24
+ def match?(method)
25
+ signature = @method_specifications[method.original_name]
39
26
 
40
- parameters.any? { |k, _| k == :keyrest }
27
+ signature.match?(method)
41
28
  end
42
29
 
43
- def key_match?(parameters, signature)
44
- optional_keys = signature.select { |k| k.is_a?(Array) && k[0] == :key }.map(&:last)
45
- return true if optional_keys.empty?
46
- actual_keys = parameters.select { |k, _| k == :key }.map(&:last)
47
- (optional_keys - actual_keys).empty?
48
- end
30
+ def ==(oth_interface)
31
+ return false unless @method_specifications.length == oth_interface.method_specifications.length
49
32
 
50
- def keyreq_match?(parameters, signature)
51
- expected_keys = signature.select { |k| k.is_a?(Array) && k[0] == :keyreq }.sort
52
- return true if expected_keys.empty?
53
- actual_keys = parameters.select { |k, _| k == :keyreq }.sort
54
- expected_keys == actual_keys
55
- end
33
+ @method_specifications.each do |name, spec|
34
+ return false unless oth_interface.method_specifications[name] && oth_interface.method_specifications[name] == spec
35
+ end
56
36
 
57
- def req_match?(parameters, signature)
58
- expected, actually = arg_counts_for(parameters, signature, :req)
59
- return true if expected == 0
60
- expected == actually
37
+ return true
61
38
  end
62
39
 
63
- def opt_match?(parameters, signature)
64
- expected, actually = arg_counts_for(parameters, signature, :opt)
65
- return true if expected == 0
66
- expected <= actually
40
+ def raw
41
+ @method_specifications.reduce({}) do |hash, (k,v)|
42
+ hash.merge(k => v.raw)
43
+ end
67
44
  end
68
45
 
69
- def rest_match?(parameters, signature)
70
- return true unless signature.include?(:rest)
46
+ protected
71
47
 
72
- parameters.any? { |k, _| k == :rest }
73
- end
74
-
75
- def arg_counts_for(parameters, signature, type)
76
- expected_req_count = signature.select { |v| v == type }.length
77
- actual_req_count = parameters.map(&:first).select { |v| v == type }.length
78
- [expected_req_count, actual_req_count]
79
- end
48
+ attr_reader :method_specifications
80
49
  end
@@ -0,0 +1,3 @@
1
+ module DesignByContract::Pattern
2
+ autoload :DependencyInjection, 'design_by_contract/pattern/dependency_injection'
3
+ end
@@ -0,0 +1,48 @@
1
+ class DesignByContract::Pattern::DependencyInjection
2
+ def initialize(target_class, initialize_signature_spec)
3
+ @target_class = target_class
4
+ @signature = DesignByContract::Signature.new(initialize_signature_spec)
5
+ @teardowns = []
6
+ end
7
+
8
+ def up
9
+ validate_initialize_method_signature
10
+ add_on_call_validation_hook
11
+ end
12
+
13
+ def down
14
+ @teardowns.each(&:call)
15
+ @teardowns.clear
16
+ end
17
+
18
+ private
19
+
20
+ def add_on_call_validation_hook
21
+ initialize_checker = Module.new
22
+ signature = @signature
23
+
24
+ initialize_checker.module_eval do
25
+ define_method(:initialize) do |*args|
26
+ raise(ArgumentError, 'argument signature missmatch') unless signature.valid?(*args)
27
+
28
+ super(*args) if defined?(super)
29
+ end
30
+ end
31
+
32
+ @target_class.__send__(:prepend, initialize_checker)
33
+ @teardowns << lambda{ initialize_checker.__send__(:remove_method, :initialize) }
34
+ end
35
+
36
+ # TODO: signature inspect
37
+ def validate_initialize_method_signature
38
+ unless @signature.match?(@target_class.instance_method(:initialize))
39
+ raise(NotImplementedError, ':initialize method signature mismatch')
40
+ end
41
+ rescue NameError
42
+ unless @signature.empty?
43
+ error_message = ":initialize method is not implemented, but contract requires one for #{@target_class}"
44
+
45
+ raise(NotImplementedError, error_message)
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,100 @@
1
+ class DesignByContract::Signature
2
+ autoload :Spec, 'design_by_contract/signature/spec'
3
+
4
+ def initialize(raw_method_specs)
5
+ @method_args_specs = raw_method_specs.map do |spec|
6
+ DesignByContract::Signature::Spec.new(spec)
7
+ end
8
+ end
9
+
10
+ def valid?(*args)
11
+ method_args_specs.each_with_index do |spec, index|
12
+ return false unless spec.interface.fulfilled_by?(args[index])
13
+ end
14
+ return true
15
+ end
16
+
17
+ def match?(parametered_object)
18
+ parameters_match?(parametered_object.parameters)
19
+ end
20
+
21
+ def ==(oth_signature)
22
+ return false unless method_args_specs.length == oth_signature.method_args_specs.length
23
+
24
+ method_args_specs.each_with_index do |spec, index|
25
+ return false unless spec == oth_signature.method_args_specs[index]
26
+ end
27
+
28
+ true
29
+ end
30
+
31
+ def empty?
32
+ method_args_specs.empty?
33
+ end
34
+
35
+ def raw
36
+ @method_args_specs.map(&:raw)
37
+ end
38
+
39
+ protected
40
+
41
+ attr_reader :method_args_specs
42
+
43
+ private
44
+
45
+ def parameters_match?(parameters)
46
+ req_match?(parameters) &&
47
+ opt_match?(parameters) &&
48
+ rest_match?(parameters) &&
49
+ keyreq_match?(parameters) &&
50
+ key_match?(parameters) &&
51
+ keyrest_match?(parameters)
52
+ end
53
+
54
+ def keyrest_match?(parameters)
55
+ return true unless @method_args_specs.any? { |s| s.type == :keyrest }
56
+
57
+ parameters.any? { |k, _| k == :keyrest }
58
+ end
59
+
60
+ def key_match?(parameters)
61
+ optional_keywords = method_args_specs.select { |s| s.type == :key }.map(&:keyword)
62
+
63
+ return true if optional_keywords.empty?
64
+ actual_keys = parameters.select { |k, _| k == :key }.map { |arg_spec| arg_spec[1] }
65
+ (optional_keywords - actual_keys).empty?
66
+ end
67
+
68
+ def keyreq_match?(parameters)
69
+ expected_keys = method_args_specs.select { |s| s.type == :keyreq }.map { |s| [s.type, s.keyword] }.sort
70
+
71
+ return true if expected_keys.empty?
72
+ actual_keys = parameters.select { |k, _| k == :keyreq }.sort
73
+ expected_keys == actual_keys
74
+ end
75
+
76
+ def req_match?(parameters)
77
+ expected_req, actually_req = arg_counts_for(parameters, :req)
78
+ expected_opt, actually_opt = arg_counts_for(parameters, :opt)
79
+ return true if expected_req.zero?
80
+ expected_req <= actually_req + actually_opt && actually_req <= expected_req
81
+ end
82
+
83
+ def opt_match?(parameters)
84
+ expected, actually = arg_counts_for(parameters, :opt)
85
+ return true if expected.zero?
86
+ expected <= actually
87
+ end
88
+
89
+ def rest_match?(parameters)
90
+ return true unless method_args_specs.any? { |s| s.type == :rest }
91
+
92
+ parameters.any? { |k, _| k == :rest }
93
+ end
94
+
95
+ def arg_counts_for(parameters, type)
96
+ expected_req_count = method_args_specs.select { |s| s.type == type }.length
97
+ actual_req_count = parameters.map(&:first).select { |v| v == type }.length
98
+ [expected_req_count, actual_req_count]
99
+ end
100
+ end
@@ -0,0 +1,69 @@
1
+ class DesignByContract::Signature::Spec
2
+ attr_reader :type, :keyword, :interface
3
+
4
+ def initialize(method_args_spec)
5
+ @type, @keyword, @interface = format(method_args_spec)
6
+ end
7
+
8
+ def ==(oth_spec)
9
+ type == oth_spec.type &&
10
+ keyword == oth_spec.keyword &&
11
+ interface == oth_spec.interface
12
+ end
13
+
14
+ def raw
15
+ [type, keyword, interface.raw]
16
+ end
17
+
18
+ private
19
+
20
+ def format(spec)
21
+ spec = [spec] if spec.is_a?(::Symbol)
22
+ raise(ArgumentError) unless spec.is_a?(::Array)
23
+
24
+ case spec.length
25
+ when 1
26
+ return parse_type(spec[0]), nil, parse_interface(nil)
27
+ when 2
28
+ return parse_type(spec[0]), parse_keyword(spec[1]), parse_interface(spec[1])
29
+ when 3
30
+ return parse_type(spec[0]), parse_keyword(spec[1]), parse_interface(spec[2])
31
+ else
32
+ raise(NotImplementedError)
33
+ end
34
+ end
35
+
36
+ ACCEPTED_TYPES = %i[req opt rest keyreq key keyreq keyrest block].freeze
37
+
38
+ def parse_type(object)
39
+ unless ACCEPTED_TYPES.include?(object)
40
+ raise(ArgumentError, 'only the following types are accepted: ' + ACCEPTED_TYPES.join(', '))
41
+ end
42
+
43
+ object
44
+ end
45
+
46
+ def parse_keyword(object)
47
+ case object
48
+ when ::Symbol, ::NilClass
49
+ return object
50
+ when ::Hash, DesignByContract::Interface
51
+ return nil
52
+ else
53
+ raise(ArgumentError, 'keyword can only be symbol')
54
+ end
55
+ end
56
+
57
+ def parse_interface(object)
58
+ case object
59
+ when DesignByContract::Interface
60
+ object
61
+ when ::Hash
62
+ DesignByContract::Interface.new(object)
63
+ when ::NilClass, ::Symbol
64
+ DesignByContract::Interface.new({})
65
+ else
66
+ raise(ArgumentError, 'interface can only be hash or interface type')
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,35 @@
1
+ module T
2
+ def t
3
+ puts 'T'
4
+ super
5
+ end
6
+ end
7
+
8
+ class Z
9
+ def t
10
+ puts 'Z'
11
+ end
12
+ end
13
+
14
+ p Z.instance_method(:t).source_location
15
+ Z.prepend(T)
16
+ p Z.instance_method(:t).source_location
17
+ p Z.instance_method(:t).super_method
18
+
19
+ Z.new.t
20
+
21
+ p Z.instance_method(:t).source_location
22
+
23
+ class M
24
+ def initialize(i_am_just_an_illusion); end
25
+
26
+ def method1(what_the_fuck); end
27
+ end
28
+
29
+ p M.method_defined?(:initialize) #=> false
30
+ p M.method_defined?(:method1) #=> true
31
+
32
+ # method_defined?
33
+ # Returns true if the named method is defined by mod
34
+ # (or its included modules and, if mod is a class, its ancestors).
35
+ # Public and protected methods are matched.
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: design_by_contract
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Adam Luzsi
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2017-11-21 00:00:00.000000000 Z
11
+ date: 2017-11-23 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -73,6 +73,11 @@ files:
73
73
  - design_by_contract.gemspec
74
74
  - lib/design_by_contract.rb
75
75
  - lib/design_by_contract/interface.rb
76
+ - lib/design_by_contract/pattern.rb
77
+ - lib/design_by_contract/pattern/dependency_injection.rb
78
+ - lib/design_by_contract/signature.rb
79
+ - lib/design_by_contract/signature/spec.rb
80
+ - spike/super_method.rb
76
81
  homepage: https://github.com/adamluzsi/design_by_contract.rb
77
82
  licenses:
78
83
  - MIT