Return structured server errors in FFI

This commit is contained in:
Tyler Hallada 2021-02-28 18:50:10 -05:00
parent e80fb6b9d0
commit dffe435c59
10 changed files with 568 additions and 294 deletions

View File

@ -5,6 +5,65 @@
#include <cassert> #include <cassert>
struct FFIServerError {
uint16_t status;
const char *title;
const char *detail;
};
struct FFIError {
enum class Tag : uint8_t {
Server,
Network,
};
struct Server_Body {
FFIServerError _0;
};
struct Network_Body {
const char *_0;
};
Tag tag;
union {
Server_Body server;
Network_Body network;
};
static FFIError Server(const FFIServerError &_0) {
FFIError result;
::new (&result.server._0) (FFIServerError)(_0);
result.tag = Tag::Server;
return result;
}
bool IsServer() const {
return tag == Tag::Server;
}
const FFIServerError& AsServer() const {
assert(IsServer());
return server._0;
}
static FFIError Network(const char *const &_0) {
FFIError result;
::new (&result.network._0) (const char*)(_0);
result.tag = Tag::Network;
return result;
}
bool IsNetwork() const {
return tag == Tag::Network;
}
const char*const & AsNetwork() const {
assert(IsNetwork());
return network._0;
}
};
template<typename T> template<typename T>
struct FFIResult { struct FFIResult {
enum class Tag : uint8_t { enum class Tag : uint8_t {
@ -17,7 +76,7 @@ struct FFIResult {
}; };
struct Err_Body { struct Err_Body {
const char *_0; FFIError _0;
}; };
Tag tag; Tag tag;
@ -42,9 +101,9 @@ struct FFIResult {
return ok._0; return ok._0;
} }
static FFIResult Err(const char *const &_0) { static FFIResult Err(const FFIError &_0) {
FFIResult result; FFIResult result;
::new (&result.err._0) (const char*)(_0); ::new (&result.err._0) (FFIError)(_0);
result.tag = Tag::Err; result.tag = Tag::Err;
return result; return result;
} }
@ -53,7 +112,7 @@ struct FFIResult {
return tag == Tag::Err; return tag == Tag::Err;
} }
const char*const & AsErr() const { const FFIError& AsErr() const {
assert(IsErr()); assert(IsErr());
return err._0; return err._0;
} }

View File

@ -83,29 +83,43 @@ pub fn update_file_caches(
pub fn from_file_cache<T: for<'de> Deserialize<'de>>(cache_path: &Path) -> Result<T> { pub fn from_file_cache<T: for<'de> Deserialize<'de>>(cache_path: &Path) -> Result<T> {
#[cfg(not(test))] #[cfg(not(test))]
let file = File::open(cache_path).context(format!( let file = File::open(cache_path).with_context(|| {
"Object not found in API or in cache: {}", format!(
cache_path.file_name().unwrap_or_default().to_string_lossy() "Object not found in API or in cache: {}",
))?; cache_path.file_name().unwrap_or_default().to_string_lossy()
)
})?;
#[cfg(test)] #[cfg(test)]
let file = tempfile()?; // cache always reads from an empty temp file in cfg(test) let file = tempfile()?; // cache always reads from an empty temp file in cfg(test)
let reader = BufReader::new(file); let reader = BufReader::new(file);
info!("returning value from cache: {:?}", cache_path); info!("returning value from cache: {:?}", cache_path);
Ok(bincode::deserialize_from(reader)?) Ok(bincode::deserialize_from(reader).with_context(|| {
format!(
"Object not found in API or in cache: {}",
cache_path.file_name().unwrap_or_default().to_string_lossy(),
)
})?)
} }
pub fn load_metadata_from_file_cache(cache_path: &Path) -> Result<Metadata> { pub fn load_metadata_from_file_cache(cache_path: &Path) -> Result<Metadata> {
#[cfg(not(test))] #[cfg(not(test))]
let file = File::open(cache_path).context(format!( let file = File::open(cache_path).with_context(|| {
"Object not found in API or in cache: {}", format!(
cache_path.file_name().unwrap_or_default().to_string_lossy() "Object not found in API or in cache: {}",
))?; cache_path.file_name().unwrap_or_default().to_string_lossy()
)
})?;
#[cfg(test)] #[cfg(test)]
let file = tempfile()?; // cache always reads from an empty temp file in cfg(test) let file = tempfile()?; // cache always reads from an empty temp file in cfg(test)
let reader = BufReader::new(file); let reader = BufReader::new(file);
info!("returning value from cache: {:?}", cache_path); info!("returning value from cache: {:?}", cache_path);
let metadata: Metadata = serde_json::from_reader(reader)?; let metadata: Metadata = serde_json::from_reader(reader).with_context(|| {
format!(
"Object not found in API or in cache: {}",
cache_path.file_name().unwrap_or_default().to_string_lossy(),
)
})?;
Ok(metadata) Ok(metadata)
} }

View File

@ -10,7 +10,11 @@ use log::{error, info};
#[cfg(test)] #[cfg(test)]
use std::{println as info, println as error}; use std::{println as info, println as error};
use crate::{log_server_error, result::FFIResult}; use crate::{
error::extract_error_from_response,
log_server_error,
result::{FFIError, FFIResult},
};
#[no_mangle] #[no_mangle]
pub extern "C" fn init() -> bool { pub extern "C" fn init() -> bool {
@ -33,34 +37,30 @@ pub extern "C" fn status_check(api_url: *const c_char) -> FFIResult<bool> {
let api_url = unsafe { CStr::from_ptr(api_url) }.to_string_lossy(); let api_url = unsafe { CStr::from_ptr(api_url) }.to_string_lossy();
info!("status_check api_url: {:?}", api_url); info!("status_check api_url: {:?}", api_url);
fn inner(api_url: &str) -> Result<Response> { fn inner(api_url: &str) -> Result<()> {
#[cfg(not(test))] #[cfg(not(test))]
let api_url = Url::parse(api_url)?.join("v1/status")?; let api_url = Url::parse(api_url)?.join("v1/status")?;
#[cfg(test)] #[cfg(test)]
let api_url = Url::parse(&mockito::server_url())?.join("v1/status")?; let api_url = Url::parse(&mockito::server_url())?.join("v1/status")?;
Ok(reqwest::blocking::get(api_url)?) let resp = reqwest::blocking::get(api_url)?;
let status = resp.status();
let bytes = resp.bytes()?;
if status.is_success() {
Ok(())
} else {
Err(extract_error_from_response(status, &bytes))
}
} }
match inner(&api_url) { match inner(&api_url) {
Ok(resp) if resp.status() == 200 => { Ok(()) => {
info!("status_check ok"); info!("status_check ok");
FFIResult::Ok(true) FFIResult::Ok(true)
} }
Ok(resp) => {
error!("status_check failed. Server error");
log_server_error(resp);
let err_string = CString::new("API returned a non-200 status code".to_string())
.expect("could not create CString")
.into_raw();
FFIResult::Err(err_string)
}
Err(err) => { Err(err) => {
error!("status_check failed. {}", err); error!("status_check failed. {}", err);
let err_string = CString::new(err.to_string()) FFIResult::Err(FFIError::from(err))
.expect("could not create CString")
.into_raw();
FFIResult::Err(err_string)
} }
} }
} }
@ -91,9 +91,17 @@ mod tests {
FFIResult::Ok(success) => { FFIResult::Ok(success) => {
assert_eq!(success, true); assert_eq!(success, true);
} }
FFIResult::Err(error) => panic!("status_check returned error: {:?}", unsafe { FFIResult::Err(error) => panic!(
CStr::from_ptr(error).to_string_lossy() "status_check returned error: {:?}",
}), match error {
FFIError::Server(server_error) =>
format!("{} {}", server_error.status, unsafe {
CStr::from_ptr(server_error.title).to_string_lossy()
}),
FFIError::Network(network_error) =>
unsafe { CStr::from_ptr(network_error).to_string_lossy() }.to_string(),
}
),
} }
} }
@ -109,12 +117,16 @@ mod tests {
mock.assert(); mock.assert();
match result { match result {
FFIResult::Ok(success) => panic!("status_check returned Ok result: {:?}", success), FFIResult::Ok(success) => panic!("status_check returned Ok result: {:?}", success),
FFIResult::Err(error) => { FFIResult::Err(error) => match error {
assert_eq!( FFIError::Server(server_error) => {
unsafe { CStr::from_ptr(error).to_string_lossy() }, assert_eq!(server_error.status, 500);
"API returned a non-200 status code" assert_eq!(
); unsafe { CStr::from_ptr(server_error.title).to_string_lossy() },
} "Internal Server Error"
);
}
_ => panic!("status_check did not return a server error"),
},
} }
} }
} }

View File

@ -1,3 +1,4 @@
use std::fmt;
use std::str; use std::str;
use anyhow::{anyhow, Error}; use anyhow::{anyhow, Error};
@ -10,27 +11,51 @@ use log::error;
#[cfg(test)] #[cfg(test)]
use std::println as error; use std::println as error;
pub fn extract_error_from_response(status: StatusCode, bytes: &Bytes) -> Error { #[derive(Debug)]
match serde_json::from_slice::<HttpApiProblem>(bytes) { pub struct ServerError {
Ok(api_problem) => { pub status: StatusCode,
let detail = api_problem.detail.unwrap_or("".to_string()); pub title: String,
error!( pub detail: Option<String>,
"Server {}: {}. {}", }
status.as_u16(),
api_problem.title, impl fmt::Display for ServerError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
if let Some(detail) = &self.detail {
write!(
f,
"Server {} {}: {}",
self.status.as_u16(),
self.title,
detail detail
); )
anyhow!(format!( } else {
"Server {}: {}. {}", write!(f, "Server {} {}", self.status.as_u16(), self.title)
status.as_u16(), }
api_problem.title, }
detail }
))
} pub fn extract_error_from_response(status: StatusCode, bytes: &Bytes) -> Error {
Err(_) => { match serde_json::from_slice::<HttpApiProblem>(bytes) {
let detail = str::from_utf8(bytes).unwrap_or("unknown"); Ok(api_problem) => {
error!("Server {}: {}", status.as_u16(), detail); let server_error = ServerError {
anyhow!(format!("Server {}: {}", status.as_u16(), detail)) status,
title: api_problem.title,
detail: api_problem.detail,
};
error!("{}", server_error);
anyhow!(server_error)
}
Err(_) => {
let title = str::from_utf8(bytes)
.unwrap_or_else(|_| &status.canonical_reason().unwrap_or("unknown"))
.to_string();
let server_error = ServerError {
status,
title,
detail: None,
};
error!("{}", server_error);
anyhow!(server_error)
} }
} }
} }

View File

@ -11,9 +11,13 @@ use log::{error, info};
use std::{println as info, println as error}; use std::{println as info, println as error};
use crate::{ use crate::{
cache::file_cache_dir, cache::from_file_cache, cache::load_metadata_from_file_cache, cache::file_cache_dir,
cache::update_file_caches, error::extract_error_from_response, log_server_error, cache::from_file_cache,
result::FFIResult, cache::load_metadata_from_file_cache,
cache::update_file_caches,
error::extract_error_from_response,
log_server_error,
result::{FFIError, FFIResult},
}; };
#[derive(Serialize, Deserialize, Debug)] #[derive(Serialize, Deserialize, Debug)]
@ -334,11 +338,7 @@ pub extern "C" fn create_interior_ref_list(
Ok(interior_ref_list) => FFIResult::Ok(interior_ref_list.id), Ok(interior_ref_list) => FFIResult::Ok(interior_ref_list.id),
Err(err) => { Err(err) => {
error!("create_interior_ref_list failed. {}", err); error!("create_interior_ref_list failed. {}", err);
// TODO: also need to drop this CString once C++ is done reading it FFIResult::Err(FFIError::from(err))
let err_string = CString::new(err.to_string())
.expect("could not create CString")
.into_raw();
FFIResult::Err(err_string)
} }
} }
} }
@ -419,11 +419,7 @@ pub extern "C" fn update_interior_ref_list(
Ok(interior_ref_list) => FFIResult::Ok(interior_ref_list.id), Ok(interior_ref_list) => FFIResult::Ok(interior_ref_list.id),
Err(err) => { Err(err) => {
error!("update_interior_ref_list failed. {}", err); error!("update_interior_ref_list failed. {}", err);
// TODO: also need to drop this CString once C++ is done reading it FFIResult::Err(FFIError::from(err))
let err_string = CString::new(err.to_string())
.expect("could not create CString")
.into_raw();
FFIResult::Err(err_string)
} }
} }
} }
@ -526,13 +522,8 @@ pub extern "C" fn get_interior_ref_list(
}) })
} }
Err(err) => { Err(err) => {
error!("interior_ref_list failed. {}", err); error!("get_interior_ref_list failed. {}", err);
// TODO: how to do error handling? FFIResult::Err(FFIError::from(err))
let err_string = CString::new(err.to_string())
.expect("could not create CString")
.into_raw();
// TODO: also need to drop this CString once C++ is done reading it
FFIResult::Err(err_string)
} }
} }
} }
@ -633,12 +624,7 @@ pub extern "C" fn get_interior_ref_list_by_shop_id(
} }
Err(err) => { Err(err) => {
error!("get_interior_ref_list_by_shop_id failed. {}", err); error!("get_interior_ref_list_by_shop_id failed. {}", err);
// TODO: how to do error handling? FFIResult::Err(FFIError::from(err))
let err_string = CString::new(err.to_string())
.expect("could not create CString")
.into_raw();
// TODO: also need to drop this CString once C++ is done reading it
FFIResult::Err(err_string)
} }
} }
} }
@ -742,11 +728,17 @@ mod tests {
FFIResult::Ok(interior_ref_list_id) => { FFIResult::Ok(interior_ref_list_id) => {
assert_eq!(interior_ref_list_id, 1); assert_eq!(interior_ref_list_id, 1);
} }
FFIResult::Err(error) => { FFIResult::Err(error) => panic!(
panic!("create_interior_ref_list returned error: {:?}", unsafe { "create_interior_ref_list returned error: {:?}",
CStr::from_ptr(error).to_string_lossy() match error {
}) FFIError::Server(server_error) =>
} format!("{} {}", server_error.status, unsafe {
CStr::from_ptr(server_error.title).to_string_lossy()
}),
FFIError::Network(network_error) =>
unsafe { CStr::from_ptr(network_error).to_string_lossy() }.to_string(),
}
),
} }
} }
@ -805,12 +797,16 @@ mod tests {
"create_interior_ref_list returned Ok result: {:?}", "create_interior_ref_list returned Ok result: {:?}",
interior_ref_list_id interior_ref_list_id
), ),
FFIResult::Err(error) => { FFIResult::Err(error) => match error {
assert_eq!( FFIError::Server(server_error) => {
unsafe { CStr::from_ptr(error).to_string_lossy() }, assert_eq!(server_error.status, 500);
"Server 500: Internal Server Error" assert_eq!(
); unsafe { CStr::from_ptr(server_error.title).to_string_lossy() },
} "Internal Server Error"
);
}
_ => panic!("create_interior_ref_list did not return a server error"),
},
} }
} }
@ -905,11 +901,17 @@ mod tests {
FFIResult::Ok(interior_ref_list_id) => { FFIResult::Ok(interior_ref_list_id) => {
assert_eq!(interior_ref_list_id, 1); assert_eq!(interior_ref_list_id, 1);
} }
FFIResult::Err(error) => { FFIResult::Err(error) => panic!(
panic!("update_interior_ref_list returned error: {:?}", unsafe { "update_interior_ref_list returned error: {:?}",
CStr::from_ptr(error).to_string_lossy() match error {
}) FFIError::Server(server_error) =>
} format!("{} {}", server_error.status, unsafe {
CStr::from_ptr(server_error.title).to_string_lossy()
}),
FFIError::Network(network_error) =>
unsafe { CStr::from_ptr(network_error).to_string_lossy() }.to_string(),
}
),
} }
} }
@ -968,12 +970,16 @@ mod tests {
"update_interior_ref_list returned Ok result: {:?}", "update_interior_ref_list returned Ok result: {:?}",
interior_ref_list_id interior_ref_list_id
), ),
FFIResult::Err(error) => { FFIResult::Err(error) => match error {
assert_eq!( FFIError::Server(server_error) => {
unsafe { CStr::from_ptr(error).to_string_lossy() }, assert_eq!(server_error.status, 500);
"Server 500: Internal Server Error" assert_eq!(
); unsafe { CStr::from_ptr(server_error.title).to_string_lossy() },
} "Internal Server Error"
);
}
_ => panic!("update_interior_ref_list did not return a server error"),
},
} }
} }
@ -1080,9 +1086,17 @@ mod tests {
assert_eq!(raw_shelf.sort_on, std::ptr::null()); assert_eq!(raw_shelf.sort_on, std::ptr::null());
assert_eq!(raw_shelf.sort_asc, true); assert_eq!(raw_shelf.sort_asc, true);
} }
FFIResult::Err(error) => panic!("get_interior_ref_list returned error: {:?}", unsafe { FFIResult::Err(error) => panic!(
CStr::from_ptr(error).to_string_lossy() "get_interior_ref_list returned error: {:?}",
}), match error {
FFIError::Server(server_error) =>
format!("{} {}", server_error.status, unsafe {
CStr::from_ptr(server_error.title).to_string_lossy()
}),
FFIError::Network(network_error) =>
unsafe { CStr::from_ptr(network_error).to_string_lossy() }.to_string(),
}
),
} }
} }
@ -1102,12 +1116,15 @@ mod tests {
"get_interior_ref_list returned Ok result: {:#x?}", "get_interior_ref_list returned Ok result: {:#x?}",
raw_interior_ref_vec raw_interior_ref_vec
), ),
FFIResult::Err(error) => { FFIResult::Err(error) => match error {
assert_eq!( FFIError::Network(network_error) => {
unsafe { CStr::from_ptr(error).to_string_lossy() }, assert_eq!(
"io error: failed to fill whole buffer" // empty tempfile unsafe { CStr::from_ptr(network_error).to_string_lossy() },
); "Object not found in API or in cache: interior_ref_list_1.bin",
} );
}
_ => panic!("get_interior_ref_list did not return a network error"),
},
} }
} }
@ -1216,7 +1233,14 @@ mod tests {
} }
FFIResult::Err(error) => panic!( FFIResult::Err(error) => panic!(
"get_interior_ref_list_by_shop_id returned error: {:?}", "get_interior_ref_list_by_shop_id returned error: {:?}",
unsafe { CStr::from_ptr(error).to_string_lossy() } match error {
FFIError::Server(server_error) =>
format!("{} {}", server_error.status, unsafe {
CStr::from_ptr(server_error.title).to_string_lossy()
}),
FFIError::Network(network_error) =>
unsafe { CStr::from_ptr(network_error).to_string_lossy() }.to_string(),
}
), ),
} }
} }
@ -1237,12 +1261,15 @@ mod tests {
"get_interior_ref_list_by_shop_id returned Ok result: {:#x?}", "get_interior_ref_list_by_shop_id returned Ok result: {:#x?}",
raw_interior_ref_vec raw_interior_ref_vec
), ),
FFIResult::Err(error) => { FFIResult::Err(error) => match error {
assert_eq!( FFIError::Network(network_error) => {
unsafe { CStr::from_ptr(error).to_string_lossy() }, assert_eq!(
"io error: failed to fill whole buffer" // empty tempfile unsafe { CStr::from_ptr(network_error).to_string_lossy() },
); "Object not found in API or in cache: shop_1_interior_ref_list.bin",
} );
}
_ => panic!("get_interior_ref_list_by_shop_id did not return a network error"),
},
} }
} }
} }

View File

@ -13,7 +13,7 @@ use std::{println as info, println as error};
use crate::{ use crate::{
cache::file_cache_dir, cache::from_file_cache, cache::load_metadata_from_file_cache, cache::file_cache_dir, cache::from_file_cache, cache::load_metadata_from_file_cache,
cache::update_file_caches, error::extract_error_from_response, log_server_error, cache::update_file_caches, error::extract_error_from_response, log_server_error,
result::FFIResult, result::{FFIError, FFIResult},
}; };
#[derive(Serialize, Deserialize, Debug, Clone)] #[derive(Serialize, Deserialize, Debug, Clone)]
@ -207,12 +207,7 @@ pub extern "C" fn create_merchandise_list(
} }
Err(err) => { Err(err) => {
error!("create_merchandise_list failed. {}", err); error!("create_merchandise_list failed. {}", err);
// TODO: how to do error handling? FFIResult::Err(FFIError::from(err))
let err_string = CString::new(err.to_string())
.expect("could not create CString")
.into_raw();
// TODO: also need to drop this CString once C++ is done reading it
FFIResult::Err(err_string)
} }
} }
} }
@ -312,12 +307,7 @@ pub extern "C" fn update_merchandise_list(
} }
Err(err) => { Err(err) => {
error!("update_merchandise_list failed. {}", err); error!("update_merchandise_list failed. {}", err);
// TODO: how to do error handling? FFIResult::Err(FFIError::from(err))
let err_string = CString::new(err.to_string())
.expect("could not create CString")
.into_raw();
// TODO: also need to drop this CString once C++ is done reading it
FFIResult::Err(err_string)
} }
} }
} }
@ -428,13 +418,8 @@ pub extern "C" fn get_merchandise_list(
FFIResult::Ok(RawMerchandiseVec { ptr, len, cap }) FFIResult::Ok(RawMerchandiseVec { ptr, len, cap })
} }
Err(err) => { Err(err) => {
error!("merchandise_list failed. {}", err); error!("get_merchandise_list failed. {}", err);
// TODO: how to do error handling? FFIResult::Err(FFIError::from(err))
let err_string = CString::new(err.to_string())
.expect("could not create CString")
.into_raw();
// TODO: also need to drop this CString once C++ is done reading it
FFIResult::Err(err_string)
} }
} }
} }
@ -539,12 +524,7 @@ pub extern "C" fn get_merchandise_list_by_shop_id(
} }
Err(err) => { Err(err) => {
error!("get_merchandise_list_by_shop_id failed. {}", err); error!("get_merchandise_list_by_shop_id failed. {}", err);
// TODO: how to do error handling? FFIResult::Err(FFIError::from(err))
let err_string = CString::new(err.to_string())
.expect("could not create CString")
.into_raw();
// TODO: also need to drop this CString once C++ is done reading it
FFIResult::Err(err_string)
} }
} }
} }
@ -627,11 +607,17 @@ mod tests {
assert_eq!(raw_merchandise.is_food, false); assert_eq!(raw_merchandise.is_food, false);
assert_eq!(raw_merchandise.price, 100); assert_eq!(raw_merchandise.price, 100);
} }
FFIResult::Err(error) => { FFIResult::Err(error) => panic!(
panic!("create_merchandise_list returned error: {:?}", unsafe { "create_merchandise_list returned error: {:?}",
CStr::from_ptr(error).to_string_lossy() match error {
}) FFIError::Server(server_error) =>
} format!("{} {}", server_error.status, unsafe {
CStr::from_ptr(server_error.title).to_string_lossy()
}),
FFIError::Network(network_error) =>
unsafe { CStr::from_ptr(network_error).to_string_lossy() }.to_string(),
}
),
} }
} }
@ -666,12 +652,16 @@ mod tests {
"create_merchandise_list returned Ok result: {:#x?}", "create_merchandise_list returned Ok result: {:#x?}",
raw_merchandise_vec raw_merchandise_vec
), ),
FFIResult::Err(error) => { FFIResult::Err(error) => match error {
assert_eq!( FFIError::Server(server_error) => {
unsafe { CStr::from_ptr(error).to_string_lossy() }, assert_eq!(server_error.status, 500);
"Server 500: Internal Server Error" assert_eq!(
); unsafe { CStr::from_ptr(server_error.title).to_string_lossy() },
} "Internal Server Error"
);
}
_ => panic!("create_merchandise_list did not return a server error"),
},
} }
} }
@ -745,11 +735,17 @@ mod tests {
assert_eq!(raw_merchandise.is_food, false); assert_eq!(raw_merchandise.is_food, false);
assert_eq!(raw_merchandise.price, 100); assert_eq!(raw_merchandise.price, 100);
} }
FFIResult::Err(error) => { FFIResult::Err(error) => panic!(
panic!("update_merchandise_list returned error: {:?}", unsafe { "update_merchandise_list returned error: {:?}",
CStr::from_ptr(error).to_string_lossy() match error {
}) FFIError::Server(server_error) =>
} format!("{} {}", server_error.status, unsafe {
CStr::from_ptr(server_error.title).to_string_lossy()
}),
FFIError::Network(network_error) =>
unsafe { CStr::from_ptr(network_error).to_string_lossy() }.to_string(),
}
),
} }
} }
@ -784,12 +780,16 @@ mod tests {
"update_merchandise_list returned Ok result: {:#x?}", "update_merchandise_list returned Ok result: {:#x?}",
raw_merchandise_vec raw_merchandise_vec
), ),
FFIResult::Err(error) => { FFIResult::Err(error) => match error {
assert_eq!( FFIError::Server(server_error) => {
unsafe { CStr::from_ptr(error).to_string_lossy() }, assert_eq!(server_error.status, 500);
"Server 500: Internal Server Error" assert_eq!(
); unsafe { CStr::from_ptr(server_error.title).to_string_lossy() },
} "Internal Server Error"
);
}
_ => panic!("update_merchandise_list did not return a server error"),
},
} }
} }
#[test] #[test]
@ -857,9 +857,17 @@ mod tests {
"VendorItemWeapon".to_string(), "VendorItemWeapon".to_string(),
); );
} }
FFIResult::Err(error) => panic!("get_merchandise_list returned error: {:?}", unsafe { FFIResult::Err(error) => panic!(
CStr::from_ptr(error).to_string_lossy() "get_merchandise_list returned error: {:?}",
}), match error {
FFIError::Server(server_error) =>
format!("{} {}", server_error.status, unsafe {
CStr::from_ptr(server_error.title).to_string_lossy()
}),
FFIError::Network(network_error) =>
unsafe { CStr::from_ptr(network_error).to_string_lossy() }.to_string(),
}
),
} }
} }
@ -879,12 +887,15 @@ mod tests {
"get_merchandise_list returned Ok result: {:#x?}", "get_merchandise_list returned Ok result: {:#x?}",
raw_merchandise_vec raw_merchandise_vec
), ),
FFIResult::Err(error) => { FFIResult::Err(error) => match error {
assert_eq!( FFIError::Network(network_error) => {
unsafe { CStr::from_ptr(error).to_string_lossy() }, assert_eq!(
"io error: failed to fill whole buffer" // empty tempfile unsafe { CStr::from_ptr(network_error).to_string_lossy() },
); "Object not found in API or in cache: merchandise_list_1.bin",
} );
}
_ => panic!("get_merchandise_list did not return a network error"),
},
} }
} }
@ -955,7 +966,14 @@ mod tests {
} }
FFIResult::Err(error) => panic!( FFIResult::Err(error) => panic!(
"get_merchandise_list_by_shop_id returned error: {:?}", "get_merchandise_list_by_shop_id returned error: {:?}",
unsafe { CStr::from_ptr(error).to_string_lossy() } match error {
FFIError::Server(server_error) =>
format!("{} {}", server_error.status, unsafe {
CStr::from_ptr(server_error.title).to_string_lossy()
}),
FFIError::Network(network_error) =>
unsafe { CStr::from_ptr(network_error).to_string_lossy() }.to_string(),
}
), ),
} }
} }
@ -976,12 +994,15 @@ mod tests {
"get_merchandise_list_by_shop_id returned Ok result: {:#x?}", "get_merchandise_list_by_shop_id returned Ok result: {:#x?}",
raw_merchandise_vec raw_merchandise_vec
), ),
FFIResult::Err(error) => { FFIResult::Err(error) => match error {
assert_eq!( FFIError::Network(network_error) => {
unsafe { CStr::from_ptr(error).to_string_lossy() }, assert_eq!(
"io error: failed to fill whole buffer" // empty tempfile unsafe { CStr::from_ptr(network_error).to_string_lossy() },
); "Object not found in API or in cache: shop_1_merchandise_list.bin",
} );
}
_ => panic!("get_merchandise_list_by_shop_id did not return a network error"),
},
} }
} }
} }

View File

@ -11,8 +11,10 @@ use log::{error, info};
use std::{println as info, println as error}; use std::{println as info, println as error};
use crate::{ use crate::{
cache::file_cache_dir, cache::update_file_caches, error::extract_error_from_response, cache::file_cache_dir,
result::FFIResult, cache::update_file_caches,
error::extract_error_from_response,
result::{FFIError, FFIResult},
}; };
#[derive(Serialize, Deserialize, Debug)] #[derive(Serialize, Deserialize, Debug)]
@ -112,11 +114,7 @@ pub extern "C" fn create_owner(
} }
Err(err) => { Err(err) => {
error!("create_owner failed. {}", err); error!("create_owner failed. {}", err);
// TODO: also need to drop this CString once C++ is done reading it FFIResult::Err(FFIError::from(err))
let err_string = CString::new(err.to_string())
.expect("could not create CString")
.into_raw();
FFIResult::Err(err_string)
} }
} }
} }
@ -182,11 +180,7 @@ pub extern "C" fn update_owner(
} }
Err(err) => { Err(err) => {
error!("update_owner failed. {}", err); error!("update_owner failed. {}", err);
// TODO: also need to drop this CString once C++ is done reading it FFIResult::Err(FFIError::from(err))
let err_string = CString::new(err.to_string())
.expect("could not create CString")
.into_raw();
FFIResult::Err(err_string)
} }
} }
} }
@ -229,9 +223,17 @@ mod tests {
); );
assert_eq!(raw_owner.mod_version, 1); assert_eq!(raw_owner.mod_version, 1);
} }
FFIResult::Err(error) => panic!("create_owner returned error: {:?}", unsafe { FFIResult::Err(error) => panic!(
CStr::from_ptr(error).to_string_lossy() "create_owner returned error: {:?}",
}), match error {
FFIError::Server(server_error) =>
format!("{} {}", server_error.status, unsafe {
CStr::from_ptr(server_error.title).to_string_lossy()
}),
FFIError::Network(network_error) =>
unsafe { CStr::from_ptr(network_error).to_string_lossy() }.to_string(),
}
),
} }
} }
@ -252,12 +254,16 @@ mod tests {
FFIResult::Ok(raw_owner) => { FFIResult::Ok(raw_owner) => {
panic!("create_owner returned Ok result: {:#x?}", raw_owner) panic!("create_owner returned Ok result: {:#x?}", raw_owner)
} }
FFIResult::Err(error) => { FFIResult::Err(error) => match error {
assert_eq!( FFIError::Server(server_error) => {
unsafe { CStr::from_ptr(error).to_string_lossy() }, assert_eq!(server_error.status, 500);
"Server 500: Internal Server Error" assert_eq!(
); unsafe { CStr::from_ptr(server_error.title).to_string_lossy() },
} "Internal Server Error"
);
}
_ => panic!("create_owner did not return a server error"),
},
} }
} }
@ -291,9 +297,17 @@ mod tests {
); );
assert_eq!(raw_owner.mod_version, 1); assert_eq!(raw_owner.mod_version, 1);
} }
FFIResult::Err(error) => panic!("update_owner returned error: {:?}", unsafe { FFIResult::Err(error) => panic!(
CStr::from_ptr(error).to_string_lossy() "update_owner returned error: {:?}",
}), match error {
FFIError::Server(server_error) =>
format!("{} {}", server_error.status, unsafe {
CStr::from_ptr(server_error.title).to_string_lossy()
}),
FFIError::Network(network_error) =>
unsafe { CStr::from_ptr(network_error).to_string_lossy() }.to_string(),
}
),
} }
} }
@ -314,12 +328,16 @@ mod tests {
FFIResult::Ok(raw_owner) => { FFIResult::Ok(raw_owner) => {
panic!("update_owner returned Ok result: {:#x?}", raw_owner) panic!("update_owner returned Ok result: {:#x?}", raw_owner)
} }
FFIResult::Err(error) => { FFIResult::Err(error) => match error {
assert_eq!( FFIError::Server(server_error) => {
unsafe { CStr::from_ptr(error).to_string_lossy() }, assert_eq!(server_error.status, 500);
"Server 500: Internal Server Error" assert_eq!(
); unsafe { CStr::from_ptr(server_error.title).to_string_lossy() },
} "Internal Server Error"
);
}
_ => panic!("update_owner did not return a server error"),
},
} }
} }
} }

View File

@ -1,8 +1,62 @@
use anyhow::Error;
use std::convert::From;
use std::ffi::CString;
use std::os::raw::c_char; use std::os::raw::c_char;
use std::ptr::null;
use crate::error::ServerError;
#[derive(Debug, PartialEq)]
#[repr(C)]
pub struct FFIServerError {
pub status: u16,
pub title: *const c_char,
pub detail: *const c_char,
}
impl From<&ServerError> for FFIServerError {
fn from(server_error: &ServerError) -> Self {
FFIServerError {
status: server_error.status.as_u16(),
// TODO: may need to drop these CStrings once C++ is done reading them
title: CString::new(server_error.title.clone())
.expect("could not create CString")
.into_raw(),
detail: match &server_error.detail {
Some(detail) => CString::new(detail.clone())
.expect("could not create CString")
.into_raw(),
None => null(),
},
}
}
}
#[derive(Debug, PartialEq)]
#[repr(C, u8)]
pub enum FFIError {
Server(FFIServerError),
Network(*const c_char),
}
impl From<Error> for FFIError {
fn from(error: Error) -> Self {
if let Some(server_error) = error.downcast_ref::<ServerError>() {
FFIError::Server(FFIServerError::from(server_error))
} else {
// TODO: also need to drop this CString once C++ is done reading it
let err_string = CString::new(error.to_string())
.expect("could not create CString")
.into_raw();
FFIError::Network(err_string)
}
}
}
#[derive(Debug, PartialEq)] #[derive(Debug, PartialEq)]
#[repr(C, u8)] #[repr(C, u8)]
pub enum FFIResult<T> { pub enum FFIResult<T> {
Ok(T), Ok(T),
Err(*const c_char), Err(FFIError),
} }

View File

@ -11,9 +11,13 @@ use log::{error, info};
use std::{println as info, println as error}; use std::{println as info, println as error};
use crate::{ use crate::{
cache::file_cache_dir, cache::from_file_cache, cache::load_metadata_from_file_cache, cache::file_cache_dir,
cache::update_file_caches, error::extract_error_from_response, log_server_error, cache::from_file_cache,
result::FFIResult, cache::load_metadata_from_file_cache,
cache::update_file_caches,
error::extract_error_from_response,
log_server_error,
result::{FFIError, FFIResult},
}; };
#[derive(Serialize, Deserialize, Debug)] #[derive(Serialize, Deserialize, Debug)]
@ -155,11 +159,7 @@ pub extern "C" fn create_shop(
} }
Err(err) => { Err(err) => {
error!("create_shop failed. {}", err); error!("create_shop failed. {}", err);
// TODO: also need to drop this CString once C++ is done reading it FFIResult::Err(FFIError::from(err))
let err_string = CString::new(err.to_string())
.expect("could not create CString")
.into_raw();
FFIResult::Err(err_string)
} }
} }
} }
@ -272,11 +272,7 @@ pub extern "C" fn update_shop(
} }
Err(err) => { Err(err) => {
error!("update_shop failed. {}", err); error!("update_shop failed. {}", err);
// TODO: also need to drop this CString once C++ is done reading it FFIResult::Err(FFIError::from(err))
let err_string = CString::new(err.to_string())
.expect("could not create CString")
.into_raw();
FFIResult::Err(err_string)
} }
} }
} }
@ -346,12 +342,8 @@ pub extern "C" fn get_shop(
FFIResult::Ok(RawShop::from(shop)) FFIResult::Ok(RawShop::from(shop))
} }
Err(err) => { Err(err) => {
error!("get_shop_list failed. {}", err); error!("get_shop failed. {}", err);
let err_string = CString::new(err.to_string()) FFIResult::Err(FFIError::from(err))
.expect("could not create CString")
.into_raw();
// TODO: also need to drop this CString once C++ is done reading it
FFIResult::Err(err_string)
} }
} }
} }
@ -419,11 +411,7 @@ pub extern "C" fn list_shops(
} }
Err(err) => { Err(err) => {
error!("list_shops failed. {}", err); error!("list_shops failed. {}", err);
let err_string = CString::new(err.to_string()) FFIResult::Err(FFIError::from(err))
.expect("could not create CString")
.into_raw();
// TODO: also need to drop this CString once C++ is done reading it
FFIResult::Err(err_string)
} }
} }
} }
@ -490,9 +478,17 @@ mod tests {
); );
assert_eq!(raw_shop.vendor_keywords_exclude, true); assert_eq!(raw_shop.vendor_keywords_exclude, true);
} }
FFIResult::Err(error) => panic!("create_shop returned error: {:?}", unsafe { FFIResult::Err(error) => panic!(
CStr::from_ptr(error).to_string_lossy() "create_shop returned error: {:?}",
}), match error {
FFIError::Server(server_error) =>
format!("{} {}", server_error.status, unsafe {
CStr::from_ptr(server_error.title).to_string_lossy()
}),
FFIError::Network(network_error) =>
unsafe { CStr::from_ptr(network_error).to_string_lossy() }.to_string(),
}
),
} }
} }
@ -511,12 +507,16 @@ mod tests {
mock.assert(); mock.assert();
match result { match result {
FFIResult::Ok(raw_shop) => panic!("create_shop returned Ok result: {:#x?}", raw_shop), FFIResult::Ok(raw_shop) => panic!("create_shop returned Ok result: {:#x?}", raw_shop),
FFIResult::Err(error) => { FFIResult::Err(error) => match error {
assert_eq!( FFIError::Server(server_error) => {
unsafe { CStr::from_ptr(error).to_string_lossy() }, assert_eq!(server_error.status, 500);
"Server 500: Internal Server Error" assert_eq!(
); unsafe { CStr::from_ptr(server_error.title).to_string_lossy() },
} "Internal Server Error"
);
}
_ => panic!("create_shop did not return a server error"),
},
} }
} }
@ -589,9 +589,17 @@ mod tests {
); );
assert_eq!(raw_shop.vendor_keywords_exclude, true); assert_eq!(raw_shop.vendor_keywords_exclude, true);
} }
FFIResult::Err(error) => panic!("update_shop returned error: {:?}", unsafe { FFIResult::Err(error) => panic!(
CStr::from_ptr(error).to_string_lossy() "update_shop returned error: {:?}",
}), match error {
FFIError::Server(server_error) =>
format!("{} {}", server_error.status, unsafe {
CStr::from_ptr(server_error.title).to_string_lossy()
}),
FFIError::Network(network_error) =>
unsafe { CStr::from_ptr(network_error).to_string_lossy() }.to_string(),
}
),
} }
} }
@ -625,12 +633,16 @@ mod tests {
mock.assert(); mock.assert();
match result { match result {
FFIResult::Ok(raw_shop) => panic!("update_shop returned Ok result: {:#x?}", raw_shop), FFIResult::Ok(raw_shop) => panic!("update_shop returned Ok result: {:#x?}", raw_shop),
FFIResult::Err(error) => { FFIResult::Err(error) => match error {
assert_eq!( FFIError::Server(server_error) => {
unsafe { CStr::from_ptr(error).to_string_lossy() }, assert_eq!(server_error.status, 500);
"Server 500: Internal Server Error" assert_eq!(
); unsafe { CStr::from_ptr(server_error.title).to_string_lossy() },
} "Internal Server Error"
);
}
_ => panic!("update_shop did not return a server error"),
},
} }
} }
@ -686,9 +698,17 @@ mod tests {
); );
assert_eq!(raw_shop.vendor_keywords_exclude, true); assert_eq!(raw_shop.vendor_keywords_exclude, true);
} }
FFIResult::Err(error) => panic!("get_shop returned error: {:?}", unsafe { FFIResult::Err(error) => panic!(
CStr::from_ptr(error).to_string_lossy() "get_shop returned error: {:?}",
}), match error {
FFIError::Server(server_error) =>
format!("{} {}", server_error.status, unsafe {
CStr::from_ptr(server_error.title).to_string_lossy()
}),
FFIError::Network(network_error) =>
unsafe { CStr::from_ptr(network_error).to_string_lossy() }.to_string(),
}
),
} }
} }
@ -705,12 +725,15 @@ mod tests {
mock.assert(); mock.assert();
match result { match result {
FFIResult::Ok(raw_shop) => panic!("get_shop returned Ok result: {:#x?}", raw_shop), FFIResult::Ok(raw_shop) => panic!("get_shop returned Ok result: {:#x?}", raw_shop),
FFIResult::Err(error) => { FFIResult::Err(error) => match error {
assert_eq!( FFIError::Network(network_error) => {
unsafe { CStr::from_ptr(error).to_string_lossy() }, assert_eq!(
"io error: failed to fill whole buffer" // empty tempfile unsafe { CStr::from_ptr(network_error).to_string_lossy() },
); "Object not found in API or in cache: shop_1.bin",
} );
}
_ => panic!("get_shop did not return a network error"),
},
} }
} }
@ -771,9 +794,17 @@ mod tests {
); );
assert_eq!(raw_shop.vendor_keywords_exclude, true); assert_eq!(raw_shop.vendor_keywords_exclude, true);
} }
FFIResult::Err(error) => panic!("list_shops returned error: {:?}", unsafe { FFIResult::Err(error) => panic!(
CStr::from_ptr(error).to_string_lossy() "list_shops returned error: {:?}",
}), match error {
FFIError::Server(server_error) =>
format!("{} {}", server_error.status, unsafe {
CStr::from_ptr(server_error.title).to_string_lossy()
}),
FFIError::Network(network_error) =>
unsafe { CStr::from_ptr(network_error).to_string_lossy() }.to_string(),
}
),
} }
} }
@ -790,12 +821,15 @@ mod tests {
mock.assert(); mock.assert();
match result { match result {
FFIResult::Ok(raw_shop) => panic!("list_shops returned Ok result: {:#x?}", raw_shop), FFIResult::Ok(raw_shop) => panic!("list_shops returned Ok result: {:#x?}", raw_shop),
FFIResult::Err(error) => { FFIResult::Err(error) => match error {
assert_eq!( FFIError::Network(network_error) => {
unsafe { CStr::from_ptr(error).to_string_lossy() }, assert_eq!(
"io error: failed to fill whole buffer" // empty tempfile unsafe { CStr::from_ptr(network_error).to_string_lossy() },
); "Object not found in API or in cache: shops.bin",
} );
}
_ => panic!("list_shops did not return a network error"),
},
} }
} }
} }

View File

@ -11,8 +11,10 @@ use log::{error, info};
use std::{println as info, println as error}; use std::{println as info, println as error};
use crate::{ use crate::{
cache::file_cache_dir, cache::update_file_caches, error::extract_error_from_response, cache::file_cache_dir,
result::FFIResult, cache::update_file_caches,
error::extract_error_from_response,
result::{FFIError, FFIResult},
}; };
#[derive(Serialize, Deserialize, Debug)] #[derive(Serialize, Deserialize, Debug)]
@ -193,11 +195,7 @@ pub extern "C" fn create_transaction(
Ok(transaction) => FFIResult::Ok(RawTransaction::from(transaction)), Ok(transaction) => FFIResult::Ok(RawTransaction::from(transaction)),
Err(err) => { Err(err) => {
error!("create_transaction failed. {}", err); error!("create_transaction failed. {}", err);
// TODO: also need to drop this CString once C++ is done reading it FFIResult::Err(FFIError::from(err))
let err_string = CString::new(err.to_string())
.expect("could not create CString")
.into_raw();
FFIResult::Err(err_string)
} }
} }
} }
@ -294,9 +292,17 @@ mod tests {
vec!["VendorItemMisc".to_string()] vec!["VendorItemMisc".to_string()]
); );
} }
FFIResult::Err(error) => panic!("create_transaction returned error: {:?}", unsafe { FFIResult::Err(error) => panic!(
CStr::from_ptr(error).to_string_lossy() "create_transaction returned error: {:?}",
}), match error {
FFIError::Server(server_error) =>
format!("{} {}", server_error.status, unsafe {
CStr::from_ptr(server_error.title).to_string_lossy()
}),
FFIError::Network(network_error) =>
unsafe { CStr::from_ptr(network_error).to_string_lossy() }.to_string(),
}
),
} }
} }
@ -344,12 +350,16 @@ mod tests {
"create_transaction returned Ok result: {:#?}", "create_transaction returned Ok result: {:#?}",
raw_transaction raw_transaction
), ),
FFIResult::Err(error) => { FFIResult::Err(error) => match error {
assert_eq!( FFIError::Server(server_error) => {
unsafe { CStr::from_ptr(error).to_string_lossy() }, assert_eq!(server_error.status, 500);
"Server 500: Internal Server Error. Some error detail" assert_eq!(
); unsafe { CStr::from_ptr(server_error.title).to_string_lossy() },
} "Internal Server Error"
);
}
_ => panic!("create_transaction did not return a server error"),
},
} }
} }
} }