blood_contracts-core 0.4.1 → 0.4.2

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: 7f3ff63364178ca281b22b2ca291904ce9a70ec47056a41f9a622dd9a00c9a55
4
- data.tar.gz: b5b000306c85eb7871fb3f4ad1c7a96d4fd8dbc8ceeab98bab49f50a78f611b0
3
+ metadata.gz: 5a95e70463f9422ef654272494ae21d3b2440f6df9f2ec93fce292d1760bf8a5
4
+ data.tar.gz: 38cae2fd0e55b1e4a32fbbafa455659add37c78ed6c03c1f250166d426bff362
5
5
  SHA512:
6
- metadata.gz: d84a0a438d5d264240e40ef8ba79f2732bcfa3f81d11533e9131b175887197c87e0ed84931126cf7aaa897c6fc7b68b7c0b17fc5ea49c03a383fec86ada6c658
7
- data.tar.gz: 614ed9eb7021fc53fc0e55384868599afcfefb84437cf52a6739cb84e615bc929a3e5ad15ead259629820b32627d2641ae0f6738e33dda05ba8aa6d6cc94a864
6
+ metadata.gz: ee8f1b8795c6906757a188e9714869420c31bdab2b77a43ef8ca299a31c9e95269c4d254e63f1cd6f425ad50fccd6d6568c7f0ace662e1c29eb1e28a8e7671b3
7
+ data.tar.gz: c7e613e87f3c9038094c15f0f0eda3bee294b69d55998ceed794d9c918065d7faff6a77bdb23760ae36e8ebc3b581de74fa26cc5b2dae34fc7deb9125c661355
data/CHANGELOG.md CHANGED
@@ -5,6 +5,13 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](http://keepachangelog.com/)
6
6
  and this project adheres to [Semantic Versioning](http://semver.org/).
7
7
 
8
+ ## master
9
+
10
+ Features:
11
+
12
+ - `BC::Tuple#argument` accepts an optional block to define the attribute class dynamically
13
+ - `BC::Tuple#match` can accept arguments as a Hash
14
+
8
15
  ## [0.4.1] - [2019-07-04]
9
16
 
10
17
  Fixes:
@@ -16,6 +23,6 @@ key elements. It violates principlke of least surprise.
16
23
 
17
24
  This is a first public release marked in change log with features extracted from production app.
18
25
  Includes:
19
- - Base class BloodContracs::Core::Refined to write your own validations
20
- - Meta classes BloodContracs::Core::Pipe, BloodContracs::Core::Sum and BloodContracs::Core::Tuple for validations composition in ADT style
21
- - BloodContracs::Core::Contract meta class as a syntactic sugar to define your own contracts with Refined validations under the hood
26
+ - Base class BloodContracts::Core::Refined to write your own validations
27
+ - Meta classes BloodContracts::Core::Pipe, BloodContracts::Core::Sum and BloodContracts::Core::Tuple for validations composition in ADT style
28
+ - BloodContracts::Core::Contract meta class as a syntactic sugar to define your own contracts with Refined validations under the hood
data/README.md CHANGED
@@ -344,10 +344,26 @@ module Registration
344
344
  # defines a reader and applies validation on `.match` call
345
345
  attribute :login, Email.or_a(Phone)
346
346
  attribute :password, Ascii
347
+ # defines an attribute using the anonymous type class
348
+ attribute :remember_me do
349
+ def match
350
+ value.to_s.in? ["checked", ""]
351
+ end
352
+
353
+ def mapped
354
+ value.to_s == "checked"
355
+ end
356
+ end
347
357
  end
348
358
  end
349
359
  ```
350
360
 
361
+ Tuple can accept either a list of arguments or a hash:
362
+ ```ruby
363
+ Registration::Form.match(login, password)
364
+ Registration::Form.match(login: login, password: password)
365
+ ```
366
+
351
367
  And the code that you'll put in your controller is something like that:
352
368
 
353
369
  ```ruby
@@ -1,6 +1,6 @@
1
1
  Gem::Specification.new do |gem|
2
2
  gem.name = "blood_contracts-core"
3
- gem.version = "0.4.1"
3
+ gem.version = "0.4.2"
4
4
  gem.authors = ["Sergey Dolganov (sclinede)"]
5
5
  gem.email = ["sclinede@evilmartians.com"]
6
6
 
@@ -1,3 +1,7 @@
1
+ require_relative "../ext/symbolize_keys.rb"
2
+
3
+ using BloodContracts::SymbolizeKeys
4
+
1
5
  module BloodContracts::Core
2
6
  # Meta refinement type, represents product of several refinement types
3
7
  class Tuple < Refined
@@ -20,6 +24,8 @@ module BloodContracts::Core
20
24
  #
21
25
  # rubocop:disable Style/SingleLineMethods
22
26
  def new(*args, **kwargs, &block)
27
+ args = lookup_hash_args(*args, kwargs)
28
+
23
29
  return super(*args, **kwargs) if @finalized
24
30
  names = args.pop.delete(:names) if args.last.is_a?(Hash)
25
31
 
@@ -34,10 +40,16 @@ module BloodContracts::Core
34
40
  # rubocop:enable Style/SingleLineMethods
35
41
 
36
42
  # Helper which registers attribute in the Tuple, also defines a reader
37
- def attribute(name, type)
38
- raise ArgumentError unless type < Refined
43
+ def attribute(name, type = nil, &block)
44
+ if type.nil?
45
+ type = type_from_block(name, &block)
46
+ else
47
+ raise ArgumentError unless type < Refined
48
+ end
49
+
39
50
  @attributes << type
40
51
  @names << name
52
+
41
53
  define_method(name) do
42
54
  match.context.dig(:attributes, name)
43
55
  end
@@ -56,6 +68,47 @@ module BloodContracts::Core
56
68
  new_klass.failure_klass ||= TupleContractFailure
57
69
  super
58
70
  end
71
+
72
+ private
73
+
74
+ # Generate an anonymous type from a block
75
+ #
76
+ # @param String name of the attribute to define the type for
77
+ #
78
+ # @param Proc block which will be evaluated in a context
79
+ # of our anonymous type
80
+ #
81
+ # @return Class
82
+ #
83
+ def type_from_block(name, &block)
84
+ raise ArgumentError unless block_given?
85
+
86
+ const_set(
87
+ "InlineType_#{name.to_s.capitalize}",
88
+ Class.new(Refined) { class_eval(&block) }
89
+ )
90
+ end
91
+
92
+ # Handle arguments passed as hash with string or
93
+ # symbol keys
94
+ #
95
+ # @param [Array<Object>] *args that can contain an array of arguments
96
+ # or a single Hash we're looking for
97
+ #
98
+ # @param [Hash<Object, Object>] **kwargs hash that can be converted to
99
+ # args when args array is empty
100
+ #
101
+ # @return [Array<Object>]
102
+ #
103
+ def lookup_hash_args(*args, **kwargs)
104
+ if args.empty?
105
+ [kwargs]
106
+ elsif args.length == 1 && args.last.is_a?(Hash)
107
+ [args.first.symbolize_keys]
108
+ else
109
+ args
110
+ end
111
+ end
59
112
  end
60
113
 
61
114
  # List of values in Tuple
@@ -0,0 +1,11 @@
1
+ module BloodContracts
2
+ module SymbolizeKeys
3
+ refine Hash do
4
+ def symbolize_keys
5
+ each_with_object({}) do |(key, value), acc|
6
+ acc[key.to_sym] = value
7
+ end
8
+ end
9
+ end
10
+ end
11
+ end
@@ -179,18 +179,111 @@ RSpec.describe BloodContracts::Core do
179
179
  subject { Test::RegistrationInput.match(email, password) }
180
180
 
181
181
  context "when valid input" do
182
+ shared_examples "is valid" do |options = {}|
183
+ it do
184
+ expect(subject).to be_valid
185
+ unless options[:without_attributes]
186
+ expect(subject.attributes).to match(attributes)
187
+ end
188
+ expect(subject.to_h).to match(email: email, password: password)
189
+ expect(subject.errors).to be_empty
190
+ expect(subject.attribute_errors).to be_empty
191
+ end
192
+ end
193
+
182
194
  let(:email) { "admin@mail.com" }
183
195
  let(:password) { "newP@ssw0rd" }
184
196
  let(:attributes) do
185
197
  { email: kind_of(Test::Email), password: kind_of(Test::Ascii) }
186
198
  end
187
199
 
188
- it do
189
- expect(subject).to be_valid
190
- expect(subject.attributes).to match(attributes)
191
- expect(subject.to_h).to match(email: email, password: password)
192
- expect(subject.errors).to be_empty
193
- expect(subject.attribute_errors).to be_empty
200
+ include_examples "is valid"
201
+
202
+ context "when attributes are defined inline" do
203
+ before do
204
+ module Test
205
+ class InlineRegistrationInput < ::BC::Tuple
206
+ attribute :email do
207
+ EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i
208
+
209
+ def match
210
+ context[:email] = unpack_refined(value).to_s
211
+ return if context[:email] =~ EMAIL_REGEX
212
+
213
+ failure("Not an email")
214
+ end
215
+
216
+ def mapped
217
+ context[:email]
218
+ end
219
+ end
220
+
221
+ attribute :password do
222
+ ASCII_REGEX = /^[[:ascii:]]+$/i
223
+
224
+ def match
225
+ context[:ascii_string] = value.to_s
226
+ return if context[:ascii_string] =~ ASCII_REGEX
227
+
228
+ failure("Not ASCII")
229
+ end
230
+
231
+ def mapped
232
+ context[:ascii_string]
233
+ end
234
+ end
235
+ end
236
+ end
237
+ end
238
+
239
+ subject { Test::InlineRegistrationInput.match(email, password) }
240
+
241
+ include_examples "is valid", without_attributes: true
242
+
243
+ context "when input is invalid" do
244
+ let(:email) { "admin" }
245
+ let(:dynamic_email_type) do
246
+ Test::InlineRegistrationInput::InlineType_Email
247
+ end
248
+ let(:dynamic_password_type) do
249
+ Test::InlineRegistrationInput::InlineType_Password
250
+ end
251
+ let(:email_error) { { dynamic_email_type => ["Not an email"] } }
252
+ let(:password) { "newP@ssw0rd" }
253
+ let(:attributes) do
254
+ attribute_errors.merge(password: kind_of(dynamic_password_type))
255
+ end
256
+ let(:attribute_errors) { { email: kind_of(BC::ContractFailure) } }
257
+ let(:tuple_invalid) do
258
+ { Test::InlineRegistrationInput => [:invalid_tuple] }
259
+ end
260
+
261
+ it do
262
+ expect(subject).to be_invalid
263
+ expect(subject.attributes).to match(attributes)
264
+ expect(subject.to_h).to match(email: email_error)
265
+ expect(subject.errors).to match_array([email_error, tuple_invalid])
266
+ expect(subject.attribute_errors).to match(attribute_errors)
267
+ end
268
+ end
269
+ end
270
+
271
+ context "when input is a Hash" do
272
+ subject do
273
+ Test::RegistrationInput.match(password: password, email: email)
274
+ end
275
+
276
+ include_examples "is valid"
277
+
278
+ context "when keys are strings" do
279
+ subject do
280
+ Test::RegistrationInput.match(
281
+ "password" => password, "email" => email
282
+ )
283
+ end
284
+
285
+ include_examples "is valid"
286
+ end
194
287
  end
195
288
  end
196
289
 
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.4.1
4
+ version: 0.4.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sergey Dolganov (sclinede)
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2019-07-04 00:00:00.000000000 Z
11
+ date: 2019-08-29 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -117,6 +117,7 @@ files:
117
117
  - lib/blood_contracts/core/sum.rb
118
118
  - lib/blood_contracts/core/tuple.rb
119
119
  - lib/blood_contracts/core/tuple_contract_failure.rb
120
+ - lib/blood_contracts/ext/symbolize_keys.rb
120
121
  - spec/blood_contracts/core_spec.rb
121
122
  - spec/spec_helper.rb
122
123
  homepage: https://github.com/sclinede/blood_contracts-core