Compare commits

..

3 Commits

Author SHA1 Message Date
jeff 0751a783dc Basic albeit mostly broken flow for creating new character. 2020-12-08 22:28:27 +00:00
jeff 92301fb1d4 Dynamically deserialize character data. 2020-12-08 22:28:27 +00:00
jeff 44dded7f28 Switch character data type to an enum, including schema patch.
Schema patch required because the diesel enum derive crate does not
seem to deal with print-schema for sqlite.
2020-12-08 22:28:27 +00:00
12 changed files with 295 additions and 91 deletions

23
Cargo.lock generated
View File

@ -342,6 +342,18 @@ dependencies = [
"r2d2", "r2d2",
] ]
[[package]]
name = "diesel-derive-enum"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "703e71c268ea2d8da9c0ab0b40d8b217179ee622209c170875d24443193a0dfb"
dependencies = [
"heck",
"proc-macro2 1.0.24",
"quote 1.0.7",
"syn 1.0.53",
]
[[package]] [[package]]
name = "diesel_derives" name = "diesel_derives"
version = "1.4.1" version = "1.4.1"
@ -368,6 +380,15 @@ version = "1.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457"
[[package]]
name = "erased-serde"
version = "0.3.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ca8b296792113e1500fd935ae487be6e00ce318952a6880555554824d6ebf38"
dependencies = [
"serde",
]
[[package]] [[package]]
name = "error-chain" name = "error-chain"
version = "0.12.4" version = "0.12.4"
@ -1436,6 +1457,8 @@ name = "tenebrous-sheets"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"diesel", "diesel",
"diesel-derive-enum",
"erased-serde",
"log 0.4.11", "log 0.4.11",
"prost", "prost",
"prost-build", "prost-build",

View File

@ -13,7 +13,9 @@ prost = "0.6"
serde = "1.0" serde = "1.0"
serde_derive = "1.0" serde_derive = "1.0"
serde_json = "1.0" serde_json = "1.0"
erased-serde = "0.3"
diesel = "1.4" diesel = "1.4"
diesel-derive-enum = { version = "1", features = ["sqlite"] }
thiserror = "1.0" thiserror = "1.0"
rust-argon2 = "0.8" rust-argon2 = "0.8"
log = "0.4" log = "0.4"

View File

@ -3,3 +3,5 @@
[print_schema] [print_schema]
file = "src/schema.rs" file = "src/schema.rs"
import_types = ["diesel::sql_types::*", "crate::models::characters::*"]
patch_file = "src/schema.patch"

View File

@ -3,7 +3,7 @@ CREATE TABLE characters(
user_id INTEGER NOT NULL, user_id INTEGER NOT NULL,
viewable BOOLEAN NOT NULL, viewable BOOLEAN NOT NULL,
character_name TEXT NOT NULL, character_name TEXT NOT NULL,
data_type TEXT NOT NULL, data_type TEXT CHECK(data_type IN ('chronicles_of_darkness_v1', 'changeling_v1')) NOT NULL,
data_version INTEGER NOT NULL, data_version INTEGER NOT NULL,
data BLOB NOT NULL data BLOB NOT NULL
); );

View File

@ -1,7 +1,17 @@
use crate::errors::Error;
use crate::models::proto::cofd::*;
use crate::models::users::User; use crate::models::users::User;
use crate::schema::characters; use crate::schema::characters;
use diesel_derive_enum::DbEnum;
use serde_derive::Serialize; use serde_derive::Serialize;
/// Dynamic character data is an opaque container type that holds
/// successfully deserialized character data protobuf object of any
/// type. It does not know what kind of type it has. This is a
/// semantically more appropriate name for what is returned from the
/// dyn_deserialize function.
pub(crate) type DynCharacterData = dyn erased_serde::Serialize;
/// Control system visibility of a character for a particular user. /// Control system visibility of a character for a particular user.
/// Implemented as a trait because there are multiple character /// Implemented as a trait because there are multiple character
/// structs that need this. /// structs that need this.
@ -29,6 +39,12 @@ pub(crate) trait Visibility {
} }
} }
#[derive(DbEnum, Debug, Serialize, PartialEq)]
pub enum CharacterDataType {
ChroniclesOfDarknessV1,
ChangelingV1,
}
/// An entry that appears in a user's character list. Properties are /// An entry that appears in a user's character list. Properties are
/// in order of table columns. /// in order of table columns.
#[derive(Serialize, Debug, Queryable)] #[derive(Serialize, Debug, Queryable)]
@ -37,7 +53,7 @@ pub struct Character {
pub user_id: i32, pub user_id: i32,
pub viewable: bool, pub viewable: bool,
pub character_name: String, pub character_name: String,
pub data_type: String, pub data_type: CharacterDataType,
pub data_version: i32, pub data_version: i32,
pub data: Vec<u8>, pub data: Vec<u8>,
} }
@ -52,13 +68,50 @@ impl Visibility for Character {
} }
} }
impl Character {
/// Attempt to deserialize the character's data into the given
/// type, which must be one of the protobuf types.
pub fn try_deserialize<T>(&self) -> Result<T, Error>
where
T: prost::Message + std::default::Default,
{
let decoded = T::decode(self.data.as_ref())?;
Ok(decoded)
}
/// Attempt to deserialize the character's data based on its
/// stored type, but return the deserialized protobuf type as a
/// trait object. Primarily used for passing character sheets to
/// templates or other places (like a REST API).
pub fn dyn_deserialize(&self) -> Result<Box<DynCharacterData>, Error> {
use CharacterDataType::*;
let decoded: Box<dyn erased_serde::Serialize> = match self.data_type {
ChroniclesOfDarknessV1 => Box::new(self.try_deserialize::<CofdSheet>()?),
ChangelingV1 => Box::new(self.try_deserialize::<ChangelingSheet>()?),
};
Ok(decoded)
}
/// Upgrade the stored protobuf character data to its newest
/// iteration, if new types have been created. Consumes self.
pub fn uprade(self) -> Result<Character, Error> {
// Currently, this just returns itself because there are no
// iterations. But we could for example go from CofdSheet v1
// to CofdSheet v2 by deserializing v1, applying a migration
// to v2, then reserializing, and copying over all other
// fields.
Ok(self)
}
}
#[derive(Serialize, Debug, Queryable)] #[derive(Serialize, Debug, Queryable)]
pub struct StrippedCharacter { pub struct StrippedCharacter {
pub id: i32, pub id: i32,
pub user_id: i32, pub user_id: i32,
pub viewable: bool, pub viewable: bool,
pub character_name: String, pub character_name: String,
pub data_type: String, pub data_type: CharacterDataType,
pub data_version: i32, pub data_version: i32,
} }
@ -80,7 +133,7 @@ pub struct NewCharacter<'a> {
pub user_id: i32, pub user_id: i32,
pub viewable: bool, pub viewable: bool,
pub character_name: &'a str, pub character_name: &'a str,
pub data_type: &'a str, pub data_type: CharacterDataType,
pub data_version: i32, pub data_version: i32,
pub data: &'a [u8], pub data: &'a [u8],
} }

View File

@ -1,32 +1,47 @@
use crate::db::{Dao, TenebrousDbConn}; use crate::db::{Dao, TenebrousDbConn};
use crate::errors::Error; use crate::errors::Error;
use crate::models::characters::Visibility; use crate::models::characters::{Character, CharacterDataType, DynCharacterData, Visibility};
use crate::models::users::User; use crate::models::users::User;
use rocket::request::Form;
use rocket::response::Redirect;
use rocket_contrib::templates::Template; use rocket_contrib::templates::Template;
use std::collections::HashMap; use std::collections::HashMap;
mod new;
pub(crate) fn routes() -> Vec<rocket::Route> { pub(crate) fn routes() -> Vec<rocket::Route> {
routes![ routes![
view_character, view_character,
new_character, new::new_character,
create_new_character, new::create_new_character,
new_character_not_logged_in, new::new_character_not_logged_in,
edit_character edit_character
] ]
} }
#[derive(FromForm)]
struct NewCharacterForm {
name: String, //TODO add game system
}
#[derive(Serialize)] #[derive(Serialize)]
struct ViewCharacterTemlate<'a, T> { struct ViewCharacterTemplate<'a> {
pub name: &'a str, pub name: &'a str,
pub username: &'a str, pub username: &'a str,
pub sheet: T, pub data_type: &'a CharacterDataType,
pub sheet: Box<DynCharacterData>,
}
fn view_character_template(user: &User, character: Character) -> Result<Template, Error> {
let character = character.uprade()?;
let context = ViewCharacterTemplate {
name: &character.character_name,
username: &user.username,
data_type: &character.data_type,
sheet: character.dyn_deserialize()?,
};
use CharacterDataType::*;
let template = match character.data_type {
ChroniclesOfDarknessV1 => Template::render("characters/view_character", context),
ChangelingV1 => Template::render("characters/view_character", context),
};
Ok(template)
} }
#[get("/<username>/<character_id>")] #[get("/<username>/<character_id>")]
@ -36,67 +51,15 @@ fn view_character(
conn: TenebrousDbConn, conn: TenebrousDbConn,
logged_in_user: Option<&User>, logged_in_user: Option<&User>,
) -> Result<Template, Error> { ) -> Result<Template, Error> {
let user = conn.load_user(&username)?.ok_or(Error::NotFound)?; let user = &conn.load_user(&username)?.ok_or(Error::NotFound)?;
let character = conn let character = conn
.load_character(character_id)? .load_character(character_id)?
.and_then(|c| c.as_visible_for(logged_in_user)) .and_then(|c| c.as_visible_for(logged_in_user))
.ok_or(Error::NotFound)?; .ok_or(Error::NotFound)?;
//TODO determine sheet type and deserialize based on that. let template = view_character_template(user, character)?;
use crate::models::proto::cofd::CofdSheet; Ok(template)
use prost::Message;
let sheet = CofdSheet::decode(character.data.as_ref())?;
let context = ViewCharacterTemlate {
name: &character.character_name,
username: &user.username,
sheet: sheet,
};
Ok(Template::render("view_character", context))
}
#[get("/new")]
fn new_character(logged_in_user: &User, conn: TenebrousDbConn) -> Result<Template, Error> {
let context = HashMap::<String, String>::new();
Ok(Template::render("new_character", context))
}
#[post("/new", data = "<form>")]
fn create_new_character(
form: Form<NewCharacterForm>,
logged_in_user: &User,
conn: TenebrousDbConn,
) -> Result<Redirect, Error> {
//TODO redirect to character edit page
//TODO redirect back to new character page with an error and filled-out form if validation errors.
//TODO add game system.
use crate::models::characters::NewCharacter;
use crate::models::proto::cofd::CofdSheet;
use prost::bytes::BytesMut;
use prost::Message;
let new_character = CofdSheet::default();
let mut buf = BytesMut::with_capacity(std::mem::size_of_val(&new_character));
new_character.encode(&mut buf)?;
let insert = NewCharacter {
user_id: logged_in_user.id,
viewable: true,
character_name: &form.name,
data_type: std::any::type_name::<CofdSheet>(),
data_version: 1,
data: &buf,
};
conn.insert_character(insert)?;
Ok(super::common::redirect_to_index())
}
#[get("/new", rank = 2)]
fn new_character_not_logged_in() -> Redirect {
super::common::redirect_to_login()
} }
#[get("/<owner>/<character_id>/edit")] #[get("/<owner>/<character_id>/edit")]

View File

@ -0,0 +1,128 @@
use crate::db::{Dao, TenebrousDbConn};
use crate::errors::Error;
use crate::models::{
characters::{CharacterDataType, NewCharacter},
proto::cofd::*,
users::User,
};
use prost::{bytes::BytesMut, Message};
use rocket::http::RawStr;
use rocket::request::{Form, FormError, FromFormValue};
use rocket::response::Redirect;
use rocket_contrib::templates::Template;
use std::collections::HashMap;
#[derive(FromForm, Serialize)]
pub(super) struct NewCharacterForm {
name: String,
system: CharacterDataType,
}
#[derive(Serialize)]
pub(super) struct RawNewCharacterForm {
name: String,
system: String,
}
impl<'v> FromFormValue<'v> for CharacterDataType {
type Error = &'v RawStr;
fn from_form_value(form_value: &'v RawStr) -> Result<CharacterDataType, &'v RawStr> {
let system = form_value.url_decode().or(Err("bad input"))?;
match system.as_ref() {
"cofd" => Ok(CharacterDataType::ChroniclesOfDarknessV1),
"changeling" => Ok(CharacterDataType::ChangelingV1),
_ => Err(form_value),
}
}
}
#[get("/new")]
pub(super) fn new_character(
logged_in_user: &User,
conn: TenebrousDbConn,
) -> Result<Template, Error> {
let mut context = HashMap::new();
let form = NewCharacterForm {
name: "".to_string(),
system: CharacterDataType::ChroniclesOfDarknessV1,
};
context.insert("form", form);
Ok(Template::render("characters/new_character", context))
}
fn new_sheet(system: &CharacterDataType) -> Result<BytesMut, Error> {
let sheet = match system {
CharacterDataType::ChroniclesOfDarknessV1 => {
let mut new_character = CofdSheet::default();
new_character.strength = 100;
let mut buf = BytesMut::with_capacity(std::mem::size_of_val(&new_character));
new_character.encode(&mut buf)?;
buf
}
CharacterDataType::ChangelingV1 => {
let mut new_character = ChangelingSheet::default();
new_character.base = Some(CofdSheet::default());
new_character.base.as_mut().unwrap().strength = 100;
let mut buf = BytesMut::with_capacity(std::mem::size_of_val(&new_character));
new_character.encode(&mut buf)?;
buf
}
};
Ok(sheet)
}
fn do_new_character(
form: Form<NewCharacterForm>,
user_id: i32,
conn: TenebrousDbConn,
) -> Result<(), Error> {
let sheet = new_sheet(&form.system)?;
let insert = NewCharacter {
user_id: user_id,
viewable: true,
character_name: &form.name,
data_type: form.system,
data_version: 1,
data: &sheet,
};
conn.insert_character(insert)?;
Ok(())
}
#[post("/new", data = "<form>")]
pub(super) fn create_new_character(
form: Result<Form<NewCharacterForm>, FormError>,
logged_in_user: &User,
conn: TenebrousDbConn,
) -> Result<Redirect, Template> {
//TODO redirect to character edit page
//TODO redirect back to new character page with an error and filled-out form if validation errors.
if let Err(e) = form {
//Not sure how to repopulate the form.
let mut context = HashMap::new();
let form = NewCharacterForm {
name: "".to_string(),
system: CharacterDataType::ChroniclesOfDarknessV1,
};
context.insert("form", form);
return Err(Template::render("characters/new_character", context));
}
match do_new_character(form.unwrap(), logged_in_user.id, conn) {
Ok(_) => Ok(crate::routes::common::redirect_to_index()),
Err(e) => {
let context = HashMap::<String, String>::new();
return Err(Template::render("characters/new_character", context));
}
}
}
#[get("/new", rank = 2)]
pub(super) fn new_character_not_logged_in() -> Redirect {
crate::routes::common::redirect_to_login()
}

18
src/schema.patch Normal file
View File

@ -0,0 +1,18 @@
This patch converts the generated schema to an enum type.
--- a/src/schema.rs
+++ b/src/schema.rs
@@ -4,13 +4,13 @@ table! {
characters (id) {
id -> Integer,
user_id -> Integer,
viewable -> Bool,
character_name -> Text,
- data_type -> Text,
+ data_type -> CharacterDataTypeMapping,
data_version -> Integer,
data -> Binary,
}
}
table! {

View File

@ -1,16 +1,22 @@
table! { table! {
use diesel::sql_types::*;
use crate::models::characters::*;
characters (id) { characters (id) {
id -> Integer, id -> Integer,
user_id -> Integer, user_id -> Integer,
viewable -> Bool, viewable -> Bool,
character_name -> Text, character_name -> Text,
data_type -> Text, data_type -> CharacterDataTypeMapping,
data_version -> Integer, data_version -> Integer,
data -> Binary, data -> Binary,
} }
} }
table! { table! {
use diesel::sql_types::*;
use crate::models::characters::*;
users (id) { users (id) {
id -> Integer, id -> Integer,
username -> Text, username -> Text,

View File

@ -0,0 +1,26 @@
{% extends "base" %}
{% block content %}
<div>
New character page.
<form action="/characters/new" method="post">
<div>
<label for="name">Name:</label>
<input id="name" name="name" type="text" value="{{ form.name }}" />
</div>
<div>
<label for="system">System:</label>
<select id="system" name="system">
<option value="cofd">Chronicles of Darkness</option>
<option value="changeling">Changeling</option>
</select>
</div>
<div>
<input type="submit" value="Create Character" />
</div>
</form>
</div>
{% endblock content %}

View File

@ -4,6 +4,7 @@
<div> <div>
<h1>Character {{name}}</h1> <h1>Character {{name}}</h1>
<h3>User: {{username}}</h3> <h3>User: {{username}}</h3>
<p>System: {{data_type}}</h3>
<p>Strength: {{sheet.strength}}</p> <p>Strength: {{sheet.strength}}</p>
</div> </div>
{% endblock content %} {% endblock content %}

View File

@ -1,18 +0,0 @@
{% extends "base" %}
{% block content %}
<div>
New character page.
<form action="/characters/new" method="post">
<div>
<label for="name">Name:</label>
<input id="name" name="name" type="text" />
</div>
<div>
<input type="submit" value="Create Character" />
</div>
</form>
</div>
{% endblock content %}