Class: Whoosh::App

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

Defined Under Namespace

Classes: AuthBuilder, HealthCheckBuilder, OpenAPIConfigBuilder, RateLimitBuilder, TokenTrackingBuilder

Instance Attribute Summary collapse

Instance Method Summary collapse

Methods included from Streaming::Helpers

#stream, #stream_llm

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



236
237
238
# File 'lib/whoosh/app.rb', line 236

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 —



259
260
261
262
263
264
265
266
267
268
# File 'lib/whoosh/app.rb', line 259

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



224
225
226
# File 'lib/whoosh/app.rb', line 224

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



228
229
230
# File 'lib/whoosh/app.rb', line 228

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



232
233
234
# File 'lib/whoosh/app.rb', line 232

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

#register_endpoint(endpoint_class) ⇒ Object



270
271
272
273
274
275
276
277
278
279
280
281
# File 'lib/whoosh/app.rb', line 270

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



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

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

#serve_static(prefix, root:) ⇒ Object



244
245
246
247
248
249
250
251
252
253
254
255
# File 'lib/whoosh/app.rb', line 244

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

#to_rackObject

— Rack interface —



285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
# File 'lib/whoosh/app.rb', line 285

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



218
219
220
221
222
# File 'lib/whoosh/app.rb', line 218

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