blood_contracts-core 0.1.0 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/bin/console +4 -4
- data/blood_contracts-core.gemspec +1 -2
- data/examples/json_response.rb +52 -31
- data/lib/blood_contracts/core/pipe.rb +72 -0
- data/lib/blood_contracts/core/refined.rb +126 -0
- data/lib/blood_contracts/core/sum.rb +48 -0
- data/lib/blood_contracts/core/tuple.rb +53 -0
- data/lib/blood_contracts/core/version.rb +1 -1
- data/lib/blood_contracts/core.rb +6 -23
- metadata +13 -10
- data/lib/blood_contracts/core/type.rb +0 -197
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: bc10cd9d71e3f255a6e3e520acb9fa39e307308a97b227107a0deca3d97b5cb6
|
4
|
+
data.tar.gz: 96d8127169a6f4bef13b1f14128c599b78038c151547521ca5faf668f6d18975
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 41554f77f48de3d91bc3fbb63662db1a3064f6e2c1fe44a7913c3e14658501b7de908a230e0f67410779ce4403d116157f64e687b16090d7be0e0acb04e5c9b0
|
7
|
+
data.tar.gz: 9d0c18ec90d9f283a13754f1dc78c43c117afe0c688bd321a4aa26101c0396eda9bb0a46061ceae2ee99288b8d4733e5981e000faeea176a7d487cf609aaf897
|
data/bin/console
CHANGED
@@ -7,8 +7,8 @@ require "blood_contracts/core"
|
|
7
7
|
# with your gem easier. You can also use a different console, if you like.
|
8
8
|
|
9
9
|
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
-
|
11
|
-
|
10
|
+
require "pry"
|
11
|
+
Pry.start
|
12
12
|
|
13
|
-
require "irb"
|
14
|
-
IRB.start(__FILE__)
|
13
|
+
# require "irb"
|
14
|
+
# IRB.start(__FILE__)
|
@@ -23,9 +23,8 @@ Gem::Specification.new do |spec|
|
|
23
23
|
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
24
24
|
spec.require_paths = ["lib"]
|
25
25
|
|
26
|
-
spec.add_runtime_dependency "dry-initializer", "~> 2.0"
|
27
|
-
|
28
26
|
spec.add_development_dependency "bundler", "~> 2.0"
|
27
|
+
spec.add_development_dependency "pry"
|
29
28
|
spec.add_development_dependency "rake", "~> 10.0"
|
30
29
|
spec.add_development_dependency "rspec", "~> 3.0"
|
31
30
|
end
|
data/examples/json_response.rb
CHANGED
@@ -2,55 +2,74 @@ require 'json'
|
|
2
2
|
require 'blood_contracts/core'
|
3
3
|
|
4
4
|
module Types
|
5
|
-
class JSON <
|
6
|
-
param :json_string
|
7
|
-
|
5
|
+
class JSON < BC::Refined
|
8
6
|
def match
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
7
|
+
super do
|
8
|
+
begin
|
9
|
+
context[:parsed] = ::JSON.parse(unpack_refined(@value))
|
10
|
+
self
|
11
|
+
rescue StandardError => error
|
12
|
+
failure(error)
|
13
|
+
end
|
14
|
+
end
|
14
15
|
end
|
15
16
|
|
16
17
|
def unpack
|
17
|
-
|
18
|
+
super { |match| match.context[:parsed] }
|
18
19
|
end
|
19
20
|
end
|
20
21
|
|
21
|
-
class Tariff <
|
22
|
-
param :json
|
23
|
-
|
22
|
+
class Tariff < BC::Refined
|
24
23
|
def match
|
25
24
|
super do
|
26
|
-
|
27
|
-
|
28
|
-
self
|
25
|
+
context[:data] = unpack_refined(@value).slice("cost", "cur").compact
|
26
|
+
context[:tariff_context] = 1
|
27
|
+
return self if context[:data].size == 2
|
28
|
+
failure(:not_a_tariff)
|
29
29
|
end
|
30
30
|
end
|
31
|
-
end
|
32
31
|
|
33
|
-
|
34
|
-
|
32
|
+
def unpack
|
33
|
+
super { |match| match.context[:data] }
|
34
|
+
end
|
35
|
+
end
|
35
36
|
|
37
|
+
class Error < BC::Refined
|
36
38
|
def match
|
37
39
|
super do
|
38
|
-
|
39
|
-
|
40
|
-
self
|
40
|
+
context[:data] = unpack_refined(value).slice("code", "message").compact
|
41
|
+
context[:known_error_context] = 1
|
42
|
+
return self if context[:data].size == 2
|
43
|
+
failure(:not_a_known_error)
|
41
44
|
end
|
42
45
|
end
|
43
|
-
end
|
44
46
|
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
option :mapped, (Tariff | Error).method(:new), default: -> { parsed }
|
47
|
+
def unpack
|
48
|
+
super { |match| match.context[:data] }
|
49
|
+
end
|
49
50
|
end
|
51
|
+
|
52
|
+
Response = BC::Pipe.new(
|
53
|
+
BC::Anything, JSON, (Tariff | Error | Tariff),
|
54
|
+
names: [:raw, :parsed, :mapped]
|
55
|
+
)
|
56
|
+
|
57
|
+
# The same is
|
58
|
+
# Response = BC::Anything.and_then(JSON).and_then(Tariff | Error) do
|
59
|
+
# class Response
|
60
|
+
# self.names = [:raw, :parsed, :mapped]
|
61
|
+
# end
|
62
|
+
#
|
63
|
+
# or
|
64
|
+
#
|
65
|
+
# Response = BC::Pipe.new(BC::Anything, JSON, Tariff | Error) do
|
66
|
+
# self.names = [:raw, :parsed, :mapped]
|
67
|
+
# end
|
50
68
|
end
|
51
69
|
|
52
70
|
def match_response(response)
|
53
|
-
|
71
|
+
match = Types::Response.match(response)
|
72
|
+
case match
|
54
73
|
when BC::ContractFailure
|
55
74
|
puts "Honeybadger.notify 'Unexpected behavior in Russian Post', context: #{match.context}"
|
56
75
|
puts "render json: { errors: 'Ooops! Not working, we've been notified. Please, try again later' }"
|
@@ -61,12 +80,14 @@ def match_response(response)
|
|
61
80
|
end
|
62
81
|
when Types::Tariff
|
63
82
|
# работаем с тарифом
|
64
|
-
puts "
|
83
|
+
puts "match.context # => #{match.context} \n\n"
|
84
|
+
puts "render json: { tariff: #{match.unpack} }"
|
65
85
|
when Types::Error
|
66
86
|
# работаем с ошибкой, e.g. адрес слишком длинный
|
67
|
-
puts "
|
87
|
+
puts "match.context # => #{match.context} \n\n"
|
88
|
+
puts "render json: { errors: [#{match.unpack['message']}] } }"
|
68
89
|
else
|
69
|
-
binding.
|
90
|
+
require'pry';binding.pry
|
70
91
|
end
|
71
92
|
end
|
72
93
|
|
@@ -85,7 +106,7 @@ match_response(error_response)
|
|
85
106
|
puts "#{'=' * 20}================================#{'=' * 20}"
|
86
107
|
|
87
108
|
|
88
|
-
puts "\n\n\n"
|
109
|
+
puts "ss => errors }\n\n\n"
|
89
110
|
puts "#{'=' * 20} WHEN UNEXPECTED RESPONSE: #{'=' * 20}"
|
90
111
|
invalid_response = '<xml>'
|
91
112
|
match_response(invalid_response)
|
@@ -0,0 +1,72 @@
|
|
1
|
+
module BloodContracts
|
2
|
+
module Core
|
3
|
+
class Pipe < Refined
|
4
|
+
class << self
|
5
|
+
attr_reader :steps, :names, :finalized
|
6
|
+
|
7
|
+
def new(*args)
|
8
|
+
return super(*args) if finalized
|
9
|
+
if args.last.is_a?(Hash)
|
10
|
+
names = args.pop.delete(:names)
|
11
|
+
end
|
12
|
+
names ||= []
|
13
|
+
|
14
|
+
raise ArgumentError unless args.all?(Class)
|
15
|
+
pipe = Class.new(Pipe) { def inspect; super; end }
|
16
|
+
pipe.instance_variable_set(:@steps, args)
|
17
|
+
pipe.instance_variable_set(:@names, names)
|
18
|
+
pipe.instance_variable_set(:@finalized, true)
|
19
|
+
pipe
|
20
|
+
end
|
21
|
+
|
22
|
+
def and_then(other_type)
|
23
|
+
raise ArgumentError unless Class === other_type
|
24
|
+
pipe = Class.new(Pipe) { def inspect; super; end }
|
25
|
+
pipe.instance_variable_set(:@steps, args)
|
26
|
+
pipe.instance_variable_set(:@names, kwargs[:names].to_a)
|
27
|
+
pipe.instance_variable_set(:@finalized, true)
|
28
|
+
pipe
|
29
|
+
end
|
30
|
+
alias :> :and_then
|
31
|
+
end
|
32
|
+
|
33
|
+
def match
|
34
|
+
super do
|
35
|
+
index = 0
|
36
|
+
self.class.steps.reduce(value) do |next_value, step|
|
37
|
+
unpacked_value = unpack_refined(next_value)
|
38
|
+
match = step.match(unpacked_value)
|
39
|
+
|
40
|
+
share_context_with(match) do |context|
|
41
|
+
context[:steps][step_name(index)] = unpacked_value
|
42
|
+
index += 1
|
43
|
+
end
|
44
|
+
|
45
|
+
break match if match.invalid?
|
46
|
+
next refine_value(yield(match)) if block_given?
|
47
|
+
match
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
private
|
53
|
+
|
54
|
+
def step_name(index)
|
55
|
+
self.class.names[index] || index
|
56
|
+
end
|
57
|
+
|
58
|
+
def steps_with_names
|
59
|
+
steps = if self.class.names.empty?
|
60
|
+
self.class.steps.map(&:to_s)
|
61
|
+
else
|
62
|
+
self.class.steps.zip(self.class.names).map { |k, n| "#{k}(#{n})" }
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def inspect
|
67
|
+
require'pry';binding.pry
|
68
|
+
"#<pipe #{self.class.name} = #{steps_with_names.join(' > ')} (value=#{@value})>"
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,126 @@
|
|
1
|
+
module BloodContracts
|
2
|
+
module Core
|
3
|
+
class Refined
|
4
|
+
class << self
|
5
|
+
def or_a(other_type)
|
6
|
+
BC::Sum.new(self, other_type)
|
7
|
+
end
|
8
|
+
alias :or_an :or_a
|
9
|
+
alias :| :or_a
|
10
|
+
|
11
|
+
def and_then(other_type)
|
12
|
+
BC::Pipe.new(self, other_type)
|
13
|
+
end
|
14
|
+
alias :> :and_then
|
15
|
+
|
16
|
+
def match(*args)
|
17
|
+
new(*args).match
|
18
|
+
end
|
19
|
+
alias :call :match
|
20
|
+
|
21
|
+
def ===(object)
|
22
|
+
return object.to_ary.any?(self) if object.is_a?(Tuple)
|
23
|
+
super
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
attr_accessor :context
|
28
|
+
attr_reader :errors, :value
|
29
|
+
|
30
|
+
def initialize(value, context: Hash.new { |h,k| h[k] = Hash.new }, **)
|
31
|
+
@errors = []
|
32
|
+
@context = context
|
33
|
+
@value = value
|
34
|
+
end
|
35
|
+
|
36
|
+
def match
|
37
|
+
return @match if defined? @match
|
38
|
+
return @match = yield if block_given?
|
39
|
+
self
|
40
|
+
end
|
41
|
+
alias :call :match
|
42
|
+
|
43
|
+
def valid?
|
44
|
+
match.errors.empty?
|
45
|
+
end
|
46
|
+
def invalid?; !valid?; end
|
47
|
+
|
48
|
+
def unpack
|
49
|
+
raise "This is not what you're looking for" if match.invalid?
|
50
|
+
return yield(match) if block_given?
|
51
|
+
|
52
|
+
unpack_refined @value
|
53
|
+
end
|
54
|
+
|
55
|
+
def failure(error = nil, errors: @errors, context: @context)
|
56
|
+
errors << error if error
|
57
|
+
ContractFailure.new(
|
58
|
+
{ self.class => errors }, context: context
|
59
|
+
)
|
60
|
+
end
|
61
|
+
|
62
|
+
def inspect
|
63
|
+
require'pry';binding.pry
|
64
|
+
super
|
65
|
+
end
|
66
|
+
|
67
|
+
protected
|
68
|
+
|
69
|
+
def refined?(object)
|
70
|
+
object.class < BloodContracts::Core::Refined
|
71
|
+
end
|
72
|
+
|
73
|
+
def share_context_with(match)
|
74
|
+
match.context = @context.merge!(match.context)
|
75
|
+
yield(match.context)
|
76
|
+
end
|
77
|
+
|
78
|
+
def refine_value(value)
|
79
|
+
refined?(value) ? value.match : Anything.new(value)
|
80
|
+
end
|
81
|
+
|
82
|
+
def unpack_refined(value)
|
83
|
+
refined?(value) ? value.unpack : value
|
84
|
+
end
|
85
|
+
|
86
|
+
def errors_by_type(matches)
|
87
|
+
Hash[
|
88
|
+
matches.map(&:class).zip(matches.map(&:errors))
|
89
|
+
].delete_if { |_, errors| errors.empty? }
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
class ContractFailure < Refined
|
94
|
+
def initialize(*)
|
95
|
+
super
|
96
|
+
@context.merge!(errors: @value.to_h)
|
97
|
+
end
|
98
|
+
|
99
|
+
def errors
|
100
|
+
context[:errors]
|
101
|
+
end
|
102
|
+
|
103
|
+
def match
|
104
|
+
self
|
105
|
+
end
|
106
|
+
|
107
|
+
def unpack
|
108
|
+
context
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
class Anything < Refined
|
113
|
+
def match
|
114
|
+
self
|
115
|
+
end
|
116
|
+
|
117
|
+
def valid?
|
118
|
+
true
|
119
|
+
end
|
120
|
+
|
121
|
+
def unpack
|
122
|
+
@value
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
require 'set'
|
2
|
+
|
3
|
+
module BloodContracts
|
4
|
+
module Core
|
5
|
+
class Sum < Refined
|
6
|
+
class << self
|
7
|
+
attr_reader :sum_of, :finalized
|
8
|
+
|
9
|
+
def new(*args)
|
10
|
+
return super if finalized
|
11
|
+
|
12
|
+
sum = Class.new(Sum) { def inspect; super; end }
|
13
|
+
sum.instance_variable_set(:@sum_of, ::Set.new(args))
|
14
|
+
sum.instance_variable_set(:@finalized, true)
|
15
|
+
sum
|
16
|
+
end
|
17
|
+
|
18
|
+
def or_a(other_type)
|
19
|
+
sum = Class.new(Sum) { def inspect; super; end }
|
20
|
+
sum.instance_variable_set(:@sum_of, ::Set.new(self.sum_of << other_type))
|
21
|
+
sum.instance_variable_set(:@finalized, true)
|
22
|
+
sum
|
23
|
+
end
|
24
|
+
alias :or_an :or_a
|
25
|
+
alias :| :or_a
|
26
|
+
end
|
27
|
+
|
28
|
+
def match
|
29
|
+
super do
|
30
|
+
or_matches = self.class.sum_of.map do |type|
|
31
|
+
match = type.match(@value, context: @context)
|
32
|
+
end
|
33
|
+
|
34
|
+
if (match = or_matches.find(&:valid?))
|
35
|
+
match.context[:errors].merge(errors_by_type(or_matches))
|
36
|
+
match
|
37
|
+
else
|
38
|
+
failure(errors: errors_by_type(or_matches))
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def inspect
|
44
|
+
"#<sum #{self.class.name} is #{self.class.sum_of.to_a.join(' or ')} (value=#{@value})>"
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
module BloodContracts
|
2
|
+
module Core
|
3
|
+
class Tuple < Refined
|
4
|
+
class << self
|
5
|
+
attr_reader :attributes, :names, :finalized
|
6
|
+
|
7
|
+
def new(*args, **kwargs)
|
8
|
+
return super(*args, **kwargs) if finalized
|
9
|
+
|
10
|
+
raise ArgumentError unless args.all?(Class)
|
11
|
+
pipe = Class.new(Tuple) { def inspect; super; end }
|
12
|
+
pipe.instance_variable_set(:@attributes, args)
|
13
|
+
pipe.instance_variable_set(:@names, kwargs[:names].to_a)
|
14
|
+
pipe.instance_variable_set(:@finalized, true)
|
15
|
+
pipe
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
attr_reader :values
|
20
|
+
def initialize(*args)
|
21
|
+
super
|
22
|
+
@values = args
|
23
|
+
end
|
24
|
+
|
25
|
+
def match
|
26
|
+
super do
|
27
|
+
matches = self.class.attributes.zip(values).map do |(type, value)|
|
28
|
+
type.match(value, context: @context)
|
29
|
+
end
|
30
|
+
next self unless (failure = matches.find?(&:invalid?)).nil?
|
31
|
+
failure
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def unpack
|
36
|
+
super { |match| match.values.map(&method(:unpack_refined)) }
|
37
|
+
end
|
38
|
+
alias :to_ary :unpack
|
39
|
+
|
40
|
+
private def values_by_names
|
41
|
+
if self.class.names.empty?
|
42
|
+
self.values
|
43
|
+
else
|
44
|
+
self.class.names.zip(attributes).map { |k, v| [k, v].join('=') }
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
private def inspect
|
49
|
+
"#<tuple #{self.class.name} (#{values_by_names.join(',')}>"
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
data/lib/blood_contracts/core.rb
CHANGED
@@ -1,30 +1,13 @@
|
|
1
|
-
|
2
|
-
require_relative "./core/
|
3
|
-
require_relative "./core/
|
1
|
+
require_relative "./core/refined.rb"
|
2
|
+
require_relative "./core/pipe.rb"
|
3
|
+
require_relative "./core/sum.rb"
|
4
|
+
require_relative "./core/tuple.rb"
|
4
5
|
require_relative "./core/version.rb"
|
5
6
|
|
6
|
-
BloodContract = BloodContracts::Core::Contract
|
7
|
-
BloodType = BloodContracts::Core::Type
|
8
7
|
|
9
8
|
module BloodContracts
|
10
9
|
module Core; end
|
11
|
-
|
12
|
-
class ContractFailure < Core::Type
|
13
|
-
def errors
|
14
|
-
context[:errors].to_h
|
15
|
-
end
|
16
|
-
|
17
|
-
def unpack
|
18
|
-
context
|
19
|
-
end
|
20
|
-
end
|
21
|
-
|
22
|
-
class Anything < Core::Type
|
23
|
-
param :data
|
24
|
-
end
|
25
10
|
end
|
26
11
|
|
27
|
-
|
28
|
-
|
29
|
-
ContractFailure = BloodContracts::ContractFailure
|
30
|
-
end
|
12
|
+
BC = BloodContracts::Core
|
13
|
+
|
metadata
CHANGED
@@ -1,23 +1,23 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: blood_contracts-core
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Sergey Dolganov
|
8
8
|
autorequire:
|
9
9
|
bindir: bin/
|
10
10
|
cert_chain: []
|
11
|
-
date: 2019-03-
|
11
|
+
date: 2019-03-25 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
|
-
name:
|
14
|
+
name: bundler
|
15
15
|
requirement: !ruby/object:Gem::Requirement
|
16
16
|
requirements:
|
17
17
|
- - "~>"
|
18
18
|
- !ruby/object:Gem::Version
|
19
19
|
version: '2.0'
|
20
|
-
type: :
|
20
|
+
type: :development
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
@@ -25,19 +25,19 @@ dependencies:
|
|
25
25
|
- !ruby/object:Gem::Version
|
26
26
|
version: '2.0'
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
|
-
name:
|
28
|
+
name: pry
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
30
30
|
requirements:
|
31
|
-
- - "
|
31
|
+
- - ">="
|
32
32
|
- !ruby/object:Gem::Version
|
33
|
-
version: '
|
33
|
+
version: '0'
|
34
34
|
type: :development
|
35
35
|
prerelease: false
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
37
37
|
requirements:
|
38
|
-
- - "
|
38
|
+
- - ">="
|
39
39
|
- !ruby/object:Gem::Version
|
40
|
-
version: '
|
40
|
+
version: '0'
|
41
41
|
- !ruby/object:Gem::Dependency
|
42
42
|
name: rake
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|
@@ -88,7 +88,10 @@ files:
|
|
88
88
|
- lib/blood_contracts-core.rb
|
89
89
|
- lib/blood_contracts/core.rb
|
90
90
|
- lib/blood_contracts/core/contract.rb
|
91
|
-
- lib/blood_contracts/core/
|
91
|
+
- lib/blood_contracts/core/pipe.rb
|
92
|
+
- lib/blood_contracts/core/refined.rb
|
93
|
+
- lib/blood_contracts/core/sum.rb
|
94
|
+
- lib/blood_contracts/core/tuple.rb
|
92
95
|
- lib/blood_contracts/core/version.rb
|
93
96
|
homepage: https://github.com/sclinede/blood_contracts-core
|
94
97
|
licenses:
|
@@ -1,197 +0,0 @@
|
|
1
|
-
module BloodContracts
|
2
|
-
module Core
|
3
|
-
class Type
|
4
|
-
extend Dry::Initializer
|
5
|
-
|
6
|
-
# TODO: share context between Refinement Types match
|
7
|
-
class << self
|
8
|
-
def attributes
|
9
|
-
@attributes ||= []
|
10
|
-
end
|
11
|
-
|
12
|
-
def param(name, *)
|
13
|
-
@attributes = attributes | [name.to_sym]
|
14
|
-
super
|
15
|
-
end
|
16
|
-
|
17
|
-
def option(name, type = nil, as: name, **)
|
18
|
-
@attributes = attributes | [as.to_sym]
|
19
|
-
super
|
20
|
-
end
|
21
|
-
|
22
|
-
def single?
|
23
|
-
attributes.size <= 1
|
24
|
-
end
|
25
|
-
|
26
|
-
attr_reader :sum
|
27
|
-
def or_a(other_type)
|
28
|
-
c = Class.new(Anything)
|
29
|
-
c.instance_variable_set(
|
30
|
-
:@sum, Set.new([self] + self.sum.to_a << other_type)
|
31
|
-
)
|
32
|
-
c
|
33
|
-
end
|
34
|
-
alias :or_an :or_a
|
35
|
-
alias :| :or_a
|
36
|
-
|
37
|
-
def match(*args)
|
38
|
-
new(*args).match
|
39
|
-
end
|
40
|
-
alias :call :match
|
41
|
-
|
42
|
-
def ===(object)
|
43
|
-
return true if (result = super)
|
44
|
-
return result unless object.class < BloodContracts::Core::Type
|
45
|
-
if object.class.send(:single?)
|
46
|
-
return result if object.class.sum.to_a.empty?
|
47
|
-
return object.class.sum.any? { |or_type| self == or_type }
|
48
|
-
end
|
49
|
-
|
50
|
-
object.attributes.values.any? { |sub_type| self === sub_type }
|
51
|
-
end
|
52
|
-
end
|
53
|
-
|
54
|
-
attr_accessor :context
|
55
|
-
attr_reader :errors, :value_handler
|
56
|
-
def initialize(*args, **kwargs)
|
57
|
-
@errors = []
|
58
|
-
@context = kwargs.delete(:context) || {}
|
59
|
-
@value_handler = self.class.single? ? Single.new(self) : Tuple.new(self)
|
60
|
-
|
61
|
-
super(*args, **kwargs)
|
62
|
-
end
|
63
|
-
|
64
|
-
def attributes
|
65
|
-
@attributes ||= self.class.dry_initializer.attributes(self)
|
66
|
-
end
|
67
|
-
|
68
|
-
def match
|
69
|
-
return @match if defined? @match
|
70
|
-
return @match = yield if block_given?
|
71
|
-
@match = value_handler.match
|
72
|
-
end
|
73
|
-
alias :call :match
|
74
|
-
|
75
|
-
def valid?
|
76
|
-
value_handler.valid?
|
77
|
-
end
|
78
|
-
def invalid?; !valid?; end
|
79
|
-
|
80
|
-
def unpack
|
81
|
-
value_handler.unpack
|
82
|
-
end
|
83
|
-
|
84
|
-
def single?
|
85
|
-
Single === value_handler
|
86
|
-
end
|
87
|
-
|
88
|
-
def refined?(object)
|
89
|
-
BloodContracts::Core::Type === object
|
90
|
-
end
|
91
|
-
|
92
|
-
def unpack_refined(value)
|
93
|
-
refined?(value) ? value.unpack : value
|
94
|
-
end
|
95
|
-
|
96
|
-
protected
|
97
|
-
|
98
|
-
class ValueHandler
|
99
|
-
def initialize(subject)
|
100
|
-
@subject = subject
|
101
|
-
end
|
102
|
-
|
103
|
-
def errors_by_type(matches)
|
104
|
-
Hash[
|
105
|
-
matches.map(&:class).zip(matches.map(&:errors))
|
106
|
-
].delete_if { |_, errors| errors.empty? }
|
107
|
-
end
|
108
|
-
|
109
|
-
def refined?(object)
|
110
|
-
@subject.refined?(object)
|
111
|
-
end
|
112
|
-
|
113
|
-
def unpack_refined(value)
|
114
|
-
@subject.unpack_refined(value)
|
115
|
-
end
|
116
|
-
end
|
117
|
-
|
118
|
-
class Tuple < ValueHandler
|
119
|
-
def initialize(*)
|
120
|
-
super
|
121
|
-
@wrapper = Struct.new(*@subject.class.attributes, keyword_init: true)
|
122
|
-
end
|
123
|
-
|
124
|
-
def wrap_unpacked(match)
|
125
|
-
@wrapper.new(
|
126
|
-
match.attributes.transform_values(&method(:unpack_refined))
|
127
|
-
)
|
128
|
-
end
|
129
|
-
|
130
|
-
def match
|
131
|
-
failed_match =
|
132
|
-
@subject.attributes.values.lazy.map(&:match).find(&:invalid?)
|
133
|
-
|
134
|
-
if failed_match
|
135
|
-
BloodContracts::ContractFailure.new(
|
136
|
-
context: { errors: errors_by_type([failed_match]) }
|
137
|
-
)
|
138
|
-
else
|
139
|
-
@subject
|
140
|
-
end
|
141
|
-
end
|
142
|
-
|
143
|
-
def unpack
|
144
|
-
raise "This is not what you're looking for" if invalid?
|
145
|
-
|
146
|
-
wrap_unpacked(@subject.match)
|
147
|
-
end
|
148
|
-
|
149
|
-
def invalid?
|
150
|
-
BloodContracts::ContractFailure === @subject.match
|
151
|
-
end
|
152
|
-
def valid?; !invalid?; end
|
153
|
-
|
154
|
-
def single?
|
155
|
-
false
|
156
|
-
end
|
157
|
-
end
|
158
|
-
|
159
|
-
class Single < ValueHandler
|
160
|
-
def match
|
161
|
-
sum = @subject.class.sum.to_a
|
162
|
-
return refined?(value) ? value.match : @subject if sum.empty?
|
163
|
-
|
164
|
-
or_matches = sum.map { |type| type.match(value) }
|
165
|
-
if (match = or_matches.find(&:valid?))
|
166
|
-
match.context.merge!(errors: errors_by_type(or_matches))
|
167
|
-
match
|
168
|
-
else
|
169
|
-
BloodContracts::ContractFailure.new(
|
170
|
-
context: { errors: errors_by_type(or_matches) }
|
171
|
-
)
|
172
|
-
end
|
173
|
-
end
|
174
|
-
|
175
|
-
def valid?
|
176
|
-
@subject.errors.empty?
|
177
|
-
end
|
178
|
-
|
179
|
-
def unpack
|
180
|
-
raise "This is not what you're looking for" unless valid?
|
181
|
-
|
182
|
-
unpack_refined value
|
183
|
-
end
|
184
|
-
|
185
|
-
def single?
|
186
|
-
true
|
187
|
-
end
|
188
|
-
|
189
|
-
private
|
190
|
-
|
191
|
-
def value
|
192
|
-
@value ||= @subject.attributes.values.first
|
193
|
-
end
|
194
|
-
end
|
195
|
-
end
|
196
|
-
end
|
197
|
-
end
|