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,159 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../lib/funapi"
|
|
4
|
+
require_relative "../lib/funapi/server/falcon"
|
|
5
|
+
require "logger"
|
|
6
|
+
|
|
7
|
+
FAKE_EMAILS = []
|
|
8
|
+
FAKE_LOGS = []
|
|
9
|
+
FAKE_WEBHOOKS = []
|
|
10
|
+
|
|
11
|
+
def send_welcome_email(email, name)
|
|
12
|
+
sleep 0.05
|
|
13
|
+
message = "Welcome email sent to #{email} for #{name}"
|
|
14
|
+
FAKE_EMAILS << message
|
|
15
|
+
puts " š§ #{message}"
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def log_signup_event(user_id, email)
|
|
19
|
+
sleep 0.02
|
|
20
|
+
log_entry = "[#{Time.now}] User #{user_id} signed up: #{email}"
|
|
21
|
+
FAKE_LOGS << log_entry
|
|
22
|
+
puts " š #{log_entry}"
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def notify_admin(user_count)
|
|
26
|
+
sleep 0.03
|
|
27
|
+
notification = "Admin notified: Total users now #{user_count}"
|
|
28
|
+
FAKE_WEBHOOKS << notification
|
|
29
|
+
puts " š #{notification}"
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def send_webhook(url, data)
|
|
33
|
+
sleep 0.04
|
|
34
|
+
webhook = "Webhook sent to #{url}: #{data}"
|
|
35
|
+
FAKE_WEBHOOKS << webhook
|
|
36
|
+
puts " š #{webhook}"
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
USERS_DB = []
|
|
40
|
+
|
|
41
|
+
UserSchema = FunApi::Schema.define do
|
|
42
|
+
required(:name).filled(:string)
|
|
43
|
+
required(:email).filled(:string)
|
|
44
|
+
optional(:notifications).filled(:bool)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
app = FunApi::App.new(
|
|
48
|
+
title: "Background Tasks Demo API",
|
|
49
|
+
version: "1.0.0",
|
|
50
|
+
description: "Demonstrating background tasks in FunApi"
|
|
51
|
+
) do |api|
|
|
52
|
+
api.register(:logger) do
|
|
53
|
+
logger = Logger.new($stdout)
|
|
54
|
+
logger.level = Logger::INFO
|
|
55
|
+
logger
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
api.get "/" do |_input, _req, _task|
|
|
59
|
+
[{
|
|
60
|
+
message: "Background Tasks Demo API",
|
|
61
|
+
endpoints: {
|
|
62
|
+
home: "GET /",
|
|
63
|
+
signup: "POST /signup (body: {name, email, notifications?})",
|
|
64
|
+
users: "GET /users",
|
|
65
|
+
send_batch: "POST /send-batch-emails",
|
|
66
|
+
stats: "GET /stats"
|
|
67
|
+
},
|
|
68
|
+
info: "Background tasks run after response is sent but before dependencies close"
|
|
69
|
+
}, 200]
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
api.post "/signup", body: UserSchema do |input, _req, _task, background:|
|
|
73
|
+
user_data = input[:body]
|
|
74
|
+
|
|
75
|
+
user_id = USERS_DB.size + 1
|
|
76
|
+
user = user_data.merge(id: user_id, created_at: Time.now.to_s)
|
|
77
|
+
USERS_DB << user
|
|
78
|
+
|
|
79
|
+
puts "\nš Handler: Creating user #{user[:name]}"
|
|
80
|
+
|
|
81
|
+
background.add_task(method(:send_welcome_email), user[:email], user[:name])
|
|
82
|
+
background.add_task(method(:log_signup_event), user_id, user[:email])
|
|
83
|
+
|
|
84
|
+
background.add_task(method(:notify_admin), USERS_DB.size) if user[:notifications]
|
|
85
|
+
|
|
86
|
+
background.add_task(
|
|
87
|
+
method(:send_webhook),
|
|
88
|
+
"https://api.example.com/hooks/user-created",
|
|
89
|
+
user.to_json
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
puts "ā
Handler: Response ready (user created)\n"
|
|
93
|
+
|
|
94
|
+
[{user: user, message: "Signup successful! Check your email."}, 201]
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
api.get "/users" do |_input, _req, _task|
|
|
98
|
+
[{users: USERS_DB, count: USERS_DB.size}, 200]
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
api.post "/send-batch-emails", depends: [:logger] do |_input, _req, _task, logger:, background:|
|
|
102
|
+
user_count = USERS_DB.size
|
|
103
|
+
|
|
104
|
+
return [{error: "No users to email"}, 400] if user_count.zero?
|
|
105
|
+
|
|
106
|
+
puts "\nš Handler: Queueing #{user_count} email tasks"
|
|
107
|
+
|
|
108
|
+
USERS_DB.each do |user|
|
|
109
|
+
background.add_task(lambda { |email, name|
|
|
110
|
+
logger.info("Sending batch email to #{email}")
|
|
111
|
+
send_welcome_email(email, name)
|
|
112
|
+
}, user[:email], user[:name])
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
puts "ā
Handler: Response ready (#{user_count} emails queued)\n"
|
|
116
|
+
|
|
117
|
+
[{message: "#{user_count} emails queued for sending", count: user_count}, 200]
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
api.get "/stats" do |_input, _req, _task|
|
|
121
|
+
[{
|
|
122
|
+
users: USERS_DB.size,
|
|
123
|
+
emails_sent: FAKE_EMAILS.size,
|
|
124
|
+
logs_created: FAKE_LOGS.size,
|
|
125
|
+
webhooks_sent: FAKE_WEBHOOKS.size
|
|
126
|
+
}, 200]
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
puts "\nš FunApi Background Tasks Demo"
|
|
131
|
+
puts "=" * 60
|
|
132
|
+
puts "\nServer starting on http://localhost:3000"
|
|
133
|
+
puts "\nš API Docs: http://localhost:3000/docs"
|
|
134
|
+
puts "\n⨠Try these examples:\n"
|
|
135
|
+
puts "\n# Check available endpoints"
|
|
136
|
+
puts "curl http://localhost:3000/"
|
|
137
|
+
puts "\n# Sign up a user (watch background tasks execute)"
|
|
138
|
+
puts "curl -X POST http://localhost:3000/signup \\"
|
|
139
|
+
puts " -H 'Content-Type: application/json' \\"
|
|
140
|
+
puts " -d '{\"name\":\"Alice\",\"email\":\"alice@example.com\",\"notifications\":true}'"
|
|
141
|
+
puts "\n# Sign up another user"
|
|
142
|
+
puts "curl -X POST http://localhost:3000/signup \\"
|
|
143
|
+
puts " -H 'Content-Type: application/json' \\"
|
|
144
|
+
puts " -d '{\"name\":\"Bob\",\"email\":\"bob@example.com\",\"notifications\":false}'"
|
|
145
|
+
puts "\n# Send batch emails to all users"
|
|
146
|
+
puts "curl -X POST http://localhost:3000/send-batch-emails"
|
|
147
|
+
puts "\n# Check stats"
|
|
148
|
+
puts "curl http://localhost:3000/stats"
|
|
149
|
+
puts "\n# List all users"
|
|
150
|
+
puts "curl http://localhost:3000/users"
|
|
151
|
+
puts "\n" + ("=" * 60)
|
|
152
|
+
puts "\nš” Notice how:"
|
|
153
|
+
puts " - Response is sent IMMEDIATELY"
|
|
154
|
+
puts " - Background tasks run AFTER handler completes"
|
|
155
|
+
puts " - Background tasks run BEFORE dependencies close"
|
|
156
|
+
puts " - Multiple tasks execute in order"
|
|
157
|
+
puts " - Tasks can access dependencies (logger, db, etc.)\n\n"
|
|
158
|
+
|
|
159
|
+
FunApi::Server::Falcon.start(app, port: 3000)
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
require_relative "lib/funapi"
|
|
2
|
+
require_relative "lib/funapi/server/falcon"
|
|
3
|
+
|
|
4
|
+
class SimpleMiddleware
|
|
5
|
+
def initialize(app)
|
|
6
|
+
@app = app
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def call(env)
|
|
10
|
+
puts "[SimpleMiddleware] Before request"
|
|
11
|
+
status, headers, body = @app.call(env)
|
|
12
|
+
puts "[SimpleMiddleware] After request - Status: #{status}"
|
|
13
|
+
headers["X-Simple-Middleware"] = "true"
|
|
14
|
+
[status, headers, body]
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
class LoggingMiddleware
|
|
19
|
+
def initialize(app, prefix = "LOG")
|
|
20
|
+
@app = app
|
|
21
|
+
@prefix = prefix
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def call(env)
|
|
25
|
+
request = Rack::Request.new(env)
|
|
26
|
+
puts "[#{@prefix}] #{request.request_method} #{request.path}"
|
|
27
|
+
@app.call(env)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
app = FunApi::App.new(
|
|
32
|
+
title: "Middleware Test API",
|
|
33
|
+
version: "1.0.0"
|
|
34
|
+
) do |api|
|
|
35
|
+
api.use SimpleMiddleware
|
|
36
|
+
api.use LoggingMiddleware, "ACCESS"
|
|
37
|
+
|
|
38
|
+
api.get "/test" do |_input, _req, _task|
|
|
39
|
+
[{message: "Middleware test successful!"}, 200]
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
api.get "/hello/:name" do |input, _req, _task|
|
|
43
|
+
name = input[:path]["name"]
|
|
44
|
+
[{greeting: "Hello, #{name}!"}, 200]
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
puts "Starting FunApi with middleware..."
|
|
49
|
+
puts "Test endpoints:"
|
|
50
|
+
puts " http://localhost:3000/test"
|
|
51
|
+
puts " http://localhost:3000/hello/World"
|
|
52
|
+
puts " http://localhost:3000/docs"
|
|
53
|
+
puts ""
|
|
54
|
+
|
|
55
|
+
FunApi::Server::Falcon.start(app, port: 3000)
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
require_relative "lib/funapi"
|
|
2
|
+
require_relative "lib/funapi/server/falcon"
|
|
3
|
+
|
|
4
|
+
UserCreateSchema = FunApi::Schema.define do
|
|
5
|
+
required(:name).filled(:string)
|
|
6
|
+
required(:email).filled(:string)
|
|
7
|
+
required(:password).filled(:string)
|
|
8
|
+
optional(:age).filled(:integer)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
UserOutputSchema = FunApi::Schema.define do
|
|
12
|
+
required(:id).filled(:integer)
|
|
13
|
+
required(:name).filled(:string)
|
|
14
|
+
required(:email).filled(:string)
|
|
15
|
+
optional(:age).filled(:integer)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
QuerySchema = FunApi::Schema.define do
|
|
19
|
+
optional(:limit).filled(:integer)
|
|
20
|
+
optional(:offset).filled(:integer)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
app = FunApi::App.new(
|
|
24
|
+
title: "User Management API",
|
|
25
|
+
version: "1.0.0",
|
|
26
|
+
description: "A simple user management API demonstrating OpenAPI generation"
|
|
27
|
+
) do |api|
|
|
28
|
+
api.get "/users", query: QuerySchema, response_schema: [UserOutputSchema] do |_input, _req, _task|
|
|
29
|
+
users = [
|
|
30
|
+
{id: 1, name: "John Doe", email: "john@example.com", age: 30},
|
|
31
|
+
{id: 2, name: "Jane Smith", email: "jane@example.com"}
|
|
32
|
+
]
|
|
33
|
+
[users, 200]
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
api.get "/users/:id", response_schema: UserOutputSchema do |input, _req, _task|
|
|
37
|
+
user_id = input[:path]["id"]
|
|
38
|
+
user = {id: user_id.to_i, name: "John Doe", email: "john@example.com", age: 30}
|
|
39
|
+
[user, 200]
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
api.post "/users", body: UserCreateSchema, response_schema: UserOutputSchema do |input, _req, _task|
|
|
43
|
+
user = input[:body].merge(id: rand(1000))
|
|
44
|
+
[user, 201]
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
api.put "/users/:id", body: UserCreateSchema, response_schema: UserOutputSchema do |input, _req, _task|
|
|
48
|
+
user_id = input[:path]["id"]
|
|
49
|
+
user = input[:body].merge(id: user_id.to_i)
|
|
50
|
+
[user, 200]
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
api.delete "/users/:id" do |_input, _req, _task|
|
|
54
|
+
[{message: "User deleted"}, 200]
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
puts "Starting server on http://localhost:9292"
|
|
59
|
+
puts "OpenAPI spec: http://localhost:9292/openapi.json"
|
|
60
|
+
puts "Swagger UI: http://localhost:9292/docs"
|
|
61
|
+
puts
|
|
62
|
+
|
|
63
|
+
FunApi::Server::Falcon.start(app, port: 9292)
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# standard:disable Style/GlobalVars
|
|
4
|
+
|
|
5
|
+
require_relative '../lib/funapi'
|
|
6
|
+
require_relative '../lib/funapi/server/falcon'
|
|
7
|
+
|
|
8
|
+
class DatabaseConnection
|
|
9
|
+
attr_reader :id, :queries_run
|
|
10
|
+
|
|
11
|
+
def initialize(id)
|
|
12
|
+
@id = id
|
|
13
|
+
@open = true
|
|
14
|
+
@queries_run = []
|
|
15
|
+
puts " ā
Database connection #{id} OPENED"
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def query(sql)
|
|
19
|
+
raise "Connection #{@id} is closed!" unless @open
|
|
20
|
+
|
|
21
|
+
@queries_run << sql
|
|
22
|
+
puts " š Running query on connection #{@id}: #{sql}"
|
|
23
|
+
{ result: "data for #{sql}" }
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def close
|
|
27
|
+
return unless @open
|
|
28
|
+
|
|
29
|
+
@open = false
|
|
30
|
+
puts " ā Database connection #{@id} CLOSED (ran #{@queries_run.length} queries)"
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def open?
|
|
34
|
+
@open
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
$connection_counter = 0
|
|
39
|
+
$all_connections = []
|
|
40
|
+
|
|
41
|
+
app = FunApi::App.new(
|
|
42
|
+
title: 'Block-Based Dependency Demo',
|
|
43
|
+
version: '1.0.0'
|
|
44
|
+
) do |api|
|
|
45
|
+
api.register(:db) do |provide|
|
|
46
|
+
$connection_counter += 1
|
|
47
|
+
conn = DatabaseConnection.new($connection_counter)
|
|
48
|
+
$all_connections << conn
|
|
49
|
+
|
|
50
|
+
provide.call(conn)
|
|
51
|
+
ensure
|
|
52
|
+
conn&.close
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
api.get '/' do |_input, _req, _task|
|
|
56
|
+
[{
|
|
57
|
+
message: 'Block-Based Dependency Demo',
|
|
58
|
+
info: 'Dependencies use Ruby blocks with ensure for cleanup',
|
|
59
|
+
pattern: 'register(:key) { |provide| resource = setup(); provide.call(resource); ensure cleanup() }',
|
|
60
|
+
endpoints: {
|
|
61
|
+
users: 'GET /users (opens db, runs query, closes db)',
|
|
62
|
+
error: 'GET /error (opens db, errors, still closes db)',
|
|
63
|
+
multiple: 'GET /multiple (opens db once, uses multiple times)'
|
|
64
|
+
},
|
|
65
|
+
stats: 'GET /stats'
|
|
66
|
+
}, 200]
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
api.get '/users',
|
|
70
|
+
depends: [:db] do |_input, _req, _task, db:|
|
|
71
|
+
puts "\nš¹ Handler executing..."
|
|
72
|
+
users = db.query('SELECT * FROM users')
|
|
73
|
+
|
|
74
|
+
puts 'š¹ Handler returning response...'
|
|
75
|
+
[users, 200]
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
api.get '/error',
|
|
79
|
+
depends: [:db] do |_input, _req, _task, db:|
|
|
80
|
+
puts "\nš¹ Handler executing..."
|
|
81
|
+
db.query('SELECT * FROM users')
|
|
82
|
+
|
|
83
|
+
puts 'š¹ Handler raising error...'
|
|
84
|
+
raise FunApi::HTTPException.new(status_code: 500, detail: 'Something went wrong!')
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
api.get '/multiple',
|
|
88
|
+
depends: {
|
|
89
|
+
db1: :db,
|
|
90
|
+
db2: :db
|
|
91
|
+
} do |_input, _req, _task, db1:, db2:|
|
|
92
|
+
puts "\nš¹ Handler executing with multiple deps..."
|
|
93
|
+
puts " db1 object_id: #{db1.object_id}"
|
|
94
|
+
puts " db2 object_id: #{db2.object_id}"
|
|
95
|
+
puts " Same instance? #{db1.equal?(db2)}"
|
|
96
|
+
|
|
97
|
+
db1.query('SELECT * FROM users')
|
|
98
|
+
db2.query('SELECT * FROM posts')
|
|
99
|
+
|
|
100
|
+
[{
|
|
101
|
+
note: 'Both db1 and db2 are the same connection (request-scoped cache)',
|
|
102
|
+
db1_id: db1.id,
|
|
103
|
+
db2_id: db2.id,
|
|
104
|
+
same_instance: db1.equal?(db2)
|
|
105
|
+
}, 200]
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
api.get '/stats' do |_input, _req, _task|
|
|
109
|
+
open_count = $all_connections.count(&:open?)
|
|
110
|
+
closed_count = $all_connections.count { |c| !c.open? }
|
|
111
|
+
|
|
112
|
+
[{
|
|
113
|
+
total_connections_created: $connection_counter,
|
|
114
|
+
currently_open: open_count,
|
|
115
|
+
closed: closed_count,
|
|
116
|
+
all_connections: $all_connections.map do |c|
|
|
117
|
+
{
|
|
118
|
+
id: c.id,
|
|
119
|
+
open: c.open?,
|
|
120
|
+
queries_run: c.queries_run.length
|
|
121
|
+
}
|
|
122
|
+
end
|
|
123
|
+
}, 200]
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
puts "\nš FunApi Block-Based Dependency Demo"
|
|
128
|
+
puts '=' * 50
|
|
129
|
+
puts "\nThis demo shows the new Ruby-idiomatic dependency pattern:"
|
|
130
|
+
puts "\n api.register(:key) do |provide|"
|
|
131
|
+
puts ' resource = setup_resource()'
|
|
132
|
+
puts ' provide.call(resource) # Yield resource to framework'
|
|
133
|
+
puts ' ensure'
|
|
134
|
+
puts ' cleanup_resource() # Always runs, even on errors'
|
|
135
|
+
puts ' end'
|
|
136
|
+
puts "\nServer starting on http://localhost:3002"
|
|
137
|
+
puts "\n⨠Try these commands:\n"
|
|
138
|
+
puts '# Open connection, run query, close connection'
|
|
139
|
+
puts 'curl http://localhost:3002/users'
|
|
140
|
+
puts "\n# Open connection, error, still close connection"
|
|
141
|
+
puts 'curl http://localhost:3002/error'
|
|
142
|
+
puts "\n# Open connection once, use multiple times (cached)"
|
|
143
|
+
puts 'curl http://localhost:3002/multiple'
|
|
144
|
+
puts "\n# Check connection stats"
|
|
145
|
+
puts 'curl http://localhost:3002/stats'
|
|
146
|
+
puts "\n" + ('=' * 50) + "\n\n"
|
|
147
|
+
|
|
148
|
+
FunApi::Server::Falcon.start(app, port: 3002)
|
|
149
|
+
|
|
150
|
+
# standard:enable Style/GlobalVars
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# standard:disable Style/GlobalVars
|
|
4
|
+
|
|
5
|
+
require_relative '../lib/funapi'
|
|
6
|
+
require_relative '../lib/funapi/server/falcon'
|
|
7
|
+
|
|
8
|
+
class DatabaseConnection
|
|
9
|
+
attr_reader :id, :queries_run
|
|
10
|
+
|
|
11
|
+
def initialize(id)
|
|
12
|
+
@id = id
|
|
13
|
+
@open = true
|
|
14
|
+
@queries_run = []
|
|
15
|
+
puts " ā
Database connection #{id} OPENED"
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def query(sql)
|
|
19
|
+
raise "Connection #{@id} is closed!" unless @open
|
|
20
|
+
|
|
21
|
+
@queries_run << sql
|
|
22
|
+
puts " š Running query on connection #{@id}: #{sql}"
|
|
23
|
+
{ result: "data for #{sql}" }
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def close
|
|
27
|
+
return unless @open
|
|
28
|
+
|
|
29
|
+
@open = false
|
|
30
|
+
puts " ā Database connection #{@id} CLOSED (ran #{@queries_run.length} queries)"
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def open?
|
|
34
|
+
@open
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
$connection_counter = 0
|
|
39
|
+
$all_connections = []
|
|
40
|
+
|
|
41
|
+
app = FunApi::App.new(
|
|
42
|
+
title: 'Dependency Cleanup Demo',
|
|
43
|
+
version: '1.0.0'
|
|
44
|
+
) do |api|
|
|
45
|
+
api.register(:db) do
|
|
46
|
+
$connection_counter += 1
|
|
47
|
+
conn = DatabaseConnection.new($connection_counter)
|
|
48
|
+
$all_connections << conn
|
|
49
|
+
|
|
50
|
+
cleanup = -> { conn.close }
|
|
51
|
+
|
|
52
|
+
[conn, cleanup]
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
api.get '/' do |_input, _req, _task|
|
|
56
|
+
[{
|
|
57
|
+
message: 'Dependency Cleanup Demo',
|
|
58
|
+
endpoints: {
|
|
59
|
+
users: 'GET /users (opens db, runs query, closes db)',
|
|
60
|
+
error: 'GET /error (opens db, errors, still closes db)',
|
|
61
|
+
multiple: 'GET /multiple (opens db once, uses multiple times)'
|
|
62
|
+
},
|
|
63
|
+
stats: 'GET /stats'
|
|
64
|
+
}, 200]
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
api.get '/users',
|
|
68
|
+
depends: [:db] do |_input, _req, _task, db:|
|
|
69
|
+
puts "\nš¹ Handler executing..."
|
|
70
|
+
users = db.query('SELECT * FROM users')
|
|
71
|
+
|
|
72
|
+
puts 'š¹ Handler returning response...'
|
|
73
|
+
[users, 200]
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
api.get '/error',
|
|
77
|
+
depends: [:db] do |_input, _req, _task, db:|
|
|
78
|
+
puts "\nš¹ Handler executing..."
|
|
79
|
+
db.query('SELECT * FROM users')
|
|
80
|
+
|
|
81
|
+
puts 'š¹ Handler raising error...'
|
|
82
|
+
raise FunApi::HTTPException.new(status_code: 500, detail: 'Something went wrong!')
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
api.get '/multiple',
|
|
86
|
+
depends: {
|
|
87
|
+
db1: :db,
|
|
88
|
+
db2: :db
|
|
89
|
+
} do |_input, _req, _task, db1:, db2:|
|
|
90
|
+
puts "\nš¹ Handler executing with multiple deps..."
|
|
91
|
+
puts " db1 object_id: #{db1.object_id}"
|
|
92
|
+
puts " db2 object_id: #{db2.object_id}"
|
|
93
|
+
puts " Same instance? #{db1.equal?(db2)}"
|
|
94
|
+
|
|
95
|
+
db1.query('SELECT * FROM users')
|
|
96
|
+
db2.query('SELECT * FROM posts')
|
|
97
|
+
|
|
98
|
+
[{
|
|
99
|
+
note: 'Both db1 and db2 are the same connection (request-scoped cache)',
|
|
100
|
+
db1_id: db1.id,
|
|
101
|
+
db2_id: db2.id,
|
|
102
|
+
same_instance: db1.equal?(db2)
|
|
103
|
+
}, 200]
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
api.get '/stats' do |_input, _req, _task|
|
|
107
|
+
open_count = $all_connections.count(&:open?)
|
|
108
|
+
closed_count = $all_connections.count { |c| !c.open? }
|
|
109
|
+
|
|
110
|
+
[{
|
|
111
|
+
total_connections_created: $connection_counter,
|
|
112
|
+
currently_open: open_count,
|
|
113
|
+
closed: closed_count,
|
|
114
|
+
all_connections: $all_connections.map do |c|
|
|
115
|
+
{
|
|
116
|
+
id: c.id,
|
|
117
|
+
open: c.open?,
|
|
118
|
+
queries_run: c.queries_run.length
|
|
119
|
+
}
|
|
120
|
+
end
|
|
121
|
+
}, 200]
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
puts "\nš FunApi Dependency Cleanup Demo"
|
|
126
|
+
puts '=' * 50
|
|
127
|
+
puts "\nThis demo shows how dependency cleanup works:"
|
|
128
|
+
puts '- Dependencies can return [resource, cleanup_proc]'
|
|
129
|
+
puts '- Cleanup runs AFTER response is sent (in ensure block)'
|
|
130
|
+
puts '- Cleanup runs even if handler raises an error'
|
|
131
|
+
puts '- Multiple references to same dependency = single instance'
|
|
132
|
+
puts "\nServer starting on http://localhost:3001"
|
|
133
|
+
puts "\n⨠Try these commands:\n"
|
|
134
|
+
puts '# Open connection, run query, close connection'
|
|
135
|
+
puts 'curl http://localhost:3001/users'
|
|
136
|
+
puts "\n# Open connection, error, still close connection"
|
|
137
|
+
puts 'curl http://localhost:3001/error'
|
|
138
|
+
puts "\n# Open connection once, use multiple times (cached)"
|
|
139
|
+
puts 'curl http://localhost:3001/multiple'
|
|
140
|
+
puts "\n# Check connection stats"
|
|
141
|
+
puts 'curl http://localhost:3001/stats'
|
|
142
|
+
puts "\n" + ('=' * 50) + "\n\n"
|
|
143
|
+
|
|
144
|
+
FunApi::Server::Falcon.start(app, port: 3001)
|
|
145
|
+
|
|
146
|
+
# standard:enable Style/GlobalVars
|