Add list_shops function, improve error messages
This commit is contained in:
parent
e0f0f26943
commit
36a6c38b41
@ -107,12 +107,19 @@ struct RawMerchandiseVec {
|
|||||||
uintptr_t cap;
|
uintptr_t cap;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
struct RawShopVec {
|
||||||
|
RawShop *ptr;
|
||||||
|
uintptr_t len;
|
||||||
|
uintptr_t cap;
|
||||||
|
};
|
||||||
|
|
||||||
/* bad hack added by thallada. See: https://github.com/eqrion/cbindgen/issues/402 */
|
/* bad hack added by thallada. See: https://github.com/eqrion/cbindgen/issues/402 */
|
||||||
struct _Helper_0 {
|
struct _Helper_0 {
|
||||||
FFIResult<bool> _bool_result;
|
FFIResult<bool> _bool_result;
|
||||||
FFIResult<int32_t> _int_result;
|
FFIResult<int32_t> _int_result;
|
||||||
FFIResult<RawOwner> _raw_owner_result;
|
FFIResult<RawOwner> _raw_owner_result;
|
||||||
FFIResult<RawShop> _raw_shop_result;
|
FFIResult<RawShop> _raw_shop_result;
|
||||||
|
FFIResult<RawShopVec> _raw_shop_vec_result;
|
||||||
FFIResult<RawInteriorRefVec> _raw_interior_ref_vec_result;
|
FFIResult<RawInteriorRefVec> _raw_interior_ref_vec_result;
|
||||||
FFIResult<RawMerchandiseVec> _raw_merchandise_vec_result;
|
FFIResult<RawMerchandiseVec> _raw_merchandise_vec_result;
|
||||||
};
|
};
|
||||||
@ -162,6 +169,8 @@ FFIResult<RawShop> get_shop(const char *api_url, const char *api_key, int32_t sh
|
|||||||
|
|
||||||
bool init();
|
bool init();
|
||||||
|
|
||||||
|
FFIResult<RawShopVec> list_shops(const char *api_url, const char *api_key);
|
||||||
|
|
||||||
FFIResult<bool> status_check(const char *api_url);
|
FFIResult<bool> status_check(const char *api_url);
|
||||||
|
|
||||||
FFIResult<RawOwner> update_owner(const char *api_url,
|
FFIResult<RawOwner> update_owner(const char *api_url,
|
||||||
|
@ -65,7 +65,7 @@ renaming_overrides_prefixing = false
|
|||||||
|
|
||||||
|
|
||||||
[export.body]
|
[export.body]
|
||||||
"RawMerchandiseVec" = """
|
"RawShopVec" = """
|
||||||
};
|
};
|
||||||
|
|
||||||
/* bad hack added by thallada. See: https://github.com/eqrion/cbindgen/issues/402 */
|
/* bad hack added by thallada. See: https://github.com/eqrion/cbindgen/issues/402 */
|
||||||
@ -74,6 +74,7 @@ struct _Helper_0 {
|
|||||||
FFIResult<int32_t> _int_result;
|
FFIResult<int32_t> _int_result;
|
||||||
FFIResult<RawOwner> _raw_owner_result;
|
FFIResult<RawOwner> _raw_owner_result;
|
||||||
FFIResult<RawShop> _raw_shop_result;
|
FFIResult<RawShop> _raw_shop_result;
|
||||||
|
FFIResult<RawShopVec> _raw_shop_vec_result;
|
||||||
FFIResult<RawInteriorRefVec> _raw_interior_ref_vec_result;
|
FFIResult<RawInteriorRefVec> _raw_interior_ref_vec_result;
|
||||||
FFIResult<RawMerchandiseVec> _raw_merchandise_vec_result;
|
FFIResult<RawMerchandiseVec> _raw_merchandise_vec_result;
|
||||||
};
|
};
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
/// Thin wrapper around HashMap that automatically assigns new entries with an incrementing key (like a database)
|
/// Thin wrapper around HashMap that automatically assigns new entries with an incrementing key (like a database)
|
||||||
use std::{fs::create_dir_all, fs::File, io::BufReader, io::Write, path::Path, path::PathBuf};
|
use std::{fs::create_dir_all, fs::File, io::BufReader, io::Write, path::Path, path::PathBuf};
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::{Context, Result};
|
||||||
use base64::{encode_config, URL_SAFE_NO_PAD};
|
use base64::{encode_config, URL_SAFE_NO_PAD};
|
||||||
use bytes::Bytes;
|
use bytes::Bytes;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
@ -37,7 +37,10 @@ pub fn update_file_cache(cache_path: &Path, bytes: &Bytes) -> Result<()> {
|
|||||||
|
|
||||||
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)?;
|
let file = File::open(cache_path).context(format!(
|
||||||
|
"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)
|
||||||
|
|
||||||
|
@ -200,7 +200,7 @@ pub extern "C" fn get_interior_ref_list(
|
|||||||
match client.get(url).header("Api-Key", api_key).send() {
|
match client.get(url).header("Api-Key", api_key).send() {
|
||||||
Ok(resp) => {
|
Ok(resp) => {
|
||||||
info!("get_interior_ref_list response from api: {:?}", &resp);
|
info!("get_interior_ref_list response from api: {:?}", &resp);
|
||||||
if !resp.status().is_server_error() {
|
if resp.status().is_success() {
|
||||||
let bytes = resp.bytes()?;
|
let bytes = resp.bytes()?;
|
||||||
update_file_cache(&cache_path, &bytes)?;
|
update_file_cache(&cache_path, &bytes)?;
|
||||||
let json = serde_json::from_slice(&bytes)?;
|
let json = serde_json::from_slice(&bytes)?;
|
||||||
|
@ -185,7 +185,7 @@ pub extern "C" fn get_merchandise_list(
|
|||||||
match client.get(url).header("Api-Key", api_key).send() {
|
match client.get(url).header("Api-Key", api_key).send() {
|
||||||
Ok(resp) => {
|
Ok(resp) => {
|
||||||
info!("get_merchandise_list response from api: {:?}", &resp);
|
info!("get_merchandise_list response from api: {:?}", &resp);
|
||||||
if !resp.status().is_server_error() {
|
if resp.status().is_success() {
|
||||||
let bytes = resp.bytes()?;
|
let bytes = resp.bytes()?;
|
||||||
update_file_cache(&cache_path, &bytes)?;
|
update_file_cache(&cache_path, &bytes)?;
|
||||||
let json = serde_json::from_slice(&bytes)?;
|
let json = serde_json::from_slice(&bytes)?;
|
||||||
|
211
src/shop.rs
211
src/shop.rs
@ -1,6 +1,6 @@
|
|||||||
use std::{ffi::CStr, ffi::CString, os::raw::c_char};
|
use std::{convert::TryFrom, ffi::CStr, ffi::CString, os::raw::c_char};
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::{anyhow, Result};
|
||||||
use reqwest::Url;
|
use reqwest::Url;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
@ -31,6 +31,20 @@ impl Shop {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<RawShop> for Shop {
|
||||||
|
fn from(raw_shop: RawShop) -> Self {
|
||||||
|
Self {
|
||||||
|
id: Some(raw_shop.id),
|
||||||
|
name: unsafe { CStr::from_ptr(raw_shop.name) }
|
||||||
|
.to_string_lossy()
|
||||||
|
.to_string(),
|
||||||
|
description: unsafe { CStr::from_ptr(raw_shop.description) }
|
||||||
|
.to_string_lossy()
|
||||||
|
.to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
#[repr(C)]
|
#[repr(C)]
|
||||||
pub struct RawShop {
|
pub struct RawShop {
|
||||||
@ -39,6 +53,32 @@ pub struct RawShop {
|
|||||||
pub description: *const c_char,
|
pub description: *const c_char,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl TryFrom<Shop> for RawShop {
|
||||||
|
type Error = anyhow::Error;
|
||||||
|
|
||||||
|
fn try_from(shop: Shop) -> Result<Self> {
|
||||||
|
if let Some(id) = shop.id {
|
||||||
|
Ok(Self {
|
||||||
|
id,
|
||||||
|
name: CString::new(shop.name).unwrap_or_default().into_raw(),
|
||||||
|
description: CString::new(shop.description)
|
||||||
|
.unwrap_or_default()
|
||||||
|
.into_raw(),
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
Err(anyhow!("shop.id is None"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
#[repr(C)]
|
||||||
|
pub struct RawShopVec {
|
||||||
|
pub ptr: *mut RawShop,
|
||||||
|
pub len: usize,
|
||||||
|
pub cap: usize,
|
||||||
|
}
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
pub extern "C" fn create_shop(
|
pub extern "C" fn create_shop(
|
||||||
api_url: *const c_char,
|
api_url: *const c_char,
|
||||||
@ -84,14 +124,8 @@ pub extern "C" fn create_shop(
|
|||||||
match inner(&api_url, &api_key, &name, &description) {
|
match inner(&api_url, &api_key, &name, &description) {
|
||||||
Ok(shop) => {
|
Ok(shop) => {
|
||||||
info!("create_shop successful");
|
info!("create_shop successful");
|
||||||
if let Some(id) = shop.id {
|
if let Ok(raw_shop) = RawShop::try_from(shop) {
|
||||||
FFIResult::Ok(RawShop {
|
FFIResult::Ok(raw_shop)
|
||||||
id,
|
|
||||||
name: CString::new(shop.name).unwrap_or_default().into_raw(),
|
|
||||||
description: CString::new(shop.description)
|
|
||||||
.unwrap_or_default()
|
|
||||||
.into_raw(),
|
|
||||||
})
|
|
||||||
} else {
|
} else {
|
||||||
error!("create_shop failed. API did not return a shop with an ID");
|
error!("create_shop failed. API did not return a shop with an ID");
|
||||||
let err_string = CString::new("API did not return a shop with an ID".to_string())
|
let err_string = CString::new("API did not return a shop with an ID".to_string())
|
||||||
@ -159,14 +193,8 @@ pub extern "C" fn update_shop(
|
|||||||
match inner(&api_url, &api_key, id, &name, &description) {
|
match inner(&api_url, &api_key, id, &name, &description) {
|
||||||
Ok(shop) => {
|
Ok(shop) => {
|
||||||
info!("update_shop successful");
|
info!("update_shop successful");
|
||||||
if let Some(id) = shop.id {
|
if let Ok(raw_shop) = RawShop::try_from(shop) {
|
||||||
FFIResult::Ok(RawShop {
|
FFIResult::Ok(raw_shop)
|
||||||
id,
|
|
||||||
name: CString::new(shop.name).unwrap_or_default().into_raw(),
|
|
||||||
description: CString::new(shop.description)
|
|
||||||
.unwrap_or_default()
|
|
||||||
.into_raw(),
|
|
||||||
})
|
|
||||||
} else {
|
} else {
|
||||||
error!("create_shop failed. API did not return a shop with an ID");
|
error!("create_shop failed. API did not return a shop with an ID");
|
||||||
let err_string = CString::new("API did not return a shop with an ID".to_string())
|
let err_string = CString::new("API did not return a shop with an ID".to_string())
|
||||||
@ -214,7 +242,7 @@ pub extern "C" fn get_shop(
|
|||||||
match client.get(url).header("Api-Key", api_key).send() {
|
match client.get(url).header("Api-Key", api_key).send() {
|
||||||
Ok(resp) => {
|
Ok(resp) => {
|
||||||
info!("get_shop response from api: {:?}", &resp);
|
info!("get_shop response from api: {:?}", &resp);
|
||||||
if !resp.status().is_server_error() {
|
if resp.status().is_success() {
|
||||||
let bytes = resp.bytes()?;
|
let bytes = resp.bytes()?;
|
||||||
update_file_cache(&cache_path, &bytes)?;
|
update_file_cache(&cache_path, &bytes)?;
|
||||||
let json = serde_json::from_slice(&bytes)?;
|
let json = serde_json::from_slice(&bytes)?;
|
||||||
@ -234,13 +262,16 @@ pub extern "C" fn get_shop(
|
|||||||
match inner(&api_url, &api_key, shop_id) {
|
match inner(&api_url, &api_key, shop_id) {
|
||||||
Ok(shop) => {
|
Ok(shop) => {
|
||||||
// TODO: need to pass this back into Rust once C++ is done with it so it can be manually dropped and the CStrings dropped from raw pointers.
|
// TODO: need to pass this back into Rust once C++ is done with it so it can be manually dropped and the CStrings dropped from raw pointers.
|
||||||
FFIResult::Ok(RawShop {
|
if let Ok(raw_shop) = RawShop::try_from(shop) {
|
||||||
id: shop_id,
|
FFIResult::Ok(raw_shop)
|
||||||
name: CString::new(shop.name).unwrap_or_default().into_raw(),
|
} else {
|
||||||
description: CString::new(shop.description)
|
error!("get_shop failed. API did not return a shop with an ID");
|
||||||
.unwrap_or_default()
|
let err_string = CString::new("API did not return a shop with an ID".to_string())
|
||||||
.into_raw(),
|
.expect("could not create CString")
|
||||||
})
|
.into_raw();
|
||||||
|
// TODO: also need to drop this CString once C++ is done reading it
|
||||||
|
FFIResult::Err(err_string)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
error!("get_shop_list failed. {}", err);
|
error!("get_shop_list failed. {}", err);
|
||||||
@ -253,9 +284,77 @@ pub extern "C" fn get_shop(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[no_mangle]
|
||||||
|
pub extern "C" fn list_shops(
|
||||||
|
api_url: *const c_char,
|
||||||
|
api_key: *const c_char,
|
||||||
|
) -> FFIResult<RawShopVec> {
|
||||||
|
let api_url = unsafe { CStr::from_ptr(api_url) }.to_string_lossy();
|
||||||
|
let api_key = unsafe { CStr::from_ptr(api_key) }.to_string_lossy();
|
||||||
|
info!("list_shops api_url: {:?}, api_key: {:?}", api_url, api_key);
|
||||||
|
|
||||||
|
fn inner(api_url: &str, api_key: &str) -> Result<Vec<Shop>> {
|
||||||
|
#[cfg(not(test))]
|
||||||
|
let url = Url::parse(api_url)?.join("v1/shops?limit=128")?;
|
||||||
|
#[cfg(test)]
|
||||||
|
let url = Url::parse(&mockito::server_url())?.join("v1/shops?limit=128")?;
|
||||||
|
info!("api_url: {:?}", url);
|
||||||
|
|
||||||
|
let client = reqwest::blocking::Client::new();
|
||||||
|
let cache_path = file_cache_dir(api_url)?.join("shops.json");
|
||||||
|
|
||||||
|
match client.get(url).header("Api-Key", api_key).send() {
|
||||||
|
Ok(resp) => {
|
||||||
|
info!("list_shops response from api: {:?}", &resp);
|
||||||
|
if resp.status().is_success() {
|
||||||
|
let bytes = resp.bytes()?;
|
||||||
|
update_file_cache(&cache_path, &bytes)?;
|
||||||
|
let json = serde_json::from_slice(&bytes)?;
|
||||||
|
Ok(json)
|
||||||
|
} else {
|
||||||
|
log_server_error(resp);
|
||||||
|
from_file_cache(&cache_path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
error!("list_shops api request error: {}", err);
|
||||||
|
from_file_cache(&cache_path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
match inner(&api_url, &api_key) {
|
||||||
|
Ok(shops) => {
|
||||||
|
// TODO: need to pass this back into Rust once C++ is done with it so it can be manually dropped and the CStrings dropped from raw pointers.
|
||||||
|
let raw_shops: Result<Vec<RawShop>> =
|
||||||
|
shops.into_iter().map(RawShop::try_from).collect();
|
||||||
|
if let Ok(raw_shops) = raw_shops {
|
||||||
|
let (ptr, len, cap) = raw_shops.into_raw_parts();
|
||||||
|
FFIResult::Ok(RawShopVec { ptr, len, cap })
|
||||||
|
} else {
|
||||||
|
error!("list_shops failed. API returned one or more shops with no ID");
|
||||||
|
let err_string =
|
||||||
|
CString::new("API returned one or more shops with no ID".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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
error!("list_shops failed. {}", 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use std::ffi::CString;
|
use std::{ffi::CString, slice};
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use mockito::mock;
|
use mockito::mock;
|
||||||
@ -423,4 +522,62 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_list_shops() {
|
||||||
|
let mock = mock("GET", "/v1/shops?limit=128")
|
||||||
|
.with_status(201)
|
||||||
|
.with_header("content-type", "application/json")
|
||||||
|
.with_body(r#"[{ "created_at": "2020-08-18T00:00:00.000", "id": 1, "name": "name", "description": "description", "updated_at": "2020-08-18T00:00:00.000" }]"#)
|
||||||
|
.create();
|
||||||
|
|
||||||
|
let api_url = CString::new("url").unwrap().into_raw();
|
||||||
|
let api_key = CString::new("api-key").unwrap().into_raw();
|
||||||
|
let result = list_shops(api_url, api_key);
|
||||||
|
mock.assert();
|
||||||
|
match result {
|
||||||
|
FFIResult::Ok(raw_shops_vec) => {
|
||||||
|
assert_eq!(raw_shops_vec.len, 1);
|
||||||
|
let raw_shops_slice = unsafe {
|
||||||
|
assert!(!raw_shops_vec.ptr.is_null());
|
||||||
|
slice::from_raw_parts(raw_shops_vec.ptr, raw_shops_vec.len)
|
||||||
|
};
|
||||||
|
let raw_shop = &raw_shops_slice[0];
|
||||||
|
assert_eq!(raw_shop.id, 1);
|
||||||
|
assert_eq!(
|
||||||
|
unsafe { CStr::from_ptr(raw_shop.name).to_string_lossy() },
|
||||||
|
"name"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
unsafe { CStr::from_ptr(raw_shop.description).to_string_lossy() },
|
||||||
|
"description"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
FFIResult::Err(error) => panic!("list_shops returned error: {:?}", unsafe {
|
||||||
|
CStr::from_ptr(error).to_string_lossy()
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_list_shops_server_error() {
|
||||||
|
let mock = mock("GET", "/v1/shops?limit=128")
|
||||||
|
.with_status(500)
|
||||||
|
.with_body("Internal Server Error")
|
||||||
|
.create();
|
||||||
|
|
||||||
|
let api_url = CString::new("url").unwrap().into_raw();
|
||||||
|
let api_key = CString::new("api-key").unwrap().into_raw();
|
||||||
|
let result = list_shops(api_url, api_key);
|
||||||
|
mock.assert();
|
||||||
|
match result {
|
||||||
|
FFIResult::Ok(raw_shop) => panic!("list_shops returned Ok result: {:#x?}", raw_shop),
|
||||||
|
FFIResult::Err(error) => {
|
||||||
|
assert_eq!(
|
||||||
|
unsafe { CStr::from_ptr(error).to_string_lossy() },
|
||||||
|
"EOF while parsing a value at line 1 column 0" // empty tempfile
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user