#![allow(unused)]
fn main() {
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())
}
}
#![allow(unused)]
fn main() {
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);
}
}
#![allow(unused)]
fn main() {
#[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));
}
}
}
[debug]
port=8001