bcdd-contract 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/.rubocop.yml +128 -0
- data/CHANGELOG.md +45 -0
- data/CODE_OF_CONDUCT.md +84 -0
- data/LICENSE.txt +21 -0
- data/README.md +964 -0
- data/Rakefile +18 -0
- data/Steepfile +32 -0
- data/examples/README.md +11 -0
- data/examples/anti_corruption_layer/README.md +212 -0
- data/examples/anti_corruption_layer/Rakefile +30 -0
- data/examples/anti_corruption_layer/app/models/payment/charge_credit_card.rb +36 -0
- data/examples/anti_corruption_layer/config.rb +20 -0
- data/examples/anti_corruption_layer/lib/payment_gateways/adapters/circle_up.rb +19 -0
- data/examples/anti_corruption_layer/lib/payment_gateways/adapters/pay_friend.rb +19 -0
- data/examples/anti_corruption_layer/lib/payment_gateways/contract.rb +15 -0
- data/examples/anti_corruption_layer/lib/payment_gateways/response.rb +5 -0
- data/examples/anti_corruption_layer/lib/payment_gateways.rb +11 -0
- data/examples/anti_corruption_layer/vendor/circle_up/client.rb +11 -0
- data/examples/anti_corruption_layer/vendor/pay_friend/client.rb +11 -0
- data/examples/business_processes/README.md +245 -0
- data/examples/business_processes/Rakefile +50 -0
- data/examples/business_processes/config.rb +14 -0
- data/examples/business_processes/lib/division.rb +58 -0
- data/examples/design_by_contract/README.md +227 -0
- data/examples/design_by_contract/Rakefile +60 -0
- data/examples/design_by_contract/config.rb +13 -0
- data/examples/design_by_contract/lib/shopping_cart.rb +62 -0
- data/examples/ports_and_adapters/README.md +246 -0
- data/examples/ports_and_adapters/Rakefile +68 -0
- data/examples/ports_and_adapters/app/models/user/record/repository.rb +13 -0
- data/examples/ports_and_adapters/app/models/user/record.rb +7 -0
- data/examples/ports_and_adapters/config.rb +28 -0
- data/examples/ports_and_adapters/db/setup.rb +16 -0
- data/examples/ports_and_adapters/lib/user/creation.rb +19 -0
- data/examples/ports_and_adapters/lib/user/data.rb +5 -0
- data/examples/ports_and_adapters/lib/user/repository.rb +24 -0
- data/examples/ports_and_adapters/test/user_test/repository.rb +21 -0
- data/lib/bcdd/contract/assertions.rb +21 -0
- data/lib/bcdd/contract/config.rb +25 -0
- data/lib/bcdd/contract/core/checker.rb +37 -0
- data/lib/bcdd/contract/core/checking.rb +38 -0
- data/lib/bcdd/contract/core/factory.rb +32 -0
- data/lib/bcdd/contract/core/proxy.rb +19 -0
- data/lib/bcdd/contract/core.rb +12 -0
- data/lib/bcdd/contract/interface.rb +25 -0
- data/lib/bcdd/contract/list.rb +45 -0
- data/lib/bcdd/contract/map/pairs.rb +47 -0
- data/lib/bcdd/contract/map/schema.rb +50 -0
- data/lib/bcdd/contract/map.rb +10 -0
- data/lib/bcdd/contract/proxy.rb +40 -0
- data/lib/bcdd/contract/registry.rb +67 -0
- data/lib/bcdd/contract/unit/checker.rb +51 -0
- data/lib/bcdd/contract/unit/factory.rb +53 -0
- data/lib/bcdd/contract/unit.rb +40 -0
- data/lib/bcdd/contract/version.rb +7 -0
- data/lib/bcdd/contract.rb +118 -0
- data/lib/bcdd-contract.rb +3 -0
- data/sig/bcdd/contract/assertions.rbs +7 -0
- data/sig/bcdd/contract/config.rbs +15 -0
- data/sig/bcdd/contract/core.rbs +60 -0
- data/sig/bcdd/contract/interface.rbs +12 -0
- data/sig/bcdd/contract/list.rbs +21 -0
- data/sig/bcdd/contract/map.rbs +45 -0
- data/sig/bcdd/contract/proxy.rbs +8 -0
- data/sig/bcdd/contract/registry.rbs +25 -0
- data/sig/bcdd/contract/unit.rbs +39 -0
- data/sig/bcdd/contract.rbs +31 -0
- metadata +116 -0
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BCDD::Contract
|
|
4
|
+
class Core::Proxy
|
|
5
|
+
def self.[](object)
|
|
6
|
+
new(object)
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def self.to_proc
|
|
10
|
+
->(object) { new(object) }
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
attr_reader :object
|
|
14
|
+
|
|
15
|
+
def initialize(object)
|
|
16
|
+
@object = object
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BCDD::Contract
|
|
4
|
+
module Interface
|
|
5
|
+
module Callbacks
|
|
6
|
+
def extended(base)
|
|
7
|
+
base.singleton_class.prepend(self::Methods)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def included(base)
|
|
11
|
+
base.prepend(self::Methods)
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def self.included(base)
|
|
16
|
+
base.extend(Callbacks) if Config.instance.interface_enabled
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
module AlwaysEnabled
|
|
20
|
+
def self.included(base)
|
|
21
|
+
base.extend(Interface::Callbacks)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BCDD::Contract
|
|
4
|
+
class List
|
|
5
|
+
class Checking
|
|
6
|
+
include Core::Checking
|
|
7
|
+
|
|
8
|
+
def initialize(checker, value)
|
|
9
|
+
@value = value
|
|
10
|
+
@errors = []
|
|
11
|
+
|
|
12
|
+
validate(checker, @errors)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def errors_message
|
|
16
|
+
valid? ? '' : "(#{errors.join('; ')})"
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
def validate(checker, errors)
|
|
22
|
+
errors << "#{value.inspect} must be a Set | Array" and return unless value.is_a?(::Set) || value.is_a?(::Array)
|
|
23
|
+
errors << 'is empty' and return if value.empty?
|
|
24
|
+
|
|
25
|
+
value.each_with_index do |vval, index|
|
|
26
|
+
val_checking = checker[vval]
|
|
27
|
+
|
|
28
|
+
errors << "#{index}: #{val_checking.errors_message}" if val_checking.invalid?
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
module Checker
|
|
34
|
+
include Core::Checker
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def self.new(strategy)
|
|
38
|
+
return strategy if strategy.is_a?(Checker)
|
|
39
|
+
|
|
40
|
+
Core::Factory.new(Checker, Checking, strategy)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private_constant :List
|
|
45
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BCDD::Contract
|
|
4
|
+
module Map::Pairs
|
|
5
|
+
class Checking
|
|
6
|
+
include Core::Checking
|
|
7
|
+
|
|
8
|
+
def initialize(schema, value)
|
|
9
|
+
@value = value
|
|
10
|
+
@errors = []
|
|
11
|
+
|
|
12
|
+
validate(schema.to_a.flatten(1), @errors)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def errors_message
|
|
16
|
+
valid? ? '' : errors.map { |msg| "(#{msg})" }.join('; ')
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
def validate(schema_key_and_value, errors)
|
|
22
|
+
skey, sval = schema_key_and_value
|
|
23
|
+
|
|
24
|
+
errors << "#{value.inspect} must be a Hash" and return unless value.is_a?(::Hash)
|
|
25
|
+
errors << 'is empty' and return if value.empty?
|
|
26
|
+
|
|
27
|
+
value.each do |vkey, vval|
|
|
28
|
+
key_checking = skey[vkey]
|
|
29
|
+
|
|
30
|
+
errors << "key: #{key_checking.errors_message}" and next if key_checking.invalid?
|
|
31
|
+
|
|
32
|
+
val_checking = sval[vval]
|
|
33
|
+
|
|
34
|
+
errors << "#{vkey}: #{val_checking.errors_message}" and next if val_checking.invalid?
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
module Checker
|
|
40
|
+
include Core::Checker
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def self.new(strategy)
|
|
44
|
+
Core::Factory.new(Checker, Checking, strategy)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BCDD::Contract
|
|
4
|
+
module Map::Schema
|
|
5
|
+
class Checking
|
|
6
|
+
include Core::Checking
|
|
7
|
+
|
|
8
|
+
def initialize(schema, value)
|
|
9
|
+
@value = value
|
|
10
|
+
@errors = {}
|
|
11
|
+
|
|
12
|
+
validate(schema, @errors)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
ErrorsMsg = ->(errors) do
|
|
16
|
+
messages = errors.map { |key, val| %(#{key}: #{val.is_a?(::Hash) ? ErrorsMsg[val] : val.join(', ')}) }
|
|
17
|
+
|
|
18
|
+
"(#{messages.join('; ')})"
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def errors_message
|
|
22
|
+
valid? ? '' : ErrorsMsg[errors]
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
def validate(schema, errors)
|
|
28
|
+
errors[value.inspect] = ['must be a Hash'] and return unless value.is_a?(::Hash)
|
|
29
|
+
|
|
30
|
+
schema.each do |skey, svalue|
|
|
31
|
+
vvalue = value[skey]
|
|
32
|
+
|
|
33
|
+
vchecking = svalue[vvalue]
|
|
34
|
+
|
|
35
|
+
errors[skey] = ['must be a Hash'] and next if svalue.is_a?(Map::Schema::Checker) && !vvalue.is_a?(::Hash)
|
|
36
|
+
|
|
37
|
+
errors[skey] = vchecking.errors and next if vchecking.invalid?
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
module Checker
|
|
43
|
+
include Core::Checker
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def self.new(strategy)
|
|
47
|
+
Core::Factory.new(Checker, Checking, strategy)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BCDD::Contract
|
|
4
|
+
# A class to inherit to create proxy objects.
|
|
5
|
+
# Which can be used to check the arguments and returned values of the proxy object's methods.
|
|
6
|
+
#
|
|
7
|
+
# @example
|
|
8
|
+
# class Calculation < ::BCDD::Contract::Proxy
|
|
9
|
+
# ValidNumber = ::BCDD::Contract[->(value, err) do
|
|
10
|
+
# err << '%p must be numeric' and return unless value.is_a?(::Numeric)
|
|
11
|
+
# err << '%p cannot be nan' and return if value.respond_to?(:nan?) && value.nan?
|
|
12
|
+
# err << '%p cannot be infinite' if value.respond_to?(:infinite?) && value.infinite?
|
|
13
|
+
# end]
|
|
14
|
+
#
|
|
15
|
+
# CannotBeZero = ::BCDD::Contract[->(arg, err) do
|
|
16
|
+
# err << '%p cannot be zero' if arg.zero?
|
|
17
|
+
# end]
|
|
18
|
+
#
|
|
19
|
+
# def divide(a, b)
|
|
20
|
+
# +ValidNumber[a]
|
|
21
|
+
# +ValidNumber[b] && +CannotBeZero[b]
|
|
22
|
+
#
|
|
23
|
+
# +ValidNumber[object.divide(a, b)]
|
|
24
|
+
# end
|
|
25
|
+
#
|
|
26
|
+
# # ... other methods ...
|
|
27
|
+
# end
|
|
28
|
+
class Proxy < Core::Proxy
|
|
29
|
+
# A class to inherit to create a proxy object that is always enabled.
|
|
30
|
+
AlwaysEnabled = ::Class.new(Core::Proxy)
|
|
31
|
+
|
|
32
|
+
def self.new(object)
|
|
33
|
+
return object unless Config.instance.proxy_enabled
|
|
34
|
+
|
|
35
|
+
instance = allocate
|
|
36
|
+
instance.send(:initialize, object)
|
|
37
|
+
instance
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BCDD::Contract
|
|
4
|
+
class Registry
|
|
5
|
+
include ::Singleton
|
|
6
|
+
|
|
7
|
+
OPTIONS = ::Set[
|
|
8
|
+
UNIT = :unit,
|
|
9
|
+
LIST = :list,
|
|
10
|
+
PAIRS = :pairs,
|
|
11
|
+
SCHEMA = :schema
|
|
12
|
+
].freeze
|
|
13
|
+
|
|
14
|
+
attr_reader :store, :names
|
|
15
|
+
|
|
16
|
+
def initialize
|
|
17
|
+
@names = {}
|
|
18
|
+
|
|
19
|
+
@store = {
|
|
20
|
+
UNIT => {},
|
|
21
|
+
LIST => {},
|
|
22
|
+
PAIRS => {},
|
|
23
|
+
SCHEMA => {}
|
|
24
|
+
}
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
Kind = ->(checker) do
|
|
28
|
+
case checker
|
|
29
|
+
when Unit::Checker then UNIT
|
|
30
|
+
when List::Checker then LIST
|
|
31
|
+
when Map::Pairs::Checker then PAIRS
|
|
32
|
+
when Map::Schema::Checker then SCHEMA
|
|
33
|
+
else raise ::ArgumentError, "Unknown checker type: #{checker.inspect}"
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def self.write(name, checker)
|
|
38
|
+
kind = Kind[checker]
|
|
39
|
+
|
|
40
|
+
return fetch(name) if instance.names.key?(name)
|
|
41
|
+
|
|
42
|
+
instance.names[name] = kind
|
|
43
|
+
|
|
44
|
+
instance.store[kind][name] = checker
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def self.fetch(name)
|
|
48
|
+
kind = instance.names[name]
|
|
49
|
+
|
|
50
|
+
kind or raise(::ArgumentError, format('%p not registered', name))
|
|
51
|
+
|
|
52
|
+
read(kind, name)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def self.unit(name)
|
|
56
|
+
read(UNIT, name)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def self.read(kind, name)
|
|
60
|
+
instance.store[kind][name]
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
private_class_method :read, :instance
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
private_constant :Registry
|
|
67
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BCDD::Contract
|
|
4
|
+
module Unit::Checker
|
|
5
|
+
include Core::Checker
|
|
6
|
+
|
|
7
|
+
SequenceMapper = ->(strategy1, strategy2) do
|
|
8
|
+
->(value, err) do
|
|
9
|
+
strategy1.call(value, err)
|
|
10
|
+
|
|
11
|
+
return unless err.empty?
|
|
12
|
+
|
|
13
|
+
strategy2.call(value, err)
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def &(other)
|
|
18
|
+
compose(other, SequenceMapper)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
ParallelMapper = ->(strategy1, strategy2) do
|
|
22
|
+
->(value, err) do
|
|
23
|
+
err1 = []
|
|
24
|
+
err2 = []
|
|
25
|
+
|
|
26
|
+
strategy1.call(value, err1)
|
|
27
|
+
strategy2.call(value, err2)
|
|
28
|
+
|
|
29
|
+
return if err1.empty? || err2.empty?
|
|
30
|
+
|
|
31
|
+
err << err1.concat(err2).map { |msg| format(msg, value) }.join(' OR ')
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def |(other)
|
|
36
|
+
compose(other, ParallelMapper)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
def compose(other, mapper)
|
|
42
|
+
other = Unit::Factory.build(other)
|
|
43
|
+
|
|
44
|
+
composed_strategy = mapper.call(strategy, other.strategy)
|
|
45
|
+
|
|
46
|
+
Unit::Factory.new(composed_strategy)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
private_constant :SequenceMapper, :ParallelMapper
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BCDD::Contract
|
|
4
|
+
module Unit::Factory
|
|
5
|
+
def self.new(strategy)
|
|
6
|
+
Core::Factory.new(Unit::Checker, Unit::Checking, strategy)
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def self.build(arg)
|
|
10
|
+
return arg if arg.is_a?(Core::Checker)
|
|
11
|
+
|
|
12
|
+
return Registry.unit(arg) if arg.is_a?(::Symbol)
|
|
13
|
+
|
|
14
|
+
return type!(::NilClass) if arg.nil?
|
|
15
|
+
|
|
16
|
+
arg.is_a?(::Proc) ? lambda!(arg) : type!(arg)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
ArityOneHandler =
|
|
20
|
+
->(strategy) do
|
|
21
|
+
->(value, err) do
|
|
22
|
+
outcome = strategy.call(value)
|
|
23
|
+
|
|
24
|
+
err << outcome if outcome.is_a?(::String)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def self.lambda!(arg)
|
|
29
|
+
(arg.is_a?(::Proc) && arg.lambda?) or raise ::ArgumentError, 'must be a lambda'
|
|
30
|
+
|
|
31
|
+
strategy =
|
|
32
|
+
case arg.arity
|
|
33
|
+
when 1 then ArityOneHandler[arg]
|
|
34
|
+
when 2 then arg
|
|
35
|
+
else raise ::ArgumentError, 'must have two arguments (value, errors)'
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
new(strategy)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def self.type!(arg)
|
|
42
|
+
arg.is_a?(::Module) or raise ::ArgumentError, format('%p must be a class, module or lambda', arg)
|
|
43
|
+
|
|
44
|
+
cache_item = Registry.unit(arg)
|
|
45
|
+
|
|
46
|
+
return cache_item if cache_item
|
|
47
|
+
|
|
48
|
+
checker = lambda!(->(value, err) { err << "%p must be a #{arg.name}" unless value.is_a?(arg) })
|
|
49
|
+
|
|
50
|
+
Registry.write(arg, checker)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BCDD::Contract
|
|
4
|
+
# A module that can be used to create contract checkers
|
|
5
|
+
# (a module that can be used to perform validations and type checkings).
|
|
6
|
+
#
|
|
7
|
+
# @example
|
|
8
|
+
# cannot_be_inf = ->(val, err) { err << '%p cannot be infinite' if val.respond_to?(:infinite?) && val.infinite? }
|
|
9
|
+
# cannot_be_nan = ->(val, err) { err << '%p cannot be nan' if val.respond_to?(:nan?) && val.nan? }
|
|
10
|
+
#
|
|
11
|
+
# IsNumeric = ::BCDD::Contract[Numeric] & cannot_be_inf & cannot_be_nan
|
|
12
|
+
#
|
|
13
|
+
# ValidNumber = IsNumeric & CannotBeNaN & CannotBeInfinity
|
|
14
|
+
module Unit
|
|
15
|
+
class Checking
|
|
16
|
+
include Core::Checking
|
|
17
|
+
|
|
18
|
+
def initialize(strategy, value)
|
|
19
|
+
@value = value
|
|
20
|
+
|
|
21
|
+
errors = [].tap { |err| strategy.call(value, err) }
|
|
22
|
+
|
|
23
|
+
@errors = errors.flat_map { |error| format(error, value) }
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def errors_message
|
|
27
|
+
errors.join(', ')
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
require_relative 'unit/checker'
|
|
32
|
+
require_relative 'unit/factory'
|
|
33
|
+
|
|
34
|
+
def self.new(arg)
|
|
35
|
+
Factory.build(arg)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private_constant :Unit
|
|
40
|
+
end
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'set'
|
|
4
|
+
require 'singleton'
|
|
5
|
+
|
|
6
|
+
require_relative 'contract/version'
|
|
7
|
+
require_relative 'contract/core'
|
|
8
|
+
require_relative 'contract/registry'
|
|
9
|
+
require_relative 'contract/config'
|
|
10
|
+
require_relative 'contract/unit'
|
|
11
|
+
require_relative 'contract/proxy'
|
|
12
|
+
require_relative 'contract/interface'
|
|
13
|
+
require_relative 'contract/assertions'
|
|
14
|
+
require_relative 'contract/map'
|
|
15
|
+
require_relative 'contract/list'
|
|
16
|
+
|
|
17
|
+
module BCDD
|
|
18
|
+
module Contract
|
|
19
|
+
class Error < StandardError; end
|
|
20
|
+
|
|
21
|
+
def self.config
|
|
22
|
+
Config.instance
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def self.configuration
|
|
26
|
+
yield(config)
|
|
27
|
+
|
|
28
|
+
config.freeze
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def self.proxy(always_enabled: false, &block)
|
|
32
|
+
proxy_class = always_enabled ? Proxy::AlwaysEnabled : Proxy
|
|
33
|
+
|
|
34
|
+
::Class.new(proxy_class, &block)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def self.error!(message)
|
|
38
|
+
raise Error, message
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def self.assert!(value, message, &block)
|
|
42
|
+
return value if (value && !block) || (value && block.call(value))
|
|
43
|
+
|
|
44
|
+
error!(format(message, value))
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def self.refute!(value, message, &block)
|
|
48
|
+
return value if (!value && !block) || (value && block && !block.call(value))
|
|
49
|
+
|
|
50
|
+
error!(format(message, value))
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def self.assert(value, ...)
|
|
54
|
+
Config.instance.assertions_enabled ? assert!(value, ...) : value
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def self.refute(value, ...)
|
|
58
|
+
Config.instance.assertions_enabled ? refute!(value, ...) : value
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def self.new(arg)
|
|
62
|
+
return arg if arg.is_a?(Core::Checker)
|
|
63
|
+
|
|
64
|
+
return schema(arg) if arg.is_a?(::Hash)
|
|
65
|
+
|
|
66
|
+
return Registry.fetch(arg) if arg.is_a?(::Symbol)
|
|
67
|
+
|
|
68
|
+
return unit(arg) unless arg.is_a?(::Array) || arg.is_a?(::Set)
|
|
69
|
+
|
|
70
|
+
list = arg.to_a.flatten
|
|
71
|
+
|
|
72
|
+
return list(list[0]) if list.size == 1
|
|
73
|
+
|
|
74
|
+
raise ::ArgumentError, 'must be one contract checker'
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
singleton_class.send(:alias_method, :[], :new)
|
|
78
|
+
|
|
79
|
+
def self.to_proc
|
|
80
|
+
->(arg) { self[arg] }
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def self.register(**kargs)
|
|
84
|
+
kargs.empty? and raise ::ArgumentError, 'must be passed as keyword arguments'
|
|
85
|
+
|
|
86
|
+
kargs.each_with_object({}) do |(key, val), memo|
|
|
87
|
+
memo[key] = Registry.write(key, new(val))
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def self.unit(arg)
|
|
92
|
+
Unit.new(arg)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def self.list(arg)
|
|
96
|
+
List.new(new(arg))
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def self.schema(arg)
|
|
100
|
+
arg.is_a?(::Hash) or raise ::ArgumentError, 'must be a Hash'
|
|
101
|
+
|
|
102
|
+
Map::Schema.new(arg.transform_values { |svalue| new(svalue) })
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def self.pairs(arg)
|
|
106
|
+
arg.is_a?(::Hash) or raise ::ArgumentError, 'must be a Hash'
|
|
107
|
+
arg.keys.size == 1 or raise ::ArgumentError, 'must have only one key and value'
|
|
108
|
+
|
|
109
|
+
key, val = arg.to_a.flatten(1)
|
|
110
|
+
|
|
111
|
+
Map::Pairs.new(new(key) => new(val))
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def self.Contract(arg)
|
|
116
|
+
Contract.new(arg)
|
|
117
|
+
end
|
|
118
|
+
end
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
module BCDD::Contract::Assertions
|
|
2
|
+
def assert!: (untyped, String) { (untyped) -> bool } -> untyped
|
|
3
|
+
def refute!: (untyped, String) { (untyped) -> bool } -> untyped
|
|
4
|
+
|
|
5
|
+
def assert: (untyped, String) { (untyped) -> bool } -> untyped
|
|
6
|
+
def refute: (untyped, String) { (untyped) -> bool } -> untyped
|
|
7
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
module BCDD::Contract
|
|
2
|
+
class Config
|
|
3
|
+
include ::Singleton
|
|
4
|
+
|
|
5
|
+
attr_accessor proxy_enabled: bool
|
|
6
|
+
attr_accessor interface_enabled: bool
|
|
7
|
+
attr_accessor assertions_enabled: bool
|
|
8
|
+
|
|
9
|
+
def self.instance: () -> Config
|
|
10
|
+
|
|
11
|
+
def initialize: () -> void
|
|
12
|
+
|
|
13
|
+
def options: () -> Hash[Symbol, bool]
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
module BCDD::Contract
|
|
2
|
+
module Core
|
|
3
|
+
class Proxy
|
|
4
|
+
attr_reader object: untyped
|
|
5
|
+
|
|
6
|
+
def self.new: (untyped) -> Proxy
|
|
7
|
+
|
|
8
|
+
def self.to_proc: () -> Proc
|
|
9
|
+
|
|
10
|
+
def initialize: (untyped) -> Proxy
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
module Checking
|
|
14
|
+
attr_reader errors: ::Array[::String]
|
|
15
|
+
|
|
16
|
+
attr_reader value: untyped
|
|
17
|
+
|
|
18
|
+
def initialize: (untyped, untyped) -> void
|
|
19
|
+
|
|
20
|
+
def valid?: () -> bool
|
|
21
|
+
def invalid?: () -> bool
|
|
22
|
+
|
|
23
|
+
alias errors? invalid?
|
|
24
|
+
|
|
25
|
+
def errors_message: () -> ::String
|
|
26
|
+
|
|
27
|
+
def raise_validation_errors!: () -> void
|
|
28
|
+
|
|
29
|
+
def value_or_raise_validation_errors!: () -> untyped
|
|
30
|
+
|
|
31
|
+
alias value! value_or_raise_validation_errors!
|
|
32
|
+
alias assert! value_or_raise_validation_errors!
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
module Checker
|
|
36
|
+
def []: (untyped) -> Checking
|
|
37
|
+
|
|
38
|
+
def ===: (untyped) -> bool
|
|
39
|
+
|
|
40
|
+
def to_proc: () -> ::Proc
|
|
41
|
+
|
|
42
|
+
def invariant: (untyped) { (untyped) -> untyped } -> untyped
|
|
43
|
+
|
|
44
|
+
def checking: () -> untyped
|
|
45
|
+
|
|
46
|
+
def strategy: () -> untyped
|
|
47
|
+
|
|
48
|
+
def const_get: (untyped, bool) -> untyped
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
module Factory
|
|
52
|
+
module Callbacks
|
|
53
|
+
def included: (untyped) -> void
|
|
54
|
+
def extended: (untyped) -> void
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def self.new: (untyped, untyped, untyped) -> ::Module
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|