sober_swag 0.11.0 → 0.16.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/workflows/lint.yml +44 -9
- data/.gitignore +2 -0
- data/.rubocop.yml +50 -5
- data/CHANGELOG.md +22 -0
- data/README.md +141 -4
- data/bin/console +18 -30
- data/bin/rspec +29 -0
- data/docs/serializers.md +74 -9
- data/example/app/controllers/application_controller.rb +5 -0
- data/lib/sober_swag.rb +1 -0
- data/lib/sober_swag/compiler.rb +1 -0
- data/lib/sober_swag/compiler/primitive.rb +75 -0
- data/lib/sober_swag/compiler/type.rb +56 -92
- data/lib/sober_swag/controller.rb +3 -9
- data/lib/sober_swag/controller/route.rb +21 -8
- data/lib/sober_swag/input_object.rb +42 -2
- 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 -4
- data/lib/sober_swag/serializer/base.rb +2 -0
- data/lib/sober_swag/serializer/meta.rb +10 -6
- 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 +16 -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
|
|
@@ -14,14 +14,13 @@ module SoberSwag
|
|
14
14
|
attr_reader :response_serializers, :response_descriptions, :controller, :method, :path, :action_name
|
15
15
|
|
16
16
|
##
|
17
|
-
# What to parse the request body
|
17
|
+
# What to parse the request body into.
|
18
18
|
attr_reader :request_body_class
|
19
19
|
##
|
20
|
-
# What to parse the request query_params
|
20
|
+
# What to parse the request query_params into.
|
21
21
|
attr_reader :query_params_class
|
22
|
-
|
23
22
|
##
|
24
|
-
# What to parse the path params into
|
23
|
+
# What to parse the path params into.
|
25
24
|
attr_reader :path_params_class
|
26
25
|
|
27
26
|
##
|
@@ -30,7 +29,7 @@ module SoberSwag
|
|
30
29
|
# If you want, you can also define utility methods in here
|
31
30
|
def request_body(base = SoberSwag::InputObject, &block)
|
32
31
|
@request_body_class = make_input_object!(base, &block)
|
33
|
-
action_module.const_set('
|
32
|
+
action_module.const_set('RequestBody', @request_body_class)
|
34
33
|
end
|
35
34
|
|
36
35
|
##
|
@@ -103,7 +102,7 @@ module SoberSwag
|
|
103
102
|
def response(status_code, description, serializer = nil, &block)
|
104
103
|
status_key = Rack::Utils.status_code(status_code)
|
105
104
|
|
106
|
-
raise ArgumentError, 'Response
|
105
|
+
raise ArgumentError, 'Response defined!' if @response_serializers.key?(status_key)
|
107
106
|
|
108
107
|
serializer ||= SoberSwag::OutputObject.define(&block)
|
109
108
|
response_module.const_set(status_code.to_s.classify, serializer)
|
@@ -124,8 +123,22 @@ module SoberSwag
|
|
124
123
|
end
|
125
124
|
|
126
125
|
def make_input_object!(base, &block)
|
127
|
-
|
128
|
-
|
126
|
+
if base.is_a?(Class)
|
127
|
+
make_input_class(base, block)
|
128
|
+
elsif block
|
129
|
+
raise ArgumentError, 'passed a non-class base and a block to an input'
|
130
|
+
else
|
131
|
+
base
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
def make_input_class(base, block)
|
136
|
+
if block
|
137
|
+
Class.new(base, &block).tap do |e|
|
138
|
+
e.transform_keys(&:to_sym) if [SoberSwag::InputObject, Dry::Struct].include?(base)
|
139
|
+
end
|
140
|
+
else
|
141
|
+
base
|
129
142
|
end
|
130
143
|
end
|
131
144
|
end
|
@@ -7,22 +7,62 @@ 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
|
##
|
13
14
|
# The name to use for this type in external documentation.
|
14
15
|
def identifier(arg = nil)
|
15
16
|
@identifier = arg if arg
|
17
|
+
|
16
18
|
@identifier || name.to_s.gsub('::', '.')
|
17
19
|
end
|
18
20
|
|
19
|
-
def
|
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
|
+
|
33
|
+
def meta(*args)
|
34
|
+
original = self
|
35
|
+
|
36
|
+
super(*args).tap do |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 }
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
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
|
21
53
|
end
|
22
54
|
|
23
55
|
def param(sym)
|
24
56
|
SoberSwag::Types::Params.const_get(sym)
|
25
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
|
26
66
|
end
|
27
67
|
end
|
28
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,13 +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
|
-
|
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
|
51
57
|
else
|
52
58
|
# Inside of this case we have a class that is some user-defined type
|
53
59
|
# We put it in our array of found types, and consider it a primitive
|
54
60
|
@found.add(@node)
|
55
|
-
Nodes::Primitive.new(@node)
|
61
|
+
Nodes::Primitive.new(@node, @node.respond_to?(:meta) ? @node.meta : {})
|
56
62
|
end
|
57
63
|
end
|
58
64
|
|
@@ -20,20 +20,24 @@ module SoberSwag
|
|
20
20
|
self.class.new(base, metadata.merge(hash))
|
21
21
|
end
|
22
22
|
|
23
|
-
##
|
24
|
-
# Delegates to `base`, adds metadata, pumbs identifiers
|
25
23
|
def lazy_type
|
26
|
-
@base.lazy_type.meta(**metadata)
|
24
|
+
@lazy_type ||= @base.lazy_type.meta(**metadata)
|
27
25
|
end
|
28
26
|
|
29
|
-
##
|
30
|
-
# Delegates to `base`, adds metadata, plumbs identifiers
|
31
27
|
def type
|
32
|
-
@base.type.meta(**metadata)
|
28
|
+
@type ||= @base.type.meta(**metadata)
|
33
29
|
end
|
34
30
|
|
35
31
|
def finalize_lazy_type!
|
36
32
|
@base.finalize_lazy_type!
|
33
|
+
# Using .meta on dry-struct returns a *new type* that wraps the old one.
|
34
|
+
# As such, we need to be a bit clever about when we tack on the identifier
|
35
|
+
# for this type.
|
36
|
+
%i[lazy_type type].each do |sym|
|
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
|
40
|
+
end
|
37
41
|
end
|
38
42
|
|
39
43
|
def lazy_type?
|
@@ -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
|
data/lib/sober_swag/types.rb
CHANGED
@@ -0,0 +1,17 @@
|
|
1
|
+
module SoberSwag
|
2
|
+
class Types
|
3
|
+
##
|
4
|
+
# An array that will be parsed from comma-separated values in a string, if given a string.
|
5
|
+
module CommaArray
|
6
|
+
def self.of(other)
|
7
|
+
SoberSwag::Types::Array.of(other).constructor { |val|
|
8
|
+
if val.is_a?(::String)
|
9
|
+
val.split(',').map(&:strip)
|
10
|
+
else
|
11
|
+
val
|
12
|
+
end
|
13
|
+
}.meta(style: :form, explode: false)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
data/lib/sober_swag/version.rb
CHANGED
data/sober_swag.gemspec
CHANGED
@@ -39,7 +39,7 @@ Gem::Specification.new do |spec|
|
|
39
39
|
spec.add_development_dependency 'pry-byebug'
|
40
40
|
spec.add_development_dependency 'rake', '~> 13.0'
|
41
41
|
spec.add_development_dependency 'rspec', '~> 3.0'
|
42
|
-
spec.add_development_dependency 'rubocop'
|
43
|
-
spec.add_development_dependency 'rubocop-rspec'
|
42
|
+
spec.add_development_dependency 'rubocop', '~> 0.93.1'
|
43
|
+
spec.add_development_dependency 'rubocop-rspec', '~> 1.44.1'
|
44
44
|
spec.add_development_dependency 'simplecov'
|
45
45
|
end
|