Return semi-proper values from the protobuf HTTP API

This commit is contained in:
jeff 2021-01-05 21:19:47 +00:00
parent dcbf801ecc
commit dd2741a0c1
11 changed files with 280 additions and 181 deletions

View File

@ -41,7 +41,7 @@ fn main() {
config.type_attribute(".", "#[serde(rename_all = \"camelCase\")]"); config.type_attribute(".", "#[serde(rename_all = \"camelCase\")]");
tonic_build::configure() tonic_build::configure()
.build_server(true) .build_server(false)
.build_client(false) .build_client(false)
.compile_with_config( .compile_with_config(
config, config,

View File

@ -31,6 +31,12 @@ message Attributes {
int32 composure = 9; int32 composure = 9;
} }
//Generic "did something succeed or not" response.
message ApiResult {
bool success = 1;
string error = 2;
}
//Update an attribute's dot amount. TODO rename to AttributesUpdate. //Update an attribute's dot amount. TODO rename to AttributesUpdate.
message UpdateAttributeRequest { message UpdateAttributeRequest {
string character_username = 1; string character_username = 1;
@ -53,7 +59,6 @@ message SkillUpdate {
CofdSheet.Skill skill = 2; CofdSheet.Skill skill = 2;
} }
//Partial update of a single skill dot amount. //Partial update of a single skill dot amount.
message UpdateSkillValueRequest { message UpdateSkillValueRequest {
string character_username = 1; string character_username = 1;
@ -73,7 +78,3 @@ message SkillSpecializationsUpdate {
message Condition { message Condition {
string name = 1; string name = 1;
} }
service CofdApi {
rpc UpdateSkillValue(UpdateSkillValueRequest) returns (CofdSheet.Skill);
}

View File

@ -1,117 +0,0 @@
use crate::db::Dao;
use crate::errors::Error;
use crate::models::characters::Character;
use crate::models::proto::cofd::api::cofd_api_server::CofdApi;
use crate::models::proto::cofd::api::UpdateSkillValueRequest;
use crate::models::proto::cofd::cofd_sheet::Skill;
use crate::models::proto::cofd::*;
use crate::models::users::User;
use std::collections::btree_map::{Entry, OccupiedEntry};
use tonic::{Request, Response, Status};
/// Load the character belonging to the given user, as long as they're
/// the owner of that character. Returns an error if user is not
/// logged in, the owner of the character is not found, or the logged
/// in user does not have the permission to access this character.
async fn load_character(
conn: &sqlx::SqlitePool,
logged_in_user: Option<User>,
owner: &str,
character_id: i32,
) -> Result<Character, Error> {
let logged_in_user = logged_in_user.ok_or(Error::NotLoggedIn)?;
let character: Character = conn
.load_character(character_id)
.await?
.ok_or(Error::NotFound)?;
if &logged_in_user.username != owner {
return Err(Error::NoPermission);
}
Ok(character)
}
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())
}
#[derive(Debug)]
pub struct CofdApiService {
pub db: sqlx::SqlitePool,
}
#[tonic::async_trait]
impl CofdApi for CofdApiService {
async fn update_skill_value(
&self,
request: Request<UpdateSkillValueRequest>, // Accept request of type HelloRequest
) -> Result<Response<Skill>, Status> {
let user_id: &str = request
.metadata()
.get("user_id")
.and_then(|user_id| user_id.to_str().ok())
.ok_or(Error::NotLoggedIn)?;
let logged_in_user = self
.db
.load_user(user_id)
.await
.map_err(|e| Error::from(e))?;
//Can use metadata map to add user id inside interceptor for auth.
let request = request.into_inner();
let mut character = load_character(
&self.db,
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)?;
self.db
.update_character_sheet(&character)
.await
.map_err(|e| Error::from(e))?; //TODO maybe use crate Error for db
let reply = Skill::default();
Ok(Response::new(reply)) // Send back our formatted greeting
}
}

View File

@ -18,25 +18,10 @@ use tonic::transport::Server;
pub mod catchers; pub mod catchers;
pub mod db; pub mod db;
pub mod errors; pub mod errors;
pub mod grpc;
pub mod migrator; pub mod migrator;
pub mod models; pub mod models;
pub mod routes; pub mod routes;
async fn make_tonic(db: sqlx::SqlitePool) -> Result<(), Box<dyn std::error::Error>> {
use crate::models::proto::cofd::api::cofd_api_server::CofdApiServer;
let addr = "[::1]:9090".parse()?;
let service = grpc::CofdApiService { db };
info!("Running Tonic");
Server::builder()
.add_service(CofdApiServer::new(service))
.serve(addr)
.await?;
Ok(())
}
async fn make_rocket(database: sqlx::SqlitePool) -> Result<(), Box<dyn std::error::Error>> { async fn make_rocket(database: sqlx::SqlitePool) -> Result<(), Box<dyn std::error::Error>> {
info!("Running Rocket"); info!("Running Rocket");
let root_routes: Vec<rocket::Route> = { let root_routes: Vec<rocket::Route> = {
@ -86,20 +71,5 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
let db = crate::db::create_pool(db_path).await?; let db = crate::db::create_pool(db_path).await?;
tokio::select! { make_rocket(db.clone()).await
result = make_rocket(db.clone()) => {
match result {
Ok(_) => info!("Shutting down Rocket."),
Err(e) => error!("Rocket error: {}", e)
}
}
result = make_tonic(db) => {
match result {
Ok(_) => info!("Shutting down Tonic."),
Err(e) => error!("Tonic error: {}", e)
}
}
}
Ok(())
} }

View File

@ -146,8 +146,8 @@ impl Character {
} }
/// Update the existing character with new serialized protobuf /// Update the existing character with new serialized protobuf
/// data. Consumes the data. /// data.
pub fn update_data<T>(&mut self, data: T) -> Result<(), Error> pub fn update_data<T>(&mut self, data: &T) -> Result<(), Error>
where where
T: prost::Message + std::default::Default, T: prost::Message + std::default::Default,
{ {

View File

@ -1,7 +1,12 @@
use crate::errors::Error; use crate::errors::Error;
use prost::bytes::BytesMut;
use rocket::data::{Data, FromData, Outcome, ToByteUnit}; use rocket::data::{Data, FromData, Outcome, ToByteUnit};
use rocket::http::{ContentType, Status};
use rocket::request::Request; use rocket::request::Request;
use rocket::response::status;
use rocket::response::{self, Responder, Response};
use std::default::Default; use std::default::Default;
use std::io::Cursor;
use std::ops::Deref; use std::ops::Deref;
pub mod cofd; pub mod cofd;
@ -10,7 +15,7 @@ pub mod cofd;
/// data submitted via POST using fetch API. Can automatically be /// data submitted via POST using fetch API. Can automatically be
/// dereferenced into its wrapped type. /// dereferenced into its wrapped type.
#[derive(Debug)] #[derive(Debug)]
pub(crate) struct Proto<T>(T) pub(crate) struct Proto<T>(pub T)
where where
T: prost::Message + Default; T: prost::Message + Default;
@ -47,6 +52,22 @@ where
} }
} }
impl<'r, T> Responder<'r, 'static> for Proto<T>
where
T: prost::Message + Default,
{
fn respond_to(self, req: &Request) -> response::Result<'static> {
let mut buf = BytesMut::with_capacity(std::mem::size_of_val(&self.0));
match self.0.encode(&mut buf) {
Ok(_) => Response::build()
.header(ContentType::new("application", "x-protobuf"))
.sized_body(buf.len(), Cursor::new(buf))
.ok(),
Err(e) => status::Custom(Status::InternalServerError, e.to_string()).respond_to(req),
}
}
}
/// Enable automatically calling methods on a decoded Proto instance. /// Enable automatically calling methods on a decoded Proto instance.
impl<T> Deref for Proto<T> impl<T> Deref for Proto<T>
where where

View File

@ -95,11 +95,11 @@ mod cofd {
} }
#[post("/rpc/cofd/update_attribute_value", data = "<req>")] #[post("/rpc/cofd/update_attribute_value", data = "<req>")]
pub(super) async fn update_attribute_value<'a>( pub(super) async fn update_attribute_value(
req: Proto<UpdateAttributeRequest>, req: Proto<UpdateAttributeRequest>,
conn: TenebrousDbConn<'_>, conn: TenebrousDbConn<'_>,
logged_in_user: Option<&User>, logged_in_user: Option<&User>,
) -> Result<&'a str, Error> { ) -> Result<Proto<ApiResult>, Error> {
let mut character = load_character( let mut character = load_character(
&conn, &conn,
logged_in_user, logged_in_user,
@ -123,9 +123,12 @@ mod cofd {
_ => Err(Error::InvalidInput), _ => Err(Error::InvalidInput),
}?; }?;
character.update_data(sheet)?; character.update_data(&sheet)?;
conn.update_character_sheet(&character).await?; conn.update_character_sheet(&character).await?;
Ok("lol") Ok(Proto(ApiResult {
success: true,
error: "".to_string(),
}))
} }
#[patch( #[patch(
@ -154,7 +157,7 @@ mod cofd {
skill_update.name, skill_update.skill skill_update.name, skill_update.skill
); );
character.update_data(sheet)?; character.update_data(&sheet)?;
conn.update_character_sheet(&character).await?; conn.update_character_sheet(&character).await?;
Ok("lol") Ok("lol")
} }
@ -164,7 +167,7 @@ mod cofd {
request: Proto<UpdateSkillValueRequest>, request: Proto<UpdateSkillValueRequest>,
conn: TenebrousDbConn<'_>, conn: TenebrousDbConn<'_>,
logged_in_user: Option<&User>, logged_in_user: Option<&User>,
) -> Result<&'a str, Error> { ) -> Result<Proto<ApiResult>, Error> {
println!("{:#?}", request); println!("{:#?}", request);
let mut character = load_character( let mut character = load_character(
&conn, &conn,
@ -175,17 +178,22 @@ mod cofd {
.await?; .await?;
let mut sheet: CofdSheet = character.try_deserialize()?; let mut sheet: CofdSheet = character.try_deserialize()?;
let skill: Option<&mut Skill> = find_skill(&mut sheet, &request.skill_name);
skill let mut skill: Option<&mut Skill> = find_skill(&mut sheet, &request.skill_name);
.map(|s| s.dots = request.skill_value) if let Some(ref mut s) = skill {
.ok_or(Error::InvalidInput)?; s.dots = request.skill_value;
}
let updated_skill: Skill = skill.map(|s| s.clone()).ok_or(Error::InvalidInput)?;
println!("updated skill value",); println!("updated skill value",);
character.update_data(sheet)?; character.update_data(&sheet)?;
conn.update_character_sheet(&character).await?; conn.update_character_sheet(&character).await?;
Ok("lol") Ok(Proto(ApiResult {
success: true,
error: "".to_string(),
}))
} }
#[put("/cofd/<owner>/<character_id>/conditions", data = "<info>")] #[put("/cofd/<owner>/<character_id>/conditions", data = "<info>")]

View File

@ -92,6 +92,30 @@ export namespace Attributes {
} }
} }
export class ApiResult extends jspb.Message {
getSuccess(): boolean;
setSuccess(value: boolean): void;
getError(): string;
setError(value: string): void;
serializeBinary(): Uint8Array;
toObject(includeInstance?: boolean): ApiResult.AsObject;
static toObject(includeInstance: boolean, msg: ApiResult): ApiResult.AsObject;
static extensions: {[key: number]: jspb.ExtensionFieldInfo<jspb.Message>};
static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo<jspb.Message>};
static serializeBinaryToWriter(message: ApiResult, writer: jspb.BinaryWriter): void;
static deserializeBinary(bytes: Uint8Array): ApiResult;
static deserializeBinaryFromReader(message: ApiResult, reader: jspb.BinaryReader): ApiResult;
}
export namespace ApiResult {
export type AsObject = {
success: boolean,
error: string,
}
}
export class UpdateAttributeRequest extends jspb.Message { export class UpdateAttributeRequest extends jspb.Message {
getCharacterUsername(): string; getCharacterUsername(): string;
setCharacterUsername(value: string): void; setCharacterUsername(value: string): void;

View File

@ -16,6 +16,7 @@ var global = Function('return this')();
var cofd_pb = require('./cofd_pb.js'); var cofd_pb = require('./cofd_pb.js');
goog.object.extend(proto, cofd_pb); goog.object.extend(proto, cofd_pb);
goog.exportSymbol('proto.models.proto.cofd.api.ApiResult', null, global);
goog.exportSymbol('proto.models.proto.cofd.api.Attributes', null, global); goog.exportSymbol('proto.models.proto.cofd.api.Attributes', null, global);
goog.exportSymbol('proto.models.proto.cofd.api.BasicInfo', null, global); goog.exportSymbol('proto.models.proto.cofd.api.BasicInfo', null, global);
goog.exportSymbol('proto.models.proto.cofd.api.Condition', null, global); goog.exportSymbol('proto.models.proto.cofd.api.Condition', null, global);
@ -66,6 +67,27 @@ if (goog.DEBUG && !COMPILED) {
*/ */
proto.models.proto.cofd.api.Attributes.displayName = 'proto.models.proto.cofd.api.Attributes'; proto.models.proto.cofd.api.Attributes.displayName = 'proto.models.proto.cofd.api.Attributes';
} }
/**
* Generated by JsPbCodeGenerator.
* @param {Array=} opt_data Optional initial data array, typically from a
* server response, or constructed directly in Javascript. The array is used
* in place and becomes part of the constructed object. It is not cloned.
* If no data is provided, the constructed object will be empty, but still
* valid.
* @extends {jspb.Message}
* @constructor
*/
proto.models.proto.cofd.api.ApiResult = function(opt_data) {
jspb.Message.initialize(this, opt_data, 0, -1, null, null);
};
goog.inherits(proto.models.proto.cofd.api.ApiResult, jspb.Message);
if (goog.DEBUG && !COMPILED) {
/**
* @public
* @override
*/
proto.models.proto.cofd.api.ApiResult.displayName = 'proto.models.proto.cofd.api.ApiResult';
}
/** /**
* Generated by JsPbCodeGenerator. * Generated by JsPbCodeGenerator.
* @param {Array=} opt_data Optional initial data array, typically from a * @param {Array=} opt_data Optional initial data array, typically from a
@ -815,6 +837,166 @@ proto.models.proto.cofd.api.Attributes.prototype.setComposure = function(value)
if (jspb.Message.GENERATE_TO_OBJECT) {
/**
* Creates an object representation of this proto.
* Field names that are reserved in JavaScript and will be renamed to pb_name.
* Optional fields that are not set will be set to undefined.
* To access a reserved field use, foo.pb_<name>, eg, foo.pb_default.
* For the list of reserved names please see:
* net/proto2/compiler/js/internal/generator.cc#kKeyword.
* @param {boolean=} opt_includeInstance Deprecated. whether to include the
* JSPB instance for transitional soy proto support:
* http://goto/soy-param-migration
* @return {!Object}
*/
proto.models.proto.cofd.api.ApiResult.prototype.toObject = function(opt_includeInstance) {
return proto.models.proto.cofd.api.ApiResult.toObject(opt_includeInstance, this);
};
/**
* Static version of the {@see toObject} method.
* @param {boolean|undefined} includeInstance Deprecated. Whether to include
* the JSPB instance for transitional soy proto support:
* http://goto/soy-param-migration
* @param {!proto.models.proto.cofd.api.ApiResult} msg The msg instance to transform.
* @return {!Object}
* @suppress {unusedLocalVariables} f is only used for nested messages
*/
proto.models.proto.cofd.api.ApiResult.toObject = function(includeInstance, msg) {
var f, obj = {
success: jspb.Message.getBooleanFieldWithDefault(msg, 1, false),
error: jspb.Message.getFieldWithDefault(msg, 2, "")
};
if (includeInstance) {
obj.$jspbMessageInstance = msg;
}
return obj;
};
}
/**
* Deserializes binary data (in protobuf wire format).
* @param {jspb.ByteSource} bytes The bytes to deserialize.
* @return {!proto.models.proto.cofd.api.ApiResult}
*/
proto.models.proto.cofd.api.ApiResult.deserializeBinary = function(bytes) {
var reader = new jspb.BinaryReader(bytes);
var msg = new proto.models.proto.cofd.api.ApiResult;
return proto.models.proto.cofd.api.ApiResult.deserializeBinaryFromReader(msg, reader);
};
/**
* Deserializes binary data (in protobuf wire format) from the
* given reader into the given message object.
* @param {!proto.models.proto.cofd.api.ApiResult} msg The message object to deserialize into.
* @param {!jspb.BinaryReader} reader The BinaryReader to use.
* @return {!proto.models.proto.cofd.api.ApiResult}
*/
proto.models.proto.cofd.api.ApiResult.deserializeBinaryFromReader = function(msg, reader) {
while (reader.nextField()) {
if (reader.isEndGroup()) {
break;
}
var field = reader.getFieldNumber();
switch (field) {
case 1:
var value = /** @type {boolean} */ (reader.readBool());
msg.setSuccess(value);
break;
case 2:
var value = /** @type {string} */ (reader.readString());
msg.setError(value);
break;
default:
reader.skipField();
break;
}
}
return msg;
};
/**
* Serializes the message to binary data (in protobuf wire format).
* @return {!Uint8Array}
*/
proto.models.proto.cofd.api.ApiResult.prototype.serializeBinary = function() {
var writer = new jspb.BinaryWriter();
proto.models.proto.cofd.api.ApiResult.serializeBinaryToWriter(this, writer);
return writer.getResultBuffer();
};
/**
* Serializes the given message to binary data (in protobuf wire
* format), writing to the given BinaryWriter.
* @param {!proto.models.proto.cofd.api.ApiResult} message
* @param {!jspb.BinaryWriter} writer
* @suppress {unusedLocalVariables} f is only used for nested messages
*/
proto.models.proto.cofd.api.ApiResult.serializeBinaryToWriter = function(message, writer) {
var f = undefined;
f = message.getSuccess();
if (f) {
writer.writeBool(
1,
f
);
}
f = message.getError();
if (f.length > 0) {
writer.writeString(
2,
f
);
}
};
/**
* optional bool success = 1;
* @return {boolean}
*/
proto.models.proto.cofd.api.ApiResult.prototype.getSuccess = function() {
return /** @type {boolean} */ (jspb.Message.getBooleanFieldWithDefault(this, 1, false));
};
/**
* @param {boolean} value
* @return {!proto.models.proto.cofd.api.ApiResult} returns this
*/
proto.models.proto.cofd.api.ApiResult.prototype.setSuccess = function(value) {
return jspb.Message.setProto3BooleanField(this, 1, value);
};
/**
* optional string error = 2;
* @return {string}
*/
proto.models.proto.cofd.api.ApiResult.prototype.getError = function() {
return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 2, ""));
};
/**
* @param {string} value
* @return {!proto.models.proto.cofd.api.ApiResult} returns this
*/
proto.models.proto.cofd.api.ApiResult.prototype.setError = function(value) {
return jspb.Message.setProto3StringField(this, 2, value);
};
if (jspb.Message.GENERATE_TO_OBJECT) { if (jspb.Message.GENERATE_TO_OBJECT) {
/** /**
* Creates an object representation of this proto. * Creates an object representation of this proto.

View File

@ -1,24 +1,31 @@
import * as jspb from "google-protobuf"; import * as jspb from "google-protobuf";
import { UpdateAttributeRequest, UpdateSkillValueRequest } from "../_proto/cofd_api_pb"; import { CofdSheet } from "../_proto/cofd_pb";
import { Skills, ApiResult, UpdateAttributeRequest, UpdateSkillValueRequest } from "../_proto/cofd_api_pb";
const PROTOBUF_CONTENT_TYPE = { 'Content-Type': 'application/x-protobuf' }; const PROTOBUF_CONTENT_TYPE = { 'Content-Type': 'application/x-protobuf' };
async function makeRequest<T extends jspb.Message>(uri: string, params: T) { function staticImplements<T>() {
return <U extends T>(constructor: U) => { constructor };
}
async function makeRequest<T extends jspb.Message>(uri: string, params: T): Promise<Uint8Array> {
let resp = await fetch(uri, { let resp = await fetch(uri, {
method: 'POST', method: 'POST',
headers: { ...PROTOBUF_CONTENT_TYPE }, headers: { ...PROTOBUF_CONTENT_TYPE },
body: params.serializeBinary() body: params.serializeBinary()
}).then(async resp => {
console.log("resp is", await resp.text());
}).catch(async err => {
console.log("err is", err.text());
}); });
const data = await resp.arrayBuffer();
return new Uint8Array(data);
} }
export async function updateSkillValue(params: UpdateSkillValueRequest) { export async function updateSkillValue(params: UpdateSkillValueRequest): Promise<ApiResult> {
await makeRequest('/api/rpc/cofd/update_skill_value', params); let data = await makeRequest('/api/rpc/cofd/update_skill_value', params);
return ApiResult.deserializeBinary(data);
} }
export async function updateAttributeValue(params: UpdateAttributeRequest) { export async function updateAttributeValue(params: UpdateAttributeRequest): Promise<ApiResult> {
await makeRequest('/api/rpc/cofd/update_attribute_value', params); let data = await makeRequest('/api/rpc/cofd/update_attribute_value', params);
return ApiResult.deserializeBinary(data);
} }

View File

@ -25,7 +25,8 @@ import * as api from "../api";
params.setCharacterId(parseInt(CHARACTER_ID)); params.setCharacterId(parseInt(CHARACTER_ID));
params.setAttributeName(attribute); params.setAttributeName(attribute);
params.setAttributeValue(newValue); params.setAttributeValue(newValue);
await api.updateAttributeValue(params); let resp = await api.updateAttributeValue(params);
console.log("got a response back", resp);
} }
Array.from(attributeInputs).forEach(input => { Array.from(attributeInputs).forEach(input => {
@ -50,7 +51,9 @@ import * as api from "../api";
params.setSkillName(attribute); params.setSkillName(attribute);
params.setSkillValue(newValue); params.setSkillValue(newValue);
await api.updateSkillValue(params); let resp = await api.updateSkillValue(params);
console.log("got a response back", resp);
} }
Array.from(skillInputs).forEach(input => { Array.from(skillInputs).forEach(input => {