funapi 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/.claude/25-09-01-OPENAPI_IMPLEMENTATION.md +233 -0
- data/.claude/25-09-05-RESPONSE_SCHEMA.md +383 -0
- data/.claude/25-09-10-OPENAPI_PLAN.md +219 -0
- data/.claude/25-10-26-MIDDLEWARE_IMPLEMENTATION.md +230 -0
- data/.claude/25-10-26-MIDDLEWARE_PLAN.md +353 -0
- data/.claude/25-10-27-BACKGROUND_TASKS_ANALYSIS.md +325 -0
- data/.claude/25-10-27-DEPENDENCY_IMPLEMENTATION_SUMMARY.md +325 -0
- data/.claude/25-10-27-DEPENDENCY_INJECTION_PLAN.md +753 -0
- data/.claude/25-12-24-LIFECYCLE_HOOKS_PLAN.md +421 -0
- data/.claude/25-12-24-PUBLISHING_AND_DOGFOODING_PLAN.md +327 -0
- data/.claude/25-12-24-TEMPLATE_RENDERING_PLAN.md +704 -0
- data/.claude/DECISIONS.md +397 -0
- data/.claude/PROJECT_PLAN.md +80 -0
- data/.claude/TESTING_PLAN.md +285 -0
- data/.claude/TESTING_STATUS.md +157 -0
- data/.tool-versions +1 -0
- data/AGENTS.md +416 -0
- data/CHANGELOG.md +5 -0
- data/CODE_OF_CONDUCT.md +132 -0
- data/LICENSE.txt +21 -0
- data/README.md +660 -0
- data/Rakefile +10 -0
- data/docs +8 -0
- data/docs-site/.gitignore +3 -0
- data/docs-site/Gemfile +9 -0
- data/docs-site/app.rb +138 -0
- data/docs-site/content/essential/handler.md +156 -0
- data/docs-site/content/essential/lifecycle.md +161 -0
- data/docs-site/content/essential/middleware.md +201 -0
- data/docs-site/content/essential/openapi.md +155 -0
- data/docs-site/content/essential/routing.md +123 -0
- data/docs-site/content/essential/validation.md +166 -0
- data/docs-site/content/getting-started/at-glance.md +82 -0
- data/docs-site/content/getting-started/key-concepts.md +150 -0
- data/docs-site/content/getting-started/quick-start.md +127 -0
- data/docs-site/content/index.md +81 -0
- data/docs-site/content/patterns/async-operations.md +137 -0
- data/docs-site/content/patterns/background-tasks.md +143 -0
- data/docs-site/content/patterns/database.md +175 -0
- data/docs-site/content/patterns/dependencies.md +141 -0
- data/docs-site/content/patterns/deployment.md +212 -0
- data/docs-site/content/patterns/error-handling.md +184 -0
- data/docs-site/content/patterns/response-schema.md +159 -0
- data/docs-site/content/patterns/templates.md +193 -0
- data/docs-site/content/patterns/testing.md +218 -0
- data/docs-site/mise.toml +2 -0
- data/docs-site/public/css/style.css +234 -0
- data/docs-site/templates/layouts/docs.html.erb +28 -0
- data/docs-site/templates/page.html.erb +3 -0
- data/docs-site/templates/partials/_nav.html.erb +19 -0
- data/examples/background_tasks_demo.rb +159 -0
- data/examples/demo_middleware.rb +55 -0
- data/examples/demo_openapi.rb +63 -0
- data/examples/dependency_block_demo.rb +150 -0
- data/examples/dependency_cleanup_demo.rb +146 -0
- data/examples/dependency_injection_demo.rb +200 -0
- data/examples/lifecycle_demo.rb +57 -0
- data/examples/middleware_demo.rb +74 -0
- data/examples/templates/layouts/application.html.erb +66 -0
- data/examples/templates/todos/_todo.html.erb +15 -0
- data/examples/templates/todos/index.html.erb +12 -0
- data/examples/templates_demo.rb +87 -0
- data/lib/funapi/application.rb +521 -0
- data/lib/funapi/async.rb +57 -0
- data/lib/funapi/background_tasks.rb +52 -0
- data/lib/funapi/config.rb +23 -0
- data/lib/funapi/database/sequel/fibered_connection_pool.rb +87 -0
- data/lib/funapi/dependency_wrapper.rb +66 -0
- data/lib/funapi/depends.rb +138 -0
- data/lib/funapi/exceptions.rb +72 -0
- data/lib/funapi/middleware/base.rb +13 -0
- data/lib/funapi/middleware/cors.rb +23 -0
- data/lib/funapi/middleware/request_logger.rb +32 -0
- data/lib/funapi/middleware/trusted_host.rb +34 -0
- data/lib/funapi/middleware.rb +4 -0
- data/lib/funapi/openapi/schema_converter.rb +85 -0
- data/lib/funapi/openapi/spec_generator.rb +179 -0
- data/lib/funapi/router.rb +43 -0
- data/lib/funapi/schema.rb +65 -0
- data/lib/funapi/server/falcon.rb +38 -0
- data/lib/funapi/template_response.rb +17 -0
- data/lib/funapi/templates.rb +111 -0
- data/lib/funapi/version.rb +5 -0
- data/lib/funapi.rb +14 -0
- data/sig/fun_api.rbs +499 -0
- metadata +220 -0
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
require_relative "schema_converter"
|
|
2
|
+
|
|
3
|
+
module FunApi
|
|
4
|
+
module OpenAPI
|
|
5
|
+
class SpecGenerator
|
|
6
|
+
def initialize(routes, info:)
|
|
7
|
+
@routes = routes
|
|
8
|
+
@info = info
|
|
9
|
+
@schemas = {}
|
|
10
|
+
@schema_counter = 0
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def generate
|
|
14
|
+
{
|
|
15
|
+
openapi: "3.0.3",
|
|
16
|
+
info: build_info,
|
|
17
|
+
paths: build_paths,
|
|
18
|
+
components: build_components
|
|
19
|
+
}
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def build_info
|
|
25
|
+
{
|
|
26
|
+
title: @info[:title],
|
|
27
|
+
version: @info[:version],
|
|
28
|
+
description: @info[:description]
|
|
29
|
+
}
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def build_paths
|
|
33
|
+
paths = {}
|
|
34
|
+
|
|
35
|
+
@routes.each do |route|
|
|
36
|
+
next if route.metadata[:internal]
|
|
37
|
+
|
|
38
|
+
path_template = convert_path_template(route.metadata[:path_template])
|
|
39
|
+
paths[path_template] ||= {}
|
|
40
|
+
|
|
41
|
+
operation = build_operation(route)
|
|
42
|
+
paths[path_template][route.verb.downcase] = operation
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
paths
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def build_operation(route)
|
|
49
|
+
operation = {}
|
|
50
|
+
|
|
51
|
+
parameters = []
|
|
52
|
+
parameters.concat(build_path_parameters(route))
|
|
53
|
+
parameters.concat(build_query_parameters(route))
|
|
54
|
+
|
|
55
|
+
operation[:parameters] = parameters unless parameters.empty?
|
|
56
|
+
operation[:requestBody] = build_request_body(route) if route.metadata[:body_schema]
|
|
57
|
+
operation[:responses] = build_responses(route)
|
|
58
|
+
|
|
59
|
+
operation
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def build_path_parameters(route)
|
|
63
|
+
route.keys.map do |key|
|
|
64
|
+
{
|
|
65
|
+
name: key,
|
|
66
|
+
in: "path",
|
|
67
|
+
required: true,
|
|
68
|
+
schema: {type: "string"}
|
|
69
|
+
}
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def build_query_parameters(route)
|
|
74
|
+
query_schema = route.metadata[:query_schema]
|
|
75
|
+
return [] unless query_schema
|
|
76
|
+
|
|
77
|
+
schema = unwrap_array_schema(query_schema)
|
|
78
|
+
json_schema = SchemaConverter.to_json_schema(schema)
|
|
79
|
+
return [] unless json_schema && json_schema[:properties]
|
|
80
|
+
|
|
81
|
+
required_fields = json_schema[:required] || []
|
|
82
|
+
|
|
83
|
+
json_schema[:properties].map do |name, prop_schema|
|
|
84
|
+
{
|
|
85
|
+
name: name.to_s,
|
|
86
|
+
in: "query",
|
|
87
|
+
required: required_fields.include?(name.to_s),
|
|
88
|
+
schema: prop_schema
|
|
89
|
+
}
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def build_request_body(route)
|
|
94
|
+
body_schema = route.metadata[:body_schema]
|
|
95
|
+
return nil unless body_schema
|
|
96
|
+
|
|
97
|
+
schema_ref = register_schema(body_schema, route.verb, route.metadata[:path_template])
|
|
98
|
+
|
|
99
|
+
{
|
|
100
|
+
required: true,
|
|
101
|
+
content: {
|
|
102
|
+
"application/json": {
|
|
103
|
+
schema: schema_ref
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def build_responses(route)
|
|
110
|
+
response_schema = route.metadata[:response_schema]
|
|
111
|
+
|
|
112
|
+
if response_schema
|
|
113
|
+
schema_ref = register_schema(response_schema, "#{route.verb}Response", route.metadata[:path_template])
|
|
114
|
+
|
|
115
|
+
{
|
|
116
|
+
"200": {
|
|
117
|
+
description: "Successful response",
|
|
118
|
+
content: {
|
|
119
|
+
"application/json": {
|
|
120
|
+
schema: schema_ref
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
else
|
|
126
|
+
{
|
|
127
|
+
"200": {
|
|
128
|
+
description: "Successful response"
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def register_schema(schema, verb, path)
|
|
135
|
+
schema_obj = unwrap_array_schema(schema)
|
|
136
|
+
is_array = schema.is_a?(Array)
|
|
137
|
+
|
|
138
|
+
schema_name = SchemaConverter.extract_schema_name(schema_obj)
|
|
139
|
+
|
|
140
|
+
unless schema_name
|
|
141
|
+
@schema_counter += 1
|
|
142
|
+
method_name = path.split("/").reject(&:empty?).map do |s|
|
|
143
|
+
s.start_with?(":") ? s[1..].capitalize : s.capitalize
|
|
144
|
+
end.join
|
|
145
|
+
schema_name = "#{method_name}#{verb.capitalize}Schema#{@schema_counter}"
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
@schemas[schema_name] = SchemaConverter.to_json_schema(schema_obj)
|
|
149
|
+
|
|
150
|
+
if is_array
|
|
151
|
+
{
|
|
152
|
+
type: "array",
|
|
153
|
+
items: {"$ref": "#/components/schemas/#{schema_name}"}
|
|
154
|
+
}
|
|
155
|
+
else
|
|
156
|
+
{"$ref": "#/components/schemas/#{schema_name}"}
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def build_components
|
|
161
|
+
{
|
|
162
|
+
schemas: @schemas
|
|
163
|
+
}
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def convert_path_template(path)
|
|
167
|
+
path.gsub(/:(\w+)/, '{\1}')
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def unwrap_array_schema(schema)
|
|
171
|
+
if schema.is_a?(Array) && schema.length == 1
|
|
172
|
+
schema.first
|
|
173
|
+
else
|
|
174
|
+
schema
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module FunApi
|
|
4
|
+
class Router
|
|
5
|
+
Route = Struct.new(:verb, :pattern, :keys, :handler, :metadata)
|
|
6
|
+
|
|
7
|
+
def initialize
|
|
8
|
+
@routes = []
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
attr_reader :routes
|
|
12
|
+
|
|
13
|
+
def add(verb, path, metadata: {}, &handler)
|
|
14
|
+
keys = []
|
|
15
|
+
|
|
16
|
+
regex = if path == "/"
|
|
17
|
+
"/"
|
|
18
|
+
else
|
|
19
|
+
path.split("/").map do |seg|
|
|
20
|
+
if seg.start_with?(":")
|
|
21
|
+
keys << seg.delete_prefix(":")
|
|
22
|
+
"([^/]+)"
|
|
23
|
+
else
|
|
24
|
+
Regexp.escape(seg)
|
|
25
|
+
end
|
|
26
|
+
end.join("/")
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
route_metadata = metadata.merge(path_template: path)
|
|
30
|
+
@routes << Route.new(verb.upcase, /\A#{regex}\z/, keys, handler, route_metadata)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def call(env)
|
|
34
|
+
req = Rack::Request.new(env)
|
|
35
|
+
route = @routes.find { |r| r.verb == req.request_method && r.pattern =~ req.path_info }
|
|
36
|
+
return [404, {"content-type" => "application/json"}, [JSON.dump(error: "Not found")]] unless route
|
|
37
|
+
|
|
38
|
+
match = route.pattern.match(req.path_info)
|
|
39
|
+
params = route.keys.zip(match.captures).to_h
|
|
40
|
+
route.handler.call(req, params)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
require "dry-schema"
|
|
2
|
+
|
|
3
|
+
module FunApi
|
|
4
|
+
class Schema
|
|
5
|
+
def self.define(&block)
|
|
6
|
+
Dry::Schema.Params(&block)
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def self.validate(schema, data, location: "body")
|
|
10
|
+
return data unless schema
|
|
11
|
+
|
|
12
|
+
if schema.is_a?(Array) && schema.length == 1
|
|
13
|
+
item_schema = schema.first
|
|
14
|
+
data_array = data.is_a?(Array) ? data : []
|
|
15
|
+
|
|
16
|
+
results = data_array.map do |item|
|
|
17
|
+
result = item_schema.call(item || {})
|
|
18
|
+
raise ValidationError.new(errors: result.errors) unless result.success?
|
|
19
|
+
|
|
20
|
+
result.to_h
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
return results
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
result = schema.call(data || {})
|
|
27
|
+
raise ValidationError.new(errors: result.errors) unless result.success?
|
|
28
|
+
|
|
29
|
+
result.to_h
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def self.validate_response(schema, data)
|
|
33
|
+
return data unless schema
|
|
34
|
+
|
|
35
|
+
if schema.is_a?(Array) && schema.length == 1
|
|
36
|
+
item_schema = schema.first
|
|
37
|
+
data_array = data.is_a?(Array) ? data : []
|
|
38
|
+
|
|
39
|
+
return data_array.map do |item|
|
|
40
|
+
result = item_schema.call(item)
|
|
41
|
+
|
|
42
|
+
unless result.success?
|
|
43
|
+
raise HTTPException.new(
|
|
44
|
+
status_code: 500,
|
|
45
|
+
detail: "Response validation failed: #{result.errors.to_h}"
|
|
46
|
+
)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
result.to_h
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
result = schema.call(data)
|
|
54
|
+
|
|
55
|
+
unless result.success?
|
|
56
|
+
raise HTTPException.new(
|
|
57
|
+
status_code: 500,
|
|
58
|
+
detail: "Response validation failed: #{result.errors.to_h}"
|
|
59
|
+
)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
result.to_h
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "falcon"
|
|
4
|
+
require "protocol/rack"
|
|
5
|
+
|
|
6
|
+
module FunApi
|
|
7
|
+
module Server
|
|
8
|
+
# Falcon server adapter - uses protocol-rack for proper Rack integration
|
|
9
|
+
class Falcon
|
|
10
|
+
def self.start(app, host: "0.0.0.0", port: 3000)
|
|
11
|
+
Async do |task|
|
|
12
|
+
falcon_app = Protocol::Rack::Adapter.new(app)
|
|
13
|
+
endpoint = ::Async::HTTP::Endpoint.parse("http://#{host}:#{port}").with(protocols: Async::HTTP::Protocol::HTTP2)
|
|
14
|
+
|
|
15
|
+
server = ::Falcon::Server.new(falcon_app, endpoint)
|
|
16
|
+
|
|
17
|
+
app.run_startup_hooks if app.respond_to?(:run_startup_hooks)
|
|
18
|
+
|
|
19
|
+
puts "Falcon listening on #{host}:#{port}"
|
|
20
|
+
puts "Try: curl http://#{host}:#{port}/hello"
|
|
21
|
+
puts "Press Ctrl+C to stop"
|
|
22
|
+
|
|
23
|
+
shutdown = -> {
|
|
24
|
+
puts "\nShutting down..."
|
|
25
|
+
app.run_shutdown_hooks if app.respond_to?(:run_shutdown_hooks)
|
|
26
|
+
task.stop
|
|
27
|
+
exit
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
trap(:INT) { shutdown.call }
|
|
31
|
+
trap(:TERM) { shutdown.call }
|
|
32
|
+
|
|
33
|
+
server.run
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module FunApi
|
|
4
|
+
class TemplateResponse
|
|
5
|
+
attr_reader :body, :status, :headers
|
|
6
|
+
|
|
7
|
+
def initialize(body, status: 200, headers: {})
|
|
8
|
+
@body = body
|
|
9
|
+
@status = status
|
|
10
|
+
@headers = {"content-type" => "text/html; charset=utf-8"}.merge(headers)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def to_response
|
|
14
|
+
[status, headers, [body]]
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "erb"
|
|
4
|
+
require "pathname"
|
|
5
|
+
require_relative "template_response"
|
|
6
|
+
require_relative "exceptions"
|
|
7
|
+
|
|
8
|
+
module FunApi
|
|
9
|
+
class Templates
|
|
10
|
+
def initialize(directory:, layout: nil)
|
|
11
|
+
@directory = Pathname.new(directory)
|
|
12
|
+
@layout = layout
|
|
13
|
+
@cache = {}
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def with_layout(layout)
|
|
17
|
+
ScopedTemplates.new(self, layout)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def render(name, layout: nil, **context)
|
|
21
|
+
content = render_template(name, **context)
|
|
22
|
+
|
|
23
|
+
layout_to_use = determine_layout(layout)
|
|
24
|
+
if layout_to_use
|
|
25
|
+
render_with_layout(layout_to_use, content, **context)
|
|
26
|
+
else
|
|
27
|
+
content
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def response(name, status: 200, headers: {}, layout: nil, **context)
|
|
32
|
+
html = render(name, layout: layout, **context)
|
|
33
|
+
TemplateResponse.new(html, status: status, headers: headers)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def render_partial(name, **context)
|
|
37
|
+
render_template(name, **context)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
def determine_layout(layout_override)
|
|
43
|
+
return nil if layout_override == false
|
|
44
|
+
return layout_override if layout_override
|
|
45
|
+
|
|
46
|
+
@layout
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def render_with_layout(layout_name, content, **context)
|
|
50
|
+
template = load_template(layout_name)
|
|
51
|
+
template_context = TemplateContext.new(self, context, content: content)
|
|
52
|
+
template.result(template_context.get_binding)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def render_template(name, **context)
|
|
56
|
+
template = load_template(name)
|
|
57
|
+
template_context = TemplateContext.new(self, context)
|
|
58
|
+
template.result(template_context.get_binding)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def load_template(name)
|
|
62
|
+
path = @directory.join(name)
|
|
63
|
+
raise TemplateNotFoundError.new(name) unless path.exist?
|
|
64
|
+
|
|
65
|
+
@cache[name] ||= ERB.new(path.read, trim_mode: "-")
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
class TemplateContext
|
|
70
|
+
def initialize(templates, context, content: nil)
|
|
71
|
+
@templates = templates
|
|
72
|
+
@content = content
|
|
73
|
+
context.each do |key, value|
|
|
74
|
+
define_singleton_method(key) { value }
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def render_partial(name, **context)
|
|
79
|
+
@templates.render_partial(name, **context)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def yield_content
|
|
83
|
+
@content
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def get_binding
|
|
87
|
+
binding
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
class ScopedTemplates
|
|
92
|
+
def initialize(templates, layout)
|
|
93
|
+
@templates = templates
|
|
94
|
+
@layout = layout
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def render(name, layout: nil, **context)
|
|
98
|
+
effective_layout = layout.nil? ? @layout : layout
|
|
99
|
+
@templates.render(name, layout: effective_layout, **context)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def response(name, status: 200, headers: {}, layout: nil, **context)
|
|
103
|
+
effective_layout = layout.nil? ? @layout : layout
|
|
104
|
+
@templates.response(name, status: status, headers: headers, layout: effective_layout, **context)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def render_partial(name, **context)
|
|
108
|
+
@templates.render_partial(name, **context)
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
data/lib/funapi.rb
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "funapi/version"
|
|
4
|
+
require_relative "funapi/exceptions"
|
|
5
|
+
require_relative "funapi/schema"
|
|
6
|
+
require_relative "funapi/depends"
|
|
7
|
+
require_relative "funapi/dependency_wrapper"
|
|
8
|
+
require_relative "funapi/async"
|
|
9
|
+
require_relative "funapi/router"
|
|
10
|
+
require_relative "funapi/application"
|
|
11
|
+
|
|
12
|
+
module FunApi
|
|
13
|
+
class Error < StandardError; end
|
|
14
|
+
end
|