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.
Files changed (55) hide show
  1. checksums.yaml +4 -4
  2. data/.github/dependabot.yml +15 -0
  3. data/.github/workflows/lint.yml +4 -9
  4. data/.github/workflows/ruby.yml +2 -6
  5. data/.gitignore +4 -0
  6. data/.rubocop.yml +50 -5
  7. data/.yardopts +7 -0
  8. data/CHANGELOG.md +29 -1
  9. data/Gemfile +8 -0
  10. data/README.md +155 -4
  11. data/bin/rspec +29 -0
  12. data/docs/serializers.md +18 -13
  13. data/example/Gemfile +2 -2
  14. data/example/app/controllers/people_controller.rb +4 -0
  15. data/example/app/controllers/posts_controller.rb +5 -0
  16. data/example/config/environments/production.rb +1 -1
  17. data/lib/sober_swag.rb +6 -1
  18. data/lib/sober_swag/compiler.rb +29 -3
  19. data/lib/sober_swag/compiler/path.rb +49 -3
  20. data/lib/sober_swag/compiler/paths.rb +20 -0
  21. data/lib/sober_swag/compiler/primitive.rb +20 -1
  22. data/lib/sober_swag/compiler/type.rb +105 -22
  23. data/lib/sober_swag/controller.rb +42 -15
  24. data/lib/sober_swag/controller/route.rb +133 -28
  25. data/lib/sober_swag/input_object.rb +117 -7
  26. data/lib/sober_swag/nodes/array.rb +19 -0
  27. data/lib/sober_swag/nodes/attribute.rb +45 -4
  28. data/lib/sober_swag/nodes/base.rb +27 -7
  29. data/lib/sober_swag/nodes/binary.rb +30 -13
  30. data/lib/sober_swag/nodes/enum.rb +16 -1
  31. data/lib/sober_swag/nodes/list.rb +20 -0
  32. data/lib/sober_swag/nodes/nullable_primitive.rb +3 -0
  33. data/lib/sober_swag/nodes/object.rb +4 -1
  34. data/lib/sober_swag/nodes/one_of.rb +11 -3
  35. data/lib/sober_swag/nodes/primitive.rb +34 -2
  36. data/lib/sober_swag/nodes/sum.rb +8 -0
  37. data/lib/sober_swag/output_object.rb +35 -4
  38. data/lib/sober_swag/output_object/definition.rb +31 -1
  39. data/lib/sober_swag/output_object/field.rb +31 -11
  40. data/lib/sober_swag/output_object/field_syntax.rb +19 -3
  41. data/lib/sober_swag/output_object/view.rb +46 -1
  42. data/lib/sober_swag/parser.rb +7 -1
  43. data/lib/sober_swag/serializer/array.rb +27 -3
  44. data/lib/sober_swag/serializer/base.rb +75 -25
  45. data/lib/sober_swag/serializer/conditional.rb +33 -1
  46. data/lib/sober_swag/serializer/field_list.rb +18 -2
  47. data/lib/sober_swag/serializer/mapped.rb +10 -1
  48. data/lib/sober_swag/serializer/optional.rb +18 -1
  49. data/lib/sober_swag/serializer/primitive.rb +3 -0
  50. data/lib/sober_swag/server.rb +27 -11
  51. data/lib/sober_swag/type/named.rb +14 -0
  52. data/lib/sober_swag/types/comma_array.rb +4 -0
  53. data/lib/sober_swag/version.rb +1 -1
  54. data/sober_swag.gemspec +2 -2
  55. 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
- # - #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)
@@ -1,5 +1,8 @@
1
1
  module SoberSwag
2
2
  module Nodes
3
+ ##
4
+ # Exactly like a {SoberSwag::Nodes::Primitive} node, except it can be null.
5
+ # @todo: make this a boolean parameter of {SoberSwag::Nodes::Primitive}
3
6
  class NullablePrimitive < Primitive
4
7
  end
5
8
  end
@@ -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
- # Swagges uses an array of OneOf types, so we
5
- # transform our sum nodes into this
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: @elemenets }
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
- attr_reader :value, :metadata
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
@@ -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::Serialzier::Base}.
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, primtive(:String).optional
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 {#define}
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
- attr_reader :fields, :views, :identifier
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)