sober_swag 0.18.0 → 0.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/dependabot.yml +15 -0
- 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 +22 -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 +116 -123
- 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/interface.rb +87 -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/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 +54 -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/interface.rb +83 -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 +54 -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 +27 -11
- 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 +72 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: da1c1c2df36ccecf700e4d3ecf104c1324f9fa35b4588b4a040238f2513bf4b8
|
|
4
|
+
data.tar.gz: 3d22562035e1d8e93b11876fc82f6a23b1d26e95952d988423559d555adf680a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3a486a7b09b631d567f509c94b3b161350de61971f148ba24f707e117fa6915e6d7be99c6782ff07662da44edc3edb56ebb1691ac0fe8e819fc138dca4513ac9
|
|
7
|
+
data.tar.gz: e4447e3a951d0867c76a6bbd30a49da136d9406d00dfe0c5e0632574746a59fa72b936eb736c573de33bfe01e1d36d36771561fed7b9a6b00be43dcaa0c82653
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# To get started with Dependabot version updates, you'll need to specify which
|
|
2
|
+
# package ecosystems to update and where the package manifests are located.
|
|
3
|
+
# Please see the documentation for all configuration options:
|
|
4
|
+
# https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
|
|
5
|
+
|
|
6
|
+
version: 2
|
|
7
|
+
updates:
|
|
8
|
+
- package-ecosystem: "bundler"
|
|
9
|
+
directory: "/"
|
|
10
|
+
schedule:
|
|
11
|
+
interval: "daily"
|
|
12
|
+
- package-ecosystem: "bundler"
|
|
13
|
+
directory: "/example"
|
|
14
|
+
schedule:
|
|
15
|
+
interval: "daily"
|
|
@@ -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,27 @@
|
|
|
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
|
+
|
|
21
|
+
## [v0.19.0] 2021-03-10
|
|
22
|
+
|
|
23
|
+
- Use [redoc](https://github.com/Redocly/redoc) for generated documentation UI
|
|
24
|
+
|
|
3
25
|
## [v0.18.0] 2021-03-02
|
|
4
26
|
|
|
5
27
|
- Add generic hash type for primitive types
|
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', '~> 4
|
|
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'
|