sober_swag 0.14.0 → 0.19.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 (39) hide show
  1. checksums.yaml +4 -4
  2. data/.github/dependabot.yml +15 -0
  3. data/.github/workflows/lint.yml +41 -9
  4. data/.github/workflows/ruby.yml +1 -5
  5. data/.gitignore +2 -0
  6. data/.rubocop.yml +50 -5
  7. data/CHANGELOG.md +34 -0
  8. data/README.md +155 -4
  9. data/bin/console +36 -0
  10. data/bin/rspec +29 -0
  11. data/docs/serializers.md +74 -9
  12. data/example/Gemfile +2 -2
  13. data/example/app/controllers/application_controller.rb +5 -0
  14. data/example/app/controllers/people_controller.rb +4 -0
  15. data/example/app/controllers/posts_controller.rb +5 -0
  16. data/lib/sober_swag.rb +1 -0
  17. data/lib/sober_swag/compiler.rb +1 -0
  18. data/lib/sober_swag/compiler/path.rb +7 -0
  19. data/lib/sober_swag/compiler/primitive.rb +77 -0
  20. data/lib/sober_swag/compiler/type.rb +57 -96
  21. data/lib/sober_swag/controller.rb +3 -9
  22. data/lib/sober_swag/controller/route.rb +30 -8
  23. data/lib/sober_swag/input_object.rb +36 -3
  24. data/lib/sober_swag/nodes/attribute.rb +8 -7
  25. data/lib/sober_swag/nodes/enum.rb +2 -2
  26. data/lib/sober_swag/nodes/primitive.rb +1 -1
  27. data/lib/sober_swag/output_object/definition.rb +14 -2
  28. data/lib/sober_swag/output_object/field_syntax.rb +16 -0
  29. data/lib/sober_swag/parser.rb +10 -5
  30. data/lib/sober_swag/serializer/base.rb +2 -0
  31. data/lib/sober_swag/serializer/meta.rb +3 -1
  32. data/lib/sober_swag/server.rb +22 -10
  33. data/lib/sober_swag/type.rb +7 -0
  34. data/lib/sober_swag/type/named.rb +35 -0
  35. data/lib/sober_swag/types.rb +2 -0
  36. data/lib/sober_swag/types/comma_array.rb +17 -0
  37. data/lib/sober_swag/version.rb +1 -1
  38. data/sober_swag.gemspec +2 -2
  39. metadata +18 -10
data/bin/console ADDED
@@ -0,0 +1,36 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'bundler/setup'
4
+ require 'sober_swag'
5
+ require 'pry'
6
+
7
+ Bio = SoberSwag.input_object do
8
+ attribute :description, SoberSwag::Types::String
9
+ attribute :gender, SoberSwag::Types::String.enum('male', 'female') | SoberSwag::Types::String
10
+ end
11
+
12
+ Person = SoberSwag.input_object do
13
+ attribute :name, SoberSwag::Types::String
14
+ attribute? :bio, Bio.optional
15
+ end
16
+
17
+ MultiFloorLocation = SoberSwag.input_object do
18
+ attribute :building, SoberSwag::Types::String.enum('science', 'mathematics', 'literature')
19
+ attribute :floor, SoberSwag::Types::String
20
+ attribute :room, SoberSwag::Types::Integer
21
+ end
22
+
23
+ SingleFloorLocation = SoberSwag.input_object do
24
+ attribute :building, SoberSwag::Types::String.enum('philosophy', 'computer science')
25
+ attribute :room, SoberSwag::Types::Integer
26
+ end
27
+
28
+ SchoolClass = SoberSwag.input_object do
29
+ attribute :prof, Person.meta(description: 'The person who teaches this class.')
30
+ attribute :students, SoberSwag::Types::Array.of(Person)
31
+ attribute :location, (SingleFloorLocation | MultiFloorLocation).meta(description: 'What building and room this is in')
32
+ end
33
+
34
+ SortDirections = SoberSwag::Types::CommaArray.of(SoberSwag::Types::String.enum('created_at', 'updated_at', '-created_at', '-updated_at'))
35
+
36
+ Pry.start
data/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')
data/docs/serializers.md CHANGED
@@ -25,21 +25,21 @@ For example, you might have a serializer that can return a date in two formats,
25
25
  In this case, it might be used as:
26
26
 
27
27
  ```ruby
28
- serializer.new(my_record, { format: :newstyle })
28
+ serializer.new(my_record, { format: :new_style })
29
29
  ```
30
30
 
31
31
  However, since it is *always* optional, you can also do:
32
32
 
33
33
  ```ruby
34
- serilaizer.new(my_record)
34
+ serializer.new(my_record)
35
35
  ```
36
36
 
37
37
  And it *should* pick some default format.
38
38
 
39
39
  ### Primitives
40
40
 
41
- Primitive serializers, or "identity serializers," are serializers that do nothing.
42
- They are implemented as [`SoberSwag::Serializer::Primitive`](../lib/sober_swag/serializer/primitive.rb), or as the `#primitive` method on a `OutputObject`.
41
+ Primitive serializers or "identity serializers" are serializers that do nothing.
42
+ They are implemented as [`SoberSwag::Serializer::Primitive`](../lib/sober_swag/serializer/primitive.rb), or as the `#primitive` method on an `OutputObject`.
43
43
  Since they don't do anything, they can be considered the most "basic" serializer.
44
44
 
45
45
  These serializers *do not* check types.
@@ -51,12 +51,12 @@ serializer.serialize(10) # => 10
51
51
  ```
52
52
 
53
53
  Thus, care should be used when working with these serializers.
54
- In the future, we might add some "debug mode" sorta thing that will do type-checking and throw errors, however, the cost of doing so in production is probably not worth it.
54
+ In the future, we might add some "debug mode" thing that will do type-checking and throw errors, however, the cost of doing so in production is probably not worth it.
55
55
 
56
56
  ### Mapped
57
57
 
58
58
  Sometimes, you can create a serializer via a *proc*.
59
- For example, let's say that I want a serializer that takes a `Date` and returns a string.
59
+ For example, let's say that I want a serializer that takes a `Date` and returns a String.
60
60
  I can do this:
61
61
 
62
62
  ```ruby
@@ -74,7 +74,7 @@ In the future, we might add a debug mode.
74
74
  Oftentimes, we want to give a serializer the ability to serialize `nil` values.
75
75
  This is often useful in serializing fields.
76
76
 
77
- It turns out that it's pretty easy to make a serializer that can serialize `nil` values: just propogate nils.
77
+ It turns out that it's pretty easy to make a serializer that can serialize `nil` values: just propogate `nil`s.
78
78
  For example, let's say I have the following code:
79
79
 
80
80
  ```ruby
@@ -99,7 +99,7 @@ Continuing our example from earlier:
99
99
  my_serializer.array.serialize([Foo.new(10, 11)]) #=> [{ bar: 10, baz: 11 }]
100
100
  ```
101
101
 
102
- This changes the type properly, too.
102
+ This changes the type properly too.
103
103
 
104
104
  ## OutputObjects
105
105
 
@@ -132,9 +132,25 @@ end
132
132
  We can see a few things here:
133
133
 
134
134
  1. You define field names with a `field` definition, which is a way to define the serializer for a single field.
135
- 2. You must provide types with field names
135
+ 2. You must provide types with field names.
136
136
  3. You can use blocks to do data formatting, which lets you pick different fields and such.
137
137
 
138
+
139
+ ### Multi
140
+
141
+ If you have a few fields of the same type, you can use `#multi` to define them all at once:
142
+
143
+ ```ruby
144
+ StudentOutputObject = SoberSwag::OutputObject.define do
145
+ multi [:first_name, :last_name], primitive(:String)
146
+ field :recent_grades, primitive(:Integer).array do |student|
147
+ student.graded_assignments.limit(100).pluck(:grade)
148
+ end
149
+ end
150
+ ```
151
+
152
+ This saves a bit of typing, and can help with refactoring later.
153
+
138
154
  ### Views
139
155
 
140
156
  Sometimes, you might want to add "variant" ways to look at data.
@@ -201,3 +217,52 @@ end
201
217
  ```
202
218
 
203
219
  For clarity (and to prevent infinitely-looping serializers on accident, we recommend you *always* use an explicit view for dependent output objects.
220
+
221
+ ### "Inheritance"
222
+
223
+ Output objects don't support inheritance.
224
+ You can't have one output object based on another.
225
+ You *can*, however, merge one into another!
226
+ Consider this case:
227
+
228
+ ```ruby
229
+ GenericBioOutput = SoberSwag::OutputObject.define do
230
+ field :name, primitive(:String)
231
+ field :brief_history, primitive(:String)
232
+ end
233
+
234
+ ExecutiveBioOutput = SoberSwag::OutputObject.define do
235
+ merge GenericBioOutput
236
+ field :company, primitive(:String)
237
+ field :position, primitive(:String)
238
+ end
239
+ ```
240
+
241
+ Using `#merge` lets you add in all the fields from one output object into another.
242
+ You can even use `merge` from within a view.
243
+
244
+ Note that `merge` does *not* copy anything but fields.
245
+ Identifiers and views will not be copied over.
246
+
247
+ ### View Inheritance
248
+
249
+ While defining a new Output Object, you *do not* have access to the definition of that output object.
250
+ So, how do I say that one view should be an extension of another?
251
+ Simple, use the `inherits:` kwarg:
252
+
253
+ ```ruby
254
+ BioOutput = SoberSwag::OutputObject.define do
255
+ field :name, primitive(:String)
256
+
257
+ view :detail do
258
+ field :bio, primitive(:String)
259
+ end
260
+
261
+ view :super_detail, inherits: :detail do
262
+ field :age, primitive(:Integer)
263
+ end
264
+ end
265
+ ```
266
+
267
+ `inherits` will automatically merge in all the fields of the referenced view.
268
+ This means that the view `super_detail` will include fields `name`, `bio`, and `age`.
data/example/Gemfile CHANGED
@@ -8,7 +8,7 @@ gem 'actionpack', '>= 6.0.3.2'
8
8
  # Use sqlite3 as the database for Active Record
9
9
  gem 'sqlite3', '~> 1.4'
10
10
  # Use Puma as the app server
11
- gem 'puma', '~> 4.3'
11
+ gem 'puma', '~> 5.2'
12
12
  # Build JSON APIs with ease. Read more: https://github.com/rails/jbuilder
13
13
  # gem 'jbuilder', '~> 2.7'
14
14
  # Use Active Model has_secure_password
@@ -34,7 +34,7 @@ group :development, :test do
34
34
  end
35
35
 
36
36
  group :development do
37
- gem 'listen', '>= 3.0.5', '< 3.2'
37
+ gem 'listen', '>= 3.0.5', '< 3.5'
38
38
  # Spring speeds up development by keeping your application running in the background. Read more: https://github.com/rails/spring
39
39
  gem 'spring'
40
40
  gem 'spring-watcher-listen', '~> 2.0.0'
@@ -1,2 +1,7 @@
1
+ ##
2
+ # Standard application controller.
1
3
  class ApplicationController < ActionController::API
4
+ rescue_from Dry::Struct::Error do
5
+ head :bad_request
6
+ end
2
7
  end
@@ -35,6 +35,7 @@ class PeopleController < ApplicationController
35
35
  request_body(PersonParams)
36
36
  response(:ok, 'the person created', PersonOutputObject)
37
37
  response(:unprocessable_entity, 'the validation errors', PersonErrorsOutputObject)
38
+ tags 'people', 'create'
38
39
  end
39
40
  def create
40
41
  p = Person.new(parsed_body.person.to_h)
@@ -50,6 +51,7 @@ class PeopleController < ApplicationController
50
51
  path_params { attribute :id, Types::Params::Integer }
51
52
  response(:ok, 'the person updated', PersonOutputObject)
52
53
  response(:unprocessable_entity, 'the validation errors', PersonErrorsOutputObject)
54
+ tags 'people', 'update'
53
55
  end
54
56
  def update
55
57
  if @person.update(parsed_body.person.to_h)
@@ -68,6 +70,7 @@ class PeopleController < ApplicationController
68
70
  attribute :view, Types::String.default('base'.freeze).enum('base', 'detail')
69
71
  end
70
72
  response(:ok, 'all the people', PersonOutputObject.array)
73
+ tags 'people', 'list'
71
74
  end
72
75
  def index
73
76
  @people = Person.all
@@ -81,6 +84,7 @@ class PeopleController < ApplicationController
81
84
  attribute :id, Types::Params::Integer
82
85
  end
83
86
  response(:ok, 'the person requested', PersonOutputObject)
87
+ tags 'people', 'show'
84
88
  end
85
89
  def show
86
90
  respond!(:ok, @person)
@@ -47,6 +47,7 @@ class PostsController < ApplicationController
47
47
  MARKDOWN
48
48
  end
49
49
  response(:ok, 'all the posts', PostOutputObject.array)
50
+ tags 'posts', 'list'
50
51
  end
51
52
  def index
52
53
  @posts = Post.all
@@ -60,6 +61,7 @@ class PostsController < ApplicationController
60
61
  path_params(ShowPath)
61
62
  query_params { attribute? :view, ViewTypes }
62
63
  response(:ok, 'the requested post', PostOutputObject)
64
+ tags 'posts', 'show'
63
65
  end
64
66
  def show
65
67
  respond!(:ok, @post, serializer_opts: { view: parsed_query.view })
@@ -68,6 +70,7 @@ class PostsController < ApplicationController
68
70
  define :post, :create, '/posts/' do
69
71
  request_body(PostCreate)
70
72
  response(:created, 'the created post', PostOutputObject)
73
+ tags 'posts', 'create'
71
74
  end
72
75
  def create
73
76
  @post = Post.new(parsed_body.post.to_h)
@@ -83,6 +86,7 @@ class PostsController < ApplicationController
83
86
  path_params(ShowPath)
84
87
  request_body(PostUpdate)
85
88
  response(:ok, 'the post updated', PostOutputObject.view(:base))
89
+ tags 'posts', 'update'
86
90
  end
87
91
  def update
88
92
  if @post.update(parsed_body.post.to_h)
@@ -95,6 +99,7 @@ class PostsController < ApplicationController
95
99
  define :delete, :destroy, '/posts/{id}' do
96
100
  path_params(ShowPath)
97
101
  response(:ok, 'the post deleted', PostOutputObject.view(:base))
102
+ tags 'posts', 'delete'
98
103
  end
99
104
  def destroy
100
105
  @post.destroy
data/lib/sober_swag.rb CHANGED
@@ -21,6 +21,7 @@ module SoberSwag
21
21
  autoload :Controller, 'sober_swag/controller'
22
22
  autoload :InputObject, 'sober_swag/input_object'
23
23
  autoload :Server, 'sober_swag/server'
24
+ autoload :Type, 'sober_swag/type'
24
25
 
25
26
  ##
26
27
  # Define a struct of something.
@@ -6,6 +6,7 @@ module SoberSwag
6
6
  class Compiler
7
7
  autoload(:Type, 'sober_swag/compiler/type')
8
8
  autoload(:Error, 'sober_swag/compiler/error')
9
+ autoload(:Primitive, 'sober_swag/compiler/primitive')
9
10
  autoload(:Path, 'sober_swag/compiler/path')
10
11
  autoload(:Paths, 'sober_swag/compiler/paths')
11
12
 
@@ -21,6 +21,7 @@ module SoberSwag
21
21
  base[:parameters] = params if params.any?
22
22
  base[:responses] = responses
23
23
  base[:requestBody] = request_body if request_body
24
+ base[:tags] = tags if tags
24
25
  base
25
26
  end
26
27
 
@@ -72,6 +73,12 @@ module SoberSwag
72
73
  }
73
74
  }
74
75
  end
76
+
77
+ def tags
78
+ return nil unless route.tags.any?
79
+
80
+ route.tags
81
+ end
75
82
  end
76
83
  end
77
84
  end
@@ -0,0 +1,77 @@
1
+ module SoberSwag
2
+ class Compiler
3
+ ##
4
+ # Compiles a primitive type.
5
+ # Almost always constructed with the values from
6
+ # {SoberSwag::Nodes::Primitive}.
7
+ class Primitive
8
+ def initialize(type)
9
+ @type = type
10
+
11
+ raise Error, "#{type.inspect} is not a class!" unless @type.is_a?(Class)
12
+ end
13
+
14
+ attr_reader :type
15
+
16
+ def swagger_primitive?
17
+ SWAGGER_PRIMITIVE_DEFS.include?(type)
18
+ end
19
+
20
+ ##
21
+ # Is the wrapped type a named type, causing us to make a ref?
22
+ def named?
23
+ type <= SoberSwag::Type::Named
24
+ end
25
+
26
+ ##
27
+ # Turn this type into a swagger hash with a proper type key.
28
+ def type_hash
29
+ if swagger_primitive?
30
+ SWAGGER_PRIMITIVE_DEFS.fetch(type)
31
+ else
32
+ {
33
+ oneOf: [
34
+ { '$ref'.to_sym => named_ref }
35
+ ]
36
+ }
37
+ end
38
+ end
39
+
40
+ DATE_PRIMITIVE = { type: :string, format: :date }.freeze
41
+ DATE_TIME_PRIMITIVE = { type: :string, format: :'date-time' }.freeze
42
+ HASH_PRIMITIVE = { type: :object, additionalProperties: true }.freeze
43
+
44
+ SWAGGER_PRIMITIVE_DEFS =
45
+ {
46
+ NilClass => :null,
47
+ TrueClass => :boolean,
48
+ FalseClass => :boolean,
49
+ Float => :number,
50
+ Integer => :integer,
51
+ String => :string
52
+ }.transform_values { |v| { type: v.freeze } }
53
+ .to_h.merge(
54
+ Date => DATE_PRIMITIVE,
55
+ DateTime => DATE_TIME_PRIMITIVE,
56
+ Time => DATE_TIME_PRIMITIVE,
57
+ Hash => HASH_PRIMITIVE
58
+ ).transform_values(&:freeze).freeze
59
+
60
+ def ref_name
61
+ raise Error, 'is not a type that is named!' if swagger_primitive?
62
+
63
+ if type <= SoberSwag::Type::Named
64
+ type.root_alias.identifier
65
+ else
66
+ type.name.gsub('::', '.')
67
+ end
68
+ end
69
+
70
+ private
71
+
72
+ def named_ref
73
+ "#/components/schemas/#{ref_name}"
74
+ end
75
+ end
76
+ end
77
+ end
@@ -4,46 +4,6 @@ module SoberSwag
4
4
  # A compiler for DRY-Struct data types, essentially.
5
5
  # It only consumes one type at a time.
6
6
  class Type # rubocop:disable Metrics/ClassLength
7
- class << self
8
- def get_ref(klass)
9
- "#/components/schemas/#{safe_name(klass)}"
10
- end
11
-
12
- def safe_name(klass)
13
- if klass.respond_to?(:identifier)
14
- klass.identifier
15
- else
16
- klass.to_s.gsub('::', '.')
17
- end
18
- end
19
-
20
- def primitive?(value)
21
- primitive_def(value) != nil
22
- end
23
-
24
- def primitive_def(value)
25
- value = value.primitive if value.is_a?(Dry::Types::Nominal)
26
-
27
- return nil unless value.is_a?(Class)
28
-
29
- if (name = primitive_name(value))
30
- { type: name }
31
- elsif value == Date
32
- { type: 'string', format: 'date' }
33
- elsif [Time, DateTime].any?(&value.ancestors.method(:include?))
34
- { type: 'string', format: 'date-time' }
35
- end
36
- end
37
-
38
- def primitive_name(value)
39
- return 'null' if value == NilClass
40
- return 'integer' if value == Integer
41
- return 'number' if value == Float
42
- return 'string' if value == String
43
- return 'boolean' if [TrueClass, FalseClass].include?(value)
44
- end
45
- end
46
-
47
7
  class TooComplicatedError < ::SoberSwag::Compiler::Error; end
48
8
  class TooComplicatedForPathError < TooComplicatedError; end
49
9
  class TooComplicatedForQueryError < TooComplicatedError; end
@@ -65,7 +25,15 @@ module SoberSwag
65
25
 
66
26
  def object_schema
67
27
  @object_schema ||=
68
- normalize(parsed_type).cata(&method(:to_object_schema))
28
+ make_object_schema
29
+ end
30
+
31
+ def object_schema_meta
32
+ return {} unless standalone? && type <= SoberSwag::Type::Named
33
+
34
+ {
35
+ description: type.description
36
+ }.reject { |_, v| v.nil? }
69
37
  end
70
38
 
71
39
  def schema_stub
@@ -81,14 +49,16 @@ module SoberSwag
81
49
  raise TooComplicatedForPathError, e.message
82
50
  end
83
51
 
52
+ DEFAULT_QUERY_SCHEMA_ATTRS = { in: :query, style: :deepObject, explode: true }.freeze
53
+
84
54
  def query_schema
85
- path_schema_stub.map { |e| e.merge(in: :query, style: :deepObject, explode: true) }
55
+ path_schema_stub.map { |e| DEFAULT_QUERY_SCHEMA_ATTRS.merge(e) }
86
56
  rescue TooComplicatedError => e
87
57
  raise TooComplicatedForQueryError, e.message
88
58
  end
89
59
 
90
60
  def ref_name
91
- self.class.safe_name(type)
61
+ SoberSwag::Compiler::Primitive.new(type).ref_name
92
62
  end
93
63
 
94
64
  def found_types
@@ -99,6 +69,10 @@ module SoberSwag
99
69
  end
100
70
  end
101
71
 
72
+ def mapped_type
73
+ @mapped_type ||= parsed_type.map { |v| SoberSwag::Compiler::Primitive.new(v).type_hash }
74
+ end
75
+
102
76
  def parsed_type
103
77
  @parsed_type ||=
104
78
  begin
@@ -121,22 +95,11 @@ module SoberSwag
121
95
 
122
96
  private
123
97
 
124
- def generate_schema_stub # rubocop:disable Metrics/MethodLength
125
- return self.class.primitive_def(type) if self.class.primitive?(type)
126
-
127
- case type
128
- when Class
129
- # refs have to be standalone
130
- # so to not interefere with our other stuff, do this horrible garbage
131
- { oneOf: [{ '$ref'.to_sym => self.class.get_ref(type) }] }
132
- when Dry::Types::Constrained
133
- self.class.new(type.type).schema_stub
134
- when Dry::Types::Array::Member
135
- { type: :array, items: self.class.new(type.member).schema_stub }
136
- when Dry::Types::Sum
137
- { oneOf: normalize(parsed_type).elements.map { |t| self.class.new(t.value).schema_stub } }
98
+ def generate_schema_stub
99
+ if type.is_a?(Class)
100
+ SoberSwag::Compiler::Primitive.new(type).type_hash
138
101
  else
139
- raise SoberSwag::Compiler::Error, "Cannot generate a schema stub for #{type} (#{type.class})"
102
+ object_schema
140
103
  end
141
104
  end
142
105
 
@@ -149,6 +112,10 @@ module SoberSwag
149
112
  end
150
113
  end
151
114
 
115
+ def make_object_schema(metadata_keys: METADATA_KEYS)
116
+ normalize(mapped_type).cata { |e| to_object_schema(e, metadata_keys) }.merge(object_schema_meta)
117
+ end
118
+
152
119
  def normalize(object)
153
120
  object.cata { |e| rewrite_sums(e) }.cata { |e| flatten_one_ofs(e) }
154
121
  end
@@ -180,29 +147,14 @@ module SoberSwag
180
147
  end
181
148
  end
182
149
 
183
- def to_object_schema(object) # rubocop:disable Metrics/MethodLength, Metrics/CyclomaticComplexity
150
+ def to_object_schema(object, metadata_keys = METADATA_KEYS) # rubocop:disable Metrics/MethodLength, Metrics/CyclomaticComplexity
184
151
  case object
185
152
  when Nodes::List
186
- {
187
- type: :array,
188
- items: object.deconstruct.first
189
- }
153
+ { type: :array, items: object.element }
190
154
  when Nodes::Enum
191
- {
192
- type: :string,
193
- enum: object.deconstruct.first
194
- }
155
+ { type: :string, enum: object.values }
195
156
  when Nodes::OneOf
196
- if object.deconstruct.include?({ type: 'null' })
197
- rejected = object.deconstruct.reject { |e| e[:type] == 'null' }
198
- if rejected.length == 1
199
- rejected.first.merge(nullable: true)
200
- else
201
- { oneOf: rejected, nullable: true }
202
- end
203
- else
204
- { oneOf: object.deconstruct }
205
- end
157
+ one_of_to_schema(object)
206
158
  when Nodes::Object
207
159
  # openAPI requires that you give a list of required attributes
208
160
  # (which IMO is the *totally* wrong thing to do but whatever)
@@ -216,41 +168,50 @@ module SoberSwag
216
168
  required: required
217
169
  }
218
170
  when Nodes::Attribute
219
- name, req, value = object.deconstruct
171
+ name, req, value, meta = object.deconstruct
172
+ value = value.merge(meta&.select { |k, _| metadata_keys.include?(k) } || {})
220
173
  if req
221
174
  [name, value.merge(required: true)]
222
175
  else
223
176
  [name, value]
224
177
  end
225
178
  when Nodes::Primitive
226
- value = object.value
227
- metadata = object.metadata
228
- type_def =
229
- if self.class.primitive?(value)
230
- self.class.primitive_def(value)
231
- else
232
- metadata.merge!(value.meta)
233
- # refs have to be on their own, this is the stupid workaround
234
- # so you can add descriptions and stuff
235
- { oneOf: [{ '$ref'.to_sym => self.class.get_ref(value) }] }
236
- end
237
- METADATA_KEYS.select(&metadata.method(:key?)).reduce(type_def) do |definition, key|
238
- definition.merge(key => metadata[key])
239
- end
179
+ object.value.merge(object.metadata.select { |k, _| metadata_keys.include?(k) })
240
180
  else
241
181
  raise ArgumentError, "Got confusing node #{object} (#{object.class})"
242
182
  end
243
183
  end
244
184
 
185
+ def one_of_to_schema(object)
186
+ if object.deconstruct.include?({ type: :null })
187
+ rejected = object.deconstruct.reject { |e| e[:type] == :null }
188
+ if rejected.length == 1
189
+ rejected.first.merge(nullable: true)
190
+ else
191
+ { oneOf: flatten_oneofs_hash(rejected), nullable: true }
192
+ end
193
+ else
194
+ { oneOf: flatten_oneofs_hash(object.deconstruct) }
195
+ end
196
+ end
197
+
198
+ def flatten_oneofs_hash(object)
199
+ object.map { |h|
200
+ h[:oneOf] || h
201
+ }.flatten
202
+ end
203
+
245
204
  def path_schema_stub
246
205
  @path_schema_stub ||=
247
- object_schema[:properties].map do |k, v|
206
+ make_object_schema(metadata_keys: METADATA_KEYS | %i[style explode])[:properties].map do |k, v|
248
207
  # ensure_uncomplicated(k, v)
249
208
  {
250
209
  name: k,
251
- schema: v.reject { |key, _| %i[required nullable].include?(key) },
252
- required: object_schema[:required].include?(k) || false
253
- }
210
+ schema: v.reject { |key, _| %i[required nullable explode style].include?(key) },
211
+ required: object_schema[:required].include?(k) || false,
212
+ style: v[:style],
213
+ explode: v[:explode]
214
+ }.reject { |_, v2| v2.nil? }
254
215
  end
255
216
  end
256
217