sober_swag 0.19.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.
- checksums.yaml +4 -4
- data/.github/workflows/lint.yml +1 -1
- data/.github/workflows/ruby.yml +1 -1
- data/.gitignore +2 -0
- data/.rubocop.yml +5 -1
- data/.yardopts +7 -0
- data/CHANGELOG.md +5 -0
- data/Gemfile +8 -0
- data/README.md +1 -1
- data/docs/serializers.md +3 -0
- data/example/Gemfile +1 -1
- data/example/config/environments/production.rb +1 -1
- data/lib/sober_swag.rb +6 -1
- data/lib/sober_swag/compiler.rb +29 -3
- data/lib/sober_swag/compiler/path.rb +42 -3
- data/lib/sober_swag/compiler/paths.rb +20 -0
- data/lib/sober_swag/compiler/primitive.rb +17 -0
- data/lib/sober_swag/compiler/type.rb +105 -22
- data/lib/sober_swag/controller.rb +39 -12
- data/lib/sober_swag/controller/route.rb +103 -20
- data/lib/sober_swag/input_object.rb +90 -7
- data/lib/sober_swag/nodes/array.rb +19 -0
- data/lib/sober_swag/nodes/attribute.rb +45 -4
- data/lib/sober_swag/nodes/base.rb +27 -7
- data/lib/sober_swag/nodes/binary.rb +30 -13
- data/lib/sober_swag/nodes/enum.rb +16 -1
- data/lib/sober_swag/nodes/list.rb +20 -0
- data/lib/sober_swag/nodes/nullable_primitive.rb +3 -0
- data/lib/sober_swag/nodes/object.rb +4 -1
- data/lib/sober_swag/nodes/one_of.rb +11 -3
- data/lib/sober_swag/nodes/primitive.rb +34 -2
- data/lib/sober_swag/nodes/sum.rb +8 -0
- data/lib/sober_swag/output_object.rb +35 -4
- data/lib/sober_swag/output_object/definition.rb +31 -1
- data/lib/sober_swag/output_object/field.rb +31 -11
- data/lib/sober_swag/output_object/field_syntax.rb +19 -3
- data/lib/sober_swag/output_object/view.rb +46 -1
- data/lib/sober_swag/parser.rb +7 -1
- data/lib/sober_swag/serializer/array.rb +27 -3
- data/lib/sober_swag/serializer/base.rb +75 -25
- data/lib/sober_swag/serializer/conditional.rb +33 -1
- data/lib/sober_swag/serializer/field_list.rb +18 -2
- data/lib/sober_swag/serializer/mapped.rb +10 -1
- data/lib/sober_swag/serializer/optional.rb +18 -1
- data/lib/sober_swag/serializer/primitive.rb +3 -0
- data/lib/sober_swag/server.rb +5 -1
- data/lib/sober_swag/type/named.rb +14 -0
- data/lib/sober_swag/types/comma_array.rb +4 -0
- data/lib/sober_swag/version.rb +1 -1
- metadata +3 -2
data/lib/sober_swag/parser.rb
CHANGED
@@ -1,13 +1,19 @@
|
|
1
1
|
module SoberSwag
|
2
2
|
##
|
3
3
|
# Parses a *Dry-Types Schema* into a set of nodes we can use to compile.
|
4
|
-
# This is mostly because the
|
4
|
+
# This is mostly because the visitor pattern sucks and catamorphisms are nice.
|
5
|
+
#
|
6
|
+
# Do not use this class directly, as it is not part of the public api.
|
7
|
+
# Instead, use classes from the {SoberSwag::Compiler} namespace.
|
5
8
|
class Parser
|
6
9
|
def initialize(node)
|
7
10
|
@node = node
|
8
11
|
@found = Set.new
|
9
12
|
end
|
10
13
|
|
14
|
+
##
|
15
|
+
# Compile to one of our internal nodes.
|
16
|
+
# @return [SoberSwag::Nodes::Base] the node that describes this type.
|
11
17
|
def to_syntax # rubocop:disable Metrics/MethodLength, Metrics/CyclomaticComplexity
|
12
18
|
case @node
|
13
19
|
when Dry::Types::Default
|
@@ -1,30 +1,54 @@
|
|
1
1
|
module SoberSwag
|
2
2
|
module Serializer
|
3
3
|
##
|
4
|
-
#
|
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
|
-
|
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
|
5
|
-
#
|
6
|
-
#
|
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
|
-
#
|
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
|
-
#
|
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
|
-
#
|
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
|
-
#
|
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
|
-
|
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
|
-
#
|
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
|
-
|
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
|
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
|
data/lib/sober_swag/server.rb
CHANGED
@@ -21,7 +21,7 @@ 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
|
24
|
+
# @param cache [Bool | Proc] if we should cache our definitions (default false)
|
25
25
|
# @param redoc_version [String] what version of the redoc library to use to display UI (default 'next', the latest version).
|
26
26
|
def initialize(
|
27
27
|
controller_proc: RAILS_CONTROLLER_PROC,
|
@@ -60,6 +60,8 @@ module SoberSwag
|
|
60
60
|
</html>
|
61
61
|
HTML
|
62
62
|
|
63
|
+
##
|
64
|
+
# Standard Rack call method.
|
63
65
|
def call(env)
|
64
66
|
req = Rack::Request.new(env)
|
65
67
|
if req.path_info&.match?(/json/si) || req.get_header('Accept')&.match?(/json/si)
|
@@ -69,6 +71,8 @@ module SoberSwag
|
|
69
71
|
end
|
70
72
|
end
|
71
73
|
|
74
|
+
private
|
75
|
+
|
72
76
|
def generate_json_string
|
73
77
|
if cache?
|
74
78
|
@json_string ||= JSON.dump(generate_swagger)
|