sober_swag 0.15.0 → 0.20.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 (55) hide show
  1. checksums.yaml +4 -4
  2. data/.github/dependabot.yml +15 -0
  3. data/.github/workflows/lint.yml +4 -9
  4. data/.github/workflows/ruby.yml +2 -6
  5. data/.gitignore +4 -0
  6. data/.rubocop.yml +50 -5
  7. data/.yardopts +7 -0
  8. data/CHANGELOG.md +29 -1
  9. data/Gemfile +8 -0
  10. data/README.md +155 -4
  11. data/bin/rspec +29 -0
  12. data/docs/serializers.md +18 -13
  13. data/example/Gemfile +2 -2
  14. data/example/app/controllers/people_controller.rb +4 -0
  15. data/example/app/controllers/posts_controller.rb +5 -0
  16. data/example/config/environments/production.rb +1 -1
  17. data/lib/sober_swag.rb +6 -1
  18. data/lib/sober_swag/compiler.rb +29 -3
  19. data/lib/sober_swag/compiler/path.rb +49 -3
  20. data/lib/sober_swag/compiler/paths.rb +20 -0
  21. data/lib/sober_swag/compiler/primitive.rb +20 -1
  22. data/lib/sober_swag/compiler/type.rb +105 -22
  23. data/lib/sober_swag/controller.rb +42 -15
  24. data/lib/sober_swag/controller/route.rb +133 -28
  25. data/lib/sober_swag/input_object.rb +117 -7
  26. data/lib/sober_swag/nodes/array.rb +19 -0
  27. data/lib/sober_swag/nodes/attribute.rb +45 -4
  28. data/lib/sober_swag/nodes/base.rb +27 -7
  29. data/lib/sober_swag/nodes/binary.rb +30 -13
  30. data/lib/sober_swag/nodes/enum.rb +16 -1
  31. data/lib/sober_swag/nodes/list.rb +20 -0
  32. data/lib/sober_swag/nodes/nullable_primitive.rb +3 -0
  33. data/lib/sober_swag/nodes/object.rb +4 -1
  34. data/lib/sober_swag/nodes/one_of.rb +11 -3
  35. data/lib/sober_swag/nodes/primitive.rb +34 -2
  36. data/lib/sober_swag/nodes/sum.rb +8 -0
  37. data/lib/sober_swag/output_object.rb +35 -4
  38. data/lib/sober_swag/output_object/definition.rb +31 -1
  39. data/lib/sober_swag/output_object/field.rb +31 -11
  40. data/lib/sober_swag/output_object/field_syntax.rb +19 -3
  41. data/lib/sober_swag/output_object/view.rb +46 -1
  42. data/lib/sober_swag/parser.rb +7 -1
  43. data/lib/sober_swag/serializer/array.rb +27 -3
  44. data/lib/sober_swag/serializer/base.rb +75 -25
  45. data/lib/sober_swag/serializer/conditional.rb +33 -1
  46. data/lib/sober_swag/serializer/field_list.rb +18 -2
  47. data/lib/sober_swag/serializer/mapped.rb +10 -1
  48. data/lib/sober_swag/serializer/optional.rb +18 -1
  49. data/lib/sober_swag/serializer/primitive.rb +3 -0
  50. data/lib/sober_swag/server.rb +27 -11
  51. data/lib/sober_swag/type/named.rb +14 -0
  52. data/lib/sober_swag/types/comma_array.rb +4 -0
  53. data/lib/sober_swag/version.rb +1 -1
  54. data/sober_swag.gemspec +2 -2
  55. metadata +13 -10
@@ -3,34 +3,89 @@ module SoberSwag
3
3
  ##
4
4
  # Describe a single controller endpoint.
5
5
  class Route
6
+ ##
7
+ # @param method [Symbol] the HTTP method to get
8
+ # @param action_name [Symbol] the name of the rails action
9
+ # (the name of the controller method, usually)
10
+ # @param path [String] an OpenAPI V3 path template,
11
+ # which should [match this format](https://swagger.io/docs/specification/describing-parameters/#path-parameters)
6
12
  def initialize(method, action_name, path)
7
13
  @method = method
8
14
  @path = path
9
15
  @action_name = action_name
10
16
  @response_serializers = {}
11
17
  @response_descriptions = {}
18
+ @tags = []
12
19
  end
13
20
 
14
- attr_reader :response_serializers, :response_descriptions, :controller, :method, :path, :action_name
21
+ ##
22
+ # A hash of response code -> response serializer
23
+ # @return [Hash{Symbol => SoberSwag::Serializer::Base}]
24
+ # response code to response serializer
25
+ attr_reader :response_serializers
26
+
27
+ ##
28
+ # A hash of response code -> response description
29
+ # @return [Hash{Symbol => String}]
30
+ # response code to response description
31
+ attr_reader :response_descriptions
32
+
33
+ ##
34
+ # The HTTP method of this route.
35
+ # @return [Symbol]
36
+ attr_reader :method
15
37
 
16
38
  ##
17
- # What to parse the request body in to.
39
+ # The swagger path specifier of this route.
40
+ # @return [String]
41
+ attr_reader :path
42
+
43
+ ##
44
+ # The name of the rails action (usually the controller method) of this route.
45
+ # @return [Symbol]
46
+ attr_reader :action_name
47
+
48
+ ##
49
+ # What to parse the request body into.
50
+ # @return [Class] a swagger-able class type for a request body.
18
51
  attr_reader :request_body_class
19
52
  ##
20
- # What to parse the request query_params in to
53
+ # What to parse the request query_params into.
54
+ # @return [Class] a swagger-able class type for query parameters.
21
55
  attr_reader :query_params_class
22
-
23
56
  ##
24
- # What to parse the path params into
57
+ # What to parse the path params into.
58
+ # @return [Class] a swagger-able class type for path parameters.
25
59
  attr_reader :path_params_class
26
60
 
61
+ ##
62
+ # Standard swagger tags.
63
+ #
64
+ # @overload tags()
65
+ # Get the tags for this route.
66
+ # @return [Array<String,Symbol>] the tags.
67
+ # @overload tags(*args)
68
+ # Set the tags for this route.
69
+ # @param tags [Array<String,Symbol>] the tags to set
70
+ # @return [Array<String,Symbol>] the tags used
71
+ def tags(*args)
72
+ return @tags if args.empty?
73
+
74
+ @tags = args.flatten
75
+ end
76
+
27
77
  ##
28
78
  # Define the request body, using SoberSwag's type-definition scheme.
29
- # The block passed will be used to define the body of a new sublcass of `base` (defaulted to {SoberSwag::InputObject}.)
30
- # If you want, you can also define utility methods in here
79
+ # The block passed will be used to define the body of a new subclass of `base` (defaulted to {SoberSwag::InputObject}.)
80
+ # @overload request_body(base)
81
+ # Give a Swagger-able type that will be used to parse the request body, and used in generated docs.
82
+ # @param base [Class] a swagger-able class
83
+ # @overload request_body(base = SoberSwag::InputObject, &block)
84
+ # Define a Swagger-able type inline to use to parse the request body.
85
+ # @see SoberSwag.input_object
31
86
  def request_body(base = SoberSwag::InputObject, &block)
32
87
  @request_body_class = make_input_object!(base, &block)
33
- action_module.const_set('ResponseBody', @request_body_class)
88
+ action_module.const_set('RequestBody', @request_body_class)
34
89
  end
35
90
 
36
91
  ##
@@ -40,9 +95,12 @@ module SoberSwag
40
95
  end
41
96
 
42
97
  ##
43
- # Define the shape of the query_params parameters, using SoberSwag's type-definition scheme.
44
- # The block passed is the body of the newly-defined type.
45
- # You can also include a base type.
98
+ # @overload query_params(base)
99
+ # Give a Swagger-able type that will be used to parse the query params, and used in generated docs.
100
+ # @param base [Class] a swagger-able class
101
+ # @overload query_params(base = SoberSwag::InputObject, &block)
102
+ # Define a Swagger-able type inline to use to parse the query params.
103
+ # @see SoberSwag.input_object
46
104
  def query_params(base = SoberSwag::InputObject, &block)
47
105
  @query_params_class = make_input_object!(base, &block)
48
106
  action_module.const_set('QueryParams', @query_params_class)
@@ -55,9 +113,12 @@ module SoberSwag
55
113
  end
56
114
 
57
115
  ##
58
- # Define the shape of the *path* parameters, using SoberSwag's type-definition scheme.
59
- # The block passed will be the body of a new subclass of `base` (defaulted to {SoberSwag::InputObject}).
60
- # Names of this should match the names in the path template originally passed to {SoberSwag::Controller::Route.new}
116
+ # @overload path_params(base)
117
+ # Give a Swagger-able type that will be used to parse the path params, and used in generated docs.
118
+ # @param base [Class] a swagger-able class
119
+ # @overload path_params(base = SoberSwag::InputObject, &block)
120
+ # Define a Swagger-able type inline to use to parse the path params.
121
+ # @see SoberSwag.input_object
61
122
  def path_params(base = SoberSwag::InputObject, &block)
62
123
  @path_params_class = make_input_object!(base, &block)
63
124
  action_module.const_set('PathParams', @path_params_class)
@@ -70,19 +131,27 @@ module SoberSwag
70
131
  end
71
132
 
72
133
  ##
73
- # Define the body of the action method in the controller.
74
- def action(&body)
75
- return @action if body.nil?
76
-
77
- @action ||= body
78
- end
79
-
134
+ # @overload description()
135
+ # Get a description of this route object.
136
+ # @return [String] markdown-formatted description
137
+ # @overload description(desc)
138
+ # Set the description of this route object.
139
+ # @param desc [String] markdown-formatted description
140
+ # @return [String] `desc`.
80
141
  def description(desc = nil)
81
142
  return @description if desc.nil?
82
143
 
83
144
  @description = desc
84
145
  end
85
146
 
147
+ ##
148
+ # @overload summary()
149
+ # Get the summary of this route object, a short string that identifies
150
+ # what it does.
151
+ # @return [String] markdown-formatted summary
152
+ # @overload summary(sum)
153
+ # Set a short, markdown-formatted summary of what this route does.
154
+ # @param sum [String] markdown-formatted summary
86
155
  def summary(sum = nil)
87
156
  return @summary if sum.nil?
88
157
 
@@ -92,18 +161,39 @@ module SoberSwag
92
161
  ##
93
162
  # The container module for all the constants this will eventually define.
94
163
  # Each class generated by this Route will be defined within this module.
164
+ # @return [Module] the module under which constants will be defined.
95
165
  def action_module
96
166
  @action_module ||= Module.new
97
167
  end
98
168
 
99
169
  ##
100
- # Define a serializer for a response with the given status code.
101
- # You may either give a serializer you defined elsewhere, or define one inline as if passed to
102
- # {SoberSwag::OutputObject.define}
170
+ # @overload response(status_code, description, &block)
171
+ # Define a new response from this route, by defining a serializer inline.
172
+ # This serializer will be defined as if with {SoberSwag::OutputObject.define}
173
+ #
174
+ # Generally, you want to define your serializers elsewhere for independent testing and such.
175
+ # However, if you have a really quick thing to serialize, this works.
176
+ # @param status_code [Symbol]
177
+ # the name of the HTTP status of this response.
178
+ # @param description [String]
179
+ # a description of what this response is, markdown-formatted
180
+ # @param block [Proc]
181
+ # passed to {SoberSwag::OutputObject.define}
182
+ #
183
+ # @overload response(status_code, description, serializer)
184
+ # Define a new response from this route, with an existing serializer.
185
+ # The generated swagger will document this response's format using the serializer.
186
+ #
187
+ # @param status_code [Symbol]
188
+ # the name of the HTTP status of this response
189
+ # @param description [String]
190
+ # a description of what this response is, markdown-formatted
191
+ # @param serializer [SoberSwag::Serializer::Base] a serializer to use for the
192
+ # body of this response
103
193
  def response(status_code, description, serializer = nil, &block)
104
194
  status_key = Rack::Utils.status_code(status_code)
105
195
 
106
- raise ArgumentError, 'Response defiend!' if @response_serializers.key?(status_key)
196
+ raise ArgumentError, 'Response defined!' if @response_serializers.key?(status_key)
107
197
 
108
198
  serializer ||= SoberSwag::OutputObject.define(&block)
109
199
  response_module.const_set(status_code.to_s.classify, serializer)
@@ -112,7 +202,8 @@ module SoberSwag
112
202
  end
113
203
 
114
204
  ##
115
- # What you should call the module of this action in your controller
205
+ # What you should call the module of this action in your controller.
206
+ # @return [String]
116
207
  def action_module_name
117
208
  action_name.to_s.classify
118
209
  end
@@ -124,8 +215,22 @@ module SoberSwag
124
215
  end
125
216
 
126
217
  def make_input_object!(base, &block)
127
- Class.new(base, &block).tap do |e|
128
- e.transform_keys(&:to_sym) if [SoberSwag::InputObject, Dry::Struct].include?(base)
218
+ if base.is_a?(Class)
219
+ make_input_class(base, block)
220
+ elsif block
221
+ raise ArgumentError, 'passed a non-class base and a block to an input'
222
+ else
223
+ base
224
+ end
225
+ end
226
+
227
+ def make_input_class(base, block)
228
+ if block
229
+ Class.new(base, &block).tap do |e|
230
+ e.transform_keys(&:to_sym) if [SoberSwag::InputObject, Dry::Struct].include?(base)
231
+ end
232
+ else
233
+ base
129
234
  end
130
235
  end
131
236
  end
@@ -1,6 +1,6 @@
1
1
  module SoberSwag
2
2
  ##
3
- # A variant of Dry::Struct that allows you to set a "model name" that is publically visible.
3
+ # A variant of Dry::Struct that allows you to set a "model name" that is publicly visible.
4
4
  # If you do not set one, it will be the Ruby class name, with any '::' replaced with a '.'.
5
5
  #
6
6
  # This otherwise behaves exactly like a Dry::Struct.
@@ -12,12 +12,67 @@ module SoberSwag
12
12
  class << self
13
13
  ##
14
14
  # The name to use for this type in external documentation.
15
- def identifier(arg = nil)
16
- @identifier = arg if arg
15
+ #
16
+ # @param new_ident [String] what to call this InputObject in external documentation.
17
+ def identifier(new_ident = nil)
18
+ @identifier = new_ident if new_ident
17
19
 
18
20
  @identifier || name.to_s.gsub('::', '.')
19
21
  end
20
22
 
23
+ ##
24
+ # @overload attribute(key, parent = SoberSwag::InputObject, &block)
25
+ # Defines an attribute as a direct sub-object.
26
+ # This block will be called as in {SoberSwag.input_object}.
27
+ # This might be useful in a case like the following:
28
+ #
29
+ # ```ruby
30
+ # class Classroom < SoberSwag::InputObject
31
+ # attribute :biographical_detail do
32
+ # attribute :student_name, primitive(:String)
33
+ # end
34
+ # end
35
+ # ```
36
+ #
37
+ # @param key [Symbol] the attribute name
38
+ # @param parent [Class] the parent class to use for the sub-object
39
+ # @overload attribute(key, type)
40
+ # Defines a new attribute with the given type.
41
+ # @param key [Symbol] the attribute name
42
+ # @param type the attribute type
43
+ def attribute(key, parent = SoberSwag::InputObject, &block)
44
+ raise ArgumentError, "parent class #{parent} is not an input object type!" unless valid_field_def?(parent, block)
45
+
46
+ super(key, parent, &block)
47
+ end
48
+
49
+ ##
50
+ # @overload attribute(key, parent = SoberSwag::InputObject, &block)
51
+ # Defines an optional attribute by defining a sub-object inline.
52
+ # This differs from a nil-able attribute as it can be *not provided*, while nilable attributes must be set to `null`.
53
+ #
54
+ # Yields to the block like in {SoberSwag.input_object}
55
+ #
56
+ # @param key [Symbol] the attribute name
57
+ # @param parent [Class] the parent class to use for the sub-object
58
+ # @overload attribute(key, type)
59
+ # Defines an optional attribute with a given type.
60
+ # This differs from a nil-able attribute as it can be *not provided*, while nilable attributes must be set to `null`.
61
+ #
62
+ # @param key [Symbol] the attribute name
63
+ # @param type the attribute type, another parsable object.
64
+ def attribute?(key, parent = SoberSwag::InputObject, &block)
65
+ raise ArgumentError, "parent class #{parent} is not an input object type!" unless valid_field_def?(parent, block)
66
+
67
+ super(key, parent, &block)
68
+ end
69
+
70
+ ##
71
+ # Add metadata keys, like `:description`, to the defined type.
72
+ # Note: does NOT mutate the type, returns a new type with the metadata added.
73
+ #
74
+ # @param args [Hash] the argument values
75
+ # @return [SoberSwag::InputObject] the new input object class
21
76
  def meta(*args)
22
77
  original = self
23
78
 
@@ -29,12 +84,67 @@ module SoberSwag
29
84
  end
30
85
  end
31
86
 
32
- def primitive(sym)
33
- SoberSwag::Types.const_get(sym)
87
+ ##
88
+ # Convenience method: you can use `.primitive` get a primitive parser for a given type.
89
+ # This lets you write:
90
+ #
91
+ # ```ruby
92
+ # class Foo < SoberSwag::InputObject
93
+ # attribute :bar, primitive(:String)
94
+ # end
95
+ # ```
96
+ #
97
+ # instead of
98
+ #
99
+ # ```ruby
100
+ # class Foo < SoberSwag::InputObject
101
+ # attribute :bar, SoberSwag::Types::String
102
+ # end
103
+ # ```
104
+ #
105
+ # @param args [Symbol] a symbol
106
+ # @return a primitive parser
107
+ def primitive(*args)
108
+ if args.length == 1
109
+ SoberSwag::Types.const_get(args.first)
110
+ else
111
+ super
112
+ end
113
+ end
114
+
115
+ ##
116
+ # Convenience method: you can use `.param` to get a parameter parser of a given type.
117
+ # Said parsers are more loose: for example, `param(:Integer)` will parse the string `"10"` into `10`, while
118
+ # `primitive(:Integer)` will throw an error.
119
+ #
120
+ # This method lets you write:
121
+ #
122
+ # ```ruby
123
+ # class Foo < SoberSwag::InputObject
124
+ # attribute :bar, param(:Integer)
125
+ # end
126
+ # ```
127
+ #
128
+ # instead of
129
+ #
130
+ # ```ruby
131
+ # class Foo < SoberSwag::InputObject
132
+ # attribute :bar, SoberSwag::Types::Param::Integer
133
+ # end
134
+ # ```
135
+ #
136
+ # @param name [Symbol] the name of the parameter type to get
137
+ # @return a parameter parser
138
+ def param(name)
139
+ SoberSwag::Types::Params.const_get(name)
34
140
  end
35
141
 
36
- def param(sym)
37
- SoberSwag::Types::Params.const_get(sym)
142
+ private
143
+
144
+ def valid_field_def?(parent, block)
145
+ return true if block.nil?
146
+
147
+ parent.is_a?(Class) && parent <= SoberSwag::InputObject
38
148
  end
39
149
  end
40
150
  end
@@ -10,18 +10,37 @@ module SoberSwag
10
10
 
11
11
  attr_reader :elements
12
12
 
13
+ ##
14
+ # @see SoberSwag::Nodes::Array#map
15
+ #
13
16
  def map(&block)
14
17
  self.class.new(elements.map { |elem| elem.map(&block) })
15
18
  end
16
19
 
20
+ ##
21
+ # @see SoberSwag::Nodes::Array#cata
22
+ #
23
+ # The block will be called with each element contained in this array node in turn, then called with a {SoberSwag::Nodes::Array} constructed
24
+ # from the resulting values.
25
+ #
26
+ # @return whatever the block yields.
17
27
  def cata(&block)
18
28
  block.call(self.class.new(elements.map { |elem| elem.cata(&block) }))
19
29
  end
20
30
 
31
+ ##
32
+ # Deconstructs into the elements.
33
+ #
34
+ # @return [Array<SoberSwag::Nodes::Base>]
21
35
  def deconstruct
22
36
  @elements
23
37
  end
24
38
 
39
+ ##
40
+ # Deconstruction for pattern-matching
41
+ #
42
+ # @return [Hash{Symbol => ::Array<SoberSwag::Nodes::Base>}]
43
+ # a hash with the elements in the `:elements` key.
25
44
  def deconstruct_keys(_keys)
26
45
  { elements: @elements }
27
46
  end
@@ -1,8 +1,16 @@
1
1
  module SoberSwag
2
2
  module Nodes
3
3
  ##
4
- # One attribute of an object.
4
+ # This is a node for one attribute of an object.
5
+ # An object type is represented by a `SoberSwag::Nodes::Object` full of these keys.
6
+ #
7
+ #
5
8
  class Attribute < Base
9
+ ##
10
+ # @param key [Symbol] the key of this attribute
11
+ # @param required [Boolean] if this attribute must be set or not
12
+ # @param value [Class] the type of this attribute
13
+ # @param meta [Hash] the metadata associated with this attribute
6
14
  def initialize(key, required, value, meta = {})
7
15
  @key = key
8
16
  @required = required
@@ -10,22 +18,55 @@ module SoberSwag
10
18
  @meta = meta
11
19
  end
12
20
 
21
+ ##
22
+ # Deconstruct into the {#key}, {#required}, {#value}, and {#meta} attributes
23
+ # of this {Attribute} object.
24
+ #
25
+ # @return [Array(Symbol, Boolean, Class, Hash)] the attributes of this object
13
26
  def deconstruct
14
27
  [key, required, value, meta]
15
28
  end
16
29
 
17
- def deconstruct_keys
30
+ ##
31
+ # Deconstructs into {#key}, {#required}, {#value}, and {#meta} attributes, as a
32
+ # hash with the attribute names as the keys.
33
+ #
34
+ # @param _keys [void] ignored
35
+ # @return [Hash] the attributes as keys.
36
+ def deconstruct_keys(_keys)
18
37
  { key: key, required: required, value: value, meta: meta }
19
38
  end
20
39
 
21
- attr_reader :key, :required, :value, :meta
40
+ ##
41
+ # @return [Symbol]
42
+ attr_reader :key
22
43
 
44
+ ##
45
+ # @return [Boolean] true if this attribute must be set, false otherwise.
46
+ attr_reader :required
47
+
48
+ ##
49
+ # @return [Class] the type of this attribute
50
+ attr_reader :value
51
+
52
+ ##
53
+ # @return [Hash] the metadata for this attribute.
54
+ attr_reader :meta
55
+
56
+ ##
57
+ # @see SoberSwag::Nodes::Base#map
23
58
  def map(&block)
24
59
  self.class.new(key, required, value.map(&block), meta)
25
60
  end
26
61
 
62
+ ##
63
+ # @see SoberSwag::Nodes::Base#cata
27
64
  def cata(&block)
28
- block.call(self.class.new(key, required, value.cata(&block), meta))
65
+ block.call(
66
+ self.class.new(
67
+ key, required, value.cata(&block), meta
68
+ )
69
+ )
29
70
  end
30
71
  end
31
72
  end