blood_contracts-ext 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/.gitignore +12 -0
- data/.rspec +3 -0
- data/.rubocop.yml +31 -0
- data/.travis.yml +19 -0
- data/CHANGELOG.md +23 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +4 -0
- data/LICENSE +21 -0
- data/README.md +369 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/blood_contracts-ext.gemspec +27 -0
- data/lib/blood_contracts/core/defineable_error.rb +61 -0
- data/lib/blood_contracts/core/exception_caught.rb +33 -0
- data/lib/blood_contracts/core/exception_handling.rb +36 -0
- data/lib/blood_contracts/core/expected_error.rb +16 -0
- data/lib/blood_contracts/core/extractable.rb +85 -0
- data/lib/blood_contracts/core/map_value.rb +45 -0
- data/lib/blood_contracts/core/policy_failure.rb +42 -0
- data/lib/blood_contracts/core/sum_policy_failure.rb +9 -0
- data/lib/blood_contracts/core/tuple_policy_failure.rb +39 -0
- data/lib/blood_contracts/ext/pipe.rb +27 -0
- data/lib/blood_contracts/ext/refined.rb +59 -0
- data/lib/blood_contracts/ext/sum.rb +29 -0
- data/lib/blood_contracts/ext/tuple.rb +28 -0
- data/lib/blood_contracts/ext.rb +28 -0
- data/spec/blood_contracts/ext/exception_caught_spec.rb +50 -0
- data/spec/blood_contracts/ext/expected_error_spec.rb +56 -0
- data/spec/blood_contracts/ext/map_value_spec.rb +54 -0
- data/spec/blood_contracts/ext/policy_failure_spec.rb +151 -0
- data/spec/blood_contracts/ext/policy_spec.rb +138 -0
- data/spec/fixtures/en.yml +19 -0
- data/spec/spec_helper.rb +24 -0
- data/spec/support/fixtures_helper.rb +11 -0
- metadata +202 -0
@@ -0,0 +1,61 @@
|
|
1
|
+
module BloodContracts::Core
|
2
|
+
# Meta class to define local errors in form of Tram::Policy::Errors
|
3
|
+
module DefineableError
|
4
|
+
# Concern with the helper to define custom Tram::Policy::Errors
|
5
|
+
module Concern
|
6
|
+
# @private
|
7
|
+
def inherited(other)
|
8
|
+
super
|
9
|
+
other.instance_variable_set(:@policy_scope, @policy_scope)
|
10
|
+
end
|
11
|
+
|
12
|
+
# Method that turns message into Tram::Policy::Errors object
|
13
|
+
#
|
14
|
+
# @param message [String, Symbol] (or translations key) for your
|
15
|
+
# custom error
|
16
|
+
# @option tags [Hash] additional context for translations
|
17
|
+
# @option sub_scope [Symbol] is a customizable path to your
|
18
|
+
# translation
|
19
|
+
# @return [Tram::Policy::Error]
|
20
|
+
#
|
21
|
+
def define_error(message, tags: {}, sub_scope: nil)
|
22
|
+
errors = Tram::Policy::Errors.new(scope: @policy_scope)
|
23
|
+
sub_scope = underscore(sub_scope || name)
|
24
|
+
message = [sub_scope, message].join(".").to_sym if message.is_a?(Symbol)
|
25
|
+
errors.add message, **tags
|
26
|
+
errors
|
27
|
+
end
|
28
|
+
|
29
|
+
# @private
|
30
|
+
private def underscore(string)
|
31
|
+
string.gsub(/([A-Z]+)([A-Z])/, '\1_\2')
|
32
|
+
.gsub(/([a-z])([A-Z])/, '\1_\2')
|
33
|
+
.gsub("__", "/")
|
34
|
+
.gsub("::", "/")
|
35
|
+
.gsub(/\s+/, "") # spaces are bad form
|
36
|
+
.gsub(/[?%*:|"<>.]+/, "") # reserved characters
|
37
|
+
.downcase
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
class << self
|
42
|
+
# Method that creates meta class for defining custom Tram::Policy::Errors
|
43
|
+
#
|
44
|
+
# @param policy_scope [Symbol] is a root for your I18n translations
|
45
|
+
# @return [Module]
|
46
|
+
#
|
47
|
+
def new(policy_scope)
|
48
|
+
m = Module.new do
|
49
|
+
def self.extended(other)
|
50
|
+
other.instance_variable_set(
|
51
|
+
:@policy_scope, instance_variable_get(:@policy_scope)
|
52
|
+
)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
m.include(Concern)
|
56
|
+
m.instance_variable_set(:@policy_scope, policy_scope)
|
57
|
+
m
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module BloodContracts::Core
|
2
|
+
# Refinement type which holds exception as a value
|
3
|
+
class ExceptionCaught < ContractFailure
|
4
|
+
# Constructs refinement type around exception
|
5
|
+
#
|
6
|
+
# @param value [Exception] value which is wrapped inside the type
|
7
|
+
# @option context [Hash] shared context of types matching pipeline
|
8
|
+
#
|
9
|
+
def initialize(value = nil, context: Hash.new { |h, k| h[k] = {} }, **)
|
10
|
+
@errors = []
|
11
|
+
@context = context
|
12
|
+
@value = value
|
13
|
+
@context[:exception] = value
|
14
|
+
end
|
15
|
+
|
16
|
+
# Predicate, whether the data is valid or not
|
17
|
+
# (for the ExceptionCaught it is always False)
|
18
|
+
#
|
19
|
+
# @return [Boolean]
|
20
|
+
#
|
21
|
+
def valid?
|
22
|
+
false
|
23
|
+
end
|
24
|
+
|
25
|
+
# Reader for the exception caught
|
26
|
+
#
|
27
|
+
# @return [Exception]
|
28
|
+
#
|
29
|
+
def exception
|
30
|
+
@context[:exception]
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module BloodContracts::Core
|
2
|
+
# Concern to wrap matching process with exception handling
|
3
|
+
#
|
4
|
+
# @example Defines a type with automatic exception handling in form of types
|
5
|
+
# class JsonType < ::BC::Refined
|
6
|
+
# prepend ExceptionHandling
|
7
|
+
#
|
8
|
+
# def match
|
9
|
+
# @context[:parsed_json] = JSON.parse(value)
|
10
|
+
# self
|
11
|
+
# end
|
12
|
+
# end
|
13
|
+
#
|
14
|
+
module ExceptionHandling
|
15
|
+
# Runs the matching process and returns an ExceptionCaught if
|
16
|
+
# StandardError happened inside match call
|
17
|
+
#
|
18
|
+
# @return [Refined]
|
19
|
+
#
|
20
|
+
def match
|
21
|
+
super
|
22
|
+
rescue StandardError => ex
|
23
|
+
exception(ex)
|
24
|
+
end
|
25
|
+
|
26
|
+
# Wraps the exception in refinement type
|
27
|
+
#
|
28
|
+
# @param exc [Exception] raised exception
|
29
|
+
# @option context [Hash] shared context of matching pipeline
|
30
|
+
# @return [ExceptionCaught]
|
31
|
+
#
|
32
|
+
def exception(exc, context: @context)
|
33
|
+
ExceptionCaught.new(exc, context: context)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module BloodContracts::Core
|
2
|
+
# Custom refinement type that converts the extracted data into
|
3
|
+
# Tram::Policy::Errors, could by used when the case is an error but
|
4
|
+
# you know how to deal with it inside application
|
5
|
+
class ExpectedError < Ext::Refined
|
6
|
+
# Generates an Tram::Policy::Errors message using the matching context
|
7
|
+
#
|
8
|
+
# @return [Tram::Policy::Errors]
|
9
|
+
def mapped
|
10
|
+
keys = self.class.extractors.keys
|
11
|
+
tags = Hash[keys.zip(@context.values_at(*keys))]
|
12
|
+
tags = @context if tags.empty?
|
13
|
+
self.class.define_error(:message, tags: tags)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,85 @@
|
|
1
|
+
module BloodContracts::Core
|
2
|
+
# Concern that turns your refinement type into a coercer with validation
|
3
|
+
# delegated to Tram::Policy
|
4
|
+
module Extractable
|
5
|
+
# @private
|
6
|
+
def self.included(other_class)
|
7
|
+
other_class.extend(ClassMethods)
|
8
|
+
end
|
9
|
+
|
10
|
+
# DSL definition
|
11
|
+
module ClassMethods
|
12
|
+
# Configuration about how to extract data from the value, just
|
13
|
+
# list of context keys and methods
|
14
|
+
attr_reader :extractors
|
15
|
+
|
16
|
+
# Tram::Policy ancestor that will be used for validation
|
17
|
+
#
|
18
|
+
# @param [Class]
|
19
|
+
#
|
20
|
+
attr_accessor :policy
|
21
|
+
|
22
|
+
# @private
|
23
|
+
def inherited(child)
|
24
|
+
super
|
25
|
+
child.instance_variable_set(:@extractors, {})
|
26
|
+
end
|
27
|
+
|
28
|
+
# DSL to define which method to use to extract data from the value
|
29
|
+
#
|
30
|
+
# @param extractor_name [Symbol] key to store the extracted data in the
|
31
|
+
# context
|
32
|
+
# @option method_name [Symbol] custom method name to use for extraction
|
33
|
+
# @return [Nothing]
|
34
|
+
#
|
35
|
+
def extract(extractor_name, method_name: extractor_name)
|
36
|
+
extractors[extractor_name] = [method_name]
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
# Turns matching process into 2 steps:
|
41
|
+
# - extraction of data from the value
|
42
|
+
# - validation using the policy_klass
|
43
|
+
#
|
44
|
+
# @return [Refined]
|
45
|
+
def match
|
46
|
+
extract!
|
47
|
+
policy_failure_match! || self
|
48
|
+
end
|
49
|
+
|
50
|
+
# Turns value into the hash of extracted data
|
51
|
+
#
|
52
|
+
# @return [Hash]
|
53
|
+
def mapped
|
54
|
+
keys = self.class.extractors.keys
|
55
|
+
Hash[keys.zip(@context.values_at(*keys))]
|
56
|
+
end
|
57
|
+
|
58
|
+
# Extracts data from the value
|
59
|
+
#
|
60
|
+
# @return [Nothing]
|
61
|
+
#
|
62
|
+
protected def extract!
|
63
|
+
self.class.extractors.each do |field, settings|
|
64
|
+
next if !context[field].nil? && !context[field].empty?
|
65
|
+
|
66
|
+
method_name, = *settings
|
67
|
+
context[field] = send(method_name.to_s)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
# Validates extracted data using policy_klass
|
72
|
+
#
|
73
|
+
# @return [Refined, Nil]
|
74
|
+
#
|
75
|
+
protected def policy_failure_match!
|
76
|
+
return unless self.class.policy
|
77
|
+
|
78
|
+
policy_input = context.reduce({}) { |a, (k, v)| a.merge!(k.to_sym => v) }
|
79
|
+
policy_instance = self.class.policy[**policy_input]
|
80
|
+
return if policy_instance.valid?
|
81
|
+
|
82
|
+
failure(policy_instance.errors)
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
module BloodContracts::Core
|
2
|
+
# Mapper in form of refinement type, transforms the value using mapper_klass
|
3
|
+
class MapValue < Refined
|
4
|
+
class << self
|
5
|
+
# Any callable object which you prefer to turn value into other form
|
6
|
+
#
|
7
|
+
# @param [Class, #call]
|
8
|
+
# @return [Class]
|
9
|
+
#
|
10
|
+
attr_accessor :mapper_klass
|
11
|
+
|
12
|
+
# Generates meta-class with predefined mapper_klass
|
13
|
+
#
|
14
|
+
# @param mapper_klass [Class, callable] callable object that will
|
15
|
+
# transform the value
|
16
|
+
# @return [MapValue]
|
17
|
+
#
|
18
|
+
def with(mapper_klass)
|
19
|
+
type = Class.new(self)
|
20
|
+
type.mapper_klass = mapper_klass
|
21
|
+
type
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
# Always successful matching process which transforms the value and
|
26
|
+
# store it in the context
|
27
|
+
#
|
28
|
+
# @return [Refined]
|
29
|
+
#
|
30
|
+
def match
|
31
|
+
context[:mapper_input] = value
|
32
|
+
context[:mapped_value] =
|
33
|
+
self.class.mapper_klass.call(**context[:mapper_input])
|
34
|
+
self
|
35
|
+
end
|
36
|
+
|
37
|
+
# Mapped representation of the value
|
38
|
+
#
|
39
|
+
# @return [Object]
|
40
|
+
#
|
41
|
+
def mapped
|
42
|
+
match.context[:mapped_value]
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
module BloodContracts::Core
|
2
|
+
# ContractFailure which holds errors in form of Tram::Policy::Errors
|
3
|
+
class PolicyFailure < ContractFailure
|
4
|
+
# Extends the type with ability to generate custom errors, to wrap and
|
5
|
+
# error message into Tram::Policy::Errors
|
6
|
+
extend DefineableError.new(:contracts)
|
7
|
+
|
8
|
+
# Builds an PolicyFailure, turns the errors into Tram::Policy::Errors
|
9
|
+
# if they are not, yet
|
10
|
+
#
|
11
|
+
# @param errors_per_type [Hash<Refined, Array<String,Symbol>>] map of
|
12
|
+
# errors per type, each type could have a list of errors
|
13
|
+
# @option context [Hash] shared context of matching pipeline
|
14
|
+
#
|
15
|
+
def initialize(errors_per_type = nil, context: {}, **)
|
16
|
+
sub_scope = context.delete(:sub_scope)
|
17
|
+
errors_per_type.to_h.transform_values! do |errors|
|
18
|
+
errors.map do |error|
|
19
|
+
next(error) if error.is_a?(Tram::Policy::Errors)
|
20
|
+
self.class.define_error(error, tags: context, sub_scope: sub_scope)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
super
|
24
|
+
end
|
25
|
+
|
26
|
+
# Merged list of Tram::Policy::Errors after the matching run
|
27
|
+
#
|
28
|
+
# @return [Array<Tram::Policy::Errors>]
|
29
|
+
#
|
30
|
+
def policy_errors
|
31
|
+
@policy_errors ||= @value.values.flatten
|
32
|
+
end
|
33
|
+
|
34
|
+
# Merged list of Tram::Policy::Errors messages (or their translations)
|
35
|
+
#
|
36
|
+
# @return [Array<String>]
|
37
|
+
#
|
38
|
+
def messages
|
39
|
+
@messages ||= policy_errors.map(&:messages).flatten
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,9 @@
|
|
1
|
+
module BloodContracts::Core
|
2
|
+
# Represents failure in Sum data matching
|
3
|
+
class SumPolicyFailure < PolicyFailure
|
4
|
+
# Custom accessor for policy errors in case of Ext::Sum types composition
|
5
|
+
def policy_errors
|
6
|
+
@policy_errors ||= @context[:sum_errors].map(&:policy_errors).flatten
|
7
|
+
end
|
8
|
+
end
|
9
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module BloodContracts::Core
|
2
|
+
# Represents failure in Tuple data matching
|
3
|
+
class TuplePolicyFailure < PolicyFailure
|
4
|
+
# Hash of attributes (name & type pairs)
|
5
|
+
#
|
6
|
+
# @return [Hash<String, Refined>]
|
7
|
+
#
|
8
|
+
def attributes
|
9
|
+
@context[:attributes]
|
10
|
+
end
|
11
|
+
|
12
|
+
# Subset of attributes which are invalid
|
13
|
+
#
|
14
|
+
# @return [Hash<String, PolicyFailure>]
|
15
|
+
#
|
16
|
+
def attribute_errors
|
17
|
+
attributes.select { |_name, type| type.invalid? }
|
18
|
+
end
|
19
|
+
|
20
|
+
# Subset of attributes which are invalid
|
21
|
+
#
|
22
|
+
# @return [Hash<String, PolicyFailure>]
|
23
|
+
#
|
24
|
+
def attribute_messages
|
25
|
+
attribute_errors.transform_values!(&:messages)
|
26
|
+
end
|
27
|
+
|
28
|
+
# Unpacked matching errors in form of a hash per attribute
|
29
|
+
#
|
30
|
+
# @return [Hash<String, PolicyFailure>]
|
31
|
+
#
|
32
|
+
def unpack_h
|
33
|
+
@unpack_h ||= attribute_errors.transform_values(&:unpack)
|
34
|
+
end
|
35
|
+
alias to_hash unpack_h
|
36
|
+
alias to_h unpack_h
|
37
|
+
alias unpack_attributes unpack_h
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module BloodContracts::Core
|
2
|
+
module Ext
|
3
|
+
# Refinement types representation of Sum types composition, extended version
|
4
|
+
class Pipe < ::BC::Pipe
|
5
|
+
# Sets the default failure_klass to PolicyFailure, to use
|
6
|
+
# Tram::Policy::Errors for errors
|
7
|
+
self.failure_klass = PolicyFailure
|
8
|
+
|
9
|
+
# @private
|
10
|
+
def self.inherited(new_klass)
|
11
|
+
new_klass.failure_klass ||= failure_klass
|
12
|
+
super
|
13
|
+
end
|
14
|
+
|
15
|
+
# Generate an PolicyFailure from the error, also stores the
|
16
|
+
# additional scope for Tram::Policy::Errors in the context
|
17
|
+
#
|
18
|
+
# @param (see BC::Refined#failure)
|
19
|
+
# @return [PolicyFailure]
|
20
|
+
#
|
21
|
+
def failure(*, **)
|
22
|
+
@context[:sub_scope] = self.class.name
|
23
|
+
super
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
module BloodContracts::Core
|
2
|
+
module Ext
|
3
|
+
# Refinement type exteneded with Extractable, ExceptionHandling and
|
4
|
+
# errors representation in form of Tram::Policy::Errors
|
5
|
+
class Refined < ::BC::Refined
|
6
|
+
# Adds ability to generate custom errors in form of Tram::Policy::Errors
|
7
|
+
extend DefineableError.new(:contracts)
|
8
|
+
|
9
|
+
# Adds extractors DSL
|
10
|
+
include Extractable
|
11
|
+
|
12
|
+
# Adds exception handling in form of refinment type
|
13
|
+
prepend ExceptionHandling
|
14
|
+
|
15
|
+
# Sets the default failure_klass to PolicyFailure, to use
|
16
|
+
# Tram::Policy::Errors for errors
|
17
|
+
self.failure_klass = PolicyFailure
|
18
|
+
|
19
|
+
class << self
|
20
|
+
# Compose types in a Sum check
|
21
|
+
# Sum passes data from type to type in parallel, only one type
|
22
|
+
# have to match
|
23
|
+
#
|
24
|
+
# @return [BC::Sum]
|
25
|
+
#
|
26
|
+
def or_a(other_type)
|
27
|
+
BC::Ext::Sum.new(self, other_type)
|
28
|
+
end
|
29
|
+
|
30
|
+
# Compose types in a Pipe check
|
31
|
+
# Pipe passes data from type to type sequentially
|
32
|
+
#
|
33
|
+
# @return [BC::Pipe]
|
34
|
+
#
|
35
|
+
def and_then(other_type)
|
36
|
+
BC::Ext::Pipe.new(self, other_type)
|
37
|
+
end
|
38
|
+
|
39
|
+
# @private
|
40
|
+
def inherited(new_klass)
|
41
|
+
new_klass.failure_klass ||= failure_klass
|
42
|
+
new_klass.prepend ExceptionHandling
|
43
|
+
super
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
# Generate an PolicyFailure from the error, also stores the
|
48
|
+
# additional scope for Tram::Policy::Errors in the context
|
49
|
+
#
|
50
|
+
# @param (see BC::Refined#failure)
|
51
|
+
# @return [PolicyFailure]
|
52
|
+
#
|
53
|
+
def failure(*, **)
|
54
|
+
@context[:sub_scope] = self.class.name
|
55
|
+
super
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module BloodContracts::Core
|
2
|
+
module Ext
|
3
|
+
# Refinement types representation of Sum types composition, extended version
|
4
|
+
class Sum < ::BC::Sum
|
5
|
+
# Sets the default failure_klass to PolicyFailure, to use
|
6
|
+
# Tram::Policy::Errors for errors
|
7
|
+
self.failure_klass = SumPolicyFailure
|
8
|
+
|
9
|
+
# @private
|
10
|
+
def self.inherited(new_klass)
|
11
|
+
new_klass.failure_klass ||= failure_klass
|
12
|
+
super
|
13
|
+
end
|
14
|
+
|
15
|
+
# Generate an PolicyFailure from the error, also stores the
|
16
|
+
# additional scope for Tram::Policy::Errors in the context.
|
17
|
+
# Also saves the Sum errors in the context
|
18
|
+
#
|
19
|
+
# @param (see BC::Refined#failure)
|
20
|
+
# @return [PolicyFailure]
|
21
|
+
#
|
22
|
+
def failure(*, **)
|
23
|
+
@context[:sum_errors] = @or_matches
|
24
|
+
@context[:sub_scope] = self.class.name
|
25
|
+
super
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module BloodContracts::Core
|
2
|
+
module Ext
|
3
|
+
# Refinement types representation of Sum types composition, extended version
|
4
|
+
class Tuple < ::BC::Tuple
|
5
|
+
# Sets the default failure_klass to PolicyFailure, to use
|
6
|
+
# Tram::Policy::Errors for errors
|
7
|
+
self.failure_klass = TuplePolicyFailure
|
8
|
+
|
9
|
+
# @private
|
10
|
+
def self.inherited(new_klass)
|
11
|
+
new_klass.failure_klass ||= failure_klass
|
12
|
+
super
|
13
|
+
end
|
14
|
+
|
15
|
+
# Generate an PolicyFailure from the error, also stores the
|
16
|
+
# additional scope for Tram::Policy::Errors in the context.
|
17
|
+
# Also saves the Tuple error in the context[:attributes] by the :base key
|
18
|
+
#
|
19
|
+
# @param (see BC::Refined#failure)
|
20
|
+
# @return [PolicyFailure]
|
21
|
+
#
|
22
|
+
def failure(*, **)
|
23
|
+
@context[:sub_scope] = self.class.name
|
24
|
+
@context[:attributes].store(:base, super)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require "blood_contracts/core"
|
2
|
+
require "tram-policy"
|
3
|
+
|
4
|
+
# Top-level scope for BloodContracts toolset
|
5
|
+
module BloodContracts
|
6
|
+
# Scope for refinement types & helpers for them
|
7
|
+
module Core
|
8
|
+
require_relative "core/defineable_error.rb"
|
9
|
+
|
10
|
+
require_relative "core/policy_failure.rb"
|
11
|
+
require_relative "core/tuple_policy_failure.rb"
|
12
|
+
require_relative "core/sum_policy_failure.rb"
|
13
|
+
require_relative "core/exception_caught.rb"
|
14
|
+
require_relative "core/exception_handling.rb"
|
15
|
+
require_relative "core/extractable.rb"
|
16
|
+
|
17
|
+
# Scope for extended refinement types
|
18
|
+
module Ext
|
19
|
+
require_relative "ext/refined.rb"
|
20
|
+
require_relative "ext/sum.rb"
|
21
|
+
require_relative "ext/pipe.rb"
|
22
|
+
require_relative "ext/tuple.rb"
|
23
|
+
end
|
24
|
+
|
25
|
+
require_relative "core/expected_error.rb"
|
26
|
+
require_relative "core/map_value.rb"
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
RSpec.describe BloodContracts::Core::ExceptionCaught do
|
2
|
+
before do
|
3
|
+
module Test
|
4
|
+
class JsonType < BC::Refined
|
5
|
+
prepend BC::ExceptionHandling
|
6
|
+
|
7
|
+
def match
|
8
|
+
@context[:json_type_input] = value
|
9
|
+
@context[:parsed_json] = JSON.parse(@context[:json_type_input])
|
10
|
+
self
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
subject { Test::JsonType.match(value) }
|
17
|
+
|
18
|
+
context "when value is valid" do
|
19
|
+
let(:value) { '{"some": "thing"}' }
|
20
|
+
let(:payload) { { "some" => "thing" } }
|
21
|
+
|
22
|
+
it do
|
23
|
+
is_expected.to be_valid
|
24
|
+
expect(subject.context[:json_type_input]).to eq(value)
|
25
|
+
expect(subject.context[:parsed_json]).to match(payload)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
context "when value is invalid" do
|
30
|
+
context "when value is a String" do
|
31
|
+
let(:value) { "nope, I'm not a JSON" }
|
32
|
+
|
33
|
+
it do
|
34
|
+
is_expected.to be_invalid
|
35
|
+
expect(subject.context[:json_type_input]).to eq(value)
|
36
|
+
expect(subject.exception).to match(kind_of(JSON::ParserError))
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
context "when value is a arbitrary object" do
|
41
|
+
let(:value) { Class.new }
|
42
|
+
|
43
|
+
it do
|
44
|
+
is_expected.to be_invalid
|
45
|
+
expect(subject.context[:json_type_input]).to eq(value)
|
46
|
+
expect(subject.exception).to match(kind_of(TypeError))
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
RSpec.describe BloodContracts::Core::ExpectedError do
|
2
|
+
before do
|
3
|
+
module Test
|
4
|
+
class PlainTextError < BC::ExpectedError
|
5
|
+
def match
|
6
|
+
@context[:parsed] ||= JSON.parse(value)
|
7
|
+
rescue JSON::ParserError
|
8
|
+
@context[:plain_text] = value.to_s
|
9
|
+
self
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
class JsonType < BC::Ext::Refined
|
14
|
+
def match
|
15
|
+
@context[:parsed] ||= JSON.parse(value)
|
16
|
+
self
|
17
|
+
end
|
18
|
+
|
19
|
+
def mapped
|
20
|
+
@context[:parsed]
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
Response = JsonType.or_a(PlainTextError)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
subject { Test::Response.match(value) }
|
29
|
+
|
30
|
+
context "when value is a JSON" do
|
31
|
+
let(:value) { '{"name": "Andrew", "registered_at": "2019-01-01"}' }
|
32
|
+
let(:payload) do
|
33
|
+
{ "name" => "Andrew", "registered_at" => "2019-01-01" }
|
34
|
+
end
|
35
|
+
|
36
|
+
it do
|
37
|
+
is_expected.to be_valid
|
38
|
+
is_expected.to match(kind_of(Test::JsonType))
|
39
|
+
expect(subject.unpack).to match(payload)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
context "when value is a plain text" do
|
44
|
+
let(:value) { "Nothing found!" }
|
45
|
+
let(:error_messages) do
|
46
|
+
["Service responded with a message: `#{value}`"]
|
47
|
+
end
|
48
|
+
|
49
|
+
it do
|
50
|
+
is_expected.to be_valid
|
51
|
+
expect(subject).to match(kind_of(Test::PlainTextError))
|
52
|
+
expect(subject.unpack).to match(kind_of(Tram::Policy::Errors))
|
53
|
+
expect(subject.unpack.messages).to match(error_messages)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|