sober_swag 0.15.0 → 0.20.0

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -10,19 +10,49 @@ module SoberSwag
10
10
  # This is a very weird, not-very-Ruby-like abstraction, *upon which* we can build abstractions that are actually use for users.
11
11
  # It lets you build abstractions like "Use this serializer if a type has this class, otherwise use this other one."
12
12
  # When composed together, you can make arbitrary decision trees.
13
+ #
14
+ # This class is heavily inspired by
15
+ # the [Decideable](https://hackage.haskell.org/package/contravariant-1.5.3/docs/Data-Functor-Contravariant-Divisible.html#t:Decidable)
16
+ # typeclass from Haskell.
13
17
  class Conditional < Base
14
18
  ##
15
19
  # Error thrown when a chooser proc returns a non left-or-right value.
16
20
  class BadChoiceError < Error; end
17
21
 
22
+ ##
23
+ # Create a new conditional serializer, from a "chooser" proc, a "left" serializer, and a "right" serializer.
24
+ #
25
+ # @param chooser [Proc,Lambda] the proc that chooses which "side" to use
26
+ # @param left [SoberSwag::Serializer::Base] a serializer for the "left" side
27
+ # @param right [SoberSwag::Serializer::Base] a serializer for the "right" side
18
28
  def initialize(chooser, left, right)
19
29
  @chooser = chooser
20
30
  @left = left
21
31
  @right = right
22
32
  end
23
33
 
24
- attr_reader :chooser, :left, :right
34
+ ##
35
+ # @return [Proc,Lambda] the "chooser" proc.
36
+ attr_reader :chooser
37
+
38
+ ##
39
+ # @return [SoberSwag::Serializer::Base] the serializer to use if the "chooser" proc chooses `:right`.
40
+ # Also called the "left-side serializer."
41
+ attr_reader :left
25
42
 
43
+ ##
44
+ # @return [SoberSwag::Serializer::Base] the serializer to use if the "chooser" proc chooses `:right`.
45
+ # Also called the "right-side serializer."
46
+ attr_reader :right
47
+
48
+ ##
49
+ # First, call {#chooser} with `object` and `options` to see what serializer to use, and *what* to serialize.
50
+ # Then, if it returns `[:left, val]`, use {#left} to serialize `val`.
51
+ # Otherwise, if it returns `[:right, val]`, use {#right} to serialize `val`.
52
+ # If it returns neither, throw {BadChoiceError}.
53
+ #
54
+ # @raise [BadChoiceError] if {#chooser} did not choose what side to use
55
+ # @return [Hash] a JSON-compatible object
26
56
  def serialize(object, options = {})
27
57
  tag, val = chooser.call(object, options)
28
58
  case tag
@@ -58,6 +88,8 @@ module SoberSwag
58
88
  left.lazy_type? || right.lazy_type?
59
89
  end
60
90
 
91
+ ##
92
+ # Finalize both {#left} and {#right}
61
93
  def finalize_lazy_type!
62
94
  [left, right].each(&:finalize_lazy_type!)
63
95
  end
@@ -1,13 +1,18 @@
1
1
  module SoberSwag
2
2
  module Serializer
3
3
  ##
4
- # Extract out a hash from a list of
5
- # name/serializer pairs.
4
+ # Extracts a JSON hash from a list of {SoberSwag::OutputObject::Field} structs.
6
5
  class FieldList < Base
6
+ ##
7
+ # Create a new field-list serializer.
8
+ #
9
+ # @param field_list [Array<SoberSwag::OutputObject::Field>] descriptions of each field
7
10
  def initialize(field_list)
8
11
  @field_list = field_list
9
12
  end
10
13
 
14
+ ##
15
+ # @return [Array<SoberSwag::OutputObject::Field>] the list of fields to use.
11
16
  attr_reader :field_list
12
17
 
13
18
  ##
@@ -16,16 +21,27 @@ module SoberSwag
16
21
  SoberSwag::Serializer.Primitive(SoberSwag::Types.const_get(symbol))
17
22
  end
18
23
 
24
+ ##
25
+ # Serialize an object to a JSON hash by using each field in the list.
26
+ # @param object [Object] object to serialize
27
+ # @param options [Hash] arbitrary options
28
+ # @return [Hash] serialized object.
19
29
  def serialize(object, options = {})
20
30
  field_list.map { |field|
21
31
  [field.name, field.serializer.serialize(object, options)]
22
32
  }.to_h
23
33
  end
24
34
 
35
+ ##
36
+ # Construct a Dry::Struct from the fields given.
37
+ # This Struct will be swagger-able.
38
+ # @return [Dry::Struct]
25
39
  def type
26
40
  @type ||= make_struct_type!
27
41
  end
28
42
 
43
+ ##
44
+ # These types are always constructed lazily.
29
45
  def lazy_type?
30
46
  true
31
47
  end
@@ -3,12 +3,21 @@ module SoberSwag
3
3
  ##
4
4
  # A new serializer by mapping over the serialization function
5
5
  class Mapped < Base
6
+ ##
7
+ # Create a new mapped serializer.
8
+ # @param base [SoberSwag::Serializer::Base] a serializer to use after mapping
9
+ # @param map_f [Proc,Lambda] a mapping function to use before serialization
6
10
  def initialize(base, map_f)
7
11
  @base = base
8
12
  @map_f = map_f
9
13
  end
10
14
 
11
- attr_reader :base, :map_f
15
+ ##
16
+ # @return [SoberSwag::Serializer::Base] serializer to use after mapping
17
+ attr_reader :base
18
+ ##
19
+ # @return [Proc, Lambda, #call] function to use before serialization
20
+ attr_reader :map_f
12
21
 
13
22
  def serialize(object, options = {})
14
23
  @base.serialize(@map_f.call(object), options)
@@ -5,11 +5,20 @@ module SoberSwag
5
5
  # this can be used to make a serializer of type 'A | nil'.
6
6
  #
7
7
  # Or, put another way, makes serializers not crash on nil values.
8
+ # If {#serialize} is passed nil, it will return `nil` immediately, and not
9
+ # try to call the serializer of {#inner}.
8
10
  class Optional < Base
11
+ ##
12
+ # An error thrown when trying to nest optional serializers.
13
+ class NestedOptionalError < Error; end
14
+ ##
15
+ # @param inner [SoberSwag::Serializer::Base] the serializer to use for non-nil values
9
16
  def initialize(inner)
10
17
  @inner = inner
11
18
  end
12
19
 
20
+ ##
21
+ # @return [SoberSwag::Serializer::Base] the serializer to use for non-nil values.
13
22
  attr_reader :inner
14
23
 
15
24
  def lazy_type?
@@ -24,6 +33,9 @@ module SoberSwag
24
33
  @inner.finalize_lazy_type!
25
34
  end
26
35
 
36
+ ##
37
+ # If `object` is nil, return `nil`.
38
+ # Otherwise, call `inner.serialize(object, options)`.
27
39
  def serialize(object, options = {})
28
40
  if object.nil?
29
41
  object
@@ -36,8 +48,13 @@ module SoberSwag
36
48
  inner.type.optional
37
49
  end
38
50
 
51
+ ##
52
+ # Since nesting optional types is bad, this will always raise an ArgumentError
53
+ #
54
+ # @raise [NestedOptionalError] always
55
+ # @return [void] nothing, always raises.
39
56
  def optional(*)
40
- raise ArgumentError, 'no nesting optionals please'
57
+ raise NestedOptionalError, 'no nesting optionals please'
41
58
  end
42
59
  end
43
60
  end
@@ -4,6 +4,9 @@ module SoberSwag
4
4
  # A class that does *no* serialization: you give it a type,
5
5
  # and it will pass any serialized input on verbatim.
6
6
  class Primitive < Base
7
+ ##
8
+ # Construct a primitive serializer with a description of the type it serializes to.
9
+ # @param type [Class] a swagger-able type
7
10
  def initialize(type)
8
11
  @type = type
9
12
  end
@@ -21,42 +21,58 @@ module SoberSwag
21
21
  # Start up.
22
22
  #
23
23
  # @param controller_proc [Proc] a proc that, when called, gives a list of {SoberSwag::Controller}s to document
24
- # @param cache [Bool | Proc] if we should cache our defintions (default false)
24
+ # @param cache [Bool | Proc] if we should cache our definitions (default false)
25
+ # @param redoc_version [String] what version of the redoc library to use to display UI (default 'next', the latest version).
25
26
  def initialize(
26
27
  controller_proc: RAILS_CONTROLLER_PROC,
27
- cache: false
28
+ cache: false,
29
+ redoc_version: 'next'
28
30
  )
29
31
  @controller_proc = controller_proc
30
32
  @cache = cache
33
+ @html = EFFECT_HTML.gsub(/REDOC_VERSION/, redoc_version)
31
34
  end
32
35
 
33
36
  EFFECT_HTML = <<~HTML.freeze
34
37
  <!DOCTYPE html>
35
38
  <html>
36
39
  <head>
37
- <title>Swagger-UI</title>
38
- <script src="https://unpkg.com/swagger-ui-dist@3/swagger-ui-bundle.js"></script>
39
- <link rel="stylesheet" href="https://unpkg.com/swagger-ui-dist@3.23.4/swagger-ui.css"></link>
40
+ <title>ReDoc</title>
41
+ <!-- needed for adaptive design -->
42
+ <meta charset="utf-8"/>
43
+ <meta name="viewport" content="width=device-width, initial-scale=1">
44
+ <link href="https://fonts.googleapis.com/css?family=Montserrat:300,400,700|Roboto:300,400,700" rel="stylesheet">
45
+
46
+ <!--
47
+ ReDoc doesn't change outer page styles
48
+ -->
49
+ <style>
50
+ body {
51
+ margin: 0;
52
+ padding: 0;
53
+ }
54
+ </style>
40
55
  </head>
41
56
  <body>
42
- <div id="swagger">
43
- </div>
44
- <script>
45
- SwaggerUIBundle({url: 'SCRIPT_NAME', dom_id: '#swagger'})
46
- </script>
57
+ <redoc spec-url='SCRIPT_NAME'></redoc>
58
+ <script src="https://cdn.jsdelivr.net/npm/redoc@REDOC_VERSION/bundles/redoc.standalone.js"> </script>
47
59
  </body>
48
60
  </html>
49
61
  HTML
50
62
 
63
+ ##
64
+ # Standard Rack call method.
51
65
  def call(env)
52
66
  req = Rack::Request.new(env)
53
67
  if req.path_info&.match?(/json/si) || req.get_header('Accept')&.match?(/json/si)
54
68
  [200, { 'Content-Type' => 'application/json' }, [generate_json_string]]
55
69
  else
56
- [200, { 'Content-Type' => 'text/html' }, [EFFECT_HTML.gsub(/SCRIPT_NAME/, env['SCRIPT_NAME'] + '.json')]]
70
+ [200, { 'Content-Type' => 'text/html' }, [@html.gsub(/SCRIPT_NAME/, "#{env['SCRIPT_NAME']}.json")]]
57
71
  end
58
72
  end
59
73
 
74
+ private
75
+
60
76
  def generate_json_string
61
77
  if cache?
62
78
  @json_string ||= JSON.dump(generate_swagger)
@@ -9,24 +9,38 @@ module SoberSwag
9
9
  # Modules that include {SoberSwag::Type::Named}
10
10
  # will automatically extend this module.
11
11
  module ClassMethods
12
+ ##
13
+ # Is this type a "wrapper" for another type?
12
14
  def alias?
13
15
  false
14
16
  end
15
17
 
18
+ ##
19
+ # The type this type is a wrapper for
16
20
  def alias_of
17
21
  nil
18
22
  end
19
23
 
24
+ ##
25
+ # The "root" type along the alias chain
20
26
  def root_alias
21
27
  alias_of || self
22
28
  end
23
29
 
30
+ ##
31
+ # @overload description()
32
+ # @return [String] a human-readable description of this type
33
+ # @overload description(arg)
34
+ # @param arg [String] a human-readable description of this type
35
+ # @return [String] `arg`
24
36
  def description(arg = nil)
25
37
  @description = arg if arg
26
38
  @description
27
39
  end
28
40
  end
29
41
 
42
+ ##
43
+ # When included, extends {SoberSwag::Type::Named::ClassMethods}
30
44
  def self.included(mod)
31
45
  mod.extend(ClassMethods)
32
46
  end
@@ -3,6 +3,10 @@ module SoberSwag
3
3
  ##
4
4
  # An array that will be parsed from comma-separated values in a string, if given a string.
5
5
  module CommaArray
6
+ ##
7
+ # Get a parser that will parse comma-separated values of another type.
8
+ # @param other [Class] a swagger-able type to parse into
9
+ # @return [SoberSwag::Types::CommaArray]
6
10
  def self.of(other)
7
11
  SoberSwag::Types::Array.of(other).constructor { |val|
8
12
  if val.is_a?(::String)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SoberSwag
4
- VERSION = '0.15.0'
4
+ VERSION = '0.20.0'
5
5
  end
data/sober_swag.gemspec CHANGED
@@ -39,7 +39,7 @@ Gem::Specification.new do |spec|
39
39
  spec.add_development_dependency 'pry-byebug'
40
40
  spec.add_development_dependency 'rake', '~> 13.0'
41
41
  spec.add_development_dependency 'rspec', '~> 3.0'
42
- spec.add_development_dependency 'rubocop'
43
- spec.add_development_dependency 'rubocop-rspec'
42
+ spec.add_development_dependency 'rubocop', '~> 0.93.1'
43
+ spec.add_development_dependency 'rubocop-rspec', '~> 1.44.1'
44
44
  spec.add_development_dependency 'simplecov'
45
45
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sober_swag
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.15.0
4
+ version: 0.20.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Anthony Super
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2020-09-02 00:00:00.000000000 Z
11
+ date: 2021-05-17 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -126,30 +126,30 @@ dependencies:
126
126
  name: rubocop
127
127
  requirement: !ruby/object:Gem::Requirement
128
128
  requirements:
129
- - - ">="
129
+ - - "~>"
130
130
  - !ruby/object:Gem::Version
131
- version: '0'
131
+ version: 0.93.1
132
132
  type: :development
133
133
  prerelease: false
134
134
  version_requirements: !ruby/object:Gem::Requirement
135
135
  requirements:
136
- - - ">="
136
+ - - "~>"
137
137
  - !ruby/object:Gem::Version
138
- version: '0'
138
+ version: 0.93.1
139
139
  - !ruby/object:Gem::Dependency
140
140
  name: rubocop-rspec
141
141
  requirement: !ruby/object:Gem::Requirement
142
142
  requirements:
143
- - - ">="
143
+ - - "~>"
144
144
  - !ruby/object:Gem::Version
145
- version: '0'
145
+ version: 1.44.1
146
146
  type: :development
147
147
  prerelease: false
148
148
  version_requirements: !ruby/object:Gem::Requirement
149
149
  requirements:
150
- - - ">="
150
+ - - "~>"
151
151
  - !ruby/object:Gem::Version
152
- version: '0'
152
+ version: 1.44.1
153
153
  - !ruby/object:Gem::Dependency
154
154
  name: simplecov
155
155
  requirement: !ruby/object:Gem::Requirement
@@ -172,6 +172,7 @@ extensions: []
172
172
  extra_rdoc_files: []
173
173
  files:
174
174
  - ".github/config/rubocop_linter_action.yml"
175
+ - ".github/dependabot.yml"
175
176
  - ".github/workflows/lint.yml"
176
177
  - ".github/workflows/ruby.yml"
177
178
  - ".gitignore"
@@ -179,12 +180,14 @@ files:
179
180
  - ".rubocop.yml"
180
181
  - ".ruby-version"
181
182
  - ".travis.yml"
183
+ - ".yardopts"
182
184
  - CHANGELOG.md
183
185
  - Gemfile
184
186
  - LICENSE.txt
185
187
  - README.md
186
188
  - Rakefile
187
189
  - bin/console
190
+ - bin/rspec
188
191
  - bin/setup
189
192
  - docs/serializers.md
190
193
  - example/.gitignore