sober_swag 0.1.0 → 0.2.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 (92) hide show
  1. checksums.yaml +4 -4
  2. data/.github/config/rubocop_linter_action.yml +5 -0
  3. data/.github/workflows/lint.yml +15 -0
  4. data/.github/workflows/ruby.yml +23 -1
  5. data/.gitignore +3 -0
  6. data/.rubocop.yml +73 -1
  7. data/.ruby-version +1 -1
  8. data/Gemfile.lock +29 -5
  9. data/README.md +109 -0
  10. data/bin/console +15 -14
  11. data/docs/serializers.md +203 -0
  12. data/example/.rspec +1 -0
  13. data/example/.ruby-version +1 -1
  14. data/example/Gemfile +10 -6
  15. data/example/Gemfile.lock +96 -76
  16. data/example/app/controllers/people_controller.rb +37 -21
  17. data/example/app/controllers/posts_controller.rb +102 -0
  18. data/example/app/models/application_record.rb +3 -0
  19. data/example/app/models/person.rb +6 -0
  20. data/example/app/models/post.rb +9 -0
  21. data/example/app/output_objects/person_errors_output_object.rb +5 -0
  22. data/example/app/output_objects/person_output_object.rb +15 -0
  23. data/example/app/output_objects/post_output_object.rb +10 -0
  24. data/example/bin/bundle +24 -20
  25. data/example/bin/rails +1 -1
  26. data/example/bin/rake +1 -1
  27. data/example/config/application.rb +11 -7
  28. data/example/config/environments/development.rb +0 -1
  29. data/example/config/environments/production.rb +3 -3
  30. data/example/config/puma.rb +5 -5
  31. data/example/config/routes.rb +3 -0
  32. data/example/config/spring.rb +4 -4
  33. data/example/db/migrate/20200311152021_create_people.rb +0 -1
  34. data/example/db/migrate/20200603172347_create_posts.rb +11 -0
  35. data/example/db/schema.rb +16 -7
  36. data/example/spec/rails_helper.rb +64 -0
  37. data/example/spec/requests/people/create_spec.rb +52 -0
  38. data/example/spec/requests/people/get_spec.rb +35 -0
  39. data/example/spec/requests/people/index_spec.rb +69 -0
  40. data/example/spec/spec_helper.rb +94 -0
  41. data/lib/sober_swag.rb +6 -3
  42. data/lib/sober_swag/compiler/error.rb +2 -0
  43. data/lib/sober_swag/compiler/path.rb +2 -5
  44. data/lib/sober_swag/compiler/paths.rb +0 -1
  45. data/lib/sober_swag/compiler/type.rb +28 -15
  46. data/lib/sober_swag/controller.rb +16 -11
  47. data/lib/sober_swag/controller/route.rb +18 -21
  48. data/lib/sober_swag/controller/undefined_body_error.rb +3 -0
  49. data/lib/sober_swag/controller/undefined_path_error.rb +3 -0
  50. data/lib/sober_swag/controller/undefined_query_error.rb +3 -0
  51. data/lib/sober_swag/input_object.rb +28 -0
  52. data/lib/sober_swag/nodes/array.rb +1 -1
  53. data/lib/sober_swag/nodes/base.rb +2 -4
  54. data/lib/sober_swag/nodes/binary.rb +2 -1
  55. data/lib/sober_swag/nodes/enum.rb +4 -2
  56. data/lib/sober_swag/nodes/list.rb +0 -1
  57. data/lib/sober_swag/nodes/primitive.rb +6 -5
  58. data/lib/sober_swag/output_object.rb +102 -0
  59. data/lib/sober_swag/output_object/definition.rb +30 -0
  60. data/lib/sober_swag/{blueprint → output_object}/field.rb +14 -4
  61. data/lib/sober_swag/{blueprint → output_object}/field_syntax.rb +1 -1
  62. data/lib/sober_swag/{blueprint → output_object}/view.rb +15 -6
  63. data/lib/sober_swag/parser.rb +5 -3
  64. data/lib/sober_swag/serializer.rb +5 -2
  65. data/lib/sober_swag/serializer/array.rb +12 -0
  66. data/lib/sober_swag/serializer/base.rb +50 -1
  67. data/lib/sober_swag/serializer/conditional.rb +15 -2
  68. data/lib/sober_swag/serializer/field_list.rb +29 -6
  69. data/lib/sober_swag/serializer/mapped.rb +12 -2
  70. data/lib/sober_swag/serializer/meta.rb +35 -0
  71. data/lib/sober_swag/serializer/optional.rb +17 -2
  72. data/lib/sober_swag/serializer/primitive.rb +4 -1
  73. data/lib/sober_swag/server.rb +83 -0
  74. data/lib/sober_swag/types.rb +3 -0
  75. data/lib/sober_swag/version.rb +1 -1
  76. data/sober_swag.gemspec +6 -4
  77. metadata +77 -44
  78. data/example/person.json +0 -4
  79. data/example/test/controllers/.keep +0 -0
  80. data/example/test/fixtures/.keep +0 -0
  81. data/example/test/fixtures/files/.keep +0 -0
  82. data/example/test/fixtures/people.yml +0 -11
  83. data/example/test/integration/.keep +0 -0
  84. data/example/test/models/.keep +0 -0
  85. data/example/test/models/person_test.rb +0 -7
  86. data/example/test/test_helper.rb +0 -13
  87. data/lib/sober_swag/blueprint.rb +0 -113
  88. data/lib/sober_swag/path.rb +0 -8
  89. data/lib/sober_swag/path/integer.rb +0 -21
  90. data/lib/sober_swag/path/lit.rb +0 -41
  91. data/lib/sober_swag/path/literal.rb +0 -29
  92. data/lib/sober_swag/path/param.rb +0 -33
@@ -1,9 +1,8 @@
1
- require 'sober_swag/blueprint'
2
-
3
1
  module SoberSwag
4
2
  module Controller
3
+ ##
4
+ # Describe a single controller endpoint.
5
5
  class Route
6
-
7
6
  def initialize(method, action_name, path)
8
7
  @method = method
9
8
  @path = path
@@ -12,12 +11,8 @@ module SoberSwag
12
11
  @response_descriptions = {}
13
12
  end
14
13
 
15
- attr_reader :response_serializers
16
- attr_reader :response_descriptions
17
- attr_reader :controller
18
- attr_reader :method
19
- attr_reader :path
20
- attr_reader :action_name
14
+ attr_reader :response_serializers, :response_descriptions, :controller, :method, :path, :action_name
15
+
21
16
  ##
22
17
  # What to parse the request body in to.
23
18
  attr_reader :request_body_class
@@ -31,10 +26,10 @@ module SoberSwag
31
26
 
32
27
  ##
33
28
  # Define the request body, using SoberSwag's type-definition scheme.
34
- # The block passed will be used to define the body of a new sublcass of `base` (defaulted to {Dry::Struct}.)
29
+ # The block passed will be used to define the body of a new sublcass of `base` (defaulted to {SoberSwag::InputObject}.)
35
30
  # If you want, you can also define utility methods in here
36
- def request_body(base = Dry::Struct, &block)
37
- @request_body_class = make_struct!(base, &block)
31
+ def request_body(base = SoberSwag::InputObject, &block)
32
+ @request_body_class = make_input_object!(base, &block)
38
33
  action_module.const_set('ResponseBody', @request_body_class)
39
34
  end
40
35
 
@@ -48,8 +43,8 @@ module SoberSwag
48
43
  # Define the shape of the query_params parameters, using SoberSwag's type-definition scheme.
49
44
  # The block passed is the body of the newly-defined type.
50
45
  # You can also include a base type.
51
- def query_params(base = Dry::Struct, &block)
52
- @query_params_class = make_struct!(base, &block)
46
+ def query_params(base = SoberSwag::InputObject, &block)
47
+ @query_params_class = make_input_object!(base, &block)
53
48
  action_module.const_set('QueryParams', @query_params_class)
54
49
  end
55
50
 
@@ -61,10 +56,10 @@ module SoberSwag
61
56
 
62
57
  ##
63
58
  # Define the shape of the *path* parameters, using SoberSwag's type-definition scheme.
64
- # The block passed will be the body of a new subclass of `base` (defaulted to {Dry::Struct}).
59
+ # The block passed will be the body of a new subclass of `base` (defaulted to {SoberSwag::InputObject}).
65
60
  # Names of this should match the names in the path template originally passed to {SoberSwag::Controller::Route.new}
66
- def path_params(base = Dry::Struct, &block)
67
- @path_params_class = make_struct!(base, &block)
61
+ def path_params(base = SoberSwag::InputObject, &block)
62
+ @path_params_class = make_input_object!(base, &block)
68
63
  action_module.const_set('PathParams', @path_params_class)
69
64
  end
70
65
 
@@ -104,13 +99,13 @@ module SoberSwag
104
99
  ##
105
100
  # Define a serializer for a response with the given status code.
106
101
  # You may either give a serializer you defined elsewhere, or define one inline as if passed to
107
- # {SoberSwag::Blueprint.define}
102
+ # {SoberSwag::OutputObject.define}
108
103
  def response(status_code, description, serializer = nil, &block)
109
104
  status_key = Rack::Utils.status_code(status_code)
110
105
 
111
106
  raise ArgumentError, 'Response defiend!' if @response_serializers.key?(status_key)
112
107
 
113
- serializer ||= SoberSwag::Blueprint.define(&block)
108
+ serializer ||= SoberSwag::OutputObject.define(&block)
114
109
  response_module.const_set(status_code.to_s.classify, serializer)
115
110
  @response_serializers[status_key] = serializer
116
111
  @response_descriptions[status_key] = description
@@ -128,8 +123,10 @@ module SoberSwag
128
123
  @response_module ||= Module.new.tap { |m| action_module.const_set(:Response, m) }
129
124
  end
130
125
 
131
- def make_struct!(base, &block)
132
- Class.new(base, &block).tap { |e| e.transform_keys(&:to_sym) if base == Dry::Struct }
126
+ def make_input_object!(base, &block)
127
+ Class.new(base, &block).tap do |e|
128
+ e.transform_keys(&:to_sym) if [SoberSwag::InputObject, Dry::Struct].include?(base)
129
+ end
133
130
  end
134
131
  end
135
132
  end
@@ -1,5 +1,8 @@
1
1
  module SoberSwag
2
2
  module Controller
3
+ ##
4
+ # Error class thrown if you have no body defined,
5
+ # but try to call `parsed_body`.
3
6
  class UndefinedBodyError < Error
4
7
  end
5
8
  end
@@ -1,5 +1,8 @@
1
1
  module SoberSwag
2
2
  module Controller
3
+ ##
4
+ # Error class thrown if you have no path defined,
5
+ # but try to call `parse_path`.
3
6
  class UndefinedPathError < Error
4
7
  end
5
8
  end
@@ -1,5 +1,8 @@
1
1
  module SoberSwag
2
2
  module Controller
3
+ ##
4
+ # Error class thrown if you have no query defined,
5
+ # but try to call `parsed_query`.
3
6
  class UndefinedQueryError < Error
4
7
  end
5
8
  end
@@ -0,0 +1,28 @@
1
+ module SoberSwag
2
+ ##
3
+ # A variant of Dry::Struct that allows you to set a "model name" that is publically visible.
4
+ # If you do not set one, it will be the Ruby class name, with any '::' replaced with a '.'.
5
+ #
6
+ # This otherwise behaves exactly like a Dry::Struct.
7
+ # Please see the documentation for that class to see how it works.
8
+ class InputObject < Dry::Struct
9
+ transform_keys(&:to_sym)
10
+
11
+ class << self
12
+ ##
13
+ # The name to use for this type in external documentation.
14
+ def identifier(arg = nil)
15
+ @identifier = arg if arg
16
+ @identifier || name.to_s.gsub('::', '.')
17
+ end
18
+
19
+ def primitive(sym)
20
+ SoberSwag::Types.const_get(sym)
21
+ end
22
+
23
+ def param(sym)
24
+ SoberSwag::Types::Params.const_get(sym)
25
+ end
26
+ end
27
+ end
28
+ end
@@ -22,7 +22,7 @@ module SoberSwag
22
22
  @elements
23
23
  end
24
24
 
25
- def deconstruct_keys(keys)
25
+ def deconstruct_keys(_keys)
26
26
  { elements: @elements }
27
27
  end
28
28
  end
@@ -9,7 +9,6 @@ module SoberSwag
9
9
  # - #deconstruct_keys, which returns a hash of *everything needed to identify the node*.
10
10
  # We use this later.
11
11
  class Base
12
-
13
12
  include Comparable
14
13
 
15
14
  ##
@@ -38,14 +37,13 @@ module SoberSwag
38
37
  #
39
38
  # When working with these definition nodes, we very often want to transform something recursively.
40
39
  # This method allows us to do so by focusing on a single level at a time, keeping the actual recursion *abstract*.
41
- def cata(&block)
40
+ def cata
42
41
  raise ArgumentError, 'Base is abstract'
43
42
  end
44
43
 
45
- def map(&block)
44
+ def map
46
45
  raise ArgumentError, 'Base is abstract'
47
46
  end
48
-
49
47
  end
50
48
  end
51
49
  end
@@ -11,6 +11,7 @@ module SoberSwag
11
11
  end
12
12
 
13
13
  attr_reader :lhs, :rhs
14
+
14
15
  ##
15
16
  # Map the root values of the node.
16
17
  # This just calls map on the lhs and the rhs
@@ -25,7 +26,7 @@ module SoberSwag
25
26
  [lhs, rhs]
26
27
  end
27
28
 
28
- def deconstruct_keys(keys)
29
+ def deconstruct_keys(_keys)
29
30
  { lhs: lhs, rhs: rhs }
30
31
  end
31
32
 
@@ -1,7 +1,9 @@
1
1
  module SoberSwag
2
2
  module Nodes
3
+ ##
4
+ # Compiler node to represent an enum value.
5
+ # Enums are special enough to have their own node.
3
6
  class Enum < Base
4
-
5
7
  def initialize(values)
6
8
  @values = values
7
9
  end
@@ -16,7 +18,7 @@ module SoberSwag
16
18
  [values]
17
19
  end
18
20
 
19
- def deconstruct_keys(keys)
21
+ def deconstruct_keys(_keys)
20
22
  { values: values }
21
23
  end
22
24
 
@@ -34,7 +34,6 @@ module SoberSwag
34
34
  element.map(&block)
35
35
  )
36
36
  end
37
-
38
37
  end
39
38
  end
40
39
  end
@@ -3,26 +3,27 @@ module SoberSwag
3
3
  ##
4
4
  # Root node of the tree
5
5
  class Primitive < Base
6
- def initialize(value)
6
+ def initialize(value, metadata = {})
7
7
  @value = value
8
+ @metadata = metadata
8
9
  end
9
10
 
10
- attr_reader :value
11
+ attr_reader :value, :metadata
11
12
 
12
13
  def map(&block)
13
14
  self.class.new(block.call(value))
14
15
  end
15
16
 
16
17
  def deconstruct
17
- [value]
18
+ [value, metadata]
18
19
  end
19
20
 
20
21
  def deconstruct_keys(_)
21
- { value: value }
22
+ { value: value, metadata: metadata }
22
23
  end
23
24
 
24
25
  def cata(&block)
25
- block.call(self.class.new(value))
26
+ block.call(self.class.new(value, metadata))
26
27
  end
27
28
  end
28
29
  end
@@ -0,0 +1,102 @@
1
+ require 'sober_swag/serializer'
2
+
3
+ module SoberSwag
4
+ ##
5
+ # Create a serializer that is heavily inspired by the "Blueprinter" library.
6
+ # This allows you to make "views" and such inside.
7
+ #
8
+ # Under the hood, this is actually all based on {SoberSwag::Serialzier::Base}.
9
+ class OutputObject < SoberSwag::Serializer::Base
10
+ autoload(:Field, 'sober_swag/output_object/field')
11
+ autoload(:Definition, 'sober_swag/output_object/definition')
12
+ autoload(:FieldSyntax, 'sober_swag/output_object/field_syntax')
13
+ autoload(:View, 'sober_swag/output_object/view')
14
+
15
+ ##
16
+ # Use a OutputObject to define a new serializer.
17
+ # It will be based on {SoberSwag::Serializer::Base}.
18
+ #
19
+ # An example is illustrative:
20
+ #
21
+ # PersonSerializer = SoberSwag::OutputObject.define do
22
+ # field :id, primitive(:Integer)
23
+ # field :name, primtive(:String).optional
24
+ #
25
+ # view :complex do
26
+ # field :age, primitive(:Integer)
27
+ # field :title, primitive(:String)
28
+ # end
29
+ # end
30
+ #
31
+ # Note: This currently will generate a new *class* that does serialization.
32
+ # However, this is only a hack to get rid of the weird naming issue when
33
+ # generating swagger from dry structs: their section of the schema area
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}
36
+ # can simply return an *instance* of SoberSwag::Serializer that does
37
+ # the correct thing, with the name you give it. This works for now, though.
38
+ def self.define(&block)
39
+ d = Definition.new.tap do |o|
40
+ o.instance_eval(&block)
41
+ end
42
+ new(d.fields, d.views, d.identifier)
43
+ end
44
+
45
+ def initialize(fields, views, identifier)
46
+ @fields = fields
47
+ @views = views
48
+ @identifier = identifier
49
+ end
50
+
51
+ attr_reader :fields, :views, :identifier
52
+
53
+ def serialize(obj, opts = {})
54
+ serializer.serialize(obj, opts)
55
+ end
56
+
57
+ def type
58
+ serializer.type
59
+ end
60
+
61
+ def view(name)
62
+ return base_serializer if name == :base
63
+
64
+ @views.find { |v| v.name == name }
65
+ end
66
+
67
+ def base
68
+ base_serializer
69
+ end
70
+
71
+ ##
72
+ # Compile down this to an appropriate serializer.
73
+ # It uses {SoberSwag::Serializer::Conditional} to do view-parsing,
74
+ # and {SoberSwag::Serializer::FieldList} to do the actual serialization.
75
+ def serializer # rubocop:disable Metrics/MethodLength
76
+ @serializer ||=
77
+ begin
78
+ views.reduce(base_serializer) do |base, view|
79
+ view_serializer = view.serializer
80
+ view_serializer.identifier("#{identifier}.#{view.name.to_s.classify}") if identifier
81
+ SoberSwag::Serializer::Conditional.new(
82
+ proc do |object, options|
83
+ if options[:view].to_s == view.name.to_s
84
+ [:left, object]
85
+ else
86
+ [:right, object]
87
+ end
88
+ end,
89
+ view_serializer,
90
+ base
91
+ )
92
+ end
93
+ end
94
+ end
95
+
96
+ def base_serializer
97
+ @base_serializer ||= SoberSwag::Serializer::FieldList.new(fields).tap do |s|
98
+ s.identifier(identifier)
99
+ end
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,30 @@
1
+ module SoberSwag
2
+ class OutputObject
3
+ ##
4
+ # Container to define a single output object.
5
+ # This is the DSL used in the base of {SoberSwag::OutputObject.define}.
6
+ class Definition
7
+ def initialize
8
+ @fields = []
9
+ @views = []
10
+ end
11
+
12
+ attr_reader :fields, :views
13
+
14
+ include FieldSyntax
15
+
16
+ def add_field!(field)
17
+ @fields << field
18
+ end
19
+
20
+ def view(name, &block)
21
+ @views << View.define(name, fields, &block)
22
+ end
23
+
24
+ def identifier(arg = nil)
25
+ @identifier = arg if arg
26
+ @identifier
27
+ end
28
+ end
29
+ end
30
+ end
@@ -1,5 +1,8 @@
1
1
  module SoberSwag
2
- class Blueprint
2
+ class OutputObject
3
+ ##
4
+ # A single field in an output object.
5
+ # Later used to make an actual serializer from this.
3
6
  class Field
4
7
  def initialize(name, serializer, from: nil, &block)
5
8
  @name = name
@@ -11,12 +14,20 @@ module SoberSwag
11
14
  attr_reader :name
12
15
 
13
16
  def serializer
14
- @serializer ||= @root_serializer.serializer.via_map(&transform_proc)
17
+ @serializer ||= resolved_serializer.serializer.via_map(&transform_proc)
18
+ end
19
+
20
+ def resolved_serializer
21
+ if @root_serializer.is_a?(Proc)
22
+ @root_serializer.call
23
+ else
24
+ @root_serializer
25
+ end
15
26
  end
16
27
 
17
28
  private
18
29
 
19
- def transform_proc
30
+ def transform_proc # rubocop:disable Metrics/MethodLength
20
31
  if @block
21
32
  @block
22
33
  else
@@ -30,7 +41,6 @@ module SoberSwag
30
41
  end
31
42
  end
32
43
  end
33
-
34
44
  end
35
45
  end
36
46
  end
@@ -1,5 +1,5 @@
1
1
  module SoberSwag
2
- class Blueprint
2
+ class OutputObject
3
3
  ##
4
4
  # Syntax for definitions that can add fields.
5
5
  module FieldSyntax