simple_contracts 0.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 +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +80 -0
- data/lib/simple_contracts.rb +7 -0
- data/lib/simple_contracts/base.rb +188 -0
- data/lib/simple_contracts/sampler.rb +58 -0
- data/lib/simple_contracts/statistics.rb +35 -0
- data/lib/simple_contracts/version.rb +5 -0
- metadata +178 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: f2363b3f293eb320fc635695476d458706c6c163f502d81b0bf0dbf32547cc24
|
4
|
+
data.tar.gz: 9db7c767d18d88e6a65a1bfb29b77b28dbab386ac009535f95c53f618a8687e5
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: b68223981593a3685823bc85b7b95a97b69593c2de8069420ebeada6e0b17ffdb2d9fb67cc1a5255e4355500d675a623ba0aed808409047b03601c145ffbf4c2
|
7
|
+
data.tar.gz: 18effeed0def8ff7893e01858208d2333e03902e0f521f13d1a6544dba4f2d6705e516fe6b0796c01815a4d51772ef001e1f5ea10b607425735640c8c9dbba2c
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2018 bibendi
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,80 @@
|
|
1
|
+
[](https://travis-ci.com/bibendi/simple_contracts)
|
2
|
+
|
3
|
+
# SimpleContracts
|
4
|
+
|
5
|
+
## Plain Old Ruby Object Implementation of Contract
|
6
|
+
|
7
|
+
This project contains the most simple implementation of Contract written in Ruby (and maybe later in other languages).
|
8
|
+
|
9
|
+
The Contract is inspired by Design by Contracts approach and pushes Fail Fast techinque further.
|
10
|
+
|
11
|
+
So, Contract is a class with the only public method , that validates some action/behavior agains Contract Rules:
|
12
|
+
- Guarantees - the rules that SHOULD be valid for each check of behavior
|
13
|
+
- Expectations - list of all expected states that COULD be valid for the behavior check
|
14
|
+
|
15
|
+
Contract validates, that:
|
16
|
+
- ALL Guarantees were met
|
17
|
+
- AT LEAST ONE Expectations was met
|
18
|
+
|
19
|
+
Otherwise, Contract raises an exception with details, at least on what step behavior was broken.
|
20
|
+
|
21
|
+
## Installation
|
22
|
+
|
23
|
+
Add this line to your application's Gemfile:
|
24
|
+
|
25
|
+
```ruby
|
26
|
+
gem 'simple_contracts'
|
27
|
+
```
|
28
|
+
|
29
|
+
And then execute:
|
30
|
+
|
31
|
+
$ bundle
|
32
|
+
|
33
|
+
Or install it yourself as:
|
34
|
+
|
35
|
+
$ gem install simple_contracts
|
36
|
+
|
37
|
+
## Usage
|
38
|
+
|
39
|
+
```ruby
|
40
|
+
class TwitterContract < SimpleContracts::Base
|
41
|
+
def initialize(post)
|
42
|
+
super
|
43
|
+
@post = post
|
44
|
+
end
|
45
|
+
|
46
|
+
private
|
47
|
+
|
48
|
+
def guarantee_verified_delete
|
49
|
+
return true if Twitter::REST::Client.statuses(@post.tweet_id).empty?
|
50
|
+
false
|
51
|
+
end
|
52
|
+
|
53
|
+
def expect_some_action1
|
54
|
+
...
|
55
|
+
end
|
56
|
+
|
57
|
+
def expect_some_action2
|
58
|
+
...
|
59
|
+
end
|
60
|
+
|
61
|
+
# ... other rules
|
62
|
+
end
|
63
|
+
|
64
|
+
@post = Post.find(params.require(:post_id))
|
65
|
+
|
66
|
+
# Use synchronously, (raises exception, "Fails Fast"™):
|
67
|
+
TwitterContract.(@post, async: false) { TwitterAPI.destroy(@post) }
|
68
|
+
|
69
|
+
# Use asynchronously (does not affect TwitterAPI.destroy,
|
70
|
+
# but tracks any problems with TwitterContract validation)
|
71
|
+
TwitterContract.(@post) { TwitterAPI.destroy(@post) }
|
72
|
+
```
|
73
|
+
|
74
|
+
## Contributing
|
75
|
+
|
76
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/bibendi/simple_contracts.
|
77
|
+
|
78
|
+
## License
|
79
|
+
|
80
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
@@ -0,0 +1,188 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'concurrent/future'
|
4
|
+
require 'logger'
|
5
|
+
|
6
|
+
require 'simple_contracts/sampler'
|
7
|
+
require 'simple_contracts/statistics'
|
8
|
+
|
9
|
+
# Base class for writting contracts.
|
10
|
+
# the only public method is SimpleContracts::Base#call (or alias SimpleContracts::Base#match!)
|
11
|
+
#
|
12
|
+
# The purpose is to validate some action against your expectations.
|
13
|
+
# There are 2 kind of them:
|
14
|
+
# - Guarantee - state that SHOULD be recognized for after every the actions
|
15
|
+
# - Expectation - state that COULD be recognized after the action
|
16
|
+
#
|
17
|
+
# The key behavior is:
|
18
|
+
# - First verify that all Guarantees are met, then
|
19
|
+
# - Then move to Expectation and verify that at least one of them met.
|
20
|
+
# - If any of those checks fail - we should recieve detailed exception - why.
|
21
|
+
#
|
22
|
+
# There are 2 kind of exceptions:
|
23
|
+
# - GuaranteeError - happens if one of the Guarantees failes
|
24
|
+
# - ExpectationsError - happens if none of Expextations were meet.
|
25
|
+
#
|
26
|
+
# Both of them raise with the @meta object, which contains extra debugging info.
|
27
|
+
module SimpleContracts
|
28
|
+
class GuaranteesError < StandardError; end
|
29
|
+
class ExpectationsError < StandardError; end
|
30
|
+
|
31
|
+
class Base
|
32
|
+
class << self
|
33
|
+
def call(*args, **kwargs)
|
34
|
+
new(*args, **kwargs).call { yield }
|
35
|
+
end
|
36
|
+
|
37
|
+
def guarantees_methods
|
38
|
+
@guarantees_methods ||= methods_with_prefix("guarantee_")
|
39
|
+
end
|
40
|
+
|
41
|
+
def expectations_methods
|
42
|
+
@expectations_methods ||= methods_with_prefix("expect_")
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
def methods_with_prefix(prefix)
|
48
|
+
private_instance_methods.
|
49
|
+
each_with_object([]) do |method_name, memo|
|
50
|
+
method_name = method_name.to_s
|
51
|
+
next unless method_name.start_with?(prefix)
|
52
|
+
memo << method_name
|
53
|
+
end.sort
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def initialize(*args, async: nil, logger: nil, sampler: nil, stats: nil, **kwargs)
|
58
|
+
@async = async.nil? ? default_async : !!async
|
59
|
+
@sampler = sampler
|
60
|
+
@stats = stats
|
61
|
+
@logger = logger
|
62
|
+
@input = {args: args, kwargs: kwargs}
|
63
|
+
@meta = {checked: [], input: @input}
|
64
|
+
end
|
65
|
+
|
66
|
+
def call
|
67
|
+
return yield unless enabled?
|
68
|
+
@output = yield
|
69
|
+
@async ? verify_async : verify
|
70
|
+
@output
|
71
|
+
end
|
72
|
+
|
73
|
+
alias match! call
|
74
|
+
|
75
|
+
def serialize
|
76
|
+
Marshal.dump(input: @input, output: @output, meta: @meta)
|
77
|
+
end
|
78
|
+
|
79
|
+
def deserialize(state_dump)
|
80
|
+
Marshal.load(state_dump)
|
81
|
+
end
|
82
|
+
|
83
|
+
def contract_name
|
84
|
+
self.class.name
|
85
|
+
end
|
86
|
+
|
87
|
+
private
|
88
|
+
|
89
|
+
def default_async
|
90
|
+
true
|
91
|
+
end
|
92
|
+
|
93
|
+
def concurrent_options
|
94
|
+
{}
|
95
|
+
end
|
96
|
+
|
97
|
+
def call_matchers
|
98
|
+
match_guarantees!
|
99
|
+
match_expectations!
|
100
|
+
end
|
101
|
+
|
102
|
+
def verify
|
103
|
+
call_matchers
|
104
|
+
rescue StandardError => error
|
105
|
+
observe_errors(Time.now, nil, error)
|
106
|
+
raise
|
107
|
+
end
|
108
|
+
|
109
|
+
def verify_async
|
110
|
+
execute_async { call_matchers }
|
111
|
+
end
|
112
|
+
|
113
|
+
def execute_async
|
114
|
+
::Concurrent::Future.
|
115
|
+
execute(concurrent_options) { yield }.
|
116
|
+
add_observer(self, :observe_errors)
|
117
|
+
end
|
118
|
+
|
119
|
+
def observe_errors(_time, _value, reason)
|
120
|
+
return unless reason
|
121
|
+
|
122
|
+
rule = rule_from_error(reason)
|
123
|
+
error = reason if rule == :unexpected_error
|
124
|
+
|
125
|
+
keep_meta(rule, error)
|
126
|
+
rescue StandardError => error
|
127
|
+
logger.error(error)
|
128
|
+
raise
|
129
|
+
end
|
130
|
+
|
131
|
+
def rule_from_error(error)
|
132
|
+
case error
|
133
|
+
when GuaranteesError
|
134
|
+
:guarantee_failure
|
135
|
+
when ExpectationsError
|
136
|
+
:expectation_failure
|
137
|
+
else
|
138
|
+
:unexpected_error
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
def keep_meta(rule, error = nil)
|
143
|
+
sample_path = sampler.sample!(rule)
|
144
|
+
meta = sample_path ? @meta.merge(sample_path: sample_path) : @meta
|
145
|
+
stats.log(rule, meta, error)
|
146
|
+
end
|
147
|
+
|
148
|
+
def sampler
|
149
|
+
@sampler ||= ::SimpleContracts::Sampler.new(self)
|
150
|
+
end
|
151
|
+
|
152
|
+
def stats
|
153
|
+
@stats ||= ::SimpleContracts::Statistics.new(contract_name, logger: logger)
|
154
|
+
end
|
155
|
+
|
156
|
+
def logger
|
157
|
+
@logger ||= ::Logger.new(STDOUT)
|
158
|
+
end
|
159
|
+
|
160
|
+
def enabled?
|
161
|
+
ENV["ENABLE_#{self.class.name}"].to_s != 'false'
|
162
|
+
end
|
163
|
+
|
164
|
+
def match_guarantees!
|
165
|
+
methods = self.class.guarantees_methods
|
166
|
+
return if methods.empty?
|
167
|
+
return if methods.all? do |method_name|
|
168
|
+
@meta[:checked] << method_name
|
169
|
+
!!send(method_name)
|
170
|
+
end
|
171
|
+
|
172
|
+
raise GuaranteesError, @meta
|
173
|
+
end
|
174
|
+
|
175
|
+
def match_expectations!
|
176
|
+
methods = self.class.expectations_methods
|
177
|
+
return if methods.empty?
|
178
|
+
return if methods.any? do |method_name|
|
179
|
+
@meta[:checked] << method_name
|
180
|
+
next unless !!send(method_name)
|
181
|
+
keep_meta(method_name)
|
182
|
+
true
|
183
|
+
end
|
184
|
+
|
185
|
+
raise ExpectationsError, @meta
|
186
|
+
end
|
187
|
+
end
|
188
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SimpleContracts
|
4
|
+
class Sampler
|
5
|
+
PATH_TEMPLATE = "%<contract_name>s/%<rule>s/%<period>i.dump"
|
6
|
+
DEFAULT_PERIOD_SIZE = 60 * 60 # every hour
|
7
|
+
|
8
|
+
def initialize(contract, period_size: nil)
|
9
|
+
@context = contract
|
10
|
+
@contract_name = contract.contract_name
|
11
|
+
@period_size = period_size || default_period_size
|
12
|
+
end
|
13
|
+
|
14
|
+
def sample!(rule)
|
15
|
+
path = sample_path(rule)
|
16
|
+
return unless need_sample?(path)
|
17
|
+
capture(rule)
|
18
|
+
path
|
19
|
+
end
|
20
|
+
|
21
|
+
def sample_path(rule, period = current_period)
|
22
|
+
File.join(
|
23
|
+
root_path,
|
24
|
+
PATH_TEMPLATE % {contract_name: @contract_name, rule: rule, period: period}
|
25
|
+
)
|
26
|
+
end
|
27
|
+
|
28
|
+
# to use in interactive Ruby session
|
29
|
+
def read(path = nil, rule: nil, period: nil)
|
30
|
+
path ||= sample_path(rule, period)
|
31
|
+
raise(ArgumentError, "Sample path should be defined") unless path
|
32
|
+
@context.deserialize(File.read(path))
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
def need_sample?(path)
|
38
|
+
!File.exist?(path)
|
39
|
+
end
|
40
|
+
|
41
|
+
def capture(rule)
|
42
|
+
FileUtils.mkdir_p(File.dirname(sample_path(rule)))
|
43
|
+
File.write(sample_path(rule), @context.serialize)
|
44
|
+
end
|
45
|
+
|
46
|
+
def current_period
|
47
|
+
Time.now.to_i / (@period_size || 1).to_i
|
48
|
+
end
|
49
|
+
|
50
|
+
def default_period_size
|
51
|
+
Integer(ENV["CONTRACT_#{@contract_name}_SAMPLE_PERIOD_SIZE"] || DEFAULT_PERIOD_SIZE)
|
52
|
+
end
|
53
|
+
|
54
|
+
def root_path
|
55
|
+
ENV["CONTRACT_ROOT_PATH"] || File.join("/tmp", "contracts")
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'json'
|
4
|
+
require 'logger'
|
5
|
+
|
6
|
+
module SimpleContracts
|
7
|
+
class Statistics
|
8
|
+
TEMPLATE = "[contracts-match] %<payload>s;"
|
9
|
+
|
10
|
+
def initialize(contract_name, logger: nil)
|
11
|
+
@contract_name = contract_name
|
12
|
+
@logger = logger
|
13
|
+
end
|
14
|
+
|
15
|
+
def log(rule, meta, error = nil)
|
16
|
+
logger.debug(log_data(rule: rule, meta: meta, error: error))
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
def logger
|
22
|
+
@logger ||= Logger.new(STDOUT)
|
23
|
+
end
|
24
|
+
|
25
|
+
def log_data(**kwargs)
|
26
|
+
TEMPLATE % {payload: payload(**kwargs)}
|
27
|
+
end
|
28
|
+
|
29
|
+
def payload(rule:, meta: nil, error: nil)
|
30
|
+
JSON.dump({
|
31
|
+
time: Time.now, contract_name: @contract_name, rule: rule, meta: meta, error: error
|
32
|
+
}.compact)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
metadata
ADDED
@@ -0,0 +1,178 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: simple_contracts
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- bibendi
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2018-07-17 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: concurrent-ruby
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: bundler
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '1.7'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '1.7'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: pry-byebug
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '3'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '3'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rake
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '10.0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '10.0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: rspec
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '3.0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '3.0'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: rubocop
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - "~>"
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0.58'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - "~>"
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0.58'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: simplecov
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - "~>"
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '0.16'
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - "~>"
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '0.16'
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: test-unit
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - "~>"
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '3'
|
118
|
+
type: :development
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - "~>"
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: '3'
|
125
|
+
- !ruby/object:Gem::Dependency
|
126
|
+
name: timecop
|
127
|
+
requirement: !ruby/object:Gem::Requirement
|
128
|
+
requirements:
|
129
|
+
- - "~>"
|
130
|
+
- !ruby/object:Gem::Version
|
131
|
+
version: '0.9'
|
132
|
+
type: :development
|
133
|
+
prerelease: false
|
134
|
+
version_requirements: !ruby/object:Gem::Requirement
|
135
|
+
requirements:
|
136
|
+
- - "~>"
|
137
|
+
- !ruby/object:Gem::Version
|
138
|
+
version: '0.9'
|
139
|
+
description:
|
140
|
+
email:
|
141
|
+
- merkushin.m.s@gmail.com
|
142
|
+
executables: []
|
143
|
+
extensions: []
|
144
|
+
extra_rdoc_files: []
|
145
|
+
files:
|
146
|
+
- LICENSE.txt
|
147
|
+
- README.md
|
148
|
+
- lib/simple_contracts.rb
|
149
|
+
- lib/simple_contracts/base.rb
|
150
|
+
- lib/simple_contracts/sampler.rb
|
151
|
+
- lib/simple_contracts/statistics.rb
|
152
|
+
- lib/simple_contracts/version.rb
|
153
|
+
homepage: https://github.com/bibendi/simple_contracts
|
154
|
+
licenses:
|
155
|
+
- MIT
|
156
|
+
metadata:
|
157
|
+
allowed_push_host: https://rubygems.org
|
158
|
+
post_install_message:
|
159
|
+
rdoc_options: []
|
160
|
+
require_paths:
|
161
|
+
- lib
|
162
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
163
|
+
requirements:
|
164
|
+
- - ">="
|
165
|
+
- !ruby/object:Gem::Version
|
166
|
+
version: '0'
|
167
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
168
|
+
requirements:
|
169
|
+
- - ">="
|
170
|
+
- !ruby/object:Gem::Version
|
171
|
+
version: '0'
|
172
|
+
requirements: []
|
173
|
+
rubyforge_project:
|
174
|
+
rubygems_version: 2.7.6
|
175
|
+
signing_key:
|
176
|
+
specification_version: 4
|
177
|
+
summary: Plain Old Ruby Object Implementation of Contract
|
178
|
+
test_files: []
|