From ee8f7e58e8b94e18fc5b558458905e2a3de3b51d Mon Sep 17 00:00:00 2001 From: projectmoon Date: Sat, 2 Jan 2021 14:51:24 +0000 Subject: [PATCH] Implement updating skills. --- proto/cofd_api.proto | 16 ++++++ src/models/proto.rs | 30 ++++++++++- src/routes/api.rs | 86 +++++++++++++++++++++++-------- static/scripts/api.js | 39 +++++++++++++- static/scripts/characters/edit.js | 33 +++++++++--- 5 files changed, 173 insertions(+), 31 deletions(-) diff --git a/proto/cofd_api.proto b/proto/cofd_api.proto index b84ee9c..4718740 100644 --- a/proto/cofd_api.proto +++ b/proto/cofd_api.proto @@ -31,6 +31,7 @@ message Attributes { int32 composure = 9; } +//Update an attribute's dot amount. TODO rename to AttributesUpdate. message Attribute { string name = 1; int32 value = 2; @@ -44,11 +45,26 @@ message Skills { repeated CofdSheet.Skill social_skills = 3; } +//Full update of a single skill message SkillUpdate { string name = 1; CofdSheet.Skill skill = 2; } + +//Partial update of a single skill dot amount. +message SkillValueUpdate { + string name = 1; + int32 value = 2; +} + +//Partial update of only a skill's specializations. The +//specializations will be overwritten with the new values. +message SkillSpecializationsUpdate { + string name = 1; + repeated string specializations = 2; +} + //Add a Condition to a Chronicles of Darkness character sheet. message Condition { string name = 1; diff --git a/src/models/proto.rs b/src/models/proto.rs index 5171ebd..0a995a4 100644 --- a/src/models/proto.rs +++ b/src/models/proto.rs @@ -6,6 +6,15 @@ use std::ops::Deref; pub mod cofd; +const CRATE_NAME: &'static str = env!("CARGO_BIN_NAME"); + +/// Convert an incoming protobuf content-type to the equivalent type +/// name produced by std::any::type_name(). Currently does NOT work +/// with nested types due to how prost generates the module names. +fn convert_to_rust_name(proto_type: &str) -> String { + format!("{}::{}", CRATE_NAME, proto_type.replace(".", "::")) +} + /// A struct wrapping a protobuf that allows it to be used as binary /// data submitted via POST using fetch API. Can automatically be /// dereferenced into its wrapped type. @@ -23,8 +32,27 @@ where { type Error = crate::errors::Error; - async fn from_data(_req: &Request<'_>, data: Data) -> Outcome { + async fn from_data(req: &Request<'_>, data: Data) -> Outcome { use rocket::http::Status; + let content_type = req.content_type(); + + let is_protobuf = content_type + .map(|ct| ct.top() == "application" && ct.sub() == "x-protobuf") + .unwrap_or(false); + + let message_type: Option = content_type.and_then(|ct| { + ct.params() + .find(|&(name, _)| name == "messageType") + .map(|(_, message_type)| convert_to_rust_name(message_type)) + }); + + if !is_protobuf { + return Outcome::Failure((Status::new(422, "invalid protobuf"), Error::InvalidInput)); + } + + if message_type.as_ref().map(String::as_str) != Some(std::any::type_name::()) { + return Outcome::Forward(data); + } let bytes: Vec = match data.open(2.mebibytes()).stream_to_vec().await { Ok(read_bytes) => read_bytes, diff --git a/src/routes/api.rs b/src/routes/api.rs index 06d45c8..6ca7186 100644 --- a/src/routes/api.rs +++ b/src/routes/api.rs @@ -14,6 +14,7 @@ pub(crate) fn routes() -> Vec { cofd::update_attributes, cofd::update_attribute, cofd::update_skills, + cofd::update_skill_value, cofd::add_condition, cofd::remove_condition ] @@ -49,6 +50,35 @@ mod cofd { use crate::models::proto::cofd::cofd_sheet::Skill; use crate::models::proto::{cofd::api::*, cofd::*, Proto}; + fn find_skill_entry<'a>( + sheet: &'a mut CofdSheet, + skill_name: &'a str, + ) -> Option> { + let all_skills = vec![ + &mut sheet.mental_skills, + &mut sheet.physical_skills, + &mut sheet.social_skills, + ]; + + // Search all skill lists for this value using "workaround" to + // break value from for loops. + let skill: Option> = 'l: loop { + for skill_map in all_skills { + if let Entry::Occupied(entry) = skill_map.entry(skill_name.to_owned()) { + break 'l Some(entry); + } + } + + break None; + }; + + skill + } + + fn find_skill<'a>(sheet: &'a mut CofdSheet, skill_name: &'a str) -> Option<&'a mut Skill> { + find_skill_entry(sheet, skill_name).map(|entry| entry.into_mut()) + } + #[post("/cofd///basic-info", data = "")] pub(super) fn update_basic_info<'a>( owner: String, @@ -101,7 +131,11 @@ mod cofd { Ok("lol") } - #[patch("/cofd///skills", data = "")] + #[patch( + "/cofd///skills", + data = "", + rank = 1 + )] pub(super) async fn update_skills<'a>( owner: String, character_id: i32, @@ -111,28 +145,11 @@ mod cofd { ) -> Result<&'a str, Error> { let mut character = load_character(&conn, logged_in_user, owner, character_id).await?; let mut sheet: CofdSheet = character.try_deserialize()?; - let skill: &Skill = skill_update.skill.as_ref().ok_or(Error::InvalidInput)?; - - let all_skills = vec![ - &mut sheet.mental_skills, - &mut sheet.physical_skills, - &mut sheet.social_skills, - ]; - - // Search all skill lists for this value using "workaround" to - // break value from for loops. - let skill_entry: Option> = 'l: loop { - for skill_map in all_skills { - if let Entry::Occupied(entry) = skill_map.entry(skill_update.name.clone()) { - break 'l Some(entry); - } - } - - break None; - }; + let updated_skill: &Skill = skill_update.skill.as_ref().ok_or(Error::InvalidInput)?; + let skill_entry = find_skill_entry(&mut sheet, &skill_update.name); skill_entry - .map(|mut entry| entry.insert(skill.clone())) + .map(|mut entry| entry.insert(updated_skill.clone())) .ok_or(Error::InvalidInput)?; println!( @@ -145,6 +162,33 @@ mod cofd { Ok("lol") } + #[patch( + "/cofd///skills", + data = "", + rank = 2 + )] + pub(super) async fn update_skill_value<'a>( + owner: String, + character_id: i32, + value_update: Proto, + conn: TenebrousDbConn<'_>, + logged_in_user: Option<&User>, + ) -> Result<&'a str, Error> { + let mut character = load_character(&conn, logged_in_user, owner, character_id).await?; + let mut sheet: CofdSheet = character.try_deserialize()?; + let skill: Option<&mut Skill> = find_skill(&mut sheet, &value_update.name); + + skill + .map(|s| s.dots = value_update.value) + .ok_or(Error::InvalidInput)?; + + println!("updated skill value",); + + character.update_data(sheet)?; + conn.update_character_sheet(&character).await?; + Ok("lol") + } + #[put("/cofd///conditions", data = "")] pub(super) fn add_condition<'a>( owner: String, diff --git a/static/scripts/api.js b/static/scripts/api.js index 39cff6d..d655e3c 100644 --- a/static/scripts/api.js +++ b/static/scripts/api.js @@ -1,9 +1,21 @@ function makeAPI(root) { - const Attribute = root.lookupType("models.proto.cofd.api.Attribute"); + //Protobuf types + const AttributeType = 'models.proto.cofd.api.Attribute'; + const Attribute = root.lookupType(AttributeType); + const SkillValueUpdateType = 'models.proto.cofd.api.SkillValueUpdate'; + const SkillValueUpdate = root.lookupType(SkillValueUpdateType); + const SkillSpecializationUpdateType = 'models.proto.cofd.api.SkillSpecializationsUpdate'; + const SkillSpecializationsUpdate = root.lookupType(SkillSpecializationUpdateType); + + const protobufContentType = (messageType) => + ({ 'Content-Type': 'application/x-protobuf; messageType="' + messageType + '"' }); const attributesResource = (username, characterID) => '/api/cofd/' + username + '/' + characterID + '/attributes'; + const skillResource = (username, characterID, skillName) => + '/api/cofd/' + username + '/' + characterID + '/skills'; + async function updateAttribute(params) { const { username, characterID, attribute, newValue } = params; @@ -16,6 +28,7 @@ function makeAPI(root) { let resp = await fetch(resource, { method: 'PATCH', + headers: { ... protobufContentType(AttributeType) }, body: Attribute.encode(req).finish() }).then(async resp => { console.log("resp is", await resp.text()); @@ -24,7 +37,29 @@ function makeAPI(root) { }); } + async function updateSkillValue(params) { + const { username, characterID, skillName, newValue } = params; + + let req = SkillValueUpdate.create({ + name: skillName, + value: parseInt(newValue) + }); + + const resource = skillResource(username, characterID); + + let resp = await fetch(resource, { + method: 'PATCH', + headers: { ... protobufContentType(SkillValueUpdateType) }, + body: SkillValueUpdate.encode(req).finish() + }).then(async resp => { + console.log("resp is", await resp.text()); + }).catch(async err => { + console.log("err is", err.text()); + }); + } + return { - updateAttribute + updateAttribute, + updateSkillValue }; } diff --git a/static/scripts/characters/edit.js b/static/scripts/characters/edit.js index ccc4216..b9e46c5 100644 --- a/static/scripts/characters/edit.js +++ b/static/scripts/characters/edit.js @@ -12,18 +12,37 @@ function setupAttributes() { const attributeInputs = document.querySelectorAll('#attributes input[type="number"]'); + async function attributeHandler(event) { + console.log("updating attr"); + const attribute = event.target.id; + const newValue = parseInt(event.target.value); + const params = { username: USERNAME, characterID: CHARACTER_ID, attribute, newValue }; + await api.updateAttribute(params); + } + Array.from(attributeInputs).forEach(input => { - input.addEventListener('change', async function(event) { - console.log("updating attr"); - const attribute = event.target.id; - const newValue = parseInt(event.target.value); - const params = { username: USERNAME, characterID: CHARACTER_ID, attribute, newValue }; - await api.updateAttribute(params); - }); + input.addEventListener('change', attributeHandler); + }); + } + + function setupSkills() { + const attributeInputs = document.querySelectorAll('#skills input[type="number"]'); + + async function skillValueHandler(event) { + console.log("updating skill value"); + const skillName = event.target.id; + const newValue = parseInt(event.target.value); + const params = { username: USERNAME, characterID: CHARACTER_ID, skillName, newValue }; + await api.updateSkillValue(params); + } + + Array.from(attributeInputs).forEach(input => { + input.addEventListener('change', skillValueHandler); }); } setupAttributes(); + setupSkills(); })().catch(e => { alert(e); });