diff --git a/src/handler_types.rs b/src/handler_types.rs new file mode 100644 index 0000000..b77c10c --- /dev/null +++ b/src/handler_types.rs @@ -0,0 +1,168 @@ +//! Handler-specific types that guarantee correctness + +use crate::adapters::{HandlebarsAdapter, NginxVariableResolver, SqliteQueryExecutor}; +use crate::config::{MainConfig, ModuleConfig}; +use crate::domain::{RequestProcessor, ValidatedConfig}; +use crate::nginx_helpers::{get_doc_root_and_uri, send_response}; +use crate::parsing; +use crate::{domain, Module}; +use ngx::core::Status; +use ngx::http::{HttpModuleLocationConf, HttpModuleMainConf}; +use ngx::ngx_log_debug_http; + +/// Proof that we have valid configuration (Ghost of Departed Proofs) +pub struct ValidConfigToken<'a> { + config: &'a ModuleConfig, +} + +impl<'a> ValidConfigToken<'a> { + /// Try to create a token - returns None if config is invalid + pub fn new(config: &'a ModuleConfig) -> Option { + if config.db_path.is_empty() || config.query.is_empty() || config.template_path.is_empty() { + return None; + } + Some(ValidConfigToken { config }) + } + + pub fn get(&self) -> &ModuleConfig { + self.config + } +} + +/// Process a request with guaranteed valid configuration +/// Returns Status directly - no Result needed, types prove correctness +pub fn process_request( + request: &mut ngx::http::Request, + config: ValidConfigToken, +) -> Status { + // Parse config into validated types + // If this fails, it's a programming error (config should be valid per token) + let validated_config = match parsing::parse_config(config.get()) { + Ok(c) => c, + Err(e) => { + ngx_log_debug_http!(request, "unexpected parse error: {}", e); + return ngx::http::HTTPStatus::INTERNAL_SERVER_ERROR.into(); + } + }; + + // Get nginx paths - can fail due to nginx state, not our logic + let (doc_root, uri) = match get_doc_root_and_uri(request) { + Ok(paths) => paths, + Err(e) => { + ngx_log_debug_http!(request, "nginx path error: {}", e); + return ngx::http::HTTPStatus::INTERNAL_SERVER_ERROR.into(); + } + }; + + // Pure function - cannot fail + let resolved_template = domain::resolve_template_path(&doc_root, &uri, &validated_config.template_path); + + // Resolve parameters - can fail if nginx variable doesn't exist + let var_resolver = NginxVariableResolver::new(request); + let resolved_params = match domain::resolve_parameters(&validated_config.parameters, &var_resolver) { + Ok(params) => params, + Err(e) => { + ngx_log_debug_http!(request, "parameter error: {}", e); + return ngx::http::HTTPStatus::BAD_REQUEST.into(); + } + }; + + // Create processor and run - uses dependency injection + let html = execute_with_processor(&validated_config, &resolved_template, &resolved_params, request); + + // Send response - proven correct by types + send_response(request, &html) +} + +/// Execute query and render with proper dependency injection +fn execute_with_processor( + config: &ValidatedConfig, + resolved_template: &domain::ResolvedTemplate, + resolved_params: &[(String, String)], + request: &mut ngx::http::Request, +) -> String { + let mut reg = handlebars::Handlebars::new(); + let reg_ptr: *mut handlebars::Handlebars<'static> = unsafe { std::mem::transmute(&mut reg) }; + let hbs_adapter = unsafe { HandlebarsAdapter::new(reg_ptr) }; + + let processor = RequestProcessor::new( + SqliteQueryExecutor, + hbs_adapter, + hbs_adapter, + ); + + let main_conf = Module::main_conf(request).expect("main config is none"); + let global_dir = if !main_conf.global_templates_dir.is_empty() { + Some(main_conf.global_templates_dir.as_str()) + } else { + None + }; + + // If processor fails, return error HTML instead of panicking + processor + .process(config, resolved_template, resolved_params, global_dir) + .unwrap_or_else(|e| { + format!( + "

Error

{}
", + e + ) + }) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_valid_config_token_accepts_valid() { + let config = ModuleConfig { + db_path: "test.db".to_string(), + query: "SELECT * FROM test".to_string(), + template_path: "test.hbs".to_string(), + query_params: vec![], + }; + + let token = ValidConfigToken::new(&config); + assert!(token.is_some()); + } + + #[test] + fn test_valid_config_token_rejects_empty_db() { + let config = ModuleConfig { + db_path: String::new(), + query: "SELECT * FROM test".to_string(), + template_path: "test.hbs".to_string(), + query_params: vec![], + }; + + let token = ValidConfigToken::new(&config); + assert!(token.is_none()); + } + + #[test] + fn test_valid_config_token_rejects_empty_query() { + let config = ModuleConfig { + db_path: "test.db".to_string(), + query: String::new(), + template_path: "test.hbs".to_string(), + query_params: vec![], + }; + + let token = ValidConfigToken::new(&config); + assert!(token.is_none()); + } + + #[test] + fn test_valid_config_token_rejects_empty_template() { + let config = ModuleConfig { + db_path: "test.db".to_string(), + query: "SELECT * FROM test".to_string(), + template_path: String::new(), + query_params: vec![], + }; + + let token = ValidConfigToken::new(&config); + assert!(token.is_none()); + } +} + diff --git a/src/lib.rs b/src/lib.rs index d56209b..a439405 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,6 +3,7 @@ mod adapters; mod config; mod domain; +mod handler_types; mod nginx_helpers; mod parsing; mod query; @@ -10,17 +11,15 @@ mod template; mod types; mod variable; -use adapters::{HandlebarsAdapter, NginxVariableResolver, SqliteQueryExecutor}; use config::{MainConfig, ModuleConfig}; -use domain::RequestProcessor; -use nginx_helpers::{get_doc_root_and_uri, log_error, send_response}; +use handler_types::{process_request, ValidConfigToken}; use ngx::ffi::{ NGX_CONF_TAKE1, NGX_CONF_TAKE2, NGX_HTTP_LOC_CONF, NGX_HTTP_MAIN_CONF, NGX_HTTP_MODULE, NGX_HTTP_LOC_CONF_OFFSET, NGX_RS_MODULE_SIGNATURE, nginx_version, ngx_command_t, ngx_conf_t, ngx_http_module_t, ngx_int_t, ngx_module_t, ngx_str_t, ngx_uint_t, }; use ngx::http::{HttpModule, HttpModuleLocationConf, HttpModuleMainConf, NgxHttpCoreModule}; -use ngx::{core::Status, http, http_request_handler, ngx_log_debug_http, ngx_modules, ngx_string}; +use ngx::{core::Status, http, http_request_handler, ngx_modules, ngx_string}; use std::os::raw::{c_char, c_void}; use std::ptr::addr_of; @@ -246,57 +245,13 @@ extern "C" fn ngx_http_howto_commands_add_param( std::ptr::null_mut() } -// HTTP request handler - minimal glue code orchestrating domain layer +// HTTP request handler - correctness guaranteed by types (Ghost of Departed Proofs) http_request_handler!(howto_access_handler, |request: &mut http::Request| { - let co = Module::location_conf(request).expect("module config is none"); + let config = Module::location_conf(request).expect("module config is none"); - // Skip if not configured - if co.db_path.is_empty() || co.query.is_empty() || co.template_path.is_empty() { - return Status::NGX_OK; + // Type-safe gate: only proceed if we have proof of valid config + match ValidConfigToken::new(config) { + Some(valid_config) => process_request(request, valid_config), + None => Status::NGX_OK, // Not configured - skip silently } - - // Parse configuration into validated domain types - let validated_config = match parsing::parse_config(co) { - Ok(config) => config, - Err(e) => return log_error(request, "config parse error", &e, http::HTTPStatus::INTERNAL_SERVER_ERROR), - }; - - // Get document root and URI from nginx - let (doc_root, uri) = match get_doc_root_and_uri(request) { - Ok(paths) => paths, - Err(e) => return log_error(request, "path resolution error", &e, http::HTTPStatus::INTERNAL_SERVER_ERROR), - }; - - // Resolve template path (pure function) - let resolved_template = domain::resolve_template_path(&doc_root, &uri, &validated_config.template_path); - - // Resolve parameters from nginx variables - let var_resolver = NginxVariableResolver::new(request); - let resolved_params = match domain::resolve_parameters(&validated_config.parameters, &var_resolver) { - Ok(params) => params, - Err(e) => return log_error(request, "parameter resolution error", &e, http::HTTPStatus::BAD_REQUEST), - }; - - // Create processor with injected dependencies - let mut reg = handlebars::Handlebars::new(); - let reg_ptr: *mut handlebars::Handlebars<'static> = unsafe { std::mem::transmute(&mut reg) }; - let hbs_adapter = unsafe { HandlebarsAdapter::new(reg_ptr) }; - let processor = RequestProcessor::new(SqliteQueryExecutor, hbs_adapter, hbs_adapter); - - // Get global template directory - let main_conf = Module::main_conf(request).expect("main config is none"); - let global_dir = if !main_conf.global_templates_dir.is_empty() { - Some(main_conf.global_templates_dir.as_str()) - } else { - None - }; - - // Process request through functional core - let body = match processor.process(&validated_config, &resolved_template, &resolved_params, global_dir) { - Ok(html) => html, - Err(e) => return log_error(request, "request processing error", &e, http::HTTPStatus::INTERNAL_SERVER_ERROR), - }; - - // Send response - send_response(request, &body) });