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"
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]]
name = "subtle"
version = "1.0.0"
@ -1469,6 +1490,7 @@ dependencies = [
"serde",
"serde_derive",
"serde_json",
"strum",
"thiserror",
]

View File

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

View File

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

View File

@ -3,7 +3,9 @@ use crate::models::proto::cofd::*;
use crate::models::users::User;
use crate::schema::characters;
use diesel_derive_enum::DbEnum;
use prost::bytes::BytesMut;
use serde_derive::Serialize;
use strum::{EnumIter, EnumString};
/// Dynamic character data is an opaque container type that holds
/// 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 {
ChroniclesOfDarknessV1,
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
/// in order of table columns.
#[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::*;
let template = match character.data_type {
ChroniclesOfDarknessV1 => Template::render("characters/view_character", context),
ChangelingV1 => Template::render("characters/view_character", context),
ChangelingV1 => Template::render("characters/view_changeling_character", context),
};
Ok(template)

View File

@ -2,74 +2,74 @@ use crate::db::{Dao, TenebrousDbConn};
use crate::errors::Error;
use crate::models::{
characters::{CharacterDataType, NewCharacter},
proto::cofd::*,
convert::ValidationError,
users::User,
};
use prost::{bytes::BytesMut, Message};
use rocket::http::RawStr;
use rocket::request::{Form, FormError, FromFormValue};
use rocket::response::Redirect;
use prost::bytes::BytesMut;
use rocket::{request::Form, response::Redirect};
use rocket_contrib::templates::Template;
use std::collections::HashMap;
use strum::IntoEnumIterator;
/// Form submission for creating a new character.
#[derive(FromForm, Serialize)]
pub(super) struct NewCharacterForm {
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)]
pub(super) struct RawNewCharacterForm {
pub(super) struct NewCharacterContext {
name: String,
system: String,
error_message: String,
selected_system: CharacterDataType,
systems: Vec<CharacterDataType>,
}
impl<'v> FromFormValue<'v> for CharacterDataType {
type Error = &'v RawStr;
impl NewCharacterContext {
/// 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> {
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),
/// Create a context from a form submission. Used to repopulate
/// fields in the form when an error is sent back.
pub fn from_form(form: &Form<NewCharacterForm>) -> NewCharacterContext {
let system: CharacterDataType = *form
.system
.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> {
use CharacterDataType::*;
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)
}
/// Create and insert a new character into the database. The form is
/// assumed to be successfully validated.
fn create_new_character(
form: Form<NewCharacterForm>,
form: &Form<NewCharacterForm>,
user_id: i32,
conn: TenebrousDbConn,
) -> 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 {
user_id: user_id,
viewable: true,
character_name: &form.name,
data_type: form.system,
data_type: system,
data_version: 1,
data: &sheet,
};
@ -78,46 +78,35 @@ fn create_new_character(
Ok(())
}
#[get("/new")]
pub(super) fn new_character_page(
logged_in_user: &User,
conn: TenebrousDbConn,
) -> Result<Template, Error> {
let mut context = HashMap::new();
let form = NewCharacterForm {
name: "".to_string(),
system: CharacterDataType::ChroniclesOfDarknessV1,
};
/// Render an error message on the new character page, repopulating
/// the form based on information from the passed-in form.
fn render_error(form: &Form<NewCharacterForm>, error: String) -> Template {
let mut context = NewCharacterContext::from_form(form);
context.error_message = error;
Template::render("characters/new_character", context)
}
context.insert("form", form);
Ok(Template::render("characters/new_character", context))
#[get("/new")]
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>")]
pub(super) fn new_character_submit(
form: Result<Form<NewCharacterForm>, FormError>,
form: Form<NewCharacterForm>,
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));
if let Err(e) = &form.system {
return Err(render_error(&form, e.to_string().clone()));
}
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()),
Err(e) => {
let context = HashMap::<String, String>::new();
return Err(Template::render("characters/new_character", context));
}
Err(e) => Err(render_error(&form, e.to_string().clone())),
}
}

View File

@ -4,17 +4,26 @@
<div>
New character page.
{% if error_message %}
<p>Error: {{ error_message }}</p>
{% endif %}
<form action="/characters/new" method="post">
<div>
<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>
<label for="system">System:</label>
<select id="system" name="system">
<option value="cofd">Chronicles of Darkness</option>
<option value="changeling">Changeling</option>
{% for system in systems %}
{% if system == selected_system %}
<option value="{{ system }}" selected="true">{{ system }}</option>
{% else %}
<option value="{{ system }}">{{ system }}</option>
{% endif %}
{% endfor %}
</select>
</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" %}
{% block content %}
<h1>Core Sheet</h1>
<div>
<h1>Character {{name}}</h1>
<h3>User: {{username}}</h3>