design_by_contract 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
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