blood_contracts-ext 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|