sober_swag 0.23.0 → 0.25.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 +4 -4
- data/CHANGELOG.md +9 -0
- data/docs/reporting.md +326 -1
- data/example/Gemfile +1 -1
- data/example/Gemfile.lock +26 -26
- data/lib/sober_swag/input_object.rb +1 -0
- data/lib/sober_swag/output_object/field_syntax.rb +2 -0
- data/lib/sober_swag/reporting/input/interface.rb +2 -0
- data/lib/sober_swag/reporting/input/list.rb +6 -0
- data/lib/sober_swag/reporting/input/number.rb +7 -0
- data/lib/sober_swag/reporting/input/struct.rb +1 -0
- data/lib/sober_swag/reporting/output/in_range.rb +64 -0
- data/lib/sober_swag/reporting/output/interface.rb +11 -4
- data/lib/sober_swag/reporting/output/struct.rb +48 -15
- data/lib/sober_swag/reporting/output.rb +1 -0
- data/lib/sober_swag/reporting/report/error.rb +4 -0
- data/lib/sober_swag/version.rb +1 -1
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 95eb8012259c21946fe6b6ab07aaa4da3b29f5d3746b0581d12a6f67b39f8f5c
|
4
|
+
data.tar.gz: d775af4081190de942d2e6d978a922dd37083c9d0a59638cd16259d1c9ad8295
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 669a66d91c6b2e6d47648a30d44829a5713ee4b0170626953359ed3df5b66f14b21ea76d84654ea4bcafb14f1fe622ce8ac8338de98df530a8d19ef47275fc0b
|
7
|
+
data.tar.gz: 92617f0c507187d52c88a07324fc0081503d5095c4bd7f192fb65d252c9dff38ae52da708f2e38665430f6f9d506703e1e1fdc95f4dc0b4c621a33ef577802a0
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,14 @@
|
|
1
1
|
# Changelog
|
2
2
|
|
3
|
+
## [v0.25.0] 2022-06-07
|
4
|
+
|
5
|
+
- Adds more description to the error raised if you try to use a non-reporting output type as the output of a field
|
6
|
+
- Add a lot more documentation for the reporting interface in [`docs/reporting.md`](docs/reporting.md)
|
7
|
+
|
8
|
+
## [v0.24.1] 2022-05-26
|
9
|
+
|
10
|
+
- Added a better `#message` to `SoberSwag::Reporting::Report::Error`
|
11
|
+
|
3
12
|
## [v0.22.0] 2021-12-21
|
4
13
|
|
5
14
|
- Added `SoberSwag::Reporting`, which is basically a v2 of the gem!
|
data/docs/reporting.md
CHANGED
@@ -120,7 +120,7 @@ A view will *always inherit all attributes of the parent object, regardless of o
|
|
120
120
|
class AlternativePersonOutput < SoberSwag::Output::Struct
|
121
121
|
field :first_name, SoberSwag::Reporting::Output.text
|
122
122
|
|
123
|
-
|
123
|
+
define_view :with_grade do
|
124
124
|
field :grade, SoberSwag::Reporting::Output.text.nilable do
|
125
125
|
if object_to_serialize.respond_to?(:grade)
|
126
126
|
object_to_serialize.grade
|
@@ -187,4 +187,329 @@ There are basically two things to keep in mind when upgrading to `SoberSwag::Rep
|
|
187
187
|
Instead, view management is now *explicit*.
|
188
188
|
This is because it was too tempting to pass data to serialize in the options key, which is against the point of the serializers.
|
189
189
|
|
190
|
+
# API Overview
|
190
191
|
|
192
|
+
This section presents an overview of the available reporting outputs and inputs.
|
193
|
+
|
194
|
+
## `SoberSwag::Reporting::Output`
|
195
|
+
|
196
|
+
This module contains reporting *outputs*.
|
197
|
+
These act as type-checked serializers.
|
198
|
+
|
199
|
+
### Primitive Types
|
200
|
+
|
201
|
+
The following "primitive types" are available:
|
202
|
+
|
203
|
+
- `SoberSwag::Reporting::Output.bool`, which returns a `SoberSwag::Reporting::Output::Bool`.
|
204
|
+
This type is for serializing boolean values, IE, `true` or `false`.
|
205
|
+
It will serialize the boolean directly to the JSON.
|
206
|
+
- `SoberSwag::Reporting::Output.null`, which returns a `SoberSwag::Reporting::Output::Null`.
|
207
|
+
This type serializes out `null` in JSON.
|
208
|
+
This can only serialize the ruby value `nil`.
|
209
|
+
- `SoberSwag::Reporting::Output.number`, returns a `SoberSwag::Reporting::Output::Number`.
|
210
|
+
This type serializes out numbers in JSON.
|
211
|
+
It can serialize out most ruby numeric types, including `Integer` and `Float`.
|
212
|
+
- `SoberSwag::Reporting::Output.text`, which returns a `SoberSwag::Reporting::Output::Text`.
|
213
|
+
This serializes out a string type in the JSON.
|
214
|
+
It can serialize out ruby strings.
|
215
|
+
|
216
|
+
### The Transforming Type
|
217
|
+
|
218
|
+
For `SoberSwag::Reporting::Output`, there's a "fundamental" type that does *transformation*, called `via_map`.
|
219
|
+
It lets you apply a ruby block before passing the input on to the serializer after it.
|
220
|
+
It's most often used like this:
|
221
|
+
|
222
|
+
```ruby
|
223
|
+
screaming_output = SoberSwag::Reporting::Output.text.via_map { |old_text| old_text.upcase }
|
224
|
+
screaming_output.call("what the heck")
|
225
|
+
# => "WHAT THE HECK"
|
226
|
+
```
|
227
|
+
|
228
|
+
Note that this calls the block *before* passing to the next serializer.
|
229
|
+
So:
|
230
|
+
|
231
|
+
```ruby
|
232
|
+
example = SoberSwag::Reporting::Output.text.via_map { |x| x.downcase }.via_map { |x| x + ", OK?" }
|
233
|
+
example.call("WHAT THE HECK?")
|
234
|
+
# => "what the heck, ok?"
|
235
|
+
```
|
236
|
+
|
237
|
+
This type winds up being extremely useful in a *lot* of places.
|
238
|
+
For example, you can use it to provide extra information to a serializer:
|
239
|
+
|
240
|
+
```ruby
|
241
|
+
serializer = MyCoolOutput.via_map { |x| CoolStruct.new(record: x, metadata: metadata_from_elsewhere) }
|
242
|
+
render json: serializer.list.call(my_record_relation)
|
243
|
+
```
|
244
|
+
|
245
|
+
### Composite Types
|
246
|
+
|
247
|
+
The following "composite types," or types built from other types, are available:
|
248
|
+
|
249
|
+
- `SoberSwag::Reporting::Output::List`, which seralizes out *lists* of values.
|
250
|
+
You can construct one in two ways:
|
251
|
+
|
252
|
+
```ruby
|
253
|
+
SoberSwag::Reporting::Output::List.new(SoberSwag::Reporting::Output.text)
|
254
|
+
# or, via the instance method
|
255
|
+
SoberSwag::Reporting::Output.text.list
|
256
|
+
```
|
257
|
+
This produces an output that can serialize to JSON arrays.
|
258
|
+
For example, either of these can produce:
|
259
|
+
|
260
|
+
```json
|
261
|
+
["foo", "bar"]
|
262
|
+
```
|
263
|
+
|
264
|
+
This serialize will work with anything that responds to `#map`.
|
265
|
+
|
266
|
+
- `SoberSwag::Reporting::Output::Dictionary`, which can be constructed via:
|
267
|
+
```ruby
|
268
|
+
SoberSwag::Reporting::Output::Dictionary.of(SoberSwag::Reporting::Output.number)
|
269
|
+
```
|
270
|
+
|
271
|
+
This type serializes out a key-value dictionary, IE, a JSON object.
|
272
|
+
So, the above can serialize:
|
273
|
+
```ruby
|
274
|
+
{ "foo": 10, "bar": 11 }
|
275
|
+
```
|
276
|
+
This type will only serialize out ruby hashes.
|
277
|
+
It will, conveniently, convert symbol keys to strings for you.
|
278
|
+
|
279
|
+
- `SoberSwag::Reporting::Output::Partitioned`, which represents the *choice* of two serializers.
|
280
|
+
It takes in a block to decide which serializer to use, a serializer to use if the block returns `true`, and a serializer to use if the block returns `false`.
|
281
|
+
That is, to serialize out *either* a string *or* a number, you might use:
|
282
|
+
```ruby
|
283
|
+
SoberSwag::Reporting::Output::Partitioned.new(
|
284
|
+
proc { |x| x.is_a?(String) },
|
285
|
+
SoberSwag::Reporting::Output.text,
|
286
|
+
SoberSwag::Reporting::Output.number
|
287
|
+
)
|
288
|
+
```
|
289
|
+
- `SoberSwag::Reporting::Output::Viewed`, which lets you define a *view map* for an object.
|
290
|
+
This is mostly used as an implementation detail, but can be occasionally useful if you want to provide
|
291
|
+
a list of "views" with no common "base," like an output object might have. In this case, the "base"
|
292
|
+
view is more of a "default" rather than a "parent."
|
293
|
+
|
294
|
+
### Validation Types
|
295
|
+
|
296
|
+
OpenAPI v3 supports some *validations* on types, in addition to raw types.
|
297
|
+
For example, you can specify in your documentation that a value will be within a *range* of values.
|
298
|
+
These `SoberSwag::Reporting::Output` types provide that documentation - and perform those validations!
|
299
|
+
|
300
|
+
- `SoberSwag::Reporting::Output::InRange` validates that a value will be within a certain *range* of values.
|
301
|
+
This is most useful with numbers.
|
302
|
+
For example:
|
303
|
+
```ruby
|
304
|
+
SoberSwag::Reporting::Output.number.in_range(0..10)
|
305
|
+
```
|
306
|
+
- `SoberSwag::Reporting::Output::Pattern` validates that a value will match a certain *pattern.*
|
307
|
+
This is useful with strings:
|
308
|
+
```ruby
|
309
|
+
SoberSwag::Reporting::Output::Pattern.new(SoberSwag::Reporting::Output.text, /foo|bar|baz|my-[0-5*/)
|
310
|
+
```
|
311
|
+
|
312
|
+
## `SoberSwag::Reporting::Input`
|
313
|
+
|
314
|
+
This module is used for *parsers*, which take in some input and return a nicer type.
|
315
|
+
|
316
|
+
### Basic Types
|
317
|
+
|
318
|
+
These types are the "primitives" of `SoberSwag::Reporting::Input`, the most basic types:
|
319
|
+
|
320
|
+
- `SoberSwag::Reporting::Input::Null` parses a JSON `null` value.
|
321
|
+
It will parse it to a ruby `nil`, naturally.
|
322
|
+
You probably want to construct one via `SoberSwag::Reporting::Input.null`.
|
323
|
+
- `SoberSwag::Reporting::Input::Number` parses a JSON number.
|
324
|
+
It will parse to either a ruby `Integer` or a ruby `Float`, depending on the format (we use Ruby's internal format for this).
|
325
|
+
You probably want to construct one via `SoberSwag::Reporting::Input.number`.
|
326
|
+
- `SoberSwag::Reporting::Input::Bool`, which parses a JSON bool (`true` or `false`).
|
327
|
+
This will parse to a ruby `true` or `false`.
|
328
|
+
You probably want to construct it with `SoberSwag::Reporting::Output.bool`.
|
329
|
+
- `SoberSwag::Reporting::Input::Text`, which parses a JSON string (`"mike stoklassa"`, `"richard evans"`, or `"jay bauman"` for example).
|
330
|
+
This will parse to a ruby string.
|
331
|
+
You probably want to construct it with `SoberSwag::Reporting::Output.text`.
|
332
|
+
|
333
|
+
### The Transforming Type
|
334
|
+
|
335
|
+
Much like `via_map` for `SoberSwag::Reporting::Output`, there's a fundamental type that does *transformation*, called the `mapped`.
|
336
|
+
This lets you do some transformation of input *after* others have ran.
|
337
|
+
So:
|
338
|
+
|
339
|
+
```ruby
|
340
|
+
quiet = SoberSwag::Reporting::Input.text.mapped { |x| x.downcase }
|
341
|
+
quiet.call("WHAT THE HECK")
|
342
|
+
# => "what the heck"
|
343
|
+
```
|
344
|
+
|
345
|
+
Note that this composes as follows:
|
346
|
+
|
347
|
+
```ruby
|
348
|
+
example = SoberSwag::Reporting::Input.text.mapped { |x| x.downcase }.mapped { |x| x + ", OK?" }
|
349
|
+
|
350
|
+
example.call("WHAT THE HECK")
|
351
|
+
# => "what the heck, OK?"
|
352
|
+
# As you can see, the *first* function applies first, then the *second*.
|
353
|
+
```
|
354
|
+
|
355
|
+
You might notice that this is the opposite behavior of of `SoberSwag::Reporting::Output::ViaMap`.
|
356
|
+
This is because *serialization* is the *opposite* of *parsing*.
|
357
|
+
Kinda neat, huh?
|
358
|
+
|
359
|
+
### Composite Types
|
360
|
+
|
361
|
+
These types work with *one or more* inputs to build up *another*.
|
362
|
+
|
363
|
+
- `SoberSwag::Reporting::Input::List`, which lets you parse a JSON array.
|
364
|
+
IE:
|
365
|
+
```ruby
|
366
|
+
SoberSwag::Reporting::Input::List.of(SoberSwag::Reporting::Input.number)
|
367
|
+
```
|
368
|
+
Lets you parse a list of numbers.
|
369
|
+
- `SoberSwag::Reporting::Input::Either`, which lets you parse one input, and if that fails, parse another.
|
370
|
+
This represents a *choice* of input types.
|
371
|
+
This is best used via:
|
372
|
+
```ruby
|
373
|
+
SoberSwag::Reporting::Input.text | SoberSwag::Reporting::Input.number
|
374
|
+
# or
|
375
|
+
SoberSwag::Reporting::Input.text.or SoberSwag::Reporting::Input.number
|
376
|
+
```
|
377
|
+
This is useful if you want to allow multiple input formats.
|
378
|
+
- `SoberSwag::Reporting::Input::Dictionary`, which lets you parse a JSON dictionary with arbitrary keys.
|
379
|
+
For example, to parse this JSON (assuming you don't know the keys ahead of time):
|
380
|
+
```json
|
381
|
+
{
|
382
|
+
"mike": 100,
|
383
|
+
"bob": 1000,
|
384
|
+
"joey": 12,
|
385
|
+
"yes": 1213
|
386
|
+
}
|
387
|
+
```
|
388
|
+
You can use:
|
389
|
+
```ruby
|
390
|
+
SoberSwag::Reporting::Input::Dictionary.of(SoberSwag::Reporting::Input.number)
|
391
|
+
```
|
392
|
+
|
393
|
+
This will parse to a Ruby hash, with string keys.
|
394
|
+
If you want symbols, you can simply use `.mapped`:
|
395
|
+
```ruby
|
396
|
+
SoberSwag::Reporting::Input::Dictionary.of(
|
397
|
+
SoberSwag::Reporting::Input.number
|
398
|
+
).mapped { |hash| hash.transform_keys(&:to_sym) }
|
399
|
+
```
|
400
|
+
Pretty cool, right?
|
401
|
+
- `SoberSwag::Reporting::Input::Enum`, which lets you parse an *enum value*.
|
402
|
+
This input will validate that the given value is in the enum.
|
403
|
+
Note that this doesn't only work with strings!
|
404
|
+
You can use:
|
405
|
+
|
406
|
+
```ruby
|
407
|
+
SoberSwag::Reporting::Input.number.enum(-1, 0, 1)
|
408
|
+
```
|
409
|
+
|
410
|
+
And things will work fine.
|
411
|
+
|
412
|
+
### Validating Types
|
413
|
+
|
414
|
+
These types provide *validation* on an input.
|
415
|
+
The validations provided match the specifications in swagger.
|
416
|
+
|
417
|
+
- `SoberSwag::Reporting::Input::InRange`, which specifies that a value should be *within a range*.
|
418
|
+
You can use it like:
|
419
|
+
|
420
|
+
```ruby
|
421
|
+
SoberSwag::Reporting::Input::InRange.new(
|
422
|
+
SoberSwag::Reporting::Input::Number,
|
423
|
+
1..100
|
424
|
+
)
|
425
|
+
```
|
426
|
+
- `SoberSwag::Reporting::Input::MultipleOf`, which specifies that a number is a *multiple of* some other number.
|
427
|
+
You can use it like this:
|
428
|
+
```ruby
|
429
|
+
SoberSwag::Reporting::Input.number.multiple_of(2)
|
430
|
+
```
|
431
|
+
|
432
|
+
Note that the `#multiple_of` method is only available on the `SoberSwag::Reporting::Input::Number` class.
|
433
|
+
- `SoberSwag::Reporting::Input::Pattern`, which lets you check that an input *matches a regexp*.
|
434
|
+
You can use it like:
|
435
|
+
|
436
|
+
```ruby
|
437
|
+
SoberSwag::Reporting::Input.text.with_pattern(/\A(R|r)ich (E|e)vans\z/)
|
438
|
+
```
|
439
|
+
|
440
|
+
Note that the `with_pattern` method is only available on `SoberSwag::Reporting::Input::Text`
|
441
|
+
|
442
|
+
#### Custom Validations
|
443
|
+
|
444
|
+
You might have a scenario where you need to do a *custom validation* that is not in this list.
|
445
|
+
In order to do this, you can use our old friend, `mapped`.
|
446
|
+
If you return any instance of `SoberSwag::Reporting::Report::Base` from via-map, it will be treated as a *parse error*.
|
447
|
+
This can be used for custom validations, like so:
|
448
|
+
|
449
|
+
```ruby
|
450
|
+
UuidInput = SoberSwag::Reporting::Input.text.format('custom-identifier').mapped do |inputted_string|
|
451
|
+
if inputted_string == 'special-value'
|
452
|
+
SoberSwag::Reporting::Report::Value.new(['was the string "special-value", which is reserved'])
|
453
|
+
else
|
454
|
+
inputted_string
|
455
|
+
end
|
456
|
+
end
|
457
|
+
```
|
458
|
+
|
459
|
+
Please note that this functionality is intended to enable data *format* validation.
|
460
|
+
**If you are making a call to a database or some API within a `mapped` block, you are doing something weird**.
|
461
|
+
Sometimes you do need to do weird things, of course, but it is generally **not appropriate** to use input validation to ensure that ids exist or whatever - leave that up to your rails models!
|
462
|
+
|
463
|
+
### Documentating Types
|
464
|
+
|
465
|
+
These types allow you to add additional documentation.
|
466
|
+
|
467
|
+
- `SoberSwag::Reporting::Input::Format`, which provides *format description*.
|
468
|
+
This lets you specify that a given input should have a given format.
|
469
|
+
Formats are just a string, so you can use custom formats:
|
470
|
+
```ruby
|
471
|
+
SoberSwag::Reporting::Input.text.format('user-uuid')
|
472
|
+
```
|
473
|
+
|
474
|
+
Note that adding a format *will not do any magic validation whatsoever*.
|
475
|
+
See the section on custom validations for how to do that.
|
476
|
+
|
477
|
+
### Converting Types
|
478
|
+
|
479
|
+
For convenience's sake, SoberSwag comes with a few built-in *converting inputs*.
|
480
|
+
These convert JSON objects into some common types that you would want to use in ruby.
|
481
|
+
|
482
|
+
- `SoberSwag::Reporting::Input::Converting::Bool`, which tries to coerce an input value to a boolean.
|
483
|
+
It will convert the following JSON values to `true`:
|
484
|
+
|
485
|
+
- The strings `"y"`, `"yes"`, `"true"`, `"t"`, or the all-caps variants of any
|
486
|
+
- The string `"1"`
|
487
|
+
- The number `1`
|
488
|
+
- a JSON `true`
|
489
|
+
|
490
|
+
And the following to false:
|
491
|
+
|
492
|
+
- The strings `"f"`, `"no"`, `"false"`, `"n"`, or any allcaps variant
|
493
|
+
- The number `0`
|
494
|
+
- An actual JSON `false`
|
495
|
+
- `SoberSwag::Reporting::Input::Converting::Date`, which tries to parse a date string.
|
496
|
+
More specifically, it:
|
497
|
+
- First tries to parse a date with [RFC 3339](https://datatracker.ietf.org/doc/html/rfc3339).
|
498
|
+
It uses [`Date#rfc3339`](https://ruby-doc.org/stdlib-3.1.2/libdoc/date/rdoc/Date.html#method-c-rfc3339) to do this.
|
499
|
+
- Then tries to parse with [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601).
|
500
|
+
It uses [`Date#iso8601`](https://ruby-doc.org/stdlib-3.1.2/libdoc/date/rdoc/Date.html#method-c-iso8601) to do this.
|
501
|
+
- If both of the above fail, return a descriptive error (more specifically, the error specifies that the string was not an RFC 3339 date string or an ISO 8601 date string).
|
502
|
+
- `SoberSwag::Reporting::Input::Converting::DateTime`, which works in much the same way.
|
503
|
+
More specifically, it...
|
504
|
+
- First tries to parse a timestamp with [RFC 3339](https://datatracker.ietf.org/doc/html/rfc3339).
|
505
|
+
It uses [`DateTime#rfc3339`](https://ruby-doc.org/stdlib-2.6.1/libdoc/date/rdoc/DateTime.html#method-c-rfc3339) to do this.
|
506
|
+
- Then tries to parse with [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601).
|
507
|
+
It uses [`DateTime#iso8601`](https://ruby-doc.org/stdlib-2.6.1/libdoc/date/rdoc/DateTime.html#method-c-iso8601) to do this.
|
508
|
+
- If both of the above fail, return a descriptive error (more specifically, the error specifies that the string was not an RFC 3339 date-time or an ISO 8601 date-time string).
|
509
|
+
- `SoberSwag::Reporting::Input::Converting::Decimal`, which tries to parse a decimal number.
|
510
|
+
If a number is passed, it will convert that number to a `BigDecimal` via `#to_d`.
|
511
|
+
If a string is passed, it uses [`Kernel#BigDecimal`](https://ruby-doc.org/stdlib-2.6/libdoc/bigdecimal/rdoc/Kernel.html#method-i-BigDecimal) to try to parse a decimal from a string.
|
512
|
+
Note: you may wish to combine this with some sort of source-length check, to ensure people cannot force you to construct extremely large, memory-intense decimals.
|
513
|
+
- `SoberSwag::Reporting::Input::Converting::Integer`, which tries to parse an integer number.
|
514
|
+
If a JSON number is passed, it uses `#to_i` to convert it to an integer.
|
515
|
+
If a string it passed, it uses [`Kernel#Integer`](https://ruby-doc.org/core-2.7.1/Kernel.html) to attempt to do the conversion, and reports an error if that fails.
|
data/example/Gemfile
CHANGED
@@ -8,7 +8,7 @@ gem 'actionpack', '>= 6.0.3.2'
|
|
8
8
|
# Use sqlite3 as the database for Active Record
|
9
9
|
gem 'sqlite3', '~> 1.4'
|
10
10
|
# Use Puma as the app server
|
11
|
-
gem 'puma', '~> 5.
|
11
|
+
gem 'puma', '~> 5.6'
|
12
12
|
# Build JSON APIs with ease. Read more: https://github.com/rails/jbuilder
|
13
13
|
# gem 'jbuilder', '~> 2.7'
|
14
14
|
# Use Active Model has_secure_password
|
data/example/Gemfile.lock
CHANGED
@@ -64,14 +64,14 @@ GEM
|
|
64
64
|
minitest (~> 5.1)
|
65
65
|
tzinfo (~> 1.1)
|
66
66
|
zeitwerk (~> 2.2, >= 2.2.2)
|
67
|
-
bootsnap (1.
|
68
|
-
msgpack (~> 1.
|
67
|
+
bootsnap (1.11.1)
|
68
|
+
msgpack (~> 1.2)
|
69
69
|
builder (3.2.4)
|
70
70
|
byebug (11.1.3)
|
71
71
|
coderay (1.1.3)
|
72
|
-
concurrent-ruby (1.1.
|
72
|
+
concurrent-ruby (1.1.10)
|
73
73
|
crass (1.0.6)
|
74
|
-
diff-lcs (1.
|
74
|
+
diff-lcs (1.5.0)
|
75
75
|
dry-configurable (0.13.0)
|
76
76
|
concurrent-ruby (~> 1.0)
|
77
77
|
dry-core (~> 0.6)
|
@@ -98,16 +98,16 @@ GEM
|
|
98
98
|
dry-types (>= 0.8.1)
|
99
99
|
rails (>= 3)
|
100
100
|
erubi (1.10.0)
|
101
|
-
ffi (1.15.
|
101
|
+
ffi (1.15.5)
|
102
102
|
globalid (1.0.0)
|
103
103
|
activesupport (>= 5.0)
|
104
|
-
i18n (1.
|
104
|
+
i18n (1.10.0)
|
105
105
|
concurrent-ruby (~> 1.0)
|
106
106
|
ice_nine (0.11.2)
|
107
|
-
listen (3.7.
|
107
|
+
listen (3.7.1)
|
108
108
|
rb-fsevent (~> 0.10, >= 0.10.3)
|
109
109
|
rb-inotify (~> 0.9, >= 0.9.10)
|
110
|
-
loofah (2.
|
110
|
+
loofah (2.16.0)
|
111
111
|
crass (~> 1.0.2)
|
112
112
|
nokogiri (>= 1.5.9)
|
113
113
|
mail (2.7.1)
|
@@ -115,17 +115,17 @@ GEM
|
|
115
115
|
marcel (1.0.2)
|
116
116
|
method_source (1.0.0)
|
117
117
|
mini_mime (1.1.2)
|
118
|
-
mini_portile2 (2.
|
119
|
-
minitest (5.
|
120
|
-
msgpack (1.
|
118
|
+
mini_portile2 (2.8.0)
|
119
|
+
minitest (5.15.0)
|
120
|
+
msgpack (1.5.1)
|
121
121
|
nio4r (2.5.8)
|
122
|
-
nokogiri (1.
|
123
|
-
mini_portile2 (~> 2.
|
122
|
+
nokogiri (1.13.6)
|
123
|
+
mini_portile2 (~> 2.8.0)
|
124
124
|
racc (~> 1.4)
|
125
125
|
pry (0.14.1)
|
126
126
|
coderay (~> 1.1)
|
127
127
|
method_source (~> 1.0)
|
128
|
-
puma (5.
|
128
|
+
puma (5.6.4)
|
129
129
|
nio4r (~> 2.0)
|
130
130
|
racc (1.6.0)
|
131
131
|
rack (2.2.3)
|
@@ -158,18 +158,18 @@ GEM
|
|
158
158
|
rake (>= 0.8.7)
|
159
159
|
thor (>= 0.20.3, < 2.0)
|
160
160
|
rake (13.0.6)
|
161
|
-
rb-fsevent (0.11.
|
161
|
+
rb-fsevent (0.11.1)
|
162
162
|
rb-inotify (0.10.1)
|
163
163
|
ffi (~> 1.0)
|
164
|
-
rspec-core (3.
|
165
|
-
rspec-support (~> 3.
|
166
|
-
rspec-expectations (3.
|
164
|
+
rspec-core (3.11.0)
|
165
|
+
rspec-support (~> 3.11.0)
|
166
|
+
rspec-expectations (3.11.0)
|
167
167
|
diff-lcs (>= 1.2.0, < 2.0)
|
168
|
-
rspec-support (~> 3.
|
169
|
-
rspec-mocks (3.
|
168
|
+
rspec-support (~> 3.11.0)
|
169
|
+
rspec-mocks (3.11.1)
|
170
170
|
diff-lcs (>= 1.2.0, < 2.0)
|
171
|
-
rspec-support (~> 3.
|
172
|
-
rspec-rails (5.
|
171
|
+
rspec-support (~> 3.11.0)
|
172
|
+
rspec-rails (5.1.2)
|
173
173
|
actionpack (>= 5.2)
|
174
174
|
activesupport (>= 5.2)
|
175
175
|
railties (>= 5.2)
|
@@ -177,7 +177,7 @@ GEM
|
|
177
177
|
rspec-expectations (~> 3.10)
|
178
178
|
rspec-mocks (~> 3.10)
|
179
179
|
rspec-support (~> 3.10)
|
180
|
-
rspec-support (3.
|
180
|
+
rspec-support (3.11.0)
|
181
181
|
spring (2.1.1)
|
182
182
|
spring-watcher-listen (2.0.1)
|
183
183
|
listen (>= 2.7, < 4.0)
|
@@ -190,14 +190,14 @@ GEM
|
|
190
190
|
activesupport (>= 5.2)
|
191
191
|
sprockets (>= 3.0.0)
|
192
192
|
sqlite3 (1.4.2)
|
193
|
-
thor (1.1
|
193
|
+
thor (1.2.1)
|
194
194
|
thread_safe (0.3.6)
|
195
195
|
tzinfo (1.2.9)
|
196
196
|
thread_safe (~> 0.1)
|
197
197
|
websocket-driver (0.7.5)
|
198
198
|
websocket-extensions (>= 0.1.0)
|
199
199
|
websocket-extensions (0.1.5)
|
200
|
-
zeitwerk (2.5.
|
200
|
+
zeitwerk (2.5.4)
|
201
201
|
|
202
202
|
PLATFORMS
|
203
203
|
ruby
|
@@ -209,7 +209,7 @@ DEPENDENCIES
|
|
209
209
|
dry-types-rails
|
210
210
|
listen (>= 3.0.5, < 3.8)
|
211
211
|
pry
|
212
|
-
puma (~> 5.
|
212
|
+
puma (~> 5.6)
|
213
213
|
rails (~> 6.0.2, >= 6.0.2.2)
|
214
214
|
rspec-rails
|
215
215
|
sober_swag!
|
@@ -42,6 +42,7 @@ module SoberSwag
|
|
42
42
|
# @param type the attribute type
|
43
43
|
def attribute(key, parent = SoberSwag::InputObject, &block)
|
44
44
|
raise ArgumentError, "parent class #{parent} is not an input object type!" unless valid_field_def?(parent, block)
|
45
|
+
raise ArgumentError, "cannot mix reporting and non-reporting types at attribute #{key}" if parent.is_a?(SoberSwag::Reporting::Input::Interface)
|
45
46
|
|
46
47
|
super(key, parent, &block)
|
47
48
|
end
|
@@ -11,6 +11,8 @@ module SoberSwag
|
|
11
11
|
# @param from [Symbol] method name to extract this field from, for convenience.
|
12
12
|
# @param block [Proc] optional way to extract this field.
|
13
13
|
def field(name, serializer, from: nil, &block)
|
14
|
+
raise ArgumentError, "do not mix reporting and non-reporting outputs (at key #{name})" if serializer.is_a?(SoberSwag::Reporting::Output::Interface)
|
15
|
+
|
14
16
|
add_field!(Field.new(name, serializer, from: from, &block))
|
15
17
|
end
|
16
18
|
|
@@ -10,6 +10,13 @@ module SoberSwag
|
|
10
10
|
input
|
11
11
|
end
|
12
12
|
|
13
|
+
##
|
14
|
+
# @param other [Integer] number to specify this is a multiple of
|
15
|
+
# @return [SoberSwag::Reporting::Input::MultipleOf]
|
16
|
+
def multiple_of(other)
|
17
|
+
MultipleOf.new(self, other)
|
18
|
+
end
|
19
|
+
|
13
20
|
def swagger_schema
|
14
21
|
[{ type: 'number' }, {}]
|
15
22
|
end
|
@@ -76,6 +76,7 @@ module SoberSwag
|
|
76
76
|
#
|
77
77
|
def add_attribute!(name, input, required:, description: nil)
|
78
78
|
raise ArgumentError, 'name must be a symbol' unless name.is_a?(Symbol)
|
79
|
+
raise ArgumentError, 'input type must be a SoberSwag::Reporting::Input::Interface' unless input.is_a?(Interface)
|
79
80
|
|
80
81
|
define_attribute(name) # defines an instance method to access this attribute
|
81
82
|
|
@@ -0,0 +1,64 @@
|
|
1
|
+
module SoberSwag
|
2
|
+
module Reporting
|
3
|
+
module Output
|
4
|
+
##
|
5
|
+
# Specify that an output will be within a certain range.
|
6
|
+
# This gets translated to `minimum` and `maximum` keys in swagger.
|
7
|
+
class InRange < Base
|
8
|
+
def initialize(output, range)
|
9
|
+
@output = output
|
10
|
+
@range = range
|
11
|
+
end
|
12
|
+
|
13
|
+
##
|
14
|
+
# @return [Interface]
|
15
|
+
attr_reader :output
|
16
|
+
|
17
|
+
##
|
18
|
+
# @return [Range]
|
19
|
+
attr_reader :range
|
20
|
+
|
21
|
+
def call(value)
|
22
|
+
output.call(value)
|
23
|
+
end
|
24
|
+
|
25
|
+
def serialize_report(value)
|
26
|
+
rep = output.serialize_report(value)
|
27
|
+
|
28
|
+
return rep if rep.is_a?(Report::Base)
|
29
|
+
|
30
|
+
return Report::Value.new(['was not in minimum/maximum range']) unless range.member?(rep)
|
31
|
+
|
32
|
+
rep
|
33
|
+
end
|
34
|
+
|
35
|
+
def swagger_schema
|
36
|
+
schema, found = output.swagger_schema
|
37
|
+
|
38
|
+
merged =
|
39
|
+
if schema.key?(:$ref)
|
40
|
+
{ allOf: [schema] }
|
41
|
+
else
|
42
|
+
schema
|
43
|
+
end.merge(maximum_portion).merge(minimum_portion)
|
44
|
+
|
45
|
+
[merged, found]
|
46
|
+
end
|
47
|
+
|
48
|
+
def maximum_portion
|
49
|
+
return {} unless range.end
|
50
|
+
|
51
|
+
res = { maximum: range.end }
|
52
|
+
res[:exclusiveMaximum] = true if range.exclude_end?
|
53
|
+
res
|
54
|
+
end
|
55
|
+
|
56
|
+
def minimum_portion
|
57
|
+
return {} unless range.begin
|
58
|
+
|
59
|
+
{ minimum: range.begin }
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -40,10 +40,21 @@ module SoberSwag
|
|
40
40
|
Referenced.new(self, name)
|
41
41
|
end
|
42
42
|
|
43
|
+
##
|
44
|
+
# @return [SoberSwag::Reporting::Output::InRange]
|
45
|
+
# Constrained values: must be within the given range.
|
46
|
+
def in_range(range)
|
47
|
+
raise ArgumentError, 'need a range' unless range.is_a?(Range)
|
48
|
+
|
49
|
+
InRange.new(self, range)
|
50
|
+
end
|
51
|
+
|
43
52
|
def list
|
44
53
|
List.new(self)
|
45
54
|
end
|
46
55
|
|
56
|
+
alias array list
|
57
|
+
|
47
58
|
##
|
48
59
|
# Partition this serializer into two potentials.
|
49
60
|
# If the block given returns *false*, we will use `other` as the serializer.
|
@@ -76,10 +87,6 @@ module SoberSwag
|
|
76
87
|
)
|
77
88
|
end
|
78
89
|
|
79
|
-
def array
|
80
|
-
List.new(self)
|
81
|
-
end
|
82
|
-
|
83
90
|
def described(description)
|
84
91
|
Described.new(self, description)
|
85
92
|
end
|
@@ -20,6 +20,8 @@ module SoberSwag
|
|
20
20
|
#
|
21
21
|
# You can access other methods from this method.
|
22
22
|
def field(name, output, description: nil, &extract)
|
23
|
+
raise ArgumentError, bad_field_message(name, output) unless output.is_a?(Interface)
|
24
|
+
|
23
25
|
define_field(name, extract)
|
24
26
|
|
25
27
|
object_fields[name] = Object::Property.new(
|
@@ -158,22 +160,20 @@ module SoberSwag
|
|
158
160
|
# @param name [Symbol] name of this view.
|
159
161
|
# @yieldself [self] a block in which you can add more fields to the view.
|
160
162
|
# @return [Class]
|
161
|
-
def define_view(name, &block)
|
162
|
-
|
163
|
-
|
164
|
-
classy_name = name.to_s.classify
|
163
|
+
def define_view(name, &block)
|
164
|
+
define_view_with_parent(name, self, block)
|
165
|
+
end
|
165
166
|
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
end
|
167
|
+
##
|
168
|
+
# Defines a view for this object, which "inherits" another view.
|
169
|
+
# @see #define_view for how views behave.
|
170
|
+
#
|
171
|
+
# @param name [Symbol] name of this view
|
172
|
+
# @param inherits [Symbol] name of the view this view inherits
|
173
|
+
# @yieldself [self] a block in which you can add more fields to this view
|
174
|
+
# @return [Class]
|
175
|
+
def define_inherited_view(name, inherits:, &block)
|
176
|
+
define_view_with_parent(name, view_class(inherits), block)
|
177
177
|
end
|
178
178
|
|
179
179
|
##
|
@@ -199,6 +199,16 @@ module SoberSwag
|
|
199
199
|
view_map.fetch(name).view(:base)
|
200
200
|
end
|
201
201
|
|
202
|
+
##
|
203
|
+
# Equivalent to .view, but returns the raw view class.
|
204
|
+
#
|
205
|
+
# @return [Class]
|
206
|
+
def view_class(name)
|
207
|
+
return self if name == :base
|
208
|
+
|
209
|
+
view_map.fetch(name)
|
210
|
+
end
|
211
|
+
|
202
212
|
attr_accessor :parent_struct
|
203
213
|
|
204
214
|
##
|
@@ -226,6 +236,29 @@ module SoberSwag
|
|
226
236
|
|
227
237
|
private
|
228
238
|
|
239
|
+
def bad_field_message(name, field_type)
|
240
|
+
[
|
241
|
+
"Output type used for field #{name.inspect} was",
|
242
|
+
"#{field_type.inspect}, which is not an instance of",
|
243
|
+
SoberSwag::Reporting::Output::Interface.name
|
244
|
+
].join(' ')
|
245
|
+
end
|
246
|
+
|
247
|
+
def define_view_with_parent(name, parent, block)
|
248
|
+
raise ArgumentError, "duplicate view #{name}" if name == :base || views.include?(name)
|
249
|
+
|
250
|
+
classy_name = name.to_s.classify
|
251
|
+
us = self # grab this so its identifier doesn't get nested under whatever parent it inherits from, since its our view
|
252
|
+
|
253
|
+
Class.new(parent).tap do |c|
|
254
|
+
c.instance_eval(&block)
|
255
|
+
c.define_singleton_method(:define_view) { |*| raise ArgumentError, 'no nesting views' }
|
256
|
+
c.define_singleton_method(:identifier) { [us.identifier, classy_name.gsub('::', '.')].join('.') }
|
257
|
+
const_set(classy_name, c)
|
258
|
+
view_map[name] = c
|
259
|
+
end
|
260
|
+
end
|
261
|
+
|
229
262
|
def identified_view_map
|
230
263
|
view_map.transform_values(&:identified_without_base).merge(base: inherited_output)
|
231
264
|
end
|
@@ -10,6 +10,7 @@ module SoberSwag
|
|
10
10
|
autoload(:Defer, 'sober_swag/reporting/output/defer')
|
11
11
|
autoload(:Described, 'sober_swag/reporting/output/described')
|
12
12
|
autoload(:Dictionary, 'sober_swag/reporting/output/dictionary')
|
13
|
+
autoload(:InRange, 'sober_swag/reporting/output/in_range')
|
13
14
|
autoload(:Interface, 'sober_swag/reporting/output/interface')
|
14
15
|
autoload(:List, 'sober_swag/reporting/output/list')
|
15
16
|
autoload(:MergeObjects, 'sober_swag/reporting/output/merge_objects')
|
data/lib/sober_swag/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: sober_swag
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.25.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Anthony Super
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2022-
|
11
|
+
date: 2022-06-07 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activesupport
|
@@ -326,6 +326,7 @@ files:
|
|
326
326
|
- lib/sober_swag/reporting/output/described.rb
|
327
327
|
- lib/sober_swag/reporting/output/dictionary.rb
|
328
328
|
- lib/sober_swag/reporting/output/enum.rb
|
329
|
+
- lib/sober_swag/reporting/output/in_range.rb
|
329
330
|
- lib/sober_swag/reporting/output/interface.rb
|
330
331
|
- lib/sober_swag/reporting/output/list.rb
|
331
332
|
- lib/sober_swag/reporting/output/merge_objects.rb
|