blood_contracts-ext 0.1.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.
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