csharp-rs 0.1.2

Generate C# type definitions from Rust structs and enums
Documentation
# csharp-rs

[![Crates.io](https://img.shields.io/crates/v/csharp-rs)](https://crates.io/crates/csharp-rs)
[![docs.rs](https://img.shields.io/docsrs/csharp-rs)](https://docs.rs/csharp-rs)
[![CI](https://github.com/Pulsar-Anvil-Studios/csharp-rs/actions/workflows/rust-checks.yml/badge.svg)](https://github.com/Pulsar-Anvil-Studios/csharp-rs/actions/workflows/rust-checks.yml)
[![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
[![MSRV](https://img.shields.io/badge/MSRV-1.85-orange.svg)](https://blog.rust-lang.org/2025/02/20/Rust-1.85.0.html)

Generate C# type definitions from Rust structs and enums via `#[derive(CSharp)]`.

Heavily inspired by the excellent [ts-rs](https://github.com/Aleph-Alpha/ts-rs) crate.

## Get Started

Add the derive macro to your Rust types:

```rust
use csharp_rs::CSharp;

#[derive(CSharp)]
#[csharp(export)]
struct Player {
    name: String,
    level: i32,
    active: bool,
    inventory: Vec<String>,
}
```

Run `cargo test` and get a generated `.cs` file:

```csharp
// <auto-generated/>
using System;
using System.Text.Json.Serialization;

namespace Generated;

public sealed record Player
{
    [JsonPropertyName("name")]
    public string Name { get; init; }

    [JsonPropertyName("level")]
    public int Level { get; init; }

    [JsonPropertyName("active")]
    public bool Active { get; init; }

    [JsonPropertyName("inventory")]
    public List<string> Inventory { get; init; }
}
```

## About

This crate was entirely vibe-coded as a side project.
It was born from a Unity game that needed to share types with a Rust HTTP backend,
and the lack of a Rust-to-C# type generation tool at the time.

## ✨ Features

- **Derive macro** on structs and enums — `#[derive(CSharp)]`
- **Serde attribute support**`rename_all`, `rename`, `skip`, `skip_serializing`, `skip_serializing_if`, `flatten`, and all 4 tagged enum representations
- **Dual serializer**`System.Text.Json` (default) and `Newtonsoft.Json`
- **C# version targeting** — Unity, C# 9, 10, 11, 12
- **Tagged enums** — auto-generated `JsonConverter<T>` for all serde tagging modes, with native `[JsonPolymorphic]` support for C# 11+
- **Optional type integrations**`chrono`, `uuid`, `serde_json` via feature flags
- **Export at test time**`#[csharp(export)]` generates `.cs` files when running `cargo test`
- **Runtime configuration**`Config` builder pattern with environment variable support

## 📦 Installation

```bash
cargo add csharp-rs
```

### Optional features

| Feature | Description |
|---------|-------------|
| `chrono-impl` | `NaiveDate` &rarr; `DateOnly`, `DateTime<Utc>` &rarr; `DateTimeOffset`, `NaiveTime` &rarr; `TimeOnly`, `Duration` &rarr; `TimeSpan` |
| `uuid-impl` | `Uuid` &rarr; `Guid` |
| `serde-json-impl` | `serde_json::Value` &rarr; `JsonElement` (STJ) / `JToken` (Newtonsoft) |

```bash
cargo add csharp-rs --features chrono-impl,uuid-impl,serde-json-impl
```

## 🚀 Quick Start

### 1. Derive and annotate your types

```rust
use csharp_rs::CSharp;

#[derive(CSharp)]
#[csharp(export)]                          // export to default directory
#[csharp(namespace = "MyGame.Shared")]     // override default namespace
struct GameState {
    round: u32,
    players: Vec<String>,
    winner: Option<String>,
}
```

### 2. Configure the output

Use the builder pattern:

```rust
use csharp_rs::{Config, Serializer, CSharpVersion};

let cfg = Config::default()
    .with_serializer(Serializer::Newtonsoft)
    .with_target(CSharpVersion::CSharp11)
    .with_namespace("MyGame.Shared")
    .with_export_dir("./generated");
```

Or set environment variables for CI:

```bash
CSHARP_RS_SERIALIZER=newtonsoft   # "stj" or "newtonsoft"
CSHARP_RS_TARGET=11               # "unity", "9", "10", "11", "12"
CSHARP_RS_NAMESPACE=MyGame.Shared
CSHARP_RS_EXPORT_DIR=./generated
```

> `#[csharp(namespace = "...")]` on a type overrides the global `Config` namespace.

### 3. Generate

```bash
cargo test
```

C# files are written to the configured export directory (default: `./csharp-bindings`).

## 🔧 Serde Attributes

| Attribute | Effect |
|-----------|--------|
| `#[serde(rename_all = "...")]` | Apply naming convention to all fields/variants |
| `#[serde(rename = "...")]` | Rename an individual field or variant |
| `#[serde(skip)]` | Omit field/variant from C# output |
| `#[serde(skip_serializing)]` | Include field as output-only |
| `#[serde(skip_serializing_if = "...")]` | Conditional serialization |
| `#[serde(flatten)]` | Inline nested struct fields or `HashMap` as extension data |
| `#[serde(tag = "...")]` | Internally tagged enum |
| `#[serde(tag = "...", content = "...")]` | Adjacently tagged enum |
| `#[serde(untagged)]` | Untagged enum |

## 🔄 Serializer Comparison

The same Rust struct generates different C# attributes depending on the serializer:

**System.Text.Json** (default):

```csharp
using System.Text.Json.Serialization;

public sealed record Player
{
    [JsonPropertyName("name")]
    public string Name { get; init; }
}
```

**Newtonsoft.Json**:

```csharp
using Newtonsoft.Json;

public sealed record Player
{
    [JsonProperty("name")]
    public string Name { get; init; }
}
```

> Unity mode generates `sealed class` with `{ get; set; }` properties instead of records.

## 🎯 C# Version Targeting

| Version | Records | File-scoped NS | `required` | `[JsonPolymorphic]` |
|---------|---------|----------------|------------|---------------------|
| Unity   | No | No | No | No |
| C# 9    | Yes | No | No | No |
| C# 10   | Yes | Yes | No | No |
| C# 11   | Yes | Yes | Yes | Yes (STJ only) |
| C# 12   | Yes | Yes | Yes | Yes (STJ only) |

> Unity mode uses `sealed class` with `{ get; set; }` properties instead of records.

## 🏷️ Enums

### Simple enums

```rust
#[derive(CSharp)]
enum Color {
    Red,
    Green,
    Blue,
}
```

```csharp
[JsonConverter(typeof(JsonStringEnumConverter))]
public enum Color
{
    Red,
    Green,
    Blue,
}
```

### Tagged enums

```rust
#[derive(CSharp)]
#[serde(tag = "type")]
enum Message {
    Request { id: String, method: String },
    Quit,
}
```

```csharp
[JsonConverter(typeof(MessageConverter))]
public abstract record Message;

public sealed record Request(
    [property: JsonPropertyName("id")] string Id,
    [property: JsonPropertyName("method")] string Method
) : Message;

public sealed record Quit : Message;

// + auto-generated MessageConverter class
```

All four serde tagging modes are supported: internally tagged, adjacently tagged, externally tagged, and untagged.

## ⚠️ Limitations

- **Tuple variants** in tagged enums are rejected with a compile error
- **Tuple structs** are not supported (only named-field structs)
- **Generic Rust types** with type parameters are not yet supported

## 🙏 Acknowledgements

This project is heavily inspired by [ts-rs](https://github.com/Aleph-Alpha/ts-rs), which generates TypeScript type definitions from Rust types.
csharp-rs follows the same derive-and-export pattern and draws from ts-rs's runtime `Config` design (introduced in ts-rs v12).

## 📄 License

MIT &mdash; see [LICENSE](LICENSE) for details.