Switch all endpoints to send and receive bincode

Instead of json. This required modifying some of the structs and created "Saved" structs for each type for deserializing bincode from the API.

Metadata is still stored as JSON.
This commit is contained in:
Tyler Hallada 2020-11-14 02:22:50 -05:00
parent 52b1a64d7e
commit 9004b4378d
11 changed files with 704 additions and 580 deletions

28
Cargo.lock generated
View File

@ -107,11 +107,13 @@ version = "0.1.0"
dependencies = [
"anyhow",
"base64 0.13.0",
"bincode",
"bytes",
"cbindgen",
"chrono",
"dirs",
"http-api-problem",
"ipnetwork",
"log",
"mockito",
"reqwest",
@ -122,6 +124,16 @@ dependencies = [
"uuid",
]
[[package]]
name = "bincode"
version = "1.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f30d3a39baa26f9651f17b375061f3233dde33424a8b72b0dbe93a68a0bc896d"
dependencies = [
"byteorder",
"serde",
]
[[package]]
name = "bitflags"
version = "1.2.1"
@ -145,6 +157,12 @@ version = "3.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2e8c087f005730276d1096a652e92a8bacee2e2472bcc9715a74d2bec38b5820"
[[package]]
name = "byteorder"
version = "1.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08c48aae112d48ed9f069b33538ea9e3e90aa263cfa3d1c24309612b1f7472de"
[[package]]
name = "bytes"
version = "0.5.5"
@ -585,6 +603,15 @@ dependencies = [
"libc",
]
[[package]]
name = "ipnetwork"
version = "0.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "02c3eaab3ac0ede60ffa41add21970a7df7d91772c03383aac6c2c3d53cc716b"
dependencies = [
"serde",
]
[[package]]
name = "itoa"
version = "0.4.6"
@ -1406,6 +1433,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9fde2f6a4bea1d6e007c4ad38c6839fa71cbb63b6dbf5b595aa38dc9b1093c11"
dependencies = [
"rand",
"serde",
]
[[package]]

View File

@ -12,15 +12,17 @@ cbindgen = "0.14.4"
[dependencies]
anyhow = "1.0"
base64 = "0.13"
bincode = "1.3"
bytes = "0.5"
chrono = { version = "0.4", features = ["serde"] }
http-api-problem = "0.17"
ipnetwork = "0.17"
mockito = "0.26.0"
reqwest = { version = "0.10", features = ["blocking", "json", "gzip"] }
log = "0.4"
simple-logging = "2.0"
dirs = "3.0"
uuid = { version = "0.8", features = ["v4"] }
uuid = { version = "0.8", features = ["serde", "v4"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
tempfile = "3.1"

View File

@ -86,7 +86,7 @@ struct RawMerchandise {
struct RawOwner {
int32_t id;
const char *name;
uint32_t mod_version;
int32_t mod_version;
};
struct RawShop {
@ -96,17 +96,17 @@ struct RawShop {
};
struct RawTransaction {
uint32_t id;
uint32_t shop_id;
int32_t id;
int32_t shop_id;
const char *mod_name;
uint32_t local_form_id;
int32_t local_form_id;
const char *name;
uint32_t form_type;
int32_t form_type;
bool is_food;
uint32_t price;
int32_t price;
bool is_sell;
uint32_t quantity;
uint32_t amount;
int32_t quantity;
int32_t amount;
};
struct RawInteriorRefVec {
@ -161,7 +161,7 @@ FFIResult<int32_t> create_merchandise_list(const char *api_url,
FFIResult<RawOwner> create_owner(const char *api_url,
const char *api_key,
const char *name,
uint32_t mod_version);
int32_t mod_version);
FFIResult<RawShop> create_shop(const char *api_url,
const char *api_key,
@ -214,9 +214,9 @@ FFIResult<int32_t> update_merchandise_list(const char *api_url,
FFIResult<RawOwner> update_owner(const char *api_url,
const char *api_key,
uint32_t id,
int32_t id,
const char *name,
uint32_t mod_version);
int32_t mod_version);
FFIResult<RawShop> update_shop(const char *api_url,
const char *api_key,

View File

@ -92,7 +92,7 @@ pub fn from_file_cache<T: for<'de> Deserialize<'de>>(cache_path: &Path) -> Resul
let reader = BufReader::new(file);
info!("returning value from cache: {:?}", cache_path);
Ok(serde_json::from_reader(reader)?)
Ok(bincode::deserialize_from(reader)?)
}
pub fn load_metadata_from_file_cache(cache_path: &Path) -> Result<Metadata> {

36
src/error.rs Normal file
View File

@ -0,0 +1,36 @@
use std::str;
use anyhow::{anyhow, Error};
use bytes::Bytes;
use http_api_problem::HttpApiProblem;
use reqwest::StatusCode;
#[cfg(not(test))]
use log::error;
#[cfg(test)]
use std::println as error;
pub fn extract_error_from_response(status: StatusCode, bytes: &Bytes) -> Error {
match serde_json::from_slice::<HttpApiProblem>(bytes) {
Ok(api_problem) => {
let detail = api_problem.detail.unwrap_or("".to_string());
error!(
"Server {}: {}. {}",
status.as_u16(),
api_problem.title,
detail
);
anyhow!(format!(
"Server {}: {}. {}",
status.as_u16(),
api_problem.title,
detail
))
}
Err(_) => {
let detail = str::from_utf8(bytes).unwrap_or("unknown");
error!("Server {}: {}", status.as_u16(), detail);
anyhow!(format!("Server {}: {}", status.as_u16(), detail))
}
}
}

View File

@ -1,6 +1,7 @@
use std::{ffi::CStr, ffi::CString, os::raw::c_char, slice};
use anyhow::Result;
use chrono::NaiveDateTime;
use reqwest::{StatusCode, Url};
use serde::{Deserialize, Serialize};
@ -11,7 +12,8 @@ use std::{println as info, println as error};
use crate::{
cache::file_cache_dir, cache::from_file_cache, cache::load_metadata_from_file_cache,
cache::update_file_caches, log_server_error, result::FFIResult,
cache::update_file_caches, error::extract_error_from_response, log_server_error,
result::FFIResult,
};
#[derive(Serialize, Deserialize, Debug)]
@ -24,9 +26,9 @@ pub struct InteriorRefList {
#[derive(Serialize, Deserialize, Debug)]
pub struct InteriorRef {
pub base_mod_name: String,
pub base_local_form_id: i32,
pub base_local_form_id: u32,
pub ref_mod_name: Option<String>,
pub ref_local_form_id: i32,
pub ref_local_form_id: u32,
pub position_x: f32,
pub position_y: f32,
pub position_z: f32,
@ -47,7 +49,7 @@ impl InteriorRefList {
base_mod_name: unsafe { CStr::from_ptr(rec.base_mod_name) }
.to_string_lossy()
.to_string(),
base_local_form_id: rec.base_local_form_id as i32,
base_local_form_id: rec.base_local_form_id,
ref_mod_name: match rec.ref_mod_name.is_null() {
true => None,
false => Some(
@ -56,7 +58,7 @@ impl InteriorRefList {
.to_string(),
),
},
ref_local_form_id: rec.ref_local_form_id as i32,
ref_local_form_id: rec.ref_local_form_id,
position_x: rec.position_x,
position_y: rec.position_y,
position_z: rec.position_z,
@ -70,6 +72,16 @@ impl InteriorRefList {
}
}
#[derive(Serialize, Deserialize, Debug)]
pub struct SavedInteriorRefList {
pub id: i32,
pub shop_id: i32,
pub owner_id: i32,
pub ref_list: Vec<InteriorRef>,
pub created_at: NaiveDateTime,
pub updated_at: NaiveDateTime,
}
#[derive(Debug)]
#[repr(C)]
pub struct RawInteriorRef {
@ -86,6 +98,29 @@ pub struct RawInteriorRef {
pub scale: u16,
}
impl From<InteriorRef> for RawInteriorRef {
fn from(interior_ref: InteriorRef) -> Self {
Self {
base_mod_name: CString::new(interior_ref.base_mod_name)
.unwrap_or_default()
.into_raw(),
base_local_form_id: interior_ref.base_local_form_id,
ref_mod_name: match interior_ref.ref_mod_name {
None => std::ptr::null(),
Some(ref_mod_name) => CString::new(ref_mod_name).unwrap_or_default().into_raw(),
},
ref_local_form_id: interior_ref.ref_local_form_id,
position_x: interior_ref.position_x,
position_y: interior_ref.position_y,
position_z: interior_ref.position_z,
angle_x: interior_ref.angle_x,
angle_y: interior_ref.angle_y,
angle_z: interior_ref.angle_z,
scale: interior_ref.scale,
}
}
}
#[derive(Debug)]
#[repr(C)]
pub struct RawInteriorRefVec {
@ -116,7 +151,7 @@ pub extern "C" fn create_interior_ref_list(
api_key: &str,
shop_id: i32,
raw_interior_ref_slice: &[RawInteriorRef],
) -> Result<InteriorRefList> {
) -> Result<SavedInteriorRefList> {
#[cfg(not(test))]
let url = Url::parse(api_url)?.join("v1/interior_ref_lists")?;
#[cfg(test)]
@ -131,37 +166,34 @@ pub extern "C" fn create_interior_ref_list(
let resp = client
.post(url)
.header("Api-Key", api_key)
.json(&interior_ref_list)
.header("Content-Type", "application/octet-stream")
.body(bincode::serialize(&interior_ref_list)?)
.send()?;
info!("create interior_ref_list response from api: {:?}", &resp);
let cache_dir = file_cache_dir(api_url)?;
let headers = resp.headers().clone();
let status = resp.status();
let bytes = resp.bytes()?;
let json: InteriorRefList = serde_json::from_slice(&bytes)?;
if let Some(id) = json.id {
let body_cache_path = cache_dir.join(format!("interior_ref_list_{}.json", id));
let metadata_cache_path =
cache_dir.join(format!("interior_ref_list_{}_metadata.json", id));
if status.is_success() {
let saved_interior_ref_list: SavedInteriorRefList = bincode::deserialize(&bytes)?;
let body_cache_path = cache_dir.join(format!(
"interior_ref_list_{}.bin",
saved_interior_ref_list.id
));
let metadata_cache_path = cache_dir.join(format!(
"interior_ref_list_{}_metadata.json",
saved_interior_ref_list.id
));
update_file_caches(body_cache_path, metadata_cache_path, bytes, headers);
Ok(saved_interior_ref_list)
} else {
Err(extract_error_from_response(status, &bytes))
}
Ok(json)
}
match inner(&api_url, &api_key, shop_id, raw_interior_ref_slice) {
Ok(interior_ref_list) => {
if let Some(id) = interior_ref_list.id {
FFIResult::Ok(id)
} else {
error!("create_interior_ref_list failed. API did not return an interior ref list with an ID");
let err_string =
CString::new("API did not return an interior ref list with an 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)
}
}
Ok(interior_ref_list) => FFIResult::Ok(interior_ref_list.id),
Err(err) => {
error!("create_interior_ref_list failed. {}", err);
// TODO: also need to drop this CString once C++ is done reading it
@ -194,7 +226,7 @@ pub extern "C" fn update_interior_ref_list(
api_key: &str,
shop_id: i32,
raw_interior_ref_slice: &[RawInteriorRef],
) -> Result<InteriorRefList> {
) -> Result<SavedInteriorRefList> {
#[cfg(not(test))]
let url = Url::parse(api_url)?.join(&format!("v1/shops/{}/interior_ref_list", shop_id))?;
#[cfg(test)]
@ -210,35 +242,29 @@ pub extern "C" fn update_interior_ref_list(
let resp = client
.patch(url)
.header("Api-Key", api_key)
.json(&interior_ref_list)
.header("Content-Type", "application/octet-stream")
.body(bincode::serialize(&interior_ref_list)?)
.send()?;
info!("update interior_ref_list response from api: {:?}", &resp);
let cache_dir = file_cache_dir(api_url)?;
let body_cache_path = cache_dir.join(format!("shop_{}_interior_ref_list.json", shop_id));
let body_cache_path = cache_dir.join(format!("shop_{}_interior_ref_list.bin", shop_id));
let metadata_cache_path =
cache_dir.join(format!("shop_{}_interior_ref_list_metadata.json", shop_id));
let headers = resp.headers().clone();
let status = resp.status();
let bytes = resp.bytes()?;
let json: InteriorRefList = serde_json::from_slice(&bytes)?;
update_file_caches(body_cache_path, metadata_cache_path, bytes, headers);
Ok(json)
if status.is_success() {
let saved_interior_ref_list: SavedInteriorRefList = bincode::deserialize(&bytes)?;
update_file_caches(body_cache_path, metadata_cache_path, bytes, headers);
Ok(saved_interior_ref_list)
} else {
Err(extract_error_from_response(status, &bytes))
}
}
match inner(&api_url, &api_key, shop_id, raw_interior_ref_slice) {
Ok(interior_ref_list) => {
if let Some(id) = interior_ref_list.id {
FFIResult::Ok(id)
} else {
error!("update_interior_ref_list failed. API did not return an interior ref list with an ID");
let err_string =
CString::new("API did not return an interior ref list with an 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)
}
}
Ok(interior_ref_list) => FFIResult::Ok(interior_ref_list.id),
Err(err) => {
error!("update_interior_ref_list failed. {}", err);
// TODO: also need to drop this CString once C++ is done reading it
@ -264,7 +290,11 @@ pub extern "C" fn get_interior_ref_list(
api_url, api_key, interior_ref_list_id
);
fn inner(api_url: &str, api_key: &str, interior_ref_list_id: i32) -> Result<InteriorRefList> {
fn inner(
api_url: &str,
api_key: &str,
interior_ref_list_id: i32,
) -> Result<SavedInteriorRefList> {
#[cfg(not(test))]
let url = Url::parse(api_url)?
.join(&format!("v1/interior_ref_lists/{}", interior_ref_list_id))?;
@ -276,12 +306,15 @@ pub extern "C" fn get_interior_ref_list(
let client = reqwest::blocking::Client::new();
let cache_dir = file_cache_dir(api_url)?;
let body_cache_path =
cache_dir.join(format!("interior_ref_list_{}.json", interior_ref_list_id));
cache_dir.join(format!("interior_ref_list_{}.bin", interior_ref_list_id));
let metadata_cache_path = cache_dir.join(format!(
"interior_ref_list_{}_metadata.json",
interior_ref_list_id
));
let mut request = client.get(url).header("Api-Key", api_key);
let mut request = client
.get(url)
.header("Api-Key", api_key)
.header("Accept", "application/octet-stream");
// TODO: load metadata from in-memory LRU cache first before trying to load from file
if let Ok(metadata) = load_metadata_from_file_cache(&metadata_cache_path) {
if let Some(etag) = metadata.etag {
@ -295,9 +328,9 @@ pub extern "C" fn get_interior_ref_list(
if resp.status().is_success() {
let headers = resp.headers().clone();
let bytes = resp.bytes()?;
let json = serde_json::from_slice(&bytes)?;
let saved_interior_ref_list = bincode::deserialize(&bytes)?;
update_file_caches(body_cache_path, metadata_cache_path, bytes, headers);
Ok(json)
Ok(saved_interior_ref_list)
} else if resp.status() == StatusCode::NOT_MODIFIED {
from_file_cache(&body_cache_path)
} else {
@ -317,26 +350,7 @@ pub extern "C" fn get_interior_ref_list(
let (ptr, len, cap) = interior_ref_list
.ref_list
.into_iter()
.map(|interior_ref| RawInteriorRef {
base_mod_name: CString::new(interior_ref.base_mod_name)
.unwrap_or_default()
.into_raw(),
base_local_form_id: interior_ref.base_local_form_id as u32,
ref_mod_name: match interior_ref.ref_mod_name {
None => std::ptr::null(),
Some(ref_mod_name) => {
CString::new(ref_mod_name).unwrap_or_default().into_raw()
}
},
ref_local_form_id: interior_ref.ref_local_form_id as u32,
position_x: interior_ref.position_x,
position_y: interior_ref.position_y,
position_z: interior_ref.position_z,
angle_x: interior_ref.angle_x,
angle_y: interior_ref.angle_y,
angle_z: interior_ref.angle_z,
scale: interior_ref.scale,
})
.map(RawInteriorRef::from)
.collect::<Vec<RawInteriorRef>>()
.into_raw_parts();
// 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.
@ -367,7 +381,7 @@ pub extern "C" fn get_interior_ref_list_by_shop_id(
api_url, api_key, shop_id
);
fn inner(api_url: &str, api_key: &str, shop_id: i32) -> Result<InteriorRefList> {
fn inner(api_url: &str, api_key: &str, shop_id: i32) -> Result<SavedInteriorRefList> {
#[cfg(not(test))]
let url = Url::parse(api_url)?.join(&format!("v1/shops/{}/interior_ref_list", shop_id))?;
#[cfg(test)]
@ -377,10 +391,13 @@ pub extern "C" fn get_interior_ref_list_by_shop_id(
let client = reqwest::blocking::Client::new();
let cache_dir = file_cache_dir(api_url)?;
let body_cache_path = cache_dir.join(format!("shop_{}_interior_ref_list.json", shop_id));
let body_cache_path = cache_dir.join(format!("shop_{}_interior_ref_list.bin", shop_id));
let metadata_cache_path =
cache_dir.join(format!("shop_{}_interior_ref_list_metadata.json", shop_id));
let mut request = client.get(url).header("Api-Key", api_key);
let mut request = client
.get(url)
.header("Api-Key", api_key)
.header("Accept", "application/octet-stream");
// TODO: load metadata from in-memory LRU cache first before trying to load from file
if let Ok(metadata) = load_metadata_from_file_cache(&metadata_cache_path) {
if let Some(etag) = metadata.etag {
@ -397,9 +414,9 @@ pub extern "C" fn get_interior_ref_list_by_shop_id(
if resp.status().is_success() {
let headers = resp.headers().clone();
let bytes = resp.bytes()?;
let json = serde_json::from_slice(&bytes)?;
let saved_interior_ref_list = bincode::deserialize(&bytes)?;
update_file_caches(body_cache_path, metadata_cache_path, bytes, headers);
Ok(json)
Ok(saved_interior_ref_list)
} else if resp.status() == StatusCode::NOT_MODIFIED {
from_file_cache(&body_cache_path)
} else {
@ -422,26 +439,7 @@ pub extern "C" fn get_interior_ref_list_by_shop_id(
let (ptr, len, cap) = interior_ref_list
.ref_list
.into_iter()
.map(|interior_ref| RawInteriorRef {
base_mod_name: CString::new(interior_ref.base_mod_name)
.unwrap_or_default()
.into_raw(),
base_local_form_id: interior_ref.base_local_form_id as u32,
ref_mod_name: match interior_ref.ref_mod_name {
None => std::ptr::null(),
Some(ref_mod_name) => {
CString::new(ref_mod_name).unwrap_or_default().into_raw()
}
},
ref_local_form_id: interior_ref.ref_local_form_id as u32,
position_x: interior_ref.position_x,
position_y: interior_ref.position_y,
position_z: interior_ref.position_z,
angle_x: interior_ref.angle_x,
angle_y: interior_ref.angle_y,
angle_z: interior_ref.angle_z,
scale: interior_ref.scale,
})
.map(RawInteriorRef::from)
.collect::<Vec<RawInteriorRef>>()
.into_raw_parts();
// 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.
@ -464,14 +462,35 @@ mod tests {
use std::ffi::CString;
use super::*;
use chrono::Utc;
use mockito::mock;
#[test]
fn test_create_interior_ref_list() {
let example = SavedInteriorRefList {
id: 1,
owner_id: 1,
shop_id: 1,
ref_list: vec![InteriorRef {
base_mod_name: "Skyrim.esm".to_string(),
base_local_form_id: 1,
ref_mod_name: Some("BazaarRealm.esp".to_string()),
ref_local_form_id: 1,
position_x: 100.,
position_y: 0.,
position_z: 100.,
angle_x: 0.,
angle_y: 0.,
angle_z: 0.,
scale: 1,
}],
created_at: Utc::now().naive_utc(),
updated_at: Utc::now().naive_utc(),
};
let mock = mock("POST", "/v1/interior_ref_lists")
.with_status(201)
.with_header("content-type", "application/json")
.with_body(r#"{ "created_at": "2020-08-18T00:00:00.000", "id": 1, "shop_id": 1, "ref_list": [], "updated_at": "2020-08-18T00:00:00.000" }"#)
.with_header("content-type", "application/octet-stream")
.with_body(bincode::serialize(&example).unwrap())
.create();
let api_url = CString::new("url").unwrap().into_raw();
@ -537,7 +556,7 @@ mod tests {
FFIResult::Err(error) => {
assert_eq!(
unsafe { CStr::from_ptr(error).to_string_lossy() },
"expected value at line 1 column 1"
"Server 500: Internal Server Error"
);
}
}
@ -545,10 +564,30 @@ mod tests {
#[test]
fn test_update_interior_ref_list() {
let example = SavedInteriorRefList {
id: 1,
owner_id: 1,
shop_id: 1,
ref_list: vec![InteriorRef {
base_mod_name: "Skyrim.esm".to_string(),
base_local_form_id: 1,
ref_mod_name: Some("BazaarRealm.esp".to_string()),
ref_local_form_id: 1,
position_x: 100.,
position_y: 0.,
position_z: 100.,
angle_x: 0.,
angle_y: 0.,
angle_z: 0.,
scale: 1,
}],
created_at: Utc::now().naive_utc(),
updated_at: Utc::now().naive_utc(),
};
let mock = mock("PATCH", "/v1/shops/1/interior_ref_list")
.with_status(201)
.with_header("content-type", "application/json")
.with_body(r#"{ "created_at": "2020-08-18T00:00:00.000", "id": 1, "shop_id": 1, "ref_list": [], "updated_at": "2020-08-18T00:00:00.000" }"#)
.with_header("content-type", "application/octet-stream")
.with_body(bincode::serialize(&example).unwrap())
.create();
let api_url = CString::new("url").unwrap().into_raw();
@ -614,7 +653,7 @@ mod tests {
FFIResult::Err(error) => {
assert_eq!(
unsafe { CStr::from_ptr(error).to_string_lossy() },
"expected value at line 1 column 1"
"Server 500: Internal Server Error"
);
}
}
@ -622,32 +661,30 @@ mod tests {
#[test]
fn test_get_interior_ref_list() {
let example = SavedInteriorRefList {
id: 1,
owner_id: 1,
shop_id: 1,
ref_list: vec![InteriorRef {
base_mod_name: "Skyrim.esm".to_string(),
base_local_form_id: 1,
ref_mod_name: Some("BazaarRealm.esp".to_string()),
ref_local_form_id: 1,
position_x: 100.,
position_y: 0.,
position_z: 100.,
angle_x: 0.,
angle_y: 0.,
angle_z: 0.,
scale: 1,
}],
created_at: Utc::now().naive_utc(),
updated_at: Utc::now().naive_utc(),
};
let mock = mock("GET", "/v1/interior_ref_lists/1")
.with_status(201)
.with_header("content-type", "application/json")
.with_body(
r#"{
"created_at": "2020-08-18T00:00:00.000",
"id": 1,
"shop_id": 1,
"ref_list": [
{
"base_mod_name": "Skyrim.esm",
"base_local_form_id": 1,
"ref_mod_name": "BazaarRealm.esp",
"ref_local_form_id": 1,
"position_x": 100.0,
"position_y": 0.0,
"position_z": 100.0,
"angle_x": 0.0,
"angle_y": 0.0,
"angle_z": 0.0,
"scale": 1
}
],
"updated_at": "2020-08-18T00:00:00.000"
}"#,
)
.with_header("content-type", "application/octet-stream")
.with_body(bincode::serialize(&example).unwrap())
.create();
let api_url = CString::new("url").unwrap().into_raw();
@ -709,7 +746,7 @@ mod tests {
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
"io error: failed to fill whole buffer" // empty tempfile
);
}
}
@ -717,32 +754,30 @@ mod tests {
#[test]
fn test_get_interior_ref_list_by_shop_id() {
let example = SavedInteriorRefList {
id: 1,
owner_id: 1,
shop_id: 1,
ref_list: vec![InteriorRef {
base_mod_name: "Skyrim.esm".to_string(),
base_local_form_id: 1,
ref_mod_name: Some("BazaarRealm.esp".to_string()),
ref_local_form_id: 1,
position_x: 100.,
position_y: 0.,
position_z: 100.,
angle_x: 0.,
angle_y: 0.,
angle_z: 0.,
scale: 1,
}],
created_at: Utc::now().naive_utc(),
updated_at: Utc::now().naive_utc(),
};
let mock = mock("GET", "/v1/shops/1/interior_ref_list")
.with_status(201)
.with_header("content-type", "application/json")
.with_body(
r#"{
"created_at": "2020-08-18T00:00:00.000",
"id": 1,
"shop_id": 1,
"ref_list": [
{
"base_mod_name": "Skyrim.esm",
"base_local_form_id": 1,
"ref_mod_name": "BazaarRealm.esp",
"ref_local_form_id": 1,
"position_x": 100.0,
"position_y": 0.0,
"position_z": 100.0,
"angle_x": 0.0,
"angle_y": 0.0,
"angle_z": 0.0,
"scale": 1
}
],
"updated_at": "2020-08-18T00:00:00.000"
}"#,
)
.with_header("content-type", "application/octet-stream")
.with_body(bincode::serialize(&example).unwrap())
.create();
let api_url = CString::new("url").unwrap().into_raw();
@ -805,7 +840,7 @@ mod tests {
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
"io error: failed to fill whole buffer" // empty tempfile
);
}
}

View File

@ -14,6 +14,7 @@ use std::println as error;
mod cache;
mod client;
mod error;
mod interior_ref_list;
mod merchandise_list;
mod owner;

View File

@ -1,6 +1,7 @@
use std::{ffi::CStr, ffi::CString, os::raw::c_char, slice};
use anyhow::Result;
use chrono::NaiveDateTime;
use reqwest::{StatusCode, Url};
use serde::{Deserialize, Serialize};
@ -11,13 +12,14 @@ use std::{println as info, println as error};
use crate::{
cache::file_cache_dir, cache::from_file_cache, cache::load_metadata_from_file_cache,
cache::update_file_caches, log_server_error, result::FFIResult,
cache::update_file_caches, error::extract_error_from_response, log_server_error,
result::FFIResult,
};
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct MerchandiseList {
pub id: Option<i32>,
pub shop_id: i32,
pub owner_id: Option<i32>,
pub form_list: Vec<Merchandise>,
}
@ -35,8 +37,8 @@ pub struct Merchandise {
impl MerchandiseList {
pub fn from_game(shop_id: i32, merch_records: &[RawMerchandise]) -> Self {
Self {
id: None,
shop_id,
owner_id: None,
form_list: merch_records
.iter()
.map(|rec| Merchandise {
@ -57,6 +59,16 @@ impl MerchandiseList {
}
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct SavedMerchandiseList {
pub id: i32,
pub shop_id: i32,
pub owner_id: i32,
pub form_list: Vec<Merchandise>,
pub created_at: NaiveDateTime,
pub updated_at: NaiveDateTime,
}
#[derive(Debug)]
#[repr(C)]
pub struct RawMerchandise {
@ -99,7 +111,7 @@ pub extern "C" fn create_merchandise_list(
api_key: &str,
shop_id: i32,
raw_merchandise_slice: &[RawMerchandise],
) -> Result<MerchandiseList> {
) -> Result<SavedMerchandiseList> {
#[cfg(not(test))]
let url = Url::parse(api_url)?.join("v1/merchandise_lists")?;
#[cfg(test)]
@ -114,39 +126,34 @@ pub extern "C" fn create_merchandise_list(
let resp = client
.post(url)
.header("Api-Key", api_key)
.json(&merchandise_list)
.header("Content-Type", "application/octet-stream")
.body(bincode::serialize(&merchandise_list)?)
.send()?;
info!("create merchandise_list response from api: {:?}", &resp);
let cache_dir = file_cache_dir(api_url)?;
let headers = resp.headers().clone();
let status = resp.status();
let bytes = resp.bytes()?;
let json: MerchandiseList = serde_json::from_slice(&bytes)?;
if let Some(id) = json.id {
let body_cache_path = cache_dir.join(format!("merchandise_list_{}.json", id));
let metadata_cache_path =
cache_dir.join(format!("merchandise_list_{}_metadata.json", id));
if status.is_success() {
let saved_merchandise_list: SavedMerchandiseList = bincode::deserialize(&bytes)?;
let body_cache_path = cache_dir.join(format!(
"merchandise_list_{}.bin",
saved_merchandise_list.id
));
let metadata_cache_path = cache_dir.join(format!(
"merchandise_list_{}_metadata.json",
saved_merchandise_list.id
));
update_file_caches(body_cache_path, metadata_cache_path, bytes, headers);
Ok(saved_merchandise_list)
} else {
Err(extract_error_from_response(status, &bytes))
}
Ok(json)
}
match inner(&api_url, &api_key, shop_id, raw_merchandise_slice) {
Ok(merchandise_list) => {
if let Some(id) = merchandise_list.id {
FFIResult::Ok(id)
} else {
error!(
"create_merchandise failed. API did not return an interior ref list with an ID"
);
let err_string =
CString::new("API did not return an interior ref list with an 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)
}
}
Ok(merchandise_list) => FFIResult::Ok(merchandise_list.id),
Err(err) => {
error!("create_merchandise_list failed. {}", err);
// TODO: also need to drop this CString once C++ is done reading it
@ -179,7 +186,7 @@ pub extern "C" fn update_merchandise_list(
api_key: &str,
shop_id: i32,
raw_merchandise_slice: &[RawMerchandise],
) -> Result<MerchandiseList> {
) -> Result<SavedMerchandiseList> {
#[cfg(not(test))]
let url = Url::parse(api_url)?.join(&format!("v1/shops/{}/merchandise_list", shop_id))?;
#[cfg(test)]
@ -195,37 +202,29 @@ pub extern "C" fn update_merchandise_list(
let resp = client
.patch(url)
.header("Api-Key", api_key)
.json(&merchandise_list)
.header("Content-Type", "application/octet-stream")
.body(bincode::serialize(&merchandise_list)?)
.send()?;
info!("update merchandise_list response from api: {:?}", &resp);
let cache_dir = file_cache_dir(api_url)?;
let body_cache_path = cache_dir.join(format!("shop_{}_merchandise_list.json", shop_id));
let body_cache_path = cache_dir.join(format!("shop_{}_merchandise_list.bin", shop_id));
let metadata_cache_path =
cache_dir.join(format!("shop_{}_merchandise_list_metadata.json", shop_id));
let headers = resp.headers().clone();
let status = resp.status();
let bytes = resp.bytes()?;
let json: MerchandiseList = serde_json::from_slice(&bytes)?;
update_file_caches(body_cache_path, metadata_cache_path, bytes, headers);
Ok(json)
if status.is_success() {
let saved_merchandise_list = bincode::deserialize(&bytes)?;
update_file_caches(body_cache_path, metadata_cache_path, bytes, headers);
Ok(saved_merchandise_list)
} else {
Err(extract_error_from_response(status, &bytes))
}
}
match inner(&api_url, &api_key, shop_id, raw_merchandise_slice) {
Ok(merchandise_list) => {
if let Some(id) = merchandise_list.id {
FFIResult::Ok(id)
} else {
error!(
"update_merchandise failed. API did not return a merchandise list with an ID"
);
let err_string =
CString::new("API did not return a merchandise list with an 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)
}
}
Ok(merchandise_list) => FFIResult::Ok(merchandise_list.id),
Err(err) => {
error!("update_merchandise_list failed. {}", err);
// TODO: also need to drop this CString once C++ is done reading it
@ -252,7 +251,11 @@ pub extern "C" fn get_merchandise_list(
api_url, api_key, merchandise_list_id
);
fn inner(api_url: &str, api_key: &str, merchandise_list_id: i32) -> Result<MerchandiseList> {
fn inner(
api_url: &str,
api_key: &str,
merchandise_list_id: i32,
) -> Result<SavedMerchandiseList> {
#[cfg(not(test))]
let url =
Url::parse(api_url)?.join(&format!("v1/merchandise_lists/{}", merchandise_list_id))?;
@ -264,12 +267,15 @@ pub extern "C" fn get_merchandise_list(
let client = reqwest::blocking::Client::new();
let cache_dir = file_cache_dir(api_url)?;
let body_cache_path =
cache_dir.join(format!("merchandise_list_{}.json", merchandise_list_id));
cache_dir.join(format!("merchandise_list_{}.bin", merchandise_list_id));
let metadata_cache_path = cache_dir.join(format!(
"merchandise_list_{}_metadata.json",
merchandise_list_id
));
let mut request = client.get(url).header("Api-Key", api_key);
let mut request = client
.get(url)
.header("Api-Key", api_key)
.header("Accept", "application/octet-stream");
// TODO: load metadata from in-memory LRU cache first before trying to load from file
if let Ok(metadata) = load_metadata_from_file_cache(&metadata_cache_path) {
if let Some(etag) = metadata.etag {
@ -283,9 +289,9 @@ pub extern "C" fn get_merchandise_list(
if resp.status().is_success() {
let headers = resp.headers().clone();
let bytes = resp.bytes()?;
let json = serde_json::from_slice(&bytes)?;
let saved_merchandise_list = bincode::deserialize(&bytes)?;
update_file_caches(body_cache_path, metadata_cache_path, bytes, headers);
Ok(json)
Ok(saved_merchandise_list)
} else if resp.status() == StatusCode::NOT_MODIFIED {
from_file_cache(&body_cache_path)
} else {
@ -348,7 +354,7 @@ pub extern "C" fn get_merchandise_list_by_shop_id(
api_url, api_key, shop_id
);
fn inner(api_url: &str, api_key: &str, shop_id: i32) -> Result<MerchandiseList> {
fn inner(api_url: &str, api_key: &str, shop_id: i32) -> Result<SavedMerchandiseList> {
#[cfg(not(test))]
let url = Url::parse(api_url)?.join(&format!("v1/shops/{}/merchandise_list", shop_id))?;
#[cfg(test)]
@ -358,10 +364,13 @@ pub extern "C" fn get_merchandise_list_by_shop_id(
let client = reqwest::blocking::Client::new();
let cache_dir = file_cache_dir(api_url)?;
let body_cache_path = cache_dir.join(format!("shop_{}_merchandise_list.json", shop_id));
let body_cache_path = cache_dir.join(format!("shop_{}_merchandise_list.bin", shop_id));
let metadata_cache_path =
cache_dir.join(format!("shop_{}_merchandise_list_metadata.json", shop_id));
let mut request = client.get(url).header("Api-Key", api_key);
let mut request = client
.get(url)
.header("Api-Key", api_key)
.header("Accept", "application/octet-stream");
// TODO: load metadata from in-memory LRU cache first before trying to load from file
if let Ok(metadata) = load_metadata_from_file_cache(&metadata_cache_path) {
if let Some(etag) = metadata.etag {
@ -378,9 +387,9 @@ pub extern "C" fn get_merchandise_list_by_shop_id(
if resp.status().is_success() {
let headers = resp.headers().clone();
let bytes = resp.bytes()?;
let json = serde_json::from_slice(&bytes)?;
let saved_merchandise_list = bincode::deserialize(&bytes)?;
update_file_caches(body_cache_path, metadata_cache_path, bytes, headers);
Ok(json)
Ok(saved_merchandise_list)
} else if resp.status() == StatusCode::NOT_MODIFIED {
from_file_cache(&body_cache_path)
} else {
@ -435,14 +444,31 @@ mod tests {
use std::ffi::CString;
use super::*;
use chrono::Utc;
use mockito::mock;
#[test]
fn test_create_merchandise_list() {
let example = SavedMerchandiseList {
id: 1,
shop_id: 1,
owner_id: 1,
form_list: vec![Merchandise {
mod_name: "Skyrim.esm".to_string(),
local_form_id: 1,
name: "Iron Sword".to_string(),
quantity: 1,
form_type: 1,
is_food: false,
price: 100,
}],
created_at: Utc::now().naive_utc(),
updated_at: Utc::now().naive_utc(),
};
let mock = mock("POST", "/v1/merchandise_lists")
.with_status(201)
.with_header("content-type", "application/json")
.with_body(r#"{ "created_at": "2020-08-18T00:00:00.000", "id": 1, "shop_id": 1, "form_list": [], "updated_at": "2020-08-18T00:00:00.000" }"#)
.with_header("content-type", "application/octet-stream")
.with_body(bincode::serialize(&example).unwrap())
.create();
let api_url = CString::new("url").unwrap().into_raw();
@ -500,7 +526,7 @@ mod tests {
FFIResult::Err(error) => {
assert_eq!(
unsafe { CStr::from_ptr(error).to_string_lossy() },
"expected value at line 1 column 1"
"Server 500: Internal Server Error"
);
}
}
@ -508,10 +534,26 @@ mod tests {
#[test]
fn test_update_merchandise_list() {
let example = SavedMerchandiseList {
id: 1,
shop_id: 1,
owner_id: 1,
form_list: vec![Merchandise {
mod_name: "Skyrim.esm".to_string(),
local_form_id: 1,
name: "Iron Sword".to_string(),
quantity: 1,
form_type: 1,
is_food: false,
price: 100,
}],
created_at: Utc::now().naive_utc(),
updated_at: Utc::now().naive_utc(),
};
let mock = mock("PATCH", "/v1/shops/1/merchandise_list")
.with_status(201)
.with_header("content-type", "application/json")
.with_body(r#"{ "created_at": "2020-08-18T00:00:00.000", "id": 1, "shop_id": 1, "form_list": [], "updated_at": "2020-08-18T00:00:00.000" }"#)
.with_header("content-type", "application/octet-stream")
.with_body(bincode::serialize(&example).unwrap())
.create();
let api_url = CString::new("url").unwrap().into_raw();
@ -569,35 +611,33 @@ mod tests {
FFIResult::Err(error) => {
assert_eq!(
unsafe { CStr::from_ptr(error).to_string_lossy() },
"expected value at line 1 column 1"
"Server 500: Internal Server Error"
);
}
}
}
#[test]
fn test_get_merchandise_list() {
let example = SavedMerchandiseList {
id: 1,
owner_id: 1,
shop_id: 1,
form_list: vec![Merchandise {
mod_name: "Skyrim.esm".to_string(),
local_form_id: 1,
name: "Iron Sword".to_string(),
quantity: 1,
form_type: 1,
is_food: false,
price: 100,
}],
created_at: Utc::now().naive_utc(),
updated_at: Utc::now().naive_utc(),
};
let mock = mock("GET", "/v1/merchandise_lists/1")
.with_status(201)
.with_header("content-type", "application/json")
.with_body(
r#"{
"created_at": "2020-08-18T00:00:00.000",
"id": 1,
"shop_id": 1,
"form_list": [
{
"mod_name": "Skyrim.esm",
"local_form_id": 1,
"name": "Iron Sword",
"quantity": 1,
"form_type": 1,
"is_food": false,
"price": 100
}
],
"updated_at": "2020-08-18T00:00:00.000"
}"#,
)
.with_header("content-type", "application/octet-stream")
.with_body(bincode::serialize(&example).unwrap())
.create();
let api_url = CString::new("url").unwrap().into_raw();
@ -655,7 +695,7 @@ mod tests {
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
"io error: failed to fill whole buffer" // empty tempfile
);
}
}
@ -663,28 +703,26 @@ mod tests {
#[test]
fn test_get_merchandise_list_by_shop_id() {
let example = SavedMerchandiseList {
id: 1,
owner_id: 1,
shop_id: 1,
form_list: vec![Merchandise {
mod_name: "Skyrim.esm".to_string(),
local_form_id: 1,
name: "Iron Sword".to_string(),
quantity: 1,
form_type: 1,
is_food: false,
price: 100,
}],
created_at: Utc::now().naive_utc(),
updated_at: Utc::now().naive_utc(),
};
let mock = mock("GET", "/v1/shops/1/merchandise_list")
.with_status(201)
.with_header("content-type", "application/json")
.with_body(
r#"{
"created_at": "2020-08-18T00:00:00.000",
"id": 1,
"shop_id": 1,
"form_list": [
{
"mod_name": "Skyrim.esm",
"local_form_id": 1,
"name": "Iron Sword",
"quantity": 1,
"form_type": 1,
"is_food": false,
"price": 100
}
],
"updated_at": "2020-08-18T00:00:00.000"
}"#,
)
.with_header("content-type", "application/octet-stream")
.with_body(bincode::serialize(&example).unwrap())
.create();
let api_url = CString::new("url").unwrap().into_raw();
@ -743,7 +781,7 @@ mod tests {
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
"io error: failed to fill whole buffer" // empty tempfile
);
}
}

View File

@ -1,6 +1,7 @@
use std::{ffi::CStr, ffi::CString, os::raw::c_char};
use anyhow::{anyhow, Result};
use anyhow::Result;
use chrono::NaiveDateTime;
use reqwest::Url;
use serde::{Deserialize, Serialize};
@ -9,23 +10,31 @@ use log::{error, info};
#[cfg(test)]
use std::{println as info, println as error};
use crate::{cache::file_cache_dir, cache::update_file_caches, result::FFIResult};
use crate::{
cache::file_cache_dir, cache::update_file_caches, error::extract_error_from_response,
result::FFIResult,
};
#[derive(Serialize, Deserialize, Debug)]
pub struct Owner {
pub id: Option<i32>,
pub name: String,
pub api_key: Option<String>,
pub mod_version: u32,
pub mod_version: i32,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct SavedOwner {
pub id: i32,
pub name: String,
pub mod_version: i32,
pub created_at: NaiveDateTime,
pub updated_at: NaiveDateTime,
}
impl Owner {
pub fn from_game(name: &str, api_key: &str, mod_version: u32) -> Self {
pub fn from_game(name: &str, mod_version: i32) -> Self {
Self {
id: None,
name: name.to_string(),
api_key: Some(api_key.to_string()),
mod_version,
mod_version: mod_version,
}
}
}
@ -35,7 +44,17 @@ impl Owner {
pub struct RawOwner {
pub id: i32,
pub name: *const c_char,
pub mod_version: u32,
pub mod_version: i32,
}
impl From<SavedOwner> for RawOwner {
fn from(raw_owner: SavedOwner) -> Self {
Self {
id: raw_owner.id,
name: CString::new(raw_owner.name).unwrap_or_default().into_raw(),
mod_version: raw_owner.mod_version,
}
}
}
#[no_mangle]
@ -43,7 +62,7 @@ pub extern "C" fn create_owner(
api_url: *const c_char,
api_key: *const c_char,
name: *const c_char,
mod_version: u32,
mod_version: i32,
) -> FFIResult<RawOwner> {
let api_url = unsafe { CStr::from_ptr(api_url) }.to_string_lossy();
let api_key = unsafe { CStr::from_ptr(api_key) }.to_string_lossy();
@ -53,55 +72,43 @@ pub extern "C" fn create_owner(
api_url, api_key, name, mod_version
);
fn inner(api_url: &str, api_key: &str, name: &str, mod_version: u32) -> Result<Owner> {
fn inner(api_url: &str, api_key: &str, name: &str, mod_version: i32) -> Result<SavedOwner> {
#[cfg(not(test))]
let url = Url::parse(api_url)?.join("v1/owners")?;
#[cfg(test)]
let url = Url::parse(&mockito::server_url())?.join("v1/owners")?;
let owner = Owner::from_game(name, api_key, mod_version);
let owner = Owner::from_game(name, mod_version);
info!("created owner from game: {:?}", &owner);
if let Some(api_key) = &owner.api_key {
let client = reqwest::blocking::Client::new();
let resp = client
.post(url)
.header("Api-Key", api_key.clone())
.json(&owner)
.send()?;
info!("create owner response from api: {:?}", &resp);
let client = reqwest::blocking::Client::new();
let resp = client
.post(url)
.header("Api-Key", api_key.clone())
.header("Content-Type", "application/octet-stream")
.body(bincode::serialize(&owner)?)
.send()?;
info!("create owner response from api: {:?}", &resp);
let cache_dir = file_cache_dir(api_url)?;
let headers = resp.headers().clone();
let bytes = resp.bytes()?;
let json: Owner = serde_json::from_slice(&bytes)?;
if let Some(id) = json.id {
let body_cache_path = cache_dir.join(format!("owner_{}.json", id));
let metadata_cache_path = cache_dir.join(format!("owner_{}_metadata.json", id));
update_file_caches(body_cache_path, metadata_cache_path, bytes, headers);
}
Ok(json)
let cache_dir = file_cache_dir(api_url)?;
let headers = resp.headers().clone();
let status = resp.status();
let bytes = resp.bytes()?;
if status.is_success() {
let saved_owner: SavedOwner = bincode::deserialize(&bytes)?;
let body_cache_path = cache_dir.join(format!("owner_{}.bin", saved_owner.id));
let metadata_cache_path =
cache_dir.join(format!("owner_{}_metadata.json", saved_owner.id));
update_file_caches(body_cache_path, metadata_cache_path, bytes, headers);
Ok(saved_owner)
} else {
Err(anyhow!("api-key not defined"))
Err(extract_error_from_response(status, &bytes))
}
}
match inner(&api_url, &api_key, &name, mod_version) {
Ok(owner) => {
info!("create_owner successful");
if let Some(id) = owner.id {
FFIResult::Ok(RawOwner {
id,
name: CString::new(owner.name).unwrap_or_default().into_raw(),
mod_version: owner.mod_version,
})
} else {
error!("create_owner failed. API did not return an owner with an ID");
let err_string = CString::new("API did not return an owner with an 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)
}
FFIResult::Ok(RawOwner::from(owner))
}
Err(err) => {
error!("create_owner failed. {}", err);
@ -118,9 +125,9 @@ pub extern "C" fn create_owner(
pub extern "C" fn update_owner(
api_url: *const c_char,
api_key: *const c_char,
id: u32,
id: i32,
name: *const c_char,
mod_version: u32,
mod_version: i32,
) -> FFIResult<RawOwner> {
let api_url = unsafe { CStr::from_ptr(api_url) }.to_string_lossy();
let api_key = unsafe { CStr::from_ptr(api_key) }.to_string_lossy();
@ -130,53 +137,48 @@ pub extern "C" fn update_owner(
api_url, api_key, name, mod_version
);
fn inner(api_url: &str, api_key: &str, id: u32, name: &str, mod_version: u32) -> Result<Owner> {
fn inner(
api_url: &str,
api_key: &str,
id: i32,
name: &str,
mod_version: i32,
) -> Result<SavedOwner> {
#[cfg(not(test))]
let url = Url::parse(api_url)?.join(&format!("v1/owners/{}", id))?;
#[cfg(test)]
let url = Url::parse(&mockito::server_url())?.join(&format!("v1/owners/{}", id))?;
let owner = Owner::from_game(name, api_key, mod_version);
let owner = Owner::from_game(name, mod_version);
info!("created owner from game: {:?}", &owner);
if let Some(api_key) = &owner.api_key {
let client = reqwest::blocking::Client::new();
let resp = client
.patch(url)
.header("Api-Key", api_key.clone())
.json(&owner)
.send()?;
info!("update owner response from api: {:?}", &resp);
let client = reqwest::blocking::Client::new();
let resp = client
.patch(url)
.header("Api-Key", api_key.clone())
.header("Content-Type", "application/octet-stream")
.body(bincode::serialize(&owner)?)
.send()?;
info!("update owner response from api: {:?}", &resp);
let cache_dir = file_cache_dir(api_url)?;
let body_cache_path = cache_dir.join(format!("owner_{}.json", id));
let metadata_cache_path = cache_dir.join(format!("owner_{}_metadata.json", id));
let headers = resp.headers().clone();
let bytes = resp.bytes()?;
let json: Owner = serde_json::from_slice(&bytes)?;
let cache_dir = file_cache_dir(api_url)?;
let body_cache_path = cache_dir.join(format!("owner_{}.bin", id));
let metadata_cache_path = cache_dir.join(format!("owner_{}_metadata.json", id));
let headers = resp.headers().clone();
let status = resp.status();
let bytes = resp.bytes()?;
if status.is_success() {
let saved_owner: SavedOwner = bincode::deserialize(&bytes)?;
update_file_caches(body_cache_path, metadata_cache_path, bytes, headers);
Ok(json)
Ok(saved_owner)
} else {
Err(anyhow!("api-key not defined"))
Err(extract_error_from_response(status, &bytes))
}
}
match inner(&api_url, &api_key, id, &name, mod_version) {
Ok(owner) => {
info!("update_owner successful");
if let Some(id) = owner.id {
FFIResult::Ok(RawOwner {
id,
name: CString::new(owner.name).unwrap_or_default().into_raw(),
mod_version: owner.mod_version,
})
} else {
error!("update_owner failed. API did not return an owner with an ID");
let err_string = CString::new("API did not return an owner with an 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)
}
FFIResult::Ok(RawOwner::from(owner))
}
Err(err) => {
error!("update_owner failed. {}", err);
@ -194,14 +196,22 @@ mod tests {
use std::ffi::CString;
use super::*;
use chrono::Utc;
use mockito::mock;
#[test]
fn test_create_owner() {
let example = SavedOwner {
id: 1,
name: "name".to_string(),
mod_version: 1,
created_at: Utc::now().naive_utc(),
updated_at: Utc::now().naive_utc(),
};
let mock = mock("POST", "/v1/owners")
.with_status(201)
.with_header("content-type", "application/json")
.with_body(r#"{ "created_at": "2020-08-18T00:00:00.000", "id": 1, "name": "name", "mod_version": 1, "updated_at": "2020-08-18T00:00:00.000" }"#)
.with_header("content-type", "application/octet-stream")
.with_body(bincode::serialize(&example).unwrap())
.create();
let api_url = CString::new("url").unwrap().into_raw();
@ -245,7 +255,7 @@ mod tests {
FFIResult::Err(error) => {
assert_eq!(
unsafe { CStr::from_ptr(error).to_string_lossy() },
"expected value at line 1 column 1"
"Server 500: Internal Server Error"
);
}
}
@ -253,10 +263,17 @@ mod tests {
#[test]
fn test_update_owner() {
let example = SavedOwner {
id: 1,
name: "name".to_string(),
mod_version: 1,
created_at: Utc::now().naive_utc(),
updated_at: Utc::now().naive_utc(),
};
let mock = mock("PATCH", "/v1/owners/1")
.with_status(201)
.with_header("content-type", "application/json")
.with_body(r#"{ "created_at": "2020-08-18T00:00:00.000", "id": 1, "name": "name", "mod_version": 1, "updated_at": "2020-08-18T00:00:00.000" }"#)
.with_header("content-type", "application/octet-stream")
.with_body(bincode::serialize(&example).unwrap())
.create();
let api_url = CString::new("url").unwrap().into_raw();
@ -300,7 +317,7 @@ mod tests {
FFIResult::Err(error) => {
assert_eq!(
unsafe { CStr::from_ptr(error).to_string_lossy() },
"expected value at line 1 column 1"
"Server 500: Internal Server Error"
);
}
}

View File

@ -1,6 +1,7 @@
use std::{convert::TryFrom, ffi::CStr, ffi::CString, os::raw::c_char};
use std::{ffi::CStr, ffi::CString, os::raw::c_char};
use anyhow::{anyhow, Result};
use anyhow::Result;
use chrono::NaiveDateTime;
use reqwest::{StatusCode, Url};
use serde::{Deserialize, Serialize};
@ -11,38 +12,35 @@ use std::{println as info, println as error};
use crate::{
cache::file_cache_dir, cache::from_file_cache, cache::load_metadata_from_file_cache,
cache::update_file_caches, log_server_error, result::FFIResult,
cache::update_file_caches, error::extract_error_from_response, log_server_error,
result::FFIResult,
};
#[derive(Serialize, Deserialize, Debug)]
pub struct Shop {
pub id: Option<i32>,
pub name: String,
pub description: String,
pub owner_id: Option<i32>,
pub description: Option<String>,
}
impl Shop {
pub fn from_game(name: &str, description: &str) -> Self {
Self {
id: None,
name: name.to_string(),
description: description.to_string(),
owner_id: None,
description: Some(description.to_string()),
}
}
}
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(Serialize, Deserialize, Debug)]
pub struct SavedShop {
pub id: i32,
pub name: String,
pub owner_id: i32,
pub description: Option<String>,
pub created_at: NaiveDateTime,
pub updated_at: NaiveDateTime,
}
#[derive(Debug)]
@ -53,20 +51,14 @@ pub struct RawShop {
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"))
impl From<SavedShop> for RawShop {
fn from(shop: SavedShop) -> Self {
Self {
id: shop.id,
name: CString::new(shop.name).unwrap_or_default().into_raw(),
description: CString::new(shop.description.unwrap_or_else(|| "".to_string()))
.unwrap_or_default()
.into_raw(),
}
}
}
@ -95,7 +87,7 @@ pub extern "C" fn create_shop(
api_url, api_key, name, description
);
fn inner(api_url: &str, api_key: &str, name: &str, description: &str) -> Result<Shop> {
fn inner(api_url: &str, api_key: &str, name: &str, description: &str) -> Result<SavedShop> {
#[cfg(not(test))]
let url = Url::parse(api_url)?.join("v1/shops")?;
#[cfg(test)]
@ -107,35 +99,31 @@ pub extern "C" fn create_shop(
let resp = client
.post(url)
.header("Api-Key", api_key)
.json(&shop)
.header("Content-Type", "application/octet-stream")
.body(bincode::serialize(&shop)?)
.send()?;
info!("create shop response from api: {:?}", &resp);
let cache_dir = file_cache_dir(api_url)?;
let headers = resp.headers().clone();
let status = resp.status();
let bytes = resp.bytes()?;
let json: Shop = serde_json::from_slice(&bytes)?;
if let Some(id) = json.id {
let body_cache_path = cache_dir.join(format!("shop_{}.json", id));
let metadata_cache_path = cache_dir.join(format!("shop_{}_metadata.json", id));
if status.is_success() {
let saved_shop: SavedShop = bincode::deserialize(&bytes)?;
let body_cache_path = cache_dir.join(format!("shop_{}.bin", saved_shop.id));
let metadata_cache_path =
cache_dir.join(format!("shop_{}_metadata.json", saved_shop.id));
update_file_caches(body_cache_path, metadata_cache_path, bytes, headers);
Ok(saved_shop)
} else {
Err(extract_error_from_response(status, &bytes))
}
Ok(json)
}
match inner(&api_url, &api_key, &name, &description) {
Ok(shop) => {
info!("create_shop successful");
if let Ok(raw_shop) = RawShop::try_from(shop) {
FFIResult::Ok(raw_shop)
} else {
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())
.expect("could not create CString")
.into_raw();
// TODO: also need to drop this CString once C++ is done reading it
FFIResult::Err(err_string)
}
FFIResult::Ok(RawShop::from(shop))
}
Err(err) => {
error!("create_shop failed. {}", err);
@ -166,7 +154,13 @@ pub extern "C" fn update_shop(
api_url, api_key, name, description
);
fn inner(api_url: &str, api_key: &str, id: u32, name: &str, description: &str) -> Result<Shop> {
fn inner(
api_url: &str,
api_key: &str,
id: u32,
name: &str,
description: &str,
) -> Result<SavedShop> {
#[cfg(not(test))]
let url = Url::parse(api_url)?.join(&format!("v1/shops/{}", id))?;
#[cfg(test)]
@ -178,33 +172,30 @@ pub extern "C" fn update_shop(
let resp = client
.patch(url)
.header("Api-Key", api_key)
.json(&shop)
.header("Content-Type", "application/octet-stream")
.body(bincode::serialize(&shop)?)
.send()?;
info!("update shop response from api: {:?}", &resp);
let cache_dir = file_cache_dir(api_url)?;
let body_cache_path = cache_dir.join(format!("shop_{}.json", id));
let body_cache_path = cache_dir.join(format!("shop_{}.bin", id));
let metadata_cache_path = cache_dir.join(format!("shop_{}_metadata.json", id));
let headers = resp.headers().clone();
let status = resp.status();
let bytes = resp.bytes()?;
let json: Shop = serde_json::from_slice(&bytes)?;
update_file_caches(body_cache_path, metadata_cache_path, bytes, headers);
Ok(json)
if status.is_success() {
let saved_shop: SavedShop = bincode::deserialize(&bytes)?;
update_file_caches(body_cache_path, metadata_cache_path, bytes, headers);
Ok(saved_shop)
} else {
Err(extract_error_from_response(status, &bytes))
}
}
match inner(&api_url, &api_key, id, &name, &description) {
Ok(shop) => {
info!("update_shop successful");
if let Ok(raw_shop) = RawShop::try_from(shop) {
FFIResult::Ok(raw_shop)
} else {
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())
.expect("could not create CString")
.into_raw();
// TODO: also need to drop this CString once C++ is done reading it
FFIResult::Err(err_string)
}
FFIResult::Ok(RawShop::from(shop))
}
Err(err) => {
error!("update_shop failed. {}", err);
@ -231,7 +222,7 @@ pub extern "C" fn get_shop(
api_url, api_key, shop_id
);
fn inner(api_url: &str, api_key: &str, shop_id: i32) -> Result<Shop> {
fn inner(api_url: &str, api_key: &str, shop_id: i32) -> Result<SavedShop> {
#[cfg(not(test))]
let url = Url::parse(api_url)?.join(&format!("v1/shops/{}", shop_id))?;
#[cfg(test)]
@ -240,9 +231,12 @@ pub extern "C" fn get_shop(
let client = reqwest::blocking::Client::new();
let cache_dir = file_cache_dir(api_url)?;
let body_cache_path = cache_dir.join(format!("shop_{}.json", shop_id));
let body_cache_path = cache_dir.join(format!("shop_{}.bin", shop_id));
let metadata_cache_path = cache_dir.join(format!("shop_{}_metadata.json", shop_id));
let mut request = client.get(url).header("Api-Key", api_key);
let mut request = client
.get(url)
.header("Api-Key", api_key)
.header("Accept", "application/octet-stream");
// TODO: load metadata from in-memory LRU cache first before trying to load from file
if let Ok(metadata) = load_metadata_from_file_cache(&metadata_cache_path) {
if let Some(etag) = metadata.etag {
@ -256,9 +250,9 @@ pub extern "C" fn get_shop(
if resp.status().is_success() {
let headers = resp.headers().clone();
let bytes = resp.bytes()?;
let json = serde_json::from_slice(&bytes)?;
let saved_shop: SavedShop = bincode::deserialize(&bytes)?;
update_file_caches(body_cache_path, metadata_cache_path, bytes, headers);
Ok(json)
Ok(saved_shop)
} else if resp.status() == StatusCode::NOT_MODIFIED {
from_file_cache(&body_cache_path)
} else {
@ -276,16 +270,7 @@ pub extern "C" fn get_shop(
match inner(&api_url, &api_key, shop_id) {
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.
if let Ok(raw_shop) = RawShop::try_from(shop) {
FFIResult::Ok(raw_shop)
} else {
error!("get_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())
.expect("could not create CString")
.into_raw();
// TODO: also need to drop this CString once C++ is done reading it
FFIResult::Err(err_string)
}
FFIResult::Ok(RawShop::from(shop))
}
Err(err) => {
error!("get_shop_list failed. {}", err);
@ -307,7 +292,7 @@ pub extern "C" fn list_shops(
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>> {
fn inner(api_url: &str, api_key: &str) -> Result<Vec<SavedShop>> {
#[cfg(not(test))]
let url = Url::parse(api_url)?.join("v1/shops?limit=128")?;
#[cfg(test)]
@ -316,9 +301,12 @@ pub extern "C" fn list_shops(
let client = reqwest::blocking::Client::new();
let cache_dir = file_cache_dir(api_url)?;
let body_cache_path = cache_dir.join("shops.json");
let body_cache_path = cache_dir.join("shops.bin");
let metadata_cache_path = cache_dir.join("shops_metadata.json");
let mut request = client.get(url).header("Api-Key", api_key);
let mut request = client
.get(url)
.header("Api-Key", api_key)
.header("Accept", "application/octet-stream");
// TODO: load metadata from in-memory LRU cache first before trying to load from file
if let Ok(metadata) = load_metadata_from_file_cache(&metadata_cache_path) {
if let Some(etag) = metadata.etag {
@ -332,9 +320,9 @@ pub extern "C" fn list_shops(
if resp.status().is_success() {
let headers = resp.headers().clone();
let bytes = resp.bytes()?;
let json = serde_json::from_slice(&bytes)?;
let saved_shops: Vec<SavedShop> = bincode::deserialize(&bytes)?;
update_file_caches(body_cache_path, metadata_cache_path, bytes, headers);
Ok(json)
Ok(saved_shops)
} else if resp.status() == StatusCode::NOT_MODIFIED {
from_file_cache(&body_cache_path)
} else {
@ -352,20 +340,9 @@ pub extern "C" fn list_shops(
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)
}
let raw_shops: Vec<RawShop> = shops.into_iter().map(RawShop::from).collect();
let (ptr, len, cap) = raw_shops.into_raw_parts();
FFIResult::Ok(RawShopVec { ptr, len, cap })
}
Err(err) => {
error!("list_shops failed. {}", err);
@ -383,14 +360,23 @@ mod tests {
use std::{ffi::CString, slice};
use super::*;
use chrono::Utc;
use mockito::mock;
#[test]
fn test_create_shop() {
let example = SavedShop {
id: 1,
owner_id: 1,
name: "name".to_string(),
description: Some("description".to_string()),
created_at: Utc::now().naive_utc(),
updated_at: Utc::now().naive_utc(),
};
let mock = mock("POST", "/v1/shops")
.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" }"#)
.with_header("content-type", "application/octet-stream")
.with_body(bincode::serialize(&example).unwrap())
.create();
let api_url = CString::new("url").unwrap().into_raw();
@ -435,7 +421,7 @@ mod tests {
FFIResult::Err(error) => {
assert_eq!(
unsafe { CStr::from_ptr(error).to_string_lossy() },
"expected value at line 1 column 1"
"Server 500: Internal Server Error"
);
}
}
@ -443,10 +429,18 @@ mod tests {
#[test]
fn test_update_shop() {
let example = SavedShop {
id: 1,
owner_id: 1,
name: "name".to_string(),
description: Some("description".to_string()),
created_at: Utc::now().naive_utc(),
updated_at: Utc::now().naive_utc(),
};
let mock = mock("PATCH", "/v1/shops/1")
.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-19T00:00:00.000" }"#)
.with_header("content-type", "application/octet-stream")
.with_body(bincode::serialize(&example).unwrap())
.create();
let api_url = CString::new("url").unwrap().into_raw();
@ -491,7 +485,7 @@ mod tests {
FFIResult::Err(error) => {
assert_eq!(
unsafe { CStr::from_ptr(error).to_string_lossy() },
"expected value at line 1 column 1"
"Server 500: Internal Server Error"
);
}
}
@ -499,10 +493,18 @@ mod tests {
#[test]
fn test_get_shop() {
let example = SavedShop {
id: 1,
owner_id: 1,
name: "name".to_string(),
description: Some("description".to_string()),
created_at: Utc::now().naive_utc(),
updated_at: Utc::now().naive_utc(),
};
let mock = mock("GET", "/v1/shops/1")
.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" }"#)
.with_header("content-type", "application/octet-stream")
.with_body(bincode::serialize(&example).unwrap())
.create();
let api_url = CString::new("url").unwrap().into_raw();
@ -543,7 +545,7 @@ mod tests {
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
"io error: failed to fill whole buffer" // empty tempfile
);
}
}
@ -551,10 +553,18 @@ mod tests {
#[test]
fn test_list_shops() {
let example = vec![SavedShop {
id: 1,
owner_id: 1,
name: "name".to_string(),
description: Some("description".to_string()),
created_at: Utc::now().naive_utc(),
updated_at: Utc::now().naive_utc(),
}];
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" }]"#)
.with_header("content-type", "application/octet-stream")
.with_body(bincode::serialize(&example).unwrap())
.create();
let api_url = CString::new("url").unwrap().into_raw();
@ -601,7 +611,7 @@ mod tests {
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
"io error: failed to fill whole buffer" // empty tempfile
);
}
}

View File

@ -1,7 +1,7 @@
use std::{convert::TryFrom, ffi::CStr, ffi::CString, os::raw::c_char, slice, str, thread};
use std::{ffi::CStr, ffi::CString, os::raw::c_char};
use anyhow::{anyhow, Result};
use http_api_problem::HttpApiProblem;
use anyhow::Result;
use chrono::NaiveDateTime;
use reqwest::Url;
use serde::{Deserialize, Serialize};
@ -11,52 +11,41 @@ use log::{error, info};
use std::{println as info, println as error};
use crate::{
cache::file_cache_dir, cache::from_file_cache, cache::update_file_caches, log_server_error,
cache::file_cache_dir, cache::update_file_caches, error::extract_error_from_response,
result::FFIResult,
};
#[derive(Serialize, Deserialize, Debug)]
pub struct Transaction {
pub id: Option<u32>,
pub shop_id: u32,
pub id: Option<i32>,
pub shop_id: i32,
pub mod_name: String,
pub local_form_id: u32,
pub local_form_id: i32,
pub name: String,
pub form_type: u32,
pub form_type: i32,
pub is_food: bool,
pub price: u32,
pub price: i32,
pub is_sell: bool,
pub quantity: u32,
pub amount: u32,
pub quantity: i32,
pub amount: i32,
}
impl Transaction {
pub fn from_game(
shop_id: u32,
mod_name: &str,
local_form_id: u32,
name: &str,
form_type: u32,
is_food: bool,
price: u32,
is_sell: bool,
quantity: u32,
amount: u32,
) -> Self {
Self {
id: None,
shop_id,
mod_name: mod_name.to_string(),
local_form_id,
name: name.to_string(),
form_type,
is_food,
price,
is_sell,
quantity,
amount,
}
}
#[derive(Serialize, Deserialize, Debug)]
pub struct SavedTransaction {
pub id: i32,
pub owner_id: i32,
pub shop_id: i32,
pub mod_name: String,
pub local_form_id: i32,
pub name: String,
pub form_type: i32,
pub is_food: bool,
pub price: i32,
pub is_sell: bool,
pub quantity: i32,
pub amount: i32,
pub created_at: NaiveDateTime,
pub updated_at: NaiveDateTime,
}
impl From<RawTransaction> for Transaction {
@ -87,23 +76,23 @@ impl From<RawTransaction> for Transaction {
#[derive(Debug)]
#[repr(C)]
pub struct RawTransaction {
pub id: u32,
pub shop_id: u32,
pub id: i32,
pub shop_id: i32,
pub mod_name: *const c_char,
pub local_form_id: u32,
pub local_form_id: i32,
pub name: *const c_char,
pub form_type: u32,
pub form_type: i32,
pub is_food: bool,
pub price: u32,
pub price: i32,
pub is_sell: bool,
pub quantity: u32,
pub amount: u32,
pub quantity: i32,
pub amount: i32,
}
impl From<Transaction> for RawTransaction {
fn from(transaction: Transaction) -> Self {
impl From<SavedTransaction> for RawTransaction {
fn from(transaction: SavedTransaction) -> Self {
Self {
id: transaction.id.unwrap_or(0),
id: transaction.id,
shop_id: transaction.shop_id,
mod_name: CString::new(transaction.mod_name)
.unwrap_or_default()
@ -144,7 +133,7 @@ pub extern "C" fn create_transaction(
api_url, api_key, transaction
);
fn inner(api_url: &str, api_key: &str, transaction: Transaction) -> Result<Transaction> {
fn inner(api_url: &str, api_key: &str, transaction: Transaction) -> Result<SavedTransaction> {
#[cfg(not(test))]
let url = Url::parse(api_url)?.join("v1/transactions")?;
#[cfg(test)]
@ -154,7 +143,8 @@ pub extern "C" fn create_transaction(
let resp = client
.post(url)
.header("Api-Key", api_key)
.json(&transaction)
.header("Content-Type", "application/octet-stream")
.body(bincode::serialize(&transaction)?)
.send()?;
info!("create transaction response from api: {:?}", &resp);
@ -163,55 +153,22 @@ pub extern "C" fn create_transaction(
let status = resp.status();
let bytes = resp.bytes()?;
if status.is_success() {
let json: Transaction = serde_json::from_slice(&bytes)?;
if let Some(id) = json.id {
let body_cache_path = cache_dir.join(format!("transaction_{}.json", id));
let metadata_cache_path =
cache_dir.join(format!("transaction_{}_metadata.json", id));
update_file_caches(body_cache_path, metadata_cache_path, bytes, headers);
}
Ok(json)
let saved_transaction: SavedTransaction = bincode::deserialize(&bytes)?;
let body_cache_path =
cache_dir.join(format!("transaction_{}.bin", saved_transaction.id));
let metadata_cache_path = cache_dir.join(format!(
"transaction_{}_metadata.json",
saved_transaction.id
));
update_file_caches(body_cache_path, metadata_cache_path, bytes, headers);
Ok(saved_transaction)
} else {
// TODO: abstract this away into a separate helper
match serde_json::from_slice::<HttpApiProblem>(&bytes) {
Ok(api_problem) => {
let detail = api_problem.detail.unwrap_or("".to_string());
error!(
"Server {}: {}. {}",
status.as_u16(),
api_problem.title,
detail
);
Err(anyhow!(format!(
"Server {}: {}. {}",
status.as_u16(),
api_problem.title,
detail
)))
}
Err(_) => {
let detail = str::from_utf8(&bytes).unwrap_or("unknown");
error!("Server {}: {}", status.as_u16(), detail);
Err(anyhow!(format!("Server {}: {}", status.as_u16(), detail)))
}
}
Err(extract_error_from_response(status, &bytes))
}
}
match inner(&api_url, &api_key, transaction) {
Ok(transaction) => {
if let Ok(raw_transaction) = RawTransaction::try_from(transaction) {
FFIResult::Ok(raw_transaction)
} else {
error!("create_transaction failed. API did not return a transaction with an ID");
let err_string =
CString::new("API did not return a transaction with an 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)
}
}
Ok(transaction) => FFIResult::Ok(RawTransaction::from(transaction)),
Err(err) => {
error!("create_transaction failed. {}", err);
// TODO: also need to drop this CString once C++ is done reading it
@ -228,31 +185,31 @@ mod tests {
use std::ffi::CString;
use super::*;
use chrono::Utc;
use mockito::mock;
#[test]
fn test_create_transaction() {
let example = SavedTransaction {
id: 1,
shop_id: 1,
owner_id: 1,
mod_name: "Skyrim.esm".to_string(),
local_form_id: 1,
name: "Item".to_string(),
form_type: 41,
is_food: false,
is_sell: false,
price: 100,
quantity: 1,
amount: 100,
created_at: Utc::now().naive_utc(),
updated_at: Utc::now().naive_utc(),
};
let mock = mock("POST", "/v1/transactions")
.with_status(201)
.with_header("content-type", "application/json")
.with_body(
r#"{
"amount": 100,
"created_at": "2020-08-18T00:00:00.000",
"form_type": 41,
"id": 1,
"is_food": false,
"is_sell": false,
"local_form_id": 1,
"mod_name": "Skyrim.esm",
"name": "Item",
"owner_id": 1,
"price": 100,
"quantity": 1,
"shop_id": 1,
"updated_at": "2020-08-18T00:00:00.000"
}"#,
)
.with_header("content-type", "application/octet-stream")
.with_body(bincode::serialize(&example).unwrap())
.create();
let api_url = CString::new("url").unwrap().into_raw();