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.
Files changed (130) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/benchmark.yml +39 -0
  3. data/.github/workflows/lint.yml +2 -4
  4. data/.github/workflows/ruby.yml +1 -1
  5. data/.gitignore +3 -0
  6. data/.rubocop.yml +6 -1
  7. data/.yardopts +7 -0
  8. data/CHANGELOG.md +18 -0
  9. data/Gemfile +12 -0
  10. data/README.md +1 -1
  11. data/bench/benchmark.rb +34 -0
  12. data/bench/benchmarks/basic_field_serializer.rb +21 -0
  13. data/bench/benchmarks/view_selection.rb +47 -0
  14. data/bin/console +30 -10
  15. data/docs/reporting.md +190 -0
  16. data/docs/serializers.md +4 -1
  17. data/example/Gemfile +2 -2
  18. data/example/Gemfile.lock +94 -101
  19. data/example/app/controllers/application_controller.rb +4 -0
  20. data/example/app/controllers/people_controller.rb +44 -28
  21. data/example/app/output_objects/identified_output.rb +7 -0
  22. data/example/app/output_objects/person_output_object.rb +37 -11
  23. data/example/app/output_objects/post_output_object.rb +0 -4
  24. data/example/app/output_objects/reporting_post_output.rb +18 -0
  25. data/example/bin/rspec +29 -0
  26. data/example/config/environments/production.rb +1 -1
  27. data/example/spec/requests/people/create_spec.rb +3 -2
  28. data/example/spec/requests/people/index_spec.rb +1 -1
  29. data/lib/sober_swag/compiler/path.rb +45 -4
  30. data/lib/sober_swag/compiler/paths.rb +20 -0
  31. data/lib/sober_swag/compiler/primitive.rb +17 -0
  32. data/lib/sober_swag/compiler/type.rb +105 -22
  33. data/lib/sober_swag/compiler.rb +87 -15
  34. data/lib/sober_swag/controller/route.rb +147 -28
  35. data/lib/sober_swag/controller.rb +57 -17
  36. data/lib/sober_swag/input_object.rb +124 -7
  37. data/lib/sober_swag/nodes/array.rb +19 -0
  38. data/lib/sober_swag/nodes/attribute.rb +45 -4
  39. data/lib/sober_swag/nodes/base.rb +27 -7
  40. data/lib/sober_swag/nodes/binary.rb +30 -13
  41. data/lib/sober_swag/nodes/enum.rb +16 -1
  42. data/lib/sober_swag/nodes/list.rb +20 -0
  43. data/lib/sober_swag/nodes/nullable_primitive.rb +3 -0
  44. data/lib/sober_swag/nodes/object.rb +4 -1
  45. data/lib/sober_swag/nodes/one_of.rb +11 -3
  46. data/lib/sober_swag/nodes/primitive.rb +34 -2
  47. data/lib/sober_swag/nodes/sum.rb +8 -0
  48. data/lib/sober_swag/output_object/definition.rb +57 -1
  49. data/lib/sober_swag/output_object/field.rb +31 -11
  50. data/lib/sober_swag/output_object/field_syntax.rb +19 -3
  51. data/lib/sober_swag/output_object/view.rb +46 -1
  52. data/lib/sober_swag/output_object.rb +40 -19
  53. data/lib/sober_swag/parser.rb +7 -1
  54. data/lib/sober_swag/reporting/compiler.rb +39 -0
  55. data/lib/sober_swag/reporting/input/base.rb +11 -0
  56. data/lib/sober_swag/reporting/input/bool.rb +19 -0
  57. data/lib/sober_swag/reporting/input/converting/bool.rb +24 -0
  58. data/lib/sober_swag/reporting/input/converting/date.rb +30 -0
  59. data/lib/sober_swag/reporting/input/converting/date_time.rb +28 -0
  60. data/lib/sober_swag/reporting/input/converting/decimal.rb +24 -0
  61. data/lib/sober_swag/reporting/input/converting/integer.rb +19 -0
  62. data/lib/sober_swag/reporting/input/converting.rb +16 -0
  63. data/lib/sober_swag/reporting/input/defer.rb +29 -0
  64. data/lib/sober_swag/reporting/input/described.rb +38 -0
  65. data/lib/sober_swag/reporting/input/dictionary.rb +37 -0
  66. data/lib/sober_swag/reporting/input/either.rb +51 -0
  67. data/lib/sober_swag/reporting/input/enum.rb +44 -0
  68. data/lib/sober_swag/reporting/input/format.rb +39 -0
  69. data/lib/sober_swag/reporting/input/in_range.rb +61 -0
  70. data/lib/sober_swag/reporting/input/interface.rb +113 -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/multiple_of.rb +36 -0
  75. data/lib/sober_swag/reporting/input/null.rb +34 -0
  76. data/lib/sober_swag/reporting/input/number.rb +19 -0
  77. data/lib/sober_swag/reporting/input/object/property.rb +53 -0
  78. data/lib/sober_swag/reporting/input/object.rb +100 -0
  79. data/lib/sober_swag/reporting/input/pattern.rb +46 -0
  80. data/lib/sober_swag/reporting/input/referenced.rb +38 -0
  81. data/lib/sober_swag/reporting/input/struct.rb +271 -0
  82. data/lib/sober_swag/reporting/input/text.rb +42 -0
  83. data/lib/sober_swag/reporting/input.rb +56 -0
  84. data/lib/sober_swag/reporting/invalid_schema_error.rb +21 -0
  85. data/lib/sober_swag/reporting/output/base.rb +25 -0
  86. data/lib/sober_swag/reporting/output/bool.rb +25 -0
  87. data/lib/sober_swag/reporting/output/defer.rb +69 -0
  88. data/lib/sober_swag/reporting/output/described.rb +42 -0
  89. data/lib/sober_swag/reporting/output/dictionary.rb +46 -0
  90. data/lib/sober_swag/reporting/output/enum.rb +47 -0
  91. data/lib/sober_swag/reporting/output/interface.rb +89 -0
  92. data/lib/sober_swag/reporting/output/list.rb +54 -0
  93. data/lib/sober_swag/reporting/output/merge_objects.rb +97 -0
  94. data/lib/sober_swag/reporting/output/null.rb +25 -0
  95. data/lib/sober_swag/reporting/output/number.rb +25 -0
  96. data/lib/sober_swag/reporting/output/object/property.rb +45 -0
  97. data/lib/sober_swag/reporting/output/object.rb +54 -0
  98. data/lib/sober_swag/reporting/output/partitioned.rb +77 -0
  99. data/lib/sober_swag/reporting/output/pattern.rb +50 -0
  100. data/lib/sober_swag/reporting/output/referenced.rb +42 -0
  101. data/lib/sober_swag/reporting/output/struct.rb +262 -0
  102. data/lib/sober_swag/reporting/output/text.rb +25 -0
  103. data/lib/sober_swag/reporting/output/via_map.rb +67 -0
  104. data/lib/sober_swag/reporting/output/viewed.rb +72 -0
  105. data/lib/sober_swag/reporting/output.rb +55 -0
  106. data/lib/sober_swag/reporting/report/base.rb +57 -0
  107. data/lib/sober_swag/reporting/report/either.rb +36 -0
  108. data/lib/sober_swag/reporting/report/error.rb +15 -0
  109. data/lib/sober_swag/reporting/report/list.rb +28 -0
  110. data/lib/sober_swag/reporting/report/merged_object.rb +25 -0
  111. data/lib/sober_swag/reporting/report/object.rb +29 -0
  112. data/lib/sober_swag/reporting/report/output.rb +14 -0
  113. data/lib/sober_swag/reporting/report/value.rb +28 -0
  114. data/lib/sober_swag/reporting/report.rb +16 -0
  115. data/lib/sober_swag/reporting.rb +11 -0
  116. data/lib/sober_swag/serializer/array.rb +27 -3
  117. data/lib/sober_swag/serializer/base.rb +75 -25
  118. data/lib/sober_swag/serializer/conditional.rb +33 -1
  119. data/lib/sober_swag/serializer/field_list.rb +23 -5
  120. data/lib/sober_swag/serializer/hash.rb +53 -0
  121. data/lib/sober_swag/serializer/mapped.rb +10 -1
  122. data/lib/sober_swag/serializer/optional.rb +18 -1
  123. data/lib/sober_swag/serializer/primitive.rb +3 -0
  124. data/lib/sober_swag/serializer.rb +1 -0
  125. data/lib/sober_swag/server.rb +5 -1
  126. data/lib/sober_swag/type/named.rb +14 -0
  127. data/lib/sober_swag/types/comma_array.rb +4 -0
  128. data/lib/sober_swag/version.rb +1 -1
  129. data/lib/sober_swag.rb +7 -1
  130. metadata +74 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 63ab4c5db5ca2e6fe63713b7416d127b3bdc730e82562d4ca13b477eaf362460
4
- data.tar.gz: ce6351db33dc3506be6baa66e228532bd8e3830c9e7b67b27c2c6b899a77677c
3
+ metadata.gz: 13d3f48bc3d3d8fb42984b13e7b260f0c5e52331cbc6b641b600511b67aed323
4
+ data.tar.gz: 32437d0fc96f42c5c05120946154108d0ffa2a6d9ad3fce648c8c49884addbce
5
5
  SHA512:
6
- metadata.gz: bab9a59d19cef6552f33555a5cac081c750df97f0364d06fb39fbd5ad4973262f43ca233d52be1f0544815830345a59574dd4913ca2ec6cb2ef7f9e465502f35
7
- data.tar.gz: f0e3db070ac4b30457d309d68a88c81b2cb090478553a622f9463e8cff9f3f67bb128f120ebcd9ce73164f21fb2e83b4a8befa02e87779e241fae96188ccc077
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
@@ -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,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
@@ -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', '~> 5.2'
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.5'
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'