sober_swag 0.14.0 → 0.19.0

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