bluepine 0.1.1 → 0.1.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (4) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1104 -0
  3. data/lib/bluepine/version.rb +1 -1
  4. metadata +5 -3
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9decc5e24159b2da22bb444768643364dc66c402856eed434d1f44cbb1fa807f
4
- data.tar.gz: 6309d91631be3713316a21b5aea15b36738ac2afef45658bfa8a2853265bd72a
3
+ metadata.gz: a0562448be374df38a2347cb1c9ef43fe4c46acfc958667d635c92b60f0bc7ec
4
+ data.tar.gz: 7a918a8d707bef2fd58d2f057ed278632736efb55d92c996cd0cdc29720864a0
5
5
  SHA512:
6
- metadata.gz: 2738f638bef59a74fb2e3f917997fb3a99a08210e718fe96d1355be228f323af51a11553575986318aadb97c927a9296f487a7a967a739b8ac0800204b364d3a
7
- data.tar.gz: 66fa03e1670e1cd513bae15fb6f3d1b2463f151616dfc0d238ef266ddc0b77ec361a7c3644c5d91a6d8b39c2835f82dafdde2b604550d2bc366c5e5239635729
6
+ metadata.gz: 55ee6489f9ec02830053678e5a8f91d90623253b4557dfea0cc0d70b6494df91b25e9b15e383a198453aef3d2e372b804265e59599aceae79fdb55749f552852
7
+ data.tar.gz: 5e34ebb0284cb702af6858c8d86a0314c4f15266244f9520a12e3d40a0ead017da6b12e0d37e90d909897cfdac754252879e8ae7438773476020a6ea2b2d265f
@@ -0,0 +1,1104 @@
1
+ # Bluepine
2
+
3
+ `Bluepine` is a DSL for defining API [Schema](#schema)/[Endpoint](#endpoint) with the capabilities to generate `Open API (v3)` spec (other specs is coming soon), validate API request and serialize object for API response based on single schema definition.
4
+
5
+ ## Table of Contents
6
+
7
+ - [Quick Start](#quick-start)
8
+ - [Defining a Schema](#defining-a-schema)
9
+ - [Serializing Schema](#serializing-schema)
10
+ - [Generating Open API (v3)](#generating-open-api-v3)
11
+ - [Installation](#installation)
12
+ - [Attributes](#attributes)
13
+ - [Creating Attribute](#creating-attribute)
14
+ - [Attribute Options](#attribute-options)
15
+ - [Custom Attribute](#custom-attribute)
16
+ - [Resolver](#resolver)
17
+ - [Manually registering schema/endpoint](#manually-registering-schemaendpoint)
18
+ - [Automatically registering schema/endpoint](#automatically-registering-schemaendpoint)
19
+ - [Serialization](#serialization)
20
+ - [Example](#serializer-example)
21
+ - [Conditional Serialization](#conditional-serialization)
22
+ - [Custom Serializer](#custom-serializer)
23
+ - [Endpoint](#endpoint)
24
+ - [Method](#endpoint-method)
25
+ - [Params](#endpoint-params)
26
+ - [Validation](#endpoint-validation)
27
+ - [Permitted Params (Rails)](#permitted-params)
28
+ - [Validation](#validation)
29
+ - [Conditional Validation](#validator-condition)
30
+ - [Custom Normalizer](#custom-normalizer)
31
+ - [Custom Validator](#custom-validator)
32
+ - [Generating API Specifications](#generating-api-specification)
33
+ - [Open API (v3)](#open-api-v3)
34
+
35
+
36
+ ## Quick Start
37
+
38
+ ### Defining a schema
39
+
40
+ Let's start by creating a simple schema. (For a complete list of attributes and its options please see [Attributes](#attributes) section)
41
+
42
+ > A schema can be created and registered separately, or we can use `Resolver` to create and register in one step.
43
+
44
+ ```ruby
45
+ require "bluepine"
46
+
47
+ # Schema is just an `ObjectAttribute`
48
+ Bluepine::Resolver.new do
49
+
50
+ # Defines :hero schema
51
+ schema :hero do
52
+ string :name, min: 4
53
+
54
+ # recursive schema
55
+ array :friends, of: :hero
56
+
57
+ # nested object
58
+ object :stats do
59
+ number :strength, default: 0
60
+ end
61
+
62
+ # reference
63
+ schema :team
64
+ end
65
+
66
+ # Defines :team schema
67
+ schema :team do
68
+ string :name, default: "Avengers"
69
+ end
70
+ end
71
+ ```
72
+
73
+ ### Serializing schema
74
+
75
+ In order to serialize schema, we just pass schema defined in previous step to `Serializer`.
76
+
77
+ > The object to be serialized can be a `Hash` or any `Object` with method/accessor.
78
+
79
+ ```ruby
80
+ hero = {
81
+ name: "Thor",
82
+ friends: [
83
+ {
84
+ name: "Iron Man",
85
+ stats: {
86
+ strength: "9"
87
+ }
88
+ }
89
+ ],
90
+ stats: {
91
+ strength: "8"
92
+ }
93
+ }
94
+
95
+ # or using our own Model class
96
+ hero = Hero.new(name: "Thor")
97
+
98
+ serializer = Bluepine::Serializer.new(resolver)
99
+ serializer.serialize(hero_schema, hero)
100
+ ```
101
+
102
+ will produce the following result
103
+
104
+ ```ruby
105
+ {
106
+ name: "Thor",
107
+ stats: {
108
+ strength: 8
109
+ },
110
+ friends: [
111
+ { name: "Iron Man", stats: { strength: 9 }, friends: [], team: { name: "Avengers" } }
112
+ ],
113
+ team: {
114
+ name: "Avengers"
115
+ }
116
+ }
117
+ ```
118
+ > Note: It converts number to string (via `Attribute.serializer`) and automatically adds missing fields and default value
119
+
120
+ ### Validating data
121
+
122
+ To validate data against defined schema. We just pass it to `Validator#validate` method.
123
+
124
+ > The payload could be a `Hash` or any `Object`.
125
+
126
+ ```ruby
127
+ payload = {
128
+ name: "Hulk",
129
+ friends: [
130
+ { name: "Tony" },
131
+ { name: "Sta"},
132
+ ],
133
+ team: {
134
+ name: "Aven"
135
+ }
136
+ }
137
+
138
+ validator = Bluepine::Validator.new(resolver)
139
+ validator.validate(user_schema, payload) # => Result
140
+ ```
141
+
142
+ It'll return `Result` object which has 2 attributes `#value` and `#errors`.
143
+
144
+ In the case of errors, `#errors` will contain all error messages
145
+
146
+ ```ruby
147
+ # Result.errors =>
148
+ {
149
+ friends: {
150
+ 1 => {
151
+ name: ["is too short (minimum is 4 characters)"]
152
+ }
153
+ },
154
+ team: {
155
+ name: ["is too short (minimum is 5 characters)"]
156
+ }
157
+ }
158
+ ```
159
+
160
+ If there's no errors, `#value` will contain normalized data.
161
+
162
+ ```ruby
163
+ # Result.value =>
164
+ {
165
+ name: "Thor",
166
+ stats: {
167
+ strength: 0
168
+ },
169
+ friends: [
170
+ {
171
+ name: "Iron Man",
172
+ stats: { strength: 0 },
173
+ friends: [],
174
+ team: {
175
+ name: "Avengers"
176
+ }
177
+ }
178
+ ],
179
+ team: { name: "Avengers" }
180
+ }
181
+ ```
182
+
183
+ > All default values will be added automatically.
184
+
185
+ ### Generating Open API (v3)
186
+
187
+ ```ruby
188
+ generator = Bluepine::Generators::OpenAPI::Generator.new(resolver)
189
+ generator.generate # => return Open API v3 Specification
190
+ ```
191
+
192
+ ## Installation
193
+
194
+ gem 'bluepine'
195
+
196
+ And then execute:
197
+
198
+ $ bundle
199
+
200
+ Or install it yourself as:
201
+
202
+ $ gem install bluepine
203
+
204
+ ## Attributes
205
+
206
+ `Attribute` is just a simple class which doesn't have any functionality/logic on its own. With this design, it decouples the logics to `validate`, `serialize`, etc from `Attribute` and let's consumers (e.g. `Validator`, `Serializer`, etc) decide it instead.
207
+
208
+ Here're pre-defined attributes that we can use.
209
+
210
+ * `string` - StringAttribute
211
+ * `boolean` - BooleanAttribute
212
+ * `number` - NumberAttribute
213
+ * `integer` - IntegerAttribute
214
+ * `float` - FloatAttribute
215
+ * `array` - [ArrayAttribute](#array-attribute)
216
+ * `object` - [ObjectAttribute](#object-attribute)
217
+ * `schema` - [SchemaAttribute](#schema-attribute)
218
+
219
+ ### Creating Attribute
220
+
221
+ There're couple of ways to create attributes. We can create it manually or using some other methods.
222
+
223
+ #### Manually Creating Attribute
224
+ Here, we're creating it manually.
225
+
226
+ ```ruby
227
+ user_schema = Bluepine::Attributes::ObjectAttribute.new(:user) do
228
+ string :username
229
+ string :password
230
+ end
231
+ ```
232
+
233
+ #### Using `Attributes.create`
234
+
235
+ This is equivalent to the above code
236
+
237
+ ```ruby
238
+ Bluepine::Attributes.create(:object, :user) do
239
+ string :username
240
+ string :password
241
+ end
242
+ ```
243
+
244
+ #### Using `Resolver`
245
+
246
+ This is probably the easiest way to create object attribute. Since it also keeps track of the created attribute for you. (So, we don't have to register it by ourself. See also [Resolver](#resolver))
247
+
248
+ ```ruby
249
+ Bluepine::Resolver.new do
250
+ schema :user do
251
+ string :username
252
+ string :password
253
+ end
254
+ end
255
+ ```
256
+
257
+ ### Array Attribute
258
+
259
+ Array attribute supports an option named `:of` which we can use to describe what kind of data can be contained inside `array`.
260
+
261
+ For example
262
+
263
+ ```ruby
264
+ schema :user do
265
+ string :name
266
+
267
+ # Indicates that each item inside must have the same structure
268
+ # as :user schema (e.g. friends: [{ name: "a", friends: []}, ...])
269
+ array :friends, of: :user
270
+
271
+ # i.e. pets: ["Joey", "Buddy", ...]
272
+ array :pets, of: :string
273
+
274
+ # When nothing is given, array can contain any kind of data
275
+ array :others
276
+ end
277
+ ```
278
+
279
+ ### Object Attribute
280
+
281
+ Most of the time, we'll be working with this attribute more often.
282
+
283
+ ```ruby
284
+ schema :user do
285
+ string :name
286
+
287
+ # nested attribute
288
+ object :address do
289
+ string :street
290
+
291
+ # more nested attribute if needed
292
+ object :country do
293
+ string :name
294
+ end
295
+ end
296
+ end
297
+ ```
298
+
299
+ ### Schema Attribute
300
+
301
+ Instead of declaring a lot of nested object. We can also use `schema` attribute to refer to other previously defined schema (DRY).
302
+
303
+ It also accepts `:of` option. (it works the same as `Array`)
304
+
305
+ ```ruby
306
+ schema :hero do
307
+ string :name
308
+
309
+ # This implies `of: :team`
310
+ schema :team
311
+
312
+ # If the field name is different, we can specify `:of` option (which work the same way as `Array`)
313
+ schema :awesome_team, of: :team
314
+ end
315
+
316
+ schema :team do
317
+ string :name
318
+ end
319
+ ```
320
+
321
+ ### Attribute Options
322
+
323
+ All attributes have common set of options avaiable
324
+
325
+ | Name | type | Description | Serializer | Validator | Open API
326
+ |-|-|-|-|-|-|
327
+ | name | `string\|symbol` | Attribute's name e.g. `email` |
328
+ | [method](#method-options) | `symbol` | When attribute's `name` differs from target's `name`, we can use this to specify a method that will be used to get the value for the attribute. | read value from specified name instead. See [Serializer `:method`](#serializer-options-method). | | |
329
+ | [match](#match-options) | `Regexp` | `Regex` that will be used to validate the attribute's value (`string` attribute) | | validates string based on given `Regexp` | Will add `Regexp` to generated `pattern` property |
330
+ | type | `string` | Data type | Attribute's type e.g. `string`, `schema` etc
331
+ | native_type | `string` | JSON's data type |
332
+ | [format](#format-options) | `string\|symbol ` | describes the format of this value. Could be arbitary value e.g. `int64`, `email` etc. | | | This'll be added to `format` property |
333
+ | [of](#of-options) | `symbol ` | specifies what type of data will be represented in `array`. The value could be attribute type e.g. `:string` or other schema e.g. `:user` | serializes data using specified value. See [Serializer `:of`](#serializer-options-of)| validates data using specified value | Create a `$ref` type schema |
334
+ | [in](#in-options) | `array` | A set of valid options e.g. `%w[thb usd ...]` | | payload value must be in this list | adds to `enum` property |
335
+ | [if/unless](#if-options) | `symbol\|proc` | Conditional validating/serializing result | serializes only when the specified value evalulates to `true`. See [Serializer `:if/:unless`](#serializer-options-if-unless) | validates only when it evalulates to `true` |
336
+ | required | `boolean` | Indicates this attribute is required (for validation). Default is `false` | | makes it mandatory | adds to `required` list |
337
+ | default | `any` | Default value for attribute | uses as default value when target's value is `nil` | populates as default value when it's not defined in payload | adds to `default` property |
338
+ | private | `boolean` | marks it as `private`. Default is `false` | Excludes this attribute from serialized value |
339
+ | deprecated | `boolean` | marks this attribute as deprecated. Default is `false` | | | adds to `deprecated` property |
340
+ | description | `string` | Description of attribute |
341
+ | spec | `string` | Specification of the value (for referencing only) |
342
+ | spec_uri | `string` | URI of `spec` |
343
+
344
+ ### Custom Attribute
345
+
346
+ If you want to add your own custom attribute. Simply create a new class and make it extends from `Attribute` and then register it to `Attributes` registry.
347
+
348
+ ```ruby
349
+ class AwesomeAttribute < Bluepine::Attributes::Attribute
350
+ # codes ...
351
+ end
352
+
353
+ # Register it
354
+ Bluepine::Attributes.register(:awesome, AwesomeAttribute)
355
+ ```
356
+
357
+ Later, we can refer to it like the following.
358
+
359
+ ```ruby
360
+ schema :user do
361
+ string :email
362
+ awesome :cool # our custom attribute
363
+ end
364
+ ```
365
+
366
+ ## Resolver
367
+
368
+ `Resolver` acts as a registry that holds references to all `schemas` and `endpoints` that we've defined.
369
+
370
+ ### Manually registering schema/endpoint
371
+
372
+ ```ruby
373
+ user_schema = create_user_schema
374
+
375
+ # pass it to the constructor
376
+ resolver = Bluepine::Resolver.new(schemas: [user_schema], endpoints: [])
377
+
378
+ # or use `#schemas` method
379
+ resolver.schemas.register(:user, user_schema)
380
+ ```
381
+
382
+ ### Automatically registering schema/endpoint
383
+
384
+ Although we can create a schema and register it manually. It'll become a tedious tasks when there're lot of schemas/endpoints to work with.
385
+
386
+ ```ruby
387
+ resolver = Bluepine::Resolver.new do
388
+
389
+ # schema is just `ObjectAttribute`
390
+ schema :user do
391
+ # codes
392
+ end
393
+
394
+ schema :group do
395
+ # codes
396
+ end
397
+
398
+ endpoint "/users" do
399
+ # codes
400
+ end
401
+ end
402
+ ```
403
+
404
+ ## Serialization
405
+
406
+ `Serializer` was designed in the way that it can serialize any type of `Attribute`. Either it's simple attribute type such as `StringAttribute` or a more complex type like `ObjectAttribute`. The `Serializer` treats it the same way.
407
+
408
+ ### <a name="serializer-example"></a> Example
409
+ #### Serializing a simple type
410
+
411
+ ```ruby
412
+ attr = Bluepine::Attributes.create(:string, :email)
413
+
414
+ serializer.serialize(attr, 3.14) # => "3.14"
415
+ ```
416
+
417
+ #### Serializing `Array`
418
+
419
+ ```ruby
420
+ attr = Bluepine::Attributes.create(:array, :heroes)
421
+
422
+ serializer.serialize(attr, ["Iron Man", "Thor"]) # => ["Iron Man", "Thor"]
423
+ ```
424
+
425
+ #### Serializing `Object`
426
+
427
+ When serializing object, the data that we want to serialize can be a `Hash` or plain `Object`.
428
+
429
+ In the following example. We serialize an instance of `Hero` class.
430
+
431
+ ```ruby
432
+ attr = Bluepine::Attributes.create(:object, :hero) do
433
+ string :name
434
+ number :power, default: 5
435
+ end
436
+
437
+ # Defines our class
438
+ class Hero
439
+ attr_reader :name, :power
440
+
441
+ def initialize(name:, power: nil)
442
+ @name = name
443
+ @power = power
444
+ end
445
+
446
+ def name
447
+ "I'm #{@name}"
448
+ end
449
+ end
450
+
451
+ thor = Hero.new(name: "Thor")
452
+
453
+ # Serializes
454
+ serializer.serialize(attr, thor) # =>
455
+
456
+ {
457
+ name: "I'm Thor",
458
+ power: 5
459
+ }
460
+ ```
461
+
462
+ ### <a name="serializer-options"></a> Options
463
+ #### <a name="serializer-options-method"></a> `:method`
464
+
465
+ *Value: Symbol* - Alternative method name
466
+
467
+ We can use this option to specify which method of the target object that will be used to get the data from.
468
+
469
+ ```ruby
470
+ # Our schema
471
+ schema :hero do
472
+ string :name, method: :awesome_name
473
+ end
474
+
475
+ class Hero
476
+ def initialize(name)
477
+ @name = name
478
+ end
479
+
480
+ def awesome_name
481
+ "I'm super #{@name}!"
482
+ end
483
+ end
484
+
485
+ hero = Hero.new(name: "Thor")
486
+
487
+ # Serializes
488
+ serializer.serialize(hero_schema, hero)
489
+ ```
490
+
491
+ will produce the following result.
492
+ ```
493
+ {
494
+ "name": "I'm super Thor!"
495
+ }
496
+ ```
497
+
498
+ #### <a name="serializer-options-of"></a> `:of`
499
+
500
+ *Value: `Symbol`* - Attribute type or Schema name e.g. `:string` or `:user`
501
+
502
+ This option allows us to refer to other schema from `array` or `schema` attribute.
503
+
504
+ In the following example. We'll re-use our previously defined `:hero` schema with our new `:team` schema.
505
+
506
+ ```ruby
507
+ schema :team do
508
+ array :heroes, of: :hero
509
+ end
510
+
511
+ class Team
512
+ attr_reader :name, :heroes
513
+
514
+ def initialize(name: name, heroes: heroes)
515
+ @name = name
516
+ @heroes = heroes
517
+ end
518
+ end
519
+
520
+ team = Team.new(name: "Avengers", heroes: [
521
+ Hero.new(name: "Thor"),
522
+ Hero.new(name: "Hulk", power: 10),
523
+ ])
524
+
525
+ # Serializes
526
+ serializer.serialize(team_schema, team)
527
+ ```
528
+
529
+ will produce the following result
530
+
531
+ ```ruby
532
+ {
533
+ name: "Avengers",
534
+ heroes: [
535
+ { name: "Thor", power: 5 }, # 5 is default value from hero schema
536
+ { name: "Hulk", power: 10 },
537
+ ]
538
+ }
539
+ ```
540
+
541
+ #### <a name="serializer-options-private"></a> `:private`
542
+
543
+ *Value: `Boolean`* - Default is `false`
544
+
545
+ When it's set to `true`. It'll exclude that attribute from serializer's result.
546
+
547
+ ```ruby
548
+ schema :hero do
549
+ string :name
550
+ number :secret_power, private: true
551
+ end
552
+
553
+ hero = Hero.new(name: "Peter", secret_power: 99)
554
+ serializer.serialize(hero_schema, hero)
555
+ ```
556
+
557
+ will exclude `secret_power` from result
558
+
559
+ ```ruby
560
+ {
561
+ name: "Peter"
562
+ }
563
+ ```
564
+
565
+ ### Conditional Serialization
566
+ #### <a name="serializer-options-private"></a> `:if/:unless`
567
+
568
+ *Possible value: `Symbol`/`Proc`*
569
+
570
+ This enables us to serialize value based on `if/unless` conditions.
571
+
572
+ ```ruby
573
+ schema :hero do
574
+ string :name
575
+
576
+ # :mode'll get serialized only when `dog_dead` is true
577
+ string :mode, if: :dog_dead
578
+
579
+ # or we can use `Proc` e.g.
580
+ # string :mode, if: ->(x) { x.dog_dead }
581
+ boolean :dog_dead, default: false
582
+ end
583
+
584
+ hero = Hero.new(name: "John Wick", mode: "Angry")
585
+ serializer.serialize(hero_schema, hero) # =>
586
+ ```
587
+
588
+ will produce
589
+
590
+ ```ruby
591
+ {
592
+ name: "John Wick",
593
+ dog_dead: false
594
+ }
595
+ ```
596
+ But if we set `dog_dead: true` the result will include `mode` value.
597
+
598
+ ```ruby
599
+ {
600
+ name: "John Wick",
601
+ mode: "Angry",
602
+ dog_dead: true,
603
+ }
604
+ ```
605
+
606
+ ### Custom Serializer
607
+
608
+ By default, each primitive types e.g. `string`, `integer`, etc. has its own serializer. We can override it by overriding `.serializer` class method.
609
+
610
+ For example. If we want to extend `boolean` attribute to treat "**on**" as a valid boolean value. We could do it like this.
611
+
612
+ ```ruby
613
+ BooleanAttribute.normalize = ->(x) { ["on", true].include?(x) ? true : false }
614
+
615
+ # Usage
616
+ schema :team do
617
+ boolean :active
618
+ end
619
+
620
+ team = Team.new(active: "on")
621
+ serializer.serialize(team_schema, team)
622
+ ```
623
+
624
+ will produce
625
+
626
+ ```ruby
627
+ {
628
+ active: true
629
+ }
630
+ ```
631
+
632
+ ## Endpoint
633
+
634
+ Endpoint represents the API endpoint and it's operations e.g. `GET`, `POST`, etc.
635
+ It groups resource's related operations together and defines a set of valid parameters that the endpoint accepts.
636
+
637
+ ### Defining Endpoint
638
+
639
+ We could define it manually as follows
640
+
641
+ ```ruby
642
+ Bluepine::Endpoint.new "/users" do
643
+ get :read, path: "/:id"
644
+ end
645
+ ```
646
+
647
+ or defines it via `Resolver`
648
+
649
+ ```ruby
650
+ Bluepine::Resolver.new do
651
+ endpoint "/heroes" do
652
+ post :create, path: "/"
653
+ end
654
+
655
+ endpoint "/teams" do
656
+ # code
657
+ end
658
+ end
659
+ ```
660
+
661
+ ### <a name="endpoint-method"></a> Method
662
+
663
+ Endpoint provides a set of http methods such as `get`, `post`, `patch`, `delete`, etc.
664
+ Each method expects a name and some other options.
665
+
666
+ > Note that name must be unique within endpoint
667
+
668
+ ```ruby
669
+ method(name, path:, params:)
670
+
671
+ # e.g.
672
+ get :read, path: "/:id"
673
+ post :create, path: "/"
674
+ ```
675
+
676
+ ### <a name="endpoint-params"></a> Params
677
+
678
+ `Params` allows us to define a set of valid parameters accepted by the Endpoint's methods (e.g. `get`, `post`, etc).
679
+
680
+ We can think of `Params` the same way as `Schema` (i.e. `ObjectAttribute`). It's just a specialize version of `ObjectAttribute`.
681
+
682
+ #### Defining default params
683
+
684
+ ```ruby
685
+ endpoint "/users" do
686
+ # declare default params
687
+ params do
688
+ string :username
689
+ string :password
690
+ end
691
+
692
+ # `params: true` will use default params for validating incoming request
693
+ post :create, params: true
694
+
695
+ # this will re-use `username` param from default params
696
+ patch :update, params: %i[username]
697
+ end
698
+ ```
699
+
700
+ #### Using no params `params: false` (default behaviour)
701
+
702
+ If we don't want our endpoint's method to re-use default params. We can specify `params: false` to endpoint method's arguments.
703
+
704
+ > Note: this is the default behaviour. So we can leave it blank.
705
+
706
+ ```ruby
707
+ get :index, path: "/" # ignore `params` means `params: false`
708
+ ```
709
+
710
+ #### Using default params `params: true`
711
+
712
+ As we've seen in the example above. Set `params: true` indicates that we want to use default params for this method.
713
+
714
+ ```ruby
715
+ post :create, path: "/", params: true
716
+ ```
717
+
718
+ #### Using subset of default params's attributes `params: %i[...]`
719
+
720
+ Let's say we want to use only some of default params's attrbiute e.g. `currency` (but not other attributes). We can specify it like this.
721
+
722
+ ```ruby
723
+ patch :update, path: "/:id", params: %i[currency]
724
+ ```
725
+
726
+ In this case it will re-use only `currency` attribute for validation.
727
+
728
+ #### Excluding some of default params's attributes `exclude: true`
729
+
730
+ Let's say the `update` method doesn't need the `default params`'s `amount` attribute (but still want to use all other attributes). We can specify it as follows.
731
+
732
+ ```ruby
733
+ patch :update, path: "/:id", params: %i[amount], exclude: true
734
+ ```
735
+
736
+ #### Overriding default params with `params: Proc`
737
+
738
+ In the case where we want to completely use the new set of params. We can use `Proc` to define it like the following.
739
+
740
+ ```ruby
741
+ # inside schema.endpoint block
742
+ patch :update, path: "/:id", params: lambda {
743
+ integer :max_amount, required: true
744
+ string :new_currency, match: /\A[a-z]{3}\z/
745
+ }
746
+ ```
747
+
748
+ It will use these new params for validating/generating specs.
749
+
750
+ #### Re-using Params from Other Service `params: Symbol`
751
+
752
+ We can also re-use params from other endpoint by specifing a `Symbol` that refers to other endpoint's params.
753
+
754
+ ```ruby
755
+ endpoint "/search" do
756
+ params do
757
+ string :query
758
+ number :limit
759
+ end
760
+ end
761
+
762
+ endpoint "/blogs" do
763
+ get :index, path: "/", params: :search
764
+ end
765
+ ```
766
+
767
+ Here we will use `search` endpoint's default params for validating `GET /users` endpoint.
768
+
769
+ ### Endpoint Validation
770
+
771
+ See [Validation - Validating Endpoint](#validating-endpoint)
772
+
773
+ ## Validation
774
+
775
+ Once we have our schema/endpoint defined. We can use validator to validate it against any data. (it uses `ActiveModel::Validations` under the hood)
776
+
777
+ Similar to `Serializer`. We can use `Validator` to validate any type of `Attribute`.
778
+
779
+ ### Example
780
+ #### Validating simple attribute
781
+
782
+ ```ruby
783
+ attr = Bluepine::Attributes.create(:string, :email)
784
+ email = true
785
+
786
+ validator.validate(attr, email) # => Result object
787
+ ```
788
+
789
+ In this case, it'll just return a `Result.errors` which contain error message
790
+
791
+ ```ruby
792
+ ["is not string"]
793
+ ```
794
+
795
+ #### Validating `Array`
796
+
797
+ ```ruby
798
+ attr = Bluepine::Attributes.create(:array, :names, of: :string)
799
+ names = ["john", 1, "doe"]
800
+
801
+ validator.validate(attr, names) # => Result object
802
+ ```
803
+
804
+ It'll return the error messages at exact index position.
805
+
806
+ ```ruby
807
+ {
808
+ 1 => ["is not string"]
809
+ }
810
+ ```
811
+
812
+ #### Validating `Object`
813
+ Most of the time, we'll work with the object type (instead of simple type like `string`, etc).
814
+ ```ruby
815
+ attr = Bluepine::Attributes.create(:object, :user) do
816
+ string :username, min: 4
817
+ string :password, min: 10
818
+ end
819
+
820
+ user = {
821
+ username: "john",
822
+ password: true,
823
+ }
824
+
825
+ validator.validate(attr, user) # => Result object
826
+ ```
827
+ Since it's an object, the errors will contain attribute names
828
+
829
+ ```ruby
830
+ {
831
+ password: [
832
+ "is not string",
833
+ "is too short (minimum is 10 characters)"
834
+ ]
835
+ }
836
+ ```
837
+
838
+ ### Options
839
+
840
+ #### <a name="validator-options-required"></a> `:required`
841
+
842
+ *Value: `Boolean`* - Default is `false`
843
+
844
+ This option makes the attribute mandatory.
845
+
846
+ ```ruby
847
+ schema :hero do
848
+ string :name, required: true
849
+ end
850
+
851
+ hero = Hero.new
852
+ validator.validate(hero_schema, hero) # => Result.errors
853
+ ```
854
+
855
+ will return
856
+
857
+ ```ruby
858
+ {
859
+ name: ["can't be blank"]
860
+ }
861
+ ```
862
+
863
+ #### <a name="validator-options-match"></a> `:match`
864
+
865
+ *Value: `Regexp`* - Regular Expression to be tested.
866
+
867
+ This option will test if string matches against given regular expression or not.
868
+
869
+ ```ruby
870
+ schema :hero do
871
+ string :name, match: /\A[a-zA-Z]+\z/
872
+ end
873
+
874
+ hero = Hero.new(name: "Mark 3")
875
+ validator.validate(hero_schema, hero) # => Result.errors
876
+ ```
877
+
878
+ will return
879
+
880
+ ```ruby
881
+ {
882
+ name: ["is not valid"]
883
+ }
884
+ ```
885
+
886
+ #### <a name="validator-options-min-max"></a> `:min/:max`
887
+
888
+ *Value: `Number`* - Apply to both `string` and `number` attribute types.
889
+
890
+ This option sets a minimum and maximum value for attribute.
891
+
892
+ ```ruby
893
+ schema :hero do
894
+ string :power, max: 100
895
+ end
896
+
897
+ hero = Hero.new(power: 200)
898
+ validator.validate(hero_schema, hero) # => Result.errors
899
+ ```
900
+
901
+ will return
902
+
903
+ ```ruby
904
+ {
905
+ power: ["must be less than or equal to 100"]
906
+ }
907
+ ```
908
+
909
+ #### <a name="validator-options-in"></a> `:in`
910
+
911
+ *Value: `Array`* - Set of valid values.
912
+
913
+ This option will test if value is in the specified list or not.
914
+
915
+ ```ruby
916
+ schema :hero do
917
+ string :status, in: ["Happy", "Angry"]
918
+ end
919
+
920
+ hero = Hero.new(status: "Mad")
921
+ validator.validate(hero_schema, hero) # => Result.errors
922
+ ```
923
+
924
+ will return
925
+
926
+ ```ruby
927
+ {
928
+ status: ["is not included in the list"]
929
+ }
930
+ ```
931
+
932
+ ### <a name="validator-condition"></a> Conditional Validation
933
+ #### <a name="validator-options-if-unless"></a> `:if/:unless`
934
+
935
+ *Possible value: `Symbol`/`Proc`*
936
+
937
+ This enables us to validate attribute based on `if/unless` conditions.
938
+
939
+ ```ruby
940
+ schema :hero do
941
+ string :name
942
+
943
+ # or we can use `Proc` e.g.
944
+ # if: ->(x) { x.is_agent }
945
+ string :agent_name, required: true, if: :is_agent
946
+
947
+ boolean :agent, default: false
948
+ end
949
+
950
+ hero = Hero.new(name: "Nick Fury", is_agent: true)
951
+ validator.validate(hero_schema, hero) # Result.errors =>
952
+ ```
953
+
954
+ will produce (because `is_agent` is `true`)
955
+
956
+ ```ruby
957
+ {
958
+ agent_name: ["can't be blank"]
959
+ }
960
+ ```
961
+
962
+ ### Custom Validator
963
+
964
+ Since the validator is based on `ActiveModel::Validations`. This make it easy to add a new custom validator.
965
+
966
+ In the following example. We create a simple password validator and register it to the password attribute.
967
+
968
+ ```ruby
969
+ # Defines custom validator
970
+ class CustomPasswordValidator < ActiveModel::Validator
971
+ def validate(record)
972
+ record.errors.add(:password, "is too short") unless record.password.length > 10
973
+ end
974
+ end
975
+
976
+ # Registers
977
+ schema :user do
978
+ string :username
979
+ string :password, validators: [CustomPasswordValidator]
980
+ end
981
+ ```
982
+
983
+ ### Custom Normalizer
984
+
985
+ It's possible to change the logic for normalizing data before passing it to the validator. For example, you might want to normalize `boolean` value before validating it.
986
+
987
+ Here, we want to normalize string such as `on` or `1` to boolean `true` first.
988
+
989
+ ```ruby
990
+ # Overrides default normalizer
991
+ BooleanAttribute.normalizer = ->(x) { [true, 1, "on"].include?(x) ? true : false }
992
+
993
+ schema :hero do
994
+ boolean :berserk
995
+ end
996
+
997
+ hero = Hero.new(berserk: 1)
998
+ validator.validate(hero_schema, hero) # Result.value
999
+ ```
1000
+ will pass the validation and `Result.value`'ll contain normalized value
1001
+
1002
+ ```ruby
1003
+ {
1004
+ berserk: true # convert 1 to true
1005
+ }
1006
+ ```
1007
+
1008
+ ### Validating `Endpoint`
1009
+
1010
+ All examples above also apply to endpoint's parameters validation.
1011
+
1012
+ Because the params is part of `Endpoint` and it's non-trivial task to retrieve endpoint's method's params. So, the `Endpoint` provides some helper methods to validate the data.
1013
+
1014
+ ```ruby
1015
+ resolver = Bluepine::Resolver.new do
1016
+ endpoint "/heroes" do
1017
+ post :create, params: lambda {
1018
+ string :name, required: true
1019
+ }
1020
+ end
1021
+ end
1022
+
1023
+ # :create is a POST method name given to the endpoint.
1024
+ resolver.endpoint(:heroes).method(:create, resolver: resolver).validate(payload) # => Result
1025
+ ```
1026
+
1027
+ ## Generating Open API (v3)
1028
+
1029
+ Once we have all schemas/endpoints defined and registered to the `Resolver`. We can simply pass it to the generator as follows.
1030
+
1031
+ ```ruby
1032
+ generator = Bluepine::Generators::OpenAPI::Generator.new(resolver, options)
1033
+ generator.generate # =>
1034
+ ```
1035
+
1036
+ will output Open API (v3) specs
1037
+
1038
+ *excerpt from the full result*
1039
+ ```js
1040
+ // endpoints
1041
+ "/users": {
1042
+ "post": {
1043
+ "requestBody": {
1044
+ "content": {
1045
+ "application/x-www-form-urlencoded": {
1046
+ "schema": {
1047
+ "type": "object",
1048
+ "properties": {
1049
+ "username": {
1050
+ "type": "string"
1051
+ },
1052
+ "accepted": {
1053
+ "type": "boolean",
1054
+ "enum": [true, false]
1055
+ },
1056
+ }
1057
+ }
1058
+ }
1059
+ }
1060
+ },
1061
+ "responses": {
1062
+ "200": {
1063
+ "content": {
1064
+ "application/json": {
1065
+ "schema": {
1066
+ "$ref": "#/components/schemas/user"
1067
+ }
1068
+ }
1069
+ }
1070
+ }
1071
+ }
1072
+ }
1073
+ }
1074
+
1075
+ // schema
1076
+ "user": {
1077
+ "type": "object",
1078
+ "properties": {
1079
+ "address": {
1080
+ "type": "object",
1081
+ "properties": {
1082
+ "city": {
1083
+ "type": "string",
1084
+ "default": "Bangkok"
1085
+ }
1086
+ }
1087
+ },
1088
+ "friends": {
1089
+ "type": "array",
1090
+ "items": {
1091
+ "$ref": "#/components/schemas/user"
1092
+ }
1093
+ }
1094
+ }
1095
+ }
1096
+ ```
1097
+
1098
+ ## Contributing
1099
+
1100
+ Bug reports and pull requests are welcome on GitHub at https://github.com/omise/bluepine. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
1101
+
1102
+ ## License
1103
+
1104
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
@@ -1,3 +1,3 @@
1
1
  module Bluepine
2
- VERSION = "0.1.1"
2
+ VERSION = "0.1.3"
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: bluepine
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.1.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Marut K
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2019-07-09 00:00:00.000000000 Z
11
+ date: 2019-07-10 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -142,8 +142,10 @@ email:
142
142
  - marut@omise.co
143
143
  executables: []
144
144
  extensions: []
145
- extra_rdoc_files: []
145
+ extra_rdoc_files:
146
+ - README.md
146
147
  files:
148
+ - README.md
147
149
  - lib/bluepine.rb
148
150
  - lib/bluepine/assertions.rb
149
151
  - lib/bluepine/attributes.rb