Skip to content

Repository files navigation

cue-dotnet

.NET 10 bindings and code-generation tooling for CUE

See LICENSE in this repository and the license of the libcue/CUE project for the respective licensing terms.

Project structure

Warning

cue-dotnet depends on the separate libcue project. The native library must be built first and copied to the root of this repository before building, testing, or running the generator.

This project integrates with the native Go implementation of cue through a CGO adapter forked from libcue:

flowchart LR
    A["cuelang/cue<br/>Go"]
    B["intresrl/libcue<br/>Go + CGO"]
    D["this repository<br/>Cue.Api<br/>P/Invoke"]
    E[".NET consumers"]
    F["this repository<br/>Cue.Generator<br/>C#"]
    G["Generated C#"]

    A --> B --> D
    D --> E
    D --> F --> G
Loading

This repository has the following layout:

  • Cue.Api --- managed .NET API over libcue using P/Invoke.
  • Cue.Generator --- CLI that compiles CUE schemas and generates C# code.
  • Examples --- sample CUE schemas, generated C# files, andgenerator debug output.

Building

Prerequisites

  • .NET 10 SDK.
  • Go 1.25.0: CGO must be enabled and a compatible C compiler must be installed.

To check CGO run:

go env CGO_ENABLED # should output '1'

Building libcue

A convenient checkout layout is:

<your_clone_directory>/
├── libcue/
└── cue-dotnet/

The output of the libcue build must be generated or copied into the root of cue-dotnet.

On Linux:

cd libcue && go build -buildmode=c-shared -o ../cue-dotnet/libcue.so

On Windows (Git Bash, msys2 or similar):

Caution

On Windows, keep the "lib" prefix in libcue.dll to avoid overwriting cue.h in libcue.

cd libcue && go build -buildmode=c-shared -o ../cue-dotnet/libcue.dll

Build cue-dotnet

After the native dependency is present in the repository root:

dotnet restore
dotnet build

Warning

Run a rebuild every time you make changes in libcue. The DLL or shared library is copied in the output directory of Cue.Api.

Cue.Api (CUE to dotnet adapter library)

CUE operations begin with a context:

CueContext owns the native CUE context while Value represents a managed wrapper around a native CUE value.

Keep the context alive for the lifetime of values created from it and dispose native-backed objects appropriately.

Cue.Generator

Cue.Generator is a .NET CLI that compiles a CUE schema and generates C# source.

You may execute it like this:

# dotnet run --project Cue.Generator -- <input.cue> <output.cs>
dotnet run --project Cue.Generator -- Examples/simple.cue generated.cs

An optional debug output path can be supplied via the --debug parameter.

To regenerate every example, use the shell script from the repository root. It discovers all .cue files beneath Examples automatically and writes the corresponding .cs and .debug.log files alongside each schema:

bash ./run-generator-examples.sh

Generator concepts

The current implementation and tests cover CUE concepts including:

The generated representation can model alternatives as interfaces and record implementations instead of arbitrarily reducing a CUE disjunction to one type.

Generator Logic

Constrained types

CUE primitive definitions with constraints are encoded as readonly record structs that wrap a value with an IsValid() validation method.

The inner value type is narrowed based on the constraint range or the literal magnitude. Unbounded or very large ranges use BigInteger, floating point uses the ExtendedNumerics library's BigDecimal type, and bounded ranges select the smallest fitting type.

When a literal is encoded in a constraint definition its type is encoded to the smallest numeric type compatible with its value. decimal is the only type used instead of BigDecimal for floating point types.

Constraint logic in IsValid() is encoded exactly as CUE expressions:

  • Range bounds become comparisons: int & >=0 & <=100value >= 0 && value <= 100
  • Regex constraints use Regex.IsMatch(): string & =~"pattern"Regex.IsMatch(value, "pattern")
  • Literal disjunctions become || chains: 1 | 5 | 10value == 1 || value == 5 || value == 10

CUE:

// Examples of constrained primitive types showing type selection and validation logic.
#Port: int & >=1 & <=65535
#Age: int & >=0 & <=150
#Percentage: number & >=0 & <=100
#CryptographicHash: int & >0 // unbounded, uses BigInteger
#Timestamp: 1234567890123456789 // large literal, uses BigInteger
#Precision: 3.141592653589793238462643383279 // arbitrary precision, uses BigDecimal
#EmailString: string & =~"^.+@.+$"
#Status: "pending" | "active" | "done"

Generated C#:

public readonly record struct Age(byte Value)
{
public static bool IsValid(byte value) => value >= 0 && value <= 150;
}
public readonly record struct CryptographicHash(BigInteger Value)
{
public static bool IsValid(BigInteger value) => value > 0;
}
public readonly record struct EmailString(string Value)
{
public static bool IsValid(string value) => Regex.IsMatch(value, "^.+@.+$");
}
public readonly record struct Port(ushort Value)
{
public static bool IsValid(ushort value) => value >= 1 && value <= 65535;
}
public readonly record struct Precision(BigDecimal Value)
{
public static bool IsValid(BigDecimal value) => value == BigDecimal.Parse("3.141592653589793238462643383278999999989006698799107083012975956508235442952113852105");
}
public readonly record struct Status(string Value)
{
public static bool IsValid(string value) => value == "pending" || value == "active" || value == "done";
}
public readonly record struct Timestamp(ulong Value)
{
public static bool IsValid(ulong value) => value == 1234567890123456789UL;

See the full generated file for complete examples of constrained primitive types with validation logic.

Structs & composition

CUE struct definitions are encoded as classes with properties. Fields are required by default; optional fields (field?) or nullable fields (null | type) become nullable/non-required properties.

CUE:

// Examples of struct definitions with required/optional/nullable fields.
#Address: {
street: string
city: string
country: string
}
#Person: {
name: string
address: #Address
email?: string // optional field
notes: null | string // nullable field
}
#Employee: {
name: string
email?: string
notes: null | string
employeeNumber: string
department: string
}

Generated C#:

public class Address
{
public required string Street { get; init; }
public required string City { get; init; }
public required string Country { get; init; }
}
public class Employee
{
public required string Name { get; init; }
public string Email { get; init; }
public required string? Notes { get; init; }
public required string EmployeeNumber { get; init; }
public required string Department { get; init; }
}
public class Person
{
public required string Name { get; init; }
public required Address Address { get; init; }
public string Email { get; init; }
public required string? Notes { get; init; }

Lists & nesting

CUE lists become List<T>. For concrete index-specific lists (e.g., [string, int, bool]), the generator creates tuples or CueList<TConcrete, TAnyIndex> types to distinguish fixed elements from variable-length tails.

Inline struct definitions are extracted as separate classes and referenced:

CUE:

// Examples of lists, tuples, and nested structures.
#Profile: {
displayName: string
settings: {
theme: string
notifications: bool
}
tags?: [...string]
}
#Order: {
id: string
items: [...{sku: string, quantity: int}]
}
// Fixed-position list (concrete indexes)
#Coordinates: [number, number, number]
// Open list (any index)
#Numbers: [...int]

Generated C#:

public readonly struct Coordinates((decimal, decimal, decimal) value)
{
public (decimal, decimal, decimal) Value { get; } = value;
public static implicit operator Coordinates((decimal, decimal, decimal) value) => new(value);
}
public readonly struct Numbers(List<long> value)
{
public List<long> Value { get; } = value;
public static implicit operator Numbers(List<long> value) => new(value);
}
public class Order
{
public required string Id { get; init; }
public required List<OrderitemsAny> Items { get; init; }
}
public class OrderitemsAny
{
public required string Sku { get; init; }
public required long Quantity { get; init; }
}
public class Profile
{
public required string DisplayName { get; init; }
public required Profilesettings Settings { get; init; }
public List<string> Tags { get; init; }
}
public class Profilesettings
{
public required string Theme { get; init; }
public required bool Notifications { get; init; }

Unions & references

Named struct disjunctions create an interface with nested record types for each variant, plus a special Value record that holds all possible branches:

CUE:

// Examples of unions and named references.
#EmailContact: {
address: string
}
#PhoneContact: {
number: string
}
#Contact: {
value: #EmailContact | #PhoneContact
}

Generated C#:

public interface ContactvalueBase
{
public record AsEmailContact(EmailContact value) : ContactvalueBase;
public record AsPhoneContact(PhoneContact value) : ContactvalueBase;
public record Value(ContactvalueBase[] Branches)
{
public bool Valid => Branches.Length == 1;
};
}
public class Contact
{
public required ContactvalueBase Value { get; init; }
}
public class EmailContact
{
public required string Address { get; init; }
}
public class PhoneContact
{
public required string Number { get; init; }

About

P/Invoke integration for libcue and CUE to C# transpiler for CUE schema definitions

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages