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 1 commit
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
2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,8 @@ futures = "0.3.17"
maplit = "1.0.2"
num_cpus = "1.13.0"
simple_logger = "2.1.0"
serde_qs = "*"
serde_urlencoded = "*"
teqm marked this conversation as resolved.
Show resolved Hide resolved

[dev-dependencies.tokio]
features = ["rt-multi-thread", "macros", "time"]
Expand Down
101 changes: 99 additions & 2 deletions src/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,72 @@ pub struct Features {
pub features: Vec<Feature>,
}

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

impl TagFilter {
teqm marked this conversation as resolved.
Show resolved Hide resolved
pub fn format(&self) -> String {
format!("{}:{}", self.name, self.value)
}
}

#[derive(Serialize, Debug)]
teqm marked this conversation as resolved.
Show resolved Hide resolved
struct FeaturesQuery {
pub project: Option<String>,
#[serde(rename = "namePrefix")]
pub name_prefix: Option<String>,
#[serde(rename = "tag")]
pub tags: Option<Vec<String>>,
}

impl Features {
pub fn endpoint(api_url: &str) -> String {
format!("{}/client/features", api_url)
}

#[cfg(feature = "surf")]
pub fn query(
project: Option<String>,
name_prefix: Option<String>,
tags: &Option<Vec<TagFilter>>,
) -> impl Serialize + std::fmt::Debug {
FeaturesQuery {
project,
name_prefix,
tags: match tags {
Some(tags) => Some(tags.iter().map(|tag| tag.format()).collect()),
None => None,
},
}
}

#[cfg(feature = "reqwest")]
pub fn query(
project: Option<String>,
name_prefix: Option<String>,
tags: &Option<Vec<TagFilter>>,
) -> impl Serialize + std::fmt::Debug {
let mut query = vec![];

if let Some(project) = project {
query.push(("project", project))
}

if let Some(name_prefix) = name_prefix {
query.push(("namePrefix", name_prefix))
}

if let Some(tags) = tags {
for tag in tags.iter() {
query.push(("tag", tag.format()))
}
}

query
}
}

#[derive(Clone, Serialize, Deserialize, Debug)]
Expand Down Expand Up @@ -133,7 +195,11 @@ pub struct MetricsBucket {

#[cfg(test)]
mod tests {
use super::Registration;
use super::{Features, Registration, TagFilter};
#[cfg(feature = "surf")]
use serde_qs::to_string;
#[cfg(feature = "reqwest")]
use serde_urlencoded::to_string;

#[test]
fn parse_reference_doc() -> Result<(), serde_json::Error> {
Expand Down Expand Up @@ -209,7 +275,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 +290,35 @@ mod tests {
..Default::default()
};
}

#[test]
fn test_features_query() {
let query = Features::query(
Some("myproject".into()),
Some("prefix".into()),
&Some(vec![
TagFilter {
name: "simple".into(),
value: "taga".into(),
},
TagFilter {
name: "simple".into(),
value: "tagb".into(),
},
]),
);

let serialized = to_string(&query).unwrap();

#[cfg(feature = "surf")]
assert_eq!(
"project=myproject&namePrefix=prefix&tag[0]=simple%3Ataga&tag[1]=simple%3Atagb",
serialized
);
#[cfg(feature = "reqwest")]
assert_eq!(
"project=myproject&namePrefix=prefix&tag=simple%3Ataga&tag=simple%3Atagb",
serialized
);
}
}
2 changes: 1 addition & 1 deletion src/bin/dump-features.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync + 'static>> {
let endpoint = api::Features::endpoint(&config.api_url);
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, None::<&Vec<()>>).await?;
dbg!(res);
Ok(())
})
Expand Down
126 changes: 121 additions & 5 deletions src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ 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, Metrics, MetricsBucket, Registration, TagFilter};
use crate::context::Context;
use crate::http::{HttpClient, HTTP};
use crate::strategy;
Expand Down Expand Up @@ -57,6 +57,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 +78,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 +92,21 @@ impl ClientBuilder {
})
}

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
}

pub fn with_name_prefix(mut self, name_prefix: String) -> Self {
self.name_prefix = Some(name_prefix);
self
}

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 +135,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 +197,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 @@ -697,7 +723,17 @@ where
self.polling.store(true, Ordering::Relaxed);
loop {
debug!("poll: retrieving features");
let res = self.http.get_json(&endpoint).await;
let res = self
.http
.get_json(
&endpoint,
Some(&Features::query(
self.project_name.clone(),
self.name_prefix.clone(),
&self.tags,
)),
)
.await;
if let Ok(res) = res {
let features: Features = res;
match self.memoize(features.features) {
Expand Down Expand Up @@ -821,9 +857,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 @@ -1317,4 +1356,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);
}
}
22 changes: 18 additions & 4 deletions src/http.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,14 +43,28 @@ where
}

/// Perform a GET. Returns errors per HttpClient::get.
pub fn get(&self, uri: &str) -> C::RequestBuilder {
pub fn get(
&self,
uri: &str,
query: Option<&impl Serialize>,
) -> Result<C::RequestBuilder, C::Error> {
let request = self.client.get(uri);
self.attach_headers(request)

let request = match query {
Some(query) => C::query(request, query)?,
None => request,
};

Ok(self.attach_headers(request))
}

/// Make a get request and parse into JSON
pub async fn get_json<T: DeserializeOwned>(&self, endpoint: &str) -> Result<T, C::Error> {
C::get_json(self.get(endpoint)).await
pub async fn get_json<T: DeserializeOwned>(
&self,
endpoint: &str,
query: Option<&impl Serialize>,
) -> Result<T, C::Error> {
C::get_json(self.get(endpoint, query)?).await
}

/// Perform a POST. Returns errors per HttpClient::post.
Expand Down
7 changes: 7 additions & 0 deletions src/http/reqwest.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,13 @@ impl HttpClient for reqwest::Client {
builder.header(key.clone(), value)
}

fn query(
builder: Self::RequestBuilder,
query: &impl Serialize,
) -> Result<Self::RequestBuilder, Self::Error> {
Ok(builder.query(query))
}

async fn get_json<T: DeserializeOwned>(req: Self::RequestBuilder) -> Result<T, Self::Error> {
req.send().await?.json::<T>().await
}
Expand Down
Loading