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.
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}"