tomyum 0.1.0.a

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