sober_swag 0.23.0 → 0.25.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|