Class: Spikard::App

Inherits:
Object
  • Object
show all
Includes:
LifecycleHooks, ProvideSupport
Defined in:
lib/spikard/app.rb

Overview

Collects route metadata so the Rust engine can execute handlers. rubocop:disable Metrics/ClassLength

Constant Summary collapse

HTTP_METHODS =
%w[GET POST PUT PATCH DELETE OPTIONS HEAD TRACE].freeze
SUPPORTED_OPTIONS =
%i[request_schema response_schema parameter_schema file_params is_async cors
body_param_name jsonrpc_method].freeze

Instance Attribute Summary collapse

Instance Method Summary collapse

Methods included from ProvideSupport

#dependencies, #provide

Methods included from LifecycleHooks

#on_error, #on_request, #on_response, #pre_handler, #pre_validation

Constructor Details

#initializeApp

Returns a new instance of App.



126
127
128
129
130
131
132
133
# File 'lib/spikard/app.rb', line 126

def initialize
  @routes = []
  @websocket_handlers = {}
  @sse_producers = {}
  @native_hooks = Spikard::Native::LifecycleRegistry.new
  @native_dependencies = Spikard::Native::DependencyRegistry.new
  @named_handlers = {}
end

Instance Attribute Details

#routesObject (readonly)

Returns the value of attribute routes.



124
125
126
# File 'lib/spikard/app.rb', line 124

def routes
  @routes
end

Instance Method Details

#default_handler_name(method, path) ⇒ Object



196
197
198
199
200
201
202
203
# File 'lib/spikard/app.rb', line 196

def default_handler_name(method, path)
  normalized_path = path.gsub(/[^a-zA-Z0-9]+/, '_').gsub(/__+/, '_')
  # ReDoS mitigation: use bounded quantifier {1,100} instead of + to prevent
  # polynomial time complexity with excessive trailing underscores
  normalized_path = normalized_path.sub(/^_{1,100}/, '').sub(/_{1,100}$/, '')
  normalized_path = 'root' if normalized_path.empty?
  "#{method.to_s.downcase}_#{normalized_path}"
end

#handler(name, &block) ⇒ Object

Raises:

  • (ArgumentError)


168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
# File 'lib/spikard/app.rb', line 168

def handler(name, &block)
  raise ArgumentError, 'block required for handler' unless block

  handler_name = name.to_s
  @named_handlers[handler_name] = block

  @routes.each do |entry|
    next unless entry.[:handler_name] == handler_name

    entry.handler = block
    next unless entry..is_a?(Hash)

    deps = extract_handler_dependencies(block)
    entry.[:handler_dependencies] = deps unless deps.empty?
  end

  block
end

#handler_mapObject



157
158
159
160
161
162
163
164
165
166
# File 'lib/spikard/app.rb', line 157

def handler_map
  map = {}
  @routes.each do |entry|
    name = entry.[:handler_name]
    # Pass raw handler - DI resolution happens in Rust layer
    map[name] = entry.handler
  end
  map.merge!(@named_handlers)
  map
end

#normalized_routes_jsonObject



187
188
189
190
191
192
193
194
# File 'lib/spikard/app.rb', line 187

def normalized_routes_json
  json = JSON.generate()
  if defined?(Spikard::Native) && Spikard::Native.respond_to?(:normalize_route_metadata)
    Spikard::Native.(json)
  else
    json
  end
end

#register_route(method, path, handler_name: nil, **options, &block) ⇒ Object



135
136
137
138
139
140
141
142
143
144
145
# File 'lib/spikard/app.rb', line 135

def register_route(method, path, handler_name: nil, **options, &block)
  method = method.to_s
  path = path.to_s
  handler_name = handler_name&.to_s
  handler = block || (handler_name && @named_handlers[handler_name])
  validate_route_arguments!(handler, handler_name, options)
   = (method, path, handler_name, options, handler)

  @routes << RouteEntry.new(, handler)
  handler
end

#route_metadataObject



153
154
155
# File 'lib/spikard/app.rb', line 153

def 
  @routes.map(&:metadata)
end

#run(config: nil, host: nil, port: nil) ⇒ Object

Run the Spikard server with the given configuration

rubocop:disable Metrics/MethodLength

Examples:

With ServerConfig

config = Spikard::ServerConfig.new(
  host: '0.0.0.0',
  port: 8080,
  compression: Spikard::CompressionConfig.new(quality: 9)
)
app.run(config: config)

With Hash

app.run(config: { host: '0.0.0.0', port: 8080 })

Backward compatible (deprecated)

app.run(host: '0.0.0.0', port: 8000)

Parameters:

  • config (ServerConfig, Hash, nil) (defaults to: nil)

    Server configuration Can be a ServerConfig object, a Hash with configuration keys, or nil to use defaults. If a Hash is provided, it will be converted to a ServerConfig. For backward compatibility, also accepts host: and port: keyword arguments.



274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
# File 'lib/spikard/app.rb', line 274

def run(config: nil, host: nil, port: nil)
  require 'json'

  # Backward compatibility: if host/port are provided directly, create a config
  if config.nil? && (host || port)
    config = ServerConfig.new(
      host: host || '127.0.0.1',
      port: port || 8000
    )
  elsif config.nil?
    config = ServerConfig.new
  elsif config.is_a?(Hash)
    config = ServerConfig.new(**config)
  end

  routes_json = normalized_routes_json

  # Get handler map
  handlers = handler_map

  # Get lifecycle hooks
  hooks = @native_hooks

  # Get WebSocket handlers and SSE producers
  ws_handlers = websocket_handlers
  sse_prods = sse_producers

  # Get dependencies for DI
  deps = @native_dependencies

  # Call the Rust extension's run_server function
  Spikard::Native.run_server(routes_json, handlers, config, hooks, ws_handlers, sse_prods, deps)

  # Keep Ruby process alive while server runs
  sleep
rescue LoadError => e
  raise 'Failed to load Spikard extension. ' \
        "Build it with: task build:ruby\n#{e.message}"
end

#sse(path, _handler_name: nil, **_options) { ... } ⇒ Proc

Register a Server-Sent Events endpoint

Examples:

app.sse('/notifications') do
  NotificationProducer.new
end

Parameters:

  • path (String)

    URL path for the SSE endpoint

Yields:

  • Factory block that returns a SseEventProducer instance

Returns:

  • (Proc)

    The factory block (for chaining)

Raises:

  • (ArgumentError)


232
233
234
235
236
237
# File 'lib/spikard/app.rb', line 232

def sse(path, _handler_name: nil, **_options, &factory)
  raise ArgumentError, 'block required for SSE producer factory' unless factory

  @sse_producers[path] = factory
  factory
end

#sse_producersHash

Get all registered SSE producers

Returns:

  • (Hash)

    Dictionary mapping paths to producer factory blocks



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

def sse_producers
  @sse_producers.dup
end

#websocket(path, _handler_name: nil, **_options) { ... } ⇒ Proc

Register a WebSocket endpoint

Examples:

app.websocket('/chat') do
  ChatHandler.new
end

Parameters:

  • path (String)

    URL path for the WebSocket endpoint

Yields:

  • Factory block that returns a WebSocketHandler instance

Returns:

  • (Proc)

    The factory block (for chaining)

Raises:

  • (ArgumentError)


215
216
217
218
219
220
# File 'lib/spikard/app.rb', line 215

def websocket(path, _handler_name: nil, **_options, &factory)
  raise ArgumentError, 'block required for WebSocket handler factory' unless factory

  @websocket_handlers[path] = factory
  factory
end

#websocket_handlersHash

Get all registered WebSocket handlers

Returns:

  • (Hash)

    Dictionary mapping paths to handler factory blocks



242
243
244
# File 'lib/spikard/app.rb', line 242

def websocket_handlers
  @websocket_handlers.dup
end