Skip to content

Commit bd12c19

Browse files
feat!: Support transient identities and traits (#23)
* feat: Support transient identities and traits Co-authored-by: Matthew Elwell <[email protected]> --------- Co-authored-by: Matthew Elwell <[email protected]>
1 parent 7b3c42b commit bd12c19

File tree

3 files changed

+195
-23
lines changed

3 files changed

+195
-23
lines changed

src/flagsmith/mod.rs

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ use flagsmith_flag_engine::identities::{Identity, Trait};
88
use flagsmith_flag_engine::segments::evaluator::get_identity_segments;
99
use flagsmith_flag_engine::segments::Segment;
1010
use log::debug;
11+
use models::SDKTrait;
1112
use reqwest::header::{self, HeaderMap};
1213
use serde_json::json;
1314
use std::collections::HashMap;
@@ -195,15 +196,26 @@ impl Flagsmith {
195196
pub fn get_identity_flags(
196197
&self,
197198
identifier: &str,
198-
traits: Option<Vec<Trait>>,
199+
traits: Option<Vec<SDKTrait>>,
200+
transient: Option<bool>,
199201
) -> Result<Flags, error::Error> {
200202
let data = self.datastore.lock().unwrap();
201203
let traits = traits.unwrap_or(vec![]);
202204
if data.environment.is_some() {
203205
let environment = data.environment.as_ref().unwrap();
204-
return self.get_identity_flags_from_document(environment, &data.identities_with_overrides_by_identifier, identifier, traits);
206+
let engine_traits: Vec<Trait> = traits.into_iter().map(|t| t.into()).collect();
207+
return self.get_identity_flags_from_document(
208+
environment,
209+
&data.identities_with_overrides_by_identifier,
210+
identifier,
211+
engine_traits,
212+
);
205213
}
206-
return self.default_handler_if_err(self.get_identity_flags_from_api(identifier, traits));
214+
return self.default_handler_if_err(self.get_identity_flags_from_api(
215+
identifier,
216+
traits,
217+
transient.unwrap_or(false),
218+
));
207219
}
208220
// Returns a list of segments that the given identity is part of
209221
pub fn get_identity_segments(
@@ -298,11 +310,12 @@ impl Flagsmith {
298310
fn get_identity_flags_from_api(
299311
&self,
300312
identifier: &str,
301-
traits: Vec<Trait>,
313+
traits: Vec<SDKTrait>,
314+
transient: bool,
302315
) -> Result<Flags, error::Error> {
303316
let method = reqwest::Method::POST;
304317

305-
let json = json!({"identifier":identifier, "traits": traits});
318+
let json = json!({"identifier":identifier, "traits": traits, "transient": transient});
306319
let response = get_json_response(
307320
&self.client,
308321
method,
@@ -586,7 +599,7 @@ mod tests {
586599

587600
// Then
588601
let flags = _flagsmith.get_environment_flags();
589-
let identity_flags = _flagsmith.get_identity_flags("overridden-id", None);
602+
let identity_flags = _flagsmith.get_identity_flags("overridden-id", None, None);
590603
assert_eq!(flags.unwrap().get_feature_value_as_string("some_feature").unwrap().to_owned(), "some-value");
591604
assert_eq!(identity_flags.unwrap().get_feature_value_as_string("some_feature").unwrap().to_owned(), "some-overridden-value");
592605
}

src/flagsmith/models.rs

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
use crate::flagsmith::analytics::AnalyticsProcessor;
22
use core::f64;
33
use flagsmith_flag_engine::features::FeatureState;
4+
use flagsmith_flag_engine::identities::Trait;
45
use flagsmith_flag_engine::types::{FlagsmithValue, FlagsmithValueType};
6+
use serde::{Deserialize, Serialize};
57
use std::collections::HashMap;
68

79
use crate::error;
@@ -156,6 +158,44 @@ impl Flags {
156158
}
157159
}
158160

161+
#[derive(Serialize, Deserialize, Debug, Clone)]
162+
pub struct SDKTrait {
163+
pub trait_key: String,
164+
pub trait_value: FlagsmithValue,
165+
#[serde(default)]
166+
pub transient: bool,
167+
}
168+
169+
impl SDKTrait {
170+
pub fn new(trait_key: String, trait_value: FlagsmithValue) -> SDKTrait {
171+
return SDKTrait {
172+
trait_key: trait_key,
173+
trait_value: trait_value,
174+
transient: Default::default(),
175+
};
176+
}
177+
pub fn new_with_transient(
178+
trait_key: String,
179+
trait_value: FlagsmithValue,
180+
transient: bool,
181+
) -> Self {
182+
return SDKTrait {
183+
trait_key: trait_key,
184+
trait_value: trait_value,
185+
transient: transient,
186+
};
187+
}
188+
}
189+
190+
impl From<SDKTrait> for Trait {
191+
fn from(t: SDKTrait) -> Self {
192+
Self {
193+
trait_key: t.trait_key,
194+
trait_value: t.trait_value,
195+
}
196+
}
197+
}
198+
159199
#[cfg(test)]
160200
mod tests {
161201
use super::*;

tests/integration_test.rs

Lines changed: 136 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use flagsmith::flagsmith::models::SDKTrait;
12
use flagsmith::flagsmith::offline_handler;
23
use flagsmith::{Flagsmith, FlagsmithOptions};
34
use flagsmith_flag_engine::identities::Trait;
@@ -136,7 +137,7 @@ fn test_offline_mode() {
136137
// When
137138
let env_flags = flagsmith.get_environment_flags().unwrap().all_flags();
138139
let identity_flags = flagsmith
139-
.get_identity_flags("test_identity", None)
140+
.get_identity_flags("test_identity", None, None)
140141
.unwrap()
141142
.all_flags();
142143

@@ -175,7 +176,7 @@ fn test_offline_handler_is_used_if_request_fails(mock_server: MockServer) {
175176
// When
176177
let env_flags = flagsmith.get_environment_flags().unwrap().all_flags();
177178
let identity_flags = flagsmith
178-
.get_identity_flags("test_identity", None)
179+
.get_identity_flags("test_identity", None, None)
179180
.unwrap()
180181
.all_flags();
181182

@@ -223,7 +224,7 @@ fn test_get_identity_flags_uses_local_environment_when_available(
223224

224225
// Then
225226
let all_flags = flagsmith
226-
.get_identity_flags("test_identity", None)
227+
.get_identity_flags("test_identity", None, None)
227228
.unwrap()
228229
.all_flags();
229230
assert_eq!(all_flags.len(), 1);
@@ -249,7 +250,8 @@ fn test_get_identity_flags_calls_api_when_no_local_environment_no_traits(
249250
.header("X-Environment-Key", ENVIRONMENT_KEY)
250251
.json_body(serde_json::json!({
251252
"identifier": identifier,
252-
"traits": []
253+
"traits": [],
254+
"transient": false,
253255
}));
254256
then.status(200).json_body(identities_json);
255257
});
@@ -263,7 +265,7 @@ fn test_get_identity_flags_calls_api_when_no_local_environment_no_traits(
263265
// When
264266

265267
let all_flags = flagsmith
266-
.get_identity_flags(identifier, None)
268+
.get_identity_flags(identifier, None, None)
267269
.unwrap()
268270
.all_flags();
269271

@@ -296,7 +298,8 @@ fn test_get_identity_flags_calls_api_when_no_local_environment_with_traits(
296298
.header("content-type", "application/json")
297299
.json_body(serde_json::json!({
298300
"identifier": identifier,
299-
"traits": [{"trait_key":trait_key, "trait_value": trait_value}]
301+
"traits": [{"trait_key":trait_key, "trait_value": trait_value, "transient": false}],
302+
"transient": false,
300303
}));
301304
then.status(200).json_body(identities_json);
302305
});
@@ -308,15 +311,15 @@ fn test_get_identity_flags_calls_api_when_no_local_environment_with_traits(
308311
let flagsmith = Flagsmith::new(ENVIRONMENT_KEY.to_string(), flagsmith_options);
309312

310313
// When
311-
let traits = vec![Trait {
312-
trait_key: trait_key.to_string(),
313-
trait_value: FlagsmithValue {
314+
let traits = vec![SDKTrait::new(
315+
trait_key.to_string(),
316+
FlagsmithValue {
314317
value: trait_value.to_string(),
315318
value_type: FlagsmithValueType::String,
316319
},
317-
}];
320+
)];
318321
let all_flags = flagsmith
319-
.get_identity_flags(identifier, Some(traits))
322+
.get_identity_flags(identifier, Some(traits), None)
320323
.unwrap()
321324
.all_flags();
322325

@@ -332,6 +335,113 @@ fn test_get_identity_flags_calls_api_when_no_local_environment_with_traits(
332335
api_mock.assert();
333336
}
334337

338+
#[rstest]
339+
fn test_get_identity_flags_calls_api_when_no_local_environment_with_transient_traits(
340+
mock_server: MockServer,
341+
identities_json: serde_json::Value,
342+
) {
343+
// Given
344+
let identifier = "test_identity";
345+
let trait_key = "trait_key1";
346+
let trait_value = "trait_value1";
347+
let transient_trait_key = "trait_key2";
348+
349+
let api_mock = mock_server.mock(|when, then| {
350+
when.method(POST)
351+
.path("/api/v1/identities/")
352+
.header("X-Environment-Key", ENVIRONMENT_KEY)
353+
.header("content-type", "application/json")
354+
.json_body(serde_json::json!({
355+
"identifier": identifier,
356+
"traits": [
357+
{"trait_key":trait_key, "trait_value": trait_value, "transient": false},
358+
{"trait_key":transient_trait_key, "trait_value": trait_value, "transient": true},
359+
],
360+
"transient": false,
361+
}));
362+
then.status(200).json_body(identities_json);
363+
});
364+
let url = mock_server.url("/api/v1/");
365+
let flagsmith_options = FlagsmithOptions {
366+
api_url: url,
367+
..Default::default()
368+
};
369+
let flagsmith = Flagsmith::new(ENVIRONMENT_KEY.to_string(), flagsmith_options);
370+
371+
// When
372+
let traits = vec![
373+
SDKTrait::new(
374+
trait_key.to_string(),
375+
FlagsmithValue {
376+
value: trait_value.to_string(),
377+
value_type: FlagsmithValueType::String,
378+
},
379+
),
380+
SDKTrait::new_with_transient(
381+
transient_trait_key.to_string(),
382+
FlagsmithValue {
383+
value: trait_value.to_string(),
384+
value_type: FlagsmithValueType::String,
385+
},
386+
true,
387+
),
388+
];
389+
flagsmith
390+
.get_identity_flags(identifier, Some(traits), None)
391+
.unwrap()
392+
.all_flags();
393+
394+
// Then
395+
api_mock.assert();
396+
}
397+
398+
#[rstest]
399+
fn test_get_identity_flags_calls_api_when_no_local_environment_with_transient_identity(
400+
mock_server: MockServer,
401+
identities_json: serde_json::Value,
402+
) {
403+
// Given
404+
let identifier = "test_identity";
405+
let trait_key = "trait_key1";
406+
let trait_value = "trai_value1";
407+
408+
let api_mock = mock_server.mock(|when, then| {
409+
when.method(POST)
410+
.path("/api/v1/identities/")
411+
.header("X-Environment-Key", ENVIRONMENT_KEY)
412+
.header("content-type", "application/json")
413+
.json_body(serde_json::json!({
414+
"identifier": identifier,
415+
"traits": [{"trait_key":trait_key, "trait_value": trait_value, "transient": false}],
416+
"transient": true,
417+
}));
418+
then.status(200).json_body(identities_json);
419+
});
420+
let url = mock_server.url("/api/v1/");
421+
let flagsmith_options = FlagsmithOptions {
422+
api_url: url,
423+
..Default::default()
424+
};
425+
let flagsmith = Flagsmith::new(ENVIRONMENT_KEY.to_string(), flagsmith_options);
426+
427+
// When
428+
let traits = vec![SDKTrait::new(
429+
trait_key.to_string(),
430+
FlagsmithValue {
431+
value: trait_value.to_string(),
432+
value_type: FlagsmithValueType::String,
433+
},
434+
)];
435+
flagsmith
436+
.get_identity_flags(identifier, Some(traits), Some(true))
437+
.unwrap()
438+
.all_flags();
439+
440+
// Then
441+
api_mock.assert();
442+
}
443+
444+
335445
#[rstest]
336446
fn test_default_flag_is_not_used_when_environment_flags_returned(
337447
mock_server: MockServer,
@@ -414,7 +524,8 @@ fn test_default_flag_is_not_used_when_identity_flags_returned(
414524
.header("X-Environment-Key", ENVIRONMENT_KEY)
415525
.json_body(serde_json::json!({
416526
"identifier": identifier,
417-
"traits": []
527+
"traits": [],
528+
"transient": false,
418529
}));
419530
then.status(200).json_body(identities_json);
420531
});
@@ -427,7 +538,9 @@ fn test_default_flag_is_not_used_when_identity_flags_returned(
427538
let flagsmith = Flagsmith::new(ENVIRONMENT_KEY.to_string(), flagsmith_options);
428539

429540
// When
430-
let flags = flagsmith.get_identity_flags(identifier, None).unwrap();
541+
let flags = flagsmith
542+
.get_identity_flags(identifier, None, None)
543+
.unwrap();
431544
let flag = flags.get_flag(fixtures::FEATURE_1_NAME).unwrap();
432545
// Then
433546
assert_eq!(flag.feature_name, fixtures::FEATURE_1_NAME);
@@ -455,7 +568,8 @@ fn test_default_flag_is_used_when_no_matching_identity_flags_returned(
455568
.header("X-Environment-Key", ENVIRONMENT_KEY)
456569
.json_body(serde_json::json!({
457570
"identifier": identifier,
458-
"traits": []
571+
"traits": [],
572+
"transient": false,
459573
}));
460574
then.status(200).json_body(identities_json);
461575
});
@@ -468,7 +582,9 @@ fn test_default_flag_is_used_when_no_matching_identity_flags_returned(
468582
let flagsmith = Flagsmith::new(ENVIRONMENT_KEY.to_string(), flagsmith_options);
469583

470584
// When
471-
let flags = flagsmith.get_identity_flags(identifier, None).unwrap();
585+
let flags = flagsmith
586+
.get_identity_flags(identifier, None, None)
587+
.unwrap();
472588
let flag = flags.get_flag("feature_that_does_not_exists").unwrap();
473589
// Then
474590
assert_eq!(flag.is_default, true);
@@ -526,7 +642,8 @@ fn test_default_flags_are_used_if_api_error_and_default_flag_handler_given_for_i
526642
.header("X-Environment-Key", ENVIRONMENT_KEY)
527643
.json_body(serde_json::json!({
528644
"identifier": identifier,
529-
"traits": []
645+
"traits": [],
646+
"transient": false,
530647
}));
531648
then.status(200).json_body({});
532649
});
@@ -539,7 +656,9 @@ fn test_default_flags_are_used_if_api_error_and_default_flag_handler_given_for_i
539656
let flagsmith = Flagsmith::new(ENVIRONMENT_KEY.to_string(), flagsmith_options);
540657

541658
// When
542-
let flags = flagsmith.get_identity_flags(identifier, None).unwrap();
659+
let flags = flagsmith
660+
.get_identity_flags(identifier, None, None)
661+
.unwrap();
543662
let flag = flags.get_flag("feature_that_does_not_exists").unwrap();
544663
// Then
545664
assert_eq!(flag.is_default, true);

0 commit comments

Comments
 (0)