diff --git a/docs/admin-panel.md b/docs/admin-panel.md new file mode 100644 index 00000000..41df8bcd --- /dev/null +++ b/docs/admin-panel.md @@ -0,0 +1,122 @@ +--- +title: Admin panel +--- + +The Cot admin panel provides an automatic interface for managing your models. It allows you to add, edit, delete and view records without writing any custom views or templates. This is perfect for prototyping your application and for managing your data in cases where you don't need a custom interface, as the Cot admin panel is automatically generated based on your models. + +## Enabling the Admin Interface + +First, add the admin app and the dependencies required to your project in `src/main.rs`: + +```rust +use cot::admin::AdminApp; +use cot::auth::db::{DatabaseUser, DatabaseUserApp}; +use cot::middleware::SessionMiddleware; +use cot::project::{MiddlewareContext, RegisterAppsContext, RootHandler, RootHandlerBuilder}; +use cot::static_files::StaticFilesMiddleware; + +struct MyProject; + +impl Project for MyProject { + fn register_apps(&self, apps: &mut AppBuilder, _context: &RegisterAppsContext) { + apps.register(DatabaseUserApp::new()); // Needed for admin authentication + apps.register_with_views(AdminApp::new(), "/admin"); // Register the admin app + apps.register_with_views(MyApp, ""); + } + + fn middlewares( + &self, + handler: RootHandlerBuilder, + app_context: &MiddlewareContext, + ) -> RootHandler { + handler + .middleware(StaticFilesMiddleware::from_context(app_context)) + .middleware(SessionMiddleware::new()) // Required for admin login + .build() + } + + // ... +} +``` + +## Admin User Creation + +By default, the admin interface uses Cot's authentication system. Therefore, you need to create an admin user if it doesn't exist: + +```rust +use async_trait::async_trait; // cargo add async-trait +use cot::ProjectContext; +use cot::auth::db::DatabaseUser; +use cot::common_types::Password; +use std::env; + +// In your main.rs: +#[async_trait] +impl App for MyApp { + async fn init(&self, context: &mut ProjectContext) -> cot::Result<()> { + // Check if admin user exists + let admin_username = env::var("ADMIN_USER") + .unwrap_or_else(|_| "admin".to_string()); + let user = DatabaseUser::get_by_username(context.database(), &admin_username).await?; + if user.is_none() { + let password = env::var("ADMIN_PASSWORD") + .unwrap_or_else(|_| "change_me".to_string()); + // Create admin user + DatabaseUser::create_user( + context.database(), + &admin_username, + &Password::new(&password) + ).await?; + } + Ok(()) + } +} +``` + +## Registering Models in the Admin + +To make your models appear in the admin interface, you need to implement the `AdminModel` trait. The easiest way is to use the `#[derive(AdminModel)]` macro: + +```rust +use cot::admin::AdminModel; +use cot::db::{model, Auto}; +use cot::form::Form; + +#[derive(Debug, Form, AdminModel)] +#[model] +struct BlogPost { + #[model(primary_key)] + id: Auto, + title: String, + content: String, + published: bool, +} +``` + +Note however that in order to derive the `AdminModel` trait, you need to also derive the `Form` and `Model` traits (the latter is provided by the `#[model]` attribute). In addition to that, your model needs to implement the `Display` trait—for instance, in the case above, we could add it like so: + +```rust +impl Display for BlogPost { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.title) + } +} +``` + +After adding the `AdminModel` trait, you can add your model to the admin panel using `DefaultAdminModelManager`. This is as easy as adding the following code to your `App` implementation: + +```rust +impl App for MyApp { + fn admin_model_managers(&self) -> Vec> { + vec![Box::new(DefaultAdminModelManager::::new())] + } + + // ... +} +``` + +Now your model can be managed through the admin interface at `http://localhost:8000/admin/`! + +## Summary + +In this chapter, you learned how to enable the Cot admin panel, create an admin user, and register your models in the admin interface. In the next chapter, we'll learn how to handle static assets in Cot. diff --git a/docs/caching.md b/docs/caching.md new file mode 100644 index 00000000..a6e98eb7 --- /dev/null +++ b/docs/caching.md @@ -0,0 +1,112 @@ +--- +title: Caching +--- + +Cot provides a flexible caching system that allows you to store and retrieve data quickly, reducing the load on your database and improving response times. The caching system is designed to be pluggable, meaning you can switch between different cache backends without changing your application code. + +## Configuration + +To use the caching system, you first need to configure it in your `ProjectConfig` (or via configuration files). + +### Configuration via TOML + +You can configure the cache in your `config/*.toml` files: + +```toml +[cache] +prefix = "myapp" # Optional: prefix for all cache keys +max_retries = 3 # Optional: max retries for cache operations (default: 3) +timeout = "5s" # Optional: timeout for cache operations (default: 5s) + +[cache.store] +type = "memory" # Options: "memory", "redis", "file" (if enabled) +``` + +For Redis: + +```toml +[cache.store] +type = "redis" +url = "redis://127.0.0.1:6379" +pool_size = 20 # Optional: connection pool size +``` + +## Usage + +You can access the cache by using the `Cache` extractor. The cache interface provides standard methods like `get`, `insert`, `remove`, etc. + +```rust +use cot::cache::Cache; +use cot::html::Html; + +async fn cache_example(cache: Cache) -> cot::Result { + // Insert a value (uses default expiration if set in config, or infinite) + cache.insert("user_1_name", "Alice").await?; + + // Get a value + let name: Option = cache.get("user_1_name").await?; + + if let Some(n) = name { + println!("Found user: {}", n); + } + + Ok(Html::new("OK")) +} +``` + +### Expiration + +You can set an expiration time for specific keys: + +```rust +use std::time::Duration; +use cot::config::Timeout; + +// Cache for 60 seconds +cache.insert_expiring( + "temp_key", + "temp_value", + Timeout::After(Duration::from_secs(60)) +).await?; +``` + +## Advanced Topics + +#### Lazy Computation + +You can use `get_or_insert_with` to lazily compute and cache values: + +```rust +let value: String = cache.get_or_insert_with("expensive_key", || async { + // Perform expensive computation + Ok("expensive_result".to_string()) +}).await?; +``` + +#### Prefix + +Sharing a cache instance between different environments (e.g., production and dev), or between different versions of the same +application can cause data collisions and bugs. To prevent this, you can specify a prefix for the cache keys. When a prefix +is set, all keys will be formatted as `{prefix}:{key}`, ensuring each server instance has its own isolated namespace. + +The prefix can be set in the configuration file: + +```toml +[cache] +prefix = "v1" +``` + +## Cache Backends + +Cot supports the following cache backends: + +- **Memory**: Stores data in memory. Fast, but data is lost when the server restarts. Good for development or short-lived cache. +- **Redis**: Stores data in a Redis instance. Persistent and shared across multiple server instances. Requires the `redis` feature. +- **File**: Stores data in files. Persistent but slower than memory/Redis. Requires configuring a path. + +To use Redis, make sure to enable the `redis` feature in your `Cargo.toml`: + +```toml +[dependencies] +cot = { version = "0.5", features = ["redis"] } +``` diff --git a/docs/db-models.md b/docs/db-models.md new file mode 100644 index 00000000..c342a372 --- /dev/null +++ b/docs/db-models.md @@ -0,0 +1,201 @@ +--- +title: Database models +--- + +Cot comes with its own ORM (Object-Relational Mapping) system, which is a layer of abstraction that allows you to interact with your database using objects instead of raw SQL queries. This makes it easier to work with your database and allows you to write more maintainable code. It abstracts over the specific database engine that you are using, so you can switch between different databases without changing your code. The Cot ORM is also capable of automatically creating migrations for you, so you can easily update your database schema as your application evolves, just by modifying the corresponding Rust structures. + +## Defining models + +To define a model in Cot, you need to create a new Rust structure that implements the `Model` trait. This trait requires you to define the name of the table that the model corresponds to, as well as the fields that the table should have. Here's an example of a simple model that represents a link in a link shortener service: + +```rust +use cot::db::{model, Auto, LimitedString}; + +#[model] +pub struct Link { + #[model(primary_key)] + id: Auto, + #[model(unique)] + slug: LimitedString<32>, + url: String, +} +``` + +There's some very useful stuff going on here, so let's break it down: + +* The `#[model]` attribute is used to mark the structure as a model. This is required for the Cot ORM to recognize it as such. +* The `id` field is a typical database primary key, which means that it uniquely identifies each row in the table. It's of type `i64`, which is a 64-bit signed integer. `Auto` wrapper is used to automatically generate a new value for this field when a new row is inserted into the table (`AUTOINCREMENT` or `SERIAL` value in the database nomenclature). +* The `slug` field is marked as `unique`, which means that each value in this field must be unique across all rows in the table. It's of type `LimitedString<32>`, which is a string with a maximum length of `32` characters. This is a custom type provided by Cot that ensures that the string is not longer than the specified length at the time of constructing an instance of the structure. + +After putting this structure in your project, you can use it to interact with the database. Before you do that though, it's necessary to create the table in the database that corresponds to this model. Cot CLI has got you covered and can automatically create migrations for you – just run the following command: + +```bash +cot migration make +``` + +This will create a new file in your `migrations` directory in the crate's src directory. We will come back to the contents of this file later in this guide, but for now, let's focus on how to use the model to interact with the database. + +## Common operations + +### Saving models + +In order to write a model instance to the database, you can use the `save` method. Note that you need to have an instance of the `Database` structure to do this – typically you can get it from the request object in your view. Here's an example of how you can save a new link to the database inside a view: + +```rust +use cot::request::extractors::RequestDb; + +async fn create_link(RequestDb(db): RequestDb) -> cot::Result { + let mut link = Link { + id: Auto::default(), + slug: LimitedString::new("slug").unwrap(), + url: "https://example.com".to_string(), + }; + link.save(db).await?; + + // ... +} +``` + +### Updating models + +Updating a model is similar to saving a new one, but you need to have an existing instance of the model that you want to update, or another instance with the same primary key. Here's an example of how you can update an existing link in the database: + +```rust +link.url = "https://example.org".to_string(); +link.save(db).await?; +``` + +Note that `.save()` is a convenient method that can be used for both creating new rows and updating existing ones. If the primary key of the model is set to `Auto`, the method will always create a new row in the database. If the primary key is set to a specific value, the method will update the row with that primary key, or create a new one if it doesn't exist. + +If you specifically want to update a row in the database for given primary key, you can use the `update` method: + +```rust +link.url = "https://example.org".to_string(); +link.update(db).await?; +``` + +Similarly, if you want to insert a new row in the database and cause an error if a row with the same primary key already exists, you can use the `insert` method: + +```rust +let mut link = Link { + id: Auto::default(), + slug: LimitedString::new("slug").unwrap(), + url: "https://example.com".to_string(), +}; +link.insert(db).await?; +``` + +### Retrieving models + +The basis for retrieving models from the database is the `Query` structure. It contains information about which model you want to retrieve and allows you to filter, sort, and limit the results. + +The easiest way to work with the `Query` structure is the `query!` macro, which allows you to write complicated queries in readable way using Rusty syntax. For example, to retrieve the link which has slug "cot" from the database, you can write: + +```rust +use cot::db::query; + +let link = query!(Link, $slug == LimitedString::new("cot").unwrap()) + .get(db) + .await?; +``` + +As you can see, the `query!` macro takes the model type as the first argument, followed by the filter expression. The filter expression supports many of the common comparison operators, such as `==`, `!=`, `>`, `<`, `>=`, and `<=`. You can also use logical operators like `&&` and `||` to combine multiple conditions. The `$` sign is used to access the fields of the model in the filter expression—this is needed so that the macro can differentiate between fields of the model and other variables. What's nice about the filter expression is that it's type-checked at compile time, so not only you won't be able to filter using a non-existent field, but also you won't be able to compare fields of different types. + +### Deleting models + +To delete a model from the database, you can use the `delete` method of the `Query` object returned by the `query!` macro. Here's an example of how you can delete a link from the database: + +```rust +query!(Link, $slug == LimitedString::new("cot").unwrap()).delete(db).await?; +``` + +### Bulk operations + +If you need to insert multiple rows at once, you can use the `bulk_insert` method. This is much more efficient than calling `save` or `insert` for each row individually, as it performs the operation in a single database query. + +```rust +let mut links = vec![ + Link { + id: Auto::default(), + slug: LimitedString::new("cot").unwrap(), + url: "https://cot.rs".to_string(), + user: ForeignKey::new(1), + }, + Link { + id: Auto::default(), + slug: LimitedString::new("rust").unwrap(), + url: "https://rust-lang.org".to_string(), + user: ForeignKey::new(1), + }, +]; + +Link::bulk_insert(db, &mut links).await?; +``` + +Note that `bulk_insert` takes a mutable slice of models, because it needs to update the primary keys of the inserted models with the values generated by the database. + +Similarly, there is also `bulk_insert_or_update` method, which works like `bulk_insert`, but updates the existing rows if they conflict with the new ones. + +```rust +Link::bulk_insert_or_update(db, &mut links).await?; +``` + +## Foreign keys + +To define a foreign key relationship between two models, you can use the `ForeignKey` type. Here's an example of how you can define a foreign key relationship between a `Link` model and some other `User` model: + +```rust +use cot::db::ForeignKey; + +#[model] +pub struct Link { + #[model(primary_key)] + id: Auto, + #[model(unique)] + slug: LimitedString<32>, + url: String, + user: ForeignKey, +} + +#[model] +pub struct User { + #[model(primary_key)] + id: Auto, + name: String, +} +``` + +When you define a foreign key relationship, Cot will automatically create a foreign key constraint in the database. This constraint will ensure that the value in the `user_id` field of the `Link` model corresponds to a valid primary key in the `User` model. + +When you retrieve a model that has a foreign key relationship, Cot will not automatically fetch the related model and populate the foreign key field with the corresponding value. Instead, you need to explicitly fetch the related model using the `get` method of the `ForeignKey` object. Here's an example of how you can fetch the related user for a link: + +```rust +let mut link = query!(Link, $slug == LimitedString::new("cot").unwrap()) + .get(db) + .await? + .expect("Link not found"); + +let user = link.user.get(db).await?; +``` + +## Database Configuration + +Configure your database connection in the configuration files inside your `config` directory: + +```toml +[database] +# SQLite +url = "sqlite://db.sqlite3?mode=rwc" + +# Or PostgreSQL +url = "postgresql://user:password@localhost/dbname" + +# Or MySQL +url = "mysql://user:password@localhost/dbname" +``` + +Cot tries to be as consistent as possible when it comes to the database engine you are using. This means that you can use SQLite for development and testing, and then switch to PostgreSQL or MySQL for production without changing your code. The only thing you need to do is to change the `url` value in the configuration file! + +## Summary + +In this chapter you learned how to define your own models in Cot, how to interact with the database using these models, and how to define foreign key relationships between models. In the next chapter, we'll try to register these models in the admin panel so that you can manage them through an easy-to-use web interface. diff --git a/docs/error-pages.md b/docs/error-pages.md new file mode 100644 index 00000000..db45ff96 --- /dev/null +++ b/docs/error-pages.md @@ -0,0 +1,115 @@ +--- +title: Error pages +--- + +Error pages in Cot provide users with helpful information when something goes wrong. Let's learn how to handle errors gracefully and create custom error pages. + +## Debug mode error pages + +In development (debug mode), Cot provides detailed error pages that include: + +* Error message and type +* Stack trace +* Request information +* Configuration details +* Route information + +The debug mode is enabled in the default `dev` configuration: + +```toml +# config/dev.toml +debug = true +``` + +Now, when you visit a non-existing page, or if your code raises an error or panics, Cot will display a detailed error page with the information useful to debug the issue. Note that the error pages in debug mode may contain sensitive information, so you should always make sure it is disabled in production! + +## Default error pages + +When the debug mode is disabled, Cot provides default error pages that do not share any information about what happened to the user. To match your service's look and feel, you'll typically want to customize them. + +## Custom error handlers + +Let's implement a custom error handler in your project: + +```rust +use cot::html::Html; +use cot::response::{IntoResponse, Response}; +use cot::error::handler::{DynErrorPageHandler, RequestError}; +use cot::Template; + +async fn error_page_handler(error: RequestError) -> cot::Result { + #[derive(Template)] + #[template(path = "error.html")] + struct ErrorTemplate { + error: RequestError, + } + + let status_code = error.status_code(); + let error_template = ErrorTemplate { error }; + let rendered = error_template.render()?; + + Ok(Html::new(rendered).with_status(status_code)) +} + +struct MyProject; + +impl Project for MyProject { + fn error_handler(&self) -> DynErrorPageHandler { + DynErrorPageHandler::new(error_page_handler) + } +} +``` + +Create `templates/error.html`: + +```html + + + + + Error + + +

{{ error.status_code().as_u16() }}

+

{{ error.status_code().canonical_reason().unwrap_or("Error") }}

+ + +``` + +Now, try to visit an undefined route or raise an error in your code. You should see the custom error pages you've created! + +## Raising errors in views + +Cot provides several ways to raise errors in your views: + +```rust +use cot::Error; +use cot::error::NotFound; +use cot::request::Request; +use cot::response::Response; + +async fn view(request: Request) -> cot::Result { + // 404 Not Found + return Err(NotFound::new())?; + + // 404 with custom message + return Err(NotFound::with_message( + "The article you're looking for doesn't exist".to_string() + ))?; + + // 500 Internal Server Error + return Err(Error::internal("Something went wrong")); + // or, by re-raising a custom error: + return Err(Error::internal(std::io::Error::other("oh no!"))); + // or, by panicking: + panic!("Something went wrong"); +} +``` + +Note that any messages that you pass to the `Error` structure will only be displayed in debug mode by default. In production, the user will see your custom error pages (which may or may not retrieve the underlying error message, depending on how you implemented them). + +## Summary + +In this chapter, you learned how to handle errors in Cot applications. You can create custom error pages, raise errors in your views, and overall provide a better user experience when something goes wrong. + +Next chapter, we'll explore automatic testing in Cot applications. diff --git a/docs/forms.md b/docs/forms.md new file mode 100644 index 00000000..9ffa7a4f --- /dev/null +++ b/docs/forms.md @@ -0,0 +1,164 @@ +--- +title: Forms +--- + +Cot has form processing capabilities that allows you to create forms and handle form submissions with ease. Processing forms is as easy as creating a Rust structure, deriving a trait, and then using one function to process the form using the request data. Cot will automatically validate the form data and handle any errors that occur. + +## Form trait + +The core of the form processing lies in the `Form` trait inside the `cot::form` module. This trait is used to define the form and the fields that are part of the form. Below is an example of how you can define a form: + +```rust +use cot::form::Form; + +#[derive(Form)] +struct ContactForm { + name: String, + email: String, + #[form(opts(max_length = 1000))] + message: String, +} +``` + +And here is how you can process the form inside a request handler: + +```rust +use cot::form::{Form, FormResult}; +use cot::html::Html; +use cot::request::{Request, RequestExt}; +use cot::response::{Response, ResponseExt}; + +async fn contact(mut request: Request) -> cot::Result { + // Handle POST request (form submission) + if request.method() == Method::POST { + match ContactForm::from_request(&mut request).await? { + FormResult::Ok(form) => { + // Form is valid! Process the data + println!("Message from {}: {}", form.name, form.message); + + // Redirect after successful submission + Ok(reverse_redirect!(request, "thank_you")?) + } + FormResult::ValidationError(context) => { + // Form has errors - render the template with error messages + let template = ContactTemplate { + request: &request, + form: context, + }; + Ok(Html::new(template.render()?).into()) + } + } + } else { + // Handle GET request (display empty form) + let template = ContactTemplate { + request: &request, + form: ContactForm::build_context(&mut request).await?, + }; + + Ok(Html::new(template.render()?).into()) + } +} +``` + +### Forms in templates + +Before this is really usable, we need to define the form in the HTML template. Thankfully, Cot provides you with a way to implement this easily, too—it can automatically generate the HTML for the form based on the form definition. + +There are several ways how can you use the forms in your templates. The easiest one is to use the form directly in the template: + +```html.j2 +{% let request = request %} + +
+ {{ form }} + + +
+``` + +This is especially useful for prototyping new forms, as it doesn't allow you to customize the rendering of your form. If you need a bit more control, you can use the `form.fields()` method to render the fields individually: + +```html.j2 +{% let request = request %} + +
+ {% for field in form.fields() %} +
+ + {{ field|safe }} + +
    + {% for error in form_context.errors_for(FormErrorTarget::Field(field.dyn_id())) %} +
  • {{ error }}
  • + {% endfor %} +
+
+ {% endfor %} + + +
+``` + +It is recommended to reuse the template code for rendering the form fields using `{% include %}` to make it easy to achieve a consistent look and feel across your application. + +## Field validation + +Cot provides several ways to validate form data: + +### Built-in validation + +```rust +#[derive(Form)] +struct ArticleForm { + // Maximum length validation + #[form(opts(max_length = 100))] + title: String, + + // Required checkbox + #[form(opts(must_be_true = true))] + confirm_publish: bool, +} +``` + +### Custom validation + +You can implement custom validation by handling the validation result: + +```rust +async fn handle_form(mut request: Request) -> cot::Result { + match ArticleForm::from_request(&mut request).await? { + FormResult::Ok(form) => { + // Add custom validation + if form.title.to_lowercase().contains("spam") { + let mut context = ArticleForm::build_context(&mut request).await?; + context.add_error( + FormErrorTarget::Field("title"), + FormFieldValidationError::from_static("Title contains spam") + ); + + // Re-render form with error + return Ok(Html::new(render_template(context)?).into()); + } + + // Process valid form... + Ok(reverse_redirect!(request, "success")?) + } + FormResult::ValidationError(context) => { + // Handle validation errors... + Ok(Html::new(render_template(context)?).into()) + } + } +} +``` + +## Summary + +In this chapter you learned how to handle forms and validate form data in Cot applications. Remember: + +* Always validate form data server-side +* Provide clear error messages +* Use appropriate field types +* Consider user experience in form layout +* Handle both GET and POST requests appropriately + +In the next chapter, we'll explore database models and how you can use them to persist data in your services. diff --git a/docs/framework-comparison.md b/docs/framework-comparison.md new file mode 100644 index 00000000..eaa9fcc3 --- /dev/null +++ b/docs/framework-comparison.md @@ -0,0 +1,48 @@ +--- +title: Framework comparison +--- + +Cot is an opinionated, batteries-included web framework for Rust, designed to feel familiar to developers coming from Django (Python) or similar high-level frameworks. While the Rust ecosystem is rich with excellent web frameworks, most of them follow a "modular" philosophy—giving you the building blocks but requiring you to assemble the rest (database, auth, admin interfaces) yourself. + +Cot takes a different approach by providing a cohesive, integrated experience out of the box. + +## Comparison Table + +| | Cot | Axum | Actix-web | Rocket | Loco | +|:----------------------|:-----------------------------------------|:--------------------------|:---------------------------|:-----------------------------------------|:--------------------------------| +| **Philosophy** | Batteries-included (Django-like) | Modular / Micro-framework | Modular / High-performance | Batteries-included | Batteries-included (Rails-like) | +| **Underlying Engine** | [Axum](https://github.com/tokio-rs/axum) | Hyper / Tokio | Actix | Hyper / Tokio | Axum | +| **ORM** | Integrated (Cot ORM, based on SeaORM) | Agnostic | Agnostic | Agnostic | SeaORM | +| **Admin Panel** | **Built-in** | ❌ | ❌ | ❌ | ❌ | +| **Migrations** | Auto-generated from structs | External (e.g., sqlx-cli) | External | External | External / CLI | +| **Templating** | Agnostic, but [Askama] built-in | Agnostic | Agnostic | Agnostic (integrates w/ Tera/Handlebars) | Agnostic (integrates w/ Tera) | + +[Askama]: https://askama.readthedocs.io/en/stable/ + +## Cot vs. Axum + +[Axum](https://github.com/tokio-rs/axum) is currently one of the most popular web frameworks in the Rust ecosystem. In fact, **Cot is built on top of Axum**. + +* **Choose Axum if:** You want full control over every component of your stack, prefer a minimalist approach, or are building a microservice that doesn't need a UI or database management. +* **Choose Cot if:** You want a full-stack experience with standard conventions. Cot handles the "glue code" for databases, authentication, and sessions so you can focus on business logic. Since Cot uses Axum internally, you often benefit from Axum's performance and compatibility with the Tower ecosystem. + +## Cot vs. Actix-web + +[Actix-web](https://actix.rs/) is known for its extreme performance and maturity. + +* **Choose Actix-web if:** Raw request-per-second performance is your primary metric, or you are deeply invested in the Actor model. +* **Choose Cot if:** You prioritize developer velocity and ease of use over squeezing the last microsecond of performance. Cot is still very fast (thanks to Rust and Axum), but it prioritizes "getting things done" with tools like the Admin panel and auto-migrations. + +## Cot vs. Rocket + +[Rocket](https://rocket.rs/) is famous for its focus on developer ergonomics and macro-based routing. + +* **Choose Rocket if:** You love its specific API style and macro magic, and don't mind picking your own database layer. +* **Choose Cot if:** You want the "Django" experience. While Rocket makes routing easy, it doesn't prescribe how to handle users, permissions, or admin interfaces. Cot provides these standard web application features out of the box. + +## Cot vs. Loco + +[Loco](https://loco.rs/) is another batteries-included framework, often described as "Rails for Rust". + +* **Choose Loco if:** You prefer the Ruby on Rails philosophy, use SeaORM, and like its specific project structure. +* **Choose Cot if:** You prefer the Django philosophy. Cot's defining features—like the auto-generated Admin panel and the specific way it maps Rust structs to database tables—are directly inspired by Django, often making it easier to quickly start a web application with standard features. diff --git a/docs/introduction.md b/docs/introduction.md new file mode 100644 index 00000000..083a74d9 --- /dev/null +++ b/docs/introduction.md @@ -0,0 +1,265 @@ +--- +title: Introduction +--- + +[bacon]: https://dystroy.org/bacon/ + +Cot is a free and open-source web framework for Rust that makes building web applications both fun and reliable. Taking inspiration from [Django](https://www.djangoproject.com/)'s developer-friendly approach, Cot combines Rust's safety guarantees with rapid development features that help you build secure web applications quickly. Whether you're coming from Django or are new to web development entirely, you'll find Cot's intuitive design helps you be productive from day one. + +## Who is this guide for? + +This guide doesn't assume any advanced knowledge in Rust or web development in general (although this will help, too!). It's aimed at beginners who are looking to get started with Cot, and will guide you through the process of setting up a new project, creating your first views, using the Cot ORM and running your application. + +If you are not familiar with Rust, you might want to start by reading the [Rust Book](https://doc.rust-lang.org/book/), which is an excellent resource for learning Rust. + +## Installing and running Cot CLI + +Let's get your first Cot project up and running! First, you'll need Cargo, Rust's package manager. If you don't have it installed, you can get it through [rustup](https://rustup.rs/). Cot requires Rust version 1.88 or later. + +Install the Cot CLI with: + +```bash +cargo install --locked cot-cli +``` + +Now create your first project: + +```bash +cot new cot_tutorial +``` + +This creates a new directory called `cot_tutorial` with a new Cot project inside. Let's explore what Cot has created for us: + +```bash +cot_tutorial +├── config # Configuration files for different environments +│ ├── dev.toml +│ └── prod.toml.example +├── src # Your application code lives here +│ ├── main.rs +│ └── migrations.rs +├── static # CSS, JavaScript, Images, and other static files +│ └── css +│ └── main.css +├── templates # HTML templates for your pages +│ └── index.html +├── .gitignore +├── bacon.toml # Configuration for live-reloading during development +└── Cargo.toml +``` + +If you don't have [bacon] installed already, we strongly recommend you to do so. It will make your development process much more pleasant by providing you with the live-reloading functionality. You can install it by running: + +```bash +cargo install --locked bacon +``` + +After you do that, you can run your Cot application by running: + +```bash +bacon serve +``` + +Or, if you don't have [bacon] installed, you can run your application with the typical: + +```bash +cargo run +``` + +Now, if you open your browser and navigate to [`localhost:8000`](http://localhost:8000), you should see a welcome page that Cot has generated for you. Congratulations, you've just created your first Cot application! + +## Command Line Interface + +Cot provides you with a CLI (Command Line Interface) for running your service. You can see all available commands by running: + +```bash +cargo run -- --help +``` + +This will show you a list of available commands and options. This will be useful later, but for now you might want to know probably the most useful options `-c/--config`, which allows you to specify the configuration file to use. By default, Cot uses the `dev.toml` file from the `config` directory. + +## Views and routing + +At the heart of any web application is the ability to handle requests and return responses—this is exactly what views do in Cot. Let's look at the view that Cot generated for us and then create our own! + +When you open the `src/main.rs` file, you'll see the following example view that has been generated for you: + +```rust +async fn index() -> cot::Result { + let index_template = IndexTemplate {}; + let rendered = index_template.render()?; + + Ok(Html::new(rendered)) +} +``` + +Further in the file you can see that this view is registered in the `App` implementation: + +```rust +struct CotTutorialApp; + +impl App for CotTutorialApp { + // ... + + fn router(&self) -> Router { + Router::with_urls([Route::with_handler_and_name("/", index, "index")]) + } +} +``` + +This is how you specify the URL the view will be available at – in this case, the view is available at the root URL of your application. The `"index"` string is the name of the view, which you can use to reverse the URL in your templates – more on that in the next chapter. + +You can add more views by adding more routes to the `Router` by simply defining more functions and registering them in the `router` method: + +```rust +async fn hello() -> Html { + Html::new("Hello World!") +} + +// inside `impl App`: + +fn router(&self) -> Router { + Router::with_urls([ + Route::with_handler_and_name("/", index, "index"), + Route::with_handler_and_name("/hello", hello, "hello"), + ]) +} +``` + +Now, when you visit [`localhost:8000/hello`](http://localhost:8000/hello) you should see `Hello World!` displayed on the page! + +### Extractors and dynamic routes + +You can also define dynamic routes by using the `Route::with_handler_and_name` method with a parameter enclosed in curly braces (e.g. `{param_name}`). How do we get the parameter value in the request handler's body, though? + +At the core of Cot's request handling are _extractors_, which allow you to extract data from the request and pass it to the handler as arguments. One of such extractors is the `Path` extractor, which allows you to extract path parameters from the URL. In order to use it, you need to define a parameter in the handler function, passing the parameter type as the generic parameter, like so: + +```rust +use cot::request::extractors::Path; + +async fn hello_name(Path(name): Path) -> cot::Result { + Ok(Html::new(format!("Hello, {}!", name))) +} + +// inside `impl App`: + +fn router(&self) -> Router { + Router::with_urls([ + Route::with_handler_and_name("/", index, "index"), + Route::with_handler_and_name("/hello", hello, "hello"), + Route::with_handler_and_name("/hello/{name}", hello_name, "hello_name"), + ]) +} +``` + +This works for multiple parameters, too—you just need to define a tuple of parameters in the handler function: + +```rust +async fn hello_name(Path((first_name, last_name)): Path<(String, String)>) -> cot::Result { + Ok(Html::new(format!("Hello, {first_name} {last_name}!"))) +} + +// inside `impl App`: + +fn router(&self) -> Router { + Router::with_urls([ + // ... + Route::with_handler_and_name("/hello/{first_name}/{last_name}/", hello_name, "hello_name"), + ]) +} +``` + +Now, when you visit [`localhost:8000/hello/John/Smith/`](http://localhost:8000/hello/John), you should see `Hello, John Smith!` displayed on the page! + +## Project structure + +### App + +Along with an example view, the entire Cot project structure has been created for you. Let's take a look one by one at what you can find in `main.rs`: + +```rust +struct CotTutorialApp; + +impl App for CotTutorialApp { + fn name(&self) -> &'static str { + env!("CARGO_CRATE_NAME") + } +``` + +An app is a collection of views and other components that make up a part of your service. Typically, they represent a part of your service, like the main website, an admin panel, or an API. An app usually corresponds to a single Rust crate, hence we're just using the name of the crate as the app name. The app name is used in many places, such as in the database table names, in the admin panel, or when reversing the URLs, so it needs to be unique in your project. + +```rust + fn migrations(&self) -> Vec> { + cot::db::migrations::wrap_migrations(migrations::MIGRATIONS) + } +``` + +This defines the database migration list that will be applied when your server starts. You shouldn't normally need to modify this, and the migrations can be generated automatically using the Cot CLI – more on this in the chapter about database models. + +```rust + fn static_files(&self) -> Vec { + static_files!("css/main.css") + } +} +``` + +This defines a list of static files that will be served by the server. More on that will be covered in the chapter about static files. + +### Project + +A project is a collection of apps, middlewares, and other components that make up your service. It ties everything together and is the entry point for your application. Here's the default project implementation's structure analyzed step by step: + +```rust +struct CotTutorialProject; + +impl Project for CotTutorialProject { + fn cli_metadata(&self) -> CliMetadata { + cot::cli::metadata!() + } +``` + +This defines the project and sets the CLI metadata (like the name, version, and description) that will be displayed when you run `cargo run -- --help` by using the metadata from your Cargo crate. + +```rust + fn register_apps(&self, apps: &mut AppBuilder, _context: &RegisterAppsContext) { + apps.register_with_views(CotTutorialApp, ""); + } +``` + +This registers all the apps that your project is using. + +```rust + fn middlewares( + &self, + handler: RootHandlerBuilder, + context: &MiddlewareContext, + ) -> RootHandler { + handler + .middleware(StaticFilesMiddleware::from_context(context)) + .middleware(LiveReloadMiddleware::from_context(context)) + .build() + } +``` + +This registers the middlewares that will be applied to all routes in the project. Note that the `LiveReloadMiddleware` may be dynamically disabled in runtime using config! + +```rust +#[cot::main] +fn main() -> impl Project { + CotTutorialProject +} +``` + +Finally, the `main` function just returns the Project implementation, which is the entry point for your application. Cot takes care of running it by providing a command line interface! + +## Final words + +In this chapter, you learned about: + +* creating a new Cot project and how the Cot project structure looks like, +* running your first Cot project, +* create views, registering them in the router and passing parameters to them. + +In the next chapter, we'll dive deeper into templates, which will allow us to create more sophisticated HTML pages. + +Remember to use `cargo doc --open` to browse the Cot documentation locally, or visit the [online documentation](https://docs.rs/cot) for more details about any of the components we've discussed. diff --git a/docs/openapi.md b/docs/openapi.md new file mode 100644 index 00000000..8af3270c --- /dev/null +++ b/docs/openapi.md @@ -0,0 +1,312 @@ +--- +title: OpenAPI +--- + +One of Cot's powerful features is its ability to automatically generate OpenAPI documentation for your web API. This allows you to create interactive API documentation with minimal configuration, making your APIs more accessible and easier to understand for users and developers. + +## Overview + +OpenAPI (formerly known as Swagger) is a specification for machine-readable interface files for describing, producing, consuming, and visualizing RESTful web services. Cot provides built-in support for: + +1. Automatic OpenAPI specification generation based on your route definitions +2. Integration with Swagger UI for interactive API documentation +3. Type-safe API development with proper schema generation + +This chapter will guide you through setting up OpenAPI in your Cot project and show you how to leverage automatic specification generation to create well-documented APIs. + +## Prerequisites + +To use OpenAPI features in Cot, you need to enable the `openapi` and `swagger-ui` features in your project's `Cargo.toml`: + +```toml +[dependencies] +cot = { version = "...", features = ["openapi", "swagger-ui"] } +schemars = "0.9" # Required for JSON Schema generation +``` + +The `schemars` crate is necessary for creating JSON Schema definitions for your request and response types. + +## Setting Up Your API + +### Define Your Data Types + +First, define your request and response data types with `serde` for serialization and `schemars` for schema generation: + +```rust +use serde::{Deserialize, Serialize}; +use schemars::JsonSchema; + +#[derive(Deserialize, JsonSchema)] +struct AddRequest { + a: i32, + b: i32, +} + +#[derive(Serialize, JsonSchema)] +struct AddResponse { + result: i32, +} +``` + +Note the use of `#[derive(JsonSchema)]` which comes from the `schemars` crate. This attribute generates schema information that Cot uses to build the OpenAPI specification. + +### Create API Handlers + +Next, create your API handlers using Cot's extractors: + +```rust +use cot::json::Json; + +async fn add(Json(add_request): Json) -> cot::Result> { + let response = AddResponse { + result: add_request.a + add_request.b, + }; + + Json(response) +} +``` + +### Use API Method Routers + +Instead of using regular method routers, use the OpenAPI-enabled versions that automatically generate API documentation: + +```rust +use cot::router::method::openapi::api_post; +use cot::router::{Route, Router}; + +fn create_router() -> Router { + Router::with_urls([ + Route::with_api_handler("/add/", api_post(add)), + ]) +} +``` + +The key differences from standard routes are: + +- Using `with_api_handler` instead of `with_handler` +- Using `api_post` instead of `post` + +### Register the Swagger UI App + +To expose the interactive documentation UI, register the `SwaggerUi` app in your project: + +```rust +use cot::openapi::swagger_ui::SwaggerUi; +use cot::static_files::StaticFilesMiddleware; +use cot::{App, AppBuilder, Project}; + +struct MyProject; + +impl Project for MyProject { + fn middlewares( + &self, + handler: RootHandlerBuilder, + context: &MiddlewareContext, + ) -> BoxedHandler { + // StaticFilesMiddleware is required for SwaggerUI to serve its assets + handler + .middleware(StaticFilesMiddleware::from_context(context)) + .build() + } + + fn register_apps(&self, apps: &mut AppBuilder, context: &RegisterAppsContext) { + // Register the Swagger UI at the /swagger path + apps.register_with_views(SwaggerUi::new(), "/swagger"); + + // Register your API app + apps.register_with_views(MyApiApp, ""); + } +} +``` + +Don't forget to include the `StaticFilesMiddleware` as it's required for the Swagger UI to serve its CSS and JavaScript files! + +## Complete Example + +Here's a complete example of a simple API with OpenAPI documentation: + +```rust +use cot::cli::CliMetadata; +use cot::config::ProjectConfig; +use cot::json::Json; +use cot::openapi::swagger_ui::SwaggerUi; +use cot::project::{MiddlewareContext, RegisterAppsContext, RootHandlerBuilder}; +use cot::router::method::openapi::api_post; +use cot::router::{Route, Router}; +use cot::static_files::StaticFilesMiddleware; +use cot::{App, AppBuilder, BoxedHandler, Project}; +use serde::{Deserialize, Serialize}; + +#[derive(Deserialize, schemars::JsonSchema)] +struct AddRequest { + a: i32, + b: i32, +} + +#[derive(Serialize, schemars::JsonSchema)] +struct AddResponse { + result: i32, +} + +async fn add(Json(add_request): Json) -> cot::Result> { + let response = AddResponse { + result: add_request.a + add_request.b, + }; + + Json(response) +} + +struct AddApp; + +impl App for AddApp { + fn name(&self) -> &'static str { + env!("CARGO_PKG_NAME") + } + + fn router(&self) -> Router { + Router::with_urls([Route::with_api_handler("/add/", api_post(add))]) + } +} + +struct ApiProject; + +impl Project for ApiProject { + fn cli_metadata(&self) -> CliMetadata { + cot::cli::metadata!() + } + + fn config(&self, _config_name: &str) -> cot::Result { + Ok(ProjectConfig::dev_default()) + } + + fn middlewares( + &self, + handler: RootHandlerBuilder, + context: &MiddlewareContext, + ) -> BoxedHandler { + handler + .middleware(StaticFilesMiddleware::from_context(context)) + .build() + } + + fn register_apps(&self, apps: &mut AppBuilder, _context: &RegisterAppsContext) { + apps.register_with_views(SwaggerUi::new(), "/swagger"); + apps.register_with_views(AddApp, ""); + } +} + +#[cot::main] +fn main() -> impl Project { + ApiProject +} +``` + +After running this example, you can: + +1. Navigate to `http://localhost:8000/swagger/` to see the interactive API documentation +2. Test your API directly from the browser using the Swagger UI, or +3. Make requests programmatically: + ```bash + curl --header "Content-Type: application/json" \ + --request POST \ + --data '{"a": 123, "b": 456}' \ + 'http://localhost:8000/add/' + ``` + +## Advanced Features + +### Using Path Parameters + +Path parameters are automatically detected and included in the OpenAPI specification: + +```rust +use cot::request::extractors::Path; + +async fn get_user(Path(user_id): Path) -> cot::Result { + // ... +} + +// Register the route +Route::with_api_handler("/users/{user_id}", api_get(get_user)) +``` + +### URL Query Parameters + +Query parameters are also supported and properly documented: + +```rust +use cot::request::extractors::UrlQuery; + +#[derive(Deserialize, JsonSchema)] +struct UserQuery { + active: Option, + role: Option, +} + +async fn list_users(UrlQuery(query): UrlQuery) -> cot::Result { + // ... +} + +// Register the route +Route::with_api_handler("/users", api_get(list_users)) +``` + +### Excluding Routes from OpenAPI Documentation + +Sometimes you might want to exclude certain routes from your API documentation. You can do this by using `NoApi`: + +```rust +use cot::openapi::NoApi; + +// This handler will be in the API docs +Route::with_api_handler("/visible", api_get(visible_handler)) + +// This handler will work but won't appear in the docs +Route::with_api_handler("/hidden", api_get(NoApi(hidden_handler))) +``` + +You can also exclude specific parameters from the OpenAPI docs: + +```rust +async fn handler( + Path(id): Path, // Included in OpenAPI docs + NoApi(context): NoApi, // Excluded from OpenAPI docs +) -> cot::Result { + // ... implementation +} +``` + +### Multiple HTTP Methods + +The `ApiMethodRouter` allows you to define multiple HTTP methods for a single route and include them all in the OpenAPI documentation: + +```rust +use cot::router::method::openapi::ApiMethodRouter; + +Route::with_api_handler( + "/items", + ApiMethodRouter::new() + .get(list_items) + .post(create_item) + .put(update_item) + .delete(delete_item) +) +``` + +Each method will be properly documented in the OpenAPI specification. + +### Implement your own OpenAPI extractor + +In order for your parameter or response type to generate OpenAPI specification, you need to implement the [`ApiOperationPart`](https://docs.rs/cot/0.5/cot/openapi/trait.ApiOperationPart.html) trait. You can study their implementations to understand how to design your own: + +* [`Json`](https://docs.rs/cot/0.5/cot/json/struct.Json.html) adds a request or response body to the operation +* [`Path`](https://docs.rs/cot/0.5/cot/request/extractors/struct.Path.html) adds path parameters +* [`UrlQuery`](https://docs.rs/cot/0.5/cot/request/extractors/struct.UrlQuery.html) adds query parameters + +The key is to modify the `Operation` object appropriately for your extractor, adding parameters, request bodies, or other OpenAPI elements as needed. + +## Conclusion + +Cot's OpenAPI integration provides a powerful way to automatically generate comprehensive API documentation while maintaining type safety. By leveraging the schema generation capabilities, you can create well-documented APIs with minimal overhead, making your services more accessible and easier to use. + +With just a few additions to your code, you get interactive documentation that stays in sync with your implementation, eliminating the common problem of outdated API docs. This feature is particularly valuable for teams working on APIs that are consumed by external developers or multiple internal teams. diff --git a/docs/sending-emails.md b/docs/sending-emails.md new file mode 100644 index 00000000..233f11a5 --- /dev/null +++ b/docs/sending-emails.md @@ -0,0 +1,75 @@ +--- +title: Sending Emails +--- + +Cot provides a unified interface for sending emails, allowing you to switch between different email backends (like SMTP, Memory, or Console) easily. This is powered by the popular [`lettre`](https://crates.io/crates/lettre) crate. + +## Configuration + +To use the email system, you need to enable the `email` feature in `cot` and configure it. + +### Enabling the Feature + +In your `Cargo.toml`: + +```toml +[dependencies] +cot = { version = "0.5", features = ["email"] } +``` + +### Configuration via TOML + +Configure the email transport in your `config/*.toml` files: + +```toml +[email] +from = "no-reply@example.com" # Default sender address (optional) + +[email.transport] +type = "smtp" # Options: "smtp", "console" +url = "smtp://user:password@localhost:587" # For SMTP +mechanism = "plain" # or "login", "xoauth2" +``` + +For development, you might want to use the `console` transport, which prints emails to stdout: + +```toml +[email.transport] +type = "console" +``` + +## Sending Emails + +You can access the email sender by using the `Email` extractor. + +```rust +use cot::common_types::Email; +use cot::email::{Email as EmailService, EmailMessage}; +use cot::html::Html; + +async fn send_welcome_email(email_sender: EmailService) -> cot::Result { + let message = EmailMessage::builder() + .from(Email::try_from("no-reply@example.com").unwrap()) + .to(vec![Email::try_from("user@example.com").unwrap()]) + .subject("Welcome to Cot!") + .body("Hello, welcome to our service!") + .build()?; + + email_sender.send(message).await?; + + Ok(Html::new("Email sent!")) +} +``` + +## Email Message Builder + +The `EmailMessage::builder()` provides a fluent interface to construct emails. It supports: + +- **From/To/Cc/Bcc**: Set recipients and sender. +- **Subject**: Set the email subject. +- **Body**: Set the plain text body. +- **Html**: Set the HTML body (if supported). +- **Attachments**: Add file attachments. + +See the [API reference](https://docs.rs/cot/0.5/cot/email/struct.EmailMessageBuilder.html) +for more details. diff --git a/docs/static-files.md b/docs/static-files.md new file mode 100644 index 00000000..fa40fab5 --- /dev/null +++ b/docs/static-files.md @@ -0,0 +1,98 @@ +--- +title: Static files +--- + +Cot provides a straightforward system for serving static files - resources that don't require server-side processing, such as images, CSS, and JavaScript files. + +## Configuration and Usage + +### Directory Structure + +The Cot CLI generates a `static` directory in your project root, which serves as the designated location for all static files. + +### Registering Static Files + +To serve static files, you'll need to register them in your application's `static_files()` method within the `CotApp` implementation. Here's a basic example: + +```rust +impl App for MyApp { + fn static_files(&self) -> Vec { + static_files!("css/main.css") + } +} +``` + +To add more files, simply include them in the `static_files!` macro. For example, after adding a logo to your project: + +```rust +impl App for MyApp { + fn static_files(&self) -> Vec { + static_files!( + "css/main.css", + "images/logo.png", + ) + } +} +``` + +You can get the URL for a static file using the `StaticFiles` extractor. For example, to get the URL for the logo: + +```rust +use cot::request::extractors::StaticFiles; + +async fn get_logo_url(static_files: StaticFiles) -> cot::Result { + Ok(static_files.url_for("images/logo.png")?.to_string()) +} +``` + +By default, static files are available at the `/static/` URL prefix. You can configure this in the project config file: + +```toml +[static_files] +url = "/assets/" +``` + +## Caching and versioning + +If you used the default project template to create your project, Cot will automatically add a hash as a query parameter in the static files URLs. This allows you to use aggressive caching strategies in production without worrying about cache invalidation. This means that, for instance, for `images/logo.png`, the URL will look like `/static/images/logo.png?v=e3b0c44298fc`, where `e3b0c44298fc` is a hash of the file contents. This way, if the file changes, the URL will change too, and the browser will fetch the new version. + +Thanks to the file hashing, you can use aggressive caching strategies in production without worrying about cache invalidation, while ensuring that users won't have to download the same asset twice. This is the default behavior in Cot, but you configure it in the project config file: + +```toml +[static_files] +rewrite = "query_param" # set to "none" to disable hashing +cache_timeout = "1year" # "Cache-Control" header value +``` + +Please refer to [humantime crate documentation](https://docs.rs/humantime/latest/humantime/fn.parse_duration.html) on the details about the `cache_timeout` configuration format. + +## Production Deployment + +### Collecting Static Files + +If you want to serve static files through a reverse proxy or CDN, you can use the `collect-static` command to gather all static files into a single directory. This is particularly useful for production environments where you want to serve static files efficiently. + +```bash +cargo run -- collect-static public/ +``` + +This command aggregates all static files into the specified directory (in this case, `public/`), making them ready for serving through your chosen infrastructure. + +### Disabling Static File Serving + +If you prefer not to serve static files through the Cot server, you can disable this functionality by removing the `StaticFilesMiddleware` from your project configuration: + +```rust +fn middlewares( + &self, + handler: RootHandlerBuilder, + context: &MiddlewareContext, +) -> RootHandler { + handler + .middleware(StaticFilesMiddleware::from_context(context)) + // ... + .build() +} +``` + +Simply remove the `.middleware(StaticFilesMiddleware ...)` line to disable static file serving. diff --git a/docs/templates.md b/docs/templates.md new file mode 100644 index 00000000..c3d258fd --- /dev/null +++ b/docs/templates.md @@ -0,0 +1,354 @@ +--- +title: Templates +--- + +Cot does not require you to use any specific templating engine. However, it provides a convenient integration with a powerful engine called [Askama](https://askama.readthedocs.io/). Askama is very similar to Jinja2, which itself was inspired by Django's template engine. It allows you to build complex templates easily while providing type safety to help catch errors at compile time. + +## Basic Syntax + +An Askama template is simply a text file that includes both static text and dynamic content. The dynamic content is introduced using variables, tags, and filters. Below is a simple Askama template: + +```html.j2 +
    + {% for item in items %} +
  • {{ item.title|capitalize }}
  • + {% endfor %} +
+``` + +We can identify the following core syntax elements: + +- **`{% ... %}` (tags)**: Used to control template logic, such as loops and conditionals. In the example above, `for item in items` iterates over a collection named `items`. +- **`{{ ... }}` (variables)**: Used to output dynamic data into the template. +- **`|capitalize` (filters)**: Modify the output of variables (e.g., `capitalize` converts the first character to uppercase). You can chain multiple filters if needed. + +An example of the rendered output (ignoring whitespace) might be: + +```html +
    +
  • First item
  • +
  • Second item
  • +
  • Third item
  • +
+``` + +To make variables like `items` available in the template, you need to define them in your Rust code and pass them into the template. + +### Example + +Here is a simple demonstration of templating with Askama in Cot: + +```rust +use cot::request::Request; +use cot::response::{Response, ResponseExt}; +use cot::{Body, StatusCode}; +use cot::Template; + +struct Item { + title: String, +} + +#[derive(Template)] +#[template(path = "index.html")] +struct IndexTemplate { + items: Vec, +} + +async fn index() -> cot::Result { + let items = vec![ + Item { title: "first item".to_string() }, + Item { title: "second item".to_string() }, + Item { title: "third item".to_string() }, + ]; + + let context = IndexTemplate { items }; + let rendered = context.render()?; + + Ok(Html::new(rendered)) +} +``` + +## Template Inheritance + +A common approach when using templates is to employ *template inheritance*. This technique lets you define a base template for shared structure and layout, and then create child templates that only override the pieces that need to differ. Askama supports this via two main concepts: + +- **`{% extends %}`**: Tells Askama which template the current file extends (the "parent" template). This tag must appear first in the file. +- **`{% block %}`**: Defines a named section in the parent template that child templates can override. By default, the block includes whatever is in the parent, but child templates may completely replace it. + +### Example + +`base.html`: + +```html.j2 + + + + + {% block title %}My Site{% endblock %} + + +
+

{% block header %}My Site{% endblock %}

+
+
+ {% block content %}{% endblock %} +
+ + +``` + +`index.html`: + +```html.j2 +{% extends "base.html" %} + +{% block title %}Home{% endblock %} +{% block header %}Welcome to my site!{% endblock %} + +{% block content %} +

This is the content of the home page.

+{% endblock %} +``` + +When you render `index.html`, it uses the overall structure from `base.html` but replaces the `title`, `header`, and `content` blocks with its own content. + +## Including Templates + +Beyond inheritance, Askama also supports *including* other templates. This is useful for reusing small, self-contained pieces of content across multiple pages. You can include a template with the `{% include %}` tag. + +### Defining Variables + +Any template included via `{% include %}` has access to the parent template's variables. Additionally, you can define new variables with the `{% let %}` tag. Askama's variables behave like Rust variables: they are immutable by default, but you can shadow an existing variable with the same name. + +### Example + +`hello.html`: + +```html.j2 +

Hello, {{ name }}!

+``` + +`index.html`: + +```html.j2 +{% let name = "Alice" %} +{% include "hello.html" %} +{% let name = "Bob" %} +{% include "hello.html" %} +``` + +Rendered output: + +```html +

Hello, Alice!

+

Hello, Bob!

+``` + +## URLs + +Linking to other pages in your application is a frequent requirement, and hardcoding URLs in templates can become a maintenance hassle. To address this, Cot provides the `cot::reverse!()` macro. This macro generates URLs based on your route definitions, validating that you’ve passed any required parameters and that the route actually exists. If you ever change your URL structure, you'll only need to update the route definitions. + +`cot::reverse!()` expects a reference to the `Urls` object (which you can obtain by extracting it from the request), the route name, and any parameters needed by that route. It returns a `Result`, so should be suffixed by "?" when used in the html template. + +### Example + +`index.html`: + +```html.j2 +{% let urls = urls %} +Home +User 42 +``` + +`main.rs`: + +```rust +use cot::request::Request; +use cot::response::{Response, ResponseExt}; +use cot::router::{Router, Route, Urls}; +use cot::{Body, StatusCode}; +use cot::Template; + +#[derive(Template)] +#[template(path = "index.html")] +struct IndexTemplate<'a> { + urls: &'a Urls, +} + +async fn index(urls: Urls) -> cot::Result { + let template = IndexTemplate { urls: &urls }; + + Ok(Html::new(template.render()?)) +} + +async fn user() -> cot::Result { + todo!() +} + +struct CotTestApp; + +impl cot::App for CotTestApp { + fn name(&self) -> &'static str { + env!("CARGO_CRATE_NAME") + } + + fn router(&self) -> Router { + Router::with_urls([ + Route::with_handler_and_name("/", index, "index"), + Route::with_handler_and_name("/user/{id}", user, "user"), + ]) + } +} +``` + +## Control Flow and Logic + +Askama offers several tags that let you control how the template renders and apply logic. Here are the most commonly used ones: + +### If + +Use the `{% if %}` tag to conditionally render parts of the template based on a certain condition. For more complex scenarios, you can include `{% elif %}` or an `{% else %}` section. + +#### Example + +```html.j2 +{% if user.is_admin %} + Welcome, admin! +{% elif user.is_logged_in %} + Welcome, user! +{% else %} + Please log in to continue. +{% endif %} +``` + +### Match + +The `{% match %}` tag matches a value against a set of Rust patterns. Use `{% when %}` to specify each pattern and its corresponding content. + +#### Example + +```html.j2 +{% match user.role %} + {% when Some with ("admin") %} + Welcome, admin! + {% when Some %} + Welcome, user! + {% when None %} +{% endmatch %} +``` + +### For + +The `{% for %}` tag allows you to iterate over a sequence of items. Inside the loop, Askama provides helpful variables such as: + +- `loop.index`: Current iteration (1-indexed). +- `loop.index0`: Current iteration (0-indexed). +- `loop.first`: `true` on the first iteration. +- `loop.last`: `true` on the last iteration. + +#### Example + +```html.j2 +
    + {% for item in items %} +
  • {{ loop.index }}. {{ item }}
  • + {% endfor %} +
+``` + +## Whitespace Control + +By default, Askama preserves all whitespace, which can sometimes cause unwanted gaps in your output when using loops or conditionals. To manage this, you can use the `-` modifier before or after a tag to trim surrounding whitespace. + +### Example + +```html.j2 +
    + {%- for item in items -%} +
  • {{ item }}
  • + {%- endfor -%} +
+``` + +This usage of `-` ensures that no extra whitespace or blank lines appear around the `
  • ` tags. + +## Comments + +You can include comments in your Askama templates using `{# ... #}`. These comments are ignored in the rendered output and can be used to document logic or temporarily disable parts of your template. They also support whitespace control via `-`. + +### Example + +```html.j2 +{# This is a comment #} +This will be rendered. +{#- +This is a multi-line comment +which won’t appear in the output. +-#} +``` + +## Custom Renderable Types + +To display custom types in Askama templates, the type must implement `Display`. This makes the type's `to_string()` output available in the template. + +### Example + +`main.rs`: + +```rust +use std::fmt::Display; +use cot::Template; + +struct Item { + title: String, +} + +impl Display for Item { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.title) + } +} + +#[derive(Template)] +#[template(path = "index.html")] +struct IndexTemplate { + item: Item, +} +``` + +`index.html`: + +```html.j2 +{{ item }} +``` + +When rendered, it will display the `title` from the `Item` struct. + +### HTML Escaping + +By default, Askama escapes all output to protect against XSS attacks. Special characters are replaced with their HTML entities. If you’re certain your data is safe and want to bypass escaping, you can implement the `HtmlSafe` marker trait. + +```rust +use askama::filters::HtmlSafe; + +impl HtmlSafe for Item {} +``` + +Be very cautious when marking output as safe; you are responsible for ensuring that the content doesn’t introduce security risks. + +To simplify generating safe HTML in Rust, Cot provides the [`HtmlTag`](https://docs.rs/cot/0.5/cot/html/struct.HtmlTag.html) type. It automatically applies escaping where necessary. + +```rust +impl Display for Item { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let mut tag = HtmlTag::input("text"); + tag.attr("value", &self.title); // The title will be safely escaped here + + write!(f, "{}", tag.render().as_str()) + } +} +``` + +## Read More + +This chapter only covers the basics of Askama. For more detailed information, advanced usage, and additional examples, check out the [Askama documentation](https://askama.readthedocs.io/). diff --git a/docs/testing.md b/docs/testing.md new file mode 100644 index 00000000..79fa21e1 --- /dev/null +++ b/docs/testing.md @@ -0,0 +1,242 @@ +--- +title: Testing +--- + +Cot includes various built-in utilities to help you test your application. This guide will cover some of the most important ones. + +## Why Test at All? + +Testing is a critical part of any application development process. By writing and running tests, you can: +1. **Ensure Code Reliability** – Tests catch bugs and regressions before they reach production, increasing overall stability and confidence in your application. +2. **Document Your Code** – Tests serve as living documentation. They show how different parts of your application are supposed to work and can act as examples for future maintainers. +3. **Facilitate Refactoring** – With a robust test suite, you can safely modify or refactor your code. If something breaks, your tests will let you know right away. +4. **Encourage Good Design** – When code is easier to test, it often means it's well-structured and follows good design principles. + +By employing Cot's testing utilities, you'll be able to verify that each piece of your application—from individual request handlers to full end-to-end processes—works correctly. + +--- + +## General Overview + +Cot provides several built-in utilities located in the [`cot::test` module](https://docs.rs/cot/0.5/cot/test/index.html) to help you create and run tests for your application. + +Typical Rust projects keep their tests in: +- A dedicated `tests/` directory (for integration tests). +- A `mod tests` section in your source files (for unit tests). + +You can run all your tests by executing: +```bash +cargo test +``` + +--- + +## Unit Testing + +Unit tests focus on testing small, isolated pieces of your application, such as individual functions, request handlers, or utility methods. Cot's `TestRequestBuilder` utility helps you create HTTP request objects in a lightweight way, without spinning up a full HTTP server. + +### Test Request Builder + +The `TestRequestBuilder` offers a fluent API for constructing HTTP requests that can be dispatched to your request handlers directly: + +```rust +// Create a GET request +let request = TestRequestBuilder::get("/").build(); + +// Create a POST request +let request = TestRequestBuilder::post("/").build(); + +// Add configuration and features +let request = TestRequestBuilder::get("/") + .with_default_config() // Add default configuration + .with_session() // Add session support + .build(); + +// Add form data +let request = TestRequestBuilder::post("/") + .form_data(&[("key", "value")]) + .build(); + +// Add JSON data +let request = TestRequestBuilder::post("/") + .json(&your_data) + .build(); +``` + +#### When to Use `TestRequestBuilder` + +- **Handler Testing**: Verify that individual handlers behave correctly given different inputs (e.g., form data, JSON bodies). +- **Config-dependent Testing**: Make sure your handlers behave as expected when certain configurations or features (like sessions) are enabled. + +--- + +## Integration Testing + +Integration tests check how multiple parts of your application work together. Cot provides a `Client` struct to help you simulate end-to-end HTTP interactions with a fully running instance of your application. + +### Test Client + +The `Client` struct lets you create a temporary instance of your Cot application and perform HTTP requests against it: + +```rust +let project = CotProject::builder() + .register_app_with_views(MyApp, "/app") + .build().await?; + +// Create a new test client +let mut client = Client::new(project); + +// Make GET requests +let response = client.get("/").await?; + +// Make custom requests +let request = http::Request::get("/").body(Body::empty())?; +let response = client.request(request).await?; +``` + +#### When to Use `Client` + +- **Full Application Testing**: Confirm that routes, middlewares, and database integrations all work as intended. +- **Multi-Request Sequences**: Test flows that require multiple requests, like login/logout or multi-step forms. + +--- + +## Test Database + +Cot's testing utilities also include the `TestDatabase` struct, which helps you create temporary databases for your tests. This allows you to test how your application interacts with data storage without polluting your real database. + +```rust +// Create SQLite test database (in-memory) +let mut test_db = TestDatabase::new_sqlite().await?; + +// Create PostgreSQL test database +let mut test_db = TestDatabase::new_postgres("test_name").await?; + +// Create MySQL test database +let mut test_db = TestDatabase::new_mysql("test_name").await?; + +// Use the test database in requests +let request = TestRequestBuilder::get("/") + .database(test_db.database()) + .build(); + +// Add authentication support +test_db.with_auth().run_migrations().await; + +// Clean up after testing +test_db.cleanup().await?; +``` + +### Best Practices + +1. **Always Clean Up Test Databases** + ```rust + let test_db = TestDatabase::new_sqlite().await?; + // ... run your tests ... + test_db.cleanup().await?; + ``` + Cleaning up helps ensure that each test runs in isolation and that temporary databases don't accumulate. Note that if a test panics, the database will **not** be cleaned up, which might be useful for debugging. On the next test run, the database will be removed automatically. + +2. **Use Unique Test Names for PostgreSQL/MySQL** + ```rust + let test_db = TestDatabase::new_postgres("unique_test_name").await?; + ``` + This prevents naming collisions when running multiple tests or suites simultaneously. + +3. **Add Migrations and Auth Support If Required** + ```rust + let mut test_db = TestDatabase::new_sqlite().await?; + test_db.with_auth().run_migrations().await; + let request = TestRequestBuilder::get("/") + .with_db_auth(test_db.database()) + .build(); + ``` + This ensures that your tests have all necessary schema and authentication information set up. + +### Environment Variables + +- `POSTGRES_URL` \ + The connection URL for PostgreSQL (default: `postgresql://cot:cot@localhost`). + +- `MYSQL_URL` \ + The connection URL for MySQL (default: `mysql://root:@localhost`). + +### Important Notes + +- PostgreSQL and MySQL test databases are created with the prefix `test_cot__`. +- The SQLite database is in-memory by default. +- Form data is currently only supported with POST requests. +- Custom migrations can be added using the `add_migrations` method on `TestDatabase`. + +## Test Cache + +Cot's testing utilities also include the `TestCache` struct, which helps you create temporary caches for your tests. This allows you to test how your application interacts with the cache without polluting your real cache. + +```rust +use cot::test::TestCache; + +// Create a memory cache +let test_cache = TestCache::new_memory(); +let cache = test_cache.cache(); + +// Or Redis (requires `redis` feature and a running Redis instance) +let test_cache = TestCache::new_redis().await?; +let cache = test_cache.cache(); + +// Use the cache in requests +let request = TestRequestBuilder::get("/") + .cache(cache) + .build(); + +// Clean up after testing (important for Redis) +test_cache.cleanup().await?; +``` + +The Redis test cache uses a randomized key prefix to ensure isolation between tests. + +## End-to-end testing + +Cot provides an end-to-end testing framework that allows you to test your entire application in a near-production environment. This is particularly useful for testing complex workflows that involve multiple components, such as user authentication, database interactions, external API calls, and your application's UI. By using the end-to-end testing framework you will be able to send real HTTP requests or use web automation tools to simulate user interactions with your application. + +The end-to-end testing framework consists of two parts: the `cot::e2e_test` macro and the `TestServerBuilder` struct. The `cot::e2e_test` macro allows you to define end-to-end tests that allow you to run your project in the background, while the `TestServerBuilder` struct allows you to create a test server that you can send your requests to. An example of how to use the `cot::e2e_test` macro and the `TestServerBuilder` struct is shown below in a simple test that checks if the server is running and returns the `Hello world!` response: + +```rust +struct TestProject; +impl cot::Project for TestProject { + // ... +} + +#[cot::e2e_test] +async fn test_e2e_server() -> Result<(), Box> { + let server = TestServerBuilder::new(TestProject).start().await; + let url = server.url(); + + // Send HTTP requests to `url` + let body = reqwest::get(url).await?.text().await?; + assert_eq!(body, "Hello world!"); + + server.close().await; + Ok(()) +} +``` + +The test above uses [reqwest](https://docs.rs/reqwest/latest/reqwest/index.html), a popular HTTP client for Rust, to send a GET request to the test server. + +Cot's end-to-end test framework is also particularly useful when using web automation tools that test your web application using an actual web browser. Some popular crates for web automation in Rust include: + +* [fantoccini](https://docs.rs/fantoccini/latest/fantoccini/) +* [thirtyfour](https://docs.rs/thirtyfour/latest/thirtyfour/) + +Please refer to the documentation of these crates for more information on how to use them. + +--- + +## Summary + +Cot's testing framework provides a robust and flexible approach to ensuring the quality of your application. + +- **Unit tests** with `TestRequestBuilder` help you verify that individual components behave as expected. +- **Integration tests** with `Client` let you test your entire application in a near-production environment, while `TestDatabase` and `TestCache` give you confidence that your data and caching layers are functioning correctly. +- **End-to-end tests** with `TestServerBuilder` allow you to verify your full application workflows in a real-world scenario. + +By integrating these testing tools into your workflow, you can deploy your Cot applications with greater confidence. Happy testing! diff --git a/docs/upgrade-guide.md b/docs/upgrade-guide.md new file mode 100644 index 00000000..eb6cb33b --- /dev/null +++ b/docs/upgrade-guide.md @@ -0,0 +1,53 @@ +--- +title: Upgrade Guide +--- + +Each version of Cot introduces new features, improvements, and sometimes breaking changes. This guide will help you understand the changes made in each version and how to adapt your code accordingly. + +As a general rule, try to upgrade one minor version at a time. Many breaking changes are introduced by first deprecating a feature in one minor version and then removing it in the next. This gives you time to adapt your code before the feature is removed, while the Rust compiler will notify you about the exact changes you need to make. + +Sometimes, though, the changes need to be made in a backwards-incompatible manner. This page will help you understand those changes and how to adapt your code. + +## From 0.4 to 0.5 + +### General + +* **MSRV Bump**: The Minimum Supported Rust Version (MSRV) has been bumped to 1.88. +* **Templates**: `cot` now re-exports `Template` trait and `#[derive(Template)]` macro. You should update your imports from `use askama::Template;` to `use cot::Template;`. This change allows you to remove `askama` from your `Cargo.toml` dependencies. +* **Database**: `Database` struct now uses `Arc` internally. If you were wrapping `Database` in `Arc` (e.g. `Arc`), you should remove the `Arc` wrapper as `Database` is now cheap to clone. + +### Forms + +* **Attribute Rename**: The `opt` attribute parameter in `#[form(...)]` macro has been renamed to `opts`. + ```rust + // Before + #[form(opt(max_length = 100))] + + // After + #[form(opts(max_length = 100))] + ``` + +### Configuration + +* **Cache Support**: Cot now includes a built-in caching system. This brings a new `[cache]` section in the configuration. If you have any existing configuration that conflicts with this, you might need to adjust it. +* **Email Support**: Similar to caching, email support has been added with a new `[email]` configuration section. + +## From 0.3 to 0.4 + +### General + +* `FromRequestParts` is called `FromRequestHead` now. Similarly, `FromRequestParts::from_request_parts` is now `FromRequestHead::from_request_head`. +* `axum::request::Parts` is now re-exported as `cot::request::RequestHead`. +* `axum::response::Parts` is now re-exported as `cot::response::ResponseHead`. + +### Error handling + +* "Not Found" handler support has been removed. Instead, there is a single project-global error handler that handles both "Not Found", "Internal Server Error", and other errors that may occur during request processing. +* The error handler is now almost a regular request handler (meaning you don't have to implement the `ErrorHandler` trait manually) and can access most of the request data, such as request path, method, headers, but also static files, root router URLs, and more. + - The main difference between a regular request handler and an error handler is that the error handler may receive an additional argument of type `RequestError`, which contains information about the error that occurred during request processing. + - On the other hand, it can **not** receive the request body, as it might have been consumed already. +* `Project::server_error_handler` method is now called `error_handler` and returns a `DynErrorPageHandler`. + +### Dependencies + +* `schemars` dependency has been updated to `0.9`. If you have any custom code to generate OpenAPI specs, (usually by implementing `AsApiOperation`, `ApiOperationPart`, or `AsApiOperation` traits inside `cot::openapi`) you may need to update it accordingly. If you're only using Cot's built-in OpenAPI support, you don't need to do anything except updating your `Cargo.toml` file.