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.
- checksums.yaml +4 -4
- data/.github/dependabot.yml +15 -0
- data/.github/workflows/lint.yml +4 -9
- data/.github/workflows/ruby.yml +2 -6
- data/.gitignore +4 -0
- data/.rubocop.yml +50 -5
- data/.yardopts +7 -0
- data/CHANGELOG.md +29 -1
- data/Gemfile +8 -0
- data/README.md +155 -4
- data/bin/rspec +29 -0
- data/docs/serializers.md +18 -13
- data/example/Gemfile +2 -2
- data/example/app/controllers/people_controller.rb +4 -0
- data/example/app/controllers/posts_controller.rb +5 -0
- 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 +49 -3
- data/lib/sober_swag/compiler/paths.rb +20 -0
- data/lib/sober_swag/compiler/primitive.rb +20 -1
- data/lib/sober_swag/compiler/type.rb +105 -22
- data/lib/sober_swag/controller.rb +42 -15
- data/lib/sober_swag/controller/route.rb +133 -28
- data/lib/sober_swag/input_object.rb +117 -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 +27 -11
- 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
- data/sober_swag.gemspec +2 -2
- metadata +13 -10
@@ -1,28 +1,44 @@
|
|
1
1
|
module SoberSwag
|
2
2
|
module Nodes
|
3
3
|
##
|
4
|
+
# @abstract
|
4
5
|
# Base Node that all other nodes inherit from.
|
5
6
|
# All nodes should define the following:
|
6
7
|
#
|
7
|
-
#
|
8
|
+
#
|
9
|
+
# - `#deconstruct`, which returns an array of *everything needed to identify the node.*
|
8
10
|
# We base comparisons on the result of deconstruction.
|
9
|
-
# -
|
11
|
+
# - `#deconstruct_keys`, which returns a hash of *everything needed to identify the node*.
|
10
12
|
# We use this later.
|
11
13
|
class Base
|
12
14
|
include Comparable
|
13
15
|
|
14
16
|
##
|
15
17
|
# Value-level comparison.
|
18
|
+
# Returns `-1`, `0`, or `+1`
|
19
|
+
# if this object is less than, equal to, or greater than `other`.
|
20
|
+
#
|
21
|
+
# @param other [Object] the other object
|
22
|
+
#
|
23
|
+
# @return [Integer]
|
24
|
+
# comparison result
|
16
25
|
def <=>(other)
|
17
|
-
return other.class.name <=> self.class.name unless other.
|
26
|
+
return other.class.name <=> self.class.name unless other.instance_of?(self.class)
|
18
27
|
|
19
28
|
deconstruct <=> other.deconstruct
|
20
29
|
end
|
21
30
|
|
31
|
+
##
|
32
|
+
# Is this object equal to the other object?
|
33
|
+
# @param other [Object] the object to compare
|
34
|
+
# @return [Boolean] yes or no
|
22
35
|
def eql?(other)
|
23
36
|
deconstruct == other.deconstruct
|
24
37
|
end
|
25
38
|
|
39
|
+
##
|
40
|
+
# Standard hash key.
|
41
|
+
# @return [Integer]
|
26
42
|
def hash
|
27
43
|
deconstruct.hash
|
28
44
|
end
|
@@ -37,17 +53,21 @@ module SoberSwag
|
|
37
53
|
#
|
38
54
|
# When working with these definition nodes, we very often want to transform something recursively.
|
39
55
|
# This method allows us to do so by focusing on a single level at a time, keeping the actual recursion *abstract*.
|
56
|
+
#
|
57
|
+
# @yieldparam node nodes contained within this node, from the bottom-up.
|
58
|
+
# This block will first transform the innermost node, then the second layer, and so on, until we get to the node you originally called `#cata` on.
|
59
|
+
# @yieldreturn [Object] the object you wish to transform into.
|
40
60
|
def cata
|
41
61
|
raise ArgumentError, 'Base is abstract'
|
42
62
|
end
|
43
63
|
|
64
|
+
##
|
65
|
+
# Map over the inner, contained type of this node.
|
66
|
+
# This will *only* map over values wrapped in a `SoberSwag::Nodes::Primitive` object.
|
67
|
+
# Unlike {#cata}, it does not transform the entire node tree.
|
44
68
|
def map
|
45
69
|
raise ArgumentError, 'Base is abstract'
|
46
70
|
end
|
47
|
-
|
48
|
-
def flatten_one_ofs
|
49
|
-
raise ArgumentError, 'Base is abstract'
|
50
|
-
end
|
51
71
|
end
|
52
72
|
end
|
53
73
|
end
|
@@ -1,37 +1,43 @@
|
|
1
1
|
module SoberSwag
|
2
2
|
module Nodes
|
3
3
|
##
|
4
|
-
# A
|
5
|
-
#
|
6
|
-
# It's cool I promise.
|
4
|
+
# A binary node: has a left and right hand side.
|
5
|
+
# Basically a node of a binary tree.
|
7
6
|
class Binary < Base
|
7
|
+
##
|
8
|
+
# @param lhs [SoberSwag::Nodes::Base] the left-hand node.
|
9
|
+
# @param rhs [SoberSwag::Nodes::Base] the right-hand node.
|
8
10
|
def initialize(lhs, rhs)
|
9
11
|
@lhs = lhs
|
10
12
|
@rhs = rhs
|
11
13
|
end
|
12
14
|
|
13
|
-
|
15
|
+
##
|
16
|
+
# @return [SoberSwag::Nodes::Base] the left-hand node
|
17
|
+
attr_reader :lhs
|
14
18
|
|
15
19
|
##
|
16
|
-
#
|
17
|
-
|
18
|
-
def map(&block)
|
19
|
-
self.class.new(
|
20
|
-
lhs.map(&block),
|
21
|
-
rhs.map(&block)
|
22
|
-
)
|
23
|
-
end
|
20
|
+
# @return [SoberSwag::Nodes::Base] the right-hand node
|
21
|
+
attr_reader :rhs
|
24
22
|
|
23
|
+
##
|
24
|
+
# Deconstructs into an array of `[lhs, rhs]`
|
25
|
+
#
|
26
|
+
# @return [Array(SoberSwag::Nodes::Base, SoberSwag::Nodes::Base)]
|
25
27
|
def deconstruct
|
26
28
|
[lhs, rhs]
|
27
29
|
end
|
28
30
|
|
31
|
+
##
|
32
|
+
# Deconstruct into a hash of attributes.
|
29
33
|
def deconstruct_keys(_keys)
|
30
34
|
{ lhs: lhs, rhs: rhs }
|
31
35
|
end
|
32
36
|
|
33
37
|
##
|
34
|
-
#
|
38
|
+
# @see SoberSwag::Nodes::Base#cata
|
39
|
+
#
|
40
|
+
# Maps over the LHS side first, then the RHS side, then the root.
|
35
41
|
def cata(&block)
|
36
42
|
block.call(
|
37
43
|
self.class.new(
|
@@ -40,6 +46,17 @@ module SoberSwag
|
|
40
46
|
)
|
41
47
|
)
|
42
48
|
end
|
49
|
+
|
50
|
+
##
|
51
|
+
# @see SoberSwag::Nodes::Base#map
|
52
|
+
#
|
53
|
+
# Maps over the LHS side first, then the RHS side, then the root.
|
54
|
+
def map(&block)
|
55
|
+
self.class.new(
|
56
|
+
lhs.map(&block),
|
57
|
+
rhs.map(&block)
|
58
|
+
)
|
59
|
+
end
|
43
60
|
end
|
44
61
|
end
|
45
62
|
end
|
@@ -2,26 +2,41 @@ module SoberSwag
|
|
2
2
|
module Nodes
|
3
3
|
##
|
4
4
|
# Compiler node to represent an enum value.
|
5
|
-
# Enums are special enough to have their own node.
|
5
|
+
# Enums are special enough to have their own node, as they are basically a constant list of always-string values.
|
6
6
|
class Enum < Base
|
7
7
|
def initialize(values)
|
8
8
|
@values = values
|
9
9
|
end
|
10
10
|
|
11
|
+
##
|
12
|
+
# @return [Array<Symbol,String>] values of the enum.
|
11
13
|
attr_reader :values
|
12
14
|
|
15
|
+
##
|
16
|
+
# Since there is nothing to map over, this node will never actually call the block given.
|
17
|
+
#
|
18
|
+
# @see SoberSwag::Nodes::Base#map
|
13
19
|
def map
|
14
20
|
dup
|
15
21
|
end
|
16
22
|
|
23
|
+
##
|
24
|
+
# Deconstructs into the enum values.
|
25
|
+
#
|
26
|
+
# @return [Array(Array<Symbol,String>)] the cases of the enum.
|
17
27
|
def deconstruct
|
18
28
|
[values]
|
19
29
|
end
|
20
30
|
|
31
|
+
##
|
32
|
+
# @return [Hash{Symbol => Array<Symbol,String>}]
|
33
|
+
# the values, wrapped in a `values:` key.
|
21
34
|
def deconstruct_keys(_keys)
|
22
35
|
{ values: values }
|
23
36
|
end
|
24
37
|
|
38
|
+
##
|
39
|
+
# @see SoberSwag::Nodes::Base#cata
|
25
40
|
def cata(&block)
|
26
41
|
block.call(dup)
|
27
42
|
end
|
@@ -6,21 +6,37 @@ module SoberSwag
|
|
6
6
|
# Unlike {SoberSwag::Nodes::Array}, this actually models arrays.
|
7
7
|
# The other one is a node that *is* an array in terms of what it contains.
|
8
8
|
# Kinda confusing, but oh well.
|
9
|
+
#
|
10
|
+
# @todo swap the names of this and {SoberSwag::Nodes::Array} so it's less confusing.
|
9
11
|
class List < Base
|
12
|
+
##
|
13
|
+
# Initialize with a node representing the type of elements in the list.
|
14
|
+
# @param element [SoberSwag::Nodes::Base] the type
|
10
15
|
def initialize(element)
|
11
16
|
@element = element
|
12
17
|
end
|
13
18
|
|
19
|
+
##
|
20
|
+
# @return [SoberSwag::Nodes::Base]
|
14
21
|
attr_reader :element
|
15
22
|
|
23
|
+
##
|
24
|
+
# @return [Array(SoberSwag::Nodes::Base)]
|
16
25
|
def deconstruct
|
17
26
|
[element]
|
18
27
|
end
|
19
28
|
|
29
|
+
##
|
30
|
+
# @return [Hash{Symbol => SoberSwag::Nodes::Base}]
|
31
|
+
# the contained type wrapped in an `element:` key.
|
20
32
|
def deconstruct_keys(_)
|
21
33
|
{ element: element }
|
22
34
|
end
|
23
35
|
|
36
|
+
##
|
37
|
+
# @see SoberSwag::Nodes::Base#cata
|
38
|
+
#
|
39
|
+
# Maps over the element type, then this `List` type.
|
24
40
|
def cata(&block)
|
25
41
|
block.call(
|
26
42
|
self.class.new(
|
@@ -29,6 +45,10 @@ module SoberSwag
|
|
29
45
|
)
|
30
46
|
end
|
31
47
|
|
48
|
+
##
|
49
|
+
# @see SoberSwag::Nodes::Base#map
|
50
|
+
#
|
51
|
+
# Maps over the element type.
|
32
52
|
def map(&block)
|
33
53
|
self.class.new(
|
34
54
|
element.map(&block)
|
@@ -2,8 +2,11 @@ module SoberSwag
|
|
2
2
|
module Nodes
|
3
3
|
##
|
4
4
|
# Objects might have attribute keys, so they're
|
5
|
-
# basically a list of attributes
|
5
|
+
# basically a list of attributes.
|
6
6
|
class Object < SoberSwag::Nodes::Array
|
7
|
+
##
|
8
|
+
# @return [Hash{Symbol => Array<SoberSwag::Nodes::Attribute>}]
|
9
|
+
# the attributes, wrapped in an `attributes:` key.
|
7
10
|
def deconstruct_keys(_)
|
8
11
|
{ attributes: @elements }
|
9
12
|
end
|
@@ -1,11 +1,19 @@
|
|
1
1
|
module SoberSwag
|
2
2
|
module Nodes
|
3
3
|
##
|
4
|
-
#
|
5
|
-
#
|
4
|
+
# OpenAPI v3 represents types that are a "choice" between multiple alternatives as an array.
|
5
|
+
# However, it is easier to model these as a sum type initially: if a type can be either an `A`, a `B`, or a `C`, we can model this as:
|
6
|
+
#
|
7
|
+
# `Sum.new(A, Sum.new(B, C))`.
|
8
|
+
#
|
9
|
+
# This means we only ever need to deal with two types at once.
|
10
|
+
# So, we initially serialize to a sum type, then later transform to this array type for further serialization.
|
6
11
|
class OneOf < ::SoberSwag::Nodes::Array
|
12
|
+
##
|
13
|
+
# @return [Hash{Symbol => SoberSwag::Nodes::Base}]
|
14
|
+
# the alternatives, wrapped in an `alternatives:` key.
|
7
15
|
def deconstruct_keys(_)
|
8
|
-
{ alternatives: @
|
16
|
+
{ alternatives: @elements }
|
9
17
|
end
|
10
18
|
end
|
11
19
|
end
|
@@ -1,27 +1,59 @@
|
|
1
1
|
module SoberSwag
|
2
2
|
module Nodes
|
3
3
|
##
|
4
|
-
# Root node of the tree
|
4
|
+
# Root node of the tree.
|
5
|
+
# This contains "primitive values."
|
6
|
+
# Initially, this is probably the types of attributes or array elements or whatever.
|
7
|
+
# As we use {#cata} to transform this, it will start containing swagger-compatible type objects.
|
8
|
+
#
|
9
|
+
# This node can contain metadata as well.
|
5
10
|
class Primitive < Base
|
11
|
+
##
|
12
|
+
# @param value [Object] the primitive value to store
|
13
|
+
# @param metadata [Hash] the metadata
|
6
14
|
def initialize(value, metadata = {})
|
7
15
|
@value = value
|
8
16
|
@metadata = metadata
|
9
17
|
end
|
10
18
|
|
11
|
-
|
19
|
+
##
|
20
|
+
# @return [Object] the contained value
|
21
|
+
attr_reader :value
|
12
22
|
|
23
|
+
##
|
24
|
+
# @return [Hash] metadata associated with the contained value.
|
25
|
+
attr_reader :metadata
|
26
|
+
|
27
|
+
##
|
28
|
+
# @see SoberSwag::Nodes::Base#map
|
29
|
+
#
|
30
|
+
# This will actually map over {#value}.
|
13
31
|
def map(&block)
|
14
32
|
self.class.new(block.call(value), metadata.dup)
|
15
33
|
end
|
16
34
|
|
35
|
+
##
|
36
|
+
# Deconstructs to the value and the metadata.
|
37
|
+
#
|
38
|
+
# @return [Array(Object, Hash)] containing value and metadata.
|
17
39
|
def deconstruct
|
18
40
|
[value, metadata]
|
19
41
|
end
|
20
42
|
|
43
|
+
##
|
44
|
+
# Wraps the attributes in a hash.
|
45
|
+
#
|
46
|
+
# @return [Hash{Symbol => Object, Hash}]
|
47
|
+
# {#value} in `value:`, {#metadata} in `metadata:`
|
21
48
|
def deconstruct_keys(_)
|
22
49
|
{ value: value, metadata: metadata }
|
23
50
|
end
|
24
51
|
|
52
|
+
##
|
53
|
+
# @see SoberSwag::Nodes::Base#cata
|
54
|
+
# As this is a root node, we actually call the block directly.
|
55
|
+
# @yieldparam node [SoberSwag::Nodes::Primitive] this node.
|
56
|
+
# @return whatever the block returns.
|
25
57
|
def cata(&block)
|
26
58
|
block.call(self.class.new(value, metadata))
|
27
59
|
end
|
data/lib/sober_swag/nodes/sum.rb
CHANGED
@@ -1,5 +1,13 @@
|
|
1
1
|
module SoberSwag
|
2
2
|
module Nodes
|
3
|
+
##
|
4
|
+
# A "Sum" type represents either one type or the other.
|
5
|
+
#
|
6
|
+
# It is called "Sum" because, if a type can be either type `A` or type `B`,
|
7
|
+
# the number of possible values for the type of `number_of_values(A) + number_of_values(B)`.
|
8
|
+
#
|
9
|
+
# Internally, this is primarily used when an object can be either one type or another.
|
10
|
+
# It will latter be flattened into {SoberSwag::Nodes::OneOf}
|
3
11
|
class Sum < Binary
|
4
12
|
end
|
5
13
|
end
|
@@ -5,7 +5,7 @@ module SoberSwag
|
|
5
5
|
# Create a serializer that is heavily inspired by the "Blueprinter" library.
|
6
6
|
# This allows you to make "views" and such inside.
|
7
7
|
#
|
8
|
-
# Under the hood, this is actually all based on {SoberSwag::
|
8
|
+
# Under the hood, this is actually all based on {SoberSwag::Serializer::Base}.
|
9
9
|
class OutputObject < SoberSwag::Serializer::Base
|
10
10
|
autoload(:Field, 'sober_swag/output_object/field')
|
11
11
|
autoload(:Definition, 'sober_swag/output_object/definition')
|
@@ -20,7 +20,7 @@ module SoberSwag
|
|
20
20
|
#
|
21
21
|
# PersonSerializer = SoberSwag::OutputObject.define do
|
22
22
|
# field :id, primitive(:Integer)
|
23
|
-
# field :name,
|
23
|
+
# field :name, primitive(:String).optional
|
24
24
|
#
|
25
25
|
# view :complex do
|
26
26
|
# field :age, primitive(:Integer)
|
@@ -32,9 +32,11 @@ module SoberSwag
|
|
32
32
|
# However, this is only a hack to get rid of the weird naming issue when
|
33
33
|
# generating swagger from dry structs: their section of the schema area
|
34
34
|
# is defined by their *Ruby Class Name*. In the future, if we get rid of this,
|
35
|
-
# we might be able to keep this on the value-level, in which case {
|
35
|
+
# we might be able to keep this on the value-level, in which case {.define}
|
36
36
|
# can simply return an *instance* of SoberSwag::Serializer that does
|
37
37
|
# the correct thing, with the name you give it. This works for now, though.
|
38
|
+
#
|
39
|
+
# @return [Class] the serializer generated.
|
38
40
|
def self.define(&block)
|
39
41
|
d = Definition.new.tap do |o|
|
40
42
|
o.instance_eval(&block)
|
@@ -42,28 +44,51 @@ module SoberSwag
|
|
42
44
|
new(d.fields, d.views, d.identifier)
|
43
45
|
end
|
44
46
|
|
47
|
+
##
|
48
|
+
# @param fields [Array<SoberSwag::OutputObject::Field>] the fields for this OutputObject
|
49
|
+
# @param views [Array<SoberSwag::OutputObject::View>] the views for this OutputObject
|
50
|
+
# @param identifier [String] the external identifier for this OutputObject
|
45
51
|
def initialize(fields, views, identifier)
|
46
52
|
@fields = fields
|
47
53
|
@views = views
|
48
54
|
@identifier = identifier
|
49
55
|
end
|
50
56
|
|
51
|
-
|
57
|
+
##
|
58
|
+
# @return [Array<SoberSwag::OutputObject::Field>]
|
59
|
+
attr_reader :fields
|
60
|
+
##
|
61
|
+
# @return [Array<SoberSwag::OutputObject::View>]
|
62
|
+
attr_reader :views
|
63
|
+
##
|
64
|
+
# @return [String] the external ID to use for this object
|
65
|
+
attr_reader :identifier
|
52
66
|
|
67
|
+
##
|
68
|
+
# Perform serialization.
|
53
69
|
def serialize(obj, opts = {})
|
54
70
|
serializer.serialize(obj, opts)
|
55
71
|
end
|
56
72
|
|
73
|
+
##
|
74
|
+
# Get a Dry::Struct of the type this OutputObject will serialize to.
|
57
75
|
def type
|
58
76
|
serializer.type
|
59
77
|
end
|
60
78
|
|
79
|
+
##
|
80
|
+
# Get a serializer for a single view contained in this output object.
|
81
|
+
# Note: given `:base`, it will return a serializer for the base OutputObject
|
82
|
+
# @param name [Symbol] the name of the view
|
83
|
+
# @return [SoberSwag::Serializer::Base] the serializer
|
61
84
|
def view(name)
|
62
85
|
return base_serializer if name == :base
|
63
86
|
|
64
87
|
@views.find { |v| v.name == name }
|
65
88
|
end
|
66
89
|
|
90
|
+
##
|
91
|
+
# A serializer for the "base type" of this OutputObject, with no views.
|
67
92
|
def base
|
68
93
|
base_serializer
|
69
94
|
end
|
@@ -72,6 +97,8 @@ module SoberSwag
|
|
72
97
|
# Compile down this to an appropriate serializer.
|
73
98
|
# It uses {SoberSwag::Serializer::Conditional} to do view-parsing,
|
74
99
|
# and {SoberSwag::Serializer::FieldList} to do the actual serialization.
|
100
|
+
#
|
101
|
+
# @todo: optimize view selection to use binary instead of linear search
|
75
102
|
def serializer # rubocop:disable Metrics/MethodLength
|
76
103
|
@serializer ||=
|
77
104
|
begin
|
@@ -92,10 +119,14 @@ module SoberSwag
|
|
92
119
|
end
|
93
120
|
end
|
94
121
|
|
122
|
+
##
|
123
|
+
# @return [String]
|
95
124
|
def to_s
|
96
125
|
"<SoberSwag::OutputObject(#{identifier})>"
|
97
126
|
end
|
98
127
|
|
128
|
+
##
|
129
|
+
# @return [SoberSwag::Serializer::FieldList] serializer for this output object.
|
99
130
|
def base_serializer
|
100
131
|
@base_serializer ||= SoberSwag::Serializer::FieldList.new(fields).tap do |s|
|
101
132
|
s.identifier(identifier)
|