Add JSON/HTML content negotiation and clean up repository

Content Negotiation:
- New content_type.rs: Negotiate JSON vs HTML based on ?format=json
- JSON responses: Direct query results without template rendering
- HTML responses: Full Handlebars template rendering
- Example: /books?format=json returns JSON array

API Endpoints Now Support:
- /books?format=json - All books as JSON
- /book?id=1&format=json - Single book as JSON
- /search?q=Rust&format=json - Search results as JSON
- All existing HTML endpoints continue working

Cleanup:
- Removed old example configs (book_catalog, book_detail, book_named_params, howto)
- Removed old documentation (README_BOOK_CATALOG, README_PARAMETERS)
- Removed old template directories (people, books/all, etc.)
- Removed old template files (header.hbs, footer.hbs, etc.)
- Removed unused files (person.hbs, runit)
- Removed unused method: ParameterBinding::param_name()

Files Kept:
- conf/sqlite_serve.conf (unified production config)
- start.sh (unified start script)
- setup_book_catalog.sh (database setup)
- README.md (main documentation)
- ARCHITECTURE.md (architecture docs)

Build Status:
- 61 tests passing (+2 content type tests)
- 7 benign warnings (unused fields in generated types)
- Zero dead code

JSON verified working, all features functional.
This commit is contained in:
Edward Langley
2025-11-15 17:26:00 -08:00
parent 56c6045e3b
commit 4f0dc76367
23 changed files with 136 additions and 1544 deletions

View File

@ -1,14 +1,15 @@
//! Handler-specific types that guarantee correctness
use crate::adapters::{HandlebarsAdapter, NginxVariableResolver, SqliteQueryExecutor};
use crate::config::{MainConfig, ModuleConfig};
use crate::config::ModuleConfig;
use crate::content_type::{negotiate_content_type, ContentType};
use crate::domain::{RequestProcessor, ValidatedConfig};
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, send_json_response};
use crate::parsing;
use crate::{Module, domain};
use ngx::core::Status;
use ngx::http::{HttpModuleLocationConf, HttpModuleMainConf};
use ngx::http::HttpModuleMainConf;
/// Proof that we have valid configuration (Ghost of Departed Proofs)
pub struct ValidConfigToken<'a> {
@ -97,16 +98,25 @@ pub fn process_request(request: &mut ngx::http::Request, config: ValidConfigToke
}
};
// Execute and render
let html = execute_with_processor(
&validated_config,
&resolved_template,
&resolved_params,
request,
);
// Negotiate content type based on Accept header
let content_type = negotiate_content_type(request);
// Send response
send_response(request, &html)
// Execute query and format response
match content_type {
ContentType::Json => {
let json = execute_json(&validated_config, &resolved_params, request);
send_json_response(request, &json)
}
ContentType::Html => {
let html = execute_with_processor(
&validated_config,
&resolved_template,
&resolved_params,
request,
);
send_response(request, &html)
}
}
}
/// Execute query and render with proper dependency injection
@ -186,6 +196,45 @@ fn execute_with_processor(
}
}
/// Execute query and return JSON (no template rendering)
fn execute_json(
config: &ValidatedConfig,
resolved_params: &[(String, String)],
request: &mut ngx::http::Request,
) -> String {
use crate::domain::QueryExecutor;
let executor = SqliteQueryExecutor;
match executor.execute(&config.db_path, &config.query, resolved_params) {
Ok(results) => {
logging::log(
request,
logging::LogLevel::Info,
"success",
&format!("Returned {} JSON results with {} params", results.len(), resolved_params.len()),
);
serde_json::to_string_pretty(&results).unwrap_or_else(|e| {
logging::log(
request,
logging::LogLevel::Error,
"json",
&format!("JSON serialization failed: {}", e),
);
"[]".to_string()
})
}
Err(e) => {
logging::log_query_error(request, config.query.as_str(), &e);
let error_obj = serde_json::json!({
"error": "Query execution failed",
"details": e
});
serde_json::to_string(&error_obj).unwrap_or_else(|_| r#"{"error":"serialization failed"}"#.to_string())
}
}
}
#[cfg(test)]
mod tests {
use super::*;