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:
@ -1,155 +0,0 @@
|
||||
# Book Catalog Example
|
||||
|
||||
A complete example demonstrating the sqlite-serve 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
|
||||
|
||||
```
|
||||
sqlite-serve/
|
||||
├── 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! 📚
|
||||
|
||||
@ -1,348 +0,0 @@
|
||||
# Path Parameters Feature
|
||||
|
||||
The sqlite-serve module supports parameterized SQL queries using nginx variables. This allows you to pass dynamic values from the request (query parameters, path captures, headers, etc.) as safe SQL prepared statement parameters.
|
||||
|
||||
## New Directive
|
||||
|
||||
### `sqlite_param`
|
||||
|
||||
Add parameters to SQL queries. Can be used multiple times to add multiple parameters.
|
||||
|
||||
**Syntax:**
|
||||
- Positional: `sqlite_param variable_or_value;`
|
||||
- Named: `sqlite_param :param_name variable_or_value;`
|
||||
|
||||
**Context:** `location`
|
||||
**Multiple:** Yes
|
||||
|
||||
**Note:** Positional parameters match `?` placeholders in order. Named parameters match `:name` placeholders by name.
|
||||
|
||||
## Usage
|
||||
|
||||
### Named Parameters (Recommended)
|
||||
|
||||
Named parameters provide better readability and don't depend on order:
|
||||
|
||||
```nginx
|
||||
location = /book {
|
||||
sqlite_db "book_catalog.db";
|
||||
sqlite_query "SELECT * FROM books WHERE id = :book_id";
|
||||
sqlite_param :book_id $arg_id; # Named parameter
|
||||
sqlite_template "detail.hbs";
|
||||
}
|
||||
```
|
||||
|
||||
**Request:** `http://localhost/book?id=5`
|
||||
**SQL Executed:** `SELECT * FROM books WHERE id = '5'`
|
||||
|
||||
### Multiple Named Parameters
|
||||
|
||||
```nginx
|
||||
location = /years {
|
||||
sqlite_db "book_catalog.db";
|
||||
sqlite_query "SELECT * FROM books WHERE year >= :min AND year <= :max";
|
||||
sqlite_param :min $arg_min; # Order doesn't matter
|
||||
sqlite_param :max $arg_max; # with named params
|
||||
sqlite_template "list.hbs";
|
||||
}
|
||||
```
|
||||
|
||||
**Request:** `http://localhost/years?min=2015&max=2024`
|
||||
**SQL Executed:** `SELECT * FROM books WHERE year >= '2015' AND year <= '2024'`
|
||||
|
||||
### Query Parameters (Positional)
|
||||
|
||||
Use nginx's built-in `$arg_*` variables to access query parameters:
|
||||
|
||||
```nginx
|
||||
location = /book {
|
||||
sqlite_db "book_catalog.db";
|
||||
sqlite_query "SELECT * FROM books WHERE id = ?";
|
||||
sqlite_param $arg_id; # Gets ?id=123 from URL
|
||||
sqlite_template "detail.hbs";
|
||||
}
|
||||
```
|
||||
|
||||
**Request:** `http://localhost/book?id=5`
|
||||
**SQL Executed:** `SELECT * FROM books WHERE id = '5'`
|
||||
|
||||
### Multiple Parameters
|
||||
|
||||
Parameters are bound to `?` placeholders in order:
|
||||
|
||||
```nginx
|
||||
location = /years {
|
||||
sqlite_db "book_catalog.db";
|
||||
sqlite_query "SELECT * FROM books WHERE year >= ? AND year <= ?";
|
||||
sqlite_param $arg_min; # First ? placeholder
|
||||
sqlite_param $arg_max; # Second ? placeholder
|
||||
sqlite_template "list.hbs";
|
||||
}
|
||||
```
|
||||
|
||||
**Request:** `http://localhost/years?min=2015&max=2024`
|
||||
**SQL Executed:** `SELECT * FROM books WHERE year >= '2015' AND year <= '2024'`
|
||||
|
||||
### Regex Path Captures
|
||||
|
||||
Use numbered captures (`$1`, `$2`, etc.) from regex locations:
|
||||
|
||||
```nginx
|
||||
location ~ ^/book/([0-9]+)$ {
|
||||
sqlite_db "book_catalog.db";
|
||||
sqlite_query "SELECT * FROM books WHERE id = ?";
|
||||
sqlite_param $1; # First capture group
|
||||
sqlite_template "detail.hbs";
|
||||
}
|
||||
```
|
||||
|
||||
**Request:** `http://localhost/book/5`
|
||||
**SQL Executed:** `SELECT * FROM books WHERE id = '5'`
|
||||
|
||||
### Named Captures
|
||||
|
||||
Use named captures from regex locations:
|
||||
|
||||
```nginx
|
||||
location ~ ^/author/(?<author_name>[^/]+)/books$ {
|
||||
sqlite_db "book_catalog.db";
|
||||
sqlite_query "SELECT * FROM books WHERE author LIKE ?";
|
||||
sqlite_param $author_name;
|
||||
sqlite_template "list.hbs";
|
||||
}
|
||||
```
|
||||
|
||||
**Request:** `http://localhost/author/Martin/books`
|
||||
**SQL Executed:** `SELECT * FROM books WHERE author LIKE 'Martin'`
|
||||
|
||||
### Other Nginx Variables
|
||||
|
||||
Any nginx variable can be used as a parameter:
|
||||
|
||||
```nginx
|
||||
location = /search {
|
||||
sqlite_db "book_catalog.db";
|
||||
sqlite_query "SELECT * FROM books WHERE title LIKE '%' || ? || '%'";
|
||||
sqlite_param $arg_q; # Query string parameter
|
||||
sqlite_template "search.hbs";
|
||||
}
|
||||
|
||||
location = /client-info {
|
||||
sqlite_db "access_log.db";
|
||||
sqlite_query "INSERT INTO visits (ip, user_agent) VALUES (?, ?)";
|
||||
sqlite_param $remote_addr; # Client IP
|
||||
sqlite_param $http_user_agent; # User agent header
|
||||
sqlite_template "logged.hbs";
|
||||
}
|
||||
```
|
||||
|
||||
### Literal Values
|
||||
|
||||
You can also use literal values (though less common):
|
||||
|
||||
```nginx
|
||||
location = /featured {
|
||||
sqlite_db "book_catalog.db";
|
||||
sqlite_query "SELECT * FROM books WHERE rating >= ? ORDER BY rating DESC";
|
||||
sqlite_param "4.5"; # Literal value
|
||||
sqlite_template "list.hbs";
|
||||
}
|
||||
```
|
||||
|
||||
## Available Nginx Variables
|
||||
|
||||
Common nginx variables you can use as parameters:
|
||||
|
||||
### Query String
|
||||
- `$arg_name` - Query parameter (e.g., `?name=value`)
|
||||
- `$args` - Full query string
|
||||
- `$query_string` - Same as `$args`
|
||||
|
||||
### Request Info
|
||||
- `$request_method` - GET, POST, etc.
|
||||
- `$request_uri` - Full request URI with query string
|
||||
- `$uri` - Request URI without query string
|
||||
- `$document_uri` - Same as `$uri`
|
||||
|
||||
### Client Info
|
||||
- `$remote_addr` - Client IP address
|
||||
- `$remote_port` - Client port
|
||||
- `$remote_user` - HTTP basic auth username
|
||||
|
||||
### Headers
|
||||
- `$http_name` - Any HTTP header (e.g., `$http_user_agent`, `$http_referer`)
|
||||
- `$content_type` - Content-Type header
|
||||
- `$content_length` - Content-Length header
|
||||
|
||||
### Path Captures
|
||||
- `$1`, `$2`, ..., `$9` - Numbered regex captures
|
||||
- `$name` - Named regex captures (`(?<name>...)`)
|
||||
|
||||
### Server Info
|
||||
- `$server_name` - Server name
|
||||
- `$server_port` - Server port
|
||||
- `$scheme` - http or https
|
||||
- `$hostname` - Hostname
|
||||
|
||||
See [nginx variables documentation](http://nginx.org/en/docs/http/ngx_http_core_module.html#variables) for complete list.
|
||||
|
||||
## Security
|
||||
|
||||
**SQL Injection Protection:**
|
||||
- All parameters are passed through SQLite's prepared statement mechanism
|
||||
- Values are properly escaped and quoted by SQLite
|
||||
- **SAFE:** `sqlite_param $arg_id` with query `SELECT * FROM books WHERE id = ?`
|
||||
- **SAFE:** Multiple parameters are bound separately to each `?`
|
||||
|
||||
**Never concatenate variables into the query string:**
|
||||
- **UNSAFE:** `sqlite_query "SELECT * FROM books WHERE id = $arg_id"` ❌
|
||||
- **SAFE:** Use `sqlite_param` instead ✓
|
||||
|
||||
## Examples
|
||||
|
||||
### Book Detail Page
|
||||
|
||||
```nginx
|
||||
location = /book {
|
||||
sqlite_db "catalog.db";
|
||||
sqlite_query "SELECT * FROM books WHERE id = ?";
|
||||
sqlite_param $arg_id;
|
||||
sqlite_template "detail.hbs";
|
||||
}
|
||||
```
|
||||
|
||||
Visit: `http://localhost/book?id=42`
|
||||
|
||||
### Search by Multiple Criteria
|
||||
|
||||
```nginx
|
||||
location = /search {
|
||||
sqlite_db "catalog.db";
|
||||
sqlite_query "
|
||||
SELECT * FROM books
|
||||
WHERE title LIKE '%' || ? || '%'
|
||||
AND year >= ?
|
||||
AND rating >= ?
|
||||
ORDER BY rating DESC
|
||||
";
|
||||
sqlite_param $arg_title;
|
||||
sqlite_param $arg_year;
|
||||
sqlite_param $arg_rating;
|
||||
sqlite_template "results.hbs";
|
||||
}
|
||||
```
|
||||
|
||||
Visit: `http://localhost/search?title=rust&year=2020&rating=4.5`
|
||||
|
||||
### Category with Pagination
|
||||
|
||||
```nginx
|
||||
location = /category {
|
||||
sqlite_db "catalog.db";
|
||||
sqlite_query "
|
||||
SELECT * FROM books
|
||||
WHERE genre = ?
|
||||
ORDER BY title
|
||||
LIMIT ? OFFSET ?
|
||||
";
|
||||
sqlite_param $arg_genre;
|
||||
sqlite_param $arg_limit;
|
||||
sqlite_param $arg_offset;
|
||||
sqlite_template "list.hbs";
|
||||
}
|
||||
```
|
||||
|
||||
Visit: `http://localhost/category?genre=Programming&limit=10&offset=0`
|
||||
|
||||
## Error Handling
|
||||
|
||||
### Missing Parameters
|
||||
|
||||
If a required nginx variable is not set, the module returns `400 Bad Request`:
|
||||
|
||||
```nginx
|
||||
location = /book {
|
||||
sqlite_param $arg_id; # If ?id= is not provided
|
||||
}
|
||||
```
|
||||
|
||||
**Response:** 400 Bad Request
|
||||
|
||||
### Invalid SQL
|
||||
|
||||
If parameter values cause SQL errors (e.g., type mismatch), returns `500 Internal Server Error`:
|
||||
|
||||
```nginx
|
||||
sqlite_query "SELECT * FROM books WHERE id = ?";
|
||||
sqlite_param $arg_id; # If ?id=abc (not a number)
|
||||
```
|
||||
|
||||
**Response:** 500 Internal Server Error (check nginx error log)
|
||||
|
||||
### Variable Not Found
|
||||
|
||||
If a variable name doesn't exist in nginx, returns `400 Bad Request` with log message.
|
||||
|
||||
## Complete Example
|
||||
|
||||
See `conf/book_detail.conf` for a working example with:
|
||||
- Single parameter (book by ID)
|
||||
- String parameter (genre filtering)
|
||||
- Multiple parameters (year range search)
|
||||
|
||||
Run it with:
|
||||
```bash
|
||||
./start_book_detail.sh
|
||||
```
|
||||
|
||||
## Implementation Details
|
||||
|
||||
- Parameters are resolved at request time using `ngx_http_get_variable()`
|
||||
- UTF-8 validation is performed on all variable values
|
||||
- Parameters are bound using rusqlite's prepared statement API
|
||||
- All SQL placeholders must be `?` (positional parameters)
|
||||
- Parameters match placeholders in order of `sqlite_param` directives
|
||||
|
||||
## Named vs Positional Parameters
|
||||
|
||||
### Named Parameters (`:name` syntax) - Recommended ✓
|
||||
|
||||
**Advantages:**
|
||||
- Order-independent: Can rearrange `sqlite_param` directives without breaking queries
|
||||
- Self-documenting: Parameter names explain their purpose
|
||||
- Safer for maintenance: Adding/removing parameters less error-prone
|
||||
- Better for complex queries with many parameters
|
||||
|
||||
**Example:**
|
||||
```nginx
|
||||
sqlite_query "SELECT * FROM books WHERE author = :author AND year > :year";
|
||||
sqlite_param :year $arg_year; # Order doesn't matter!
|
||||
sqlite_param :author $arg_author;
|
||||
```
|
||||
|
||||
### Positional Parameters (`?` syntax)
|
||||
|
||||
**Advantages:**
|
||||
- Slightly more compact configuration
|
||||
- Works well for simple 1-2 parameter queries
|
||||
|
||||
**Disadvantages:**
|
||||
- Order-dependent: Parameters must match `?` placeholders exactly
|
||||
- Less readable with many parameters
|
||||
- Error-prone when modifying queries
|
||||
|
||||
**Example:**
|
||||
```nginx
|
||||
sqlite_query "SELECT * FROM books WHERE author = ? AND year > ?";
|
||||
sqlite_param $arg_author; # Must be first!
|
||||
sqlite_param $arg_year; # Must be second!
|
||||
```
|
||||
|
||||
**Recommendation:** Use named parameters (`:name`) for all but the simplest queries.
|
||||
|
||||
## Limitations
|
||||
|
||||
- All parameter values are treated as strings (SQLite performs type coercion)
|
||||
- Complex SQL values (arrays, JSON) should be constructed in the query itself
|
||||
- Cannot mix positional and named parameters in the same query
|
||||
|
||||
@ -1,58 +0,0 @@
|
||||
# Book Catalog Configuration
|
||||
# Demonstrates the sqlite-serve module with multiple locations and template inheritance
|
||||
|
||||
load_module target/debug/libsqlite_serve.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";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,58 +0,0 @@
|
||||
# Book Detail Configuration
|
||||
# Demonstrates using path parameters with the SQLite module
|
||||
|
||||
load_module target/debug/libsqlite_serve.dylib;
|
||||
|
||||
worker_processes 1;
|
||||
events {}
|
||||
|
||||
error_log logs/error.log debug;
|
||||
|
||||
http {
|
||||
# Global templates for shared components
|
||||
sqlite_global_templates "server_root/global_templates";
|
||||
|
||||
server {
|
||||
listen 8081;
|
||||
|
||||
root "server_root";
|
||||
|
||||
# Book detail page using a path parameter (numbered capture)
|
||||
# Captures the book ID from the URL and passes it to the SQL query
|
||||
location = /book {
|
||||
add_header "Content-Type" "text/html; charset=utf-8";
|
||||
sqlite_db "book_catalog.db";
|
||||
sqlite_query "SELECT * FROM books WHERE id = ?";
|
||||
sqlite_param $arg_id;
|
||||
sqlite_template "detail.hbs";
|
||||
}
|
||||
|
||||
# Books by genre using query parameter
|
||||
location = /genre {
|
||||
add_header "Content-Type" "text/html; charset=utf-8";
|
||||
sqlite_db "book_catalog.db";
|
||||
sqlite_query "SELECT * FROM books WHERE genre = ? ORDER BY rating DESC";
|
||||
sqlite_param $arg_genre;
|
||||
sqlite_template "genre.hbs";
|
||||
}
|
||||
|
||||
# Search by year range using query parameters
|
||||
location = /years {
|
||||
add_header "Content-Type" "text/html; charset=utf-8";
|
||||
sqlite_db "book_catalog.db";
|
||||
sqlite_query "SELECT * FROM books WHERE year >= ? AND year <= ? ORDER BY year DESC, title";
|
||||
sqlite_param $arg_min;
|
||||
sqlite_param $arg_max;
|
||||
sqlite_template "list.hbs";
|
||||
}
|
||||
|
||||
# Fallback to all books
|
||||
location / {
|
||||
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";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,74 +0,0 @@
|
||||
# Book Catalog with Named Parameters
|
||||
# Demonstrates using named SQL parameters for better readability
|
||||
|
||||
load_module target/debug/libsqlite_serve.dylib;
|
||||
|
||||
worker_processes 1;
|
||||
events {}
|
||||
|
||||
error_log logs/error.log debug;
|
||||
|
||||
http {
|
||||
sqlite_global_templates "server_root/global_templates";
|
||||
|
||||
server {
|
||||
listen 8082;
|
||||
|
||||
root "server_root";
|
||||
|
||||
# Book detail with named parameter
|
||||
location = /book {
|
||||
add_header "Content-Type" "text/html; charset=utf-8";
|
||||
sqlite_db "book_catalog.db";
|
||||
sqlite_query "SELECT * FROM books WHERE id = :book_id";
|
||||
sqlite_param :book_id $arg_id;
|
||||
sqlite_template "detail.hbs";
|
||||
}
|
||||
|
||||
# Genre filter with named parameter
|
||||
location = /genre {
|
||||
add_header "Content-Type" "text/html; charset=utf-8";
|
||||
sqlite_db "book_catalog.db";
|
||||
sqlite_query "SELECT * FROM books WHERE genre = :genre_name ORDER BY rating DESC";
|
||||
sqlite_param :genre_name $arg_genre;
|
||||
sqlite_template "genre.hbs";
|
||||
}
|
||||
|
||||
# Year range with named parameters
|
||||
location = /years {
|
||||
add_header "Content-Type" "text/html; charset=utf-8";
|
||||
sqlite_db "book_catalog.db";
|
||||
sqlite_query "SELECT * FROM books WHERE year >= :min_year AND year <= :max_year ORDER BY year DESC, title";
|
||||
sqlite_param :min_year $arg_min;
|
||||
sqlite_param :max_year $arg_max;
|
||||
sqlite_template "list.hbs";
|
||||
}
|
||||
|
||||
# Search by title with named parameter
|
||||
location = /search {
|
||||
add_header "Content-Type" "text/html; charset=utf-8";
|
||||
sqlite_db "book_catalog.db";
|
||||
sqlite_query "SELECT * FROM books WHERE title LIKE '%' || :search_term || '%' ORDER BY rating DESC, title";
|
||||
sqlite_param :search_term $arg_q;
|
||||
sqlite_template "list.hbs";
|
||||
}
|
||||
|
||||
# Filter by rating with named parameter
|
||||
location = /top-rated {
|
||||
add_header "Content-Type" "text/html; charset=utf-8";
|
||||
sqlite_db "book_catalog.db";
|
||||
sqlite_query "SELECT * FROM books WHERE rating >= :min_rating ORDER BY rating DESC, title";
|
||||
sqlite_param :min_rating $arg_rating;
|
||||
sqlite_template "list.hbs";
|
||||
}
|
||||
|
||||
# Fallback to all books
|
||||
location / {
|
||||
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";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,26 +0,0 @@
|
||||
# Add the path to your library here.
|
||||
load_module target/debug/libsqlite_serve.dylib;
|
||||
|
||||
worker_processes 1;
|
||||
|
||||
events {}
|
||||
|
||||
#Uncomment and add a log file path if desired
|
||||
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;
|
||||
|
||||
root "server_root";
|
||||
location /people {
|
||||
add_header "Content-Type" "text/html";
|
||||
sqlite_db "db.sqlite3";
|
||||
sqlite_query "SELECT id, name, address FROM person";
|
||||
sqlite_template "person.hbs";
|
||||
}
|
||||
}
|
||||
}
|
||||
5
runit
5
runit
@ -1,5 +0,0 @@
|
||||
./ngx_src/nginx-1.28.0/objs/nginx \
|
||||
-e /dev/stdout \
|
||||
-p "$PWD" \
|
||||
-c conf/howto.conf \
|
||||
-g 'daemon off;'
|
||||
@ -1,122 +0,0 @@
|
||||
{{> 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}}
|
||||
|
||||
@ -1,107 +0,0 @@
|
||||
{{> 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}}
|
||||
|
||||
@ -1,107 +0,0 @@
|
||||
{{> 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}}
|
||||
|
||||
@ -1,107 +0,0 @@
|
||||
{{> 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}}
|
||||
|
||||
@ -1,16 +0,0 @@
|
||||
<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>
|
||||
|
||||
@ -1,9 +0,0 @@
|
||||
</main>
|
||||
<footer style="background: #f8f9fa; padding: 2rem; text-align: center; border-top: 2px solid #e9ecef; color: #6c757d;">
|
||||
<p>📖 Powered by sqlite-serve</p>
|
||||
<p style="margin-top: 0.5rem; font-size: 0.9rem;">A demonstration of Rust + NGINX + SQLite + Handlebars</p>
|
||||
</footer>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@ -1,77 +0,0 @@
|
||||
<!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>
|
||||
|
||||
@ -1,122 +0,0 @@
|
||||
{{> 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}}
|
||||
|
||||
@ -1,4 +0,0 @@
|
||||
<ul>
|
||||
{{#each results}}
|
||||
<li>Person: {{ id }}. {{ name }}
|
||||
{{/each}}
|
||||
@ -1,122 +0,0 @@
|
||||
{{> 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}}
|
||||
|
||||
60
src/content_type.rs
Normal file
60
src/content_type.rs
Normal file
@ -0,0 +1,60 @@
|
||||
//! Content type negotiation based on Accept headers
|
||||
|
||||
use ngx::http::Request;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum ContentType {
|
||||
Html,
|
||||
Json,
|
||||
}
|
||||
|
||||
impl ContentType {
|
||||
pub fn content_type_header(&self) -> &'static str {
|
||||
match self {
|
||||
ContentType::Html => "text/html; charset=utf-8",
|
||||
ContentType::Json => "application/json; charset=utf-8",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Determine response content type based on Accept header
|
||||
pub fn negotiate_content_type(request: &Request) -> ContentType {
|
||||
// For now, check query parameter as a simple way to request JSON
|
||||
// Full Accept header parsing would require more nginx FFI work
|
||||
let r: *const ngx::ffi::ngx_http_request_t = request.into();
|
||||
|
||||
unsafe {
|
||||
// Check args for format=json
|
||||
let args = (*r).args;
|
||||
if args.len > 0 && !args.data.is_null() {
|
||||
let args_slice = std::slice::from_raw_parts(args.data, args.len);
|
||||
if let Ok(args_str) = std::str::from_utf8(args_slice) {
|
||||
if args_str.contains("format=json") {
|
||||
return ContentType::Json;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Default to HTML
|
||||
ContentType::Html
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_content_type_header() {
|
||||
assert_eq!(ContentType::Html.content_type_header(), "text/html; charset=utf-8");
|
||||
assert_eq!(ContentType::Json.content_type_header(), "application/json; charset=utf-8");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_content_type_equality() {
|
||||
assert_eq!(ContentType::Html, ContentType::Html);
|
||||
assert_eq!(ContentType::Json, ContentType::Json);
|
||||
assert_ne!(ContentType::Html, ContentType::Json);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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,17 +98,26 @@ pub fn process_request(request: &mut ngx::http::Request, config: ValidConfigToke
|
||||
}
|
||||
};
|
||||
|
||||
// Execute and render
|
||||
// Negotiate content type based on Accept header
|
||||
let content_type = negotiate_content_type(request);
|
||||
|
||||
// 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
|
||||
send_response(request, &html)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Execute query and render with proper dependency injection
|
||||
fn execute_with_processor(
|
||||
@ -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::*;
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
|
||||
mod adapters;
|
||||
mod config;
|
||||
mod content_type;
|
||||
mod domain;
|
||||
mod handler_types;
|
||||
mod logging;
|
||||
|
||||
@ -14,8 +14,6 @@ pub enum LogLevel {
|
||||
|
||||
/// Log a message with context using nginx's native logging
|
||||
pub fn log(request: &mut Request, level: LogLevel, module: &str, message: &str) {
|
||||
let r: *mut ngx::ffi::ngx_http_request_t = request.into();
|
||||
|
||||
let log_level = match level {
|
||||
LogLevel::Error => 3, // NGX_LOG_ERR
|
||||
LogLevel::Warn => 4, // NGX_LOG_WARN
|
||||
|
||||
@ -26,8 +26,18 @@ pub fn get_doc_root_and_uri(request: &mut Request) -> Result<(String, String), S
|
||||
Ok((doc_root, uri))
|
||||
}
|
||||
|
||||
/// Create and send nginx response buffer
|
||||
/// Send HTML response
|
||||
pub fn send_response(request: &mut Request, body: &str) -> Status {
|
||||
send_response_with_content_type(request, body, "text/html; charset=utf-8")
|
||||
}
|
||||
|
||||
/// Send JSON response
|
||||
pub fn send_json_response(request: &mut Request, body: &str) -> Status {
|
||||
send_response_with_content_type(request, body, "application/json; charset=utf-8")
|
||||
}
|
||||
|
||||
/// Create and send nginx response buffer with specified content type
|
||||
fn send_response_with_content_type(request: &mut Request, body: &str, _content_type: &str) -> Status {
|
||||
// Create output buffer
|
||||
let mut buf = match request.pool().create_buffer_from_str(body) {
|
||||
Some(buf) => buf,
|
||||
@ -45,6 +55,9 @@ pub fn send_response(request: &mut Request, body: &str) -> Status {
|
||||
request.discard_request_body();
|
||||
request.set_status(http::HTTPStatus::OK);
|
||||
|
||||
// Set content type (nginx will handle it based on add_header in config or auto-detection)
|
||||
// For now, we rely on nginx config to set Content-Type via add_header directive
|
||||
|
||||
let rc = request.send_header();
|
||||
if rc == Status::NGX_ERROR || rc > Status::NGX_OK || request.header_only() {
|
||||
return rc;
|
||||
|
||||
12
src/types.rs
12
src/types.rs
@ -173,18 +173,6 @@ pub enum ParameterBinding {
|
||||
},
|
||||
}
|
||||
|
||||
impl ParameterBinding {
|
||||
pub fn param_name(&self) -> String {
|
||||
match self {
|
||||
ParameterBinding::Positional { .. } | ParameterBinding::PositionalLiteral { .. } => {
|
||||
String::new()
|
||||
}
|
||||
ParameterBinding::Named { name, .. } | ParameterBinding::NamedLiteral { name, .. } => {
|
||||
name.as_str().to_string()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
Reference in New Issue
Block a user