blood_contracts-core 0.3.5 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,31 @@
1
+ module BloodContracts::Core
2
+ # Represents failure in during of Tuple data matching
3
+ class TupleContractFailure < ContractFailure
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, ContractFailure>]
15
+ #
16
+ def attribute_errors
17
+ attributes.select { |_name, type| type.invalid? }
18
+ end
19
+
20
+ # Unpacked matching errors in form of a hash per attribute
21
+ #
22
+ # @return [Hash<String, ContractFailure>]
23
+ #
24
+ def unpack_h
25
+ @unpack_h ||= attribute_errors.transform_values(&:unpack)
26
+ end
27
+ alias to_hash unpack_h
28
+ alias to_h unpack_h
29
+ alias unpack_attributes unpack_h
30
+ end
31
+ end
@@ -1,14 +1,20 @@
1
- require_relative "./core/refined.rb"
2
- require_relative "./core/pipe.rb"
3
- require_relative "./core/contract.rb"
4
- require_relative "./core/sum.rb"
5
- require_relative "./core/tuple.rb"
6
- require_relative "./core/version.rb"
7
-
8
-
1
+ # Top-level scope for BloodContracts collection of gems
9
2
  module BloodContracts
10
- module Core; end
3
+ # Scope for BloodContracts::Core classes
4
+ module Core
5
+ require_relative "./core/refined.rb"
6
+ require_relative "./core/contract_failure.rb"
7
+ require_relative "./core/anything.rb"
8
+ require_relative "./core/pipe.rb"
9
+ require_relative "./core/contract.rb"
10
+ require_relative "./core/sum.rb"
11
+ require_relative "./core/tuple.rb"
12
+ require_relative "./core/tuple_contract_failure.rb"
13
+
14
+ # constant aliases
15
+ Or = Sum
16
+ AndThen = Pipe
17
+ end
11
18
  end
12
19
 
13
20
  BC = BloodContracts::Core
14
-
@@ -0,0 +1,314 @@
1
+
2
+ require "spec_helper"
3
+
4
+ RSpec.describe BloodContracts::Core do
5
+ before do
6
+ module Test
7
+ class Json < ::BC::Refined
8
+ require "json"
9
+
10
+ def match
11
+ context[:raw_value] = unpack_refined(value).to_s
12
+ context[:parsed] =
13
+ JSON.parse(context[:raw_value], symbolize_names: true)
14
+ nil
15
+ rescue JSON::ParserError => exception
16
+ failure(exception)
17
+ end
18
+
19
+ def mapped
20
+ context[:parsed]
21
+ end
22
+ end
23
+
24
+ class Phone < ::BC::Refined
25
+ REGEX = /\A(\+7|8)(9|8)\d{9}\z/i
26
+
27
+ def match
28
+ context[:phone] = unpack_refined(value).to_s
29
+ context[:clean_phone] = context[:phone].gsub(/[\s\(\)-]/, "")
30
+ return if context[:clean_phone] =~ REGEX
31
+
32
+ failure("Not a phone")
33
+ end
34
+
35
+ def mapped
36
+ context[:clean_phone]
37
+ end
38
+ end
39
+
40
+ class Email < ::BC::Refined
41
+ REGEX = /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i
42
+
43
+ def match
44
+ context[:email] = unpack_refined(value).to_s
45
+ return if context[:email] =~ REGEX
46
+
47
+ failure("Not an email")
48
+ end
49
+
50
+ def mapped
51
+ context[:email]
52
+ end
53
+ end
54
+
55
+ class Ascii < ::BC::Refined
56
+ REGEX = /^[[:ascii:]]+$/i
57
+
58
+ def match
59
+ context[:ascii_string] = value.to_s
60
+ return if context[:ascii_string] =~ REGEX
61
+
62
+ failure("Not ASCII")
63
+ end
64
+
65
+ def mapped
66
+ context[:ascii_string]
67
+ end
68
+ end
69
+ end
70
+ end
71
+
72
+ context "standalone validation" do
73
+ subject { Test::Ascii.match(value) }
74
+
75
+ context "when value is valid" do
76
+ let(:value) { "i'm written in pure ASCII" }
77
+
78
+ it do
79
+ is_expected.to be_valid
80
+ expect { subject.unpack }.not_to raise_error
81
+ expect(subject.unpack).to eq(value)
82
+ expect(subject.context).to match({ ascii_string: value })
83
+ end
84
+ end
85
+
86
+ context "when value is invalid" do
87
+ let(:value) { "I'm ∑ritten nøt in åßçii" }
88
+ let(:error) { { Test::Ascii => ["Not ASCII"] } }
89
+ let(:validation_context) { { ascii_string: value, errors: [error] } }
90
+
91
+ it do
92
+ is_expected.to be_invalid
93
+ expect(subject.errors).to match([error])
94
+ expect(subject.messages).to match(["Not ASCII"])
95
+ expect(subject.unpack).to match(error)
96
+ end
97
+ end
98
+ end
99
+
100
+ context "Sum composition" do
101
+ before do
102
+ module Test
103
+ Login = Email.or_a(Phone)
104
+ end
105
+ end
106
+
107
+ subject { Test::Login.match(login) }
108
+
109
+ context "when login is valid" do
110
+ context "when login is email" do
111
+ let(:login) { "admin@example.com" }
112
+ let(:errors) { [{ Test::Phone => ["Not a phone"] }] }
113
+ let(:validation_context) do
114
+ hash_including(
115
+ phone: login,
116
+ clean_phone: login,
117
+ errors: errors,
118
+ email: login
119
+ )
120
+ end
121
+
122
+ it do
123
+ is_expected.to be_valid
124
+ expect { subject.unpack }.not_to raise_error
125
+ expect(subject.unpack).to eq(login)
126
+ expect(subject.context).to match(validation_context)
127
+ end
128
+ end
129
+
130
+ context "when login is phone" do
131
+ let(:login) { "8(800) 200 - 11 - 00" }
132
+ let(:cleaned_phone) { "88002001100" }
133
+ let(:errors) { [{ Test::Email => ["Not an email"] }] }
134
+ let(:validation_context) do
135
+ hash_including(
136
+ phone: login,
137
+ clean_phone: cleaned_phone,
138
+ errors: errors,
139
+ email: login
140
+ )
141
+ end
142
+
143
+ it do
144
+ is_expected.to be_valid
145
+ expect { subject.unpack }.not_to raise_error
146
+ expect(subject.unpack).to eq(cleaned_phone)
147
+ expect(subject.context).to match(validation_context)
148
+ end
149
+ end
150
+ end
151
+
152
+ context "when login is invalid" do
153
+ let(:login) { "I'm something else" }
154
+ let(:errors) do
155
+ [
156
+ { Test::Email => ["Not an email"] },
157
+ { Test::Phone => ["Not a phone"] },
158
+ { Test::Login => [:no_matches] }
159
+ ]
160
+ end
161
+
162
+ it do
163
+ is_expected.to be_invalid
164
+ expect(subject.errors).to match_array(errors)
165
+ expect(subject.unpack).to match({ Test::Login => [:no_matches] })
166
+ end
167
+ end
168
+ end
169
+
170
+ context "Tuple composition" do
171
+ before do
172
+ module Test
173
+ class RegistrationInput < ::BC::Tuple
174
+ attribute :email, Email
175
+ attribute :password, Ascii
176
+ end
177
+ end
178
+ end
179
+
180
+ subject { Test::RegistrationInput.match(email, password) }
181
+
182
+ context "when valid input" do
183
+ let(:email) { "admin@mail.com" }
184
+ let(:password) { "newP@ssw0rd" }
185
+ let(:attributes) do
186
+ { email: kind_of(Test::Email), password: kind_of(Test::Ascii) }
187
+ end
188
+
189
+ it do
190
+ expect(subject).to be_valid
191
+ expect(subject.attributes).to match(attributes)
192
+ expect(subject.to_h).to match(email: email, password: password)
193
+ expect(subject.errors).to be_empty
194
+ expect(subject.attribute_errors).to be_empty
195
+ end
196
+ end
197
+
198
+ context "when input is invalid" do
199
+ let(:email) { "admin" }
200
+ let(:email_error) { { Test::Email => ["Not an email"] } }
201
+ let(:password) { "newP@ssw0rd" }
202
+ let(:attributes) do
203
+ attribute_errors.merge(password: kind_of(Test::Ascii))
204
+ end
205
+ let(:attribute_errors) { { email: kind_of(BC::ContractFailure) } }
206
+ let(:tuple_invalid) { { Test::RegistrationInput => [:invalid_tuple] } }
207
+
208
+ it do
209
+ expect(subject).to be_invalid
210
+ expect(subject.attributes).to match(attributes)
211
+ expect(subject.to_h).to match(email: email_error)
212
+ expect(subject.errors).to match_array([email_error, tuple_invalid])
213
+ expect(subject.attribute_errors).to match(attribute_errors)
214
+ end
215
+ end
216
+ end
217
+
218
+ context "Pipe composition" do
219
+ before do
220
+ module Test
221
+ class RegistrationInput < ::BC::Tuple
222
+ attribute :login, Email.or_a(Phone)
223
+ attribute :password, Ascii
224
+ end
225
+
226
+ ResponseParser = BC::Pipe.new do
227
+ step :parse, Json
228
+ step :validate, RegistrationInput
229
+ end
230
+ end
231
+ end
232
+
233
+ subject { Test::ResponseParser.match(response) }
234
+
235
+ context "when value is invalid JSON" do
236
+ let(:response) { "<xml>" }
237
+ let(:error) { { Test::Json => [kind_of(JSON::ParserError)] } }
238
+ let(:validation_context) do
239
+ {
240
+ raw_value: response,
241
+ errors: [error],
242
+ steps: ["BloodContracts::Core::ContractFailure"],
243
+ steps_values: { parse: response }
244
+ }
245
+ end
246
+
247
+ it do
248
+ is_expected.to be_invalid
249
+ is_expected.to be_kind_of(BC::ContractFailure)
250
+ expect(subject.unpack).to match(error)
251
+ expect(subject.context).to match(validation_context)
252
+ end
253
+ end
254
+
255
+ context "when value is valid JSON" do
256
+ context "when value is invalid registration data" do
257
+ let(:response) { '{"phone":"+78889992211"}' }
258
+ let(:error) { { Test::RegistrationInput => [:invalid_tuple] } }
259
+ let(:errors) do
260
+ [
261
+ { Test::Email => ["Not an email"] },
262
+ { Test::Phone => ["Not a phone"] },
263
+ { Test::Ascii => ["Not ASCII"] },
264
+ { Test::RegistrationInput => [:invalid_tuple] }
265
+ ]
266
+ end
267
+ let(:validation_context) do
268
+ hash_including(
269
+ raw_value: response,
270
+ errors: array_including(errors),
271
+ steps: ["Test::Json", "BloodContracts::Core::TupleContractFailure"],
272
+ steps_values: {
273
+ parse: response,
274
+ validate: { phone: "+78889992211" }
275
+ }
276
+ )
277
+ end
278
+
279
+ it do
280
+ is_expected.to be_invalid
281
+ is_expected.to be_kind_of(BC::TupleContractFailure)
282
+ expect(subject.unpack).to match(error)
283
+ expect(subject.context).to match(validation_context)
284
+ end
285
+ end
286
+
287
+ context "when value is valid registration data" do
288
+ context "when login is an email" do
289
+ let(:response) { '{"login":"admin@example.com", "password":"111"}' }
290
+ let(:payload) { { login: "admin@example.com", password: "111" } }
291
+
292
+ it do
293
+ is_expected.to be_valid
294
+ is_expected.to be_kind_of(Test::RegistrationInput)
295
+ expect(subject.unpack).to match(["admin@example.com", "111"])
296
+ expect(subject.to_h).to match(payload)
297
+ end
298
+ end
299
+
300
+ context "when login is a phone" do
301
+ let(:response) { '{"login":"8 (999) 123-33-12", "password":"111"}' }
302
+ let(:payload) { { login: "89991233312", password: "111" } }
303
+
304
+ it do
305
+ is_expected.to be_valid
306
+ is_expected.to be_kind_of(Test::RegistrationInput)
307
+ expect(subject.unpack).to match(%w[89991233312 111])
308
+ expect(subject.to_h).to match(payload)
309
+ end
310
+ end
311
+ end
312
+ end
313
+ end
314
+ end
@@ -0,0 +1,26 @@
1
+ begin
2
+ require "pry"
3
+ rescue LoadError
4
+ nil
5
+ end
6
+
7
+ require "bundler/setup"
8
+ require "blood_contracts/core"
9
+
10
+ RSpec.configure do |config|
11
+ # Enable flags like --only-failures and --next-failure
12
+ config.example_status_persistence_file_path = ".rspec_status"
13
+
14
+ # Disable RSpec exposing methods globally on `Module` and `main`
15
+ config.disable_monkey_patching!
16
+
17
+ config.expect_with :rspec do |c|
18
+ c.syntax = :expect
19
+ end
20
+
21
+ config.around do |example|
22
+ module Test; end
23
+ example.run
24
+ Object.send(:remove_const, :Test)
25
+ end
26
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: blood_contracts-core
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.5
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
- - Sergey Dolganov
7
+ - Sergey Dolganov (sclinede)
8
8
  autorequire:
9
- bindir: bin/
9
+ bindir: bin
10
10
  cert_chain: []
11
- date: 2019-06-21 00:00:00.000000000 Z
11
+ date: 2019-07-02 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -66,16 +66,36 @@ dependencies:
66
66
  - - "~>"
67
67
  - !ruby/object:Gem::Version
68
68
  version: '3.0'
69
- description: Core classes for Contracts API validation
69
+ - !ruby/object:Gem::Dependency
70
+ name: rubocop
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '0.49'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '0.49'
83
+ description: Core classes for data validation with contracts approach (using Either
84
+ + Writer monad combination & ADT for composition)
70
85
  email:
71
86
  - sclinede@evilmartians.com
72
87
  executables: []
73
88
  extensions: []
74
- extra_rdoc_files: []
89
+ extra_rdoc_files:
90
+ - CODE_OF_CONDUCT.md
91
+ - README.md
92
+ - CHANGELOG.md
75
93
  files:
76
94
  - ".gitignore"
77
95
  - ".rspec"
96
+ - ".rubocop.yml"
78
97
  - ".travis.yml"
98
+ - CHANGELOG.md
79
99
  - CODE_OF_CONDUCT.md
80
100
  - Gemfile
81
101
  - LICENSE.txt
@@ -89,12 +109,16 @@ files:
89
109
  - examples/tuple.rb
90
110
  - lib/blood_contracts-core.rb
91
111
  - lib/blood_contracts/core.rb
112
+ - lib/blood_contracts/core/anything.rb
92
113
  - lib/blood_contracts/core/contract.rb
114
+ - lib/blood_contracts/core/contract_failure.rb
93
115
  - lib/blood_contracts/core/pipe.rb
94
116
  - lib/blood_contracts/core/refined.rb
95
117
  - lib/blood_contracts/core/sum.rb
96
118
  - lib/blood_contracts/core/tuple.rb
97
- - lib/blood_contracts/core/version.rb
119
+ - lib/blood_contracts/core/tuple_contract_failure.rb
120
+ - spec/blood_contracts/core_spec.rb
121
+ - spec/spec_helper.rb
98
122
  homepage: https://github.com/sclinede/blood_contracts-core
99
123
  licenses:
100
124
  - MIT
@@ -107,7 +131,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
107
131
  requirements:
108
132
  - - ">="
109
133
  - !ruby/object:Gem::Version
110
- version: '0'
134
+ version: '2.4'
111
135
  required_rubygems_version: !ruby/object:Gem::Requirement
112
136
  requirements:
113
137
  - - ">="
@@ -117,5 +141,7 @@ requirements: []
117
141
  rubygems_version: 3.0.3
118
142
  signing_key:
119
143
  specification_version: 4
120
- summary: Core classes for Contracts API validation
121
- test_files: []
144
+ summary: Core classes for data validation with contracts approach
145
+ test_files:
146
+ - spec/blood_contracts/core_spec.rb
147
+ - spec/spec_helper.rb
@@ -1,5 +0,0 @@
1
- module BloodContracts
2
- module Core
3
- VERSION = "0.3.5"
4
- end
5
- end