flows 0.1.0 → 0.5.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/workflows/test.yml +38 -0
- data/.gitignore +9 -1
- data/.mdlrc +1 -0
- data/.reek.yml +54 -0
- data/.rubocop.yml +44 -2
- data/.ruby-version +1 -1
- data/.yardopts +1 -0
- data/CHANGELOG.md +65 -0
- data/README.md +186 -256
- data/Rakefile +35 -1
- data/bin/.rubocop.yml +5 -0
- data/bin/all_the_errors +55 -0
- data/bin/benchmark +69 -78
- data/bin/benchmark_cli/compare.rb +118 -0
- data/bin/benchmark_cli/compare/a_plus_b.rb +22 -0
- data/bin/benchmark_cli/compare/base.rb +45 -0
- data/bin/benchmark_cli/compare/command.rb +47 -0
- data/bin/benchmark_cli/compare/ten_steps.rb +22 -0
- data/bin/benchmark_cli/examples.rb +23 -0
- data/bin/benchmark_cli/examples/.rubocop.yml +19 -0
- data/bin/benchmark_cli/examples/a_plus_b/dry_do.rb +23 -0
- data/bin/benchmark_cli/examples/a_plus_b/dry_transaction.rb +17 -0
- data/bin/benchmark_cli/examples/a_plus_b/flows_do.rb +22 -0
- data/bin/benchmark_cli/examples/a_plus_b/flows_railway.rb +13 -0
- data/bin/benchmark_cli/examples/a_plus_b/flows_scp.rb +13 -0
- data/bin/benchmark_cli/examples/a_plus_b/flows_scp_mut.rb +13 -0
- data/bin/benchmark_cli/examples/a_plus_b/flows_scp_oc.rb +21 -0
- data/bin/benchmark_cli/examples/a_plus_b/trailblazer.rb +15 -0
- data/bin/benchmark_cli/examples/ten_steps/dry_do.rb +70 -0
- data/bin/benchmark_cli/examples/ten_steps/dry_transaction.rb +64 -0
- data/bin/benchmark_cli/examples/ten_steps/flows_do.rb +69 -0
- data/bin/benchmark_cli/examples/ten_steps/flows_railway.rb +58 -0
- data/bin/benchmark_cli/examples/ten_steps/flows_scp.rb +58 -0
- data/bin/benchmark_cli/examples/ten_steps/flows_scp_mut.rb +58 -0
- data/bin/benchmark_cli/examples/ten_steps/flows_scp_oc.rb +66 -0
- data/bin/benchmark_cli/examples/ten_steps/trailblazer.rb +60 -0
- data/bin/benchmark_cli/helpers.rb +12 -0
- data/bin/benchmark_cli/ruby.rb +15 -0
- data/bin/benchmark_cli/ruby/command.rb +38 -0
- data/bin/benchmark_cli/ruby/method_exec.rb +71 -0
- data/bin/benchmark_cli/ruby/self_class.rb +69 -0
- data/bin/benchmark_cli/ruby/structs.rb +90 -0
- data/bin/console +1 -0
- data/bin/docserver +7 -0
- data/bin/errors +130 -0
- data/bin/errors_cli/contract_error_demo.rb +49 -0
- data/bin/errors_cli/di_error_demo.rb +38 -0
- data/bin/errors_cli/flow_error_demo.rb +22 -0
- data/bin/errors_cli/flows_router_error_demo.rb +15 -0
- data/bin/errors_cli/oc_error_demo.rb +40 -0
- data/bin/errors_cli/railway_error_demo.rb +10 -0
- data/bin/errors_cli/result_error_demo.rb +13 -0
- data/bin/errors_cli/scp_error_demo.rb +17 -0
- data/docs/.nojekyll +0 -0
- data/docs/README.md +13 -0
- data/docs/_sidebar.md +2 -0
- data/docs/index.html +30 -0
- data/flows.gemspec +27 -2
- data/forspell.dict +17 -0
- data/lefthook.yml +21 -0
- data/lib/flows.rb +13 -5
- data/lib/flows/contract.rb +402 -0
- data/lib/flows/contract/array.rb +55 -0
- data/lib/flows/contract/case_eq.rb +43 -0
- data/lib/flows/contract/compose.rb +77 -0
- data/lib/flows/contract/either.rb +53 -0
- data/lib/flows/contract/error.rb +25 -0
- data/lib/flows/contract/hash.rb +75 -0
- data/lib/flows/contract/hash_of.rb +70 -0
- data/lib/flows/contract/helpers.rb +22 -0
- data/lib/flows/contract/predicate.rb +34 -0
- data/lib/flows/contract/transformer.rb +50 -0
- data/lib/flows/contract/tuple.rb +70 -0
- data/lib/flows/flow.rb +96 -7
- data/lib/flows/flow/errors.rb +29 -0
- data/lib/flows/flow/node.rb +132 -0
- data/lib/flows/flow/router.rb +29 -0
- data/lib/flows/flow/router/custom.rb +59 -0
- data/lib/flows/flow/router/errors.rb +11 -0
- data/lib/flows/flow/router/simple.rb +25 -0
- data/lib/flows/plugin.rb +14 -0
- data/lib/flows/plugin/dependency_injector.rb +159 -0
- data/lib/flows/plugin/dependency_injector/dependency.rb +24 -0
- data/lib/flows/plugin/dependency_injector/dependency_definition.rb +16 -0
- data/lib/flows/plugin/dependency_injector/dependency_list.rb +57 -0
- data/lib/flows/plugin/dependency_injector/errors.rb +58 -0
- data/lib/flows/plugin/implicit_init.rb +45 -0
- data/lib/flows/plugin/output_contract.rb +85 -0
- data/lib/flows/plugin/output_contract/dsl.rb +48 -0
- data/lib/flows/plugin/output_contract/errors.rb +74 -0
- data/lib/flows/plugin/output_contract/wrapper.rb +55 -0
- data/lib/flows/plugin/profiler.rb +114 -0
- data/lib/flows/plugin/profiler/injector.rb +35 -0
- data/lib/flows/plugin/profiler/report.rb +48 -0
- data/lib/flows/plugin/profiler/report/events.rb +43 -0
- data/lib/flows/plugin/profiler/report/flat.rb +41 -0
- data/lib/flows/plugin/profiler/report/flat/method_report.rb +81 -0
- data/lib/flows/plugin/profiler/report/raw.rb +15 -0
- data/lib/flows/plugin/profiler/report/tree.rb +98 -0
- data/lib/flows/plugin/profiler/report/tree/calculated_node.rb +116 -0
- data/lib/flows/plugin/profiler/report/tree/node.rb +35 -0
- data/lib/flows/plugin/profiler/wrapper.rb +53 -0
- data/lib/flows/railway.rb +154 -0
- data/lib/flows/railway/dsl.rb +18 -0
- data/lib/flows/railway/errors.rb +17 -0
- data/lib/flows/railway/step.rb +24 -0
- data/lib/flows/railway/step_list.rb +38 -0
- data/lib/flows/result.rb +189 -2
- data/lib/flows/result/do.rb +172 -0
- data/lib/flows/result/err.rb +12 -6
- data/lib/flows/result/errors.rb +29 -17
- data/lib/flows/result/helpers.rb +25 -3
- data/lib/flows/result/ok.rb +12 -6
- data/lib/flows/shared_context_pipeline.rb +299 -0
- data/lib/flows/shared_context_pipeline/dsl.rb +12 -0
- data/lib/flows/shared_context_pipeline/dsl/callbacks.rb +38 -0
- data/lib/flows/shared_context_pipeline/dsl/tracks.rb +52 -0
- data/lib/flows/shared_context_pipeline/errors.rb +17 -0
- data/lib/flows/shared_context_pipeline/mutation_step.rb +29 -0
- data/lib/flows/shared_context_pipeline/router_definition.rb +21 -0
- data/lib/flows/shared_context_pipeline/step.rb +44 -0
- data/lib/flows/shared_context_pipeline/track.rb +54 -0
- data/lib/flows/shared_context_pipeline/track_list.rb +51 -0
- data/lib/flows/shared_context_pipeline/wrap.rb +74 -0
- data/lib/flows/util.rb +17 -0
- data/lib/flows/util/inheritable_singleton_vars.rb +86 -0
- data/lib/flows/util/inheritable_singleton_vars/dup_strategy.rb +98 -0
- data/lib/flows/util/inheritable_singleton_vars/isolation_strategy.rb +91 -0
- data/lib/flows/util/prepend_to_class.rb +179 -0
- data/lib/flows/version.rb +1 -1
- metadata +288 -20
- data/.travis.yml +0 -8
- data/Gemfile.lock +0 -119
- data/bin/demo +0 -66
- data/bin/examples.rb +0 -159
- data/bin/profile_10steps +0 -64
- data/bin/ruby_benchmarks +0 -26
- data/lib/flows/node.rb +0 -27
- data/lib/flows/operation.rb +0 -54
- data/lib/flows/operation/builder.rb +0 -130
- data/lib/flows/operation/builder/build_router.rb +0 -37
- data/lib/flows/operation/dsl.rb +0 -72
- data/lib/flows/operation/errors.rb +0 -75
- data/lib/flows/operation/executor.rb +0 -78
- data/lib/flows/result_router.rb +0 -14
- data/lib/flows/router.rb +0 -22
@@ -0,0 +1,55 @@
|
|
1
|
+
module Flows
|
2
|
+
class Contract
|
3
|
+
# Makes a contract for Array from contract for array's element.
|
4
|
+
#
|
5
|
+
# If underlying contract has transformation -
|
6
|
+
# each array element will be transformed.
|
7
|
+
#
|
8
|
+
# @example
|
9
|
+
# vector = Flows::Contract::Array.new(Numeric)
|
10
|
+
#
|
11
|
+
# vector === 10
|
12
|
+
# # => false
|
13
|
+
#
|
14
|
+
# vector === [10, 20]
|
15
|
+
# # => true
|
16
|
+
class Array < Contract
|
17
|
+
# Stop search for a new type mismatch in elements
|
18
|
+
# if CHECK_LIMIT errors already found.
|
19
|
+
CHECK_LIMIT = 10
|
20
|
+
|
21
|
+
ARRAY_CONTRACT = CaseEq.new(::Array)
|
22
|
+
|
23
|
+
# @param element_contract [Contract, Object] contract for each element. For not-contract values {CaseEq} used.
|
24
|
+
def initialize(element_contract)
|
25
|
+
@contract = to_contract(element_contract)
|
26
|
+
end
|
27
|
+
|
28
|
+
# @see Contract#check!
|
29
|
+
def check!(other)
|
30
|
+
ARRAY_CONTRACT.check!(other)
|
31
|
+
|
32
|
+
raise Error.new(other, report_errors(other)) unless other.all?(&@contract)
|
33
|
+
|
34
|
+
true
|
35
|
+
end
|
36
|
+
|
37
|
+
# @see Contract#transform!
|
38
|
+
def transform!(other)
|
39
|
+
check!(other)
|
40
|
+
|
41
|
+
other.map { |elem| @contract.transform!(elem) }
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
|
46
|
+
def report_errors(other)
|
47
|
+
other.reject(&@contract)[0..CHECK_LIMIT].map do |elem|
|
48
|
+
element_error = @contract.check(elem).error
|
49
|
+
|
50
|
+
merge_nested_errors("array element `#{elem.inspect}` is invalid:", element_error)
|
51
|
+
end.join("\n")
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
module Flows
|
2
|
+
class Contract
|
3
|
+
# Makes a contract from provided object's case equality check.
|
4
|
+
#
|
5
|
+
# @example String contract
|
6
|
+
# string_check = Flows::Contract::CaseEq.new(String)
|
7
|
+
#
|
8
|
+
# string_check.check(111)
|
9
|
+
# # => Flows::Result::Err.new('must match `String`')
|
10
|
+
#
|
11
|
+
# string_check === 'sdfdsfsd'
|
12
|
+
# # => true
|
13
|
+
#
|
14
|
+
# @example Integer contract with custom error message
|
15
|
+
# int_check = Flows::Contract::CaseEq.new(Integer, 'must be an integer')
|
16
|
+
#
|
17
|
+
# int_check.check('111')
|
18
|
+
# # => Flows::Result::Err.new('must be an integer')
|
19
|
+
#
|
20
|
+
# string_check === 'sdfdsfsd'
|
21
|
+
# # => true
|
22
|
+
class CaseEq < Contract
|
23
|
+
# @param object [#===] object with case equality
|
24
|
+
# @param error_message [String] you may override default error message
|
25
|
+
def initialize(object, error_message = nil)
|
26
|
+
@object = object
|
27
|
+
@error_message = error_message
|
28
|
+
end
|
29
|
+
|
30
|
+
# @see Contract#check!
|
31
|
+
def check!(other)
|
32
|
+
unless @object === other
|
33
|
+
value_error =
|
34
|
+
@error_message ||
|
35
|
+
"must match `#{@object.inspect}`, but has class `#{other.class.inspect}` and value `#{other.inspect}`"
|
36
|
+
raise Error.new(other, value_error)
|
37
|
+
end
|
38
|
+
|
39
|
+
true
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
module Flows
|
2
|
+
class Contract
|
3
|
+
# Allows to combine two or more contracts.
|
4
|
+
#
|
5
|
+
# From type system perspective - this composition is intersection of types.
|
6
|
+
# It means that value passes contract if it passes each particular contract in a composition.
|
7
|
+
#
|
8
|
+
# ## Composition and Transform Laws
|
9
|
+
#
|
10
|
+
# _Golden rule:_ don't use contracts with transformations in composition if you can.
|
11
|
+
# In the most cases you can compose contracts without transformations
|
12
|
+
# and apply one transformation to composite contract.
|
13
|
+
#
|
14
|
+
# Composition of contracts' transformations MUST obey Transform Laws (see {Contract} documentation for details).
|
15
|
+
# To achieve this each particular transform MUST obey following additional laws:
|
16
|
+
#
|
17
|
+
# # let `c` be a contract composition
|
18
|
+
#
|
19
|
+
# # 1. each transform should not leave composite type
|
20
|
+
# #
|
21
|
+
# # for any `x` valid for composite type
|
22
|
+
# c.check!(x) == true
|
23
|
+
# # and for any contract `c_i` from composition:
|
24
|
+
# c.check!(c_i.transform!(x)) == true
|
25
|
+
#
|
26
|
+
# # 2. tranforms can be applied in any order
|
27
|
+
# #
|
28
|
+
# # for any `x` valid for composite type
|
29
|
+
# c.check!(x) == true
|
30
|
+
# # for any two contracts `c_i` and `c_j` from composition:
|
31
|
+
# c_i(c_j(x)) == c_j(c_i(x))
|
32
|
+
#
|
33
|
+
# Why do we need the first law?
|
34
|
+
# To prevent situations when original value matches composite type,
|
35
|
+
# but transformed value doesn't. Example:
|
36
|
+
#
|
37
|
+
# Flows::Contract.make do
|
38
|
+
# compose(
|
39
|
+
# transform(either(String, Symbol), &:to_sym),
|
40
|
+
# String
|
41
|
+
# )
|
42
|
+
# end
|
43
|
+
#
|
44
|
+
# Second laws makes composition of transforms to obey 2nd transform law.
|
45
|
+
# Example of correct composable transforms:
|
46
|
+
#
|
47
|
+
# Flows::Contract.make do
|
48
|
+
# compose(
|
49
|
+
# transform(String, &:strip),
|
50
|
+
# transform(String, &:trim)
|
51
|
+
# )
|
52
|
+
# end
|
53
|
+
#
|
54
|
+
# Formal proof is based on [this theorem proof](https://math.stackexchange.com/questions/600978/equivalence-relation-composition-problem).
|
55
|
+
class Compose < Contract
|
56
|
+
# @param contracts [Array<Contract, Object>] contract list. Non-contract elements will be wrapped with {CaseEq}.
|
57
|
+
def initialize(*contracts)
|
58
|
+
raise 'Contract list must not be empty' if contracts.length.zero?
|
59
|
+
|
60
|
+
@contracts = contracts.map(&method(:to_contract))
|
61
|
+
end
|
62
|
+
|
63
|
+
# @see Contract#check!
|
64
|
+
def check!(other)
|
65
|
+
@contracts.each { |con| con.check!(other) }
|
66
|
+
true
|
67
|
+
end
|
68
|
+
|
69
|
+
# @see Contract#transform!
|
70
|
+
def transform!(other)
|
71
|
+
@contracts.reduce(other) do |value, con|
|
72
|
+
con.transform!(value)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
module Flows
|
2
|
+
class Contract
|
3
|
+
# Allows to combine two or more contracts with "or" logic.
|
4
|
+
#
|
5
|
+
# First matching contract from provided list will be used.
|
6
|
+
#
|
7
|
+
# @example
|
8
|
+
# str_or_sym = Flows::Contract::Either.new(String, Symbol)
|
9
|
+
#
|
10
|
+
# str_or_sym === 'AAA'
|
11
|
+
# # => true
|
12
|
+
#
|
13
|
+
# str_or_sym === :AAA
|
14
|
+
# # => true
|
15
|
+
#
|
16
|
+
# str_or_sym === 111
|
17
|
+
# # => false
|
18
|
+
class Either < Contract
|
19
|
+
# @param contracts [Array<Contract, Object>] contract list. Non-contract elements will be wrapped with {CaseEq}.
|
20
|
+
def initialize(*contracts)
|
21
|
+
raise 'Contract list must not be empty' if contracts.length.zero?
|
22
|
+
|
23
|
+
@contracts = contracts.map(&method(:to_contract))
|
24
|
+
end
|
25
|
+
|
26
|
+
# @see Contract#check!
|
27
|
+
def check!(other)
|
28
|
+
errors = @contracts.each_with_object([]) do |con, errs|
|
29
|
+
result = con.check(other)
|
30
|
+
|
31
|
+
return true if result.ok?
|
32
|
+
|
33
|
+
errs << result.error
|
34
|
+
end
|
35
|
+
|
36
|
+
raise Error.new(other, errors.join("\nOR "))
|
37
|
+
end
|
38
|
+
|
39
|
+
# @see Contract#transform!
|
40
|
+
def transform!(other)
|
41
|
+
errors = @contracts.each_with_object([]) do |con, errs|
|
42
|
+
result = con.transform(other)
|
43
|
+
|
44
|
+
return result.unwrap if result.ok?
|
45
|
+
|
46
|
+
errs << result.error
|
47
|
+
end
|
48
|
+
|
49
|
+
raise Error.new(other, errors.join("\nOR "))
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Flows
|
2
|
+
class Contract
|
3
|
+
# Class for {Type} errors.
|
4
|
+
class Error < ::Flows::Error
|
5
|
+
attr_reader :value
|
6
|
+
attr_reader :value_error
|
7
|
+
|
8
|
+
# @param value [Object] checked value
|
9
|
+
# @param value_error [String] error message
|
10
|
+
def initialize(value, value_error)
|
11
|
+
@value = value
|
12
|
+
@value_error = value_error
|
13
|
+
end
|
14
|
+
|
15
|
+
def message
|
16
|
+
[
|
17
|
+
'type check failed for:',
|
18
|
+
" `#{@value.inspect}`",
|
19
|
+
"---\n",
|
20
|
+
@value_error
|
21
|
+
].join("\n")
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
module Flows
|
2
|
+
class Contract
|
3
|
+
# Contract for Ruby `Hash` with specified contracts for keys and values.
|
4
|
+
#
|
5
|
+
# If key contract has transformation - Hash keys will be transformed.
|
6
|
+
#
|
7
|
+
# If value contract has transformation - Hash values will be transformed.
|
8
|
+
#
|
9
|
+
# @example
|
10
|
+
# sym_int_hash = Flows::Contract::Hash.new(Symbol, Integer)
|
11
|
+
#
|
12
|
+
# sym_int_hash === { a: 1, b: 2 }
|
13
|
+
# # => true
|
14
|
+
#
|
15
|
+
# sym_int_hash === { a: 1, b: 'BBB' }
|
16
|
+
# # => true
|
17
|
+
class Hash < Contract
|
18
|
+
# Stop search for a new type mismatch in keys or values
|
19
|
+
# if CHECK_LIMIT errors already found.
|
20
|
+
#
|
21
|
+
# Applied separately for keys and values.
|
22
|
+
CHECK_LIMIT = 10
|
23
|
+
|
24
|
+
HASH_TYPE = CaseEq.new(::Hash)
|
25
|
+
|
26
|
+
# @param key_contract [Contract, Object] contract for keys, non-contract values will be wrapped with {CaseEq}
|
27
|
+
# @param value_contract [Contract, Object] contract for values, non-contract values will be wrapped with {CaseEq}
|
28
|
+
def initialize(key_contract, value_contract)
|
29
|
+
@key_contract = to_contract(key_contract)
|
30
|
+
@value_contract = to_contract(value_contract)
|
31
|
+
end
|
32
|
+
|
33
|
+
def check!(other)
|
34
|
+
HASH_TYPE.check!(other)
|
35
|
+
|
36
|
+
unless other.keys.all?(&@key_contract) && other.values.all?(&@value_contract)
|
37
|
+
value_error = report_error(other)
|
38
|
+
raise Error.new(other, value_error)
|
39
|
+
end
|
40
|
+
|
41
|
+
true
|
42
|
+
end
|
43
|
+
|
44
|
+
def transform!(other)
|
45
|
+
check!(other)
|
46
|
+
|
47
|
+
other
|
48
|
+
.transform_keys { |key| @key_contract.transform!(key) }
|
49
|
+
.transform_values { |value| @value_contract.transform!(value) }
|
50
|
+
end
|
51
|
+
|
52
|
+
private
|
53
|
+
|
54
|
+
def report_error(other)
|
55
|
+
(invalid_key_errors(other) + invalid_value_errors(other)).join("\n")
|
56
|
+
end
|
57
|
+
|
58
|
+
def invalid_key_errors(other)
|
59
|
+
other.keys.reject(&@key_contract)[0..CHECK_LIMIT].map do |key|
|
60
|
+
key_error = @key_contract.check(key).error
|
61
|
+
|
62
|
+
merge_nested_errors("hash key `#{key.inspect}` is invalid:", key_error)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def invalid_value_errors(other)
|
67
|
+
other.values.reject(&@value_contract)[0..CHECK_LIMIT].map do |value|
|
68
|
+
value_error = @value_contract.check(value).error
|
69
|
+
|
70
|
+
merge_nested_errors("hash value `#{value.inspect}` is invalid:", value_error)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
module Flows
|
2
|
+
class Contract
|
3
|
+
# Contract for Ruby `Hash` with specified structure.
|
4
|
+
#
|
5
|
+
# Hash can have extra keys. Extra keys will be removed after transform.
|
6
|
+
# Underlying contracts' transforms will be applied to correspond values.
|
7
|
+
#
|
8
|
+
# @example
|
9
|
+
# point_type = Flows::Contract::HashOf.new(x: Numeric, y: Numeric)
|
10
|
+
#
|
11
|
+
# point_type === { x: 1, y: 2.0 }
|
12
|
+
# # => true
|
13
|
+
#
|
14
|
+
# point_type === { x: 1, y: 2.0, name: 'Petr' }
|
15
|
+
# # => true
|
16
|
+
#
|
17
|
+
# point_type.cast(x: 1, y: 2.0, name: 'Petr').unwrap
|
18
|
+
# # => { x: 1, y: 2.0 }
|
19
|
+
#
|
20
|
+
# point_type.check({ x: 1, name: 'Petr' })
|
21
|
+
# # => Flows::Result::Error.new('missing key `:y`')
|
22
|
+
#
|
23
|
+
# point_type.check({ x: 1, y: 'Vasya' })
|
24
|
+
# # => Flows::Result::Error.new('key `:y` has an invalid value: must match `Numeric`')
|
25
|
+
class HashOf < Contract
|
26
|
+
HASH_CONTRACT = CaseEq.new(::Hash)
|
27
|
+
|
28
|
+
def initialize(shape = {})
|
29
|
+
@shape = shape.transform_values(&method(:to_contract))
|
30
|
+
end
|
31
|
+
|
32
|
+
def check!(other)
|
33
|
+
HASH_CONTRACT.check!(other)
|
34
|
+
|
35
|
+
errors = check_shape(other)
|
36
|
+
|
37
|
+
raise Error.new(other, errors.join("\n")) if errors.any?
|
38
|
+
|
39
|
+
true
|
40
|
+
end
|
41
|
+
|
42
|
+
def transform!(other)
|
43
|
+
check!(other)
|
44
|
+
|
45
|
+
other
|
46
|
+
.slice(*@shape.keys)
|
47
|
+
.map { |key, value| [key, @shape[key].transform!(value)] }
|
48
|
+
.to_h
|
49
|
+
end
|
50
|
+
|
51
|
+
private
|
52
|
+
|
53
|
+
# :reek:DuplicateMethodCall
|
54
|
+
def check_shape(other)
|
55
|
+
@shape.each_with_object([]) do |(key, type), errors|
|
56
|
+
unless other.key?(key)
|
57
|
+
errors << "missing hash key `#{key.inspect}`"
|
58
|
+
next
|
59
|
+
end
|
60
|
+
|
61
|
+
result = type.check(other[key])
|
62
|
+
|
63
|
+
if result.err?
|
64
|
+
errors << merge_nested_errors("hash key `#{key.inspect}` has an invalid assigned value:", result.error)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require 'forwardable'
|
2
|
+
|
3
|
+
module Flows
|
4
|
+
class Contract
|
5
|
+
# Shortcuts for contract creation.
|
6
|
+
module Helpers
|
7
|
+
extend Forwardable
|
8
|
+
|
9
|
+
def_delegator CaseEq, :new, :case_eq
|
10
|
+
def_delegator Predicate, :new, :predicate
|
11
|
+
|
12
|
+
def_delegator Transformer, :new, :transformer
|
13
|
+
def_delegator Compose, :new, :compose
|
14
|
+
def_delegator Either, :new, :either
|
15
|
+
|
16
|
+
def_delegator Flows::Contract::Hash, :new, :hash
|
17
|
+
def_delegator HashOf, :new, :hash_of
|
18
|
+
def_delegator Flows::Contract::Array, :new, :array
|
19
|
+
def_delegator Tuple, :new, :tuple
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module Flows
|
2
|
+
class Contract
|
3
|
+
# Makes a contract from 1-argument lambda.
|
4
|
+
#
|
5
|
+
# Such lambdas works like [predicates](https://en.wikipedia.org/wiki/Predicate_(mathematical_logic)).
|
6
|
+
#
|
7
|
+
# @example
|
8
|
+
# positive_check = Flows::Contract::Predicate.new 'must be a positive integer' do |x|
|
9
|
+
# x.is_a?(Integer) && x > 0
|
10
|
+
# end
|
11
|
+
#
|
12
|
+
# positive_check === 10
|
13
|
+
# # => true
|
14
|
+
#
|
15
|
+
# positive_check === -100
|
16
|
+
# # => false
|
17
|
+
class Predicate < Contract
|
18
|
+
# @param error_message error message if check fails
|
19
|
+
# @yield [object] lambda to wrap into a contract
|
20
|
+
# @yieldreturn [Boolean] lambda should return a boolean
|
21
|
+
def initialize(error_message, &block)
|
22
|
+
@error_message = error_message
|
23
|
+
@block = block
|
24
|
+
end
|
25
|
+
|
26
|
+
# @see Contract#check!
|
27
|
+
def check!(other)
|
28
|
+
raise Error.new(other, @error_message) unless @block === other
|
29
|
+
|
30
|
+
true
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|