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
@@ -1,6 +1,6 @@
|
|
1
1
|
module SoberSwag
|
2
2
|
##
|
3
|
-
# A variant of Dry::Struct that allows you to set a "model name" that is
|
3
|
+
# A variant of Dry::Struct that allows you to set a "model name" that is publicly visible.
|
4
4
|
# If you do not set one, it will be the Ruby class name, with any '::' replaced with a '.'.
|
5
5
|
#
|
6
6
|
# This otherwise behaves exactly like a Dry::Struct.
|
@@ -12,24 +12,67 @@ module SoberSwag
|
|
12
12
|
class << self
|
13
13
|
##
|
14
14
|
# The name to use for this type in external documentation.
|
15
|
-
|
16
|
-
|
15
|
+
#
|
16
|
+
# @param new_ident [String] what to call this InputObject in external documentation.
|
17
|
+
def identifier(new_ident = nil)
|
18
|
+
@identifier = new_ident if new_ident
|
17
19
|
|
18
20
|
@identifier || name.to_s.gsub('::', '.')
|
19
21
|
end
|
20
22
|
|
23
|
+
##
|
24
|
+
# @overload attribute(key, parent = SoberSwag::InputObject, &block)
|
25
|
+
# Defines an attribute as a direct sub-object.
|
26
|
+
# This block will be called as in {SoberSwag.input_object}.
|
27
|
+
# This might be useful in a case like the following:
|
28
|
+
#
|
29
|
+
# ```ruby
|
30
|
+
# class Classroom < SoberSwag::InputObject
|
31
|
+
# attribute :biographical_detail do
|
32
|
+
# attribute :student_name, primitive(:String)
|
33
|
+
# end
|
34
|
+
# end
|
35
|
+
# ```
|
36
|
+
#
|
37
|
+
# @param key [Symbol] the attribute name
|
38
|
+
# @param parent [Class] the parent class to use for the sub-object
|
39
|
+
# @overload attribute(key, type)
|
40
|
+
# Defines a new attribute with the given type.
|
41
|
+
# @param key [Symbol] the attribute name
|
42
|
+
# @param type the attribute type
|
21
43
|
def attribute(key, parent = SoberSwag::InputObject, &block)
|
22
44
|
raise ArgumentError, "parent class #{parent} is not an input object type!" unless valid_field_def?(parent, block)
|
23
45
|
|
24
46
|
super(key, parent, &block)
|
25
47
|
end
|
26
48
|
|
49
|
+
##
|
50
|
+
# @overload attribute(key, parent = SoberSwag::InputObject, &block)
|
51
|
+
# Defines an optional attribute by defining a sub-object inline.
|
52
|
+
# This differs from a nil-able attribute as it can be *not provided*, while nilable attributes must be set to `null`.
|
53
|
+
#
|
54
|
+
# Yields to the block like in {SoberSwag.input_object}
|
55
|
+
#
|
56
|
+
# @param key [Symbol] the attribute name
|
57
|
+
# @param parent [Class] the parent class to use for the sub-object
|
58
|
+
# @overload attribute(key, type)
|
59
|
+
# Defines an optional attribute with a given type.
|
60
|
+
# This differs from a nil-able attribute as it can be *not provided*, while nilable attributes must be set to `null`.
|
61
|
+
#
|
62
|
+
# @param key [Symbol] the attribute name
|
63
|
+
# @param type the attribute type, another parsable object.
|
27
64
|
def attribute?(key, parent = SoberSwag::InputObject, &block)
|
28
65
|
raise ArgumentError, "parent class #{parent} is not an input object type!" unless valid_field_def?(parent, block)
|
29
66
|
|
30
67
|
super(key, parent, &block)
|
31
68
|
end
|
32
69
|
|
70
|
+
##
|
71
|
+
# Add metadata keys, like `:description`, to the defined type.
|
72
|
+
# Note: does NOT mutate the type, returns a new type with the metadata added.
|
73
|
+
#
|
74
|
+
# @param args [Hash] the argument values
|
75
|
+
# @return [SoberSwag::InputObject] the new input object class
|
33
76
|
def meta(*args)
|
34
77
|
original = self
|
35
78
|
|
@@ -42,8 +85,25 @@ module SoberSwag
|
|
42
85
|
end
|
43
86
|
|
44
87
|
##
|
45
|
-
#
|
46
|
-
#
|
88
|
+
# Convenience method: you can use `.primitive` get a primitive parser for a given type.
|
89
|
+
# This lets you write:
|
90
|
+
#
|
91
|
+
# ```ruby
|
92
|
+
# class Foo < SoberSwag::InputObject
|
93
|
+
# attribute :bar, primitive(:String)
|
94
|
+
# end
|
95
|
+
# ```
|
96
|
+
#
|
97
|
+
# instead of
|
98
|
+
#
|
99
|
+
# ```ruby
|
100
|
+
# class Foo < SoberSwag::InputObject
|
101
|
+
# attribute :bar, SoberSwag::Types::String
|
102
|
+
# end
|
103
|
+
# ```
|
104
|
+
#
|
105
|
+
# @param args [Symbol] a symbol
|
106
|
+
# @return a primitive parser
|
47
107
|
def primitive(*args)
|
48
108
|
if args.length == 1
|
49
109
|
SoberSwag::Types.const_get(args.first)
|
@@ -52,8 +112,31 @@ module SoberSwag
|
|
52
112
|
end
|
53
113
|
end
|
54
114
|
|
55
|
-
|
56
|
-
|
115
|
+
##
|
116
|
+
# Convenience method: you can use `.param` to get a parameter parser of a given type.
|
117
|
+
# Said parsers are more loose: for example, `param(:Integer)` will parse the string `"10"` into `10`, while
|
118
|
+
# `primitive(:Integer)` will throw an error.
|
119
|
+
#
|
120
|
+
# This method lets you write:
|
121
|
+
#
|
122
|
+
# ```ruby
|
123
|
+
# class Foo < SoberSwag::InputObject
|
124
|
+
# attribute :bar, param(:Integer)
|
125
|
+
# end
|
126
|
+
# ```
|
127
|
+
#
|
128
|
+
# instead of
|
129
|
+
#
|
130
|
+
# ```ruby
|
131
|
+
# class Foo < SoberSwag::InputObject
|
132
|
+
# attribute :bar, SoberSwag::Types::Param::Integer
|
133
|
+
# end
|
134
|
+
# ```
|
135
|
+
#
|
136
|
+
# @param name [Symbol] the name of the parameter type to get
|
137
|
+
# @return a parameter parser
|
138
|
+
def param(name)
|
139
|
+
SoberSwag::Types::Params.const_get(name)
|
57
140
|
end
|
58
141
|
|
59
142
|
private
|
@@ -10,18 +10,37 @@ module SoberSwag
|
|
10
10
|
|
11
11
|
attr_reader :elements
|
12
12
|
|
13
|
+
##
|
14
|
+
# @see SoberSwag::Nodes::Array#map
|
15
|
+
#
|
13
16
|
def map(&block)
|
14
17
|
self.class.new(elements.map { |elem| elem.map(&block) })
|
15
18
|
end
|
16
19
|
|
20
|
+
##
|
21
|
+
# @see SoberSwag::Nodes::Array#cata
|
22
|
+
#
|
23
|
+
# The block will be called with each element contained in this array node in turn, then called with a {SoberSwag::Nodes::Array} constructed
|
24
|
+
# from the resulting values.
|
25
|
+
#
|
26
|
+
# @return whatever the block yields.
|
17
27
|
def cata(&block)
|
18
28
|
block.call(self.class.new(elements.map { |elem| elem.cata(&block) }))
|
19
29
|
end
|
20
30
|
|
31
|
+
##
|
32
|
+
# Deconstructs into the elements.
|
33
|
+
#
|
34
|
+
# @return [Array<SoberSwag::Nodes::Base>]
|
21
35
|
def deconstruct
|
22
36
|
@elements
|
23
37
|
end
|
24
38
|
|
39
|
+
##
|
40
|
+
# Deconstruction for pattern-matching
|
41
|
+
#
|
42
|
+
# @return [Hash{Symbol => ::Array<SoberSwag::Nodes::Base>}]
|
43
|
+
# a hash with the elements in the `:elements` key.
|
25
44
|
def deconstruct_keys(_keys)
|
26
45
|
{ elements: @elements }
|
27
46
|
end
|
@@ -1,8 +1,16 @@
|
|
1
1
|
module SoberSwag
|
2
2
|
module Nodes
|
3
3
|
##
|
4
|
-
#
|
4
|
+
# This is a node for one attribute of an object.
|
5
|
+
# An object type is represented by a `SoberSwag::Nodes::Object` full of these keys.
|
6
|
+
#
|
7
|
+
#
|
5
8
|
class Attribute < Base
|
9
|
+
##
|
10
|
+
# @param key [Symbol] the key of this attribute
|
11
|
+
# @param required [Boolean] if this attribute must be set or not
|
12
|
+
# @param value [Class] the type of this attribute
|
13
|
+
# @param meta [Hash] the metadata associated with this attribute
|
6
14
|
def initialize(key, required, value, meta = {})
|
7
15
|
@key = key
|
8
16
|
@required = required
|
@@ -10,22 +18,55 @@ module SoberSwag
|
|
10
18
|
@meta = meta
|
11
19
|
end
|
12
20
|
|
21
|
+
##
|
22
|
+
# Deconstruct into the {#key}, {#required}, {#value}, and {#meta} attributes
|
23
|
+
# of this {Attribute} object.
|
24
|
+
#
|
25
|
+
# @return [Array(Symbol, Boolean, Class, Hash)] the attributes of this object
|
13
26
|
def deconstruct
|
14
27
|
[key, required, value, meta]
|
15
28
|
end
|
16
29
|
|
17
|
-
|
30
|
+
##
|
31
|
+
# Deconstructs into {#key}, {#required}, {#value}, and {#meta} attributes, as a
|
32
|
+
# hash with the attribute names as the keys.
|
33
|
+
#
|
34
|
+
# @param _keys [void] ignored
|
35
|
+
# @return [Hash] the attributes as keys.
|
36
|
+
def deconstruct_keys(_keys)
|
18
37
|
{ key: key, required: required, value: value, meta: meta }
|
19
38
|
end
|
20
39
|
|
21
|
-
|
40
|
+
##
|
41
|
+
# @return [Symbol]
|
42
|
+
attr_reader :key
|
22
43
|
|
44
|
+
##
|
45
|
+
# @return [Boolean] true if this attribute must be set, false otherwise.
|
46
|
+
attr_reader :required
|
47
|
+
|
48
|
+
##
|
49
|
+
# @return [Class] the type of this attribute
|
50
|
+
attr_reader :value
|
51
|
+
|
52
|
+
##
|
53
|
+
# @return [Hash] the metadata for this attribute.
|
54
|
+
attr_reader :meta
|
55
|
+
|
56
|
+
##
|
57
|
+
# @see SoberSwag::Nodes::Base#map
|
23
58
|
def map(&block)
|
24
59
|
self.class.new(key, required, value.map(&block), meta)
|
25
60
|
end
|
26
61
|
|
62
|
+
##
|
63
|
+
# @see SoberSwag::Nodes::Base#cata
|
27
64
|
def cata(&block)
|
28
|
-
block.call(
|
65
|
+
block.call(
|
66
|
+
self.class.new(
|
67
|
+
key, required, value.cata(&block), meta
|
68
|
+
)
|
69
|
+
)
|
29
70
|
end
|
30
71
|
end
|
31
72
|
end
|
@@ -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)
|