| Crates.io | cargo-mercury-cli |
| lib.rs | cargo-mercury-cli |
| version | 0.1.0 |
| created_at | 2026-01-19 21:16:13.105759+00 |
| updated_at | 2026-01-19 21:16:13.105759+00 |
| description | CLI tool for generating PureScript types from Rust using Mercury |
| homepage | https://github.com/lethalgem/mercury |
| repository | https://github.com/lethalgem/mercury |
| max_upload_size | |
| id | 2055379 |
| size | 33,985 |
Automatic PureScript type generation from Rust
Mercury is a code generator that automatically creates PureScript type definitions and Argonaut JSON codecs from annotated Rust types. It eliminates manual synchronization between your Rust backend and PureScript frontend, preventing type mismatches and reducing boilerplate.
#[mercury] and get PureScript typesrename_all, rename, skip, and other serde attributesEncodeJson and DecodeJson instancesOption<T>, Vec<T>, DateTime, Uuid, and nested types# Cargo.toml
[dependencies]
mercury-derive = "0.1"
# For the CLI tool (optional)
[workspace.dependencies]
mercury = "0.1"
Or install the CLI globally:
cargo install mercury-cli
use mercury_derive::mercury;
use serde::{Deserialize, Serialize};
#[mercury]
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CreateProductRequest {
pub product_name: String,
pub price: i32,
pub is_active: bool,
pub tags: Vec<String>,
}
#[mercury]
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum ProductStatus {
Active,
Archived,
}
# If installed globally:
mercury generate
# Or from your workspace:
cargo run --bin mercury -- generate
-- frontend/src/Generated/Generated/Models.purs
module Generated.Models where
import Prelude
import Data.Argonaut.Decode.Class (class DecodeJson)
import Data.Argonaut.Encode.Class (class EncodeJson)
import Data.Maybe (Maybe(..))
newtype CreateProductRequest = CreateProductRequest
{ productName :: String
, price :: Int
, isActive :: Boolean
, tags :: Array String
}
-- Codecs automatically generated!
instance decodeCreateProductRequest :: DecodeJson CreateProductRequest
instance encodeCreateProductRequest :: EncodeJson CreateProductRequest
data ProductStatus = Active | Archived
instance decodeProductStatus :: DecodeJson ProductStatus
instance encodeProductStatus :: EncodeJson ProductStatus
Mercury maps Rust types to their PureScript equivalents:
| Rust Type | PureScript Type | Notes |
|---|---|---|
i32, i64 |
Int |
|
f32, f64 |
Number |
|
bool |
Boolean |
|
String |
String |
|
Option<T> |
Maybe T |
Nullable fields |
Vec<T> |
Array T |
|
chrono::DateTime<Utc> |
String |
ISO 8601 format |
uuid::Uuid |
MerchantFacingId |
Custom newtype wrapper |
| Custom types | Same name | Enums and structs |
Mercury handles arbitrarily nested types:
#[mercury]
pub struct UserList {
pub users: Vec<Option<User>>,
pub admin: Option<Admin>,
}
Generates:
newtype UserList = UserList
{ users :: Array (Maybe User)
, admin :: Maybe Admin
}
Mercury respects your serde configuration:
rename_all#[mercury]
#[serde(rename_all = "camelCase")]
pub struct ApiResponse {
pub user_id: i32, // → userId in JSON
pub created_at: String, // → createdAt in JSON
}
Supported rename rules:
camelCase - user_name → userNamePascalCase - user_name → UserNamesnake_case - UserName → user_nameSCREAMING_SNAKE_CASE - user_name → USER_NAMEkebab-case - user_name → user-namelowercase - UserName → usernameUPPERCASE - user_name → USER_NAMErenameOverride individual field names:
#[mercury]
pub struct User {
#[serde(rename = "id")]
pub user_id: i32,
}
skip and skip_serializingExclude fields from generated types:
#[mercury]
pub struct User {
pub email: String,
#[serde(skip_serializing)]
pub password_hash: String, // Not included in PureScript
}
Mercury organizes output by source file location:
Your Rust project:
├── src/models.rs → Generated.Models
├── src/user.rs → Generated.User
└── src/product/
├── core.rs → Generated.Product.Core
└── variant.rs → Generated.Product.Variant
Generated PureScript:
frontend/src/Generated/Generated/
├── Models.purs
├── User.purs
└── Product/
├── Core.purs
└── Variant.purs
Mercury automatically generates import statements when types reference other types:
// src/user.rs
#[mercury]
pub enum UserRole { User, Admin }
// src/models.rs
#[mercury]
pub struct UserInfo {
pub role: UserRole, // References UserRole
}
Generates with automatic import:
-- Generated.Models
module Generated.Models where
import Generated.User (UserRole) -- Automatically added!
newtype UserInfo = UserInfo
{ role :: UserRole
}
cargo run --bin mercury -- generate
Output:
✓ Scanning workspace...
✓ Found 25 types in 8 files
✓ Generating PureScript modules...
Generated.Models (17 types)
Generated.Merchant (1 type)
Generated.Product.Core (7 types)
✓ Generated 25 types in 3 modules
✓ Wrote 3 files to frontend/src/Generated/
--workspace <path> - Path to workspace root (default: current directory)--verbose - Show detailed progress--output <dir> - Specify output directory (default: frontend/src/Generated)Use the check command to verify generated code is up-to-date (useful for CI):
cargo run --bin mercury -- check
cargo run --bin mercury -- check --fail-on-diff # Exit with error if out of sync
This is useful in CI pipelines to ensure generated types stay synchronized with Rust definitions.
Rust:
#[mercury]
#[serde(rename_all = "camelCase")]
pub struct UpdateProductRequest {
pub product_id: i32,
pub new_name: Option<String>,
pub new_price: Option<i32>,
}
Generated PureScript:
newtype UpdateProductRequest = UpdateProductRequest
{ productId :: Int
, newName :: Maybe String
, newPrice :: Maybe Int
}
instance decodeUpdateProductRequest :: DecodeJson UpdateProductRequest where
decodeJson json = do
obj <- decodeJson json
productId <- obj .: "productId"
newName <- obj .:? "newName" -- Uses .:? for Maybe (treats null and missing as Nothing)
newPrice <- obj .:? "newPrice"
pure $ UpdateProductRequest { productId, newName, newPrice }
instance encodeUpdateProductRequest :: EncodeJson UpdateProductRequest where
encodeJson (UpdateProductRequest record) =
encodeJson
{ "productId": record.productId
, "newName": record.newName
, "newPrice": record.newPrice
}
Note on Optional Fields: Mercury uses .:? (getFieldOptional') for Option<T> fields, which treats both missing JSON keys and null values as Nothing. This matches Rust's serde behavior when Option::None fields are omitted from JSON (the default, or with #[serde(skip_serializing_if = "Option::is_none")]).
Rust:
#[mercury]
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum OrderStatus {
Pending,
Confirmed,
Shipped,
Delivered,
}
Generated PureScript:
data OrderStatus
= Pending
| Confirmed
| Shipped
| Delivered
instance decodeOrderStatus :: DecodeJson OrderStatus where
decodeJson json = do
str <- decodeJson json
case str of
"pending" -> Right Pending
"confirmed" -> Right Confirmed
"shipped" -> Right Shipped
"delivered" -> Right Delivered
_ -> Left $ TypeMismatch "Invalid OrderStatus"
instance encodeOrderStatus :: EncodeJson OrderStatus where
encodeJson value =
let
str = case value of
Pending -> "pending"
Confirmed -> "confirmed"
Shipped -> "shipped"
Delivered -> "delivered"
in
encodeJson str
Mercury includes comprehensive testing. See TESTING.md for details.
Test Coverage:
Run tests:
cd lib/mercury
cargo test
All tests must pass with 100% success rate.
See PUBLISHING.md for instructions on publishing Mercury to crates.io.
Mercury's pipeline:
#[mercury] annotationssyn crate to parse Rust syntax treeslib/mercury-derive/ - Procedural macro (#[mercury] attribute)
lib/mercury/
├── src/
│ ├── lib.rs - Public API and pipeline orchestration
│ ├── scanner.rs - Find #[mercury] annotations
│ ├── parser.rs - Parse Rust AST with syn
│ ├── analyzer.rs - Type mapping logic
│ ├── codegen.rs - Generate PureScript types
│ ├── codec_gen.rs - Generate Argonaut codecs
│ ├── writer.rs - File writing and organization
│ └── error.rs - Error types and messages
└── tests/ - Integration tests
Mercury currently does not support:
struct Wrapper<T> not supportedstruct Point(i32, i32) not supportedVec<(K, V)> insteadVariant(Data))These limitations may be addressed in future versions.
#[mercury] annotationcargo run --bin mercury -- generateEnsure generated code stays in sync:
# .github/workflows/ci.yml
- name: Generate PureScript types
run: cargo run --bin mercury -- generate
- name: Check for uncommitted changes
run: |
if ! git diff --exit-code frontend/src/Generated/; then
echo "Generated code is out of sync!"
echo "Run: cargo run --bin mercury -- generate"
exit 1
fi
#[mercury]cargo test in mercury crate#[mercury] is presentpubContributions are welcome! Please feel free to submit a Pull Request.
Licensed under either of:
at your option.
For issues or questions, please open an issue on GitHub.
Generated with Mercury - Keeping Rust and PureScript types in perfect sync.