Skip to content

Instantly share code, notes, and snippets.

@soareschen
Created December 25, 2024 20:17
Show Gist options
  • Select an option

  • Save soareschen/fcde9d72f01509b4cee9c8d4bedbedf2 to your computer and use it in GitHub Desktop.

Select an option

Save soareschen/fcde9d72f01509b4cee9c8d4bedbedf2 to your computer and use it in GitHub Desktop.

Revisions

  1. soareschen created this gist Dec 25, 2024.
    174 changes: 174 additions & 0 deletions cgp-cake.rs
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,174 @@
    // This is an example of how the Scala cake pattern can be used in CGP,
    // following the example at https://www.baeldung.com/scala/cake-pattern

    pub mod traits {
    use std::collections::BTreeMap;

    use anyhow::Error;
    use cgp::prelude::*;

    // For simplicity of the example, we use `dyn` trait to represent test cases
    pub trait TestCase {
    fn execute_test(&self, test_environment: &BTreeMap<String, String>) -> Result<(), Error>;
    }

    // This follows the TestEnvironmentComponent Scala trait
    #[cgp_component {
    name: TestEnvironmentComponent,
    provider: TestEnvironmentGetter,
    context: Context,
    }]
    pub trait HasTestEnvironment {
    fn test_environment(&self) -> &BTreeMap<String, String>;
    }

    // This follows the TestExecutorComponent Scala trait
    #[cgp_component {
    name: TestExecutorComponent,
    provider: TestExecutor,
    context: Context,
    }]
    pub trait CanExecuteTests {
    fn execute_tests(&self, tests: &[Box<dyn TestCase>]) -> Result<(), Error>;
    }

    // This follows the LoggingComponent Scala trait
    #[cgp_component {
    name: LoggerComponent,
    provider: Logger,
    context: Context,
    }]
    pub trait CanLog {
    fn log(&self, level: &str, message: &str);
    }
    }

    pub mod impls {
    use core::marker::PhantomData;

    use anyhow::Error;

    use super::traits::*;

    // We show an example test executor that runs all tests in serial.
    // The name implies that we can also define a different test executor that runs all tests in parallel.
    pub struct ExecuteAllTestsInSerial;

    impl<Context> TestExecutor<Context> for ExecuteAllTestsInSerial
    where
    // We use dependency injection to require `Context` to implement `HasTestEnvironment`
    Context: HasTestEnvironment,
    {
    fn execute_tests(context: &Context, tests: &[Box<dyn TestCase>]) -> Result<(), Error> {
    let test_environment = context.test_environment();

    for test in tests {
    test.execute_test(test_environment)?;
    }

    Ok(())
    }
    }

    // We have a middleware test executor that would log the test environment using the logger,
    // and then use `InExecutor` to execute the tests
    pub struct LogAndExecuteTests<InExecutor>(pub PhantomData<InExecutor>);

    impl<Context, InHandler> TestExecutor<Context> for LogAndExecuteTests<InHandler>
    where
    // We use dependency injection to require `Context` to implement both `CanLog` and `HasTestEnvironment`
    Context: CanLog + HasTestEnvironment,
    InHandler: TestExecutor<Context>,
    {
    fn execute_tests(context: &Context, tests: &[Box<dyn TestCase>]) -> Result<(), Error> {
    context.log(
    "INFO",
    &format!(
    "Executing tests with environments: {:?}",
    context.test_environment()
    ),
    );

    InHandler::execute_tests(context, tests)
    }
    }

    // A simple logger implementation that logs using `prinln!`.
    // We could later replace this with more sophisticated loggers, such as `TracingLogger`.
    pub struct PrintLoggger;

    impl<Context> Logger<Context> for PrintLoggger {
    fn log(_context: &Context, level: &str, message: &str) {
    println!("[{level}] {message}");
    }
    }
    }

    pub mod contexts {
    use std::collections::BTreeMap;

    use cgp::prelude::*;

    use super::impls::*;
    use super::traits::*;

    pub struct MyTestContext {
    pub test_environment: BTreeMap<String, String>,
    }

    pub struct MyTestContextComponents;

    impl HasComponents for MyTestContext {
    type Components = MyTestContextComponents;
    }

    // We can easily customize how we want to execute the test and display logs,
    // by choosing the appropriate providers.
    delegate_components! {
    MyTestContextComponents {
    TestExecutorComponent: LogAndExecuteTests<ExecuteAllTestsInSerial>,
    LoggerComponent: PrintLoggger,
    }
    }

    // Define a custom `TestEnvironmentGetter` implementation for `MyTestContext`.
    // It is also possible to have more general getter implementation, but we'd
    // skip that inside this example.
    impl TestEnvironmentGetter<MyTestContext> for MyTestContextComponents {
    fn test_environment(context: &MyTestContext) -> &BTreeMap<String, String> {
    &context.test_environment
    }
    }
    }

    #[cfg(test)]
    pub mod tests {
    use std::collections::BTreeMap;

    use anyhow::Error;

    use super::contexts::MyTestContext;
    use super::traits::*;

    pub struct DummyTest;

    impl TestCase for DummyTest {
    fn execute_test(
    &self,
    _test_environment: &std::collections::BTreeMap<String, String>,
    ) -> Result<(), anyhow::Error> {
    Ok(())
    }
    }

    #[test]
    fn run_dummy_test() -> Result<(), Error> {
    let test_context = MyTestContext {
    test_environment: BTreeMap::from([("foo".into(), "bar".into())]),
    };

    test_context.execute_tests(&[Box::new(DummyTest)])?;

    Ok(())
    }
    }