sober_swag 0.21.0 → 0.22.0

Sign up to get free protection for your applications and to get access to all the features.
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