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 +4 -4
- data/README.md +83 -6
- data/VERSION +1 -1
- data/lib/design_by_contract.rb +38 -0
- data/lib/design_by_contract/interface.rb +24 -55
- data/lib/design_by_contract/pattern.rb +3 -0
- data/lib/design_by_contract/pattern/dependency_injection.rb +48 -0
- data/lib/design_by_contract/signature.rb +100 -0
- data/lib/design_by_contract/signature/spec.rb +69 -0
- data/spike/super_method.rb +35 -0
- metadata +7 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 1e4752cf5928c7732b9687ce87e0bfa87fa6ac8d
|
4
|
+
data.tar.gz: 5a40db6dfc4a45d6e2128061acdf57f403d07d05
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-
###
|
23
|
+
### Design pattern fulfilling methods
|
24
24
|
|
25
|
-
|
25
|
+
This are created to increase productivity and, make test fail fast in order to eliminate unwanted hiccup
|
26
26
|
|
27
|
-
|
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
|
-
|
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
|
-
|
37
|
-
|
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.2.0
|
data/lib/design_by_contract.rb
CHANGED
@@ -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
|
11
|
+
return false unless signature.match?(implementator_class.instance_method(name))
|
18
12
|
end
|
19
13
|
true
|
20
14
|
end
|
21
15
|
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
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
|
38
|
-
|
24
|
+
def match?(method)
|
25
|
+
signature = @method_specifications[method.original_name]
|
39
26
|
|
40
|
-
|
27
|
+
signature.match?(method)
|
41
28
|
end
|
42
29
|
|
43
|
-
def
|
44
|
-
|
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
|
-
|
51
|
-
|
52
|
-
|
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
|
-
|
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
|
64
|
-
|
65
|
-
|
66
|
-
|
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
|
-
|
70
|
-
return true unless signature.include?(:rest)
|
46
|
+
protected
|
71
47
|
|
72
|
-
|
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,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.
|
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-
|
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
|