Skip to content

Commit

Permalink
Merge branch 'master' into no-db-feture
Browse files Browse the repository at this point in the history
  • Loading branch information
kaplanelad authored Nov 22, 2023
2 parents c60424f + 787d021 commit 85841ee
Show file tree
Hide file tree
Showing 16 changed files with 474 additions and 31 deletions.
2 changes: 0 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ clap = { version = "4.4.7", features = ["derive"], optional = true }
sea-orm = { version = "0.12.4", features = [
"sqlx-postgres", # `DATABASE_DRIVER` feature
"sqlx-sqlite",
"sqlx-mysql",
"runtime-tokio-rustls",
"macros",
], optional = true }
Expand Down Expand Up @@ -92,7 +91,6 @@ features = [
"runtime-tokio-rustls", # `ASYNC_RUNTIME` feature
"sqlx-postgres", # `DATABASE_DRIVER` feature
"sqlx-sqlite",
"sqlx-mysql",
]


Expand Down
62 changes: 61 additions & 1 deletion docs/0_cli.md
Original file line number Diff line number Diff line change
@@ -1 +1,61 @@
# too early
# CLI

Create your starter app:

```rust
$ cargo install rustyrails-cli
$ rustyrails new
< follow the guide >
```


Now `cd` into your app, set up a convenience `rr` alias and try out the various commands:

```
$ cd myapp
$ alias rr='cargo run --'
$ rr --help
```

You can now drive your development through the CLI:

```
$ rr generate model posts
$ rr generate controller posts
$ rr db migrate
$ rr start
```

And running tests or working with Rust is just as you already know:

```
$ cargo build
$ cargo test
```

## Starting your app
To run you app, run:

```
$ rr start
```

## Background workers

Based on your configuration (in `config/`), your workers will know how to operate:

```yaml
workers:
# requires Redis
mode: BackgroundQueue

# can also use:
# ForegroundBlocking - great for testing
# BackgroundAsync - for same-process jobs, using tokio async
```

And now, you can run the actual process in various ways:

* `rr start --worker` - run only a worker and process background jobs. This is great for scale. Run one service app with `rr start`, and then run many process based workers with `rr start --worker` distributed on any machine you want.
* `rr start --server-and-worker` - will run both a service and a background worker processor in the same unix process. It uses Tokio for executing background jobs. This is great for those cases when you want to run on a single server without too much of an expense or have constrained resources.

2 changes: 1 addition & 1 deletion docs/10_faq.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
# FAQ

(collect as they come)
TBD
18 changes: 17 additions & 1 deletion docs/1_config.md
Original file line number Diff line number Diff line change
@@ -1 +1,17 @@
# too early
# Configuration

Configuration in `rustyrails` lives in `config/` and by default sets up 3 different environments:

```
config/
development.yaml
production.yaml
test.yaml
```

An environment is picked up automatically based on:

* A command line flag: `rr start --environment production`, if not given, fallback to
* `RR_ENV` or `RAILS_ENV` or `NODE_ENV`

When nothing is given, the default value is `development`.
122 changes: 120 additions & 2 deletions docs/3_models.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,123 @@
# Models

## Adding a model
Models in `rustyrails` mean entity classes that allow for easy database querying and writes, but also migrations and seeding.

## Testing models

## Fat models, slim controllers

`rustyrails` models **are designed after active record**. This means they're a central point in your universe, and every logic or operation your app has should be there.

It means that `User::create` creates a user **but also** `user.buy(product)` will buy a product.

If you agree with that direction you'll get these for free:

* **Time-effective testing**, because testing your model tests most if not all of your logic and moving parts.
* Ability to run complete app workflows **from _tasks_, or from workers and other places**.
* Effectively **compose features** and use cases by combining models, and nothing else.
* Essentially, **models become your app** and controllers are just one way to expose your app to the world.

We use [`SeaORM`](https://www.sea-ql.org/SeaORM/) as the main ORM behind our ActiveRecord abstraction.

* _Why not Diesel?_ - although Diesel has better performance, its macros, and general approach felt incompatible with what we were trying to do
* _Why not sqlx_ - SeaORM uses sqlx under the hood, so the plumbing is there for you to use `sqlx` raw if you wish.


## Example model

The life of a `rustyrails` model starts with a _migration_, then an _entity_ Rust code is generated for you automatically from the database structure:


```
src/
models/
_entities/ <--- autogenerated code
users.rs <--- the bare entity and helper traits
users.rs <--- your custom activerecord code
```

Using the `users` activerecord would be just as you use it under SeaORM [see examples here](https://www.sea-ql.org/SeaORM/docs/next/basic-crud/select/)

Adding functionality to the `users` activerecord is by _extension_:

```rust
impl super::_entities::users::ActiveModel {
/// .
///
/// # Errors
///
/// .
pub fn validate(&self) -> Result<(), DbErr> {
let validator: ModelValidator = self.into();
validator.validate().map_err(validation::into_db_error)
}
}
```

## Migrations

To add a new model _you have to use a migration_.

```
$ rr generate model posts
```

Creates a migration in the root of your project in `migration/`.
You can now apply it:

```
$ rr db migrate
```

And generate back entities (Rust code) from it:

```
$ rr db entities
```


## Configuration

Model configuration that's available to you is exciting because it controls all aspects of development, testing, and production, with a ton of goodies, coming from production experience.


```yaml
# .. other sections ..

database:
uri: postgres://localhost:5432/rr_app
# uri: sqlite://db.sqlite?mode=rwc
enable_logging: false
min_connections: 1
max_connections: 1
auto_migrate: true
dangerously_truncate: true
dangerously_recreate: true
```
By combining these flags, you can create different expriences to help you be more productive.
You can truncate before an app starts -- which is useful for running tests, or you can recreate the entire DB when the app starts -- which is useful for integration tests or setting up a new environment. In production, you want these turned off (hence the "dangerously" part).
## Testing
If you used the generator to crate a model migration, you should also have an auto generated model test in `tests/models/posts.rs` (remember we generated a model named `post`?)

A typical test contains everything you need to set up test data, boot the app, and reset the database automatically before the testing code runs. It looks like this:

```rust
async fn can_find_by_pid() {
configure_insta!();
let boot = testing::boot_test::<App, Migrator>().await;
testing::seed::<App>(&boot.app_context.db).await.unwrap();
let existing_user =
Model::find_by_pid(&boot.app_context.db, "11111111-1111-1111-1111-111111111111").await;
let non_existing_user_results =
Model::find_by_email(&boot.app_context.db, "23232323-2323-2323-2323-232323232323").await;
assert_debug_snapshot!(existing_user);
assert_debug_snapshot!(non_existing_user_results);
}
```
4 changes: 3 additions & 1 deletion docs/4_seeding.md
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
# not implemented yet
# TBD

Implemented, just need to document.
29 changes: 28 additions & 1 deletion docs/5_views.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,33 @@
# Views

Views are any form of output that a controller can use. Mostly these are strongly typed Rust struct which can be serialized to JSON.

They are separate from controllers to create a form-follow-function dynamic, where we treat various JSON outputs as separately maintainable things.


Though there's nothing technical about this separation, just the psychology of having views in a `views/` folder enables different thinking about:

* Breaking changes
* Versioning
* Other forms of serialization

Respond in your controller in this way:

```rust
use rustyrails::{
controller::format,
Result,
};
use views::user::CurrentResponse;

fn hello() -> Result<Json<CurrentResponse>>{
// ...
format::json(CurrentResponse::new(&user))
}
```
## Adding views

## Using views
Just drop any serializable struct in `views/` and `use` it from a controller. It is recommended to use `serde` but you can think of any other way you like to serialize data.



47 changes: 46 additions & 1 deletion docs/6_controllers.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,50 @@
# Controllers

A controller is a regular Rust module that exposes a `routes()` method which we then use to compose into the root app router.

## Adding Controllers

## Building controllers
First add a controller in `controllers/users.rs`

```rust
use axum::{extract::State, routing::get, Json};
use rustyrails::{
app::AppContext,
controller::{format, middleware, Routes},
Result,
};

use crate::{models::_entities::users, views::user::CurrentResponse};

async fn current(
auth: middleware::auth::Auth,
State(ctx): State<AppContext>,
) -> Result<Json<CurrentResponse>> {
let user = users::Model::find_by_pid(&ctx.db, &auth.claims.pid).await?;
format::json(CurrentResponse::new(&user))
}

pub fn routes() -> Routes {
Routes::new().prefix("user").add("/current", get(current))
}
```

Update the `mod` file: `controllers/mod.rs`:

```rust
pub mod user;
```

And register the routes in your main `app.rs` file:

```rust
// ...
impl Hooks for App {
fn routes() -> AppRoutes {
AppRoutes::with_default_routes()
.add_route(controllers::auth::routes())
.add_route(controllers::user::routes()) // <--- add this
}
```


66 changes: 65 additions & 1 deletion docs/7_mailers.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,70 @@
# Mailers

A mailer will deliver emails in the background using the existing `rustyrails` background worker infrastructure. It will all be seamless for you.

## Adding a mailer

## Using mailers

To use an existing mailer, mostly in your controller:

```rust
use crate::{
mailers::auth::AuthMailer,
}

// in your controllers/auth.rs
async fn register(
State(ctx): State<AppContext>,
Json(params): Json<RegisterParams>,
) -> Result<Json<()>> {
// .. register a user ..
AuthMailer::send_welcome(&ctx, &user.email).await.unwrap();
}
```

This will enqueue a mail delivery job. The action is instant because the delivery will be performed later in the background.

## Adding a mailer

Now, you need to define your mailer, in `mailers/auth.rs`, add:

```rust
static welcome: Dir<'_> = include_dir!("src/mailers/auth/welcome");
impl AuthMailer {
/// Sending welcome email the the given user
///
/// # Errors
///
/// When email sending is failed
pub async fn send_welcome(ctx: &AppContext, _user_id: &str) -> Result<()> {
Self::mail_template(
ctx,
&welcome,
Args {
to: "[email protected]".to_string(),
locals: json!({
"name": "joe"
}),
..Default::default()
},
)
.await?;
Ok(())
}
}
```

Each mailer has an opinionated, predefined folder structure:

```
src/
mailers/
auth/
welcome/ <-- all the parts of an email, all templates
subject.t
html.t
text.t
auth.rs <-- mailer definition
```


Loading

0 comments on commit 85841ee

Please sign in to comment.