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
data/sig/fun_api.rbs
ADDED
|
@@ -0,0 +1,499 @@
|
|
|
1
|
+
# Type definitions for FunApi
|
|
2
|
+
# A minimal, async-first Ruby web framework inspired by FastAPI
|
|
3
|
+
|
|
4
|
+
module FunApi
|
|
5
|
+
VERSION: String
|
|
6
|
+
|
|
7
|
+
# Base error class for FunApi
|
|
8
|
+
class Error < StandardError
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
# Type aliases for common patterns
|
|
12
|
+
# Using permissive types for third-party integrations
|
|
13
|
+
|
|
14
|
+
# Rack environment hash
|
|
15
|
+
type rack_env = Hash[String, untyped]
|
|
16
|
+
|
|
17
|
+
# Standard Rack response: [status, headers, body]
|
|
18
|
+
type rack_response = [Integer, Hash[String, String], Array[String]]
|
|
19
|
+
|
|
20
|
+
# Route handler input hash with path params, query params, and body
|
|
21
|
+
type route_input = {
|
|
22
|
+
path: Hash[String, String],
|
|
23
|
+
query: Hash[Symbol, untyped],
|
|
24
|
+
body: untyped
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
# Route handler return value: [data, status_code]
|
|
28
|
+
type handler_return = [untyped, Integer]
|
|
29
|
+
|
|
30
|
+
# OpenAPI configuration
|
|
31
|
+
type openapi_config = {
|
|
32
|
+
title: String,
|
|
33
|
+
version: String,
|
|
34
|
+
description: String
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
# Dependency specification: array of symbols or hash mapping names to dependencies
|
|
38
|
+
type depends_spec = Array[Symbol] | Hash[Symbol, untyped] | nil
|
|
39
|
+
|
|
40
|
+
# The main application class - entry point for FunApi
|
|
41
|
+
#
|
|
42
|
+
# Example:
|
|
43
|
+
# app = FunApi::App.new(title: "My API") do |api|
|
|
44
|
+
# api.get '/hello' do |input, req, task|
|
|
45
|
+
# [{ message: 'Hello!' }, 200]
|
|
46
|
+
# end
|
|
47
|
+
# end
|
|
48
|
+
#
|
|
49
|
+
class App
|
|
50
|
+
attr_reader openapi_config: openapi_config
|
|
51
|
+
attr_reader container: untyped
|
|
52
|
+
attr_reader startup_hooks: Array[^() -> void]
|
|
53
|
+
attr_reader shutdown_hooks: Array[^() -> void]
|
|
54
|
+
|
|
55
|
+
# Create a new FunApi application
|
|
56
|
+
#
|
|
57
|
+
# @param title OpenAPI documentation title
|
|
58
|
+
# @param version API version string
|
|
59
|
+
# @param description API description for OpenAPI docs
|
|
60
|
+
# @yield [api] Configuration block receiving the app instance
|
|
61
|
+
def initialize: (?title: String, ?version: String, ?description: String) ?{ (App) -> void } -> void
|
|
62
|
+
|
|
63
|
+
# Register a startup hook to run before the server accepts requests
|
|
64
|
+
#
|
|
65
|
+
# Example:
|
|
66
|
+
# api.on_startup { DB.connect }
|
|
67
|
+
def on_startup: () { () -> void } -> self
|
|
68
|
+
|
|
69
|
+
# Register a shutdown hook to run when the server stops
|
|
70
|
+
#
|
|
71
|
+
# Example:
|
|
72
|
+
# api.on_shutdown { DB.disconnect }
|
|
73
|
+
def on_shutdown: () { () -> void } -> self
|
|
74
|
+
|
|
75
|
+
# Run all registered startup hooks
|
|
76
|
+
def run_startup_hooks: () -> void
|
|
77
|
+
|
|
78
|
+
# Run all registered shutdown hooks (errors are logged but don't stop other hooks)
|
|
79
|
+
def run_shutdown_hooks: () -> void
|
|
80
|
+
|
|
81
|
+
# Register a dependency in the container
|
|
82
|
+
#
|
|
83
|
+
# Example:
|
|
84
|
+
# api.register(:db) { Database.new }
|
|
85
|
+
# api.register(:logger) { Logger.new(STDOUT) }
|
|
86
|
+
def register: (Symbol key) { () -> untyped } -> void
|
|
87
|
+
|
|
88
|
+
# Resolve a dependency from the container
|
|
89
|
+
def resolve: (Symbol key) -> untyped
|
|
90
|
+
|
|
91
|
+
# Define a GET route
|
|
92
|
+
#
|
|
93
|
+
# @param path URL path pattern (e.g., '/users/:id')
|
|
94
|
+
# @param query Optional query parameter schema
|
|
95
|
+
# @param response_schema Optional response validation schema
|
|
96
|
+
# @param depends Dependencies to inject into the handler
|
|
97
|
+
# @yield [input, req, task] Route handler block
|
|
98
|
+
def get: (
|
|
99
|
+
String path,
|
|
100
|
+
?query: untyped,
|
|
101
|
+
?response_schema: untyped,
|
|
102
|
+
?depends: depends_spec
|
|
103
|
+
) { (route_input, untyped, untyped, **untyped) -> (handler_return | TemplateResponse) } -> void
|
|
104
|
+
|
|
105
|
+
# Define a POST route
|
|
106
|
+
#
|
|
107
|
+
# @param path URL path pattern
|
|
108
|
+
# @param body Optional request body schema
|
|
109
|
+
# @param query Optional query parameter schema
|
|
110
|
+
# @param response_schema Optional response validation schema
|
|
111
|
+
# @param depends Dependencies to inject into the handler
|
|
112
|
+
# @yield [input, req, task] Route handler block
|
|
113
|
+
def post: (
|
|
114
|
+
String path,
|
|
115
|
+
?body: untyped,
|
|
116
|
+
?query: untyped,
|
|
117
|
+
?response_schema: untyped,
|
|
118
|
+
?depends: depends_spec
|
|
119
|
+
) { (route_input, untyped, untyped, **untyped) -> (handler_return | TemplateResponse) } -> void
|
|
120
|
+
|
|
121
|
+
# Define a PUT route
|
|
122
|
+
def put: (
|
|
123
|
+
String path,
|
|
124
|
+
?body: untyped,
|
|
125
|
+
?query: untyped,
|
|
126
|
+
?response_schema: untyped,
|
|
127
|
+
?depends: depends_spec
|
|
128
|
+
) { (route_input, untyped, untyped, **untyped) -> (handler_return | TemplateResponse) } -> void
|
|
129
|
+
|
|
130
|
+
# Define a PATCH route
|
|
131
|
+
def patch: (
|
|
132
|
+
String path,
|
|
133
|
+
?body: untyped,
|
|
134
|
+
?query: untyped,
|
|
135
|
+
?response_schema: untyped,
|
|
136
|
+
?depends: depends_spec
|
|
137
|
+
) { (route_input, untyped, untyped, **untyped) -> (handler_return | TemplateResponse) } -> void
|
|
138
|
+
|
|
139
|
+
# Define a DELETE route
|
|
140
|
+
def delete: (
|
|
141
|
+
String path,
|
|
142
|
+
?query: untyped,
|
|
143
|
+
?response_schema: untyped,
|
|
144
|
+
?depends: depends_spec
|
|
145
|
+
) { (route_input, untyped, untyped, **untyped) -> (handler_return | TemplateResponse) } -> void
|
|
146
|
+
|
|
147
|
+
# Add Rack middleware to the stack
|
|
148
|
+
#
|
|
149
|
+
# Example:
|
|
150
|
+
# api.use Rack::Session::Cookie, secret: 'key'
|
|
151
|
+
def use: (Class middleware, *untyped args) ?{ () -> void } -> self
|
|
152
|
+
|
|
153
|
+
# Add CORS middleware with sensible defaults
|
|
154
|
+
#
|
|
155
|
+
# Example:
|
|
156
|
+
# api.add_cors(allow_origins: ['http://localhost:3000'])
|
|
157
|
+
def add_cors: (
|
|
158
|
+
?allow_origins: Array[String],
|
|
159
|
+
?allow_methods: Array[String],
|
|
160
|
+
?allow_headers: Array[String],
|
|
161
|
+
?expose_headers: Array[String],
|
|
162
|
+
?max_age: Integer,
|
|
163
|
+
?allow_credentials: bool
|
|
164
|
+
) -> void
|
|
165
|
+
|
|
166
|
+
# Add trusted host validation middleware
|
|
167
|
+
#
|
|
168
|
+
# Example:
|
|
169
|
+
# api.add_trusted_host(allowed_hosts: ['example.com', /\.example\.com$/])
|
|
170
|
+
def add_trusted_host: (allowed_hosts: Array[String | Regexp]) -> void
|
|
171
|
+
|
|
172
|
+
# Add request logging middleware
|
|
173
|
+
def add_request_logger: (?logger: untyped, ?level: Symbol) -> void
|
|
174
|
+
|
|
175
|
+
# Add gzip compression for JSON responses
|
|
176
|
+
def add_gzip: () -> void
|
|
177
|
+
|
|
178
|
+
# Rack interface - called by the web server
|
|
179
|
+
def call: (rack_env env) -> rack_response
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
# Schema validation using dry-schema
|
|
183
|
+
#
|
|
184
|
+
# Example:
|
|
185
|
+
# UserSchema = FunApi::Schema.define do
|
|
186
|
+
# required(:name).filled(:string)
|
|
187
|
+
# required(:email).filled(:string)
|
|
188
|
+
# end
|
|
189
|
+
#
|
|
190
|
+
class Schema
|
|
191
|
+
# Define a new validation schema using dry-schema DSL
|
|
192
|
+
#
|
|
193
|
+
# Returns a Dry::Schema::Params object
|
|
194
|
+
def self.define: () { () -> void } -> untyped
|
|
195
|
+
|
|
196
|
+
# Validate data against a schema, raising ValidationError on failure
|
|
197
|
+
#
|
|
198
|
+
# @param schema The schema to validate against (or array with single schema for arrays)
|
|
199
|
+
# @param data The data to validate
|
|
200
|
+
# @param location Error location prefix ('body', 'query')
|
|
201
|
+
# @return Validated and coerced data
|
|
202
|
+
def self.validate: (untyped schema, untyped data, ?location: String) -> untyped
|
|
203
|
+
|
|
204
|
+
# Validate response data, raising HTTPException on failure
|
|
205
|
+
def self.validate_response: (untyped schema, untyped data) -> untyped
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
# HTTP exception for returning error responses
|
|
209
|
+
#
|
|
210
|
+
# Example:
|
|
211
|
+
# raise FunApi::HTTPException.new(status_code: 404, detail: "User not found")
|
|
212
|
+
#
|
|
213
|
+
class HTTPException < StandardError
|
|
214
|
+
attr_reader status_code: Integer
|
|
215
|
+
attr_reader detail: String | Array[Hash[Symbol, untyped]]
|
|
216
|
+
attr_reader headers: Hash[String, String]
|
|
217
|
+
|
|
218
|
+
# Create an HTTP exception
|
|
219
|
+
#
|
|
220
|
+
# @param status_code HTTP status code (e.g., 404, 500)
|
|
221
|
+
# @param detail Error message or structured error details
|
|
222
|
+
# @param headers Additional response headers
|
|
223
|
+
def initialize: (status_code: Integer, ?detail: String?, ?headers: Hash[String, String]?) -> void
|
|
224
|
+
|
|
225
|
+
# Convert to Rack response format
|
|
226
|
+
def to_response: () -> rack_response
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
# Validation error with detailed field-level errors
|
|
230
|
+
#
|
|
231
|
+
# Automatically raised by Schema.validate on validation failure
|
|
232
|
+
#
|
|
233
|
+
class ValidationError < HTTPException
|
|
234
|
+
attr_reader errors: untyped
|
|
235
|
+
|
|
236
|
+
# Create a validation error from schema errors
|
|
237
|
+
def initialize: (errors: untyped, ?headers: Hash[String, String]?) -> void
|
|
238
|
+
|
|
239
|
+
def to_response: () -> rack_response
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
# Error raised when a template file is not found
|
|
243
|
+
class TemplateNotFoundError < StandardError
|
|
244
|
+
attr_reader template_name: String
|
|
245
|
+
|
|
246
|
+
def initialize: (String template_name) -> void
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
# ERB template renderer with layout and partial support
|
|
250
|
+
#
|
|
251
|
+
# Example:
|
|
252
|
+
# templates = FunApi::Templates.new(
|
|
253
|
+
# directory: 'templates',
|
|
254
|
+
# layout: 'layouts/application.html.erb'
|
|
255
|
+
# )
|
|
256
|
+
#
|
|
257
|
+
# api.get '/' do |input, req, task|
|
|
258
|
+
# templates.response('home.html.erb', title: 'Home')
|
|
259
|
+
# end
|
|
260
|
+
#
|
|
261
|
+
class Templates
|
|
262
|
+
# Create a new template renderer
|
|
263
|
+
#
|
|
264
|
+
# @param directory Path to templates directory
|
|
265
|
+
# @param layout Optional default layout template path
|
|
266
|
+
def initialize: (directory: String, ?layout: String?) -> void
|
|
267
|
+
|
|
268
|
+
# Create a scoped renderer with a different default layout
|
|
269
|
+
#
|
|
270
|
+
# Example:
|
|
271
|
+
# admin = templates.with_layout('layouts/admin.html.erb')
|
|
272
|
+
def with_layout: (String layout) -> ScopedTemplates
|
|
273
|
+
|
|
274
|
+
# Render a template to a string
|
|
275
|
+
#
|
|
276
|
+
# @param name Template file path relative to directory
|
|
277
|
+
# @param layout Override layout (false to disable, string to use different layout)
|
|
278
|
+
# @param context Template variables as keyword arguments
|
|
279
|
+
def render: (String name, ?layout: String | false | nil, **untyped context) -> String
|
|
280
|
+
|
|
281
|
+
# Render a template and wrap in TemplateResponse
|
|
282
|
+
#
|
|
283
|
+
# @param name Template file path
|
|
284
|
+
# @param status HTTP status code
|
|
285
|
+
# @param headers Additional response headers
|
|
286
|
+
# @param layout Override layout
|
|
287
|
+
# @param context Template variables
|
|
288
|
+
def response: (
|
|
289
|
+
String name,
|
|
290
|
+
?status: Integer,
|
|
291
|
+
?headers: Hash[String, String],
|
|
292
|
+
?layout: String | false | nil,
|
|
293
|
+
**untyped context
|
|
294
|
+
) -> TemplateResponse
|
|
295
|
+
|
|
296
|
+
# Render a partial template (no layout applied)
|
|
297
|
+
def render_partial: (String name, **untyped context) -> String
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
# Scoped template renderer with a fixed layout
|
|
301
|
+
class ScopedTemplates
|
|
302
|
+
def initialize: (Templates templates, String layout) -> void
|
|
303
|
+
|
|
304
|
+
def render: (String name, ?layout: String | false | nil, **untyped context) -> String
|
|
305
|
+
|
|
306
|
+
def response: (
|
|
307
|
+
String name,
|
|
308
|
+
?status: Integer,
|
|
309
|
+
?headers: Hash[String, String],
|
|
310
|
+
?layout: String | false | nil,
|
|
311
|
+
**untyped context
|
|
312
|
+
) -> TemplateResponse
|
|
313
|
+
|
|
314
|
+
def render_partial: (String name, **untyped context) -> String
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
# HTML response wrapper for template rendering
|
|
318
|
+
#
|
|
319
|
+
# Returned from Templates#response, signals the framework to return HTML
|
|
320
|
+
# instead of JSON
|
|
321
|
+
#
|
|
322
|
+
class TemplateResponse
|
|
323
|
+
attr_reader body: String
|
|
324
|
+
attr_reader status: Integer
|
|
325
|
+
attr_reader headers: Hash[String, String]
|
|
326
|
+
|
|
327
|
+
def initialize: (String body, ?status: Integer, ?headers: Hash[String, String]) -> void
|
|
328
|
+
|
|
329
|
+
# Convert to Rack response format
|
|
330
|
+
def to_response: () -> rack_response
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
# Background task manager for post-response execution
|
|
334
|
+
#
|
|
335
|
+
# Tasks run after the response is sent but before dependencies are cleaned up.
|
|
336
|
+
# Perfect for emails, logging, webhooks.
|
|
337
|
+
#
|
|
338
|
+
# Example:
|
|
339
|
+
# api.post '/signup' do |input, req, task, background:|
|
|
340
|
+
# user = create_user(input[:body])
|
|
341
|
+
# background.add_task(method(:send_welcome_email), user[:email])
|
|
342
|
+
# [{ user: user }, 201]
|
|
343
|
+
# end
|
|
344
|
+
#
|
|
345
|
+
class BackgroundTasks
|
|
346
|
+
def initialize: (untyped task) -> void
|
|
347
|
+
|
|
348
|
+
# Add a task to execute after response is sent
|
|
349
|
+
#
|
|
350
|
+
# @param callable A callable object (lambda, proc, method)
|
|
351
|
+
# @param args Positional arguments to pass to callable
|
|
352
|
+
# @param kwargs Keyword arguments to pass to callable
|
|
353
|
+
def add_task: (
|
|
354
|
+
^(*untyped, **untyped) -> void callable,
|
|
355
|
+
*untyped args,
|
|
356
|
+
**untyped kwargs
|
|
357
|
+
) -> nil
|
|
358
|
+
|
|
359
|
+
# Execute all queued tasks (called automatically by framework)
|
|
360
|
+
def execute: () -> void
|
|
361
|
+
|
|
362
|
+
# Check if there are any queued tasks
|
|
363
|
+
def empty?: () -> bool
|
|
364
|
+
|
|
365
|
+
# Number of queued tasks
|
|
366
|
+
def size: () -> Integer
|
|
367
|
+
end
|
|
368
|
+
|
|
369
|
+
# Dependency injection wrapper with sub-dependency support
|
|
370
|
+
#
|
|
371
|
+
# Example:
|
|
372
|
+
# db_dep = FunApi::Depends.new(->(req:) { DB.connect(req.env['tenant']) })
|
|
373
|
+
#
|
|
374
|
+
class Depends
|
|
375
|
+
attr_reader callable: ^(**untyped) -> untyped
|
|
376
|
+
attr_reader sub_dependencies: Hash[Symbol, untyped]
|
|
377
|
+
|
|
378
|
+
# Create a new dependency
|
|
379
|
+
#
|
|
380
|
+
# @param callable A callable that produces the dependency value
|
|
381
|
+
# @param sub_dependencies Named dependencies this depends on
|
|
382
|
+
def initialize: (^(**untyped) -> untyped callable, **untyped sub_dependencies) -> void
|
|
383
|
+
|
|
384
|
+
# Resolve this dependency and its sub-dependencies
|
|
385
|
+
#
|
|
386
|
+
# @param context Resolution context with :input, :req, :task, :container
|
|
387
|
+
# @param cache Dependency cache for deduplication
|
|
388
|
+
# @return [value, cleanup_proc] tuple
|
|
389
|
+
def call: (Hash[Symbol, untyped] context, ?Hash[untyped, untyped] cache) -> [untyped, (^() -> void)?]
|
|
390
|
+
end
|
|
391
|
+
|
|
392
|
+
# Factory method for creating Depends instances
|
|
393
|
+
#
|
|
394
|
+
# Example:
|
|
395
|
+
# FunApi.Depends(->(req:) { extract_user(req) })
|
|
396
|
+
#
|
|
397
|
+
def self.Depends: (^(**untyped) -> untyped callable, **untyped sub_dependencies) -> Depends
|
|
398
|
+
|
|
399
|
+
# Simple dependency wrapper (no cleanup needed)
|
|
400
|
+
class SimpleDependency
|
|
401
|
+
attr_reader resource: untyped
|
|
402
|
+
|
|
403
|
+
def initialize: (untyped resource) -> void
|
|
404
|
+
def call: () -> untyped
|
|
405
|
+
def cleanup: () -> void
|
|
406
|
+
end
|
|
407
|
+
|
|
408
|
+
# Managed dependency with explicit cleanup
|
|
409
|
+
class ManagedDependency
|
|
410
|
+
attr_reader resource: untyped
|
|
411
|
+
attr_reader cleanup_proc: ^() -> void
|
|
412
|
+
|
|
413
|
+
def initialize: (untyped resource, ^() -> void cleanup_proc) -> void
|
|
414
|
+
def call: () -> untyped
|
|
415
|
+
def cleanup: () -> void
|
|
416
|
+
end
|
|
417
|
+
|
|
418
|
+
# Block-based dependency with yield-style cleanup (like Python context managers)
|
|
419
|
+
class BlockDependency
|
|
420
|
+
def initialize: (^(^(untyped) -> untyped) -> untyped block) -> void
|
|
421
|
+
def call: () -> untyped
|
|
422
|
+
def cleanup: () -> void
|
|
423
|
+
end
|
|
424
|
+
|
|
425
|
+
# Built-in middleware
|
|
426
|
+
module Middleware
|
|
427
|
+
# Base class for middleware (optional to inherit from)
|
|
428
|
+
class Base
|
|
429
|
+
def initialize: (untyped app) -> void
|
|
430
|
+
def call: (rack_env env) -> rack_response
|
|
431
|
+
end
|
|
432
|
+
|
|
433
|
+
# CORS middleware
|
|
434
|
+
class Cors
|
|
435
|
+
def initialize: (
|
|
436
|
+
untyped app,
|
|
437
|
+
?allow_origins: Array[String],
|
|
438
|
+
?allow_methods: Array[String],
|
|
439
|
+
?allow_headers: Array[String],
|
|
440
|
+
?expose_headers: Array[String],
|
|
441
|
+
?max_age: Integer,
|
|
442
|
+
?allow_credentials: bool
|
|
443
|
+
) -> void
|
|
444
|
+
|
|
445
|
+
def call: (rack_env env) -> rack_response
|
|
446
|
+
end
|
|
447
|
+
|
|
448
|
+
# Trusted host validation middleware
|
|
449
|
+
class TrustedHost
|
|
450
|
+
def initialize: (untyped app, allowed_hosts: Array[String | Regexp]) -> void
|
|
451
|
+
def call: (rack_env env) -> rack_response
|
|
452
|
+
end
|
|
453
|
+
|
|
454
|
+
# Request logging middleware
|
|
455
|
+
class RequestLogger
|
|
456
|
+
def initialize: (untyped app, ?logger: untyped, ?level: Symbol) -> void
|
|
457
|
+
def call: (rack_env env) -> rack_response
|
|
458
|
+
end
|
|
459
|
+
end
|
|
460
|
+
|
|
461
|
+
# Server adapters
|
|
462
|
+
module Server
|
|
463
|
+
# Falcon server adapter
|
|
464
|
+
#
|
|
465
|
+
# Example:
|
|
466
|
+
# FunApi::Server::Falcon.start(app, port: 3000)
|
|
467
|
+
#
|
|
468
|
+
class Falcon
|
|
469
|
+
# Start the Falcon server with the given app
|
|
470
|
+
#
|
|
471
|
+
# @param app FunApi::App instance
|
|
472
|
+
# @param host Bind address
|
|
473
|
+
# @param port Port number
|
|
474
|
+
def self.start: (App app, ?host: String, ?port: Integer) -> void
|
|
475
|
+
end
|
|
476
|
+
end
|
|
477
|
+
|
|
478
|
+
# Router for path matching (internal, but exposed for advanced use)
|
|
479
|
+
class Router
|
|
480
|
+
# Route struct holding route metadata
|
|
481
|
+
class Route
|
|
482
|
+
attr_accessor verb: String
|
|
483
|
+
attr_accessor pattern: Regexp
|
|
484
|
+
attr_accessor keys: Array[String]
|
|
485
|
+
attr_accessor handler: ^(untyped, Hash[String, String]) -> rack_response
|
|
486
|
+
attr_accessor metadata: Hash[Symbol, untyped]
|
|
487
|
+
end
|
|
488
|
+
|
|
489
|
+
attr_reader routes: Array[Route]
|
|
490
|
+
|
|
491
|
+
def initialize: () -> void
|
|
492
|
+
|
|
493
|
+
# Add a route
|
|
494
|
+
def add: (String verb, String path, ?metadata: Hash[Symbol, untyped]) { (untyped, Hash[String, String]) -> rack_response } -> void
|
|
495
|
+
|
|
496
|
+
# Rack interface
|
|
497
|
+
def call: (rack_env env) -> rack_response
|
|
498
|
+
end
|
|
499
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: funapi
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Felipe Moyano
|
|
8
|
+
bindir: exe
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: async
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '2.8'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - ">="
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '2.8'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: dry-container
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - ">="
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '0.11'
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - ">="
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '0.11'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: dry-schema
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - ">="
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '1.13'
|
|
47
|
+
type: :runtime
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - ">="
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '1.13'
|
|
54
|
+
- !ruby/object:Gem::Dependency
|
|
55
|
+
name: falcon
|
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - ">="
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: '0.44'
|
|
61
|
+
type: :runtime
|
|
62
|
+
prerelease: false
|
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
64
|
+
requirements:
|
|
65
|
+
- - ">="
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
version: '0.44'
|
|
68
|
+
- !ruby/object:Gem::Dependency
|
|
69
|
+
name: rack
|
|
70
|
+
requirement: !ruby/object:Gem::Requirement
|
|
71
|
+
requirements:
|
|
72
|
+
- - ">="
|
|
73
|
+
- !ruby/object:Gem::Version
|
|
74
|
+
version: 3.0.0
|
|
75
|
+
- - "<"
|
|
76
|
+
- !ruby/object:Gem::Version
|
|
77
|
+
version: '4'
|
|
78
|
+
type: :runtime
|
|
79
|
+
prerelease: false
|
|
80
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
81
|
+
requirements:
|
|
82
|
+
- - ">="
|
|
83
|
+
- !ruby/object:Gem::Version
|
|
84
|
+
version: 3.0.0
|
|
85
|
+
- - "<"
|
|
86
|
+
- !ruby/object:Gem::Version
|
|
87
|
+
version: '4'
|
|
88
|
+
- !ruby/object:Gem::Dependency
|
|
89
|
+
name: rack-cors
|
|
90
|
+
requirement: !ruby/object:Gem::Requirement
|
|
91
|
+
requirements:
|
|
92
|
+
- - ">="
|
|
93
|
+
- !ruby/object:Gem::Version
|
|
94
|
+
version: '2.0'
|
|
95
|
+
type: :runtime
|
|
96
|
+
prerelease: false
|
|
97
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
98
|
+
requirements:
|
|
99
|
+
- - ">="
|
|
100
|
+
- !ruby/object:Gem::Version
|
|
101
|
+
version: '2.0'
|
|
102
|
+
description: Get an API started quickly, with performance in mind and, of course,
|
|
103
|
+
have some fun with Ruby.
|
|
104
|
+
email:
|
|
105
|
+
- gafemoyano@gmail.com
|
|
106
|
+
executables: []
|
|
107
|
+
extensions: []
|
|
108
|
+
extra_rdoc_files: []
|
|
109
|
+
files:
|
|
110
|
+
- ".claude/25-09-01-OPENAPI_IMPLEMENTATION.md"
|
|
111
|
+
- ".claude/25-09-05-RESPONSE_SCHEMA.md"
|
|
112
|
+
- ".claude/25-09-10-OPENAPI_PLAN.md"
|
|
113
|
+
- ".claude/25-10-26-MIDDLEWARE_IMPLEMENTATION.md"
|
|
114
|
+
- ".claude/25-10-26-MIDDLEWARE_PLAN.md"
|
|
115
|
+
- ".claude/25-10-27-BACKGROUND_TASKS_ANALYSIS.md"
|
|
116
|
+
- ".claude/25-10-27-DEPENDENCY_IMPLEMENTATION_SUMMARY.md"
|
|
117
|
+
- ".claude/25-10-27-DEPENDENCY_INJECTION_PLAN.md"
|
|
118
|
+
- ".claude/25-12-24-LIFECYCLE_HOOKS_PLAN.md"
|
|
119
|
+
- ".claude/25-12-24-PUBLISHING_AND_DOGFOODING_PLAN.md"
|
|
120
|
+
- ".claude/25-12-24-TEMPLATE_RENDERING_PLAN.md"
|
|
121
|
+
- ".claude/DECISIONS.md"
|
|
122
|
+
- ".claude/PROJECT_PLAN.md"
|
|
123
|
+
- ".claude/TESTING_PLAN.md"
|
|
124
|
+
- ".claude/TESTING_STATUS.md"
|
|
125
|
+
- ".tool-versions"
|
|
126
|
+
- AGENTS.md
|
|
127
|
+
- CHANGELOG.md
|
|
128
|
+
- CODE_OF_CONDUCT.md
|
|
129
|
+
- LICENSE.txt
|
|
130
|
+
- README.md
|
|
131
|
+
- Rakefile
|
|
132
|
+
- docs
|
|
133
|
+
- docs-site/.gitignore
|
|
134
|
+
- docs-site/Gemfile
|
|
135
|
+
- docs-site/app.rb
|
|
136
|
+
- docs-site/content/essential/handler.md
|
|
137
|
+
- docs-site/content/essential/lifecycle.md
|
|
138
|
+
- docs-site/content/essential/middleware.md
|
|
139
|
+
- docs-site/content/essential/openapi.md
|
|
140
|
+
- docs-site/content/essential/routing.md
|
|
141
|
+
- docs-site/content/essential/validation.md
|
|
142
|
+
- docs-site/content/getting-started/at-glance.md
|
|
143
|
+
- docs-site/content/getting-started/key-concepts.md
|
|
144
|
+
- docs-site/content/getting-started/quick-start.md
|
|
145
|
+
- docs-site/content/index.md
|
|
146
|
+
- docs-site/content/patterns/async-operations.md
|
|
147
|
+
- docs-site/content/patterns/background-tasks.md
|
|
148
|
+
- docs-site/content/patterns/database.md
|
|
149
|
+
- docs-site/content/patterns/dependencies.md
|
|
150
|
+
- docs-site/content/patterns/deployment.md
|
|
151
|
+
- docs-site/content/patterns/error-handling.md
|
|
152
|
+
- docs-site/content/patterns/response-schema.md
|
|
153
|
+
- docs-site/content/patterns/templates.md
|
|
154
|
+
- docs-site/content/patterns/testing.md
|
|
155
|
+
- docs-site/mise.toml
|
|
156
|
+
- docs-site/public/css/style.css
|
|
157
|
+
- docs-site/templates/layouts/docs.html.erb
|
|
158
|
+
- docs-site/templates/page.html.erb
|
|
159
|
+
- docs-site/templates/partials/_nav.html.erb
|
|
160
|
+
- examples/background_tasks_demo.rb
|
|
161
|
+
- examples/demo_middleware.rb
|
|
162
|
+
- examples/demo_openapi.rb
|
|
163
|
+
- examples/dependency_block_demo.rb
|
|
164
|
+
- examples/dependency_cleanup_demo.rb
|
|
165
|
+
- examples/dependency_injection_demo.rb
|
|
166
|
+
- examples/lifecycle_demo.rb
|
|
167
|
+
- examples/middleware_demo.rb
|
|
168
|
+
- examples/templates/layouts/application.html.erb
|
|
169
|
+
- examples/templates/todos/_todo.html.erb
|
|
170
|
+
- examples/templates/todos/index.html.erb
|
|
171
|
+
- examples/templates_demo.rb
|
|
172
|
+
- lib/funapi.rb
|
|
173
|
+
- lib/funapi/application.rb
|
|
174
|
+
- lib/funapi/async.rb
|
|
175
|
+
- lib/funapi/background_tasks.rb
|
|
176
|
+
- lib/funapi/config.rb
|
|
177
|
+
- lib/funapi/database/sequel/fibered_connection_pool.rb
|
|
178
|
+
- lib/funapi/dependency_wrapper.rb
|
|
179
|
+
- lib/funapi/depends.rb
|
|
180
|
+
- lib/funapi/exceptions.rb
|
|
181
|
+
- lib/funapi/middleware.rb
|
|
182
|
+
- lib/funapi/middleware/base.rb
|
|
183
|
+
- lib/funapi/middleware/cors.rb
|
|
184
|
+
- lib/funapi/middleware/request_logger.rb
|
|
185
|
+
- lib/funapi/middleware/trusted_host.rb
|
|
186
|
+
- lib/funapi/openapi/schema_converter.rb
|
|
187
|
+
- lib/funapi/openapi/spec_generator.rb
|
|
188
|
+
- lib/funapi/router.rb
|
|
189
|
+
- lib/funapi/schema.rb
|
|
190
|
+
- lib/funapi/server/falcon.rb
|
|
191
|
+
- lib/funapi/template_response.rb
|
|
192
|
+
- lib/funapi/templates.rb
|
|
193
|
+
- lib/funapi/version.rb
|
|
194
|
+
- sig/fun_api.rbs
|
|
195
|
+
homepage: https://github.com/gafemoyano/funapi
|
|
196
|
+
licenses:
|
|
197
|
+
- MIT
|
|
198
|
+
metadata:
|
|
199
|
+
homepage_uri: https://github.com/gafemoyano/funapi
|
|
200
|
+
source_code_uri: https://github.com/gafemoyano/funapi
|
|
201
|
+
changelog_uri: https://github.com/gafemoyano/funapi/blob/main/CHANGELOG.md
|
|
202
|
+
rubygems_mfa_required: 'true'
|
|
203
|
+
rdoc_options: []
|
|
204
|
+
require_paths:
|
|
205
|
+
- lib
|
|
206
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
207
|
+
requirements:
|
|
208
|
+
- - ">="
|
|
209
|
+
- !ruby/object:Gem::Version
|
|
210
|
+
version: 3.2.0
|
|
211
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
212
|
+
requirements:
|
|
213
|
+
- - ">="
|
|
214
|
+
- !ruby/object:Gem::Version
|
|
215
|
+
version: '0'
|
|
216
|
+
requirements: []
|
|
217
|
+
rubygems_version: 3.7.1
|
|
218
|
+
specification_version: 4
|
|
219
|
+
summary: Minimal async web framework for ruby inspired by FastAPI
|
|
220
|
+
test_files: []
|