Compare commits

..

No commits in common. "0751a783dceb1f63b8c4184a3c7a64a598a2199f" and "93ed6799466f7b141e00ca3a4c09dbcbcdadb2cc" have entirely different histories.

12 changed files with 92 additions and 296 deletions

23
Cargo.lock generated
View File

@ -342,18 +342,6 @@ 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"
@ -380,15 +368,6 @@ 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"
@ -1457,8 +1436,6 @@ 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,9 +13,7 @@ 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,5 +3,3 @@
[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 CHECK(data_type IN ('chronicles_of_darkness_v1', 'changeling_v1')) NOT NULL, data_type TEXT NOT NULL,
data_version INTEGER NOT NULL, data_version INTEGER NOT NULL,
data BLOB NOT NULL data BLOB NOT NULL
); );

View File

@ -1,17 +1,7 @@
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.
@ -39,12 +29,6 @@ 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)]
@ -53,7 +37,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: CharacterDataType, pub data_type: String,
pub data_version: i32, pub data_version: i32,
pub data: Vec<u8>, pub data: Vec<u8>,
} }
@ -68,50 +52,13 @@ 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: CharacterDataType, pub data_type: String,
pub data_version: i32, pub data_version: i32,
} }
@ -133,7 +80,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: CharacterDataType, pub data_type: &'a str,
pub data_version: i32, pub data_version: i32,
pub data: &'a [u8], pub data: &'a [u8],
} }

View File

@ -1,47 +1,32 @@
use crate::db::{Dao, TenebrousDbConn}; use crate::db::{Dao, TenebrousDbConn};
use crate::errors::Error; use crate::errors::Error;
use crate::models::characters::{Character, CharacterDataType, DynCharacterData, Visibility}; use crate::models::characters::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::new_character, new_character,
new::create_new_character, create_new_character,
new::new_character_not_logged_in, new_character_not_logged_in,
edit_character edit_character
] ]
} }
#[derive(Serialize)] #[derive(FromForm)]
struct ViewCharacterTemplate<'a> { struct NewCharacterForm {
pub name: &'a str, name: String, //TODO add game system
pub username: &'a str,
pub data_type: &'a CharacterDataType,
pub sheet: Box<DynCharacterData>,
} }
fn view_character_template(user: &User, character: Character) -> Result<Template, Error> { #[derive(Serialize)]
let character = character.uprade()?; struct ViewCharacterTemlate<'a, T> {
pub name: &'a str,
let context = ViewCharacterTemplate { pub username: &'a str,
name: &character.character_name, pub sheet: T,
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>")]
@ -51,15 +36,67 @@ 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)?;
let template = view_character_template(user, character)?; //TODO determine sheet type and deserialize based on that.
Ok(template) use crate::models::proto::cofd::CofdSheet;
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

@ -1,128 +0,0 @@
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()
}

View File

@ -1,18 +0,0 @@
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,22 +1,16 @@
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 -> CharacterDataTypeMapping, data_type -> Text,
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

@ -1,26 +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" 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

@ -0,0 +1,18 @@
{% 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 %}

View File

@ -4,7 +4,6 @@
<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 %}