blood_contracts-core 0.1.0 → 0.2.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/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
|