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

View File

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

View File

@ -86,7 +86,7 @@ struct RawMerchandise {
struct RawOwner { struct RawOwner {
int32_t id; int32_t id;
const char *name; const char *name;
uint32_t mod_version; int32_t mod_version;
}; };
struct RawShop { struct RawShop {
@ -96,17 +96,17 @@ struct RawShop {
}; };
struct RawTransaction { struct RawTransaction {
uint32_t id; int32_t id;
uint32_t shop_id; int32_t shop_id;
const char *mod_name; const char *mod_name;
uint32_t local_form_id; int32_t local_form_id;
const char *name; const char *name;
uint32_t form_type; int32_t form_type;
bool is_food; bool is_food;
uint32_t price; int32_t price;
bool is_sell; bool is_sell;
uint32_t quantity; int32_t quantity;
uint32_t amount; int32_t amount;
}; };
struct RawInteriorRefVec { struct RawInteriorRefVec {
@ -161,7 +161,7 @@ FFIResult<int32_t> create_merchandise_list(const char *api_url,
FFIResult<RawOwner> create_owner(const char *api_url, FFIResult<RawOwner> create_owner(const char *api_url,
const char *api_key, const char *api_key,
const char *name, const char *name,
uint32_t mod_version); int32_t mod_version);
FFIResult<RawShop> create_shop(const char *api_url, FFIResult<RawShop> create_shop(const char *api_url,
const char *api_key, 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, FFIResult<RawOwner> update_owner(const char *api_url,
const char *api_key, const char *api_key,
uint32_t id, int32_t id,
const char *name, const char *name,
uint32_t mod_version); int32_t mod_version);
FFIResult<RawShop> update_shop(const char *api_url, FFIResult<RawShop> update_shop(const char *api_url,
const char *api_key, 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); let reader = BufReader::new(file);
info!("returning value from cache: {:?}", cache_path); 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> { 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 std::{ffi::CStr, ffi::CString, os::raw::c_char, slice};
use anyhow::Result; use anyhow::Result;
use chrono::NaiveDateTime;
use reqwest::{StatusCode, Url}; use reqwest::{StatusCode, Url};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -11,7 +12,8 @@ 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, log_server_error, result::FFIResult, cache::update_file_caches, error::extract_error_from_response, log_server_error,
result::FFIResult,
}; };
#[derive(Serialize, Deserialize, Debug)] #[derive(Serialize, Deserialize, Debug)]
@ -24,9 +26,9 @@ pub struct InteriorRefList {
#[derive(Serialize, Deserialize, Debug)] #[derive(Serialize, Deserialize, Debug)]
pub struct InteriorRef { pub struct InteriorRef {
pub base_mod_name: String, 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_mod_name: Option<String>,
pub ref_local_form_id: i32, pub ref_local_form_id: u32,
pub position_x: f32, pub position_x: f32,
pub position_y: f32, pub position_y: f32,
pub position_z: f32, pub position_z: f32,
@ -47,7 +49,7 @@ impl InteriorRefList {
base_mod_name: unsafe { CStr::from_ptr(rec.base_mod_name) } base_mod_name: unsafe { CStr::from_ptr(rec.base_mod_name) }
.to_string_lossy() .to_string_lossy()
.to_string(), .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() { ref_mod_name: match rec.ref_mod_name.is_null() {
true => None, true => None,
false => Some( false => Some(
@ -56,7 +58,7 @@ impl InteriorRefList {
.to_string(), .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_x: rec.position_x,
position_y: rec.position_y, position_y: rec.position_y,
position_z: rec.position_z, 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)] #[derive(Debug)]
#[repr(C)] #[repr(C)]
pub struct RawInteriorRef { pub struct RawInteriorRef {
@ -86,6 +98,29 @@ pub struct RawInteriorRef {
pub scale: u16, 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)] #[derive(Debug)]
#[repr(C)] #[repr(C)]
pub struct RawInteriorRefVec { pub struct RawInteriorRefVec {
@ -116,7 +151,7 @@ pub extern "C" fn create_interior_ref_list(
api_key: &str, api_key: &str,
shop_id: i32, shop_id: i32,
raw_interior_ref_slice: &[RawInteriorRef], raw_interior_ref_slice: &[RawInteriorRef],
) -> Result<InteriorRefList> { ) -> Result<SavedInteriorRefList> {
#[cfg(not(test))] #[cfg(not(test))]
let url = Url::parse(api_url)?.join("v1/interior_ref_lists")?; let url = Url::parse(api_url)?.join("v1/interior_ref_lists")?;
#[cfg(test)] #[cfg(test)]
@ -131,37 +166,34 @@ pub extern "C" fn create_interior_ref_list(
let resp = client let resp = client
.post(url) .post(url)
.header("Api-Key", api_key) .header("Api-Key", api_key)
.json(&interior_ref_list) .header("Content-Type", "application/octet-stream")
.body(bincode::serialize(&interior_ref_list)?)
.send()?; .send()?;
info!("create interior_ref_list response from api: {:?}", &resp); info!("create interior_ref_list response from api: {:?}", &resp);
let cache_dir = file_cache_dir(api_url)?; let cache_dir = file_cache_dir(api_url)?;
let headers = resp.headers().clone(); let headers = resp.headers().clone();
let status = resp.status();
let bytes = resp.bytes()?; let bytes = resp.bytes()?;
let json: InteriorRefList = serde_json::from_slice(&bytes)?; if status.is_success() {
if let Some(id) = json.id { let saved_interior_ref_list: SavedInteriorRefList = bincode::deserialize(&bytes)?;
let body_cache_path = cache_dir.join(format!("interior_ref_list_{}.json", id)); let body_cache_path = cache_dir.join(format!(
let metadata_cache_path = "interior_ref_list_{}.bin",
cache_dir.join(format!("interior_ref_list_{}_metadata.json", id)); 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); 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) { match inner(&api_url, &api_key, shop_id, raw_interior_ref_slice) {
Ok(interior_ref_list) => { Ok(interior_ref_list) => FFIResult::Ok(interior_ref_list.id),
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)
}
}
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 // 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, api_key: &str,
shop_id: i32, shop_id: i32,
raw_interior_ref_slice: &[RawInteriorRef], raw_interior_ref_slice: &[RawInteriorRef],
) -> Result<InteriorRefList> { ) -> Result<SavedInteriorRefList> {
#[cfg(not(test))] #[cfg(not(test))]
let url = Url::parse(api_url)?.join(&format!("v1/shops/{}/interior_ref_list", shop_id))?; let url = Url::parse(api_url)?.join(&format!("v1/shops/{}/interior_ref_list", shop_id))?;
#[cfg(test)] #[cfg(test)]
@ -210,35 +242,29 @@ pub extern "C" fn update_interior_ref_list(
let resp = client let resp = client
.patch(url) .patch(url)
.header("Api-Key", api_key) .header("Api-Key", api_key)
.json(&interior_ref_list) .header("Content-Type", "application/octet-stream")
.body(bincode::serialize(&interior_ref_list)?)
.send()?; .send()?;
info!("update interior_ref_list response from api: {:?}", &resp); info!("update interior_ref_list response from api: {:?}", &resp);
let cache_dir = file_cache_dir(api_url)?; 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 = let metadata_cache_path =
cache_dir.join(format!("shop_{}_interior_ref_list_metadata.json", shop_id)); cache_dir.join(format!("shop_{}_interior_ref_list_metadata.json", shop_id));
let headers = resp.headers().clone(); let headers = resp.headers().clone();
let status = resp.status();
let bytes = resp.bytes()?; let bytes = resp.bytes()?;
let json: InteriorRefList = serde_json::from_slice(&bytes)?; if status.is_success() {
let saved_interior_ref_list: SavedInteriorRefList = bincode::deserialize(&bytes)?;
update_file_caches(body_cache_path, metadata_cache_path, bytes, headers); update_file_caches(body_cache_path, metadata_cache_path, bytes, headers);
Ok(json) 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) { match inner(&api_url, &api_key, shop_id, raw_interior_ref_slice) {
Ok(interior_ref_list) => { Ok(interior_ref_list) => FFIResult::Ok(interior_ref_list.id),
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)
}
}
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 // 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 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))] #[cfg(not(test))]
let url = Url::parse(api_url)? let url = Url::parse(api_url)?
.join(&format!("v1/interior_ref_lists/{}", interior_ref_list_id))?; .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 client = reqwest::blocking::Client::new();
let cache_dir = file_cache_dir(api_url)?; let cache_dir = file_cache_dir(api_url)?;
let body_cache_path = 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!( let metadata_cache_path = cache_dir.join(format!(
"interior_ref_list_{}_metadata.json", "interior_ref_list_{}_metadata.json",
interior_ref_list_id 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 // 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 Ok(metadata) = load_metadata_from_file_cache(&metadata_cache_path) {
if let Some(etag) = metadata.etag { if let Some(etag) = metadata.etag {
@ -295,9 +328,9 @@ pub extern "C" fn get_interior_ref_list(
if resp.status().is_success() { if resp.status().is_success() {
let headers = resp.headers().clone(); let headers = resp.headers().clone();
let bytes = resp.bytes()?; 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); 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 { } else if resp.status() == StatusCode::NOT_MODIFIED {
from_file_cache(&body_cache_path) from_file_cache(&body_cache_path)
} else { } else {
@ -317,26 +350,7 @@ pub extern "C" fn get_interior_ref_list(
let (ptr, len, cap) = interior_ref_list let (ptr, len, cap) = interior_ref_list
.ref_list .ref_list
.into_iter() .into_iter()
.map(|interior_ref| RawInteriorRef { .map(RawInteriorRef::from)
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,
})
.collect::<Vec<RawInteriorRef>>() .collect::<Vec<RawInteriorRef>>()
.into_raw_parts(); .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. // 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 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))] #[cfg(not(test))]
let url = Url::parse(api_url)?.join(&format!("v1/shops/{}/interior_ref_list", shop_id))?; let url = Url::parse(api_url)?.join(&format!("v1/shops/{}/interior_ref_list", shop_id))?;
#[cfg(test)] #[cfg(test)]
@ -377,10 +391,13 @@ pub extern "C" fn get_interior_ref_list_by_shop_id(
let client = reqwest::blocking::Client::new(); let client = reqwest::blocking::Client::new();
let cache_dir = file_cache_dir(api_url)?; 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 = let metadata_cache_path =
cache_dir.join(format!("shop_{}_interior_ref_list_metadata.json", shop_id)); 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 // 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 Ok(metadata) = load_metadata_from_file_cache(&metadata_cache_path) {
if let Some(etag) = metadata.etag { 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() { if resp.status().is_success() {
let headers = resp.headers().clone(); let headers = resp.headers().clone();
let bytes = resp.bytes()?; 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); 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 { } else if resp.status() == StatusCode::NOT_MODIFIED {
from_file_cache(&body_cache_path) from_file_cache(&body_cache_path)
} else { } else {
@ -422,26 +439,7 @@ pub extern "C" fn get_interior_ref_list_by_shop_id(
let (ptr, len, cap) = interior_ref_list let (ptr, len, cap) = interior_ref_list
.ref_list .ref_list
.into_iter() .into_iter()
.map(|interior_ref| RawInteriorRef { .map(RawInteriorRef::from)
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,
})
.collect::<Vec<RawInteriorRef>>() .collect::<Vec<RawInteriorRef>>()
.into_raw_parts(); .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. // 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 std::ffi::CString;
use super::*; use super::*;
use chrono::Utc;
use mockito::mock; use mockito::mock;
#[test] #[test]
fn test_create_interior_ref_list() { 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") let mock = mock("POST", "/v1/interior_ref_lists")
.with_status(201) .with_status(201)
.with_header("content-type", "application/json") .with_header("content-type", "application/octet-stream")
.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_body(bincode::serialize(&example).unwrap())
.create(); .create();
let api_url = CString::new("url").unwrap().into_raw(); let api_url = CString::new("url").unwrap().into_raw();
@ -537,7 +556,7 @@ mod tests {
FFIResult::Err(error) => { FFIResult::Err(error) => {
assert_eq!( assert_eq!(
unsafe { CStr::from_ptr(error).to_string_lossy() }, 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] #[test]
fn test_update_interior_ref_list() { 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") let mock = mock("PATCH", "/v1/shops/1/interior_ref_list")
.with_status(201) .with_status(201)
.with_header("content-type", "application/json") .with_header("content-type", "application/octet-stream")
.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_body(bincode::serialize(&example).unwrap())
.create(); .create();
let api_url = CString::new("url").unwrap().into_raw(); let api_url = CString::new("url").unwrap().into_raw();
@ -614,7 +653,7 @@ mod tests {
FFIResult::Err(error) => { FFIResult::Err(error) => {
assert_eq!( assert_eq!(
unsafe { CStr::from_ptr(error).to_string_lossy() }, 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] #[test]
fn test_get_interior_ref_list() { 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") let mock = mock("GET", "/v1/interior_ref_lists/1")
.with_status(201) .with_status(201)
.with_header("content-type", "application/json") .with_header("content-type", "application/octet-stream")
.with_body( .with_body(bincode::serialize(&example).unwrap())
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"
}"#,
)
.create(); .create();
let api_url = CString::new("url").unwrap().into_raw(); let api_url = CString::new("url").unwrap().into_raw();
@ -709,7 +746,7 @@ mod tests {
FFIResult::Err(error) => { FFIResult::Err(error) => {
assert_eq!( assert_eq!(
unsafe { CStr::from_ptr(error).to_string_lossy() }, 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] #[test]
fn test_get_interior_ref_list_by_shop_id() { 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") let mock = mock("GET", "/v1/shops/1/interior_ref_list")
.with_status(201) .with_status(201)
.with_header("content-type", "application/json") .with_header("content-type", "application/octet-stream")
.with_body( .with_body(bincode::serialize(&example).unwrap())
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"
}"#,
)
.create(); .create();
let api_url = CString::new("url").unwrap().into_raw(); let api_url = CString::new("url").unwrap().into_raw();
@ -805,7 +840,7 @@ mod tests {
FFIResult::Err(error) => { FFIResult::Err(error) => {
assert_eq!( assert_eq!(
unsafe { CStr::from_ptr(error).to_string_lossy() }, 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 cache;
mod client; mod client;
mod error;
mod interior_ref_list; mod interior_ref_list;
mod merchandise_list; mod merchandise_list;
mod owner; mod owner;

View File

@ -1,6 +1,7 @@
use std::{ffi::CStr, ffi::CString, os::raw::c_char, slice}; use std::{ffi::CStr, ffi::CString, os::raw::c_char, slice};
use anyhow::Result; use anyhow::Result;
use chrono::NaiveDateTime;
use reqwest::{StatusCode, Url}; use reqwest::{StatusCode, Url};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -11,13 +12,14 @@ 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, log_server_error, result::FFIResult, cache::update_file_caches, error::extract_error_from_response, log_server_error,
result::FFIResult,
}; };
#[derive(Serialize, Deserialize, Debug, Clone)] #[derive(Serialize, Deserialize, Debug, Clone)]
pub struct MerchandiseList { pub struct MerchandiseList {
pub id: Option<i32>,
pub shop_id: i32, pub shop_id: i32,
pub owner_id: Option<i32>,
pub form_list: Vec<Merchandise>, pub form_list: Vec<Merchandise>,
} }
@ -35,8 +37,8 @@ pub struct Merchandise {
impl MerchandiseList { impl MerchandiseList {
pub fn from_game(shop_id: i32, merch_records: &[RawMerchandise]) -> Self { pub fn from_game(shop_id: i32, merch_records: &[RawMerchandise]) -> Self {
Self { Self {
id: None,
shop_id, shop_id,
owner_id: None,
form_list: merch_records form_list: merch_records
.iter() .iter()
.map(|rec| Merchandise { .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)] #[derive(Debug)]
#[repr(C)] #[repr(C)]
pub struct RawMerchandise { pub struct RawMerchandise {
@ -99,7 +111,7 @@ pub extern "C" fn create_merchandise_list(
api_key: &str, api_key: &str,
shop_id: i32, shop_id: i32,
raw_merchandise_slice: &[RawMerchandise], raw_merchandise_slice: &[RawMerchandise],
) -> Result<MerchandiseList> { ) -> Result<SavedMerchandiseList> {
#[cfg(not(test))] #[cfg(not(test))]
let url = Url::parse(api_url)?.join("v1/merchandise_lists")?; let url = Url::parse(api_url)?.join("v1/merchandise_lists")?;
#[cfg(test)] #[cfg(test)]
@ -114,39 +126,34 @@ pub extern "C" fn create_merchandise_list(
let resp = client let resp = client
.post(url) .post(url)
.header("Api-Key", api_key) .header("Api-Key", api_key)
.json(&merchandise_list) .header("Content-Type", "application/octet-stream")
.body(bincode::serialize(&merchandise_list)?)
.send()?; .send()?;
info!("create merchandise_list response from api: {:?}", &resp); info!("create merchandise_list response from api: {:?}", &resp);
let cache_dir = file_cache_dir(api_url)?; let cache_dir = file_cache_dir(api_url)?;
let headers = resp.headers().clone(); let headers = resp.headers().clone();
let status = resp.status();
let bytes = resp.bytes()?; let bytes = resp.bytes()?;
let json: MerchandiseList = serde_json::from_slice(&bytes)?; if status.is_success() {
if let Some(id) = json.id { let saved_merchandise_list: SavedMerchandiseList = bincode::deserialize(&bytes)?;
let body_cache_path = cache_dir.join(format!("merchandise_list_{}.json", id)); let body_cache_path = cache_dir.join(format!(
let metadata_cache_path = "merchandise_list_{}.bin",
cache_dir.join(format!("merchandise_list_{}_metadata.json", id)); 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); 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) { match inner(&api_url, &api_key, shop_id, raw_merchandise_slice) {
Ok(merchandise_list) => { Ok(merchandise_list) => FFIResult::Ok(merchandise_list.id),
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)
}
}
Err(err) => { Err(err) => {
error!("create_merchandise_list failed. {}", err); error!("create_merchandise_list failed. {}", err);
// TODO: also need to drop this CString once C++ is done reading it // 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, api_key: &str,
shop_id: i32, shop_id: i32,
raw_merchandise_slice: &[RawMerchandise], raw_merchandise_slice: &[RawMerchandise],
) -> Result<MerchandiseList> { ) -> Result<SavedMerchandiseList> {
#[cfg(not(test))] #[cfg(not(test))]
let url = Url::parse(api_url)?.join(&format!("v1/shops/{}/merchandise_list", shop_id))?; let url = Url::parse(api_url)?.join(&format!("v1/shops/{}/merchandise_list", shop_id))?;
#[cfg(test)] #[cfg(test)]
@ -195,37 +202,29 @@ pub extern "C" fn update_merchandise_list(
let resp = client let resp = client
.patch(url) .patch(url)
.header("Api-Key", api_key) .header("Api-Key", api_key)
.json(&merchandise_list) .header("Content-Type", "application/octet-stream")
.body(bincode::serialize(&merchandise_list)?)
.send()?; .send()?;
info!("update merchandise_list response from api: {:?}", &resp); info!("update merchandise_list response from api: {:?}", &resp);
let cache_dir = file_cache_dir(api_url)?; 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 = let metadata_cache_path =
cache_dir.join(format!("shop_{}_merchandise_list_metadata.json", shop_id)); cache_dir.join(format!("shop_{}_merchandise_list_metadata.json", shop_id));
let headers = resp.headers().clone(); let headers = resp.headers().clone();
let status = resp.status();
let bytes = resp.bytes()?; let bytes = resp.bytes()?;
let json: MerchandiseList = serde_json::from_slice(&bytes)?; if status.is_success() {
let saved_merchandise_list = bincode::deserialize(&bytes)?;
update_file_caches(body_cache_path, metadata_cache_path, bytes, headers); update_file_caches(body_cache_path, metadata_cache_path, bytes, headers);
Ok(json) Ok(saved_merchandise_list)
} else {
Err(extract_error_from_response(status, &bytes))
}
} }
match inner(&api_url, &api_key, shop_id, raw_merchandise_slice) { match inner(&api_url, &api_key, shop_id, raw_merchandise_slice) {
Ok(merchandise_list) => { Ok(merchandise_list) => FFIResult::Ok(merchandise_list.id),
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)
}
}
Err(err) => { Err(err) => {
error!("update_merchandise_list failed. {}", err); error!("update_merchandise_list failed. {}", err);
// TODO: also need to drop this CString once C++ is done reading it // 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 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))] #[cfg(not(test))]
let url = let url =
Url::parse(api_url)?.join(&format!("v1/merchandise_lists/{}", merchandise_list_id))?; 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 client = reqwest::blocking::Client::new();
let cache_dir = file_cache_dir(api_url)?; let cache_dir = file_cache_dir(api_url)?;
let body_cache_path = 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!( let metadata_cache_path = cache_dir.join(format!(
"merchandise_list_{}_metadata.json", "merchandise_list_{}_metadata.json",
merchandise_list_id 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 // 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 Ok(metadata) = load_metadata_from_file_cache(&metadata_cache_path) {
if let Some(etag) = metadata.etag { if let Some(etag) = metadata.etag {
@ -283,9 +289,9 @@ pub extern "C" fn get_merchandise_list(
if resp.status().is_success() { if resp.status().is_success() {
let headers = resp.headers().clone(); let headers = resp.headers().clone();
let bytes = resp.bytes()?; 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); update_file_caches(body_cache_path, metadata_cache_path, bytes, headers);
Ok(json) Ok(saved_merchandise_list)
} else if resp.status() == StatusCode::NOT_MODIFIED { } else if resp.status() == StatusCode::NOT_MODIFIED {
from_file_cache(&body_cache_path) from_file_cache(&body_cache_path)
} else { } else {
@ -348,7 +354,7 @@ pub extern "C" fn get_merchandise_list_by_shop_id(
api_url, api_key, 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))] #[cfg(not(test))]
let url = Url::parse(api_url)?.join(&format!("v1/shops/{}/merchandise_list", shop_id))?; let url = Url::parse(api_url)?.join(&format!("v1/shops/{}/merchandise_list", shop_id))?;
#[cfg(test)] #[cfg(test)]
@ -358,10 +364,13 @@ pub extern "C" fn get_merchandise_list_by_shop_id(
let client = reqwest::blocking::Client::new(); let client = reqwest::blocking::Client::new();
let cache_dir = file_cache_dir(api_url)?; 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 = let metadata_cache_path =
cache_dir.join(format!("shop_{}_merchandise_list_metadata.json", shop_id)); 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 // 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 Ok(metadata) = load_metadata_from_file_cache(&metadata_cache_path) {
if let Some(etag) = metadata.etag { 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() { if resp.status().is_success() {
let headers = resp.headers().clone(); let headers = resp.headers().clone();
let bytes = resp.bytes()?; 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); update_file_caches(body_cache_path, metadata_cache_path, bytes, headers);
Ok(json) Ok(saved_merchandise_list)
} else if resp.status() == StatusCode::NOT_MODIFIED { } else if resp.status() == StatusCode::NOT_MODIFIED {
from_file_cache(&body_cache_path) from_file_cache(&body_cache_path)
} else { } else {
@ -435,14 +444,31 @@ mod tests {
use std::ffi::CString; use std::ffi::CString;
use super::*; use super::*;
use chrono::Utc;
use mockito::mock; use mockito::mock;
#[test] #[test]
fn test_create_merchandise_list() { 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") let mock = mock("POST", "/v1/merchandise_lists")
.with_status(201) .with_status(201)
.with_header("content-type", "application/json") .with_header("content-type", "application/octet-stream")
.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_body(bincode::serialize(&example).unwrap())
.create(); .create();
let api_url = CString::new("url").unwrap().into_raw(); let api_url = CString::new("url").unwrap().into_raw();
@ -500,7 +526,7 @@ mod tests {
FFIResult::Err(error) => { FFIResult::Err(error) => {
assert_eq!( assert_eq!(
unsafe { CStr::from_ptr(error).to_string_lossy() }, 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] #[test]
fn test_update_merchandise_list() { 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") let mock = mock("PATCH", "/v1/shops/1/merchandise_list")
.with_status(201) .with_status(201)
.with_header("content-type", "application/json") .with_header("content-type", "application/octet-stream")
.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_body(bincode::serialize(&example).unwrap())
.create(); .create();
let api_url = CString::new("url").unwrap().into_raw(); let api_url = CString::new("url").unwrap().into_raw();
@ -569,35 +611,33 @@ mod tests {
FFIResult::Err(error) => { FFIResult::Err(error) => {
assert_eq!( assert_eq!(
unsafe { CStr::from_ptr(error).to_string_lossy() }, unsafe { CStr::from_ptr(error).to_string_lossy() },
"expected value at line 1 column 1" "Server 500: Internal Server Error"
); );
} }
} }
} }
#[test] #[test]
fn test_get_merchandise_list() { 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") let mock = mock("GET", "/v1/merchandise_lists/1")
.with_status(201) .with_status(201)
.with_header("content-type", "application/json") .with_header("content-type", "application/octet-stream")
.with_body( .with_body(bincode::serialize(&example).unwrap())
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"
}"#,
)
.create(); .create();
let api_url = CString::new("url").unwrap().into_raw(); let api_url = CString::new("url").unwrap().into_raw();
@ -655,7 +695,7 @@ mod tests {
FFIResult::Err(error) => { FFIResult::Err(error) => {
assert_eq!( assert_eq!(
unsafe { CStr::from_ptr(error).to_string_lossy() }, 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] #[test]
fn test_get_merchandise_list_by_shop_id() { 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") let mock = mock("GET", "/v1/shops/1/merchandise_list")
.with_status(201) .with_status(201)
.with_header("content-type", "application/json") .with_header("content-type", "application/octet-stream")
.with_body( .with_body(bincode::serialize(&example).unwrap())
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"
}"#,
)
.create(); .create();
let api_url = CString::new("url").unwrap().into_raw(); let api_url = CString::new("url").unwrap().into_raw();
@ -743,7 +781,7 @@ mod tests {
FFIResult::Err(error) => { FFIResult::Err(error) => {
assert_eq!( assert_eq!(
unsafe { CStr::from_ptr(error).to_string_lossy() }, 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 std::{ffi::CStr, ffi::CString, os::raw::c_char};
use anyhow::{anyhow, Result}; use anyhow::Result;
use chrono::NaiveDateTime;
use reqwest::Url; use reqwest::Url;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -9,23 +10,31 @@ 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::{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)] #[derive(Serialize, Deserialize, Debug)]
pub struct Owner { pub struct Owner {
pub id: Option<i32>,
pub name: String, pub name: String,
pub api_key: Option<String>, pub mod_version: i32,
pub mod_version: u32, }
#[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 { 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 { Self {
id: None,
name: name.to_string(), 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 struct RawOwner {
pub id: i32, pub id: i32,
pub name: *const c_char, 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] #[no_mangle]
@ -43,7 +62,7 @@ pub extern "C" fn create_owner(
api_url: *const c_char, api_url: *const c_char,
api_key: *const c_char, api_key: *const c_char,
name: *const c_char, name: *const c_char,
mod_version: u32, mod_version: i32,
) -> FFIResult<RawOwner> { ) -> FFIResult<RawOwner> {
let api_url = unsafe { CStr::from_ptr(api_url) }.to_string_lossy(); let api_url = unsafe { CStr::from_ptr(api_url) }.to_string_lossy();
let api_key = unsafe { CStr::from_ptr(api_key) }.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 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))] #[cfg(not(test))]
let url = Url::parse(api_url)?.join("v1/owners")?; let url = Url::parse(api_url)?.join("v1/owners")?;
#[cfg(test)] #[cfg(test)]
let url = Url::parse(&mockito::server_url())?.join("v1/owners")?; 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); info!("created owner from game: {:?}", &owner);
if let Some(api_key) = &owner.api_key {
let client = reqwest::blocking::Client::new(); let client = reqwest::blocking::Client::new();
let resp = client let resp = client
.post(url) .post(url)
.header("Api-Key", api_key.clone()) .header("Api-Key", api_key.clone())
.json(&owner) .header("Content-Type", "application/octet-stream")
.body(bincode::serialize(&owner)?)
.send()?; .send()?;
info!("create owner response from api: {:?}", &resp); info!("create owner response from api: {:?}", &resp);
let cache_dir = file_cache_dir(api_url)?; let cache_dir = file_cache_dir(api_url)?;
let headers = resp.headers().clone(); let headers = resp.headers().clone();
let status = resp.status();
let bytes = resp.bytes()?; let bytes = resp.bytes()?;
let json: Owner = serde_json::from_slice(&bytes)?; if status.is_success() {
if let Some(id) = json.id { let saved_owner: SavedOwner = bincode::deserialize(&bytes)?;
let body_cache_path = cache_dir.join(format!("owner_{}.json", id)); let body_cache_path = cache_dir.join(format!("owner_{}.bin", saved_owner.id));
let metadata_cache_path = cache_dir.join(format!("owner_{}_metadata.json", 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); update_file_caches(body_cache_path, metadata_cache_path, bytes, headers);
} Ok(saved_owner)
Ok(json)
} else { } else {
Err(anyhow!("api-key not defined")) Err(extract_error_from_response(status, &bytes))
} }
} }
match inner(&api_url, &api_key, &name, mod_version) { match inner(&api_url, &api_key, &name, mod_version) {
Ok(owner) => { Ok(owner) => {
info!("create_owner successful"); info!("create_owner successful");
if let Some(id) = owner.id { FFIResult::Ok(RawOwner::from(owner))
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)
}
} }
Err(err) => { Err(err) => {
error!("create_owner failed. {}", err); error!("create_owner failed. {}", err);
@ -118,9 +125,9 @@ pub extern "C" fn create_owner(
pub extern "C" fn update_owner( pub extern "C" fn update_owner(
api_url: *const c_char, api_url: *const c_char,
api_key: *const c_char, api_key: *const c_char,
id: u32, id: i32,
name: *const c_char, name: *const c_char,
mod_version: u32, mod_version: i32,
) -> FFIResult<RawOwner> { ) -> FFIResult<RawOwner> {
let api_url = unsafe { CStr::from_ptr(api_url) }.to_string_lossy(); let api_url = unsafe { CStr::from_ptr(api_url) }.to_string_lossy();
let api_key = unsafe { CStr::from_ptr(api_key) }.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 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))] #[cfg(not(test))]
let url = Url::parse(api_url)?.join(&format!("v1/owners/{}", id))?; let url = Url::parse(api_url)?.join(&format!("v1/owners/{}", id))?;
#[cfg(test)] #[cfg(test)]
let url = Url::parse(&mockito::server_url())?.join(&format!("v1/owners/{}", id))?; 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); info!("created owner from game: {:?}", &owner);
if let Some(api_key) = &owner.api_key {
let client = reqwest::blocking::Client::new(); let client = reqwest::blocking::Client::new();
let resp = client let resp = client
.patch(url) .patch(url)
.header("Api-Key", api_key.clone()) .header("Api-Key", api_key.clone())
.json(&owner) .header("Content-Type", "application/octet-stream")
.body(bincode::serialize(&owner)?)
.send()?; .send()?;
info!("update owner response from api: {:?}", &resp); info!("update owner response from api: {:?}", &resp);
let cache_dir = file_cache_dir(api_url)?; let cache_dir = file_cache_dir(api_url)?;
let body_cache_path = cache_dir.join(format!("owner_{}.json", id)); let body_cache_path = cache_dir.join(format!("owner_{}.bin", id));
let metadata_cache_path = cache_dir.join(format!("owner_{}_metadata.json", id)); let metadata_cache_path = cache_dir.join(format!("owner_{}_metadata.json", id));
let headers = resp.headers().clone(); let headers = resp.headers().clone();
let status = resp.status();
let bytes = resp.bytes()?; let bytes = resp.bytes()?;
let json: Owner = serde_json::from_slice(&bytes)?; if status.is_success() {
let saved_owner: SavedOwner = bincode::deserialize(&bytes)?;
update_file_caches(body_cache_path, metadata_cache_path, bytes, headers); update_file_caches(body_cache_path, metadata_cache_path, bytes, headers);
Ok(json) Ok(saved_owner)
} else { } else {
Err(anyhow!("api-key not defined")) Err(extract_error_from_response(status, &bytes))
} }
} }
match inner(&api_url, &api_key, id, &name, mod_version) { match inner(&api_url, &api_key, id, &name, mod_version) {
Ok(owner) => { Ok(owner) => {
info!("update_owner successful"); info!("update_owner successful");
if let Some(id) = owner.id { FFIResult::Ok(RawOwner::from(owner))
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)
}
} }
Err(err) => { Err(err) => {
error!("update_owner failed. {}", err); error!("update_owner failed. {}", err);
@ -194,14 +196,22 @@ mod tests {
use std::ffi::CString; use std::ffi::CString;
use super::*; use super::*;
use chrono::Utc;
use mockito::mock; use mockito::mock;
#[test] #[test]
fn test_create_owner() { 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") let mock = mock("POST", "/v1/owners")
.with_status(201) .with_status(201)
.with_header("content-type", "application/json") .with_header("content-type", "application/octet-stream")
.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_body(bincode::serialize(&example).unwrap())
.create(); .create();
let api_url = CString::new("url").unwrap().into_raw(); let api_url = CString::new("url").unwrap().into_raw();
@ -245,7 +255,7 @@ mod tests {
FFIResult::Err(error) => { FFIResult::Err(error) => {
assert_eq!( assert_eq!(
unsafe { CStr::from_ptr(error).to_string_lossy() }, 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] #[test]
fn test_update_owner() { 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") let mock = mock("PATCH", "/v1/owners/1")
.with_status(201) .with_status(201)
.with_header("content-type", "application/json") .with_header("content-type", "application/octet-stream")
.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_body(bincode::serialize(&example).unwrap())
.create(); .create();
let api_url = CString::new("url").unwrap().into_raw(); let api_url = CString::new("url").unwrap().into_raw();
@ -300,7 +317,7 @@ mod tests {
FFIResult::Err(error) => { FFIResult::Err(error) => {
assert_eq!( assert_eq!(
unsafe { CStr::from_ptr(error).to_string_lossy() }, 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 reqwest::{StatusCode, Url};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -11,38 +12,35 @@ 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, log_server_error, result::FFIResult, cache::update_file_caches, error::extract_error_from_response, log_server_error,
result::FFIResult,
}; };
#[derive(Serialize, Deserialize, Debug)] #[derive(Serialize, Deserialize, Debug)]
pub struct Shop { pub struct Shop {
pub id: Option<i32>,
pub name: String, pub name: String,
pub description: String, pub owner_id: Option<i32>,
pub description: Option<String>,
} }
impl Shop { impl Shop {
pub fn from_game(name: &str, description: &str) -> Self { pub fn from_game(name: &str, description: &str) -> Self {
Self { Self {
id: None,
name: name.to_string(), name: name.to_string(),
description: description.to_string(), owner_id: None,
description: Some(description.to_string()),
} }
} }
} }
impl From<RawShop> for Shop { #[derive(Serialize, Deserialize, Debug)]
fn from(raw_shop: RawShop) -> Self { pub struct SavedShop {
Self { pub id: i32,
id: Some(raw_shop.id), pub name: String,
name: unsafe { CStr::from_ptr(raw_shop.name) } pub owner_id: i32,
.to_string_lossy() pub description: Option<String>,
.to_string(), pub created_at: NaiveDateTime,
description: unsafe { CStr::from_ptr(raw_shop.description) } pub updated_at: NaiveDateTime,
.to_string_lossy()
.to_string(),
}
}
} }
#[derive(Debug)] #[derive(Debug)]
@ -53,20 +51,14 @@ pub struct RawShop {
pub description: *const c_char, pub description: *const c_char,
} }
impl TryFrom<Shop> for RawShop { impl From<SavedShop> for RawShop {
type Error = anyhow::Error; fn from(shop: SavedShop) -> Self {
Self {
fn try_from(shop: Shop) -> Result<Self> { id: shop.id,
if let Some(id) = shop.id {
Ok(Self {
id,
name: CString::new(shop.name).unwrap_or_default().into_raw(), name: CString::new(shop.name).unwrap_or_default().into_raw(),
description: CString::new(shop.description) description: CString::new(shop.description.unwrap_or_else(|| "".to_string()))
.unwrap_or_default() .unwrap_or_default()
.into_raw(), .into_raw(),
})
} else {
Err(anyhow!("shop.id is None"))
} }
} }
} }
@ -95,7 +87,7 @@ pub extern "C" fn create_shop(
api_url, api_key, name, description 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))] #[cfg(not(test))]
let url = Url::parse(api_url)?.join("v1/shops")?; let url = Url::parse(api_url)?.join("v1/shops")?;
#[cfg(test)] #[cfg(test)]
@ -107,35 +99,31 @@ pub extern "C" fn create_shop(
let resp = client let resp = client
.post(url) .post(url)
.header("Api-Key", api_key) .header("Api-Key", api_key)
.json(&shop) .header("Content-Type", "application/octet-stream")
.body(bincode::serialize(&shop)?)
.send()?; .send()?;
info!("create shop response from api: {:?}", &resp); info!("create shop response from api: {:?}", &resp);
let cache_dir = file_cache_dir(api_url)?; let cache_dir = file_cache_dir(api_url)?;
let headers = resp.headers().clone(); let headers = resp.headers().clone();
let status = resp.status();
let bytes = resp.bytes()?; let bytes = resp.bytes()?;
let json: Shop = serde_json::from_slice(&bytes)?; if status.is_success() {
if let Some(id) = json.id { let saved_shop: SavedShop = bincode::deserialize(&bytes)?;
let body_cache_path = cache_dir.join(format!("shop_{}.json", id)); let body_cache_path = cache_dir.join(format!("shop_{}.bin", saved_shop.id));
let metadata_cache_path = cache_dir.join(format!("shop_{}_metadata.json", 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); 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) { match inner(&api_url, &api_key, &name, &description) {
Ok(shop) => { Ok(shop) => {
info!("create_shop successful"); info!("create_shop successful");
if let Ok(raw_shop) = RawShop::try_from(shop) { FFIResult::Ok(RawShop::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)
}
} }
Err(err) => { Err(err) => {
error!("create_shop failed. {}", err); error!("create_shop failed. {}", err);
@ -166,7 +154,13 @@ pub extern "C" fn update_shop(
api_url, api_key, name, description 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))] #[cfg(not(test))]
let url = Url::parse(api_url)?.join(&format!("v1/shops/{}", id))?; let url = Url::parse(api_url)?.join(&format!("v1/shops/{}", id))?;
#[cfg(test)] #[cfg(test)]
@ -178,33 +172,30 @@ pub extern "C" fn update_shop(
let resp = client let resp = client
.patch(url) .patch(url)
.header("Api-Key", api_key) .header("Api-Key", api_key)
.json(&shop) .header("Content-Type", "application/octet-stream")
.body(bincode::serialize(&shop)?)
.send()?; .send()?;
info!("update shop response from api: {:?}", &resp); info!("update shop response from api: {:?}", &resp);
let cache_dir = file_cache_dir(api_url)?; 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 metadata_cache_path = cache_dir.join(format!("shop_{}_metadata.json", id));
let headers = resp.headers().clone(); let headers = resp.headers().clone();
let status = resp.status();
let bytes = resp.bytes()?; let bytes = resp.bytes()?;
let json: Shop = serde_json::from_slice(&bytes)?; if status.is_success() {
let saved_shop: SavedShop = bincode::deserialize(&bytes)?;
update_file_caches(body_cache_path, metadata_cache_path, bytes, headers); update_file_caches(body_cache_path, metadata_cache_path, bytes, headers);
Ok(json) Ok(saved_shop)
} else {
Err(extract_error_from_response(status, &bytes))
}
} }
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 Ok(raw_shop) = RawShop::try_from(shop) { FFIResult::Ok(RawShop::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)
}
} }
Err(err) => { Err(err) => {
error!("update_shop failed. {}", err); error!("update_shop failed. {}", err);
@ -231,7 +222,7 @@ pub extern "C" fn get_shop(
api_url, api_key, shop_id 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))] #[cfg(not(test))]
let url = Url::parse(api_url)?.join(&format!("v1/shops/{}", shop_id))?; let url = Url::parse(api_url)?.join(&format!("v1/shops/{}", shop_id))?;
#[cfg(test)] #[cfg(test)]
@ -240,9 +231,12 @@ pub extern "C" fn get_shop(
let client = reqwest::blocking::Client::new(); let client = reqwest::blocking::Client::new();
let cache_dir = file_cache_dir(api_url)?; 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 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 // 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 Ok(metadata) = load_metadata_from_file_cache(&metadata_cache_path) {
if let Some(etag) = metadata.etag { if let Some(etag) = metadata.etag {
@ -256,9 +250,9 @@ pub extern "C" fn get_shop(
if resp.status().is_success() { if resp.status().is_success() {
let headers = resp.headers().clone(); let headers = resp.headers().clone();
let bytes = resp.bytes()?; 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); update_file_caches(body_cache_path, metadata_cache_path, bytes, headers);
Ok(json) Ok(saved_shop)
} else if resp.status() == StatusCode::NOT_MODIFIED { } else if resp.status() == StatusCode::NOT_MODIFIED {
from_file_cache(&body_cache_path) from_file_cache(&body_cache_path)
} else { } else {
@ -276,16 +270,7 @@ 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.
if let Ok(raw_shop) = RawShop::try_from(shop) { FFIResult::Ok(RawShop::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)
}
} }
Err(err) => { Err(err) => {
error!("get_shop_list failed. {}", 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(); let api_key = unsafe { CStr::from_ptr(api_key) }.to_string_lossy();
info!("list_shops api_url: {:?}, api_key: {:?}", api_url, api_key); 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))] #[cfg(not(test))]
let url = Url::parse(api_url)?.join("v1/shops?limit=128")?; let url = Url::parse(api_url)?.join("v1/shops?limit=128")?;
#[cfg(test)] #[cfg(test)]
@ -316,9 +301,12 @@ pub extern "C" fn list_shops(
let client = reqwest::blocking::Client::new(); let client = reqwest::blocking::Client::new();
let cache_dir = file_cache_dir(api_url)?; 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 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 // 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 Ok(metadata) = load_metadata_from_file_cache(&metadata_cache_path) {
if let Some(etag) = metadata.etag { if let Some(etag) = metadata.etag {
@ -332,9 +320,9 @@ pub extern "C" fn list_shops(
if resp.status().is_success() { if resp.status().is_success() {
let headers = resp.headers().clone(); let headers = resp.headers().clone();
let bytes = resp.bytes()?; 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); update_file_caches(body_cache_path, metadata_cache_path, bytes, headers);
Ok(json) Ok(saved_shops)
} else if resp.status() == StatusCode::NOT_MODIFIED { } else if resp.status() == StatusCode::NOT_MODIFIED {
from_file_cache(&body_cache_path) from_file_cache(&body_cache_path)
} else { } else {
@ -352,20 +340,9 @@ pub extern "C" fn list_shops(
match inner(&api_url, &api_key) { match inner(&api_url, &api_key) {
Ok(shops) => { 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. // 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>> = let raw_shops: Vec<RawShop> = shops.into_iter().map(RawShop::from).collect();
shops.into_iter().map(RawShop::try_from).collect();
if let Ok(raw_shops) = raw_shops {
let (ptr, len, cap) = raw_shops.into_raw_parts(); let (ptr, len, cap) = raw_shops.into_raw_parts();
FFIResult::Ok(RawShopVec { ptr, len, cap }) 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) => { Err(err) => {
error!("list_shops failed. {}", err); error!("list_shops failed. {}", err);
@ -383,14 +360,23 @@ mod tests {
use std::{ffi::CString, slice}; use std::{ffi::CString, slice};
use super::*; use super::*;
use chrono::Utc;
use mockito::mock; use mockito::mock;
#[test] #[test]
fn test_create_shop() { 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") let mock = mock("POST", "/v1/shops")
.with_status(201) .with_status(201)
.with_header("content-type", "application/json") .with_header("content-type", "application/octet-stream")
.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_body(bincode::serialize(&example).unwrap())
.create(); .create();
let api_url = CString::new("url").unwrap().into_raw(); let api_url = CString::new("url").unwrap().into_raw();
@ -435,7 +421,7 @@ mod tests {
FFIResult::Err(error) => { FFIResult::Err(error) => {
assert_eq!( assert_eq!(
unsafe { CStr::from_ptr(error).to_string_lossy() }, 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] #[test]
fn test_update_shop() { 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") let mock = mock("PATCH", "/v1/shops/1")
.with_status(201) .with_status(201)
.with_header("content-type", "application/json") .with_header("content-type", "application/octet-stream")
.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_body(bincode::serialize(&example).unwrap())
.create(); .create();
let api_url = CString::new("url").unwrap().into_raw(); let api_url = CString::new("url").unwrap().into_raw();
@ -491,7 +485,7 @@ mod tests {
FFIResult::Err(error) => { FFIResult::Err(error) => {
assert_eq!( assert_eq!(
unsafe { CStr::from_ptr(error).to_string_lossy() }, 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] #[test]
fn test_get_shop() { 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") let mock = mock("GET", "/v1/shops/1")
.with_status(201) .with_status(201)
.with_header("content-type", "application/json") .with_header("content-type", "application/octet-stream")
.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_body(bincode::serialize(&example).unwrap())
.create(); .create();
let api_url = CString::new("url").unwrap().into_raw(); let api_url = CString::new("url").unwrap().into_raw();
@ -543,7 +545,7 @@ mod tests {
FFIResult::Err(error) => { FFIResult::Err(error) => {
assert_eq!( assert_eq!(
unsafe { CStr::from_ptr(error).to_string_lossy() }, 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] #[test]
fn test_list_shops() { 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") let mock = mock("GET", "/v1/shops?limit=128")
.with_status(201) .with_status(201)
.with_header("content-type", "application/json") .with_header("content-type", "application/octet-stream")
.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_body(bincode::serialize(&example).unwrap())
.create(); .create();
let api_url = CString::new("url").unwrap().into_raw(); let api_url = CString::new("url").unwrap().into_raw();
@ -601,7 +611,7 @@ mod tests {
FFIResult::Err(error) => { FFIResult::Err(error) => {
assert_eq!( assert_eq!(
unsafe { CStr::from_ptr(error).to_string_lossy() }, 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 anyhow::Result;
use http_api_problem::HttpApiProblem; use chrono::NaiveDateTime;
use reqwest::Url; use reqwest::Url;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -11,52 +11,41 @@ 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::update_file_caches, log_server_error, cache::file_cache_dir, cache::update_file_caches, error::extract_error_from_response,
result::FFIResult, result::FFIResult,
}; };
#[derive(Serialize, Deserialize, Debug)] #[derive(Serialize, Deserialize, Debug)]
pub struct Transaction { pub struct Transaction {
pub id: Option<u32>, pub id: Option<i32>,
pub shop_id: u32, pub shop_id: i32,
pub mod_name: String, pub mod_name: String,
pub local_form_id: u32, pub local_form_id: i32,
pub name: String, pub name: String,
pub form_type: u32, pub form_type: i32,
pub is_food: bool, pub is_food: bool,
pub price: u32, pub price: i32,
pub is_sell: bool, pub is_sell: bool,
pub quantity: u32, pub quantity: i32,
pub amount: u32, pub amount: i32,
} }
impl Transaction { #[derive(Serialize, Deserialize, Debug)]
pub fn from_game( pub struct SavedTransaction {
shop_id: u32, pub id: i32,
mod_name: &str, pub owner_id: i32,
local_form_id: u32, pub shop_id: i32,
name: &str, pub mod_name: String,
form_type: u32, pub local_form_id: i32,
is_food: bool, pub name: String,
price: u32, pub form_type: i32,
is_sell: bool, pub is_food: bool,
quantity: u32, pub price: i32,
amount: u32, pub is_sell: bool,
) -> Self { pub quantity: i32,
Self { pub amount: i32,
id: None, pub created_at: NaiveDateTime,
shop_id, pub updated_at: NaiveDateTime,
mod_name: mod_name.to_string(),
local_form_id,
name: name.to_string(),
form_type,
is_food,
price,
is_sell,
quantity,
amount,
}
}
} }
impl From<RawTransaction> for Transaction { impl From<RawTransaction> for Transaction {
@ -87,23 +76,23 @@ impl From<RawTransaction> for Transaction {
#[derive(Debug)] #[derive(Debug)]
#[repr(C)] #[repr(C)]
pub struct RawTransaction { pub struct RawTransaction {
pub id: u32, pub id: i32,
pub shop_id: u32, pub shop_id: i32,
pub mod_name: *const c_char, pub mod_name: *const c_char,
pub local_form_id: u32, pub local_form_id: i32,
pub name: *const c_char, pub name: *const c_char,
pub form_type: u32, pub form_type: i32,
pub is_food: bool, pub is_food: bool,
pub price: u32, pub price: i32,
pub is_sell: bool, pub is_sell: bool,
pub quantity: u32, pub quantity: i32,
pub amount: u32, pub amount: i32,
} }
impl From<Transaction> for RawTransaction { impl From<SavedTransaction> for RawTransaction {
fn from(transaction: Transaction) -> Self { fn from(transaction: SavedTransaction) -> Self {
Self { Self {
id: transaction.id.unwrap_or(0), id: transaction.id,
shop_id: transaction.shop_id, shop_id: transaction.shop_id,
mod_name: CString::new(transaction.mod_name) mod_name: CString::new(transaction.mod_name)
.unwrap_or_default() .unwrap_or_default()
@ -144,7 +133,7 @@ pub extern "C" fn create_transaction(
api_url, api_key, 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))] #[cfg(not(test))]
let url = Url::parse(api_url)?.join("v1/transactions")?; let url = Url::parse(api_url)?.join("v1/transactions")?;
#[cfg(test)] #[cfg(test)]
@ -154,7 +143,8 @@ pub extern "C" fn create_transaction(
let resp = client let resp = client
.post(url) .post(url)
.header("Api-Key", api_key) .header("Api-Key", api_key)
.json(&transaction) .header("Content-Type", "application/octet-stream")
.body(bincode::serialize(&transaction)?)
.send()?; .send()?;
info!("create transaction response from api: {:?}", &resp); info!("create transaction response from api: {:?}", &resp);
@ -163,55 +153,22 @@ pub extern "C" fn create_transaction(
let status = resp.status(); let status = resp.status();
let bytes = resp.bytes()?; let bytes = resp.bytes()?;
if status.is_success() { if status.is_success() {
let json: Transaction = serde_json::from_slice(&bytes)?; let saved_transaction: SavedTransaction = bincode::deserialize(&bytes)?;
if let Some(id) = json.id { let body_cache_path =
let body_cache_path = cache_dir.join(format!("transaction_{}.json", id)); cache_dir.join(format!("transaction_{}.bin", saved_transaction.id));
let metadata_cache_path = let metadata_cache_path = cache_dir.join(format!(
cache_dir.join(format!("transaction_{}_metadata.json", id)); "transaction_{}_metadata.json",
saved_transaction.id
));
update_file_caches(body_cache_path, metadata_cache_path, bytes, headers); update_file_caches(body_cache_path, metadata_cache_path, bytes, headers);
} Ok(saved_transaction)
Ok(json)
} else { } else {
// TODO: abstract this away into a separate helper Err(extract_error_from_response(status, &bytes))
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)))
}
}
} }
} }
match inner(&api_url, &api_key, transaction) { match inner(&api_url, &api_key, transaction) {
Ok(transaction) => { Ok(transaction) => FFIResult::Ok(RawTransaction::from(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)
}
}
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 // TODO: also need to drop this CString once C++ is done reading it
@ -228,31 +185,31 @@ mod tests {
use std::ffi::CString; use std::ffi::CString;
use super::*; use super::*;
use chrono::Utc;
use mockito::mock; use mockito::mock;
#[test] #[test]
fn test_create_transaction() { 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") let mock = mock("POST", "/v1/transactions")
.with_status(201) .with_status(201)
.with_header("content-type", "application/json") .with_header("content-type", "application/octet-stream")
.with_body( .with_body(bincode::serialize(&example).unwrap())
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"
}"#,
)
.create(); .create();
let api_url = CString::new("url").unwrap().into_raw(); let api_url = CString::new("url").unwrap().into_raw();