field-injection
Implements field injection with
SKILL.mdskills/field-injection/SKILL.md
GitHubField Injection
Rules
Inject<T>fields → auto-injected, no annotationArc<T>orTfields → require#[injectable(inject)]- External/non-injectable fields → use
#[injectable(ctor)]constructor instead
Basic patterns
use injectable::prelude::*;
// ── Inject<T>: auto-injected (most common) ──────────────────────────────────
#[injectable]
struct UserService {
db: Inject<Database>, // no #[injectable(inject)] needed
cache: Inject<Cache>,
}
// ── Arc<T>: explicit #[injectable(inject)] ──────────────────────────────────────────────
#[injectable]
struct RepoService {
#[injectable(inject)]
db: Arc<Database>,
}
// ── Fields with factories ────────────────────────────────────────────────────
#[injectable(factory)]
async fn make_pool(cfg: Inject<AppConfig>) -> Result<Pool<Sqlite>, sqlx::Error> {
sqlx::SqlitePool::connect(&cfg.db_url).await
}
#[injectable]
struct Database {
#[injectable(inject(use_factory_async = self::make_pool))]
pool: Pool<Sqlite>,
}
// ── Non-injectable fields → use constructor ──────────────────────────────────
struct Service {
db: Inject<Database>,
name: String, // String is not injectable
}
#[injectable]
impl Service {
#[injectable(ctor)]
fn new(db: Inject<Database>) -> Self {
Self { db, name: "default".into() }
}
}
Scoping
#[injectable] // Singleton (default)
struct Cache { db: Inject<Database> }
#[injectable(scope = Transient)] // Fresh instance each resolution
struct RequestLogger { db: Inject<Database> }
Resolve
let container = Container::builder().build().await?;
let svc = container.resolve::<UserService>().await?;
// or via context (uses singleton cache):
let svc: Inject<UserService> = container.context().extract().await?;
See guides/02-field-injection.md for full reference.