Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add support for namePrefix, tags and project filters #54

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ murmur3 = "0.5.1"
rand = "0.8.4"
rustversion = "1.0.7"
serde_json = "1.0.68"
serde_qs = "0.12.0"
serde_plain = "1.0.0"
surf = { version = "2.3.1", optional = true }

Expand Down
81 changes: 77 additions & 4 deletions src/api.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
// Copyright 2020 Cognite AS
//! <https://docs.getunleash.io/api/client/features>
use serde_qs;
use std::collections::HashMap;
use std::default::Default;
use std::fmt::Display;

use chrono::Utc;
use serde::{Deserialize, Serialize};
Expand All @@ -13,9 +15,53 @@ pub struct Features {
pub features: Vec<Feature>,
}

#[derive(Debug, PartialEq, Clone)]
pub struct TagFilter {
pub name: String,
pub value: String,
}

impl Display for TagFilter {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "{}:{}", self.name, self.value)
}
}

#[derive(Serialize, Debug, PartialEq, Clone)]
pub struct FeaturesQuery {
pub project: Option<String>,
#[serde(rename = "namePrefix")]
pub name_prefix: Option<String>,
#[serde(rename = "tag")]
pub tags: Option<Vec<String>>,
}

impl FeaturesQuery {
pub fn new(
project: Option<String>,
name_prefix: Option<String>,
tags: &Option<Vec<TagFilter>>,
) -> Self {
FeaturesQuery {
project,
name_prefix,
tags: tags
.as_ref()
.map(|tags| tags.iter().map(ToString::to_string).collect()),
}
}
}

impl Features {
pub fn endpoint(api_url: &str) -> String {
format!("{}/client/features", api_url)
pub fn endpoint(api_url: &str, query: Option<&FeaturesQuery>) -> String {
let url = format!("{}/client/features", api_url);

let url = match query {
Some(query) => format!("{}?{}", url, serde_qs::to_string(query).unwrap()),
None => url,
};

url
}
}

Expand Down Expand Up @@ -133,7 +179,8 @@ pub struct MetricsBucket {

#[cfg(test)]
mod tests {
use super::Registration;
use super::{Features, Registration, TagFilter};
use crate::api::FeaturesQuery;

#[test]
fn parse_reference_doc() -> Result<(), serde_json::Error> {
Expand Down Expand Up @@ -209,7 +256,7 @@ mod tests {
]
}
"#;
let parsed: super::Features = serde_json::from_str(data)?;
let parsed: Features = serde_json::from_str(data)?;
assert_eq!(1, parsed.version);
Ok(())
}
Expand All @@ -224,4 +271,30 @@ mod tests {
..Default::default()
};
}

#[test]
fn test_features_endpoint() {
let endpoint = Features::endpoint(
"http://host.example.com:1234/api",
Some(&FeaturesQuery::new(
Some("myproject".into()),
Some("prefix".into()),
&Some(vec![
TagFilter {
name: "simple".into(),
value: "taga".into(),
},
TagFilter {
name: "simple".into(),
value: "tagb".into(),
},
]),
)),
);

assert_eq!(
"http://host.example.com:1234/api/client/features?project=myproject&namePrefix=prefix&tag[0]=simple%3Ataga&tag[1]=simple%3Atagb",
endpoint
);
}
}
4 changes: 2 additions & 2 deletions src/bin/dump-features.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@ use unleash_api_client::http;
fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync + 'static>> {
task::block_on(async {
let config = EnvironmentConfig::from_env()?;
let endpoint = api::Features::endpoint(&config.api_url);
let endpoint = api::Features::endpoint(&config.api_url, None);
let client: http::HTTP<surf::Client> =
http::HTTP::new(config.app_name, config.instance_id, config.secret)?;
let res: api::Features = client.get(&endpoint).recv_json().await?;
let res: api::Features = client.get_json(&endpoint).await?;
dbg!(res);
Ok(())
})
Expand Down
134 changes: 129 additions & 5 deletions src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@ use rand::Rng;
use serde::de::DeserializeOwned;
use serde::Serialize;

use crate::api::{self, Feature, Features, Metrics, MetricsBucket, Registration};
use crate::api::{
self, Feature, Features, FeaturesQuery, Metrics, MetricsBucket, Registration, TagFilter,
};
use crate::context::Context;
use crate::http::{HttpClient, HTTP};
use crate::strategy;
Expand Down Expand Up @@ -57,6 +59,9 @@ pub struct ClientBuilder {
disable_metric_submission: bool,
enable_str_features: bool,
interval: u64,
project_name: Option<String>,
name_prefix: Option<String>,
tags: Option<Vec<TagFilter>>,
strategies: HashMap<String, strategy::Strategy>,
}

Expand All @@ -75,6 +80,9 @@ impl ClientBuilder {
Ok(Client {
api_url: api_url.into(),
app_name: app_name.into(),
project_name: self.project_name,
name_prefix: self.name_prefix,
tags: self.tags,
disable_metric_submission: self.disable_metric_submission,
enable_str_features: self.enable_str_features,
instance_id: instance_id.into(),
Expand All @@ -86,6 +94,30 @@ impl ClientBuilder {
})
}

/// Only fetch feature toggles belonging to the specified project
///
/// <https://docs.getunleash.io/reference/api/legacy/unleash/client/features#filter-feature-toggles>
pub fn with_project_name(mut self, project_name: String) -> Self {
teqm marked this conversation as resolved.
Show resolved Hide resolved
self.project_name = Some(project_name);
self
}

/// Only fetch feature toggles with the provided name prefix
///
/// <https://docs.getunleash.io/reference/api/legacy/unleash/client/features#filter-feature-toggles>
pub fn with_name_prefix(mut self, name_prefix: String) -> Self {
self.name_prefix = Some(name_prefix);
self
}

/// Only fetch feature toggles tagged with the list of tags
///
/// <https://docs.getunleash.io/reference/api/legacy/unleash/client/features#filter-feature-toggles>
pub fn with_tags(mut self, tags: Vec<TagFilter>) -> Self {
self.tags = Some(tags);
self
}

pub fn disable_metric_submission(mut self) -> Self {
self.disable_metric_submission = true;
self
Expand Down Expand Up @@ -114,11 +146,13 @@ impl Default for ClientBuilder {
enable_str_features: false,
interval: 15000,
strategies: Default::default(),
project_name: None,
name_prefix: None,
tags: None,
};
result
.strategy("default", Box::new(&strategy::default))
.strategy("applicationHostname", Box::new(&strategy::hostname))
.strategy("default", Box::new(&strategy::default))
.strategy("gradualRolloutRandom", Box::new(&strategy::random))
.strategy("gradualRolloutSessionId", Box::new(&strategy::session_id))
.strategy("gradualRolloutUserId", Box::new(&strategy::user_id))
Expand Down Expand Up @@ -174,6 +208,9 @@ where
{
api_url: String,
app_name: String,
project_name: Option<String>,
name_prefix: Option<String>,
tags: Option<Vec<TagFilter>>,
disable_metric_submission: bool,
enable_str_features: bool,
instance_id: String,
Expand Down Expand Up @@ -694,7 +731,14 @@ where
/// stop_poll is called().
pub async fn poll_for_updates(&self) {
// TODO: add an event / pipe to permit immediate exit.
let endpoint = Features::endpoint(&self.api_url);
let endpoint = Features::endpoint(
&self.api_url,
Some(&FeaturesQuery::new(
self.project_name.clone(),
self.name_prefix.clone(),
&self.tags,
)),
);
let metrics_endpoint = Metrics::endpoint(&self.api_url);
self.polling.store(true, Ordering::Relaxed);
loop {
Expand Down Expand Up @@ -823,9 +867,12 @@ mod tests {
use serde::{Deserialize, Serialize};

use super::{ClientBuilder, Variant};
use crate::api::{self, Feature, Features, Strategy};
use crate::api::{self, Feature, Features, Strategy, TagFilter};
use crate::context::{Context, IPAddress};
use crate::strategy;
use crate::http::HTTP;
use crate::{strategy, Client};
use arc_swap::ArcSwapOption;
use std::fmt::{Debug, Formatter};

cfg_if::cfg_if! {
if #[cfg(feature = "surf")] {
Expand Down Expand Up @@ -1319,4 +1366,81 @@ mod tests {
assert_eq!(variant2, c.get_variant_str("two", &session1));
assert_eq!(variant1, c.get_variant_str("two", &host1));
}

#[test]
fn test_builder() {
#[derive(Debug, Deserialize, Serialize, Enum, Clone)]
enum NoFeatures {}

let api_url = "http://127.0.0.1:1234/";
let instance_id = "test";
let app_name = "foo";
let project_name = "myproject".to_string();
let name_prefix = "prefix".to_string();
let tags = vec![
TagFilter {
name: "simple".into(),
value: "taga".into(),
},
TagFilter {
name: "simple".into(),
value: "tagb".into(),
},
];

let client: Client<NoFeatures, HttpClient> = Client {
api_url: api_url.into(),
disable_metric_submission: false,
enable_str_features: false,
instance_id: instance_id.into(),
interval: 15000,
polling: Default::default(),
http: HTTP::new(app_name.into(), instance_id.into(), None).unwrap(),
strategies: Default::default(),
project_name: Some(project_name.clone()),
name_prefix: Some(name_prefix.clone()),
tags: Some(tags.clone()),
app_name: app_name.into(),
cached_state: ArcSwapOption::from(None),
};

let client_from_builder = ClientBuilder::default()
.with_project_name(project_name)
.with_name_prefix(name_prefix)
.with_tags(tags)
.into_client::<NoFeatures, HttpClient>(api_url, app_name, instance_id, None)
.unwrap();

impl PartialEq for Client<NoFeatures, HttpClient> {
fn eq(&self, other: &Self) -> bool {
self.api_url == other.api_url
&& self.app_name == other.app_name
&& self.project_name == other.project_name
&& self.name_prefix == other.name_prefix
&& self.tags == other.tags
&& self.disable_metric_submission == other.disable_metric_submission
&& self.enable_str_features == other.enable_str_features
&& self.instance_id == other.instance_id
&& self.interval == other.interval
}
}

impl Debug for Client<NoFeatures, HttpClient> {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Client<NoFeatures, HttpClient>")
.field("api_url", &self.api_url)
.field("app_name", &self.app_name)
.field("project_name", &self.project_name)
.field("name_prefix", &self.name_prefix)
.field("tags", &self.tags)
.field("disable_metric_submission", &self.disable_metric_submission)
.field("enable_str_features", &self.enable_str_features)
.field("instance_id", &self.instance_id)
.field("interval", &self.interval)
.finish()
}
}

assert_eq!(client, client_from_builder);
}
}