What is a Context
?
In the Gadget SDK, a Context
simply holds any a job may have, that aren't direct inputs.
A Context can include various elements such as:
- HTTP clients for making network requests
- Database connections for data persistence
- Loggers for recording application events
- Configuration objects for environment-specific settings
- Authentication tokens or credentials
- Caches or in-memory data stores
The Context pattern is closely related to the Dependency Injection (DI) design pattern, which is a technique for achieving Inversion of Control (IoC) between classes and their dependencies.
for more information on dependency injection, see:
- dependency injection principles, practices, and patterns (opens in a new tab)
- martin fowler's article on dependency injection (opens in a new tab)
Why do we need a Context
?
The Context pattern offers several significant benefits in software design and development:
-
Decoupling: By passing dependencies through a Context, we decouple the job implementation from the specific implementations of its dependencies. This makes the code more modular and easier to maintain.
-
Testability: With a Context, it's easier to mock or stub dependencies during unit testing, allowing for more comprehensive and isolated tests.
-
Flexibility: Contexts allow for easy swapping of implementations, which is particularly useful when adapting to different environments (e.g., development, staging, production).
-
Separation of Concerns: The Context pattern helps separate the configuration and setup of dependencies from their usage, leading to cleaner, more focused code.
-
Reusability: Jobs that rely on a Context can be more easily reused in different scenarios by simply providing a different Context.
Example: "Is it down for everyone or just me?" Service
Let's examine an example of a blueprint similar to the "Down for Everyone or Just Me!" service. This example demonstrates how a Context can be used to provide an HTTP client to a job.
// Define the Context struct with an HTTP client
#[derive(Debug, Clone)]
struct Context {
http_client: reqwest::Client,
}
impl Context {
// Constructor for creating a new Context
fn new() -> Self {
Self {
http_client: reqwest::Client::new(),
}
}
// Getter method for accessing the HTTP client
fn http_client(&self) -> &reqwest::Client {
&self.http_client
}
}
/// Job to check if a website is down for everyone or just you.
#[job(id = 1, params(url), result(_))]
pub async fn down_for_everyone_or_just_me(
ctx: &Context, // The Context is passed as a parameter
url: String,
) -> Result<String, reqwest::Error> {
// Access the HTTP client from the Context
let client = ctx.http_client();
// Make a GET request to the specified URL
let response = client.get(&url).send().await?;
let status = response.status();
// Return a message based on the HTTP status code
Ok(match status {
reqwest::StatusCode::OK => format!("It's just you! {url} looks up from here."),
_ => format!("It's not just you! {url} looks down from here."),
})
}
In this example, the Context
struct encapsulates an HTTP client, which is then used by the down_for_everyone_or_just_me
job. This design allows for easy testing and potential replacement of the HTTP client implementation without changing the job's code.
What is a Context Extension?
A Context Extension is a powerful feature in the Gadget SDK that allows you to add functionality to a Context without modifying its original structure. It's essentially a trait that can be implemented for your Context, providing additional methods and capabilities.
Context Extensions offer several advantages:
- Modularity: You can add new functionality without changing existing code.
- Separation of Concerns: Different aspects of the Context can be defined and implemented separately.
- Reusability: Extensions can be shared across different Context types.
- Flexibility: You can choose which extensions to implement based on your needs.
For more on extension methods and traits in Rust:
- Rust by Example: Traits (opens in a new tab)
- The Rust Programming Language: Traits: Defining Shared Behavior (opens in a new tab)
Built-in Context Extensions
The Gadget SDK provides several built-in Context Extensions that offer common functionality:
KeystoreContext
: Provides access to the GenericKeyStore (opens in a new tab), useful for managing cryptographic keys and secrets.EVMProviderContext
: Offers access to the EVM (Ethereum Virtual Machine) Provider, which is an RPC Client for interacting with EVM-compatible blockchains.TangleClientContext
: Provides access to the Tangle Client (Subxt), used for interacting with the Tangle network.ServicesContext
: Allows access to the current Service instance properties, useful for service-specific operations.
for more documentation about the built-in extensions, please refer to the API Documentation (opens in a new tab).
These extensions can be easily added to your Context using derive macros, as shown in the following example.
Example: Using the KeystoreContext
Extension
Here's an example of how to use the built-in KeystoreContext
extension:
use std::convert::Infallible;
use gadget_sdk::ctx::KeystoreContext;
use gadget_sdk::config::StdGadgetConfigurtion;
#[derive(Debug, Clone, KeystoreContext)] // Derive the KeystoreContext extension
struct Context {
http_client: reqwest::Client,
#[config]
sdk_config: StdGadgetConfigurtion,
}
#[job(id = 2, params(x), result(_))]
pub fn x_squared(ctx: &Context, x: u64) -> Result<u64, Infallible> {
let keystore = ctx.keystore(); // Access the keystore using the extension method
// Use the keystore here ...
let x_squared = x.pow(2);
Ok(x_squared)
}
By deriving KeystoreContext
, we add the keystore()
method to our Context
, allowing easy access to the GenericKeyStore
. This pattern can be applied to other built-in extensions as well.
How to create a custom Context Extension?
Context Extensions are just Rust traits that can be implemented for your Context struct. Here is an example of how you can create a custom Context Extension:
- Define the goal of the extension and the methods it should provide.
- Create a new trait with the required methods.
- Create a derive macro to automatically implement the trait for your Context struct (optional).
- Implement the trait for your Context struct, providing the required methods.
Creating a Custom Context Extension
Creating a custom Context Extension involves the following steps:
- Define the extension's purpose and required methods.
- Create a new trait with these methods.
- (Optional) Create a derive macro for automatic trait implementation.
- Implement the trait for your Context struct.
Here's an improved example of a custom HttpClientContext
extension:
use reqwest::Client;
// Define the HttpClientContext trait
pub trait HttpClientContext {
fn http_client(&self) -> Client;
}
// Implement the trait for our Context
impl HttpClientContext for Context {
fn http_client(&self) -> Client {
self.http_client.clone()
}
}
// Example job using the custom extension
#[job(id = 3, params(url), result(_))]
pub async fn fetch_url(ctx: &Context, url: String) -> Result<String, reqwest::Error> {
let client = ctx.http_client();
let response = client.get(&url).send().await?;
let body = response.text().await?;
Ok(body)
}
Summary and Conclusion
In this guide, we've explored the concepts of Context and Context Extensions in the Gadget SDK:
- We learned that a Context encapsulates dependencies and environmental setup for jobs.
- We saw how Contexts promote decoupling, testability, and reusability in code.
- We examined built-in Context Extensions provided by the Gadget SDK.
- We created a custom Context Extension to add new functionality.
These patterns are fundamental to writing maintainable, testable, and flexible blueprints in the Gadget SDK ecosystem.