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.
Files changed (87) hide show
  1. checksums.yaml +7 -0
  2. data/.claude/25-09-01-OPENAPI_IMPLEMENTATION.md +233 -0
  3. data/.claude/25-09-05-RESPONSE_SCHEMA.md +383 -0
  4. data/.claude/25-09-10-OPENAPI_PLAN.md +219 -0
  5. data/.claude/25-10-26-MIDDLEWARE_IMPLEMENTATION.md +230 -0
  6. data/.claude/25-10-26-MIDDLEWARE_PLAN.md +353 -0
  7. data/.claude/25-10-27-BACKGROUND_TASKS_ANALYSIS.md +325 -0
  8. data/.claude/25-10-27-DEPENDENCY_IMPLEMENTATION_SUMMARY.md +325 -0
  9. data/.claude/25-10-27-DEPENDENCY_INJECTION_PLAN.md +753 -0
  10. data/.claude/25-12-24-LIFECYCLE_HOOKS_PLAN.md +421 -0
  11. data/.claude/25-12-24-PUBLISHING_AND_DOGFOODING_PLAN.md +327 -0
  12. data/.claude/25-12-24-TEMPLATE_RENDERING_PLAN.md +704 -0
  13. data/.claude/DECISIONS.md +397 -0
  14. data/.claude/PROJECT_PLAN.md +80 -0
  15. data/.claude/TESTING_PLAN.md +285 -0
  16. data/.claude/TESTING_STATUS.md +157 -0
  17. data/.tool-versions +1 -0
  18. data/AGENTS.md +416 -0
  19. data/CHANGELOG.md +5 -0
  20. data/CODE_OF_CONDUCT.md +132 -0
  21. data/LICENSE.txt +21 -0
  22. data/README.md +660 -0
  23. data/Rakefile +10 -0
  24. data/docs +8 -0
  25. data/docs-site/.gitignore +3 -0
  26. data/docs-site/Gemfile +9 -0
  27. data/docs-site/app.rb +138 -0
  28. data/docs-site/content/essential/handler.md +156 -0
  29. data/docs-site/content/essential/lifecycle.md +161 -0
  30. data/docs-site/content/essential/middleware.md +201 -0
  31. data/docs-site/content/essential/openapi.md +155 -0
  32. data/docs-site/content/essential/routing.md +123 -0
  33. data/docs-site/content/essential/validation.md +166 -0
  34. data/docs-site/content/getting-started/at-glance.md +82 -0
  35. data/docs-site/content/getting-started/key-concepts.md +150 -0
  36. data/docs-site/content/getting-started/quick-start.md +127 -0
  37. data/docs-site/content/index.md +81 -0
  38. data/docs-site/content/patterns/async-operations.md +137 -0
  39. data/docs-site/content/patterns/background-tasks.md +143 -0
  40. data/docs-site/content/patterns/database.md +175 -0
  41. data/docs-site/content/patterns/dependencies.md +141 -0
  42. data/docs-site/content/patterns/deployment.md +212 -0
  43. data/docs-site/content/patterns/error-handling.md +184 -0
  44. data/docs-site/content/patterns/response-schema.md +159 -0
  45. data/docs-site/content/patterns/templates.md +193 -0
  46. data/docs-site/content/patterns/testing.md +218 -0
  47. data/docs-site/mise.toml +2 -0
  48. data/docs-site/public/css/style.css +234 -0
  49. data/docs-site/templates/layouts/docs.html.erb +28 -0
  50. data/docs-site/templates/page.html.erb +3 -0
  51. data/docs-site/templates/partials/_nav.html.erb +19 -0
  52. data/examples/background_tasks_demo.rb +159 -0
  53. data/examples/demo_middleware.rb +55 -0
  54. data/examples/demo_openapi.rb +63 -0
  55. data/examples/dependency_block_demo.rb +150 -0
  56. data/examples/dependency_cleanup_demo.rb +146 -0
  57. data/examples/dependency_injection_demo.rb +200 -0
  58. data/examples/lifecycle_demo.rb +57 -0
  59. data/examples/middleware_demo.rb +74 -0
  60. data/examples/templates/layouts/application.html.erb +66 -0
  61. data/examples/templates/todos/_todo.html.erb +15 -0
  62. data/examples/templates/todos/index.html.erb +12 -0
  63. data/examples/templates_demo.rb +87 -0
  64. data/lib/funapi/application.rb +521 -0
  65. data/lib/funapi/async.rb +57 -0
  66. data/lib/funapi/background_tasks.rb +52 -0
  67. data/lib/funapi/config.rb +23 -0
  68. data/lib/funapi/database/sequel/fibered_connection_pool.rb +87 -0
  69. data/lib/funapi/dependency_wrapper.rb +66 -0
  70. data/lib/funapi/depends.rb +138 -0
  71. data/lib/funapi/exceptions.rb +72 -0
  72. data/lib/funapi/middleware/base.rb +13 -0
  73. data/lib/funapi/middleware/cors.rb +23 -0
  74. data/lib/funapi/middleware/request_logger.rb +32 -0
  75. data/lib/funapi/middleware/trusted_host.rb +34 -0
  76. data/lib/funapi/middleware.rb +4 -0
  77. data/lib/funapi/openapi/schema_converter.rb +85 -0
  78. data/lib/funapi/openapi/spec_generator.rb +179 -0
  79. data/lib/funapi/router.rb +43 -0
  80. data/lib/funapi/schema.rb +65 -0
  81. data/lib/funapi/server/falcon.rb +38 -0
  82. data/lib/funapi/template_response.rb +17 -0
  83. data/lib/funapi/templates.rb +111 -0
  84. data/lib/funapi/version.rb +5 -0
  85. data/lib/funapi.rb +14 -0
  86. data/sig/fun_api.rbs +499 -0
  87. 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