From 470814c8218f641891674af2976e2aebfd596eea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Ma=C4=87kowski?= Date: Mon, 17 Feb 2025 23:36:37 +0100 Subject: [PATCH 01/20] feat: finish the rest of the guide chapters, add FAQ and Licenses pages --- admin-panel.md | 109 +++++++++++++++ db-models.md | 170 +++++++++++++++++++++++ error-pages.md | 174 +++++++++++++++++++++++ forms.md | 177 ++++++++++++++++++++++++ introduction.md | 248 +++++++++++++++++++++++++++++++++ static-files.md | 73 ++++++++++ templates.md | 359 ++++++++++++++++++++++++++++++++++++++++++++++++ testing.md | 182 ++++++++++++++++++++++++ 8 files changed, 1492 insertions(+) create mode 100644 admin-panel.md create mode 100644 db-models.md create mode 100644 error-pages.md create mode 100644 forms.md create mode 100644 introduction.md create mode 100644 static-files.md create mode 100644 templates.md create mode 100644 testing.md diff --git a/admin-panel.md b/admin-panel.md new file mode 100644 index 00000000..5127623f --- /dev/null +++ b/admin-panel.md @@ -0,0 +1,109 @@ +--- +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, LiveReloadMiddleware}; +use cot::project::{WithApps, WithConfig}; +use cot::static_files::StaticFilesMiddleware; + +struct MyProject; + +impl Project for MyProject { + fn register_apps(&self, apps: &mut AppBuilder, _context: &ProjectContext) { + 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: cot::project::RootHandlerBuilder, + app_context: &ProjectContext, + ) -> BoxedHandler { + handler + .middleware(StaticFilesMiddleware::from_app_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 cot::auth::db::{DatabaseUser, DatabaseUserCredentials}; +use cot::auth::Password; + +// 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 user = DatabaseUser::get_by_username(context.database(), "admin").await?; + if user.is_none() { + // Create admin user + DatabaseUser::create_user( + context.database(), + "admin", // username + &Password::new("admin") // 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; + +#[model] +#[derive(Debug, Form, AdminModel)] +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, you primary key needs to be implementing the `FromStr` and `Display` traits, and your model needs to implement the `Display` trait. + +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/db-models.md b/db-models.md new file mode 100644 index 00000000..b0d513c5 --- /dev/null +++ b/db-models.md @@ -0,0 +1,170 @@ +--- +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 make-migrations +``` + +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 +async fn create_link(request: Request) -> cot::Result { + let mut link = Link { + id: Auto::default(), + slug: LimitedString::new("slug").unwrap(), + url: "https://example.com".to_string(), + }; + link.save(request.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(request.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(request.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(request.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(request.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(request.db()).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(request.db()) + .await? + .expect("Link not found"); + +let user = link.user.get(request.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've 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/error-pages.md b/error-pages.md new file mode 100644 index 00000000..36b6c9fe --- /dev/null +++ b/error-pages.md @@ -0,0 +1,174 @@ +--- +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. The two types of error pages that can be customized are: + +* 404 Not Found +* 500 Internal Server Error + +## Custom error handlers + +Let's implement custom error handlers in your project: + +```rust +use cot::project::{ErrorPageHandler, Project}; +use cot::response::{Response, ResponseExt}; +use cot::{Body, StatusCode}; + +struct CustomNotFound; +impl ErrorPageHandler for CustomNotFound { + fn handle(&self) -> cot::Result { + Ok(Response::new_html( + StatusCode::NOT_FOUND, + Body::fixed(include_str!("404.html")), + )) + } +} + +struct CustomServerError; +impl ErrorPageHandler for CustomServerError { + fn handle(&self) -> cot::Result { + Ok(Response::new_html( + StatusCode::INTERNAL_SERVER_ERROR, + Body::fixed(include_str!("500.html")), + )) + } +} + +struct MyProject; + +impl Project for MyProject { + fn not_found_handler(&self) -> Box { + Box::new(CustomNotFound) + } + + fn server_error_handler(&self) -> Box { + Box::new(CustomServerError) + } +} +``` + +Create `404.html`: + +```html + + + + + 404 Not Found + + +

404

+

Page Not Found

+

Oopsies! The page you're looking for doesn't exist.

+

Return to Homepage

+ + +``` + +Create `500.html`: + +```html + + + + + 500 Server Error + + +

500

+

Server Error

+

Oopsies! Something went wrong on our end. Please try again later.

+

Return to Homepage

+ + +``` + +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 +async fn view(request: Request) -> cot::Result { + // 404 Not Found + return Err(cot::Error::not_found()); + + // 404 with custom message + return Err(cot::Error::not_found_message( + "The article you're looking for doesn't exist".to_string() + )); + + // Custom error + return Err(cot::Error::custom("Something went wrong")); +} +``` + +Note that any messages that you pass to the `Error` structure will only be displayed in debug mode. In production, the user will see your custom error pages that do not have access to the error message. + +## Handling specific errors + +You can handle specific errors in your views: + +```rust +async fn view_article(request: Request) -> cot::Result { + // will display a 404 error page if the article ID is not an integer + let article_id: i32 = request + .path_params() + .parse() + .map_err(|_| Error::not_found_message("Invalid article ID".to_string()))?; + + // will display a 404 page if the article is not found in the database + let article = query!(Article, $id == article_id) + .get(request.db()) + .await? + .ok_or_else(|| Error::not_found_message( + format!("Article {} not found", article_id) + ))?; + + if article.name.is_empty() { + // both of these will display a 500 error page: + return Err(Error::custom("Article name should never be empty!")); + // or: + panic!("Article name should never be empty!"); + } + + Ok(Response::new_html( + StatusCode::OK, + Body::fixed(render_article(&article)?), + )) +} +``` + +## 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 be able to handle specific errors. + +Next chapter, we'll explore automatic testing in Cot applications. diff --git a/forms.md b/forms.md new file mode 100644 index 00000000..d0791632 --- /dev/null +++ b/forms.md @@ -0,0 +1,177 @@ +--- +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(opt(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::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(Response::new_html( + StatusCode::OK, + Body::fixed(template.render()?) + )) + } + } + } else { + // Handle GET request (display empty form) + let template = ContactTemplate { + request: &request, + form: ContactForm::build_context(&mut request).await?, + }; + + Ok(Response::new_html( + StatusCode::OK, + Body::fixed(template.render()?) + )) + } +} +``` + +### 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(opt(max_length = 100))] + title: String, + + // Required checkbox + #[form(opt(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(Response::new_html( + StatusCode::OK, + Body::fixed(render_template(context)?) + )); + } + + // Process valid form... + Ok(reverse_redirect!(request, "success")?) + } + FormResult::ValidationError(context) => { + // Handle validation errors... + Ok(Response::new_html( + StatusCode::OK, + Body::fixed(render_template(context)?) + )) + } + } +} +``` + +## Summary + +In this 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 can you use them to persist data in your services. diff --git a/introduction.md b/introduction.md new file mode 100644 index 00000000..cdfeed13 --- /dev/null +++ b/introduction.md @@ -0,0 +1,248 @@ +--- +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/). + +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(request: Request) -> cot::Result { + let index_template = IndexTemplate {}; + let rendered = index_template.render()?; + + Ok(Response::new_html(StatusCode::OK, Body::fixed(rendered))) +} +``` + +Further in the file you can see that this view is registered in the `App` implementation: + +```rust +struct GownoApp; + +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(request: Request) -> cot::Result { + Ok(Response::new_html(StatusCode::OK, Body::fixed("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! + +### 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}`). This parameter will be available in the `Request` object, and you can extract it using the `path_params().parse()` method. It will automatically infer the type of the parameter(s) based on the type of the variable you assign it to. Here's an example: + +```rust +async fn hello_name(request: Request) -> cot::Result { + let name: String = request.path_params().parse()?; + + Ok(Response::new_html(StatusCode::OK, Body::fixed(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"), + ]) +} +``` + +Now, when you visit [`localhost:8000/hello/John`](http://localhost:8000/hello/John), you should see `Hello, John!` 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<(String, Bytes)> { + 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: &ProjectContext) { + apps.register_with_views(CotTutorialApp, ""); + } +``` + +This registers all the apps that your project is using. + +```rust + fn middlewares( + &self, + handler: RootHandlerBuilder, + context: &ProjectContext, + ) -> BoxedHandler { + handler + .middleware(StaticFilesMiddleware::from_app_context(context)) + .middleware(LiveReloadMiddleware::from_app_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/static-files.md b/static-files.md new file mode 100644 index 00000000..a594a2a8 --- /dev/null +++ b/static-files.md @@ -0,0 +1,73 @@ +--- +title: Static files +--- + + + +# 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 CotApp for MyApp { + fn static_files(&self) -> Vec<(String, Bytes)> { + 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 CotApp for MyApp { + fn static_files(&self) -> Vec<(String, Bytes)> { + static_files!( + "css/main.css", + "images/logo.png" + ) + } +} +``` + +All registered files are automatically served under the `/static` path. For instance, in the example above, you can access the logo at `/static/images/logo.png`. + +## Production Deployment + +### Collecting Static Files + +For production environments, it's recommended to serve static files through specialized services rather than the Cot server for performance and security reasons. Options include: +- Reverse proxy servers (e.g., [nginx](https://nginx.org/), [Caddy](https://caddyserver.com/)) +- Content delivery networks (e.g., [Cloudflare](https://www.cloudflare.com/)) + +To facilitate this deployment strategy, Cot provides a `collect-static` CLI command that consolidates static files from all registered apps (including the Cot Admin app) into a single directory: + +```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 +let project = CotProject::builder() + // ... + .middleware_with_context(StaticFilesMiddleware::from_app_context) + .middleware(LiveReloadMiddleware::new()) + .build() + .await?; +``` + +Simply remove the `.middleware_with_context(StaticFilesMiddleware ...)` line to disable static file serving. diff --git a/templates.md b/templates.md new file mode 100644 index 00000000..182f9fb6 --- /dev/null +++ b/templates.md @@ -0,0 +1,359 @@ +--- +title: Templates +--- + + + +Cot does not require you to use any specific templating engine. However, it provides a convenient integration with a powerful engine called [Rinja](https://rinja.readthedocs.io/). Rinja 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 + +A Rinja 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 Rinja 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 Rinja in Cot: + +```rust +use cot::request::Request; +use cot::response::{Response, ResponseExt}; +use cot::{Body, StatusCode}; +use rinja::Template; + +struct Item { + title: String, +} + +#[derive(Template)] +#[template(path = "index.html")] +struct IndexTemplate { + items: Vec, +} + +async fn index(_request: Request) -> 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(Response::new_html(StatusCode::OK, Body::fixed(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. Rinja supports this via two main concepts: + +- **`{% extends %}`**: Tells Rinja 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, Rinja 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. Rinja'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 `Request` object, the route name, and any parameters needed by that route. + +### Example + +`index.html`: + +```html.j2 +{% let request = request %} +Home +User 42 +``` + +`main.rs`: + +```rust +use cot::request::Request; +use cot::response::{Response, ResponseExt}; +use cot::router::{Router, Route}; +use cot::{Body, StatusCode}; +use rinja::Template; + +#[derive(Template)] +#[template(path = "index.html")] +struct IndexTemplate<'a> { + request: &'a Request, +} + +async fn index(request: Request) -> cot::Result { + let template = IndexTemplate { request: &request }; + + Ok(Response::new_html( + StatusCode::OK, + Body::fixed(template.render()?), + )) +} + +async fn user(_request: Request) -> 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 + +Rinja 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, Rinja 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, Rinja 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 Rinja 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 Rinja 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 rinja::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, Rinja 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 rinja::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.1/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 Rinja. For more detailed information, advanced usage, and additional examples, check out the [Rinja documentation](https://rinja.readthedocs.io/). diff --git a/testing.md b/testing.md new file mode 100644 index 00000000..827791ac --- /dev/null +++ b/testing.md @@ -0,0 +1,182 @@ +--- +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.1/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`. + +--- + +## 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` give you confidence that your data layer is functioning correctly, whether you're using SQLite, PostgreSQL, or MySQL. + +By integrating these testing tools into your workflow, you can deploy your Cot applications with greater confidence. Happy testing! From b1b741415141917ad36a48d4cbec165546038e72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Ma=C4=87kowski?= Date: Tue, 18 Feb 2025 09:13:02 +0100 Subject: [PATCH 02/20] feat: add FAQ --- introduction.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/introduction.md b/introduction.md index cdfeed13..f4f37fc9 100644 --- a/introduction.md +++ b/introduction.md @@ -16,7 +16,7 @@ If you are not familiar with Rust, you might want to start by reading the [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/). +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.84 or later. Install the Cot CLI with: From 2832577f8ae472de83de6358634ef4c7331a9492 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Ma=C4=87kowski?= Date: Tue, 18 Feb 2025 16:02:09 +0100 Subject: [PATCH 03/20] chore: fix invalid struct name in introduction --- introduction.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/introduction.md b/introduction.md index f4f37fc9..f5df8f90 100644 --- a/introduction.md +++ b/introduction.md @@ -98,7 +98,7 @@ async fn index(request: Request) -> cot::Result { Further in the file you can see that this view is registered in the `App` implementation: ```rust -struct GownoApp; +struct CotTutorialApp; impl App for CotTutorialApp { // ... From ead9edf6d6b5f2bda0de92eeea872133ef4532ee Mon Sep 17 00:00:00 2001 From: Mandar Vaze Date: Wed, 19 Feb 2025 01:52:15 +0530 Subject: [PATCH 04/20] Secure admin user creation (#8) Read admin username and password from the environment variable --- admin-panel.md | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/admin-panel.md b/admin-panel.md index 5127623f..c3fff860 100644 --- a/admin-panel.md +++ b/admin-panel.md @@ -46,6 +46,7 @@ impl Project for MyProject { 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 std::env; use cot::auth::db::{DatabaseUser, DatabaseUserCredentials}; use cot::auth::Password; @@ -54,13 +55,17 @@ use cot::auth::Password; 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").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("admin") // password + &admin_username, + &Password::new(&password) ).await?; } Ok(()) From de9a689dd61f8fbf28df7c02c22235897842e5fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Ma=C4=87kowski?= Date: Wed, 19 Feb 2025 19:17:36 +0100 Subject: [PATCH 05/20] feat: add a Framework comparison page stub --- framework-comparison.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 framework-comparison.md diff --git a/framework-comparison.md b/framework-comparison.md new file mode 100644 index 00000000..7e5df432 --- /dev/null +++ b/framework-comparison.md @@ -0,0 +1,5 @@ +--- +title: Framework comparison +--- + + From 7acc7649210c5641c09c52a2d896bc93fae87909 Mon Sep 17 00:00:00 2001 From: kecci Date: Thu, 13 Mar 2025 15:45:13 +0700 Subject: [PATCH 06/20] docs: update guide introduction dynamic routes example (missing closing bracket) (#16) Co-authored-by: Kecci Kun --- introduction.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/introduction.md b/introduction.md index f5df8f90..676750fc 100644 --- a/introduction.md +++ b/introduction.md @@ -138,7 +138,7 @@ You can also define dynamic routes by using the `Route::with_handler_and_name` m async fn hello_name(request: Request) -> cot::Result { let name: String = request.path_params().parse()?; - Ok(Response::new_html(StatusCode::OK, Body::fixed(format!("Hello, {}!", name))) + Ok(Response::new_html(StatusCode::OK, Body::fixed(format!("Hello, {}!", name)))) } // inside `impl App`: From e5f420f45408f4f08703c6a698bc37fcbd88e77a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Ma=C4=87kowski?= Date: Tue, 25 Mar 2025 12:03:26 +0100 Subject: [PATCH 07/20] chore: fix the code in admin-panel.md --- admin-panel.md | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/admin-panel.md b/admin-panel.md index c3fff860..e8b60be5 100644 --- a/admin-panel.md +++ b/admin-panel.md @@ -82,8 +82,8 @@ use cot::admin::AdminModel; use cot::db::{model, Auto}; use cot::form::Form; -#[model] #[derive(Debug, Form, AdminModel)] +#[model] struct BlogPost { #[model(primary_key)] id: Auto, @@ -93,7 +93,15 @@ struct BlogPost { } ``` -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, you primary key needs to be implementing the `FromStr` and `Display` traits, and your model needs to implement the `Display` trait. +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: From e2feabde8390988632536eb28156000f27a957d1 Mon Sep 17 00:00:00 2001 From: Marek Grzelak Date: Wed, 26 Mar 2025 06:03:06 +0100 Subject: [PATCH 08/20] feat: setup guide versioning (#17) * feat: setup guide version handling * feat: make version switcher work * feat: automatically grab all versions * feat: handle too high versions * feat: show old version alert * chore: remove repeated disclaimer * feat: make content dependent on version * fix: review comments --- admin-panel.md | 2 -- db-models.md | 2 -- error-pages.md | 2 -- forms.md | 2 -- static-files.md | 4 ---- templates.md | 2 -- testing.md | 2 -- 7 files changed, 16 deletions(-) diff --git a/admin-panel.md b/admin-panel.md index e8b60be5..0b2a4ceb 100644 --- a/admin-panel.md +++ b/admin-panel.md @@ -2,8 +2,6 @@ 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 diff --git a/db-models.md b/db-models.md index b0d513c5..4c8c8cba 100644 --- a/db-models.md +++ b/db-models.md @@ -2,8 +2,6 @@ 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 diff --git a/error-pages.md b/error-pages.md index 36b6c9fe..5756e295 100644 --- a/error-pages.md +++ b/error-pages.md @@ -2,8 +2,6 @@ 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 diff --git a/forms.md b/forms.md index d0791632..d91cb105 100644 --- a/forms.md +++ b/forms.md @@ -2,8 +2,6 @@ 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 diff --git a/static-files.md b/static-files.md index a594a2a8..6666df09 100644 --- a/static-files.md +++ b/static-files.md @@ -2,10 +2,6 @@ title: Static files --- - - -# 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 diff --git a/templates.md b/templates.md index 182f9fb6..61996530 100644 --- a/templates.md +++ b/templates.md @@ -2,8 +2,6 @@ title: Templates --- - - Cot does not require you to use any specific templating engine. However, it provides a convenient integration with a powerful engine called [Rinja](https://rinja.readthedocs.io/). Rinja 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 diff --git a/testing.md b/testing.md index 827791ac..6860ae45 100644 --- a/testing.md +++ b/testing.md @@ -2,8 +2,6 @@ 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? From c1f42521768b8c536f7912834ad2590eba426022 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Ma=C4=87kowski?= Date: Wed, 26 Mar 2025 06:58:10 +0100 Subject: [PATCH 09/20] feat: add v0.2 --- db-models.md | 20 +++++++++++--------- error-pages.md | 11 +++++------ introduction.md | 33 ++++++++++++++++++++++++++------- templates.md | 19 ++++++++++--------- 4 files changed, 52 insertions(+), 31 deletions(-) diff --git a/db-models.md b/db-models.md index 4c8c8cba..e5ded276 100644 --- a/db-models.md +++ b/db-models.md @@ -42,13 +42,15 @@ This will create a new file in your `migrations` directory in the crate's src di 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 -async fn create_link(request: Request) -> cot::Result { +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(request.db()).await?; + link.save(db).await?; // ... } @@ -60,7 +62,7 @@ Updating a model is similar to saving a new one, but you need to have an existin ```rust link.url = "https://example.org".to_string(); -link.save(request.db()).await?; +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. @@ -69,7 +71,7 @@ If you specifically want to update a row in the database for given primary key, ```rust link.url = "https://example.org".to_string(); -link.update(request.db()).await?; +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: @@ -80,7 +82,7 @@ let mut link = Link { slug: LimitedString::new("slug").unwrap(), url: "https://example.com".to_string(), }; -link.insert(request.db()).await?; +link.insert(db).await?; ``` ### Retrieving models @@ -93,7 +95,7 @@ The easiest way to work with the `Query` structure is the `query!` macro, which use cot::db::query; let link = query!(Link, $slug == LimitedString::new("cot").unwrap()) - .get(request.db()) + .get(db) .await?; ``` @@ -104,7 +106,7 @@ As you can see, the `query!` macro takes the model type as the first argument, f 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(request.db()).await?; +query!(Link, $slug == LimitedString::new("cot").unwrap()).delete(db).await?; ``` ## Foreign keys @@ -138,11 +140,11 @@ When you retrieve a model that has a foreign key relationship, Cot will not auto ```rust let mut link = query!(Link, $slug == LimitedString::new("cot").unwrap()) - .get(request.db()) + .get(db) .await? .expect("Link not found"); -let user = link.user.get(request.db()).await?; +let user = link.user.get(db).await?; ``` ## Database Configuration diff --git a/error-pages.md b/error-pages.md index 5756e295..77a6d9a7 100644 --- a/error-pages.md +++ b/error-pages.md @@ -136,12 +136,11 @@ Note that any messages that you pass to the `Error` structure will only be displ You can handle specific errors in your views: ```rust -async fn view_article(request: Request) -> cot::Result { - // will display a 404 error page if the article ID is not an integer - let article_id: i32 = request - .path_params() - .parse() - .map_err(|_| Error::not_found_message("Invalid article ID".to_string()))?; +async fn view_article(RequestDb(db): RequestDb, Path(article_id): Path) -> cot::Result { + // will display a 404 error page if the article ID is below 0 + if article_id < 0 { + return Error::not_found_message("Invalid article ID".to_string()); + } // will display a 404 page if the article is not found in the database let article = query!(Article, $id == article_id) diff --git a/introduction.md b/introduction.md index 676750fc..01e18999 100644 --- a/introduction.md +++ b/introduction.md @@ -87,7 +87,7 @@ At the heart of any web application is the ability to handle requests and return 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(request: Request) -> cot::Result { +async fn index() -> cot::Result { let index_template = IndexTemplate {}; let rendered = index_template.render()?; @@ -114,7 +114,7 @@ This is how you specify the URL the view will be available at – in this case, 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(request: Request) -> cot::Result { +async fn hello() -> cot::Result { Ok(Response::new_html(StatusCode::OK, Body::fixed("Hello World!"))) } @@ -130,14 +130,16 @@ fn router(&self) -> Router { Now, when you visit [`localhost:8000/hello`](http://localhost:8000/hello) you should see `Hello World!` displayed on the page! -### Dynamic routes +### 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}`). This parameter will be available in the `Request` object, and you can extract it using the `path_params().parse()` method. It will automatically infer the type of the parameter(s) based on the type of the variable you assign it to. Here's an example: +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 -async fn hello_name(request: Request) -> cot::Result { - let name: String = request.path_params().parse()?; +use cot::request::extractors::Path; +async fn hello_name(Path(name): Path) -> cot::Result { Ok(Response::new_html(StatusCode::OK, Body::fixed(format!("Hello, {}!", name)))) } @@ -152,7 +154,24 @@ fn router(&self) -> Router { } ``` -Now, when you visit [`localhost:8000/hello/John`](http://localhost:8000/hello/John), you should see `Hello, John!` displayed on the page! +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(Response::new_html(StatusCode::OK, Body::fixed(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 diff --git a/templates.md b/templates.md index 61996530..93ef73ac 100644 --- a/templates.md +++ b/templates.md @@ -54,7 +54,7 @@ struct IndexTemplate { items: Vec, } -async fn index(_request: Request) -> cot::Result { +async fn index() -> cot::Result { let items = vec![ Item { title: "first item".to_string() }, Item { title: "second item".to_string() }, @@ -148,22 +148,23 @@ Rendered output: 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 `Request` object, the route name, and any parameters needed by that route. +`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. ### Example `index.html`: ```html.j2 -{% let request = request %} -Home -User 42 +{% let urls = urls %} +Home +User 42 ``` `main.rs`: ```rust use cot::request::Request; +use cot::request::extractors::Urls; use cot::response::{Response, ResponseExt}; use cot::router::{Router, Route}; use cot::{Body, StatusCode}; @@ -172,11 +173,11 @@ use rinja::Template; #[derive(Template)] #[template(path = "index.html")] struct IndexTemplate<'a> { - request: &'a Request, + urls: &'a Urls, } -async fn index(request: Request) -> cot::Result { - let template = IndexTemplate { request: &request }; +async fn index(urls: Urls) -> cot::Result { + let template = IndexTemplate { urls: &urls }; Ok(Response::new_html( StatusCode::OK, @@ -184,7 +185,7 @@ async fn index(request: Request) -> cot::Result { )) } -async fn user(_request: Request) -> cot::Result { +async fn user() -> cot::Result { todo!() } From 13bb90cde14872238d791cb71529b4fa06b38f2f Mon Sep 17 00:00:00 2001 From: Marek 'seqre' Grzelak Date: Tue, 4 Mar 2025 20:00:02 +0100 Subject: [PATCH 10/20] docs: update migrations command to match new cli version --- db-models.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/db-models.md b/db-models.md index e5ded276..c6a357eb 100644 --- a/db-models.md +++ b/db-models.md @@ -30,7 +30,7 @@ There's some very useful stuff going on here, so let's break it down: 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 make-migrations +cot migrations 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. From f44482c667fad50f2457f38cd89a898ba5bc8695 Mon Sep 17 00:00:00 2001 From: Julien Rebetez Date: Mon, 31 Mar 2025 09:30:09 +0200 Subject: [PATCH 11/20] docs: Fix Urls import path in Templates guide (#21) Using `use cot::request::extractors::Urls;` yields an error 'struct Urls is private' --- templates.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates.md b/templates.md index 93ef73ac..f6db73d4 100644 --- a/templates.md +++ b/templates.md @@ -164,7 +164,7 @@ Linking to other pages in your application is a frequent requirement, and hardco ```rust use cot::request::Request; -use cot::request::extractors::Urls; +use cot::router::Urls; use cot::response::{Response, ResponseExt}; use cot::router::{Router, Route}; use cot::{Body, StatusCode}; From 6b4a12c1f7a2e64e60bba20b2e08cfee61348cb9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Ma=C4=87kowski?= Date: Mon, 31 Mar 2025 09:46:44 +0200 Subject: [PATCH 12/20] docs: optimize imports --- templates.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/templates.md b/templates.md index f6db73d4..7022bce7 100644 --- a/templates.md +++ b/templates.md @@ -164,9 +164,8 @@ Linking to other pages in your application is a frequent requirement, and hardco ```rust use cot::request::Request; -use cot::router::Urls; use cot::response::{Response, ResponseExt}; -use cot::router::{Router, Route}; +use cot::router::{Router, Route, Urls}; use cot::{Body, StatusCode}; use rinja::Template; From 5d00681d2ce7ae32e5ae2d3f3d28fa7b23181888 Mon Sep 17 00:00:00 2001 From: ClanEver <562211524@qq.com> Date: Mon, 12 May 2025 11:59:52 +0800 Subject: [PATCH 13/20] docs: fix command typo in `db-models.md` (#28) --- db-models.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/db-models.md b/db-models.md index c6a357eb..c8904244 100644 --- a/db-models.md +++ b/db-models.md @@ -30,7 +30,7 @@ There's some very useful stuff going on here, so let's break it down: 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 migrations make +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. From 68b66720f8bdf1655100fd3dfad9774892c4f80c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Ma=C4=87kowski?= Date: Tue, 13 May 2025 10:11:55 +0100 Subject: [PATCH 14/20] feat: add v0.3 docs (#29) * feat: add v0.3 docs * one more fix --- db-models.md | 2 +- forms.md | 3 +- introduction.md | 18 +-- openapi.md | 312 ++++++++++++++++++++++++++++++++++++++++++++++++ static-files.md | 43 +++++-- templates.md | 38 +++--- testing.md | 38 +++++- 7 files changed, 414 insertions(+), 40 deletions(-) create mode 100644 openapi.md diff --git a/db-models.md b/db-models.md index c8904244..c9f260bc 100644 --- a/db-models.md +++ b/db-models.md @@ -44,7 +44,7 @@ In order to write a model instance to the database, you can use the `save` metho ```rust use cot::request::extractors::RequestDb; -async fn create_link(RequestDb(db): RequestDb) -> cot::Result { +async fn create_link(RequestDb(db): RequestDb) -> cot::Result { let mut link = Link { id: Auto::default(), slug: LimitedString::new("slug").unwrap(), diff --git a/forms.md b/forms.md index d91cb105..35257e37 100644 --- a/forms.md +++ b/forms.md @@ -32,7 +32,8 @@ async fn contact(mut request: Request) -> cot::Result { if request.method() == Method::POST { match ContactForm::from_request(&mut request).await? { FormResult::Ok(form) => { - // Form is valid! Process the data + // Form is valid! Process the datause cot::html::Html; + println!("Message from {}: {}", form.name, form.message); // Redirect after successful submission diff --git a/introduction.md b/introduction.md index 01e18999..4392c49e 100644 --- a/introduction.md +++ b/introduction.md @@ -87,11 +87,11 @@ At the heart of any web application is the ability to handle requests and return 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 { +async fn index() -> cot::Result { let index_template = IndexTemplate {}; let rendered = index_template.render()?; - Ok(Response::new_html(StatusCode::OK, Body::fixed(rendered))) + Ok(Html::new(rendered)) } ``` @@ -114,8 +114,8 @@ This is how you specify the URL the view will be available at – in this case, 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() -> cot::Result { - Ok(Response::new_html(StatusCode::OK, Body::fixed("Hello World!"))) +async fn hello() -> Html { + Html::new("Hello World!") } // inside `impl App`: @@ -139,8 +139,8 @@ At the core of Cot's request handling are _extractors_, which allow you to extra ```rust use cot::request::extractors::Path; -async fn hello_name(Path(name): Path) -> cot::Result { - Ok(Response::new_html(StatusCode::OK, Body::fixed(format!("Hello, {}!", name)))) +async fn hello_name(Path(name): Path) -> cot::Result { + Ok(Html::new(format!("Hello, {}!", name))) } // inside `impl App`: @@ -157,8 +157,8 @@ fn router(&self) -> Router { 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(Response::new_html(StatusCode::OK, Body::fixed(format!("Hello, {first_name} {last_name}!")))) +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`: @@ -199,7 +199,7 @@ An app is a collection of views and other components that make up a part of your 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<(String, Bytes)> { + fn static_files(&self) -> Vec { static_files!("css/main.css") } } diff --git a/openapi.md b/openapi.md new file mode 100644 index 00000000..ef55cceb --- /dev/null +++ b/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.8" # 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.3/cot/openapi/trait.ApiOperationPart.html) trait. You can study their implementations to understand how to design your own: + +* [`Json`](https://docs.rs/cot/0.3/cot/json/struct.Json.html) adds a request or response body to the operation +* [`Path`](https://docs.rs/cot/0.3/cot/request/extractors/struct.Path.html) adds path parameters +* [`UrlQuery`](https://docs.rs/cot/0.3/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/static-files.md b/static-files.md index 6666df09..72806da1 100644 --- a/static-files.md +++ b/static-files.md @@ -16,7 +16,7 @@ To serve static files, you'll need to register them in your application's `stati ```rust impl CotApp for MyApp { - fn static_files(&self) -> Vec<(String, Bytes)> { + fn static_files(&self) -> Vec { static_files!("css/main.css") } } @@ -26,26 +26,51 @@ To add more files, simply include them in the `static_files!` macro. For example ```rust impl CotApp for MyApp { - fn static_files(&self) -> Vec<(String, Bytes)> { + fn static_files(&self) -> Vec { static_files!( "css/main.css", - "images/logo.png" + "images/logo.png", ) } } ``` -All registered files are automatically served under the `/static` path. For instance, in the example above, you can access the logo at `/static/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) -> String { + static_files.url_for("images/logo.png") +} +``` + +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 -For production environments, it's recommended to serve static files through specialized services rather than the Cot server for performance and security reasons. Options include: -- Reverse proxy servers (e.g., [nginx](https://nginx.org/), [Caddy](https://caddyserver.com/)) -- Content delivery networks (e.g., [Cloudflare](https://www.cloudflare.com/)) - -To facilitate this deployment strategy, Cot provides a `collect-static` CLI command that consolidates static files from all registered apps (including the Cot Admin app) into a single directory: +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/ diff --git a/templates.md b/templates.md index 7022bce7..04852a49 100644 --- a/templates.md +++ b/templates.md @@ -2,11 +2,11 @@ title: Templates --- -Cot does not require you to use any specific templating engine. However, it provides a convenient integration with a powerful engine called [Rinja](https://rinja.readthedocs.io/). Rinja 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. +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 -A Rinja 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 Rinja template: +A 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
      @@ -36,13 +36,13 @@ To make variables like `items` available in the template, you need to define the ### Example -Here is a simple demonstration of templating with Rinja in Cot: +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 rinja::Template; +use askama::Template; struct Item { title: String, @@ -70,9 +70,9 @@ async fn index() -> cot::Result { ## 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. Rinja supports this via two main concepts: +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 Rinja which template the current file extends (the "parent" template). This tag must appear first in the file. +- **`{% 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 @@ -114,11 +114,11 @@ When you render `index.html`, it uses the overall structure from `base.html` but ## Including Templates -Beyond inheritance, Rinja 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. +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. Rinja's variables behave like Rust variables: they are immutable by default, but you can shadow an existing variable with the same name. +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 @@ -167,7 +167,7 @@ use cot::request::Request; use cot::response::{Response, ResponseExt}; use cot::router::{Router, Route, Urls}; use cot::{Body, StatusCode}; -use rinja::Template; +use askama::Template; #[derive(Template)] #[template(path = "index.html")] @@ -206,7 +206,7 @@ impl cot::App for CotTestApp { ## Control Flow and Logic -Rinja offers several tags that let you control how the template renders and apply logic. Here are the most commonly used ones: +Askama offers several tags that let you control how the template renders and apply logic. Here are the most commonly used ones: ### If @@ -242,7 +242,7 @@ The `{% match %}` tag matches a value against a set of Rust patterns. Use `{% wh ### For -The `{% for %}` tag allows you to iterate over a sequence of items. Inside the loop, Rinja provides helpful variables such as: +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). @@ -261,7 +261,7 @@ The `{% for %}` tag allows you to iterate over a sequence of items. Inside the l ## Whitespace Control -By default, Rinja 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. +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 @@ -277,7 +277,7 @@ This usage of `-` ensures that no extra whitespace or blank lines appear around ## Comments -You can include comments in your Rinja 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 `-`. +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 @@ -292,7 +292,7 @@ which won’t appear in the output. ## Custom Renderable Types -To display custom types in Rinja templates, the type must implement `Display`. This makes the type's `to_string()` output available in the template. +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 @@ -300,7 +300,7 @@ To display custom types in Rinja templates, the type must implement `Display`. T ```rust use std::fmt::Display; -use rinja::Template; +use askama::Template; struct Item { title: String, @@ -329,17 +329,17 @@ When rendered, it will display the `title` from the `Item` struct. ### HTML Escaping -By default, Rinja 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. +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 rinja::filters::HtmlSafe; +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.1/cot/html/struct.HtmlTag.html) type. It automatically applies escaping where necessary. +To simplify generating safe HTML in Rust, Cot provides the [`HtmlTag`](https://docs.rs/cot/0.3/cot/html/struct.HtmlTag.html) type. It automatically applies escaping where necessary. ```rust impl Display for Item { @@ -354,4 +354,4 @@ impl Display for Item { ## Read More -This chapter only covers the basics of Rinja. For more detailed information, advanced usage, and additional examples, check out the [Rinja documentation](https://rinja.readthedocs.io/). +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/testing.md b/testing.md index 6860ae45..161e9e98 100644 --- a/testing.md +++ b/testing.md @@ -18,7 +18,7 @@ By employing Cot's testing utilities, you'll be able to verify that each piece o ## General Overview -Cot provides several built-in utilities located in the [`cot::test` module](https://docs.rs/cot/0.1/cot/test/index.html) to help you create and run tests for your application. +Cot provides several built-in utilities located in the [`cot::test` module](https://docs.rs/cot/0.3/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). @@ -168,6 +168,41 @@ test_db.cleanup().await?; - Form data is currently only supported with POST requests. - Custom migrations can be added using the `add_migrations` method on `TestDatabase`. +## 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 @@ -176,5 +211,6 @@ Cot's testing framework provides a robust and flexible approach to ensuring the - **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` give you confidence that your data layer is functioning correctly, whether you're using SQLite, PostgreSQL, or MySQL. +- **End-to-end tests** TODO By integrating these testing tools into your workflow, you can deploy your Cot applications with greater confidence. Happy testing! From 6fc1b74fea1f4b3a11198494c220418f902c9018 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Ma=C4=87kowski?= Date: Thu, 11 Sep 2025 22:13:13 +0100 Subject: [PATCH 15/20] feat: cot v0.4 docs, "master" version (#44) * feat: cot v0.4 initial docs, "master" version * more fixes * more fixes * fixes * fix * add v0.4 --- admin-panel.md | 12 ++--- error-pages.md | 136 ++++++++++++++--------------------------------- introduction.md | 6 +-- static-files.md | 18 ++++--- templates.md | 11 ++-- upgrade-guide.md | 29 ++++++++++ 6 files changed, 93 insertions(+), 119 deletions(-) create mode 100644 upgrade-guide.md diff --git a/admin-panel.md b/admin-panel.md index 0b2a4ceb..699f8bab 100644 --- a/admin-panel.md +++ b/admin-panel.md @@ -11,14 +11,14 @@ First, add the admin app and the dependencies required to your project in `src/m ```rust use cot::admin::AdminApp; use cot::auth::db::{DatabaseUser, DatabaseUserApp}; -use cot::middleware::{SessionMiddleware, LiveReloadMiddleware}; -use cot::project::{WithApps, WithConfig}; +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: &ProjectContext) { + 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, ""); @@ -26,9 +26,9 @@ impl Project for MyProject { fn middlewares( &self, - handler: cot::project::RootHandlerBuilder, - app_context: &ProjectContext, - ) -> BoxedHandler { + handler: RootHandlerBuilder, + app_context: &MiddlewareContext, + ) -> RootHandler { handler .middleware(StaticFilesMiddleware::from_app_context(app_context)) .middleware(SessionMiddleware::new()) // Required for admin login diff --git a/error-pages.md b/error-pages.md index 77a6d9a7..c9c331ee 100644 --- a/error-pages.md +++ b/error-pages.md @@ -25,85 +25,53 @@ Now, when you visit a non-existing page, or if your code raises an error or pani ## 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. The two types of error pages that can be customized are: - -* 404 Not Found -* 500 Internal Server Error +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 custom error handlers in your project: +Let's implement a custom error handler in your project: ```rust -use cot::project::{ErrorPageHandler, Project}; -use cot::response::{Response, ResponseExt}; -use cot::{Body, StatusCode}; - -struct CustomNotFound; -impl ErrorPageHandler for CustomNotFound { - fn handle(&self) -> cot::Result { - Ok(Response::new_html( - StatusCode::NOT_FOUND, - Body::fixed(include_str!("404.html")), - )) +use askama::Template; +use cot::html::Html; +use cot::response::{IntoResponse, Response}; +use cot::error::handler::{DynErrorPageHandler, RequestError}; + +async fn error_page_handler(error: RequestError) -> cot::Result { + #[derive(Template)] + #[template(path = "error.html")] + struct ErrorTemplate { + error: RequestError, } -} -struct CustomServerError; -impl ErrorPageHandler for CustomServerError { - fn handle(&self) -> cot::Result { - Ok(Response::new_html( - StatusCode::INTERNAL_SERVER_ERROR, - Body::fixed(include_str!("500.html")), - )) - } + 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 not_found_handler(&self) -> Box { - Box::new(CustomNotFound) - } - - fn server_error_handler(&self) -> Box { - Box::new(CustomServerError) + fn error_handler(&self) -> DynErrorPageHandler { + DynErrorPageHandler::new(error_page_handler) } } ``` -Create `404.html`: - -```html - - - - - 404 Not Found - - -

      404

      -

      Page Not Found

      -

      Oopsies! The page you're looking for doesn't exist.

      -

      Return to Homepage

      - - -``` - -Create `500.html`: +Create `templates/error.html`: ```html - 500 Server Error + Error -

      500

      -

      Server Error

      -

      Oopsies! Something went wrong on our end. Please try again later.

      -

      Return to Homepage

      +

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

      +

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

      ``` @@ -115,57 +83,33 @@ Now, try to visit an undefined route or raise an error in your code. You should 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(cot::Error::not_found()); + return Err(NotFound::new())?; // 404 with custom message - return Err(cot::Error::not_found_message( + return Err(NotFound::with_message( "The article you're looking for doesn't exist".to_string() - )); - - // Custom error - return Err(cot::Error::custom("Something went wrong")); + ))?; + + // 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. In production, the user will see your custom error pages that do not have access to the error message. - -## Handling specific errors - -You can handle specific errors in your views: - -```rust -async fn view_article(RequestDb(db): RequestDb, Path(article_id): Path) -> cot::Result { - // will display a 404 error page if the article ID is below 0 - if article_id < 0 { - return Error::not_found_message("Invalid article ID".to_string()); - } - - // will display a 404 page if the article is not found in the database - let article = query!(Article, $id == article_id) - .get(request.db()) - .await? - .ok_or_else(|| Error::not_found_message( - format!("Article {} not found", article_id) - ))?; - - if article.name.is_empty() { - // both of these will display a 500 error page: - return Err(Error::custom("Article name should never be empty!")); - // or: - panic!("Article name should never be empty!"); - } - - Ok(Response::new_html( - StatusCode::OK, - Body::fixed(render_article(&article)?), - )) -} -``` +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 be able to handle specific errors. +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/introduction.md b/introduction.md index 4392c49e..dac1be85 100644 --- a/introduction.md +++ b/introduction.md @@ -223,7 +223,7 @@ impl Project for CotTutorialProject { 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: &ProjectContext) { + fn register_apps(&self, apps: &mut AppBuilder, _context: &RegisterAppsContext) { apps.register_with_views(CotTutorialApp, ""); } ``` @@ -234,8 +234,8 @@ This registers all the apps that your project is using. fn middlewares( &self, handler: RootHandlerBuilder, - context: &ProjectContext, - ) -> BoxedHandler { + context: &MiddlewareContext, + ) -> RootHandler { handler .middleware(StaticFilesMiddleware::from_app_context(context)) .middleware(LiveReloadMiddleware::from_app_context(context)) diff --git a/static-files.md b/static-files.md index 72806da1..2684085f 100644 --- a/static-files.md +++ b/static-files.md @@ -83,12 +83,16 @@ This command aggregates all static files into the specified directory (in this c 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 -let project = CotProject::builder() - // ... - .middleware_with_context(StaticFilesMiddleware::from_app_context) - .middleware(LiveReloadMiddleware::new()) - .build() - .await?; +fn middlewares( + &self, + handler: RootHandlerBuilder, + context: &MiddlewareContext, +) -> RootHandler { + handler + .middleware(StaticFilesMiddleware::from_context(context)) + // ... + .build() +} ``` -Simply remove the `.middleware_with_context(StaticFilesMiddleware ...)` line to disable static file serving. +Simply remove the `.middleware(StaticFilesMiddleware ...)` line to disable static file serving. diff --git a/templates.md b/templates.md index 04852a49..356ce7ec 100644 --- a/templates.md +++ b/templates.md @@ -54,7 +54,7 @@ struct IndexTemplate { items: Vec, } -async fn index() -> cot::Result { +async fn index() -> cot::Result { let items = vec![ Item { title: "first item".to_string() }, Item { title: "second item".to_string() }, @@ -64,7 +64,7 @@ async fn index() -> cot::Result { let context = IndexTemplate { items }; let rendered = context.render()?; - Ok(Response::new_html(StatusCode::OK, Body::fixed(rendered))) + Ok(Html::new(rendered)) } ``` @@ -175,13 +175,10 @@ struct IndexTemplate<'a> { urls: &'a Urls, } -async fn index(urls: Urls) -> cot::Result { +async fn index(urls: Urls) -> cot::Result { let template = IndexTemplate { urls: &urls }; - Ok(Response::new_html( - StatusCode::OK, - Body::fixed(template.render()?), - )) + Ok(Html::new(template.render()?)) } async fn user() -> cot::Result { diff --git a/upgrade-guide.md b/upgrade-guide.md new file mode 100644 index 00000000..034c68a8 --- /dev/null +++ b/upgrade-guide.md @@ -0,0 +1,29 @@ +--- +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 notice 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.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. From e4212093fc0fae2ce36590241b092fbdc354659b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Ma=C4=87kowski?= Date: Wed, 21 Jan 2026 19:56:36 +0000 Subject: [PATCH 16/20] feat: v0.5 docs (#60) --- admin-panel.md | 2 +- caching.md | 112 ++++++++++++++++++++++++++++++++++++++++ db-models.md | 33 +++++++++++- forms.md | 34 ++++-------- framework-comparison.md | 45 +++++++++++++++- introduction.md | 6 +-- openapi.md | 10 ++-- sending-emails.md | 75 +++++++++++++++++++++++++++ static-files.md | 8 +-- templates.md | 8 +-- testing.md | 32 ++++++++++-- upgrade-guide.md | 26 +++++++++- 12 files changed, 345 insertions(+), 46 deletions(-) create mode 100644 caching.md create mode 100644 sending-emails.md diff --git a/admin-panel.md b/admin-panel.md index 699f8bab..393d08f4 100644 --- a/admin-panel.md +++ b/admin-panel.md @@ -30,7 +30,7 @@ impl Project for MyProject { app_context: &MiddlewareContext, ) -> RootHandler { handler - .middleware(StaticFilesMiddleware::from_app_context(app_context)) + .middleware(StaticFilesMiddleware::from_context(app_context)) .middleware(SessionMiddleware::new()) // Required for admin login .build() } diff --git a/caching.md b/caching.md new file mode 100644 index 00000000..a6e98eb7 --- /dev/null +++ b/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/db-models.md b/db-models.md index c9f260bc..c342a372 100644 --- a/db-models.md +++ b/db-models.md @@ -109,6 +109,37 @@ To delete a model from the database, you can use the `delete` method of the `Que 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: @@ -167,4 +198,4 @@ Cot tries to be as consistent as possible when it comes to the database engine y ## Summary -In this chapter you've 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. +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/forms.md b/forms.md index 35257e37..9ffa7a4f 100644 --- a/forms.md +++ b/forms.md @@ -15,7 +15,7 @@ use cot::form::Form; struct ContactForm { name: String, email: String, - #[form(opt(max_length = 1000))] + #[form(opts(max_length = 1000))] message: String, } ``` @@ -24,6 +24,7 @@ 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}; @@ -32,8 +33,7 @@ async fn contact(mut request: Request) -> cot::Result { if request.method() == Method::POST { match ContactForm::from_request(&mut request).await? { FormResult::Ok(form) => { - // Form is valid! Process the datause cot::html::Html; - + // Form is valid! Process the data println!("Message from {}: {}", form.name, form.message); // Redirect after successful submission @@ -45,10 +45,7 @@ async fn contact(mut request: Request) -> cot::Result { request: &request, form: context, }; - Ok(Response::new_html( - StatusCode::OK, - Body::fixed(template.render()?) - )) + Ok(Html::new(template.render()?).into()) } } } else { @@ -58,10 +55,7 @@ async fn contact(mut request: Request) -> cot::Result { form: ContactForm::build_context(&mut request).await?, }; - Ok(Response::new_html( - StatusCode::OK, - Body::fixed(template.render()?) - )) + Ok(Html::new(template.render()?).into()) } } ``` @@ -117,11 +111,11 @@ Cot provides several ways to validate form data: #[derive(Form)] struct ArticleForm { // Maximum length validation - #[form(opt(max_length = 100))] + #[form(opts(max_length = 100))] title: String, // Required checkbox - #[form(opt(must_be_true = true))] + #[form(opts(must_be_true = true))] confirm_publish: bool, } ``` @@ -143,10 +137,7 @@ async fn handle_form(mut request: Request) -> cot::Result { ); // Re-render form with error - return Ok(Response::new_html( - StatusCode::OK, - Body::fixed(render_template(context)?) - )); + return Ok(Html::new(render_template(context)?).into()); } // Process valid form... @@ -154,10 +145,7 @@ async fn handle_form(mut request: Request) -> cot::Result { } FormResult::ValidationError(context) => { // Handle validation errors... - Ok(Response::new_html( - StatusCode::OK, - Body::fixed(render_template(context)?) - )) + Ok(Html::new(render_template(context)?).into()) } } } @@ -165,7 +153,7 @@ async fn handle_form(mut request: Request) -> cot::Result { ## Summary -In this you learned how to handle forms and validate form data in Cot applications. Remember: +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 @@ -173,4 +161,4 @@ In this you learned how to handle forms and validate form data in Cot applicatio * Consider user experience in form layout * Handle both GET and POST requests appropriately -In the next chapter, we'll explore database models and how can you use them to persist data in your services. +In the next chapter, we'll explore database models and how you can use them to persist data in your services. diff --git a/framework-comparison.md b/framework-comparison.md index 7e5df432..eaa9fcc3 100644 --- a/framework-comparison.md +++ b/framework-comparison.md @@ -2,4 +2,47 @@ 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/introduction.md b/introduction.md index dac1be85..ecfff28c 100644 --- a/introduction.md +++ b/introduction.md @@ -16,7 +16,7 @@ If you are not familiar with Rust, you might want to start by reading the [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.84 or later. +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: @@ -237,8 +237,8 @@ This registers all the apps that your project is using. context: &MiddlewareContext, ) -> RootHandler { handler - .middleware(StaticFilesMiddleware::from_app_context(context)) - .middleware(LiveReloadMiddleware::from_app_context(context)) + .middleware(StaticFilesMiddleware::from_context(context)) + .middleware(LiveReloadMiddleware::from_context(context)) .build() } ``` diff --git a/openapi.md b/openapi.md index ef55cceb..8af3270c 100644 --- a/openapi.md +++ b/openapi.md @@ -21,7 +21,7 @@ To use OpenAPI features in Cot, you need to enable the `openapi` and `swagger-ui ```toml [dependencies] cot = { version = "...", features = ["openapi", "swagger-ui"] } -schemars = "0.8" # Required for JSON Schema generation +schemars = "0.9" # Required for JSON Schema generation ``` The `schemars` crate is necessary for creating JSON Schema definitions for your request and response types. @@ -297,11 +297,11 @@ 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.3/cot/openapi/trait.ApiOperationPart.html) trait. You can study their implementations to understand how to design your own: +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.3/cot/json/struct.Json.html) adds a request or response body to the operation -* [`Path`](https://docs.rs/cot/0.3/cot/request/extractors/struct.Path.html) adds path parameters -* [`UrlQuery`](https://docs.rs/cot/0.3/cot/request/extractors/struct.UrlQuery.html) adds query parameters +* [`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. diff --git a/sending-emails.md b/sending-emails.md new file mode 100644 index 00000000..233f11a5 --- /dev/null +++ b/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/static-files.md b/static-files.md index 2684085f..fa40fab5 100644 --- a/static-files.md +++ b/static-files.md @@ -15,7 +15,7 @@ The Cot CLI generates a `static` directory in your project root, which serves as 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 CotApp for MyApp { +impl App for MyApp { fn static_files(&self) -> Vec { static_files!("css/main.css") } @@ -25,7 +25,7 @@ impl CotApp for MyApp { To add more files, simply include them in the `static_files!` macro. For example, after adding a logo to your project: ```rust -impl CotApp for MyApp { +impl App for MyApp { fn static_files(&self) -> Vec { static_files!( "css/main.css", @@ -40,8 +40,8 @@ You can get the URL for a static file using the `StaticFiles` extractor. For exa ```rust use cot::request::extractors::StaticFiles; -async fn get_logo_url(static_files: StaticFiles) -> String { - static_files.url_for("images/logo.png") +async fn get_logo_url(static_files: StaticFiles) -> cot::Result { + Ok(static_files.url_for("images/logo.png")?.to_string()) } ``` diff --git a/templates.md b/templates.md index 356ce7ec..a559ddd7 100644 --- a/templates.md +++ b/templates.md @@ -6,7 +6,7 @@ Cot does not require you to use any specific templating engine. However, it prov ## Basic Syntax -A 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: +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
        @@ -42,7 +42,7 @@ Here is a simple demonstration of templating with Askama in Cot: use cot::request::Request; use cot::response::{Response, ResponseExt}; use cot::{Body, StatusCode}; -use askama::Template; +use cot::Template; struct Item { title: String, @@ -167,7 +167,7 @@ use cot::request::Request; use cot::response::{Response, ResponseExt}; use cot::router::{Router, Route, Urls}; use cot::{Body, StatusCode}; -use askama::Template; +use cot::Template; #[derive(Template)] #[template(path = "index.html")] @@ -336,7 +336,7 @@ 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.3/cot/html/struct.HtmlTag.html) type. It automatically applies escaping where necessary. +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 { diff --git a/testing.md b/testing.md index 161e9e98..79fa21e1 100644 --- a/testing.md +++ b/testing.md @@ -18,7 +18,7 @@ By employing Cot's testing utilities, you'll be able to verify that each piece o ## General Overview -Cot provides several built-in utilities located in the [`cot::test` module](https://docs.rs/cot/0.3/cot/test/index.html) to help you create and run tests for your application. +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). @@ -168,6 +168,32 @@ test_db.cleanup().await?; - 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. @@ -210,7 +236,7 @@ Please refer to the documentation of these crates for more information on how to 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` give you confidence that your data layer is functioning correctly, whether you're using SQLite, PostgreSQL, or MySQL. -- **End-to-end tests** TODO +- **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/upgrade-guide.md b/upgrade-guide.md index 034c68a8..eb6cb33b 100644 --- a/upgrade-guide.md +++ b/upgrade-guide.md @@ -4,10 +4,34 @@ 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 notice you about the exact changes you need to make. +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 From 170683be7f64fbb922096d3abc32598af7f107f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Ma=C4=87kowski?= Date: Thu, 22 Jan 2026 14:08:22 +0000 Subject: [PATCH 17/20] chore: use cot::Template instead of askama::Template in guide (#62) --- error-pages.md | 2 +- templates.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/error-pages.md b/error-pages.md index c9c331ee..db45ff96 100644 --- a/error-pages.md +++ b/error-pages.md @@ -32,10 +32,10 @@ When the debug mode is disabled, Cot provides default error pages that do not sh Let's implement a custom error handler in your project: ```rust -use askama::Template; 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)] diff --git a/templates.md b/templates.md index a559ddd7..982c4f0f 100644 --- a/templates.md +++ b/templates.md @@ -297,7 +297,7 @@ To display custom types in Askama templates, the type must implement `Display`. ```rust use std::fmt::Display; -use askama::Template; +use cot::Template; struct Item { title: String, From 937e45e8494a55b65e10ff8523d430d26187b1a8 Mon Sep 17 00:00:00 2001 From: Martin Jul Date: Sun, 25 Jan 2026 16:42:17 +0100 Subject: [PATCH 18/20] fix: admin user retrieval code in `admin-panel.md` (#64) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Mateusz Maćkowski --- admin-panel.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/admin-panel.md b/admin-panel.md index 393d08f4..41df8bcd 100644 --- a/admin-panel.md +++ b/admin-panel.md @@ -44,9 +44,11 @@ impl Project for MyProject { 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; -use cot::auth::db::{DatabaseUser, DatabaseUserCredentials}; -use cot::auth::Password; // In your main.rs: #[async_trait] @@ -55,7 +57,7 @@ impl App for MyApp { // 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").await?; + 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()); From 148de3211294382fef01c4a585c05b5e33bb2477 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Ma=C4=87kowski?= Date: Mon, 16 Feb 2026 19:23:08 +0100 Subject: [PATCH 19/20] chore: remove disclaimer from introduction.md (#71) --- introduction.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/introduction.md b/introduction.md index ecfff28c..083a74d9 100644 --- a/introduction.md +++ b/introduction.md @@ -4,8 +4,6 @@ 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? From e79c39c42cac3342e38638aced289b3a5b74ae44 Mon Sep 17 00:00:00 2001 From: Alex Bissessur <61658620+xelab04@users.noreply.github.com> Date: Sun, 22 Feb 2026 17:52:48 +0400 Subject: [PATCH 20/20] chore: update on functionality of `cot::reverse` in docs (#73) --- templates.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/templates.md b/templates.md index 982c4f0f..c3d258fd 100644 --- a/templates.md +++ b/templates.md @@ -148,7 +148,7 @@ Rendered output: 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. +`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 @@ -156,8 +156,8 @@ Linking to other pages in your application is a frequent requirement, and hardco ```html.j2 {% let urls = urls %} -Home -User 42 +Home +User 42 ``` `main.rs`: