Skip to content

Commit

Permalink
feat: list known features (#157)
Browse files Browse the repository at this point in the history
  • Loading branch information
sighphyre authored Oct 29, 2024
1 parent c02d4cd commit 443f662
Show file tree
Hide file tree
Showing 9 changed files with 204 additions and 1 deletion.
29 changes: 29 additions & 0 deletions dotnet-engine/Yggdrasil.Engine.Tests/YggdrasilEngineTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -256,4 +256,33 @@ public void Valid_Json_With_An_Invalid_State_Update_Raises_An_Error()
var engine = new YggdrasilEngine();
Assert.Throws<YggdrasilEngineException>(() => engine.TakeState(testData));
}

[Test]
public void Features_That_Get_Set_Can_Be_Listed()
{
var testDataObject = new
{
Version = 2,
Features = new[] {
new {
Name = "with.impression.data",
Type = "release",
Enabled = true,
ImpressionData = true,
Strategies = new [] {
new {
Name = "default",
Parameters = new Dictionary<string, string>()
}
}
}
}
};

var testData = JsonSerializer.Serialize(testDataObject, options);
var engine = new YggdrasilEngine();
engine.TakeState(testData);
var knownFeatures = engine.ListKnownToggles();
Assert.AreEqual(1, knownFeatures.Count);
}
}
7 changes: 7 additions & 0 deletions dotnet-engine/Yggdrasil.Engine/FFI.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ internal static class FFI
private static extern IntPtr should_emit_impression_event(IntPtr ptr, string toggle_name);
[DllImport("yggdrasilffi", SetLastError = true, CallingConvention = CallingConvention.Cdecl)]
private static extern IntPtr built_in_strategies(IntPtr ptr);
[DllImport("yggdrasilffi", SetLastError = true, CallingConvention = CallingConvention.Cdecl)]
private static extern IntPtr list_known_toggles(IntPtr ptr);

public static IntPtr NewEngine()
{
Expand Down Expand Up @@ -92,4 +94,9 @@ public static IntPtr BuiltInStrategies(IntPtr ptr)
{
return built_in_strategies(ptr);
}

public static IntPtr ListKnownToggles(IntPtr ptr)
{
return list_known_toggles(ptr);
}
}
15 changes: 15 additions & 0 deletions dotnet-engine/Yggdrasil.Engine/Types.cs
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,21 @@ public override int GetHashCode()
}
}

public class FeatureDefinition
{
public FeatureDefinition(string name, string project, string? type)
{
Name = name;
Project = project;
Type = type;
}
public string Name { get; set; }

public string Project { get; set; }

public string? Type { get; set; }
}

public class YggdrasilEngineException : Exception
{
public YggdrasilEngineException(string message) : base(message) { }
Expand Down
11 changes: 11 additions & 0 deletions dotnet-engine/Yggdrasil.Engine/YggdrasilEngine.cs
Original file line number Diff line number Diff line change
Expand Up @@ -83,4 +83,15 @@ public void CountVariant(string featureName, string variantName)
{
FFI.CountVariant(state, featureName, variantName);
}

public ICollection<FeatureDefinition> ListKnownToggles()
{
var featureDefinitionsPtr = FFI.ListKnownToggles(state);
var knownFeatures = FFIReader.ReadComplex<List<FeatureDefinition>>(featureDefinitionsPtr);
if (knownFeatures == null)
{
return new List<FeatureDefinition>();
}
return knownFeatures;
}
}
9 changes: 9 additions & 0 deletions ruby-engine/lib/yggdrasil_engine.rb
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ class YggdrasilEngine
attach_function :count_toggle, %i[pointer string bool], :void
attach_function :count_variant, %i[pointer string string], :void

attach_function :list_known_toggles, [:pointer], :pointer

def initialize
@engine = YggdrasilEngine.new_engine
@custom_strategy_handler = CustomStrategyHandler.new
Expand Down Expand Up @@ -125,6 +127,13 @@ def get_metrics
metrics[:value]
end

def list_known_toggles
response_ptr = YggdrasilEngine.list_known_toggles(@engine)
response_json = response_ptr.read_string
YggdrasilEngine.free_response(response_ptr)
JSON.parse(response_json, symbolize_names: true)
end

def register_custom_strategies(strategies)
@custom_strategy_handler.register_custom_strategies(strategies)
end
Expand Down
10 changes: 10 additions & 0 deletions ruby-engine/spec/yggdrasil_engine_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,16 @@ def test_suite_variant(base_variant)

expect(metric[:variants][:disabled]).to eq(1)
end

it 'should list all the features that were loaded' do
suite_path = File.join('../client-specification/specifications', '01-simple-examples.json')
suite_data = JSON.parse(File.read(suite_path))

yggdrasil_engine.take_state(suite_data['state'].to_json)

toggles = yggdrasil_engine.list_known_toggles()
expect(toggles.length).to eq(3)
end
end
end

Expand Down
31 changes: 31 additions & 0 deletions unleash-yggdrasil/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ const VARIANT_NORMALIZATION_SEED: u32 = 86028157;
pub struct CompiledToggle {
pub name: String,
pub enabled: bool,
pub feature_type: Option<String>,
pub compiled_strategy: RuleFragment,
pub compiled_variant_strategy: Option<Vec<(RuleFragment, Vec<CompiledVariant>, String)>>,
pub variants: Vec<CompiledVariant>,
Expand All @@ -42,6 +43,15 @@ pub struct CompiledToggle {
pub dependencies: Vec<FeatureDependency>,
}

#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ToggleDefinition {
pub name: String,
#[serde(rename = "type", skip_serializing_if = "Option::is_none")]
pub feature_type: Option<String>,
pub project: String,
}

#[derive(Debug, Serialize, Deserialize)]
pub struct CompiledVariant {
pub name: String,
Expand All @@ -56,6 +66,7 @@ impl Default for CompiledToggle {
Self {
name: Default::default(),
enabled: Default::default(),
feature_type: None,
compiled_strategy: Box::new(|_| true),
compiled_variant_strategy: None,
variants: Default::default(),
Expand Down Expand Up @@ -146,6 +157,7 @@ pub fn compile_state(
CompiledToggle {
name: toggle.name.clone(),
enabled: toggle.enabled,
feature_type: toggle.feature_type.clone(),
compiled_variant_strategy: variant_rule,
variants: compile_variants(&toggle.variants),
compiled_strategy: compile_rule(rule.as_str()).unwrap_or_else(|e| {
Expand Down Expand Up @@ -415,6 +427,25 @@ impl EngineState {
})
}

pub fn list_known_toggles(&self) -> Vec<ToggleDefinition> {
self.compiled_state
.as_ref()
.map(|state| {
state
.iter()
.map(|pair| {
let toggle = pair.1;
ToggleDefinition {
feature_type: toggle.feature_type.clone(),
name: toggle.name.clone(),
project: toggle.project.clone(),
}
})
.collect::<Vec<ToggleDefinition>>()
})
.unwrap_or_default()
}

pub fn should_emit_impression_event(&self, name: &str) -> bool {
self.compiled_state
.as_ref()
Expand Down
80 changes: 79 additions & 1 deletion yggdrasilffi/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use std::{
use libc::c_void;
use serde::{de::DeserializeOwned, Deserialize, Serialize};
use unleash_types::{client_features::ClientFeatures, client_metrics::MetricBucket};
use unleash_yggdrasil::{Context, EngineState, EvalWarning, ExtendedVariantDef};
use unleash_yggdrasil::{Context, EngineState, EvalWarning, ExtendedVariantDef, ToggleDefinition};

#[derive(Serialize, Deserialize)]
struct Response<T> {
Expand Down Expand Up @@ -384,6 +384,28 @@ pub unsafe extern "C" fn should_emit_impression_event(
result_to_json_ptr(result)
}

/// Lists the features currently known by the engine, as set by take_state
/// This is a reduced definition and only includes metadata for the feature,
/// not the properties required to calculate the enabled state of the feature.
/// Returns a JSON encoded response of type `Response`.
///
/// # Safety
///
/// The caller is responsible for ensuring the engine_ptr is a valid pointer to an unleash engine.
/// An invalid pointer to unleash engine will result in undefined behaviour.
/// The caller is responsible for freeing the allocated memory, in case the response is not null. This can be done by calling
/// `free_response` and passing in the pointer returned by this method. Failure to do so will result in a leak.
#[no_mangle]
pub unsafe extern "C" fn list_known_toggles(engine_ptr: *mut c_void) -> *mut c_char {
let result: Result<Option<Vec<ToggleDefinition>>, FFIError> = (|| {
let engine = get_engine(engine_ptr)?;

Ok(Some(engine.list_known_toggles()))
})();

result_to_json_ptr(result)
}

#[cfg(test)]
mod tests {
use std::ffi::{CStr, CString};
Expand Down Expand Up @@ -591,4 +613,60 @@ mod tests {
assert!(warnings.is_none());
}
}

#[test]
fn listing_known_features_returns_a_list_of_toggle_definitions() {
let engine_ptr = new_engine();

let client_features = ClientFeatures {
features: vec![
ClientFeature {
name: "toggle1".into(),
enabled: true,
strategies: Some(vec![Strategy {
name: "default".into(),
constraints: None,
parameters: None,
segments: None,
sort_order: None,
variants: None,
}]),
..Default::default()
},
ClientFeature {
name: "toggle2".into(),
enabled: true,
strategies: Some(vec![Strategy {
name: "default".into(),
constraints: None,
parameters: None,
segments: None,
sort_order: None,
variants: None,
}]),
..Default::default()
},
],
query: None,
segments: None,
version: 2,
};

unsafe {
let engine = &mut *(engine_ptr as *mut EngineState);
engine.take_state(client_features);

let string_response = super::list_known_toggles(engine_ptr);
let response = CStr::from_ptr(string_response).to_str().unwrap();
let known_features: Response<Vec<super::ToggleDefinition>> =
serde_json::from_str(response).unwrap();

assert!(known_features.status_code == ResponseCode::Ok);
let known_features = known_features.value.expect("Expected known features");

assert_eq!(known_features.len(), 2);
assert!(known_features.iter().any(|t| t.name == "toggle1"));
assert!(known_features.iter().any(|t| t.name == "toggle2"));
}
}
}
13 changes: 13 additions & 0 deletions yggdrasilwasm/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -193,4 +193,17 @@ impl Engine {

serde_wasm_bindgen::to_value(&response).unwrap()
}

#[wasm_bindgen(js_name = listKnownFeatures)]
pub fn list_known_toggles(&self) -> JsValue {
let known_toggles = self.engine.list_known_toggles();

let response = Response {
status_code: ResponseCode::Ok,
value: Some(known_toggles),
error_message: None,
};

serde_wasm_bindgen::to_value(&response).unwrap()
}
}

0 comments on commit 443f662

Please sign in to comment.