Context-Generic Programming (CGP)

repository·main·Indexed 42 days ago

https://github.com/contextgeneric/cgp

A Rust library and language extension that enables pluggable trait implementations at compile-time. CGP allows developers to write core logic abstract over dependencies, letting different contexts (such as test, production, or embedded) provide concrete implementations without runtime overhead. It includes tools like cargo-cgp for improved error diagnostics and cgp-macro-test-util for snapshot testing of macros.

Tokens
28K
Snippets
107
Records
126
Agent score
53%

What's inside CGP

  1. Use `cgp-macro-test-util` for snapshot testing CGP macros

    main

    The cgp-macro-test-util crate provides a family of snapshot_*! procedural macros designed to pin the code generated by core CGP macros.

    When you use a snapshot macro, it performs two simultaneous actions:

    1. Emits the real generated code: It expands the underlying CGP macro into the module exactly as it would in production. This ensures that traits, structs, and implementations remain live so you can still write tests against their runtime behavior.
    2. Generates a #[test]: It creates a test function that asserts a pretty-printed string of that same expansion against an inline insta snapshot.

    This allows you to verify that macro expansions remain stable without breaking the compile-time or runtime coverage of your code.

  2. CGP Project Organization

    main

    CGP is organized into small, layered crates. Most users only need to depend on the single cgp crate, which provides access to the core functionality via cgp::prelude.

    Pluggable error backends are available as separate, opt-in crates:

    • cgp-error-anyhow
    • cgp-error-eyre
    • cgp-error-std
  3. What is Context-Generic Programming (CGP)?

    main

    Context-Generic Programming (CGP) is a language extension for Rust that enables pluggable trait implementations at compile-time. Unlike standard Rust, where a trait has exactly one implementation per type, CGP allows a single trait to have multiple interchangeable implementations.

    Each context can choose which implementation to use via a small wiring table resolved statically by the compiler. This provides high flexibility without any runtime performance cost. The cgp crate serves as a facade that re-exports all necessary micro-crates through cgp::prelude.

  4. How Context-Generic Programming (CGP) works

    main

    CGP is a language extension for Rust that allows one interface to have multiple interchangeable implementations, chosen per context (e.g., an application, a test, or a specific deployment) at compile time.

    Core Concepts

    • Contexts: Plain data structs that hold dependencies (e.g., database pools, clients).
    • Implicit Arguments: Using the #[implicit] attribute allows a function to automatically read a dependency from the context struct without passing it as an explicit argument.
    • Components and Providers: For complex interfaces, you define a #[cgp_component] trait. You then write multiple #[cgp_impl] implementations. Each context then uses delegate_components! to pick which implementation to use.
    • Zero-Cost: All wiring is resolved at compile time. Unused implementations are not included in the binary, and calls are direct (no dyn or runtime dispatch).
    use cgp::prelude::*;
    
    // 1. Define a Context
    #[derive(HasField)]
    pub struct App {
        pub database: PgPool,
    }
    
    // 2. Define an operation with implicit dependencies
    #[cgp_fn]
    pub async fn get_user(
        &self,
        #[implicit] database: &PgPool, // Automatically pulled from App
        user_id: &UserId,
    ) -> anyhow::Result<User> {
        // ...
    }
    
    // 3. Define a Component for complex interfaces
    #[cgp_component(StorageObjectFetcher)]
    pub trait CanFetchStorageObject {
        async fn fetch_storage_object(&self, object_id: &str) -> anyhow::Result<Vec<u8>>;
    }
    
    // 4. Provide multiple implementations
    #[cgp_impl(new FetchS3Object)]
    impl StorageObjectFetcher { /* S3 logic */ }
    
    #[cgp_impl(new FetchGCloudObject)]
    impl StorageObjectFetcher { /* GCloud logic */ }
    
    // 5. Wire the context to a specific implementation
    delegate_components! {
        App {
            StorageObjectFetcherComponent: FetchS3Object,
        }
    }
  5. Snapshot formatting and limitations

    main

    Formatting

    Snapshots are formatted using prettyplease after stripping macro-prelude noise via cgp_macro_core::functions::strip_macro_prelude. This ensures that snapshots are stable, readable Rust source code.

    Limitations

    • Derive Macros: A derive member re-emits the annotated item itself. This is because derive macros only produce the code they add, not the struct or enum they decorate.
  6. When to use CGP

    main

    CGP is best used when a capability requires multiple implementations and the specific implementation is determined by the context rather than runtime logic.

    Use CaseRecommended Tool
    One implementationPlain trait or function
    Small, closed set of variantsenum and match
    Runtime selection (plugins, config-driven)dyn Trait (dynamic dispatch)
    Global uniqueness required (e.g., Ord)Standard coherent traits
    Context-specific implementationsCGP
  7. Install CGP

    main

    To use CGP in your project, add the cgp crate via Cargo.

    Note on Versions:

    • cargo add cgp installs the current stable release (v0.7.0).
    • The documentation in this repository describes v0.8.0 (currently in pre-release). To use the features described in these docs, you must depend on the main branch.

    CGP requires Rust 1.89+ on the stable toolchain.

    # Install stable v0.7.0
    cargo add cgp
    
    # OR: Install v0.8.0 (as described in these docs)
    # Add this to your Cargo.toml:
    cgp = { git = "https://github.com/contextgeneric/cgp" }
  8. Rules for reading versus linking to sibling projects

    main

    When interacting with sibling projects, distinguish between reading (consuming information) and linking (providing references in documentation).

    Reading a sibling

    When reading source files or documentation from a sibling project, always use the specific branch or tag recorded in the project registry (typically main). This ensures cross-project consistency.

    Linking to a sibling

    When adding a link to a committed file within this repository, do not use relative filesystem paths (like ../cargo-cgp/...). Instead, always use a GitHub URL pointing to the main branch to ensure the link works for users who only have this specific repository checked out.

    Correct Link Format: https://github.com/contextgeneric/<project>/blob/main/<path>

  9. Workflow for managing snapshots with `insta`

    main

    When adding a new snapshot, follow this workflow:

    1. Initialize with an empty snapshot: Write the test using an empty inline snapshot: assert_snapshot!(output, @"").
    2. Run tests to generate snapshots: Use cargo insta test to let insta capture the expansion.
    3. Review and Accept: Use cargo insta review to inspect the generated code and cargo insta test --accept to confirm it.

    Commands:

    # Run tests and generate snapshots
    cargo insta test -p cgp-tests --test getter
    
    # Review snapshots
    cargo insta review
    
    # Accept all non-interactive changes
    cargo insta test -p cgp-tests --accept
    
    # Force update via environment variable
    INSTA_UPDATE=always cargo test -p cgp-tests
    cargo insta test -p cgp-tests --test getter   # then: cargo insta review
    cargo insta test -p cgp-tests --accept        # accept everything non-interactively
    INSTA_UPDATE=always cargo test -p cgp-tests   # or via the env var
  10. Install cargo-cgp for readable errors

    main

    CGP wiring mistakes can produce complex compiler errors. cargo-cgp is a diagnostic tool that rewrites these errors into a compact, root-cause-first format. It is highly recommended for development.

    To set it up:

    1. Install the front-end.
    2. Run the setup command to provision the necessary compiler and driver.
    3. Use cargo cgp check instead of cargo check to see improved error messages.
    # Install the tool
    cargo install cargo-cgp
    
    # Provision the compiler/driver
    cargo cgp setup
    
    # Use for checking errors
    cargo cgp check
  11. What is the `Field` type and how is it used?

    main

    The Field<Tag, Value> type represents a named field entry within a product type (like a struct) or a sum type (like an enum).

    It is a wrapper around a Value, parameterized by a phantom Tag type that represents the field's name at the type level. Common tags include:

    • Type-level strings: Symbol!("name")
    • Type-level indices: Index<0>

    Field is primarily used by the HasFields derive macro to include field names in the generic representation of a struct or enum. When you derive HasFields, the macro generates a Fields type alias that uses Field to associate each field's name (the Tag) with its data type (Value).

    #[derive(HasFields)]
    pub struct MyContext {
        pub name: String,
        pub age: u8,
    }
    
    // The generated implementation looks like this:
    impl HasFields for MyContext {
        type Fields = Product![Field<Symbol!("name"), String>, Field<Symbol!("age"), u8>];
    }