2020-11-07 05:34:09 +00:00
|
|
|
use std::{convert::TryFrom, ffi::CStr, ffi::CString, os::raw::c_char, slice, str, thread};
|
2020-10-30 04:25:17 +00:00
|
|
|
|
|
|
|
use anyhow::{anyhow, Result};
|
2020-11-02 06:38:29 +00:00
|
|
|
use http_api_problem::HttpApiProblem;
|
2020-10-30 04:25:17 +00:00
|
|
|
use reqwest::Url;
|
|
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
|
|
|
|
#[cfg(not(test))]
|
|
|
|
use log::{error, info};
|
|
|
|
#[cfg(test)]
|
|
|
|
use std::{println as info, println as error};
|
|
|
|
|
|
|
|
use crate::{
|
2020-11-07 05:34:09 +00:00
|
|
|
cache::file_cache_dir, cache::from_file_cache, cache::update_file_caches, log_server_error,
|
|
|
|
result::FFIResult,
|
2020-10-30 04:25:17 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
#[derive(Serialize, Deserialize, Debug)]
|
|
|
|
pub struct Transaction {
|
|
|
|
pub id: Option<u32>,
|
|
|
|
pub shop_id: u32,
|
|
|
|
pub mod_name: String,
|
|
|
|
pub local_form_id: u32,
|
2020-11-01 02:43:14 +00:00
|
|
|
pub name: String,
|
|
|
|
pub form_type: u32,
|
|
|
|
pub is_food: bool,
|
|
|
|
pub price: u32,
|
2020-10-30 04:25:17 +00:00
|
|
|
pub is_sell: bool,
|
|
|
|
pub quantity: u32,
|
|
|
|
pub amount: u32,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl Transaction {
|
|
|
|
pub fn from_game(
|
|
|
|
shop_id: u32,
|
|
|
|
mod_name: &str,
|
|
|
|
local_form_id: u32,
|
2020-11-01 02:43:14 +00:00
|
|
|
name: &str,
|
|
|
|
form_type: u32,
|
|
|
|
is_food: bool,
|
|
|
|
price: u32,
|
2020-10-30 04:25:17 +00:00
|
|
|
is_sell: bool,
|
|
|
|
quantity: u32,
|
|
|
|
amount: u32,
|
|
|
|
) -> Self {
|
|
|
|
Self {
|
|
|
|
id: None,
|
|
|
|
shop_id,
|
|
|
|
mod_name: mod_name.to_string(),
|
|
|
|
local_form_id,
|
2020-11-01 02:43:14 +00:00
|
|
|
name: name.to_string(),
|
|
|
|
form_type,
|
|
|
|
is_food,
|
|
|
|
price,
|
2020-10-30 04:25:17 +00:00
|
|
|
is_sell,
|
|
|
|
quantity,
|
|
|
|
amount,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl From<RawTransaction> for Transaction {
|
|
|
|
fn from(raw_transaction: RawTransaction) -> Self {
|
|
|
|
Self {
|
2020-11-02 06:38:29 +00:00
|
|
|
id: match raw_transaction.id {
|
|
|
|
0 => None,
|
|
|
|
_ => Some(raw_transaction.id),
|
|
|
|
},
|
2020-10-30 04:25:17 +00:00
|
|
|
shop_id: raw_transaction.shop_id,
|
|
|
|
mod_name: unsafe { CStr::from_ptr(raw_transaction.mod_name) }
|
|
|
|
.to_string_lossy()
|
|
|
|
.to_string(),
|
|
|
|
local_form_id: raw_transaction.local_form_id,
|
2020-11-01 02:43:14 +00:00
|
|
|
name: unsafe { CStr::from_ptr(raw_transaction.name) }
|
|
|
|
.to_string_lossy()
|
|
|
|
.to_string(),
|
|
|
|
form_type: raw_transaction.form_type,
|
|
|
|
is_food: raw_transaction.is_food,
|
|
|
|
price: raw_transaction.price,
|
2020-10-30 04:25:17 +00:00
|
|
|
is_sell: raw_transaction.is_sell,
|
|
|
|
quantity: raw_transaction.quantity,
|
|
|
|
amount: raw_transaction.amount,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Debug)]
|
|
|
|
#[repr(C)]
|
|
|
|
pub struct RawTransaction {
|
2020-11-02 06:38:29 +00:00
|
|
|
pub id: u32,
|
2020-10-30 04:25:17 +00:00
|
|
|
pub shop_id: u32,
|
|
|
|
pub mod_name: *const c_char,
|
|
|
|
pub local_form_id: u32,
|
2020-11-01 02:43:14 +00:00
|
|
|
pub name: *const c_char,
|
|
|
|
pub form_type: u32,
|
|
|
|
pub is_food: bool,
|
|
|
|
pub price: u32,
|
2020-10-30 04:25:17 +00:00
|
|
|
pub is_sell: bool,
|
|
|
|
pub quantity: u32,
|
|
|
|
pub amount: u32,
|
|
|
|
}
|
|
|
|
|
2020-11-01 02:43:14 +00:00
|
|
|
impl From<Transaction> for RawTransaction {
|
|
|
|
fn from(transaction: Transaction) -> Self {
|
|
|
|
Self {
|
2020-11-02 06:38:29 +00:00
|
|
|
id: transaction.id.unwrap_or(0),
|
2020-11-01 02:43:14 +00:00
|
|
|
shop_id: transaction.shop_id,
|
|
|
|
mod_name: CString::new(transaction.mod_name)
|
|
|
|
.unwrap_or_default()
|
|
|
|
.into_raw(),
|
|
|
|
local_form_id: transaction.local_form_id,
|
|
|
|
name: CString::new(transaction.name)
|
|
|
|
.unwrap_or_default()
|
|
|
|
.into_raw(),
|
|
|
|
form_type: transaction.form_type,
|
|
|
|
is_food: transaction.is_food,
|
|
|
|
price: transaction.price,
|
|
|
|
is_sell: transaction.is_sell,
|
|
|
|
quantity: transaction.quantity,
|
|
|
|
amount: transaction.amount,
|
2020-10-30 04:25:17 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Debug)]
|
|
|
|
#[repr(C)]
|
|
|
|
pub struct RawTransactionVec {
|
|
|
|
pub ptr: *mut RawTransaction,
|
|
|
|
pub len: usize,
|
|
|
|
pub cap: usize,
|
|
|
|
}
|
|
|
|
|
|
|
|
#[no_mangle]
|
|
|
|
pub extern "C" fn create_transaction(
|
|
|
|
api_url: *const c_char,
|
|
|
|
api_key: *const c_char,
|
2020-11-01 02:43:14 +00:00
|
|
|
raw_transaction: RawTransaction,
|
2020-10-30 04:25:17 +00:00
|
|
|
) -> FFIResult<RawTransaction> {
|
|
|
|
let api_url = unsafe { CStr::from_ptr(api_url) }.to_string_lossy();
|
|
|
|
let api_key = unsafe { CStr::from_ptr(api_key) }.to_string_lossy();
|
2020-11-01 02:43:14 +00:00
|
|
|
let transaction = Transaction::from(raw_transaction);
|
2020-10-30 04:25:17 +00:00
|
|
|
info!(
|
|
|
|
"create_transaction api_url: {:?}, api_key: {:?}, transaction: {:?}",
|
|
|
|
api_url, api_key, transaction
|
|
|
|
);
|
|
|
|
|
|
|
|
fn inner(api_url: &str, api_key: &str, transaction: Transaction) -> Result<Transaction> {
|
|
|
|
#[cfg(not(test))]
|
|
|
|
let url = Url::parse(api_url)?.join("v1/transactions")?;
|
|
|
|
#[cfg(test)]
|
|
|
|
let url = Url::parse(&mockito::server_url())?.join("v1/transactions")?;
|
|
|
|
|
|
|
|
let client = reqwest::blocking::Client::new();
|
|
|
|
let resp = client
|
|
|
|
.post(url)
|
|
|
|
.header("Api-Key", api_key)
|
|
|
|
.json(&transaction)
|
|
|
|
.send()?;
|
|
|
|
info!("create transaction response from api: {:?}", &resp);
|
2020-11-06 00:19:17 +00:00
|
|
|
|
|
|
|
let cache_dir = file_cache_dir(api_url)?;
|
|
|
|
let headers = resp.headers().clone();
|
2020-11-02 06:38:29 +00:00
|
|
|
let status = resp.status();
|
2020-10-30 04:25:17 +00:00
|
|
|
let bytes = resp.bytes()?;
|
2020-11-02 06:38:29 +00:00
|
|
|
if status.is_success() {
|
|
|
|
let json: Transaction = serde_json::from_slice(&bytes)?;
|
|
|
|
if let Some(id) = json.id {
|
2020-11-07 05:34:09 +00:00
|
|
|
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);
|
2020-11-02 06:38:29 +00:00
|
|
|
}
|
|
|
|
Ok(json)
|
|
|
|
} else {
|
2020-11-06 00:19:17 +00:00
|
|
|
// TODO: abstract this away into a separate helper
|
2020-11-02 06:38:29 +00:00
|
|
|
match serde_json::from_slice::<HttpApiProblem>(&bytes) {
|
|
|
|
Ok(api_problem) => {
|
|
|
|
let detail = api_problem.detail.unwrap_or("".to_string());
|
2020-11-06 00:19:17 +00:00
|
|
|
error!(
|
|
|
|
"Server {}: {}. {}",
|
|
|
|
status.as_u16(),
|
|
|
|
api_problem.title,
|
|
|
|
detail
|
|
|
|
);
|
2020-11-02 06:38:29 +00:00
|
|
|
Err(anyhow!(format!(
|
2020-11-06 00:19:17 +00:00
|
|
|
"Server {}: {}. {}",
|
|
|
|
status.as_u16(),
|
|
|
|
api_problem.title,
|
|
|
|
detail
|
2020-11-02 06:38:29 +00:00
|
|
|
)))
|
|
|
|
}
|
|
|
|
Err(_) => {
|
|
|
|
let detail = str::from_utf8(&bytes).unwrap_or("unknown");
|
2020-11-06 00:19:17 +00:00
|
|
|
error!("Server {}: {}", status.as_u16(), detail);
|
|
|
|
Err(anyhow!(format!("Server {}: {}", status.as_u16(), detail)))
|
2020-11-02 06:38:29 +00:00
|
|
|
}
|
|
|
|
}
|
2020-10-30 04:25:17 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
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)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
Err(err) => {
|
|
|
|
error!("create_transaction failed. {}", err);
|
|
|
|
// TODO: also need to drop this CString once C++ is done reading it
|
|
|
|
let err_string = CString::new(err.to_string())
|
|
|
|
.expect("could not create CString")
|
|
|
|
.into_raw();
|
|
|
|
FFIResult::Err(err_string)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
mod tests {
|
|
|
|
use std::ffi::CString;
|
|
|
|
|
|
|
|
use super::*;
|
|
|
|
use mockito::mock;
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn test_create_transaction() {
|
|
|
|
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",
|
2020-11-01 02:43:14 +00:00
|
|
|
"form_type": 41,
|
2020-10-30 04:25:17 +00:00
|
|
|
"id": 1,
|
2020-11-01 02:43:14 +00:00
|
|
|
"is_food": false,
|
2020-10-30 04:25:17 +00:00
|
|
|
"is_sell": false,
|
|
|
|
"local_form_id": 1,
|
|
|
|
"mod_name": "Skyrim.esm",
|
2020-11-01 02:43:14 +00:00
|
|
|
"name": "Item",
|
2020-10-30 04:25:17 +00:00
|
|
|
"owner_id": 1,
|
2020-11-01 02:43:14 +00:00
|
|
|
"price": 100,
|
2020-10-30 04:25:17 +00:00
|
|
|
"quantity": 1,
|
|
|
|
"shop_id": 1,
|
|
|
|
"updated_at": "2020-08-18T00:00:00.000"
|
|
|
|
}"#,
|
|
|
|
)
|
|
|
|
.create();
|
|
|
|
|
|
|
|
let api_url = CString::new("url").unwrap().into_raw();
|
|
|
|
let api_key = CString::new("api-key").unwrap().into_raw();
|
|
|
|
let mod_name = CString::new("Skyrim.esm").unwrap().into_raw();
|
2020-11-01 02:43:14 +00:00
|
|
|
let name = CString::new("Item").unwrap().into_raw();
|
|
|
|
let raw_transaction = RawTransaction {
|
2020-11-02 06:38:29 +00:00
|
|
|
id: 0,
|
2020-11-01 02:43:14 +00:00
|
|
|
shop_id: 1,
|
|
|
|
mod_name,
|
|
|
|
local_form_id: 1,
|
|
|
|
name,
|
|
|
|
form_type: 41,
|
|
|
|
is_food: false,
|
|
|
|
price: 100,
|
|
|
|
is_sell: false,
|
|
|
|
amount: 100,
|
|
|
|
quantity: 1,
|
|
|
|
};
|
|
|
|
let result = create_transaction(api_url, api_key, raw_transaction);
|
2020-10-30 04:25:17 +00:00
|
|
|
mock.assert();
|
|
|
|
match result {
|
|
|
|
FFIResult::Ok(raw_transaction) => {
|
2020-11-02 06:38:29 +00:00
|
|
|
assert_eq!(raw_transaction.id, 1);
|
2020-10-30 04:25:17 +00:00
|
|
|
assert_eq!(raw_transaction.shop_id, 1);
|
|
|
|
assert_eq!(
|
|
|
|
unsafe { CStr::from_ptr(raw_transaction.mod_name).to_string_lossy() },
|
|
|
|
"Skyrim.esm"
|
|
|
|
);
|
|
|
|
assert_eq!(raw_transaction.local_form_id, 1);
|
2020-11-01 02:43:14 +00:00
|
|
|
assert_eq!(
|
|
|
|
unsafe { CStr::from_ptr(raw_transaction.name).to_string_lossy() },
|
|
|
|
"Item"
|
|
|
|
);
|
|
|
|
assert_eq!(raw_transaction.form_type, 41);
|
|
|
|
assert_eq!(raw_transaction.is_food, false);
|
|
|
|
assert_eq!(raw_transaction.price, 100);
|
2020-10-30 04:25:17 +00:00
|
|
|
assert_eq!(raw_transaction.is_sell, false);
|
|
|
|
assert_eq!(raw_transaction.quantity, 1);
|
|
|
|
assert_eq!(raw_transaction.amount, 100);
|
|
|
|
}
|
|
|
|
FFIResult::Err(error) => panic!("create_transaction returned error: {:?}", unsafe {
|
|
|
|
CStr::from_ptr(error).to_string_lossy()
|
|
|
|
}),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn test_create_transaction_server_error() {
|
|
|
|
let mock = mock("POST", "/v1/transactions")
|
|
|
|
.with_status(500)
|
2020-11-06 00:19:17 +00:00
|
|
|
.with_header("content-type", "application/problem+json")
|
|
|
|
.with_body(
|
|
|
|
r#"{
|
|
|
|
"detail": "Some error detail",
|
|
|
|
"instance": "https://httpstatuses.com/500",
|
|
|
|
"status": 500,
|
|
|
|
"title": "Internal Server Error"
|
|
|
|
}"#,
|
|
|
|
)
|
2020-10-30 04:25:17 +00:00
|
|
|
.create();
|
|
|
|
|
|
|
|
let api_url = CString::new("url").unwrap().into_raw();
|
|
|
|
let api_key = CString::new("api-key").unwrap().into_raw();
|
|
|
|
let mod_name = CString::new("Skyrim.esm").unwrap().into_raw();
|
2020-11-01 02:43:14 +00:00
|
|
|
let name = CString::new("Item").unwrap().into_raw();
|
|
|
|
let raw_transaction = RawTransaction {
|
2020-11-02 06:38:29 +00:00
|
|
|
id: 0,
|
2020-11-01 02:43:14 +00:00
|
|
|
shop_id: 1,
|
|
|
|
mod_name,
|
|
|
|
local_form_id: 1,
|
|
|
|
name,
|
|
|
|
form_type: 41,
|
|
|
|
is_food: false,
|
|
|
|
price: 100,
|
|
|
|
is_sell: false,
|
|
|
|
amount: 100,
|
|
|
|
quantity: 1,
|
|
|
|
};
|
|
|
|
let result = create_transaction(api_url, api_key, raw_transaction);
|
2020-10-30 04:25:17 +00:00
|
|
|
mock.assert();
|
|
|
|
match result {
|
|
|
|
FFIResult::Ok(raw_transaction) => panic!(
|
|
|
|
"create_transaction returned Ok result: {:#?}",
|
|
|
|
raw_transaction
|
|
|
|
),
|
|
|
|
FFIResult::Err(error) => {
|
|
|
|
assert_eq!(
|
|
|
|
unsafe { CStr::from_ptr(error).to_string_lossy() },
|
2020-11-06 00:19:17 +00:00
|
|
|
"Server 500: Internal Server Error. Some error detail"
|
2020-10-30 04:25:17 +00:00
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|