Class: GRApiManager::Server
- Inherits:
-
Object
- Object
- GRApiManager::Server
- Defined in:
- lib/gr_api_manager.rb
Overview
Server — the public-facing DSL.
Instance Attribute Summary collapse
-
#app_class ⇒ Object
readonly
Returns the value of attribute app_class.
Instance Method Summary collapse
-
#initialize(port: nil, bearer_token: nil, permitted_hosts: [], prefix: '', max_body_size: GRApiManager.mb(50), dev_mode: false, rate_limit: nil, rate_limit_window: 60) ⇒ Server
constructor
Initializes the server configuration.
-
#register_route(verb, path, options = {}, &block) ⇒ Object
Core routing logic: auth validation, body parsing, param merging, validation, execution.
-
#run!(workers: nil, threads: '2:8') ⇒ Object
Starts the Sinatra server.
Constructor Details
#initialize(port: nil, bearer_token: nil, permitted_hosts: [], prefix: '', max_body_size: GRApiManager.mb(50), dev_mode: false, rate_limit: nil, rate_limit_window: 60) ⇒ Server
Initializes the server configuration.
Options:
port: Integer – listening port (default: ENV['PORT'] || 4000)
bearer_token: String – Bearer token for auth (default: ENV['API_TOKEN'])
permitted_hosts: Array – host allowlist; empty = allow all
prefix: String – route prefix, e.g. '/api/v1'
max_body_size: Integer – maximum accepted body in bytes (default: 50 MB)
dev_mode: Boolean – show full stack traces on 500 (default: false)
rate_limit: Integer – max requests per IP per window (nil = disabled)
rate_limit_window: Integer – sliding window in seconds (default: 60)
293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 |
# File 'lib/gr_api_manager.rb', line 293 def initialize( port: nil, bearer_token: nil, permitted_hosts: [], prefix: '', max_body_size: GRApiManager.mb(50), dev_mode: false, rate_limit: nil, rate_limit_window: 60 ) @port = port || ENV['PORT'] || 4000 @token = bearer_token || ENV['API_TOKEN'] @permitted_hosts = permitted_hosts.empty? ? [] : permitted_hosts @prefix = prefix @max_body_size = max_body_size @dev_mode = dev_mode @rate_limiter = rate_limit ? GRApiManager::RateLimiter.new( max_requests: rate_limit, window_seconds: rate_limit_window ) : nil @app_class = Class.new(Sinatra::Base) do # Logs HTTP requests with status-based color coding. def log_request(method, path, status_code) color = status_code.between?(200, 299) ? "\e[32m" : "\e[31m" puts "[#{Time.now.strftime('%H:%M:%S')}] #{color}#{method} #{path} - #{status_code}\e[0m" end # Casts string URL parameters to native Ruby types (Integer, Float, Boolean). # Leaves values untouched if they are already non-String (e.g. FilePayload). def smart_parse(hash) hash.transform_values do |val| next val unless val.is_a?(String) case val when 'true' then true when 'false' then false when /^\d+$/ then val.to_i when /^\d+\.\d+$/ then val.to_f else val end end end end configure_app end |
Instance Attribute Details
#app_class ⇒ Object (readonly)
Returns the value of attribute app_class.
280 281 282 |
# File 'lib/gr_api_manager.rb', line 280 def app_class @app_class end |
Instance Method Details
#register_route(verb, path, options = {}, &block) ⇒ Object
Core routing logic: auth validation, body parsing, param merging, validation, execution.
Supported body formats (auto-detected via Content-Type):
application/json – standard JSON body
multipart/form-data – form fields + file uploads
application/octet-stream – raw binary stream
image/*, video/*, audio/* – raw binary media
application/pdf, etc. – raw binary document
text/plain – plain text body
Inside your block, params will contain:
:_files => { field: FilePayload } – for multipart uploads
:_raw_binary => FilePayload – for raw binary/media bodies
:_raw_text => String – for text/plain bodies
Route options:
auth: Boolean – require Bearer Token (default: true)
requires: Array – required parameter keys [:name, :email, ...]
437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 |
# File 'lib/gr_api_manager.rb', line 437 def register_route(verb, path, = {}, &block) verb_up = verb.to_s.upcase require_auth = .fetch(:auth, true) required_params = .fetch(:requires, []) # Construct the full path with the optional prefix. full_path = File.join('/', @prefix.to_s, path.to_s).gsub(%r{/+}, '/') handler = proc do # 1. Authentication check if require_auth auth_header = request.env["HTTP_AUTHORIZATION"] halt 401, { error: "Token required. Format: 'Bearer <token>'" }.to_json if auth_header.nil? halt 403, { error: "Invalid token" }.to_json if auth_header.split(" ").last != settings.token end # 2. Body parsing — smart detection based on Content-Type parsed_body = {} if %w[POST PUT PATCH].include?(verb_up) begin parsed_body = GRApiManager::BodyParser.parse(request) rescue ArgumentError => e halt 400, { error: e. }.to_json end end # 3. Merge query/path parameters with parsed body. # URL params go through smart_parse; body values are left as-is # (so FilePayload objects, arrays, etc. are preserved). url_params = smart_parse(params.reject { |_, v| v.is_a?(Hash) && v.key?(:tempfile) }) all_params = url_params.merge(parsed_body) # 4. Declarative parameter validation (skips special _ keys and FilePayload values). missing = required_params.select do |p| val = all_params[p.to_sym] val.nil? || (val.is_a?(String) && val.strip.empty?) end if missing.any? status 400 log_request(verb_up, full_path, 400) next { error: "Missing required parameters", required: missing }.to_json end # 5. Execute user-defined block result = instance_exec(all_params, &block) log_request(verb_up, full_path, response.status) # If the block returned a String (e.g. already rendered binary data), # pass it through unchanged. Otherwise serialize to JSON. result.is_a?(String) ? result : result.to_json end @app_class.send(verb.downcase, full_path, &handler) end |
#run!(workers: nil, threads: '2:8') ⇒ Object
Starts the Sinatra server.
Options:
workers: Integer – Puma worker processes (default: ENV['WEB_CONCURRENCY'] || 2)
threads: String – min:max thread count per worker (default: '2:8')
For sustained high traffic, run behind Nginx as a reverse proxy. See the README section "High Traffic & Concurrency" for production tuning.
501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 |
# File 'lib/gr_api_manager.rb', line 501 def run!(workers: nil, threads: '2:8') w = (workers || ENV.fetch('WEB_CONCURRENCY', 2)).to_i min_t, max_t = threads.to_s.split(':').map(&:to_i) max_t ||= min_t mb = (@max_body_size.to_f / 1_048_576).round(1) # Use Puma as the application server for concurrency. @app_class.set :server, :puma @app_class.set :server_settings, { workers: w, min_threads: min_t, max_threads: max_t } # Background thread to purge stale rate-limit entries (prevents memory growth). if @rate_limiter rl = @rate_limiter Thread.new do loop do sleep rl.window_seconds * 2 rl.cleanup! end end end rl_info = if @rate_limiter "#{@rate_limiter.max_requests} req / #{@rate_limiter.window_seconds}s per IP" else 'Disabled' end puts "=============================================" puts " GR API MANAGER STARTED" puts " Port : #{@port}" puts " Auth : #{@token ? 'Enabled' : 'Public (no token)'}" puts " Prefix : #{@prefix.empty? ? '/' : @prefix}" puts " Max Body : #{mb} MB" puts " Workers : #{w} | Threads: #{min_t}:#{max_t}" puts " Rate Limit: #{rl_info}" puts " Dev Mode : #{@dev_mode ? 'ON ⚠️ (disable in production)' : 'Off'}" puts "=============================================" @app_class.run! end |