From a729ccb4a2a691a9c5bd1ff99f6ed88eb997b0c9 Mon Sep 17 00:00:00 2001 From: projectmoon Date: Tue, 5 Jan 2021 21:19:47 +0000 Subject: [PATCH] Return semi-proper values from the protobuf HTTP API --- build.rs | 2 +- proto/cofd_api.proto | 11 +- src/grpc.rs | 117 ---------------- src/main.rs | 32 +---- src/models/characters.rs | 4 +- src/models/proto.rs | 23 +++- src/routes/api.rs | 32 +++-- static/scripts/_proto/cofd_api_pb.d.ts | 24 ++++ static/scripts/_proto/cofd_api_pb.js | 182 +++++++++++++++++++++++++ static/scripts/src/api.ts | 27 ++-- static/scripts/src/characters/edit.ts | 7 +- 11 files changed, 280 insertions(+), 181 deletions(-) delete mode 100644 src/grpc.rs diff --git a/build.rs b/build.rs index 1832669..12c2ab5 100644 --- a/build.rs +++ b/build.rs @@ -41,7 +41,7 @@ fn main() { config.type_attribute(".", "#[serde(rename_all = \"camelCase\")]"); tonic_build::configure() - .build_server(true) + .build_server(false) .build_client(false) .compile_with_config( config, diff --git a/proto/cofd_api.proto b/proto/cofd_api.proto index 5b7ab58..72f9791 100644 --- a/proto/cofd_api.proto +++ b/proto/cofd_api.proto @@ -31,6 +31,12 @@ message Attributes { 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. message UpdateAttributeRequest { string character_username = 1; @@ -53,7 +59,6 @@ message SkillUpdate { CofdSheet.Skill skill = 2; } - //Partial update of a single skill dot amount. message UpdateSkillValueRequest { string character_username = 1; @@ -72,8 +77,4 @@ message SkillSpecializationsUpdate { //Add a Condition to a Chronicles of Darkness character sheet. message Condition { string name = 1; -} - -service CofdApi { - rpc UpdateSkillValue(UpdateSkillValueRequest) returns (CofdSheet.Skill); } \ No newline at end of file diff --git a/src/grpc.rs b/src/grpc.rs deleted file mode 100644 index ca75494..0000000 --- a/src/grpc.rs +++ /dev/null @@ -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, - owner: &str, - character_id: i32, -) -> Result { - 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> { - 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()) -} - -#[derive(Debug)] -pub struct CofdApiService { - pub db: sqlx::SqlitePool, -} - -#[tonic::async_trait] -impl CofdApi for CofdApiService { - async fn update_skill_value( - &self, - request: Request, // Accept request of type HelloRequest - ) -> Result, 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 - } -} diff --git a/src/main.rs b/src/main.rs index 5748883..4c90547 100644 --- a/src/main.rs +++ b/src/main.rs @@ -18,25 +18,10 @@ use tonic::transport::Server; pub mod catchers; pub mod db; pub mod errors; -pub mod grpc; pub mod migrator; pub mod models; pub mod routes; -async fn make_tonic(db: sqlx::SqlitePool) -> Result<(), Box> { - 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> { info!("Running Rocket"); let root_routes: Vec = { @@ -86,20 +71,5 @@ async fn main() -> Result<(), Box> { let db = crate::db::create_pool(db_path).await?; - tokio::select! { - 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(()) + make_rocket(db.clone()).await } diff --git a/src/models/characters.rs b/src/models/characters.rs index b6155c4..f75476e 100644 --- a/src/models/characters.rs +++ b/src/models/characters.rs @@ -146,8 +146,8 @@ impl Character { } /// Update the existing character with new serialized protobuf - /// data. Consumes the data. - pub fn update_data(&mut self, data: T) -> Result<(), Error> + /// data. + pub fn update_data(&mut self, data: &T) -> Result<(), Error> where T: prost::Message + std::default::Default, { diff --git a/src/models/proto.rs b/src/models/proto.rs index 31b6d95..c998348 100644 --- a/src/models/proto.rs +++ b/src/models/proto.rs @@ -1,7 +1,12 @@ use crate::errors::Error; +use prost::bytes::BytesMut; use rocket::data::{Data, FromData, Outcome, ToByteUnit}; +use rocket::http::{ContentType, Status}; use rocket::request::Request; +use rocket::response::status; +use rocket::response::{self, Responder, Response}; use std::default::Default; +use std::io::Cursor; use std::ops::Deref; pub mod cofd; @@ -10,7 +15,7 @@ pub mod cofd; /// data submitted via POST using fetch API. Can automatically be /// dereferenced into its wrapped type. #[derive(Debug)] -pub(crate) struct Proto(T) +pub(crate) struct Proto(pub T) where T: prost::Message + Default; @@ -47,6 +52,22 @@ where } } +impl<'r, T> Responder<'r, 'static> for Proto +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. impl Deref for Proto where diff --git a/src/routes/api.rs b/src/routes/api.rs index 7378e2c..4948f04 100644 --- a/src/routes/api.rs +++ b/src/routes/api.rs @@ -95,11 +95,11 @@ mod cofd { } #[post("/rpc/cofd/update_attribute_value", data = "")] - pub(super) async fn update_attribute_value<'a>( + pub(super) async fn update_attribute_value( req: Proto, conn: TenebrousDbConn<'_>, logged_in_user: Option<&User>, - ) -> Result<&'a str, Error> { + ) -> Result, Error> { let mut character = load_character( &conn, logged_in_user, @@ -123,9 +123,12 @@ mod cofd { _ => Err(Error::InvalidInput), }?; - character.update_data(sheet)?; + character.update_data(&sheet)?; conn.update_character_sheet(&character).await?; - Ok("lol") + Ok(Proto(ApiResult { + success: true, + error: "".to_string(), + })) } #[patch( @@ -154,7 +157,7 @@ mod cofd { skill_update.name, skill_update.skill ); - character.update_data(sheet)?; + character.update_data(&sheet)?; conn.update_character_sheet(&character).await?; Ok("lol") } @@ -164,7 +167,7 @@ mod cofd { request: Proto, conn: TenebrousDbConn<'_>, logged_in_user: Option<&User>, - ) -> Result<&'a str, Error> { + ) -> Result, Error> { println!("{:#?}", request); let mut character = load_character( &conn, @@ -175,17 +178,22 @@ mod cofd { .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)?; + let mut skill: Option<&mut Skill> = find_skill(&mut sheet, &request.skill_name); + if let Some(ref mut s) = skill { + s.dots = request.skill_value; + } + + let updated_skill: Skill = skill.map(|s| s.clone()).ok_or(Error::InvalidInput)?; println!("updated skill value",); - character.update_data(sheet)?; + character.update_data(&sheet)?; conn.update_character_sheet(&character).await?; - Ok("lol") + Ok(Proto(ApiResult { + success: true, + error: "".to_string(), + })) } #[put("/cofd///conditions", data = "")] diff --git a/static/scripts/_proto/cofd_api_pb.d.ts b/static/scripts/_proto/cofd_api_pb.d.ts index 2ec1803..a76e7d7 100644 --- a/static/scripts/_proto/cofd_api_pb.d.ts +++ b/static/scripts/_proto/cofd_api_pb.d.ts @@ -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}; + static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo}; + 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 { getCharacterUsername(): string; setCharacterUsername(value: string): void; diff --git a/static/scripts/_proto/cofd_api_pb.js b/static/scripts/_proto/cofd_api_pb.js index 470f0f6..db51682 100644 --- a/static/scripts/_proto/cofd_api_pb.js +++ b/static/scripts/_proto/cofd_api_pb.js @@ -16,6 +16,7 @@ var global = Function('return this')(); var cofd_pb = require('./cofd_pb.js'); 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.BasicInfo', 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'; } +/** + * 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. * @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_, 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) { /** * Creates an object representation of this proto. diff --git a/static/scripts/src/api.ts b/static/scripts/src/api.ts index 352e540..2e33fbe 100644 --- a/static/scripts/src/api.ts +++ b/static/scripts/src/api.ts @@ -1,24 +1,31 @@ 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' }; -async function makeRequest(uri: string, params: T) { +function staticImplements() { + return (constructor: U) => { constructor }; +} + + +async function makeRequest(uri: string, params: T): Promise { let resp = await fetch(uri, { method: 'POST', headers: { ...PROTOBUF_CONTENT_TYPE }, 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) { - await makeRequest('/api/rpc/cofd/update_skill_value', params); +export async function updateSkillValue(params: UpdateSkillValueRequest): Promise { + let data = await makeRequest('/api/rpc/cofd/update_skill_value', params); + return ApiResult.deserializeBinary(data); } -export async function updateAttributeValue(params: UpdateAttributeRequest) { - await makeRequest('/api/rpc/cofd/update_attribute_value', params); +export async function updateAttributeValue(params: UpdateAttributeRequest): Promise { + let data = await makeRequest('/api/rpc/cofd/update_attribute_value', params); + return ApiResult.deserializeBinary(data); } diff --git a/static/scripts/src/characters/edit.ts b/static/scripts/src/characters/edit.ts index f607ff6..b520327 100644 --- a/static/scripts/src/characters/edit.ts +++ b/static/scripts/src/characters/edit.ts @@ -25,7 +25,8 @@ import * as api from "../api"; params.setCharacterId(parseInt(CHARACTER_ID)); params.setAttributeName(attribute); 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 => { @@ -50,7 +51,9 @@ import * as api from "../api"; params.setSkillName(attribute); 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 => {