Add book catalog example with global/local template system

Features:
- Global template configuration (sqlite_global_templates directive)
- Template path resolution relative to location
- Automatic template loading from directories
- Local templates override global templates
- Content handler installation via directive setter

Book Catalog Example:
- Complete working example with 10 technical books
- SQLite database with setup script
- 4 browseable pages (all, programming, databases, computer-science)
- Shared global templates (header, footer, book_card partial)
- Category-specific local templates with unique theming
- Responsive gradient UI design
- Working navigation and filtering

Configuration:
- sqlite_global_templates: HTTP main-level directive for shared templates
- sqlite_template: Location-level directive (sets content handler)
- Template resolution: {doc_root}{uri}/{template_name}
- All .hbs files in directories auto-loaded as partials

Technical improvements:
- Fixed content handler setup (not phase handler)
- Proper HttpModuleMainConf and HttpModuleLocationConf traits
- Template directory scanning and registration
- Error handling with debug logging
This commit is contained in:
Edward Langley
2025-11-15 14:52:39 -08:00
parent 63fbee6694
commit 9132d7485d
15 changed files with 1002 additions and 28 deletions

155
README_BOOK_CATALOG.md Normal file
View File

@ -0,0 +1,155 @@
# Book Catalog Example
A complete example demonstrating the nginx-test SQLite module with a read-only book catalog.
## Features
- **SQLite Database**: Stores book information (title, author, ISBN, year, genre, description, rating)
- **Multiple Views**: Different locations for browsing by category
- **Template Inheritance**: Global templates (header, footer, book_card) shared across all pages
- **Local Templates**: Category-specific styling and layouts
- **Responsive Design**: Modern, gradient-styled UI
## Setup
### 1. Create and populate the database
```bash
chmod +x setup_book_catalog.sh
./setup_book_catalog.sh
```
This creates `book_catalog.db` with 10 sample technical books across three genres:
- Programming
- Databases
- Computer Science
### 2. Build the module
```bash
direnv exec "$PWD" cargo build
```
### 3. Start nginx
```bash
./ngx_src/nginx-1.28.0/objs/nginx -c conf/book_catalog.conf -p .
```
### 4. Visit the catalog
Open your browser to:
- http://localhost:8080/ (redirects to all books)
- http://localhost:8080/books/all
- http://localhost:8080/books/programming
- http://localhost:8080/books/databases
- http://localhost:8080/books/computer-science
### 5. Stop nginx
```bash
./ngx_src/nginx-1.28.0/objs/nginx -s stop -c conf/book_catalog.conf -p .
```
## Directory Structure
```
nginx-test/
├── book_catalog.db # SQLite database
├── setup_book_catalog.sh # Database setup script
├── conf/
│ └── book_catalog.conf # Nginx configuration
└── server_root/
├── global_templates/ # Shared templates
│ ├── header.hbs # Page header with navigation
│ ├── footer.hbs # Page footer
│ └── book_card.hbs # Reusable book card partial
└── books/
├── all/
│ └── list.hbs # All books page
├── programming/
│ └── list.hbs # Programming books page
├── databases/
│ └── list.hbs # Database books page
└── computer-science/
└── list.hbs # CS books page
```
## How It Works
### Template Loading Order
For each request, the module:
1. **Loads global templates** from `server_root/global_templates/`:
- `header.hbs` - Page structure and navigation
- `footer.hbs` - Page footer
- `book_card.hbs` - Book display component
2. **Loads local templates** from the location's directory:
- Each category has its own `list.hbs` with custom styling
- Local templates can override global ones
3. **Renders the main template** with SQL query results
### Template Usage
**In list.hbs:**
```handlebars
{{> header}}
<div class="book-grid">
{{#each results}}
{{> book_card}}
{{/each}}
</div>
{{> footer}}
```
The `{{> header}}`, `{{> book_card}}`, and `{{> footer}}` are partials loaded from the global templates directory.
### SQL Queries
Each location runs a different SQL query:
- **All books**: `SELECT * FROM books ORDER BY rating DESC, title`
- **Programming**: `SELECT * FROM books WHERE genre = 'Programming' ...`
- **Databases**: `SELECT * FROM books WHERE genre = 'Databases' ...`
- **Computer Science**: `SELECT * FROM books WHERE genre = 'Computer Science' ...`
Results are passed to the template as a `results` array.
## Customization
### Adding More Books
```bash
sqlite3 book_catalog.db
```
```sql
INSERT INTO books (title, author, isbn, year, genre, description, rating)
VALUES ('Your Book', 'Author Name', '978-XXXXXXXXXX', 2024, 'Programming', 'Description here', 4.5);
```
### Adding New Categories
1. Create a new genre in the database
2. Add a location block in `book_catalog.conf`
3. Create a template directory under `server_root/books/`
4. Add the category to the navigation in `header.hbs`
### Styling
Each category's `list.hbs` contains embedded CSS. Modify the `<style>` section to change colors, layouts, etc.
## Architecture Notes
- **Read-only**: All SQL queries are SELECT statements only
- **Performance**: Templates are loaded fresh for each request (suitable for development)
- **Security**: No user input is processed; all queries are predefined
- **Scalability**: SQLite is suitable for read-heavy workloads with moderate traffic
Enjoy exploring the book catalog! 📚

BIN
book_catalog.db Normal file

Binary file not shown.

58
conf/book_catalog.conf Normal file
View File

@ -0,0 +1,58 @@
# Book Catalog Configuration
# Demonstrates the nginx-test SQLite module with multiple locations and template inheritance
load_module target/debug/libnginx_test.dylib;
worker_processes 1;
events {}
error_log logs/error.log debug;
http {
# Global templates for shared components (header, footer, book_card partial)
sqlite_global_templates "server_root/global_templates";
server {
listen 8080;
root "server_root";
# Default redirect to all books
location = / {
return 301 /books/all;
}
# All books
location /books/all {
add_header "Content-Type" "text/html; charset=utf-8";
sqlite_db "book_catalog.db";
sqlite_query "SELECT * FROM books ORDER BY rating DESC, title";
sqlite_template "list.hbs";
}
# Programming books only
location /books/programming {
add_header "Content-Type" "text/html; charset=utf-8";
sqlite_db "book_catalog.db";
sqlite_query "SELECT * FROM books WHERE genre = 'Programming' ORDER BY rating DESC, title";
sqlite_template "list.hbs";
}
# Database books only
location /books/databases {
add_header "Content-Type" "text/html; charset=utf-8";
sqlite_db "book_catalog.db";
sqlite_query "SELECT * FROM books WHERE genre = 'Databases' ORDER BY rating DESC, title";
sqlite_template "list.hbs";
}
# Computer Science books only
location /books/computer-science {
add_header "Content-Type" "text/html; charset=utf-8";
sqlite_db "book_catalog.db";
sqlite_query "SELECT * FROM books WHERE genre = 'Computer Science' ORDER BY rating DESC, title";
sqlite_template "list.hbs";
}
}
}

View File

@ -9,10 +9,14 @@ events {}
error_log logs/error.log debug;
http {
# Optional: Global templates directory for shared partials/layouts
# sqlite_global_templates "server_root/global_templates";
server {
listen 8080;
location / {
root "server_root";
location /people {
add_header "Content-Type" "text/html";
sqlite_db "db.sqlite3";
sqlite_query "SELECT id, name, address FROM person";

View File

@ -0,0 +1,122 @@
{{> header}}
<style>
.book-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
gap: 1.5rem;
margin-top: 1.5rem;
}
.book-card {
background: #fff;
border: 2px solid #e9ecef;
border-radius: 8px;
padding: 1.5rem;
transition: all 0.3s;
}
.book-card:hover {
transform: translateY(-4px);
box-shadow: 0 8px 16px rgba(102, 126, 234, 0.2);
border-color: #667eea;
}
.book-header {
display: flex;
justify-content: space-between;
align-items: start;
margin-bottom: 0.5rem;
}
.book-title {
color: #2d3748;
font-size: 1.25rem;
margin-bottom: 0.25rem;
flex: 1;
}
.book-rating {
background: #fef3c7;
color: #92400e;
padding: 0.25rem 0.75rem;
border-radius: 20px;
font-weight: bold;
font-size: 0.9rem;
white-space: nowrap;
margin-left: 0.5rem;
}
.book-author {
color: #667eea;
font-weight: 500;
margin-bottom: 0.75rem;
}
.book-description {
color: #4a5568;
margin-bottom: 1rem;
line-height: 1.5;
}
.book-details {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
font-size: 0.875rem;
}
.book-genre {
background: #667eea;
color: white;
padding: 0.25rem 0.75rem;
border-radius: 4px;
font-weight: 500;
}
.book-year {
background: #e9ecef;
color: #495057;
padding: 0.25rem 0.75rem;
border-radius: 4px;
}
.book-isbn {
color: #6c757d;
font-family: monospace;
font-size: 0.8rem;
}
.stats {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 1.5rem;
border-radius: 8px;
margin-bottom: 2rem;
display: flex;
justify-content: space-around;
text-align: center;
}
.stat-item h2 {
font-size: 2.5rem;
margin-bottom: 0.25rem;
}
.stat-item p {
opacity: 0.9;
font-size: 0.9rem;
}
</style>
<div class="stats">
<div class="stat-item">
<h2>📚</h2>
<p>Book Collection</p>
</div>
<div class="stat-item">
<h2>⭐</h2>
<p>Top Rated</p>
</div>
<div class="stat-item">
<h2>🔍</h2>
<p>Browse All</p>
</div>
</div>
<h2 style="color: #2d3748; margin-bottom: 1rem;">All Books in Collection</h2>
<div class="book-grid">
{{#each results}}
{{> book_card}}
{{/each}}
</div>
{{> footer}}

View File

@ -0,0 +1,107 @@
{{> header}}
<style>
.book-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
gap: 1.5rem;
margin-top: 1.5rem;
}
.book-card {
background: #fff;
border: 2px solid #e9ecef;
border-radius: 8px;
padding: 1.5rem;
transition: all 0.3s;
}
.book-card:hover {
transform: translateY(-4px);
box-shadow: 0 8px 16px rgba(59, 130, 246, 0.2);
border-color: #3b82f6;
}
.book-header {
display: flex;
justify-content: space-between;
align-items: start;
margin-bottom: 0.5rem;
}
.book-title {
color: #2d3748;
font-size: 1.25rem;
margin-bottom: 0.25rem;
flex: 1;
}
.book-rating {
background: #fef3c7;
color: #92400e;
padding: 0.25rem 0.75rem;
border-radius: 20px;
font-weight: bold;
font-size: 0.9rem;
white-space: nowrap;
margin-left: 0.5rem;
}
.book-author {
color: #3b82f6;
font-weight: 500;
margin-bottom: 0.75rem;
}
.book-description {
color: #4a5568;
margin-bottom: 1rem;
line-height: 1.5;
}
.book-details {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
font-size: 0.875rem;
}
.book-genre {
background: #3b82f6;
color: white;
padding: 0.25rem 0.75rem;
border-radius: 4px;
font-weight: 500;
}
.book-year {
background: #e9ecef;
color: #495057;
padding: 0.25rem 0.75rem;
border-radius: 4px;
}
.book-isbn {
color: #6c757d;
font-family: monospace;
font-size: 0.8rem;
}
.category-header {
background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%);
color: white;
padding: 1.5rem;
border-radius: 8px;
margin-bottom: 2rem;
}
.category-header h2 {
font-size: 2rem;
margin-bottom: 0.5rem;
}
</style>
<div class="category-header">
<h2>🎓 Computer Science Books</h2>
<p>Fundamental concepts, algorithms, and theoretical computer science</p>
</div>
<div class="book-grid">
{{#each results}}
{{> book_card}}
{{/each}}
</div>
{{#unless results}}
<p style="text-align: center; color: #6c757d; padding: 2rem;">No computer science books found.</p>
{{/unless}}
{{> footer}}

View File

@ -0,0 +1,107 @@
{{> header}}
<style>
.book-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
gap: 1.5rem;
margin-top: 1.5rem;
}
.book-card {
background: #fff;
border: 2px solid #e9ecef;
border-radius: 8px;
padding: 1.5rem;
transition: all 0.3s;
}
.book-card:hover {
transform: translateY(-4px);
box-shadow: 0 8px 16px rgba(239, 68, 68, 0.2);
border-color: #ef4444;
}
.book-header {
display: flex;
justify-content: space-between;
align-items: start;
margin-bottom: 0.5rem;
}
.book-title {
color: #2d3748;
font-size: 1.25rem;
margin-bottom: 0.25rem;
flex: 1;
}
.book-rating {
background: #fef3c7;
color: #92400e;
padding: 0.25rem 0.75rem;
border-radius: 20px;
font-weight: bold;
font-size: 0.9rem;
white-space: nowrap;
margin-left: 0.5rem;
}
.book-author {
color: #ef4444;
font-weight: 500;
margin-bottom: 0.75rem;
}
.book-description {
color: #4a5568;
margin-bottom: 1rem;
line-height: 1.5;
}
.book-details {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
font-size: 0.875rem;
}
.book-genre {
background: #ef4444;
color: white;
padding: 0.25rem 0.75rem;
border-radius: 4px;
font-weight: 500;
}
.book-year {
background: #e9ecef;
color: #495057;
padding: 0.25rem 0.75rem;
border-radius: 4px;
}
.book-isbn {
color: #6c757d;
font-family: monospace;
font-size: 0.8rem;
}
.category-header {
background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%);
color: white;
padding: 1.5rem;
border-radius: 8px;
margin-bottom: 2rem;
}
.category-header h2 {
font-size: 2rem;
margin-bottom: 0.5rem;
}
</style>
<div class="category-header">
<h2>🗄️ Database Books</h2>
<p>Deep dive into database systems, design, and data management</p>
</div>
<div class="book-grid">
{{#each results}}
{{> book_card}}
{{/each}}
</div>
{{#unless results}}
<p style="text-align: center; color: #6c757d; padding: 2rem;">No database books found.</p>
{{/unless}}
{{> footer}}

View File

@ -0,0 +1,107 @@
{{> header}}
<style>
.book-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
gap: 1.5rem;
margin-top: 1.5rem;
}
.book-card {
background: #fff;
border: 2px solid #e9ecef;
border-radius: 8px;
padding: 1.5rem;
transition: all 0.3s;
}
.book-card:hover {
transform: translateY(-4px);
box-shadow: 0 8px 16px rgba(102, 126, 234, 0.2);
border-color: #667eea;
}
.book-header {
display: flex;
justify-content: space-between;
align-items: start;
margin-bottom: 0.5rem;
}
.book-title {
color: #2d3748;
font-size: 1.25rem;
margin-bottom: 0.25rem;
flex: 1;
}
.book-rating {
background: #fef3c7;
color: #92400e;
padding: 0.25rem 0.75rem;
border-radius: 20px;
font-weight: bold;
font-size: 0.9rem;
white-space: nowrap;
margin-left: 0.5rem;
}
.book-author {
color: #667eea;
font-weight: 500;
margin-bottom: 0.75rem;
}
.book-description {
color: #4a5568;
margin-bottom: 1rem;
line-height: 1.5;
}
.book-details {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
font-size: 0.875rem;
}
.book-genre {
background: #10b981;
color: white;
padding: 0.25rem 0.75rem;
border-radius: 4px;
font-weight: 500;
}
.book-year {
background: #e9ecef;
color: #495057;
padding: 0.25rem 0.75rem;
border-radius: 4px;
}
.book-isbn {
color: #6c757d;
font-family: monospace;
font-size: 0.8rem;
}
.category-header {
background: linear-gradient(135deg, #10b981 0%, #059669 100%);
color: white;
padding: 1.5rem;
border-radius: 8px;
margin-bottom: 2rem;
}
.category-header h2 {
font-size: 2rem;
margin-bottom: 0.5rem;
}
</style>
<div class="category-header">
<h2>💻 Programming Books</h2>
<p>Books focused on programming languages, practices, and software development</p>
</div>
<div class="book-grid">
{{#each results}}
{{> book_card}}
{{/each}}
</div>
{{#unless results}}
<p style="text-align: center; color: #6c757d; padding: 2rem;">No programming books found.</p>
{{/unless}}
{{> footer}}

View File

@ -0,0 +1,16 @@
<div class="book-card">
<div class="book-header">
<h3 class="book-title">{{title}}</h3>
<div class="book-rating">⭐ {{rating}}</div>
</div>
<p class="book-author">by {{author}}</p>
<p class="book-description">{{description}}</p>
<div class="book-details">
<span class="book-genre">{{genre}}</span>
<span class="book-year">{{year}}</span>
{{#if isbn}}
<span class="book-isbn">ISBN: {{isbn}}</span>
{{/if}}
</div>
</div>

View File

@ -0,0 +1,9 @@
</main>
<footer style="background: #f8f9fa; padding: 2rem; text-align: center; border-top: 2px solid #e9ecef; color: #6c757d;">
<p>📖 Powered by nginx-test SQLite Module</p>
<p style="margin-top: 0.5rem; font-size: 0.9rem;">A demonstration of Rust + NGINX + SQLite + Handlebars</p>
</footer>
</div>
</body>
</html>

View File

@ -0,0 +1,77 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Book Catalog</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
line-height: 1.6;
color: #333;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 20px;
}
.container {
max-width: 1200px;
margin: 0 auto;
background: white;
border-radius: 12px;
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
overflow: hidden;
}
header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 2rem;
text-align: center;
}
header h1 {
font-size: 2.5rem;
margin-bottom: 0.5rem;
text-shadow: 2px 2px 4px rgba(0,0,0,0.2);
}
header p {
font-size: 1.1rem;
opacity: 0.95;
}
nav {
background: #f8f9fa;
padding: 1rem 2rem;
border-bottom: 2px solid #e9ecef;
}
nav a {
color: #667eea;
text-decoration: none;
margin-right: 1.5rem;
font-weight: 500;
transition: color 0.3s;
}
nav a:hover {
color: #764ba2;
}
main {
padding: 2rem;
}
</style>
</head>
<body>
<div class="container">
<header>
<h1>📚 Book Catalog</h1>
<p>Explore our collection of technical books</p>
</header>
<nav>
<a href="/books/all">All Books</a>
<a href="/books/programming">Programming</a>
<a href="/books/databases">Databases</a>
<a href="/books/computer-science">Computer Science</a>
</nav>
<main>

52
setup_book_catalog.sh Executable file
View File

@ -0,0 +1,52 @@
#!/usr/bin/env bash
# Setup script for book catalog example
set -e
echo "Setting up book catalog database..."
# Create the database
rm -f book_catalog.db
sqlite3 book_catalog.db <<EOF
-- Create books table
CREATE TABLE books (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
author TEXT NOT NULL,
isbn TEXT,
year INTEGER,
genre TEXT,
description TEXT,
rating REAL
);
-- Insert sample book data
INSERT INTO books (title, author, isbn, year, genre, description, rating) VALUES
('The Pragmatic Programmer', 'Andrew Hunt, David Thomas', '978-0135957059', 2019, 'Programming', 'Your journey to mastery in software craftsmanship.', 4.5),
('Clean Code', 'Robert C. Martin', '978-0132350884', 2008, 'Programming', 'A handbook of agile software craftsmanship.', 4.7),
('Design Patterns', 'Gang of Four', '978-0201633610', 1994, 'Programming', 'Elements of reusable object-oriented software.', 4.6),
('The Rust Programming Language', 'Steve Klabnik, Carol Nichols', '978-1718503106', 2023, 'Programming', 'The official book on the Rust programming language.', 4.8),
('Structure and Interpretation of Computer Programs', 'Harold Abelson, Gerald Jay Sussman', '978-0262510871', 1996, 'Computer Science', 'Classic text on programming and computer science.', 4.9),
('Introduction to Algorithms', 'Thomas H. Cormen et al.', '978-0262033848', 2009, 'Computer Science', 'Comprehensive algorithms textbook.', 4.6),
('Code Complete', 'Steve McConnell', '978-0735619678', 2004, 'Programming', 'A practical handbook of software construction.', 4.5),
('Designing Data-Intensive Applications', 'Martin Kleppmann', '978-1449373320', 2017, 'Databases', 'The big ideas behind reliable, scalable, and maintainable systems.', 4.8),
('Database System Concepts', 'Abraham Silberschatz et al.', '978-0078022159', 2019, 'Databases', 'Comprehensive introduction to database systems.', 4.4),
('The Art of Computer Programming, Vol. 1', 'Donald Knuth', '978-0201896831', 1997, 'Computer Science', 'Fundamental algorithms and analysis.', 4.7);
-- Create a view for books by genre
CREATE VIEW books_by_genre AS
SELECT genre, COUNT(*) as count, AVG(rating) as avg_rating
FROM books
GROUP BY genre
ORDER BY count DESC;
EOF
echo "Database created: book_catalog.db"
echo ""
echo "Sample data:"
sqlite3 book_catalog.db "SELECT COUNT(*) as total_books FROM books;"
echo ""
echo "Books by genre:"
sqlite3 book_catalog.db "SELECT * FROM books_by_genre;"

View File

@ -1,10 +1,10 @@
use handlebars::Handlebars;
use ngx::core::Buffer;
use ngx::ffi::{
NGX_CONF_TAKE1, NGX_HTTP_LOC_CONF, NGX_HTTP_MODULE, NGX_HTTP_LOC_CONF_OFFSET,
NGX_RS_MODULE_SIGNATURE, nginx_version, ngx_array_push, ngx_chain_t, ngx_command_t,
ngx_conf_t, ngx_http_handler_pt, ngx_http_module_t,
ngx_http_phases_NGX_HTTP_ACCESS_PHASE, ngx_int_t, ngx_module_t, ngx_str_t, ngx_uint_t,
NGX_CONF_TAKE1, NGX_HTTP_LOC_CONF, NGX_HTTP_MAIN_CONF, NGX_HTTP_MODULE,
NGX_HTTP_LOC_CONF_OFFSET, NGX_RS_MODULE_SIGNATURE, nginx_version, ngx_chain_t,
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, MergeConfigError, NgxHttpCoreModule,
@ -25,23 +25,9 @@ impl http::HttpModule for Module {
unsafe { &*addr_of!(ngx_http_howto_module) }
}
unsafe extern "C" fn postconfiguration(cf: *mut ngx_conf_t) -> ngx_int_t {
unsafe {
let htcf =
NgxHttpCoreModule::main_conf_mut(&*cf).expect("failed to get core main conf");
let h = ngx_array_push(
&mut htcf.phases[ngx_http_phases_NGX_HTTP_ACCESS_PHASE as usize].handlers,
) as *mut ngx_http_handler_pt;
if h.is_null() {
return core::Status::NGX_ERROR.into();
}
// set an Access phase handler
*h = Some(howto_access_handler);
unsafe extern "C" fn postconfiguration(_cf: *mut ngx_conf_t) -> ngx_int_t {
core::Status::NGX_OK.into()
}
}
}
// Implement HttpModuleLocationConf to define our location-specific configuration
@ -49,6 +35,11 @@ unsafe impl HttpModuleLocationConf for Module {
type LocationConf = ModuleConfig;
}
// Implement HttpModuleMainConf to define our global configuration
unsafe impl HttpModuleMainConf for Module {
type MainConf = MainConfig;
}
// Create a ModuleConfig to save our configuration state.
#[derive(Debug, Default)]
struct ModuleConfig {
@ -57,6 +48,21 @@ struct ModuleConfig {
template_path: String,
}
// Global configuration for shared templates
#[derive(Debug, Default)]
struct MainConfig {
global_templates_dir: String,
}
impl http::Merge for MainConfig {
fn merge(&mut self, prev: &MainConfig) -> Result<(), MergeConfigError> {
if self.global_templates_dir.is_empty() {
self.global_templates_dir = prev.global_templates_dir.clone();
}
Ok(())
}
}
// Implement our Merge trait to merge configuration with higher layers.
impl http::Merge for ModuleConfig {
fn merge(&mut self, prev: &ModuleConfig) -> Result<(), MergeConfigError> {
@ -83,8 +89,8 @@ impl http::Merge for ModuleConfig {
static ngx_http_howto_module_ctx: ngx_http_module_t = ngx_http_module_t {
preconfiguration: Some(Module::preconfiguration),
postconfiguration: Some(Module::postconfiguration),
create_main_conf: None,
init_main_conf: None,
create_main_conf: Some(Module::create_main_conf),
init_main_conf: Some(Module::init_main_conf),
create_srv_conf: None,
merge_srv_conf: None,
create_loc_conf: Some(Module::create_loc_conf),
@ -133,7 +139,15 @@ pub static mut ngx_http_howto_module: ngx_module_t = ngx_module_t {
// sure to terminate the array with an empty command.
#[unsafe(no_mangle)]
#[allow(non_upper_case_globals)]
static mut ngx_http_howto_commands: [ngx_command_t; 4] = [
static mut ngx_http_howto_commands: [ngx_command_t; 5] = [
ngx_command_t {
name: ngx_string!("sqlite_global_templates"),
type_: (NGX_HTTP_MAIN_CONF | NGX_CONF_TAKE1) as ngx_uint_t,
set: Some(ngx_http_howto_commands_set_global_templates),
conf: 0, // Main conf offset
offset: 0,
post: std::ptr::null_mut(),
},
ngx_command_t {
name: ngx_string!("sqlite_db"),
type_: (NGX_HTTP_LOC_CONF | NGX_CONF_TAKE1) as ngx_uint_t,
@ -171,6 +185,21 @@ static mut ngx_http_howto_commands: [ngx_command_t; 4] = [
},
];
#[unsafe(no_mangle)]
extern "C" fn ngx_http_howto_commands_set_global_templates(
cf: *mut ngx_conf_t,
_cmd: *mut ngx_command_t,
conf: *mut c_void,
) -> *mut c_char {
unsafe {
let conf = &mut *(conf as *mut MainConfig);
let args = (*(*cf).args).elts as *mut ngx_str_t;
conf.global_templates_dir = (*args.add(1)).to_string();
};
std::ptr::null_mut()
}
#[unsafe(no_mangle)]
extern "C" fn ngx_http_howto_commands_set_db_path(
cf: *mut ngx_conf_t,
@ -211,11 +240,51 @@ extern "C" fn ngx_http_howto_commands_set_template_path(
let conf = &mut *(conf as *mut ModuleConfig);
let args = (*(*cf).args).elts as *mut ngx_str_t;
conf.template_path = (*args.add(1)).to_string();
// Set the content handler for this location
let clcf = NgxHttpCoreModule::location_conf_mut(&*cf)
.expect("failed to get core location conf");
clcf.handler = Some(howto_access_handler);
};
std::ptr::null_mut()
}
// Load all .hbs templates from a directory into the Handlebars registry
fn load_templates_from_dir(reg: &mut Handlebars, dir_path: &str) -> std::io::Result<usize> {
use std::fs;
use std::path::Path;
let dir = Path::new(dir_path);
if !dir.exists() || !dir.is_dir() {
return Ok(0);
}
let mut count = 0;
for entry in fs::read_dir(dir)? {
let entry = entry?;
let path = entry.path();
if path.is_file() {
if let Some(ext) = path.extension() {
if ext == "hbs" {
if let Some(stem) = path.file_stem() {
if let Some(name) = stem.to_str() {
if let Err(e) = reg.register_template_file(name, &path) {
eprintln!("Failed to register template {}: {}", path.display(), e);
} else {
count += 1;
}
}
}
}
}
}
}
Ok(count)
}
// Execute a generic SQL query and return results as JSON-compatible data
fn execute_query(db_path: &str, query: &str) -> Result<Vec<std::collections::HashMap<String, serde_json::Value>>> {
let conn = Connection::open(db_path)?;
@ -271,6 +340,33 @@ http_request_handler!(howto_access_handler, |request: &mut http::Request| {
ngx_log_debug_http!(request, "sqlite module handler called");
// Resolve template path relative to document root and location
let core_loc_conf =
NgxHttpCoreModule::location_conf(request).expect("failed to get core location conf");
let doc_root = match (*core_loc_conf).root.to_str() {
Ok(s) => s,
Err(e) => {
ngx_log_debug_http!(request, "failed to decode root path: {}", e);
return http::HTTPStatus::INTERNAL_SERVER_ERROR.into();
}
};
let uri = match request.path().to_str() {
Ok(s) => s,
Err(e) => {
ngx_log_debug_http!(request, "failed to decode URI path: {}", e);
return http::HTTPStatus::INTERNAL_SERVER_ERROR.into();
}
};
let template_full_path = format!("{}{}/{}", doc_root, uri, co.template_path);
ngx_log_debug_http!(request, "resolved template path: {}", template_full_path);
// Get the directory containing the main template for local templates
let template_dir = std::path::Path::new(&template_full_path)
.parent()
.and_then(|p| p.to_str())
.unwrap_or("");
// Execute the configured SQL query
let results = match execute_query(&co.db_path, &co.query) {
Ok(results) => results,
@ -280,12 +376,39 @@ http_request_handler!(howto_access_handler, |request: &mut http::Request| {
}
};
// Setup Handlebars and register the configured template
// Setup Handlebars and load templates
let mut reg = Handlebars::new();
match reg.register_template_file("template", &co.template_path) {
Ok(_) => (),
// First, load global templates if configured
let main_conf = Module::main_conf(request).expect("main config is none");
if !main_conf.global_templates_dir.is_empty() {
match load_templates_from_dir(&mut reg, &main_conf.global_templates_dir) {
Ok(count) => {
ngx_log_debug_http!(request, "loaded {} global templates from {}", count, main_conf.global_templates_dir);
}
Err(e) => {
ngx_log_debug_http!(request, "failed to load template: {}", e);
ngx_log_debug_http!(request, "warning: failed to load global templates: {}", e);
}
}
}
// Then, load local templates (these override global ones)
match load_templates_from_dir(&mut reg, template_dir) {
Ok(count) => {
ngx_log_debug_http!(request, "loaded {} local templates from {}", count, template_dir);
}
Err(e) => {
ngx_log_debug_http!(request, "warning: failed to load local templates: {}", e);
}
}
// Finally, register the main template (overriding if it was loaded from directories)
match reg.register_template_file("template", &template_full_path) {
Ok(_) => {
ngx_log_debug_http!(request, "registered main template: {}", template_full_path);
},
Err(e) => {
ngx_log_debug_http!(request, "failed to load main template: {}", e);
return http::HTTPStatus::INTERNAL_SERVER_ERROR.into();
}
}

37
start_book_catalog.sh Executable file
View File

@ -0,0 +1,37 @@
#!/usr/bin/env bash
# Quick start script for the book catalog example
set -e
echo "📚 Starting Book Catalog..."
echo ""
# Check if database exists
if [ ! -f "book_catalog.db" ]; then
echo "Database not found. Running setup..."
./setup_book_catalog.sh
echo ""
fi
# Check if module is built
if [ ! -f "target/debug/libnginx_test.dylib" ]; then
echo "Module not built. Building..."
direnv exec "$PWD" cargo build
echo ""
fi
# Start nginx
echo "Starting nginx on http://localhost:8080"
./ngx_src/nginx-1.28.0/objs/nginx -c conf/book_catalog.conf -p .
echo ""
echo "✅ Book Catalog is running!"
echo ""
echo "Visit:"
echo " • http://localhost:8080/books/all - All books"
echo " • http://localhost:8080/books/programming - Programming books"
echo " • http://localhost:8080/books/databases - Database books"
echo " • http://localhost:8080/books/computer-science - CS books"
echo ""
echo "To stop: ./ngx_src/nginx-1.28.0/objs/nginx -s stop -c conf/book_catalog.conf -p ."