sober_swag 0.11.0 → 0.16.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/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
|