sober_swag 0.1.0 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/config/rubocop_linter_action.yml +5 -0
- data/.github/workflows/lint.yml +15 -0
- data/.github/workflows/ruby.yml +23 -1
- data/.gitignore +3 -0
- data/.rubocop.yml +73 -1
- data/.ruby-version +1 -1
- data/Gemfile.lock +29 -5
- data/README.md +109 -0
- data/bin/console +15 -14
- data/docs/serializers.md +203 -0
- data/example/.rspec +1 -0
- data/example/.ruby-version +1 -1
- data/example/Gemfile +10 -6
- data/example/Gemfile.lock +96 -76
- data/example/app/controllers/people_controller.rb +37 -21
- data/example/app/controllers/posts_controller.rb +102 -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 +28 -15
- 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 +2 -4
- 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 +1 -1
- data/lib/sober_swag/{blueprint → output_object}/view.rb +15 -6
- data/lib/sober_swag/parser.rb +5 -3
- 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 +15 -2
- data/lib/sober_swag/serializer/field_list.rb +29 -6
- data/lib/sober_swag/serializer/mapped.rb +12 -2
- 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 +6 -4
- metadata +77 -44
- 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
@@ -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,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
|
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
|
-
|
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(
|
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
|
@@ -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
|
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 ||=
|
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
|