Files
nginx-serve/src/handler_types.rs
Edward Langley 56c6045e3b Use ngx_log_error! macro and fix formatting
- Switched from ngx_log_error_core() to ngx_log_error! macro
- Changed error_log level from debug to info (cleaner output)
- Formatting cleanup across all modules (cargo fmt)
- Removed trailing newlines and fixed indentation

Logging now properly uses nginx's macro system for better
integration with nginx's log handling.
2025-11-15 17:16:55 -08:00

245 lines
8.0 KiB
Rust

//! Handler-specific types that guarantee correctness
use crate::adapters::{HandlebarsAdapter, NginxVariableResolver, SqliteQueryExecutor};
use crate::config::{MainConfig, ModuleConfig};
use crate::domain::{RequestProcessor, ValidatedConfig};
use crate::logging;
use crate::nginx_helpers::{get_doc_root_and_uri, send_response};
use crate::parsing;
use crate::{Module, domain};
use ngx::core::Status;
use ngx::http::{HttpModuleLocationConf, HttpModuleMainConf};
/// 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<Self> {
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 {
logging::log(
request,
logging::LogLevel::Debug,
"handler",
&format!(
"Processing request for {}",
request.unparsed_uri().to_str().unwrap_or("unknown")
),
);
// Parse config into validated types
let validated_config = match parsing::parse_config(config.get()) {
Ok(c) => c,
Err(e) => {
logging::log_config_error(request, "configuration", "", &e);
return ngx::http::HTTPStatus::INTERNAL_SERVER_ERROR.into();
}
};
// Get nginx paths
let (doc_root, uri) = match get_doc_root_and_uri(request) {
Ok(paths) => paths,
Err(e) => {
logging::log(
request,
logging::LogLevel::Error,
"nginx",
&format!("Path resolution failed: {}", e),
);
return ngx::http::HTTPStatus::INTERNAL_SERVER_ERROR.into();
}
};
// Resolve template path (pure function - cannot fail)
let resolved_template =
domain::resolve_template_path(&doc_root, &uri, &validated_config.template_path);
logging::log(
request,
logging::LogLevel::Debug,
"template",
&format!("Resolved template: {}", resolved_template.full_path()),
);
// Resolve parameters
let var_resolver = NginxVariableResolver::new(request);
let resolved_params =
match domain::resolve_parameters(&validated_config.parameters, &var_resolver) {
Ok(params) => {
if !params.is_empty() {
logging::log(
request,
logging::LogLevel::Debug,
"params",
&format!("Resolved {} parameters", params.len()),
);
}
params
}
Err(e) => {
logging::log_param_error(request, "variable", &e);
return ngx::http::HTTPStatus::BAD_REQUEST.into();
}
};
// Execute and render
let html = execute_with_processor(
&validated_config,
&resolved_template,
&resolved_params,
request,
);
// Send response
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() {
logging::log_template_loading(request, "global", 0, &main_conf.global_templates_dir);
Some(main_conf.global_templates_dir.as_str())
} else {
None
};
// Process through functional core
match processor.process(config, resolved_template, resolved_params, global_dir) {
Ok(html) => {
// Count results for logging (parse HTML or trust it worked)
logging::log(
request,
logging::LogLevel::Info,
"success",
&format!(
"Rendered {} with {} params",
resolved_template
.full_path()
.split('/')
.last()
.unwrap_or("template"),
resolved_params.len()
),
);
html
}
Err(e) => {
// Log detailed error information
if e.contains("query") {
logging::log_query_error(request, config.query.as_str(), &e);
} else if e.contains("template") {
logging::log_template_error(request, resolved_template.full_path(), &e);
} else {
logging::log(
request,
logging::LogLevel::Error,
"processing",
&format!("Request processing failed: {}", e),
);
}
// Return user-friendly error page
format!(
r#"<!DOCTYPE html>
<html>
<head><title>Error - sqlite-serve</title></head>
<body style="font-family: monospace; max-width: 800px; margin: 2rem auto; padding: 0 1rem;">
<h1 style="color: #CC9393;">Request Processing Error</h1>
<p style="color: #A6A689;">An error occurred while processing your request.</p>
<details style="margin-top: 1rem; background: #1111; padding: 1rem; border-left: 3px solid #CC9393;">
<summary style="cursor: pointer; color: #DFAF8F; font-weight: bold;">Error Details</summary>
<pre style="margin-top: 1rem; color: #DCDCCC; overflow-x: auto;">{}</pre>
</details>
<p style="margin-top: 2rem;"><a href="/" style="color: #7CB8BB;">← Back to Home</a></p>
</body>
</html>"#,
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());
}
}