Developers
Understanding Blueprint Macros
Context and Context Extensions

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:

Why do we need a Context?

The Context pattern offers several significant benefits in software design and development:

  1. 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.

  2. Testability: With a Context, it's easier to mock or stub dependencies during unit testing, allowing for more comprehensive and isolated tests.

  3. Flexibility: Contexts allow for easy swapping of implementations, which is particularly useful when adapting to different environments (e.g., development, staging, production).

  4. Separation of Concerns: The Context pattern helps separate the configuration and setup of dependencies from their usage, leading to cleaner, more focused code.

  5. 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:

  1. Modularity: You can add new functionality without changing existing code.
  2. Separation of Concerns: Different aspects of the Context can be defined and implemented separately.
  3. Reusability: Extensions can be shared across different Context types.
  4. Flexibility: You can choose which extensions to implement based on your needs.

For more on extension methods and traits in Rust:

Built-in Context Extensions

The Gadget SDK provides several built-in Context Extensions that offer common functionality:

  1. KeystoreContext: Provides access to the GenericKeyStore (opens in a new tab), useful for managing cryptographic keys and secrets.
  2. EVMProviderContext: Offers access to the EVM (Ethereum Virtual Machine) Provider, which is an RPC Client for interacting with EVM-compatible blockchains.
  3. TangleClientContext: Provides access to the Tangle Client (Subxt), used for interacting with the Tangle network.
  4. 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:

  1. Define the goal of the extension and the methods it should provide.
  2. Create a new trait with the required methods.
  3. Create a derive macro to automatically implement the trait for your Context struct (optional).
  4. 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:

  1. Define the extension's purpose and required methods.
  2. Create a new trait with these methods.
  3. (Optional) Create a derive macro for automatic trait implementation.
  4. 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:

  1. We learned that a Context encapsulates dependencies and environmental setup for jobs.
  2. We saw how Contexts promote decoupling, testability, and reusability in code.
  3. We examined built-in Context Extensions provided by the Gadget SDK.
  4. 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.