sober_swag 0.18.0 → 0.22.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (128) 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 +6 -1
  8. data/.yardopts +7 -0
  9. data/CHANGELOG.md +22 -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/bin/console +30 -10
  16. data/docs/reporting.md +190 -0
  17. data/docs/serializers.md +4 -1
  18. data/example/Gemfile +2 -2
  19. data/example/Gemfile.lock +116 -123
  20. data/example/app/controllers/application_controller.rb +4 -0
  21. data/example/app/controllers/people_controller.rb +44 -28
  22. data/example/app/output_objects/identified_output.rb +7 -0
  23. data/example/app/output_objects/person_output_object.rb +37 -11
  24. data/example/app/output_objects/post_output_object.rb +0 -4
  25. data/example/app/output_objects/reporting_post_output.rb +18 -0
  26. data/example/bin/rspec +29 -0
  27. data/example/config/environments/production.rb +1 -1
  28. data/example/spec/requests/people/create_spec.rb +3 -2
  29. data/example/spec/requests/people/index_spec.rb +1 -1
  30. data/lib/sober_swag/compiler/path.rb +45 -4
  31. data/lib/sober_swag/compiler/paths.rb +20 -0
  32. data/lib/sober_swag/compiler/primitive.rb +17 -0
  33. data/lib/sober_swag/compiler/type.rb +105 -22
  34. data/lib/sober_swag/compiler.rb +87 -15
  35. data/lib/sober_swag/controller/route.rb +147 -28
  36. data/lib/sober_swag/controller.rb +57 -17
  37. data/lib/sober_swag/input_object.rb +124 -7
  38. data/lib/sober_swag/nodes/array.rb +19 -0
  39. data/lib/sober_swag/nodes/attribute.rb +45 -4
  40. data/lib/sober_swag/nodes/base.rb +27 -7
  41. data/lib/sober_swag/nodes/binary.rb +30 -13
  42. data/lib/sober_swag/nodes/enum.rb +16 -1
  43. data/lib/sober_swag/nodes/list.rb +20 -0
  44. data/lib/sober_swag/nodes/nullable_primitive.rb +3 -0
  45. data/lib/sober_swag/nodes/object.rb +4 -1
  46. data/lib/sober_swag/nodes/one_of.rb +11 -3
  47. data/lib/sober_swag/nodes/primitive.rb +34 -2
  48. data/lib/sober_swag/nodes/sum.rb +8 -0
  49. data/lib/sober_swag/output_object/definition.rb +57 -1
  50. data/lib/sober_swag/output_object/field.rb +31 -11
  51. data/lib/sober_swag/output_object/field_syntax.rb +19 -3
  52. data/lib/sober_swag/output_object/view.rb +46 -1
  53. data/lib/sober_swag/output_object.rb +40 -19
  54. data/lib/sober_swag/parser.rb +7 -1
  55. data/lib/sober_swag/reporting/compiler.rb +39 -0
  56. data/lib/sober_swag/reporting/input/base.rb +11 -0
  57. data/lib/sober_swag/reporting/input/bool.rb +19 -0
  58. data/lib/sober_swag/reporting/input/converting/bool.rb +24 -0
  59. data/lib/sober_swag/reporting/input/converting/date.rb +30 -0
  60. data/lib/sober_swag/reporting/input/converting/date_time.rb +28 -0
  61. data/lib/sober_swag/reporting/input/converting/decimal.rb +24 -0
  62. data/lib/sober_swag/reporting/input/converting/integer.rb +19 -0
  63. data/lib/sober_swag/reporting/input/converting.rb +16 -0
  64. data/lib/sober_swag/reporting/input/defer.rb +29 -0
  65. data/lib/sober_swag/reporting/input/described.rb +38 -0
  66. data/lib/sober_swag/reporting/input/dictionary.rb +37 -0
  67. data/lib/sober_swag/reporting/input/either.rb +51 -0
  68. data/lib/sober_swag/reporting/input/enum.rb +44 -0
  69. data/lib/sober_swag/reporting/input/format.rb +39 -0
  70. data/lib/sober_swag/reporting/input/interface.rb +87 -0
  71. data/lib/sober_swag/reporting/input/list.rb +44 -0
  72. data/lib/sober_swag/reporting/input/mapped.rb +36 -0
  73. data/lib/sober_swag/reporting/input/merge_objects.rb +72 -0
  74. data/lib/sober_swag/reporting/input/null.rb +34 -0
  75. data/lib/sober_swag/reporting/input/number.rb +19 -0
  76. data/lib/sober_swag/reporting/input/object/property.rb +53 -0
  77. data/lib/sober_swag/reporting/input/object.rb +100 -0
  78. data/lib/sober_swag/reporting/input/pattern.rb +46 -0
  79. data/lib/sober_swag/reporting/input/referenced.rb +38 -0
  80. data/lib/sober_swag/reporting/input/struct.rb +271 -0
  81. data/lib/sober_swag/reporting/input/text.rb +42 -0
  82. data/lib/sober_swag/reporting/input.rb +54 -0
  83. data/lib/sober_swag/reporting/invalid_schema_error.rb +21 -0
  84. data/lib/sober_swag/reporting/output/base.rb +25 -0
  85. data/lib/sober_swag/reporting/output/bool.rb +25 -0
  86. data/lib/sober_swag/reporting/output/defer.rb +69 -0
  87. data/lib/sober_swag/reporting/output/described.rb +42 -0
  88. data/lib/sober_swag/reporting/output/dictionary.rb +46 -0
  89. data/lib/sober_swag/reporting/output/interface.rb +83 -0
  90. data/lib/sober_swag/reporting/output/list.rb +54 -0
  91. data/lib/sober_swag/reporting/output/merge_objects.rb +97 -0
  92. data/lib/sober_swag/reporting/output/null.rb +25 -0
  93. data/lib/sober_swag/reporting/output/number.rb +25 -0
  94. data/lib/sober_swag/reporting/output/object/property.rb +45 -0
  95. data/lib/sober_swag/reporting/output/object.rb +54 -0
  96. data/lib/sober_swag/reporting/output/partitioned.rb +77 -0
  97. data/lib/sober_swag/reporting/output/pattern.rb +50 -0
  98. data/lib/sober_swag/reporting/output/referenced.rb +42 -0
  99. data/lib/sober_swag/reporting/output/struct.rb +262 -0
  100. data/lib/sober_swag/reporting/output/text.rb +25 -0
  101. data/lib/sober_swag/reporting/output/via_map.rb +67 -0
  102. data/lib/sober_swag/reporting/output/viewed.rb +72 -0
  103. data/lib/sober_swag/reporting/output.rb +54 -0
  104. data/lib/sober_swag/reporting/report/base.rb +57 -0
  105. data/lib/sober_swag/reporting/report/either.rb +36 -0
  106. data/lib/sober_swag/reporting/report/error.rb +15 -0
  107. data/lib/sober_swag/reporting/report/list.rb +28 -0
  108. data/lib/sober_swag/reporting/report/merged_object.rb +25 -0
  109. data/lib/sober_swag/reporting/report/object.rb +29 -0
  110. data/lib/sober_swag/reporting/report/output.rb +14 -0
  111. data/lib/sober_swag/reporting/report/value.rb +28 -0
  112. data/lib/sober_swag/reporting/report.rb +16 -0
  113. data/lib/sober_swag/reporting.rb +11 -0
  114. data/lib/sober_swag/serializer/array.rb +27 -3
  115. data/lib/sober_swag/serializer/base.rb +75 -25
  116. data/lib/sober_swag/serializer/conditional.rb +33 -1
  117. data/lib/sober_swag/serializer/field_list.rb +23 -5
  118. data/lib/sober_swag/serializer/hash.rb +53 -0
  119. data/lib/sober_swag/serializer/mapped.rb +10 -1
  120. data/lib/sober_swag/serializer/optional.rb +18 -1
  121. data/lib/sober_swag/serializer/primitive.rb +3 -0
  122. data/lib/sober_swag/serializer.rb +1 -0
  123. data/lib/sober_swag/server.rb +27 -11
  124. data/lib/sober_swag/type/named.rb +14 -0
  125. data/lib/sober_swag/types/comma_array.rb +4 -0
  126. data/lib/sober_swag/version.rb +1 -1
  127. data/lib/sober_swag.rb +7 -1
  128. metadata +72 -2
@@ -0,0 +1,28 @@
1
+ module SoberSwag
2
+ module Reporting
3
+ module Report
4
+ ##
5
+ # Report for a single value.
6
+ # Basically a wrapper around an array of strings.
7
+ class Value < Base
8
+ ##
9
+ # @param problems [Array<String>] problems with it
10
+ def initialize(problems)
11
+ @problems = problems
12
+ end
13
+
14
+ ##
15
+ # @return [Array<String>] the problems the value had
16
+ attr_reader :problems
17
+
18
+ def each_error
19
+ return enum_for(:each_error) unless block_given?
20
+
21
+ problems.each do |problem|
22
+ yield nil, problem
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,16 @@
1
+ module SoberSwag
2
+ module Reporting
3
+ ##
4
+ # Namespace modules for the various "reporters," or things that provide error handling.
5
+ module Report
6
+ autoload :Base, 'sober_swag/reporting/report/base'
7
+ autoload :Either, 'sober_swag/reporting/report/either'
8
+ autoload :Error, 'sober_swag/reporting/report/error'
9
+ autoload :Object, 'sober_swag/reporting/report/object'
10
+ autoload :Output, 'sober_swag/reporting/report/output'
11
+ autoload :MergedObject, 'sober_swag/reporting/report/merged_object'
12
+ autoload :Value, 'sober_swag/reporting/report/value'
13
+ autoload :List, 'sober_swag/reporting/report/list'
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,11 @@
1
+ module SoberSwag
2
+ ##
3
+ # A new module for parsers with better error reporting.
4
+ module Reporting
5
+ autoload :Input, 'sober_swag/reporting/input'
6
+ autoload :Report, 'sober_swag/reporting/report'
7
+ autoload :Output, 'sober_swag/reporting/output'
8
+ autoload :Compiler, 'sober_swag/reporting/compiler'
9
+ autoload :InvalidSchemaError, 'sober_swag/reporting/invalid_schema_error'
10
+ end
11
+ end
@@ -1,30 +1,54 @@
1
1
  module SoberSwag
2
2
  module Serializer
3
3
  ##
4
- # Make a serialize of arrays out of a serializer of the elements
4
+ # Transform a serializer that works on elements to one that works on Arrays
5
5
  class Array < Base
6
+ ##
7
+ # Make an array serializer out of another serializer.
8
+ # @param element_serializer [SoberSwag::Serializer::Base] the serializer to use for each element.
6
9
  def initialize(element_serializer)
7
10
  @element_serializer = element_serializer
8
11
  end
9
12
 
13
+ ##
14
+ # The serializer that will be used for each element in the array.
15
+ #
16
+ # @return [SoberSwag::Serializer::Base]
17
+ attr_reader :element_serializer
18
+
19
+ ##
20
+ # Delegates to {#element_serializer}
10
21
  def lazy_type?
11
22
  @element_serializer.lazy_type?
12
23
  end
13
24
 
25
+ ##
26
+ # Delegates to {#element_serializer}, wrapped in an array
14
27
  def lazy_type
15
28
  SoberSwag::Types::Array.of(@element_serializer.lazy_type)
16
29
  end
17
30
 
31
+ ##
32
+ # Delegates to {#element_serializer}
18
33
  def finalize_lazy_type!
19
34
  @element_serializer.finalize_lazy_type!
20
35
  end
21
36
 
22
- attr_reader :element_serializer
23
-
37
+ ##
38
+ # Serialize an array of objects that can be serialized with {#element_serializer}
39
+ # by calling `element_serializer.serialize` for each item in this array.
40
+ #
41
+ # Note: since ruby is duck-typed, anything that responds to the `#map` method from
42
+ # [Enumerable](https://ruby-doc.org/core-3.0.0/Enumerable.html) should work!
43
+ #
44
+ # @param object [Array<Object>,#map] collection of objects to serialize
45
+ # @return [Array<Object>] JSON-compatible array
24
46
  def serialize(object, options = {})
25
47
  object.map { |a| element_serializer.serialize(a, options) }
26
48
  end
27
49
 
50
+ ##
51
+ # The type of items returned from {#serialize}
28
52
  def type
29
53
  SoberSwag::Types::Array.of(element_serializer.type)
30
54
  end
@@ -1,34 +1,91 @@
1
1
  module SoberSwag
2
2
  module Serializer
3
3
  ##
4
- # Base interface class that all other serializers are subclasses of.
5
- # This also defines methods as stubs, which is sort of bad ruby style, but makes documentation
6
- # easier to generate.
4
+ # Base class for everything that provides serialization functionality in SoberSwag.
5
+ # SoberSwag serializers transform Ruby types into JSON types, with some associated *schema*.
6
+ # This schema is then used in the generated OpenAPI V3 documentation.
7
7
  class Base
8
8
  ##
9
9
  # Return a new serializer that is an *array* of elements of this serializer.
10
+ # This serializer will take in an array, and use `self` to serialize every element.
11
+ #
12
+ # @return [SoberSwag::Serializer::Array]
10
13
  def array
11
14
  SoberSwag::Serializer::Array.new(self)
12
15
  end
13
16
 
14
17
  ##
15
- # Returns a serializer that will pass `nil` values on unscathed
18
+ # Returns a serializer that will pass `nil` values on unscathed.
19
+ # That means that if you try to serialize `nil` with it, it will result in a JSON `null`.
20
+ # @return [SoberSwag::Serializer::Optional]
16
21
  def optional
17
22
  SoberSwag::Serializer::Optional.new(self)
18
23
  end
19
24
 
20
25
  alias nilable optional
21
26
 
27
+ ##
28
+ # Add metadata onto the *type* of a serializer.
29
+ # Note that this *returns a new serializer with metadata added* and does not perform mutation.
30
+ # @param hash [Hash] the metadata to set.
31
+ # @return [SoberSwag::Serializer::Meta] a serializer with metadata added
32
+ def meta(hash)
33
+ SoberSwag::Serializer::Meta.new(self, hash)
34
+ end
35
+
36
+ ##
37
+ # Get a new serializer that will first run the given block before serializing an object.
38
+ # For example, if you have a serializer for strings called `StringSerializer`,
39
+ # and you want to serialize `Date` objects via encoding them to a standardized string format,
40
+ # you can use:
41
+ #
42
+ # ```
43
+ # DateSerializer = StringSerializer.via_map do |date|
44
+ # date.strftime('%Y-%m-%d')
45
+ # end
46
+ # ```
47
+ #
48
+ # @yieldparam [Object] the object before serialization
49
+ # @yieldreturn [Object] a transformed object, that will
50
+ # be passed to {#serialize}
51
+ # @return [SoberSwag::Serializer::Mapped] the new serializer
52
+ def via_map(&block)
53
+ SoberSwag::Serializer::Mapped.new(self, block)
54
+ end
55
+
22
56
  ##
23
57
  # Is this type lazily defined?
24
58
  #
25
- # Used for mutual recursion.
59
+ # If we have two serializers that are *mutually recursive*, we need to do some "fun" magic to make that work.
60
+ # This comes up in a case like:
61
+ #
62
+ # ```ruby
63
+ # SchoolClass = SoberSwag::OutputObject.define do
64
+ # field :name, primitive(:String)
65
+ # view :detail do
66
+ # field :students, -> { Student.view(:base) }
67
+ # end
68
+ # end
69
+ #
70
+ # Student = SoberSwag::OutputObject.define do
71
+ # field :name, primitive(:String)
72
+ # view :detail do
73
+ # field :classes, -> { SchoolClass.view(:base) }
74
+ # end
75
+ # end
76
+ # ```
77
+ #
78
+ # This would result in an infinite loop if we tried to define the type struct the easy way.
79
+ # So, we instead use mutation to achieve "laziness."
26
80
  def lazy_type?
27
81
  false
28
82
  end
29
83
 
30
84
  ##
31
85
  # The lazy version of this type, for mutual recursion.
86
+ # @see #lazy_type? for why this is needed
87
+ #
88
+ # Once you call {#finalize_lazy_type!}, the type will be "fleshed out," and can be actually used.
32
89
  def lazy_type
33
90
  type
34
91
  end
@@ -43,43 +100,36 @@ module SoberSwag
43
100
 
44
101
  ##
45
102
  # Serialize an object.
103
+ # @abstract
46
104
  def serialize(_object, _options = {})
47
105
  raise ArgumentError, 'not implemented!'
48
106
  end
49
107
 
50
108
  ##
51
109
  # Get the type that we serialize to.
110
+ # @abstract
52
111
  def type
53
112
  raise ArgumentError, 'not implemented!'
54
113
  end
55
114
 
56
115
  ##
57
- # Add metadata onto the *type* of a serializer.
58
- def meta(hash)
59
- SoberSwag::Serializer::Meta.new(self, hash)
60
- end
61
-
62
- ##
63
- # If I am a serializer for type 'a', and you give me a way to turn 'a's into 'b's,
64
- # I can give you a serializer for type 'b' by running the funciton you gave.
65
- # For example, if I am a serializer for {String}, and you know how to turn
66
- # an {Int} into a {String}, I can now serialize {Int}s (by turning them into a string).
116
+ # Returns self.
67
117
  #
68
- # Note that the *declared* type of this is *not* changed: from a user's perspective,
69
- # they see a "string"
70
- def via_map(&block)
71
- SoberSwag::Serializer::Mapped.new(self, block)
72
- end
73
-
74
- ##
75
- # Serializer lets you get a serializer from things that might be classes
76
- # because of the blueprint naming hack.
118
+ # This exists due to a hack.
77
119
  def serializer
78
120
  self
79
121
  end
80
122
 
81
123
  ##
82
- # Get the type name of this to be used externally, or set it if an argument is provided
124
+ # @overload identifier()
125
+ # Returns the external identifier, used to uniquely identify this object within
126
+ # the schemas section of an OpenAPI v3 document.
127
+ # @return [String] the identifier.
128
+ # @overload identifier(arg)
129
+ # Sets the external identifier to use to uniquely identify
130
+ # this object within the schemas section of an OpenAPI v3 document.
131
+ # @param arg [String] the identifier to use
132
+ # @return [String] the identifer set
83
133
  def identifier(arg = nil)
84
134
  @identifier = arg if arg
85
135
  @identifier
@@ -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,29 @@ 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
- field_list.map { |field|
21
- [field.name, field.serializer.serialize(object, options)]
22
- }.to_h
30
+ {}.tap do |hash|
31
+ field_list.each do |field|
32
+ hash[field.name] = field.serializer.serialize(object, options)
33
+ end
34
+ end
23
35
  end
24
36
 
37
+ ##
38
+ # Construct a Dry::Struct from the fields given.
39
+ # This Struct will be swagger-able.
40
+ # @return [Dry::Struct]
25
41
  def type
26
42
  @type ||= make_struct_type!
27
43
  end
28
44
 
45
+ ##
46
+ # These types are always constructed lazily.
29
47
  def lazy_type?
30
48
  true
31
49
  end
@@ -0,0 +1,53 @@
1
+ require 'set'
2
+
3
+ module SoberSwag
4
+ module Serializer
5
+ ##
6
+ # Serialize via hash lookup.
7
+ # This is used to speed up serialization of views, but it may be useful elsewhere.
8
+ #
9
+ class Hash < Base
10
+ ##
11
+ # @param choices [Hash<Object => SoberSwag::Serializer::Base>] hash of serializers
12
+ # that we might use.
13
+ # @param default [SoberSwag::Serializer::Base] default to use if key not found.
14
+ # @param key_proc [Proc<Object, Hash>] extract the key we are interested in from the proc.
15
+ # Will be called with the object to serialize and the options hash.
16
+ def initialize(choices, default, key_proc)
17
+ @choices = choices
18
+ @default = default
19
+ @key_proc = key_proc
20
+ end
21
+
22
+ attr_reader :choices, :default, :key_proc
23
+
24
+ def serialize(object, options = {})
25
+ key = key_proc.call(object, options)
26
+
27
+ choices.fetch(key) { default }.serialize(object, options)
28
+ end
29
+
30
+ ##
31
+ # @return [Set<SoberSwag::Serializer::Base>]
32
+ def possible_serializers
33
+ @possible_serializers ||= (choices.values + [default]).to_set
34
+ end
35
+
36
+ def lazy_type?
37
+ possible_serializers.any?(&:lazy_type?)
38
+ end
39
+
40
+ def finalize_lazy_type!
41
+ possible_serializers.each(&:finalize_lazy_type!)
42
+ end
43
+
44
+ def lazy_type
45
+ @lazy_type ||= possible_serializers.map(&:lazy_type).reduce(:|)
46
+ end
47
+
48
+ def type
49
+ @type ||= possible_serializers.map(&:type).reduce(:|)
50
+ end
51
+ end
52
+ end
53
+ 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
@@ -10,6 +10,7 @@ module SoberSwag
10
10
  autoload(:Mapped, 'sober_swag/serializer/mapped')
11
11
  autoload(:Optional, 'sober_swag/serializer/optional')
12
12
  autoload(:FieldList, 'sober_swag/serializer/field_list')
13
+ autoload(:Hash, 'sober_swag/serializer/hash')
13
14
  autoload(:Meta, 'sober_swag/serializer/meta')
14
15
 
15
16
  class << self
@@ -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.18.0'
4
+ VERSION = '0.22.0'
5
5
  end