sober_swag 0.1.0 → 0.6.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 +4 -0
- data/.github/workflows/lint.yml +15 -0
- data/.github/workflows/ruby.yml +33 -2
- data/.gitignore +4 -0
- data/.rubocop.yml +75 -1
- data/.ruby-version +1 -1
- data/README.md +154 -1
- data/bin/console +16 -15
- data/docs/serializers.md +203 -0
- data/example/.rspec +1 -0
- data/example/.ruby-version +1 -1
- data/example/Gemfile +9 -7
- data/example/Gemfile.lock +96 -79
- data/example/app/controllers/people_controller.rb +41 -23
- data/example/app/controllers/posts_controller.rb +110 -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 +86 -56
- 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 +5 -3
- 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 +2 -2
- data/lib/sober_swag/{blueprint → output_object}/view.rb +15 -6
- data/lib/sober_swag/parser.rb +9 -4
- 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 +19 -2
- data/lib/sober_swag/serializer/field_list.rb +29 -6
- data/lib/sober_swag/serializer/mapped.rb +15 -3
- 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 +8 -4
- metadata +79 -47
- data/Gemfile.lock +0 -92
- 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
@@ -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
|
@@ -1,5 +1,5 @@
|
|
1
1
|
module SoberSwag
|
2
|
-
class
|
2
|
+
class OutputObject
|
3
3
|
##
|
4
4
|
# Syntax for definitions that can add fields.
|
5
5
|
module FieldSyntax
|
@@ -10,7 +10,7 @@ module SoberSwag
|
|
10
10
|
##
|
11
11
|
# Given a symbol to this, we will use a primitive name
|
12
12
|
def primitive(name)
|
13
|
-
SoberSwag::Serializer.
|
13
|
+
SoberSwag::Serializer.primitive(SoberSwag::Types.const_get(name))
|
14
14
|
end
|
15
15
|
end
|
16
16
|
end
|
@@ -1,14 +1,16 @@
|
|
1
1
|
module SoberSwag
|
2
|
-
class
|
2
|
+
class OutputObject
|
3
|
+
##
|
4
|
+
# DSL for defining a view.
|
5
|
+
# Used in `view` blocks within {SoberSwag::OutputObject.define}.
|
3
6
|
class View
|
4
|
-
|
5
7
|
def self.define(name, base_fields, &block)
|
6
|
-
|
8
|
+
new(name, base_fields).tap do |view|
|
7
9
|
view.instance_eval(&block)
|
8
10
|
end
|
9
11
|
end
|
10
12
|
|
11
|
-
class NestingError < Error; end
|
13
|
+
class NestingError < Error; end
|
12
14
|
|
13
15
|
include FieldSyntax
|
14
16
|
|
@@ -19,8 +21,16 @@ module SoberSwag
|
|
19
21
|
|
20
22
|
attr_reader :name, :fields
|
21
23
|
|
24
|
+
def serialize(obj, opts = {})
|
25
|
+
serializer.serialize(obj, opts)
|
26
|
+
end
|
27
|
+
|
28
|
+
def type
|
29
|
+
serializer.type
|
30
|
+
end
|
31
|
+
|
22
32
|
def except!(name)
|
23
|
-
@fields.
|
33
|
+
@fields.reject! { |f| f.name == name }
|
24
34
|
end
|
25
35
|
|
26
36
|
def view(*)
|
@@ -38,7 +48,6 @@ module SoberSwag
|
|
38
48
|
@serializer ||=
|
39
49
|
SoberSwag::Serializer::FieldList.new(fields)
|
40
50
|
end
|
41
|
-
|
42
51
|
end
|
43
52
|
end
|
44
53
|
end
|
data/lib/sober_swag/parser.rb
CHANGED
@@ -1,12 +1,18 @@
|
|
1
1
|
module SoberSwag
|
2
|
+
##
|
3
|
+
# Parses a *Dry-Types Schema* into a set of nodes we can use to compile.
|
4
|
+
# This is mostly because the vistior pattern sucks and catamorphisms are nice.
|
2
5
|
class Parser
|
3
6
|
def initialize(node)
|
4
7
|
@node = node
|
5
8
|
@found = Set.new
|
6
9
|
end
|
7
10
|
|
8
|
-
def to_syntax
|
11
|
+
def to_syntax # rubocop:disable Metrics/MethodLength, Metrics/CyclomaticComplexity
|
9
12
|
case @node
|
13
|
+
when Dry::Types::Default
|
14
|
+
# we handle this elsewhere, so
|
15
|
+
bind(Parser.new(@node.type))
|
10
16
|
when Dry::Types::Array::Member
|
11
17
|
Nodes::List.new(bind(Parser.new(@node.member)))
|
12
18
|
when Dry::Types::Enum
|
@@ -18,7 +24,7 @@ module SoberSwag
|
|
18
24
|
when Dry::Types::Schema::Key
|
19
25
|
Nodes::Attribute.new(
|
20
26
|
@node.name,
|
21
|
-
@node.required?,
|
27
|
+
@node.required? && !@node.type.default?,
|
22
28
|
bind(Parser.new(@node.type))
|
23
29
|
)
|
24
30
|
when Dry::Types::Sum
|
@@ -41,7 +47,7 @@ module SoberSwag
|
|
41
47
|
bind(Parser.new(@node.type))
|
42
48
|
when Dry::Types::Nominal
|
43
49
|
# start off with the moral equivalent of NodeTree[String]
|
44
|
-
Nodes::Primitive.new(@node.primitive)
|
50
|
+
Nodes::Primitive.new(@node.primitive, @node.meta)
|
45
51
|
else
|
46
52
|
# Inside of this case we have a class that is some user-defined type
|
47
53
|
# We put it in our array of found types, and consider it a primitive
|
@@ -68,6 +74,5 @@ module SoberSwag
|
|
68
74
|
@found += other.found
|
69
75
|
result
|
70
76
|
end
|
71
|
-
|
72
77
|
end
|
73
78
|
end
|
@@ -1,4 +1,7 @@
|
|
1
1
|
module SoberSwag
|
2
|
+
##
|
3
|
+
# Container module for serializers.
|
4
|
+
# The interface for these is described in {SoberSwag::Serializer::Base}.
|
2
5
|
module Serializer
|
3
6
|
autoload(:Base, 'sober_swag/serializer/base')
|
4
7
|
autoload(:Primitive, 'sober_swag/serializer/primitive')
|
@@ -7,6 +10,7 @@ module SoberSwag
|
|
7
10
|
autoload(:Mapped, 'sober_swag/serializer/mapped')
|
8
11
|
autoload(:Optional, 'sober_swag/serializer/optional')
|
9
12
|
autoload(:FieldList, 'sober_swag/serializer/field_list')
|
13
|
+
autoload(:Meta, 'sober_swag/serializer/meta')
|
10
14
|
|
11
15
|
class << self
|
12
16
|
##
|
@@ -14,10 +18,9 @@ module SoberSwag
|
|
14
18
|
# in values raw.
|
15
19
|
#
|
16
20
|
# @param contained {Class} Dry::Type to use
|
17
|
-
def
|
21
|
+
def primitive(contained)
|
18
22
|
SoberSwag::Serializer::Primitive.new(contained)
|
19
23
|
end
|
20
24
|
end
|
21
|
-
|
22
25
|
end
|
23
26
|
end
|
@@ -7,6 +7,18 @@ module SoberSwag
|
|
7
7
|
@element_serializer = element_serializer
|
8
8
|
end
|
9
9
|
|
10
|
+
def lazy_type?
|
11
|
+
@element_serializer.lazy_type?
|
12
|
+
end
|
13
|
+
|
14
|
+
def lazy_type
|
15
|
+
SoberSwag::Types::Array.of(@element_serializer.lazy_type)
|
16
|
+
end
|
17
|
+
|
18
|
+
def finalize_lazy_type!
|
19
|
+
@element_serializer.finalize_lazy_type!
|
20
|
+
end
|
21
|
+
|
10
22
|
attr_reader :element_serializer
|
11
23
|
|
12
24
|
def serialize(object, options = {})
|
@@ -1,7 +1,10 @@
|
|
1
1
|
module SoberSwag
|
2
2
|
module Serializer
|
3
|
+
##
|
4
|
+
# Base interface class that all other serializers are subclasses of.
|
5
|
+
# This also defines methods as stubs, which is sort of bad ruby style, but makes documentation
|
6
|
+
# easier to generate.
|
3
7
|
class Base
|
4
|
-
|
5
8
|
##
|
6
9
|
# Return a new serializer that is an *array* of elements of this serializer.
|
7
10
|
def array
|
@@ -14,6 +17,46 @@ module SoberSwag
|
|
14
17
|
SoberSwag::Serializer::Optional.new(self)
|
15
18
|
end
|
16
19
|
|
20
|
+
##
|
21
|
+
# Is this type lazily defined?
|
22
|
+
#
|
23
|
+
# Used for mutual recursion.
|
24
|
+
def lazy_type?
|
25
|
+
false
|
26
|
+
end
|
27
|
+
|
28
|
+
##
|
29
|
+
# The lazy version of this type, for mutual recursion.
|
30
|
+
def lazy_type
|
31
|
+
type
|
32
|
+
end
|
33
|
+
|
34
|
+
##
|
35
|
+
# Finalize a lazy type.
|
36
|
+
#
|
37
|
+
# Should be idempotent: call it once, and it does nothing on subsequent calls (but returns the type).
|
38
|
+
def finalize_lazy_type!
|
39
|
+
type
|
40
|
+
end
|
41
|
+
|
42
|
+
##
|
43
|
+
# Serialize an object.
|
44
|
+
def serialize(_object, _options = {})
|
45
|
+
raise ArgumentError, 'not implemented!'
|
46
|
+
end
|
47
|
+
|
48
|
+
##
|
49
|
+
# Get the type that we serialize to.
|
50
|
+
def type
|
51
|
+
raise ArgumentError, 'not implemented!'
|
52
|
+
end
|
53
|
+
|
54
|
+
##
|
55
|
+
# Add metadata onto the *type* of a serializer.
|
56
|
+
def meta(hash)
|
57
|
+
SoberSwag::Serializer::Meta.new(self, hash)
|
58
|
+
end
|
59
|
+
|
17
60
|
##
|
18
61
|
# If I am a serializer for type 'a', and you give me a way to turn 'a's into 'b's,
|
19
62
|
# I can give you a serializer for type 'b' by running the funciton you gave.
|
@@ -33,6 +76,12 @@ module SoberSwag
|
|
33
76
|
self
|
34
77
|
end
|
35
78
|
|
79
|
+
##
|
80
|
+
# Get the type name of this to be used externally, or set it if an argument is provided
|
81
|
+
def identifier(arg = nil)
|
82
|
+
@identifier = arg if arg
|
83
|
+
@identifier
|
84
|
+
end
|
36
85
|
end
|
37
86
|
end
|
38
87
|
end
|
@@ -25,9 +25,10 @@ module SoberSwag
|
|
25
25
|
|
26
26
|
def serialize(object, options = {})
|
27
27
|
tag, val = chooser.call(object, options)
|
28
|
-
|
28
|
+
case tag
|
29
|
+
when :left
|
29
30
|
left.serialize(val, options)
|
30
|
-
|
31
|
+
when :right
|
31
32
|
right.serialize(val, options)
|
32
33
|
else
|
33
34
|
raise BadChoiceError, "result of chooser proc was not a left or right, but a #{val.class}"
|
@@ -44,6 +45,22 @@ module SoberSwag
|
|
44
45
|
left.type | right.type
|
45
46
|
end
|
46
47
|
end
|
48
|
+
|
49
|
+
def lazy_type
|
50
|
+
if left.lazy_type == right.lazy_type
|
51
|
+
left.lazy_type
|
52
|
+
else
|
53
|
+
left.lazy_type | right.lazy_type
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def lazy_type?
|
58
|
+
left.lazy_type? || right.lazy_type?
|
59
|
+
end
|
60
|
+
|
61
|
+
def finalize_lazy_type!
|
62
|
+
[left, right].each(&:finalize_lazy_type!)
|
63
|
+
end
|
47
64
|
end
|
48
65
|
end
|
49
66
|
end
|
@@ -4,7 +4,6 @@ module SoberSwag
|
|
4
4
|
# Extract out a hash from a list of
|
5
5
|
# name/serializer pairs.
|
6
6
|
class FieldList < Base
|
7
|
-
|
8
7
|
def initialize(field_list)
|
9
8
|
@field_list = field_list
|
10
9
|
end
|
@@ -17,8 +16,7 @@ module SoberSwag
|
|
17
16
|
SoberSwag::Serializer.Primitive(SoberSwag::Types.const_get(symbol))
|
18
17
|
end
|
19
18
|
|
20
|
-
|
21
|
-
def serialize(object, options)
|
19
|
+
def serialize(object, options = {})
|
22
20
|
field_list.map { |field|
|
23
21
|
[field.name, field.serializer.serialize(object, options)]
|
24
22
|
}.to_h
|
@@ -28,17 +26,42 @@ module SoberSwag
|
|
28
26
|
@type ||= make_struct_type!
|
29
27
|
end
|
30
28
|
|
29
|
+
def lazy_type?
|
30
|
+
true
|
31
|
+
end
|
32
|
+
|
33
|
+
def lazy_type
|
34
|
+
struct_class
|
35
|
+
end
|
36
|
+
|
37
|
+
def finalize_lazy_type!
|
38
|
+
make_struct_type!
|
39
|
+
end
|
40
|
+
|
31
41
|
private
|
32
42
|
|
33
|
-
def make_struct_type!
|
43
|
+
def make_struct_type! # rubocop:disable Metrics/MethodLength
|
44
|
+
# mutual recursion makes this really, really annoying.
|
45
|
+
return struct_class if @made_struct_type
|
46
|
+
|
34
47
|
f = field_list
|
35
|
-
|
48
|
+
s = identifier
|
49
|
+
struct_class.instance_eval do
|
50
|
+
identifier(s)
|
36
51
|
f.each do |field|
|
37
|
-
attribute field.name, field.serializer.
|
52
|
+
attribute field.name, field.serializer.lazy_type
|
38
53
|
end
|
39
54
|
end
|
55
|
+
@made_struct_type = true
|
56
|
+
|
57
|
+
field_list.map(&:serializer).each(&:finalize_lazy_type!)
|
58
|
+
|
59
|
+
struct_class
|
40
60
|
end
|
41
61
|
|
62
|
+
def struct_class
|
63
|
+
@struct_class ||= Class.new(SoberSwag::InputObject)
|
64
|
+
end
|
42
65
|
end
|
43
66
|
end
|
44
67
|
end
|
@@ -3,16 +3,29 @@ module SoberSwag
|
|
3
3
|
##
|
4
4
|
# A new serializer by mapping over the serialization function
|
5
5
|
class Mapped < Base
|
6
|
-
|
7
6
|
def initialize(base, map_f)
|
8
7
|
@base = base
|
9
8
|
@map_f = map_f
|
10
9
|
end
|
11
10
|
|
12
|
-
|
11
|
+
attr_reader :base, :map_f
|
12
|
+
|
13
|
+
def serialize(object, options = {})
|
13
14
|
@base.serialize(@map_f.call(object), options)
|
14
15
|
end
|
15
16
|
|
17
|
+
def lazy_type?
|
18
|
+
@base.lazy_type?
|
19
|
+
end
|
20
|
+
|
21
|
+
def lazy_type
|
22
|
+
@base.lazy_type
|
23
|
+
end
|
24
|
+
|
25
|
+
def finalize_lazy_type!
|
26
|
+
@base.finalize_lazy_type!
|
27
|
+
end
|
28
|
+
|
16
29
|
def type
|
17
30
|
@base.type
|
18
31
|
end
|
@@ -23,7 +36,6 @@ module SoberSwag
|
|
23
36
|
def via_map(&block)
|
24
37
|
SoberSwag::Serializer::Mapped.new(@base, @map_f >> block)
|
25
38
|
end
|
26
|
-
|
27
39
|
end
|
28
40
|
end
|
29
41
|
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module SoberSwag
|
2
|
+
module Serializer
|
3
|
+
##
|
4
|
+
# Provides metadata on a serializer.
|
5
|
+
# All actions delegate to the base.
|
6
|
+
class Meta < Base
|
7
|
+
def initialize(base, meta)
|
8
|
+
@base = base
|
9
|
+
@meta = meta
|
10
|
+
end
|
11
|
+
|
12
|
+
attr_reader :base, :meta
|
13
|
+
|
14
|
+
def serialize(args, opts = {})
|
15
|
+
base.serialize(args, opts)
|
16
|
+
end
|
17
|
+
|
18
|
+
def lazy_type
|
19
|
+
@base.lazy_type.meta(**meta)
|
20
|
+
end
|
21
|
+
|
22
|
+
def type
|
23
|
+
@base.type.meta(**meta)
|
24
|
+
end
|
25
|
+
|
26
|
+
def finalize_lazy_type!
|
27
|
+
@base.finalize_lazy_type!
|
28
|
+
end
|
29
|
+
|
30
|
+
def lazy_type?
|
31
|
+
@base.lazy_type?
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|