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.
Files changed (92) hide show
  1. checksums.yaml +4 -4
  2. data/.github/config/rubocop_linter_action.yml +5 -0
  3. data/.github/workflows/lint.yml +15 -0
  4. data/.github/workflows/ruby.yml +23 -1
  5. data/.gitignore +3 -0
  6. data/.rubocop.yml +73 -1
  7. data/.ruby-version +1 -1
  8. data/Gemfile.lock +29 -5
  9. data/README.md +109 -0
  10. data/bin/console +15 -14
  11. data/docs/serializers.md +203 -0
  12. data/example/.rspec +1 -0
  13. data/example/.ruby-version +1 -1
  14. data/example/Gemfile +10 -6
  15. data/example/Gemfile.lock +96 -76
  16. data/example/app/controllers/people_controller.rb +37 -21
  17. data/example/app/controllers/posts_controller.rb +102 -0
  18. data/example/app/models/application_record.rb +3 -0
  19. data/example/app/models/person.rb +6 -0
  20. data/example/app/models/post.rb +9 -0
  21. data/example/app/output_objects/person_errors_output_object.rb +5 -0
  22. data/example/app/output_objects/person_output_object.rb +15 -0
  23. data/example/app/output_objects/post_output_object.rb +10 -0
  24. data/example/bin/bundle +24 -20
  25. data/example/bin/rails +1 -1
  26. data/example/bin/rake +1 -1
  27. data/example/config/application.rb +11 -7
  28. data/example/config/environments/development.rb +0 -1
  29. data/example/config/environments/production.rb +3 -3
  30. data/example/config/puma.rb +5 -5
  31. data/example/config/routes.rb +3 -0
  32. data/example/config/spring.rb +4 -4
  33. data/example/db/migrate/20200311152021_create_people.rb +0 -1
  34. data/example/db/migrate/20200603172347_create_posts.rb +11 -0
  35. data/example/db/schema.rb +16 -7
  36. data/example/spec/rails_helper.rb +64 -0
  37. data/example/spec/requests/people/create_spec.rb +52 -0
  38. data/example/spec/requests/people/get_spec.rb +35 -0
  39. data/example/spec/requests/people/index_spec.rb +69 -0
  40. data/example/spec/spec_helper.rb +94 -0
  41. data/lib/sober_swag.rb +6 -3
  42. data/lib/sober_swag/compiler/error.rb +2 -0
  43. data/lib/sober_swag/compiler/path.rb +2 -5
  44. data/lib/sober_swag/compiler/paths.rb +0 -1
  45. data/lib/sober_swag/compiler/type.rb +28 -15
  46. data/lib/sober_swag/controller.rb +16 -11
  47. data/lib/sober_swag/controller/route.rb +18 -21
  48. data/lib/sober_swag/controller/undefined_body_error.rb +3 -0
  49. data/lib/sober_swag/controller/undefined_path_error.rb +3 -0
  50. data/lib/sober_swag/controller/undefined_query_error.rb +3 -0
  51. data/lib/sober_swag/input_object.rb +28 -0
  52. data/lib/sober_swag/nodes/array.rb +1 -1
  53. data/lib/sober_swag/nodes/base.rb +2 -4
  54. data/lib/sober_swag/nodes/binary.rb +2 -1
  55. data/lib/sober_swag/nodes/enum.rb +4 -2
  56. data/lib/sober_swag/nodes/list.rb +0 -1
  57. data/lib/sober_swag/nodes/primitive.rb +6 -5
  58. data/lib/sober_swag/output_object.rb +102 -0
  59. data/lib/sober_swag/output_object/definition.rb +30 -0
  60. data/lib/sober_swag/{blueprint → output_object}/field.rb +14 -4
  61. data/lib/sober_swag/{blueprint → output_object}/field_syntax.rb +1 -1
  62. data/lib/sober_swag/{blueprint → output_object}/view.rb +15 -6
  63. data/lib/sober_swag/parser.rb +5 -3
  64. data/lib/sober_swag/serializer.rb +5 -2
  65. data/lib/sober_swag/serializer/array.rb +12 -0
  66. data/lib/sober_swag/serializer/base.rb +50 -1
  67. data/lib/sober_swag/serializer/conditional.rb +15 -2
  68. data/lib/sober_swag/serializer/field_list.rb +29 -6
  69. data/lib/sober_swag/serializer/mapped.rb +12 -2
  70. data/lib/sober_swag/serializer/meta.rb +35 -0
  71. data/lib/sober_swag/serializer/optional.rb +17 -2
  72. data/lib/sober_swag/serializer/primitive.rb +4 -1
  73. data/lib/sober_swag/server.rb +83 -0
  74. data/lib/sober_swag/types.rb +3 -0
  75. data/lib/sober_swag/version.rb +1 -1
  76. data/sober_swag.gemspec +6 -4
  77. metadata +77 -44
  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
@@ -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,11 +1,14 @@
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
10
13
  when Dry::Types::Array::Member
11
14
  Nodes::List.new(bind(Parser.new(@node.member)))
@@ -41,7 +44,7 @@ module SoberSwag
41
44
  bind(Parser.new(@node.type))
42
45
  when Dry::Types::Nominal
43
46
  # start off with the moral equivalent of NodeTree[String]
44
- Nodes::Primitive.new(@node.primitive)
47
+ Nodes::Primitive.new(@node.primitive, @node.meta)
45
48
  else
46
49
  # Inside of this case we have a class that is some user-defined type
47
50
  # We put it in our array of found types, and consider it a primitive
@@ -68,6 +71,5 @@ module SoberSwag
68
71
  @found += other.found
69
72
  result
70
73
  end
71
-
72
74
  end
73
75
  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) # rubocop:disable Naming/MethodName
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,18 @@ module SoberSwag
44
45
  left.type | right.type
45
46
  end
46
47
  end
48
+
49
+ def lazy_type
50
+ left.lazy_type | right.lazy_type
51
+ end
52
+
53
+ def lazy_type?
54
+ left.lazy_type? || right.lazy_type?
55
+ end
56
+
57
+ def finalize_lazy_type!
58
+ [left, right].each(&:finalize_lazy_type!)
59
+ end
47
60
  end
48
61
  end
49
62
  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,7 +3,6 @@ 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
@@ -13,6 +12,18 @@ module SoberSwag
13
12
  @base.serialize(@map_f.call(object), options)
14
13
  end
15
14
 
15
+ def lazy_type?
16
+ @base.lazy_type?
17
+ end
18
+
19
+ def lazy_type
20
+ @base.lazy_type
21
+ end
22
+
23
+ def finalize_lazy_type!
24
+ @base.finalize_lazy_type!
25
+ end
26
+
16
27
  def type
17
28
  @base.type
18
29
  end
@@ -23,7 +34,6 @@ module SoberSwag
23
34
  def via_map(&block)
24
35
  SoberSwag::Serializer::Mapped.new(@base, @map_f >> block)
25
36
  end
26
-
27
37
  end
28
38
  end
29
39
  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
@@ -1,13 +1,29 @@
1
1
  module SoberSwag
2
2
  module Serializer
3
+ ##
4
+ # Given something that serializes a type 'A',
5
+ # this can be used to make a serializer of type 'A | nil'.
6
+ #
7
+ # Or, put another way, makes serializers not crash on nil values.
3
8
  class Optional < Base
4
-
5
9
  def initialize(inner)
6
10
  @inner = inner
7
11
  end
8
12
 
9
13
  attr_reader :inner
10
14
 
15
+ def lazy_type?
16
+ @inner.lazy_type?
17
+ end
18
+
19
+ def lazy_type
20
+ @inner.lazy_type.optional
21
+ end
22
+
23
+ def finalize_lazy_type!
24
+ @inner.finalize_lazy_type!
25
+ end
26
+
11
27
  def serialize(object, options = {})
12
28
  if object.nil?
13
29
  object
@@ -23,7 +39,6 @@ module SoberSwag
23
39
  def optional(*)
24
40
  raise ArgumentError, 'no nesting optionals please'
25
41
  end
26
-
27
42
  end
28
43
  end
29
44
  end
@@ -1,5 +1,8 @@
1
1
  module SoberSwag
2
2
  module Serializer
3
+ ##
4
+ # A class that does *no* serialization: you give it a type,
5
+ # and it will pass any serialized input on verbatim.
3
6
  class Primitive < Base
4
7
  def initialize(type)
5
8
  @type = type
@@ -7,7 +10,7 @@ module SoberSwag
7
10
 
8
11
  attr_reader :type
9
12
 
10
- def serialize(object, options = {})
13
+ def serialize(object, _options = {})
11
14
  object
12
15
  end
13
16
  end
@@ -0,0 +1,83 @@
1
+ require 'set'
2
+
3
+ module SoberSwag
4
+ ##
5
+ # A basic, rack-only server to serve up swagger definitions.
6
+ # By default it is configured to work with rails, but you can pass stuff to initialize.
7
+ class Server
8
+ RAILS_CONTROLLER_PROC = proc do
9
+ Rails.application.routes.routes.map { |route|
10
+ route.defaults[:controller]
11
+ }.to_set.reject(&:nil?).map { |controller|
12
+ "#{controller}_controller".classify.constantize
13
+ }.filter { |controller| controller.ancestors.include?(SoberSwag::Controller) }
14
+ end
15
+
16
+ ##
17
+ # Start up.
18
+ #
19
+ # @param controller_proc [Proc] a proc that, when called, gives a list of {SoberSwag::Controller}s to document
20
+ # @param cache [Bool | Proc] if we should cache our defintions (default false)
21
+ def initialize(
22
+ controller_proc: RAILS_CONTROLLER_PROC,
23
+ cache: false
24
+ )
25
+ @controller_proc = controller_proc
26
+ @cache = cache
27
+ end
28
+
29
+ EFFECT_HTML = <<~HTML.freeze
30
+ <!DOCTYPE html>
31
+ <html>
32
+ <head>
33
+ <title>Swagger-UI</title>
34
+ <script src="https://unpkg.com/swagger-ui-dist@3/swagger-ui-bundle.js"></script>
35
+ <link rel="stylesheet" href="https://unpkg.com/swagger-ui-dist@3.23.4/swagger-ui.css"></link>
36
+ </head>
37
+ <body>
38
+ <div id="swagger">
39
+ </div>
40
+ <script>
41
+ SwaggerUIBundle({url: 'SCRIPT_NAME', dom_id: '#swagger'})
42
+ </script>
43
+ </body>
44
+ </html>
45
+ HTML
46
+
47
+ def call(env)
48
+ req = Rack::Request.new(env)
49
+ if req.path_info&.match?(/json/si) || req.get_header('Accept')&.match?(/json/si)
50
+ [200, { 'Content-Type' => 'application/json' }, [generate_json_string]]
51
+ else
52
+ [200, { 'Content-Type' => 'text/html' }, [EFFECT_HTML.gsub(/SCRIPT_NAME/, env['SCRIPT_NAME'] + '.json')]]
53
+ end
54
+ end
55
+
56
+ def generate_json_string
57
+ if cache?
58
+ @json_string ||= JSON.dump(generate_swagger)
59
+ else
60
+ JSON.dump(generate_swagger)
61
+ end
62
+ end
63
+
64
+ def cache?
65
+ @cache.respond_to?(:call) ? @cache.call : @cache
66
+ end
67
+
68
+ def generate_swagger
69
+ routes = sober_controllers.flat_map(&:defined_routes).reduce(SoberSwag::Compiler.new) { |c, r| c.add_route(r) }
70
+ {
71
+ openapi: '3.0.0',
72
+ info: {
73
+ version: '1',
74
+ title: 'SoberSwag Swagger'
75
+ }
76
+ }.merge(routes.to_swagger)
77
+ end
78
+
79
+ def sober_controllers
80
+ @controller_proc.call
81
+ end
82
+ end
83
+ end