-
-
Save makvoid/406c0a8167c01942c9c147ed10a823a8 to your computer and use it in GitHub Desktop.
"How to use Algolia as a game engine debugging tool in Rust" Example #4
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
use serde::{Serialize}; | |
use std::{time::{SystemTime}}; | |
use urlencoding; | |
// The credentials data | |
const ALGOLIA_AGENT: &str = "Algolia DataSender for Rust Dev (0.0.1)"; | |
// The reqwest client type used by the library | |
type ClientType = reqwest::blocking::Client; | |
// ID GENERATOR | |
// ------------ | |
// Generates unique IDs to use as ObjectIds | |
pub struct IdGenerator { | |
prefix: String, | |
idx: i32, | |
} | |
impl IdGenerator { | |
pub fn new(prefix: String) -> Self { | |
IdGenerator { prefix: prefix, idx: 0 } | |
} | |
pub fn new_from_time() -> Self { | |
let now = SystemTime::now(); | |
let prefix = format!("{:?}", now.duration_since(std::time::UNIX_EPOCH).unwrap_or_default()); | |
Self::new(prefix) | |
} | |
pub fn next_i32(&mut self) -> i32 { | |
let idx = self.idx; | |
self.idx += 1; | |
idx | |
} | |
pub fn next_string(&mut self) -> String { | |
let next_id = self.next_i32(); | |
format!("{}_{}", self.prefix.as_str(), next_id) | |
} | |
} | |
// SENDER | |
// ------ | |
pub struct AlgoliaSender { | |
app_id: String, | |
api_key: String, | |
index_name: String, | |
data_buffer: Vec<String>, | |
client: ClientType, | |
id_generator: IdGenerator, | |
} | |
impl AlgoliaSender { | |
pub fn new(app_id: &str, api_key: &str, index_name: &str) -> Self { | |
AlgoliaSender { | |
app_id: String::from(app_id), | |
api_key: String::from(api_key), | |
index_name: String::from(index_name), | |
data_buffer: vec![], | |
client: ClientType::new(), | |
id_generator: IdGenerator::new_from_time(), | |
} | |
} | |
// adds an already serialized value to be sent | |
pub fn add_raw_item(&mut self, data: String) { | |
self.data_buffer.push(data); | |
} | |
// Adds a value that will be serialized for sending | |
pub fn add_item<T>(&mut self, v: &T) | |
where | |
T: Serialize, | |
{ | |
match serde_json::to_string(v) { | |
Ok(serialized) => { | |
self.add_raw_item(serialized); | |
// self.data_buffer.push(serialized); | |
} | |
Err(_) => {} | |
} | |
} | |
// Sends items to the ingestion endpoint in a batch job | |
pub fn send_items(&mut self) { | |
let index_name = self.index_name.clone(); | |
self.send_items_to_index(index_name.as_str()); | |
} | |
// Sends items to the ingestion endpoint in a batch job | |
pub fn send_items_to_index(&mut self, index_name: &str) { | |
// convert a pre-serialized JSON object into a request object for a batch request | |
fn build_batch_request_entry(id: &String, data: &String) -> Option<String> { | |
if data.len() <= 2 { | |
return None; | |
} | |
// Skip the opening curly brace and inject our objectID into the already serialized object | |
let remaining_data:String = data.chars().skip(1).collect(); | |
Some(format!("{{\"action\":\"updateObject\",\"body\":{{\"objectID\":\"{}\",{}}}", id, remaining_data)) | |
} | |
// wrap the individual requests into a batch | |
fn wrap_batch_request_entry(rows: &Vec<String>) -> String { | |
format!("{{\"requests\":[{}]}}", rows.join(",")) | |
} | |
// wrap the data | |
let data = wrap_batch_request_entry( | |
&self | |
.data_buffer | |
.iter() | |
.filter_map(|entry: &String| -> Option<String> { build_batch_request_entry(&self.id_generator.next_string(), entry) }) | |
.collect(), | |
); | |
// get the URI for the index | |
let uri = self.uri_for_index(index_name); | |
let uri_with_client = format!("{}?x-algolia-agent={}", uri, ALGOLIA_AGENT); | |
let res = self | |
.client | |
.post(uri_with_client) | |
.header("x-algolia-api-key", self.api_key.as_str()) | |
.header("x-algolia-application-id", self.app_id.as_str()) | |
.body(data) | |
.send(); | |
match res { | |
Err(err) => { | |
println!("ERROR while sending to Algolia: {}", err) | |
} | |
Ok(resp) => { | |
let status = resp.status(); | |
let body = resp.text().unwrap_or_default(); | |
if status != 200 { | |
// log the problem for now | |
println!("ERROR while sending to Algolia: {}\n {}", status, body); | |
} | |
} | |
} | |
// TODO: this clean is tricky to place, as we want to keep unsent data, but it may accumulate to very large amounts | |
self.data_buffer.clear(); | |
} | |
// returns a full URI for an index name and the current app | |
fn uri_for_index(&self, index_name: &str) -> String { | |
// build the URI for the batch | |
let host = format!("{}.algolia.net", self.app_id.to_lowercase()); | |
let path = format!("/1/indexes/{}/batch", urlencoding::encode(index_name)); | |
format!("https://{}{}", host, path) | |
} | |
} | |
// C API | |
// ----- | |
// Constructor for the AlgoliaSender struct from C. | |
// Returns nullptr if any of the parameters are empty. | |
#[no_mangle] | |
pub unsafe extern "C" fn algolia_sender_new(app_id: *const libc::c_char, api_key: *const libc::c_char, index_name: *const libc::c_char) -> *mut AlgoliaSender { | |
// rustify all parameters | |
let app_id_str: &str = std::ffi::CStr::from_ptr(app_id).to_str().unwrap_or(""); | |
let api_key_str: &str = std::ffi::CStr::from_ptr(api_key).to_str().unwrap_or(""); | |
let index_name_str: &str = std::ffi::CStr::from_ptr(index_name).to_str().unwrap_or(""); | |
// check these parameters | |
if app_id_str.is_empty() || api_key_str.is_empty() || index_name_str.is_empty() { | |
return std::ptr::null_mut(); | |
} | |
// create a new sender and Box it, and use `Box::into_raw()` to get a pointer that outlives this function call | |
let struct_instance = AlgoliaSender::new(app_id_str, api_key_str, index_name_str); | |
let boxed = Box::new(struct_instance); | |
Box::into_raw(boxed) | |
} | |
// Destructor for the AlgoliaSender struct from C | |
#[no_mangle] | |
pub unsafe extern "C" fn algolia_sender_destroy(struct_instance: *mut AlgoliaSender) { | |
// let the Rust lifetime take over and destroy the instance after the function is done | |
Box::from_raw(struct_instance); | |
} | |
// Adds an item to be sent to the target sender | |
#[no_mangle] | |
pub unsafe extern "C" fn algolia_sender_add_item(a: *mut AlgoliaSender, data: *const libc::c_char) { | |
// convert data | |
let data_str = match std::ffi::CStr::from_ptr(data).to_str() { | |
Err(_) => return, | |
Ok(s) => String::from(s), | |
}; | |
// attempt to add it | |
match a.as_mut() { | |
None => return, | |
Some(sender) => sender.add_raw_item(data_str), | |
}; | |
} | |
// Trigger the sending of items | |
#[no_mangle] | |
pub unsafe extern "C" fn algolia_sender_send_items(a: *mut AlgoliaSender) { | |
// attempt to send the items | |
match a.as_mut() { | |
None => return, | |
Some(sender) => sender.send_items(), | |
}; | |
} | |
#[derive(Serialize, Debug)] | |
struct Float3 { | |
x: f32, | |
y: f32, | |
z: f32, | |
} | |
pub trait ObjectId { | |
fn object_id() -> Option<String> { None } | |
} | |
impl ObjectId for Float3 { | |
fn object_id() -> Option<String> { | |
None | |
} | |
} | |
fn main() -> Result<(), Box<dyn std::error::Error>> { | |
// The credentials data | |
const APP_ID: &str = ""; | |
const INDEX_NAME: &str = ""; | |
const API_KEY: &str = ""; | |
let mut sender = AlgoliaSender::new(APP_ID, API_KEY, INDEX_NAME); | |
sender.add_item(&Float3 { | |
x: 5.1, | |
y: 5.2, | |
z: 5.3, | |
}); | |
sender.add_item(&Float3 { | |
x: 6.1, | |
y: 6.2, | |
z: 6.3, | |
}); | |
sender.send_items(); | |
return Ok(()); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment