blood_contracts-ext 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (37) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +12 -0
  3. data/.rspec +3 -0
  4. data/.rubocop.yml +31 -0
  5. data/.travis.yml +19 -0
  6. data/CHANGELOG.md +23 -0
  7. data/CODE_OF_CONDUCT.md +74 -0
  8. data/Gemfile +4 -0
  9. data/LICENSE +21 -0
  10. data/README.md +369 -0
  11. data/Rakefile +6 -0
  12. data/bin/console +14 -0
  13. data/bin/setup +8 -0
  14. data/blood_contracts-ext.gemspec +27 -0
  15. data/lib/blood_contracts/core/defineable_error.rb +61 -0
  16. data/lib/blood_contracts/core/exception_caught.rb +33 -0
  17. data/lib/blood_contracts/core/exception_handling.rb +36 -0
  18. data/lib/blood_contracts/core/expected_error.rb +16 -0
  19. data/lib/blood_contracts/core/extractable.rb +85 -0
  20. data/lib/blood_contracts/core/map_value.rb +45 -0
  21. data/lib/blood_contracts/core/policy_failure.rb +42 -0
  22. data/lib/blood_contracts/core/sum_policy_failure.rb +9 -0
  23. data/lib/blood_contracts/core/tuple_policy_failure.rb +39 -0
  24. data/lib/blood_contracts/ext/pipe.rb +27 -0
  25. data/lib/blood_contracts/ext/refined.rb +59 -0
  26. data/lib/blood_contracts/ext/sum.rb +29 -0
  27. data/lib/blood_contracts/ext/tuple.rb +28 -0
  28. data/lib/blood_contracts/ext.rb +28 -0
  29. data/spec/blood_contracts/ext/exception_caught_spec.rb +50 -0
  30. data/spec/blood_contracts/ext/expected_error_spec.rb +56 -0
  31. data/spec/blood_contracts/ext/map_value_spec.rb +54 -0
  32. data/spec/blood_contracts/ext/policy_failure_spec.rb +151 -0
  33. data/spec/blood_contracts/ext/policy_spec.rb +138 -0
  34. data/spec/fixtures/en.yml +19 -0
  35. data/spec/spec_helper.rb +24 -0
  36. data/spec/support/fixtures_helper.rb +11 -0
  37. metadata +202 -0
@@ -0,0 +1,54 @@
1
+ RSpec.describe BloodContracts::Core::MapValue do
2
+ before do
3
+ module Test
4
+ require "forwardable"
5
+
6
+ class JsonMapper
7
+ def self.call(**payload)
8
+ JSON.pretty_generate(payload)
9
+ end
10
+ end
11
+
12
+ class ContactType < ::BC::Ext::Refined
13
+ extend Forwardable
14
+ extract :name
15
+ extract :phone
16
+ extract :manager_id
17
+
18
+ def_delegators :@value, :name, :phone, :manager_id
19
+ end
20
+
21
+ ContactJsonType = ContactType.and_then(BC::MapValue.with(JsonMapper))
22
+ Contact = Struct.new(:name, :phone, :manager_id)
23
+ end
24
+ end
25
+
26
+ subject { Test::ContactJsonType.match(value) }
27
+
28
+ context "when value is a valid contact" do
29
+ let(:value) { Test::Contact.new("Nick Cage", "2-333-111-444", 4113) }
30
+ let(:valid_json) do
31
+ <<~JSON.strip
32
+ {
33
+ "name": "Nick Cage",
34
+ "phone": "2-333-111-444",
35
+ "manager_id": 4113
36
+ }
37
+ JSON
38
+ end
39
+
40
+ it do
41
+ is_expected.to be_valid
42
+ expect(subject.unpack).to eq(valid_json)
43
+ end
44
+ end
45
+
46
+ context "when value is an invalid contact" do
47
+ let(:value) { "Nick Cage, 2-333-111-444, 4113" }
48
+
49
+ it do
50
+ is_expected.to be_invalid
51
+ is_expected.to match(kind_of(::BC::ExceptionCaught))
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,151 @@
1
+ RSpec.describe BloodContracts::Core::PolicyFailure do
2
+ before do
3
+ module Test
4
+ class Phone < ::BC::Ext::Refined
5
+ REGEX = /\A(\+7|8)(9|8)\d{9}\z/i
6
+
7
+ def match
8
+ context[:phone_input] = value.to_s
9
+ clean_phone = context[:phone_input].gsub(/[\s\(\)-]/, "")
10
+ return failure(:invalid_phone) if clean_phone !~ REGEX
11
+ context[:clean_phone] = clean_phone
12
+
13
+ self
14
+ end
15
+
16
+ def mapped
17
+ context[:clean_phone]
18
+ end
19
+ end
20
+
21
+ class Email < ::BC::Ext::Refined
22
+ REGEX = /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i
23
+ INVALID_EMAIL = define_error(:invalid_email)
24
+
25
+ def match
26
+ context[:email_input] = value.to_s
27
+ return failure(INVALID_EMAIL) if context[:email_input] !~ REGEX
28
+ context[:email] = context[:email_input]
29
+
30
+ self
31
+ end
32
+
33
+ def mapped
34
+ context[:email]
35
+ end
36
+ end
37
+
38
+ class Ascii < ::BC::Ext::Refined
39
+ REGEX = /^[[:ascii:]]+$/i
40
+
41
+ def match
42
+ context[:ascii_input] = value.to_s
43
+ return failure("Not ASCII") if context[:ascii_input] !~ REGEX
44
+ context[:ascii] = context[:ascii_input]
45
+
46
+ self
47
+ end
48
+
49
+ def mapped
50
+ context[:ascii]
51
+ end
52
+ end
53
+
54
+ Login = Email.or_a(Phone)
55
+ end
56
+ end
57
+
58
+ describe "Errors composition with Tram::Policy::Errors" do
59
+ context "when using `OR` composition" do
60
+ subject { Test::Login.match(value) }
61
+
62
+ context "when value is invalid" do
63
+ let(:value) { "tasf" }
64
+ let(:errors_list) do
65
+ [
66
+ { Test::Login => [kind_of(Tram::Policy::Errors)] },
67
+ { Test::Email => [kind_of(Tram::Policy::Errors)] },
68
+ { Test::Phone => [kind_of(Tram::Policy::Errors)] }
69
+ ]
70
+ end
71
+ let(:policy_errors) do
72
+ [
73
+ kind_of(Tram::Policy::Errors),
74
+ kind_of(Tram::Policy::Errors)
75
+ ]
76
+ end
77
+ let(:validation_context) { { phone_input: value, email_input: value } }
78
+ let(:messages) do
79
+ [
80
+ "Value `tasf` is not a valid phone",
81
+ "Given value is not a valid email"
82
+ ]
83
+ end
84
+
85
+ it do
86
+ is_expected.to be_invalid
87
+ expect { subject.unpack }.not_to raise_error
88
+ expect(subject.errors).to match_array(errors_list)
89
+ expect(subject.policy_errors).to match(policy_errors)
90
+ expect(subject.messages).to match_array(messages)
91
+ expect(subject.context).to include(validation_context)
92
+ end
93
+ end
94
+ end
95
+
96
+ context "when using `AND` composition" do
97
+ subject { Test::RegistrationInput.match(email, phone) }
98
+
99
+ before do
100
+ module Test
101
+ class RegistrationInput < ::BC::Ext::Tuple
102
+ attribute :login, Login
103
+ attribute :password, Ascii
104
+ end
105
+ end
106
+ end
107
+
108
+ context "when input is invalid" do
109
+ let(:email) { "admin" }
110
+ let(:phone) { "not_a_phone" }
111
+ let(:login_error) { { Test::Login => [kind_of(Tram::Policy::Errors)] } }
112
+ let(:password) { "newP@ssw0rd" }
113
+ let(:attributes) do
114
+ attribute_errors.merge(password: kind_of(Test::Ascii))
115
+ end
116
+ let(:attribute_errors) do
117
+ {
118
+ login: kind_of(BC::PolicyFailure),
119
+ base: kind_of(BC::TuplePolicyFailure)
120
+ }
121
+ end
122
+ let(:errors) do
123
+ [
124
+ { Test::Email => [kind_of(Tram::Policy::Errors)] },
125
+ { Test::Phone => [kind_of(Tram::Policy::Errors)] },
126
+ { Test::Login => [kind_of(Tram::Policy::Errors)] },
127
+ { Test::RegistrationInput => [kind_of(Tram::Policy::Errors)] }
128
+ ]
129
+ end
130
+ let(:attribute_messages) do
131
+ {
132
+ login: [
133
+ "Given value is not a valid email",
134
+ "Value `admin` is not a valid phone"
135
+ ],
136
+ base: ["Data for registration is invalid"]
137
+ }
138
+ end
139
+
140
+ it do
141
+ expect(subject).to be_invalid
142
+ expect(subject.attributes).to match(attributes)
143
+ expect(subject.to_h).to match(hash_including(login: login_error))
144
+ expect(subject.errors).to match_array(errors)
145
+ expect(subject.attribute_errors).to match(attribute_errors)
146
+ expect(subject.attribute_messages).to match(attribute_messages)
147
+ end
148
+ end
149
+ end
150
+ end
151
+ end
@@ -0,0 +1,138 @@
1
+ RSpec.describe "BC::Ext::Refined validation delegated to Tram::Policy" do
2
+ before do
3
+ module Test
4
+ class AddressPolicy < Tram::Policy
5
+ option :country_code
6
+ option :city
7
+ option :street
8
+
9
+ validate :city_correctness
10
+ validate :country_code_correctness
11
+ validate :street_correctness
12
+
13
+ def country_code_correctness
14
+ return if country_code.to_s.size == 2
15
+ errors.add :invalid_country_code, value: country_code
16
+ end
17
+
18
+ def city_correctness
19
+ return if (2..30).cover?(city.to_s.size)
20
+ errors.add :invalid_city_name, value: city
21
+ end
22
+
23
+ def street_correctness
24
+ return if (5..75).cover?(street.to_s.size)
25
+ errors.add :invalid_street, value: street
26
+ end
27
+ end
28
+
29
+ class AddressType < BC::Ext::Refined
30
+ self.policy = AddressPolicy
31
+
32
+ extract :city
33
+ extract :country_code, method_name: :country
34
+ extract :street
35
+
36
+ def city
37
+ return value.city if value.respond_to?(:city)
38
+ stringify_hash_keys(value.to_h)
39
+ .values_at("city", "City")
40
+ .compact
41
+ .first
42
+ end
43
+
44
+ def country
45
+ return value.country if value.respond_to?(:country)
46
+ stringify_hash_keys(value.to_h)
47
+ .values_at("country", "country_code", "CountryCode")
48
+ .compact
49
+ .first
50
+ end
51
+
52
+ def street
53
+ return value.street if value.respond_to?(:street)
54
+ stringify_hash_keys(value.to_h)
55
+ .values_at("street", "street_line", "StreetLine")
56
+ .compact
57
+ .first
58
+ end
59
+
60
+ private def stringify_hash_keys(hash)
61
+ hash.reduce({}) { |a, (k, v)| a.merge!(k.to_s => v) }
62
+ end
63
+
64
+ end
65
+
66
+ Address = Struct.new(:country, :city, :street)
67
+ end
68
+ end
69
+
70
+ subject { Test::AddressType.match(value) }
71
+
72
+ context "when input is valid address" do
73
+ let(:mapped_data) do
74
+ { city: "Moscow", country_code: "RU", street: "ul. Novoslobodskaya" }
75
+ end
76
+
77
+ context "when value is an Test::Address" do
78
+ let(:value) do
79
+ Test::Address.new("RU", "Moscow", "ul. Novoslobodskaya")
80
+ end
81
+
82
+ it do
83
+ expect(subject).to be_valid
84
+ expect(subject).to be_kind_of(Test::AddressType)
85
+ expect(subject.unpack).to match(mapped_data)
86
+ end
87
+ end
88
+
89
+ context "when value is a parsed json" do
90
+ let(:json) do
91
+ '{"CountryCode": "RU", '\
92
+ '"City": "Moscow", '\
93
+ '"StreetLine": "ul. Novoslobodskaya"}'
94
+ end
95
+ let(:value) { JSON.parse(json) }
96
+
97
+ it do
98
+ expect(subject).to be_valid
99
+ expect(subject).to be_kind_of(Test::AddressType)
100
+ expect(subject.unpack).to match(mapped_data)
101
+ end
102
+ end
103
+ end
104
+
105
+ context "when input is invalid address" do
106
+ let(:mapped_data) do
107
+ { Test::AddressType => [kind_of(Tram::Policy::Errors)] }
108
+ end
109
+ let(:error_messages) do
110
+ ["The value `M` is not a valid city name",
111
+ "The value `RUS` is not a valid ISO country code",
112
+ "The value `ul` is not a valid street"]
113
+ end
114
+
115
+ context "when value is an Test::Address" do
116
+ let(:value) { Test::Address.new("RUS", "M", "ul") }
117
+
118
+ it do
119
+ expect(subject).to be_invalid
120
+ expect(subject).to be_kind_of(BC::PolicyFailure)
121
+ expect(subject.unpack).to match(mapped_data)
122
+ expect(subject.messages).to match(error_messages)
123
+ end
124
+ end
125
+
126
+ context "when value is a parsed json" do
127
+ let(:json) { '{"country": "RUS", "city": "M", "street": "ul"}' }
128
+ let(:value) { JSON.parse(json) }
129
+
130
+ it do
131
+ expect(subject).to be_invalid
132
+ expect(subject).to be_kind_of(BC::PolicyFailure)
133
+ expect(subject.unpack).to match(mapped_data)
134
+ expect(subject.messages).to match(error_messages)
135
+ end
136
+ end
137
+ end
138
+ end
@@ -0,0 +1,19 @@
1
+ ---
2
+ en:
3
+ bad: Something bad has happened
4
+ contracts:
5
+ test/email:
6
+ invalid_email: Given value is not a valid email
7
+ test/phone:
8
+ invalid_phone: Value `%{phone_input}` is not a valid phone
9
+ test/login:
10
+ no_matches: Data for login is invalid
11
+ test/registration_input:
12
+ invalid_tuple: Data for registration is invalid
13
+ test/plain_text_error:
14
+ message: "Service responded with a message: `%{plain_text}`"
15
+ tram-policy:
16
+ test/address_policy:
17
+ invalid_city_name: The value `%{value}` is not a valid city name
18
+ invalid_country_code: The value `%{value}` is not a valid ISO country code
19
+ invalid_street: The value `%{value}` is not a valid street
@@ -0,0 +1,24 @@
1
+ require "bundler/setup"
2
+ require "blood_contracts/ext"
3
+
4
+ require_relative "support/fixtures_helper.rb"
5
+
6
+ RSpec.configure do |config|
7
+ # Enable flags like --only-failures and --next-failure
8
+ config.example_status_persistence_file_path = ".rspec_status"
9
+
10
+ # Disable RSpec exposing methods globally on `Module` and `main`
11
+ config.disable_monkey_patching!
12
+
13
+ config.expect_with :rspec do |c|
14
+ c.syntax = :expect
15
+ end
16
+
17
+ config.around do |example|
18
+ I18n.available_locales = %w[en]
19
+ I18n.backend.store_translations :en, yaml_fixture_file("en.yml")["en"]
20
+ module Test; end
21
+ example.run
22
+ Object.send(:remove_const, :Test)
23
+ end
24
+ end
@@ -0,0 +1,11 @@
1
+ def fixture_file_path(filename)
2
+ File.expand_path "spec/fixtures/#{filename}"
3
+ end
4
+
5
+ def yaml_fixture_file(filename)
6
+ YAML.load_file(fixture_file_path(filename))
7
+ end
8
+
9
+ def load_fixture(filename)
10
+ load fixture_file_path(filename)
11
+ end
metadata ADDED
@@ -0,0 +1,202 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: blood_contracts-ext
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Sergey Dolganov (sclinede)
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2019-07-04 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: blood_contracts-core
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '0.4'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '0.4'
27
+ - !ruby/object:Gem::Dependency
28
+ name: tram-policy
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '2.0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '2.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: i18n
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '1.0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '1.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: bundler
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '2.0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '2.0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: pry
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: rake
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '10.0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '10.0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: rspec
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: '3.0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: '3.0'
111
+ - !ruby/object:Gem::Dependency
112
+ name: rubocop
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - "~>"
116
+ - !ruby/object:Gem::Version
117
+ version: '0.49'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - "~>"
123
+ - !ruby/object:Gem::Version
124
+ version: '0.49'
125
+ description: Extra helpers for BloodContracts::Core
126
+ email:
127
+ - sclinede@evilmartians.com
128
+ executables: []
129
+ extensions: []
130
+ extra_rdoc_files:
131
+ - CODE_OF_CONDUCT.md
132
+ - README.md
133
+ - LICENSE
134
+ - CHANGELOG.md
135
+ files:
136
+ - ".gitignore"
137
+ - ".rspec"
138
+ - ".rubocop.yml"
139
+ - ".travis.yml"
140
+ - CHANGELOG.md
141
+ - CODE_OF_CONDUCT.md
142
+ - Gemfile
143
+ - LICENSE
144
+ - README.md
145
+ - Rakefile
146
+ - bin/console
147
+ - bin/setup
148
+ - blood_contracts-ext.gemspec
149
+ - lib/blood_contracts/core/defineable_error.rb
150
+ - lib/blood_contracts/core/exception_caught.rb
151
+ - lib/blood_contracts/core/exception_handling.rb
152
+ - lib/blood_contracts/core/expected_error.rb
153
+ - lib/blood_contracts/core/extractable.rb
154
+ - lib/blood_contracts/core/map_value.rb
155
+ - lib/blood_contracts/core/policy_failure.rb
156
+ - lib/blood_contracts/core/sum_policy_failure.rb
157
+ - lib/blood_contracts/core/tuple_policy_failure.rb
158
+ - lib/blood_contracts/ext.rb
159
+ - lib/blood_contracts/ext/pipe.rb
160
+ - lib/blood_contracts/ext/refined.rb
161
+ - lib/blood_contracts/ext/sum.rb
162
+ - lib/blood_contracts/ext/tuple.rb
163
+ - spec/blood_contracts/ext/exception_caught_spec.rb
164
+ - spec/blood_contracts/ext/expected_error_spec.rb
165
+ - spec/blood_contracts/ext/map_value_spec.rb
166
+ - spec/blood_contracts/ext/policy_failure_spec.rb
167
+ - spec/blood_contracts/ext/policy_spec.rb
168
+ - spec/fixtures/en.yml
169
+ - spec/spec_helper.rb
170
+ - spec/support/fixtures_helper.rb
171
+ homepage: https://github.com/sclinede/blood_contracts-ext
172
+ licenses:
173
+ - MIT
174
+ metadata: {}
175
+ post_install_message:
176
+ rdoc_options: []
177
+ require_paths:
178
+ - lib
179
+ required_ruby_version: !ruby/object:Gem::Requirement
180
+ requirements:
181
+ - - ">="
182
+ - !ruby/object:Gem::Version
183
+ version: '2.4'
184
+ required_rubygems_version: !ruby/object:Gem::Requirement
185
+ requirements:
186
+ - - ">="
187
+ - !ruby/object:Gem::Version
188
+ version: '0'
189
+ requirements: []
190
+ rubygems_version: 3.0.3
191
+ signing_key:
192
+ specification_version: 4
193
+ summary: Extra helpers for BloodContracts::Core
194
+ test_files:
195
+ - spec/blood_contracts/ext/exception_caught_spec.rb
196
+ - spec/blood_contracts/ext/expected_error_spec.rb
197
+ - spec/blood_contracts/ext/map_value_spec.rb
198
+ - spec/blood_contracts/ext/policy_failure_spec.rb
199
+ - spec/blood_contracts/ext/policy_spec.rb
200
+ - spec/fixtures/en.yml
201
+ - spec/spec_helper.rb
202
+ - spec/support/fixtures_helper.rb