Finished and mostly clean new character flow.

Lots of changes and reorganization to support the ability to create
new characters.
This commit is contained in:
jeff 2020-12-09 21:25:31 +00:00
parent e59e9a5ebf
commit 31a3fa2e46
10 changed files with 163 additions and 75 deletions

22
Cargo.lock generated
View File

@ -1404,6 +1404,27 @@ version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3015a7d0a5fd5105c91c3710d42f9ccf0abfb287d62206484dcc67f9569a6483" checksum = "3015a7d0a5fd5105c91c3710d42f9ccf0abfb287d62206484dcc67f9569a6483"
[[package]]
name = "strum"
version = "0.20.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7318c509b5ba57f18533982607f24070a55d353e90d4cae30c467cdb2ad5ac5c"
dependencies = [
"strum_macros",
]
[[package]]
name = "strum_macros"
version = "0.20.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ee8bc6b87a5112aeeab1f4a9f7ab634fe6cbefc4850006df31267f4cfb9e3149"
dependencies = [
"heck",
"proc-macro2 1.0.24",
"quote 1.0.7",
"syn 1.0.53",
]
[[package]] [[package]]
name = "subtle" name = "subtle"
version = "1.0.0" version = "1.0.0"
@ -1469,6 +1490,7 @@ dependencies = [
"serde", "serde",
"serde_derive", "serde_derive",
"serde_json", "serde_json",
"strum",
"thiserror", "thiserror",
] ]

View File

@ -21,6 +21,7 @@ rust-argon2 = "0.8"
log = "0.4" log = "0.4"
rand = "0.7" rand = "0.7"
rocket = { version= "0.4.6", features = ["private-cookies"] } rocket = { version= "0.4.6", features = ["private-cookies"] }
strum = { version = "0.20", features = ["derive"] }
[dependencies.rocket_contrib] [dependencies.rocket_contrib]
version = "0.4.6" version = "0.4.6"

View File

@ -1,3 +1,4 @@
pub mod characters; pub mod characters;
pub mod convert;
pub mod proto; pub mod proto;
pub mod users; pub mod users;

View File

@ -3,7 +3,9 @@ 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 diesel_derive_enum::DbEnum;
use prost::bytes::BytesMut;
use serde_derive::Serialize; use serde_derive::Serialize;
use strum::{EnumIter, EnumString};
/// Dynamic character data is an opaque container type that holds /// Dynamic character data is an opaque container type that holds
/// successfully deserialized character data protobuf object of any /// successfully deserialized character data protobuf object of any
@ -39,12 +41,36 @@ pub(crate) trait Visibility {
} }
} }
#[derive(DbEnum, Debug, Serialize, PartialEq, Clone, Copy)] #[derive(DbEnum, Debug, Serialize, PartialEq, Clone, Copy, EnumIter, EnumString)]
pub enum CharacterDataType { pub enum CharacterDataType {
ChroniclesOfDarknessV1, ChroniclesOfDarknessV1,
ChangelingV1, ChangelingV1,
} }
impl CharacterDataType {
pub fn create_data(&self) -> Result<BytesMut, Error> {
use prost::Message;
use CharacterDataType::*;
let data: BytesMut = match self {
ChroniclesOfDarknessV1 => {
let sheet = CofdSheet::default();
let mut buf = BytesMut::with_capacity(std::mem::size_of_val(&sheet));
sheet.encode(&mut buf)?;
buf
}
ChangelingV1 => {
let mut sheet = ChangelingSheet::default();
sheet.base = Some(CofdSheet::default());
let mut buf = BytesMut::with_capacity(std::mem::size_of_val(&sheet));
sheet.encode(&mut buf)?;
buf
}
};
Ok(data)
}
}
/// 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)]

28
src/models/convert.rs Normal file
View File

@ -0,0 +1,28 @@
use super::characters::CharacterDataType;
use rocket::http::RawStr;
use rocket::request::FromFormValue;
use std::str::FromStr;
use thiserror::Error;
/// Validation errors specific to the new character form.
#[derive(Serialize, Error, Debug, Clone)]
pub enum ValidationError {
#[error("bad UTF-8 encoding")]
BadEncoding,
#[error("invalid game system: {0}")]
InvalidGameSystem(String),
}
impl<'v> FromFormValue<'v> for CharacterDataType {
type Error = ValidationError;
fn from_form_value(form_value: &'v RawStr) -> Result<CharacterDataType, ValidationError> {
let system = form_value
.url_decode()
.or(Err(ValidationError::BadEncoding))?;
CharacterDataType::from_str(&system)
.or_else(|_| Err(ValidationError::InvalidGameSystem(system.to_string())))
}
}

View File

@ -38,7 +38,7 @@ fn view_character_template(user: &User, character: Character) -> Result<Template
use CharacterDataType::*; use CharacterDataType::*;
let template = match character.data_type { let template = match character.data_type {
ChroniclesOfDarknessV1 => Template::render("characters/view_character", context), ChroniclesOfDarknessV1 => Template::render("characters/view_character", context),
ChangelingV1 => Template::render("characters/view_character", context), ChangelingV1 => Template::render("characters/view_changeling_character", context),
}; };
Ok(template) Ok(template)

View File

@ -2,74 +2,74 @@ use crate::db::{Dao, TenebrousDbConn};
use crate::errors::Error; use crate::errors::Error;
use crate::models::{ use crate::models::{
characters::{CharacterDataType, NewCharacter}, characters::{CharacterDataType, NewCharacter},
proto::cofd::*, convert::ValidationError,
users::User, users::User,
}; };
use prost::{bytes::BytesMut, Message}; use prost::bytes::BytesMut;
use rocket::http::RawStr; use rocket::{request::Form, response::Redirect};
use rocket::request::{Form, FormError, FromFormValue};
use rocket::response::Redirect;
use rocket_contrib::templates::Template; use rocket_contrib::templates::Template;
use std::collections::HashMap; use strum::IntoEnumIterator;
/// Form submission for creating a new character.
#[derive(FromForm, Serialize)] #[derive(FromForm, Serialize)]
pub(super) struct NewCharacterForm { pub(super) struct NewCharacterForm {
name: String, name: String,
system: CharacterDataType, system: Result<CharacterDataType, ValidationError>,
} }
/// Template context for the new character page. Serves as both a
/// means of populating form fields and reindering error messages.
#[derive(Serialize)] #[derive(Serialize)]
pub(super) struct RawNewCharacterForm { pub(super) struct NewCharacterContext {
name: String, name: String,
system: String, error_message: String,
selected_system: CharacterDataType,
systems: Vec<CharacterDataType>,
} }
impl<'v> FromFormValue<'v> for CharacterDataType { impl NewCharacterContext {
type Error = &'v RawStr; /// Default empty context, used when page first loads.
pub fn default() -> NewCharacterContext {
NewCharacterContext {
name: "".to_string(),
error_message: "".to_string(),
selected_system: CharacterDataType::ChroniclesOfDarknessV1,
systems: CharacterDataType::iter().collect(),
}
}
fn from_form_value(form_value: &'v RawStr) -> Result<CharacterDataType, &'v RawStr> { /// Create a context from a form submission. Used to repopulate
let system = form_value.url_decode().or(Err("bad input"))?; /// fields in the form when an error is sent back.
match system.as_ref() { pub fn from_form(form: &Form<NewCharacterForm>) -> NewCharacterContext {
"cofd" => Ok(CharacterDataType::ChroniclesOfDarknessV1), let system: CharacterDataType = *form
"changeling" => Ok(CharacterDataType::ChangelingV1), .system
_ => Err(form_value), .as_ref()
.unwrap_or(&CharacterDataType::ChroniclesOfDarknessV1);
NewCharacterContext {
name: form.name.clone(),
error_message: "".to_string(),
selected_system: system,
systems: CharacterDataType::iter().collect(),
} }
} }
} }
fn create_new_sheet(system: &CharacterDataType) -> Result<BytesMut, Error> { /// Create and insert a new character into the database. The form is
use CharacterDataType::*; /// assumed to be successfully validated.
let sheet: BytesMut = match system {
ChroniclesOfDarknessV1 => {
let sheet = CofdSheet::default();
let mut buf = BytesMut::with_capacity(std::mem::size_of_val(&sheet));
sheet.encode(&mut buf)?;
buf
}
ChangelingV1 => {
let mut sheet = ChangelingSheet::default();
sheet.base = Some(CofdSheet::default());
let mut buf = BytesMut::with_capacity(std::mem::size_of_val(&sheet));
sheet.encode(&mut buf)?;
buf
}
};
Ok(sheet)
}
fn create_new_character( fn create_new_character(
form: Form<NewCharacterForm>, form: &Form<NewCharacterForm>,
user_id: i32, user_id: i32,
conn: TenebrousDbConn, conn: TenebrousDbConn,
) -> Result<(), Error> { ) -> Result<(), Error> {
let sheet = create_new_sheet(&form.system)?; let system: CharacterDataType = *form.system.as_ref().unwrap();
let sheet: BytesMut = system.create_data()?;
let insert = NewCharacter { let insert = NewCharacter {
user_id: user_id, user_id: user_id,
viewable: true, viewable: true,
character_name: &form.name, character_name: &form.name,
data_type: form.system, data_type: system,
data_version: 1, data_version: 1,
data: &sheet, data: &sheet,
}; };
@ -78,46 +78,35 @@ fn create_new_character(
Ok(()) Ok(())
} }
#[get("/new")] /// Render an error message on the new character page, repopulating
pub(super) fn new_character_page( /// the form based on information from the passed-in form.
logged_in_user: &User, fn render_error(form: &Form<NewCharacterForm>, error: String) -> Template {
conn: TenebrousDbConn, let mut context = NewCharacterContext::from_form(form);
) -> Result<Template, Error> { context.error_message = error;
let mut context = HashMap::new(); Template::render("characters/new_character", context)
let form = NewCharacterForm { }
name: "".to_string(),
system: CharacterDataType::ChroniclesOfDarknessV1,
};
context.insert("form", form); #[get("/new")]
Ok(Template::render("characters/new_character", context)) pub(super) fn new_character_page(_logged_in_user: &User) -> Result<Template, Error> {
Ok(Template::render(
"characters/new_character",
NewCharacterContext::default(),
))
} }
#[post("/new", data = "<form>")] #[post("/new", data = "<form>")]
pub(super) fn new_character_submit( pub(super) fn new_character_submit(
form: Result<Form<NewCharacterForm>, FormError>, form: Form<NewCharacterForm>,
logged_in_user: &User, logged_in_user: &User,
conn: TenebrousDbConn, conn: TenebrousDbConn,
) -> Result<Redirect, Template> { ) -> Result<Redirect, Template> {
//TODO redirect to character edit page if let Err(e) = &form.system {
//TODO redirect back to new character page with an error and filled-out form if validation errors. return Err(render_error(&form, e.to_string().clone()));
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 create_new_character(form.unwrap(), logged_in_user.id, conn) { match create_new_character(&form, logged_in_user.id, conn) {
Ok(_) => Ok(crate::routes::common::redirect_to_index()), Ok(_) => Ok(crate::routes::common::redirect_to_index()),
Err(e) => { Err(e) => Err(render_error(&form, e.to_string().clone())),
let context = HashMap::<String, String>::new();
return Err(Template::render("characters/new_character", context));
}
} }
} }

View File

@ -4,17 +4,26 @@
<div> <div>
New character page. New character page.
{% if error_message %}
<p>Error: {{ error_message }}</p>
{% endif %}
<form action="/characters/new" method="post"> <form action="/characters/new" method="post">
<div> <div>
<label for="name">Name:</label> <label for="name">Name:</label>
<input id="name" name="name" type="text" value="{{ form.name }}" /> <input id="name" name="name" type="text" value="{{ name }}" />
</div> </div>
<div> <div>
<label for="system">System:</label> <label for="system">System:</label>
<select id="system" name="system"> <select id="system" name="system">
<option value="cofd">Chronicles of Darkness</option> {% for system in systems %}
<option value="changeling">Changeling</option> {% if system == selected_system %}
<option value="{{ system }}" selected="true">{{ system }}</option>
{% else %}
<option value="{{ system }}">{{ system }}</option>
{% endif %}
{% endfor %}
</select> </select>
</div> </div>

View File

@ -0,0 +1,11 @@
{% extends "base" %}
{% block content %}
<h1>Changeling Sheet</h1>
<div>
<h1>Character {{name}}</h1>
<h3>User: {{username}}</h3>
<p>System: {{data_type}}</h3>
<p>Strength: {{sheet.base.strength}}</p>
</div>
{% endblock content %}

View File

@ -1,6 +1,7 @@
{% extends "base" %} {% extends "base" %}
{% block content %} {% block content %}
<h1>Core Sheet</h1>
<div> <div>
<h1>Character {{name}}</h1> <h1>Character {{name}}</h1>
<h3>User: {{username}}</h3> <h3>User: {{username}}</h3>