flows 0.2.0 → 0.6.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/.github/workflows/{build.yml → test.yml} +5 -10
- data/.gitignore +9 -1
- data/.mdlrc +1 -1
- data/.reek.yml +54 -0
- data/.rubocop.yml +26 -7
- data/.rubocop_todo.yml +27 -0
- data/.ruby-version +1 -1
- data/.yardopts +1 -0
- data/CHANGELOG.md +81 -0
- data/Gemfile +0 -6
- data/README.md +167 -363
- data/Rakefile +35 -1
- data/bin/.rubocop.yml +5 -0
- data/bin/all_the_errors +55 -0
- data/bin/benchmark +73 -105
- 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 +22 -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 +138 -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/interface_error_demo.rb +17 -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/README.md +3 -187
- data/docs/_sidebar.md +0 -24
- data/docs/index.html +1 -1
- data/flows.gemspec +27 -2
- data/forspell.dict +9 -0
- data/lefthook.yml +9 -0
- data/lib/flows.rb +11 -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 +24 -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 +15 -0
- data/lib/flows/plugin/dependency_injector.rb +170 -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 +55 -0
- data/lib/flows/plugin/dependency_injector/errors.rb +58 -0
- data/lib/flows/plugin/implicit_init.rb +45 -0
- data/lib/flows/plugin/interface.rb +84 -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 +80 -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 +34 -0
- data/lib/flows/plugin/profiler/wrapper.rb +53 -0
- data/lib/flows/railway.rb +140 -34
- data/lib/flows/railway/dsl.rb +8 -18
- data/lib/flows/railway/errors.rb +8 -12
- data/lib/flows/railway/step.rb +24 -0
- data/lib/flows/railway/step_list.rb +38 -0
- data/lib/flows/result.rb +188 -2
- data/lib/flows/result/do.rb +158 -16
- 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 +342 -0
- data/lib/flows/shared_context_pipeline/dsl.rb +12 -0
- data/lib/flows/shared_context_pipeline/dsl/callbacks.rb +35 -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 +30 -0
- data/lib/flows/shared_context_pipeline/router_definition.rb +21 -0
- data/lib/flows/shared_context_pipeline/step.rb +55 -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 +73 -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 +100 -0
- data/lib/flows/util/inheritable_singleton_vars/isolation_strategy.rb +91 -0
- data/lib/flows/util/prepend_to_class.rb +191 -0
- data/lib/flows/version.rb +1 -1
- metadata +253 -38
- data/Gemfile.lock +0 -174
- data/bin/demo +0 -66
- data/bin/examples.rb +0 -195
- data/bin/profile_10steps +0 -106
- data/bin/ruby_benchmarks +0 -26
- data/docs/CNAME +0 -1
- data/docs/contributing/benchmarks_profiling.md +0 -3
- data/docs/contributing/local_development.md +0 -3
- data/docs/flow/direct_usage.md +0 -3
- data/docs/flow/general_idea.md +0 -3
- data/docs/operation/basic_usage.md +0 -1
- data/docs/operation/inject_steps.md +0 -3
- data/docs/operation/lambda_steps.md +0 -3
- data/docs/operation/result_shapes.md +0 -3
- data/docs/operation/routing_tracks.md +0 -3
- data/docs/operation/wrapping_steps.md +0 -3
- data/docs/overview/performance.md +0 -336
- data/docs/railway/basic_usage.md +0 -232
- data/docs/result_objects/basic_usage.md +0 -196
- data/docs/result_objects/do_notation.md +0 -139
- data/lib/flows/node.rb +0 -27
- data/lib/flows/operation.rb +0 -52
- 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 -93
- data/lib/flows/operation/errors.rb +0 -75
- data/lib/flows/operation/executor.rb +0 -78
- data/lib/flows/railway/builder.rb +0 -68
- data/lib/flows/railway/executor.rb +0 -23
- data/lib/flows/result_router.rb +0 -14
- data/lib/flows/router.rb +0 -22
@@ -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,24 @@
|
|
1
|
+
module Flows
|
2
|
+
class Contract
|
3
|
+
# Class for {Type} errors.
|
4
|
+
class Error < ::Flows::Error
|
5
|
+
attr_reader :value, :value_error
|
6
|
+
|
7
|
+
# @param value [Object] checked value
|
8
|
+
# @param value_error [String] error message
|
9
|
+
def initialize(value, value_error)
|
10
|
+
@value = value
|
11
|
+
@value_error = value_error
|
12
|
+
end
|
13
|
+
|
14
|
+
def message
|
15
|
+
[
|
16
|
+
'type check failed for:',
|
17
|
+
" `#{@value.inspect}`",
|
18
|
+
"---\n",
|
19
|
+
@value_error
|
20
|
+
].join("\n")
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
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
|
@@ -0,0 +1,50 @@
|
|
1
|
+
module Flows
|
2
|
+
class Contract
|
3
|
+
# Adds transformation to an existing contract.
|
4
|
+
#
|
5
|
+
# If original contract already has a transform -
|
6
|
+
# final transformation will be composition of original and new one.
|
7
|
+
#
|
8
|
+
# You MUST obey Transformation Laws (see {Contract} documentation for details).
|
9
|
+
#
|
10
|
+
# @example Upcase strings
|
11
|
+
# up_str = Flows::Contract::Transformer.new(String) { |str| str.upcase }
|
12
|
+
#
|
13
|
+
# up_str.transform!('megatron')
|
14
|
+
# # => 'MEGATRON'
|
15
|
+
#
|
16
|
+
# up_str.transform(:megatron).error
|
17
|
+
# # => 'must match `String`'
|
18
|
+
#
|
19
|
+
# @example Strip and upcase strings
|
20
|
+
# strip_str = Flows::Contract::Transformer.new(String, &:strip)
|
21
|
+
# up_stip_str = Flows::Contract::Transformer.new(strip_str, &:upcase)
|
22
|
+
#
|
23
|
+
# up_str.transform!(' megatron ')
|
24
|
+
# # => 'MEGATRON'
|
25
|
+
#
|
26
|
+
# up_str.cast(:megatron).error
|
27
|
+
# # => 'must match `String`'
|
28
|
+
class Transformer < Contract
|
29
|
+
# @param contract [Contract, Object] in case of non-contract argument {CaseEq} is automatically applied.
|
30
|
+
# @yield [object] transform implementation
|
31
|
+
# @yieldreturn [object] result of transform. Must obey transformation laws.
|
32
|
+
def initialize(contract, &transform_proc)
|
33
|
+
@contract = to_contract(contract)
|
34
|
+
@transform = transform_proc
|
35
|
+
end
|
36
|
+
|
37
|
+
# @see Contract#check!
|
38
|
+
def check!(other)
|
39
|
+
@contract.check!(other)
|
40
|
+
end
|
41
|
+
|
42
|
+
# @see Contract#transform!
|
43
|
+
def transform!(other)
|
44
|
+
@transform.call(
|
45
|
+
@contract.transform!(other)
|
46
|
+
)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|