sober_swag 0.14.0 → 0.19.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/dependabot.yml +15 -0
- data/.github/workflows/lint.yml +41 -9
- data/.github/workflows/ruby.yml +1 -5
- data/.gitignore +2 -0
- data/.rubocop.yml +50 -5
- data/CHANGELOG.md +34 -0
- data/README.md +155 -4
- data/bin/console +36 -0
- data/bin/rspec +29 -0
- data/docs/serializers.md +74 -9
- data/example/Gemfile +2 -2
- data/example/app/controllers/application_controller.rb +5 -0
- data/example/app/controllers/people_controller.rb +4 -0
- data/example/app/controllers/posts_controller.rb +5 -0
- data/lib/sober_swag.rb +1 -0
- data/lib/sober_swag/compiler.rb +1 -0
- data/lib/sober_swag/compiler/path.rb +7 -0
- data/lib/sober_swag/compiler/primitive.rb +77 -0
- data/lib/sober_swag/compiler/type.rb +57 -96
- data/lib/sober_swag/controller.rb +3 -9
- data/lib/sober_swag/controller/route.rb +30 -8
- data/lib/sober_swag/input_object.rb +36 -3
- data/lib/sober_swag/nodes/attribute.rb +8 -7
- data/lib/sober_swag/nodes/enum.rb +2 -2
- data/lib/sober_swag/nodes/primitive.rb +1 -1
- data/lib/sober_swag/output_object/definition.rb +14 -2
- data/lib/sober_swag/output_object/field_syntax.rb +16 -0
- data/lib/sober_swag/parser.rb +10 -5
- data/lib/sober_swag/serializer/base.rb +2 -0
- data/lib/sober_swag/serializer/meta.rb +3 -1
- data/lib/sober_swag/server.rb +22 -10
- data/lib/sober_swag/type.rb +7 -0
- data/lib/sober_swag/type/named.rb +35 -0
- data/lib/sober_swag/types.rb +2 -0
- data/lib/sober_swag/types/comma_array.rb +17 -0
- data/lib/sober_swag/version.rb +1 -1
- data/sober_swag.gemspec +2 -2
- metadata +18 -10
@@ -17,12 +17,6 @@ module SoberSwag
|
|
17
17
|
include ::Dry::Types()
|
18
18
|
end
|
19
19
|
|
20
|
-
included do
|
21
|
-
rescue_from Dry::Struct::Error do
|
22
|
-
head :bad_request
|
23
|
-
end
|
24
|
-
end
|
25
|
-
|
26
20
|
class_methods do
|
27
21
|
##
|
28
22
|
# Define a new action with the given HTTP method, action name, and path.
|
@@ -96,7 +90,7 @@ module SoberSwag
|
|
96
90
|
r = current_action_def
|
97
91
|
raise UndefinedPathError unless r&.path_params_class
|
98
92
|
|
99
|
-
r.path_params_class.
|
93
|
+
r.path_params_class.call(request.path_parameters)
|
100
94
|
end
|
101
95
|
end
|
102
96
|
|
@@ -110,7 +104,7 @@ module SoberSwag
|
|
110
104
|
r = current_action_def
|
111
105
|
raise UndefinedBodyError unless r&.request_body_class
|
112
106
|
|
113
|
-
r.request_body_class.
|
107
|
+
r.request_body_class.call(body_params)
|
114
108
|
end
|
115
109
|
end
|
116
110
|
|
@@ -124,7 +118,7 @@ module SoberSwag
|
|
124
118
|
r = current_action_def
|
125
119
|
raise UndefinedQueryError unless r&.query_params_class
|
126
120
|
|
127
|
-
r.query_params_class.
|
121
|
+
r.query_params_class.call(request.query_parameters)
|
128
122
|
end
|
129
123
|
end
|
130
124
|
|
@@ -9,28 +9,36 @@ module SoberSwag
|
|
9
9
|
@action_name = action_name
|
10
10
|
@response_serializers = {}
|
11
11
|
@response_descriptions = {}
|
12
|
+
@tags = []
|
12
13
|
end
|
13
14
|
|
14
15
|
attr_reader :response_serializers, :response_descriptions, :controller, :method, :path, :action_name
|
15
16
|
|
16
17
|
##
|
17
|
-
# What to parse the request body
|
18
|
+
# What to parse the request body into.
|
18
19
|
attr_reader :request_body_class
|
19
20
|
##
|
20
|
-
# What to parse the request query_params
|
21
|
+
# What to parse the request query_params into.
|
21
22
|
attr_reader :query_params_class
|
22
|
-
|
23
23
|
##
|
24
|
-
# What to parse the path params into
|
24
|
+
# What to parse the path params into.
|
25
25
|
attr_reader :path_params_class
|
26
26
|
|
27
|
+
##
|
28
|
+
# Standard swagger tags.
|
29
|
+
def tags(*args)
|
30
|
+
return @tags if args.empty?
|
31
|
+
|
32
|
+
@tags = args.flatten
|
33
|
+
end
|
34
|
+
|
27
35
|
##
|
28
36
|
# Define the request body, using SoberSwag's type-definition scheme.
|
29
37
|
# The block passed will be used to define the body of a new sublcass of `base` (defaulted to {SoberSwag::InputObject}.)
|
30
38
|
# If you want, you can also define utility methods in here
|
31
39
|
def request_body(base = SoberSwag::InputObject, &block)
|
32
40
|
@request_body_class = make_input_object!(base, &block)
|
33
|
-
action_module.const_set('
|
41
|
+
action_module.const_set('RequestBody', @request_body_class)
|
34
42
|
end
|
35
43
|
|
36
44
|
##
|
@@ -103,7 +111,7 @@ module SoberSwag
|
|
103
111
|
def response(status_code, description, serializer = nil, &block)
|
104
112
|
status_key = Rack::Utils.status_code(status_code)
|
105
113
|
|
106
|
-
raise ArgumentError, 'Response
|
114
|
+
raise ArgumentError, 'Response defined!' if @response_serializers.key?(status_key)
|
107
115
|
|
108
116
|
serializer ||= SoberSwag::OutputObject.define(&block)
|
109
117
|
response_module.const_set(status_code.to_s.classify, serializer)
|
@@ -124,8 +132,22 @@ module SoberSwag
|
|
124
132
|
end
|
125
133
|
|
126
134
|
def make_input_object!(base, &block)
|
127
|
-
|
128
|
-
|
135
|
+
if base.is_a?(Class)
|
136
|
+
make_input_class(base, block)
|
137
|
+
elsif block
|
138
|
+
raise ArgumentError, 'passed a non-class base and a block to an input'
|
139
|
+
else
|
140
|
+
base
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
def make_input_class(base, block)
|
145
|
+
if block
|
146
|
+
Class.new(base, &block).tap do |e|
|
147
|
+
e.transform_keys(&:to_sym) if [SoberSwag::InputObject, Dry::Struct].include?(base)
|
148
|
+
end
|
149
|
+
else
|
150
|
+
base
|
129
151
|
end
|
130
152
|
end
|
131
153
|
end
|
@@ -7,6 +7,7 @@ module SoberSwag
|
|
7
7
|
# Please see the documentation for that class to see how it works.
|
8
8
|
class InputObject < Dry::Struct
|
9
9
|
transform_keys(&:to_sym)
|
10
|
+
include SoberSwag::Type::Named
|
10
11
|
|
11
12
|
class << self
|
12
13
|
##
|
@@ -17,19 +18,51 @@ module SoberSwag
|
|
17
18
|
@identifier || name.to_s.gsub('::', '.')
|
18
19
|
end
|
19
20
|
|
21
|
+
def attribute(key, parent = SoberSwag::InputObject, &block)
|
22
|
+
raise ArgumentError, "parent class #{parent} is not an input object type!" unless valid_field_def?(parent, block)
|
23
|
+
|
24
|
+
super(key, parent, &block)
|
25
|
+
end
|
26
|
+
|
27
|
+
def attribute?(key, parent = SoberSwag::InputObject, &block)
|
28
|
+
raise ArgumentError, "parent class #{parent} is not an input object type!" unless valid_field_def?(parent, block)
|
29
|
+
|
30
|
+
super(key, parent, &block)
|
31
|
+
end
|
32
|
+
|
20
33
|
def meta(*args)
|
34
|
+
original = self
|
35
|
+
|
21
36
|
super(*args).tap do |result|
|
22
|
-
result
|
37
|
+
return result unless result.is_a?(Class)
|
38
|
+
|
39
|
+
result.define_singleton_method(:alias?) { true }
|
40
|
+
result.define_singleton_method(:alias_of) { original }
|
23
41
|
end
|
24
42
|
end
|
25
43
|
|
26
|
-
|
27
|
-
|
44
|
+
##
|
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
|
47
|
+
def primitive(*args)
|
48
|
+
if args.length == 1
|
49
|
+
SoberSwag::Types.const_get(args.first)
|
50
|
+
else
|
51
|
+
super
|
52
|
+
end
|
28
53
|
end
|
29
54
|
|
30
55
|
def param(sym)
|
31
56
|
SoberSwag::Types::Params.const_get(sym)
|
32
57
|
end
|
58
|
+
|
59
|
+
private
|
60
|
+
|
61
|
+
def valid_field_def?(parent, block)
|
62
|
+
return true if block.nil?
|
63
|
+
|
64
|
+
parent.is_a?(Class) && parent <= SoberSwag::InputObject
|
65
|
+
end
|
33
66
|
end
|
34
67
|
end
|
35
68
|
end
|
@@ -2,29 +2,30 @@ module SoberSwag
|
|
2
2
|
module Nodes
|
3
3
|
##
|
4
4
|
# One attribute of an object.
|
5
|
-
class Attribute
|
6
|
-
def initialize(key, required, value)
|
5
|
+
class Attribute < Base
|
6
|
+
def initialize(key, required, value, meta = {})
|
7
7
|
@key = key
|
8
8
|
@required = required
|
9
9
|
@value = value
|
10
|
+
@meta = meta
|
10
11
|
end
|
11
12
|
|
12
13
|
def deconstruct
|
13
|
-
[key, required, value]
|
14
|
+
[key, required, value, meta]
|
14
15
|
end
|
15
16
|
|
16
17
|
def deconstruct_keys
|
17
|
-
{ key: key, required: required, value: value }
|
18
|
+
{ key: key, required: required, value: value, meta: meta }
|
18
19
|
end
|
19
20
|
|
20
|
-
attr_reader :key, :required, :value
|
21
|
+
attr_reader :key, :required, :value, :meta
|
21
22
|
|
22
23
|
def map(&block)
|
23
|
-
self.class.new(key, required, value.map(&block))
|
24
|
+
self.class.new(key, required, value.map(&block), meta)
|
24
25
|
end
|
25
26
|
|
26
27
|
def cata(&block)
|
27
|
-
block.call(self.class.new(key, required, value.cata(&block)))
|
28
|
+
block.call(self.class.new(key, required, value.cata(&block), meta))
|
28
29
|
end
|
29
30
|
end
|
30
31
|
end
|
@@ -17,8 +17,14 @@ module SoberSwag
|
|
17
17
|
@fields << field
|
18
18
|
end
|
19
19
|
|
20
|
-
def view(name, &block)
|
21
|
-
|
20
|
+
def view(name, inherits: nil, &block)
|
21
|
+
initial_fields =
|
22
|
+
if inherits.nil? || inherits == :base
|
23
|
+
fields
|
24
|
+
else
|
25
|
+
find_view(inherits).fields
|
26
|
+
end
|
27
|
+
view = View.define(name, initial_fields, &block)
|
22
28
|
|
23
29
|
view.identifier("#{@identifier}.#{name.to_s.classify}") if identifier
|
24
30
|
|
@@ -29,6 +35,12 @@ module SoberSwag
|
|
29
35
|
@identifier = arg if arg
|
30
36
|
@identifier
|
31
37
|
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
def find_view(name)
|
42
|
+
@views.find { |view| view.name == name } || (raise ArgumentError, "no view #{name.inspect} defined!")
|
43
|
+
end
|
32
44
|
end
|
33
45
|
end
|
34
46
|
end
|
@@ -7,11 +7,27 @@ module SoberSwag
|
|
7
7
|
add_field!(Field.new(name, serializer, from: from, &block))
|
8
8
|
end
|
9
9
|
|
10
|
+
##
|
11
|
+
# Similar to #field, but adds multiple at once.
|
12
|
+
# Named #multi because #fields was already taken.
|
13
|
+
def multi(names, serializer)
|
14
|
+
names.each { |name| field(name, serializer) }
|
15
|
+
end
|
16
|
+
|
10
17
|
##
|
11
18
|
# Given a symbol to this, we will use a primitive name
|
12
19
|
def primitive(name)
|
13
20
|
SoberSwag::Serializer.primitive(SoberSwag::Types.const_get(name))
|
14
21
|
end
|
22
|
+
|
23
|
+
##
|
24
|
+
# Merge in anything that has a list of fields, and use it.
|
25
|
+
# Note that merging in a full blueprint *will not* also merge in views, just fields defined on the base.
|
26
|
+
def merge(other)
|
27
|
+
other.fields.each do |field|
|
28
|
+
add_field!(field)
|
29
|
+
end
|
30
|
+
end
|
15
31
|
end
|
16
32
|
end
|
17
33
|
end
|
data/lib/sober_swag/parser.rb
CHANGED
@@ -25,7 +25,8 @@ module SoberSwag
|
|
25
25
|
Nodes::Attribute.new(
|
26
26
|
@node.name,
|
27
27
|
@node.required? && !@node.type.default?,
|
28
|
-
bind(Parser.new(@node.type))
|
28
|
+
bind(Parser.new(@node.type)),
|
29
|
+
@node.meta
|
29
30
|
)
|
30
31
|
when Dry::Types::Sum
|
31
32
|
left = bind(Parser.new(@node.left))
|
@@ -46,14 +47,18 @@ module SoberSwag
|
|
46
47
|
when Dry::Types::Constrained
|
47
48
|
bind(Parser.new(@node.type))
|
48
49
|
when Dry::Types::Nominal
|
49
|
-
|
50
|
-
|
51
|
-
|
50
|
+
if @node.respond_to?(:type) && @node.type.is_a?(Dry::Types::Constrained)
|
51
|
+
bind(Parser.new(@node.type))
|
52
|
+
else
|
53
|
+
old_meta = @node.primitive.respond_to?(:meta) ? @node.primitive.meta : {}
|
54
|
+
# start off with the moral equivalent of NodeTree[String]
|
55
|
+
Nodes::Primitive.new(@node.primitive, old_meta.merge(@node.meta))
|
56
|
+
end
|
52
57
|
else
|
53
58
|
# Inside of this case we have a class that is some user-defined type
|
54
59
|
# We put it in our array of found types, and consider it a primitive
|
55
60
|
@found.add(@node)
|
56
|
-
Nodes::Primitive.new(@node)
|
61
|
+
Nodes::Primitive.new(@node, @node.respond_to?(:meta) ? @node.meta : {})
|
57
62
|
end
|
58
63
|
end
|
59
64
|
|
@@ -34,7 +34,9 @@ module SoberSwag
|
|
34
34
|
# As such, we need to be a bit clever about when we tack on the identifier
|
35
35
|
# for this type.
|
36
36
|
%i[lazy_type type].each do |sym|
|
37
|
-
|
37
|
+
if @base.public_send(sym).respond_to?(:identifier) && public_send(sym).respond_to?(:identifier)
|
38
|
+
public_send(sym).identifier(@base.public_send(sym).identifier)
|
39
|
+
end
|
38
40
|
end
|
39
41
|
end
|
40
42
|
|
data/lib/sober_swag/server.rb
CHANGED
@@ -22,28 +22,40 @@ module SoberSwag
|
|
22
22
|
#
|
23
23
|
# @param controller_proc [Proc] a proc that, when called, gives a list of {SoberSwag::Controller}s to document
|
24
24
|
# @param cache [Bool | Proc] if we should cache our defintions (default false)
|
25
|
+
# @param redoc_version [String] what version of the redoc library to use to display UI (default 'next', the latest version).
|
25
26
|
def initialize(
|
26
27
|
controller_proc: RAILS_CONTROLLER_PROC,
|
27
|
-
cache: false
|
28
|
+
cache: false,
|
29
|
+
redoc_version: 'next'
|
28
30
|
)
|
29
31
|
@controller_proc = controller_proc
|
30
32
|
@cache = cache
|
33
|
+
@html = EFFECT_HTML.gsub(/REDOC_VERSION/, redoc_version)
|
31
34
|
end
|
32
35
|
|
33
36
|
EFFECT_HTML = <<~HTML.freeze
|
34
37
|
<!DOCTYPE html>
|
35
38
|
<html>
|
36
39
|
<head>
|
37
|
-
<title>
|
38
|
-
|
39
|
-
<
|
40
|
+
<title>ReDoc</title>
|
41
|
+
<!-- needed for adaptive design -->
|
42
|
+
<meta charset="utf-8"/>
|
43
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
44
|
+
<link href="https://fonts.googleapis.com/css?family=Montserrat:300,400,700|Roboto:300,400,700" rel="stylesheet">
|
45
|
+
|
46
|
+
<!--
|
47
|
+
ReDoc doesn't change outer page styles
|
48
|
+
-->
|
49
|
+
<style>
|
50
|
+
body {
|
51
|
+
margin: 0;
|
52
|
+
padding: 0;
|
53
|
+
}
|
54
|
+
</style>
|
40
55
|
</head>
|
41
56
|
<body>
|
42
|
-
<
|
43
|
-
</
|
44
|
-
<script>
|
45
|
-
SwaggerUIBundle({url: 'SCRIPT_NAME', dom_id: '#swagger'})
|
46
|
-
</script>
|
57
|
+
<redoc spec-url='SCRIPT_NAME'></redoc>
|
58
|
+
<script src="https://cdn.jsdelivr.net/npm/redoc@REDOC_VERSION/bundles/redoc.standalone.js"> </script>
|
47
59
|
</body>
|
48
60
|
</html>
|
49
61
|
HTML
|
@@ -53,7 +65,7 @@ module SoberSwag
|
|
53
65
|
if req.path_info&.match?(/json/si) || req.get_header('Accept')&.match?(/json/si)
|
54
66
|
[200, { 'Content-Type' => 'application/json' }, [generate_json_string]]
|
55
67
|
else
|
56
|
-
[200, { 'Content-Type' => 'text/html' }, [
|
68
|
+
[200, { 'Content-Type' => 'text/html' }, [@html.gsub(/SCRIPT_NAME/, "#{env['SCRIPT_NAME']}.json")]]
|
57
69
|
end
|
58
70
|
end
|
59
71
|
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module SoberSwag
|
2
|
+
module Type
|
3
|
+
##
|
4
|
+
# Mixin module used to identify types that should be considered
|
5
|
+
# standalone, named types from SoberSwag's perspective.
|
6
|
+
module Named
|
7
|
+
##
|
8
|
+
# Class Methods Module.
|
9
|
+
# Modules that include {SoberSwag::Type::Named}
|
10
|
+
# will automatically extend this module.
|
11
|
+
module ClassMethods
|
12
|
+
def alias?
|
13
|
+
false
|
14
|
+
end
|
15
|
+
|
16
|
+
def alias_of
|
17
|
+
nil
|
18
|
+
end
|
19
|
+
|
20
|
+
def root_alias
|
21
|
+
alias_of || self
|
22
|
+
end
|
23
|
+
|
24
|
+
def description(arg = nil)
|
25
|
+
@description = arg if arg
|
26
|
+
@description
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.included(mod)
|
31
|
+
mod.extend(ClassMethods)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|