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.
This commit is contained in:
1
build.rs
1
build.rs
@ -7,4 +7,3 @@ fn main() {
|
|||||||
println!("cargo:rustc-link-arg=dynamic_lookup");
|
println!("cargo:rustc-link-arg=dynamic_lookup");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -8,7 +8,7 @@ events {
|
|||||||
worker_connections 1024;
|
worker_connections 1024;
|
||||||
}
|
}
|
||||||
|
|
||||||
error_log logs/error.log debug;
|
error_log logs/error.log info;
|
||||||
|
|
||||||
http {
|
http {
|
||||||
# Global templates for shared components
|
# Global templates for shared components
|
||||||
|
|||||||
@ -41,8 +41,7 @@ impl QueryExecutor for SqliteQueryExecutor {
|
|||||||
query: &SqlQuery,
|
query: &SqlQuery,
|
||||||
params: &[(String, String)],
|
params: &[(String, String)],
|
||||||
) -> Result<Vec<HashMap<String, Value>>, String> {
|
) -> Result<Vec<HashMap<String, Value>>, String> {
|
||||||
query::execute_query(db_path.as_str(), query.as_str(), params)
|
query::execute_query(db_path.as_str(), query.as_str(), params).map_err(|e| e.to_string())
|
||||||
.map_err(|e| e.to_string())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -54,7 +53,7 @@ pub struct HandlebarsAdapter {
|
|||||||
|
|
||||||
impl HandlebarsAdapter {
|
impl HandlebarsAdapter {
|
||||||
/// Create adapter from mutable handlebars registry
|
/// Create adapter from mutable handlebars registry
|
||||||
///
|
///
|
||||||
/// # Safety
|
/// # Safety
|
||||||
/// Caller must ensure the registry outlives this adapter
|
/// Caller must ensure the registry outlives this adapter
|
||||||
pub unsafe fn new(registry: *mut Handlebars<'static>) -> Self {
|
pub unsafe fn new(registry: *mut Handlebars<'static>) -> Self {
|
||||||
@ -81,7 +80,11 @@ impl TemplateLoader for HandlebarsAdapter {
|
|||||||
|
|
||||||
impl TemplateRenderer for HandlebarsAdapter {
|
impl TemplateRenderer for HandlebarsAdapter {
|
||||||
fn render(&self, template_name: &str, data: &Value) -> Result<String, String> {
|
fn render(&self, template_name: &str, data: &Value) -> Result<String, String> {
|
||||||
unsafe { (*self.registry).render(template_name, data).map_err(|e| e.to_string()) }
|
unsafe {
|
||||||
|
(*self.registry)
|
||||||
|
.render(template_name, data)
|
||||||
|
.map_err(|e| e.to_string())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -136,9 +139,7 @@ mod tests {
|
|||||||
let reg_ptr: *mut Handlebars<'static> = unsafe { std::mem::transmute(&mut reg) };
|
let reg_ptr: *mut Handlebars<'static> = unsafe { std::mem::transmute(&mut reg) };
|
||||||
let adapter = unsafe { HandlebarsAdapter::new(reg_ptr) };
|
let adapter = unsafe { HandlebarsAdapter::new(reg_ptr) };
|
||||||
|
|
||||||
adapter
|
adapter.register_template("test", &template_path).unwrap();
|
||||||
.register_template("test", &template_path)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let data = serde_json::json!({"name": "World"});
|
let data = serde_json::json!({"name": "World"});
|
||||||
let rendered = adapter.render("test", &data).unwrap();
|
let rendered = adapter.render("test", &data).unwrap();
|
||||||
@ -148,4 +149,3 @@ mod tests {
|
|||||||
let _ = fs::remove_dir_all(temp_dir);
|
let _ = fs::remove_dir_all(temp_dir);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -130,4 +130,3 @@ mod tests {
|
|||||||
assert_eq!(config.global_templates_dir, "templates/global");
|
assert_eq!(config.global_templates_dir, "templates/global");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -25,7 +25,7 @@ impl ResolvedTemplate {
|
|||||||
pub fn full_path(&self) -> &str {
|
pub fn full_path(&self) -> &str {
|
||||||
&self.full_path
|
&self.full_path
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn directory(&self) -> &str {
|
pub fn directory(&self) -> &str {
|
||||||
&self.directory
|
&self.directory
|
||||||
}
|
}
|
||||||
@ -231,11 +231,9 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_resolve_parameters_positional() {
|
fn test_resolve_parameters_positional() {
|
||||||
let bindings = vec![
|
let bindings = vec![ParameterBinding::Positional {
|
||||||
ParameterBinding::Positional {
|
variable: NginxVariable::parse("$arg_id").unwrap(),
|
||||||
variable: NginxVariable::parse("$arg_id").unwrap(),
|
}];
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
let resolver = MockVariableResolver;
|
let resolver = MockVariableResolver;
|
||||||
let resolved = resolve_parameters(&bindings, &resolver).unwrap();
|
let resolved = resolve_parameters(&bindings, &resolver).unwrap();
|
||||||
@ -247,12 +245,10 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_resolve_parameters_named() {
|
fn test_resolve_parameters_named() {
|
||||||
let bindings = vec![
|
let bindings = vec![ParameterBinding::Named {
|
||||||
ParameterBinding::Named {
|
name: ParamName::parse(":book_id").unwrap(),
|
||||||
name: ParamName::parse(":book_id").unwrap(),
|
variable: NginxVariable::parse("$arg_id").unwrap(),
|
||||||
variable: NginxVariable::parse("$arg_id").unwrap(),
|
}];
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
let resolver = MockVariableResolver;
|
let resolver = MockVariableResolver;
|
||||||
let resolved = resolve_parameters(&bindings, &resolver).unwrap();
|
let resolved = resolve_parameters(&bindings, &resolver).unwrap();
|
||||||
@ -264,11 +260,9 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_resolve_parameters_literal() {
|
fn test_resolve_parameters_literal() {
|
||||||
let bindings = vec![
|
let bindings = vec![ParameterBinding::PositionalLiteral {
|
||||||
ParameterBinding::PositionalLiteral {
|
value: "constant".to_string(),
|
||||||
value: "constant".to_string(),
|
}];
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
let resolver = MockVariableResolver;
|
let resolver = MockVariableResolver;
|
||||||
let resolved = resolve_parameters(&bindings, &resolver).unwrap();
|
let resolved = resolve_parameters(&bindings, &resolver).unwrap();
|
||||||
@ -291,11 +285,8 @@ mod tests {
|
|||||||
directory: "templates".to_string(),
|
directory: "templates".to_string(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let processor = RequestProcessor::new(
|
let processor =
|
||||||
MockQueryExecutor,
|
RequestProcessor::new(MockQueryExecutor, MockTemplateLoader, MockTemplateRenderer);
|
||||||
MockTemplateLoader,
|
|
||||||
MockTemplateRenderer,
|
|
||||||
);
|
|
||||||
|
|
||||||
let result = processor.process(&config, &resolved_template, &[], None);
|
let result = processor.process(&config, &resolved_template, &[], None);
|
||||||
|
|
||||||
@ -303,4 +294,3 @@ mod tests {
|
|||||||
assert!(result.unwrap().contains("Rendered"));
|
assert!(result.unwrap().contains("Rendered"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -6,7 +6,7 @@ use crate::domain::{RequestProcessor, ValidatedConfig};
|
|||||||
use crate::logging;
|
use crate::logging;
|
||||||
use crate::nginx_helpers::{get_doc_root_and_uri, send_response};
|
use crate::nginx_helpers::{get_doc_root_and_uri, send_response};
|
||||||
use crate::parsing;
|
use crate::parsing;
|
||||||
use crate::{domain, Module};
|
use crate::{Module, domain};
|
||||||
use ngx::core::Status;
|
use ngx::core::Status;
|
||||||
use ngx::http::{HttpModuleLocationConf, HttpModuleMainConf};
|
use ngx::http::{HttpModuleLocationConf, HttpModuleMainConf};
|
||||||
|
|
||||||
@ -31,15 +31,15 @@ impl<'a> ValidConfigToken<'a> {
|
|||||||
|
|
||||||
/// Process a request with guaranteed valid configuration
|
/// Process a request with guaranteed valid configuration
|
||||||
/// Returns Status directly - no Result needed, types prove correctness
|
/// Returns Status directly - no Result needed, types prove correctness
|
||||||
pub fn process_request(
|
pub fn process_request(request: &mut ngx::http::Request, config: ValidConfigToken) -> Status {
|
||||||
request: &mut ngx::http::Request,
|
|
||||||
config: ValidConfigToken,
|
|
||||||
) -> Status {
|
|
||||||
logging::log(
|
logging::log(
|
||||||
request,
|
request,
|
||||||
logging::LogLevel::Debug,
|
logging::LogLevel::Debug,
|
||||||
"handler",
|
"handler",
|
||||||
&format!("Processing request for {}", request.unparsed_uri().to_str().unwrap_or("unknown")),
|
&format!(
|
||||||
|
"Processing request for {}",
|
||||||
|
request.unparsed_uri().to_str().unwrap_or("unknown")
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Parse config into validated types
|
// Parse config into validated types
|
||||||
@ -55,13 +55,19 @@ pub fn process_request(
|
|||||||
let (doc_root, uri) = match get_doc_root_and_uri(request) {
|
let (doc_root, uri) = match get_doc_root_and_uri(request) {
|
||||||
Ok(paths) => paths,
|
Ok(paths) => paths,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
logging::log(request, logging::LogLevel::Error, "nginx", &format!("Path resolution failed: {}", e));
|
logging::log(
|
||||||
|
request,
|
||||||
|
logging::LogLevel::Error,
|
||||||
|
"nginx",
|
||||||
|
&format!("Path resolution failed: {}", e),
|
||||||
|
);
|
||||||
return ngx::http::HTTPStatus::INTERNAL_SERVER_ERROR.into();
|
return ngx::http::HTTPStatus::INTERNAL_SERVER_ERROR.into();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Resolve template path (pure function - cannot fail)
|
// Resolve template path (pure function - cannot fail)
|
||||||
let resolved_template = domain::resolve_template_path(&doc_root, &uri, &validated_config.template_path);
|
let resolved_template =
|
||||||
|
domain::resolve_template_path(&doc_root, &uri, &validated_config.template_path);
|
||||||
|
|
||||||
logging::log(
|
logging::log(
|
||||||
request,
|
request,
|
||||||
@ -72,27 +78,33 @@ pub fn process_request(
|
|||||||
|
|
||||||
// Resolve parameters
|
// Resolve parameters
|
||||||
let var_resolver = NginxVariableResolver::new(request);
|
let var_resolver = NginxVariableResolver::new(request);
|
||||||
let resolved_params = match domain::resolve_parameters(&validated_config.parameters, &var_resolver) {
|
let resolved_params =
|
||||||
Ok(params) => {
|
match domain::resolve_parameters(&validated_config.parameters, &var_resolver) {
|
||||||
if !params.is_empty() {
|
Ok(params) => {
|
||||||
logging::log(
|
if !params.is_empty() {
|
||||||
request,
|
logging::log(
|
||||||
logging::LogLevel::Debug,
|
request,
|
||||||
"params",
|
logging::LogLevel::Debug,
|
||||||
&format!("Resolved {} parameters", params.len()),
|
"params",
|
||||||
);
|
&format!("Resolved {} parameters", params.len()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
params
|
||||||
}
|
}
|
||||||
params
|
Err(e) => {
|
||||||
}
|
logging::log_param_error(request, "variable", &e);
|
||||||
Err(e) => {
|
return ngx::http::HTTPStatus::BAD_REQUEST.into();
|
||||||
logging::log_param_error(request, "variable", &e);
|
}
|
||||||
return ngx::http::HTTPStatus::BAD_REQUEST.into();
|
};
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Execute and render
|
// Execute and render
|
||||||
let html = execute_with_processor(&validated_config, &resolved_template, &resolved_params, request);
|
let html = execute_with_processor(
|
||||||
|
&validated_config,
|
||||||
|
&resolved_template,
|
||||||
|
&resolved_params,
|
||||||
|
request,
|
||||||
|
);
|
||||||
|
|
||||||
// Send response
|
// Send response
|
||||||
send_response(request, &html)
|
send_response(request, &html)
|
||||||
}
|
}
|
||||||
@ -107,21 +119,12 @@ fn execute_with_processor(
|
|||||||
let mut reg = handlebars::Handlebars::new();
|
let mut reg = handlebars::Handlebars::new();
|
||||||
let reg_ptr: *mut handlebars::Handlebars<'static> = unsafe { std::mem::transmute(&mut reg) };
|
let reg_ptr: *mut handlebars::Handlebars<'static> = unsafe { std::mem::transmute(&mut reg) };
|
||||||
let hbs_adapter = unsafe { HandlebarsAdapter::new(reg_ptr) };
|
let hbs_adapter = unsafe { HandlebarsAdapter::new(reg_ptr) };
|
||||||
|
|
||||||
let processor = RequestProcessor::new(
|
let processor = RequestProcessor::new(SqliteQueryExecutor, hbs_adapter, hbs_adapter);
|
||||||
SqliteQueryExecutor,
|
|
||||||
hbs_adapter,
|
|
||||||
hbs_adapter,
|
|
||||||
);
|
|
||||||
|
|
||||||
let main_conf = Module::main_conf(request).expect("main config is none");
|
let main_conf = Module::main_conf(request).expect("main config is none");
|
||||||
let global_dir = if !main_conf.global_templates_dir.is_empty() {
|
let global_dir = if !main_conf.global_templates_dir.is_empty() {
|
||||||
logging::log_template_loading(
|
logging::log_template_loading(request, "global", 0, &main_conf.global_templates_dir);
|
||||||
request,
|
|
||||||
"global",
|
|
||||||
0,
|
|
||||||
&main_conf.global_templates_dir,
|
|
||||||
);
|
|
||||||
Some(main_conf.global_templates_dir.as_str())
|
Some(main_conf.global_templates_dir.as_str())
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
@ -137,7 +140,11 @@ fn execute_with_processor(
|
|||||||
"success",
|
"success",
|
||||||
&format!(
|
&format!(
|
||||||
"Rendered {} with {} params",
|
"Rendered {} with {} params",
|
||||||
resolved_template.full_path().split('/').last().unwrap_or("template"),
|
resolved_template
|
||||||
|
.full_path()
|
||||||
|
.split('/')
|
||||||
|
.last()
|
||||||
|
.unwrap_or("template"),
|
||||||
resolved_params.len()
|
resolved_params.len()
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@ -235,4 +242,3 @@ mod tests {
|
|||||||
assert!(token.is_none());
|
assert!(token.is_none());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
12
src/lib.rs
12
src/lib.rs
@ -13,11 +13,11 @@ mod types;
|
|||||||
mod variable;
|
mod variable;
|
||||||
|
|
||||||
use config::{MainConfig, ModuleConfig};
|
use config::{MainConfig, ModuleConfig};
|
||||||
use handler_types::{process_request, ValidConfigToken};
|
use handler_types::{ValidConfigToken, process_request};
|
||||||
use ngx::ffi::{
|
use ngx::ffi::{
|
||||||
NGX_CONF_TAKE1, NGX_CONF_TAKE2, NGX_HTTP_LOC_CONF, NGX_HTTP_MAIN_CONF, NGX_HTTP_MODULE,
|
NGX_CONF_TAKE1, NGX_CONF_TAKE2, NGX_HTTP_LOC_CONF, NGX_HTTP_LOC_CONF_OFFSET,
|
||||||
NGX_HTTP_LOC_CONF_OFFSET, NGX_RS_MODULE_SIGNATURE, nginx_version, ngx_command_t, ngx_conf_t,
|
NGX_HTTP_MAIN_CONF, NGX_HTTP_MODULE, NGX_RS_MODULE_SIGNATURE, nginx_version, ngx_command_t,
|
||||||
ngx_http_module_t, ngx_int_t, ngx_module_t, ngx_str_t, ngx_uint_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::http::{HttpModule, HttpModuleLocationConf, HttpModuleMainConf, NgxHttpCoreModule};
|
||||||
use ngx::{core::Status, http, http_request_handler, ngx_modules, ngx_string};
|
use ngx::{core::Status, http, http_request_handler, ngx_modules, ngx_string};
|
||||||
@ -209,8 +209,8 @@ extern "C" fn ngx_http_howto_commands_set_template_path(
|
|||||||
conf.template_path = (*args.add(1)).to_string();
|
conf.template_path = (*args.add(1)).to_string();
|
||||||
|
|
||||||
// Set the content handler for this location
|
// Set the content handler for this location
|
||||||
let clcf = NgxHttpCoreModule::location_conf_mut(&*cf)
|
let clcf =
|
||||||
.expect("failed to get core location conf");
|
NgxHttpCoreModule::location_conf_mut(&*cf).expect("failed to get core location conf");
|
||||||
clcf.handler = Some(howto_access_handler);
|
clcf.handler = Some(howto_access_handler);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -15,29 +15,21 @@ pub enum LogLevel {
|
|||||||
/// Log a message with context using nginx's native logging
|
/// Log a message with context using nginx's native logging
|
||||||
pub fn log(request: &mut Request, level: LogLevel, module: &str, message: &str) {
|
pub fn log(request: &mut Request, level: LogLevel, module: &str, message: &str) {
|
||||||
let r: *mut ngx::ffi::ngx_http_request_t = request.into();
|
let r: *mut ngx::ffi::ngx_http_request_t = request.into();
|
||||||
|
|
||||||
let log_level = match level {
|
let log_level = match level {
|
||||||
LogLevel::Error => 3, // NGX_LOG_ERR
|
LogLevel::Error => 3, // NGX_LOG_ERR
|
||||||
LogLevel::Warn => 4, // NGX_LOG_WARN
|
LogLevel::Warn => 4, // NGX_LOG_WARN
|
||||||
LogLevel::Info => 6, // NGX_LOG_INFO
|
LogLevel::Info => 6, // NGX_LOG_INFO
|
||||||
LogLevel::Debug => 7, // NGX_LOG_DEBUG
|
LogLevel::Debug => 7, // NGX_LOG_DEBUG
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let r: *mut ngx::ffi::ngx_http_request_t = request.into();
|
||||||
unsafe {
|
unsafe {
|
||||||
let connection = (*r).connection;
|
let connection = (*r).connection;
|
||||||
if !connection.is_null() {
|
if !connection.is_null() {
|
||||||
let log_ptr = (*connection).log;
|
let log = (*connection).log;
|
||||||
if !log_ptr.is_null() {
|
if !log.is_null() {
|
||||||
// ngx_log_error! requires a string literal, so we use the C API directly
|
ngx_log_error!(log_level, log, "[sqlite-serve:{}] {}", module, message);
|
||||||
let c_msg = std::ffi::CString::new(format!("[sqlite-serve:{}] {}", module, message))
|
|
||||||
.unwrap_or_else(|_| std::ffi::CString::new("log message error").unwrap());
|
|
||||||
|
|
||||||
ngx::ffi::ngx_log_error_core(
|
|
||||||
log_level as ngx::ffi::ngx_uint_t,
|
|
||||||
log_ptr,
|
|
||||||
0,
|
|
||||||
c_msg.as_ptr(),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -128,4 +120,3 @@ mod tests {
|
|||||||
assert_eq!(levels.len(), 4);
|
assert_eq!(levels.len(), 4);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -28,7 +28,6 @@ pub fn get_doc_root_and_uri(request: &mut Request) -> Result<(String, String), S
|
|||||||
|
|
||||||
/// Create and send nginx response buffer
|
/// Create and send nginx response buffer
|
||||||
pub fn send_response(request: &mut Request, body: &str) -> Status {
|
pub fn send_response(request: &mut Request, body: &str) -> Status {
|
||||||
|
|
||||||
// Create output buffer
|
// Create output buffer
|
||||||
let mut buf = match request.pool().create_buffer_from_str(body) {
|
let mut buf = match request.pool().create_buffer_from_str(body) {
|
||||||
Some(buf) => buf,
|
Some(buf) => buf,
|
||||||
@ -45,7 +44,7 @@ pub fn send_response(request: &mut Request, body: &str) -> Status {
|
|||||||
|
|
||||||
request.discard_request_body();
|
request.discard_request_body();
|
||||||
request.set_status(http::HTTPStatus::OK);
|
request.set_status(http::HTTPStatus::OK);
|
||||||
|
|
||||||
let rc = request.send_header();
|
let rc = request.send_header();
|
||||||
if rc == Status::NGX_ERROR || rc > Status::NGX_OK || request.header_only() {
|
if rc == Status::NGX_ERROR || rc > Status::NGX_OK || request.header_only() {
|
||||||
return rc;
|
return rc;
|
||||||
@ -57,8 +56,12 @@ pub fn send_response(request: &mut Request, body: &str) -> Status {
|
|||||||
|
|
||||||
/// Log and return error status (deprecated - use logging module directly)
|
/// Log and return error status (deprecated - use logging module directly)
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
pub fn log_error(request: &mut Request, context: &str, error: &str, status: http::HTTPStatus) -> Status {
|
pub fn log_error(
|
||||||
|
request: &mut Request,
|
||||||
|
context: &str,
|
||||||
|
error: &str,
|
||||||
|
status: http::HTTPStatus,
|
||||||
|
) -> Status {
|
||||||
logging::log(request, logging::LogLevel::Error, context, error);
|
logging::log(request, logging::LogLevel::Error, context, error);
|
||||||
status.into()
|
status.into()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -2,15 +2,16 @@
|
|||||||
|
|
||||||
use crate::config::ModuleConfig;
|
use crate::config::ModuleConfig;
|
||||||
use crate::domain::ValidatedConfig;
|
use crate::domain::ValidatedConfig;
|
||||||
use crate::types::{DatabasePath, NginxVariable, ParamName, ParameterBinding, SqlQuery, TemplatePath};
|
use crate::types::{
|
||||||
|
DatabasePath, NginxVariable, ParamName, ParameterBinding, SqlQuery, TemplatePath,
|
||||||
|
};
|
||||||
|
|
||||||
/// Parse raw configuration into validated domain configuration
|
/// Parse raw configuration into validated domain configuration
|
||||||
pub fn parse_config(config: &ModuleConfig) -> Result<ValidatedConfig, String> {
|
pub fn parse_config(config: &ModuleConfig) -> Result<ValidatedConfig, String> {
|
||||||
let db_path = DatabasePath::parse(&config.db_path)
|
let db_path =
|
||||||
.map_err(|e| format!("invalid db_path: {}", e))?;
|
DatabasePath::parse(&config.db_path).map_err(|e| format!("invalid db_path: {}", e))?;
|
||||||
|
|
||||||
let query = SqlQuery::parse(&config.query)
|
let query = SqlQuery::parse(&config.query).map_err(|e| format!("invalid query: {}", e))?;
|
||||||
.map_err(|e| format!("invalid query: {}", e))?;
|
|
||||||
|
|
||||||
let template_path = TemplatePath::parse(&config.template_path)
|
let template_path = TemplatePath::parse(&config.template_path)
|
||||||
.map_err(|e| format!("invalid template_path: {}", e))?;
|
.map_err(|e| format!("invalid template_path: {}", e))?;
|
||||||
@ -26,9 +27,7 @@ pub fn parse_config(config: &ModuleConfig) -> Result<ValidatedConfig, String> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Parse parameter configuration into typed bindings
|
/// Parse parameter configuration into typed bindings
|
||||||
fn parse_parameter_bindings(
|
fn parse_parameter_bindings(params: &[(String, String)]) -> Result<Vec<ParameterBinding>, String> {
|
||||||
params: &[(String, String)],
|
|
||||||
) -> Result<Vec<ParameterBinding>, String> {
|
|
||||||
let mut bindings = Vec::new();
|
let mut bindings = Vec::new();
|
||||||
|
|
||||||
for (param_name, var_name) in params {
|
for (param_name, var_name) in params {
|
||||||
@ -169,4 +168,3 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
40
src/query.rs
40
src/query.rs
@ -31,11 +31,9 @@ pub fn execute_query(
|
|||||||
let value: Value = match row.get_ref(i)? {
|
let value: Value = match row.get_ref(i)? {
|
||||||
rusqlite::types::ValueRef::Null => Value::Null,
|
rusqlite::types::ValueRef::Null => Value::Null,
|
||||||
rusqlite::types::ValueRef::Integer(v) => Value::Number(v.into()),
|
rusqlite::types::ValueRef::Integer(v) => Value::Number(v.into()),
|
||||||
rusqlite::types::ValueRef::Real(v) => {
|
rusqlite::types::ValueRef::Real(v) => serde_json::Number::from_f64(v)
|
||||||
serde_json::Number::from_f64(v)
|
.map(Value::Number)
|
||||||
.map(Value::Number)
|
.unwrap_or(Value::Null),
|
||||||
.unwrap_or(Value::Null)
|
|
||||||
}
|
|
||||||
rusqlite::types::ValueRef::Text(v) => {
|
rusqlite::types::ValueRef::Text(v) => {
|
||||||
Value::String(String::from_utf8_lossy(v).to_string())
|
Value::String(String::from_utf8_lossy(v).to_string())
|
||||||
}
|
}
|
||||||
@ -90,11 +88,8 @@ mod tests {
|
|||||||
|
|
||||||
{
|
{
|
||||||
let conn = Connection::open(temp_path).unwrap();
|
let conn = Connection::open(temp_path).unwrap();
|
||||||
conn.execute(
|
conn.execute("CREATE TABLE test (id INTEGER, name TEXT, value REAL)", [])
|
||||||
"CREATE TABLE test (id INTEGER, name TEXT, value REAL)",
|
.unwrap();
|
||||||
[],
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
conn.execute(
|
conn.execute(
|
||||||
"INSERT INTO test VALUES (1, 'first', 1.5), (2, 'second', 2.5)",
|
"INSERT INTO test VALUES (1, 'first', 1.5), (2, 'second', 2.5)",
|
||||||
[],
|
[],
|
||||||
@ -104,10 +99,7 @@ mod tests {
|
|||||||
|
|
||||||
let results = execute_query(temp_path, "SELECT * FROM test ORDER BY id", &[]).unwrap();
|
let results = execute_query(temp_path, "SELECT * FROM test ORDER BY id", &[]).unwrap();
|
||||||
assert_eq!(results.len(), 2);
|
assert_eq!(results.len(), 2);
|
||||||
assert_eq!(
|
assert_eq!(results[0].get("id").unwrap(), &Value::Number(1.into()));
|
||||||
results[0].get("id").unwrap(),
|
|
||||||
&Value::Number(1.into())
|
|
||||||
);
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
results[0].get("name").unwrap(),
|
results[0].get("name").unwrap(),
|
||||||
&Value::String("first".to_string())
|
&Value::String("first".to_string())
|
||||||
@ -157,8 +149,11 @@ mod tests {
|
|||||||
|
|
||||||
{
|
{
|
||||||
let conn = Connection::open(temp_path).unwrap();
|
let conn = Connection::open(temp_path).unwrap();
|
||||||
conn.execute("CREATE TABLE books (id INTEGER, title TEXT, year INTEGER)", [])
|
conn.execute(
|
||||||
.unwrap();
|
"CREATE TABLE books (id INTEGER, title TEXT, year INTEGER)",
|
||||||
|
[],
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
conn.execute(
|
conn.execute(
|
||||||
"INSERT INTO books VALUES (1, 'Old Book', 2000), (2, 'New Book', 2020), (3, 'Newer Book', 2023)",
|
"INSERT INTO books VALUES (1, 'Old Book', 2000), (2, 'New Book', 2020), (3, 'Newer Book', 2023)",
|
||||||
[],
|
[],
|
||||||
@ -218,10 +213,7 @@ mod tests {
|
|||||||
let row = &results[0];
|
let row = &results[0];
|
||||||
|
|
||||||
assert_eq!(row.get("id").unwrap(), &Value::Number(42.into()));
|
assert_eq!(row.get("id").unwrap(), &Value::Number(42.into()));
|
||||||
assert_eq!(
|
assert_eq!(row.get("name").unwrap(), &Value::String("test".to_string()));
|
||||||
row.get("name").unwrap(),
|
|
||||||
&Value::String("test".to_string())
|
|
||||||
);
|
|
||||||
assert_eq!(row.get("price").unwrap().as_f64().unwrap(), 3.14);
|
assert_eq!(row.get("price").unwrap().as_f64().unwrap(), 3.14);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
row.get("data").unwrap(),
|
row.get("data").unwrap(),
|
||||||
@ -242,8 +234,11 @@ mod tests {
|
|||||||
|
|
||||||
{
|
{
|
||||||
let conn = Connection::open(temp_path).unwrap();
|
let conn = Connection::open(temp_path).unwrap();
|
||||||
conn.execute("CREATE TABLE books (id INTEGER, genre TEXT, rating REAL)", [])
|
conn.execute(
|
||||||
.unwrap();
|
"CREATE TABLE books (id INTEGER, genre TEXT, rating REAL)",
|
||||||
|
[],
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
conn.execute(
|
conn.execute(
|
||||||
"INSERT INTO books VALUES
|
"INSERT INTO books VALUES
|
||||||
(1, 'Fiction', 4.5),
|
(1, 'Fiction', 4.5),
|
||||||
@ -324,4 +319,3 @@ mod tests {
|
|||||||
let _ = fs::remove_file(temp_path);
|
let _ = fs::remove_file(temp_path);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -157,4 +157,3 @@ mod tests {
|
|||||||
let _ = fs::remove_dir_all(temp_dir);
|
let _ = fs::remove_dir_all(temp_dir);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
51
src/types.rs
51
src/types.rs
@ -36,16 +36,16 @@ impl SqlQuery {
|
|||||||
pub fn parse(query: impl Into<String>) -> Result<Self, String> {
|
pub fn parse(query: impl Into<String>) -> Result<Self, String> {
|
||||||
let query = query.into();
|
let query = query.into();
|
||||||
let trimmed = query.trim().to_uppercase();
|
let trimmed = query.trim().to_uppercase();
|
||||||
|
|
||||||
if trimmed.is_empty() {
|
if trimmed.is_empty() {
|
||||||
return Err("query cannot be empty".to_string());
|
return Err("query cannot be empty".to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure it's a SELECT query (read-only)
|
// Ensure it's a SELECT query (read-only)
|
||||||
if !trimmed.starts_with("SELECT") {
|
if !trimmed.starts_with("SELECT") {
|
||||||
return Err("only SELECT queries are allowed".to_string());
|
return Err("only SELECT queries are allowed".to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(SqlQuery(query))
|
Ok(SqlQuery(query))
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -62,16 +62,16 @@ impl TemplatePath {
|
|||||||
/// Parse and validate a template path
|
/// Parse and validate a template path
|
||||||
pub fn parse(path: impl AsRef<Path>) -> Result<Self, String> {
|
pub fn parse(path: impl AsRef<Path>) -> Result<Self, String> {
|
||||||
let path = path.as_ref();
|
let path = path.as_ref();
|
||||||
|
|
||||||
if path.as_os_str().is_empty() {
|
if path.as_os_str().is_empty() {
|
||||||
return Err("template path cannot be empty".to_string());
|
return Err("template path cannot be empty".to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure it's a .hbs file
|
// Ensure it's a .hbs file
|
||||||
if path.extension().and_then(|e| e.to_str()) != Some("hbs") {
|
if path.extension().and_then(|e| e.to_str()) != Some("hbs") {
|
||||||
return Err("template must be a .hbs file".to_string());
|
return Err("template must be a .hbs file".to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(TemplatePath(path.to_path_buf()))
|
Ok(TemplatePath(path.to_path_buf()))
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -92,28 +92,28 @@ impl NginxVariable {
|
|||||||
/// Parse a nginx variable name
|
/// Parse a nginx variable name
|
||||||
pub fn parse(name: impl Into<String>) -> Result<Self, String> {
|
pub fn parse(name: impl Into<String>) -> Result<Self, String> {
|
||||||
let name = name.into();
|
let name = name.into();
|
||||||
|
|
||||||
if name.is_empty() {
|
if name.is_empty() {
|
||||||
return Err("variable name cannot be empty".to_string());
|
return Err("variable name cannot be empty".to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
if !name.starts_with('$') {
|
if !name.starts_with('$') {
|
||||||
return Err(format!("variable name must start with $: {}", name));
|
return Err(format!("variable name must start with $: {}", name));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the part after $
|
// Get the part after $
|
||||||
let var_name = &name[1..];
|
let var_name = &name[1..];
|
||||||
if var_name.is_empty() {
|
if var_name.is_empty() {
|
||||||
return Err("variable name after $ cannot be empty".to_string());
|
return Err("variable name after $ cannot be empty".to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(NginxVariable(name))
|
Ok(NginxVariable(name))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn as_str(&self) -> &str {
|
pub fn as_str(&self) -> &str {
|
||||||
&self.0
|
&self.0
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the variable name without the $ prefix
|
/// Get the variable name without the $ prefix
|
||||||
pub fn name(&self) -> &str {
|
pub fn name(&self) -> &str {
|
||||||
&self.0[1..]
|
&self.0[1..]
|
||||||
@ -128,23 +128,23 @@ impl ParamName {
|
|||||||
/// Parse a SQL parameter name
|
/// Parse a SQL parameter name
|
||||||
pub fn parse(name: impl Into<String>) -> Result<Self, String> {
|
pub fn parse(name: impl Into<String>) -> Result<Self, String> {
|
||||||
let name = name.into();
|
let name = name.into();
|
||||||
|
|
||||||
if name.is_empty() {
|
if name.is_empty() {
|
||||||
return Err("parameter name cannot be empty".to_string());
|
return Err("parameter name cannot be empty".to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
if !name.starts_with(':') {
|
if !name.starts_with(':') {
|
||||||
return Err(format!("parameter name must start with :: {}", name));
|
return Err(format!("parameter name must start with :: {}", name));
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(ParamName(name))
|
Ok(ParamName(name))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create an empty (positional) parameter name
|
/// Create an empty (positional) parameter name
|
||||||
pub fn positional() -> Self {
|
pub fn positional() -> Self {
|
||||||
ParamName(String::new())
|
ParamName(String::new())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_positional(&self) -> bool {
|
pub fn is_positional(&self) -> bool {
|
||||||
self.0.is_empty()
|
self.0.is_empty()
|
||||||
}
|
}
|
||||||
@ -157,10 +157,20 @@ impl ParamName {
|
|||||||
/// A parameter binding (param name + variable or literal)
|
/// A parameter binding (param name + variable or literal)
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub enum ParameterBinding {
|
pub enum ParameterBinding {
|
||||||
Positional { variable: NginxVariable },
|
Positional {
|
||||||
PositionalLiteral { value: String },
|
variable: NginxVariable,
|
||||||
Named { name: ParamName, variable: NginxVariable },
|
},
|
||||||
NamedLiteral { name: ParamName, value: String },
|
PositionalLiteral {
|
||||||
|
value: String,
|
||||||
|
},
|
||||||
|
Named {
|
||||||
|
name: ParamName,
|
||||||
|
variable: NginxVariable,
|
||||||
|
},
|
||||||
|
NamedLiteral {
|
||||||
|
name: ParamName,
|
||||||
|
value: String,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ParameterBinding {
|
impl ParameterBinding {
|
||||||
@ -300,4 +310,3 @@ mod tests {
|
|||||||
assert!(result.unwrap_err().contains(":"));
|
assert!(result.unwrap_err().contains(":"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -86,8 +86,8 @@ mod tests {
|
|||||||
fn test_named_params_parsing() {
|
fn test_named_params_parsing() {
|
||||||
// Test parameter name parsing logic
|
// Test parameter name parsing logic
|
||||||
let test_cases = vec![
|
let test_cases = vec![
|
||||||
(2, false, ""), // sqlite_param $arg_id
|
(2, false, ""), // sqlite_param $arg_id
|
||||||
(3, true, ":book_id"), // sqlite_param :book_id $arg_id
|
(3, true, ":book_id"), // sqlite_param :book_id $arg_id
|
||||||
];
|
];
|
||||||
|
|
||||||
for (nelts, expected_is_named, expected_param_name) in test_cases {
|
for (nelts, expected_is_named, expected_param_name) in test_cases {
|
||||||
@ -103,4 +103,3 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user