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:
155
README_BOOK_CATALOG.md
Normal file
155
README_BOOK_CATALOG.md
Normal 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
BIN
book_catalog.db
Normal file
Binary file not shown.
58
conf/book_catalog.conf
Normal file
58
conf/book_catalog.conf
Normal 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";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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";
|
||||
|
||||
122
server_root/books/all/list.hbs
Normal file
122
server_root/books/all/list.hbs
Normal 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}}
|
||||
|
||||
107
server_root/books/computer-science/list.hbs
Normal file
107
server_root/books/computer-science/list.hbs
Normal 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}}
|
||||
|
||||
107
server_root/books/databases/list.hbs
Normal file
107
server_root/books/databases/list.hbs
Normal 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}}
|
||||
|
||||
107
server_root/books/programming/list.hbs
Normal file
107
server_root/books/programming/list.hbs
Normal 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}}
|
||||
|
||||
16
server_root/global_templates/book_card.hbs
Normal file
16
server_root/global_templates/book_card.hbs
Normal 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>
|
||||
|
||||
9
server_root/global_templates/footer.hbs
Normal file
9
server_root/global_templates/footer.hbs
Normal 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>
|
||||
|
||||
77
server_root/global_templates/header.hbs
Normal file
77
server_root/global_templates/header.hbs
Normal 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
52
setup_book_catalog.sh
Executable 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;"
|
||||
|
||||
177
src/lib.rs
177
src/lib.rs
@ -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,22 +25,8 @@ 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);
|
||||
core::Status::NGX_OK.into()
|
||||
}
|
||||
unsafe extern "C" fn postconfiguration(_cf: *mut ngx_conf_t) -> ngx_int_t {
|
||||
core::Status::NGX_OK.into()
|
||||
}
|
||||
}
|
||||
|
||||
@ -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, "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, "failed to load template: {}", 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
37
start_book_catalog.sh
Executable 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 ."
|
||||
|
||||
Reference in New Issue
Block a user