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.
Files changed (50) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/lint.yml +1 -1
  3. data/.github/workflows/ruby.yml +1 -1
  4. data/.gitignore +2 -0
  5. data/.rubocop.yml +5 -1
  6. data/.yardopts +7 -0
  7. data/CHANGELOG.md +5 -0
  8. data/Gemfile +8 -0
  9. data/README.md +1 -1
  10. data/docs/serializers.md +3 -0
  11. data/example/Gemfile +1 -1
  12. data/example/config/environments/production.rb +1 -1
  13. data/lib/sober_swag.rb +6 -1
  14. data/lib/sober_swag/compiler.rb +29 -3
  15. data/lib/sober_swag/compiler/path.rb +42 -3
  16. data/lib/sober_swag/compiler/paths.rb +20 -0
  17. data/lib/sober_swag/compiler/primitive.rb +17 -0
  18. data/lib/sober_swag/compiler/type.rb +105 -22
  19. data/lib/sober_swag/controller.rb +39 -12
  20. data/lib/sober_swag/controller/route.rb +103 -20
  21. data/lib/sober_swag/input_object.rb +90 -7
  22. data/lib/sober_swag/nodes/array.rb +19 -0
  23. data/lib/sober_swag/nodes/attribute.rb +45 -4
  24. data/lib/sober_swag/nodes/base.rb +27 -7
  25. data/lib/sober_swag/nodes/binary.rb +30 -13
  26. data/lib/sober_swag/nodes/enum.rb +16 -1
  27. data/lib/sober_swag/nodes/list.rb +20 -0
  28. data/lib/sober_swag/nodes/nullable_primitive.rb +3 -0
  29. data/lib/sober_swag/nodes/object.rb +4 -1
  30. data/lib/sober_swag/nodes/one_of.rb +11 -3
  31. data/lib/sober_swag/nodes/primitive.rb +34 -2
  32. data/lib/sober_swag/nodes/sum.rb +8 -0
  33. data/lib/sober_swag/output_object.rb +35 -4
  34. data/lib/sober_swag/output_object/definition.rb +31 -1
  35. data/lib/sober_swag/output_object/field.rb +31 -11
  36. data/lib/sober_swag/output_object/field_syntax.rb +19 -3
  37. data/lib/sober_swag/output_object/view.rb +46 -1
  38. data/lib/sober_swag/parser.rb +7 -1
  39. data/lib/sober_swag/serializer/array.rb +27 -3
  40. data/lib/sober_swag/serializer/base.rb +75 -25
  41. data/lib/sober_swag/serializer/conditional.rb +33 -1
  42. data/lib/sober_swag/serializer/field_list.rb +18 -2
  43. data/lib/sober_swag/serializer/mapped.rb +10 -1
  44. data/lib/sober_swag/serializer/optional.rb +18 -1
  45. data/lib/sober_swag/serializer/primitive.rb +3 -0
  46. data/lib/sober_swag/server.rb +5 -1
  47. data/lib/sober_swag/type/named.rb +14 -0
  48. data/lib/sober_swag/types/comma_array.rb +4 -0
  49. data/lib/sober_swag/version.rb +1 -1
  50. 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 publically visible.
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
- def identifier(arg = nil)
16
- @identifier = arg if arg
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
- # .primitive is already defined on Dry::Struct, so forward to the superclass if
46
- # not called as a way to get a primitive type
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
- def param(sym)
56
- SoberSwag::Types::Params.const_get(sym)
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
- # One attribute of an object.
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
- def deconstruct_keys
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
- attr_reader :key, :required, :value, :meta
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(self.class.new(key, required, value.cata(&block), meta))
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
- # - #deconstruct, which returns an array of *everything needed to idenitfy the node.*
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
- # - #deconstruct_keys, which returns a hash of *everything needed to identify the node*.
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.class == self.class
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
- attr_reader :lhs, :rhs
15
+ ##
16
+ # @return [SoberSwag::Nodes::Base] the left-hand node
17
+ attr_reader :lhs
14
18
 
15
19
  ##
16
- # Map the root values of the node.
17
- # This just calls map on the lhs and the rhs
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
- # Perform a catamorphism on this node.
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)