sober_swag 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (114) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/ruby.yml +33 -0
  3. data/.gitignore +11 -0
  4. data/.rspec +3 -0
  5. data/.rubocop.yml +7 -0
  6. data/.ruby-version +1 -0
  7. data/.travis.yml +7 -0
  8. data/Gemfile +6 -0
  9. data/Gemfile.lock +92 -0
  10. data/LICENSE.txt +21 -0
  11. data/README.md +7 -0
  12. data/Rakefile +8 -0
  13. data/bin/console +38 -0
  14. data/bin/setup +8 -0
  15. data/example/.gitignore +24 -0
  16. data/example/.ruby-version +1 -0
  17. data/example/Gemfile +42 -0
  18. data/example/Gemfile.lock +212 -0
  19. data/example/README.md +24 -0
  20. data/example/Rakefile +6 -0
  21. data/example/app/controllers/application_controller.rb +2 -0
  22. data/example/app/controllers/concerns/.keep +0 -0
  23. data/example/app/controllers/people_controller.rb +74 -0
  24. data/example/app/jobs/application_job.rb +7 -0
  25. data/example/app/models/application_record.rb +3 -0
  26. data/example/app/models/concerns/.keep +0 -0
  27. data/example/app/models/person.rb +2 -0
  28. data/example/bin/bundle +114 -0
  29. data/example/bin/rails +9 -0
  30. data/example/bin/rake +9 -0
  31. data/example/bin/setup +33 -0
  32. data/example/bin/spring +17 -0
  33. data/example/config/application.rb +37 -0
  34. data/example/config/boot.rb +4 -0
  35. data/example/config/credentials.yml.enc +1 -0
  36. data/example/config/database.yml +25 -0
  37. data/example/config/environment.rb +5 -0
  38. data/example/config/environments/development.rb +44 -0
  39. data/example/config/environments/production.rb +91 -0
  40. data/example/config/environments/test.rb +38 -0
  41. data/example/config/initializers/application_controller_renderer.rb +8 -0
  42. data/example/config/initializers/backtrace_silencers.rb +7 -0
  43. data/example/config/initializers/cors.rb +16 -0
  44. data/example/config/initializers/filter_parameter_logging.rb +4 -0
  45. data/example/config/initializers/inflections.rb +16 -0
  46. data/example/config/initializers/mime_types.rb +4 -0
  47. data/example/config/initializers/wrap_parameters.rb +14 -0
  48. data/example/config/locales/en.yml +33 -0
  49. data/example/config/puma.rb +38 -0
  50. data/example/config/routes.rb +6 -0
  51. data/example/config/spring.rb +6 -0
  52. data/example/config.ru +5 -0
  53. data/example/db/migrate/20200311152021_create_people.rb +12 -0
  54. data/example/db/schema.rb +23 -0
  55. data/example/db/seeds.rb +7 -0
  56. data/example/lib/tasks/.keep +0 -0
  57. data/example/log/.keep +0 -0
  58. data/example/person.json +4 -0
  59. data/example/public/robots.txt +1 -0
  60. data/example/test/controllers/.keep +0 -0
  61. data/example/test/fixtures/.keep +0 -0
  62. data/example/test/fixtures/files/.keep +0 -0
  63. data/example/test/fixtures/people.yml +11 -0
  64. data/example/test/integration/.keep +0 -0
  65. data/example/test/models/.keep +0 -0
  66. data/example/test/models/person_test.rb +7 -0
  67. data/example/test/test_helper.rb +13 -0
  68. data/example/tmp/.keep +0 -0
  69. data/example/vendor/.keep +0 -0
  70. data/lib/sober_swag/blueprint/field.rb +36 -0
  71. data/lib/sober_swag/blueprint/field_syntax.rb +17 -0
  72. data/lib/sober_swag/blueprint/view.rb +44 -0
  73. data/lib/sober_swag/blueprint.rb +113 -0
  74. data/lib/sober_swag/compiler/error.rb +5 -0
  75. data/lib/sober_swag/compiler/path.rb +80 -0
  76. data/lib/sober_swag/compiler/paths.rb +54 -0
  77. data/lib/sober_swag/compiler/type.rb +235 -0
  78. data/lib/sober_swag/compiler.rb +107 -0
  79. data/lib/sober_swag/controller/route.rb +136 -0
  80. data/lib/sober_swag/controller/undefined_body_error.rb +6 -0
  81. data/lib/sober_swag/controller/undefined_path_error.rb +6 -0
  82. data/lib/sober_swag/controller/undefined_query_error.rb +6 -0
  83. data/lib/sober_swag/controller.rb +157 -0
  84. data/lib/sober_swag/nodes/array.rb +30 -0
  85. data/lib/sober_swag/nodes/attribute.rb +31 -0
  86. data/lib/sober_swag/nodes/base.rb +51 -0
  87. data/lib/sober_swag/nodes/binary.rb +44 -0
  88. data/lib/sober_swag/nodes/enum.rb +28 -0
  89. data/lib/sober_swag/nodes/list.rb +40 -0
  90. data/lib/sober_swag/nodes/nullable_primitive.rb +6 -0
  91. data/lib/sober_swag/nodes/object.rb +12 -0
  92. data/lib/sober_swag/nodes/one_of.rb +12 -0
  93. data/lib/sober_swag/nodes/primitive.rb +29 -0
  94. data/lib/sober_swag/nodes/sum.rb +6 -0
  95. data/lib/sober_swag/nodes.rb +20 -0
  96. data/lib/sober_swag/parser.rb +73 -0
  97. data/lib/sober_swag/path/integer.rb +21 -0
  98. data/lib/sober_swag/path/lit.rb +41 -0
  99. data/lib/sober_swag/path/literal.rb +29 -0
  100. data/lib/sober_swag/path/param.rb +33 -0
  101. data/lib/sober_swag/path.rb +8 -0
  102. data/lib/sober_swag/serializer/array.rb +21 -0
  103. data/lib/sober_swag/serializer/base.rb +38 -0
  104. data/lib/sober_swag/serializer/conditional.rb +49 -0
  105. data/lib/sober_swag/serializer/field_list.rb +44 -0
  106. data/lib/sober_swag/serializer/mapped.rb +29 -0
  107. data/lib/sober_swag/serializer/optional.rb +29 -0
  108. data/lib/sober_swag/serializer/primitive.rb +15 -0
  109. data/lib/sober_swag/serializer.rb +23 -0
  110. data/lib/sober_swag/types.rb +5 -0
  111. data/lib/sober_swag/version.rb +5 -0
  112. data/lib/sober_swag.rb +29 -0
  113. data/sober_swag.gemspec +40 -0
  114. metadata +269 -0
@@ -0,0 +1,107 @@
1
+ module SoberSwag
2
+ ##
3
+ # Compiler for an entire API.
4
+ #
5
+ # This compiler has a *lot* of state as we need to get
6
+ class Compiler
7
+ autoload(:Type, 'sober_swag/compiler/type')
8
+ autoload(:Error, 'sober_swag/compiler/error')
9
+ autoload(:Path, 'sober_swag/compiler/path')
10
+ autoload(:Paths, 'sober_swag/compiler/paths')
11
+
12
+ def initialize
13
+ @types = Set.new
14
+ @paths = Paths.new
15
+ end
16
+
17
+ ##
18
+ # Convert a compiler to the overall type definition.
19
+ def to_swagger
20
+ {
21
+ paths: path_schemas,
22
+ components: {
23
+ schemas: object_schemas
24
+ }
25
+ }
26
+ end
27
+
28
+ ##
29
+ # Add a path to be compiled.
30
+ # @param route [SoberSwag::Controller::Route] the route to add.
31
+ def add_route(route)
32
+ tap { @paths.add_route(route) }
33
+ end
34
+
35
+ def object_schemas
36
+ @types.map { |v| [v.ref_name, v.object_schema] }.to_h
37
+ end
38
+
39
+ ##
40
+ # The path section of the swagger schema.
41
+ def path_schemas
42
+ @paths.paths_list(self)
43
+ end
44
+
45
+ ##
46
+ # Compile a type to a new, path-params list.
47
+ # This will add all subtypes to the found types list.
48
+ def path_params_for(type)
49
+ with_types_discovered(type).path_schema
50
+ end
51
+
52
+ ##
53
+ # Get the query params list for a type.
54
+ # All found types will be added to the reference dictionary.
55
+ def query_params_for(type)
56
+ with_types_discovered(type).query_schema
57
+ end
58
+
59
+ ##
60
+ # Get the request body definition for a type.
61
+ # This will always be a ref.
62
+ def body_for(type)
63
+ add_type(type)
64
+ Type.new(type).schema_stub
65
+ end
66
+
67
+ ##
68
+ # Get the definition of a response type
69
+ def response_for(type)
70
+ body_for(type)
71
+ end
72
+
73
+ ##
74
+ # Get the existing schema for a given type
75
+ def schema_for(type)
76
+ @types.find { |type_comp| type_comp.type == type }&.object_schema
77
+ end
78
+
79
+ ##
80
+ # Add a type in the types reference dictionary, essentially
81
+ def add_type(type)
82
+ # use tap here to avoid an explicit self at the end of this
83
+ # which makes this method chainable
84
+ tap do
85
+ type_compiler = Type.new(type)
86
+
87
+ ##
88
+ # Do nothing if we already have a type
89
+ return self if @types.include?(type_compiler)
90
+
91
+ @types.add(type_compiler) if type_compiler.standalone?
92
+
93
+ type_compiler.found_types.each do |ft|
94
+ add_type(ft)
95
+ end
96
+ end
97
+ end
98
+
99
+ private
100
+
101
+ def with_types_discovered(type)
102
+ Type.new(type).tap do |type_compiler|
103
+ type_compiler.found_types.each { |ft| add_type(ft) }
104
+ end
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,136 @@
1
+ require 'sober_swag/blueprint'
2
+
3
+ module SoberSwag
4
+ module Controller
5
+ class Route
6
+
7
+ def initialize(method, action_name, path)
8
+ @method = method
9
+ @path = path
10
+ @action_name = action_name
11
+ @response_serializers = {}
12
+ @response_descriptions = {}
13
+ end
14
+
15
+ attr_reader :response_serializers
16
+ attr_reader :response_descriptions
17
+ attr_reader :controller
18
+ attr_reader :method
19
+ attr_reader :path
20
+ attr_reader :action_name
21
+ ##
22
+ # What to parse the request body in to.
23
+ attr_reader :request_body_class
24
+ ##
25
+ # What to parse the request query_params in to
26
+ attr_reader :query_params_class
27
+
28
+ ##
29
+ # What to parse the path params into
30
+ attr_reader :path_params_class
31
+
32
+ ##
33
+ # 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 {Dry::Struct}.)
35
+ # If you want, you can also define utility methods in here
36
+ def request_body(base = Dry::Struct, &block)
37
+ @request_body_class = make_struct!(base, &block)
38
+ action_module.const_set('ResponseBody', @request_body_class)
39
+ end
40
+
41
+ ##
42
+ # Does this route have a body defined?
43
+ def request_body?
44
+ !request_body_class.nil?
45
+ end
46
+
47
+ ##
48
+ # Define the shape of the query_params parameters, using SoberSwag's type-definition scheme.
49
+ # The block passed is the body of the newly-defined type.
50
+ # You can also include a base type.
51
+ def query_params(base = Dry::Struct, &block)
52
+ @query_params_class = make_struct!(base, &block)
53
+ action_module.const_set('QueryParams', @query_params_class)
54
+ end
55
+
56
+ ##
57
+ # Does this route have query params defined?
58
+ def query_params?
59
+ !query_params_class.nil?
60
+ end
61
+
62
+ ##
63
+ # 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 {Dry::Struct}).
65
+ # Names of this should match the names in the path template originally passed to {SoberSwag::Controller::Route.new}
66
+ def path_params(base = Dry::Struct, &block)
67
+ @path_params_class = make_struct!(base, &block)
68
+ action_module.const_set('PathParams', @path_params_class)
69
+ end
70
+
71
+ ##
72
+ # Does this route have path params defined?
73
+ def path_params?
74
+ !path_params_class.nil?
75
+ end
76
+
77
+ ##
78
+ # Define the body of the action method in the controller.
79
+ def action(&body)
80
+ return @action if body.nil?
81
+
82
+ @action ||= body
83
+ end
84
+
85
+ def description(desc = nil)
86
+ return @description if desc.nil?
87
+
88
+ @description = desc
89
+ end
90
+
91
+ def summary(sum = nil)
92
+ return @summary if sum.nil?
93
+
94
+ @summary = sum
95
+ end
96
+
97
+ ##
98
+ # The container module for all the constants this will eventually define.
99
+ # Each class generated by this Route will be defined within this module.
100
+ def action_module
101
+ @action_module ||= Module.new
102
+ end
103
+
104
+ ##
105
+ # Define a serializer for a response with the given status code.
106
+ # You may either give a serializer you defined elsewhere, or define one inline as if passed to
107
+ # {SoberSwag::Blueprint.define}
108
+ def response(status_code, description, serializer = nil, &block)
109
+ status_key = Rack::Utils.status_code(status_code)
110
+
111
+ raise ArgumentError, 'Response defiend!' if @response_serializers.key?(status_key)
112
+
113
+ serializer ||= SoberSwag::Blueprint.define(&block)
114
+ response_module.const_set(status_code.to_s.classify, serializer)
115
+ @response_serializers[status_key] = serializer
116
+ @response_descriptions[status_key] = description
117
+ end
118
+
119
+ ##
120
+ # What you should call the module of this action in your controller
121
+ def action_module_name
122
+ action_name.to_s.classify
123
+ end
124
+
125
+ private
126
+
127
+ def response_module
128
+ @response_module ||= Module.new.tap { |m| action_module.const_set(:Response, m) }
129
+ end
130
+
131
+ def make_struct!(base, &block)
132
+ Class.new(base, &block).tap { |e| e.transform_keys(&:to_sym) if base == Dry::Struct }
133
+ end
134
+ end
135
+ end
136
+ end
@@ -0,0 +1,6 @@
1
+ module SoberSwag
2
+ module Controller
3
+ class UndefinedBodyError < Error
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,6 @@
1
+ module SoberSwag
2
+ module Controller
3
+ class UndefinedPathError < Error
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,6 @@
1
+ module SoberSwag
2
+ module Controller
3
+ class UndefinedQueryError < Error
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,157 @@
1
+ require 'active_support/concern'
2
+
3
+ module SoberSwag
4
+ ##
5
+ # Controller concern
6
+ module Controller
7
+ extend ActiveSupport::Concern
8
+
9
+ autoload :UndefinedBodyError, 'sober_swag/controller/undefined_body_error'
10
+ autoload :UndefinedPathError, 'sober_swag/controller/undefined_path_error'
11
+ autoload :UndefinedQueryError, 'sober_swag/controller/undefined_query_error'
12
+
13
+ module Types
14
+ include ::Dry::Types()
15
+ end
16
+
17
+ class_methods do
18
+ ##
19
+ # Define a new action with the given HTTP method, action name, and path.
20
+ # This will eventaully delegate to making an actual method on your controller,
21
+ # so you can use controllers as you wish with no harm.
22
+ #
23
+ # This method takes a block, evaluated in the context of a {SoberSwag::Controller::Route}.
24
+ # Used like:
25
+ # define(:get, :show, '/posts/{id}') do
26
+ # path_params do
27
+ # attribute :id, Types::Integer
28
+ # end
29
+ # action do
30
+ # @post = Post.find(parsed_path.id)
31
+ # render json: @post
32
+ # end
33
+ # end
34
+ #
35
+ # This will define an "action module" on this class to contain the generated types.
36
+ # In the above example, the following constants will be deifned on the controller:
37
+ # PostsController::Show # the container module for everything in this action
38
+ # PostsController::Show::PathParams # the dry-struct type for the path attribute.
39
+ # So, in the same controller, you can refer to Show::PathParams to get the type created by the 'path_params' block above.
40
+ def define(method, action, path, &block)
41
+ r = Route.new(method, action, path)
42
+ r.instance_eval(&block)
43
+ const_set(r.action_module_name, r.action_module)
44
+ defined_routes << r
45
+ define_method(action, r.action) if r.action
46
+ end
47
+
48
+ ##
49
+ # All the routes that this controller knows about.
50
+ def defined_routes
51
+ @defined_routes ||= []
52
+ end
53
+
54
+ ##
55
+ # Find a route with the given name.
56
+ def find_route(name)
57
+ defined_routes.find { |r| r.action_name.to_s == name.to_s }
58
+ end
59
+
60
+ ##
61
+ # A swagger definition for *this controller only*.
62
+ def swagger_info
63
+ @swagger_info ||=
64
+ begin
65
+ res = defined_routes.reduce(SoberSwag::Compiler.new) { |c, r| c.add_route(r) }
66
+ {
67
+ openapi: '3.0.0',
68
+ info: {
69
+ version: '1',
70
+ title: self.name
71
+ }
72
+ }.merge(res.to_swagger)
73
+ end
74
+ end
75
+ end
76
+
77
+ ##
78
+ # Action to get the singular swagger for this entire API.
79
+ def swagger
80
+ render json: self.class.swagger_info
81
+ end
82
+
83
+ ##
84
+ # Get the path parameters, parsed into the type you defined with {SoberSwag::Controller.define}
85
+ # @raise [UndefinedPathError] if there's no path params defined for this route
86
+ # @raise [Dry::Struct::Error] if we cannot convert the path params to the defined type.
87
+ def parsed_path
88
+ @parsed_path ||=
89
+ begin
90
+ r = current_action_def
91
+ raise UndefinedPathError unless r&.path_params_class
92
+ r.path_params_class.new(request.path_parameters)
93
+ end
94
+ end
95
+
96
+ ##
97
+ # Get the request body, parsed into the type you defined with {SoberSwag::Controller.define}.
98
+ # @raise [UndefinedBodyError] if there's no request body defined for this route
99
+ # @raise [Dry::Struct::Error] if we cannot convert the path params to the defined type.
100
+ def parsed_body
101
+ @parsed_body ||=
102
+ begin
103
+ r = current_action_def
104
+ raise UndefinedBodyError unless r&.request_body_class
105
+ r.request_body_class.new(body_params)
106
+ end
107
+ end
108
+
109
+ ##
110
+ # Get the query params, parsed into the type you defined with {SoberSwag::Controller.define}
111
+ # @raise [UndefinedQueryError] if there's no query params defined for this route
112
+ # @raise [Dry::Struct::Error] if we cannot convert the path params to the defined type.
113
+ def parsed_query
114
+ @parsed_query ||=
115
+ begin
116
+ r = current_action_def
117
+ raise UndefinedQueryError unless r&.query_params_class
118
+ r.query_params_class.new(request.query_parameters)
119
+ end
120
+ end
121
+
122
+ ##
123
+ # Respond with the serialized type that you defined for this route.
124
+ # @todo figure out how to specify views and other options for the serializer here
125
+ # @param status [Symbol] the HTTP status symbol to use for the status code
126
+ # @param entity the thing to serialize
127
+ def respond!(status, entity)
128
+ r = current_action_def
129
+ serializer = r.response_serializers[Rack::Utils.status_code(status)]
130
+ serializer ||= serializer.new if serializer.respond_to?(:new)
131
+ render json: serializer.serialize(entity)
132
+ end
133
+
134
+ ##
135
+ # Obtain a parameters hash of *only* those parameters which come in the hash.
136
+ # These will be *unsafe* in the sense that they will all be allowed.
137
+ # This kinda violates the "be liberal in what you accept" principle,
138
+ # but it keeps the docs honest: parameters sent in the body *must* be
139
+ # in the body.
140
+ def body_params
141
+ bparams = params.reject do |k, _|
142
+ request.query_parameters.key?(k) || request.path_parameters.key?(k)
143
+ end
144
+ bparams.permit(bparams.keys)
145
+ end
146
+
147
+ ##
148
+ # Get the action-definition for the current action.
149
+ # Under the hood, delegates to the `:action` key of rails params.
150
+ def current_action_def
151
+ self.class.find_route(params[:action])
152
+ end
153
+
154
+ end
155
+ end
156
+
157
+ require 'sober_swag/controller/route'
@@ -0,0 +1,30 @@
1
+ module SoberSwag
2
+ module Nodes
3
+ ##
4
+ # Base class for nodes that contain arrays of other nodes.
5
+ # This is very different from an attribute representing a node which *is* an array of some element type!!
6
+ class Array < Base
7
+ def initialize(elements)
8
+ @elements = elements
9
+ end
10
+
11
+ attr_reader :elements
12
+
13
+ def map(&block)
14
+ self.class.new(elements.map { |elem| elem.map(&block) })
15
+ end
16
+
17
+ def cata(&block)
18
+ block.call(self.class.new(elements.map { |elem| elem.cata(&block) }))
19
+ end
20
+
21
+ def deconstruct
22
+ @elements
23
+ end
24
+
25
+ def deconstruct_keys(keys)
26
+ { elements: @elements }
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,31 @@
1
+ module SoberSwag
2
+ module Nodes
3
+ ##
4
+ # One attribute of an object.
5
+ class Attribute
6
+ def initialize(key, required, value)
7
+ @key = key
8
+ @required = required
9
+ @value = value
10
+ end
11
+
12
+ def deconstruct
13
+ [key, required, value]
14
+ end
15
+
16
+ def deconstruct_keys
17
+ { key: key, required: required, value: value }
18
+ end
19
+
20
+ attr_reader :key, :required, :value
21
+
22
+ def map(&block)
23
+ self.class.new(key, required, value.map(&block))
24
+ end
25
+
26
+ def cata(&block)
27
+ block.call(self.class.new(key, required, value.cata(&block)))
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,51 @@
1
+ module SoberSwag
2
+ module Nodes
3
+ ##
4
+ # Base Node that all other nodes inherit from.
5
+ # All nodes should define the following:
6
+ #
7
+ # - #deconstruct, which returns an array of *everything needed to idenitfy the node.*
8
+ # We base comparisons on the result of deconstruction.
9
+ # - #deconstruct_keys, which returns a hash of *everything needed to identify the node*.
10
+ # We use this later.
11
+ class Base
12
+
13
+ include Comparable
14
+
15
+ ##
16
+ # Value-level comparison.
17
+ def <=>(other)
18
+ return other.class.name <=> self.class.name unless other.class == self.class
19
+
20
+ deconstruct <=> other.deconstruct
21
+ end
22
+
23
+ def eql?(other)
24
+ deconstruct == other.deconstruct
25
+ end
26
+
27
+ def hash
28
+ deconstruct.hash
29
+ end
30
+
31
+ ##
32
+ # Perform a catamorphism, or, a deep-first recursion.
33
+ #
34
+ # The basic way this works is deceptively simple: When you use 'cata' on a node,
35
+ # it will call the block you gave it with the *deepest* nodes in the tree first.
36
+ # It will then use the result of that block to reconstruct their *parent* node, and then
37
+ # *call cata again* on the parent, and so on until we reach the top.
38
+ #
39
+ # When working with these definition nodes, we very often want to transform something recursively.
40
+ # This method allows us to do so by focusing on a single level at a time, keeping the actual recursion *abstract*.
41
+ def cata(&block)
42
+ raise ArgumentError, 'Base is abstract'
43
+ end
44
+
45
+ def map(&block)
46
+ raise ArgumentError, 'Base is abstract'
47
+ end
48
+
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,44 @@
1
+ module SoberSwag
2
+ module Nodes
3
+ ##
4
+ # A
5
+ #
6
+ # It's cool I promise.
7
+ class Binary < Base
8
+ def initialize(lhs, rhs)
9
+ @lhs = lhs
10
+ @rhs = rhs
11
+ end
12
+
13
+ attr_reader :lhs, :rhs
14
+ ##
15
+ # Map the root values of the node.
16
+ # This just calls map on the lhs and the rhs
17
+ def map(&block)
18
+ self.class.new(
19
+ lhs.map(&block),
20
+ rhs.map(&block)
21
+ )
22
+ end
23
+
24
+ def deconstruct
25
+ [lhs, rhs]
26
+ end
27
+
28
+ def deconstruct_keys(keys)
29
+ { lhs: lhs, rhs: rhs }
30
+ end
31
+
32
+ ##
33
+ # Perform a catamorphism on this node.
34
+ def cata(&block)
35
+ block.call(
36
+ self.class.new(
37
+ lhs.cata(&block),
38
+ rhs.cata(&block)
39
+ )
40
+ )
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,28 @@
1
+ module SoberSwag
2
+ module Nodes
3
+ class Enum < Base
4
+
5
+ def initialize(values)
6
+ @values = values
7
+ end
8
+
9
+ attr_reader :values
10
+
11
+ def map(&block)
12
+ self.class.new(@values.map(&block))
13
+ end
14
+
15
+ def deconstruct
16
+ [values]
17
+ end
18
+
19
+ def deconstruct_keys(keys)
20
+ { values: values }
21
+ end
22
+
23
+ def cata(&block)
24
+ block.call(dup)
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,40 @@
1
+ module SoberSwag
2
+ module Nodes
3
+ ##
4
+ # A List of the contained element types.
5
+ #
6
+ # Unlike {SoberSwag::Nodes::Array}, this actually models arrays.
7
+ # The other one is a node that *is* an array in terms of what it contains.
8
+ # Kinda confusing, but oh well.
9
+ class List < Base
10
+ def initialize(element)
11
+ @element = element
12
+ end
13
+
14
+ attr_reader :element
15
+
16
+ def deconstruct
17
+ [element]
18
+ end
19
+
20
+ def deconstruct_keys(_)
21
+ { element: element }
22
+ end
23
+
24
+ def cata(&block)
25
+ block.call(
26
+ self.class.new(
27
+ element.cata(&block)
28
+ )
29
+ )
30
+ end
31
+
32
+ def map(&block)
33
+ self.class.new(
34
+ element.map(&block)
35
+ )
36
+ end
37
+
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,6 @@
1
+ module SoberSwag
2
+ module Nodes
3
+ class NullablePrimitive < Primitive
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,12 @@
1
+ module SoberSwag
2
+ module Nodes
3
+ ##
4
+ # Objects might have attribute keys, so they're
5
+ # basically a list of attributes
6
+ class Object < SoberSwag::Nodes::Array
7
+ def deconstruct_keys(_)
8
+ { attributes: @elements }
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,12 @@
1
+ module SoberSwag
2
+ module Nodes
3
+ ##
4
+ # Swagges uses an array of OneOf types, so we
5
+ # transform our sum nodes into this
6
+ class OneOf < ::SoberSwag::Nodes::Array
7
+ def deconstruct_keys(_)
8
+ { alternatives: @elemenets }
9
+ end
10
+ end
11
+ end
12
+ end