Skip to content
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
2 changes: 2 additions & 0 deletions .cargo/config.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[target.wasm32-wasip1]
runner = "viceroy run -C fastly.unit-test.toml -- "
2 changes: 2 additions & 0 deletions .cargo/nextest.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[profile.default]
platform = { target_arch = "wasm32" }
17 changes: 14 additions & 3 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,22 @@ 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"

[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
reqwest = { version = "0.11", features = ["json", "blocking"] }

[target.'cfg(target_arch = "wasm32")'.dependencies]
reqwest = { version = "0.11", features = ["json"] }
fastly = { version = "^0.11.5" }

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

[target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies]
httpmock = "0.6"
15 changes: 15 additions & 0 deletions fastly.unit-test.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# This file describes a Fastly Compute@Edge package. To learn more visit:
# https://developer.fastly.com/reference/fastly-toml/

authors = [
"Gagan Trivedi <[email protected]>",
"Kim Gustyr <[email protected]>",
]
description = "Flagsmith Rust Client."
language = "rust"
manifest_version = 2
name = "flagsmith-rust-client"
service_id = ""

[scripts]
build = "cargo build --release --target wasm32-wasip1 --color always"
11 changes: 11 additions & 0 deletions scripts/fastly-unit-test.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
#!/usr/bin/env bash

function run-specs() {
cargo nextest run --target wasm32-wasip1 flagsmith::client
}

if [ "$0" = "${BASH_SOURCE[0]}" ]; then
set -eo pipefail
run-specs "${@:-}"
exit $?
fi
19 changes: 19 additions & 0 deletions scripts/test.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
#!/usr/bin/env bash

function run-default-tests() {
echo "Running default tests"
cargo test
}

function run-fastly-tests() {
echo "Running fastly/wasm tests"
./scripts/fastly-unit-test.sh
}

if [ "$0" = "${BASH_SOURCE[0]}" ]; then
set -eo pipefail

run-default-tests && run-fastly-tests

exit $?
fi
23 changes: 11 additions & 12 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,22 +72,22 @@ 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");
}
}

#[cfg(test)]
#[cfg(all(test, not(target_arch = "wasm32")))]
mod tests {
use super::*;
use httpmock::prelude::*;
Expand Down
90 changes: 90 additions & 0 deletions src/flagsmith/client/blocking_client.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
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()
}

fn as_u16(&self) -> u16 {
self.as_u16()
}
}

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,
}
}
}
76 changes: 76 additions & 0 deletions src/flagsmith/client/client.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
use std::time::Duration;

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

#[cfg(not(target_arch = "wasm32"))]
use crate::flagsmith::client::blocking_client::BlockingClient;
#[cfg(target_arch = "wasm32")]
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;

/// Exists for Unit Testing purposes
#[allow(dead_code)]
fn as_u16(&self) -> u16;
}

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(target_arch = "wasm32"))]
pub inner: BlockingClient,

#[cfg(target_arch = "wasm32")]
pub inner: FastlyClient,
}

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

#[cfg(target_arch = "wasm32")]
pub fn new(headers: HeaderMap, timeout: Duration) -> Self {
Self {
inner: FastlyClient::new(headers, timeout),
}
}
}
Loading