Vendor dependencies

This commit is contained in:
2026-08-01 16:11:49 +03:00
parent 7f139a0241
commit 6b5e7f0f8b
29706 changed files with 9575646 additions and 0 deletions
@@ -0,0 +1,49 @@
use std::path::Path;
use async_trait::async_trait;
use rustyrails::{
app::{AppContext, Hooks},
controller::AppRoutes,
db::{self, truncate_table},
task::Tasks,
worker::{AppWorker, Processor},
Result,
};
use sea_orm::DatabaseConnection;
use crate::{
controllers,
models::_entities::{notes, users},
tasks::user_report::UserReport,
workers::downloader::DownloadWorker,
};
pub struct App;
#[async_trait]
impl Hooks for App {
fn routes() -> AppRoutes {
AppRoutes::with_default_routes()
.add_route(controllers::notes::routes())
.add_route(controllers::auth::routes())
.add_route(controllers::user::routes())
}
fn connect_workers<'a>(p: &'a mut Processor, ctx: &'a AppContext) {
p.register(DownloadWorker::build(ctx));
}
fn register_tasks(tasks: &mut Tasks) {
tasks.register(UserReport);
}
async fn truncate(db: &DatabaseConnection) -> Result<()> {
truncate_table(db, users::Entity).await?;
truncate_table(db, notes::Entity).await?;
Ok(())
}
async fn seed(db: &DatabaseConnection, base: &Path) -> Result<()> {
db::seed::<users::ActiveModel>(db, &base.join("users.yaml").display().to_string()).await?;
Ok(())
}
}
@@ -0,0 +1,8 @@
use blo::app::App;
use migration::Migrator;
use rustyrails::cli;
#[tokio::main]
async fn main() -> eyre::Result<()> {
cli::main::<App, Migrator>().await
}
@@ -0,0 +1,62 @@
use axum::{extract::State, routing::post, Json};
use rustyrails::{
app::AppContext,
controller::{format, unauthorized, Routes},
Result,
};
use crate::{
mailers::auth::AuthMailer,
models::users::{self, LoginParams, RegisterParams},
views::auth::LoginResponse,
};
async fn register(
State(ctx): State<AppContext>,
Json(params): Json<RegisterParams>,
) -> Result<Json<()>> {
let res = users::Model::create_with_password(&ctx.db, &params).await;
let user = match res {
Ok(user) => user,
Err(err) => {
tracing::info!(
message = err.to_string(),
user_email = &params.email,
"could not register user",
);
return format::json(());
}
};
// TODO:: send website base uri
AuthMailer::send_welcome(&ctx, &user.email).await.unwrap();
format::json(())
}
async fn login(
State(ctx): State<AppContext>,
Json(params): Json<LoginParams>,
) -> Result<Json<LoginResponse>> {
let user = users::Model::find_by_email(&ctx.db, &params.email).await?;
let valid = user.verify_password(&params.password)?;
if !valid {
return unauthorized("unauthorized access");
}
let token = user
.generate_jwt(&ctx.config.auth.secret, &ctx.config.auth.expiration)
.or_else(|_| unauthorized("unauthorized!"))?;
format::json(LoginResponse::new(&user, &token))
}
pub fn routes() -> Routes {
Routes::new()
.prefix("auth")
.add("/register", post(register))
.add("/login", post(login))
}
@@ -0,0 +1,3 @@
pub mod auth;
pub mod notes;
pub mod user;
@@ -0,0 +1,80 @@
#![allow(clippy::unused_async)]
use axum::{extract::State, routing::get};
use rustyrails::{
app::AppContext,
controller::{format, Routes},
errors::Error,
worker::AppWorker,
Result,
};
use sea_orm::EntityTrait;
// user imports
use crate::{
mailers::auth::AuthMailer,
models::_entities::users,
workers::downloader::{DownloadWorker, DownloadWorkerArgs},
};
/// Benchmark function for a simple hello-world endpoint.
///
/// # Errors
///
/// Errors related to formatting the response.
pub async fn bench_hello(_req_body: String) -> Result<String> {
format::text("hello")
}
/// Benchmark function for database operations.
///
/// This function is used to benchmark database operations by performing a
/// simple query to retrieve a user entity by ID. It utilizes the
/// `Entity::find_by_id` method provided by the `users` module.
///
/// # Errors
///
/// When db query fails
pub async fn bench_db(State(ctx): State<AppContext>) -> Result<()> {
let _ = users::Entity::find_by_id(1).one(&ctx.db).await?;
format::empty()
}
/// return echo message
pub async fn echo(req_body: String) -> String {
req_body
}
/// A simple endpoint for demonstrating asynchronous tasks in a web application.
///
/// # Errors
///
/// when send welcome message fails or there is an error when preform download
/// task
pub async fn hello(State(ctx): State<AppContext>) -> Result<String> {
DownloadWorker::perform_later(
&ctx,
DownloadWorkerArgs {
user_guid: "foo".to_string(),
},
)
.await
.map_err(|e| {
tracing::error!(
error = e.to_string(),
"could not perform the download worker"
);
Error::Any("could not perform the download worker ".into())
})?;
AuthMailer::send_welcome(&ctx, "foobar").await?;
format::text("hello")
}
pub fn routes() -> Routes {
Routes::new()
.add("/", get(hello))
.add("/echo", get(echo))
.add("/bench_db", get(bench_db))
.add("/bench_hello", get(bench_hello))
}
@@ -0,0 +1,20 @@
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))
}
@@ -0,0 +1,15 @@
---
- id: 1
pid: 11111111-1111-1111-1111-111111111111
email: user1@example.com
password: "$2b$12$gf4o2FShIahg/GY6YkK2wOcs8w4.lu444wP6BL3FyjX0GsxnEV6ZW"
name: user1
created_at: "2023-11-12T12:34:56.789"
updated_at: "2023-11-12T12:34:56.789"
- id: 2
pid: 22222222-2222-2222-2222-222222222222
email: user2@example.com
password: "$2b$12$gf4o2FShIahg/GY6YkK2wOcs8w4.lu444wP6BL3FyjX0GsxnEV6ZW"
name: user2
created_at: "2023-11-12T12:34:56.789"
updated_at: "2023-11-12T12:34:56.789"
@@ -0,0 +1,7 @@
pub mod app;
pub mod controllers;
pub mod mailers;
pub mod models;
pub mod tasks;
pub mod views;
pub mod workers;
@@ -0,0 +1,40 @@
// auth mailer
#![allow(non_upper_case_globals)]
use include_dir::{include_dir, Dir};
use rustyrails::{
app::AppContext,
mailer::{Args, Mailer},
Result,
};
use serde_json::json;
static welcome: Dir<'_> = include_dir!("src/mailers/auth/welcome");
// #[derive(Mailer)] // -- disabled for faster build speed. it works. but lets
// move on for now.
#[allow(clippy::module_name_repetitions)]
pub struct AuthMailer {}
impl Mailer for AuthMailer {}
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: "foo@example.com".to_string(),
locals: json!({
"name": "joe"
}),
..Default::default()
},
)
.await?;
Ok(())
}
}
@@ -0,0 +1,8 @@
;<html>
<body>
You can
<a href="http://localhost:/reset/<%= resetToken %>">reset your password</a>
</body>
</html>
@@ -0,0 +1 @@
Your reset password link
@@ -0,0 +1,3 @@
Reset your password with this link:
http://localhost/reset/<%= resetToken %>
@@ -0,0 +1,10 @@
;<html>
<body>
You can{' '}
<a href="http://localhost:/verify/<%= verifyToken %>">
verify your account
</a>
</body>
</html>
@@ -0,0 +1 @@
Welcome <%= name %>
@@ -0,0 +1,4 @@
Welcome <%= name %>, you can now log in.
Verify your account with the link below:
http://localhost/verify/<%= verifyToken %>
@@ -0,0 +1 @@
pub mod auth;
@@ -0,0 +1,6 @@
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.4
pub mod prelude;
pub mod notes;
pub mod users;
@@ -0,0 +1,37 @@
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.4
use sea_orm::entity::prelude::*;
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
#[sea_orm(table_name = "notes")]
pub struct Model {
pub created_at: DateTime,
pub updated_at: DateTime,
#[sea_orm(primary_key)]
pub id: i32,
#[sea_orm(unique)]
pub pid: Uuid,
pub title: Option<String>,
pub content: Option<String>,
pub owner_id: i32,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
#[sea_orm(
belongs_to = "super::users::Entity",
from = "Column::OwnerId",
to = "super::users::Column::Id",
on_update = "Cascade",
on_delete = "Cascade"
)]
Users,
}
impl Related<super::users::Entity> for Entity {
fn to() -> RelationDef {
Relation::Users.def()
}
}
impl ActiveModelBehavior for ActiveModel {}
@@ -0,0 +1,3 @@
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.4
pub use super::{notes::Entity as Notes, users::Entity as Users};
@@ -0,0 +1,33 @@
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.4
use sea_orm::entity::prelude::*;
use serde::Deserialize;
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Deserialize)]
#[sea_orm(table_name = "users")]
pub struct Model {
pub created_at: DateTime,
pub updated_at: DateTime,
#[sea_orm(primary_key)]
pub id: i32,
#[sea_orm(unique)]
pub pid: Uuid,
#[sea_orm(unique)]
pub email: String,
pub password: String,
pub name: String,
pub reset_token: Option<String>,
pub reset_sent_at: Option<DateTime>,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
#[sea_orm(has_many = "super::notes::Entity")]
Notes,
}
impl Related<super::notes::Entity> for Entity {
fn to() -> RelationDef {
Relation::Notes.def()
}
}
@@ -0,0 +1,2 @@
pub mod _entities;
pub mod users;
@@ -0,0 +1,173 @@
// TODO(review): base components must be re-exported
use rustyrails::{
auth,
model::{ModelError, ModelResult},
validation,
validator::Validate,
};
use sea_orm::{
entity::prelude::*, ActiveValue, DatabaseConnection, DbErr, ModelTrait, TransactionTrait,
};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
pub use super::_entities::users::{self, ActiveModel, Entity, Model};
#[derive(Debug, Deserialize, Serialize)]
pub struct LoginParams {
pub email: String,
pub password: String,
}
#[derive(Debug, Deserialize, Serialize)]
pub struct RegisterParams {
pub email: String,
pub password: String,
pub name: String,
}
#[derive(Debug, Validate, Deserialize)]
pub struct ModelValidator {
#[validate(length(min = 2, message = "Name must be at least 2 characters long."))]
pub name: String,
#[validate(custom = "validation::is_valid_email")]
pub email: String,
}
impl From<&ActiveModel> for ModelValidator {
fn from(value: &ActiveModel) -> Self {
Self {
name: value.name.as_ref().to_string(),
email: value.email.as_ref().to_string(),
}
}
}
#[async_trait::async_trait]
impl ActiveModelBehavior for super::_entities::users::ActiveModel {
async fn before_save<C>(self, _db: &C, insert: bool) -> Result<Self, DbErr>
where
C: ConnectionTrait,
{
{
self.validate()?;
if insert {
let mut this = self;
this.pid = ActiveValue::Set(Uuid::new_v4());
Ok(this)
} else {
Ok(self)
}
}
}
}
impl super::_entities::users::Model {
/// .
///
/// # Errors
///
/// This function will return an error if .
pub async fn notes(
&self,
db: &DatabaseConnection,
) -> Result<Vec<super::_entities::notes::Model>, DbErr> {
self.find_related(super::_entities::prelude::Notes)
.all(db)
.await
}
/// .
///
/// # Errors
///
/// .
pub async fn find_by_email(db: &DatabaseConnection, email: &str) -> ModelResult<Self> {
let user = users::Entity::find()
.filter(users::Column::Email.eq(email))
.one(db)
.await?;
user.ok_or_else(|| ModelError::EntityNotFound)
}
/// .
///
/// # Errors
///
/// .
pub async fn find_by_pid(db: &DatabaseConnection, pid: &str) -> ModelResult<Self> {
let parse_uuid = Uuid::parse_str(pid).map_err(|e| ModelError::Message(e.to_string()))?;
let user = users::Entity::find()
.filter(users::Column::Pid.eq(parse_uuid))
.one(db)
.await?;
user.ok_or_else(|| ModelError::EntityNotFound)
}
/// .
///
/// # Errors
///
/// .
pub fn verify_password(&self, password: &str) -> ModelResult<bool> {
Ok(auth::verify_password(password, &self.password)?)
}
/// .
///
/// # Errors
///
/// .
pub async fn create_with_password(
db: &DatabaseConnection,
params: &RegisterParams,
) -> ModelResult<Self> {
let txn = db.begin().await?;
if users::Entity::find()
.filter(users::Column::Email.eq(&params.email))
.one(&txn)
.await?
.is_some()
{
return Err(ModelError::EntityExists {});
}
let password_hash = auth::hash_password(&params.password)?;
let user = users::ActiveModel {
//TODO(review): there might be a 'trick' to moves between params and partial
// ActiveValue values to save this rhs-lhs coding
email: ActiveValue::set(params.email.to_string()),
password: ActiveValue::set(password_hash),
name: ActiveValue::set(params.name.to_string()),
..Default::default()
}
.insert(&txn)
.await?;
txn.commit().await?;
Ok(user)
}
/// .
///
/// # Errors
///
/// .
pub fn generate_jwt(&self, secret: &str, expiration: &u64) -> ModelResult<String> {
Ok(auth::JWT::new(secret).generate_token(expiration, self.pid.to_string())?)
}
}
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)
}
}
@@ -0,0 +1 @@
pub mod user_report;
@@ -0,0 +1,33 @@
use std::collections::BTreeMap;
use async_trait::async_trait;
use rustyrails::{
app::AppContext,
task::{Task, TaskInfo},
Result,
};
use sea_orm::EntityTrait;
use crate::models::_entities::users;
pub struct UserReport;
#[async_trait]
impl Task for UserReport {
fn task(&self) -> TaskInfo {
TaskInfo {
name: "user_report".to_string(),
detail: "output a user report".to_string(),
}
}
async fn run(&self, app_context: &AppContext, vars: &BTreeMap<String, String>) -> Result<()> {
let users = users::Entity::find().all(&app_context.db).await?;
println!("args: {vars:?}");
println!("!!! user_report: listing users !!!");
println!("------------------------");
for user in &users {
println!("user: {}", user.email);
}
println!("done: {} users", users.len());
Ok(())
}
}
@@ -0,0 +1,21 @@
use serde::{Deserialize, Serialize};
use crate::models::_entities::users;
#[derive(Debug, Deserialize, Serialize)]
pub struct LoginResponse {
pub token: String,
pub pid: String,
pub name: String,
}
impl LoginResponse {
#[must_use]
pub fn new(user: &users::Model, token: &String) -> Self {
Self {
token: token.to_string(),
pid: user.pid.to_string(),
name: user.name.clone(),
}
}
}
@@ -0,0 +1,2 @@
pub mod auth;
pub mod user;
@@ -0,0 +1,21 @@
use serde::{Deserialize, Serialize};
use crate::models::_entities::users;
#[derive(Debug, Deserialize, Serialize)]
pub struct CurrentResponse {
pub pid: String,
pub name: String,
pub email: String,
}
impl CurrentResponse {
#[must_use]
pub fn new(user: &users::Model) -> Self {
Self {
pid: user.pid.to_string(),
name: user.name.clone(),
email: user.email.clone(),
}
}
}
@@ -0,0 +1,49 @@
use std::time::Duration;
use async_trait::async_trait;
use rustyrails::{
app::AppContext,
worker::{AppWorker, Result, Worker},
};
use sea_orm::EntityTrait;
use serde::{Deserialize, Serialize};
use tokio::time::sleep;
use crate::models::users;
pub struct DownloadWorker {
pub ctx: AppContext,
}
#[derive(Deserialize, Debug, Serialize)]
pub struct DownloadWorkerArgs {
pub user_guid: String,
}
impl AppWorker<DownloadWorkerArgs> for DownloadWorker {
fn build(ctx: &AppContext) -> Self {
Self { ctx: ctx.clone() }
}
}
#[async_trait]
impl Worker<DownloadWorkerArgs> for DownloadWorker {
async fn perform(&self, args: DownloadWorkerArgs) -> Result<()> {
// TODO: Some actual work goes here...
println!("================================================");
println!("Sending payment report to user {}", args.user_guid);
sleep(Duration::from_millis(2000)).await;
let all = users::Entity::find()
.all(&self.ctx.db)
.await
.map_err(Box::from)?;
for post in &all {
let notes = post.notes(&self.ctx.db).await;
println!("post: {} {:?}", post.id, notes);
}
println!("================================================");
Ok(())
}
}
@@ -0,0 +1 @@
pub mod downloader;
@@ -0,0 +1,32 @@
to: tests/fixtures/realistic/generated/controllers/{{name | snake_case }}.rs
injections:
- into: tests/fixtures/realistic/generated/controllers/mod.rs
append: true
content: "pub mod {{ name | snake_case }};"
- into: tests/fixtures/realistic/generated/app.rs
after: "AppRoutes::"
content: " .add_route(controllers::{{ name | snake_case }}::routes())"
---
#![allow(clippy::unused_async)]
use axum::{extract::State, routing::get};
use rustyrails::{
app::AppContext,
controller::{format, Routes},
Result,
};
pub async fn echo(req_body: String) -> String {
req_body
}
pub async fn hello(State(ctx): State<AppContext>) -> Result<String> {
// do something with context (database, etc)
format::text("hello")
}
pub fn routes() -> Routes {
Routes::new()
.prefix("{{ name | snake_case }}")
.add("/", get(hello))
.add("/echo", get(echo))
}
@@ -0,0 +1,51 @@
use std::path::Path;
use async_trait::async_trait;
use rustyrails::{
app::{AppContext, Hooks},
controller::AppRoutes,
db::{self, truncate_table},
task::Tasks,
worker::{AppWorker, Processor},
Result,
};
use sea_orm::DatabaseConnection;
use crate::{
controllers,
models::_entities::{notes, users},
tasks::user_report::UserReport,
workers::downloader::DownloadWorker,
};
pub struct App;
#[async_trait]
impl Hooks for App {
fn routes() -> AppRoutes {
AppRoutes::with_default_routes()
.add_route(controllers::email_stats::routes())
.add_route(controllers::notes::routes())
.add_route(controllers::auth::routes())
.add_route(controllers::user::routes())
}
fn connect_workers<'a>(p: &'a mut Processor, ctx: &'a AppContext) {
p.register(DownloadWorker::build(ctx));
}
fn register_tasks(tasks: &mut Tasks) {
tasks.register(tasks::email_stats::EmailStats);
tasks.register(UserReport);
}
async fn truncate(db: &DatabaseConnection) -> Result<()> {
truncate_table(db, users::Entity).await?;
truncate_table(db, notes::Entity).await?;
Ok(())
}
async fn seed(db: &DatabaseConnection, base: &Path) -> Result<()> {
db::seed::<users::ActiveModel>(db, &base.join("users.yaml").display().to_string()).await?;
Ok(())
}
}
@@ -0,0 +1,8 @@
use blo::app::App;
use migration::Migrator;
use rustyrails::cli;
#[tokio::main]
async fn main() -> eyre::Result<()> {
cli::main::<App, Migrator>().await
}
@@ -0,0 +1,62 @@
use axum::{extract::State, routing::post, Json};
use rustyrails::{
app::AppContext,
controller::{format, unauthorized, Routes},
Result,
};
use crate::{
mailers::auth::AuthMailer,
models::users::{self, LoginParams, RegisterParams},
views::auth::LoginResponse,
};
async fn register(
State(ctx): State<AppContext>,
Json(params): Json<RegisterParams>,
) -> Result<Json<()>> {
let res = users::Model::create_with_password(&ctx.db, &params).await;
let user = match res {
Ok(user) => user,
Err(err) => {
tracing::info!(
message = err.to_string(),
user_email = &params.email,
"could not register user",
);
return format::json(());
}
};
// TODO:: send website base uri
AuthMailer::send_welcome(&ctx, &user.email).await.unwrap();
format::json(())
}
async fn login(
State(ctx): State<AppContext>,
Json(params): Json<LoginParams>,
) -> Result<Json<LoginResponse>> {
let user = users::Model::find_by_email(&ctx.db, &params.email).await?;
let valid = user.verify_password(&params.password)?;
if !valid {
return unauthorized("unauthorized access");
}
let token = user
.generate_jwt(&ctx.config.auth.secret, &ctx.config.auth.expiration)
.or_else(|_| unauthorized("unauthorized!"))?;
format::json(LoginResponse::new(&user, &token))
}
pub fn routes() -> Routes {
Routes::new()
.prefix("auth")
.add("/register", post(register))
.add("/login", post(login))
}
@@ -0,0 +1,23 @@
#![allow(clippy::unused_async)]
use axum::{extract::State, routing::get};
use rustyrails::{
app::AppContext,
controller::{format, Routes},
Result,
};
pub async fn echo(req_body: String) -> String {
req_body
}
pub async fn hello(State(ctx): State<AppContext>) -> Result<String> {
// do something with context (database, etc)
format::text("hello")
}
pub fn routes() -> Routes {
Routes::new()
.prefix("email_stats")
.add("/", get(hello))
.add("/echo", get(echo))
}
@@ -0,0 +1,5 @@
pub mod auth;
pub mod notes;
pub mod user;
pub mod email_stats;
@@ -0,0 +1,80 @@
#![allow(clippy::unused_async)]
use axum::{extract::State, routing::get};
use rustyrails::{
app::AppContext,
controller::{format, Routes},
errors::Error,
worker::AppWorker,
Result,
};
use sea_orm::EntityTrait;
// user imports
use crate::{
mailers::auth::AuthMailer,
models::_entities::users,
workers::downloader::{DownloadWorker, DownloadWorkerArgs},
};
/// Benchmark function for a simple hello-world endpoint.
///
/// # Errors
///
/// Errors related to formatting the response.
pub async fn bench_hello(_req_body: String) -> Result<String> {
format::text("hello")
}
/// Benchmark function for database operations.
///
/// This function is used to benchmark database operations by performing a
/// simple query to retrieve a user entity by ID. It utilizes the
/// `Entity::find_by_id` method provided by the `users` module.
///
/// # Errors
///
/// When db query fails
pub async fn bench_db(State(ctx): State<AppContext>) -> Result<()> {
let _ = users::Entity::find_by_id(1).one(&ctx.db).await?;
format::empty()
}
/// return echo message
pub async fn echo(req_body: String) -> String {
req_body
}
/// A simple endpoint for demonstrating asynchronous tasks in a web application.
///
/// # Errors
///
/// when send welcome message fails or there is an error when preform download
/// task
pub async fn hello(State(ctx): State<AppContext>) -> Result<String> {
DownloadWorker::perform_later(
&ctx,
DownloadWorkerArgs {
user_guid: "foo".to_string(),
},
)
.await
.map_err(|e| {
tracing::error!(
error = e.to_string(),
"could not perform the download worker"
);
Error::Any("could not perform the download worker ".into())
})?;
AuthMailer::send_welcome(&ctx, "foobar").await?;
format::text("hello")
}
pub fn routes() -> Routes {
Routes::new()
.add("/", get(hello))
.add("/echo", get(echo))
.add("/bench_db", get(bench_db))
.add("/bench_hello", get(bench_hello))
}
@@ -0,0 +1,20 @@
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))
}
@@ -0,0 +1,15 @@
---
- id: 1
pid: 11111111-1111-1111-1111-111111111111
email: user1@example.com
password: "$2b$12$gf4o2FShIahg/GY6YkK2wOcs8w4.lu444wP6BL3FyjX0GsxnEV6ZW"
name: user1
created_at: "2023-11-12T12:34:56.789"
updated_at: "2023-11-12T12:34:56.789"
- id: 2
pid: 22222222-2222-2222-2222-222222222222
email: user2@example.com
password: "$2b$12$gf4o2FShIahg/GY6YkK2wOcs8w4.lu444wP6BL3FyjX0GsxnEV6ZW"
name: user2
created_at: "2023-11-12T12:34:56.789"
updated_at: "2023-11-12T12:34:56.789"
@@ -0,0 +1,7 @@
pub mod app;
pub mod controllers;
pub mod mailers;
pub mod models;
pub mod tasks;
pub mod views;
pub mod workers;
@@ -0,0 +1,40 @@
// auth mailer
#![allow(non_upper_case_globals)]
use include_dir::{include_dir, Dir};
use rustyrails::{
app::AppContext,
mailer::{Args, Mailer},
Result,
};
use serde_json::json;
static welcome: Dir<'_> = include_dir!("src/mailers/auth/welcome");
// #[derive(Mailer)] // -- disabled for faster build speed. it works. but lets
// move on for now.
#[allow(clippy::module_name_repetitions)]
pub struct AuthMailer {}
impl Mailer for AuthMailer {}
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: "foo@example.com".to_string(),
locals: json!({
"name": "joe"
}),
..Default::default()
},
)
.await?;
Ok(())
}
}
@@ -0,0 +1,8 @@
;<html>
<body>
You can
<a href="http://localhost:/reset/<%= resetToken %>">reset your password</a>
</body>
</html>
@@ -0,0 +1 @@
Your reset password link
@@ -0,0 +1,3 @@
Reset your password with this link:
http://localhost/reset/<%= resetToken %>
@@ -0,0 +1,10 @@
;<html>
<body>
You can{' '}
<a href="http://localhost:/verify/<%= verifyToken %>">
verify your account
</a>
</body>
</html>
@@ -0,0 +1 @@
Welcome <%= name %>
@@ -0,0 +1,4 @@
Welcome <%= name %>, you can now log in.
Verify your account with the link below:
http://localhost/verify/<%= verifyToken %>
@@ -0,0 +1 @@
pub mod auth;
@@ -0,0 +1,6 @@
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.4
pub mod prelude;
pub mod notes;
pub mod users;
@@ -0,0 +1,37 @@
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.4
use sea_orm::entity::prelude::*;
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
#[sea_orm(table_name = "notes")]
pub struct Model {
pub created_at: DateTime,
pub updated_at: DateTime,
#[sea_orm(primary_key)]
pub id: i32,
#[sea_orm(unique)]
pub pid: Uuid,
pub title: Option<String>,
pub content: Option<String>,
pub owner_id: i32,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
#[sea_orm(
belongs_to = "super::users::Entity",
from = "Column::OwnerId",
to = "super::users::Column::Id",
on_update = "Cascade",
on_delete = "Cascade"
)]
Users,
}
impl Related<super::users::Entity> for Entity {
fn to() -> RelationDef {
Relation::Users.def()
}
}
impl ActiveModelBehavior for ActiveModel {}
@@ -0,0 +1,3 @@
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.4
pub use super::{notes::Entity as Notes, users::Entity as Users};
@@ -0,0 +1,33 @@
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.4
use sea_orm::entity::prelude::*;
use serde::Deserialize;
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Deserialize)]
#[sea_orm(table_name = "users")]
pub struct Model {
pub created_at: DateTime,
pub updated_at: DateTime,
#[sea_orm(primary_key)]
pub id: i32,
#[sea_orm(unique)]
pub pid: Uuid,
#[sea_orm(unique)]
pub email: String,
pub password: String,
pub name: String,
pub reset_token: Option<String>,
pub reset_sent_at: Option<DateTime>,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
#[sea_orm(has_many = "super::notes::Entity")]
Notes,
}
impl Related<super::notes::Entity> for Entity {
fn to() -> RelationDef {
Relation::Notes.def()
}
}
@@ -0,0 +1,2 @@
pub mod _entities;
pub mod users;
@@ -0,0 +1,173 @@
// TODO(review): base components must be re-exported
use rustyrails::{
auth,
model::{ModelError, ModelResult},
validation,
validator::Validate,
};
use sea_orm::{
entity::prelude::*, ActiveValue, DatabaseConnection, DbErr, ModelTrait, TransactionTrait,
};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
pub use super::_entities::users::{self, ActiveModel, Entity, Model};
#[derive(Debug, Deserialize, Serialize)]
pub struct LoginParams {
pub email: String,
pub password: String,
}
#[derive(Debug, Deserialize, Serialize)]
pub struct RegisterParams {
pub email: String,
pub password: String,
pub name: String,
}
#[derive(Debug, Validate, Deserialize)]
pub struct ModelValidator {
#[validate(length(min = 2, message = "Name must be at least 2 characters long."))]
pub name: String,
#[validate(custom = "validation::is_valid_email")]
pub email: String,
}
impl From<&ActiveModel> for ModelValidator {
fn from(value: &ActiveModel) -> Self {
Self {
name: value.name.as_ref().to_string(),
email: value.email.as_ref().to_string(),
}
}
}
#[async_trait::async_trait]
impl ActiveModelBehavior for super::_entities::users::ActiveModel {
async fn before_save<C>(self, _db: &C, insert: bool) -> Result<Self, DbErr>
where
C: ConnectionTrait,
{
{
self.validate()?;
if insert {
let mut this = self;
this.pid = ActiveValue::Set(Uuid::new_v4());
Ok(this)
} else {
Ok(self)
}
}
}
}
impl super::_entities::users::Model {
/// .
///
/// # Errors
///
/// This function will return an error if .
pub async fn notes(
&self,
db: &DatabaseConnection,
) -> Result<Vec<super::_entities::notes::Model>, DbErr> {
self.find_related(super::_entities::prelude::Notes)
.all(db)
.await
}
/// .
///
/// # Errors
///
/// .
pub async fn find_by_email(db: &DatabaseConnection, email: &str) -> ModelResult<Self> {
let user = users::Entity::find()
.filter(users::Column::Email.eq(email))
.one(db)
.await?;
user.ok_or_else(|| ModelError::EntityNotFound)
}
/// .
///
/// # Errors
///
/// .
pub async fn find_by_pid(db: &DatabaseConnection, pid: &str) -> ModelResult<Self> {
let parse_uuid = Uuid::parse_str(pid).map_err(|e| ModelError::Message(e.to_string()))?;
let user = users::Entity::find()
.filter(users::Column::Pid.eq(parse_uuid))
.one(db)
.await?;
user.ok_or_else(|| ModelError::EntityNotFound)
}
/// .
///
/// # Errors
///
/// .
pub fn verify_password(&self, password: &str) -> ModelResult<bool> {
Ok(auth::verify_password(password, &self.password)?)
}
/// .
///
/// # Errors
///
/// .
pub async fn create_with_password(
db: &DatabaseConnection,
params: &RegisterParams,
) -> ModelResult<Self> {
let txn = db.begin().await?;
if users::Entity::find()
.filter(users::Column::Email.eq(&params.email))
.one(&txn)
.await?
.is_some()
{
return Err(ModelError::EntityExists {});
}
let password_hash = auth::hash_password(&params.password)?;
let user = users::ActiveModel {
//TODO(review): there might be a 'trick' to moves between params and partial
// ActiveValue values to save this rhs-lhs coding
email: ActiveValue::set(params.email.to_string()),
password: ActiveValue::set(password_hash),
name: ActiveValue::set(params.name.to_string()),
..Default::default()
}
.insert(&txn)
.await?;
txn.commit().await?;
Ok(user)
}
/// .
///
/// # Errors
///
/// .
pub fn generate_jwt(&self, secret: &str, expiration: &u64) -> ModelResult<String> {
Ok(auth::JWT::new(secret).generate_token(expiration, self.pid.to_string())?)
}
}
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)
}
}
@@ -0,0 +1,26 @@
use std::collections::BTreeMap;
use async_trait::async_trait;
use rustyrails::{
app::AppContext,
task::{Task, TaskInfo},
Result,
};
use sea_orm::EntityTrait;
pub struct EmailStats;
#[async_trait]
impl Task for EmailStats {
fn task(&self) -> TaskInfo {
TaskInfo {
name: "email_stats".to_string(),
detail: "a sample task".to_string(),
}
}
async fn run(&self, _app_context: &AppContext, _vars: &BTreeMap<String, String>) -> Result<()> {
println!("hello email_stats");
Ok(())
}
}
@@ -0,0 +1,3 @@
pub mod user_report;
pub mod email_stats;
@@ -0,0 +1,33 @@
use std::collections::BTreeMap;
use async_trait::async_trait;
use rustyrails::{
app::AppContext,
task::{Task, TaskInfo},
Result,
};
use sea_orm::EntityTrait;
use crate::models::_entities::users;
pub struct UserReport;
#[async_trait]
impl Task for UserReport {
fn task(&self) -> TaskInfo {
TaskInfo {
name: "user_report".to_string(),
detail: "output a user report".to_string(),
}
}
async fn run(&self, app_context: &AppContext, vars: &BTreeMap<String, String>) -> Result<()> {
let users = users::Entity::find().all(&app_context.db).await?;
println!("args: {vars:?}");
println!("!!! user_report: listing users !!!");
println!("------------------------");
for user in &users {
println!("user: {}", user.email);
}
println!("done: {} users", users.len());
Ok(())
}
}
@@ -0,0 +1,21 @@
use serde::{Deserialize, Serialize};
use crate::models::_entities::users;
#[derive(Debug, Deserialize, Serialize)]
pub struct LoginResponse {
pub token: String,
pub pid: String,
pub name: String,
}
impl LoginResponse {
#[must_use]
pub fn new(user: &users::Model, token: &String) -> Self {
Self {
token: token.to_string(),
pid: user.pid.to_string(),
name: user.name.clone(),
}
}
}
@@ -0,0 +1,2 @@
pub mod auth;
pub mod user;
@@ -0,0 +1,21 @@
use serde::{Deserialize, Serialize};
use crate::models::_entities::users;
#[derive(Debug, Deserialize, Serialize)]
pub struct CurrentResponse {
pub pid: String,
pub name: String,
pub email: String,
}
impl CurrentResponse {
#[must_use]
pub fn new(user: &users::Model) -> Self {
Self {
pid: user.pid.to_string(),
name: user.name.clone(),
email: user.email.clone(),
}
}
}
@@ -0,0 +1,49 @@
use std::time::Duration;
use async_trait::async_trait;
use rustyrails::{
app::AppContext,
worker::{AppWorker, Result, Worker},
};
use sea_orm::EntityTrait;
use serde::{Deserialize, Serialize};
use tokio::time::sleep;
use crate::models::users;
pub struct DownloadWorker {
pub ctx: AppContext,
}
#[derive(Deserialize, Debug, Serialize)]
pub struct DownloadWorkerArgs {
pub user_guid: String,
}
impl AppWorker<DownloadWorkerArgs> for DownloadWorker {
fn build(ctx: &AppContext) -> Self {
Self { ctx: ctx.clone() }
}
}
#[async_trait]
impl Worker<DownloadWorkerArgs> for DownloadWorker {
async fn perform(&self, args: DownloadWorkerArgs) -> Result<()> {
// TODO: Some actual work goes here...
println!("================================================");
println!("Sending payment report to user {}", args.user_guid);
sleep(Duration::from_millis(2000)).await;
let all = users::Entity::find()
.all(&self.ctx.db)
.await
.map_err(Box::from)?;
for post in &all {
let notes = post.notes(&self.ctx.db).await;
println!("post: {} {:?}", post.id, notes);
}
println!("================================================");
Ok(())
}
}
@@ -0,0 +1 @@
pub mod downloader;
@@ -0,0 +1,35 @@
to: tests/fixtures/realistic/generated/tasks/{{name | snake_case }}.rs
injections:
- into: tests/fixtures/realistic/generated/tasks/mod.rs
append: true
content: "pub mod {{ name | snake_case }};"
- into: tests/fixtures/realistic/generated/app.rs
after: "fn register_tasks"
content: " tasks.register(tasks::{{ name | snake_case }}::{{ name | pascal_case }});"
---
use std::collections::BTreeMap;
use async_trait::async_trait;
use rustyrails::{
app::AppContext,
task::{Task, TaskInfo},
Result,
};
use sea_orm::EntityTrait;
pub struct {{ name | pascal_case }};
#[async_trait]
impl Task for {{ name | pascal_case }} {
fn task(&self) -> TaskInfo {
TaskInfo {
name: "{{ name }}".to_string(),
detail: "a sample task".to_string(),
}
}
async fn run(&self, _app_context: &AppContext, _vars: &BTreeMap<String, String>) -> Result<()> {
println!("hello {{ name }}");
Ok(())
}
}
@@ -0,0 +1,6 @@
this is a mod file
and this is another line
pub class Foobar {
blah
}
@@ -0,0 +1,9 @@
this is a mod file
and this is another line
Hello {
// should not be modified
}
pub class Foobar {
blah
}
@@ -0,0 +1,2 @@
this is a mod file
and this is another line
@@ -0,0 +1,6 @@
this is a mod file
and this is another line
pub class Foobar {
blah
}
@@ -0,0 +1,13 @@
this is a mod file
and this is another line
vec![
// should not be modified
]
pub class Foobar {
blah
vec![
1,
2,
]
}
@@ -0,0 +1,2 @@
this is a mod file
and this is another line
@@ -0,0 +1,5 @@
this is a mod file
Delete this line
foo
Delete this line
bar
@@ -0,0 +1,3 @@
this file should be skipped
yep.
@@ -0,0 +1,7 @@
this is a mod file
and this is another line
pub class Foobar {
field: integer
blah
}
@@ -0,0 +1,10 @@
this is a mod file
and this is another line
Hello {
// should not be modified
}
pub class Foobar {
field: integer
blah
}
@@ -0,0 +1,4 @@
this is a mod file
and this is another line
this was appended
@@ -0,0 +1,7 @@
this is a mod file
and this is another line
// doc comment
pub class Foobar {
blah
}
@@ -0,0 +1,14 @@
this is a mod file
and this is another line
vec![
// should not be modified
]
pub class Foobar {
blah
vec![
1,
2,
before-last
]
}
@@ -0,0 +1,4 @@
hello, this is the file body.
variable: Post
@@ -0,0 +1,3 @@
this was prepended
this is a mod file
and this is another line
@@ -0,0 +1,3 @@
this is a mod file
foo
bar
@@ -0,0 +1,3 @@
this file should be skipped
yep.
@@ -0,0 +1,33 @@
{% set something = 1 -%}
to: tests/fixtures/test1/generated/{{name}}.txt
injections:
- into: tests/fixtures/test1/generated/prepend.txt
prepend: true
content: "this was prepended"
- into: tests/fixtures/test1/generated/append.txt
append: true
content: "this was appended"
- into: tests/fixtures/test1/generated/skipped.txt
skip_if: "be skipped"
append: true
content: "this was appended"
- into: tests/fixtures/test1/generated/before.txt
content: "// doc comment"
before: "pub class"
- into: tests/fixtures/test1/generated/before_last.txt
content: "before-last"
before_last: "\\]"
- into: tests/fixtures/test1/generated/after.txt
content: "field: integer"
after: "pub class"
- into: tests/fixtures/test1/generated/after_last.txt
content: "field: integer"
after_last: "\\{"
- into: tests/fixtures/test1/generated/remove_lines.txt
content: ""
remove_lines: "Delete this line"
---
hello, this is the file body.
variable: {{ name | pascal_case }}
+96
View File
@@ -0,0 +1,96 @@
#![allow(non_snake_case)]
use std::fs;
use fs_extra::{self, dir::CopyOptions};
use rrgen::RRgen;
use serde_json::json;
#[test]
fn test_generate() {
let FROM = "tests/fixtures/test1/app";
let GENERATED = "tests/fixtures/test1/generated";
let vars = json!({"name": "post"});
fs_extra::dir::remove(GENERATED).unwrap();
fs_extra::dir::copy(
FROM,
GENERATED,
&CopyOptions {
copy_inside: true,
..Default::default()
},
)
.unwrap();
let rgen = RRgen::default();
rgen.generate(
&fs::read_to_string("tests/fixtures/test1/template.t").unwrap(),
&vars,
)
.unwrap();
assert!(!dir_diff::is_different(GENERATED, "tests/fixtures/test1/expected").unwrap());
}
#[test]
fn test_generate_with_working_dir() {
let tree_fs = tree_fs::TreeBuilder::default()
.drop(true)
.create()
.expect("create temp file");
let FROM = "tests/fixtures/test1/app";
let GENERATED = "tests/fixtures/test1/generated";
let vars = json!({"name": "post"});
fs_extra::dir::copy(
FROM,
tree_fs.root.join(GENERATED),
&CopyOptions {
copy_inside: true,
..Default::default()
},
)
.unwrap();
let rgen = RRgen::with_working_dir(&tree_fs.root);
rgen.generate(
&fs::read_to_string("tests/fixtures/test1/template.t").unwrap(),
&vars,
)
.unwrap();
assert!(!dir_diff::is_different(
tree_fs.root.join(GENERATED),
"tests/fixtures/test1/expected"
)
.unwrap());
}
#[test]
fn test_realistic() {
let FROM = "tests/fixtures/realistic/app";
let GENERATED = "tests/fixtures/realistic/generated";
let vars = json!({"name": "email_stats"});
fs_extra::dir::remove(GENERATED).unwrap();
fs_extra::dir::copy(
FROM,
GENERATED,
&CopyOptions {
copy_inside: true,
..Default::default()
},
)
.unwrap();
let rgen = RRgen::default();
rgen.generate(
&fs::read_to_string("tests/fixtures/realistic/controller.t").unwrap(),
&vars,
)
.unwrap();
rgen.generate(
&fs::read_to_string("tests/fixtures/realistic/task.t").unwrap(),
&vars,
)
.unwrap();
assert!(!dir_diff::is_different(GENERATED, "tests/fixtures/realistic/expected").unwrap());
}