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.
Files changed (128) hide show
  1. checksums.yaml +4 -4
  2. data/.github/dependabot.yml +15 -0
  3. data/.github/workflows/benchmark.yml +39 -0
  4. data/.github/workflows/lint.yml +2 -4
  5. data/.github/workflows/ruby.yml +1 -1
  6. data/.gitignore +3 -0
  7. data/.rubocop.yml +6 -1
  8. data/.yardopts +7 -0
  9. data/CHANGELOG.md +22 -0
  10. data/Gemfile +12 -0
  11. data/README.md +1 -1
  12. data/bench/benchmark.rb +34 -0
  13. data/bench/benchmarks/basic_field_serializer.rb +21 -0
  14. data/bench/benchmarks/view_selection.rb +47 -0
  15. data/bin/console +30 -10
  16. data/docs/reporting.md +190 -0
  17. data/docs/serializers.md +4 -1
  18. data/example/Gemfile +2 -2
  19. data/example/Gemfile.lock +116 -123
  20. data/example/app/controllers/application_controller.rb +4 -0
  21. data/example/app/controllers/people_controller.rb +44 -28
  22. data/example/app/output_objects/identified_output.rb +7 -0
  23. data/example/app/output_objects/person_output_object.rb +37 -11
  24. data/example/app/output_objects/post_output_object.rb +0 -4
  25. data/example/app/output_objects/reporting_post_output.rb +18 -0
  26. data/example/bin/rspec +29 -0
  27. data/example/config/environments/production.rb +1 -1
  28. data/example/spec/requests/people/create_spec.rb +3 -2
  29. data/example/spec/requests/people/index_spec.rb +1 -1
  30. data/lib/sober_swag/compiler/path.rb +45 -4
  31. data/lib/sober_swag/compiler/paths.rb +20 -0
  32. data/lib/sober_swag/compiler/primitive.rb +17 -0
  33. data/lib/sober_swag/compiler/type.rb +105 -22
  34. data/lib/sober_swag/compiler.rb +87 -15
  35. data/lib/sober_swag/controller/route.rb +147 -28
  36. data/lib/sober_swag/controller.rb +57 -17
  37. data/lib/sober_swag/input_object.rb +124 -7
  38. data/lib/sober_swag/nodes/array.rb +19 -0
  39. data/lib/sober_swag/nodes/attribute.rb +45 -4
  40. data/lib/sober_swag/nodes/base.rb +27 -7
  41. data/lib/sober_swag/nodes/binary.rb +30 -13
  42. data/lib/sober_swag/nodes/enum.rb +16 -1
  43. data/lib/sober_swag/nodes/list.rb +20 -0
  44. data/lib/sober_swag/nodes/nullable_primitive.rb +3 -0
  45. data/lib/sober_swag/nodes/object.rb +4 -1
  46. data/lib/sober_swag/nodes/one_of.rb +11 -3
  47. data/lib/sober_swag/nodes/primitive.rb +34 -2
  48. data/lib/sober_swag/nodes/sum.rb +8 -0
  49. data/lib/sober_swag/output_object/definition.rb +57 -1
  50. data/lib/sober_swag/output_object/field.rb +31 -11
  51. data/lib/sober_swag/output_object/field_syntax.rb +19 -3
  52. data/lib/sober_swag/output_object/view.rb +46 -1
  53. data/lib/sober_swag/output_object.rb +40 -19
  54. data/lib/sober_swag/parser.rb +7 -1
  55. data/lib/sober_swag/reporting/compiler.rb +39 -0
  56. data/lib/sober_swag/reporting/input/base.rb +11 -0
  57. data/lib/sober_swag/reporting/input/bool.rb +19 -0
  58. data/lib/sober_swag/reporting/input/converting/bool.rb +24 -0
  59. data/lib/sober_swag/reporting/input/converting/date.rb +30 -0
  60. data/lib/sober_swag/reporting/input/converting/date_time.rb +28 -0
  61. data/lib/sober_swag/reporting/input/converting/decimal.rb +24 -0
  62. data/lib/sober_swag/reporting/input/converting/integer.rb +19 -0
  63. data/lib/sober_swag/reporting/input/converting.rb +16 -0
  64. data/lib/sober_swag/reporting/input/defer.rb +29 -0
  65. data/lib/sober_swag/reporting/input/described.rb +38 -0
  66. data/lib/sober_swag/reporting/input/dictionary.rb +37 -0
  67. data/lib/sober_swag/reporting/input/either.rb +51 -0
  68. data/lib/sober_swag/reporting/input/enum.rb +44 -0
  69. data/lib/sober_swag/reporting/input/format.rb +39 -0
  70. data/lib/sober_swag/reporting/input/interface.rb +87 -0
  71. data/lib/sober_swag/reporting/input/list.rb +44 -0
  72. data/lib/sober_swag/reporting/input/mapped.rb +36 -0
  73. data/lib/sober_swag/reporting/input/merge_objects.rb +72 -0
  74. data/lib/sober_swag/reporting/input/null.rb +34 -0
  75. data/lib/sober_swag/reporting/input/number.rb +19 -0
  76. data/lib/sober_swag/reporting/input/object/property.rb +53 -0
  77. data/lib/sober_swag/reporting/input/object.rb +100 -0
  78. data/lib/sober_swag/reporting/input/pattern.rb +46 -0
  79. data/lib/sober_swag/reporting/input/referenced.rb +38 -0
  80. data/lib/sober_swag/reporting/input/struct.rb +271 -0
  81. data/lib/sober_swag/reporting/input/text.rb +42 -0
  82. data/lib/sober_swag/reporting/input.rb +54 -0
  83. data/lib/sober_swag/reporting/invalid_schema_error.rb +21 -0
  84. data/lib/sober_swag/reporting/output/base.rb +25 -0
  85. data/lib/sober_swag/reporting/output/bool.rb +25 -0
  86. data/lib/sober_swag/reporting/output/defer.rb +69 -0
  87. data/lib/sober_swag/reporting/output/described.rb +42 -0
  88. data/lib/sober_swag/reporting/output/dictionary.rb +46 -0
  89. data/lib/sober_swag/reporting/output/interface.rb +83 -0
  90. data/lib/sober_swag/reporting/output/list.rb +54 -0
  91. data/lib/sober_swag/reporting/output/merge_objects.rb +97 -0
  92. data/lib/sober_swag/reporting/output/null.rb +25 -0
  93. data/lib/sober_swag/reporting/output/number.rb +25 -0
  94. data/lib/sober_swag/reporting/output/object/property.rb +45 -0
  95. data/lib/sober_swag/reporting/output/object.rb +54 -0
  96. data/lib/sober_swag/reporting/output/partitioned.rb +77 -0
  97. data/lib/sober_swag/reporting/output/pattern.rb +50 -0
  98. data/lib/sober_swag/reporting/output/referenced.rb +42 -0
  99. data/lib/sober_swag/reporting/output/struct.rb +262 -0
  100. data/lib/sober_swag/reporting/output/text.rb +25 -0
  101. data/lib/sober_swag/reporting/output/via_map.rb +67 -0
  102. data/lib/sober_swag/reporting/output/viewed.rb +72 -0
  103. data/lib/sober_swag/reporting/output.rb +54 -0
  104. data/lib/sober_swag/reporting/report/base.rb +57 -0
  105. data/lib/sober_swag/reporting/report/either.rb +36 -0
  106. data/lib/sober_swag/reporting/report/error.rb +15 -0
  107. data/lib/sober_swag/reporting/report/list.rb +28 -0
  108. data/lib/sober_swag/reporting/report/merged_object.rb +25 -0
  109. data/lib/sober_swag/reporting/report/object.rb +29 -0
  110. data/lib/sober_swag/reporting/report/output.rb +14 -0
  111. data/lib/sober_swag/reporting/report/value.rb +28 -0
  112. data/lib/sober_swag/reporting/report.rb +16 -0
  113. data/lib/sober_swag/reporting.rb +11 -0
  114. data/lib/sober_swag/serializer/array.rb +27 -3
  115. data/lib/sober_swag/serializer/base.rb +75 -25
  116. data/lib/sober_swag/serializer/conditional.rb +33 -1
  117. data/lib/sober_swag/serializer/field_list.rb +23 -5
  118. data/lib/sober_swag/serializer/hash.rb +53 -0
  119. data/lib/sober_swag/serializer/mapped.rb +10 -1
  120. data/lib/sober_swag/serializer/optional.rb +18 -1
  121. data/lib/sober_swag/serializer/primitive.rb +3 -0
  122. data/lib/sober_swag/serializer.rb +1 -0
  123. data/lib/sober_swag/server.rb +27 -11
  124. data/lib/sober_swag/type/named.rb +14 -0
  125. data/lib/sober_swag/types/comma_array.rb +4 -0
  126. data/lib/sober_swag/version.rb +1 -1
  127. data/lib/sober_swag.rb +7 -1
  128. metadata +72 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9eb6d22ca8888336349e7026415930671b82c71a038170b0beacca33067b04c2
4
- data.tar.gz: ed557b57dd3dd9f6f7372c183453b49ba2b63ff73e64170743e000abd2cda922
3
+ metadata.gz: da1c1c2df36ccecf700e4d3ecf104c1324f9fa35b4588b4a040238f2513bf4b8
4
+ data.tar.gz: 3d22562035e1d8e93b11876fc82f6a23b1d26e95952d988423559d555adf680a
5
5
  SHA512:
6
- metadata.gz: df3229da4dd3e9a6a326b276c415e65a8796e6c99a12340bffe8ca998496c604af6b10e71590f137c4e6f9036defc71747f790b980fc812fbcd61151e2831751
7
- data.tar.gz: 01bb12cb3887d0c8143156a04f10f3c4d4ad4c363a65be404f7a8ba08c01280fee2ca1a6f600623421ca5109651a836ca74a048e3d169a922abda3668e452e27
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
@@ -14,13 +14,11 @@ on:
14
14
  branches: [ master ]
15
15
 
16
16
  jobs:
17
- test:
18
-
17
+ lint:
19
18
  runs-on: ubuntu-latest
20
19
  strategy:
21
20
  matrix:
22
- ruby: [ '2.6', '2.7' ]
23
-
21
+ ruby: [ '2.6', '2.7', '3.0' ]
24
22
  steps:
25
23
  - uses: actions/checkout@v2
26
24
  - name: Set up Ruby
@@ -19,7 +19,7 @@ jobs:
19
19
  runs-on: ubuntu-latest
20
20
  strategy:
21
21
  matrix:
22
- ruby: [ '2.6', '2.7' ]
22
+ ruby: [ '2.6', '2.7', '3.0' ]
23
23
  steps:
24
24
  - uses: actions/checkout@v2
25
25
  - name: Set up Ruby
data/.gitignore CHANGED
@@ -15,3 +15,6 @@ Gemfile.lock
15
15
  *.gem
16
16
 
17
17
  /vendor/
18
+
19
+ .yardoc
20
+ benchmark_results.yaml
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
@@ -0,0 +1,7 @@
1
+ --markup-provider=redcarpet
2
+ --markup=markdown
3
+ --plugin activesupport-concern
4
+ --plugin solargraph
5
+ --private
6
+ --protected
7
+ --files docs/serializers.md
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
@@ -4,3 +4,15 @@ source 'https://rubygems.org'
4
4
 
5
5
  # Specify your gem's dependencies in sober_swag.gemspec
6
6
  gemspec
7
+
8
+ gem 'yard'
9
+
10
+ gem 'redcarpet'
11
+
12
+ gem 'yard-activesupport-concern'
13
+
14
+ gem 'solargraph'
15
+
16
+ gem 'benchmark-ips'
17
+
18
+ gem 'rspec-its'
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
- - [Serializers](docs/serializers.md)
14
+ - {file:docs/serializers.md Serializers}
15
15
 
16
16
  ## Types for a fully-automated API
17
17
 
@@ -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
- SchoolClass = SoberSwag.input_object do
29
- attribute :prof, Person.meta(description: 'The person who teaches this class.')
30
- attribute :students, SoberSwag::Types::Array.of(Person)
31
- attribute :location, (SingleFloorLocation | MultiFloorLocation).meta(description: 'What building and room this is in')
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
- SortDirections = SoberSwag::Types::CommaArray.of(SoberSwag::Types::String.enum('created_at', 'updated_at', '-created_at', '-updated_at'))
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 exmaple, and API for a college.
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.3'
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.2'
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'