-
Notifications
You must be signed in to change notification settings - Fork 255
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge branch 'master' into no-db-feture
- Loading branch information
Showing
16 changed files
with
474 additions
and
31 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,3 @@ | ||
# FAQ | ||
|
||
(collect as they come) | ||
TBD |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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`. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,3 @@ | ||
# not implemented yet | ||
# TBD | ||
|
||
Implemented, just need to document. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. | ||
|
||
|
||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} | ||
``` | ||
|
||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
``` | ||
|
||
|
Oops, something went wrong.