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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 13d3f48bc3d3d8fb42984b13e7b260f0c5e52331cbc6b641b600511b67aed323
4
- data.tar.gz: 32437d0fc96f42c5c05120946154108d0ffa2a6d9ad3fce648c8c49884addbce
3
+ metadata.gz: 95eb8012259c21946fe6b6ab07aaa4da3b29f5d3746b0581d12a6f67b39f8f5c
4
+ data.tar.gz: d775af4081190de942d2e6d978a922dd37083c9d0a59638cd16259d1c9ad8295
5
5
  SHA512:
6
- metadata.gz: 2e643f5d730c20eac9671c6e306f63c8c0b679dd26eab69a5eea91aa456c094f240399c7058d59300e7c333c3747980a814a7db43f979f2e800cdc9ef940f577
7
- data.tar.gz: 4454eee231879d67313f1d9c1d1b094faf1131d3487f68d96fa1e529055ce48954ad8fa74eeef78b675baf7a4404afb778d607ce701b27a3e40bbeb513aaab15
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
- view :with_grade do
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.4'
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.9.3)
68
- msgpack (~> 1.0)
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.9)
72
+ concurrent-ruby (1.1.10)
73
73
  crass (1.0.6)
74
- diff-lcs (1.4.4)
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.4)
101
+ ffi (1.15.5)
102
102
  globalid (1.0.0)
103
103
  activesupport (>= 5.0)
104
- i18n (1.8.11)
104
+ i18n (1.10.0)
105
105
  concurrent-ruby (~> 1.0)
106
106
  ice_nine (0.11.2)
107
- listen (3.7.0)
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.12.0)
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.6.1)
119
- minitest (5.14.4)
120
- msgpack (1.4.2)
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.12.5)
123
- mini_portile2 (~> 2.6.1)
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.5.2)
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.0)
161
+ rb-fsevent (0.11.1)
162
162
  rb-inotify (0.10.1)
163
163
  ffi (~> 1.0)
164
- rspec-core (3.10.1)
165
- rspec-support (~> 3.10.0)
166
- rspec-expectations (3.10.1)
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.10.0)
169
- rspec-mocks (3.10.2)
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.10.0)
172
- rspec-rails (5.0.2)
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.10.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.0)
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.1)
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.4)
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
 
@@ -35,6 +35,8 @@ module SoberSwag
35
35
  List.new(self)
36
36
  end
37
37
 
38
+ alias array list
39
+
38
40
  ##
39
41
  # Constrained values: must be in range.
40
42
  # @return [InRange]
@@ -6,6 +6,12 @@ module SoberSwag
6
6
  #
7
7
  # Called List to avoid name conflicts.
8
8
  class List < Base
9
+ ##
10
+ # @see #new
11
+ def self.of(element)
12
+ initialize(element)
13
+ end
14
+
9
15
  ##
10
16
  # @param element [Base] the parser for elements
11
17
  def initialize(element)
@@ -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) # rubocop:disable Metrics/MethodLength
162
- raise ArgumentError, "duplicate view #{name}" if name == :base || views.include?(name)
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
- Class.new(self).tap do |c|
167
- c.instance_eval(&block)
168
- c.define_singleton_method(:define_view) do |*|
169
- raise ArgumentError, 'no nesting views'
170
- end
171
- c.define_singleton_method(:identifier) do
172
- [parent_struct.identifier, classy_name.gsub('::', '.')].join('.')
173
- end
174
- const_set(classy_name, c)
175
- view_map[name] = c
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')
@@ -8,6 +8,10 @@ module SoberSwag
8
8
  @report = report
9
9
  end
10
10
 
11
+ def message
12
+ "Reported errors: #{report.full_errors.join(', ')}"
13
+ end
14
+
11
15
  attr_reader :report
12
16
  end
13
17
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SoberSwag
4
- VERSION = '0.23.0'
4
+ VERSION = '0.25.0'
5
5
  end
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.23.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-01-03 00:00:00.000000000 Z
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