sober_swag 0.1.0 → 0.2.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 +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
|