From ab97cbec197789c882bff7f1e762925865af8c81 Mon Sep 17 00:00:00 2001 From: "Javier G. Sogo" Date: Mon, 16 Dec 2024 17:09:34 +0100 Subject: [PATCH 1/4] New AppConfigurationClient trait + IBMCloud implementation Signed-off-by: Javier G. Sogo --- examples/demo.rs | 12 +- src/client/app_configuration_client.rs | 315 +------------------- src/client/app_configuration_ibm_cloud.rs | 331 ++++++++++++++++++++++ src/client/feature_proxy.rs | 4 +- src/client/mod.rs | 2 + src/client/property_proxy.rs | 4 +- src/lib.rs | 6 +- src/tests/mod.rs | 6 +- src/tests/test_get_feature.rs | 8 +- src/tests/test_get_feature_ids.rs | 4 +- src/tests/test_get_property.rs | 6 +- src/tests/test_get_property_ids.rs | 4 +- src/tests/test_using_example_data.rs | 6 +- tests/test_app_config.rs | 15 +- 14 files changed, 385 insertions(+), 338 deletions(-) create mode 100644 src/client/app_configuration_ibm_cloud.rs diff --git a/examples/demo.rs b/examples/demo.rs index c88f5be..824e8aa 100644 --- a/examples/demo.rs +++ b/examples/demo.rs @@ -15,7 +15,8 @@ use std::{collections::HashMap, env, thread, time::Duration}; use appconfiguration_rust_sdk::{ - AppConfigurationClient, AttrValue, Entity, Feature, Property, Value, + AppConfigurationClient, AppConfigurationClientIBMCloud, AttrValue, Entity, Feature, Property, + Value, }; use dotenvy::dotenv; use std::error::Error; @@ -50,8 +51,13 @@ fn main() -> std::result::Result<(), Box> { let feature_id = env::var("FEATURE_ID").expect("FEATURE_ID should be set."); let property_id = env::var("PROPERTY_ID").expect("PROPERTY_ID should be set."); - let client = - AppConfigurationClient::new(&apikey, ®ion, &guid, &environment_id, &collection_id)?; + let client = AppConfigurationClientIBMCloud::new( + &apikey, + ®ion, + &guid, + &environment_id, + &collection_id, + )?; let entity = CustomerEntity { id: "user123".to_string(), diff --git a/src/client/app_configuration_client.rs b/src/client/app_configuration_client.rs index f16ce36..a84c025 100644 --- a/src/client/app_configuration_client.rs +++ b/src/client/app_configuration_client.rs @@ -12,316 +12,23 @@ // See the License for the specific language governing permissions and // limitations under the License. -use crate::client::cache::ConfigurationSnapshot; +use crate::Result; + +use crate::client::feature_proxy::FeatureProxy; use crate::client::feature_snapshot::FeatureSnapshot; -pub use crate::client::feature_proxy::FeatureProxy; -use crate::client::http; +use crate::client::property_proxy::PropertyProxy; use crate::client::property_snapshot::PropertySnapshot; -pub use crate::client::property_proxy::PropertyProxy; -use crate::errors::{ConfigurationAccessError, Error, Result}; -use crate::models::Segment; -use std::collections::{HashMap, HashSet}; -use std::net::TcpStream; -use std::sync::{Arc, Mutex}; -use std::thread; -use std::time::Duration; -use tungstenite::stream::MaybeTlsStream; -use tungstenite::Message; -use tungstenite::WebSocket; - -/// App Configuration client for browsing, and evaluating features and -/// properties. -#[derive(Debug)] -pub struct AppConfigurationClient { - pub(crate) latest_config_snapshot: Arc>, - pub(crate) _thread_terminator: std::sync::mpsc::Sender<()>, -} - -impl AppConfigurationClient { - /// Creates a client to retrieve configurations for a specific collection. - /// To uniquely address a collection the following is required: - /// - `region` - /// - `guid`: Identifies an instance - /// - `environment_id` - /// - `collection_id` - /// In addition `api_key` is required for authentication - pub fn new( - apikey: &str, - region: &str, - guid: &str, - environment_id: &str, - collection_id: &str, - ) -> Result { - let access_token = http::get_access_token(&apikey)?; - - // Populate initial configuration - let latest_config_snapshot: Arc> = - Arc::new(Mutex::new(Self::get_configuration_snapshot( - &access_token, - region, - guid, - environment_id, - collection_id, - )?)); - - // start monitoring configuration - let terminator = Self::update_cache_in_background( - latest_config_snapshot.clone(), - apikey, - region, - guid, - environment_id, - collection_id, - )?; - - let client = AppConfigurationClient { - latest_config_snapshot, - _thread_terminator: terminator, - }; - - Ok(client) - } - - fn get_configuration_snapshot( - access_token: &str, - region: &str, - guid: &str, - environment_id: &str, - collection_id: &str, - ) -> Result { - let configuration = http::get_configuration( - // TODO: access_token might expire. This will cause issues with long-running apps - &access_token, - ®ion, - &guid, - &collection_id, - &environment_id, - )?; - ConfigurationSnapshot::new(environment_id, configuration) - } - - fn wait_for_configuration_update( - socket: &mut WebSocket>, - access_token: &str, - region: &str, - guid: &str, - collection_id: &str, - environment_id: &str, - ) -> Result { - loop { - // read() blocks until something happens. - match socket.read()? { - Message::Text(text) => match text.as_str() { - "test message" => {} // periodically sent by the server - _ => { - return Self::get_configuration_snapshot( - access_token, - region, - guid, - environment_id, - collection_id, - ); - } - }, - Message::Close(_) => { - return Err(Error::Other("Connection closed by the server".into())); - } - _ => {} - } - } - } - - fn update_configuration_on_change( - mut socket: WebSocket>, - latest_config_snapshot: Arc>, - access_token: String, - region: String, - guid: String, - collection_id: String, - environment_id: String, - ) -> std::sync::mpsc::Sender<()> { - let (sender, receiver) = std::sync::mpsc::channel(); - - thread::spawn(move || { - loop { - // If the sender has gone (AppConfiguration instance is dropped), then finish this thread - if let Err(e) = receiver.try_recv() { - if e == std::sync::mpsc::TryRecvError::Disconnected { - break; - } - } - - let config_snapshot = Self::wait_for_configuration_update( - &mut socket, - &access_token, - ®ion, - &guid, - &collection_id, - &environment_id, - ); - - match config_snapshot { - Ok(config_snapshot) => *latest_config_snapshot.lock()? = config_snapshot, - Err(e) => { - println!("Waiting for configuration update failed. Stopping to monitor for changes.: {e}"); - break; - } - } - } - Ok::<(), Error>(()) - }); - - sender - } - - pub fn get_feature_ids(&self) -> Result> { - Ok(self - .latest_config_snapshot - .lock()? - .features - .keys() - .cloned() - .collect()) - } - - pub fn get_feature(&self, feature_id: &str) -> Result { - let config_snapshot = self.latest_config_snapshot.lock()?; - - // Get the feature from the snapshot - let feature = config_snapshot.get_feature(feature_id)?; - - // Get the segment rules that apply to this feature - let segments = { - let all_segment_ids = feature - .segment_rules - .iter() - .flat_map(|targeting_rule| { - targeting_rule - .rules - .iter() - .flat_map(|segment| &segment.segments) - }) - .cloned() - .collect::>(); - let segments: HashMap = config_snapshot - .segments - .iter() - .filter(|&(key, _)| all_segment_ids.contains(key)) - .map(|(k, v)| (k.clone(), v.clone())) - .collect(); - - // Integrity DB check: all segment_ids should be available in the snapshot - if all_segment_ids.len() != segments.len() { - return Err(ConfigurationAccessError::MissingSegments { - resource_id: feature_id.to_string(), - } - .into()); - } - - segments - }; - - Ok(FeatureSnapshot::new(feature.clone(), segments)) - } - - /// Searches for the feature `feature_id` inside the current configured - /// collection, and environment. - /// - /// Return `Ok(feature)` if the feature exists or `Err` if it does not. - pub fn get_feature_proxy<'a>(&'a self, feature_id: &str) -> Result> { - // FIXME: there is and was no validation happening if the feature exists. - // Comments and error messages in FeatureProxy suggest that this should happen here. - // same applies for properties. - Ok(FeatureProxy::new(self, feature_id.to_string())) - } - - pub fn get_property_ids(&self) -> Result> { - Ok(self - .latest_config_snapshot - .lock() - .map_err(|_| ConfigurationAccessError::LockAcquisitionError)? - .properties - .keys() - .cloned() - .collect()) - } - - pub fn get_property(&self, property_id: &str) -> Result { - let config_snapshot = self.latest_config_snapshot.lock()?; - - // Get the property from the snapshot - let property = config_snapshot.get_property(property_id)?; - - // Get the segment rules that apply to this property - let segments = { - let all_segment_ids = property - .segment_rules - .iter() - .flat_map(|targeting_rule| { - targeting_rule - .rules - .iter() - .flat_map(|segment| &segment.segments) - }) - .cloned() - .collect::>(); - let segments: HashMap = config_snapshot - .segments - .iter() - .filter(|&(key, _)| all_segment_ids.contains(key)) - .map(|(k, v)| (k.clone(), v.clone())) - .collect(); - - // Integrity DB check: all segment_ids should be available in the snapshot - if all_segment_ids.len() != segments.len() { - // FIXME: Return some kind of DBIntegrity error - return Err(ConfigurationAccessError::MissingSegments { - resource_id: property_id.to_string(), - } - .into()); - } - segments - }; +pub trait AppConfigurationClient { + fn get_feature_ids(&self) -> Result>; - Ok(PropertySnapshot::new(property.clone(), segments)) - } + fn get_feature(&self, feature_id: &str) -> Result; - /// Searches for the property `property_id` inside the current configured - /// collection, and environment. - /// - /// Return `Ok(property)` if the feature exists or `Err` if it does not. - pub fn get_property_proxy(&self, property_id: &str) -> Result { - Ok(PropertyProxy::new(self, property_id.to_string())) - } + fn get_feature_proxy<'a>(&'a self, feature_id: &str) -> Result>; - fn update_cache_in_background( - latest_config_snapshot: Arc>, - apikey: &str, - region: &str, - guid: &str, - environment_id: &str, - collection_id: &str, - ) -> Result> { - let access_token = http::get_access_token(&apikey)?; - let (socket, _response) = http::get_configuration_monitoring_websocket( - &access_token, - ®ion, - &guid, - &collection_id, - &environment_id, - )?; + fn get_property_ids(&self) -> Result>; - let sender = Self::update_configuration_on_change( - socket, - latest_config_snapshot, - access_token, - region.to_string(), - guid.to_string(), - collection_id.to_string(), - environment_id.to_string(), - ); + fn get_property(&self, property_id: &str) -> Result; - Ok(sender) - } + fn get_property_proxy(&self, property_id: &str) -> Result; } diff --git a/src/client/app_configuration_ibm_cloud.rs b/src/client/app_configuration_ibm_cloud.rs new file mode 100644 index 0000000..e576cc0 --- /dev/null +++ b/src/client/app_configuration_ibm_cloud.rs @@ -0,0 +1,331 @@ +// (C) Copyright IBM Corp. 2024. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::client::cache::ConfigurationSnapshot; +pub use crate::client::feature_proxy::FeatureProxy; +use crate::client::feature_snapshot::FeatureSnapshot; +use crate::client::http; +pub use crate::client::property_proxy::PropertyProxy; +use crate::client::property_snapshot::PropertySnapshot; +use crate::errors::{ConfigurationAccessError, Error, Result}; +use crate::models::Segment; +use std::collections::{HashMap, HashSet}; +use std::net::TcpStream; +use std::sync::{Arc, Mutex}; +use std::thread; + +use tungstenite::stream::MaybeTlsStream; +use tungstenite::Message; +use tungstenite::WebSocket; + +use super::AppConfigurationClient; + +/// App Configuration client for browsing, and evaluating features and +/// properties. +#[derive(Debug)] +pub struct AppConfigurationClientIBMCloud { + pub(crate) latest_config_snapshot: Arc>, + pub(crate) _thread_terminator: std::sync::mpsc::Sender<()>, +} + +impl AppConfigurationClientIBMCloud { + /// Creates a client to retrieve configurations for a specific collection. + /// To uniquely address a collection the following is required: + /// - `region` + /// - `guid`: Identifies an instance + /// - `environment_id` + /// - `collection_id` + /// In addition `api_key` is required for authentication + pub fn new( + apikey: &str, + region: &str, + guid: &str, + environment_id: &str, + collection_id: &str, + ) -> Result { + let access_token = http::get_access_token(&apikey)?; + + // Populate initial configuration + let latest_config_snapshot: Arc> = + Arc::new(Mutex::new(Self::get_configuration_snapshot( + &access_token, + region, + guid, + environment_id, + collection_id, + )?)); + + // start monitoring configuration + let terminator = Self::update_cache_in_background( + latest_config_snapshot.clone(), + apikey, + region, + guid, + environment_id, + collection_id, + )?; + + let client = AppConfigurationClientIBMCloud { + latest_config_snapshot, + _thread_terminator: terminator, + }; + + Ok(client) + } + + fn get_configuration_snapshot( + access_token: &str, + region: &str, + guid: &str, + environment_id: &str, + collection_id: &str, + ) -> Result { + let configuration = http::get_configuration( + // TODO: access_token might expire. This will cause issues with long-running apps + &access_token, + ®ion, + &guid, + &collection_id, + &environment_id, + )?; + ConfigurationSnapshot::new(environment_id, configuration) + } + + fn wait_for_configuration_update( + socket: &mut WebSocket>, + access_token: &str, + region: &str, + guid: &str, + collection_id: &str, + environment_id: &str, + ) -> Result { + loop { + // read() blocks until something happens. + match socket.read()? { + Message::Text(text) => match text.as_str() { + "test message" => {} // periodically sent by the server + _ => { + return Self::get_configuration_snapshot( + access_token, + region, + guid, + environment_id, + collection_id, + ); + } + }, + Message::Close(_) => { + return Err(Error::Other("Connection closed by the server".into())); + } + _ => {} + } + } + } + + fn update_configuration_on_change( + mut socket: WebSocket>, + latest_config_snapshot: Arc>, + access_token: String, + region: String, + guid: String, + collection_id: String, + environment_id: String, + ) -> std::sync::mpsc::Sender<()> { + let (sender, receiver) = std::sync::mpsc::channel(); + + thread::spawn(move || { + loop { + // If the sender has gone (AppConfiguration instance is dropped), then finish this thread + if let Err(e) = receiver.try_recv() { + if e == std::sync::mpsc::TryRecvError::Disconnected { + break; + } + } + + let config_snapshot = Self::wait_for_configuration_update( + &mut socket, + &access_token, + ®ion, + &guid, + &collection_id, + &environment_id, + ); + + match config_snapshot { + Ok(config_snapshot) => *latest_config_snapshot.lock()? = config_snapshot, + Err(e) => { + println!("Waiting for configuration update failed. Stopping to monitor for changes.: {e}"); + break; + } + } + } + Ok::<(), Error>(()) + }); + + sender + } + + fn update_cache_in_background( + latest_config_snapshot: Arc>, + apikey: &str, + region: &str, + guid: &str, + environment_id: &str, + collection_id: &str, + ) -> Result> { + let access_token = http::get_access_token(&apikey)?; + let (socket, _response) = http::get_configuration_monitoring_websocket( + &access_token, + ®ion, + &guid, + &collection_id, + &environment_id, + )?; + + let sender = Self::update_configuration_on_change( + socket, + latest_config_snapshot, + access_token, + region.to_string(), + guid.to_string(), + collection_id.to_string(), + environment_id.to_string(), + ); + + Ok(sender) + } +} + +impl AppConfigurationClient for AppConfigurationClientIBMCloud { + fn get_feature_ids(&self) -> Result> { + Ok(self + .latest_config_snapshot + .lock()? + .features + .keys() + .cloned() + .collect()) + } + + fn get_feature(&self, feature_id: &str) -> Result { + let config_snapshot = self.latest_config_snapshot.lock()?; + + // Get the feature from the snapshot + let feature = config_snapshot.get_feature(feature_id)?; + + // Get the segment rules that apply to this feature + let segments = { + let all_segment_ids = feature + .segment_rules + .iter() + .flat_map(|targeting_rule| { + targeting_rule + .rules + .iter() + .flat_map(|segment| &segment.segments) + }) + .cloned() + .collect::>(); + let segments: HashMap = config_snapshot + .segments + .iter() + .filter(|&(key, _)| all_segment_ids.contains(key)) + .map(|(k, v)| (k.clone(), v.clone())) + .collect(); + + // Integrity DB check: all segment_ids should be available in the snapshot + if all_segment_ids.len() != segments.len() { + return Err(ConfigurationAccessError::MissingSegments { + resource_id: feature_id.to_string(), + } + .into()); + } + + segments + }; + + Ok(FeatureSnapshot::new(feature.clone(), segments)) + } + + /// Searches for the feature `feature_id` inside the current configured + /// collection, and environment. + /// + /// Return `Ok(feature)` if the feature exists or `Err` if it does not. + fn get_feature_proxy<'a>(&'a self, feature_id: &str) -> Result> { + // FIXME: there is and was no validation happening if the feature exists. + // Comments and error messages in FeatureProxy suggest that this should happen here. + // same applies for properties. + Ok(FeatureProxy::new(self, feature_id.to_string())) + } + + fn get_property_ids(&self) -> Result> { + Ok(self + .latest_config_snapshot + .lock() + .map_err(|_| ConfigurationAccessError::LockAcquisitionError)? + .properties + .keys() + .cloned() + .collect()) + } + + fn get_property(&self, property_id: &str) -> Result { + let config_snapshot = self.latest_config_snapshot.lock()?; + + // Get the property from the snapshot + let property = config_snapshot.get_property(property_id)?; + + // Get the segment rules that apply to this property + let segments = { + let all_segment_ids = property + .segment_rules + .iter() + .flat_map(|targeting_rule| { + targeting_rule + .rules + .iter() + .flat_map(|segment| &segment.segments) + }) + .cloned() + .collect::>(); + let segments: HashMap = config_snapshot + .segments + .iter() + .filter(|&(key, _)| all_segment_ids.contains(key)) + .map(|(k, v)| (k.clone(), v.clone())) + .collect(); + + // Integrity DB check: all segment_ids should be available in the snapshot + if all_segment_ids.len() != segments.len() { + // FIXME: Return some kind of DBIntegrity error + return Err(ConfigurationAccessError::MissingSegments { + resource_id: property_id.to_string(), + } + .into()); + } + + segments + }; + + Ok(PropertySnapshot::new(property.clone(), segments)) + } + + /// Searches for the property `property_id` inside the current configured + /// collection, and environment. + /// + /// Return `Ok(property)` if the feature exists or `Err` if it does not. + fn get_property_proxy(&self, property_id: &str) -> Result { + Ok(PropertyProxy::new(self, property_id.to_string())) + } +} diff --git a/src/client/feature_proxy.rs b/src/client/feature_proxy.rs index eab0762..68431c5 100644 --- a/src/client/feature_proxy.rs +++ b/src/client/feature_proxy.rs @@ -23,12 +23,12 @@ use super::feature_snapshot::FeatureSnapshot; use super::AppConfigurationClient; pub struct FeatureProxy<'a> { - client: &'a AppConfigurationClient, + client: &'a dyn AppConfigurationClient, feature_id: String, } impl<'a> FeatureProxy<'a> { - pub(crate) fn new(client: &'a AppConfigurationClient, feature_id: String) -> Self { + pub(crate) fn new(client: &'a dyn AppConfigurationClient, feature_id: String) -> Self { Self { client, feature_id } } diff --git a/src/client/mod.rs b/src/client/mod.rs index a4d5d6a..78f0e29 100644 --- a/src/client/mod.rs +++ b/src/client/mod.rs @@ -13,6 +13,7 @@ // limitations under the License. mod app_configuration_client; +mod app_configuration_ibm_cloud; pub(crate) mod cache; pub(crate) mod feature_snapshot; @@ -23,5 +24,6 @@ pub(crate) mod property_proxy; pub use app_configuration_client::AppConfigurationClient; +pub use app_configuration_ibm_cloud::AppConfigurationClientIBMCloud; pub const REGION_US_SOUTH: &str = "us-south"; diff --git a/src/client/property_proxy.rs b/src/client/property_proxy.rs index 42dd72a..8c5ffba 100644 --- a/src/client/property_proxy.rs +++ b/src/client/property_proxy.rs @@ -20,12 +20,12 @@ use crate::value::Value; use crate::Entity; pub struct PropertyProxy<'a> { - client: &'a AppConfigurationClient, + client: &'a dyn AppConfigurationClient, property_id: String, } impl<'a> PropertyProxy<'a> { - pub(crate) fn new(client: &'a AppConfigurationClient, property_id: String) -> Self { + pub(crate) fn new(client: &'a dyn AppConfigurationClient, property_id: String) -> Self { Self { client, property_id, diff --git a/src/lib.rs b/src/lib.rs index 0361a76..1af1523 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -21,12 +21,12 @@ mod property; mod segment_evaluation; mod value; -pub use client::AppConfigurationClient; -pub use entity::{Entity, AttrValue}; +pub use client::{AppConfigurationClient, AppConfigurationClientIBMCloud}; +pub use entity::{AttrValue, Entity}; +pub use errors::{Error, Result}; pub use feature::Feature; pub use property::Property; pub use value::Value; -pub use errors::{Result, Error}; #[cfg(test)] mod tests; diff --git a/src/tests/mod.rs b/src/tests/mod.rs index 9d3d28e..6a2f11e 100644 --- a/src/tests/mod.rs +++ b/src/tests/mod.rs @@ -21,7 +21,7 @@ mod test_get_property_ids; mod test_using_example_data; use crate::client::cache::ConfigurationSnapshot; -use crate::client::AppConfigurationClient; +use crate::client::AppConfigurationClientIBMCloud; use crate::entity::AttrValue; use crate::models::tests::example_configuration_enterprise; use crate::models::Configuration; @@ -57,14 +57,14 @@ impl Entity for GenericEntity { } #[fixture] -fn client_enterprise(example_configuration_enterprise: Configuration) -> AppConfigurationClient { +fn client_enterprise(example_configuration_enterprise: Configuration) -> AppConfigurationClientIBMCloud { let configuration_snapshot = ConfigurationSnapshot::new("dev", example_configuration_enterprise).unwrap(); // Create the client let (sender, _) = std::sync::mpsc::channel(); - AppConfigurationClient { + AppConfigurationClientIBMCloud { latest_config_snapshot: Arc::new(Mutex::new(configuration_snapshot)), _thread_terminator: sender, } diff --git a/src/tests/test_get_feature.rs b/src/tests/test_get_feature.rs index 0eb36a4..8d5382b 100644 --- a/src/tests/test_get_feature.rs +++ b/src/tests/test_get_feature.rs @@ -15,16 +15,16 @@ use crate::models::Configuration; use crate::client::cache::ConfigurationSnapshot; -use crate::client::AppConfigurationClient; +use crate::client::{AppConfigurationClient, AppConfigurationClientIBMCloud}; use rstest::*; use super::client_enterprise; -use crate::models::tests::configuration_feature1_enabled; use crate::feature::Feature; +use crate::models::tests::configuration_feature1_enabled; #[rstest] fn test_get_feature_persistence( - client_enterprise: AppConfigurationClient, + client_enterprise: AppConfigurationClientIBMCloud, configuration_feature1_enabled: Configuration, ) { let feature = client_enterprise.get_feature("f1").unwrap(); @@ -48,7 +48,7 @@ fn test_get_feature_persistence( } #[rstest] -fn test_get_feature_doesnt_exist(client_enterprise: AppConfigurationClient) { +fn test_get_feature_doesnt_exist(client_enterprise: AppConfigurationClientIBMCloud) { let feature = client_enterprise.get_feature("non-existing"); assert!(feature.is_err()); assert_eq!( diff --git a/src/tests/test_get_feature_ids.rs b/src/tests/test_get_feature_ids.rs index 4ecabfc..68ca36b 100644 --- a/src/tests/test_get_feature_ids.rs +++ b/src/tests/test_get_feature_ids.rs @@ -13,11 +13,11 @@ // limitations under the License. use super::client_enterprise; -use crate::client::AppConfigurationClient; +use crate::client::{AppConfigurationClient, AppConfigurationClientIBMCloud}; use rstest::*; #[rstest] -fn test_get_feature_ids(client_enterprise: AppConfigurationClient) { +fn test_get_feature_ids(client_enterprise: AppConfigurationClientIBMCloud) { let mut features = client_enterprise.get_feature_ids().unwrap(); features.sort(); assert_eq!( diff --git a/src/tests/test_get_property.rs b/src/tests/test_get_property.rs index 00ebd1f..1a17719 100644 --- a/src/tests/test_get_property.rs +++ b/src/tests/test_get_property.rs @@ -15,7 +15,7 @@ use crate::models::Configuration; use crate::client::cache::ConfigurationSnapshot; -use crate::client::AppConfigurationClient; +use crate::client::{AppConfigurationClient, AppConfigurationClientIBMCloud}; use rstest::*; use super::client_enterprise; @@ -24,7 +24,7 @@ use crate::property::Property; #[rstest] fn test_get_property_persistence( - client_enterprise: AppConfigurationClient, + client_enterprise: AppConfigurationClientIBMCloud, configuration_property1_enabled: Configuration, ) { let property = client_enterprise.get_property("p1").unwrap(); @@ -48,7 +48,7 @@ fn test_get_property_persistence( } #[rstest] -fn test_get_property_doesnt_exist(client_enterprise: AppConfigurationClient) { +fn test_get_property_doesnt_exist(client_enterprise: AppConfigurationClientIBMCloud) { let property = client_enterprise.get_property("non-existing"); assert!(property.is_err()); assert_eq!( diff --git a/src/tests/test_get_property_ids.rs b/src/tests/test_get_property_ids.rs index 36d0426..fe2be27 100644 --- a/src/tests/test_get_property_ids.rs +++ b/src/tests/test_get_property_ids.rs @@ -13,11 +13,11 @@ // limitations under the License. use super::client_enterprise; -use crate::client::AppConfigurationClient; +use crate::client::{AppConfigurationClient, AppConfigurationClientIBMCloud}; use rstest::*; #[rstest] -fn test_get_property_ids(client_enterprise: AppConfigurationClient) { +fn test_get_property_ids(client_enterprise: AppConfigurationClientIBMCloud) { let mut properties = client_enterprise.get_property_ids().unwrap(); properties.sort(); assert_eq!( diff --git a/src/tests/test_using_example_data.rs b/src/tests/test_using_example_data.rs index fea97a9..d7d3bd7 100644 --- a/src/tests/test_using_example_data.rs +++ b/src/tests/test_using_example_data.rs @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -use crate::client::AppConfigurationClient; +use crate::client::{AppConfigurationClientIBMCloud, AppConfigurationClient}; use crate::tests::TrivialEntity; use rstest::*; @@ -20,7 +20,7 @@ use super::client_enterprise; use crate::{Feature, Property, Value}; #[rstest] -fn test_get_a_specific_feature(client_enterprise: AppConfigurationClient) { +fn test_get_a_specific_feature(client_enterprise: AppConfigurationClientIBMCloud) { let specific_feature = client_enterprise.get_feature_proxy("f1").unwrap(); let name = specific_feature.get_name().unwrap(); @@ -33,7 +33,7 @@ fn test_get_a_specific_feature(client_enterprise: AppConfigurationClient) { } #[rstest] -fn test_get_a_specific_property(client_enterprise: AppConfigurationClient) { +fn test_get_a_specific_property(client_enterprise: AppConfigurationClientIBMCloud) { let property = client_enterprise.get_property_proxy("p1").unwrap(); let name = property.get_name().unwrap(); diff --git a/tests/test_app_config.rs b/tests/test_app_config.rs index c95d466..86476ba 100644 --- a/tests/test_app_config.rs +++ b/tests/test_app_config.rs @@ -16,7 +16,8 @@ use dotenvy::dotenv; use rstest::*; use appconfiguration_rust_sdk::{ - AppConfigurationClient, AttrValue, Entity, Feature, Property, Value, + AppConfigurationClient, AppConfigurationClientIBMCloud, AttrValue, Entity, Feature, Property, + Value, }; use std::collections::HashMap; use std::env; @@ -34,7 +35,7 @@ impl Entity for TrivialEntity { } #[fixture] -fn setup_client() -> AppConfigurationClient { +fn setup_client() -> AppConfigurationClientIBMCloud { dotenv().expect( ".env file not found. Create one with the required variables in order to run the tests.", ); @@ -44,18 +45,18 @@ fn setup_client() -> AppConfigurationClient { //TODO: Our current pricing plan doesn't allow more than 1 collection, so we are using // car-rentals so far. - AppConfigurationClient::new(&apikey, ®ion, &guid, "testing", "car-rentals").unwrap() + AppConfigurationClientIBMCloud::new(&apikey, ®ion, &guid, "testing", "car-rentals").unwrap() } #[rstest] -fn test_get_list_of_features(setup_client: AppConfigurationClient) { +fn test_get_list_of_features(setup_client: AppConfigurationClientIBMCloud) { let features = setup_client.get_feature_ids().unwrap(); assert_eq!(features.len(), 4); } #[rstest] -fn test_get_a_specific_feature(setup_client: AppConfigurationClient) { +fn test_get_a_specific_feature(setup_client: AppConfigurationClientIBMCloud) { let specific_feature = setup_client .get_feature_proxy("test-feature-flag-1") .unwrap(); @@ -70,14 +71,14 @@ fn test_get_a_specific_feature(setup_client: AppConfigurationClient) { } #[rstest] -fn test_get_list_of_properties(setup_client: AppConfigurationClient) { +fn test_get_list_of_properties(setup_client: AppConfigurationClientIBMCloud) { let properties = setup_client.get_property_ids().unwrap(); assert_eq!(properties.len(), 2); } #[rstest] -fn test_get_a_specific_property(setup_client: AppConfigurationClient) { +fn test_get_a_specific_property(setup_client: AppConfigurationClientIBMCloud) { let property = setup_client.get_property_proxy("test-property-1").unwrap(); let name = property.get_name().unwrap(); From 0dcd11e15f1dc92c2f2bbb127866c1c61fa304d8 Mon Sep 17 00:00:00 2001 From: "Javier G. Sogo" Date: Mon, 16 Dec 2024 17:19:58 +0100 Subject: [PATCH 2/4] Move comments to the public API Signed-off-by: Javier G. Sogo --- src/client/app_configuration_client.rs | 9 +++++++ src/client/app_configuration_ibm_cloud.rs | 30 +++++++++-------------- 2 files changed, 21 insertions(+), 18 deletions(-) diff --git a/src/client/app_configuration_client.rs b/src/client/app_configuration_client.rs index a84c025..51df36f 100644 --- a/src/client/app_configuration_client.rs +++ b/src/client/app_configuration_client.rs @@ -19,16 +19,25 @@ use crate::client::feature_snapshot::FeatureSnapshot; use crate::client::property_proxy::PropertyProxy; use crate::client::property_snapshot::PropertySnapshot; +/// AppConfiguration client for browsing, and evaluating features and properties. pub trait AppConfigurationClient { fn get_feature_ids(&self) -> Result>; fn get_feature(&self, feature_id: &str) -> Result; + /// Searches for the feature `feature_id` inside the current configured + /// collection, and environment. + /// + /// Return `Ok(feature)` if the feature exists or `Err` if it does not. fn get_feature_proxy<'a>(&'a self, feature_id: &str) -> Result>; fn get_property_ids(&self) -> Result>; fn get_property(&self, property_id: &str) -> Result; + /// Searches for the property `property_id` inside the current configured + /// collection, and environment. + /// + /// Return `Ok(property)` if the feature exists or `Err` if it does not. fn get_property_proxy(&self, property_id: &str) -> Result; } diff --git a/src/client/app_configuration_ibm_cloud.rs b/src/client/app_configuration_ibm_cloud.rs index e576cc0..2593637 100644 --- a/src/client/app_configuration_ibm_cloud.rs +++ b/src/client/app_configuration_ibm_cloud.rs @@ -31,8 +31,7 @@ use tungstenite::WebSocket; use super::AppConfigurationClient; -/// App Configuration client for browsing, and evaluating features and -/// properties. +/// AppConfiguration client connection to IBM Cloud. #[derive(Debug)] pub struct AppConfigurationClientIBMCloud { pub(crate) latest_config_snapshot: Arc>, @@ -40,13 +39,16 @@ pub struct AppConfigurationClientIBMCloud { } impl AppConfigurationClientIBMCloud { - /// Creates a client to retrieve configurations for a specific collection. - /// To uniquely address a collection the following is required: - /// - `region` - /// - `guid`: Identifies an instance - /// - `environment_id` - /// - `collection_id` - /// In addition `api_key` is required for authentication + + /// Creates a new [`AppConfigurationClient`] connecting to IBM Cloud + /// + /// # Arguments + /// + /// * `apikey` - The encrypted API key. + /// * `region` - Region name where the App Configuration service instance is created + /// * `guid` - Instance ID of the App Configuration service. Obtain it from the service credentials section of the App Configuration dashboard + /// * `environment_id` - ID of the environment created in App Configuration service instance under the Environments section. + /// * `collection_id` - ID of the collection created in App Configuration service instance under the Collections section pub fn new( apikey: &str, region: &str, @@ -54,7 +56,7 @@ impl AppConfigurationClientIBMCloud { environment_id: &str, collection_id: &str, ) -> Result { - let access_token = http::get_access_token(&apikey)?; + let access_token = http::get_access_token(apikey)?; // Populate initial configuration let latest_config_snapshot: Arc> = @@ -258,10 +260,6 @@ impl AppConfigurationClient for AppConfigurationClientIBMCloud { Ok(FeatureSnapshot::new(feature.clone(), segments)) } - /// Searches for the feature `feature_id` inside the current configured - /// collection, and environment. - /// - /// Return `Ok(feature)` if the feature exists or `Err` if it does not. fn get_feature_proxy<'a>(&'a self, feature_id: &str) -> Result> { // FIXME: there is and was no validation happening if the feature exists. // Comments and error messages in FeatureProxy suggest that this should happen here. @@ -321,10 +319,6 @@ impl AppConfigurationClient for AppConfigurationClientIBMCloud { Ok(PropertySnapshot::new(property.clone(), segments)) } - /// Searches for the property `property_id` inside the current configured - /// collection, and environment. - /// - /// Return `Ok(property)` if the feature exists or `Err` if it does not. fn get_property_proxy(&self, property_id: &str) -> Result { Ok(PropertyProxy::new(self, property_id.to_string())) } From 3e58162837b51646897a0f7293c22e40b0c2de55 Mon Sep 17 00:00:00 2001 From: "Javier G. Sogo" Date: Tue, 17 Dec 2024 15:36:04 +0100 Subject: [PATCH 3/4] Fix merge conflicts Signed-off-by: Javier G. Sogo --- src/feature.rs | 4 ++-- src/lib.rs | 35 +++++++++++++++++++---------------- src/property.rs | 4 ++-- 3 files changed, 23 insertions(+), 20 deletions(-) diff --git a/src/feature.rs b/src/feature.rs index f941e78..29472b3 100644 --- a/src/feature.rs +++ b/src/feature.rs @@ -33,7 +33,7 @@ pub trait Feature { /// /// ``` /// # use appconfiguration_rust_sdk::{AppConfigurationClient, Feature, Result, Entity, Value}; - /// # fn doctest_get_value(client: AppConfigurationClient, entity: &impl Entity) -> Result<()> { + /// # fn doctest_get_value(client: impl AppConfigurationClient, entity: &impl Entity) -> Result<()> { /// let feature = client.get_feature("my_feature")?; /// let value: Value = feature.get_value(entity)?; /// @@ -55,7 +55,7 @@ pub trait Feature { /// /// ``` /// # use appconfiguration_rust_sdk::{AppConfigurationClient, Feature, Result, Entity}; - /// # fn doctest_get_value_into(client: AppConfigurationClient, entity: &impl Entity) -> Result<()> { + /// # fn doctest_get_value_into(client: impl AppConfigurationClient, entity: &impl Entity) -> Result<()> { /// let feature = client.get_feature("my_f64_feature")?; /// let value: f64 = feature.get_value_into(entity)?; /// diff --git a/src/lib.rs b/src/lib.rs index 2253315..a68026c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -16,30 +16,33 @@ //! evaluation based on the configuration on IBM Cloud App Configuration service. //! //! # Overview -//! +//! //! [IBM Cloud App Configuration](https://cloud.ibm.com/docs/app-configuration) is a centralized //! feature management and configuration service on [IBM Cloud](https://www.cloud.ibm.com) for //! use with web and mobile applications, microservices, and distributed environments. -//! +//! //! Instrument your applications with App Configuration Rust SDK, and use the App Configuration //! dashboard, API or CLI to define feature flags or properties, organized into collections and //! targeted to segments. Change feature flag states in the cloud to activate or deactivate features //! in your application or environment, when required. You can also manage the properties for distributed //! applications centrally. -//! +//! //! # Pre-requisites -//! +//! //! You will need the `apikey`, `region` and `guid` for the AppConfiguration you want to connect to //! from your [IBMCloud account](https://cloud.ibm.com/). -//! +//! //! # Usage -//! +//! //! **Note.-** This crate is still under heavy development. Breaking changes are expected. -//! +//! //! Create your client with the context (environment and collection) you want to connect to -//! +//! //! ``` -//! use appconfiguration_rust_sdk::{AppConfigurationClient, Entity, Result, Value, Feature}; +//! use appconfiguration_rust_sdk::{ +//! AppConfigurationClient, AppConfigurationClientIBMCloud, +//! Entity, Result, Value, Feature +//! }; //! # use std::collections::HashMap; //! # pub struct MyEntity; //! # impl Entity for MyEntity { @@ -56,27 +59,27 @@ //! # let guid: &str = "12345678-1234-1234-1234-12345678abcd"; //! # let environment_id: &str = "production"; //! # let collection_id: &str = "ecommerce"; -//! +//! //! // Create the client connecting to the server -//! let client = AppConfigurationClient::new(&apikey, ®ion, &guid, &environment_id, &collection_id)?; -//! +//! let client = AppConfigurationClientIBMCloud::new(&apikey, ®ion, &guid, &environment_id, &collection_id)?; +//! //! // Get the feature you want to evaluate for your entities //! let feature = client.get_feature("AB_testing_feature")?; -//! +//! //! // Evaluate feature value for each one of your entities //! let user = MyEntity; // Implements Entity -//! +//! //! let value_for_this_user = feature.get_value(&user)?.try_into()?; //! if value_for_this_user { //! println!("Feature {} is active for user {}", feature.get_name()?, user.get_id()); //! } else { //! println!("User {} keeps using the legacy workflow", user.get_id()); //! } -//! +//! //! # Ok(()) //! # } //! ``` -//! +//! mod client; mod entity; mod errors; diff --git a/src/property.rs b/src/property.rs index 5aae2f7..516ea7c 100644 --- a/src/property.rs +++ b/src/property.rs @@ -26,7 +26,7 @@ pub trait Property { /// /// ``` /// # use appconfiguration_rust_sdk::{AppConfigurationClient, Property, Result, Entity, Value}; - /// # fn doctest_get_value(client: AppConfigurationClient, entity: &impl Entity) -> Result<()> { + /// # fn doctest_get_value(client: impl AppConfigurationClient, entity: &impl Entity) -> Result<()> { /// let property = client.get_property("my_property")?; /// let value: Value = property.get_value(entity)?; /// @@ -48,7 +48,7 @@ pub trait Property { /// /// ``` /// # use appconfiguration_rust_sdk::{AppConfigurationClient, Property, Result, Entity}; - /// # fn doctest_get_value_into(client: AppConfigurationClient, entity: &impl Entity) -> Result<()> { + /// # fn doctest_get_value_into(client: impl AppConfigurationClient, entity: &impl Entity) -> Result<()> { /// let property = client.get_property("my_bool_feature")?; /// let value: bool = property.get_value_into(entity)?; /// From b2e158a355b565145af8ab1dbba3833449b90946 Mon Sep 17 00:00:00 2001 From: "Javier G. Sogo" Date: Tue, 17 Dec 2024 15:39:06 +0100 Subject: [PATCH 4/4] Regenerate README Signed-off-by: Javier G. Sogo --- README.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 2c71160..dd9ee0f 100644 --- a/README.md +++ b/README.md @@ -29,10 +29,13 @@ from your [IBMCloud account](https://cloud.ibm.com/). Create your client with the context (environment and collection) you want to connect to ```rust -use appconfiguration_rust_sdk::{AppConfigurationClient, Entity, Result, Value, Feature}; +use appconfiguration_rust_sdk::{ + AppConfigurationClient, AppConfigurationClientIBMCloud, + Entity, Result, Value, Feature +}; // Create the client connecting to the server -let client = AppConfigurationClient::new(&apikey, ®ion, &guid, &environment_id, &collection_id)?; +let client = AppConfigurationClientIBMCloud::new(&apikey, ®ion, &guid, &environment_id, &collection_id)?; // Get the feature you want to evaluate for your entities let feature = client.get_feature("AB_testing_feature")?;