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.
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