npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@hafley66/alloy-rs

v0.1.0

Published

TypeSpec-to-Rust code generation. Turns TypeSpec models into compilable Rust crates with correct `use`, `mod`, serde derives, and cross-file references.

Readme

@hafley/alloy-rs

TypeSpec-to-Rust code generation. Turns TypeSpec models into compilable Rust crates with correct use, mod, serde derives, and cross-file references.

What it generates

Given TypeSpec like this:

model User {
  id: int64;
  name: string;
  email?: string;
  address: Address;
  role: Role;
}

model Address {
  street: string;
  city: string;
}

enum Role { Admin, Member, Guest }

model Team {
  name: string;
  members: User[];
  metadata: Record<string, string>;
  created_at: utcDateTime;
}

You get a crate:

src/
  lib.rs              # pub mod models;
  models.rs           # all model types in one file

Where models.rs looks like:

use chrono::DateTime;
use chrono::Utc;
use serde::Deserialize;
use serde::Serialize;
use std::collections::HashMap;

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Address {
  pub street: String,
  pub city: String,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum Role {
  Admin,
  Member,
  Guest,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct User {
  pub id: i64,
  pub name: String,
  pub email: Option<String>,
  pub address: Address,
  pub role: Role,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Team {
  pub name: String,
  pub members: Vec<User>,
  pub metadata: HashMap<String, String>,
  pub created_at: DateTime<Utc>,
}

Related types in the same module reference each other directly -- no use crate:: needed within the file. Cross-file references (e.g. from route handlers) still produce use crate::models::User automatically.

Using with TypeSpec

The adapter converts a TypeSpec program into Rust output in two steps: extract types from the compiler, then emit.

import { compile } from "@typespec/compiler";
import { programToTypeDefs } from "@hafley/alloy-rs/adapters";
import { emitCrate } from "@hafley/alloy-rs/emitter";

// 1. Compile a .tsp file (or pass an existing Program from a custom emitter)
const program = await compile("./main.tsp");

// 2. Extract models and enums from all user-defined namespaces
//    Skips TypeSpec stdlib types. Resolves scalar chains (e.g. uuid -> string).
const types = programToTypeDefs(program);

// 3. Emit a Rust crate
const output = emitCrate(types);

programToTypeDefs walks the program's global namespace, recursing into child namespaces while skipping the TypeSpec stdlib. It converts:

  • model with scalar properties into struct definitions
  • enum into enum definitions with optional values
  • model references (a property typed as another model) into cross-type references
  • T[] (Array with indexer) into Vec<T>
  • Record<K, V> (Record with indexer) into HashMap<K, V>
  • Scalar chains (scalar uuid extends string) are resolved to the root scalar name

For finer control, use namespaceToTypeDefs to target a specific namespace:

import { namespaceToTypeDefs } from "@hafley/alloy-rs/adapters";

// Extract from a specific namespace, non-recursive
const types = namespaceToTypeDefs(myNamespace);

// Or recurse into sub-namespaces
const allTypes = namespaceToTypeDefs(myNamespace, { recursive: true });

Types that the adapter doesn't handle (unions, literals, etc.) fall back to string.

Type mapping

| TypeSpec | Rust | Crate dependency | |---|---|---| | string | String | - | | boolean | bool | - | | int8 .. int64 | i8 .. i64 | - | | uint8 .. uint64 | u8 .. u64 | - | | float32, float64 | f32, f64 | - | | bytes | Vec<u8> | - | | utcDateTime | DateTime<Utc> | chrono | | offsetDateTime | DateTime<FixedOffset> | chrono | | plainDate | NaiveDate | chrono | | plainTime | NaiveTime | chrono | | duration | Duration | std | | uuid | Uuid | uuid | | decimal | Decimal | rust_decimal | | T? (optional) | Option<T> | - | | T[] | Vec<T> | - | | Record<K, V> | HashMap<K, V> | std | | model reference | type name + use crate:: | - | | enum reference | type name + use crate:: | - |

How to: serde structs and enums

The components handle serde at two levels -- container attributes on structs/enums, and field attributes on individual fields.

import { Output, render, refkey, List } from "@alloy-js/core";
import { StructDeclaration, StructField } from "@hafley/alloy-rs";
import { EnumDeclaration, UnitVariant, StructVariant } from "@hafley/alloy-rs";
import { SourceFile, CrateDirectory, ModDirectory } from "@hafley/alloy-rs";
import { VisibilityContext } from "@hafley/alloy-rs";

// Struct with serde field config
<SourceFile path="event.rs" externalUses={["serde::Deserialize", "serde::Serialize"]}>
  <StructDeclaration
    name="Event"
    derive={["Debug", "Clone", "Serialize", "Deserialize"]}
    serde={{ denyUnknownFields: true }}
  >
    <List hardline>
      <StructField name="id" type="i64" />
      <StructField name="name" type="String" />
      <StructField name="metadata" type="String"
        serde={{ skipSerializingIf: "String::is_empty" }} />
      <StructField name="internal" type="bool"
        serde={{ skip: true }} />
      <StructField name="config" type="Config"
        serde={{ flatten: true }} />
    </List>
  </StructDeclaration>
</SourceFile>

Produces:

use serde::Deserialize;
use serde::Serialize;

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Event {
  pub id: i64,
  pub name: String,
  #[serde(skip_serializing_if = "String::is_empty")]
  pub metadata: String,
  #[serde(skip)]
  pub internal: bool,
  #[serde(flatten)]
  pub config: Config,
}

Tagged enums work the same way:

<EnumDeclaration
  name="Message"
  derive={["Debug", "Clone", "Serialize", "Deserialize"]}
  serde={{ tag: "type", content: "data" }}
>
  <List hardline>
    <StructVariant name="Text">
      <StructField name="body" type="String" />
    </StructVariant>
    <StructVariant name="Image">
      <StructField name="url" type="String" />
      <StructField name="width" type="u32" />
    </StructVariant>
    <UnitVariant name="Ping" />
  </List>
</EnumDeclaration>

Produces:

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", content = "data")]
pub enum Message {
  Text {
    pub body: String,
  },
  Image {
    pub url: String,
    pub width: u32,
  },
  Ping,
}

Serde config options

Container (serde prop on StructDeclaration / EnumDeclaration):

| Config key | Rust output | |---|---| | tag: "t" | #[serde(tag = "t")] | | content: "c" | #[serde(content = "c")] | | untagged: true | #[serde(untagged)] | | denyUnknownFields: true | #[serde(deny_unknown_fields)] | | default: true | #[serde(default)] | | transparent: true | #[serde(transparent)] |

Field (serde prop on StructField):

| Config key | Rust output | |---|---| | skip: true | #[serde(skip)] | | skipSerializing: true | #[serde(skip_serializing)] | | skipDeserializing: true | #[serde(skip_deserializing)] | | skipSerializingIf: "Option::is_none" | #[serde(skip_serializing_if = "Option::is_none")] | | default: true | #[serde(default)] | | default: "default_val" | #[serde(default = "default_val")] | | flatten: true | #[serde(flatten)] | | with: "my_serde" | #[serde(with = "my_serde")] | | alias: "old_name" | #[serde(alias = "old_name")] |

How to: axum endpoints

Two components for endpoints, depending on whether you want paired files or zone-based single files.

Paired files with <Endpoint>

Generates name_auto.rs (framework glue, regenerated) and name.rs (domain logic, yours).

import { Output, render, refkey, List } from "@alloy-js/core";
import { StructDeclaration, StructField, Endpoint } from "@hafley/alloy-rs";
import { SourceFile, CrateDirectory, ModDirectory } from "@hafley/alloy-rs";
import { VisibilityContext } from "@hafley/alloy-rs";
import { ZoneProvider, Zone } from "@hafley/alloy-rs";
import { FunctionDeclaration } from "@hafley/alloy-rs";

const userKey = refkey();
const postKey = refkey();

render(
  <Output>
    <VisibilityContext.Provider value="pub">
      <ZoneProvider>
        <CrateDirectory>
          {/* Models -- all in one file */}
          <SourceFile path="models.rs" externalUses={["serde::Deserialize", "serde::Serialize"]}>
            <StructDeclaration name="User" refkey={userKey}
              derive={["Debug", "Clone", "Serialize", "Deserialize"]}>
              <List hardline>
                <StructField name="id" type="i64" />
                <StructField name="name" type="String" />
              </List>
            </StructDeclaration>
            {"\n"}
            <StructDeclaration name="Post" refkey={postKey}
              derive={["Debug", "Clone", "Serialize", "Deserialize"]}>
              <List hardline>
                <StructField name="id" type="i64" />
                <StructField name="title" type="String" />
              </List>
            </StructDeclaration>
          </SourceFile>

          {/* Endpoints */}
          <ModDirectory name="routes">
            <Endpoint
              name="list_users"
              path="/users"
              method="get"
              routePath="routes::list_users_auto"
              responseModel={userKey}
              responseList
            />
            <Endpoint
              name="get_user"
              path="/users/:id"
              method="get"
              routePath="routes::get_user_auto"
              responseModel={userKey}
              params={[{
                extractor: "Path(user_id)",
                extractorType: "Path<i64>",
                name: "user_id",
                type: "i64",
              }]}
            />
          </ModDirectory>

          {/* Router with zone collection */}
          <SourceFile path="lib.rs" externalUses={["axum::Router", "axum::routing::get"]}>
            <FunctionDeclaration name="router" returns="Router">
              Router::new()<Zone name="router" />
            </FunctionDeclaration>
          </SourceFile>
        </CrateDirectory>
      </ZoneProvider>
    </VisibilityContext.Provider>
  </Output>
);

This produces:

src/
  lib.rs
  models.rs                    # User + Post in one file
  routes/
    mod.rs                     # pub mod list_users_auto; pub mod list_users; ...
    list_users_auto.rs         # axum handler, regenerated
    list_users.rs              # domain stub, yours to edit
    get_user_auto.rs
    get_user.rs

models.rs:

use serde::Deserialize;
use serde::Serialize;

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct User {
  pub id: i64,
  pub name: String,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Post {
  pub id: i64,
  pub title: String,
}

list_users_auto.rs:

use axum::Json;

use crate::models::User;

pub async fn list_users() -> Json<Vec<User>> {
  Json(crate::routes::list_users::list_users_impl().await)
}

list_users.rs:

pub async fn list_users_impl() -> Vec<User> {
  todo!()
}

lib.rs collects routes from all endpoints via the <Zone>:

pub mod models;
pub mod routes;

use axum::Router;
use axum::routing::get;

pub fn router() -> Router {
  Router::new()
    .route("/users", get(routes::list_users_auto::list_users))
    .route("/users/:id", get(routes::get_user_auto::get_user))
}

Single file with <AxumEndpoint>

Uses ReplaceFile with auto/manual zones in one file. On re-emit, auto zones are replaced while manual zones are preserved from the existing file on disk.

<AxumEndpoint
  name="list_users"
  path="/users"
  method="get"
  routePath="routes::list_users"
  responseModel={userKey}
  responseList
  existingFile="/path/to/existing/list_users.rs"  // optional: preserves manual zones
/>

Produces a single list_users.rs with zone sigils:

// alloy-handler-start
pub async fn list_users() -> Json<Vec<User>> {
  Json(list_users_impl().await)
}
// alloy-handler-end

// manual zone below -- your code survives re-generation
pub async fn list_users_impl() -> Vec<User> {
  todo!()
}

How to: file and module structure

Three components control the crate layout. Everything nests.

<CrateDirectory> -- crate root

Creates the root scope. All use crate:: paths resolve relative to this.

<CrateDirectory>
  <SourceFile path="lib.rs" />   {/* or main.rs */}
  {/* children become the crate contents */}
</CrateDirectory>

<ModDirectory> -- module with auto mod.rs

Creates a directory with an auto-generated mod.rs. Any <SourceFile> inside it gets a pub mod entry in mod.rs. Use this when a module genuinely needs multiple files (e.g. routes with auto/manual pairs). For a bag of related types, prefer a single <SourceFile> instead.

{/* Routes need separate files (auto/manual split), so ModDirectory fits */}
<ModDirectory name="routes">
  <SourceFile path="list_users_auto.rs" .../>
  <SourceFile path="list_users.rs" .../>
</ModDirectory>

{/* Models are just declarations -- one file is cleaner */}
<SourceFile path="models.rs" ...>
  <StructDeclaration name="User" .../>
  <StructDeclaration name="Post" .../>
</SourceFile>

<SourceFile> -- .rs file with use management

Handles two categories of imports separately:

  • externalUses prop: explicit external crate imports (serde::Serialize, chrono::Utc, etc.)
  • Cross-file references: when a <Reference refkey={...} /> points to a declaration in another file, the use crate:: import is generated and grouped automatically
<SourceFile path="handler.rs" externalUses={["axum::Json", "axum::extract::Path"]}>
  {/* declarations and references here */}
</SourceFile>

Produces:

use axum::Json;
use axum::extract::Path;

use crate::models::user::User;    // auto-generated from Reference

// ... declarations

External uses are sorted alphabetically. Crate uses appear after a blank line separator.

Cross-file references with refkey

The refkey() function creates a stable identifier. Assign it when declaring a type, reference it from anywhere else in the tree. The framework resolves the correct use crate:: path.

const userKey = refkey();

// In models.rs
<StructDeclaration name="User" refkey={userKey} ...>

// In routes/handler.rs -- auto-generates: use crate::models::User;
<FunctionDeclaration returns={<>Json&lt;Vec&lt;<Reference refkey={userKey} />&gt;&gt;</>} ...>

References within the same file resolve directly (no use needed). References across files produce use crate:: imports automatically.

Append zones for collection patterns

<Zone> renders collected content. <AppendTo> teleports content into a named zone from anywhere in the tree. Useful for router registration, plugin lists, or any pattern where multiple components contribute to a single output location.

<ZoneProvider>
  {/* Zone renders here */}
  <SourceFile path="lib.rs">
    <FunctionDeclaration name="router" returns="Router">
      Router::new()<Zone name="router" />
    </FunctionDeclaration>
  </SourceFile>

  {/* These can be anywhere in the tree */}
  <AppendTo zone="router">
    {"\n"}.route("/users", get(routes::users::list_users))
  </AppendTo>
  <AppendTo zone="router">
    {"\n"}.route("/posts", get(routes::posts::list_posts))
  </AppendTo>
</ZoneProvider>

Multi-transport bindings

Endpoints can bind to multiple transports from the same operation definition. Each transport has an exhaustive extraction matrix:

| Param source | HTTP (axum) | WebSocket | |---|---|---| | path | Path(name): Path<T> | name: T | | query | Query(name): Query<T> | name: T | | body | Json(name): Json<T> | name: T | | header | name: HeaderMap | name: T | | resolved (per-request) | Extension(name): Extension<T> | conn.extensions().get::<T>() | | resolved (shared) | State(name): State<T> | conn.state::<T>() |

The domain function stays the same. Only the transport wrappers differ.

Running tests

pnpm test              # snapshot tests only
pnpm test:cargo        # also runs cargo check on generated crates (slower)