sober_swag 0.17.0 → 0.21.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (58) hide show
  1. checksums.yaml +4 -4
  2. data/.github/dependabot.yml +15 -0
  3. data/.github/workflows/benchmark.yml +39 -0
  4. data/.github/workflows/lint.yml +2 -4
  5. data/.github/workflows/ruby.yml +1 -1
  6. data/.gitignore +3 -0
  7. data/.rubocop.yml +5 -1
  8. data/.yardopts +7 -0
  9. data/CHANGELOG.md +21 -0
  10. data/Gemfile +12 -0
  11. data/README.md +1 -1
  12. data/bench/benchmark.rb +34 -0
  13. data/bench/benchmarks/basic_field_serializer.rb +21 -0
  14. data/bench/benchmarks/view_selection.rb +47 -0
  15. data/docs/serializers.md +4 -1
  16. data/example/Gemfile +2 -2
  17. data/example/Gemfile.lock +46 -44
  18. data/example/config/environments/production.rb +1 -1
  19. data/lib/sober_swag/compiler/path.rb +42 -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/compiler.rb +29 -3
  24. data/lib/sober_swag/controller/route.rb +103 -20
  25. data/lib/sober_swag/controller.rb +39 -12
  26. data/lib/sober_swag/input_object.rb +124 -7
  27. data/lib/sober_swag/nodes/array.rb +19 -0
  28. data/lib/sober_swag/nodes/attribute.rb +45 -4
  29. data/lib/sober_swag/nodes/base.rb +27 -7
  30. data/lib/sober_swag/nodes/binary.rb +30 -13
  31. data/lib/sober_swag/nodes/enum.rb +16 -1
  32. data/lib/sober_swag/nodes/list.rb +20 -0
  33. data/lib/sober_swag/nodes/nullable_primitive.rb +3 -0
  34. data/lib/sober_swag/nodes/object.rb +4 -1
  35. data/lib/sober_swag/nodes/one_of.rb +11 -3
  36. data/lib/sober_swag/nodes/primitive.rb +34 -2
  37. data/lib/sober_swag/nodes/sum.rb +8 -0
  38. data/lib/sober_swag/output_object/definition.rb +57 -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/output_object.rb +40 -19
  43. data/lib/sober_swag/parser.rb +7 -1
  44. data/lib/sober_swag/serializer/array.rb +27 -3
  45. data/lib/sober_swag/serializer/base.rb +75 -25
  46. data/lib/sober_swag/serializer/conditional.rb +33 -1
  47. data/lib/sober_swag/serializer/field_list.rb +23 -5
  48. data/lib/sober_swag/serializer/hash.rb +53 -0
  49. data/lib/sober_swag/serializer/mapped.rb +10 -1
  50. data/lib/sober_swag/serializer/optional.rb +18 -1
  51. data/lib/sober_swag/serializer/primitive.rb +3 -0
  52. data/lib/sober_swag/serializer.rb +1 -0
  53. data/lib/sober_swag/server.rb +27 -11
  54. data/lib/sober_swag/type/named.rb +14 -0
  55. data/lib/sober_swag/types/comma_array.rb +4 -0
  56. data/lib/sober_swag/version.rb +1 -1
  57. data/lib/sober_swag.rb +6 -1
  58. metadata +9 -2
@@ -60,7 +60,7 @@ Rails.application.configure do
60
60
  # config.logger = ActiveSupport::TaggedLogging.new(Syslog::Logger.new 'app-name')
61
61
 
62
62
  if ENV['RAILS_LOG_TO_STDOUT'].present?
63
- logger = ActiveSupport::Logger.new(STDOUT)
63
+ logger = ActiveSupport::Logger.new($stdout)
64
64
  logger.formatter = config.log_formatter
65
65
  config.logger = ActiveSupport::TaggedLogging.new(logger)
66
66
  end
@@ -1,8 +1,11 @@
1
1
  module SoberSwag
2
2
  class Compiler
3
3
  ##
4
- # Compile a singular path, and that's it.
5
- # Only handles the actual body.
4
+ # This compiler transforms a {SoberSwag::Controller::Route} object into its associated OpenAPI V3 definition.
5
+ # These definitions are [called "paths" in the OpenAPI V3 spec](https://swagger.io/docs/specification/paths-and-operations/),
6
+ # thus the name of this compiler.
7
+ #
8
+ # It only compiles a *single* "path" at a time.
6
9
  class Path
7
10
  ##
8
11
  # @param route [SoberSwag::Controller::Route] a route to use
@@ -12,8 +15,18 @@ module SoberSwag
12
15
  @compiler = compiler
13
16
  end
14
17
 
15
- attr_reader :route, :compiler
18
+ ##
19
+ # @return [SoberSwag::Controller::Route]
20
+ attr_reader :route
21
+
22
+ ##
23
+ # @return [SoberSwag::Compiler] the compiler used for type compilation
24
+ attr_reader :compiler
16
25
 
26
+ ##
27
+ # The OpenAPI V3 "path" object for the associated {SoberSwag::Controller::Route}
28
+ #
29
+ # @return [Hash] the OpenAPI V3 description
17
30
  def schema
18
31
  base = {}
19
32
  base[:summary] = route.summary if route.summary
@@ -25,6 +38,11 @@ module SoberSwag
25
38
  base
26
39
  end
27
40
 
41
+ ##
42
+ # An array of "response" objects from swagger.
43
+ #
44
+ # @return [Hash{String => Hash}]
45
+ # response code to response object.
28
46
  def responses # rubocop:disable Metrics/MethodLength
29
47
  route.response_serializers.map { |status, serializer|
30
48
  [
@@ -41,10 +59,19 @@ module SoberSwag
41
59
  }.to_h
42
60
  end
43
61
 
62
+ ##
63
+ # An array of all parameters, be they in the query or in the path.
64
+ # See [this page](https://swagger.io/docs/specification/serialization/) for what that looks like.
65
+ #
66
+ # @return [Array<Hash>]
44
67
  def params
45
68
  query_params + path_params
46
69
  end
47
70
 
71
+ ##
72
+ # An array of schemas for all query parameters.
73
+ #
74
+ # @return [Array<Hash>] the schemas
48
75
  def query_params
49
76
  if route.query_params_class
50
77
  compiler.query_params_for(route.query_params_class)
@@ -53,6 +80,10 @@ module SoberSwag
53
80
  end
54
81
  end
55
82
 
83
+ ##
84
+ # An array of schemas for all path parameters.
85
+ #
86
+ # @return [Array<Hash>] the schemas
56
87
  def path_params
57
88
  if route.path_params_class
58
89
  compiler.path_params_for(route.path_params_class)
@@ -61,6 +92,11 @@ module SoberSwag
61
92
  end
62
93
  end
63
94
 
95
+ ##
96
+ # The schema for a request body.
97
+ # Matches [this spec.](https://swagger.io/docs/specification/paths-and-operations/)
98
+ #
99
+ # @return [Hash] the schema
64
100
  def request_body
65
101
  return nil unless route.request_body_class
66
102
 
@@ -74,6 +110,9 @@ module SoberSwag
74
110
  }
75
111
  end
76
112
 
113
+ ##
114
+ # The tags for this path.
115
+ # @return [Array<String>] the tags
77
116
  def tags
78
117
  return nil unless route.tags.any?
79
118
 
@@ -2,15 +2,28 @@ module SoberSwag
2
2
  class Compiler
3
3
  ##
4
4
  # Compile multiple routes into a paths set.
5
+ # This basically just aggregates {SoberSwag::Controller::Route} objects.
5
6
  class Paths
7
+ ##
8
+ # Set up a new paths compiler with no routes in it.
6
9
  def initialize
7
10
  @routes = []
8
11
  end
9
12
 
13
+ ##
14
+ # Add on a new {SoberSwag::Controller::Route}
15
+ #
16
+ # @param route [SoberSwag::Controller::Route] the route description to add to compilation
17
+ # @return [SoberSwag::Compiler::Paths] self
10
18
  def add_route(route)
11
19
  @routes << route
20
+
21
+ self
12
22
  end
13
23
 
24
+ ##
25
+ # In the OpenAPI V3 spec, we group action definitions by their path.
26
+ # This helps us do that.
14
27
  def grouped_paths
15
28
  routes.group_by(&:path)
16
29
  end
@@ -20,6 +33,9 @@ module SoberSwag
20
33
  # paths list. Since this is only a compiler for paths,
21
34
  # it has *no idea* how to handle types. So, it takes a compiler
22
35
  # which it will use to do that for it.
36
+ #
37
+ # @param compiler [SoberSwag::Compiler::Type] the type compiler to use
38
+ # @return [Hash] a schema for all contained routes.
23
39
  def paths_list(compiler)
24
40
  grouped_paths.transform_values do |values|
25
41
  values.map { |route|
@@ -31,6 +47,8 @@ module SoberSwag
31
47
  ##
32
48
  # Get a list of all types we discovered when compiling
33
49
  # the paths.
50
+ #
51
+ # @yield [Class] all the types found in all the routes described in here.
34
52
  def found_types
35
53
  return enum_for(:found_types) unless block_given?
36
54
 
@@ -41,6 +59,8 @@ module SoberSwag
41
59
  end
42
60
  end
43
61
 
62
+ ##
63
+ # @return [Array<SoberSwag::Controller::Route>] the routes to document
44
64
  attr_reader :routes
45
65
 
46
66
  private
@@ -4,7 +4,11 @@ module SoberSwag
4
4
  # Compiles a primitive type.
5
5
  # Almost always constructed with the values from
6
6
  # {SoberSwag::Nodes::Primitive}.
7
+ #
8
+ # This works by either generating a swagger primitive definition, *or* a `$ref` to one with a given identifier.
7
9
  class Primitive
10
+ ##
11
+ # @param type [Class] the swagger-able class to document.
8
12
  def initialize(type)
9
13
  @type = type
10
14
 
@@ -13,6 +17,8 @@ module SoberSwag
13
17
 
14
18
  attr_reader :type
15
19
 
20
+ ##
21
+ # Is this documenting one of the build-in swagger types?
16
22
  def swagger_primitive?
17
23
  SWAGGER_PRIMITIVE_DEFS.include?(type)
18
24
  end
@@ -25,6 +31,9 @@ module SoberSwag
25
31
 
26
32
  ##
27
33
  # Turn this type into a swagger hash with a proper type key.
34
+ # This is suitable for use as the value of a `schema` key in a definition.
35
+ #
36
+ # @return [Hash] the schema.
28
37
  def type_hash
29
38
  if swagger_primitive?
30
39
  SWAGGER_PRIMITIVE_DEFS.fetch(type)
@@ -37,9 +46,16 @@ module SoberSwag
37
46
  end
38
47
  end
39
48
 
49
+ ##
50
+ # Primitive schema used for ruby `Date` values.
40
51
  DATE_PRIMITIVE = { type: :string, format: :date }.freeze
52
+ ##
53
+ # Primitive schema used for ruby `DateTime` values.
41
54
  DATE_TIME_PRIMITIVE = { type: :string, format: :'date-time' }.freeze
55
+ HASH_PRIMITIVE = { type: :object, additionalProperties: true }.freeze
42
56
 
57
+ ##
58
+ # Map of types that are considered "primitive types" in the OpenAPI V3 spec.
43
59
  SWAGGER_PRIMITIVE_DEFS =
44
60
  {
45
61
  NilClass => :null,
@@ -52,9 +68,12 @@ module SoberSwag
52
68
  .to_h.merge(
53
69
  Date => DATE_PRIMITIVE,
54
70
  DateTime => DATE_TIME_PRIMITIVE,
55
- Time => DATE_TIME_PRIMITIVE
71
+ Time => DATE_TIME_PRIMITIVE,
72
+ Hash => HASH_PRIMITIVE
56
73
  ).transform_values(&:freeze).freeze
57
74
 
75
+ ##
76
+ # @return [String] the schema reference
58
77
  def ref_name
59
78
  raise Error, 'is not a type that is named!' if swagger_primitive?
60
79
 
@@ -1,45 +1,95 @@
1
1
  module SoberSwag
2
2
  class Compiler
3
3
  ##
4
- # A compiler for DRY-Struct data types, essentially.
5
- # It only consumes one type at a time.
4
+ # A compiler for swagger-able types.
5
+ #
6
+ # This class turns Swagger-able types into a *schema*.
7
+ # This Schema may be:
8
+ # - a [schema object](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md#schemaObject) with {#object_schema}
9
+ # - a [path schema](https://swagger.io/docs/specification/describing-parameters/#path-parameters) with {#path_schema}
10
+ # - a [query schema](https://swagger.io/docs/specification/describing-parameters/#query-parameters) with {#query_schema}
11
+ #
12
+ # As such, it compiles all types to all applicable schemas.
13
+ #
14
+ # While this class compiles *one* type at a time, it *keeps track* of the other types needed to describe this schema.
15
+ # It stores these types in a set, available at {#found_types}.
16
+ #
17
+ # For example, with a schema like:
18
+ #
19
+ # ```ruby
20
+ # class Bar < SoberSwag::InputObject
21
+ # attribute :baz, primitive(:String)
22
+ # end
23
+ #
24
+ # class Foo < SoberSwag::InputObject
25
+ # attribute :bar, Bar
26
+ # end
27
+ # ```
28
+ #
29
+ # If you compile `Foo` with this class, {#found_types} will include `Bar`.
30
+ #
6
31
  class Type # rubocop:disable Metrics/ClassLength
32
+ ##
33
+ # An error raised when a type is too complicated for a given schema.
34
+ # This may be due to containing too many layers of nesting.
7
35
  class TooComplicatedError < ::SoberSwag::Compiler::Error; end
36
+ ##
37
+ # An error raised when a type is too complicated to transform into a *path* schema.
8
38
  class TooComplicatedForPathError < TooComplicatedError; end
39
+ ##
40
+ # An error raised when a type is too complicated to transform into a *query* schema.
9
41
  class TooComplicatedForQueryError < TooComplicatedError; end
10
42
 
43
+ ##
44
+ # A list of acceptable keys to use as metadata for an object schema.
45
+ # All other metadata keys defined on a type with {SoberSwag::InputObject.meta} will be ignored.
46
+ #
47
+ # @return [Array<Symbol>] valid keys.
11
48
  METADATA_KEYS = %i[description deprecated].freeze
12
49
 
50
+ ##
51
+ # Create a new compiler for a swagger-able type.
52
+ # @param type [Class] the type to compile
13
53
  def initialize(type)
14
54
  @type = type
15
55
  end
16
56
 
57
+ ##
58
+ # @return [Class] the type we are compiling.
17
59
  attr_reader :type
18
60
 
19
61
  ##
20
62
  # Is this type standalone, IE, worth serializing on its own
21
63
  # in the schemas section of our schema?
64
+ # @return [true,false]
22
65
  def standalone?
23
66
  type.is_a?(Class)
24
67
  end
25
68
 
69
+ ##
70
+ # Get back the [schema object](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md#schemaObject)
71
+ # for the type described.
72
+ #
73
+ # @return [Hash]
26
74
  def object_schema
27
75
  @object_schema ||=
28
76
  make_object_schema
29
77
  end
30
78
 
31
- def object_schema_meta
32
- return {} unless standalone? && type <= SoberSwag::Type::Named
33
-
34
- {
35
- description: type.description
36
- }.reject { |_, v| v.nil? }
37
- end
38
-
79
+ ##
80
+ # Give a "stub type" for this schema.
81
+ # This is suitable to use as the schema for attributes of other schemas.
82
+ # Almost always generates a ref object.
83
+ # @return [Hash] the OpenAPI V3 schema stub
39
84
  def schema_stub
40
85
  @schema_stub ||= generate_schema_stub
41
86
  end
42
87
 
88
+ ##
89
+ # The schema for this type when it is path of the path.
90
+ #
91
+ # @raise [TooComplicatedForPathError] when the compiled type is too complicated to use in a path
92
+ # @return [Hash] a [path parameters hash](https://swagger.io/docs/specification/describing-parameters/#path-parameters) for this type.
43
93
  def path_schema
44
94
  path_schema_stub.map do |e|
45
95
  ensure_uncomplicated(e[:name], e[:schema])
@@ -51,16 +101,30 @@ module SoberSwag
51
101
 
52
102
  DEFAULT_QUERY_SCHEMA_ATTRS = { in: :query, style: :deepObject, explode: true }.freeze
53
103
 
104
+ ##
105
+ # The schema for this type when it is part of the query.
106
+ # @raise [TooComplicatedForQueryError] when this type is too complicated to use in a query schema
107
+ # @return [Hash] a [query parameters hash](https://swagger.io/docs/specification/describing-parameters/#query-parameters) for this type.
54
108
  def query_schema
55
109
  path_schema_stub.map { |e| DEFAULT_QUERY_SCHEMA_ATTRS.merge(e) }
56
110
  rescue TooComplicatedError => e
57
111
  raise TooComplicatedForQueryError, e.message
58
112
  end
59
113
 
114
+ ##
115
+ # Get the name of this type if it is to be used in a `$ref` key.
116
+ # This is useful if we are going to use this type compiler to compile an *attribute* of another object.
117
+ #
118
+ # @return [String] a reference specifier for this type
60
119
  def ref_name
61
120
  SoberSwag::Compiler::Primitive.new(type).ref_name
62
121
  end
63
122
 
123
+ ##
124
+ # Get a set of all other types needed to compile this type.
125
+ # This set will *not* include the type being compiled.
126
+ #
127
+ # @return [Set<Class>]
64
128
  def found_types
65
129
  @found_types ||=
66
130
  begin
@@ -69,32 +133,51 @@ module SoberSwag
69
133
  end
70
134
  end
71
135
 
72
- def mapped_type
73
- @mapped_type ||= parsed_type.map { |v| SoberSwag::Compiler::Primitive.new(v).type_hash }
74
- end
75
-
76
- def parsed_type
77
- @parsed_type ||=
78
- begin
79
- (parsed,) = parsed_result
80
- parsed
81
- end
82
- end
83
-
136
+ ##
137
+ # This type, parsed into an AST.
84
138
  def parsed_result
85
139
  @parsed_result ||= Parser.new(type_for_parser).run_parser
86
140
  end
87
141
 
142
+ ##
143
+ # Standard ruby equality.
88
144
  def eql?(other)
89
145
  other.class == self.class && other.type == type
90
146
  end
91
147
 
148
+ ##
149
+ # Standard ruby hashing method.
150
+ # Compilers hash to the same value if they are compiling the same type.
92
151
  def hash
93
152
  [self.class, type].hash
94
153
  end
95
154
 
96
155
  private
97
156
 
157
+ ##
158
+ # Get metadata attributes to be used if compiling an object schema.
159
+ #
160
+ # @return [Hash]
161
+ def object_schema_meta
162
+ return {} unless standalone? && type <= SoberSwag::Type::Named
163
+
164
+ {
165
+ description: type.description
166
+ }.reject { |_, v| v.nil? }
167
+ end
168
+
169
+ def parsed_type
170
+ @parsed_type ||=
171
+ begin
172
+ (parsed,) = parsed_result
173
+ parsed
174
+ end
175
+ end
176
+
177
+ def mapped_type
178
+ @mapped_type ||= parsed_type.map { |v| SoberSwag::Compiler::Primitive.new(v).type_hash }
179
+ end
180
+
98
181
  def generate_schema_stub
99
182
  if type.is_a?(Class)
100
183
  SoberSwag::Compiler::Primitive.new(type).type_hash
@@ -17,6 +17,8 @@ module SoberSwag
17
17
 
18
18
  ##
19
19
  # Convert a compiler to the overall type definition.
20
+ #
21
+ # @return Hash the swagger definition.
20
22
  def to_swagger
21
23
  {
22
24
  paths: path_schemas,
@@ -29,16 +31,23 @@ module SoberSwag
29
31
  ##
30
32
  # Add a path to be compiled.
31
33
  # @param route [SoberSwag::Controller::Route] the route to add.
34
+ # @return [Compiler] self
32
35
  def add_route(route)
33
36
  tap { @paths.add_route(route) }
34
37
  end
35
38
 
39
+ ##
40
+ # Get the schema of each object type defined in this Compiler.
41
+ #
42
+ # @return [Hash]
36
43
  def object_schemas
37
44
  @types.map { |v| [v.ref_name, v.object_schema] }.to_h
38
45
  end
39
46
 
40
47
  ##
41
48
  # The path section of the swagger schema.
49
+ #
50
+ # @return [Hash]
42
51
  def path_schemas
43
52
  @paths.paths_list(self)
44
53
  end
@@ -46,6 +55,9 @@ module SoberSwag
46
55
  ##
47
56
  # Compile a type to a new, path-params list.
48
57
  # This will add all subtypes to the found types list.
58
+ #
59
+ # @param type [Class] the type to get a path_params definition for
60
+ # @return [Hash]
49
61
  def path_params_for(type)
50
62
  with_types_discovered(type).path_schema
51
63
  end
@@ -53,6 +65,9 @@ module SoberSwag
53
65
  ##
54
66
  # Get the query params list for a type.
55
67
  # All found types will be added to the reference dictionary.
68
+ #
69
+ # @param type [Class] the type to get the query_params definitions for
70
+ # @return [Hash]
56
71
  def query_params_for(type)
57
72
  with_types_discovered(type).query_schema
58
73
  end
@@ -60,25 +75,36 @@ module SoberSwag
60
75
  ##
61
76
  # Get the request body definition for a type.
62
77
  # This will always be a ref.
78
+ #
79
+ # @param type [Class] the type to get the body definition for
80
+ # @return [Hash]
63
81
  def body_for(type)
64
82
  add_type(type)
65
83
  Type.new(type).schema_stub
66
84
  end
67
85
 
68
86
  ##
69
- # Get the definition of a response type
87
+ # Get the definition of a response type.
88
+ #
89
+ # This is an alias of {#body_for}
90
+ # @see body_for
70
91
  def response_for(type)
71
92
  body_for(type)
72
93
  end
73
94
 
74
95
  ##
75
- # Get the existing schema for a given type
96
+ # Get the existing schema for a given type.
97
+ #
98
+ # @param type [Class] the type to get the schema for
99
+ # @return [Hash,nil] the swagger schema for this object, or nil if it was not found.
76
100
  def schema_for(type)
77
101
  @types.find { |type_comp| type_comp.type == type }&.object_schema
78
102
  end
79
103
 
80
104
  ##
81
- # Add a type in the types reference dictionary, essentially
105
+ # Add a type in the types reference dictionary, essentially.
106
+ # @param type [Class] the type to compiler
107
+ # @return [SoberSwag::Compiler] self
82
108
  def add_type(type)
83
109
  # use tap here to avoid an explicit self at the end of this
84
110
  # which makes this method chainable