blood_contracts-core 0.3.5 → 0.4.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.
@@ -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