- TODO
Rocket People and groups
examples/rocket/people-and-groups/src/db.rs
use rocket::fairing::AdHoc; use serde::{Deserialize, Serialize}; use surrealdb::engine::remote::ws::Client; use surrealdb::engine::remote::ws::Ws; use surrealdb::opt::auth::Root; use surrealdb::opt::Resource; use surrealdb::sql::{Id, Thing}; use surrealdb::Surreal; const NAMESPACE: &str = "rust-slides"; const PERSON: &str = "person"; const GROUP: &str = "group"; const MEMBERSHIP: &str = "membership"; #[derive(Debug, Deserialize, Serialize, Clone, PartialEq)] pub struct Person { pub id: Thing, pub name: String, } #[derive(Debug, Deserialize, Serialize, Clone, PartialEq)] pub struct Group { pub id: Thing, pub name: String, pub owner: Thing, } #[derive(Debug, Deserialize, Serialize, Clone, PartialEq)] pub struct Membership { pub id: Thing, pub person: Thing, pub group: Thing, } #[derive(Debug, Deserialize, Serialize, Clone, PartialEq)] pub struct MembershipWithPersonAndGroup { pub id: Thing, pub person: Person, pub group: Group, } #[derive(Debug, Deserialize, Serialize, Clone, PartialEq)] pub struct GroupWithOwner { pub id: Thing, pub name: String, pub owner: Person, } /// # Panics /// /// Panics when it fails to connect to the database #[must_use] pub fn fairing(database: String) -> AdHoc { AdHoc::on_ignite("Managed Database Connection", |rocket| async { let address = "127.0.0.1:8000"; let username = "root"; let password = "root"; let dbh = Surreal::new::<Ws>(address).await.unwrap(); dbh.signin(Root { username, password }).await.unwrap(); dbh.use_ns(NAMESPACE).use_db(database).await.unwrap(); let query = format!( "DEFINE INDEX membership_ids ON TABLE {MEMBERSHIP} COLUMNS {PERSON}, {GROUP} UNIQUE" ); dbh.query(query).await.unwrap(); rocket.manage(dbh) }) } pub async fn clear(dbh: &Surreal<Client>, database: String) -> surrealdb::Result<()> { rocket::info!("Clearing database"); let result = dbh .query(format!("REMOVE DATABASE IF EXISTS `{database}`;")) .await?; result.check()?; Ok(()) } pub async fn add_person(dbh: &Surreal<Client>, name: &str) -> surrealdb::Result<Person> { let entry = Person { id: Thing::from((PERSON, Id::ulid())), name: name.to_owned(), }; dbh.create(Resource::from(PERSON)) .content(entry.clone()) .await?; Ok(entry) } pub async fn add_group(dbh: &Surreal<Client>, name: &str, uid: &str) -> surrealdb::Result<Group> { let entry = Group { id: Thing::from((GROUP, Id::ulid())), name: name.to_owned(), owner: Thing::from((PERSON, Id::from(uid))), }; dbh.create(Resource::from(GROUP)) .content(entry.clone()) .await?; Ok(entry) } pub async fn add_member(dbh: &Surreal<Client>, uid: &str, gid: &str) -> surrealdb::Result<()> { let entry = Membership { id: Thing::from((MEMBERSHIP, Id::ulid())), person: Thing::from((PERSON, Id::from(uid))), group: Thing::from((GROUP, Id::from(gid))), }; dbh.create(Resource::from(MEMBERSHIP)) .content(entry.clone()) .await?; Ok(()) } pub async fn get_memberships( dbh: &Surreal<Client>, ) -> surrealdb::Result<Vec<MembershipWithPersonAndGroup>> { let mut response = dbh .query("SELECT * FROM type::table($table) FETCH person, group") .bind(("table", MEMBERSHIP)) .await?; let entries: Vec<MembershipWithPersonAndGroup> = response.take(0)?; rocket::info!("Response: {:?}", entries); Ok(entries) } pub async fn get_memberships_of_person( dbh: &Surreal<Client>, uid: &str, ) -> surrealdb::Result<Vec<MembershipWithPersonAndGroup>> { let mut response = dbh .query("SELECT * FROM type::table($table) WHERE person=type::thing($person_table, $uid) FETCH person, group") .bind(("table", MEMBERSHIP)) .bind(("person_table", PERSON)) .bind(("uid", uid.to_owned())) .await?; let entries: Vec<MembershipWithPersonAndGroup> = response.take(0)?; rocket::info!("Response: {:?}", entries); Ok(entries) } pub async fn delete_membership(dbh: &Surreal<Client>, id: &str) -> surrealdb::Result<()> { let res = dbh .query("DELETE type::table($table) WHERE id=type::thing($table, $id)") .bind(("table", MEMBERSHIP)) .bind(("id", id.to_owned())) .await?; res.check()?; Ok(()) } pub async fn get_people(dbh: &Surreal<Client>) -> surrealdb::Result<Vec<Person>> { let mut response = dbh .query("SELECT * FROM type::table($table);") .bind(("table", PERSON)) .await?; let entries: Vec<Person> = response.take(0)?; Ok(entries) } pub async fn get_person(dbh: &Surreal<Client>, id: &str) -> surrealdb::Result<Option<Person>> { dbh.select((PERSON, id)).await } pub async fn get_person_with_groups( dbh: &Surreal<Client>, id: &str, ) -> surrealdb::Result<Option<(Person, Vec<Group>, Vec<MembershipWithPersonAndGroup>)>> { let person = get_person(dbh, id).await?; match person { None => Ok(None), Some(person) => { let groups = get_groups_by_owner(dbh, id).await?; let memberships = get_memberships_of_person(dbh, id).await?; Ok(Some((person, groups, memberships))) } } } pub async fn get_groups_by_owner( dbh: &Surreal<Client>, uid: &str, ) -> surrealdb::Result<Vec<Group>> { rocket::info!("Getting groups for {}", uid); let mut response = dbh .query( "SELECT * FROM type::table($group_table) WHERE owner=type::thing($user_table, $uid);", ) .bind(("group_table", GROUP)) .bind(("uid", uid.to_owned())) .bind(("user_table", PERSON)) .await?; let entries: Vec<Group> = response.take(0)?; Ok(entries) } pub async fn get_groups(dbh: &Surreal<Client>) -> surrealdb::Result<Vec<Group>> { let mut response = dbh.query(format!("SELECT * FROM {GROUP};")).await?; let entries: Vec<Group> = response.take(0)?; Ok(entries) } #[derive(Debug, Deserialize, Serialize, Clone, PartialEq)] struct GroupId { group: Thing, } pub async fn get_groups_not(dbh: &Surreal<Client>, uid: &str) -> surrealdb::Result<Vec<Group>> { let mut response = dbh .query(format!( "SELECT group FROM {MEMBERSHIP} WHERE person = type::thing($person_table, $uid)" )) .bind(("person_table", PERSON)) .bind(("uid", uid.to_owned())) .await?; let entries: Vec<GroupId> = response.take(0)?; println!("ids: {:?}", entries); let mut response = dbh.query(format!("SELECT * FROM {GROUP} WHERE {GROUP}.id IN (SELECT group FROM {MEMBERSHIP} WHERE person = type::thing($person_table, $uid))")) .bind(("person_table", PERSON)) .bind(("uid", uid.to_owned())).await?; let entries: Vec<Group> = response.take(0)?; Ok(entries) } pub async fn get_group(dbh: &Surreal<Client>, id: &str) -> surrealdb::Result<Option<Group>> { dbh.select((GROUP, id)).await } pub async fn get_group_with_owner( dbh: &Surreal<Client>, id: &str, ) -> surrealdb::Result<Option<GroupWithOwner>> { let mut response = dbh .query("SELECT * FROM type::table($group_table) WHERE id=type::thing($id) FETCH owner") .bind(("group_table", GROUP)) .bind(("id", format!("{GROUP}:{id}"))) .await?; let entries: Vec<GroupWithOwner> = response.take(0)?; rocket::info!("Response: {:?}", entries); Ok(entries.first().cloned()) }
examples/rocket/people-and-groups/src/mytera.rs
use std::collections::HashMap; use rocket_dyn_templates::tera::Tera; use tera::{to_value, Result, Value}; fn object2id(val: &Value, _map: &HashMap<String, Value>) -> Result<Value> { let s = val["id"]["String"].as_str().unwrap(); Ok(to_value(s).unwrap()) } pub fn customize(tera: &mut Tera) { tera.register_filter("object2id", object2id); }
examples/rocket/people-and-groups/src/main.rs
#[macro_use] extern crate rocket; use std::vec; use rocket::form::Form; use rocket::State; use rocket_dyn_templates::{context, Template}; use surrealdb::engine::remote::ws::Client; use surrealdb::Surreal; struct Config { database: String, } pub mod db; mod mytera; #[derive(FromForm)] struct AddPerson<'r> { name: &'r str, } #[derive(FromForm)] struct AddGroup<'r> { name: &'r str, uid: &'r str, } #[derive(FromForm)] struct AddMember<'r> { gid: &'r str, uid: &'r str, } // ---------------------------------- #[get("/")] async fn get_index(dbh: &State<Surreal<Client>>) -> Template { let people = db::get_people(dbh).await.unwrap(); let groups = db::get_groups(dbh).await.unwrap(); Template::render( "index", context! { title: "People and Groups", people: people, groups: groups, }, ) } #[get("/clear")] async fn clear_db(dbh: &State<Surreal<Client>>, config: &State<Config>) -> Template { rocket::info!("Clearing database {}", config.database); db::clear(dbh, config.database.clone()).await.unwrap(); Template::render( "database_cleared", context! { title: "Database cleared", }, ) } #[get("/people")] async fn get_people(dbh: &State<Surreal<Client>>) -> Template { let people = db::get_people(dbh).await.unwrap(); rocket::info!("People: {:?}", people); Template::render( "people", context! { title: "People", people, }, ) } #[post("/add-person", data = "<input>")] async fn post_add_person(dbh: &State<Surreal<Client>>, input: Form<AddPerson<'_>>) -> Template { let name = input.name.trim(); rocket::info!("Add person called '{name}'"); let person = db::add_person(dbh, name).await.unwrap(); Template::render( "person_added", context! { title: "Person added", person, }, ) } #[get("/person/<id>")] async fn get_person(dbh: &State<Surreal<Client>>, id: String) -> Option<Template> { if let Some((person, owned_groups, memberships)) = db::get_person_with_groups(dbh, &id).await.unwrap() { return Some(Template::render( "person", context! { title: person.name.clone(), person, owned_groups, memberships, }, )); } None } #[get("/groups")] async fn get_groups(dbh: &State<Surreal<Client>>) -> Template { let groups = db::get_groups(dbh).await.unwrap(); rocket::info!("Groups: {:?}", groups); Template::render( "groups", context! { title: "Groups", groups, }, ) } #[get("/add-group?<uid>")] async fn get_add_group(dbh: &State<Surreal<Client>>, uid: String) -> Template { let person = db::get_person(dbh, &uid).await.unwrap().unwrap(); Template::render( "add_group", context! { title: format!("Add Group owned by {}", person.name), uid: uid.to_string(), }, ) } #[post("/add-group", data = "<input>")] async fn post_add_group(dbh: &State<Surreal<Client>>, input: Form<AddGroup<'_>>) -> Template { let name = input.name.trim(); let uid = input.uid.trim(); rocket::info!("Add group called '{name}' to user '{uid}'"); let group = db::add_group(dbh, name, uid).await.unwrap(); let person = db::get_person(dbh, uid).await.unwrap().unwrap(); Template::render( "group_added", context! { title: format!("Group called {} owned by {} was added", name, person.name), uid: uid.to_string(), group, }, ) } #[get("/add-membership?<uid>")] async fn get_add_membership(dbh: &State<Surreal<Client>>, uid: String) -> Template { let person = db::get_person(dbh, &uid).await.unwrap().unwrap(); //let groups = db::get_groups(dbh).await.unwrap(); let groups = db::get_groups_not(dbh, &uid).await.unwrap(); //let memberships = db::get_memberships_of_person(dbh, &person.id.to_string()).await.unwrap(); // remove the groups that the person already owns or is a member of let groups = groups .into_iter() .filter(|group| group.owner.id != person.id.id) .collect::<Vec<_>>(); Template::render( "add_membership", context! { title: format!("Add {} to one of the groups as a member", person.name), uid: uid.to_string(), groups }, ) } #[post("/add-membership", data = "<input>")] async fn post_add_membership(dbh: &State<Surreal<Client>>, input: Form<AddMember<'_>>) -> Template { let gid = input.gid.trim(); let uid = input.uid.trim(); rocket::info!("Add person '{uid}' to group '{gid}'"); let group = db::get_group(dbh, gid).await.unwrap().unwrap(); let person = db::get_person(dbh, uid).await.unwrap().unwrap(); db::add_member(dbh, uid, gid).await.unwrap(); Template::render( "added_to_group", context! { title: format!("'{}' was added to group '{}'", person.name, group.name), person, group, }, ) } #[get("/delete-membership?<id>")] async fn delete_membership(dbh: &State<Surreal<Client>>, id: String) -> Template { db::delete_membership(dbh, &id).await.unwrap(); Template::render( "membership_deleted", context! { title: "Membership deleted", }, ) } #[get("/memberships")] async fn get_membership(dbh: &State<Surreal<Client>>) -> Template { let memberships = db::get_memberships(dbh).await.unwrap(); Template::render( "memberships", context! { title: "Memberships", memberships }, ) } #[get("/group/<id>")] async fn get_group(dbh: &State<Surreal<Client>>, id: String) -> Option<Template> { if let Some(group) = db::get_group_with_owner(dbh, &id).await.unwrap() { rocket::info!("Group: {}", group.owner.id); rocket::info!("Group: {:?}", group); return Some(Template::render( "group", context! { title: group.name.clone(), group, }, )); } None } #[launch] fn rocket() -> _ { start(String::from("people-and-groups")) } fn start(database: String) -> rocket::Rocket<rocket::Build> { rocket::build() .mount( "/", routes![ clear_db, delete_membership, get_add_membership, get_index, get_people, get_person, get_groups, get_group, get_membership, get_add_group, post_add_group, post_add_membership, post_add_person, ], ) .manage(Config { database: database.clone(), }) .attach(Template::custom(|engines| { mytera::customize(&mut engines.tera); })) .attach(db::fairing(database)) } #[cfg(test)] mod tests { use super::start; use rocket::http::{ContentType, Status}; use rocket::local::blocking::Client; //use scraper::{Html, Selector}; #[test] fn test_main() { let client = Client::tracked(start(String::from("test-people-and-groups"))).unwrap(); let response = client.get("/clear").dispatch(); assert_eq!(response.status(), Status::Ok); // Make sure we can clear the database even if it does not exist let response = client.get("/clear").dispatch(); assert_eq!(response.status(), Status::Ok); let response = client.get("/").dispatch(); assert_eq!(response.status(), Status::Ok); let html = response.into_string().unwrap(); assert!(html.contains(r#"<title>People and Groups</title>"#)); assert!(html.contains(r#"<div>Number of people: 0</div>"#)); assert!(html.contains(r#"<div>Number of groups: 0</div>"#)); let response = client .post("/add-person") .header(ContentType::Form) .body("name=John") .dispatch(); assert_eq!(response.status(), Status::Ok); let html = response.into_string().unwrap(); assert!(html.contains(r#"Person added:"#)); let document = scraper::Html::parse_document(&html); let selector = scraper::Selector::parse("#added").unwrap(); assert_eq!( &document.select(&selector).next().unwrap().inner_html(), "John" ); let john_path = &document .select(&selector) .next() .unwrap() .attr("href") .unwrap(); let john_id = john_path.split('/').nth(2).unwrap(); let response = client .post("/add-person") .header(ContentType::Form) .body("name=Mary Ann") .dispatch(); assert_eq!(response.status(), Status::Ok); let html = response.into_string().unwrap(); assert!(html.contains(r#"Person added:"#)); let document = scraper::Html::parse_document(&html); let selector = scraper::Selector::parse("#added").unwrap(); assert_eq!( &document.select(&selector).next().unwrap().inner_html(), "Mary Ann" ); let mary_ann_path = &document .select(&selector) .next() .unwrap() .attr("href") .unwrap(); let mary_ann_id = mary_ann_path.split('/').nth(2).unwrap(); let response = client.get("/people").dispatch(); assert_eq!(response.status(), Status::Ok); let html = response.into_string().unwrap(); assert!(html.contains(r#">John</a></li>"#)); assert!(html.contains(r#">Mary Ann</a></li>"#)); let response = client.get("/").dispatch(); assert_eq!(response.status(), Status::Ok); let html = response.into_string().unwrap(); assert!(html.contains(r#"<title>People and Groups</title>"#)); assert!(html.contains(r#"<div>Number of people: 2</div>"#)); assert!(html.contains(r#"<div>Number of groups: 0</div>"#)); let response = client.get(john_path.to_string()).dispatch(); assert_eq!(response.status(), Status::Ok); let html = response.into_string().unwrap(); assert!(html.contains(r#"<h2>Name: John</h2>"#)); let expected = format!(r#"<div><a href="/add-group?uid={john_id}">add group</a></div>"#); assert!(html.contains(&expected)); let response = client.get(format!("/add-group?uid={john_id}")).dispatch(); assert_eq!(response.status(), Status::Ok); let html = response.into_string().unwrap(); assert!(html.contains(r#"<title>Add Group owned by John</title>"#)); let response = client .post("/add-group") .header(ContentType::Form) .body(format!("name=group one&uid={mary_ann_id}")) .dispatch(); assert_eq!(response.status(), Status::Ok); let html = response.into_string().unwrap(); println!("html: {}", html); assert!(html.contains(r#"<h1>Group called group one owned by Mary Ann was added</h1>"#)); assert!(html.contains(r#"Group added:"#)); let document = scraper::Html::parse_document(&html); let selector = scraper::Selector::parse("#added").unwrap(); assert_eq!( &document.select(&selector).next().unwrap().inner_html(), "group one" ); let group_one_path = &document .select(&selector) .next() .unwrap() .attr("href") .unwrap(); let _group_one_id = group_one_path.split('/').nth(2).unwrap(); let response = client.get(group_one_path.to_string()).dispatch(); assert_eq!(response.status(), Status::Ok); let html = response.into_string().unwrap(); assert!(html.contains(r#"<h2>Name: group one</h2>"#)); let expected = format!(r#"<h2>Owner name: <a href="/person/{mary_ann_id}">Mary Ann</a></h2>"#); assert!(html.contains(&expected)); } }
examples/rocket/people-and-groups/Rocket.toml
[debug] port=8001