Skip to content

Commit

Permalink
Event now expects property values in properties map (#1)
Browse files Browse the repository at this point in the history
* Utilize PropertyValue, so we can get both numbers and strings from into event properties

* Add tests for deserializing behaviour
  • Loading branch information
nudded authored Nov 5, 2024
1 parent 603888c commit d7125a9
Show file tree
Hide file tree
Showing 17 changed files with 241 additions and 73 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/ruby.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ jobs:
- name: Set up Ruby 3.3
uses: ruby/setup-ruby@v1
with:
ruby-version: '3.3'
ruby-version: '3.3.4'
- name: install dependencies
run: bundle install
- name: build
Expand Down
2 changes: 1 addition & 1 deletion .tool-versions
Original file line number Diff line number Diff line change
@@ -1 +1 @@
ruby 3.3.5
ruby 3.3.4
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion expression-core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ version = "0.1.0"
edition = "2021"

[dependencies]
bigdecimal = "0.4.5"
bigdecimal = { version = "0.4.6", features = ["serde-json"] }
lazy_static = "1.5.0"
pest = "2.7.13"
pest_derive = "2.7.13"
Expand Down
28 changes: 14 additions & 14 deletions expression-core/src/evaluate.rs
Original file line number Diff line number Diff line change
@@ -1,24 +1,19 @@
use std::{collections::HashMap, fmt::Display};
use std::fmt::Display;

use bigdecimal::{BigDecimal, RoundingMode, ToPrimitive};
use serde::Deserialize;
use thiserror::Error;

use crate::parser::{EventAttribute, Expression, Function, Operation};
use crate::{
parser::{EventAttribute, Expression, Function, Operation},
Event, PropertyValue,
};

#[derive(Debug, PartialEq)]
pub enum ExpressionValue {
Number(BigDecimal),
String(String),
}

#[derive(Debug, Default, PartialEq, Deserialize)]
pub struct Event {
pub code: String,
pub timestamp: u64,
pub properties: HashMap<String, String>,
}

impl Expression {
pub fn evaluate(&self, event: &Event) -> EvaluationResult<ExpressionValue> {
let evaluated_expr = match self {
Expand Down Expand Up @@ -141,10 +136,15 @@ impl EventAttribute {
.get(name)
.ok_or(ExpressionError::MissingVariable(name.clone()))?;

if let Ok(decimal_value) = value.parse() {
ExpressionValue::Number(decimal_value)
} else {
ExpressionValue::String(value.clone())
match value {
PropertyValue::String(s) => {
if let Ok(decimal_value) = s.parse() {
ExpressionValue::Number(decimal_value)
} else {
ExpressionValue::String(s.clone())
}
}
PropertyValue::Number(d) => ExpressionValue::Number(d.to_owned()),
}
}
};
Expand Down
109 changes: 109 additions & 0 deletions expression-core/src/event.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
use std::collections::HashMap;

use bigdecimal::BigDecimal;
use serde::Deserialize;

#[derive(Debug, Default, PartialEq, Deserialize)]
pub struct Event {
pub code: String,
pub timestamp: u64,
pub properties: HashMap<String, PropertyValue>,
}

#[derive(Debug, PartialEq, Deserialize)]
#[serde(untagged)]
pub enum PropertyValue {
String(String),
Number(BigDecimal),
}

impl From<&str> for PropertyValue {
fn from(value: &str) -> Self {
PropertyValue::String(value.into())
}
}
impl From<String> for PropertyValue {
fn from(value: String) -> Self {
PropertyValue::String(value)
}
}

impl From<u64> for PropertyValue {
fn from(value: u64) -> Self {
PropertyValue::Number(value.into())
}
}

#[cfg(test)]
mod tests {
use serde_json::json;

use super::*;

#[test]
fn test_deserialize_with_numbers() {
let json = json!({
"code": "testing",
"timestamp": 123124123123i64,
"properties": {
"text": "text_value",
"number": 300
}
});

let event = serde_json::from_value::<Event>(json).expect("expected json to parse");

let code = "testing".to_owned();
let timestamp = 123124123123;
let mut properties = HashMap::default();
properties.insert("number".to_owned(), PropertyValue::Number(300.into()));
properties.insert(
"text".to_owned(),
PropertyValue::String("text_value".into()),
);

assert_eq!(
event,
Event {
code,
timestamp,
properties
}
)
}

#[test]
fn test_deserialize_with_floating_point() {
let json = r#"{
"code": "testing",
"timestamp": 123124123123,
"properties": {
"text": "text_value",
"number": 3.2
}
}"#;

let event = serde_json::from_str::<Event>(json).expect("expected json to parse");

let code = "testing".to_owned();
let timestamp = 123124123123;
let mut properties = HashMap::default();
properties.insert(
"number".to_owned(),
PropertyValue::Number("3.2".parse().unwrap()),
);
properties.insert(
"text".to_owned(),
PropertyValue::String("text_value".into()),
);

assert_eq!(
event,
Event {
code,
timestamp,
properties
}
)
}
}
4 changes: 3 additions & 1 deletion expression-core/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
pub use evaluate::{EvaluationResult, Event, ExpressionValue};
pub use evaluate::{EvaluationResult, ExpressionValue};
pub use event::{Event, PropertyValue};
pub use parser::{Expression, ExpressionParser, ParseError};
pub use pest::Parser;

mod evaluate;
mod event;
mod parser;
10 changes: 4 additions & 6 deletions expression-go/bindings.h
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,14 @@
#include <stdint.h>
#include <stdlib.h>

typedef struct Expression Expression;

/**
* # Safety
* Pass in a valid string
* Pass in a valid strings
*/
Expression *parse(const char *input);
char *evaluate(const char *input, const char *event);

/**
* # Safety
* Pass in a valid string
* Only pass in pointers to strings that have been obtained through `evaluate`
*/
const char *evaluate(Expression *expr, const char *event);
void free_evaluate(char *ptr);
Binary file removed expression-go/expressiongo
Binary file not shown.
21 changes: 15 additions & 6 deletions expression-go/main.go
Original file line number Diff line number Diff line change
@@ -1,17 +1,26 @@
package main

// #cgo LDFLAGS: -L../target/debug -lexpression_go
// #cgo LDFLAGS: -L../target/release -lexpression_go
// #include <stdio.h>
// #include <stdlib.h>
// #include "bindings.h"
import "C"
import "unsafe"

func main() {
cs := C.CString("event.timestamp+event.properties.a")
event := C.CString("{\"code\":\"123\",\"timestamp\":2,\"properties\":{\"a\": \"123\"}}")
expr := C.parse(cs)
cs := C.CString("concat(event.properties.a, 'test')")
event := C.CString("{\"code\":\"13\",\"timestamp\":2,\"properties\":{\"a\": 123.12}}")

result := C.GoString(C.evaluate(expr, event))
println(result)
ptr := C.evaluate(cs, event)
if ptr != nil {

result := C.GoString(ptr)
println(result)

C.free_evaluate(ptr)
}

C.free(unsafe.Pointer(cs))
C.free(unsafe.Pointer(event))

}
55 changes: 33 additions & 22 deletions expression-go/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,32 +1,43 @@
use std::ffi::{c_char, CStr, CString};
use std::{
ffi::{c_char, CStr, CString},
ptr::null_mut,
};

use expression_core::{Event, Expression, ExpressionParser};
use expression_core::ExpressionParser;

#[no_mangle]
/// # Safety
/// Pass in a valid string
pub unsafe extern "C" fn parse(input: *const c_char) -> *mut Expression {
/// Pass in a valid strings
pub unsafe extern "C" fn evaluate(input: *const c_char, event: *const c_char) -> *mut c_char {
let input = unsafe { CStr::from_ptr(input).to_str().unwrap().to_owned() };
let expression = ExpressionParser::parse_expression(&input).unwrap();
Box::into_raw(Box::new(expression))

// Cannot parse expression -> return null
let Ok(expr) = ExpressionParser::parse_expression(&input) else {
return null_mut();
};

let json = unsafe { CStr::from_ptr(event).to_str().unwrap() };

// TODO: solve the fact that json will contain numbers, deserialize will fail as it's expecting only
// strings. in the Event::properties which is a HashMap<String, String>
let Ok(event) = serde_json::from_str(json) else {
return null_mut();
};

// evaluate expression, errors are not returned, but we do catch them and return null
let Ok(res) = expr.evaluate(&event) else {
return null_mut();
};

let Ok(temp) = CString::new(res.to_string()) else {
return null_mut();
};
temp.into_raw()
}

#[no_mangle]
/// # Safety
/// Pass in a valid string
pub unsafe extern "C" fn evaluate(expr: *mut Expression, event: *const c_char) -> *const c_char {
let json = unsafe { CStr::from_ptr(event).to_str().unwrap() };
let event: Event = serde_json::from_str(json).unwrap();

let expr = unsafe { expr.as_ref().unwrap() };
match expr.evaluate(&event).unwrap() {
expression_core::ExpressionValue::Number(d) => {
let temp = CString::new(d.to_string()).unwrap();
temp.into_raw()
}
expression_core::ExpressionValue::String(d) => {
let temp = CString::new(d).unwrap();
temp.into_raw()
}
}
/// Only pass in pointers to strings that have been obtained through `evaluate`
pub unsafe extern "C" fn free_evaluate(ptr: *mut c_char) {
unsafe { drop(CString::from_raw(ptr)) }
}
4 changes: 2 additions & 2 deletions expression-js/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ elem.oninput = function () {
error.innerHTML = "";
try {
expression = parseExpression(elem.value);
output.innerHTML = evaluateExpression(expression, "code", 1231254123, {
started_at: 123124123,
output.innerHTML = evaluateExpression(expression, "code", BigInt(1231254123123125), {
started_at: 1231254123123125,
ended_at: 1241231241,
replicas: 8,
});
Expand Down
23 changes: 13 additions & 10 deletions expression-js/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,9 @@ use std::collections::HashMap;
use js_sys::Reflect;
use wasm_bindgen::prelude::*;

use bigdecimal::ToPrimitive;
use bigdecimal::{BigDecimal, FromPrimitive, ToPrimitive};

use expression_core::{ExpressionParser, ExpressionValue};
use web_sys::console;
use expression_core::{ExpressionParser, ExpressionValue, PropertyValue};
extern crate console_error_panic_hook;

#[wasm_bindgen(start)]
Expand All @@ -29,7 +28,7 @@ pub fn parse_expression(expression: String) -> Result<Expression, String> {
pub fn evaluate_expression(
expression: Expression,
code: String,
timestamp: u32,
timestamp: u64,
js_properties: &JsValue,
) -> Result<JsValue, JsValue> {
let mut properties = HashMap::new();
Expand All @@ -38,21 +37,25 @@ pub fn evaluate_expression(

for key in keys {
let value = Reflect::get(js_properties, &key)?;
console::log_1(&value);

let insert = if value.is_string() {
String::try_from(value)?
let property_value = if value.is_string() {
String::try_from(value)?.into()
} else if value.is_bigint() {
let n = u64::try_from(value)?;
PropertyValue::Number(n.into())
} else {
let n = f64::try_from(value)?;
format!("{n}")
PropertyValue::Number(
BigDecimal::from_f64(n).ok_or("failed to convert property value")?,
)
};

properties.insert(key.as_string().ok_or("expected string")?, insert);
properties.insert(key.as_string().ok_or("expected string")?, property_value);
}

let event = expression_core::Event {
code,
timestamp: timestamp.into(),
timestamp,
properties,
};

Expand Down
2 changes: 1 addition & 1 deletion expression-ruby/Gemfile
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
source "https://rubygems.org"

ruby "3.3.5"
ruby "3.3.4"

gemspec
Loading

0 comments on commit d7125a9

Please sign in to comment.