Compare commits

...

3 Commits

Author SHA1 Message Date
jeff 76c67425b1 RPC-ify update attribute API. 2021-01-02 22:11:58 +00:00
jeff 9968cc4a6e Update skill value as rpc API (though not grpc) 2021-01-02 22:11:58 +00:00
jeff d1e29b40ed Implement updating skills. 2021-01-02 22:11:58 +00:00
5 changed files with 227 additions and 65 deletions

View File

@ -31,9 +31,12 @@ message Attributes {
int32 composure = 9;
}
message Attribute {
string name = 1;
int32 value = 2;
//Update an attribute's dot amount. TODO rename to AttributesUpdate.
message UpdateAttributeRequest {
string character_username = 1;
int32 character_id = 2;
string attribute_name = 3;
int32 attribute_value = 4;
}
//Update skill entries in a Chronicles of Darkness character sheet.
@ -44,12 +47,33 @@ 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 UpdateSkillValueRequest {
string character_username = 1;
int32 character_id = 2;
string skill_name = 3;
int32 skill_value = 4;
}
//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;
}
service CofdApi {
rpc UpdateSkillValue(UpdateSkillValueRequest) returns (CofdSheet.Skill);
}

View File

@ -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,28 @@ where
{
type Error = crate::errors::Error;
async fn from_data(_req: &Request<'_>, data: Data) -> Outcome<Self, Error> {
async fn from_data(req: &Request<'_>, data: Data) -> Outcome<Self, Error> {
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<String> = 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::<T>()) {
println!("message type is {:?}", message_type);
return Outcome::Forward(data);
}
let bytes: Vec<u8> = match data.open(2.mebibytes()).stream_to_vec().await {
Ok(read_bytes) => read_bytes,

View File

@ -14,6 +14,7 @@ pub(crate) fn routes() -> Vec<rocket::Route> {
cofd::update_attributes,
cofd::update_attribute,
cofd::update_skills,
cofd::update_skill_value,
cofd::add_condition,
cofd::remove_condition
]
@ -26,7 +27,7 @@ pub(crate) fn routes() -> Vec<rocket::Route> {
async fn load_character(
conn: &TenebrousDbConn<'_>,
logged_in_user: Option<&User>,
owner: String,
owner: &str,
character_id: i32,
) -> Result<Character, Error> {
let logged_in_user = logged_in_user.ok_or(Error::NotLoggedIn)?;
@ -36,7 +37,7 @@ async fn load_character(
.await?
.ok_or(Error::NotFound)?;
if logged_in_user.username != owner {
if &logged_in_user.username != owner {
return Err(Error::NoPermission);
}
@ -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<OccupiedEntry<'a, String, Skill>> {
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<OccupiedEntry<_, _>> = '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/<owner>/<character_id>/basic-info", data = "<info>")]
pub(super) fn update_basic_info<'a>(
owner: String,
@ -67,41 +97,45 @@ mod cofd {
"lol"
}
#[patch("/cofd/<owner>/<character_id>/attributes", data = "<attr_update>")]
#[post("/rpc/cofd/update_attribute", data = "<req>")]
pub(super) async fn update_attribute<'a>(
owner: String,
character_id: i32,
attr_update: Proto<Attribute>,
req: Proto<UpdateAttributeRequest>,
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 mut character = load_character(
&conn,
logged_in_user,
&req.character_username,
req.character_id,
)
.await?;
match attr_update.name.to_lowercase().as_ref() {
"strength" => Ok(sheet.strength = attr_update.value),
"dexterity" => Ok(sheet.dexterity = attr_update.value),
"stamina" => Ok(sheet.stamina = attr_update.value),
"intelligence" => Ok(sheet.intelligence = attr_update.value),
"wits" => Ok(sheet.wits = attr_update.value),
"resolve" => Ok(sheet.resolve = attr_update.value),
"presence" => Ok(sheet.presence = attr_update.value),
"manipulation" => Ok(sheet.manipulation = attr_update.value),
"composure" => Ok(sheet.composure = attr_update.value),
let mut sheet: CofdSheet = character.try_deserialize()?;
let value = req.attribute_value;
match req.attribute_name.to_lowercase().as_ref() {
"strength" => Ok(sheet.strength = value),
"dexterity" => Ok(sheet.dexterity = value),
"stamina" => Ok(sheet.stamina = value),
"intelligence" => Ok(sheet.intelligence = value),
"wits" => Ok(sheet.wits = value),
"resolve" => Ok(sheet.resolve = value),
"presence" => Ok(sheet.presence = value),
"manipulation" => Ok(sheet.manipulation = value),
"composure" => Ok(sheet.composure = value),
_ => Err(Error::InvalidInput),
}?;
println!(
"updated {} attribute {} to {}",
character.character_name, attr_update.name, attr_update.value
);
character.update_data(sheet)?;
conn.update_character_sheet(&character).await?;
Ok("lol")
}
#[patch("/cofd/<owner>/<character_id>/skills", data = "<skill_update>")]
#[patch(
"/cofd/<owner>/<character_id>/skills",
data = "<skill_update>",
rank = 1
)]
pub(super) async fn update_skills<'a>(
owner: String,
character_id: i32,
@ -109,30 +143,13 @@ mod cofd {
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 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<OccupiedEntry<_, _>> = '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,35 @@ mod cofd {
Ok("lol")
}
#[post("/rpc/cofd/update_skill_value", data = "<request>")]
pub(super) async fn update_skill_value<'a>(
request: Proto<UpdateSkillValueRequest>,
conn: TenebrousDbConn<'_>,
logged_in_user: Option<&User>,
) -> Result<&'a str, Error> {
println!("{:#?}", request);
let mut character = load_character(
&conn,
logged_in_user,
&request.character_username,
request.character_id,
)
.await?;
let mut sheet: CofdSheet = character.try_deserialize()?;
let skill: Option<&mut Skill> = find_skill(&mut sheet, &request.skill_name);
skill
.map(|s| s.dots = request.skill_value)
.ok_or(Error::InvalidInput)?;
println!("updated skill value",);
character.update_data(sheet)?;
conn.update_character_sheet(&character).await?;
Ok("lol")
}
#[put("/cofd/<owner>/<character_id>/conditions", data = "<info>")]
pub(super) fn add_condition<'a>(
owner: String,

View File

@ -1,22 +1,65 @@
function makeAPI(root) {
const Attribute = root.lookupType("models.proto.cofd.api.Attribute");
//Protobuf types
const UpdateAttributeRequestType = 'models.proto.cofd.api.UpdateAttributeRequest';
const UpdateAttributeRequest = root.lookupType(UpdateAttributeRequestType);
const UpdateSkillValueRequestType = 'models.proto.cofd.api.UpdateSkillValueRequest';
const UpdateSkillValueRequest = root.lookupType(UpdateSkillValueRequestType);
//TODO rpc-ify
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';
function verifyAndCreate(protobufType, payload) {
let err = protobufType.verify(payload);
if (err) throw err;
return protobufType.create(payload);
}
async function updateAttribute(params) {
const { username, characterID, attribute, newValue } = params;
let req = Attribute.create({
name: attribute,
value: parseInt(newValue)
let req = verifyAndCreate(UpdateAttributeRequest, {
characterUsername: username,
characterId: parseInt(characterID),
attributeName: attribute,
attributeValue: parseInt(newValue)
});
const resource = attributesResource(username, characterID);
let resp = await fetch('/api/rpc/cofd/update_attribute', {
method: 'POST',
headers: { ... protobufContentType(UpdateAttributeRequestType) },
body: UpdateAttributeRequest.encode(req).finish()
}).then(async resp => {
console.log("resp is", await resp.text());
}).catch(async err => {
console.log("err is", err.text());
});
}
let resp = await fetch(resource, {
method: 'PATCH',
body: Attribute.encode(req).finish()
async function updateSkillValue(params) {
const { username, characterID, skillName, newValue } = params;
let req = verifyAndCreate(UpdateSkillValueRequest, {
characterUsername: username,
characterId: parseInt(characterID),
skillName: skillName,
skillValue: parseInt(newValue)
});
let resp = await fetch('/api/rpc/cofd/update_skill_value', {
method: 'POST',
headers: { ... protobufContentType(UpdateSkillValueRequestType) },
body: UpdateSkillValueRequest.encode(req).finish()
}).then(async resp => {
console.log("resp is", await resp.text());
}).catch(async err => {
@ -25,6 +68,7 @@ function makeAPI(root) {
}
return {
updateAttribute
updateAttribute,
updateSkillValue
};
}

View File

@ -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);
});