Skip to content
Open
Show file tree
Hide file tree
Changes from 4 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
14 changes: 12 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
[package]
name = "flagsmith"
version = "2.0.0"
authors = ["Gagan Trivedi <[email protected]>", "Kim Gustyr <[email protected]>"]
authors = [
"Gagan Trivedi <[email protected]>",
"Kim Gustyr <[email protected]>",
]
edition = "2021"
license = "BSD-3-Clause"
description = "Flagsmith SDK for Rust"
Expand All @@ -16,14 +19,21 @@ keywords = ["Flagsmith", "feature-flag", "remote-config"]
[dependencies]
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
reqwest = { version = "0.11", features = ["json", "blocking"] }
url = "2.1"
chrono = { version = "0.4" }
log = "0.4"
flume = "0.10.14"

flagsmith-flag-engine = "0.4.0"

reqwest = { version = "0.11", features = ["json"] }
fastly = { version = "^0.11.5", optional = true }

[dev-dependencies]
httpmock = "0.6"
rstest = "0.12.0"

[features]
default = ["blocking"]
non_blocking = ["fastly"]
blocking = ["reqwest/blocking"]
21 changes: 10 additions & 11 deletions src/flagsmith/analytics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ use serde_json;
use std::{collections::HashMap, thread};

use std::sync::{Arc, RwLock};

use crate::flagsmith::client::client::{ClientLike, ClientRequestBuilder, Method, SafeClient};
static ANALYTICS_TIMER_IN_MILLI: u64 = 10 * 1000;

#[derive(Clone, Debug)]
Expand All @@ -21,11 +23,8 @@ impl AnalyticsProcessor {
timer: Option<u64>,
) -> Self {
let (tx, rx) = flume::unbounded();
let client = reqwest::blocking::Client::builder()
.default_headers(headers)
.timeout(timeout)
.build()
.unwrap();
let client = SafeClient::new(headers.clone(), timeout);

let analytics_endpoint = format!("{}analytics/flags/", api_url);
let timer = timer.unwrap_or(ANALYTICS_TIMER_IN_MILLI);

Expand Down Expand Up @@ -73,16 +72,16 @@ impl AnalyticsProcessor {
}
}

fn flush(
client: &reqwest::blocking::Client,
analytics_data: &HashMap<String, u32>,
analytics_endpoint: &str,
) {
fn flush(client: &SafeClient, analytics_data: &HashMap<String, u32>, analytics_endpoint: &str) {
if analytics_data.len() == 0 {
return;
}
let body = serde_json::to_string(&analytics_data).unwrap();
let resp = client.post(analytics_endpoint).body(body).send();
let req = client
.inner
.request(Method::POST, analytics_endpoint.to_string())
.with_body(body);
let resp = req.send();
if resp.is_err() {
warn!("Failed to send analytics data");
}
Expand Down
86 changes: 86 additions & 0 deletions src/flagsmith/client/blocking_client.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
use std::time::Duration;

use serde::de::DeserializeOwned;

use crate::flagsmith::client::client::Method;
use crate::flagsmith::client::client::{
ClientLike, ClientRequestBuilder, ClientResponse, ResponseStatusCode,
};

impl From<Method> for reqwest::Method {
fn from(value: Method) -> Self {
match value {
Method::OPTIONS => reqwest::Method::OPTIONS,
Method::GET => reqwest::Method::GET,
Method::POST => reqwest::Method::POST,
Method::PUT => reqwest::Method::PUT,
Method::DELETE => reqwest::Method::DELETE,
Method::HEAD => reqwest::Method::HEAD,
Method::TRACE => reqwest::Method::TRACE,
Method::CONNECT => reqwest::Method::CONNECT,
Method::PATCH => reqwest::Method::PATCH,
}
}
}

#[derive(Clone)]
pub struct BlockingClient {
reqwest_client: reqwest::blocking::Client,
}

impl ResponseStatusCode for reqwest::StatusCode {
fn is_success(&self) -> bool {
self.is_success()
}
}

impl ClientResponse for reqwest::blocking::Response {
fn status(&self) -> impl ResponseStatusCode {
self.status()
}

fn text(self) -> Result<String, ()> {
match self.text() {
Ok(res) => Ok(res),
Err(_) => Err(()),
}
}

fn json<T: DeserializeOwned>(self) -> Result<T, ()> {
match self.json() {
Ok(res) => Ok(res),
Err(_) => Err(()),
}
}
}

impl ClientRequestBuilder for reqwest::blocking::RequestBuilder {
fn with_body(self, body: String) -> Self {
self.body(body)
}

fn send(self) -> Result<impl ClientResponse, ()> {
match self.send() {
Ok(res) => Ok(res),
Err(_) => Err(()),
}
}
}

impl ClientLike for BlockingClient {
fn request(&self, method: super::client::Method, url: String) -> impl ClientRequestBuilder {
self.reqwest_client.request(method.into(), url)
}

fn new(headers: reqwest::header::HeaderMap, timeout: Duration) -> Self {
let inner = reqwest::blocking::Client::builder()
.default_headers(headers)
.timeout(timeout)
.build()
.unwrap();

Self {
reqwest_client: inner,
}
}
}
72 changes: 72 additions & 0 deletions src/flagsmith/client/client.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
use std::time::Duration;

use reqwest::header::HeaderMap;
use serde::de::DeserializeOwned;

#[cfg(not(feature = "non_blocking"))]
use crate::flagsmith::client::blocking_client::BlockingClient;
#[cfg(feature = "non_blocking")]
use crate::flagsmith::client::fastly_client::FastlyClient;

pub enum Method {
OPTIONS,
GET,
POST,
PUT,
DELETE,
HEAD,
TRACE,
CONNECT,
PATCH,
}

pub trait ResponseStatusCode {
fn is_success(&self) -> bool;
}

pub trait ClientRequestBuilder {
fn with_body(self, body: String) -> Self;

// TODO return type
fn send(self) -> Result<impl ClientResponse, ()>;
}

pub trait ClientResponse {
fn status(&self) -> impl ResponseStatusCode;

// TODO return error type
fn text(self) -> Result<String, ()>;

// TODO return error type
fn json<T: DeserializeOwned>(self) -> Result<T, ()>;
}

pub trait ClientLike {
fn new(headers: HeaderMap, timeout: Duration) -> Self;
fn request(&self, method: Method, url: String) -> impl ClientRequestBuilder;
}

#[derive(Clone)]
pub struct SafeClient {
#[cfg(not(feature = "non_blocking"))]
pub inner: BlockingClient,

#[cfg(feature = "non_blocking")]
pub inner: FastlyClient,
}

impl SafeClient {
#[cfg(not(feature = "non_blocking"))]
pub fn new(headers: HeaderMap, timeout: Duration) -> Self {
Self {
inner: BlockingClient::new(headers, timeout),
}
}

#[cfg(feature = "non_blocking")]
pub fn new(headers: HeaderMap, timeout: Duration) -> Self {
Self {
inner: FastlyClient::new(headers, timeout),
}
}
}
115 changes: 115 additions & 0 deletions src/flagsmith/client/fastly_client.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
use std::io::Read;

use reqwest::header::HeaderMap;

use crate::flagsmith::{
self,
client::client::{
ClientLike, ClientRequestBuilder, ClientResponse, Method, ResponseStatusCode,
},
};
use fastly::http;

impl From<Method> for http::Method {
fn from(value: Method) -> Self {
match value {
Method::OPTIONS => http::Method::OPTIONS,
Method::GET => http::Method::GET,
Method::POST => http::Method::POST,
Method::PUT => http::Method::PUT,
Method::DELETE => http::Method::DELETE,
Method::HEAD => http::Method::HEAD,
Method::TRACE => http::Method::TRACE,
Method::CONNECT => http::Method::CONNECT,
Method::PATCH => http::Method::PATCH,
}
}
}

impl super::client::ResponseStatusCode for http::StatusCode {
fn is_success(&self) -> bool {
let raw = self.as_u16();

raw >= 200 && raw <= 299
}
}

impl super::client::ClientResponse for http::Response {
fn status(&self) -> impl ResponseStatusCode {
self.get_status()
}

fn text(mut self) -> Result<String, ()> {
let mut buf = String::new();
if self.get_body_mut().read_to_string(&mut buf).is_ok() {
Ok(buf)
} else {
Err(())
}
}

fn json<T: serde::de::DeserializeOwned>(mut self) -> Result<T, ()> {
match self.take_body_json::<T>() {
Ok(res) => Ok(res),
Err(_) => Err(()),
}
}
}

/// Wrapper to help with abstraction of the client interface.
struct FastlyRequestBuilder {
backend: String,
request: Result<http::Request, ()>,
}

impl ClientRequestBuilder for FastlyRequestBuilder {
fn with_body(mut self, body: String) -> Self {
if let Ok(ref mut req) = self.request {
req.set_body(body);
}

self
}

fn send(self) -> Result<impl ClientResponse, ()> {
if let Ok(req) = self.request {
match req.send(self.backend) {
Ok(res) => Ok(res),
Err(_) => Err(()),
}
} else {
Err(())
}
}
}

#[derive(Clone)]
pub struct FastlyClient {
default_headers: HeaderMap,
}

impl ClientLike for FastlyClient {
fn new(headers: HeaderMap, _timeout: std::time::Duration) -> Self {
Self {
default_headers: headers,
}
}

fn request(&self, method: super::client::Method, url: String) -> impl ClientRequestBuilder {
let mut req = http::Request::new(
<flagsmith::client::client::Method as Into<http::Method>>::into(method),
url,
);

for (name, value) in &self.default_headers {
if let Ok(header_val) = value.to_str() {
req.append_header(name.to_string(), header_val);
}
}

FastlyRequestBuilder {
backend: "flagsmith".to_string(),
request: Ok(req),
}
}
}
5 changes: 5 additions & 0 deletions src/flagsmith/client/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#[cfg(not(feature = "non_blocking"))]
pub mod blocking_client;
pub mod client;
#[cfg(feature = "non_blocking")]
pub mod fastly_client;
Loading