sober_swag 0.24.1 → 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: 85928715576aecc38e7a63176e14595a22531501e36ef9a4ef6202e03e08d8ea
4
- data.tar.gz: 12940b348760152cef2e20c96adb15fc9ebf59e9aaacfd13741da83841c45bdf
3
+ metadata.gz: 95eb8012259c21946fe6b6ab07aaa4da3b29f5d3746b0581d12a6f67b39f8f5c
4
+ data.tar.gz: d775af4081190de942d2e6d978a922dd37083c9d0a59638cd16259d1c9ad8295
5
5
  SHA512:
6
- metadata.gz: 88722e20a08f2b80b64501756474bcbd06758c2b7bcb498b0f0d316f5d335f43e5d937579f1888b8a5332eb536c0172618c75485a52bf6096c6ebe823e8e3f3e
7
- data.tar.gz: 3f98c37fb29945196ce348f48886d9f27b9c913c83aca5520d96318d9711b5d5fc97347566e73c93538e931ad7d70c2ad725a006197504d2197a9e55432158c4
6
+ metadata.gz: 669a66d91c6b2e6d47648a30d44829a5713ee4b0170626953359ed3df5b66f14b21ea76d84654ea4bcafb14f1fe622ce8ac8338de98df530a8d19ef47275fc0b
7
+ data.tar.gz: 92617f0c507187d52c88a07324fc0081503d5095c4bd7f192fb65d252c9dff38ae52da708f2e38665430f6f9d506703e1e1fdc95f4dc0b4c621a33ef577802a0
data/CHANGELOG.md CHANGED
@@ -1,5 +1,10 @@
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
+
3
8
  ## [v0.24.1] 2022-05-26
4
9
 
5
10
  - Added a better `#message` to `SoberSwag::Reporting::Report::Error`
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.lock CHANGED
@@ -119,7 +119,7 @@ GEM
119
119
  minitest (5.15.0)
120
120
  msgpack (1.5.1)
121
121
  nio4r (2.5.8)
122
- nokogiri (1.13.4)
122
+ nokogiri (1.13.6)
123
123
  mini_portile2 (~> 2.8.0)
124
124
  racc (~> 1.4)
125
125
  pry (0.14.1)
@@ -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
@@ -53,6 +53,8 @@ module SoberSwag
53
53
  List.new(self)
54
54
  end
55
55
 
56
+ alias array list
57
+
56
58
  ##
57
59
  # Partition this serializer into two potentials.
58
60
  # If the block given returns *false*, we will use `other` as the serializer.
@@ -85,10 +87,6 @@ module SoberSwag
85
87
  )
86
88
  end
87
89
 
88
- def array
89
- List.new(self)
90
- end
91
-
92
90
  def described(description)
93
91
  Described.new(self, description)
94
92
  end
@@ -20,7 +20,7 @@ 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, "output of field #{name} is not a SoberSwag::Reporting::Output::Interface" unless output.is_a?(Interface)
23
+ raise ArgumentError, bad_field_message(name, output) unless output.is_a?(Interface)
24
24
 
25
25
  define_field(name, extract)
26
26
 
@@ -236,6 +236,14 @@ module SoberSwag
236
236
 
237
237
  private
238
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
+
239
247
  def define_view_with_parent(name, parent, block)
240
248
  raise ArgumentError, "duplicate view #{name}" if name == :base || views.include?(name)
241
249
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SoberSwag
4
- VERSION = '0.24.1'
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.24.1
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-05-26 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