Skip to main content

Extension Builder

ExtensionBuilder validates dependencies at compile time using type-level lists and the Subset trait, then build() returns a TypedExtensionRegistry or error.

ExtensionBuilder provides type-safe extension registration with compile-time dependency checking using Rust's type system.

Basic Usage

use systemprompt::extension::prelude::*;

let registry = ExtensionBuilder::new()
    .extension(UsersExtension::default())
    .extension(OAuthExtension::default())
    .extension(MyExtension::default())
    .build()?;

Type-Level Tracking

The builder tracks registered types at compile time:

pub struct ExtensionBuilder<Registered: TypeList = ()> {
    extensions: Vec<Box<dyn AnyExtension>>,
    _marker: PhantomData<Registered>,
}

Each extension() call extends the type list:

impl<R: TypeList> ExtensionBuilder<R> {
    pub fn extension<E>(mut self, ext: E) -> ExtensionBuilder<(E, R)>
    where
        E: ExtensionType + Dependencies + std::fmt::Debug + 'static,
        E::Deps: Subset<R>,  // Compile-time check!
    {
        // ...
    }
}

Dependency Validation

Dependencies are validated at compile time. Deps is a cons-style type list, written as nested pairs terminated by ():

// UsersExtension has no dependencies
impl Dependencies for UsersExtension {
    type Deps = ();
}

// OAuthExtension depends on UsersExtension
impl Dependencies for OAuthExtension {
    type Deps = (UsersExtension, ());
}

// This compiles:
ExtensionBuilder::new()
    .extension(UsersExtension::default())  // Registered: (Users, ())
    .extension(OAuthExtension::default())  // Deps (Users, ()) subset of (Users, ()) ✓
    .build()

// This fails at compile time:
ExtensionBuilder::new()
    .extension(OAuthExtension::default())  // Deps (Users, ()) NOT subset of () ✗
    .build()

Specialized Builders

Schema Extensions

ExtensionBuilder::new()
    .schema_extension(DatabaseExtension::default())
    .schema_extension(UsersExtension::default())
    .build()

API Extensions

ExtensionBuilder::new()
    .api_extension(ContentApiExtension::default())
    .api_extension(FilesApiExtension::default())
    .build()

Mixed Extensions

ExtensionBuilder::new()
    .extension(DatabaseExtension::default())
    .schema_extension(UsersExtension::default())
    .api_extension(ContentApiExtension::default())
    .build()

TypeList

The type list tracks registered extensions:

pub trait TypeList: 'static {
    fn contains_type<T: 'static>() -> bool;
    fn type_ids() -> Vec<TypeId>;
    fn len() -> usize;
}

// Empty list
impl TypeList for () {
    fn contains_type<T: 'static>() -> bool { false }
    fn type_ids() -> Vec<TypeId> { vec![] }
    fn len() -> usize { 0 }
}

// Cons list
impl<Head: 'static, Tail: TypeList> TypeList for (Head, Tail) {
    fn contains_type<T: 'static>() -> bool {
        TypeId::of::<Head>() == TypeId::of::<T>() || Tail::contains_type::<T>()
    }
    // ...
}

Subset Trait

Validates dependency satisfaction:

pub trait Subset<B: TypeList>: TypeList {
    fn is_subset_of() -> bool;
}

// Empty is subset of anything
impl<B: TypeList> Subset<B> for () {
    fn is_subset_of() -> bool { true }
}

// (Head, Tail) is subset of B if Head is in B and Tail is subset of B
impl<Head: 'static, Tail: TypeList + Subset<B>, B: TypeList> Subset<B> for (Head, Tail) {
    fn is_subset_of() -> bool {
        B::contains_type::<Head>() && Tail::is_subset_of()
    }
}

TypedExtensionRegistry

The builder produces a typed registry:

pub struct TypedExtensionRegistry {
    extensions: Vec<Box<dyn AnyExtension>>,
    by_id: HashMap<String, usize>,
    api_paths: Vec<String>,
}

impl TypedExtensionRegistry {
    pub fn has(&self, id: &str) -> bool;
    pub fn get(&self, id: &str) -> Option<&dyn AnyExtension>;
    pub fn schema_extensions(&self) -> impl Iterator<Item = &dyn SchemaExtensionTyped>;
    pub fn api_extensions(&self) -> impl Iterator<Item = &dyn ApiExtensionTypedDyn>;
    pub fn all_extensions(&self) -> impl Iterator<Item = &dyn AnyExtension>;
    pub fn api_paths(&self) -> &[String];
    pub fn len(&self) -> usize;
    pub fn is_empty(&self) -> bool;
}

Error Handling

Runtime validation can still fail:

pub fn build(self) -> Result<TypedExtensionRegistry, LoaderError> {
    let mut registry = TypedExtensionRegistry::new();
    let mut sorted = self.extensions;
    sorted.sort_by_key(|e| e.priority());

    for ext in sorted {
        // Check for duplicates
        if registry.has(ext.id()) {
            return Err(LoaderError::DuplicateExtension(ext.id().to_owned()));
        }

        // Validate API base paths as they are added
        if let Some(api) = ext.as_api() {
            registry.validate_api_path(ext.id(), api.base_path())?;
        }

        registry.add_boxed(ext);
    }

    Ok(registry)
}

Example: Complete Setup

use systemprompt::extension::prelude::*;

// Define extensions with dependencies
#[derive(Debug, Default)]
struct DatabaseExtension;

impl ExtensionType for DatabaseExtension {
    const ID: &'static str = "database";
    const NAME: &'static str = "Database";
    const VERSION: &'static str = "0.1.0";
    const PRIORITY: u32 = 1;
}

impl Dependencies for DatabaseExtension {
    type Deps = ();
}

#[derive(Debug, Default)]
struct UsersExtension;

impl ExtensionType for UsersExtension {
    const ID: &'static str = "users";
    const NAME: &'static str = "Users";
    const VERSION: &'static str = "0.1.0";
    const PRIORITY: u32 = 10;
}

impl Dependencies for UsersExtension {
    type Deps = (DatabaseExtension, ());
}

// Build registry
fn create_registry() -> Result<TypedExtensionRegistry, LoaderError> {
    ExtensionBuilder::new()
        .extension(DatabaseExtension)
        .extension(UsersExtension)
        .build()
}

When to Use

Use ExtensionBuilder when:

  • Building a fixed set of extensions
  • Want compile-time dependency validation
  • Need type-safe extension access

Use ExtensionRegistry::discover() when:

  • Extensions register dynamically
  • Using register_extension! macro
  • Building plugin systems