Class: Whoosh::App

Inherits:
Object
  • Object
show all
Defined in:
lib/whoosh/app.rb

Defined Under Namespace

Classes: AuthBuilder, HealthCheckBuilder, OpenAPIConfigBuilder, RateLimitBuilder, TokenTrackingBuilder

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(root: Dir.pwd) ⇒ App

Returns a new instance of App.



11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
# File 'lib/whoosh/app.rb', line 11

def initialize(root: Dir.pwd)
  EnvLoader.load(root)
  @config = Config.load(root: root)
  @router = Router.new
  @middleware_stack = Middleware::Stack.new
  @di = DependencyInjection.new
  @error_handlers = {}
  @default_error_handler = nil
  @logger = Whoosh::Logger.new(
    format: @config.log_format.to_sym,
    level: @config.log_level.to_sym
  )
  @group_prefix = ""
  @group_middleware = []
  @group_metadata = {}
  @plugin_registry = Plugins::Registry.new
  load_plugin_config
  auto_register_cache
  auto_register_database
  auto_register_storage
  auto_register_http
  auto_register_vectors
  auto_register_ai
  auto_configure_jobs
  @metrics = Metrics.new
  auto_register_metrics
  @authenticator = nil
  @rate_limiter_instance = nil
  @token_tracker = Auth::TokenTracker.new
  @acl = Auth::AccessControl.new
  @instrumentation = Instrumentation.new
  @mcp_server = MCP::Server.new
  @mcp_manager = MCP::ClientManager.new
  @openapi_config = { title: "Whoosh API", version: Whoosh::VERSION }
  @docs_config = {}
  @shutdown = Shutdown.new(logger: @logger)

  setup_default_middleware
end

Instance Attribute Details

#aclObject (readonly)

Returns the value of attribute acl.



9
10
11
# File 'lib/whoosh/app.rb', line 9

def acl
  @acl
end

#authenticatorObject (readonly)

Returns the value of attribute authenticator.



9
10
11
# File 'lib/whoosh/app.rb', line 9

def authenticator
  @authenticator
end

#configObject (readonly)

Returns the value of attribute config.



9
10
11
# File 'lib/whoosh/app.rb', line 9

def config
  @config
end

#instrumentationObject (readonly)

Returns the value of attribute instrumentation.



9
10
11
# File 'lib/whoosh/app.rb', line 9

def instrumentation
  @instrumentation
end

#loggerObject (readonly)

Returns the value of attribute logger.



9
10
11
# File 'lib/whoosh/app.rb', line 9

def logger
  @logger
end

#mcp_managerObject (readonly)

Returns the value of attribute mcp_manager.



9
10
11
# File 'lib/whoosh/app.rb', line 9

def mcp_manager
  @mcp_manager
end

#mcp_serverObject (readonly)

Returns the value of attribute mcp_server.



9
10
11
# File 'lib/whoosh/app.rb', line 9

def mcp_server
  @mcp_server
end

#metricsObject (readonly)

Returns the value of attribute metrics.



9
10
11
# File 'lib/whoosh/app.rb', line 9

def metrics
  @metrics
end

#plugin_registryObject (readonly)

Returns the value of attribute plugin_registry.



9
10
11
# File 'lib/whoosh/app.rb', line 9

def plugin_registry
  @plugin_registry
end

#rate_limiter_instanceObject (readonly)

Returns the value of attribute rate_limiter_instance.



9
10
11
# File 'lib/whoosh/app.rb', line 9

def rate_limiter_instance
  @rate_limiter_instance
end

#shutdownObject (readonly)

Returns the value of attribute shutdown.



9
10
11
# File 'lib/whoosh/app.rb', line 9

def shutdown
  @shutdown
end

#token_trackerObject (readonly)

Returns the value of attribute token_tracker.



9
10
11
# File 'lib/whoosh/app.rb', line 9

def token_tracker
  @token_tracker
end

Instance Method Details

#access_control(&block) ⇒ Object



156
157
158
# File 'lib/whoosh/app.rb', line 156

def access_control(&block)
  @acl.instance_eval(&block)
end

#auth(&block) ⇒ Object

— Auth DSL —



139
140
141
142
143
# File 'lib/whoosh/app.rb', line 139

def auth(&block)
  builder = AuthBuilder.new
  builder.instance_eval(&block)
  @authenticator = builder.build
end

#delete(path, **opts, &block) ⇒ Object



69
70
71
# File 'lib/whoosh/app.rb', line 69

def delete(path, **opts, &block)
  add_route("DELETE", path, **opts, &block)
end

#docs(enabled: true, redoc: false) ⇒ Object

— Docs DSL —



168
169
170
# File 'lib/whoosh/app.rb', line 168

def docs(enabled: true, redoc: false)
  @docs_config = { enabled: enabled, redoc: redoc }
end

#download(data, filename:, content_type: nil) ⇒ Object



257
258
259
# File 'lib/whoosh/app.rb', line 257

def download(data, filename:, content_type: nil)
  Response.download(data, filename: filename, content_type: content_type || "application/octet-stream")
end

#get(path, **opts, &block) ⇒ Object

— HTTP verb methods —



53
54
55
# File 'lib/whoosh/app.rb', line 53

def get(path, **opts, &block)
  add_route("GET", path, **opts, &block)
end

#group(prefix, middleware: [], **metadata, &block) ⇒ Object

— Route groups —



79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
# File 'lib/whoosh/app.rb', line 79

def group(prefix, middleware: [], **, &block)
  previous_prefix = @group_prefix
  previous_middleware = @group_middleware
   = @group_metadata

  @group_prefix = "#{previous_prefix}#{prefix}"
  @group_middleware = previous_middleware + middleware
  @group_metadata = .merge()

  instance_eval(&block)
ensure
  @group_prefix = previous_prefix
  @group_middleware = previous_middleware
  @group_metadata = 
end

#health_check(path: "/healthz", &block) ⇒ Object

— Health check —



182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
# File 'lib/whoosh/app.rb', line 182

def health_check(path: "/healthz", &block)
  probes = {}
  if block
    builder = HealthCheckBuilder.new
    builder.instance_eval(&block)
    probes = builder.probes
  end

  get path do
    checks = {}
    all_ok = true
    probes.each do |name, probe_block|
      begin
        probe_block.call
        checks[name.to_s] = "ok"
      rescue => e
        checks[name.to_s] = "fail: #{e.message}"
        all_ok = false
      end
    end

    result = { status: all_ok ? "ok" : "degraded", version: Whoosh::VERSION }
    result[:checks] = checks unless checks.empty?

    if all_ok
      result
    else
      [503, { "content-type" => "application/json" }, [Serialization::Json.encode(result)]]
    end
  end
end

#load_endpoints(dir) ⇒ Object

— Endpoint loading —



280
281
282
283
284
285
286
287
288
289
# File 'lib/whoosh/app.rb', line 280

def load_endpoints(dir)
  before = ObjectSpace.each_object(Class).select { |k| k < Endpoint }.to_set

  Dir.glob(File.join(dir, "**", "*.rb")).sort.each do |file|
    require file
  end

  after = ObjectSpace.each_object(Class).select { |k| k < Endpoint }.to_set
  (after - before).each { |klass| register_endpoint(klass) }
end

#mcp_client(name, command:, **options) ⇒ Object

— MCP DSL —



162
163
164
# File 'lib/whoosh/app.rb', line 162

def mcp_client(name, command:, **options)
  @mcp_manager.register(name, command: command, **options)
end

#on_error(exception_class = nil, &block) ⇒ Object

— Error handling —



103
104
105
106
107
108
109
# File 'lib/whoosh/app.rb', line 103

def on_error(exception_class = nil, &block)
  if exception_class
    @error_handlers[exception_class] = block
  else
    @default_error_handler = block
  end
end

#on_event(event, &block) ⇒ Object

— Instrumentation —



113
114
115
# File 'lib/whoosh/app.rb', line 113

def on_event(event, &block)
  @instrumentation.on(event, &block)
end

#openapi(&block) ⇒ Object

— OpenAPI DSL —



174
175
176
177
178
# File 'lib/whoosh/app.rb', line 174

def openapi(&block)
  builder = OpenAPIConfigBuilder.new
  builder.instance_eval(&block)
  @openapi_config.merge!(builder.to_h)
end

#options(path, **opts, &block) ⇒ Object



73
74
75
# File 'lib/whoosh/app.rb', line 73

def options(path, **opts, &block)
  add_route("OPTIONS", path, **opts, &block)
end

#paginate(collection, page:, per_page: 20) ⇒ Object



245
246
247
# File 'lib/whoosh/app.rb', line 245

def paginate(collection, page:, per_page: 20)
  Paginate.offset(collection, page: page, per_page: per_page)
end

#paginate_cursor(collection, cursor: nil, limit: 20, column: :id) ⇒ Object



249
250
251
# File 'lib/whoosh/app.rb', line 249

def paginate_cursor(collection, cursor: nil, limit: 20, column: :id)
  Paginate.cursor(collection, cursor: cursor, limit: limit, column: column)
end

#patch(path, **opts, &block) ⇒ Object



65
66
67
# File 'lib/whoosh/app.rb', line 65

def patch(path, **opts, &block)
  add_route("PATCH", path, **opts, &block)
end

#plugin(name, enabled: true, **config) ⇒ Object

— Plugin DSL —



125
126
127
128
129
130
131
# File 'lib/whoosh/app.rb', line 125

def plugin(name, enabled: true, **config)
  if enabled == false
    @plugin_registry.disable(name)
  else
    @plugin_registry.configure(name, config) unless config.empty?
  end
end

#post(path, **opts, &block) ⇒ Object



57
58
59
# File 'lib/whoosh/app.rb', line 57

def post(path, **opts, &block)
  add_route("POST", path, **opts, &block)
end

#provide(name, scope: :singleton, &block) ⇒ Object

— Dependency injection —



97
98
99
# File 'lib/whoosh/app.rb', line 97

def provide(name, scope: :singleton, &block)
  @di.provide(name, scope: scope, &block)
end

#put(path, **opts, &block) ⇒ Object



61
62
63
# File 'lib/whoosh/app.rb', line 61

def put(path, **opts, &block)
  add_route("PUT", path, **opts, &block)
end

#rate_limit(&block) ⇒ Object



145
146
147
148
149
# File 'lib/whoosh/app.rb', line 145

def rate_limit(&block)
  builder = RateLimitBuilder.new
  builder.instance_eval(&block)
  @rate_limiter_instance = builder.build
end

#redirect(url, status: 302) ⇒ Object



253
254
255
# File 'lib/whoosh/app.rb', line 253

def redirect(url, status: 302)
  Response.redirect(url, status: status)
end

#register_endpoint(endpoint_class) ⇒ Object



291
292
293
294
295
296
297
298
299
300
301
302
# File 'lib/whoosh/app.rb', line 291

def register_endpoint(endpoint_class)
  endpoint_class.declared_routes.each do |route|
    handler = {
      block: nil,
      endpoint_class: endpoint_class,
      request_schema: route[:request_schema],
      response_schema: route[:response_schema],
      middleware: []
    }
    @router.add(route[:method], route[:path], handler, **route[:metadata])
  end
end

#routesObject

— Route listing —



119
120
121
# File 'lib/whoosh/app.rb', line 119

def routes
  @router.routes
end

#send_file(path, content_type: nil) ⇒ Object



261
262
263
# File 'lib/whoosh/app.rb', line 261

def send_file(path, content_type: nil)
  Response.file(path, content_type: content_type)
end

#serve_static(prefix, root:) ⇒ Object



265
266
267
268
269
270
271
272
273
274
275
276
# File 'lib/whoosh/app.rb', line 265

def serve_static(prefix, root:)
  get "#{prefix}/:_static_path" do |req|
    file_path = File.join(root, req.params[:_static_path])
    real = File.realpath(file_path) rescue nil
    real_root = File.realpath(root) rescue root
    if real && real.start_with?(real_root) && File.file?(real)
      Response.file(real)
    else
      Response.not_found
    end
  end
end

#setup_plugin_accessorsObject



133
134
135
# File 'lib/whoosh/app.rb', line 133

def setup_plugin_accessors
  @plugin_registry.define_accessors(self)
end

#stream(type, &block) ⇒ Object

— Streaming helpers —



216
217
218
219
220
221
222
223
224
225
226
227
# File 'lib/whoosh/app.rb', line 216

def stream(type, &block)
  case type
  when :sse
    body = Streaming::StreamBody.new do |out|
      sse = Streaming::SSE.new(out)
      block.call(sse)
    end
    [200, Streaming::SSE.headers, body]
  else
    raise ArgumentError, "Unknown stream type: #{type}"
  end
end

#stream_llm(&block) ⇒ Object



229
230
231
232
233
234
235
236
# File 'lib/whoosh/app.rb', line 229

def stream_llm(&block)
  body = Streaming::StreamBody.new do |out|
    llm_stream = Streaming::LlmStream.new(out)
    block.call(llm_stream)
    llm_stream.finish
  end
  [200, Streaming::LlmStream.headers, body]
end

#to_rackObject

— Rack interface —



306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
# File 'lib/whoosh/app.rb', line 306

def to_rack
  @rack_app ||= begin
    @di.validate!
    register_mcp_tools
    register_doc_routes if @config.docs_enabled?
    register_metrics_route
    @router.freeze!

    # Compile the entire middleware + handler into a single lambda
    # This eliminates 4x nested method calls per request
    app = build_compiled_handler
    start_job_workers
    @shutdown.register { @di.close_all }
    @shutdown.register { @mcp_manager.shutdown_all }
    @shutdown.install_signal_handlers!
    app
  end
end

#token_tracking(&block) ⇒ Object



151
152
153
154
# File 'lib/whoosh/app.rb', line 151

def token_tracking(&block)
  builder = TokenTrackingBuilder.new(@token_tracker)
  builder.instance_eval(&block)
end

#websocket(env, &block) ⇒ Object

WebSocket endpoint helper — use in handle_request, returns hijack response



239
240
241
242
243
# File 'lib/whoosh/app.rb', line 239

def websocket(env, &block)
  ws = Streaming::WebSocket.new(env)
  block.call(ws)
  ws.rack_response
end