sober_swag 0.19.0 → 0.23.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/workflows/benchmark.yml +39 -0
- data/.github/workflows/lint.yml +2 -4
- data/.github/workflows/ruby.yml +1 -1
- data/.gitignore +3 -0
- data/.rubocop.yml +6 -1
- data/.yardopts +7 -0
- data/CHANGELOG.md +18 -0
- data/Gemfile +12 -0
- data/README.md +1 -1
- data/bench/benchmark.rb +34 -0
- data/bench/benchmarks/basic_field_serializer.rb +21 -0
- data/bench/benchmarks/view_selection.rb +47 -0
- data/bin/console +30 -10
- data/docs/reporting.md +190 -0
- data/docs/serializers.md +4 -1
- data/example/Gemfile +2 -2
- data/example/Gemfile.lock +94 -101
- data/example/app/controllers/application_controller.rb +4 -0
- data/example/app/controllers/people_controller.rb +44 -28
- data/example/app/output_objects/identified_output.rb +7 -0
- data/example/app/output_objects/person_output_object.rb +37 -11
- data/example/app/output_objects/post_output_object.rb +0 -4
- data/example/app/output_objects/reporting_post_output.rb +18 -0
- data/example/bin/rspec +29 -0
- data/example/config/environments/production.rb +1 -1
- data/example/spec/requests/people/create_spec.rb +3 -2
- data/example/spec/requests/people/index_spec.rb +1 -1
- data/lib/sober_swag/compiler/path.rb +45 -4
- data/lib/sober_swag/compiler/paths.rb +20 -0
- data/lib/sober_swag/compiler/primitive.rb +17 -0
- data/lib/sober_swag/compiler/type.rb +105 -22
- data/lib/sober_swag/compiler.rb +87 -15
- data/lib/sober_swag/controller/route.rb +147 -28
- data/lib/sober_swag/controller.rb +57 -17
- data/lib/sober_swag/input_object.rb +124 -7
- data/lib/sober_swag/nodes/array.rb +19 -0
- data/lib/sober_swag/nodes/attribute.rb +45 -4
- data/lib/sober_swag/nodes/base.rb +27 -7
- data/lib/sober_swag/nodes/binary.rb +30 -13
- data/lib/sober_swag/nodes/enum.rb +16 -1
- data/lib/sober_swag/nodes/list.rb +20 -0
- data/lib/sober_swag/nodes/nullable_primitive.rb +3 -0
- data/lib/sober_swag/nodes/object.rb +4 -1
- data/lib/sober_swag/nodes/one_of.rb +11 -3
- data/lib/sober_swag/nodes/primitive.rb +34 -2
- data/lib/sober_swag/nodes/sum.rb +8 -0
- data/lib/sober_swag/output_object/definition.rb +57 -1
- data/lib/sober_swag/output_object/field.rb +31 -11
- data/lib/sober_swag/output_object/field_syntax.rb +19 -3
- data/lib/sober_swag/output_object/view.rb +46 -1
- data/lib/sober_swag/output_object.rb +40 -19
- data/lib/sober_swag/parser.rb +7 -1
- data/lib/sober_swag/reporting/compiler.rb +39 -0
- data/lib/sober_swag/reporting/input/base.rb +11 -0
- data/lib/sober_swag/reporting/input/bool.rb +19 -0
- data/lib/sober_swag/reporting/input/converting/bool.rb +24 -0
- data/lib/sober_swag/reporting/input/converting/date.rb +30 -0
- data/lib/sober_swag/reporting/input/converting/date_time.rb +28 -0
- data/lib/sober_swag/reporting/input/converting/decimal.rb +24 -0
- data/lib/sober_swag/reporting/input/converting/integer.rb +19 -0
- data/lib/sober_swag/reporting/input/converting.rb +16 -0
- data/lib/sober_swag/reporting/input/defer.rb +29 -0
- data/lib/sober_swag/reporting/input/described.rb +38 -0
- data/lib/sober_swag/reporting/input/dictionary.rb +37 -0
- data/lib/sober_swag/reporting/input/either.rb +51 -0
- data/lib/sober_swag/reporting/input/enum.rb +44 -0
- data/lib/sober_swag/reporting/input/format.rb +39 -0
- data/lib/sober_swag/reporting/input/in_range.rb +61 -0
- data/lib/sober_swag/reporting/input/interface.rb +113 -0
- data/lib/sober_swag/reporting/input/list.rb +44 -0
- data/lib/sober_swag/reporting/input/mapped.rb +36 -0
- data/lib/sober_swag/reporting/input/merge_objects.rb +72 -0
- data/lib/sober_swag/reporting/input/multiple_of.rb +36 -0
- data/lib/sober_swag/reporting/input/null.rb +34 -0
- data/lib/sober_swag/reporting/input/number.rb +19 -0
- data/lib/sober_swag/reporting/input/object/property.rb +53 -0
- data/lib/sober_swag/reporting/input/object.rb +100 -0
- data/lib/sober_swag/reporting/input/pattern.rb +46 -0
- data/lib/sober_swag/reporting/input/referenced.rb +38 -0
- data/lib/sober_swag/reporting/input/struct.rb +271 -0
- data/lib/sober_swag/reporting/input/text.rb +42 -0
- data/lib/sober_swag/reporting/input.rb +56 -0
- data/lib/sober_swag/reporting/invalid_schema_error.rb +21 -0
- data/lib/sober_swag/reporting/output/base.rb +25 -0
- data/lib/sober_swag/reporting/output/bool.rb +25 -0
- data/lib/sober_swag/reporting/output/defer.rb +69 -0
- data/lib/sober_swag/reporting/output/described.rb +42 -0
- data/lib/sober_swag/reporting/output/dictionary.rb +46 -0
- data/lib/sober_swag/reporting/output/enum.rb +47 -0
- data/lib/sober_swag/reporting/output/interface.rb +89 -0
- data/lib/sober_swag/reporting/output/list.rb +54 -0
- data/lib/sober_swag/reporting/output/merge_objects.rb +97 -0
- data/lib/sober_swag/reporting/output/null.rb +25 -0
- data/lib/sober_swag/reporting/output/number.rb +25 -0
- data/lib/sober_swag/reporting/output/object/property.rb +45 -0
- data/lib/sober_swag/reporting/output/object.rb +54 -0
- data/lib/sober_swag/reporting/output/partitioned.rb +77 -0
- data/lib/sober_swag/reporting/output/pattern.rb +50 -0
- data/lib/sober_swag/reporting/output/referenced.rb +42 -0
- data/lib/sober_swag/reporting/output/struct.rb +262 -0
- data/lib/sober_swag/reporting/output/text.rb +25 -0
- data/lib/sober_swag/reporting/output/via_map.rb +67 -0
- data/lib/sober_swag/reporting/output/viewed.rb +72 -0
- data/lib/sober_swag/reporting/output.rb +55 -0
- data/lib/sober_swag/reporting/report/base.rb +57 -0
- data/lib/sober_swag/reporting/report/either.rb +36 -0
- data/lib/sober_swag/reporting/report/error.rb +15 -0
- data/lib/sober_swag/reporting/report/list.rb +28 -0
- data/lib/sober_swag/reporting/report/merged_object.rb +25 -0
- data/lib/sober_swag/reporting/report/object.rb +29 -0
- data/lib/sober_swag/reporting/report/output.rb +14 -0
- data/lib/sober_swag/reporting/report/value.rb +28 -0
- data/lib/sober_swag/reporting/report.rb +16 -0
- data/lib/sober_swag/reporting.rb +11 -0
- data/lib/sober_swag/serializer/array.rb +27 -3
- data/lib/sober_swag/serializer/base.rb +75 -25
- data/lib/sober_swag/serializer/conditional.rb +33 -1
- data/lib/sober_swag/serializer/field_list.rb +23 -5
- data/lib/sober_swag/serializer/hash.rb +53 -0
- data/lib/sober_swag/serializer/mapped.rb +10 -1
- data/lib/sober_swag/serializer/optional.rb +18 -1
- data/lib/sober_swag/serializer/primitive.rb +3 -0
- data/lib/sober_swag/serializer.rb +1 -0
- data/lib/sober_swag/server.rb +5 -1
- data/lib/sober_swag/type/named.rb +14 -0
- data/lib/sober_swag/types/comma_array.rb +4 -0
- data/lib/sober_swag/version.rb +1 -1
- data/lib/sober_swag.rb +7 -1
- metadata +74 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 13d3f48bc3d3d8fb42984b13e7b260f0c5e52331cbc6b641b600511b67aed323
|
4
|
+
data.tar.gz: 32437d0fc96f42c5c05120946154108d0ffa2a6d9ad3fce648c8c49884addbce
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 2e643f5d730c20eac9671c6e306f63c8c0b679dd26eab69a5eea91aa456c094f240399c7058d59300e7c333c3747980a814a7db43f979f2e800cdc9ef940f577
|
7
|
+
data.tar.gz: 4454eee231879d67313f1d9c1d1b094faf1131d3487f68d96fa1e529055ce48954ad8fa74eeef78b675baf7a4404afb778d607ce701b27a3e40bbeb513aaab15
|
@@ -0,0 +1,39 @@
|
|
1
|
+
name: Ruby Benchmark
|
2
|
+
|
3
|
+
on:
|
4
|
+
push:
|
5
|
+
branches: [ master ]
|
6
|
+
pull_request:
|
7
|
+
branches: [ master ]
|
8
|
+
|
9
|
+
jobs:
|
10
|
+
benchmark:
|
11
|
+
|
12
|
+
runs-on: ubuntu-latest
|
13
|
+
strategy:
|
14
|
+
matrix:
|
15
|
+
ruby: [ '2.6', '2.7', '3.0' ]
|
16
|
+
|
17
|
+
steps:
|
18
|
+
- uses: actions/checkout@v2
|
19
|
+
- name: Set up Ruby
|
20
|
+
uses: ruby/setup-ruby@v1
|
21
|
+
with:
|
22
|
+
ruby-version: ${{ matrix.ruby }}
|
23
|
+
- uses: actions/cache@v2
|
24
|
+
with:
|
25
|
+
path: vendor/bundle
|
26
|
+
key: ${{ runner.os }}-${{ matrix.ruby }}-gem-deps-${{ hashFiles('**/Gemfile.lock') }}
|
27
|
+
restore-keys: |
|
28
|
+
${{ runner.os }}-${{ matrix.ruby }}-gem-deps-
|
29
|
+
- name: Install dependencies
|
30
|
+
run: |
|
31
|
+
bundle config path vendor/bundle
|
32
|
+
bundle install
|
33
|
+
- name: Run Benchmark
|
34
|
+
run: bundle exec ruby bench/benchmark.rb
|
35
|
+
- uses: actions/upload-artifact@v2
|
36
|
+
with:
|
37
|
+
name: benchmark-result
|
38
|
+
path: benchmark_results.yaml
|
39
|
+
if-no-files-found: error
|
data/.github/workflows/lint.yml
CHANGED
data/.github/workflows/ruby.yml
CHANGED
data/.gitignore
CHANGED
data/.rubocop.yml
CHANGED
@@ -2,10 +2,12 @@ require: rubocop-rspec
|
|
2
2
|
|
3
3
|
AllCops:
|
4
4
|
TargetRubyVersion: 2.6.0
|
5
|
+
NewCops: enable
|
5
6
|
Exclude:
|
6
7
|
- 'bin/bundle'
|
7
8
|
- 'example/bin/bundle'
|
8
9
|
- 'vendor/bundle/**/*'
|
10
|
+
- 'bin/console'
|
9
11
|
|
10
12
|
Layout/LineLength:
|
11
13
|
Max: 160
|
@@ -22,6 +24,9 @@ Metrics/BlockLength:
|
|
22
24
|
- 'sober_swag.gemspec'
|
23
25
|
- 'example/spec/**/*.rb'
|
24
26
|
|
27
|
+
Lint/MissingSuper:
|
28
|
+
Enabled: false
|
29
|
+
|
25
30
|
RSpec/ImplicitBlockExpectation:
|
26
31
|
Enabled: false
|
27
32
|
|
@@ -123,4 +128,4 @@ Style/FrozenStringLiteralComment:
|
|
123
128
|
Enabled: false
|
124
129
|
|
125
130
|
Style/BlockDelimiters:
|
126
|
-
EnforcedStyle: braces_for_chaining
|
131
|
+
EnforcedStyle: braces_for_chaining
|
data/.yardopts
ADDED
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,23 @@
|
|
1
1
|
# Changelog
|
2
2
|
|
3
|
+
## [v0.22.0] 2021-12-21
|
4
|
+
|
5
|
+
- Added `SoberSwag::Reporting`, which is basically a v2 of the gem!
|
6
|
+
Docs for this can be found in [docs/reporting.md].
|
7
|
+
|
8
|
+
## [v0.21.0] 2021-09-02
|
9
|
+
|
10
|
+
- Added a new method of serializing views based on hash lookups, improving performance
|
11
|
+
- Added a benchmarking suite
|
12
|
+
- Added `except` parameter to the `merge` method, which allows a specified field to be excluded from the merge.
|
13
|
+
- Add `type_key` to output objects, for easily serializing out type fields with a constant string.
|
14
|
+
- Added `type_attribute` to `SoberSwag::InputObject` to add easy constant-value disambiguation.
|
15
|
+
|
16
|
+
## [v0.20.0] 2021-05-17
|
17
|
+
|
18
|
+
- Added YARD documentation to almost every method
|
19
|
+
|
20
|
+
|
3
21
|
## [v0.19.0] 2021-03-10
|
4
22
|
|
5
23
|
- Use [redoc](https://github.com/Redocly/redoc) for generated documentation UI
|
data/Gemfile
CHANGED
data/README.md
CHANGED
@@ -11,7 +11,7 @@ An introductory presentation is available [here](https://www.icloud.com/keynote/
|
|
11
11
|
|
12
12
|
Further documentation on using the gem is available in the `docs/` directory:
|
13
13
|
|
14
|
-
-
|
14
|
+
- {file:docs/serializers.md Serializers}
|
15
15
|
|
16
16
|
## Types for a fully-automated API
|
17
17
|
|
data/bench/benchmark.rb
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
require 'bundler/setup'
|
2
|
+
|
3
|
+
require 'sober_swag'
|
4
|
+
|
5
|
+
require 'yaml'
|
6
|
+
require 'benchmark/ips'
|
7
|
+
|
8
|
+
##
|
9
|
+
# Quick and dirty way to benchmark things.
|
10
|
+
class Bench
|
11
|
+
class << self
|
12
|
+
def report(name, &block)
|
13
|
+
puts name
|
14
|
+
|
15
|
+
data[name] ||= Benchmark.ips(&block).data
|
16
|
+
end
|
17
|
+
|
18
|
+
def data
|
19
|
+
@data ||= {}
|
20
|
+
end
|
21
|
+
|
22
|
+
def write!(filename)
|
23
|
+
File.open(filename, 'w') do |f|
|
24
|
+
f << YAML.dump(data)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
Dir['bench/benchmarks/**/*.rb'].sort.each do |file|
|
31
|
+
require_relative file.gsub(%r{^bench/}, '')
|
32
|
+
end
|
33
|
+
|
34
|
+
Bench.write!('benchmark_results.yaml')
|
@@ -0,0 +1,21 @@
|
|
1
|
+
##
|
2
|
+
# Bench test for serializing multiple fields.
|
3
|
+
class BasicFieldSerializer
|
4
|
+
Idea = Struct.new(:name, :grade, :cool)
|
5
|
+
|
6
|
+
Output = SoberSwag::OutputObject.define do
|
7
|
+
field :name, primitive(:String)
|
8
|
+
field :grade, primitive(:Integer)
|
9
|
+
field :cool, primitive(:Bool)
|
10
|
+
end
|
11
|
+
|
12
|
+
OutputSerializer = Output.serializer
|
13
|
+
|
14
|
+
MyIdea = Idea.new('Bob', 12, false)
|
15
|
+
|
16
|
+
Bench.report 'Basic Field Serializers' do |bm|
|
17
|
+
bm.report('Output Object') { Output.serialize(MyIdea) }
|
18
|
+
bm.report('Serializer of Output Object') { OutputSerializer.serialize(MyIdea) }
|
19
|
+
bm.compare!
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
##
|
2
|
+
# Benchmark for speed of selecting what view to use.
|
3
|
+
class ViewSelection
|
4
|
+
Accomplishment = Struct.new(:name, :description)
|
5
|
+
Person = Struct.new(:first_name, :last_name, :accomplishments)
|
6
|
+
|
7
|
+
MyPerson = Person.new(
|
8
|
+
'Joeseph',
|
9
|
+
'Biden',
|
10
|
+
[
|
11
|
+
Accomplishment.new('Became President', 'Won a Presidential Election'),
|
12
|
+
Accomplishment.new('Oldest President', 'Oldest man to be elected president at time of election'),
|
13
|
+
Accomplishment.new('Became Senator', 'Got Elected to the Senate'),
|
14
|
+
Accomplishment.new('Youngest Senator', 'Youngest person elected Senator at time of election')
|
15
|
+
]
|
16
|
+
)
|
17
|
+
|
18
|
+
AccomplishmentSerializer = SoberSwag::OutputObject.define do
|
19
|
+
field :name, primitive(:String)
|
20
|
+
|
21
|
+
view :detail do
|
22
|
+
field :description, primitive(:String)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
PersonSerializer = SoberSwag::OutputObject.define do
|
27
|
+
field :first_name, primitive(:String)
|
28
|
+
field :last_name, primitive(:String)
|
29
|
+
|
30
|
+
# make a bunch of dummy views
|
31
|
+
1.upto(10).each { |n| view(:"view_#{n}") {} }
|
32
|
+
|
33
|
+
view :detail do
|
34
|
+
field :accomplishments, AccomplishmentSerializer.view(:detail)
|
35
|
+
end
|
36
|
+
|
37
|
+
1.upto(10).each { |n| view(:"view_after_#{n}") {} }
|
38
|
+
end
|
39
|
+
|
40
|
+
Bench.report 'View Selection' do |bm|
|
41
|
+
bm.report('With no view') { PersonSerializer.serialize(MyPerson) }
|
42
|
+
|
43
|
+
bm.report('With a view') { PersonSerializer.serialize(MyPerson, { view: :detail }) }
|
44
|
+
|
45
|
+
bm.compare!
|
46
|
+
end
|
47
|
+
end
|
data/bin/console
CHANGED
@@ -9,11 +9,6 @@ Bio = SoberSwag.input_object do
|
|
9
9
|
attribute :gender, SoberSwag::Types::String.enum('male', 'female') | SoberSwag::Types::String
|
10
10
|
end
|
11
11
|
|
12
|
-
Person = SoberSwag.input_object do
|
13
|
-
attribute :name, SoberSwag::Types::String
|
14
|
-
attribute? :bio, Bio.optional
|
15
|
-
end
|
16
|
-
|
17
12
|
MultiFloorLocation = SoberSwag.input_object do
|
18
13
|
attribute :building, SoberSwag::Types::String.enum('science', 'mathematics', 'literature')
|
19
14
|
attribute :floor, SoberSwag::Types::String
|
@@ -25,12 +20,37 @@ SingleFloorLocation = SoberSwag.input_object do
|
|
25
20
|
attribute :room, SoberSwag::Types::Integer
|
26
21
|
end
|
27
22
|
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
23
|
+
SortDirections = SoberSwag::Types::CommaArray.of(SoberSwag::Types::String.enum('created_at', 'updated_at', '-created_at', '-updated_at'))
|
24
|
+
|
25
|
+
# test
|
26
|
+
class Whatever < SoberSwag::Reporting::Input::Struct
|
27
|
+
attribute :first_name, SoberSwag::Reporting::Input::Text.new
|
28
|
+
attribute :last_name, SoberSwag::Reporting::Input::Text.new
|
29
|
+
attribute? :father, SoberSwag::Reporting::Input::Null.new | SoberSwag::Reporting::Input::Defer.new(proc { Whatever }), description: 'if the father is in our db, will be present'
|
30
|
+
attribute? :mother, SoberSwag::Reporting::Input::Null.new | SoberSwag::Reporting::Input::Defer.new(proc { Whatever }), description: 'if the mother is in our db, will be present'
|
32
31
|
end
|
33
32
|
|
34
|
-
|
33
|
+
# Kinda neat thing
|
34
|
+
class Otherwised < Whatever
|
35
|
+
attribute :ident, SoberSwag::Reporting::Input::Text.new.with_pattern(/^[A-Za-z0-9]+$/)
|
36
|
+
end
|
37
|
+
|
38
|
+
ArrayOfPeople = Otherwised.or(Whatever).list
|
39
|
+
|
40
|
+
##
|
41
|
+
# Output for a person
|
42
|
+
class OutputPerson < SoberSwag::Reporting::Output::Struct
|
43
|
+
field :first_name, SoberSwag::Reporting::Output::Text.new
|
44
|
+
field :last_name, SoberSwag::Reporting::Output::Text.new
|
45
|
+
define_view :detail do
|
46
|
+
field :initials, SoberSwag::Reporting::Output::Text.new do |obj|
|
47
|
+
[obj.first_name, obj.last_name].map { |e| e[0..0] }.map { |e| "#{e}." }.join(' ')
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
Person = Struct.new(:first_name, :last_name)
|
53
|
+
|
54
|
+
OutputPerson.view(:base)
|
35
55
|
|
36
56
|
Pry.start
|
data/docs/reporting.md
ADDED
@@ -0,0 +1,190 @@
|
|
1
|
+
# SoberSwag Reporting
|
2
|
+
|
3
|
+
`SoberSwag::Reporting` is a new module that provides a more *composable* interface of sober swag types.
|
4
|
+
Unlike the base version, it is *not* based on `dry-types`, instead using a simpler scheme.
|
5
|
+
It also allows for modeling of more complex type domains, and more reusable types.
|
6
|
+
|
7
|
+
The module is called `SoberSwag::Reporting` because it *reports* what happened on failure.
|
8
|
+
Consider trying to parse a struct with a first and last name, both of which need to be non-empty strings.
|
9
|
+
If I give it this input:
|
10
|
+
|
11
|
+
```json
|
12
|
+
{
|
13
|
+
"first_name": 10,
|
14
|
+
"last_name": ""
|
15
|
+
}
|
16
|
+
```
|
17
|
+
|
18
|
+
I will get back a report, which can tell me:
|
19
|
+
|
20
|
+
```json
|
21
|
+
{
|
22
|
+
"$.first_name": ["must be a string"],
|
23
|
+
"$.last_name": ["does not match pattern (.+)"]
|
24
|
+
}
|
25
|
+
```
|
26
|
+
|
27
|
+
As you can see, we get a dictionary of JSON-path values to errors.
|
28
|
+
|
29
|
+
More interestingly, you can use serializers in "reporting mode," which will *verify* that you're actually serializing what you said you would.
|
30
|
+
If you mess up, it'll give you a report of where the errors were.
|
31
|
+
This is intended to be used to make writing specs easier: to check that your serializer gives the right types, just use it in reporting mode and check the value!
|
32
|
+
|
33
|
+
## Basic Design
|
34
|
+
|
35
|
+
SoberSwag's reporting module is designed from the ground up to be *node-based*.
|
36
|
+
Everything conforms to a common interface.
|
37
|
+
For reporting *inputs*, all values:
|
38
|
+
|
39
|
+
- Have a method `call` which converts an input to the desired format, or a report if there was an error
|
40
|
+
- Have a method `call!` which converts an input to the desired format, or raises an exception if there was an error
|
41
|
+
- Have a method `swagger_schema` which converts the node to its swagger schema.
|
42
|
+
|
43
|
+
|
44
|
+
For reporting *outputs*, all values:
|
45
|
+
|
46
|
+
- Have a method `call` which serializes out a value
|
47
|
+
- Have a method `serialize_report` which serializes in "reporting mode," IE, it will return a report if serialization happened improperly
|
48
|
+
- Have a method `views` which returns a *set of applicable views*.
|
49
|
+
This is used to implement a serializer with many alternatives.
|
50
|
+
These views *propagate* correctly.
|
51
|
+
So if you have views `[:base, :detail]` on a serializer for a person, a serializer for an array of people will have the same views.
|
52
|
+
- Have a method `view` which takes in an argument, and returns a serializer specialized to that view.
|
53
|
+
- Have a method `swagger_schema` which converts the node to its swagger schema
|
54
|
+
|
55
|
+
From there, everything is done via composition.
|
56
|
+
Nodes delegate to other nodes to provide functionality like "wrap this type in a common reference" and "validate that this string matches this regexp."
|
57
|
+
Currently, the only validator built-in is matching a regexp or being a member of an enum, but we may add more in the future.
|
58
|
+
You can also use `.mapped` to do custom validation:
|
59
|
+
|
60
|
+
```ruby
|
61
|
+
NotTheStringBob = SoberSwag::Reporting::Input.text.mapped do |text|
|
62
|
+
if text == "Bob"
|
63
|
+
Report::Value.new(['was the string bob I specifically told you not to be the string bob'])
|
64
|
+
else
|
65
|
+
text
|
66
|
+
end
|
67
|
+
end
|
68
|
+
```
|
69
|
+
|
70
|
+
## Input Structs
|
71
|
+
|
72
|
+
SoberSwag's reporting mode includes a class called `SoberSwag::Reporting::Input::Struct`.
|
73
|
+
It can be used to model *struct inputs*, IE, inputs that have some properties and are represented by JSON objects.
|
74
|
+
|
75
|
+
These structs behave much like Ruby structs, and implement inheritance *correctly*.
|
76
|
+
This means that the following works:
|
77
|
+
|
78
|
+
```ruby
|
79
|
+
class Person < SoberSwag::Reporting::Input::Struct
|
80
|
+
attribute :first_name, SoberSwag::Reporting::Input.text
|
81
|
+
attribute :last_name, SoberSwag::Reporting::Input.text
|
82
|
+
end
|
83
|
+
|
84
|
+
class GradedPerson < Person
|
85
|
+
attribute :grade, SoberSwag::Reporting::Input.text.enum('A', 'B', 'C', 'D', 'F')
|
86
|
+
end
|
87
|
+
```
|
88
|
+
|
89
|
+
## Output Structs
|
90
|
+
|
91
|
+
SoberSwag's Reporting Output Structs work much the same way.
|
92
|
+
You can define *fields* on them, which they will serialize.
|
93
|
+
You can use them to define how to serialize an object, like so:
|
94
|
+
|
95
|
+
```ruby
|
96
|
+
class PersonOutput < SoberSwag::Output::Struct
|
97
|
+
field :first_name, SoberSwag::Reporting::Output.text
|
98
|
+
field :last_name, SoberSwag::Reporting::Output.text
|
99
|
+
|
100
|
+
field :grade, SoberSwag::Reporting::Output.text.nilable do
|
101
|
+
if object_to_serialize.respond_to?(:grade)
|
102
|
+
object_to_serialize.grade
|
103
|
+
else
|
104
|
+
nil
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
field :has_grade, SoberSwag::Reporting::Output.bool do
|
109
|
+
grade.nil? # fields are defined as *methods* on the output struct
|
110
|
+
end
|
111
|
+
end
|
112
|
+
```
|
113
|
+
|
114
|
+
Output Structs can also have *views*.
|
115
|
+
Views can be nested only once - if you use a serializer with views as the key of an object, we will *always* use the base view.
|
116
|
+
This prevents some weirdness with the non-reporting SoberSwag serializers, where views could technically be read by child objects in some circumstances as they were only passed in the `view` key.
|
117
|
+
A view will *always inherit all attributes of the parent object, regardless of order.*
|
118
|
+
|
119
|
+
```ruby
|
120
|
+
class AlternativePersonOutput < SoberSwag::Output::Struct
|
121
|
+
field :first_name, SoberSwag::Reporting::Output.text
|
122
|
+
|
123
|
+
view :with_grade do
|
124
|
+
field :grade, SoberSwag::Reporting::Output.text.nilable do
|
125
|
+
if object_to_serialize.respond_to?(:grade)
|
126
|
+
object_to_serialize.grade
|
127
|
+
else
|
128
|
+
nil
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
field :last_name, SoberSwag::Reporting::Output.text
|
134
|
+
end
|
135
|
+
|
136
|
+
AlternativePersonOutput.views # => Set.new(:base, :with_grade)
|
137
|
+
AlternativePersonOutput.view(:with_grade).serialize(my_person) # includes the last_name field
|
138
|
+
```
|
139
|
+
|
140
|
+
View relationships are modeled with *composition*.
|
141
|
+
This leads to slightly more natural to read swagger schemas.
|
142
|
+
|
143
|
+
## Dictionary Types
|
144
|
+
|
145
|
+
SoberSwag's reporting outputs allow defining a *dictionary* of key-value types.
|
146
|
+
This lets you represent an object like this in your schema:
|
147
|
+
|
148
|
+
```json
|
149
|
+
{
|
150
|
+
"name": "Advanced Time Travel",
|
151
|
+
"student_grades": {
|
152
|
+
"student_id_1": "F",
|
153
|
+
"student_id_2": "F"
|
154
|
+
}
|
155
|
+
}
|
156
|
+
```
|
157
|
+
|
158
|
+
This type would probably be represented by:
|
159
|
+
|
160
|
+
```ruby
|
161
|
+
class Classroom < SoberSwag::Reporting::Input::Struct
|
162
|
+
attribute :name, SoberSwag::Reporting::Input.text
|
163
|
+
attribute :student_grades, SoberSwag::Reporting::Input::Dictionary.of(
|
164
|
+
SoberSwag::Reporting::Input.text.enum('A', 'B', 'C', 'D', 'F')
|
165
|
+
)
|
166
|
+
end
|
167
|
+
```
|
168
|
+
|
169
|
+
## Referenced Types
|
170
|
+
|
171
|
+
If you have a type you use a lot, and you want to refer to it by a common name, you can describe it like so:
|
172
|
+
|
173
|
+
```ruby
|
174
|
+
GradeEnum = SoberSwag::Reporting::Input.text.enum('A', 'B', 'C', 'D', 'F').referenced('GradeEnum')
|
175
|
+
```
|
176
|
+
|
177
|
+
This will now be represented as a Reference type in generated swagger.
|
178
|
+
|
179
|
+
## Things not present
|
180
|
+
|
181
|
+
There are basically two things to keep in mind when upgrading to `SoberSwag::Reporting`.
|
182
|
+
|
183
|
+
1. There is no longer a `default` for a type.
|
184
|
+
This is because that was really hard to model in Swagger, and can be better served via use of `.mapped` and `.optional`.
|
185
|
+
We may add this back eventually.
|
186
|
+
2. Serializers no longer take an arbitrary `options` key.
|
187
|
+
Instead, view management is now *explicit*.
|
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
|
+
|
190
|
+
|
data/docs/serializers.md
CHANGED
@@ -105,7 +105,7 @@ This changes the type properly too.
|
|
105
105
|
|
106
106
|
98% of the time, when we're writing web APIs, we want to transform our domain objects into JSON objects.
|
107
107
|
We often want different ways to do this, too.
|
108
|
-
Consider, for
|
108
|
+
Consider, for example, an API for a college.
|
109
109
|
We might want to provide one detailed way to serialize a student, which includes their full name, grade, student ID, GPA, and so on.
|
110
110
|
On another page, we might want to display a classroom with a list of students.
|
111
111
|
However, on the classroom page, we don't want to serialize a full student: that's sending too much data.
|
@@ -241,6 +241,9 @@ end
|
|
241
241
|
Using `#merge` lets you add in all the fields from one output object into another.
|
242
242
|
You can even use `merge` from within a view.
|
243
243
|
|
244
|
+
Exclude any unneeded fields from the merge by passing a hash:
|
245
|
+
`merge GenericBioOutput, { except: [:position] }`
|
246
|
+
|
244
247
|
Note that `merge` does *not* copy anything but fields.
|
245
248
|
Identifiers and views will not be copied over.
|
246
249
|
|
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.4'
|
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
|
@@ -34,7 +34,7 @@ group :development, :test do
|
|
34
34
|
end
|
35
35
|
|
36
36
|
group :development do
|
37
|
-
gem 'listen', '>= 3.0.5', '< 3.
|
37
|
+
gem 'listen', '>= 3.0.5', '< 3.8'
|
38
38
|
# Spring speeds up development by keeping your application running in the background. Read more: https://github.com/rails/spring
|
39
39
|
gem 'spring'
|
40
40
|
gem 'spring-watcher-listen', '~> 2.0.0'
|