sober_swag 0.1.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 (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