Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 87 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,30 +14,74 @@

## Features

- Export structs and enums to [Typescript](https://www.typescriptlang.org)
- Get function types to use in libraries like [tauri-specta](https://github.com/oscartbeaumont/tauri-specta)
- Supports wide range of common crates in Rust ecosystem
- Supports type inference - can determine type of `fn demo() -> impl Type`.
- Export structs and enums to multiple languages
- Get function types to use in libraries like [tauri-specta](https://github.com/oscartbeaumont/tauri-specta)
- Supports wide range of common crates in Rust ecosystem
- Supports type inference - can determine type of `fn demo() -> impl Type`

## Language Support

| Language | Status | Exporter | Features |
| --------------- | -------------- | ----------------------------------------------------------------- | ------------------------------------------------- |
| **TypeScript** | ✅ **Stable** | [`specta-typescript`](https://crates.io/crates/specta-typescript) | Full type support, generics, unions |
| **Swift** | ✅ **Stable** | [`specta-swift`](https://crates.io/crates/specta-swift) | Idiomatic Swift, custom Codable, Duration support |
| **Rust** | 🚧 **Partial** | [`specta-rust`](https://crates.io/crates/specta-rust) | Basic types work, structs/enums in progress |
| **OpenAPI** | 🚧 **Partial** | [`specta-openapi`](https://crates.io/crates/specta-openapi) | Primitives work, complex types in progress |
| **Go** | 🚧 **Planned** | [`specta-go`](https://crates.io/crates/specta-go) | Go structs and interfaces |
| **Kotlin** | 🚧 **Planned** | [`specta-kotlin`](https://crates.io/crates/specta-kotlin) | Kotlin data classes and sealed classes |
| **JSON Schema** | 🚧 **Planned** | [`specta-jsonschema`](https://crates.io/crates/specta-jsonschema) | JSON Schema generation |
| **Zod** | 🚧 **Planned** | [`specta-zod`](https://crates.io/crates/specta-zod) | Zod schema validation |
| **Python** | 🚧 **Planned** | `specta-python` | Python dataclasses and type hints |
| **C#** | 🚧 **Planned** | `specta-csharp` | C# classes and enums |
| **Java** | 🚧 **Planned** | `specta-java` | Java POJOs and enums |

### Legend

- ✅ **Stable**: Production-ready with comprehensive test coverage
- 🚧 **Partial**: Basic functionality implemented, complex types in progress
- 🚧 **Planned**: In development or planned for future release

## Implementation Status

The Specta ecosystem is actively developed with varying levels of completeness:

- **Production Ready (2)**: TypeScript and Swift exporters are fully functional with comprehensive test coverage
- **Partially Implemented (2)**: Rust and OpenAPI exporters have basic functionality working, with complex types in progress
- **Planned (7)**: Go, Kotlin, JSON Schema, Zod, Python, C#, and Java exporters are in development

For the most up-to-date status of each exporter, check the individual crate documentation and issue trackers.

## Ecosystem

Specta can be used in your application either directly or through a library which simplifies the process of using it.

- [rspc](https://github.com/oscartbeaumont/rspc) - Easily building end-to-end typesafe APIs
- [tauri-specta](https://github.com/oscartbeaumont/tauri-specta) - Typesafe Tauri commands and events
- [TauRPC](https://github.com/MatsDK/TauRPC) - Tauri extension to give you a fully-typed IPC layer.
- [rspc](https://github.com/oscartbeaumont/rspc) - Easily building end-to-end typesafe APIs
- [tauri-specta](https://github.com/oscartbeaumont/tauri-specta) - Typesafe Tauri commands and events
- [TauRPC](https://github.com/MatsDK/TauRPC) - Tauri extension to give you a fully-typed IPC layer.

## Usage

Add the [`specta`](https://docs.rs/specta) crate along with any Specta language exporter crate, for example [`specta-typescript`](https://docs.rs/specta-typescript).
Add the [`specta`](https://docs.rs/specta) crate along with any Specta language exporter crate:

```bash
# Core Specta library
cargo add specta
cargo add specta_typescript

# Language exporters (choose one or more)
cargo add specta_typescript # TypeScript (stable)
cargo add specta_swift # Swift (stable)
cargo add specta_rust # Rust (partial - basic types)
cargo add specta_openapi # OpenAPI/Swagger (partial - primitives)
# cargo add specta_go # Go (planned)
# cargo add specta_kotlin # Kotlin (planned)
# cargo add specta_jsonschema # JSON Schema (planned)
# cargo add specta_zod # Zod schemas (planned)
```

Then you can use Specta like following:

### TypeScript Example

```rust
use specta::{Type, TypeCollection};
use specta_typescript::Typescript;
Expand Down Expand Up @@ -89,6 +133,40 @@ export type TypeOne = { a: string; b: GenericType<number>; cccccc: MyEnum };

```

### Multi-Language Export Example

You can export the same types to multiple languages:

```rust
use specta::{Type, TypeCollection};
use specta_typescript::Typescript;
use specta_swift::Swift;

#[derive(Type)]
pub struct User {
pub id: u32,
pub name: String,
pub email: Option<String>,
}

fn main() {
let types = TypeCollection::default()
.register::<User>();

// Export to TypeScript (stable)
Typescript::default()
.export_to("./types.ts", &types)
.unwrap();

// Export to Swift (stable)
Swift::default()
.export_to("./Types.swift", &types)
.unwrap();

// Note: Other exporters are in development
}
```

A common use case is to export all types for which `specta::Type` is derived into a single file:

```rust
Expand Down
140 changes: 88 additions & 52 deletions specta-macros/src/type/enum.rs
Original file line number Diff line number Diff line change
Expand Up @@ -126,58 +126,94 @@ pub fn parse_enum(
})
.collect::<syn::Result<Vec<_>>>()?;

let (can_flatten, repr) = match (enum_attrs.untagged, &enum_attrs.tag, &enum_attrs.content) {
(None, None, None) => (
// TODO: We treat the default being externally tagged but that is a bad assumption.
// Fix this with: https://github.com/specta-rs/specta/issues/384
data.variants.iter().any(|v| match &v.fields {
Fields::Unnamed(f) if f.unnamed.len() == 1 => true,
Fields::Named(_) => true,
_ => false,
}),
quote!(None),
),
(Some(false), None, None) => (
data.variants.iter().any(|v| match &v.fields {
Fields::Unnamed(f) if f.unnamed.len() == 1 => true,
Fields::Named(_) => true,
_ => false,
}),
quote!(Some(#crate_ref::datatype::EnumRepr::External)),
),
(Some(false) | None, Some(tag), None) => (
data.variants
.iter()
.any(|v| matches!(&v.fields, Fields::Unit | Fields::Named(_))),
quote!(Some(#crate_ref::datatype::EnumRepr::Internal { tag: #tag.into() })),
),
(Some(false) | None, Some(tag), Some(content)) => (
true,
quote!(Some(#crate_ref::datatype::EnumRepr::Adjacent { tag: #tag.into(), content: #content.into() })),
),
(Some(true), None, None) => (
data.variants
.iter()
.any(|v| matches!(&v.fields, Fields::Unit | Fields::Named(_))),
quote!(Some(#crate_ref::datatype::EnumRepr::Untagged)),
),
(Some(true), Some(_), None) => {
return Err(Error::new(
Span::call_site(),
"untagged cannot be used with tag",
))
}
(Some(true), _, Some(_)) => {
return Err(Error::new(
Span::call_site(),
"untagged cannot be used with content",
))
}
(Some(false) | None, None, Some(_)) => {
return Err(Error::new(
Span::call_site(),
"content cannot be used without tag",
))
// Check if this should be a string enum
let is_string_enum = data
.variants
.iter()
.all(|v| matches!(&v.fields, Fields::Unit))
&& container_attrs.rename_all.is_some()
&& enum_attrs.untagged.is_none()
&& enum_attrs.tag.is_none()
&& enum_attrs.content.is_none();

let (can_flatten, repr) = if is_string_enum {
// Generate string enum representation
let rename_all = container_attrs
.rename_all
.as_ref()
.map(|inflection| {
let inflection_str = match inflection {
crate::utils::Inflection::Lower => "lowercase",
crate::utils::Inflection::Upper => "UPPERCASE",
crate::utils::Inflection::Camel => "camelCase",
crate::utils::Inflection::Snake => "snake_case",
crate::utils::Inflection::Pascal => "PascalCase",
crate::utils::Inflection::ScreamingSnake => "SCREAMING_SNAKE_CASE",
crate::utils::Inflection::Kebab => "kebab-case",
crate::utils::Inflection::ScreamingKebab => "SCREAMING-KEBAB-CASE",
};
quote!(Some(#inflection_str.into()))
})
.unwrap_or_else(|| quote!(None));

(
false, // String enums can't be flattened
quote!(Some(#crate_ref::datatype::EnumRepr::String { rename_all: #rename_all })),
)
} else {
match (enum_attrs.untagged, &enum_attrs.tag, &enum_attrs.content) {
(None, None, None) => (
// TODO: We treat the default being externally tagged but that is a bad assumption.
// Fix this with: https://github.com/specta-rs/specta/issues/384
data.variants.iter().any(|v| match &v.fields {
Fields::Unnamed(f) if f.unnamed.len() == 1 => true,
Fields::Named(_) => true,
_ => false,
}),
quote!(None),
),
(Some(false), None, None) => (
data.variants.iter().any(|v| match &v.fields {
Fields::Unnamed(f) if f.unnamed.len() == 1 => true,
Fields::Named(_) => true,
_ => false,
}),
quote!(Some(#crate_ref::datatype::EnumRepr::External)),
),
(Some(false) | None, Some(tag), None) => (
data.variants
.iter()
.any(|v| matches!(&v.fields, Fields::Unit | Fields::Named(_))),
quote!(Some(#crate_ref::datatype::EnumRepr::Internal { tag: #tag.into() })),
),
(Some(false) | None, Some(tag), Some(content)) => (
true,
quote!(Some(#crate_ref::datatype::EnumRepr::Adjacent { tag: #tag.into(), content: #content.into() })),
),
(Some(true), None, None) => (
data.variants
.iter()
.any(|v| matches!(&v.fields, Fields::Unit | Fields::Named(_))),
quote!(Some(#crate_ref::datatype::EnumRepr::Untagged)),
),
(Some(true), Some(_), None) => {
return Err(Error::new(
Span::call_site(),
"untagged cannot be used with tag",
))
}
(Some(true), _, Some(_)) => {
return Err(Error::new(
Span::call_site(),
"untagged cannot be used with content",
))
}
(Some(false) | None, None, Some(_)) => {
return Err(Error::new(
Span::call_site(),
"content cannot be used without tag",
))
}
}
};

Expand Down
2 changes: 2 additions & 0 deletions specta-serde/src/validate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,8 @@ fn validate_internally_tag_enum_datatype(
EnumRepr::Internal { .. } => {}
// Eg. `{ "type": "variant", "c": {} }` is a map-type so valid.
EnumRepr::Adjacent { .. } => {}
// String enums serialize as strings, which are valid
EnumRepr::String { .. } => {}
},
// `()` is `null` and is valid
DataType::Tuple(ty) if ty.elements().is_empty() => {}
Expand Down
37 changes: 35 additions & 2 deletions specta-swift/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
name = "specta-swift"
description = "Export your Rust types to Swift"
version = "0.0.1"
authors = ["Oscar Beaumont <[email protected]>"]
authors = ["Jamie Pine <[email protected]>"]
edition = "2021"
license = "MIT"
repository = "https://github.com/oscartbeaumont/specta"
Expand All @@ -19,4 +19,37 @@ rustdoc-args = ["--cfg", "docsrs"]
workspace = true

[dependencies]
specta = { path = "../specta" }
specta = { path = "../specta", features = ["derive", "uuid", "chrono"] }
specta-serde = { path = "../specta-serde" }
thiserror = "1.0"
serde = { version = "1.0", features = ["derive"] }

[dev-dependencies]
insta = "1.42"
trybuild = "1.0"
uuid = "1.12.1"
chrono = { version = "0.4.40", features = ["clock"] }

[[example]]
name = "basic_types"
path = "examples/basic_types.rs"

[[example]]
name = "advanced_unions"
path = "examples/advanced_unions.rs"

[[example]]
name = "configuration_options"
path = "examples/configuration_options.rs"

[[example]]
name = "special_types"
path = "examples/special_types.rs"

[[example]]
name = "string_enums"
path = "examples/string_enums.rs"

[[example]]
name = "comprehensive_demo"
path = "examples/comprehensive_demo.rs"
Loading
Loading