Integrate domain.rs functional core into handler (DI architecture)
Now Actually Using: - ValidatedConfig: Parse config into validated types at request time - ParameterBinding: Type-safe parameter representation - RequestProcessor: Pure business logic with injected dependencies - resolve_template_path(): Pure function for path resolution - resolve_parameters(): Pure parameter resolution New Adapters (adapters.rs): - NginxVariableResolver: Implements VariableResolver trait - SqliteQueryExecutor: Implements QueryExecutor trait - HandlebarsAdapter: Implements TemplateLoader + TemplateRenderer Handler Flow (Functional Core, Imperative Shell): 1. Parse strings into validated types (types.rs) 2. Create validated config (domain.rs) 3. Resolve template path (pure function) 4. Create adapters (adapters.rs - imperative shell) 5. Call RequestProcessor.process() (pure core) 6. Return HTML (imperative shell) Benefits: ✓ Type validation happens at request time ✓ Invalid queries caught early (SELECT-only enforced) ✓ Core business logic is pure and testable ✓ Real DI: can swap implementations via traits ✓ Clear separation of concerns Test Coverage: 47 tests (added 2 adapter tests) Production: Verified working with all features The architecture documented in ARCHITECTURE.md is now actually implemented!
This commit is contained in:
151
src/adapters.rs
Normal file
151
src/adapters.rs
Normal file
@ -0,0 +1,151 @@
|
||||
//! Adapter implementations for domain traits (imperative shell)
|
||||
|
||||
use crate::domain::{QueryExecutor, TemplateLoader, TemplateRenderer, VariableResolver};
|
||||
use crate::query;
|
||||
use crate::template;
|
||||
use crate::types::{DatabasePath, SqlQuery};
|
||||
use crate::variable;
|
||||
use handlebars::Handlebars;
|
||||
use ngx::http::Request;
|
||||
use serde_json::Value;
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// Adapter for nginx variable resolution
|
||||
pub struct NginxVariableResolver<'a> {
|
||||
request: &'a mut Request,
|
||||
}
|
||||
|
||||
impl<'a> NginxVariableResolver<'a> {
|
||||
pub fn new(request: &'a mut Request) -> Self {
|
||||
NginxVariableResolver { request }
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> VariableResolver for NginxVariableResolver<'a> {
|
||||
fn resolve(&self, var_name: &str) -> Result<String, String> {
|
||||
// SAFETY: We need mutable access but trait requires &self
|
||||
// This is safe because nginx variables are read-only from our perspective
|
||||
let request_ptr = self.request as *const Request as *mut Request;
|
||||
let request = unsafe { &mut *request_ptr };
|
||||
variable::resolve_variable(request, var_name)
|
||||
}
|
||||
}
|
||||
|
||||
/// Adapter for SQLite query execution
|
||||
pub struct SqliteQueryExecutor;
|
||||
|
||||
impl QueryExecutor for SqliteQueryExecutor {
|
||||
fn execute(
|
||||
&self,
|
||||
db_path: &DatabasePath,
|
||||
query: &SqlQuery,
|
||||
params: &[(String, String)],
|
||||
) -> Result<Vec<HashMap<String, Value>>, String> {
|
||||
query::execute_query(db_path.as_str(), query.as_str(), params)
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
/// Adapter for Handlebars template operations (using raw pointer for interior mutability)
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct HandlebarsAdapter {
|
||||
registry: *mut Handlebars<'static>,
|
||||
}
|
||||
|
||||
impl HandlebarsAdapter {
|
||||
/// Create adapter from mutable handlebars registry
|
||||
///
|
||||
/// # Safety
|
||||
/// Caller must ensure the registry outlives this adapter
|
||||
pub unsafe fn new(registry: *mut Handlebars<'static>) -> Self {
|
||||
HandlebarsAdapter { registry }
|
||||
}
|
||||
}
|
||||
|
||||
impl TemplateLoader for HandlebarsAdapter {
|
||||
fn load_from_dir(&self, dir_path: &str) -> Result<usize, String> {
|
||||
unsafe {
|
||||
template::load_templates_from_dir(&mut *self.registry, dir_path)
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
fn register_template(&self, name: &str, path: &str) -> Result<(), String> {
|
||||
unsafe {
|
||||
(*self.registry)
|
||||
.register_template_file(name, path)
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TemplateRenderer for HandlebarsAdapter {
|
||||
fn render(&self, template_name: &str, data: &Value) -> Result<String, String> {
|
||||
unsafe { (*self.registry).render(template_name, data).map_err(|e| e.to_string()) }
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_sqlite_query_executor() {
|
||||
use rusqlite::Connection;
|
||||
use std::fs;
|
||||
|
||||
let temp_path = "/tmp/test_adapter_executor.db";
|
||||
let _ = fs::remove_file(temp_path);
|
||||
|
||||
{
|
||||
let conn = Connection::open(temp_path).unwrap();
|
||||
conn.execute("CREATE TABLE test (id INTEGER, name TEXT)", [])
|
||||
.unwrap();
|
||||
conn.execute("INSERT INTO test VALUES (1, 'test')", [])
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
let executor = SqliteQueryExecutor;
|
||||
let db_path = DatabasePath::parse(temp_path).unwrap();
|
||||
let query = SqlQuery::parse("SELECT * FROM test").unwrap();
|
||||
|
||||
let results = executor.execute(&db_path, &query, &[]).unwrap();
|
||||
assert_eq!(results.len(), 1);
|
||||
assert_eq!(
|
||||
results[0].get("name").unwrap(),
|
||||
&Value::String("test".to_string())
|
||||
);
|
||||
|
||||
let _ = fs::remove_file(temp_path);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_handlebars_adapter() {
|
||||
use std::fs;
|
||||
use std::io::Write;
|
||||
|
||||
let temp_dir = "/tmp/test_adapter_hbs";
|
||||
let _ = fs::remove_dir_all(temp_dir);
|
||||
fs::create_dir_all(temp_dir).unwrap();
|
||||
|
||||
let template_path = format!("{}/test.hbs", temp_dir);
|
||||
let mut file = fs::File::create(&template_path).unwrap();
|
||||
file.write_all(b"Hello {{name}}").unwrap();
|
||||
|
||||
let mut reg = Handlebars::new();
|
||||
let reg_ptr: *mut Handlebars<'static> = unsafe { std::mem::transmute(&mut reg) };
|
||||
let adapter = unsafe { HandlebarsAdapter::new(reg_ptr) };
|
||||
|
||||
adapter
|
||||
.register_template("test", &template_path)
|
||||
.unwrap();
|
||||
|
||||
let data = serde_json::json!({"name": "World"});
|
||||
let rendered = adapter.render("test", &data).unwrap();
|
||||
|
||||
assert_eq!(rendered, "Hello World");
|
||||
|
||||
let _ = fs::remove_dir_all(temp_dir);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user