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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6b07e48354690944459676628a2a1e954fc39bb3751cb8245c4e66ca9f3bfa71
4
- data.tar.gz: db15168f735faead06ebf84ecf46c3554c44bea67da669e2e48d6631f5586cac
3
+ metadata.gz: 7c89ea50c7af537eb81bfd8f949165ffaffde2bdeb9aec92668e601acc90802c
4
+ data.tar.gz: 6c85bee20832ba39fc933987823b1dcbc7583ab0f991599837a2f0625d74d826
5
5
  SHA512:
6
- metadata.gz: 04af6db1580a874e06ed3f1c63bea6741e21cdd6f107e7b44364c3dedf06114ac0d09ad4e9555d1719125c8ed42a5b32b40d35df5e33fcc44da62dec82b0e79f
7
- data.tar.gz: 2e21d3eec2392af4729752b5099039eafc9b9709bf240768dd3beec023e03581b02c89dc4fb87f95f0b6b9b601a703e6f3aa3df5ff2386817006638c9cd7f0ad
6
+ metadata.gz: fe7e788aac1df68e58bd5e28f2dfcf875f5b47deca6b0012cd309b81749f147b712290608d513bf97421f5064813e3a44a14888d63c774ac8f01cfa0027c27c0
7
+ data.tar.gz: 0a349cdc0f9f668fe7cda58a3531e322f6b76ee8116640ce4aad9e25d7ae58680f8a3c00cadc80c202b525974d6a66637b0c35ec5e7947a93d9e70bb375af061
data/.rubocop.yml ADDED
@@ -0,0 +1,31 @@
1
+ ---
2
+
3
+ AllCops:
4
+ DisplayCopNames: true
5
+ DisplayStyleGuide: true
6
+ StyleGuideCopsOnly: true
7
+ TargetRubyVersion: 2.4
8
+ Exclude:
9
+ - examples/*
10
+ - blood_contracts-core.gemspec
11
+ - vendor/bundle/**/*
12
+
13
+ Metrics/LineLength:
14
+ AllowHeredoc: true
15
+ AllowURI: true
16
+ URISchemes:
17
+ - http
18
+ - https
19
+
20
+ Style/ClassAndModuleChildren:
21
+ Enabled: false
22
+
23
+ Style/Documentation:
24
+ Enabled: false
25
+
26
+ Style/StringLiterals:
27
+ EnforcedStyle: double_quotes
28
+
29
+ Naming/FileName:
30
+ Exclude:
31
+ - lib/blood_contracts-core.rb
data/.travis.yml CHANGED
@@ -1,7 +1,19 @@
1
1
  ---
2
- sudo: false
2
+ sudo: false
3
3
  language: ruby
4
- cache: bundler
4
+ cache: bundler
5
+ before_install:
6
+ - gem install bundler --no-document
7
+ - gem update --system
8
+ script:
9
+ - bundle exec rspec
10
+ - bundle exec rubocop
5
11
  rvm:
6
- - 2.6.2
7
- before_install: gem install bundler -v 2.0.1
12
+ - 2.4.0
13
+ - 2.6.0
14
+ - ruby-head
15
+ - jruby-head
16
+ matrix:
17
+ allow_failures:
18
+ - rvm: ruby-head
19
+ - rvm: jruby-head
data/CHANGELOG.md ADDED
@@ -0,0 +1,14 @@
1
+ # Change Log
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](http://keepachangelog.com/)
6
+ and this project adheres to [Semantic Versioning](http://semver.org/).
7
+
8
+ ## [0.4.0] - [2019-06-25]
9
+
10
+ This is a first public release marked in change log with features extracted from production app.
11
+ Includes:
12
+ - Base class BloodContracs::Core::Refined to write your own validations
13
+ - Meta classes BloodContracs::Core::Pipe, BloodContracs::Core::Sum and BloodContracs::Core::Tuple for validations composition in ADT style
14
+ - BloodContracs::Core::Contract meta class as a syntactic sugar to define your own contracts with Refined validations under the hood
data/README.md CHANGED
@@ -1,7 +1,56 @@
1
+ [![Build Status](https://travis-ci.org/sclinede/blood_contracts-core.svg?branch=master)][travis]
2
+ [![Code Climate](https://codeclimate.com/github/sclinede/blood_contracts-core/badges/gpa.svg)][codeclimate]
3
+
4
+ [gem]: https://rubygems.org/gems/blood_contracts-core
5
+ [travis]: https://travis-ci.org/sclinede/blood_contracts-core
6
+ [codeclimate]: https://codeclimate.com/github/sclinede/blood_contracts-core
7
+ [adt_wiki]: https://en.wikipedia.org/wiki/Algebraic_data_type
8
+ [functional_programming_wiki]: https://en.wikipedia.org/wiki/Functional_programming
9
+ [refinement_types_wiki]: https://en.wikipedia.org/wiki/Refinement_type
10
+ [ebaymag]: https://ebaymag.com/
11
+
1
12
  # BloodContracts::Core
2
13
 
3
- Simple implementation of Refinement Types and Contract (based on types).
4
- Would be the basement for the BloodContracts API production testing framework.
14
+ Simple and agile Ruby data validation tool inspired by refinement types and functional approach
15
+
16
+ * **Powerful**. [Algebraic Data Type][adt_wiki] guarantees that gem is enough to implement any kind of complex data validation, while [Functional Approach][functional_programming_wiki] gives you full control over validation outcomes
17
+ * **Simple**. You could write your first [Refinment Type][refinement_types_wiki] as simple as single Ruby method in single class
18
+ * **Independent**. It have no dependencies and you need nothing more to write your complex validations
19
+ * **Rubyish**. DSL is inspired by Ruby Struct. If you love Ruby way you'd like the BloodContracts types
20
+ * **Born in production**. Created on basis of [eBaymag][ebaymag] project, used as a tool to control and monitor data inside API communication
21
+
22
+ ```ruby
23
+ # Write your "types" as simple as...
24
+ class Email < ::BC::Refined
25
+ REGEX = /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i
26
+
27
+ def match
28
+ return if (context[:email] = value.to_s) =~ REGEX
29
+ failure(:invalid_email)
30
+ end
31
+ end
32
+
33
+ class Phone < ::BC::Refined
34
+ REGEX = /\A(\+7|8)(9|8)\d{9}\z/i
35
+
36
+ def match
37
+ return if (context[:phone] = value.to_s) =~ REGEX
38
+ failure(:invalid_phone)
39
+ end
40
+ end
41
+
42
+ # ... compose them...
43
+ Login = Email.or_a(Phone)
44
+
45
+ # ... and match!
46
+ case match = Login.match("not-a-login")
47
+ when Phone, Email
48
+ match # use as you wish, you exactly know what kind of login you received
49
+ when BC::ContractFailure # translate error message
50
+ match.messages # => [:no_matches, :invalid_phone, :invalid_email]
51
+ else raise # to make sure you covered all scenarios (Functional Way)
52
+ end
53
+ ```
5
54
 
6
55
  ## Installation
7
56
 
@@ -19,15 +68,324 @@ Or install it yourself as:
19
68
 
20
69
  $ gem install blood_contracts-core
21
70
 
22
- ## Usage
71
+ ## Refinment Data Type (BC::Refined class)
72
+
73
+ Refinement type is an Algebraic Data Type (read, you could compose it with other types) with some predicate to check against the data.
74
+ In Ruby we've implemented it as a class with method `.match` which accepts single argument - _value_ which could be any kind of object.
75
+ This method ALWAYS returns ancestor of BC::Refined. So the most common usage would be:
76
+
77
+ ```ruby
78
+ case match = RegistrationFormType.match(params)
79
+ when RegistrationFormType
80
+ match.to_h # converts your data to valid Ruby hash
81
+ when BC::ContractFailure
82
+ match.messages # deal with error messages
83
+ else raise # remember the matching should be exhaustive (simplifies debugging, I promise 🙏)
84
+ end
85
+ ```
86
+
87
+ To create your first type just inherit class from BC::Refined and implement method `#match`.
88
+
89
+ The method should:
90
+ - return self or nil on successful validation
91
+ - return BC::ContractFailure instance by calling method `#failure` and provide error text/symbol
92
+
93
+ ```ruby
94
+ require 'countries' # gem with data about countries
95
+ class Country < BC::Refined
96
+ def match
97
+ return if ISO3166::Country.find_country_by_alpha2(context[:country_name] = value.to_s)
98
+ failure(:unknown_country)
99
+ end
100
+ end
101
+ ```
102
+
103
+ Also, you could improve the successful outcome by mapping VALID data to something more appropriate, for example you could normalize data. For that you need only implement `#mapped`
104
+
105
+ ```ruby
106
+ require 'countries' # gem with data about countries
107
+ class Country < BC::Refined
108
+ def match
109
+ context[:country_string] = value.to_s
110
+ context[:found_country] = ISO3166::Country.find_country_by_alpha2(context[:country_string])
111
+ return if context[:found_country]
112
+ failure(:unknown_country)
113
+ end
114
+
115
+ def mapped
116
+ context[:found_country].name
117
+ end
118
+ end
119
+
120
+ case match = Country.call("CI")
121
+ when Country
122
+ match.unpack # => "Côte d'Ivoire"
123
+ when BC::ContractFailure
124
+ match.messages # => [:unknown_country]
125
+ else raise # ... you know why
126
+ end
127
+ ```
128
+
129
+ Okay, we passed through single value validation. How about complex cases?
130
+
131
+ Imagine you want to validate response from some JSON API, let's write your first API client with refinement types together.
132
+
133
+ For this example we'll create RubygemsAPI client:
134
+
135
+ ```ruby
136
+ require 'net/http'
137
+
138
+ module RubygemsAPI
139
+ class Client
140
+ ROOT = "https://rubygems.org/api/v1/gems/".freeze
141
+
142
+ def self.get(path)
143
+ uri = URI.parse(File.join(ROOT, path))
144
+ http = Net::HTTP.new(uri.host, uri.port)
145
+ http.use_ssl = true
146
+ http.get(uri.request_uri).body
147
+ end
148
+
149
+ def self.gem(name)
150
+ Validation.call get("#{name}.json")
151
+ end
152
+ end
153
+ end
154
+ ```
155
+
156
+ But what is the RubygemsAPI::Validation class?
157
+
158
+ ## "And Then" Composition (BC::Pipe class)
159
+
160
+ Our API client just reads a document from the Internet, which is why first we need to parse it as JSON and then extract something useful.
161
+ This is where `#and_then` method quite useful. It runs validation over first BC::Refined and only if the first validation was successful calls the other one.
162
+ Otherwise we'll just receive `BC::ContractFailure`, you know.
163
+
164
+ Our first challenge is to read Ruby gem info from the API, so we need two types: Json (for parsing) and Gem (for gem info)
165
+
166
+ ```ruby
167
+ module RubygemsAPI
168
+ require 'json'
169
+
170
+ class Json < BC::Refined
171
+ def match
172
+ # now it's easy to understand why we caught JSON::ParserError
173
+ context[:response] = value.to_s
174
+ context[:parsed] = ::JSON.parse(context[:response])
175
+ self
176
+ rescue JSON::ParserError => ex
177
+ context[:exception] = ex # now we could easily playaround with exception and reraise it
178
+ failure(:invalid_json)
179
+ end
180
+
181
+ # so the next validation in the pipe will receive parsed response, not unparsed string
182
+ def mapped
183
+ context[:parsed]
184
+ end
185
+ end
186
+
187
+ class GemInfo < BC::Refined
188
+ # I chose some data that is interesing for me
189
+ INFO_KEYS = %w(name downloads info authors version homepage_uri source_code_uri)
190
+
191
+ def match
192
+ # We have to make sure that result is a hash with appropriate keys
193
+ is_a_project = value.is_a?(Hash) && (INFO_KEYS - value.keys).empty?
194
+ return failure(:reponse_is_not_gem_info) unless is_a_project
195
+
196
+ context[:gem_info] = value.slice(*INFO_KEYS)
197
+ self
198
+ end
199
+
200
+ def mapped
201
+ context[:gem_info]
202
+ end
203
+ end
204
+
205
+ # Simple "and_then" composition will look like that:
206
+ Validation = Json.and_then(GemInfo)
207
+ end
208
+ ```
209
+
210
+ Let's test our API client!
211
+
212
+ ```ruby
213
+ gem = RubygemsAPI::Client.gem("rack") # => #<RubygemAPI::GemInfo ...>
214
+ gem.unpack # => {"name" => ..., "authors" => ...}
215
+ ```
216
+
217
+ Nice!
218
+ But wait, what if we misspelled gem name?
219
+
220
+ ```ruby
221
+ gem = RubygemsAPI::Client.gem("big-bada-bum") # => #<BC::ContractFailure ...>
222
+ gem.messages # => [:invalid_json]
223
+ # hmmm, wait... what?
224
+ gem.context[:response] # => "This rubygem could not be found."
225
+ # it is plain text. yes. :(
226
+ ```
227
+
228
+ It would be great to show that original message to our user, but how?
229
+
230
+
231
+ ## "Or" Composition (BC::Sum class)
232
+
233
+ Actually, we could add another type in our validation using "Or" composition. Use it by calling `#or_a` / `#or_an` method on your BC::Refined class.
234
+ Let's try:
235
+
236
+ ```ruby
237
+ module RubygemsAPI
238
+ # ...
239
+
240
+ class PlainTextError < BC::Refined
241
+ def match
242
+ context[:response] = value.to_s
243
+ # to avoid multiple parsing of response, we'll try to save it
244
+ context[:parsed] = JSON.parse(context[:response])
245
+ failure(:non_plain_text_response)
246
+ rescue JSON::ParserError
247
+ self
248
+ end
249
+
250
+ def mapped
251
+ context[:response]
252
+ end
253
+ end
254
+
255
+ Validation = PlainTextError.or_a(Json.and_then(GemInfo))
256
+ end
257
+ ```
258
+
259
+ Let's test our API client, again!
260
+
261
+ ```ruby
262
+ gem = RubygemsAPI::Client.gem("rack") # => #<RubygemAPI::GemInfo ...>
263
+ gem.unpack # => {"name" => ..., "authors" => ...}
264
+
265
+ # good, but how about not found case?
266
+ gem = RubygemsAPI::Client.gem("big-bada-bum") # => #<RubygemAPI::PlainTextError ...>
267
+ gem.unpack # => "This rubygem could not be found."
268
+ ```
269
+
270
+ And of course we could use it in a case statement:
271
+ ```ruby
272
+ case gem = RubygemsAPI::Client.gem("rack")
273
+ when GemInfo
274
+ gem.unpack # show data to user
275
+ when PlaintTextError
276
+ {message: gem.unpack, status: 400} # wrap it into json response
277
+ when BC::ContractFailure
278
+ match.messages
279
+ else raise # ... you know why
280
+ end
281
+ ```
282
+
283
+ It was a nice run!
284
+
285
+ So actually only one other scenario left to show.
286
+
287
+ Do you remember the Login type from the beginning? Let's try to implement simple registration form validation.
288
+
289
+ ## "And" Composition (BC::Tuple class)
290
+
291
+ If you'll try to represent complex data with refinement types the best tool is "and" composition or "product" of types. Sounds wierd?
292
+
293
+ But you actually work with that concept all the time. It's just a record or struct.
294
+
295
+ Let's write our registration form validation with a Struct:
296
+
297
+ ```ruby
298
+ RegistrationForm = Struct.new(:login, :password) do
299
+ def self.call(login, password)
300
+ # validation logic
301
+ end
302
+ end
303
+ ```
304
+
305
+ So, the BloodContracts version will look the same, except you only need to implement types for login and password:
306
+
307
+ ```ruby
308
+ module Registration
309
+ class Email < ::BC::Refined
310
+ REGEX = /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i
311
+
312
+ def match
313
+ context[:email_input] = value.to_s
314
+ return failure(:invalid_email) unless context[:email_input] =~ REGEX
315
+ context[:email] = context[:email_input]
316
+ self
317
+ end
318
+ end
319
+
320
+ class Phone < ::BC::Refined
321
+ REGEX = /\A(\+7|8)(9|8)\d{9}\z/i
322
+
323
+ def match
324
+ context[:phone_input] = value.to_s
325
+ return failure(:invalid_phone) unless context[:phone_input] =~ REGEX
326
+ context[:phone] = context[:phone_input]
327
+ self
328
+ end
329
+ end
330
+
331
+ class Ascii < ::BC::Refined
332
+ REGEX = /^[[:ascii:]]+$/i
333
+
334
+ def match
335
+ context[:ascii_input] = value.to_s
336
+ return failure(:not_ascii) unless context[:ascii_input] =~ REGEX
337
+ context[:ascii_string] = context[:ascii_input]
338
+ self
339
+ end
340
+ end
341
+
342
+ # Create meta class as the Struct.new
343
+ Form = BC::Tuple.new do
344
+ # defines a reader and applies validation on `.match` call
345
+ attribute :login, Email.or_a(Phone)
346
+ attribute :password, Ascii
347
+ end
348
+ end
349
+ ```
350
+
351
+ And the code that you'll put in your controller is something like that:
352
+
353
+ ```ruby
354
+ class RegistrationController < ActionController::Base
355
+ def create
356
+ case match = Registration::Form.match(params)
357
+ when Registration::Form
358
+ # login here is either Phone or Email
359
+ # password here is always ASCII only string
360
+ user = User.find_or_create!(login: match.login) do |user|
361
+ user.password = match.password
362
+ user.email = match.context[:email]
363
+ user.phone = match.context[:phone]
364
+ end
365
+ render json: {code: 200, user_id: user.id, message: "User was successfully created!"}
366
+ when BC::ContractFailure
367
+ message = match.messages.map(&I18n.method(:t)).join("\n")
368
+ render json: {code: 400, message: message}
369
+ else
370
+ Honeybadger.notify("Invalid BloodContracts usage", context: match.inspect)
371
+ render json: {code: 500, message: "Unexpected contract behavior. Fix me ASAP"}
372
+ end
373
+ end
374
+ end
375
+ ```
376
+
377
+ Now, you're ready to write any kind of complex data validation with BloodContracts
378
+
379
+ What are the next steps?
23
380
 
24
- TODO: Write usage instructions here
381
+ Soon we'll announce `blood_contracts-extended` and `blood_contracts-monitoring`, which will help you monitor the data (what types and how often matches in your system) and
382
+ even collect for you unique samples of the communication (up to the types that matched).
25
383
 
26
384
  ## Development
27
385
 
28
386
  After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
29
387
 
30
- To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
388
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `gemspec`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
31
389
 
32
390
  ## Contributing
33
391
 
data/Rakefile CHANGED
@@ -3,4 +3,4 @@ require "rspec/core/rake_task"
3
3
 
4
4
  RSpec::Core::RakeTask.new(:spec)
5
5
 
6
- task :default => :spec
6
+ task default: :spec
@@ -1,30 +1,23 @@
1
+ Gem::Specification.new do |gem|
2
+ gem.name = "blood_contracts-core"
3
+ gem.version = "0.4.0"
4
+ gem.authors = ["Sergey Dolganov (sclinede)"]
5
+ gem.email = ["sclinede@evilmartians.com"]
1
6
 
2
- lib = File.expand_path("../lib", __FILE__)
3
- $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
- require "blood_contracts/core/version"
7
+ gem.summary = "Core classes for data validation with contracts approach"
8
+ gem.description = "Core classes for data validation with contracts approach (using Either + Writer monad combination & ADT for composition)"
9
+ gem.homepage = "https://github.com/sclinede/blood_contracts-core"
10
+ gem.license = "MIT"
5
11
 
6
- Gem::Specification.new do |spec|
7
- spec.name = "blood_contracts-core"
8
- spec.version = BloodContracts::Core::VERSION
9
- spec.authors = ["Sergey Dolganov"]
10
- spec.email = ["sclinede@evilmartians.com"]
12
+ gem.files = `git ls-files`.split($INPUT_RECORD_SEPARATOR)
13
+ gem.test_files = gem.files.grep(/^spec/)
14
+ gem.extra_rdoc_files = Dir["CODE_OF_CONDUCT.md", "README.md", "LICENSE", "CHANGELOG.md"]
11
15
 
12
- spec.summary = %q{Core classes for Contracts API validation}
13
- spec.description = %q{Core classes for Contracts API validation}
14
- spec.homepage = "https://github.com/sclinede/blood_contracts-core"
15
- spec.license = "MIT"
16
+ gem.required_ruby_version = ">= 2.4"
16
17
 
17
- # Specify which files should be added to the gem when it is released.
18
- # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
19
- spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
20
- `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
21
- end
22
- spec.bindir = "bin/"
23
- spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
24
- spec.require_paths = ["lib"]
25
-
26
- spec.add_development_dependency "bundler", "~> 2.0"
27
- spec.add_development_dependency "pry"
28
- spec.add_development_dependency "rake", "~> 10.0"
29
- spec.add_development_dependency "rspec", "~> 3.0"
18
+ gem.add_development_dependency "bundler", "~> 2.0"
19
+ gem.add_development_dependency "pry"
20
+ gem.add_development_dependency "rake", "~> 10.0"
21
+ gem.add_development_dependency "rspec", "~> 3.0"
22
+ gem.add_development_dependency "rubocop", "~> 0.49"
30
23
  end
@@ -1,57 +1,51 @@
1
- require 'json'
2
- require 'blood_contracts/core'
1
+ require "json"
2
+ require "blood_contracts/core"
3
3
 
4
4
  module Types
5
5
  class JSON < BC::Refined
6
6
  def match
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
7
+ context[:parsed] = ::JSON.parse(value)
8
+ self
9
+ rescue StandardError => error
10
+ failure(error)
15
11
  end
16
12
 
17
- def unpack
18
- super { |match| match.context[:parsed] }
13
+ def mapped
14
+ context[:parsed]
19
15
  end
20
16
  end
21
17
 
22
18
  class Tariff < BC::Refined
23
19
  def match
24
- super do
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
- end
20
+ context[:data] = value.slice("cost", "cur").compact
21
+ context[:tariff_context] = 1
22
+ return if context[:data].size == 2
23
+
24
+ failure(:not_a_tariff)
30
25
  end
31
26
 
32
- def unpack
33
- super { |match| match.context[:data] }
27
+ def mapped
28
+ context[:data]
34
29
  end
35
30
  end
36
31
 
37
32
  class Error < BC::Refined
38
33
  def match
39
- super do
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)
44
- end
34
+ context[:data] = unpack_refined(value).slice("code", "message").compact
35
+ context[:known_error_context] = 1
36
+ return if context[:data].size == 2
37
+
38
+ failure(:not_a_known_error)
45
39
  end
46
40
 
47
- def unpack
48
- super { |match| match.context[:data] }
41
+ def mapped
42
+ context[:data]
49
43
  end
50
44
  end
51
45
 
52
46
  Response = BC::Pipe.new(
53
47
  BC::Anything, JSON, (Tariff | Error | Tariff),
54
- names: [:raw, :parsed, :mapped]
48
+ names: %i[raw parsed mapped]
55
49
  )
56
50
 
57
51
  # The same is
@@ -70,14 +64,6 @@ end
70
64
  def match_response(response)
71
65
  match = Types::Response.match(response)
72
66
  case match
73
- when BC::ContractFailure
74
- puts "Honeybadger.notify 'Unexpected behavior in Russian Post', context: #{match.context}"
75
- puts "render json: { errors: 'Ooops! Not working, we've been notified. Please, try again later' }"
76
-
77
- return unless ENV["RAISE"]
78
- match.errors.values.flatten.find do |v|
79
- raise v if StandardError === v
80
- end
81
67
  when Types::Tariff
82
68
  # работаем с тарифом
83
69
  puts "match.context # => #{match.context} \n\n"
@@ -86,8 +72,16 @@ def match_response(response)
86
72
  # работаем с ошибкой, e.g. адрес слишком длинный
87
73
  puts "match.context # => #{match.context} \n\n"
88
74
  puts "render json: { errors: [#{match.unpack['message']}] } }"
75
+ when BC::ContractFailure
76
+ puts "Honeybadger.notify 'Unexpected behavior in Russian Post', context: #{match.context}"
77
+ puts "render json: { errors: 'Ooops! Not working, we've been notified. Please, try again later' }"
78
+
79
+ return unless ENV["RAISE"]
80
+ match.errors.values.flatten.find do |v|
81
+ raise v if StandardError === v
82
+ end
89
83
  else
90
- require'pry';binding.pry
84
+ raise
91
85
  end
92
86
  end
93
87
 
@@ -98,16 +92,14 @@ valid_response = '{"cost": 1000, "cur": "RUB"}'
98
92
  match_response(valid_response)
99
93
  puts "#{'=' * 20}================================#{'=' * 20}"
100
94
 
101
-
102
95
  puts "\n\n\n"
103
96
  puts "#{'=' * 20} WHEN KNOWN API ERROR RESPONSE: #{'=' * 20}"
104
97
  error_response = '{"code": 123, "message": "Too Long Address"}'
105
98
  match_response(error_response)
106
99
  puts "#{'=' * 20}================================#{'=' * 20}"
107
100
 
108
-
109
101
  puts "ss => errors }\n\n\n"
110
102
  puts "#{'=' * 20} WHEN UNEXPECTED RESPONSE: #{'=' * 20}"
111
- invalid_response = '<xml>'
103
+ invalid_response = "<xml>"
112
104
  match_response(invalid_response)
113
105
  puts "#{'=' * 20}================================#{'=' * 20}"