sober_swag 0.1.0 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (92) hide show
  1. checksums.yaml +4 -4
  2. data/.github/config/rubocop_linter_action.yml +4 -0
  3. data/.github/workflows/lint.yml +15 -0
  4. data/.github/workflows/ruby.yml +33 -2
  5. data/.gitignore +4 -0
  6. data/.rubocop.yml +75 -1
  7. data/.ruby-version +1 -1
  8. data/README.md +154 -1
  9. data/bin/console +16 -15
  10. data/docs/serializers.md +203 -0
  11. data/example/.rspec +1 -0
  12. data/example/.ruby-version +1 -1
  13. data/example/Gemfile +9 -7
  14. data/example/Gemfile.lock +96 -79
  15. data/example/app/controllers/people_controller.rb +41 -23
  16. data/example/app/controllers/posts_controller.rb +110 -0
  17. data/example/app/models/application_record.rb +3 -0
  18. data/example/app/models/person.rb +6 -0
  19. data/example/app/models/post.rb +9 -0
  20. data/example/app/output_objects/person_errors_output_object.rb +5 -0
  21. data/example/app/output_objects/person_output_object.rb +15 -0
  22. data/example/app/output_objects/post_output_object.rb +10 -0
  23. data/example/bin/bundle +24 -20
  24. data/example/bin/rails +1 -1
  25. data/example/bin/rake +1 -1
  26. data/example/config/application.rb +11 -7
  27. data/example/config/environments/development.rb +0 -1
  28. data/example/config/environments/production.rb +3 -3
  29. data/example/config/puma.rb +5 -5
  30. data/example/config/routes.rb +3 -0
  31. data/example/config/spring.rb +4 -4
  32. data/example/db/migrate/20200311152021_create_people.rb +0 -1
  33. data/example/db/migrate/20200603172347_create_posts.rb +11 -0
  34. data/example/db/schema.rb +16 -7
  35. data/example/spec/rails_helper.rb +64 -0
  36. data/example/spec/requests/people/create_spec.rb +52 -0
  37. data/example/spec/requests/people/get_spec.rb +35 -0
  38. data/example/spec/requests/people/index_spec.rb +69 -0
  39. data/example/spec/spec_helper.rb +94 -0
  40. data/lib/sober_swag.rb +6 -3
  41. data/lib/sober_swag/compiler/error.rb +2 -0
  42. data/lib/sober_swag/compiler/path.rb +2 -5
  43. data/lib/sober_swag/compiler/paths.rb +0 -1
  44. data/lib/sober_swag/compiler/type.rb +86 -56
  45. data/lib/sober_swag/controller.rb +16 -11
  46. data/lib/sober_swag/controller/route.rb +18 -21
  47. data/lib/sober_swag/controller/undefined_body_error.rb +3 -0
  48. data/lib/sober_swag/controller/undefined_path_error.rb +3 -0
  49. data/lib/sober_swag/controller/undefined_query_error.rb +3 -0
  50. data/lib/sober_swag/input_object.rb +28 -0
  51. data/lib/sober_swag/nodes/array.rb +1 -1
  52. data/lib/sober_swag/nodes/base.rb +5 -3
  53. data/lib/sober_swag/nodes/binary.rb +2 -1
  54. data/lib/sober_swag/nodes/enum.rb +4 -2
  55. data/lib/sober_swag/nodes/list.rb +0 -1
  56. data/lib/sober_swag/nodes/primitive.rb +6 -5
  57. data/lib/sober_swag/output_object.rb +102 -0
  58. data/lib/sober_swag/output_object/definition.rb +30 -0
  59. data/lib/sober_swag/{blueprint → output_object}/field.rb +14 -4
  60. data/lib/sober_swag/{blueprint → output_object}/field_syntax.rb +2 -2
  61. data/lib/sober_swag/{blueprint → output_object}/view.rb +15 -6
  62. data/lib/sober_swag/parser.rb +9 -4
  63. data/lib/sober_swag/serializer.rb +5 -2
  64. data/lib/sober_swag/serializer/array.rb +12 -0
  65. data/lib/sober_swag/serializer/base.rb +50 -1
  66. data/lib/sober_swag/serializer/conditional.rb +19 -2
  67. data/lib/sober_swag/serializer/field_list.rb +29 -6
  68. data/lib/sober_swag/serializer/mapped.rb +15 -3
  69. data/lib/sober_swag/serializer/meta.rb +35 -0
  70. data/lib/sober_swag/serializer/optional.rb +17 -2
  71. data/lib/sober_swag/serializer/primitive.rb +4 -1
  72. data/lib/sober_swag/server.rb +83 -0
  73. data/lib/sober_swag/types.rb +3 -0
  74. data/lib/sober_swag/version.rb +1 -1
  75. data/sober_swag.gemspec +8 -4
  76. metadata +79 -47
  77. data/Gemfile.lock +0 -92
  78. data/example/person.json +0 -4
  79. data/example/test/controllers/.keep +0 -0
  80. data/example/test/fixtures/.keep +0 -0
  81. data/example/test/fixtures/files/.keep +0 -0
  82. data/example/test/fixtures/people.yml +0 -11
  83. data/example/test/integration/.keep +0 -0
  84. data/example/test/models/.keep +0 -0
  85. data/example/test/models/person_test.rb +0 -7
  86. data/example/test/test_helper.rb +0 -13
  87. data/lib/sober_swag/blueprint.rb +0 -113
  88. data/lib/sober_swag/path.rb +0 -8
  89. data/lib/sober_swag/path/integer.rb +0 -21
  90. data/lib/sober_swag/path/lit.rb +0 -41
  91. data/lib/sober_swag/path/literal.rb +0 -29
  92. 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 Blueprint
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 ||= @root_serializer.serializer.via_map(&transform_proc)
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 Blueprint
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.Primitive(SoberSwag::Types.const_get(name))
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 Blueprint
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
- self.new(name, base_fields).tap do |view|
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.select! { |f| f.name != name }
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
@@ -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 Primitive(contained)
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
- if tag == :left
28
+ case tag
29
+ when :left
29
30
  left.serialize(val, options)
30
- elsif tag == :right
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
- Class.new(Dry::Struct) do
48
+ s = identifier
49
+ struct_class.instance_eval do
50
+ identifier(s)
36
51
  f.each do |field|
37
- attribute field.name, field.serializer.type
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
- def serialize(object, options)
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