sober_swag 0.21.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 (82) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +1 -0
  3. data/CHANGELOG.md +5 -0
  4. data/bin/console +30 -10
  5. data/docs/reporting.md +190 -0
  6. data/example/Gemfile +2 -2
  7. data/example/Gemfile.lock +92 -101
  8. data/example/app/controllers/application_controller.rb +4 -0
  9. data/example/app/controllers/people_controller.rb +44 -28
  10. data/example/app/output_objects/identified_output.rb +7 -0
  11. data/example/app/output_objects/person_output_object.rb +37 -11
  12. data/example/app/output_objects/post_output_object.rb +0 -4
  13. data/example/app/output_objects/reporting_post_output.rb +18 -0
  14. data/example/bin/rspec +29 -0
  15. data/example/spec/requests/people/create_spec.rb +3 -2
  16. data/example/spec/requests/people/index_spec.rb +1 -1
  17. data/lib/sober_swag/compiler/path.rb +3 -1
  18. data/lib/sober_swag/compiler.rb +58 -12
  19. data/lib/sober_swag/controller/route.rb +44 -8
  20. data/lib/sober_swag/controller.rb +18 -5
  21. data/lib/sober_swag/reporting/compiler.rb +39 -0
  22. data/lib/sober_swag/reporting/input/base.rb +11 -0
  23. data/lib/sober_swag/reporting/input/bool.rb +19 -0
  24. data/lib/sober_swag/reporting/input/converting/bool.rb +24 -0
  25. data/lib/sober_swag/reporting/input/converting/date.rb +30 -0
  26. data/lib/sober_swag/reporting/input/converting/date_time.rb +28 -0
  27. data/lib/sober_swag/reporting/input/converting/decimal.rb +24 -0
  28. data/lib/sober_swag/reporting/input/converting/integer.rb +19 -0
  29. data/lib/sober_swag/reporting/input/converting.rb +16 -0
  30. data/lib/sober_swag/reporting/input/defer.rb +29 -0
  31. data/lib/sober_swag/reporting/input/described.rb +38 -0
  32. data/lib/sober_swag/reporting/input/dictionary.rb +37 -0
  33. data/lib/sober_swag/reporting/input/either.rb +51 -0
  34. data/lib/sober_swag/reporting/input/enum.rb +44 -0
  35. data/lib/sober_swag/reporting/input/format.rb +39 -0
  36. data/lib/sober_swag/reporting/input/interface.rb +87 -0
  37. data/lib/sober_swag/reporting/input/list.rb +44 -0
  38. data/lib/sober_swag/reporting/input/mapped.rb +36 -0
  39. data/lib/sober_swag/reporting/input/merge_objects.rb +72 -0
  40. data/lib/sober_swag/reporting/input/null.rb +34 -0
  41. data/lib/sober_swag/reporting/input/number.rb +19 -0
  42. data/lib/sober_swag/reporting/input/object/property.rb +53 -0
  43. data/lib/sober_swag/reporting/input/object.rb +100 -0
  44. data/lib/sober_swag/reporting/input/pattern.rb +46 -0
  45. data/lib/sober_swag/reporting/input/referenced.rb +38 -0
  46. data/lib/sober_swag/reporting/input/struct.rb +271 -0
  47. data/lib/sober_swag/reporting/input/text.rb +42 -0
  48. data/lib/sober_swag/reporting/input.rb +54 -0
  49. data/lib/sober_swag/reporting/invalid_schema_error.rb +21 -0
  50. data/lib/sober_swag/reporting/output/base.rb +25 -0
  51. data/lib/sober_swag/reporting/output/bool.rb +25 -0
  52. data/lib/sober_swag/reporting/output/defer.rb +69 -0
  53. data/lib/sober_swag/reporting/output/described.rb +42 -0
  54. data/lib/sober_swag/reporting/output/dictionary.rb +46 -0
  55. data/lib/sober_swag/reporting/output/interface.rb +83 -0
  56. data/lib/sober_swag/reporting/output/list.rb +54 -0
  57. data/lib/sober_swag/reporting/output/merge_objects.rb +97 -0
  58. data/lib/sober_swag/reporting/output/null.rb +25 -0
  59. data/lib/sober_swag/reporting/output/number.rb +25 -0
  60. data/lib/sober_swag/reporting/output/object/property.rb +45 -0
  61. data/lib/sober_swag/reporting/output/object.rb +54 -0
  62. data/lib/sober_swag/reporting/output/partitioned.rb +77 -0
  63. data/lib/sober_swag/reporting/output/pattern.rb +50 -0
  64. data/lib/sober_swag/reporting/output/referenced.rb +42 -0
  65. data/lib/sober_swag/reporting/output/struct.rb +262 -0
  66. data/lib/sober_swag/reporting/output/text.rb +25 -0
  67. data/lib/sober_swag/reporting/output/via_map.rb +67 -0
  68. data/lib/sober_swag/reporting/output/viewed.rb +72 -0
  69. data/lib/sober_swag/reporting/output.rb +54 -0
  70. data/lib/sober_swag/reporting/report/base.rb +57 -0
  71. data/lib/sober_swag/reporting/report/either.rb +36 -0
  72. data/lib/sober_swag/reporting/report/error.rb +15 -0
  73. data/lib/sober_swag/reporting/report/list.rb +28 -0
  74. data/lib/sober_swag/reporting/report/merged_object.rb +25 -0
  75. data/lib/sober_swag/reporting/report/object.rb +29 -0
  76. data/lib/sober_swag/reporting/report/output.rb +14 -0
  77. data/lib/sober_swag/reporting/report/value.rb +28 -0
  78. data/lib/sober_swag/reporting/report.rb +16 -0
  79. data/lib/sober_swag/reporting.rb +11 -0
  80. data/lib/sober_swag/version.rb +1 -1
  81. data/lib/sober_swag.rb +1 -0
  82. metadata +65 -2
@@ -5,35 +5,40 @@ class PeopleController < ApplicationController
5
5
 
6
6
  before_action :load_person, only: %i[show update]
7
7
 
8
- PersonBodyParams = SoberSwag.input_object do
9
- identifier 'PersonBodyParams'
8
+ ##
9
+ # Parameters to create or update a person.
10
+ class ReportingPersonParams < SoberSwag::Reporting::Input::Struct
11
+ identifier 'PersonReportingParams'
10
12
 
11
- attribute :first_name, SoberSwag::Types::String
12
- attribute :last_name, SoberSwag::Types::String
13
- attribute? :date_of_birth, SoberSwag::Types::Params::DateTime.optional
13
+ attribute :first_name, SoberSwag::Reporting::Input::Text.new.with_pattern(/.+/)
14
+ attribute :last_name, SoberSwag::Reporting::Input::Text.new.with_pattern(/.+/)
15
+ attribute? :date_of_birth, SoberSwag::Reporting::Input::Converting::DateTime.optional
14
16
  end
15
17
 
16
- PersonBodyPatchParams = SoberSwag.input_object(PersonBodyParams) do
17
- identifier 'PersonBodyPatchParams'
18
+ ##
19
+ # Parameters to create a person.
20
+ class ReportingPersonCreate < SoberSwag::Reporting::Input::Struct
21
+ identifier 'ReportingPersonCreate'
18
22
 
19
- attribute? :first_name, SoberSwag::Types::String
20
- attribute? :last_name, SoberSwag::Types::String
21
- attribute? :date_of_birth, SoberSwag::Types::Params::DateTime.optional
23
+ attribute :person, ReportingPersonParams
22
24
  end
23
25
 
24
- PersonParams = SoberSwag.input_object do
25
- identifier 'PersonParams'
26
- attribute :person, PersonBodyParams
27
- end
26
+ ##
27
+ # Patch body for a person.
28
+ class ReportingPersonPatchParams < SoberSwag::Reporting::Input::Struct
29
+ identifier 'PersonReportingPatchParams'
28
30
 
29
- PersonPatchParams = SoberSwag.input_object do
30
- identifier 'PersonPatchParams'
31
- attribute :person, PersonBodyPatchParams
31
+ attribute? :first_name, SoberSwag::Reporting::Input.text.with_pattern(/.+/)
32
+ attribute? :last_name, SoberSwag::Reporting::Input.text.with_pattern(/.+/)
33
+ attribute? :date_of_birth, SoberSwag::Reporting::Input::Converting::DateTime.optional
32
34
  end
33
35
 
34
36
  define :post, :create, '/people/' do
35
- request_body(PersonParams)
37
+ summary 'Create a person'
38
+
39
+ request_body(ReportingPersonCreate)
36
40
  response(:ok, 'the person created', PersonOutputObject)
41
+ response(:bad_request, 'the parse errors', SoberSwag::Reporting::Report::Output)
37
42
  response(:unprocessable_entity, 'the validation errors', PersonErrorsOutputObject)
38
43
  tags 'people', 'create'
39
44
  end
@@ -47,9 +52,14 @@ class PeopleController < ApplicationController
47
52
  end
48
53
 
49
54
  define :patch, :update, '/people/{id}' do
50
- request_body(PersonPatchParams)
51
- path_params { attribute :id, Types::Params::Integer }
55
+ summary 'Update a person'
56
+
57
+ request_body(reporting: true) do
58
+ attribute :person, ReportingPersonPatchParams
59
+ end
60
+ path_params(reporting: true) { attribute :id, SoberSwag::Reporting::Input::Converting::Integer }
52
61
  response(:ok, 'the person updated', PersonOutputObject)
62
+ response(:bad_request, 'the parse errors', SoberSwag::Reporting::Report::Output)
53
63
  response(:unprocessable_entity, 'the validation errors', PersonErrorsOutputObject)
54
64
  tags 'people', 'update'
55
65
  end
@@ -62,28 +72,34 @@ class PeopleController < ApplicationController
62
72
  end
63
73
 
64
74
  define :get, :index, '/people/' do
65
- query_params do
75
+ summary 'List persons'
76
+
77
+ query_params(reporting: true) do
66
78
  attribute? :filters do
67
- attribute? :first_name, Types::String
68
- attribute? :last_name, Types::String
79
+ attribute? :first_name, SoberSwag::Reporting::Input.text
80
+ attribute? :last_name, SoberSwag::Reporting::Input.text
69
81
  end
70
- attribute :view, Types::String.default('base'.freeze).enum('base', 'detail')
82
+ attribute? :view, SoberSwag::Reporting::Input.text.enum('base', 'detail')
71
83
  end
72
- response(:ok, 'all the people', PersonOutputObject.array)
84
+ response(:ok, 'all the people', PersonOutputObject.list)
85
+ response(:bad_request, 'the parse errors', SoberSwag::Reporting::Report::Output)
73
86
  tags 'people', 'list'
74
87
  end
75
88
  def index
76
89
  @people = Person.all
77
90
  @people = @people.where('UPPER(first_name) LIKE UPPER(?)', "%#{parsed_query.filters.first_name}%") if parsed_query.filters&.first_name
78
91
  @people = @people.where('UPPER(last_name) LIKE UPPER(?)', "%#{parsed_query.filters.last_name}%") if parsed_query.filters&.last_name
79
- respond!(:ok, @people.includes(:posts), serializer_opts: { view: parsed_query.view })
92
+ respond!(:ok, @people.includes(:posts), serializer_opts: { view: parsed_query.view || :base })
80
93
  end
81
94
 
82
95
  define :get, :show, '/people/{id}' do
83
- path_params do
84
- attribute :id, Types::Params::Integer
96
+ summary 'Get a single person by id'
97
+
98
+ path_params(reporting: true) do
99
+ attribute :id, SoberSwag::Reporting::Input::Converting::Integer
85
100
  end
86
101
  response(:ok, 'the person requested', PersonOutputObject)
102
+ response(:bad_request, 'the parse errors', SoberSwag::Reporting::Report::Output)
87
103
  tags 'people', 'show'
88
104
  end
89
105
  def show
@@ -0,0 +1,7 @@
1
+ ##
2
+ # Base serializer for objects with a global id of some variety.
3
+ class IdentifiedOutput < SoberSwag::Reporting::Output::Struct
4
+ field :global_id, SoberSwag::Reporting::Output.text.nilable do
5
+ object_to_serialize.try(:to_global_id).try(:to_s)
6
+ end
7
+ end
@@ -1,15 +1,41 @@
1
- PersonOutputObject = SoberSwag::OutputObject.define do
2
- identifier 'Person'
3
- field :id, primitive(:Integer).meta(description: 'Unique ID')
4
- field :first_name, primitive(:String).meta(description: <<~MARKDOWN)
5
- This is the first name of a person.
6
- Note that you can't use this as a unique identifier, and you really should understand how names work before using this.
7
- [Falsehoods programmers believe about names](https://www.kalzumeus.com/2010/06/17/falsehoods-programmers-believe-about-names/)
8
- is a good thing to read!
1
+ ##
2
+ # Output object that serializes a person.
3
+ class PersonOutputObject < IdentifiedOutput
4
+ identifier 'PersonOutput'
5
+
6
+ description <<~MARKDOWN
7
+ Basic as hell serializer for a person.
9
8
  MARKDOWN
10
- field :last_name, primitive(:String)
11
9
 
12
- view :detail do
13
- field :posts, -> { PostOutputObject.array }
10
+ field :id, SoberSwag::Reporting::Output::Text.new.via_map(&:to_s)
11
+ field(
12
+ :first_name,
13
+ SoberSwag::Reporting::Output::Text.new,
14
+ description: <<~MARKDOWN
15
+ This is the first name of a person.
16
+ Note that you can't use this as a unique identifier, and you really should understand how names work before using this.
17
+ [Falsehoods programmers believe about names](https://www.kalzumeus.com/2010/06/17/falsehoods-programmers-believe-about-names/)
18
+ is a good thing to read!
19
+ MARKDOWN
20
+ )
21
+ field(
22
+ :last_name, SoberSwag::Reporting::Output::Text.new
23
+ )
24
+
25
+ define_view :detail do
26
+ field(
27
+ :initials,
28
+ SoberSwag::Reporting::Output::Text.new
29
+ ) do |person|
30
+ [person.first_name[0..0], person.last_name[0..0]].compact.map { |x| "#{x}." }.join(' ')
31
+ end
32
+
33
+ field(
34
+ :posts,
35
+ SoberSwag::Reporting::Output::Defer.defer do
36
+ ReportingPostOutput.view(:base).array
37
+ end,
38
+ description: 'all posts this user has made'
39
+ )
14
40
  end
15
41
  end
@@ -3,8 +3,4 @@ PostOutputObject = SoberSwag::OutputObject.define do
3
3
  field :id, primitive(:Integer)
4
4
  field :title, primitive(:String)
5
5
  field :body, primitive(:String)
6
-
7
- view :detail do
8
- field :person, -> { PersonOutputObject.view(:base) }
9
- end
10
6
  end
@@ -0,0 +1,18 @@
1
+ ##
2
+ # A reporting output object for a post.
3
+ class ReportingPostOutput < SoberSwag::Reporting::Output::Struct
4
+ identifier 'ReportingPostOutput'
5
+
6
+ field :id, SoberSwag::Reporting::Output::Text.new.via_map(&:to_s)
7
+ field :title, SoberSwag::Reporting::Output::Text.new
8
+ field :body, SoberSwag::Reporting::Output::Text.new
9
+
10
+ define_view :detail do
11
+ field(
12
+ :person,
13
+ SoberSwag::Reporting::Output::Defer.defer do
14
+ PersonOutputObject.view(:base)
15
+ end
16
+ )
17
+ end
18
+ end
data/example/bin/rspec ADDED
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ #
5
+ # This file was generated by Bundler.
6
+ #
7
+ # The application 'rspec' is installed as part of a gem, and
8
+ # this file is here to facilitate running it.
9
+ #
10
+
11
+ require 'pathname'
12
+ ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile',
13
+ Pathname.new(__FILE__).realpath)
14
+
15
+ bundle_binstub = File.expand_path('bundle', __dir__)
16
+
17
+ if File.file?(bundle_binstub)
18
+ if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/
19
+ load(bundle_binstub)
20
+ else
21
+ abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
22
+ Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
23
+ end
24
+ end
25
+
26
+ require 'rubygems'
27
+ require 'bundler/setup'
28
+
29
+ load Gem.bin_path('rspec-core', 'rspec')
@@ -34,7 +34,8 @@ RSpec.describe 'people controller create', type: :request do
34
34
 
35
35
  it { should_not be_successful }
36
36
  it { should_not be_server_error }
37
- it { should be_unprocessable }
37
+ it { should be_bad_request }
38
+ it { should have_attributes(status: 400) }
38
39
  end
39
40
 
40
41
  describe 'the act of requesting' do
@@ -46,7 +47,7 @@ RSpec.describe 'people controller create', type: :request do
46
47
  describe 'the response body' do
47
48
  subject { request && response && JSON.parse(response.body) }
48
49
 
49
- it { should have_key('first_name') }
50
+ it { should have_key('$.person.first_name') }
50
51
  end
51
52
  end
52
53
  end
@@ -30,7 +30,7 @@ RSpec.describe 'Index action for people' do
30
30
  end
31
31
 
32
32
  it 'has the right person' do
33
- expect(parsed_body).to include(include('id' => person.id))
33
+ expect(parsed_body).to include(include('id' => person.id.to_s))
34
34
  end
35
35
  end
36
36
 
@@ -51,7 +51,9 @@ module SoberSwag
51
51
  description: route.response_descriptions[status],
52
52
  content: {
53
53
  'application/json': {
54
- schema: compiler.response_for(serializer.type)
54
+ schema: compiler.response_for(
55
+ serializer.respond_to?(:swagger_schema) ? serializer : serializer.type
56
+ )
55
57
  }
56
58
  }
57
59
  }
@@ -13,6 +13,7 @@ module SoberSwag
13
13
  def initialize
14
14
  @types = Set.new
15
15
  @paths = Paths.new
16
+ @reporting_types = SoberSwag::Reporting::Compiler::Schema.new
16
17
  end
17
18
 
18
19
  ##
@@ -28,6 +29,10 @@ module SoberSwag
28
29
  }
29
30
  end
30
31
 
32
+ ##
33
+ # @return [SoberSwag::Reporting::Compiler::Schema]
34
+ attr_reader :reporting_types
35
+
31
36
  ##
32
37
  # Add a path to be compiled.
33
38
  # @param route [SoberSwag::Controller::Route] the route to add.
@@ -41,7 +46,9 @@ module SoberSwag
41
46
  #
42
47
  # @return [Hash]
43
48
  def object_schemas
44
- @types.map { |v| [v.ref_name, v.object_schema] }.to_h
49
+ @types.map { |v| [v.ref_name, v.object_schema] }.to_h.merge(
50
+ reporting_types.references
51
+ )
45
52
  end
46
53
 
47
54
  ##
@@ -59,7 +66,13 @@ module SoberSwag
59
66
  # @param type [Class] the type to get a path_params definition for
60
67
  # @return [Hash]
61
68
  def path_params_for(type)
62
- with_types_discovered(type).path_schema
69
+ compiler = with_types_discovered(type)
70
+
71
+ if compiler.respond_to?(:swagger_path_schema)
72
+ compiler.swagger_path_schema
73
+ else
74
+ compiler.path_schema
75
+ end
63
76
  end
64
77
 
65
78
  ##
@@ -69,7 +82,13 @@ module SoberSwag
69
82
  # @param type [Class] the type to get the query_params definitions for
70
83
  # @return [Hash]
71
84
  def query_params_for(type)
72
- with_types_discovered(type).query_schema
85
+ compiler = with_types_discovered(type)
86
+
87
+ if compiler.respond_to?(:swagger_query_schema)
88
+ compiler.swagger_query_schema
89
+ else
90
+ compiler.query_schema
91
+ end
73
92
  end
74
93
 
75
94
  ##
@@ -80,6 +99,9 @@ module SoberSwag
80
99
  # @return [Hash]
81
100
  def body_for(type)
82
101
  add_type(type)
102
+
103
+ return reporting_types.compile(type) if type.respond_to?(:swagger_schema)
104
+
83
105
  Type.new(type).schema_stub
84
106
  end
85
107
 
@@ -109,23 +131,47 @@ module SoberSwag
109
131
  # use tap here to avoid an explicit self at the end of this
110
132
  # which makes this method chainable
111
133
  tap do
112
- type_compiler = Type.new(type)
134
+ if type.is_a?(SoberSwag::Reporting::Input::Interface) || type.is_a?(SoberSwag::Reporting::Output::Interface)
135
+ add_reporting_type(type)
136
+ else
137
+ add_dry_type(type)
138
+ end
139
+ end
140
+ end
113
141
 
114
- ##
115
- # Do nothing if we already have a type
116
- return self if @types.include?(type_compiler)
142
+ private
117
143
 
118
- @types.add(type_compiler) if type_compiler.standalone?
144
+ def add_dry_type(type)
145
+ type_compiler = Type.new(type)
119
146
 
120
- type_compiler.found_types.each do |ft|
121
- add_type(ft)
122
- end
147
+ ##
148
+ # Do nothing if we already have a type
149
+ return self if @types.include?(type_compiler)
150
+
151
+ @types.add(type_compiler) if type_compiler.standalone?
152
+
153
+ type_compiler.found_types.each do |ft|
154
+ add_type(ft)
123
155
  end
124
156
  end
125
157
 
126
- private
158
+ def add_reporting_type(type)
159
+ reporting_types.compile(type)
160
+ end
127
161
 
128
162
  def with_types_discovered(type)
163
+ if type.respond_to?(:swagger_schema)
164
+ with_reporting_types_discovered(type)
165
+ else
166
+ with_dry_types_discovered(type)
167
+ end
168
+ end
169
+
170
+ def with_reporting_types_discovered(type)
171
+ type.tap { |t| reporting_types.compile(t) }
172
+ end
173
+
174
+ def with_dry_types_discovered(type)
129
175
  Type.new(type).tap do |type_compiler|
130
176
  type_compiler.found_types.each { |ft| add_type(ft) }
131
177
  end
@@ -2,7 +2,7 @@ module SoberSwag
2
2
  module Controller
3
3
  ##
4
4
  # Describe a single controller endpoint.
5
- class Route
5
+ class Route # rubocop:disable Metrics/ClassLength
6
6
  ##
7
7
  # @param method [Symbol] the HTTP method to get
8
8
  # @param action_name [Symbol] the name of the rails action
@@ -83,8 +83,11 @@ module SoberSwag
83
83
  # @overload request_body(base = SoberSwag::InputObject, &block)
84
84
  # Define a Swagger-able type inline to use to parse the request body.
85
85
  # @see SoberSwag.input_object
86
- def request_body(base = SoberSwag::InputObject, &block)
87
- @request_body_class = make_input_object!(base, &block)
86
+ # @overload request_body(base = SoberSwag::Reporting::Input::Struct, reporting: true, &block)
87
+ # Define a swagger-able type inline, using the new reporting system.
88
+ # @see SoberSwag::Reporting::Input::Struct
89
+ def request_body(base = SoberSwag::InputObject, reporting: false, &block)
90
+ @request_body_class = make_input_object!(base, reporting: reporting, &block)
88
91
  action_module.const_set('RequestBody', @request_body_class)
89
92
  end
90
93
 
@@ -101,8 +104,11 @@ module SoberSwag
101
104
  # @overload query_params(base = SoberSwag::InputObject, &block)
102
105
  # Define a Swagger-able type inline to use to parse the query params.
103
106
  # @see SoberSwag.input_object
104
- def query_params(base = SoberSwag::InputObject, &block)
105
- @query_params_class = make_input_object!(base, &block)
107
+ # @overload query_params(base = SoberSwag::Reporting::Input::Struct, reporting: true, &block)
108
+ # Define a swagger-able type inline, using the new reporting system.
109
+ # @see SoberSwag::Reporting::Input::Struct
110
+ def query_params(base = SoberSwag::InputObject, reporting: false, &block)
111
+ @query_params_class = make_input_object!(base, reporting: reporting, &block)
106
112
  action_module.const_set('QueryParams', @query_params_class)
107
113
  end
108
114
 
@@ -119,8 +125,11 @@ module SoberSwag
119
125
  # @overload path_params(base = SoberSwag::InputObject, &block)
120
126
  # Define a Swagger-able type inline to use to parse the path params.
121
127
  # @see SoberSwag.input_object
122
- def path_params(base = SoberSwag::InputObject, &block)
123
- @path_params_class = make_input_object!(base, &block)
128
+ # @overload path_params(base = SoberSwag::Reporting::Input::Struct, reporting: true, &block)
129
+ # Define a swagger-able type inline, using the new reporting system.
130
+ # @see SoberSwag::Reporting::Input::Struct
131
+ def path_params(base = SoberSwag::InputObject, reporting: false, &block)
132
+ @path_params_class = make_input_object!(base, reporting: reporting, &block)
124
133
  action_module.const_set('PathParams', @path_params_class)
125
134
  end
126
135
 
@@ -214,7 +223,34 @@ module SoberSwag
214
223
  @response_module ||= Module.new.tap { |m| action_module.const_set(:Response, m) }
215
224
  end
216
225
 
217
- def make_input_object!(base, &block)
226
+ def make_input_object!(base, reporting:, &block)
227
+ if reporting
228
+ make_reporting_input!(
229
+ base == SoberSwag::InputObject ? SoberSwag::Reporting::Input::Struct : base,
230
+ &block
231
+ )
232
+ else
233
+ make_non_reporting_input!(base, &block)
234
+ end
235
+ end
236
+
237
+ def make_reporting_input!(base, &block)
238
+ if block
239
+ raise ArgumentError, 'non-class passed along with block' unless base.is_a?(Class)
240
+
241
+ make_reporting_input_struct!(base, &block)
242
+ else
243
+ base
244
+ end
245
+ end
246
+
247
+ def make_reporting_input_struct!(base, &block)
248
+ raise ArgumentError, 'base class must be a soberswag reporting class!' unless base <= SoberSwag::Reporting::Input::Struct
249
+
250
+ Class.new(base, &block)
251
+ end
252
+
253
+ def make_non_reporting_input!(base, &block)
218
254
  if base.is_a?(Class)
219
255
  make_input_class(base, block)
220
256
  elsif block
@@ -114,7 +114,7 @@ module SoberSwag
114
114
  r = current_action_def
115
115
  raise UndefinedPathError unless r&.path_params_class
116
116
 
117
- r.path_params_class.call(request.path_parameters)
117
+ build_parsed_sober_swag(r.path_params_class, request.path_parameters)
118
118
  end
119
119
  end
120
120
 
@@ -128,7 +128,7 @@ module SoberSwag
128
128
  r = current_action_def
129
129
  raise UndefinedBodyError unless r&.request_body_class
130
130
 
131
- r.request_body_class.call(body_params)
131
+ build_parsed_sober_swag(r.request_body_class, body_params)
132
132
  end
133
133
  end
134
134
 
@@ -142,7 +142,7 @@ module SoberSwag
142
142
  r = current_action_def
143
143
  raise UndefinedQueryError unless r&.query_params_class
144
144
 
145
- r.query_params_class.call(request.query_parameters)
145
+ build_parsed_sober_swag(r.query_params_class, request.query_parameters)
146
146
  end
147
147
  end
148
148
 
@@ -154,8 +154,13 @@ module SoberSwag
154
154
  def respond!(status, entity, serializer_opts: {}, rails_opts: {})
155
155
  r = current_action_def
156
156
  serializer = r.response_serializers[Rack::Utils.status_code(status)]
157
- serializer ||= serializer.new if serializer.respond_to?(:new)
158
- render json: serializer.serialize(entity, serializer_opts), status: status, **rails_opts
157
+ if serializer.respond_to?(:reporting?) && serializer.reporting?
158
+ serializer = serializer.view(serializer_opts[:view].to_sym) if serializer_opts.key?(:view)
159
+ render json: serializer.call(entity), status: status, **rails_opts
160
+ else
161
+ serializer ||= serializer.new if serializer.respond_to?(:new)
162
+ render json: serializer.serialize(entity, serializer_opts), status: status, **rails_opts
163
+ end
159
164
  end
160
165
 
161
166
  ##
@@ -177,6 +182,14 @@ module SoberSwag
177
182
  def current_action_def
178
183
  self.class.find_route(params[:action])
179
184
  end
185
+
186
+ def build_parsed_sober_swag(parser, params)
187
+ if parser.respond_to?(:call!)
188
+ parser.call!(params)
189
+ else
190
+ parser.call(params)
191
+ end
192
+ end
180
193
  end
181
194
  end
182
195
 
@@ -0,0 +1,39 @@
1
+ module SoberSwag
2
+ module Reporting
3
+ module Compiler
4
+ ##
5
+ # Compile component schemas.
6
+ class Schema
7
+ def initialize
8
+ @references = {}
9
+ @referenced_schemas = Set.new
10
+ end
11
+
12
+ ##
13
+ # Hash of references to type definitions.
14
+ # Suitable for us as the components hash.
15
+ attr_reader :references
16
+
17
+ def compile(value)
18
+ compile_inner(value.swagger_schema)
19
+ end
20
+
21
+ def compile_inner(value)
22
+ initial, found = value
23
+
24
+ merge_found(found)
25
+
26
+ initial
27
+ end
28
+
29
+ def merge_found(found)
30
+ found.each do |k, v|
31
+ next unless @referenced_schemas.add?(k)
32
+
33
+ @references[k] = compile_inner(v.call)
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,11 @@
1
+ module SoberSwag
2
+ module Reporting
3
+ module Input
4
+ ##
5
+ # Base class for all inputs in the cool new schema style.
6
+ class Base
7
+ include Interface
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,19 @@
1
+ module SoberSwag
2
+ module Reporting
3
+ module Input
4
+ ##
5
+ # Convert booleans.
6
+ class Bool < Base
7
+ def call(val)
8
+ return Report::Value.new(['was not a bool']) unless [true, false].include?(val)
9
+
10
+ val
11
+ end
12
+
13
+ def swagger_schema
14
+ [{ type: 'boolean' }, {}]
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,24 @@
1
+ module SoberSwag
2
+ module Reporting
3
+ module Input
4
+ module Converting
5
+ ##
6
+ # Try to convert a boolean-like value to an actual boolean.
7
+ Bool = # rubocop:disable Naming/ConstantName
8
+ (SoberSwag::Reporting::Input::Bool.new |
9
+ (
10
+ SoberSwag::Reporting::Input::Text
11
+ .new
12
+ .enum(*(%w[y yes true t].flat_map { |x| [x, x.upcase] } + ['1'])) |
13
+ SoberSwag::Reporting::Input::Number.new.enum(1)).mapped { |_| true } |
14
+ (
15
+ SoberSwag::Reporting::Input::Text
16
+ .new
17
+ .enum(*(%w[false no n f].flat_map { |x| [x, x.upcase] } + ['0'])) |
18
+ SoberSwag::Reporting::Input::Number.new.enum(0)
19
+ ).mapped { |_| false }
20
+ )
21
+ end
22
+ end
23
+ end
24
+ end