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
@@ -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