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.
- checksums.yaml +7 -0
- data/.github/workflows/ruby.yml +33 -0
- data/.gitignore +11 -0
- data/.rspec +3 -0
- data/.rubocop.yml +7 -0
- data/.ruby-version +1 -0
- data/.travis.yml +7 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +92 -0
- data/LICENSE.txt +21 -0
- data/README.md +7 -0
- data/Rakefile +8 -0
- data/bin/console +38 -0
- data/bin/setup +8 -0
- data/example/.gitignore +24 -0
- data/example/.ruby-version +1 -0
- data/example/Gemfile +42 -0
- data/example/Gemfile.lock +212 -0
- data/example/README.md +24 -0
- data/example/Rakefile +6 -0
- data/example/app/controllers/application_controller.rb +2 -0
- data/example/app/controllers/concerns/.keep +0 -0
- data/example/app/controllers/people_controller.rb +74 -0
- data/example/app/jobs/application_job.rb +7 -0
- data/example/app/models/application_record.rb +3 -0
- data/example/app/models/concerns/.keep +0 -0
- data/example/app/models/person.rb +2 -0
- data/example/bin/bundle +114 -0
- data/example/bin/rails +9 -0
- data/example/bin/rake +9 -0
- data/example/bin/setup +33 -0
- data/example/bin/spring +17 -0
- data/example/config/application.rb +37 -0
- data/example/config/boot.rb +4 -0
- data/example/config/credentials.yml.enc +1 -0
- data/example/config/database.yml +25 -0
- data/example/config/environment.rb +5 -0
- data/example/config/environments/development.rb +44 -0
- data/example/config/environments/production.rb +91 -0
- data/example/config/environments/test.rb +38 -0
- data/example/config/initializers/application_controller_renderer.rb +8 -0
- data/example/config/initializers/backtrace_silencers.rb +7 -0
- data/example/config/initializers/cors.rb +16 -0
- data/example/config/initializers/filter_parameter_logging.rb +4 -0
- data/example/config/initializers/inflections.rb +16 -0
- data/example/config/initializers/mime_types.rb +4 -0
- data/example/config/initializers/wrap_parameters.rb +14 -0
- data/example/config/locales/en.yml +33 -0
- data/example/config/puma.rb +38 -0
- data/example/config/routes.rb +6 -0
- data/example/config/spring.rb +6 -0
- data/example/config.ru +5 -0
- data/example/db/migrate/20200311152021_create_people.rb +12 -0
- data/example/db/schema.rb +23 -0
- data/example/db/seeds.rb +7 -0
- data/example/lib/tasks/.keep +0 -0
- data/example/log/.keep +0 -0
- data/example/person.json +4 -0
- data/example/public/robots.txt +1 -0
- data/example/test/controllers/.keep +0 -0
- data/example/test/fixtures/.keep +0 -0
- data/example/test/fixtures/files/.keep +0 -0
- data/example/test/fixtures/people.yml +11 -0
- data/example/test/integration/.keep +0 -0
- data/example/test/models/.keep +0 -0
- data/example/test/models/person_test.rb +7 -0
- data/example/test/test_helper.rb +13 -0
- data/example/tmp/.keep +0 -0
- data/example/vendor/.keep +0 -0
- data/lib/sober_swag/blueprint/field.rb +36 -0
- data/lib/sober_swag/blueprint/field_syntax.rb +17 -0
- data/lib/sober_swag/blueprint/view.rb +44 -0
- data/lib/sober_swag/blueprint.rb +113 -0
- data/lib/sober_swag/compiler/error.rb +5 -0
- data/lib/sober_swag/compiler/path.rb +80 -0
- data/lib/sober_swag/compiler/paths.rb +54 -0
- data/lib/sober_swag/compiler/type.rb +235 -0
- data/lib/sober_swag/compiler.rb +107 -0
- data/lib/sober_swag/controller/route.rb +136 -0
- data/lib/sober_swag/controller/undefined_body_error.rb +6 -0
- data/lib/sober_swag/controller/undefined_path_error.rb +6 -0
- data/lib/sober_swag/controller/undefined_query_error.rb +6 -0
- data/lib/sober_swag/controller.rb +157 -0
- data/lib/sober_swag/nodes/array.rb +30 -0
- data/lib/sober_swag/nodes/attribute.rb +31 -0
- data/lib/sober_swag/nodes/base.rb +51 -0
- data/lib/sober_swag/nodes/binary.rb +44 -0
- data/lib/sober_swag/nodes/enum.rb +28 -0
- data/lib/sober_swag/nodes/list.rb +40 -0
- data/lib/sober_swag/nodes/nullable_primitive.rb +6 -0
- data/lib/sober_swag/nodes/object.rb +12 -0
- data/lib/sober_swag/nodes/one_of.rb +12 -0
- data/lib/sober_swag/nodes/primitive.rb +29 -0
- data/lib/sober_swag/nodes/sum.rb +6 -0
- data/lib/sober_swag/nodes.rb +20 -0
- data/lib/sober_swag/parser.rb +73 -0
- data/lib/sober_swag/path/integer.rb +21 -0
- data/lib/sober_swag/path/lit.rb +41 -0
- data/lib/sober_swag/path/literal.rb +29 -0
- data/lib/sober_swag/path/param.rb +33 -0
- data/lib/sober_swag/path.rb +8 -0
- data/lib/sober_swag/serializer/array.rb +21 -0
- data/lib/sober_swag/serializer/base.rb +38 -0
- data/lib/sober_swag/serializer/conditional.rb +49 -0
- data/lib/sober_swag/serializer/field_list.rb +44 -0
- data/lib/sober_swag/serializer/mapped.rb +29 -0
- data/lib/sober_swag/serializer/optional.rb +29 -0
- data/lib/sober_swag/serializer/primitive.rb +15 -0
- data/lib/sober_swag/serializer.rb +23 -0
- data/lib/sober_swag/types.rb +5 -0
- data/lib/sober_swag/version.rb +5 -0
- data/lib/sober_swag.rb +29 -0
- data/sober_swag.gemspec +40 -0
- 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,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
|