Skip to content

Instantly share code, notes, and snippets.

@makvoid
Created August 25, 2022 16:34
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save makvoid/406c0a8167c01942c9c147ed10a823a8 to your computer and use it in GitHub Desktop.
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
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