#![warn(missing_docs)]
#![deny(unsafe_code)]
pub trait ApiOperation<C, P> {
type Output;
type Error;
fn execute(context: &mut C, parameters: &P) -> Result<Self::Output, Self::Error>;
}
pub trait Execute<C, P> {
type Output;
type Error;
fn execute_on(self, context: &mut C, parameters: &P) -> Result<Self::Output, Self::Error>;
}
impl<T, C, P> Execute<C, P> for T
where
T: ApiOperation<C, P>,
{
type Output = T::Output;
type Error = T::Error;
fn execute_on(self, context: &mut C, parameters: &P) -> Result<Self::Output, Self::Error> {
T::execute(context, parameters)
}
}
#[derive(Debug, Clone)]
pub struct ApiExecutor<C> {
context: C,
}
impl<C> ApiExecutor<C> {
pub fn new(context: C) -> Self {
Self { context }
}
pub fn execute<P, Op>(&mut self, _op: Op, parameters: &P) -> Result<Op::Output, Op::Error>
where
Op: ApiOperation<C, P>,
{
Op::execute(&mut self.context, parameters)
}
pub fn context(&self) -> &C {
&self.context
}
pub fn context_mut(&mut self) -> &mut C {
&mut self.context
}
}
#[cfg(test)]
mod tests {
use super::*;
#[derive(Debug, Clone)]
pub struct DatabaseContext {
connection_pool: String,
transaction_count: u32,
cache: std::collections::HashMap<String, String>,
}
impl DatabaseContext {
pub fn new(connection: String) -> Self {
Self {
connection_pool: connection,
transaction_count: 0,
cache: std::collections::HashMap::new(),
}
}
pub fn increment_transaction(&mut self) {
self.transaction_count += 1;
}
pub fn transaction_count(&self) -> u32 {
self.transaction_count
}
pub fn connection_pool(&self) -> &str {
&self.connection_pool
}
pub fn cache(&self) -> &std::collections::HashMap<String, String> {
&self.cache
}
pub fn cache_mut(&mut self) -> &mut std::collections::HashMap<String, String> {
&mut self.cache
}
}
#[test]
fn test_crate_compiles() {
}
#[test]
fn test_documentation_is_accessible() {
assert_eq!(env!("CARGO_PKG_NAME"), "apithing");
assert_eq!(env!("CARGO_PKG_VERSION"), "0.1.0");
}
#[test]
fn test_api_operation_trait_compiles() {
#[derive(Debug)]
struct TestContext {
counter: u32,
}
#[derive(Debug)]
struct TestProps {
value: String,
}
#[derive(Debug, PartialEq)]
struct TestOutput {
result: String,
count: u32,
}
#[derive(Debug, PartialEq)]
enum TestError {
EmptyValue,
}
struct TestOperation;
impl ApiOperation<TestContext, TestProps> for TestOperation {
type Output = TestOutput;
type Error = TestError;
fn execute(
context: &mut TestContext,
parameters: &TestProps,
) -> Result<TestOutput, TestError> {
if parameters.value.is_empty() {
return Err(TestError::EmptyValue);
}
context.counter += 1;
Ok(TestOutput {
result: parameters.value.clone(),
count: context.counter,
})
}
}
let mut context = TestContext { counter: 0 };
let parameters = TestProps {
value: "test".to_string(),
};
let result = TestOperation::execute(&mut context, ¶meters).unwrap();
assert_eq!(result.result, "test");
assert_eq!(result.count, 1);
assert_eq!(context.counter, 1);
}
#[test]
fn test_execute_trait() {
#[derive(Debug)]
struct SimpleContext {
data: String,
}
#[derive(Debug)]
struct SimpleProps {
input: String,
}
struct SimpleOperation;
impl ApiOperation<SimpleContext, SimpleProps> for SimpleOperation {
type Output = String;
type Error = ();
fn execute(
context: &mut SimpleContext,
parameters: &SimpleProps,
) -> Result<String, ()> {
context.data = parameters.input.clone();
Ok(format!("Processed: {}", parameters.input))
}
}
let mut context = SimpleContext {
data: String::new(),
};
let parameters = SimpleProps {
input: "test input".to_string(),
};
let result = SimpleOperation
.execute_on(&mut context, ¶meters)
.unwrap();
assert_eq!(result, "Processed: test input");
assert_eq!(context.data, "test input");
}
#[test]
fn test_database_context() {
let mut context = DatabaseContext::new("test_connection".to_string());
assert_eq!(context.connection_pool(), "test_connection");
assert_eq!(context.transaction_count(), 0);
assert!(context.cache().is_empty());
context.increment_transaction();
assert_eq!(context.transaction_count(), 1);
context
.cache_mut()
.insert("key1".to_string(), "value1".to_string());
assert_eq!(context.cache().len(), 1);
assert_eq!(context.cache().get("key1"), Some(&"value1".to_string()));
}
#[test]
fn test_api_executor() {
#[derive(Debug)]
struct CounterProps {
increment: u32,
}
struct IncrementOperation;
impl ApiOperation<DatabaseContext, CounterProps> for IncrementOperation {
type Output = u32;
type Error = ();
fn execute(
context: &mut DatabaseContext,
parameters: &CounterProps,
) -> Result<u32, ()> {
for _ in 0..parameters.increment {
context.increment_transaction();
}
Ok(context.transaction_count())
}
}
let mut executor = ApiExecutor::new(DatabaseContext::new("test".to_string()));
assert_eq!(executor.context().transaction_count(), 0);
let parameters = CounterProps { increment: 3 };
let result = executor.execute(IncrementOperation, ¶meters).unwrap();
assert_eq!(result, 3);
assert_eq!(executor.context().transaction_count(), 3);
let parameters2 = CounterProps { increment: 2 };
let result2 = executor.execute(IncrementOperation, ¶meters2).unwrap();
assert_eq!(result2, 5);
assert_eq!(executor.context().transaction_count(), 5);
}
#[test]
fn test_examples_compile() {
use std::collections::HashMap;
#[derive(Debug)]
struct ExampleAppContext {
transaction_count: u32,
cache: HashMap<String, String>,
}
impl ExampleAppContext {
fn new(_connection: String) -> Self {
Self {
transaction_count: 0,
cache: HashMap::new(),
}
}
fn increment_transaction(&mut self) {
self.transaction_count += 1;
}
fn transaction_count(&self) -> u32 {
self.transaction_count
}
fn cache_mut(&mut self) -> &mut HashMap<String, String> {
&mut self.cache
}
}
#[derive(Debug, Clone)]
struct ExampleCreateUserProps {
name: String,
email: String,
}
#[derive(Debug, Clone)]
struct ExampleUser {
id: u64,
name: String,
email: String,
}
#[derive(Debug)]
enum ExampleUserError {
InvalidEmail,
}
struct ExampleCreateUser;
impl ApiOperation<ExampleAppContext, ExampleCreateUserProps> for ExampleCreateUser {
type Output = ExampleUser;
type Error = ExampleUserError;
fn execute(
context: &mut ExampleAppContext,
parameters: &ExampleCreateUserProps,
) -> Result<ExampleUser, ExampleUserError> {
if !parameters.email.contains('@') {
return Err(ExampleUserError::InvalidEmail);
}
context.increment_transaction();
let user = ExampleUser {
id: context.transaction_count() as u64,
name: parameters.name.clone(),
email: parameters.email.clone(),
};
let cache_key = format!("user_{}", user.id);
let cache_value = format!("{}:{}", user.name, user.email);
context.cache_mut().insert(cache_key, cache_value);
Ok(user)
}
}
let mut context = ExampleAppContext::new("test_db".to_string());
let parameters = ExampleCreateUserProps {
name: "Test User".to_string(),
email: "test@example.com".to_string(),
};
let result = ExampleCreateUser::execute(&mut context, ¶meters);
assert!(result.is_ok());
let user = result.unwrap();
assert_eq!(user.name, "Test User");
assert_eq!(user.email, "test@example.com");
assert_eq!(context.transaction_count(), 1);
}
#[test]
fn test_executor_pattern_example() {
use std::collections::HashMap;
#[derive(Debug)]
struct ExecutorExampleContext {
transaction_count: u32,
cache: HashMap<String, String>,
}
impl ExecutorExampleContext {
fn new(_connection: String) -> Self {
Self {
transaction_count: 0,
cache: HashMap::new(),
}
}
fn increment_transaction(&mut self) {
self.transaction_count += 1;
}
fn transaction_count(&self) -> u32 {
self.transaction_count
}
fn cache_mut(&mut self) -> &mut HashMap<String, String> {
&mut self.cache
}
}
#[derive(Debug, Clone)]
struct ExecutorCreateUserProps {
name: String,
email: String,
}
#[derive(Debug, Clone)]
struct ExecutorUser {
id: u64,
name: String,
email: String,
}
#[derive(Debug)]
enum ExecutorUserError {
InvalidEmail,
}
struct ExecutorCreateUser;
impl ApiOperation<ExecutorExampleContext, ExecutorCreateUserProps> for ExecutorCreateUser {
type Output = ExecutorUser;
type Error = ExecutorUserError;
fn execute(
context: &mut ExecutorExampleContext,
parameters: &ExecutorCreateUserProps,
) -> Result<ExecutorUser, ExecutorUserError> {
if !parameters.email.contains('@') {
return Err(ExecutorUserError::InvalidEmail);
}
context.increment_transaction();
let user = ExecutorUser {
id: context.transaction_count() as u64,
name: parameters.name.clone(),
email: parameters.email.clone(),
};
let cache_key = format!("user_{}", user.id);
let cache_value = format!("{}:{}", user.name, user.email);
context.cache_mut().insert(cache_key, cache_value);
Ok(user)
}
}
let mut executor =
ApiExecutor::new(ExecutorExampleContext::new("executor_test_db".to_string()));
let parameters = ExecutorCreateUserProps {
name: "Executor User".to_string(),
email: "executor@example.com".to_string(),
};
let result = executor.execute(ExecutorCreateUser, ¶meters);
assert!(result.is_ok());
let user = result.unwrap();
assert_eq!(user.name, "Executor User");
assert_eq!(user.email, "executor@example.com");
assert_eq!(executor.context().transaction_count(), 1);
}
#[test]
fn test_context_sharing() {
#[derive(Debug)]
struct StoreProps {
key: String,
value: String,
}
#[derive(Debug)]
struct RetrieveProps {
key: String,
}
struct StoreOperation;
struct RetrieveOperation;
impl ApiOperation<DatabaseContext, StoreProps> for StoreOperation {
type Output = ();
type Error = ();
fn execute(context: &mut DatabaseContext, parameters: &StoreProps) -> Result<(), ()> {
context
.cache_mut()
.insert(parameters.key.clone(), parameters.value.clone());
context.increment_transaction();
Ok(())
}
}
impl ApiOperation<DatabaseContext, RetrieveProps> for RetrieveOperation {
type Output = Option<String>;
type Error = ();
fn execute(
context: &mut DatabaseContext,
parameters: &RetrieveProps,
) -> Result<Option<String>, ()> {
Ok(context.cache().get(¶meters.key).cloned())
}
}
let mut executor = ApiExecutor::new(DatabaseContext::new("shared".to_string()));
let store_parameters = StoreProps {
key: "test_key".to_string(),
value: "test_value".to_string(),
};
executor.execute(StoreOperation, &store_parameters).unwrap();
let retrieve_parameters = RetrieveProps {
key: "test_key".to_string(),
};
let retrieved = executor
.execute(RetrieveOperation, &retrieve_parameters)
.unwrap();
assert_eq!(retrieved, Some("test_value".to_string()));
assert_eq!(executor.context().transaction_count(), 1);
}
}