sober_swag 0.1.0 → 0.6.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/config/rubocop_linter_action.yml +4 -0
- data/.github/workflows/lint.yml +15 -0
- data/.github/workflows/ruby.yml +33 -2
- data/.gitignore +4 -0
- data/.rubocop.yml +75 -1
- data/.ruby-version +1 -1
- data/README.md +154 -1
- data/bin/console +16 -15
- data/docs/serializers.md +203 -0
- data/example/.rspec +1 -0
- data/example/.ruby-version +1 -1
- data/example/Gemfile +9 -7
- data/example/Gemfile.lock +96 -79
- data/example/app/controllers/people_controller.rb +41 -23
- data/example/app/controllers/posts_controller.rb +110 -0
- data/example/app/models/application_record.rb +3 -0
- data/example/app/models/person.rb +6 -0
- data/example/app/models/post.rb +9 -0
- data/example/app/output_objects/person_errors_output_object.rb +5 -0
- data/example/app/output_objects/person_output_object.rb +15 -0
- data/example/app/output_objects/post_output_object.rb +10 -0
- data/example/bin/bundle +24 -20
- data/example/bin/rails +1 -1
- data/example/bin/rake +1 -1
- data/example/config/application.rb +11 -7
- data/example/config/environments/development.rb +0 -1
- data/example/config/environments/production.rb +3 -3
- data/example/config/puma.rb +5 -5
- data/example/config/routes.rb +3 -0
- data/example/config/spring.rb +4 -4
- data/example/db/migrate/20200311152021_create_people.rb +0 -1
- data/example/db/migrate/20200603172347_create_posts.rb +11 -0
- data/example/db/schema.rb +16 -7
- data/example/spec/rails_helper.rb +64 -0
- data/example/spec/requests/people/create_spec.rb +52 -0
- data/example/spec/requests/people/get_spec.rb +35 -0
- data/example/spec/requests/people/index_spec.rb +69 -0
- data/example/spec/spec_helper.rb +94 -0
- data/lib/sober_swag.rb +6 -3
- data/lib/sober_swag/compiler/error.rb +2 -0
- data/lib/sober_swag/compiler/path.rb +2 -5
- data/lib/sober_swag/compiler/paths.rb +0 -1
- data/lib/sober_swag/compiler/type.rb +86 -56
- data/lib/sober_swag/controller.rb +16 -11
- data/lib/sober_swag/controller/route.rb +18 -21
- data/lib/sober_swag/controller/undefined_body_error.rb +3 -0
- data/lib/sober_swag/controller/undefined_path_error.rb +3 -0
- data/lib/sober_swag/controller/undefined_query_error.rb +3 -0
- data/lib/sober_swag/input_object.rb +28 -0
- data/lib/sober_swag/nodes/array.rb +1 -1
- data/lib/sober_swag/nodes/base.rb +5 -3
- data/lib/sober_swag/nodes/binary.rb +2 -1
- data/lib/sober_swag/nodes/enum.rb +4 -2
- data/lib/sober_swag/nodes/list.rb +0 -1
- data/lib/sober_swag/nodes/primitive.rb +6 -5
- data/lib/sober_swag/output_object.rb +102 -0
- data/lib/sober_swag/output_object/definition.rb +30 -0
- data/lib/sober_swag/{blueprint → output_object}/field.rb +14 -4
- data/lib/sober_swag/{blueprint → output_object}/field_syntax.rb +2 -2
- data/lib/sober_swag/{blueprint → output_object}/view.rb +15 -6
- data/lib/sober_swag/parser.rb +9 -4
- data/lib/sober_swag/serializer.rb +5 -2
- data/lib/sober_swag/serializer/array.rb +12 -0
- data/lib/sober_swag/serializer/base.rb +50 -1
- data/lib/sober_swag/serializer/conditional.rb +19 -2
- data/lib/sober_swag/serializer/field_list.rb +29 -6
- data/lib/sober_swag/serializer/mapped.rb +15 -3
- data/lib/sober_swag/serializer/meta.rb +35 -0
- data/lib/sober_swag/serializer/optional.rb +17 -2
- data/lib/sober_swag/serializer/primitive.rb +4 -1
- data/lib/sober_swag/server.rb +83 -0
- data/lib/sober_swag/types.rb +3 -0
- data/lib/sober_swag/version.rb +1 -1
- data/sober_swag.gemspec +8 -4
- metadata +79 -47
- data/Gemfile.lock +0 -92
- data/example/person.json +0 -4
- data/example/test/controllers/.keep +0 -0
- data/example/test/fixtures/.keep +0 -0
- data/example/test/fixtures/files/.keep +0 -0
- data/example/test/fixtures/people.yml +0 -11
- data/example/test/integration/.keep +0 -0
- data/example/test/models/.keep +0 -0
- data/example/test/models/person_test.rb +0 -7
- data/example/test/test_helper.rb +0 -13
- data/lib/sober_swag/blueprint.rb +0 -113
- data/lib/sober_swag/path.rb +0 -8
- data/lib/sober_swag/path/integer.rb +0 -21
- data/lib/sober_swag/path/lit.rb +0 -41
- data/lib/sober_swag/path/literal.rb +0 -29
- data/lib/sober_swag/path/param.rb +0 -33
@@ -10,10 +10,19 @@ module SoberSwag
|
|
10
10
|
autoload :UndefinedPathError, 'sober_swag/controller/undefined_path_error'
|
11
11
|
autoload :UndefinedQueryError, 'sober_swag/controller/undefined_query_error'
|
12
12
|
|
13
|
+
##
|
14
|
+
# Types module, so you can more easily access Types::Whatever
|
15
|
+
# without having to type SoberSwag::Types::Whatever.
|
13
16
|
module Types
|
14
17
|
include ::Dry::Types()
|
15
18
|
end
|
16
19
|
|
20
|
+
included do
|
21
|
+
rescue_from Dry::Struct::Error do
|
22
|
+
head :bad_request
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
17
26
|
class_methods do
|
18
27
|
##
|
19
28
|
# Define a new action with the given HTTP method, action name, and path.
|
@@ -65,10 +74,7 @@ module SoberSwag
|
|
65
74
|
res = defined_routes.reduce(SoberSwag::Compiler.new) { |c, r| c.add_route(r) }
|
66
75
|
{
|
67
76
|
openapi: '3.0.0',
|
68
|
-
info: {
|
69
|
-
version: '1',
|
70
|
-
title: self.name
|
71
|
-
}
|
77
|
+
info: { version: '1', title: name }
|
72
78
|
}.merge(res.to_swagger)
|
73
79
|
end
|
74
80
|
end
|
@@ -89,6 +95,7 @@ module SoberSwag
|
|
89
95
|
begin
|
90
96
|
r = current_action_def
|
91
97
|
raise UndefinedPathError unless r&.path_params_class
|
98
|
+
|
92
99
|
r.path_params_class.new(request.path_parameters)
|
93
100
|
end
|
94
101
|
end
|
@@ -102,6 +109,7 @@ module SoberSwag
|
|
102
109
|
begin
|
103
110
|
r = current_action_def
|
104
111
|
raise UndefinedBodyError unless r&.request_body_class
|
112
|
+
|
105
113
|
r.request_body_class.new(body_params)
|
106
114
|
end
|
107
115
|
end
|
@@ -115,6 +123,7 @@ module SoberSwag
|
|
115
123
|
begin
|
116
124
|
r = current_action_def
|
117
125
|
raise UndefinedQueryError unless r&.query_params_class
|
126
|
+
|
118
127
|
r.query_params_class.new(request.query_parameters)
|
119
128
|
end
|
120
129
|
end
|
@@ -124,11 +133,11 @@ module SoberSwag
|
|
124
133
|
# @todo figure out how to specify views and other options for the serializer here
|
125
134
|
# @param status [Symbol] the HTTP status symbol to use for the status code
|
126
135
|
# @param entity the thing to serialize
|
127
|
-
def respond!(status, entity)
|
136
|
+
def respond!(status, entity, serializer_opts: {}, rails_opts: {})
|
128
137
|
r = current_action_def
|
129
138
|
serializer = r.response_serializers[Rack::Utils.status_code(status)]
|
130
139
|
serializer ||= serializer.new if serializer.respond_to?(:new)
|
131
|
-
render json: serializer.serialize(entity)
|
140
|
+
render json: serializer.serialize(entity, serializer_opts), status: status, **rails_opts
|
132
141
|
end
|
133
142
|
|
134
143
|
##
|
@@ -138,10 +147,7 @@ module SoberSwag
|
|
138
147
|
# but it keeps the docs honest: parameters sent in the body *must* be
|
139
148
|
# in the body.
|
140
149
|
def body_params
|
141
|
-
|
142
|
-
request.query_parameters.key?(k) || request.path_parameters.key?(k)
|
143
|
-
end
|
144
|
-
bparams.permit(bparams.keys)
|
150
|
+
request.request_parameters
|
145
151
|
end
|
146
152
|
|
147
153
|
##
|
@@ -150,7 +156,6 @@ module SoberSwag
|
|
150
156
|
def current_action_def
|
151
157
|
self.class.find_route(params[:action])
|
152
158
|
end
|
153
|
-
|
154
159
|
end
|
155
160
|
end
|
156
161
|
|
@@ -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
|
-
|
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 {
|
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 =
|
37
|
-
@request_body_class =
|
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 =
|
52
|
-
@query_params_class =
|
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 {
|
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 =
|
67
|
-
@path_params_class =
|
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::
|
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::
|
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
|
132
|
-
Class.new(base, &block).tap
|
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
|
@@ -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
|
@@ -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,17 @@ 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
|
40
|
+
def cata
|
42
41
|
raise ArgumentError, 'Base is abstract'
|
43
42
|
end
|
44
43
|
|
45
|
-
def map
|
44
|
+
def map
|
46
45
|
raise ArgumentError, 'Base is abstract'
|
47
46
|
end
|
48
47
|
|
48
|
+
def flatten_one_ofs
|
49
|
+
raise ArgumentError, 'Base is abstract'
|
50
|
+
end
|
49
51
|
end
|
50
52
|
end
|
51
53
|
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(
|
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(
|
21
|
+
def deconstruct_keys(_keys)
|
20
22
|
{ values: values }
|
21
23
|
end
|
22
24
|
|
@@ -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
|