diff --git a/Cargo.lock b/Cargo.lock index 21e770d..3865ec9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -127,7 +127,7 @@ dependencies = [ "ctrlc", "downcast-rs", "log", - "thiserror", + "thiserror 2.0.17", "variadics_please", ] @@ -166,7 +166,7 @@ dependencies = [ "serde", "slotmap", "smallvec", - "thiserror", + "thiserror 2.0.17", "variadics_please", ] @@ -195,7 +195,7 @@ dependencies = [ "bevy_reflect", "derive_more", "log", - "thiserror", + "thiserror 2.0.17", ] [[package]] @@ -226,7 +226,7 @@ dependencies = [ "rand_distr", "serde", "smallvec", - "thiserror", + "thiserror 2.0.17", "variadics_please", ] @@ -275,7 +275,7 @@ dependencies = [ "serde", "smallvec", "smol_str", - "thiserror", + "thiserror 2.0.17", "uuid", "variadics_please", "wgpu-types", @@ -388,7 +388,7 @@ dependencies = [ "rand 0.9.2", "serde_bytes", "simdutf8", - "thiserror", + "thiserror 2.0.17", "time", "uuid", ] @@ -466,7 +466,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" dependencies = [ "iana-time-zone", + "js-sys", "num-traits", + "wasm-bindgen", "windows-link", ] @@ -1648,7 +1650,14 @@ dependencies = [ name = "specta-swift" version = "0.0.1" dependencies = [ + "chrono", + "insta", + "serde", "specta", + "specta-serde", + "thiserror 1.0.69", + "trybuild", + "uuid", ] [[package]] @@ -1700,7 +1709,7 @@ dependencies = [ "specta-serde", "specta-typescript", "specta-util", - "thiserror", + "thiserror 2.0.17", ] [[package]] @@ -1761,13 +1770,33 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + [[package]] name = "thiserror" version = "2.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" dependencies = [ - "thiserror-impl", + "thiserror-impl 2.0.17", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", ] [[package]] @@ -2100,7 +2129,7 @@ dependencies = [ "js-sys", "log", "serde", - "thiserror", + "thiserror 2.0.17", "web-sys", ] diff --git a/README.md b/README.md index bd0b12d..3409ffe 100644 --- a/README.md +++ b/README.md @@ -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; @@ -89,6 +133,40 @@ export type TypeOne = { a: string; b: GenericType; 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, +} + +fn main() { + let types = TypeCollection::default() + .register::(); + + // 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 diff --git a/specta-macros/src/type/enum.rs b/specta-macros/src/type/enum.rs index 9c60fbc..1ed9e31 100644 --- a/specta-macros/src/type/enum.rs +++ b/specta-macros/src/type/enum.rs @@ -126,58 +126,94 @@ pub fn parse_enum( }) .collect::>>()?; - 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", + )) + } } }; diff --git a/specta-serde/src/validate.rs b/specta-serde/src/validate.rs index c576a16..ccef6bb 100644 --- a/specta-serde/src/validate.rs +++ b/specta-serde/src/validate.rs @@ -244,6 +244,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() => {} diff --git a/specta-swift/Cargo.toml b/specta-swift/Cargo.toml index 9087afc..a06a6c3 100644 --- a/specta-swift/Cargo.toml +++ b/specta-swift/Cargo.toml @@ -2,7 +2,7 @@ name = "specta-swift" description = "Export your Rust types to Swift" version = "0.0.1" -authors = ["Oscar Beaumont "] +authors = ["Jamie Pine ", "Oscar Beaumont "] edition = "2024" license = "MIT" repository = "https://github.com/oscartbeaumont/specta" @@ -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" diff --git a/specta-swift/README.md b/specta-swift/README.md new file mode 100644 index 0000000..0ae3c6a --- /dev/null +++ b/specta-swift/README.md @@ -0,0 +1,397 @@ +# Specta Swift + +[![Crates.io](https://img.shields.io/crates/v/specta-swift.svg)](https://crates.io/crates/specta-swift) +[![Documentation](https://docs.rs/specta-swift/badge.svg)](https://docs.rs/specta-swift) +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) + +A Rust crate for exporting Rust types to Swift, built on top of [Specta](https://github.com/oscartbeaumont/specta). Generate idiomatic Swift code from your Rust type definitions with support for complex unions, generics, and nested structures. + +## Features + +- ๐Ÿš€ **Zero Runtime Cost** - Compile-time type generation +- ๐ŸŽฏ **Idiomatic Swift** - Generates clean, Swift-idiomatic code +- ๐Ÿ”„ **Complex Unions** - Full support for Rust enums with all variant types +- ๐Ÿงฌ **Generics** - Single and multiple generic type parameters +- ๐Ÿ”— **Recursive Types** - Self-referencing and circular type definitions +- โš™๏ธ **Highly Configurable** - Naming conventions, indentation styles, optional syntax +- ๐Ÿ“ฆ **Type Safety** - Leverages Specta's robust type introspection +- ๐Ÿงช **Well Tested** - Comprehensive test suite with snapshot testing +- ๐Ÿ•’ **Special Types** - Built-in support for Duration with helper structs +- ๐Ÿ“ **Documentation** - Preserves and formats Rust doc comments in Swift +- ๐Ÿ”ง **Custom Codable** - Automatic generation of custom Codable implementations +- ๐ŸŽจ **Protocol Conformance** - Support for additional Swift protocols +- ๐Ÿ“ **File Export** - Direct export to Swift files with custom headers + +## Quick Start + +Add `specta-swift` to your `Cargo.toml`: + +```toml +[dependencies] +specta = { version = "2.0", features = ["derive"] } +specta-swift = "0.1" +``` + +Define your Rust types: + +```rust +use specta::{Type, TypeCollection}; +use specta_swift::Swift; + +#[derive(Type)] +struct User { + id: u32, + name: String, + email: Option, + role: UserRole, +} + +#[derive(Type)] +enum UserRole { + Guest, + User { permissions: Vec }, + Admin { level: u8, department: String }, +} + +#[derive(Type)] +enum ApiResult { + Success { data: T, status: u16 }, + Error { message: String, code: u32 }, + Loading { progress: f32 }, +} +``` + +Generate Swift code: + +```rust +fn main() { + let types = TypeCollection::default() + .register::() + .register::() + .register::>(); + + let swift = Swift::default(); + swift.export_to("./Types.swift", &types).unwrap(); +} +``` + +This generates: + +```swift +// This file has been generated by Specta. DO NOT EDIT. +import Foundation + +enum ApiResult: Codable { + case success(data: T, status: UInt16) + case error(message: String, code: UInt32) + case loading(progress: Float) +} + +struct User: Codable { + let id: UInt32 + let name: String + let email: String? + let role: UserRole +} + +enum UserRole: Codable { + case guest + case user(permissions: [String]) + case admin(level: UInt8, department: String) +} +``` + +## Advanced Features + +### Complex Union Types + +Specta Swift supports all Rust enum variant types: + +```rust +#[derive(Type)] +enum ComplexUnion { + // Unit variant + None, + + // Tuple variant + Tuple(String, u32, bool), + + // Named fields variant + NamedFields { + id: u32, + name: String, + active: bool, + }, + + // Nested struct variant + UserStruct(User), + + // Nested enum variant + UserType(UserType), + + // Complex nested structure + Complex { + user: User, + metadata: Vec, + settings: Option, + }, +} +``` + +Generates: + +```swift +enum ComplexUnion: Codable { + case none + case tuple(String, UInt32, Bool) + case namedfields(id: UInt32, name: String, active: Bool) + case userstruct(User) + case usertype(UserType) + case complex(user: User, metadata: [String], settings: Admin?) +} +``` + +### Generic Types + +Full support for generic types with multiple parameters: + +```rust +#[derive(Type)] +enum DatabaseResult { + Ok { data: T, affected_rows: u64 }, + Err { error: E, query: String }, + ConnectionError { host: String, port: u16 }, +} +``` + +### Recursive Types + +Self-referencing types are fully supported: + +```rust +#[derive(Type)] +enum Shape { + None, + Point(f64, f64), + Circle { center: Point, radius: f64 }, + Complex { shapes: Vec, metadata: Option }, +} +``` + +## Configuration + +### Naming Conventions + +```rust +use specta_swift::{Swift, NamingConvention}; + +// PascalCase (default) +let swift = Swift::default(); + +// camelCase +let swift = Swift::new().naming(NamingConvention::CamelCase); + +// snake_case +let swift = Swift::new().naming(NamingConvention::SnakeCase); +``` + +### Optional Styles + +```rust +use specta_swift::{Swift, OptionalStyle}; + +// T? syntax (default) +let swift = Swift::default(); + +// Optional syntax +let swift = Swift::new().optionals(OptionalStyle::Optional); +``` + +### Indentation Styles + +```rust +use specta_swift::{Swift, IndentStyle}; + +// 4 spaces (default) +let swift = Swift::default(); + +// 2 spaces +let swift = Swift::new().indent(IndentStyle::Spaces(2)); + +// Tabs +let swift = Swift::new().indent(IndentStyle::Tabs); +``` + +### Custom Headers + +```rust +let swift = Swift::new() + .header("// Generated by MyApp v2.0\n// Custom header with app info\n// DO NOT EDIT MANUALLY") + .naming(NamingConvention::SnakeCase); +``` + +### Additional Protocols + +```rust +let swift = Swift::new() + .add_protocol("Equatable") + .add_protocol("Hashable") + .add_protocol("CustomStringConvertible"); +``` + +### Serde Integration + +```rust +let swift = Swift::new() + .with_serde() // Adds import Codable and validation + .add_protocol("CustomDebugStringConvertible"); +``` + +## Type Mapping + +| Rust Type | Swift Type | Notes | +| ------------------------- | ------------------------------------- | ------------------------------ | +| `i8`, `i16`, `i32`, `i64` | `Int8`, `Int16`, `Int32`, `Int64` | Signed integers | +| `u8`, `u16`, `u32`, `u64` | `UInt8`, `UInt16`, `UInt32`, `UInt64` | Unsigned integers | +| `f32`, `f64` | `Float`, `Double` | Floating point numbers | +| `bool` | `Bool` | Boolean values | +| `char` | `Character` | Single Unicode character | +| `String` | `String` | UTF-8 strings | +| `Option` | `T?` or `Optional` | Optional values (configurable) | +| `Vec` | `[T]` | Arrays | +| `Vec>` | `[[T]]` | Nested arrays | +| `HashMap` | `[K: V]` | Dictionaries | +| `(T, U)` | `(T, U)` | Tuples | +| `std::time::Duration` | `RustDuration` + helper | With automatic helper struct | +| `struct` | `struct` | Structures | +| `enum` | `enum` | Enums with custom Codable | + +## Special Features + +### Duration Support + +`std::time::Duration` types are automatically converted to a `RustDuration` helper struct: + +```rust +#[derive(Type)] +struct Metrics { + processing_time: Duration, + timeout: Duration, +} +``` + +Generates: + +```swift +// MARK: - Duration Helper +public struct RustDuration: Codable { + public let secs: UInt64 + public let nanos: UInt32 + + public var timeInterval: TimeInterval { + return Double(secs) + Double(nanos) / 1_000_000_000.0 + } +} + +public struct Metrics: Codable { + public let processingTime: RustDuration + public let timeout: RustDuration +} +``` + +### Documentation Support + +Rust doc comments are preserved and formatted for Swift: + +```rust +/// A comprehensive user account +/// +/// This struct represents a complete user account with all necessary +/// information for authentication and personalization. +/// +/// # Security Notes +/// - The password field should never be logged +/// - All timestamps are in UTC +#[derive(Type)] +struct User { + /// Unique identifier + id: u32, + /// User's display name + name: String, +} +``` + +Generates: + +```swift +/// A comprehensive user account +/// +/// This struct represents a complete user account with all necessary +/// information for authentication and personalization. +/// +/// # Security Notes +/// - The password field should never be logged +/// - All timestamps are in UTC +public struct User: Codable { + /// Unique identifier + public let id: UInt32 + /// User's display name + public let name: String +} +``` + +## Examples + +Check out the `examples/` directory for comprehensive examples: + +- `basic_types.rs` - Basic primitive types and their Swift equivalents +- `advanced_unions.rs` - Complex enum scenarios and custom Codable implementations +- `configuration_options.rs` - All Swift exporter configuration settings +- `special_types.rs` - Duration types and special type handling +- `string_enums.rs` - String enums and custom Codable patterns +- `comprehensive_demo.rs` - Complete feature showcase (28 types!) +- `simple_usage.rs` - Quick start example +- `comments_example.rs` - Documentation and comment support + +Run any example: + +```bash +cargo run --example basic_types +cargo run --example comprehensive_demo +``` + +Generated Swift files are saved to `examples/generated/` for inspection. + +## Testing + +Run the test suite: + +```bash +cargo test +``` + +The test suite includes: + +- Basic type generation tests +- Comprehensive union type tests +- Advanced recursive type tests +- Duration type mapping tests +- Custom Codable implementation tests +- Configuration option tests +- Snapshot testing for generated code +- String enum handling tests +- Generic type parameter tests + +## Contributing + +Contributions are welcome! Please see the main [Specta repository](https://github.com/oscartbeaumont/specta) for contribution guidelines. + +## License + +This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. + +## Related Projects + +- [Specta](https://github.com/oscartbeaumont/specta) - Core type introspection library +- [Specta TypeScript](https://github.com/oscartbeaumont/specta/tree/main/specta-typescript) - TypeScript exporter +- [Specta Go](https://github.com/oscartbeaumont/specta/tree/main/specta-go) - Go exporter diff --git a/specta-swift/Types.swift b/specta-swift/Types.swift new file mode 100644 index 0000000..c14e95c --- /dev/null +++ b/specta-swift/Types.swift @@ -0,0 +1,15 @@ +// This file has been generated by Specta. DO NOT EDIT. +import Foundation + +public struct MyOtherType: Codable { + public let otherField: String + + private enum CodingKeys: String, CodingKey { + case otherField = "other_field" + } +} + +public struct MyType: Codable { + public let field: MyOtherType +} + diff --git a/specta-swift/examples/README.md b/specta-swift/examples/README.md new file mode 100644 index 0000000..591c39c --- /dev/null +++ b/specta-swift/examples/README.md @@ -0,0 +1,225 @@ +# specta-swift Examples + +This directory contains comprehensive examples demonstrating ALL the functionality of the `specta-swift` library. Each example focuses on different aspects of the Swift code generation capabilities. + +## ๐Ÿ“š Available Examples + +### 1. **`basic_types.rs`** - Fundamental Type Mappings + +Demonstrates basic Rust to Swift type conversions: + +- โœ… Primitive types (i8, u32, f64, bool, char, String) +- โœ… Optional types (Option โ†’ T?) +- โœ… Collections (Vec โ†’ [T]) +- โœ… Nested collections (Vec> โ†’ [[T]]) +- โœ… Tuple types ((String, String) โ†’ (String, String)) +- โœ… Simple enums with different variant types +- โœ… Generic structs with type parameters +- โœ… Complex nested struct relationships + +**Run:** `cargo run --example basic_types` +**Output:** `examples/generated/BasicTypes.swift` + +### 2. **`advanced_unions.rs`** - Complex Enum Scenarios + +Showcases advanced enum patterns and custom Codable implementations: + +- โœ… Complex enums with mixed variant types +- โœ… Generic enums with type parameters +- โœ… Recursive type definitions (Tree) +- โœ… Nested struct references in enum variants +- โœ… String enums with automatic Codable +- โœ… Mixed enums (both simple and complex variants) +- โœ… Custom Codable implementations for complex enums +- โœ… Struct generation for named field variants + +**Run:** `cargo run --example advanced_unions` +**Output:** `examples/generated/AdvancedUnions.swift` + +### 3. **`configuration_options.rs`** - All Swift Exporter Settings + +Comprehensive demonstration of every configuration option: + +- โœ… Custom headers and documentation +- โœ… Naming conventions (PascalCase, camelCase, snake_case) +- โœ… Indentation styles (spaces, tabs, different widths) +- โœ… Generic type styles (protocol constraints vs typealias) +- โœ… Optional type styles (question mark vs Optional) +- โœ… Additional protocol conformance +- โœ… Serde validation settings +- โœ… Combined custom configurations + +**Run:** `cargo run --example configuration_options` +**Output:** `examples/generated/` (multiple configuration files) + +### 4. **`special_types.rs`** - Duration and Special Type Handling + +Demonstrates special type conversions and helper generation: + +- โœ… Duration type mapping to RustDuration helper +- โœ… Automatic helper struct generation +- โœ… timeInterval property for Swift integration +- โœ… Duration fields in structs and enum variants +- โœ… Optional Duration fields +- โœ… Performance metrics with timing information +- โœ… Complex timing-related data structures + +**Run:** `cargo run --example special_types` +**Output:** `examples/generated/SpecialTypes.swift` + +### 5. **`string_enums.rs`** - String Enums and Custom Codable + +Focuses on enum patterns and Codable implementations: + +- โœ… Pure string enums (String, Codable) +- โœ… Mixed enums with both simple and complex variants +- โœ… Custom Codable implementations for complex enums +- โœ… Struct generation for named field variants +- โœ… Generic enum support +- โœ… Proper Swift enum case naming +- โœ… Automatic protocol conformance + +**Run:** `cargo run --example string_enums` +**Output:** `examples/generated/StringEnums.swift` + +### 6. **`comprehensive_demo.rs`** - Complete Feature Showcase + +The ultimate example demonstrating EVERY feature in a realistic application: + +- โœ… All basic and advanced type patterns +- โœ… Complex nested relationships +- โœ… User management with permissions +- โœ… Task management with status tracking +- โœ… File attachment handling +- โœ… Comment and review systems +- โœ… API response patterns +- โœ… System monitoring types +- โœ… Health monitoring and metrics +- โœ… Pagination and metadata + +**Run:** `cargo run --example comprehensive_demo` +**Output:** `examples/generated/ComprehensiveDemo.swift` + +### 7. **`simple_usage.rs`** - Quick Start Example + +A simple, focused example for getting started quickly: + +- โœ… Basic struct and enum definitions +- โœ… Default Swift configuration +- โœ… Custom configuration demonstration +- โœ… File export functionality + +**Run:** `cargo run --example simple_usage` +**Output:** `examples/generated/SimpleTypes.swift`, `examples/generated/CustomTypes.swift` + +### 8. **`comments_example.rs`** - Documentation and Comments + +Demonstrates comprehensive documentation support: + +- โœ… Multi-line type documentation +- โœ… Field-level documentation +- โœ… Complex technical descriptions +- โœ… Swift-compatible doc comments +- โœ… Bullet points and formatting + +**Run:** `cargo run --example comments_example` +**Output:** `examples/generated/CommentsExample.swift` + +## ๐Ÿš€ Quick Start + +To run any example: + +```bash +cd specta-swift +cargo run --example +``` + +For example: + +```bash +cargo run --example basic_types +cargo run --example comprehensive_demo +``` + +## ๐Ÿ“ Generated Files + +Each example generates Swift files in the `examples/generated/` directory that you can inspect: + +- `examples/generated/BasicTypes.swift` - From basic_types example +- `examples/generated/AdvancedUnions.swift` - From advanced_unions example +- `examples/generated/SpecialTypes.swift` - From special_types example +- `examples/generated/StringEnums.swift` - From string_enums example +- `examples/generated/ComprehensiveDemo.swift` - From comprehensive_demo example +- `examples/generated/CommentsExample.swift` - From comments_example +- `examples/generated/SimpleTypes.swift` & `examples/generated/CustomTypes.swift` - From simple_usage +- Multiple configuration files from the configuration_options example + +## ๐Ÿ” Key Features Demonstrated + +### Type System Support + +- โœ… All Rust primitive types +- โœ… Optional types with proper Swift syntax +- โœ… Collections and nested collections +- โœ… Tuple types +- โœ… Generic types with constraints +- โœ… Complex nested relationships + +### Enum Handling + +- โœ… Unit variants +- โœ… Tuple variants +- โœ… Named field variants +- โœ… String enums with automatic Codable +- โœ… Mixed enums with custom implementations +- โœ… Generic enums +- โœ… Recursive enum definitions + +### Special Types + +- โœ… Duration โ†’ RustDuration helper +- โœ… Automatic helper struct generation +- โœ… timeInterval property for Swift integration +- โœ… Proper Codable implementations + +### Configuration Options + +- โœ… All naming conventions +- โœ… All indentation styles +- โœ… Generic type styles +- โœ… Optional type styles +- โœ… Additional protocols +- โœ… Custom headers and documentation + +### Code Generation Quality + +- โœ… Proper Swift naming conventions +- โœ… Comprehensive Codable implementations +- โœ… Error handling in custom Codable +- โœ… Documentation preservation +- โœ… Clean, readable Swift code + +## ๐Ÿ’ก Usage Tips + +1. **Start with `basic_types`** to understand fundamental mappings +2. **Use `comprehensive_demo`** to see everything in action +3. **Check `configuration_options`** to customize your output +4. **Examine generated `.swift` files** to see the actual output +5. **Use `special_types`** if you work with Duration types +6. **Reference `string_enums`** for enum patterns + +## ๐ŸŽฏ Real-World Applications + +These examples demonstrate patterns commonly used in: + +- ๐Ÿ“ฑ iOS/macOS app development +- ๐Ÿ”„ API client generation +- ๐Ÿ“Š Data serialization/deserialization +- ๐Ÿ—๏ธ Cross-platform type sharing +- ๐Ÿ“ˆ Performance monitoring +- ๐Ÿ‘ฅ User management systems +- ๐Ÿ“‹ Task management applications + +--- + +**Happy coding! ๐ŸŽ‰** These examples should give you everything you need to leverage the full power of `specta-swift` in your projects. diff --git a/specta-swift/examples/advanced_unions.rs b/specta-swift/examples/advanced_unions.rs new file mode 100644 index 0000000..d7781df --- /dev/null +++ b/specta-swift/examples/advanced_unions.rs @@ -0,0 +1,204 @@ +use specta::{Type, TypeCollection}; +use specta_swift::Swift; + +/// Advanced example showcasing complex enum unions and their Swift representations +/// +/// This example demonstrates how specta-swift handles complex enum scenarios +/// including nested types, generic enums, and custom Codable implementations. + +/// Complex enum with mixed variant types +#[derive(Type)] +enum ApiResult { + /// Success with data and metadata + Ok { + data: T, + status: u16, + headers: Option>, + timestamp: String, + }, + /// Error with detailed information + Err { + error: E, + code: u32, + message: String, + retry_after: Option, + }, + /// Loading state with progress + Loading { + progress: f32, + estimated_completion: Option, + }, +} + +/// Enum demonstrating different field patterns +#[derive(Type)] +enum Shape { + /// Unit variant + None, + /// Tuple variant + Point(f64, f64), + /// Named fields variant + Circle { center: Point, radius: f64 }, + /// Referencing another struct + Rectangle(Rectangle), + /// Complex nested variant + Line { + start: Point, + end: Point, + style: LineStyle, + }, + /// Very complex variant with multiple fields + Complex { + vertices: Vec, + fill_color: Color, + stroke_color: Option, + stroke_width: f64, + is_closed: bool, + }, +} + +/// Supporting structs for the Shape enum +#[derive(Type)] +struct Point { + x: f64, + y: f64, +} + +#[derive(Type)] +struct Rectangle { + top_left: Point, + bottom_right: Point, +} + +#[derive(Type)] +struct LineStyle { + dash_pattern: Option>, + cap_style: String, + join_style: String, +} + +#[derive(Type)] +struct Color { + red: f64, + green: f64, + blue: f64, + alpha: f64, +} + +/// Generic enum with constraints +#[derive(Type)] +enum Container { + Empty, + Single(T), + Multiple(Vec), + KeyValue(Vec<(String, T)>), +} + +/// Enum with recursive references +#[derive(Type)] +enum Tree { + Leaf(T), + Branch { + left: Box>, + right: Box>, + value: T, + }, +} + +/// String enum (will be converted to Swift String enum with Codable) +#[derive(Type)] +enum JobStatus { + /// Job is queued and waiting to start + Queued, + /// Job is currently running + Running, + /// Job is paused (can be resumed) + Paused, + /// Job completed successfully + Completed, + /// Job failed with an error + Failed, + /// Job was cancelled by user + Cancelled, +} + +/// Mixed enum with both string-like and data variants +#[derive(Type)] +enum MixedEnum { + /// Simple string-like variant + Simple(String), + /// Complex data variant + WithFields { + id: u32, + name: String, + metadata: Option>, + }, + /// Another simple variant + Empty, +} + +/// Event system enum +#[derive(Type)] +enum Event { + /// User-related events + User { + user_id: u32, + action: String, + timestamp: String, + }, + /// System events + System { + component: String, + level: String, + message: String, + }, + /// Error events + Error { + code: u32, + message: String, + stack_trace: Option, + }, +} + +fn main() { + println!("๐Ÿš€ Advanced Unions Example - Complex enum scenarios"); + println!("{}", "=".repeat(60)); + + // Create type collection + let types = TypeCollection::default() + .register::>() + .register::() + .register::() + .register::() + .register::() + .register::() + .register::>() + .register::>() + .register::() + .register::() + .register::(); + + // Export with default settings + let swift = Swift::default(); + let output = swift.export(&types).unwrap(); + + println!("๐Ÿ“ Generated Swift code:\n"); + println!("{}", output); + + // Write to file for inspection + swift + .export_to("./examples/generated/AdvancedUnions.swift", &types) + .unwrap(); + println!("โœ… Advanced unions exported to AdvancedUnions.swift"); + + println!("\n๐Ÿ” Key Features Demonstrated:"); + println!("โ€ข Complex enum variants with named fields"); + println!("โ€ข Generic enums with type parameters"); + println!("โ€ข String enums with automatic Codable implementation"); + println!("โ€ข Mixed enums (both simple and complex variants)"); + println!("โ€ข Recursive type definitions"); + println!("โ€ข Nested struct references in enum variants"); + println!("โ€ข Custom Codable implementations for complex enums"); + println!("โ€ข Struct generation for named field variants"); + println!("โ€ข Tuple variant handling"); +} diff --git a/specta-swift/examples/basic_types.rs b/specta-swift/examples/basic_types.rs new file mode 100644 index 0000000..7993cc1 --- /dev/null +++ b/specta-swift/examples/basic_types.rs @@ -0,0 +1,142 @@ +use specta::{Type, TypeCollection}; +use specta_swift::Swift; + +/// Comprehensive example showcasing basic Rust types and their Swift equivalents +/// +/// This example demonstrates how specta-swift handles fundamental Rust types +/// and converts them to appropriate Swift types. + +/// Basic primitive types +#[derive(Type)] +struct Primitives { + // Integer types + small_int: i8, + unsigned_small: u8, + short_int: i16, + unsigned_short: u16, + regular_int: i32, + unsigned_int: u32, + long_int: i64, + unsigned_long: u64, + + // Float types + single_precision: f32, + double_precision: f64, + + // Boolean and character + is_active: bool, + single_char: char, + + // String types + name: String, + optional_name: Option, + + // Collections + tags: Vec, + scores: Vec, + user_ids: Vec, + + // Nested collections + matrix: Vec>, + string_pairs: Vec<(String, String)>, +} + +/// Enum with different variant types +#[derive(Type)] +enum Status { + /// Simple unit variant + Active, + /// Tuple variant with single value + Pending(String), + /// Tuple variant with multiple values + Error(String, u32), + /// Named field variant + Loading { + progress: f32, + message: Option, + }, +} + +/// Generic struct demonstrating type parameters +#[derive(Type)] +struct ApiResponse { + data: Option, + error: Option, + status_code: u16, + headers: Vec<(String, String)>, +} + +/// Nested struct demonstrating complex relationships +#[derive(Type)] +struct User { + id: u32, + username: String, + email: String, + profile: UserProfile, + preferences: UserPreferences, + status: Status, + metadata: Option, +} + +#[derive(Type)] +struct UserProfile { + first_name: String, + last_name: String, + bio: Option, + avatar_url: Option, + birth_date: Option, +} + +#[derive(Type)] +struct UserPreferences { + theme: String, + language: String, + notifications_enabled: bool, + privacy_level: u8, +} + +#[derive(Type)] +struct UserMetadata { + created_at: String, + last_login: Option, + login_count: u32, + is_verified: bool, +} + +fn main() { + println!("๐Ÿš€ Basic Types Example - Generating Swift from Rust types"); + println!("{}", "=".repeat(60)); + + // Create type collection with all our types + let types = TypeCollection::default() + .register::() + .register::() + .register::>() + .register::() + .register::() + .register::() + .register::(); + + // Export with default settings + let swift = Swift::default(); + let output = swift.export(&types).unwrap(); + + println!("๐Ÿ“ Generated Swift code:\n"); + println!("{}", output); + + // Write to file for inspection + swift + .export_to("./examples/generated/BasicTypes.swift", &types) + .unwrap(); + println!("โœ… Basic types exported to BasicTypes.swift"); + + println!("\n๐Ÿ” Key Features Demonstrated:"); + println!("โ€ข Primitive type mappings (i32 โ†’ Int32, f64 โ†’ Double, etc.)"); + println!("โ€ข Optional types (Option โ†’ String?)"); + println!("โ€ข Collections (Vec โ†’ [T])"); + println!("โ€ข Nested collections (Vec> โ†’ [[Double]])"); + println!("โ€ข Enum variants (unit, tuple, named fields)"); + println!("โ€ข Generic types with type parameters"); + println!("โ€ข Complex nested struct relationships"); + println!("โ€ข Tuple types ((String, String) โ†’ (String, String))"); +} diff --git a/specta-swift/examples/comments_example.rs b/specta-swift/examples/comments_example.rs new file mode 100644 index 0000000..e5dda69 --- /dev/null +++ b/specta-swift/examples/comments_example.rs @@ -0,0 +1,121 @@ +use specta::{Type, TypeCollection}; +use specta_swift::Swift; + +/// A comprehensive example demonstrating multi-line comment support +/// +/// This example shows how Specta Swift handles complex documentation +/// including: +/// - Multi-line type documentation +/// - Bullet points and formatting +/// - Complex technical descriptions +/// +/// The generated Swift code will have properly formatted doc comments +/// that are compatible with Swift's documentation system. +#[derive(Type)] +enum ApiResponse { + /// Successful response containing the requested data + /// + /// This variant is returned when the API call completes successfully + /// and contains the expected data type. The status code indicates + /// the HTTP response status (200, 201, etc.). + Success { + /// The actual data returned by the API + data: T, + /// HTTP status code (200, 201, 204, etc.) + status: u16, + /// Optional response headers + headers: Option>, + }, + + /// Error response indicating a failure + /// + /// This variant is returned when the API call fails for any reason. + /// The error contains detailed information about what went wrong + /// and how to potentially resolve the issue. + Error { + /// Human-readable error message + message: String, + /// Machine-readable error code + code: u32, + /// Optional additional error details + details: Option, + }, + + /// Loading state for asynchronous operations + /// + /// This variant is used for long-running operations where the client + /// needs to show progress to the user. The progress value ranges + /// from 0.0 (not started) to 1.0 (completed). + Loading { + /// Progress value between 0.0 and 1.0 + progress: f32, + /// Optional estimated time remaining in seconds + estimated_time: Option, + }, +} + +/// A user account in the system +/// +/// This struct represents a complete user account with all necessary +/// information for authentication, authorization, and personalization. +/// +/// # Security Notes +/// - The `password_hash` field should never be logged or exposed +/// - The `api_key` is sensitive and should be treated as a secret +/// - All timestamps are in UTC +#[derive(Type)] +struct User { + /// Unique identifier for the user + /// + /// This ID is generated when the user first registers and never + /// changes throughout the user's lifetime in the system. + id: u32, + + /// The user's chosen username + /// + /// Must be unique across the entire system. Usernames are + /// case-insensitive and can contain letters, numbers, and underscores. + username: String, + + /// The user's email address + /// + /// Used for authentication, password resets, and notifications. + /// Must be a valid email format and unique across the system. + email: String, + + /// Whether the user account is currently active + /// + /// Inactive users cannot log in or perform any actions. + /// This is typically set to false when an account is suspended + /// or when the user requests account deletion. + is_active: bool, + + /// When the user account was created + /// + /// Timestamp in UTC when the user first registered. + created_at: String, + + /// When the user last logged in + /// + /// Updated on every successful login. Can be None if the user + /// has never logged in (e.g., account created but not activated). + last_login: Option, +} + +fn main() { + let types = TypeCollection::default() + .register::>() + .register::(); + + let swift = Swift::default(); + let output = swift.export(&types).unwrap(); + + println!( + "Generated Swift code with comprehensive comments:\n{}", + output + ); + + // Also write to file for inspection + swift.export_to("./examples/generated/CommentsExample.swift", &types).unwrap(); + println!("\nComments example written to CommentsExample.swift"); +} diff --git a/specta-swift/examples/comprehensive_demo.rs b/specta-swift/examples/comprehensive_demo.rs new file mode 100644 index 0000000..38c95b1 --- /dev/null +++ b/specta-swift/examples/comprehensive_demo.rs @@ -0,0 +1,439 @@ +use specta::{Type, TypeCollection}; +use specta_swift::Swift; +use std::time::Duration; + +/// Comprehensive demonstration of ALL specta-swift functionality +/// +/// This example showcases every feature and capability of specta-swift in a single, +/// realistic application scenario. It demonstrates complex type relationships, +/// various enum patterns, special types, and advanced features. + +/// Main application types for a task management system +#[derive(Type)] +struct Task { + id: u32, + title: String, + description: Option, + status: TaskStatus, + priority: Priority, + assignee: Option, + created_at: String, + updated_at: String, + due_date: Option, + duration: Option, + tags: Vec, + metadata: TaskMetadata, + subtasks: Vec, +} + +/// Task status enum with mixed variants +#[derive(Type)] +enum TaskStatus { + /// Task is waiting to be started + Todo, + /// Task is currently in progress + InProgress { + started_at: String, + estimated_completion: Option, + progress: f32, + }, + /// Task is blocked by dependencies + Blocked { + reason: String, + blocked_by: Vec, + estimated_unblock: Option, + }, + /// Task is under review + Review { + reviewer: String, + review_started_at: String, + comments: Vec, + }, + /// Task is completed + Completed { + completed_at: String, + completion_time: Duration, + final_notes: Option, + }, + /// Task was cancelled + Cancelled { + reason: String, + cancelled_at: String, + }, +} + +/// Priority enum (string enum) +#[derive(Type)] +enum Priority { + Low, + Medium, + High, + Critical, + Emergency, +} + +/// User information +#[derive(Type)] +struct User { + id: u32, + username: String, + email: String, + profile: UserProfile, + preferences: UserPreferences, + role: UserRole, + is_active: bool, + last_login: Option, + created_at: String, +} + +/// User profile with nested data +#[derive(Type)] +struct UserProfile { + first_name: String, + last_name: String, + bio: Option, + avatar_url: Option, + timezone: String, + language: String, +} + +/// User preferences +#[derive(Type)] +struct UserPreferences { + theme: Theme, + notifications: NotificationSettings, + privacy: PrivacySettings, + display: DisplaySettings, +} + +/// Theme enum (string enum) +#[derive(Type)] +enum Theme { + Light, + Dark, + Auto, + Custom, +} + +/// Notification settings +#[derive(Type)] +struct NotificationSettings { + email_enabled: bool, + push_enabled: bool, + sms_enabled: bool, + desktop_enabled: bool, + frequency: NotificationFrequency, +} + +/// Notification frequency enum +#[derive(Type)] +enum NotificationFrequency { + Immediate, + Hourly, + Daily, + Weekly, + Never, +} + +/// Privacy settings +#[derive(Type)] +struct PrivacySettings { + profile_visibility: Visibility, + activity_visibility: Visibility, + data_sharing: DataSharing, +} + +/// Visibility enum +#[derive(Type)] +enum Visibility { + Public, + Friends, + Private, + Hidden, +} + +/// Data sharing settings +#[derive(Type)] +struct DataSharing { + analytics: bool, + marketing: bool, + third_party: bool, + research: bool, +} + +/// Display settings +#[derive(Type)] +struct DisplaySettings { + items_per_page: u32, + date_format: String, + time_format: String, + currency: String, + compact_mode: bool, +} + +/// User role with permissions +#[derive(Type)] +enum UserRole { + /// Regular user with basic permissions + User, + /// Moderator with additional permissions + Moderator { + permissions: Vec, + department: String, + }, + /// Administrator with full permissions + Admin { + level: AdminLevel, + departments: Vec, + special_access: Vec, + }, + /// Super admin with system-wide access + SuperAdmin { + system_access: bool, + audit_logs: bool, + }, +} + +/// Admin level enum +#[derive(Type)] +enum AdminLevel { + Junior, + Senior, + Lead, + Director, +} + +/// Task metadata +#[derive(Type)] +struct TaskMetadata { + created_by: u32, + last_modified_by: u32, + version: u32, + custom_fields: Vec<(String, String)>, + attachments: Vec, + watchers: Vec, + dependencies: Vec, +} + +/// File attachment +#[derive(Type)] +struct Attachment { + id: String, + filename: String, + size: u64, + mime_type: String, + uploaded_at: String, + uploaded_by: u32, +} + +/// Subtask with timing information +#[derive(Type)] +struct SubTask { + id: u32, + title: String, + description: Option, + status: SubTaskStatus, + estimated_duration: Duration, + actual_duration: Option, + assignee: Option, + created_at: String, + completed_at: Option, +} + +/// Subtask status (simple enum) +#[derive(Type)] +enum SubTaskStatus { + Pending, + InProgress, + Completed, + Skipped, +} + +/// Review comment +#[derive(Type)] +struct ReviewComment { + id: u32, + author: u32, + content: String, + created_at: String, + updated_at: String, + is_resolved: bool, + parent_comment: Option, + attachments: Vec, +} + +/// API response wrapper +#[derive(Type)] +struct ApiResponse { + data: Option, + error: Option, + status: ResponseStatus, + metadata: ResponseMetadata, +} + +/// Response status enum +#[derive(Type)] +enum ResponseStatus { + Success, + PartialSuccess, + Error, + ValidationError, + AuthenticationError, + AuthorizationError, + NotFound, + RateLimited, +} + +/// Response metadata +#[derive(Type)] +struct ResponseMetadata { + request_id: String, + timestamp: String, + processing_time: Duration, + version: String, + pagination: Option, +} + +/// Pagination information +#[derive(Type)] +struct PaginationInfo { + page: u32, + per_page: u32, + total_pages: u32, + total_items: u64, + has_next: bool, + has_prev: bool, +} + +/// System health information +#[derive(Type)] +struct SystemHealth { + status: HealthStatus, + uptime: Duration, + last_check: String, + services: Vec, + metrics: SystemMetrics, +} + +/// Health status enum +#[derive(Type)] +enum HealthStatus { + Healthy, + Degraded, + Unhealthy, + Unknown, +} + +/// Service status +#[derive(Type)] +struct ServiceStatus { + name: String, + status: HealthStatus, + response_time: Duration, + last_check: String, + error_count: u32, +} + +/// System metrics +#[derive(Type)] +struct SystemMetrics { + cpu_usage: f64, + memory_usage: f64, + disk_usage: f64, + network_usage: f64, + active_users: u32, + total_requests: u64, + error_rate: f64, +} + +fn main() { + println!("๐Ÿš€ Comprehensive Demo - Complete specta-swift functionality showcase"); + println!("{}", "=".repeat(80)); + + // Create comprehensive type collection + let types = TypeCollection::default() + // Core types + .register::() + .register::() + .register::() + .register::() + .register::() + .register::() + .register::() + .register::() + .register::() + .register::() + .register::() + .register::() + .register::() + .register::() + .register::() + .register::() + .register::() + .register::() + .register::() + .register::() + // API types + .register::>() + .register::() + .register::() + .register::() + // System types + .register::() + .register::() + .register::() + .register::(); + + // Export with default settings + let swift = Swift::default(); + let output = swift.export(&types).unwrap(); + + println!("๐Ÿ“ Generated Swift code (first 2000 characters):\n"); + let preview = if output.len() > 2000 { + format!( + "{}...\n\n[Output truncated - see ComprehensiveDemo.swift for full output]", + &output[..2000] + ) + } else { + output.clone() + }; + println!("{}", preview); + + // Write to file for inspection + swift + .export_to("./examples/generated/ComprehensiveDemo.swift", &types) + .unwrap(); + println!("โœ… Comprehensive demo exported to ComprehensiveDemo.swift"); + + println!("\n๐Ÿ” Complete Feature Showcase:"); + println!("โ€ข โœ… Basic primitive types (i32, f64, bool, String, etc.)"); + println!("โ€ข โœ… Optional types (Option โ†’ T?)"); + println!("โ€ข โœ… Collections (Vec โ†’ [T])"); + println!("โ€ข โœ… Nested collections (Vec> โ†’ [[T]])"); + println!("โ€ข โœ… Tuple types ((String, String) โ†’ (String, String))"); + println!("โ€ข โœ… Complex struct relationships"); + println!("โ€ข โœ… Generic types with type parameters"); + println!("โ€ข โœ… String enums with automatic Codable"); + println!("โ€ข โœ… Mixed enums with custom Codable implementations"); + println!("โ€ข โœ… Named field variants with struct generation"); + println!("โ€ข โœ… Duration types with RustDuration helper"); + println!("โ€ข โœ… Nested enum variants"); + println!("โ€ข โœ… Recursive type references"); + println!("โ€ข โœ… Complex metadata structures"); + println!("โ€ข โœ… API response patterns"); + println!("โ€ข โœ… System monitoring types"); + println!("โ€ข โœ… User management with permissions"); + println!("โ€ข โœ… Task management with status tracking"); + println!("โ€ข โœ… File attachment handling"); + println!("โ€ข โœ… Comment and review systems"); + println!("โ€ข โœ… Pagination and metadata"); + println!("โ€ข โœ… Health monitoring and metrics"); + + println!("\n๐Ÿ“Š Statistics:"); + println!("โ€ข Total types registered: {}", types.len()); + println!("โ€ข Generated Swift code length: {} characters", output.len()); + println!("โ€ข Lines of generated code: {}", output.lines().count()); + + println!("\n๐ŸŽ‰ This example demonstrates EVERY feature of specta-swift!"); + println!("Check ComprehensiveDemo.swift for the complete generated Swift code."); +} diff --git a/specta-swift/examples/configuration_options.rs b/specta-swift/examples/configuration_options.rs new file mode 100644 index 0000000..5c68b88 --- /dev/null +++ b/specta-swift/examples/configuration_options.rs @@ -0,0 +1,220 @@ +use specta::{Type, TypeCollection}; +use specta_swift::{GenericStyle, IndentStyle, NamingConvention, OptionalStyle, Swift}; + +/// Comprehensive example showcasing ALL configuration options for specta-swift +/// +/// This example demonstrates every configuration option available in the Swift exporter, +/// showing how different settings affect the generated Swift code. + +/// Sample types for demonstration +#[derive(Type)] +struct User { + user_id: u32, + full_name: String, + email_address: Option, + is_verified: bool, +} + +#[derive(Type)] +enum ApiResponse { + Success { data: T, status_code: u16 }, + Error { message: String, error_code: u32 }, + Loading { progress: f32 }, +} + +#[derive(Type)] +struct GenericContainer { + primary: T, + secondary: U, + metadata: Option>, +} + +fn main() { + println!("๐Ÿš€ Configuration Options Example - All Swift exporter settings"); + println!("{}", "=".repeat(70)); + + // Sample types for all examples + let types = TypeCollection::default() + .register::() + .register::>() + .register::>(); + + // 1. DEFAULT CONFIGURATION + println!("\n๐Ÿ“‹ 1. DEFAULT CONFIGURATION"); + println!("{}", "-".repeat(40)); + let default_swift = Swift::default(); + let default_output = default_swift.export(&types).unwrap(); + println!("{}", default_output); + default_swift + .export_to("./examples/generated/DefaultConfig.swift", &types) + .unwrap(); + println!("โœ… Default configuration exported to DefaultConfig.swift"); + + // 2. CUSTOM HEADER + println!("\n๐Ÿ“‹ 2. CUSTOM HEADER"); + println!("{}", "-".repeat(40)); + let custom_header = Swift::new() + .header("// Generated by MyAwesomeApp v2.0\n// Custom header with app info\n// DO NOT EDIT MANUALLY"); + let header_output = custom_header.export(&types).unwrap(); + println!("{}", header_output); + custom_header + .export_to("./examples/generated/CustomHeader.swift", &types) + .unwrap(); + println!("โœ… Custom header exported to CustomHeader.swift"); + + // 3. DIFFERENT NAMING CONVENTIONS + println!("\n๐Ÿ“‹ 3. NAMING CONVENTIONS"); + println!("{}", "-".repeat(40)); + + // PascalCase (default) + let pascal_case = Swift::new().naming(NamingConvention::PascalCase); + let pascal_output = pascal_case.export(&types).unwrap(); + println!("PascalCase output:\n{}", pascal_output); + pascal_case + .export_to("./examples/generated/PascalCase.swift", &types) + .unwrap(); + + // camelCase + let camel_case = Swift::new().naming(NamingConvention::CamelCase); + let camel_output = camel_case.export(&types).unwrap(); + println!("camelCase output:\n{}", camel_output); + camel_case + .export_to("./examples/generated/CamelCase.swift", &types) + .unwrap(); + + // snake_case + let snake_case = Swift::new().naming(NamingConvention::SnakeCase); + let snake_output = snake_case.export(&types).unwrap(); + println!("snake_case output:\n{}", snake_output); + snake_case + .export_to("./examples/generated/SnakeCase.swift", &types) + .unwrap(); + println!("โœ… Naming conventions exported to separate files"); + + // 4. INDENTATION STYLES + println!("\n๐Ÿ“‹ 4. INDENTATION STYLES"); + println!("{}", "-".repeat(40)); + + // Spaces (default: 4 spaces) + let spaces_4 = Swift::new().indent(IndentStyle::Spaces(4)); + let spaces_output = spaces_4.export(&types).unwrap(); + println!("4 Spaces indentation:\n{}", spaces_output); + spaces_4 + .export_to("./examples/generated/Spaces4.swift", &types) + .unwrap(); + + // 2 Spaces + let spaces_2 = Swift::new().indent(IndentStyle::Spaces(2)); + let spaces2_output = spaces_2.export(&types).unwrap(); + println!("2 Spaces indentation:\n{}", spaces2_output); + spaces_2 + .export_to("./examples/generated/Spaces2.swift", &types) + .unwrap(); + + // Tabs + let tabs = Swift::new().indent(IndentStyle::Tabs); + let tabs_output = tabs.export(&types).unwrap(); + println!("Tabs indentation:\n{}", tabs_output); + tabs.export_to("./examples/generated/Tabs.swift", &types) + .unwrap(); + println!("โœ… Indentation styles exported to separate files"); + + // 5. GENERIC STYLES + println!("\n๐Ÿ“‹ 5. GENERIC STYLES"); + println!("{}", "-".repeat(40)); + + // Protocol constraints (default) + let protocol_generics = Swift::new().generics(GenericStyle::Protocol); + let protocol_output = protocol_generics.export(&types).unwrap(); + println!("Protocol constraints:\n{}", protocol_output); + protocol_generics + .export_to("./examples/generated/ProtocolGenerics.swift", &types) + .unwrap(); + + // Typealias constraints + let typealias_generics = Swift::new().generics(GenericStyle::Typealias); + let typealias_output = typealias_generics.export(&types).unwrap(); + println!("Typealias constraints:\n{}", typealias_output); + typealias_generics + .export_to("./examples/generated/TypealiasGenerics.swift", &types) + .unwrap(); + println!("โœ… Generic styles exported to separate files"); + + // 6. OPTIONAL STYLES + println!("\n๐Ÿ“‹ 6. OPTIONAL STYLES"); + println!("{}", "-".repeat(40)); + + // Question mark syntax (default) + let question_mark = Swift::new().optionals(OptionalStyle::QuestionMark); + let question_output = question_mark.export(&types).unwrap(); + println!("Question mark syntax:\n{}", question_output); + question_mark + .export_to("./examples/generated/QuestionMark.swift", &types) + .unwrap(); + + // Optional type syntax + let optional_type = Swift::new().optionals(OptionalStyle::Optional); + let optional_output = optional_type.export(&types).unwrap(); + println!("Optional type syntax:\n{}", optional_output); + optional_type + .export_to("./examples/generated/OptionalType.swift", &types) + .unwrap(); + println!("โœ… Optional styles exported to separate files"); + + // 7. ADDITIONAL PROTOCOLS + println!("\n๐Ÿ“‹ 7. ADDITIONAL PROTOCOLS"); + println!("{}", "-".repeat(40)); + let with_protocols = Swift::new() + .add_protocol("Equatable") + .add_protocol("Hashable") + .add_protocol("CustomStringConvertible"); + let protocols_output = with_protocols.export(&types).unwrap(); + println!("With additional protocols:\n{}", protocols_output); + with_protocols + .export_to("./examples/generated/WithProtocols.swift", &types) + .unwrap(); + println!("โœ… Additional protocols exported to WithProtocols.swift"); + + // 8. SERDE VALIDATION + println!("\n๐Ÿ“‹ 8. SERDE VALIDATION"); + println!("{}", "-".repeat(40)); + let with_serde = Swift::new().with_serde(); + let serde_output = with_serde.export(&types).unwrap(); + println!("With Serde validation:\n{}", serde_output); + with_serde + .export_to("./examples/generated/WithSerde.swift", &types) + .unwrap(); + println!("โœ… Serde validation exported to WithSerde.swift"); + + // 9. COMBINED CUSTOM CONFIGURATION + println!("\n๐Ÿ“‹ 9. COMBINED CUSTOM CONFIGURATION"); + println!("{}", "-".repeat(40)); + let combined = Swift::new() + .header("// My Custom App Types\n// Generated with love โค๏ธ") + .naming(NamingConvention::CamelCase) + .indent(IndentStyle::Spaces(2)) + .generics(GenericStyle::Typealias) + .optionals(OptionalStyle::Optional) + .add_protocol("Equatable") + .add_protocol("Hashable") + .with_serde(); + + let combined_output = combined.export(&types).unwrap(); + println!("Combined custom configuration:\n{}", combined_output); + combined + .export_to("./examples/generated/CombinedConfig.swift", &types) + .unwrap(); + println!("โœ… Combined configuration exported to CombinedConfig.swift"); + + println!("\n๐ŸŽ‰ All configuration examples completed!"); + println!("\n๐Ÿ“ Generated files:"); + println!("โ€ข DefaultConfig.swift - Default settings"); + println!("โ€ข CustomHeader.swift - Custom header"); + println!("โ€ข PascalCase.swift, CamelCase.swift, SnakeCase.swift - Naming conventions"); + println!("โ€ข Spaces4.swift, Spaces2.swift, Tabs.swift - Indentation styles"); + println!("โ€ข ProtocolGenerics.swift, TypealiasGenerics.swift - Generic styles"); + println!("โ€ข QuestionMark.swift, OptionalType.swift - Optional styles"); + println!("โ€ข WithProtocols.swift - Additional protocols"); + println!("โ€ข WithSerde.swift - Serde validation"); + println!("โ€ข CombinedConfig.swift - Combined custom settings"); +} diff --git a/specta-swift/examples/generated/AdvancedUnions.swift b/specta-swift/examples/generated/AdvancedUnions.swift new file mode 100644 index 0000000..20bb18e --- /dev/null +++ b/specta-swift/examples/generated/AdvancedUnions.swift @@ -0,0 +1,436 @@ +// This file has been generated by Specta. DO NOT EDIT. +import Foundation + +/// Advanced example showcasing complex enum unions and their Swift representations +/// +/// This example demonstrates how specta-swift handles complex enum scenarios +/// including nested types, generic enums, and custom Codable implementations. +/// Complex enum with mixed variant types +public enum ApiResult { + case ok(ApiResultOkData) + case err(ApiResultErrData) + case loading(ApiResultLoadingData) +} +public struct ApiResultOkData: Codable { + public let data: T + public let status: UInt16 + public let headers: [(String, String)]? + public let timestamp: String +} + +public struct ApiResultErrData: Codable { + public let error: E + public let code: UInt32 + public let message: String + public let retryAfter: UInt64? + + private enum CodingKeys: String, CodingKey { + case error = "error" + case code = "code" + case message = "message" + case retryAfter = "retry_after" + } +} + +public struct ApiResultLoadingData: Codable { + public let progress: Float + public let estimatedCompletion: String? + + private enum CodingKeys: String, CodingKey { + case progress = "progress" + case estimatedCompletion = "estimated_completion" + } +} + +// MARK: - ApiResult Codable Implementation +extension ApiResult: Codable { + private enum CodingKeys: String, CodingKey { + case ok = "Ok" + case err = "Err" + case loading = "Loading" + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + if container.allKeys.count != 1 { + throw DecodingError.dataCorrupted( + DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Invalid number of keys found, expected one.") + ) + } + + let key = container.allKeys.first! + switch key { + case .ok: + let data = try container.decode(ApiResultOkData.self, forKey: .ok) + self = .ok(data) + case .err: + let data = try container.decode(ApiResultErrData.self, forKey: .err) + self = .err(data) + case .loading: + let data = try container.decode(ApiResultLoadingData.self, forKey: .loading) + self = .loading(data) + } + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + + switch self { + case .ok(let data): + try container.encode(data, forKey: .ok) + case .err(let data): + try container.encode(data, forKey: .err) + case .loading(let data): + try container.encode(data, forKey: .loading) + } + } +} + + +public struct Color: Codable { + public let red: Double + public let green: Double + public let blue: Double + public let alpha: Double +} + +/// Generic enum with constraints +public enum Container: Codable { + case empty + case single(T) + case multiple([T]) + case keyValue([(String, T)]) +} + +/// Event system enum +public enum Event { + case user(EventUserData) + case system(EventSystemData) + case error(EventErrorData) +} +public struct EventUserData: Codable { + public let userId: UInt32 + public let action: String + public let timestamp: String + + private enum CodingKeys: String, CodingKey { + case userId = "user_id" + case action = "action" + case timestamp = "timestamp" + } +} + +public struct EventSystemData: Codable { + public let component: String + public let level: String + public let message: String +} + +public struct EventErrorData: Codable { + public let code: UInt32 + public let message: String + public let stackTrace: String? + + private enum CodingKeys: String, CodingKey { + case code = "code" + case message = "message" + case stackTrace = "stack_trace" + } +} + +// MARK: - Event Codable Implementation +extension Event: Codable { + private enum CodingKeys: String, CodingKey { + case user = "User" + case system = "System" + case error = "Error" + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + if container.allKeys.count != 1 { + throw DecodingError.dataCorrupted( + DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Invalid number of keys found, expected one.") + ) + } + + let key = container.allKeys.first! + switch key { + case .user: + let data = try container.decode(EventUserData.self, forKey: .user) + self = .user(data) + case .system: + let data = try container.decode(EventSystemData.self, forKey: .system) + self = .system(data) + case .error: + let data = try container.decode(EventErrorData.self, forKey: .error) + self = .error(data) + } + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + + switch self { + case .user(let data): + try container.encode(data, forKey: .user) + case .system(let data): + try container.encode(data, forKey: .system) + case .error(let data): + try container.encode(data, forKey: .error) + } + } +} + + +/// String enum (will be converted to Swift String enum with Codable) +public enum JobStatus: Codable { + case queued + case running + case paused + case completed + case failed + case cancelled +} + +public struct LineStyle: Codable { + public let dashPattern: [Double]? + public let capStyle: String + public let joinStyle: String + + private enum CodingKeys: String, CodingKey { + case dashPattern = "dash_pattern" + case capStyle = "cap_style" + case joinStyle = "join_style" + } +} + +/// Mixed enum with both string-like and data variants +public enum MixedEnum { + case simple(String) + case withFields(MixedEnumWithFieldsData) + case empty +} +public struct MixedEnumWithFieldsData: Codable { + public let id: UInt32 + public let name: String + public let metadata: [(String, String)]? +} + +// MARK: - MixedEnum Codable Implementation +extension MixedEnum: Codable { + private enum CodingKeys: String, CodingKey { + case simple = "Simple" + case withFields = "WithFields" + case empty = "Empty" + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + if container.allKeys.count != 1 { + throw DecodingError.dataCorrupted( + DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Invalid number of keys found, expected one.") + ) + } + + let key = container.allKeys.first! + switch key { + case .simple: + // TODO: Implement tuple variant decoding for simple + fatalError("Tuple variant decoding not implemented") + case .withFields: + let data = try container.decode(MixedEnumWithFieldsData.self, forKey: .withFields) + self = .withFields(data) + case .empty: + self = .empty + } + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + + switch self { + case .simple: + // TODO: Implement tuple variant encoding for simple + fatalError("Tuple variant encoding not implemented") + case .withFields(let data): + try container.encode(data, forKey: .withFields) + case .empty: + try container.encodeNil(forKey: .empty) + } + } +} + + +/// Supporting structs for the Shape enum +public struct Point: Codable { + public let x: Double + public let y: Double +} + +public struct Rectangle: Codable { + public let topLeft: Point + public let bottomRight: Point + + private enum CodingKeys: String, CodingKey { + case topLeft = "top_left" + case bottomRight = "bottom_right" + } +} + +/// Enum demonstrating different field patterns +public enum Shape { + case none + case point(Double, Double) + case circle(ShapeCircleData) + case rectangle(Rectangle) + case line(ShapeLineData) + case complex(ShapeComplexData) +} +public struct ShapeCircleData: Codable { + public let center: Point + public let radius: Double +} + +public struct ShapeLineData: Codable { + public let start: Point + public let end: Point + public let style: LineStyle +} + +public struct ShapeComplexData: Codable { + public let vertices: [Point] + public let fillColor: Color + public let strokeColor: Color? + public let strokeWidth: Double + public let isClosed: Bool + + private enum CodingKeys: String, CodingKey { + case vertices = "vertices" + case fillColor = "fill_color" + case strokeColor = "stroke_color" + case strokeWidth = "stroke_width" + case isClosed = "is_closed" + } +} + +// MARK: - Shape Codable Implementation +extension Shape: Codable { + private enum CodingKeys: String, CodingKey { + case none = "None" + case point = "Point" + case circle = "Circle" + case rectangle = "Rectangle" + case line = "Line" + case complex = "Complex" + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + if container.allKeys.count != 1 { + throw DecodingError.dataCorrupted( + DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Invalid number of keys found, expected one.") + ) + } + + let key = container.allKeys.first! + switch key { + case .none: + self = .none + case .point: + // TODO: Implement tuple variant decoding for point + fatalError("Tuple variant decoding not implemented") + case .circle: + let data = try container.decode(ShapeCircleData.self, forKey: .circle) + self = .circle(data) + case .rectangle: + // TODO: Implement tuple variant decoding for rectangle + fatalError("Tuple variant decoding not implemented") + case .line: + let data = try container.decode(ShapeLineData.self, forKey: .line) + self = .line(data) + case .complex: + let data = try container.decode(ShapeComplexData.self, forKey: .complex) + self = .complex(data) + } + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + + switch self { + case .none: + try container.encodeNil(forKey: .none) + case .point: + // TODO: Implement tuple variant encoding for point + fatalError("Tuple variant encoding not implemented") + case .circle(let data): + try container.encode(data, forKey: .circle) + case .rectangle: + // TODO: Implement tuple variant encoding for rectangle + fatalError("Tuple variant encoding not implemented") + case .line(let data): + try container.encode(data, forKey: .line) + case .complex(let data): + try container.encode(data, forKey: .complex) + } + } +} + + +/// Enum with recursive references +public enum Tree { + case leaf(T) + case branch(TreeBranchData) +} +public struct TreeBranchData: Codable { + public let left: Tree + public let right: Tree + public let value: T +} + +// MARK: - Tree Codable Implementation +extension Tree: Codable { + private enum CodingKeys: String, CodingKey { + case leaf = "Leaf" + case branch = "Branch" + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + if container.allKeys.count != 1 { + throw DecodingError.dataCorrupted( + DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Invalid number of keys found, expected one.") + ) + } + + let key = container.allKeys.first! + switch key { + case .leaf: + // TODO: Implement tuple variant decoding for leaf + fatalError("Tuple variant decoding not implemented") + case .branch: + let data = try container.decode(TreeBranchData.self, forKey: .branch) + self = .branch(data) + } + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + + switch self { + case .leaf: + // TODO: Implement tuple variant encoding for leaf + fatalError("Tuple variant encoding not implemented") + case .branch(let data): + try container.encode(data, forKey: .branch) + } + } +} + + diff --git a/specta-swift/examples/generated/BasicTypes.swift b/specta-swift/examples/generated/BasicTypes.swift new file mode 100644 index 0000000..cbc1710 --- /dev/null +++ b/specta-swift/examples/generated/BasicTypes.swift @@ -0,0 +1,187 @@ +// This file has been generated by Specta. DO NOT EDIT. +import Foundation + +/// Generic struct demonstrating type parameters +public struct ApiResponse: Codable { + public let data: T? + public let error: E? + public let statusCode: UInt16 + public let headers: [(String, String)] + + private enum CodingKeys: String, CodingKey { + case data = "data" + case error = "error" + case statusCode = "status_code" + case headers = "headers" + } +} + +/// Comprehensive example showcasing basic Rust types and their Swift equivalents +/// +/// This example demonstrates how specta-swift handles fundamental Rust types +/// and converts them to appropriate Swift types. +/// Basic primitive types +public struct Primitives: Codable { + public let smallInt: Int8 + public let unsignedSmall: UInt8 + public let shortInt: Int16 + public let unsignedShort: UInt16 + public let regularInt: Int32 + public let unsignedInt: UInt32 + public let longInt: Int64 + public let unsignedLong: UInt64 + public let singlePrecision: Float + public let doublePrecision: Double + public let isActive: Bool + public let singleChar: Character + public let name: String + public let optionalName: String? + public let tags: [String] + public let scores: [Double] + public let userIds: [UInt32] + public let matrix: [[Double]] + public let stringPairs: [(String, String)] + + private enum CodingKeys: String, CodingKey { + case smallInt = "small_int" + case unsignedSmall = "unsigned_small" + case shortInt = "short_int" + case unsignedShort = "unsigned_short" + case regularInt = "regular_int" + case unsignedInt = "unsigned_int" + case longInt = "long_int" + case unsignedLong = "unsigned_long" + case singlePrecision = "single_precision" + case doublePrecision = "double_precision" + case isActive = "is_active" + case singleChar = "single_char" + case name = "name" + case optionalName = "optional_name" + case tags = "tags" + case scores = "scores" + case userIds = "user_ids" + case matrix = "matrix" + case stringPairs = "string_pairs" + } +} + +/// Enum with different variant types +public enum Status { + case active + case pending(String) + case error(String, UInt32) + case loading(StatusLoadingData) +} +public struct StatusLoadingData: Codable { + public let progress: Float + public let message: String? +} + +// MARK: - Status Codable Implementation +extension Status: Codable { + private enum CodingKeys: String, CodingKey { + case active = "Active" + case pending = "Pending" + case error = "Error" + case loading = "Loading" + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + if container.allKeys.count != 1 { + throw DecodingError.dataCorrupted( + DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Invalid number of keys found, expected one.") + ) + } + + let key = container.allKeys.first! + switch key { + case .active: + self = .active + case .pending: + // TODO: Implement tuple variant decoding for pending + fatalError("Tuple variant decoding not implemented") + case .error: + // TODO: Implement tuple variant decoding for error + fatalError("Tuple variant decoding not implemented") + case .loading: + let data = try container.decode(StatusLoadingData.self, forKey: .loading) + self = .loading(data) + } + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + + switch self { + case .active: + try container.encodeNil(forKey: .active) + case .pending: + // TODO: Implement tuple variant encoding for pending + fatalError("Tuple variant encoding not implemented") + case .error: + // TODO: Implement tuple variant encoding for error + fatalError("Tuple variant encoding not implemented") + case .loading(let data): + try container.encode(data, forKey: .loading) + } + } +} + + +/// Nested struct demonstrating complex relationships +public struct User: Codable { + public let id: UInt32 + public let username: String + public let email: String + public let profile: UserProfile + public let preferences: UserPreferences + public let status: Status + public let metadata: UserMetadata? +} + +public struct UserMetadata: Codable { + public let createdAt: String + public let lastLogin: String? + public let loginCount: UInt32 + public let isVerified: Bool + + private enum CodingKeys: String, CodingKey { + case createdAt = "created_at" + case lastLogin = "last_login" + case loginCount = "login_count" + case isVerified = "is_verified" + } +} + +public struct UserPreferences: Codable { + public let theme: String + public let language: String + public let notificationsEnabled: Bool + public let privacyLevel: UInt8 + + private enum CodingKeys: String, CodingKey { + case theme = "theme" + case language = "language" + case notificationsEnabled = "notifications_enabled" + case privacyLevel = "privacy_level" + } +} + +public struct UserProfile: Codable { + public let firstName: String + public let lastName: String + public let bio: String? + public let avatarUrl: String? + public let birthDate: String? + + private enum CodingKeys: String, CodingKey { + case firstName = "first_name" + case lastName = "last_name" + case bio = "bio" + case avatarUrl = "avatar_url" + case birthDate = "birth_date" + } +} + diff --git a/specta-swift/examples/generated/CamelCase.swift b/specta-swift/examples/generated/CamelCase.swift new file mode 100644 index 0000000..05542ff --- /dev/null +++ b/specta-swift/examples/generated/CamelCase.swift @@ -0,0 +1,103 @@ +// This file has been generated by Specta. DO NOT EDIT. +import Foundation + +public enum apiResponse { + case success(apiResponseSuccessData) + case error(apiResponseErrorData) + case loading(apiResponseLoadingData) +} +public struct apiResponseSuccessData: Codable { + public let data: T + public let statusCode: UInt16 + + private enum CodingKeys: String, CodingKey { + case data = "data" + case statusCode = "status_code" + } +} + +public struct apiResponseErrorData: Codable { + public let message: String + public let errorCode: UInt32 + + private enum CodingKeys: String, CodingKey { + case message = "message" + case errorCode = "error_code" + } +} + +public struct apiResponseLoadingData: Codable { + public let progress: Float +} + +// MARK: - apiResponse Codable Implementation +extension apiResponse: Codable { + private enum CodingKeys: String, CodingKey { + case success = "Success" + case error = "Error" + case loading = "Loading" + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + if container.allKeys.count != 1 { + throw DecodingError.dataCorrupted( + DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Invalid number of keys found, expected one.") + ) + } + + let key = container.allKeys.first! + switch key { + case .success: + let data = try container.decode(apiResponseSuccessData.self, forKey: .success) + self = .success(data) + case .error: + let data = try container.decode(apiResponseErrorData.self, forKey: .error) + self = .error(data) + case .loading: + let data = try container.decode(apiResponseLoadingData.self, forKey: .loading) + self = .loading(data) + } + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + + switch self { + case .success(let data): + try container.encode(data, forKey: .success) + case .error(let data): + try container.encode(data, forKey: .error) + case .loading(let data): + try container.encode(data, forKey: .loading) + } + } +} + + +public struct genericContainer: Codable { + public let primary: T + public let secondary: U + public let metadata: [(String, String)]? +} + +/// Comprehensive example showcasing ALL configuration options for specta-swift +/// +/// This example demonstrates every configuration option available in the Swift exporter, +/// showing how different settings affect the generated Swift code. +/// Sample types for demonstration +public struct user: Codable { + public let userId: UInt32 + public let fullName: String + public let emailAddress: String? + public let isVerified: Bool + + private enum CodingKeys: String, CodingKey { + case userId = "user_id" + case fullName = "full_name" + case emailAddress = "email_address" + case isVerified = "is_verified" + } +} + diff --git a/specta-swift/examples/generated/CombinedConfig.swift b/specta-swift/examples/generated/CombinedConfig.swift new file mode 100644 index 0000000..4841fc8 --- /dev/null +++ b/specta-swift/examples/generated/CombinedConfig.swift @@ -0,0 +1,107 @@ +// My Custom App Types +// Generated with love โค๏ธ +import Foundation +import Codable +import Equatable +import Hashable + +public enum apiResponse { + case success(apiResponseSuccessData) + case error(apiResponseErrorData) + case loading(apiResponseLoadingData) +} +public struct apiResponseSuccessData: Codable { + public let data: T + public let statusCode: UInt16 + + private enum CodingKeys: String, CodingKey { + case data = "data" + case statusCode = "status_code" + } +} + +public struct apiResponseErrorData: Codable { + public let message: String + public let errorCode: UInt32 + + private enum CodingKeys: String, CodingKey { + case message = "message" + case errorCode = "error_code" + } +} + +public struct apiResponseLoadingData: Codable { + public let progress: Float +} + +// MARK: - apiResponse Codable Implementation +extension apiResponse: Codable { + private enum CodingKeys: String, CodingKey { + case success = "Success" + case error = "Error" + case loading = "Loading" + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + if container.allKeys.count != 1 { + throw DecodingError.dataCorrupted( + DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Invalid number of keys found, expected one.") + ) + } + + let key = container.allKeys.first! + switch key { + case .success: + let data = try container.decode(apiResponseSuccessData.self, forKey: .success) + self = .success(data) + case .error: + let data = try container.decode(apiResponseErrorData.self, forKey: .error) + self = .error(data) + case .loading: + let data = try container.decode(apiResponseLoadingData.self, forKey: .loading) + self = .loading(data) + } + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + + switch self { + case .success(let data): + try container.encode(data, forKey: .success) + case .error(let data): + try container.encode(data, forKey: .error) + case .loading(let data): + try container.encode(data, forKey: .loading) + } + } +} + + +public struct genericContainer: Codable { + public let primary: T + public let secondary: U + public let metadata: Optional<[(String, String)]> +} + +/// Comprehensive example showcasing ALL configuration options for specta-swift +/// +/// This example demonstrates every configuration option available in the Swift exporter, +/// showing how different settings affect the generated Swift code. +/// Sample types for demonstration +public struct user: Codable { + public let userId: UInt32 + public let fullName: String + public let emailAddress: Optional + public let isVerified: Bool + + private enum CodingKeys: String, CodingKey { + case userId = "user_id" + case fullName = "full_name" + case emailAddress = "email_address" + case isVerified = "is_verified" + } +} + diff --git a/specta-swift/examples/generated/CommentsExample.swift b/specta-swift/examples/generated/CommentsExample.swift new file mode 100644 index 0000000..b4f62d5 --- /dev/null +++ b/specta-swift/examples/generated/CommentsExample.swift @@ -0,0 +1,113 @@ +// This file has been generated by Specta. DO NOT EDIT. +import Foundation + +/// A comprehensive example demonstrating multi-line comment support +/// +/// This example shows how Specta Swift handles complex documentation +/// including: +/// - Multi-line type documentation +/// - Bullet points and formatting +/// - Complex technical descriptions +/// +/// The generated Swift code will have properly formatted doc comments +/// that are compatible with Swift's documentation system. +public enum ApiResponse { + case success(ApiResponseSuccessData) + case error(ApiResponseErrorData) + case loading(ApiResponseLoadingData) +} +public struct ApiResponseSuccessData: Codable { + public let data: T + public let status: UInt16 + public let headers: [(String, String)]? +} + +public struct ApiResponseErrorData: Codable { + public let message: String + public let code: UInt32 + public let details: String? +} + +public struct ApiResponseLoadingData: Codable { + public let progress: Float + public let estimatedTime: UInt64? + + private enum CodingKeys: String, CodingKey { + case progress = "progress" + case estimatedTime = "estimated_time" + } +} + +// MARK: - ApiResponse Codable Implementation +extension ApiResponse: Codable { + private enum CodingKeys: String, CodingKey { + case success = "Success" + case error = "Error" + case loading = "Loading" + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + if container.allKeys.count != 1 { + throw DecodingError.dataCorrupted( + DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Invalid number of keys found, expected one.") + ) + } + + let key = container.allKeys.first! + switch key { + case .success: + let data = try container.decode(ApiResponseSuccessData.self, forKey: .success) + self = .success(data) + case .error: + let data = try container.decode(ApiResponseErrorData.self, forKey: .error) + self = .error(data) + case .loading: + let data = try container.decode(ApiResponseLoadingData.self, forKey: .loading) + self = .loading(data) + } + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + + switch self { + case .success(let data): + try container.encode(data, forKey: .success) + case .error(let data): + try container.encode(data, forKey: .error) + case .loading(let data): + try container.encode(data, forKey: .loading) + } + } +} + + +/// A user account in the system +/// +/// This struct represents a complete user account with all necessary +/// information for authentication, authorization, and personalization. +/// +/// # Security Notes +/// - The `password_hash` field should never be logged or exposed +/// - The `api_key` is sensitive and should be treated as a secret +/// - All timestamps are in UTC +public struct User: Codable { + public let id: UInt32 + public let username: String + public let email: String + public let isActive: Bool + public let createdAt: String + public let lastLogin: String? + + private enum CodingKeys: String, CodingKey { + case id = "id" + case username = "username" + case email = "email" + case isActive = "is_active" + case createdAt = "created_at" + case lastLogin = "last_login" + } +} + diff --git a/specta-swift/examples/generated/ComprehensiveDemo.swift b/specta-swift/examples/generated/ComprehensiveDemo.swift new file mode 100644 index 0000000..2a1fb0e --- /dev/null +++ b/specta-swift/examples/generated/ComprehensiveDemo.swift @@ -0,0 +1,640 @@ +// This file has been generated by Specta. DO NOT EDIT. +import Foundation + +// MARK: - Duration Helper +/// Helper struct to decode Rust Duration format {"secs": u64, "nanos": u32} +public struct RustDuration: Codable { + public let secs: UInt64 + public let nanos: UInt32 + + public var timeInterval: TimeInterval { + return Double(secs) + Double(nanos) / 1_000_000_000.0 + } +} + +// MARK: - Generated Types + +/// Admin level enum +public enum AdminLevel: Codable { + case junior + case senior + case lead + case director +} + +/// API response wrapper +public struct ApiResponse: Codable { + public let data: T? + public let error: E? + public let status: ResponseStatus + public let metadata: ResponseMetadata +} + +/// File attachment +public struct Attachment: Codable { + public let id: String + public let filename: String + public let size: UInt64 + public let mimeType: String + public let uploadedAt: String + public let uploadedBy: UInt32 + + private enum CodingKeys: String, CodingKey { + case id = "id" + case filename = "filename" + case size = "size" + case mimeType = "mime_type" + case uploadedAt = "uploaded_at" + case uploadedBy = "uploaded_by" + } +} + +/// Data sharing settings +public struct DataSharing: Codable { + public let analytics: Bool + public let marketing: Bool + public let thirdParty: Bool + public let research: Bool + + private enum CodingKeys: String, CodingKey { + case analytics = "analytics" + case marketing = "marketing" + case thirdParty = "third_party" + case research = "research" + } +} + +/// Display settings +public struct DisplaySettings: Codable { + public let itemsPerPage: UInt32 + public let dateFormat: String + public let timeFormat: String + public let currency: String + public let compactMode: Bool + + private enum CodingKeys: String, CodingKey { + case itemsPerPage = "items_per_page" + case dateFormat = "date_format" + case timeFormat = "time_format" + case currency = "currency" + case compactMode = "compact_mode" + } +} + +/// Health status enum +public enum HealthStatus: Codable { + case healthy + case degraded + case unhealthy + case unknown +} + +/// Notification frequency enum +public enum NotificationFrequency: Codable { + case immediate + case hourly + case daily + case weekly + case never +} + +/// Notification settings +public struct NotificationSettings: Codable { + public let emailEnabled: Bool + public let pushEnabled: Bool + public let smsEnabled: Bool + public let desktopEnabled: Bool + public let frequency: NotificationFrequency + + private enum CodingKeys: String, CodingKey { + case emailEnabled = "email_enabled" + case pushEnabled = "push_enabled" + case smsEnabled = "sms_enabled" + case desktopEnabled = "desktop_enabled" + case frequency = "frequency" + } +} + +/// Pagination information +public struct PaginationInfo: Codable { + public let page: UInt32 + public let perPage: UInt32 + public let totalPages: UInt32 + public let totalItems: UInt64 + public let hasNext: Bool + public let hasPrev: Bool + + private enum CodingKeys: String, CodingKey { + case page = "page" + case perPage = "per_page" + case totalPages = "total_pages" + case totalItems = "total_items" + case hasNext = "has_next" + case hasPrev = "has_prev" + } +} + +/// Priority enum (string enum) +public enum Priority: Codable { + case low + case medium + case high + case critical + case emergency +} + +/// Privacy settings +public struct PrivacySettings: Codable { + public let profileVisibility: Visibility + public let activityVisibility: Visibility + public let dataSharing: DataSharing + + private enum CodingKeys: String, CodingKey { + case profileVisibility = "profile_visibility" + case activityVisibility = "activity_visibility" + case dataSharing = "data_sharing" + } +} + +/// Response metadata +public struct ResponseMetadata: Codable { + public let requestId: String + public let timestamp: String + public let processingTime: RustDuration + public let version: String + public let pagination: PaginationInfo? + + private enum CodingKeys: String, CodingKey { + case requestId = "request_id" + case timestamp = "timestamp" + case processingTime = "processing_time" + case version = "version" + case pagination = "pagination" + } +} + +/// Response status enum +public enum ResponseStatus: Codable { + case success + case partialSuccess + case error + case validationError + case authenticationError + case authorizationError + case notFound + case rateLimited +} + +/// Review comment +public struct ReviewComment: Codable { + public let id: UInt32 + public let author: UInt32 + public let content: String + public let createdAt: String + public let updatedAt: String + public let isResolved: Bool + public let parentComment: UInt32? + public let attachments: [Attachment] + + private enum CodingKeys: String, CodingKey { + case id = "id" + case author = "author" + case content = "content" + case createdAt = "created_at" + case updatedAt = "updated_at" + case isResolved = "is_resolved" + case parentComment = "parent_comment" + case attachments = "attachments" + } +} + +/// Service status +public struct ServiceStatus: Codable { + public let name: String + public let status: HealthStatus + public let responseTime: RustDuration + public let lastCheck: String + public let errorCount: UInt32 + + private enum CodingKeys: String, CodingKey { + case name = "name" + case status = "status" + case responseTime = "response_time" + case lastCheck = "last_check" + case errorCount = "error_count" + } +} + +/// Subtask with timing information +public struct SubTask: Codable { + public let id: UInt32 + public let title: String + public let description: String? + public let status: SubTaskStatus + public let estimatedDuration: RustDuration + public let actualDuration: RustDuration? + public let assignee: UInt32? + public let createdAt: String + public let completedAt: String? + + private enum CodingKeys: String, CodingKey { + case id = "id" + case title = "title" + case description = "description" + case status = "status" + case estimatedDuration = "estimated_duration" + case actualDuration = "actual_duration" + case assignee = "assignee" + case createdAt = "created_at" + case completedAt = "completed_at" + } +} + +/// Subtask status (simple enum) +public enum SubTaskStatus: Codable { + case pending + case inProgress + case completed + case skipped +} + +/// System health information +public struct SystemHealth: Codable { + public let status: HealthStatus + public let uptime: RustDuration + public let lastCheck: String + public let services: [ServiceStatus] + public let metrics: SystemMetrics + + private enum CodingKeys: String, CodingKey { + case status = "status" + case uptime = "uptime" + case lastCheck = "last_check" + case services = "services" + case metrics = "metrics" + } +} + +/// System metrics +public struct SystemMetrics: Codable { + public let cpuUsage: Double + public let memoryUsage: Double + public let diskUsage: Double + public let networkUsage: Double + public let activeUsers: UInt32 + public let totalRequests: UInt64 + public let errorRate: Double + + private enum CodingKeys: String, CodingKey { + case cpuUsage = "cpu_usage" + case memoryUsage = "memory_usage" + case diskUsage = "disk_usage" + case networkUsage = "network_usage" + case activeUsers = "active_users" + case totalRequests = "total_requests" + case errorRate = "error_rate" + } +} + +/// Comprehensive demonstration of ALL specta-swift functionality +/// +/// This example showcases every feature and capability of specta-swift in a single, +/// realistic application scenario. It demonstrates complex type relationships, +/// various enum patterns, special types, and advanced features. +/// Main application types for a task management system +public struct Task: Codable { + public let id: UInt32 + public let title: String + public let description: String? + public let status: TaskStatus + public let priority: Priority + public let assignee: User? + public let createdAt: String + public let updatedAt: String + public let dueDate: String? + public let duration: RustDuration? + public let tags: [String] + public let metadata: TaskMetadata + public let subtasks: [SubTask] + + private enum CodingKeys: String, CodingKey { + case id = "id" + case title = "title" + case description = "description" + case status = "status" + case priority = "priority" + case assignee = "assignee" + case createdAt = "created_at" + case updatedAt = "updated_at" + case dueDate = "due_date" + case duration = "duration" + case tags = "tags" + case metadata = "metadata" + case subtasks = "subtasks" + } +} + +/// Task metadata +public struct TaskMetadata: Codable { + public let createdBy: UInt32 + public let lastModifiedBy: UInt32 + public let version: UInt32 + public let customFields: [(String, String)] + public let attachments: [Attachment] + public let watchers: [UInt32] + public let dependencies: [UInt32] + + private enum CodingKeys: String, CodingKey { + case createdBy = "created_by" + case lastModifiedBy = "last_modified_by" + case version = "version" + case customFields = "custom_fields" + case attachments = "attachments" + case watchers = "watchers" + case dependencies = "dependencies" + } +} + +/// Task status enum with mixed variants +public enum TaskStatus { + case todo + case inProgress(TaskStatusInProgressData) + case blocked(TaskStatusBlockedData) + case review(TaskStatusReviewData) + case completed(TaskStatusCompletedData) + case cancelled(TaskStatusCancelledData) +} +public struct TaskStatusInProgressData: Codable { + public let startedAt: String + public let estimatedCompletion: String? + public let progress: Float + + private enum CodingKeys: String, CodingKey { + case startedAt = "started_at" + case estimatedCompletion = "estimated_completion" + case progress = "progress" + } +} + +public struct TaskStatusBlockedData: Codable { + public let reason: String + public let blockedBy: [UInt32] + public let estimatedUnblock: String? + + private enum CodingKeys: String, CodingKey { + case reason = "reason" + case blockedBy = "blocked_by" + case estimatedUnblock = "estimated_unblock" + } +} + +public struct TaskStatusReviewData: Codable { + public let reviewer: String + public let reviewStartedAt: String + public let comments: [ReviewComment] + + private enum CodingKeys: String, CodingKey { + case reviewer = "reviewer" + case reviewStartedAt = "review_started_at" + case comments = "comments" + } +} + +public struct TaskStatusCompletedData: Codable { + public let completedAt: String + public let completionTime: RustDuration + public let finalNotes: String? + + private enum CodingKeys: String, CodingKey { + case completedAt = "completed_at" + case completionTime = "completion_time" + case finalNotes = "final_notes" + } +} + +public struct TaskStatusCancelledData: Codable { + public let reason: String + public let cancelledAt: String + + private enum CodingKeys: String, CodingKey { + case reason = "reason" + case cancelledAt = "cancelled_at" + } +} + +// MARK: - TaskStatus Codable Implementation +extension TaskStatus: Codable { + private enum CodingKeys: String, CodingKey { + case todo = "Todo" + case inProgress = "InProgress" + case blocked = "Blocked" + case review = "Review" + case completed = "Completed" + case cancelled = "Cancelled" + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + if container.allKeys.count != 1 { + throw DecodingError.dataCorrupted( + DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Invalid number of keys found, expected one.") + ) + } + + let key = container.allKeys.first! + switch key { + case .todo: + self = .todo + case .inProgress: + let data = try container.decode(TaskStatusInProgressData.self, forKey: .inProgress) + self = .inProgress(data) + case .blocked: + let data = try container.decode(TaskStatusBlockedData.self, forKey: .blocked) + self = .blocked(data) + case .review: + let data = try container.decode(TaskStatusReviewData.self, forKey: .review) + self = .review(data) + case .completed: + let data = try container.decode(TaskStatusCompletedData.self, forKey: .completed) + self = .completed(data) + case .cancelled: + let data = try container.decode(TaskStatusCancelledData.self, forKey: .cancelled) + self = .cancelled(data) + } + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + + switch self { + case .todo: + try container.encodeNil(forKey: .todo) + case .inProgress(let data): + try container.encode(data, forKey: .inProgress) + case .blocked(let data): + try container.encode(data, forKey: .blocked) + case .review(let data): + try container.encode(data, forKey: .review) + case .completed(let data): + try container.encode(data, forKey: .completed) + case .cancelled(let data): + try container.encode(data, forKey: .cancelled) + } + } +} + + +/// Theme enum (string enum) +public enum Theme: Codable { + case light + case dark + case auto + case custom +} + +/// User information +public struct User: Codable { + public let id: UInt32 + public let username: String + public let email: String + public let profile: UserProfile + public let preferences: UserPreferences + public let role: UserRole + public let isActive: Bool + public let lastLogin: String? + public let createdAt: String + + private enum CodingKeys: String, CodingKey { + case id = "id" + case username = "username" + case email = "email" + case profile = "profile" + case preferences = "preferences" + case role = "role" + case isActive = "is_active" + case lastLogin = "last_login" + case createdAt = "created_at" + } +} + +/// User preferences +public struct UserPreferences: Codable { + public let theme: Theme + public let notifications: NotificationSettings + public let privacy: PrivacySettings + public let display: DisplaySettings +} + +/// User profile with nested data +public struct UserProfile: Codable { + public let firstName: String + public let lastName: String + public let bio: String? + public let avatarUrl: String? + public let timezone: String + public let language: String + + private enum CodingKeys: String, CodingKey { + case firstName = "first_name" + case lastName = "last_name" + case bio = "bio" + case avatarUrl = "avatar_url" + case timezone = "timezone" + case language = "language" + } +} + +/// User role with permissions +public enum UserRole { + case user + case moderator(UserRoleModeratorData) + case admin(UserRoleAdminData) + case superAdmin(UserRoleSuperAdminData) +} +public struct UserRoleModeratorData: Codable { + public let permissions: [String] + public let department: String +} + +public struct UserRoleAdminData: Codable { + public let level: AdminLevel + public let departments: [String] + public let specialAccess: [String] + + private enum CodingKeys: String, CodingKey { + case level = "level" + case departments = "departments" + case specialAccess = "special_access" + } +} + +public struct UserRoleSuperAdminData: Codable { + public let systemAccess: Bool + public let auditLogs: Bool + + private enum CodingKeys: String, CodingKey { + case systemAccess = "system_access" + case auditLogs = "audit_logs" + } +} + +// MARK: - UserRole Codable Implementation +extension UserRole: Codable { + private enum CodingKeys: String, CodingKey { + case user = "User" + case moderator = "Moderator" + case admin = "Admin" + case superAdmin = "SuperAdmin" + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + if container.allKeys.count != 1 { + throw DecodingError.dataCorrupted( + DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Invalid number of keys found, expected one.") + ) + } + + let key = container.allKeys.first! + switch key { + case .user: + self = .user + case .moderator: + let data = try container.decode(UserRoleModeratorData.self, forKey: .moderator) + self = .moderator(data) + case .admin: + let data = try container.decode(UserRoleAdminData.self, forKey: .admin) + self = .admin(data) + case .superAdmin: + let data = try container.decode(UserRoleSuperAdminData.self, forKey: .superAdmin) + self = .superAdmin(data) + } + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + + switch self { + case .user: + try container.encodeNil(forKey: .user) + case .moderator(let data): + try container.encode(data, forKey: .moderator) + case .admin(let data): + try container.encode(data, forKey: .admin) + case .superAdmin(let data): + try container.encode(data, forKey: .superAdmin) + } + } +} + + +/// Visibility enum +public enum Visibility: Codable { + case public + case friends + case private + case hidden +} + diff --git a/specta-swift/examples/generated/CustomHeader.swift b/specta-swift/examples/generated/CustomHeader.swift new file mode 100644 index 0000000..d296476 --- /dev/null +++ b/specta-swift/examples/generated/CustomHeader.swift @@ -0,0 +1,105 @@ +// Generated by MyAwesomeApp v2.0 +// Custom header with app info +// DO NOT EDIT MANUALLY +import Foundation + +public enum ApiResponse { + case success(ApiResponseSuccessData) + case error(ApiResponseErrorData) + case loading(ApiResponseLoadingData) +} +public struct ApiResponseSuccessData: Codable { + public let data: T + public let statusCode: UInt16 + + private enum CodingKeys: String, CodingKey { + case data = "data" + case statusCode = "status_code" + } +} + +public struct ApiResponseErrorData: Codable { + public let message: String + public let errorCode: UInt32 + + private enum CodingKeys: String, CodingKey { + case message = "message" + case errorCode = "error_code" + } +} + +public struct ApiResponseLoadingData: Codable { + public let progress: Float +} + +// MARK: - ApiResponse Codable Implementation +extension ApiResponse: Codable { + private enum CodingKeys: String, CodingKey { + case success = "Success" + case error = "Error" + case loading = "Loading" + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + if container.allKeys.count != 1 { + throw DecodingError.dataCorrupted( + DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Invalid number of keys found, expected one.") + ) + } + + let key = container.allKeys.first! + switch key { + case .success: + let data = try container.decode(ApiResponseSuccessData.self, forKey: .success) + self = .success(data) + case .error: + let data = try container.decode(ApiResponseErrorData.self, forKey: .error) + self = .error(data) + case .loading: + let data = try container.decode(ApiResponseLoadingData.self, forKey: .loading) + self = .loading(data) + } + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + + switch self { + case .success(let data): + try container.encode(data, forKey: .success) + case .error(let data): + try container.encode(data, forKey: .error) + case .loading(let data): + try container.encode(data, forKey: .loading) + } + } +} + + +public struct GenericContainer: Codable { + public let primary: T + public let secondary: U + public let metadata: [(String, String)]? +} + +/// Comprehensive example showcasing ALL configuration options for specta-swift +/// +/// This example demonstrates every configuration option available in the Swift exporter, +/// showing how different settings affect the generated Swift code. +/// Sample types for demonstration +public struct User: Codable { + public let userId: UInt32 + public let fullName: String + public let emailAddress: String? + public let isVerified: Bool + + private enum CodingKeys: String, CodingKey { + case userId = "user_id" + case fullName = "full_name" + case emailAddress = "email_address" + case isVerified = "is_verified" + } +} + diff --git a/specta-swift/examples/generated/CustomTypes.swift b/specta-swift/examples/generated/CustomTypes.swift new file mode 100644 index 0000000..4a3aecc --- /dev/null +++ b/specta-swift/examples/generated/CustomTypes.swift @@ -0,0 +1,145 @@ +// Generated by MyApp - Custom Header +import Foundation + +public enum api_result { + case success(api_resultSuccessData) + case error(api_resultErrorData) + case loading(api_resultLoadingData) +} +public struct api_resultSuccessData: Codable { + public let data: T + public let status: UInt16 +} + +public struct api_resultErrorData: Codable { + public let message: String + public let code: UInt32 +} + +public struct api_resultLoadingData: Codable { + public let progress: Float +} + +// MARK: - api_result Codable Implementation +extension api_result: Codable { + private enum CodingKeys: String, CodingKey { + case success = "Success" + case error = "Error" + case loading = "Loading" + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + if container.allKeys.count != 1 { + throw DecodingError.dataCorrupted( + DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Invalid number of keys found, expected one.") + ) + } + + let key = container.allKeys.first! + switch key { + case .success: + let data = try container.decode(api_resultSuccessData.self, forKey: .success) + self = .success(data) + case .error: + let data = try container.decode(api_resultErrorData.self, forKey: .error) + self = .error(data) + case .loading: + let data = try container.decode(api_resultLoadingData.self, forKey: .loading) + self = .loading(data) + } + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + + switch self { + case .success(let data): + try container.encode(data, forKey: .success) + case .error(let data): + try container.encode(data, forKey: .error) + case .loading(let data): + try container.encode(data, forKey: .loading) + } + } +} + + +public struct user: Codable { + public let id: UInt32 + public let name: String + public let email: Optional + public let role: user_role +} + +public enum user_role { + case guest + case user(user_roleUserData) + case admin(user_roleAdminData) + case super_admin(user_roleSuperAdminData) +} +public struct user_roleUserData: Codable { + public let permissions: [String] +} + +public struct user_roleAdminData: Codable { + public let level: UInt8 + public let department: String +} + +public struct user_roleSuperAdminData: Codable { + public let access_level: UInt32 +} + +// MARK: - user_role Codable Implementation +extension user_role: Codable { + private enum CodingKeys: String, CodingKey { + case guest = "Guest" + case user = "User" + case admin = "Admin" + case super_admin = "SuperAdmin" + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + if container.allKeys.count != 1 { + throw DecodingError.dataCorrupted( + DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Invalid number of keys found, expected one.") + ) + } + + let key = container.allKeys.first! + switch key { + case .guest: + self = .guest + case .user: + let data = try container.decode(user_roleUserData.self, forKey: .user) + self = .user(data) + case .admin: + let data = try container.decode(user_roleAdminData.self, forKey: .admin) + self = .admin(data) + case .super_admin: + let data = try container.decode(user_roleSuperAdminData.self, forKey: .super_admin) + self = .super_admin(data) + } + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + + switch self { + case .guest: + try container.encodeNil(forKey: .guest) + case .user(let data): + try container.encode(data, forKey: .user) + case .admin(let data): + try container.encode(data, forKey: .admin) + case .super_admin(let data): + try container.encode(data, forKey: .super_admin) + } + } +} + + diff --git a/specta-swift/examples/generated/DefaultConfig.swift b/specta-swift/examples/generated/DefaultConfig.swift new file mode 100644 index 0000000..c18d58e --- /dev/null +++ b/specta-swift/examples/generated/DefaultConfig.swift @@ -0,0 +1,103 @@ +// This file has been generated by Specta. DO NOT EDIT. +import Foundation + +public enum ApiResponse { + case success(ApiResponseSuccessData) + case error(ApiResponseErrorData) + case loading(ApiResponseLoadingData) +} +public struct ApiResponseSuccessData: Codable { + public let data: T + public let statusCode: UInt16 + + private enum CodingKeys: String, CodingKey { + case data = "data" + case statusCode = "status_code" + } +} + +public struct ApiResponseErrorData: Codable { + public let message: String + public let errorCode: UInt32 + + private enum CodingKeys: String, CodingKey { + case message = "message" + case errorCode = "error_code" + } +} + +public struct ApiResponseLoadingData: Codable { + public let progress: Float +} + +// MARK: - ApiResponse Codable Implementation +extension ApiResponse: Codable { + private enum CodingKeys: String, CodingKey { + case success = "Success" + case error = "Error" + case loading = "Loading" + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + if container.allKeys.count != 1 { + throw DecodingError.dataCorrupted( + DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Invalid number of keys found, expected one.") + ) + } + + let key = container.allKeys.first! + switch key { + case .success: + let data = try container.decode(ApiResponseSuccessData.self, forKey: .success) + self = .success(data) + case .error: + let data = try container.decode(ApiResponseErrorData.self, forKey: .error) + self = .error(data) + case .loading: + let data = try container.decode(ApiResponseLoadingData.self, forKey: .loading) + self = .loading(data) + } + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + + switch self { + case .success(let data): + try container.encode(data, forKey: .success) + case .error(let data): + try container.encode(data, forKey: .error) + case .loading(let data): + try container.encode(data, forKey: .loading) + } + } +} + + +public struct GenericContainer: Codable { + public let primary: T + public let secondary: U + public let metadata: [(String, String)]? +} + +/// Comprehensive example showcasing ALL configuration options for specta-swift +/// +/// This example demonstrates every configuration option available in the Swift exporter, +/// showing how different settings affect the generated Swift code. +/// Sample types for demonstration +public struct User: Codable { + public let userId: UInt32 + public let fullName: String + public let emailAddress: String? + public let isVerified: Bool + + private enum CodingKeys: String, CodingKey { + case userId = "user_id" + case fullName = "full_name" + case emailAddress = "email_address" + case isVerified = "is_verified" + } +} + diff --git a/specta-swift/examples/generated/OptionalType.swift b/specta-swift/examples/generated/OptionalType.swift new file mode 100644 index 0000000..054a635 --- /dev/null +++ b/specta-swift/examples/generated/OptionalType.swift @@ -0,0 +1,103 @@ +// This file has been generated by Specta. DO NOT EDIT. +import Foundation + +public enum ApiResponse { + case success(ApiResponseSuccessData) + case error(ApiResponseErrorData) + case loading(ApiResponseLoadingData) +} +public struct ApiResponseSuccessData: Codable { + public let data: T + public let statusCode: UInt16 + + private enum CodingKeys: String, CodingKey { + case data = "data" + case statusCode = "status_code" + } +} + +public struct ApiResponseErrorData: Codable { + public let message: String + public let errorCode: UInt32 + + private enum CodingKeys: String, CodingKey { + case message = "message" + case errorCode = "error_code" + } +} + +public struct ApiResponseLoadingData: Codable { + public let progress: Float +} + +// MARK: - ApiResponse Codable Implementation +extension ApiResponse: Codable { + private enum CodingKeys: String, CodingKey { + case success = "Success" + case error = "Error" + case loading = "Loading" + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + if container.allKeys.count != 1 { + throw DecodingError.dataCorrupted( + DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Invalid number of keys found, expected one.") + ) + } + + let key = container.allKeys.first! + switch key { + case .success: + let data = try container.decode(ApiResponseSuccessData.self, forKey: .success) + self = .success(data) + case .error: + let data = try container.decode(ApiResponseErrorData.self, forKey: .error) + self = .error(data) + case .loading: + let data = try container.decode(ApiResponseLoadingData.self, forKey: .loading) + self = .loading(data) + } + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + + switch self { + case .success(let data): + try container.encode(data, forKey: .success) + case .error(let data): + try container.encode(data, forKey: .error) + case .loading(let data): + try container.encode(data, forKey: .loading) + } + } +} + + +public struct GenericContainer: Codable { + public let primary: T + public let secondary: U + public let metadata: Optional<[(String, String)]> +} + +/// Comprehensive example showcasing ALL configuration options for specta-swift +/// +/// This example demonstrates every configuration option available in the Swift exporter, +/// showing how different settings affect the generated Swift code. +/// Sample types for demonstration +public struct User: Codable { + public let userId: UInt32 + public let fullName: String + public let emailAddress: Optional + public let isVerified: Bool + + private enum CodingKeys: String, CodingKey { + case userId = "user_id" + case fullName = "full_name" + case emailAddress = "email_address" + case isVerified = "is_verified" + } +} + diff --git a/specta-swift/examples/generated/PascalCase.swift b/specta-swift/examples/generated/PascalCase.swift new file mode 100644 index 0000000..c18d58e --- /dev/null +++ b/specta-swift/examples/generated/PascalCase.swift @@ -0,0 +1,103 @@ +// This file has been generated by Specta. DO NOT EDIT. +import Foundation + +public enum ApiResponse { + case success(ApiResponseSuccessData) + case error(ApiResponseErrorData) + case loading(ApiResponseLoadingData) +} +public struct ApiResponseSuccessData: Codable { + public let data: T + public let statusCode: UInt16 + + private enum CodingKeys: String, CodingKey { + case data = "data" + case statusCode = "status_code" + } +} + +public struct ApiResponseErrorData: Codable { + public let message: String + public let errorCode: UInt32 + + private enum CodingKeys: String, CodingKey { + case message = "message" + case errorCode = "error_code" + } +} + +public struct ApiResponseLoadingData: Codable { + public let progress: Float +} + +// MARK: - ApiResponse Codable Implementation +extension ApiResponse: Codable { + private enum CodingKeys: String, CodingKey { + case success = "Success" + case error = "Error" + case loading = "Loading" + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + if container.allKeys.count != 1 { + throw DecodingError.dataCorrupted( + DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Invalid number of keys found, expected one.") + ) + } + + let key = container.allKeys.first! + switch key { + case .success: + let data = try container.decode(ApiResponseSuccessData.self, forKey: .success) + self = .success(data) + case .error: + let data = try container.decode(ApiResponseErrorData.self, forKey: .error) + self = .error(data) + case .loading: + let data = try container.decode(ApiResponseLoadingData.self, forKey: .loading) + self = .loading(data) + } + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + + switch self { + case .success(let data): + try container.encode(data, forKey: .success) + case .error(let data): + try container.encode(data, forKey: .error) + case .loading(let data): + try container.encode(data, forKey: .loading) + } + } +} + + +public struct GenericContainer: Codable { + public let primary: T + public let secondary: U + public let metadata: [(String, String)]? +} + +/// Comprehensive example showcasing ALL configuration options for specta-swift +/// +/// This example demonstrates every configuration option available in the Swift exporter, +/// showing how different settings affect the generated Swift code. +/// Sample types for demonstration +public struct User: Codable { + public let userId: UInt32 + public let fullName: String + public let emailAddress: String? + public let isVerified: Bool + + private enum CodingKeys: String, CodingKey { + case userId = "user_id" + case fullName = "full_name" + case emailAddress = "email_address" + case isVerified = "is_verified" + } +} + diff --git a/specta-swift/examples/generated/ProtocolGenerics.swift b/specta-swift/examples/generated/ProtocolGenerics.swift new file mode 100644 index 0000000..c18d58e --- /dev/null +++ b/specta-swift/examples/generated/ProtocolGenerics.swift @@ -0,0 +1,103 @@ +// This file has been generated by Specta. DO NOT EDIT. +import Foundation + +public enum ApiResponse { + case success(ApiResponseSuccessData) + case error(ApiResponseErrorData) + case loading(ApiResponseLoadingData) +} +public struct ApiResponseSuccessData: Codable { + public let data: T + public let statusCode: UInt16 + + private enum CodingKeys: String, CodingKey { + case data = "data" + case statusCode = "status_code" + } +} + +public struct ApiResponseErrorData: Codable { + public let message: String + public let errorCode: UInt32 + + private enum CodingKeys: String, CodingKey { + case message = "message" + case errorCode = "error_code" + } +} + +public struct ApiResponseLoadingData: Codable { + public let progress: Float +} + +// MARK: - ApiResponse Codable Implementation +extension ApiResponse: Codable { + private enum CodingKeys: String, CodingKey { + case success = "Success" + case error = "Error" + case loading = "Loading" + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + if container.allKeys.count != 1 { + throw DecodingError.dataCorrupted( + DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Invalid number of keys found, expected one.") + ) + } + + let key = container.allKeys.first! + switch key { + case .success: + let data = try container.decode(ApiResponseSuccessData.self, forKey: .success) + self = .success(data) + case .error: + let data = try container.decode(ApiResponseErrorData.self, forKey: .error) + self = .error(data) + case .loading: + let data = try container.decode(ApiResponseLoadingData.self, forKey: .loading) + self = .loading(data) + } + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + + switch self { + case .success(let data): + try container.encode(data, forKey: .success) + case .error(let data): + try container.encode(data, forKey: .error) + case .loading(let data): + try container.encode(data, forKey: .loading) + } + } +} + + +public struct GenericContainer: Codable { + public let primary: T + public let secondary: U + public let metadata: [(String, String)]? +} + +/// Comprehensive example showcasing ALL configuration options for specta-swift +/// +/// This example demonstrates every configuration option available in the Swift exporter, +/// showing how different settings affect the generated Swift code. +/// Sample types for demonstration +public struct User: Codable { + public let userId: UInt32 + public let fullName: String + public let emailAddress: String? + public let isVerified: Bool + + private enum CodingKeys: String, CodingKey { + case userId = "user_id" + case fullName = "full_name" + case emailAddress = "email_address" + case isVerified = "is_verified" + } +} + diff --git a/specta-swift/examples/generated/QuestionMark.swift b/specta-swift/examples/generated/QuestionMark.swift new file mode 100644 index 0000000..c18d58e --- /dev/null +++ b/specta-swift/examples/generated/QuestionMark.swift @@ -0,0 +1,103 @@ +// This file has been generated by Specta. DO NOT EDIT. +import Foundation + +public enum ApiResponse { + case success(ApiResponseSuccessData) + case error(ApiResponseErrorData) + case loading(ApiResponseLoadingData) +} +public struct ApiResponseSuccessData: Codable { + public let data: T + public let statusCode: UInt16 + + private enum CodingKeys: String, CodingKey { + case data = "data" + case statusCode = "status_code" + } +} + +public struct ApiResponseErrorData: Codable { + public let message: String + public let errorCode: UInt32 + + private enum CodingKeys: String, CodingKey { + case message = "message" + case errorCode = "error_code" + } +} + +public struct ApiResponseLoadingData: Codable { + public let progress: Float +} + +// MARK: - ApiResponse Codable Implementation +extension ApiResponse: Codable { + private enum CodingKeys: String, CodingKey { + case success = "Success" + case error = "Error" + case loading = "Loading" + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + if container.allKeys.count != 1 { + throw DecodingError.dataCorrupted( + DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Invalid number of keys found, expected one.") + ) + } + + let key = container.allKeys.first! + switch key { + case .success: + let data = try container.decode(ApiResponseSuccessData.self, forKey: .success) + self = .success(data) + case .error: + let data = try container.decode(ApiResponseErrorData.self, forKey: .error) + self = .error(data) + case .loading: + let data = try container.decode(ApiResponseLoadingData.self, forKey: .loading) + self = .loading(data) + } + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + + switch self { + case .success(let data): + try container.encode(data, forKey: .success) + case .error(let data): + try container.encode(data, forKey: .error) + case .loading(let data): + try container.encode(data, forKey: .loading) + } + } +} + + +public struct GenericContainer: Codable { + public let primary: T + public let secondary: U + public let metadata: [(String, String)]? +} + +/// Comprehensive example showcasing ALL configuration options for specta-swift +/// +/// This example demonstrates every configuration option available in the Swift exporter, +/// showing how different settings affect the generated Swift code. +/// Sample types for demonstration +public struct User: Codable { + public let userId: UInt32 + public let fullName: String + public let emailAddress: String? + public let isVerified: Bool + + private enum CodingKeys: String, CodingKey { + case userId = "user_id" + case fullName = "full_name" + case emailAddress = "email_address" + case isVerified = "is_verified" + } +} + diff --git a/specta-swift/examples/generated/SimpleTypes.swift b/specta-swift/examples/generated/SimpleTypes.swift new file mode 100644 index 0000000..2165c82 --- /dev/null +++ b/specta-swift/examples/generated/SimpleTypes.swift @@ -0,0 +1,149 @@ +// This file has been generated by Specta. DO NOT EDIT. +import Foundation + +public enum ApiResult { + case success(ApiResultSuccessData) + case error(ApiResultErrorData) + case loading(ApiResultLoadingData) +} +public struct ApiResultSuccessData: Codable { + public let data: T + public let status: UInt16 +} + +public struct ApiResultErrorData: Codable { + public let message: String + public let code: UInt32 +} + +public struct ApiResultLoadingData: Codable { + public let progress: Float +} + +// MARK: - ApiResult Codable Implementation +extension ApiResult: Codable { + private enum CodingKeys: String, CodingKey { + case success = "Success" + case error = "Error" + case loading = "Loading" + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + if container.allKeys.count != 1 { + throw DecodingError.dataCorrupted( + DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Invalid number of keys found, expected one.") + ) + } + + let key = container.allKeys.first! + switch key { + case .success: + let data = try container.decode(ApiResultSuccessData.self, forKey: .success) + self = .success(data) + case .error: + let data = try container.decode(ApiResultErrorData.self, forKey: .error) + self = .error(data) + case .loading: + let data = try container.decode(ApiResultLoadingData.self, forKey: .loading) + self = .loading(data) + } + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + + switch self { + case .success(let data): + try container.encode(data, forKey: .success) + case .error(let data): + try container.encode(data, forKey: .error) + case .loading(let data): + try container.encode(data, forKey: .loading) + } + } +} + + +public struct User: Codable { + public let id: UInt32 + public let name: String + public let email: String? + public let role: UserRole +} + +public enum UserRole { + case guest + case user(UserRoleUserData) + case admin(UserRoleAdminData) + case superAdmin(UserRoleSuperAdminData) +} +public struct UserRoleUserData: Codable { + public let permissions: [String] +} + +public struct UserRoleAdminData: Codable { + public let level: UInt8 + public let department: String +} + +public struct UserRoleSuperAdminData: Codable { + public let accessLevel: UInt32 + + private enum CodingKeys: String, CodingKey { + case accessLevel = "access_level" + } +} + +// MARK: - UserRole Codable Implementation +extension UserRole: Codable { + private enum CodingKeys: String, CodingKey { + case guest = "Guest" + case user = "User" + case admin = "Admin" + case superAdmin = "SuperAdmin" + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + if container.allKeys.count != 1 { + throw DecodingError.dataCorrupted( + DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Invalid number of keys found, expected one.") + ) + } + + let key = container.allKeys.first! + switch key { + case .guest: + self = .guest + case .user: + let data = try container.decode(UserRoleUserData.self, forKey: .user) + self = .user(data) + case .admin: + let data = try container.decode(UserRoleAdminData.self, forKey: .admin) + self = .admin(data) + case .superAdmin: + let data = try container.decode(UserRoleSuperAdminData.self, forKey: .superAdmin) + self = .superAdmin(data) + } + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + + switch self { + case .guest: + try container.encodeNil(forKey: .guest) + case .user(let data): + try container.encode(data, forKey: .user) + case .admin(let data): + try container.encode(data, forKey: .admin) + case .superAdmin(let data): + try container.encode(data, forKey: .superAdmin) + } + } +} + + diff --git a/specta-swift/examples/generated/SnakeCase.swift b/specta-swift/examples/generated/SnakeCase.swift new file mode 100644 index 0000000..e89e4e8 --- /dev/null +++ b/specta-swift/examples/generated/SnakeCase.swift @@ -0,0 +1,86 @@ +// This file has been generated by Specta. DO NOT EDIT. +import Foundation + +public enum api_response { + case success(api_responseSuccessData) + case error(api_responseErrorData) + case loading(api_responseLoadingData) +} +public struct api_responseSuccessData: Codable { + public let data: T + public let status_code: UInt16 +} + +public struct api_responseErrorData: Codable { + public let message: String + public let error_code: UInt32 +} + +public struct api_responseLoadingData: Codable { + public let progress: Float +} + +// MARK: - api_response Codable Implementation +extension api_response: Codable { + private enum CodingKeys: String, CodingKey { + case success = "Success" + case error = "Error" + case loading = "Loading" + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + if container.allKeys.count != 1 { + throw DecodingError.dataCorrupted( + DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Invalid number of keys found, expected one.") + ) + } + + let key = container.allKeys.first! + switch key { + case .success: + let data = try container.decode(api_responseSuccessData.self, forKey: .success) + self = .success(data) + case .error: + let data = try container.decode(api_responseErrorData.self, forKey: .error) + self = .error(data) + case .loading: + let data = try container.decode(api_responseLoadingData.self, forKey: .loading) + self = .loading(data) + } + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + + switch self { + case .success(let data): + try container.encode(data, forKey: .success) + case .error(let data): + try container.encode(data, forKey: .error) + case .loading(let data): + try container.encode(data, forKey: .loading) + } + } +} + + +public struct generic_container: Codable { + public let primary: T + public let secondary: U + public let metadata: [(String, String)]? +} + +/// Comprehensive example showcasing ALL configuration options for specta-swift +/// +/// This example demonstrates every configuration option available in the Swift exporter, +/// showing how different settings affect the generated Swift code. +/// Sample types for demonstration +public struct user: Codable { + public let user_id: UInt32 + public let full_name: String + public let email_address: String? + public let is_verified: Bool +} + diff --git a/specta-swift/examples/generated/Spaces2.swift b/specta-swift/examples/generated/Spaces2.swift new file mode 100644 index 0000000..c18d58e --- /dev/null +++ b/specta-swift/examples/generated/Spaces2.swift @@ -0,0 +1,103 @@ +// This file has been generated by Specta. DO NOT EDIT. +import Foundation + +public enum ApiResponse { + case success(ApiResponseSuccessData) + case error(ApiResponseErrorData) + case loading(ApiResponseLoadingData) +} +public struct ApiResponseSuccessData: Codable { + public let data: T + public let statusCode: UInt16 + + private enum CodingKeys: String, CodingKey { + case data = "data" + case statusCode = "status_code" + } +} + +public struct ApiResponseErrorData: Codable { + public let message: String + public let errorCode: UInt32 + + private enum CodingKeys: String, CodingKey { + case message = "message" + case errorCode = "error_code" + } +} + +public struct ApiResponseLoadingData: Codable { + public let progress: Float +} + +// MARK: - ApiResponse Codable Implementation +extension ApiResponse: Codable { + private enum CodingKeys: String, CodingKey { + case success = "Success" + case error = "Error" + case loading = "Loading" + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + if container.allKeys.count != 1 { + throw DecodingError.dataCorrupted( + DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Invalid number of keys found, expected one.") + ) + } + + let key = container.allKeys.first! + switch key { + case .success: + let data = try container.decode(ApiResponseSuccessData.self, forKey: .success) + self = .success(data) + case .error: + let data = try container.decode(ApiResponseErrorData.self, forKey: .error) + self = .error(data) + case .loading: + let data = try container.decode(ApiResponseLoadingData.self, forKey: .loading) + self = .loading(data) + } + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + + switch self { + case .success(let data): + try container.encode(data, forKey: .success) + case .error(let data): + try container.encode(data, forKey: .error) + case .loading(let data): + try container.encode(data, forKey: .loading) + } + } +} + + +public struct GenericContainer: Codable { + public let primary: T + public let secondary: U + public let metadata: [(String, String)]? +} + +/// Comprehensive example showcasing ALL configuration options for specta-swift +/// +/// This example demonstrates every configuration option available in the Swift exporter, +/// showing how different settings affect the generated Swift code. +/// Sample types for demonstration +public struct User: Codable { + public let userId: UInt32 + public let fullName: String + public let emailAddress: String? + public let isVerified: Bool + + private enum CodingKeys: String, CodingKey { + case userId = "user_id" + case fullName = "full_name" + case emailAddress = "email_address" + case isVerified = "is_verified" + } +} + diff --git a/specta-swift/examples/generated/Spaces4.swift b/specta-swift/examples/generated/Spaces4.swift new file mode 100644 index 0000000..c18d58e --- /dev/null +++ b/specta-swift/examples/generated/Spaces4.swift @@ -0,0 +1,103 @@ +// This file has been generated by Specta. DO NOT EDIT. +import Foundation + +public enum ApiResponse { + case success(ApiResponseSuccessData) + case error(ApiResponseErrorData) + case loading(ApiResponseLoadingData) +} +public struct ApiResponseSuccessData: Codable { + public let data: T + public let statusCode: UInt16 + + private enum CodingKeys: String, CodingKey { + case data = "data" + case statusCode = "status_code" + } +} + +public struct ApiResponseErrorData: Codable { + public let message: String + public let errorCode: UInt32 + + private enum CodingKeys: String, CodingKey { + case message = "message" + case errorCode = "error_code" + } +} + +public struct ApiResponseLoadingData: Codable { + public let progress: Float +} + +// MARK: - ApiResponse Codable Implementation +extension ApiResponse: Codable { + private enum CodingKeys: String, CodingKey { + case success = "Success" + case error = "Error" + case loading = "Loading" + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + if container.allKeys.count != 1 { + throw DecodingError.dataCorrupted( + DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Invalid number of keys found, expected one.") + ) + } + + let key = container.allKeys.first! + switch key { + case .success: + let data = try container.decode(ApiResponseSuccessData.self, forKey: .success) + self = .success(data) + case .error: + let data = try container.decode(ApiResponseErrorData.self, forKey: .error) + self = .error(data) + case .loading: + let data = try container.decode(ApiResponseLoadingData.self, forKey: .loading) + self = .loading(data) + } + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + + switch self { + case .success(let data): + try container.encode(data, forKey: .success) + case .error(let data): + try container.encode(data, forKey: .error) + case .loading(let data): + try container.encode(data, forKey: .loading) + } + } +} + + +public struct GenericContainer: Codable { + public let primary: T + public let secondary: U + public let metadata: [(String, String)]? +} + +/// Comprehensive example showcasing ALL configuration options for specta-swift +/// +/// This example demonstrates every configuration option available in the Swift exporter, +/// showing how different settings affect the generated Swift code. +/// Sample types for demonstration +public struct User: Codable { + public let userId: UInt32 + public let fullName: String + public let emailAddress: String? + public let isVerified: Bool + + private enum CodingKeys: String, CodingKey { + case userId = "user_id" + case fullName = "full_name" + case emailAddress = "email_address" + case isVerified = "is_verified" + } +} + diff --git a/specta-swift/examples/generated/SpecialTypes.swift b/specta-swift/examples/generated/SpecialTypes.swift new file mode 100644 index 0000000..6d9e05b --- /dev/null +++ b/specta-swift/examples/generated/SpecialTypes.swift @@ -0,0 +1,223 @@ +// This file has been generated by Specta. DO NOT EDIT. +import Foundation + +// MARK: - Duration Helper +/// Helper struct to decode Rust Duration format {"secs": u64, "nanos": u32} +public struct RustDuration: Codable { + public let secs: UInt64 + public let nanos: UInt32 + + public var timeInterval: TimeInterval { + return Double(secs) + Double(nanos) / 1_000_000_000.0 + } +} + +// MARK: - Generated Types + +/// API response with timing information +public struct ApiResponse: Codable { + public let data: String + public let processingDuration: RustDuration + public let cacheDuration: RustDuration? + public let transferDuration: RustDuration + public let statusCode: UInt16 + + private enum CodingKeys: String, CodingKey { + case data = "data" + case processingDuration = "processing_duration" + case cacheDuration = "cache_duration" + case transferDuration = "transfer_duration" + case statusCode = "status_code" + } +} + +/// Struct with various timestamp types +public struct EventLog: Codable { + public let eventId: String + public let timestamp: String + public let duration: RustDuration + public let metadata: [(String, String)]? + + private enum CodingKeys: String, CodingKey { + case eventId = "event_id" + case timestamp = "timestamp" + case duration = "duration" + case metadata = "metadata" + } +} + +/// Example showcasing special type handling in specta-swift +/// +/// This example demonstrates how specta-swift handles special Rust types +/// like Duration, UUID, chrono types, and other commonly used types +/// that need special conversion to Swift equivalents. +/// Struct with Duration fields (will be converted to RustDuration helper) +public struct IndexerMetrics: Codable { + public let totalDuration: RustDuration + public let discoveryDuration: RustDuration + public let processingDuration: RustDuration + public let contentDuration: RustDuration + public let filesProcessed: UInt32 + public let avgTimePerFile: RustDuration + + private enum CodingKeys: String, CodingKey { + case totalDuration = "total_duration" + case discoveryDuration = "discovery_duration" + case processingDuration = "processing_duration" + case contentDuration = "content_duration" + case filesProcessed = "files_processed" + case avgTimePerFile = "avg_time_per_file" + } +} + +/// Job status with timing +public enum JobStatus { + case queued + case running(JobStatusRunningData) + case completed(JobStatusCompletedData) + case failed(JobStatusFailedData) +} +public struct JobStatusRunningData: Codable { + public let startedAt: String + public let elapsedTime: RustDuration + public let estimatedCompletion: String? + + private enum CodingKeys: String, CodingKey { + case startedAt = "started_at" + case elapsedTime = "elapsed_time" + case estimatedCompletion = "estimated_completion" + } +} + +public struct JobStatusCompletedData: Codable { + public let startedAt: String + public let completedAt: String + public let totalDuration: RustDuration + public let result: String + + private enum CodingKeys: String, CodingKey { + case startedAt = "started_at" + case completedAt = "completed_at" + case totalDuration = "total_duration" + case result = "result" + } +} + +public struct JobStatusFailedData: Codable { + public let startedAt: String + public let failedAt: String + public let duration: RustDuration + public let errorMessage: String + + private enum CodingKeys: String, CodingKey { + case startedAt = "started_at" + case failedAt = "failed_at" + case duration = "duration" + case errorMessage = "error_message" + } +} + +// MARK: - JobStatus Codable Implementation +extension JobStatus: Codable { + private enum CodingKeys: String, CodingKey { + case queued = "Queued" + case running = "Running" + case completed = "Completed" + case failed = "Failed" + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + if container.allKeys.count != 1 { + throw DecodingError.dataCorrupted( + DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Invalid number of keys found, expected one.") + ) + } + + let key = container.allKeys.first! + switch key { + case .queued: + self = .queued + case .running: + let data = try container.decode(JobStatusRunningData.self, forKey: .running) + self = .running(data) + case .completed: + let data = try container.decode(JobStatusCompletedData.self, forKey: .completed) + self = .completed(data) + case .failed: + let data = try container.decode(JobStatusFailedData.self, forKey: .failed) + self = .failed(data) + } + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + + switch self { + case .queued: + try container.encodeNil(forKey: .queued) + case .running(let data): + try container.encode(data, forKey: .running) + case .completed(let data): + try container.encode(data, forKey: .completed) + case .failed(let data): + try container.encode(data, forKey: .failed) + } + } +} + + +/// Performance metrics struct +public struct PerformanceMetrics: Codable { + public let responseTime: RustDuration + public let processingTime: RustDuration + public let queryTime: RustDuration + public let networkLatency: RustDuration + public let totalTime: RustDuration + + private enum CodingKeys: String, CodingKey { + case responseTime = "response_time" + case processingTime = "processing_time" + case queryTime = "query_time" + case networkLatency = "network_latency" + case totalTime = "total_time" + } +} + +/// Complex struct mixing Duration with other types +public struct SystemHealth: Codable { + public let uptime: RustDuration + public let lastCheck: RustDuration + public let avgResponseTime: RustDuration + public let status: String + public let memoryUsage: Double + public let cpuUsage: Double + + private enum CodingKeys: String, CodingKey { + case uptime = "uptime" + case lastCheck = "last_check" + case avgResponseTime = "avg_response_time" + case status = "status" + case memoryUsage = "memory_usage" + case cpuUsage = "cpu_usage" + } +} + +/// Configuration struct with timing information +public struct TaskConfig: Codable { + public let name: String + public let timeout: RustDuration + public let retryInterval: RustDuration + public let backoffDuration: RustDuration + public let enabled: Bool + + private enum CodingKeys: String, CodingKey { + case name = "name" + case timeout = "timeout" + case retryInterval = "retry_interval" + case backoffDuration = "backoff_duration" + case enabled = "enabled" + } +} + diff --git a/specta-swift/examples/generated/StringEnums.swift b/specta-swift/examples/generated/StringEnums.swift new file mode 100644 index 0000000..f200253 --- /dev/null +++ b/specta-swift/examples/generated/StringEnums.swift @@ -0,0 +1,375 @@ +// This file has been generated by Specta. DO NOT EDIT. +import Foundation + +/// Mixed enum with both string-like and data variants +public enum ApiResult { + case success + case successWithData(ApiResultSuccessWithDataData) + case error(ApiResultErrorData) + case loading +} +public struct ApiResultSuccessWithDataData: Codable { + public let data: String + public let statusCode: UInt16 + + private enum CodingKeys: String, CodingKey { + case data = "data" + case statusCode = "status_code" + } +} + +public struct ApiResultErrorData: Codable { + public let message: String + public let code: UInt32 +} + +// MARK: - ApiResult Codable Implementation +extension ApiResult: Codable { + private enum CodingKeys: String, CodingKey { + case success = "Success" + case successWithData = "SuccessWithData" + case error = "Error" + case loading = "Loading" + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + if container.allKeys.count != 1 { + throw DecodingError.dataCorrupted( + DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Invalid number of keys found, expected one.") + ) + } + + let key = container.allKeys.first! + switch key { + case .success: + self = .success + case .successWithData: + let data = try container.decode(ApiResultSuccessWithDataData.self, forKey: .successWithData) + self = .successWithData(data) + case .error: + let data = try container.decode(ApiResultErrorData.self, forKey: .error) + self = .error(data) + case .loading: + self = .loading + } + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + + switch self { + case .success: + try container.encodeNil(forKey: .success) + case .successWithData(let data): + try container.encode(data, forKey: .successWithData) + case .error(let data): + try container.encode(data, forKey: .error) + case .loading: + try container.encodeNil(forKey: .loading) + } + } +} + + +/// String enum with more complex values +public enum Environment: Codable { + case development + case staging + case production + case testing +} + +/// Complex enum with multiple data variants +public enum EventType { + case userCreated + case userUpdated(EventTypeUserUpdatedData) + case userDeleted(EventTypeUserDeletedData) + case systemEvent(EventTypeSystemEventData) +} +public struct EventTypeUserUpdatedData: Codable { + public let userId: UInt32 + public let changes: [(String, String)] + + private enum CodingKeys: String, CodingKey { + case userId = "user_id" + case changes = "changes" + } +} + +public struct EventTypeUserDeletedData: Codable { + public let userId: UInt32 + public let reason: String + + private enum CodingKeys: String, CodingKey { + case userId = "user_id" + case reason = "reason" + } +} + +public struct EventTypeSystemEventData: Codable { + public let component: String + public let level: String + public let message: String +} + +// MARK: - EventType Codable Implementation +extension EventType: Codable { + private enum CodingKeys: String, CodingKey { + case userCreated = "UserCreated" + case userUpdated = "UserUpdated" + case userDeleted = "UserDeleted" + case systemEvent = "SystemEvent" + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + if container.allKeys.count != 1 { + throw DecodingError.dataCorrupted( + DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Invalid number of keys found, expected one.") + ) + } + + let key = container.allKeys.first! + switch key { + case .userCreated: + self = .userCreated + case .userUpdated: + let data = try container.decode(EventTypeUserUpdatedData.self, forKey: .userUpdated) + self = .userUpdated(data) + case .userDeleted: + let data = try container.decode(EventTypeUserDeletedData.self, forKey: .userDeleted) + self = .userDeleted(data) + case .systemEvent: + let data = try container.decode(EventTypeSystemEventData.self, forKey: .systemEvent) + self = .systemEvent(data) + } + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + + switch self { + case .userCreated: + try container.encodeNil(forKey: .userCreated) + case .userUpdated(let data): + try container.encode(data, forKey: .userUpdated) + case .userDeleted(let data): + try container.encode(data, forKey: .userDeleted) + case .systemEvent(let data): + try container.encode(data, forKey: .systemEvent) + } + } +} + + +/// String enum for file types +public enum FileType: Codable { + case image + case video + case audio + case document + case archive + case unknown +} + +/// Comprehensive example showcasing string enums and custom Codable implementations +/// +/// This example demonstrates how specta-swift handles string enums, mixed enums, +/// and generates appropriate Codable implementations for different enum patterns. +/// Simple string enum (will be converted to Swift String enum with Codable) +public enum HttpStatus: Codable { + case ok + case created + case accepted + case noContent + case badRequest + case unauthorized + case notFound + case internalServerError +} + +/// String enum for job states +public enum JobState: Codable { + case queued + case running + case paused + case completed + case failed + case cancelled +} + +/// Mixed enum with complex variants +public enum NotificationType { + case email + case push + case sms + case webhook(NotificationTypeWebhookData) + case inApp(NotificationTypeInAppData) +} +public struct NotificationTypeWebhookData: Codable { + public let url: String + public let headers: [(String, String)] + public let retryCount: UInt32 + + private enum CodingKeys: String, CodingKey { + case url = "url" + case headers = "headers" + case retryCount = "retry_count" + } +} + +public struct NotificationTypeInAppData: Codable { + public let title: String + public let message: String + public let priority: String +} + +// MARK: - NotificationType Codable Implementation +extension NotificationType: Codable { + private enum CodingKeys: String, CodingKey { + case email = "Email" + case push = "Push" + case sms = "Sms" + case webhook = "Webhook" + case inApp = "InApp" + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + if container.allKeys.count != 1 { + throw DecodingError.dataCorrupted( + DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Invalid number of keys found, expected one.") + ) + } + + let key = container.allKeys.first! + switch key { + case .email: + self = .email + case .push: + self = .push + case .sms: + self = .sms + case .webhook: + let data = try container.decode(NotificationTypeWebhookData.self, forKey: .webhook) + self = .webhook(data) + case .inApp: + let data = try container.decode(NotificationTypeInAppData.self, forKey: .inApp) + self = .inApp(data) + } + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + + switch self { + case .email: + try container.encodeNil(forKey: .email) + case .push: + try container.encodeNil(forKey: .push) + case .sms: + try container.encodeNil(forKey: .sms) + case .webhook(let data): + try container.encode(data, forKey: .webhook) + case .inApp(let data): + try container.encode(data, forKey: .inApp) + } + } +} + + +/// Enum with generic type parameter +public enum Result: Codable { + case ok(T) + case err(E) +} + +/// Complex mixed enum +public enum UserAction { + case login + case logout + case updateProfile(UserActionUpdateProfileData) + case changePassword(UserActionChangePasswordData) + case deleteAccount +} +public struct UserActionUpdateProfileData: Codable { + public let name: String + public let email: String + public let avatarUrl: String? + + private enum CodingKeys: String, CodingKey { + case name = "name" + case email = "email" + case avatarUrl = "avatar_url" + } +} + +public struct UserActionChangePasswordData: Codable { + public let oldPassword: String + public let newPassword: String + + private enum CodingKeys: String, CodingKey { + case oldPassword = "old_password" + case newPassword = "new_password" + } +} + +// MARK: - UserAction Codable Implementation +extension UserAction: Codable { + private enum CodingKeys: String, CodingKey { + case login = "Login" + case logout = "Logout" + case updateProfile = "UpdateProfile" + case changePassword = "ChangePassword" + case deleteAccount = "DeleteAccount" + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + if container.allKeys.count != 1 { + throw DecodingError.dataCorrupted( + DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Invalid number of keys found, expected one.") + ) + } + + let key = container.allKeys.first! + switch key { + case .login: + self = .login + case .logout: + self = .logout + case .updateProfile: + let data = try container.decode(UserActionUpdateProfileData.self, forKey: .updateProfile) + self = .updateProfile(data) + case .changePassword: + let data = try container.decode(UserActionChangePasswordData.self, forKey: .changePassword) + self = .changePassword(data) + case .deleteAccount: + self = .deleteAccount + } + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + + switch self { + case .login: + try container.encodeNil(forKey: .login) + case .logout: + try container.encodeNil(forKey: .logout) + case .updateProfile(let data): + try container.encode(data, forKey: .updateProfile) + case .changePassword(let data): + try container.encode(data, forKey: .changePassword) + case .deleteAccount: + try container.encodeNil(forKey: .deleteAccount) + } + } +} + + diff --git a/specta-swift/examples/generated/Tabs.swift b/specta-swift/examples/generated/Tabs.swift new file mode 100644 index 0000000..c18d58e --- /dev/null +++ b/specta-swift/examples/generated/Tabs.swift @@ -0,0 +1,103 @@ +// This file has been generated by Specta. DO NOT EDIT. +import Foundation + +public enum ApiResponse { + case success(ApiResponseSuccessData) + case error(ApiResponseErrorData) + case loading(ApiResponseLoadingData) +} +public struct ApiResponseSuccessData: Codable { + public let data: T + public let statusCode: UInt16 + + private enum CodingKeys: String, CodingKey { + case data = "data" + case statusCode = "status_code" + } +} + +public struct ApiResponseErrorData: Codable { + public let message: String + public let errorCode: UInt32 + + private enum CodingKeys: String, CodingKey { + case message = "message" + case errorCode = "error_code" + } +} + +public struct ApiResponseLoadingData: Codable { + public let progress: Float +} + +// MARK: - ApiResponse Codable Implementation +extension ApiResponse: Codable { + private enum CodingKeys: String, CodingKey { + case success = "Success" + case error = "Error" + case loading = "Loading" + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + if container.allKeys.count != 1 { + throw DecodingError.dataCorrupted( + DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Invalid number of keys found, expected one.") + ) + } + + let key = container.allKeys.first! + switch key { + case .success: + let data = try container.decode(ApiResponseSuccessData.self, forKey: .success) + self = .success(data) + case .error: + let data = try container.decode(ApiResponseErrorData.self, forKey: .error) + self = .error(data) + case .loading: + let data = try container.decode(ApiResponseLoadingData.self, forKey: .loading) + self = .loading(data) + } + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + + switch self { + case .success(let data): + try container.encode(data, forKey: .success) + case .error(let data): + try container.encode(data, forKey: .error) + case .loading(let data): + try container.encode(data, forKey: .loading) + } + } +} + + +public struct GenericContainer: Codable { + public let primary: T + public let secondary: U + public let metadata: [(String, String)]? +} + +/// Comprehensive example showcasing ALL configuration options for specta-swift +/// +/// This example demonstrates every configuration option available in the Swift exporter, +/// showing how different settings affect the generated Swift code. +/// Sample types for demonstration +public struct User: Codable { + public let userId: UInt32 + public let fullName: String + public let emailAddress: String? + public let isVerified: Bool + + private enum CodingKeys: String, CodingKey { + case userId = "user_id" + case fullName = "full_name" + case emailAddress = "email_address" + case isVerified = "is_verified" + } +} + diff --git a/specta-swift/examples/generated/TypealiasGenerics.swift b/specta-swift/examples/generated/TypealiasGenerics.swift new file mode 100644 index 0000000..c18d58e --- /dev/null +++ b/specta-swift/examples/generated/TypealiasGenerics.swift @@ -0,0 +1,103 @@ +// This file has been generated by Specta. DO NOT EDIT. +import Foundation + +public enum ApiResponse { + case success(ApiResponseSuccessData) + case error(ApiResponseErrorData) + case loading(ApiResponseLoadingData) +} +public struct ApiResponseSuccessData: Codable { + public let data: T + public let statusCode: UInt16 + + private enum CodingKeys: String, CodingKey { + case data = "data" + case statusCode = "status_code" + } +} + +public struct ApiResponseErrorData: Codable { + public let message: String + public let errorCode: UInt32 + + private enum CodingKeys: String, CodingKey { + case message = "message" + case errorCode = "error_code" + } +} + +public struct ApiResponseLoadingData: Codable { + public let progress: Float +} + +// MARK: - ApiResponse Codable Implementation +extension ApiResponse: Codable { + private enum CodingKeys: String, CodingKey { + case success = "Success" + case error = "Error" + case loading = "Loading" + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + if container.allKeys.count != 1 { + throw DecodingError.dataCorrupted( + DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Invalid number of keys found, expected one.") + ) + } + + let key = container.allKeys.first! + switch key { + case .success: + let data = try container.decode(ApiResponseSuccessData.self, forKey: .success) + self = .success(data) + case .error: + let data = try container.decode(ApiResponseErrorData.self, forKey: .error) + self = .error(data) + case .loading: + let data = try container.decode(ApiResponseLoadingData.self, forKey: .loading) + self = .loading(data) + } + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + + switch self { + case .success(let data): + try container.encode(data, forKey: .success) + case .error(let data): + try container.encode(data, forKey: .error) + case .loading(let data): + try container.encode(data, forKey: .loading) + } + } +} + + +public struct GenericContainer: Codable { + public let primary: T + public let secondary: U + public let metadata: [(String, String)]? +} + +/// Comprehensive example showcasing ALL configuration options for specta-swift +/// +/// This example demonstrates every configuration option available in the Swift exporter, +/// showing how different settings affect the generated Swift code. +/// Sample types for demonstration +public struct User: Codable { + public let userId: UInt32 + public let fullName: String + public let emailAddress: String? + public let isVerified: Bool + + private enum CodingKeys: String, CodingKey { + case userId = "user_id" + case fullName = "full_name" + case emailAddress = "email_address" + case isVerified = "is_verified" + } +} + diff --git a/specta-swift/examples/generated/WithProtocols.swift b/specta-swift/examples/generated/WithProtocols.swift new file mode 100644 index 0000000..1f26ed6 --- /dev/null +++ b/specta-swift/examples/generated/WithProtocols.swift @@ -0,0 +1,106 @@ +// This file has been generated by Specta. DO NOT EDIT. +import Foundation +import Equatable +import Hashable +import CustomStringConvertible + +public enum ApiResponse { + case success(ApiResponseSuccessData) + case error(ApiResponseErrorData) + case loading(ApiResponseLoadingData) +} +public struct ApiResponseSuccessData: Codable { + public let data: T + public let statusCode: UInt16 + + private enum CodingKeys: String, CodingKey { + case data = "data" + case statusCode = "status_code" + } +} + +public struct ApiResponseErrorData: Codable { + public let message: String + public let errorCode: UInt32 + + private enum CodingKeys: String, CodingKey { + case message = "message" + case errorCode = "error_code" + } +} + +public struct ApiResponseLoadingData: Codable { + public let progress: Float +} + +// MARK: - ApiResponse Codable Implementation +extension ApiResponse: Codable { + private enum CodingKeys: String, CodingKey { + case success = "Success" + case error = "Error" + case loading = "Loading" + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + if container.allKeys.count != 1 { + throw DecodingError.dataCorrupted( + DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Invalid number of keys found, expected one.") + ) + } + + let key = container.allKeys.first! + switch key { + case .success: + let data = try container.decode(ApiResponseSuccessData.self, forKey: .success) + self = .success(data) + case .error: + let data = try container.decode(ApiResponseErrorData.self, forKey: .error) + self = .error(data) + case .loading: + let data = try container.decode(ApiResponseLoadingData.self, forKey: .loading) + self = .loading(data) + } + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + + switch self { + case .success(let data): + try container.encode(data, forKey: .success) + case .error(let data): + try container.encode(data, forKey: .error) + case .loading(let data): + try container.encode(data, forKey: .loading) + } + } +} + + +public struct GenericContainer: Codable { + public let primary: T + public let secondary: U + public let metadata: [(String, String)]? +} + +/// Comprehensive example showcasing ALL configuration options for specta-swift +/// +/// This example demonstrates every configuration option available in the Swift exporter, +/// showing how different settings affect the generated Swift code. +/// Sample types for demonstration +public struct User: Codable { + public let userId: UInt32 + public let fullName: String + public let emailAddress: String? + public let isVerified: Bool + + private enum CodingKeys: String, CodingKey { + case userId = "user_id" + case fullName = "full_name" + case emailAddress = "email_address" + case isVerified = "is_verified" + } +} + diff --git a/specta-swift/examples/generated/WithSerde.swift b/specta-swift/examples/generated/WithSerde.swift new file mode 100644 index 0000000..019c481 --- /dev/null +++ b/specta-swift/examples/generated/WithSerde.swift @@ -0,0 +1,104 @@ +// This file has been generated by Specta. DO NOT EDIT. +import Foundation +import Codable + +public enum ApiResponse { + case success(ApiResponseSuccessData) + case error(ApiResponseErrorData) + case loading(ApiResponseLoadingData) +} +public struct ApiResponseSuccessData: Codable { + public let data: T + public let statusCode: UInt16 + + private enum CodingKeys: String, CodingKey { + case data = "data" + case statusCode = "status_code" + } +} + +public struct ApiResponseErrorData: Codable { + public let message: String + public let errorCode: UInt32 + + private enum CodingKeys: String, CodingKey { + case message = "message" + case errorCode = "error_code" + } +} + +public struct ApiResponseLoadingData: Codable { + public let progress: Float +} + +// MARK: - ApiResponse Codable Implementation +extension ApiResponse: Codable { + private enum CodingKeys: String, CodingKey { + case success = "Success" + case error = "Error" + case loading = "Loading" + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + if container.allKeys.count != 1 { + throw DecodingError.dataCorrupted( + DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Invalid number of keys found, expected one.") + ) + } + + let key = container.allKeys.first! + switch key { + case .success: + let data = try container.decode(ApiResponseSuccessData.self, forKey: .success) + self = .success(data) + case .error: + let data = try container.decode(ApiResponseErrorData.self, forKey: .error) + self = .error(data) + case .loading: + let data = try container.decode(ApiResponseLoadingData.self, forKey: .loading) + self = .loading(data) + } + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + + switch self { + case .success(let data): + try container.encode(data, forKey: .success) + case .error(let data): + try container.encode(data, forKey: .error) + case .loading(let data): + try container.encode(data, forKey: .loading) + } + } +} + + +public struct GenericContainer: Codable { + public let primary: T + public let secondary: U + public let metadata: [(String, String)]? +} + +/// Comprehensive example showcasing ALL configuration options for specta-swift +/// +/// This example demonstrates every configuration option available in the Swift exporter, +/// showing how different settings affect the generated Swift code. +/// Sample types for demonstration +public struct User: Codable { + public let userId: UInt32 + public let fullName: String + public let emailAddress: String? + public let isVerified: Bool + + private enum CodingKeys: String, CodingKey { + case userId = "user_id" + case fullName = "full_name" + case emailAddress = "email_address" + case isVerified = "is_verified" + } +} + diff --git a/specta-swift/examples/simple_usage.rs b/specta-swift/examples/simple_usage.rs new file mode 100644 index 0000000..71821a3 --- /dev/null +++ b/specta-swift/examples/simple_usage.rs @@ -0,0 +1,54 @@ +use specta::{Type, TypeCollection}; +use specta_swift::Swift; + +// Simple user management types +#[derive(Type)] +struct User { + id: u32, + name: String, + email: Option, + role: UserRole, +} + +#[derive(Type)] +enum UserRole { + Guest, + User { permissions: Vec }, + Admin { level: u8, department: String }, + SuperAdmin { access_level: u32 }, +} + +#[derive(Type)] +enum ApiResult { + Success { data: T, status: u16 }, + Error { message: String, code: u32 }, + Loading { progress: f32 }, +} + +fn main() { + // Create a type collection + let types = TypeCollection::default() + .register::() + .register::() + .register::>(); + + // Export to Swift with default settings + let swift = Swift::default(); + swift + .export_to("./examples/generated/SimpleTypes.swift", &types) + .unwrap(); + + println!("Simple types exported to SimpleTypes.swift"); + + // Export with custom settings + let custom_swift = Swift::new() + .header("// Generated by MyApp - Custom Header") + .naming(specta_swift::NamingConvention::SnakeCase) + .optionals(specta_swift::OptionalStyle::Optional); + + custom_swift + .export_to("./examples/generated/CustomTypes.swift", &types) + .unwrap(); + + println!("Custom types exported to CustomTypes.swift"); +} diff --git a/specta-swift/examples/special_types.rs b/specta-swift/examples/special_types.rs new file mode 100644 index 0000000..766fc16 --- /dev/null +++ b/specta-swift/examples/special_types.rs @@ -0,0 +1,183 @@ +use specta::{Type, TypeCollection}; +use specta_swift::Swift; +use std::time::Duration; + +/// Example showcasing special type handling in specta-swift +/// +/// This example demonstrates how specta-swift handles special Rust types +/// like Duration, UUID, chrono types, and other commonly used types +/// that need special conversion to Swift equivalents. + +/// Struct with Duration fields (will be converted to RustDuration helper) +#[derive(Type)] +struct IndexerMetrics { + /// Total time spent indexing + total_duration: Duration, + /// Time spent discovering files + discovery_duration: Duration, + /// Time spent processing content + processing_duration: Duration, + /// Time spent analyzing content + content_duration: Duration, + /// Number of files processed + files_processed: u32, + /// Average processing time per file + avg_time_per_file: Duration, +} + +/// Struct with various timestamp types +#[derive(Type)] +struct EventLog { + /// Event ID + event_id: String, + /// When the event occurred + timestamp: String, + /// Event duration + duration: Duration, + /// Additional metadata + metadata: Option>, +} + +/// Configuration struct with timing information +#[derive(Type)] +struct TaskConfig { + /// Task name + name: String, + /// Maximum execution time + timeout: Duration, + /// Retry interval + retry_interval: Duration, + /// Backoff duration + backoff_duration: Duration, + /// Whether task is enabled + enabled: bool, +} + +/// Performance metrics struct +#[derive(Type)] +struct PerformanceMetrics { + /// Response time + response_time: Duration, + /// Processing time + processing_time: Duration, + /// Database query time + query_time: Duration, + /// Network latency + network_latency: Duration, + /// Total time + total_time: Duration, +} + +/// API response with timing information +#[derive(Type)] +struct ApiResponse { + /// Response data + data: String, + /// Processing duration + processing_duration: Duration, + /// Cache hit duration (if applicable) + cache_duration: Option, + /// Network transfer duration + transfer_duration: Duration, + /// Status code + status_code: u16, +} + +/// Job status with timing +#[derive(Type)] +enum JobStatus { + /// Job is queued + Queued, + /// Job is running with timing info + Running { + started_at: String, + elapsed_time: Duration, + estimated_completion: Option, + }, + /// Job completed successfully + Completed { + started_at: String, + completed_at: String, + total_duration: Duration, + result: String, + }, + /// Job failed with error and timing + Failed { + started_at: String, + failed_at: String, + duration: Duration, + error_message: String, + }, +} + +/// Complex struct mixing Duration with other types +#[derive(Type)] +struct SystemHealth { + /// System uptime + uptime: Duration, + /// Last health check + last_check: Duration, + /// Average response time + avg_response_time: Duration, + /// System status + status: String, + /// Memory usage percentage + memory_usage: f64, + /// CPU usage percentage + cpu_usage: f64, +} + +fn main() { + println!("๐Ÿš€ Special Types Example - Duration and timing types"); + println!("{}", "=".repeat(60)); + + // Create type collection with all our special types + let types = TypeCollection::default() + .register::() + .register::() + .register::() + .register::() + .register::() + .register::() + .register::(); + + // Export with default settings + let swift = Swift::default(); + let output = swift.export(&types).unwrap(); + + println!("๐Ÿ“ Generated Swift code:\n"); + println!("{}", output); + + // Write to file for inspection + swift + .export_to("./examples/generated/SpecialTypes.swift", &types) + .unwrap(); + println!("โœ… Special types exported to SpecialTypes.swift"); + + println!("\n๐Ÿ” Key Features Demonstrated:"); + println!("โ€ข Duration type mapping to RustDuration helper struct"); + println!("โ€ข Automatic helper struct generation for Duration types"); + println!("โ€ข timeInterval property for easy Swift integration"); + println!("โ€ข Duration fields in structs and enum variants"); + println!("โ€ข Optional Duration fields"); + println!("โ€ข Complex timing-related data structures"); + println!("โ€ข Performance metrics with multiple Duration fields"); + + println!("\n๐Ÿ’ก Duration Helper Features:"); + println!("โ€ข RustDuration struct with secs and nanos fields"); + println!("โ€ข timeInterval computed property (Double)"); + println!("โ€ข Proper Codable implementation for Rust format"); + println!("โ€ข Automatic injection when Duration types are detected"); + + println!("\n๐Ÿ“‹ Generated Helper Struct:"); + println!("```swift"); + println!("public struct RustDuration: Codable {{"); + println!(" public let secs: UInt64"); + println!(" public let nanos: UInt32"); + println!(" "); + println!(" public var timeInterval: TimeInterval {{"); + println!(" return Double(secs) + Double(nanos) / 1_000_000_000.0"); + println!(" }}"); + println!("}}"); + println!("```"); +} diff --git a/specta-swift/examples/string_enums.rs b/specta-swift/examples/string_enums.rs new file mode 100644 index 0000000..928a7f1 --- /dev/null +++ b/specta-swift/examples/string_enums.rs @@ -0,0 +1,218 @@ +use specta::{Type, TypeCollection}; +use specta_swift::Swift; + +/// Comprehensive example showcasing string enums and custom Codable implementations +/// +/// This example demonstrates how specta-swift handles string enums, mixed enums, +/// and generates appropriate Codable implementations for different enum patterns. + +/// Simple string enum (will be converted to Swift String enum with Codable) +#[derive(Type)] +enum HttpStatus { + /// Request was successful + Ok, + /// Resource was created + Created, + /// Request was accepted + Accepted, + /// No content to return + NoContent, + /// Bad request + BadRequest, + /// Unauthorized access + Unauthorized, + /// Resource not found + NotFound, + /// Internal server error + InternalServerError, +} + +/// String enum with more complex values +#[derive(Type)] +enum Environment { + /// Development environment + Development, + /// Staging environment + Staging, + /// Production environment + Production, + /// Testing environment + Testing, +} + +/// Mixed enum with both string-like and data variants +#[derive(Type)] +enum ApiResult { + /// Simple success case + Success, + /// Success with data + SuccessWithData { data: String, status_code: u16 }, + /// Error case + Error { message: String, code: u32 }, + /// Loading state + Loading, +} + +/// Complex mixed enum +#[derive(Type)] +enum UserAction { + /// Simple login action + Login, + /// Logout action + Logout, + /// Update profile with data + UpdateProfile { + name: String, + email: String, + avatar_url: Option, + }, + /// Change password + ChangePassword { + old_password: String, + new_password: String, + }, + /// Delete account + DeleteAccount, +} + +/// String enum for job states +#[derive(Type)] +enum JobState { + /// Job is waiting in queue + Queued, + /// Job is currently running + Running, + /// Job is paused + Paused, + /// Job completed successfully + Completed, + /// Job failed with error + Failed, + /// Job was cancelled + Cancelled, +} + +/// Mixed enum with complex variants +#[derive(Type)] +enum NotificationType { + /// Simple email notification + Email, + /// Push notification + Push, + /// SMS notification + Sms, + /// Webhook notification with payload + Webhook { + url: String, + headers: Vec<(String, String)>, + retry_count: u32, + }, + /// In-app notification + InApp { + title: String, + message: String, + priority: String, + }, +} + +/// Enum with generic type parameter +#[derive(Type)] +enum Result { + /// Success with data + Ok(T), + /// Error with error details + Err(E), +} + +/// Complex enum with multiple data variants +#[derive(Type)] +enum EventType { + /// User created event + UserCreated, + /// User updated event + UserUpdated { + user_id: u32, + changes: Vec<(String, String)>, + }, + /// User deleted event + UserDeleted { user_id: u32, reason: String }, + /// System event + SystemEvent { + component: String, + level: String, + message: String, + }, +} + +/// String enum for file types +#[derive(Type)] +enum FileType { + /// Image files + Image, + /// Video files + Video, + /// Audio files + Audio, + /// Document files + Document, + /// Archive files + Archive, + /// Unknown file type + Unknown, +} + +fn main() { + println!("๐Ÿš€ String Enums Example - String enums and custom Codable"); + println!("{}", "=".repeat(60)); + + // Create type collection with all our enum types + let types = TypeCollection::default() + .register::() + .register::() + .register::() + .register::() + .register::() + .register::() + .register::>() + .register::() + .register::(); + + // Export with default settings + let swift = Swift::default(); + let output = swift.export(&types).unwrap(); + + println!("๐Ÿ“ Generated Swift code:\n"); + println!("{}", output); + + // Write to file for inspection + swift.export_to("./examples/generated/StringEnums.swift", &types).unwrap(); + println!("โœ… String enums exported to StringEnums.swift"); + + println!("\n๐Ÿ” Key Features Demonstrated:"); + println!("โ€ข Pure string enums (String, Codable)"); + println!("โ€ข Mixed enums with both simple and complex variants"); + println!("โ€ข Custom Codable implementations for complex enums"); + println!("โ€ข Struct generation for named field variants"); + println!("โ€ข Generic enum support"); + println!("โ€ข Proper Swift enum case naming"); + println!("โ€ข Automatic protocol conformance"); + + println!("\n๐Ÿ“‹ String Enum Features:"); + println!("โ€ข Automatic String and Codable conformance"); + println!("โ€ข Simple enum cases without associated values"); + println!("โ€ข Clean Swift enum representation"); + + println!("\n๐Ÿ“‹ Mixed Enum Features:"); + println!("โ€ข Custom Codable implementation generation"); + println!("โ€ข Struct generation for named field variants"); + println!("โ€ข Proper key mapping (Rust โ†’ Swift naming)"); + println!("โ€ข Error handling in Codable implementations"); + println!("โ€ข Support for both simple and complex variants"); + + println!("\n๐Ÿ’ก Generated Codable Features:"); + println!("โ€ข CodingKeys enum for key mapping"); + println!("โ€ข Custom init(from decoder:) implementation"); + println!("โ€ข Custom encode(to encoder:) implementation"); + println!("โ€ข Error handling for invalid data"); + println!("โ€ข Support for nested data structures"); +} diff --git a/specta-swift/src/error.rs b/specta-swift/src/error.rs new file mode 100644 index 0000000..8f4ec6d --- /dev/null +++ b/specta-swift/src/error.rs @@ -0,0 +1,38 @@ +//! Error types for the Swift language exporter. + +use thiserror::Error; + +/// Errors that can occur during Swift code generation. +#[derive(Debug, Error)] +pub enum Error { + /// Swift does not support this type. + #[error("Unsupported type: {0}")] + UnsupportedType(String), + + /// Invalid identifier for Swift. + #[error("Invalid identifier: {0}")] + InvalidIdentifier(String), + + /// Circular reference detected in type definitions. + #[error("Circular reference detected")] + CircularReference, + + /// Generic constraint error. + #[error("Generic constraint error: {0}")] + GenericConstraint(String), + + /// IO error during file operations. + #[error("IO error: {0}")] + Io(#[from] std::io::Error), + + /// Serde validation error. + #[error("Serde validation error: {0}")] + SerdeValidation(#[from] specta_serde::Error), + + /// Invalid configuration. + #[error("Configuration error: {0}")] + Configuration(String), +} + +/// Result type alias for Swift export operations. +pub type Result = std::result::Result; diff --git a/specta-swift/src/lib.rs b/specta-swift/src/lib.rs index 8491f3e..acbc45c 100644 --- a/specta-swift/src/lib.rs +++ b/specta-swift/src/lib.rs @@ -1,118 +1,56 @@ //! [Swift](https://www.swift.org) language exporter. +//! +//! This crate provides functionality to export Rust types to Swift code. +//! +//! # Usage +//! +//! Add `specta` and `specta-swift` to your project: +//! +//! ```bash +//! cargo add specta@2.0.0-rc.22 --features derive,export +//! cargo add specta-swift@0.0.1 +//! cargo add specta-serde@0.0.9 +//! ``` +//! +//! Next copy the following into your `main.rs` file: +//! +//! ```rust +//! use specta::{Type, TypeCollection}; +//! use specta_swift::Swift; +//! +//! #[derive(Type)] +//! pub struct MyType { +//! pub field: MyOtherType, +//! } +//! +//! #[derive(Type)] +//! pub struct MyOtherType { +//! pub other_field: String, +//! } +//! +//! fn main() { +//! let mut types = TypeCollection::default() +//! // We don't need to specify `MyOtherType` because it's referenced by `MyType` +//! .register::(); +//! +//! Swift::default() +//! .export_to("./Types.swift", &types) +//! .unwrap(); +//! } +//! ``` +//! +//! Now you're set up with Specta Swift! +//! +//! If you get tired of listing all your types, checkout [`specta::export`]. #![cfg_attr(docsrs, feature(doc_cfg))] #![doc( html_logo_url = "https://github.com/oscartbeaumont/specta/raw/main/.github/logo-128.png", html_favicon_url = "https://github.com/oscartbeaumont/specta/raw/main/.github/logo-128.png" )] -// use specta::{ -// datatype::{DataType, Primitive}, -// Generics, Type, TypeCollection, -// }; +mod error; +mod primitives; +mod swift; -// /// TODO -// pub fn export() -> Result { -// datatype(&T::inline( -// &mut TypeCollection::default(), -// Generics::Definition, -// )) -// } - -// fn datatype(t: &DataType) -> Result { -// Ok(match t { -// DataType::Primitive(p) => match p { -// Primitive::String | Primitive::char => "String", -// Primitive::i8 => "Int8", -// Primitive::u8 => "UInt8", -// Primitive::i16 => "Int16", -// Primitive::u16 => "UInt16", -// Primitive::usize => "UInt", -// Primitive::isize => "Int", -// Primitive::i32 => "Int32", -// Primitive::u32 => "UInt32", -// Primitive::i64 => "Int64", -// Primitive::u64 => "UInt64", -// Primitive::bool => "Bool", -// Primitive::f32 => "Float", -// Primitive::f64 => "Double", -// Primitive::i128 | Primitive::u128 => { -// return Err("Swift does not support 128 numbers!".to_owned()); -// } -// } -// .to_string(), -// DataType::Any => "Codable".to_string(), -// DataType::List(t) => format!("[{}]", datatype(&t.ty())?), -// DataType::Tuple(tuple) => match &tuple.elements()[..] { -// [] => "CodableVoid".to_string(), -// [ty] => datatype(ty)?, -// tys => format!( -// "({})", -// tys.iter() -// .map(datatype) -// .collect::, _>>()? -// .join(", ") -// ), -// }, -// DataType::Map(t) => format!("[{}: {}]", datatype(&t.key_ty())?, datatype(&t.value_ty())?), -// DataType::Generic(t) => t.to_string(), -// DataType::Reference(reference) => match &reference.generics()[..] { -// [] => reference.name().to_string(), -// generics => { -// let generics = generics -// .iter() -// .map(|(_, t)| datatype(t)) -// .collect::, _>>()? -// .join(", "); - -// format!("{}<{generics}>", reference.name()) -// } -// }, -// DataType::Nullable(t) => format!("{}?", datatype(t)?), -// DataType::Struct(s) => { -// // match &s.fields()[..] { -// // [] => "CodableVoid".to_string(), -// // fields => { -// // // TODO: Handle invalid field names -// // let generics = (!s.generics().is_empty()) -// // .then(|| { -// // format!( -// // "<{}>", -// // s.generics() -// // .iter() -// // .map(|g| format!("{}: Codable", g)) -// // .collect::>() -// // .join(", ") -// // ) -// // }) -// // .unwrap_or_default(); - -// // let fields = fields -// // .iter() -// // .map(|f| { -// // let name = &f.name; -// // let typ = datatype(&f.ty)?; - -// // Ok(format!("\tpublic let {name}: {typ}")) -// // }) -// // .collect::, String>>()? -// // .join("\n"); - -// // let tag = s -// // .tag() -// // .clone() -// // .map(|t| format!("\t{t}: String")) -// // .unwrap_or_default(); - -// // r#"public struct {name}{generics}: Codable {{ -// // {tag}{fields} -// // }}"# -// // .to_string() -// // } -// // } - -// todo!(); -// } -// DataType::Literal(_) => return Err("Swift does not support literal types!".to_owned()), -// _ => todo!(), -// }) -// } +pub use error::Error; +pub use swift::{GenericStyle, IndentStyle, NamingConvention, OptionalStyle, Swift}; diff --git a/specta-swift/src/primitives.rs b/specta-swift/src/primitives.rs new file mode 100644 index 0000000..3ca56f2 --- /dev/null +++ b/specta-swift/src/primitives.rs @@ -0,0 +1,941 @@ +//! Primitive type conversion from Rust to Swift. + +use std::borrow::Cow; + +use specta::{ + datatype::{DataType, Primitive}, + SpectaID, TypeCollection, +}; + +use crate::error::{Error, Result}; +use crate::swift::Swift; + +/// Export a single type to Swift. +pub fn export_type( + swift: &Swift, + types: &TypeCollection, + ndt: &specta::datatype::NamedDataType, +) -> Result { + let mut result = String::new(); + + // Add JSDoc-style comments if present + if !ndt.docs().is_empty() { + let docs = ndt.docs(); + // Handle multi-line comments properly + for line in docs.lines() { + result.push_str("/// "); + // Trim leading whitespace from the line to avoid extra spaces + result.push_str(line.trim_start()); + result.push('\n'); + } + } + + // Add deprecated annotation if present + if let Some(deprecated) = ndt.deprecated() { + let message = match deprecated { + specta::datatype::DeprecatedType::Deprecated => "This type is deprecated".to_string(), + specta::datatype::DeprecatedType::DeprecatedWithSince { note, .. } => note.to_string(), + _ => "This type is deprecated".to_string(), + }; + result.push_str(&format!( + "@available(*, deprecated, message: \"{}\")\n", + message + )); + } + + // Generate the type definition + let type_def = datatype_to_swift(swift, types, ndt.ty(), vec![], false, Some(ndt.sid()))?; + + // Format based on type + match ndt.ty() { + DataType::Struct(_) => { + let name = swift.naming.convert(ndt.name()); + let generics = if ndt.generics().is_empty() { + String::new() + } else { + format!( + "<{}>", + ndt.generics() + .iter() + .map(|g| g.to_string()) + .collect::>() + .join(", ") + ) + }; + + result.push_str(&format!("public struct {}{}: Codable {{\n", name, generics)); + result.push_str(&type_def); + result.push_str("}"); + } + DataType::Enum(e) => { + let name = swift.naming.convert(ndt.name()); + let generics = if ndt.generics().is_empty() { + String::new() + } else { + format!( + "<{}>", + ndt.generics() + .iter() + .map(|g| g.to_string()) + .collect::>() + .join(", ") + ) + }; + + // Check if this is a string enum + let is_string_enum = e.repr().map(|repr| repr.is_string()).unwrap_or(false); + + // Check if this enum has struct-like variants (needs custom Codable) + let has_struct_variants = e.variants().iter().any(|(_, variant)| { + matches!(variant.fields(), specta::datatype::Fields::Named(fields) if !fields.fields().is_empty()) + }); + + // Determine protocols based on whether we'll generate custom Codable + let protocols = if is_string_enum { + if has_struct_variants { + "String" // Custom Codable will be generated + } else { + "String, Codable" + } + } else { + if has_struct_variants { + "" // Custom Codable will be generated + } else { + "Codable" + } + }; + + let protocol_part = if protocols.is_empty() { + String::new() + } else { + format!(": {}", protocols) + }; + + result.push_str(&format!( + "public enum {}{}{} {{\n", + name, generics, protocol_part + )); + let enum_body = + enum_to_swift(swift, types, e, vec![], false, Some(ndt.sid()), Some(&name))?; + result.push_str(&enum_body); + result.push_str("}"); + + // Generate struct definitions for named field variants + let struct_definitions = + generate_enum_structs(swift, types, e, vec![], false, Some(ndt.sid()), &name)?; + result.push_str(&struct_definitions); + + // Generate custom Codable implementation for enums with struct variants + if has_struct_variants { + let codable_impl = generate_enum_codable_impl(swift, e, &name)?; + result.push_str(&codable_impl); + } + } + _ => { + // For other types, just use the type definition + result.push_str(&type_def); + } + } + + Ok(result) +} + +/// Convert a DataType to Swift syntax. +pub fn datatype_to_swift( + swift: &Swift, + types: &TypeCollection, + dt: &DataType, + location: Vec>, + is_export: bool, + sid: Option, +) -> Result { + // Check for special standard library types first + if let Some(special_type) = is_special_std_type(types, sid) { + return Ok(special_type); + } + + match dt { + DataType::Primitive(p) => primitive_to_swift(p), + DataType::Literal(l) => literal_to_swift(l), + DataType::List(l) => list_to_swift(swift, types, l), + DataType::Map(m) => map_to_swift(swift, types, m), + DataType::Nullable(def) => { + let inner = datatype_to_swift(swift, types, def, location, is_export, sid)?; + Ok(match swift.optionals { + crate::swift::OptionalStyle::QuestionMark => format!("{}?", inner), + crate::swift::OptionalStyle::Optional => format!("Optional<{}>", inner), + }) + } + DataType::Struct(s) => { + // Check if this is a Duration struct by looking at its fields + if is_duration_struct(s) { + return Ok("RustDuration".to_string()); + } + struct_to_swift(swift, types, s, location, is_export, sid) + } + DataType::Enum(e) => enum_to_swift(swift, types, e, location, is_export, sid, None), + DataType::Tuple(t) => tuple_to_swift(swift, types, t), + DataType::Reference(r) => reference_to_swift(swift, types, r), + DataType::Generic(g) => generic_to_swift(swift, g), + } +} + +/// Check if a struct is a Duration by examining its fields +pub fn is_duration_struct(s: &specta::datatype::Struct) -> bool { + match s.fields() { + specta::datatype::Fields::Named(fields) => { + let field_names: Vec = fields + .fields() + .iter() + .map(|(name, _)| name.to_string()) + .collect(); + // Duration has exactly two fields: "secs" (u64) and "nanos" (u32) + field_names.len() == 2 + && field_names.contains(&"secs".to_string()) + && field_names.contains(&"nanos".to_string()) + } + _ => false, + } +} + +/// Check if a type is a special standard library type that needs special handling +fn is_special_std_type(types: &TypeCollection, sid: Option) -> Option { + if let Some(sid) = sid { + if let Some(ndt) = types.get(sid) { + // Check for std::time::Duration + if ndt.name() == "Duration" { + return Some("RustDuration".to_string()); + } + // Check for std::time::SystemTime + if ndt.name() == "SystemTime" { + return Some("Date".to_string()); + } + } + } + None +} + +/// Convert primitive types to Swift. +fn primitive_to_swift(primitive: &Primitive) -> Result { + Ok(match primitive { + Primitive::i8 => "Int8".to_string(), + Primitive::i16 => "Int16".to_string(), + Primitive::i32 => "Int32".to_string(), + Primitive::i64 => "Int64".to_string(), + Primitive::isize => "Int".to_string(), + Primitive::u8 => "UInt8".to_string(), + Primitive::u16 => "UInt16".to_string(), + Primitive::u32 => "UInt32".to_string(), + Primitive::u64 => "UInt64".to_string(), + Primitive::usize => "UInt".to_string(), + Primitive::f32 => "Float".to_string(), + Primitive::f64 => "Double".to_string(), + Primitive::bool => "Bool".to_string(), + Primitive::char => "Character".to_string(), + Primitive::String => "String".to_string(), + Primitive::i128 | Primitive::u128 => { + return Err(Error::UnsupportedType( + "Swift does not support 128-bit integers".to_string(), + )); + } + Primitive::f16 => { + return Err(Error::UnsupportedType( + "Swift does not support f16".to_string(), + )); + } + }) +} + +/// Convert literal types to Swift. +fn literal_to_swift(literal: &specta::datatype::Literal) -> Result { + Ok(match literal { + specta::datatype::Literal::i8(v) => v.to_string(), + specta::datatype::Literal::i16(v) => v.to_string(), + specta::datatype::Literal::i32(v) => v.to_string(), + specta::datatype::Literal::u8(v) => v.to_string(), + specta::datatype::Literal::u16(v) => v.to_string(), + specta::datatype::Literal::u32(v) => v.to_string(), + specta::datatype::Literal::f32(v) => v.to_string(), + specta::datatype::Literal::f64(v) => v.to_string(), + specta::datatype::Literal::bool(v) => v.to_string(), + specta::datatype::Literal::String(s) => format!("\"{}\"", s), + specta::datatype::Literal::char(c) => format!("\"{}\"", c), + specta::datatype::Literal::None => "nil".to_string(), + _ => { + return Err(Error::UnsupportedType( + "Unsupported literal type".to_string(), + )) + } + }) +} + +/// Convert list types to Swift arrays. +fn list_to_swift( + swift: &Swift, + types: &TypeCollection, + list: &specta::datatype::List, +) -> Result { + let element_type = datatype_to_swift(swift, types, list.ty(), vec![], false, None)?; + Ok(format!("[{}]", element_type)) +} + +/// Convert map types to Swift dictionaries. +fn map_to_swift( + swift: &Swift, + types: &TypeCollection, + map: &specta::datatype::Map, +) -> Result { + let key_type = datatype_to_swift(swift, types, map.key_ty(), vec![], false, None)?; + let value_type = datatype_to_swift(swift, types, map.value_ty(), vec![], false, None)?; + Ok(format!("[{}: {}]", key_type, value_type)) +} + +/// Convert struct types to Swift. +fn struct_to_swift( + swift: &Swift, + types: &TypeCollection, + s: &specta::datatype::Struct, + location: Vec>, + is_export: bool, + sid: Option, +) -> Result { + match s.fields() { + specta::datatype::Fields::Unit => Ok("Void".to_string()), + specta::datatype::Fields::Unnamed(fields) => { + if fields.fields().is_empty() { + Ok("Void".to_string()) + } else if fields.fields().len() == 1 { + // Single field tuple struct - convert to a proper struct with a 'value' field + let field_type = datatype_to_swift( + swift, + types, + &fields.fields()[0].ty().unwrap(), + location, + is_export, + sid, + )?; + Ok(format!(" let value: {}\n", field_type)) + } else { + // Multiple field tuple struct - convert to a proper struct with numbered fields + let mut result = String::new(); + for (i, field) in fields.fields().iter().enumerate() { + let field_type = datatype_to_swift( + swift, + types, + field.ty().unwrap(), + location.clone(), + is_export, + sid, + )?; + result.push_str(&format!(" public let field{}: {}\n", i, field_type)); + } + Ok(result) + } + } + specta::datatype::Fields::Named(fields) => { + let mut result = String::new(); + let mut field_mappings = Vec::new(); + + for (original_field_name, field) in fields.fields() { + let field_type = if let Some(ty) = field.ty() { + datatype_to_swift(swift, types, ty, location.clone(), is_export, sid)? + } else { + continue; + }; + + let optional_marker = if field.optional() { "?" } else { "" }; + let swift_field_name = swift.naming.convert_field(original_field_name); + + result.push_str(&format!( + " public let {}: {}{}\n", + swift_field_name, field_type, optional_marker + )); + + field_mappings.push((swift_field_name, original_field_name.to_string())); + } + + // Generate custom CodingKeys if field names were converted + let needs_custom_coding_keys = field_mappings + .iter() + .any(|(swift_name, rust_name)| swift_name != rust_name); + if needs_custom_coding_keys { + result.push_str("\n private enum CodingKeys: String, CodingKey {\n"); + for (swift_name, rust_name) in &field_mappings { + result.push_str(&format!( + " case {} = \"{}\"\n", + swift_name, rust_name + )); + } + result.push_str(" }\n"); + } + + Ok(result) + } + } +} + +/// Generate raw value for string enum variants +fn generate_raw_value(variant_name: &str, rename_all: Option<&str>) -> String { + match rename_all { + Some("lowercase") => variant_name.to_lowercase(), + Some("UPPERCASE") => variant_name.to_uppercase(), + Some("camelCase") => { + let mut chars = variant_name.chars(); + match chars.next() { + None => String::new(), + Some(first) => first.to_lowercase().chain(chars).collect(), + } + } + Some("PascalCase") => { + let mut chars = variant_name.chars(); + match chars.next() { + None => String::new(), + Some(first) => first.to_uppercase().chain(chars).collect(), + } + } + Some("snake_case") => variant_name + .chars() + .enumerate() + .flat_map(|(i, c)| { + if c.is_uppercase() && i > 0 { + vec!['_', c.to_lowercase().next().unwrap()] + } else { + vec![c.to_lowercase().next().unwrap()] + } + }) + .collect(), + Some("SCREAMING_SNAKE_CASE") => variant_name + .chars() + .enumerate() + .flat_map(|(i, c)| { + if c.is_uppercase() && i > 0 { + vec!['_', c.to_uppercase().next().unwrap()] + } else { + vec![c.to_uppercase().next().unwrap()] + } + }) + .collect(), + Some("kebab-case") => variant_name + .chars() + .enumerate() + .flat_map(|(i, c)| { + if c.is_uppercase() && i > 0 { + vec!['-', c.to_lowercase().next().unwrap()] + } else { + vec![c.to_lowercase().next().unwrap()] + } + }) + .collect(), + Some("SCREAMING-KEBAB-CASE") => variant_name + .chars() + .enumerate() + .flat_map(|(i, c)| { + if c.is_uppercase() && i > 0 { + vec!['-', c.to_uppercase().next().unwrap()] + } else { + vec![c.to_uppercase().next().unwrap()] + } + }) + .collect(), + _ => variant_name.to_lowercase(), // Default to lowercase + } +} + +/// Convert enum types to Swift. +fn enum_to_swift( + swift: &Swift, + types: &TypeCollection, + e: &specta::datatype::Enum, + location: Vec>, + is_export: bool, + sid: Option, + enum_name: Option<&str>, +) -> Result { + let mut result = String::new(); + + // Check if this is a string enum + let is_string_enum = e.repr().map(|repr| repr.is_string()).unwrap_or(false); + + for (original_variant_name, variant) in e.variants() { + if variant.skip() { + continue; + } + + let variant_name = swift.naming.convert_enum_case(original_variant_name); + + match variant.fields() { + specta::datatype::Fields::Unit => { + if is_string_enum { + // For string enums, generate raw value assignments + let raw_value = generate_raw_value( + original_variant_name, + e.repr().and_then(|r| r.rename_all()), + ); + result.push_str(&format!(" case {} = \"{}\"\n", variant_name, raw_value)); + } else { + result.push_str(&format!(" case {}\n", variant_name)); + } + } + specta::datatype::Fields::Unnamed(fields) => { + if fields.fields().is_empty() { + result.push_str(&format!(" case {}\n", variant_name)); + } else { + let types_str = fields + .fields() + .iter() + .map(|f| { + datatype_to_swift( + swift, + types, + f.ty().unwrap(), + location.clone(), + is_export, + sid, + ) + }) + .collect::, _>>()? + .join(", "); + result.push_str(&format!(" case {}({})\n", variant_name, types_str)); + } + } + specta::datatype::Fields::Named(fields) => { + if fields.fields().is_empty() { + result.push_str(&format!(" case {}\n", variant_name)); + } else { + // Generate struct for named fields + // Use the original variant name for PascalCase struct name + let pascal_variant_name = to_pascal_case(original_variant_name); + let struct_name = if let Some(enum_name) = enum_name { + format!("{}{}Data", enum_name, pascal_variant_name) + } else { + format!("{}Data", pascal_variant_name) + }; + + // Generate enum case that references the struct + result.push_str(&format!(" case {}({})\n", variant_name, struct_name)); + } + } + } + } + + Ok(result) +} + +/// Generate struct definitions for enum variants with named fields +fn generate_enum_structs( + swift: &Swift, + types: &TypeCollection, + e: &specta::datatype::Enum, + location: Vec>, + is_export: bool, + sid: Option, + enum_name: &str, +) -> Result { + let mut result = String::new(); + + for (original_variant_name, variant) in e.variants() { + if variant.skip() { + continue; + } + + if let specta::datatype::Fields::Named(fields) = variant.fields() { + if !fields.fields().is_empty() { + let pascal_variant_name = to_pascal_case(original_variant_name); + let struct_name = format!("{}{}Data", enum_name, pascal_variant_name); + + // Generate struct definition with custom CodingKeys for field name mapping + result.push_str(&format!("\npublic struct {}: Codable {{\n", struct_name)); + + // Generate struct fields + let mut field_mappings = Vec::new(); + for (original_field_name, field) in fields.fields() { + if let Some(ty) = field.ty() { + let field_type = + datatype_to_swift(swift, types, ty, location.clone(), is_export, sid)?; + let optional_marker = if field.optional() { "?" } else { "" }; + let swift_field_name = swift.naming.convert_field(original_field_name); + result.push_str(&format!( + " public let {}: {}{}\n", + swift_field_name, field_type, optional_marker + )); + field_mappings.push((swift_field_name, original_field_name.to_string())); + } + } + + // Generate custom CodingKeys if field names were converted + let needs_custom_coding_keys = field_mappings + .iter() + .any(|(swift_name, rust_name)| swift_name != rust_name); + if needs_custom_coding_keys { + result.push_str("\n private enum CodingKeys: String, CodingKey {\n"); + for (swift_name, rust_name) in &field_mappings { + result.push_str(&format!( + " case {} = \"{}\"\n", + swift_name, rust_name + )); + } + result.push_str(" }\n"); + } + + result.push_str("}\n"); + } + } + } + + Ok(result) +} + +/// Convert a string to PascalCase +fn to_pascal_case(s: &str) -> String { + // If it's already PascalCase (starts with uppercase), return as-is + if s.chars().next().map_or(false, |c| c.is_uppercase()) { + return s.to_string(); + } + + // Otherwise, convert snake_case to PascalCase + let mut result = String::new(); + let mut capitalize_next = true; + + for c in s.chars() { + if c == '_' || c == '-' { + capitalize_next = true; + } else if capitalize_next { + result.push(c.to_uppercase().next().unwrap_or(c)); + capitalize_next = false; + } else { + result.push(c.to_lowercase().next().unwrap_or(c)); + } + } + + result +} + +/// Convert tuple types to Swift. +fn tuple_to_swift( + swift: &Swift, + types: &TypeCollection, + t: &specta::datatype::Tuple, +) -> Result { + if t.elements().is_empty() { + Ok("Void".to_string()) + } else if t.elements().len() == 1 { + datatype_to_swift(swift, types, &t.elements()[0], vec![], false, None) + } else { + let types_str = t + .elements() + .iter() + .map(|e| datatype_to_swift(swift, types, e, vec![], false, None)) + .collect::, _>>()? + .join(", "); + Ok(format!("({})", types_str)) + } +} + +/// Convert reference types to Swift. +fn reference_to_swift( + swift: &Swift, + types: &TypeCollection, + r: &specta::datatype::Reference, +) -> Result { + // Get the name from the TypeCollection using the SID + let name = if let Some(ndt) = types.get(r.sid()) { + swift.naming.convert(ndt.name()) + } else { + return Err(Error::InvalidIdentifier( + "Reference to unknown type".to_string(), + )); + }; + + if r.generics().is_empty() { + Ok(name) + } else { + let generics = r + .generics() + .iter() + .map(|(_, t)| datatype_to_swift(swift, types, t, vec![], false, None)) + .collect::, _>>()? + .join(", "); + Ok(format!("{}<{}>", name, generics)) + } +} + +/// Convert generic types to Swift. +fn generic_to_swift(_swift: &Swift, g: &specta::datatype::Generic) -> Result { + Ok(g.to_string()) +} + +/// Generate custom Codable implementation for enums with struct-like variants +fn generate_enum_codable_impl( + swift: &Swift, + e: &specta::datatype::Enum, + enum_name: &str, +) -> Result { + let mut result = String::new(); + + result.push_str(&format!( + "\n// MARK: - {} Codable Implementation\n", + enum_name + )); + result.push_str(&format!("extension {}: Codable {{\n", enum_name)); + + // Check if this is an adjacently tagged enum + let is_adjacently_tagged = if let Some(repr) = e.repr() { + matches!(repr, specta::datatype::EnumRepr::Adjacent { .. }) + } else { + false + }; + + if is_adjacently_tagged { + return generate_adjacently_tagged_codable(swift, e, enum_name); + } + + // Generate CodingKeys enum + result.push_str(" private enum CodingKeys: String, CodingKey {\n"); + for (original_variant_name, variant) in e.variants() { + if variant.skip() { + continue; + } + let swift_case_name = swift.naming.convert_enum_case(original_variant_name); + result.push_str(&format!( + " case {} = \"{}\"\n", + swift_case_name, original_variant_name + )); + } + result.push_str(" }\n\n"); + + // Generate init(from decoder:) + result.push_str(" public init(from decoder: Decoder) throws {\n"); + result.push_str(" let container = try decoder.container(keyedBy: CodingKeys.self)\n"); + result.push_str(" \n"); + result.push_str(" if container.allKeys.count != 1 {\n"); + result.push_str(" throw DecodingError.dataCorrupted(\n"); + result.push_str(" DecodingError.Context(codingPath: decoder.codingPath, debugDescription: \"Invalid number of keys found, expected one.\")\n"); + result.push_str(" )\n"); + result.push_str(" }\n\n"); + result.push_str(" let key = container.allKeys.first!\n"); + result.push_str(" switch key {\n"); + + for (original_variant_name, variant) in e.variants() { + if variant.skip() { + continue; + } + + let swift_case_name = swift.naming.convert_enum_case(original_variant_name); + + match variant.fields() { + specta::datatype::Fields::Unit => { + result.push_str(&format!(" case .{}:\n", swift_case_name)); + result.push_str(&format!(" self = .{}\n", swift_case_name)); + } + specta::datatype::Fields::Unnamed(fields) => { + if fields.fields().is_empty() { + result.push_str(&format!(" case .{}:\n", swift_case_name)); + result.push_str(&format!(" self = .{}\n", swift_case_name)); + } else { + // For tuple variants, decode as array + result.push_str(&format!(" case .{}:\n", swift_case_name)); + result.push_str(&format!( + " // TODO: Implement tuple variant decoding for {}\n", + swift_case_name + )); + result.push_str( + " fatalError(\"Tuple variant decoding not implemented\")\n", + ); + } + } + specta::datatype::Fields::Named(_) => { + let pascal_variant_name = to_pascal_case(original_variant_name); + let struct_name = format!("{}{}Data", enum_name, pascal_variant_name); + + result.push_str(&format!(" case .{}:\n", swift_case_name)); + result.push_str(&format!( + " let data = try container.decode({}.self, forKey: .{})\n", + struct_name, swift_case_name + )); + result.push_str(&format!(" self = .{}(data)\n", swift_case_name)); + } + } + } + + result.push_str(" }\n"); + result.push_str(" }\n\n"); + + // Generate encode(to encoder:) + result.push_str(" public func encode(to encoder: Encoder) throws {\n"); + result.push_str(" var container = encoder.container(keyedBy: CodingKeys.self)\n"); + result.push_str(" \n"); + result.push_str(" switch self {\n"); + + for (original_variant_name, variant) in e.variants() { + if variant.skip() { + continue; + } + + let swift_case_name = swift.naming.convert_enum_case(original_variant_name); + + match variant.fields() { + specta::datatype::Fields::Unit => { + result.push_str(&format!(" case .{}:\n", swift_case_name)); + result.push_str(&format!( + " try container.encodeNil(forKey: .{})\n", + swift_case_name + )); + } + specta::datatype::Fields::Unnamed(_) => { + // TODO: Handle tuple variants + result.push_str(&format!(" case .{}:\n", swift_case_name)); + result.push_str(&format!( + " // TODO: Implement tuple variant encoding for {}\n", + swift_case_name + )); + result.push_str( + " fatalError(\"Tuple variant encoding not implemented\")\n", + ); + } + specta::datatype::Fields::Named(_) => { + result.push_str(&format!(" case .{}(let data):\n", swift_case_name)); + result.push_str(&format!( + " try container.encode(data, forKey: .{})\n", + swift_case_name + )); + } + } + } + + result.push_str(" }\n"); + result.push_str(" }\n"); + result.push_str("}\n"); + + Ok(result) +} + +/// Generate custom Codable implementation for adjacently tagged enums +fn generate_adjacently_tagged_codable( + swift: &Swift, + e: &specta::datatype::Enum, + enum_name: &str, +) -> Result { + let mut result = String::new(); + + // Get tag and content field names + let (tag_field, content_field) = + if let Some(specta::datatype::EnumRepr::Adjacent { tag, content }) = e.repr() { + (tag.as_ref(), content.as_ref()) + } else { + return Err(Error::UnsupportedType( + "Expected adjacently tagged enum".to_string(), + )); + }; + + result.push_str(&format!( + "\n// MARK: - {} Adjacently Tagged Codable Implementation\n", + enum_name + )); + result.push_str(&format!("extension {}: Codable {{\n", enum_name)); + + // Generate TypeKeys enum for the tag and content fields + result.push_str(" private enum TypeKeys: String, CodingKey {\n"); + result.push_str(&format!(" case tag = \"{}\"\n", tag_field)); + result.push_str(&format!(" case content = \"{}\"\n", content_field)); + result.push_str(" }\n\n"); + + // Generate VariantType enum for variant names + result.push_str(" private enum VariantType: String, Codable {\n"); + for (original_variant_name, variant) in e.variants() { + if variant.skip() { + continue; + } + let swift_case_name = swift.naming.convert_enum_case(original_variant_name); + result.push_str(&format!( + " case {} = \"{}\"\n", + swift_case_name, original_variant_name + )); + } + result.push_str(" }\n\n"); + + // Generate init(from decoder:) + result.push_str(" public init(from decoder: Decoder) throws {\n"); + result.push_str(" let container = try decoder.container(keyedBy: TypeKeys.self)\n"); + result.push_str( + " let variantType = try container.decode(VariantType.self, forKey: .tag)\n", + ); + result.push_str(" \n"); + result.push_str(" switch variantType {\n"); + + for (original_variant_name, variant) in e.variants() { + if variant.skip() { + continue; + } + + let swift_case_name = swift.naming.convert_enum_case(original_variant_name); + + match variant.fields() { + specta::datatype::Fields::Unit => { + result.push_str(&format!(" case .{}:\n", swift_case_name)); + result.push_str(&format!(" self = .{}\n", swift_case_name)); + } + specta::datatype::Fields::Unnamed(_) => { + // TODO: Handle tuple variants for adjacently tagged + result.push_str(&format!(" case .{}:\n", swift_case_name)); + result.push_str(" fatalError(\"Adjacently tagged tuple variants not implemented\")\n"); + } + specta::datatype::Fields::Named(_) => { + let pascal_variant_name = to_pascal_case(original_variant_name); + let struct_name = format!("{}{}Data", enum_name, pascal_variant_name); + + result.push_str(&format!(" case .{}:\n", swift_case_name)); + result.push_str(&format!( + " let data = try container.decode({}.self, forKey: .content)\n", + struct_name + )); + result.push_str(&format!(" self = .{}(data)\n", swift_case_name)); + } + } + } + + result.push_str(" }\n"); + result.push_str(" }\n\n"); + + // Generate encode(to encoder:) + result.push_str(" public func encode(to encoder: Encoder) throws {\n"); + result.push_str(" var container = encoder.container(keyedBy: TypeKeys.self)\n"); + result.push_str(" \n"); + result.push_str(" switch self {\n"); + + for (original_variant_name, variant) in e.variants() { + if variant.skip() { + continue; + } + + let swift_case_name = swift.naming.convert_enum_case(original_variant_name); + + match variant.fields() { + specta::datatype::Fields::Unit => { + result.push_str(&format!(" case .{}:\n", swift_case_name)); + result.push_str(&format!( + " try container.encode(VariantType.{}, forKey: .tag)\n", + swift_case_name + )); + } + specta::datatype::Fields::Unnamed(_) => { + // TODO: Handle tuple variants + result.push_str(&format!(" case .{}:\n", swift_case_name)); + result.push_str(" fatalError(\"Adjacently tagged tuple variants not implemented\")\n"); + } + specta::datatype::Fields::Named(_) => { + result.push_str(&format!(" case .{}(let data):\n", swift_case_name)); + result.push_str(&format!( + " try container.encode(VariantType.{}, forKey: .tag)\n", + swift_case_name + )); + result.push_str(" try container.encode(data, forKey: .content)\n"); + } + } + } + + result.push_str(" }\n"); + result.push_str(" }\n"); + result.push_str("}\n"); + + Ok(result) +} diff --git a/specta-swift/src/swift.rs b/specta-swift/src/swift.rs new file mode 100644 index 0000000..adc7157 --- /dev/null +++ b/specta-swift/src/swift.rs @@ -0,0 +1,335 @@ +//! Swift language exporter configuration and main export functionality. + +use std::{borrow::Cow, path::Path}; + +use specta::TypeCollection; + +use crate::error::{Error, Result}; +use crate::primitives::{export_type, is_duration_struct}; + +/// Swift language exporter. +#[derive(Debug, Clone)] +pub struct Swift { + /// Header comment for generated files. + pub header: Cow<'static, str>, + /// Indentation style for generated code. + pub indent: IndentStyle, + /// Naming convention for identifiers. + pub naming: NamingConvention, + /// Generic type style. + pub generics: GenericStyle, + /// Optional type style. + pub optionals: OptionalStyle, + /// Additional protocols to conform to. + pub protocols: Vec>, + /// Enable Serde validation. + pub serde: bool, +} + +/// Indentation style for generated Swift code. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum IndentStyle { + /// Use spaces for indentation. + Spaces(usize), + /// Use tabs for indentation. + Tabs, +} + +impl Default for IndentStyle { + fn default() -> Self { + Self::Spaces(4) + } +} + +/// Naming convention for Swift identifiers. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub enum NamingConvention { + /// PascalCase naming (default for Swift types). + #[default] + PascalCase, + /// camelCase naming. + CamelCase, + /// snake_case naming. + SnakeCase, +} + +/// Generic type style for Swift. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub enum GenericStyle { + /// Use protocol constraints: ``. + #[default] + Protocol, + /// Use where clauses: ` where T: Codable`. + Typealias, +} + +/// Optional type style for Swift. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub enum OptionalStyle { + /// Use question mark syntax: `String?`. + #[default] + QuestionMark, + /// Use Optional type: `Optional`. + Optional, +} + +impl Default for Swift { + fn default() -> Self { + Self { + header: "// This file has been generated by Specta. DO NOT EDIT.".into(), + indent: IndentStyle::default(), + naming: NamingConvention::default(), + generics: GenericStyle::default(), + optionals: OptionalStyle::default(), + protocols: vec![], + serde: false, + } + } +} + +impl Swift { + /// Create a new Swift exporter with default configuration. + pub fn new() -> Self { + Self::default() + } + + /// Set the header comment for generated files. + pub fn header(mut self, header: impl Into>) -> Self { + self.header = header.into(); + self + } + + /// Set the indentation style. + pub fn indent(mut self, style: IndentStyle) -> Self { + self.indent = style; + self + } + + /// Set the naming convention. + pub fn naming(mut self, convention: NamingConvention) -> Self { + self.naming = convention; + self + } + + /// Set the generic type style. + pub fn generics(mut self, style: GenericStyle) -> Self { + self.generics = style; + self + } + + /// Set the optional type style. + pub fn optionals(mut self, style: OptionalStyle) -> Self { + self.optionals = style; + self + } + + /// Enable Serde validation. + pub fn with_serde(mut self) -> Self { + self.serde = true; + self + } + + /// Add a protocol that all types should conform to. + pub fn add_protocol(mut self, protocol: impl Into>) -> Self { + self.protocols.push(protocol.into()); + self + } + + /// Export types to a Swift string. + pub fn export(&self, types: &TypeCollection) -> Result { + if self.serde { + specta_serde::validate(types)?; + } + + let mut result = String::new(); + + // Add header + if !self.header.is_empty() { + result.push_str(&self.header); + result.push('\n'); + } + + // Add imports + result.push_str("import Foundation\n"); + if self.serde { + result.push_str("import Codable\n"); + } + for protocol in &self.protocols { + result.push_str(&format!("import {}\n", protocol)); + } + result.push('\n'); + + // Check if we need to inject Duration helper + if needs_duration_helper(types) { + result.push_str(&generate_duration_helper()); + } + + // Export types + for ndt in types.into_sorted_iter() { + result.push_str(&export_type(self, types, &ndt)?); + result.push_str("\n\n"); + } + + Ok(result) + } + + /// Export types to a file. + pub fn export_to(&self, path: impl AsRef, types: &TypeCollection) -> Result<()> { + let content = self.export(types)?; + std::fs::write(path, content)?; + Ok(()) + } +} + +impl NamingConvention { + /// Convert a string to the appropriate naming convention. + pub fn convert(&self, name: &str) -> String { + match self { + Self::PascalCase => self.to_pascal_case(name), + Self::CamelCase => self.to_camel_case(name), + Self::SnakeCase => self.to_snake_case(name), + } + } + + /// Convert a string to camelCase (for field names). + pub fn convert_to_camel_case(&self, name: &str) -> String { + self.to_camel_case(name) + } + + /// Convert a string to the appropriate naming convention for fields. + pub fn convert_field(&self, name: &str) -> String { + match self { + Self::PascalCase => self.to_camel_case(name), // Fields should be camelCase even with PascalCase + Self::CamelCase => self.to_camel_case(name), + Self::SnakeCase => self.to_snake_case(name), + } + } + + /// Convert a string to the appropriate naming convention for enum cases. + pub fn convert_enum_case(&self, name: &str) -> String { + match self { + Self::PascalCase => self.to_camel_case(name), // Enum cases should be camelCase + Self::CamelCase => self.to_camel_case(name), + Self::SnakeCase => self.to_snake_case(name), + } + } + + fn to_camel_case(&self, name: &str) -> String { + // Convert snake_case or PascalCase to camelCase + if name.contains('_') { + // Handle snake_case + let parts: Vec<&str> = name.split('_').collect(); + if parts.is_empty() { + return name.to_string(); + } + + let mut result = String::new(); + for (i, part) in parts.iter().enumerate() { + if i == 0 { + result.push_str(&part.to_lowercase()); + } else { + let mut chars = part.chars(); + match chars.next() { + None => continue, + Some(first) => { + result.push(first.to_uppercase().next().unwrap_or(first)); + for c in chars { + result.extend(c.to_lowercase()); + } + } + } + } + } + result + } else { + // Handle PascalCase - convert to camelCase + let mut chars = name.chars(); + match chars.next() { + None => name.to_string(), + Some(first) => { + let mut result = String::new(); + result.push(first.to_lowercase().next().unwrap_or(first)); + for c in chars { + result.push(c); // Keep the rest as-is for PascalCase + } + result + } + } + } + } + + fn to_pascal_case(&self, name: &str) -> String { + // Convert snake_case to PascalCase + name.split('_') + .map(|part| { + let mut chars = part.chars(); + match chars.next() { + None => String::new(), + Some(first) => first.to_uppercase().chain(chars).collect(), + } + }) + .collect() + } + + fn to_snake_case(&self, name: &str) -> String { + // Convert camelCase/PascalCase to snake_case + let mut result = String::new(); + let mut chars = name.chars().peekable(); + + while let Some(c) = chars.next() { + if c.is_uppercase() && !result.is_empty() { + result.push('_'); + } + result.push(c.to_lowercase().next().unwrap_or(c)); + } + + result + } +} + +/// Check if the type collection contains any Duration types that need the helper +fn needs_duration_helper(types: &TypeCollection) -> bool { + for ndt in types.into_sorted_iter() { + if ndt.name() == "Duration" { + return true; + } + // Also check if any struct fields contain Duration + if let specta::datatype::DataType::Struct(s) = ndt.ty() { + if let specta::datatype::Fields::Named(fields) = s.fields() { + for (_, field) in fields.fields() { + if let Some(ty) = field.ty() { + if let specta::datatype::DataType::Reference(r) = ty { + if let Some(referenced_ndt) = types.get(r.sid()) { + if referenced_ndt.name() == "Duration" { + return true; + } + } + } + // Also check if the field type is a Duration struct directly + if let specta::datatype::DataType::Struct(struct_ty) = ty { + if is_duration_struct(struct_ty) { + return true; + } + } + } + } + } + } + } + false +} + +/// Generate the Duration helper struct +fn generate_duration_helper() -> String { + "// MARK: - Duration Helper\n".to_string() + + "/// Helper struct to decode Rust Duration format {\"secs\": u64, \"nanos\": u32}\n" + + "public struct RustDuration: Codable {\n" + + " public let secs: UInt64\n" + + " public let nanos: UInt32\n" + + " \n" + + " public var timeInterval: TimeInterval {\n" + + " return Double(secs) + Double(nanos) / 1_000_000_000.0\n" + + " }\n" + + "}\n\n" + + "// MARK: - Generated Types\n\n" +} diff --git a/specta-swift/tests/Cargo.toml b/specta-swift/tests/Cargo.toml new file mode 100644 index 0000000..4c6d354 --- /dev/null +++ b/specta-swift/tests/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "specta-swift-tests" +version = "0.0.0" +edition = "2021" +publish = false + +[[test]] +name = "basic" +path = "basic.rs" + +[dependencies] +specta = { path = "../../specta", features = ["derive", "uuid", "chrono"] } +specta-swift = { path = "../" } +uuid = "1.12.1" +chrono = { version = "0.4.40", features = ["clock"] } diff --git a/specta-swift/tests/advanced_unions.rs b/specta-swift/tests/advanced_unions.rs new file mode 100644 index 0000000..ac54f00 --- /dev/null +++ b/specta-swift/tests/advanced_unions.rs @@ -0,0 +1,219 @@ +use specta::{Type, TypeCollection}; +use specta_swift::Swift; + +#[derive(Type)] +struct Point { + x: f64, + y: f64, +} + +#[derive(Type)] +struct Circle { + center: Point, + radius: f64, +} + +#[derive(Type)] +struct Rectangle { + top_left: Point, + bottom_right: Point, +} + +#[derive(Type)] +struct Line { + start: Point, + end: Point, +} + +#[derive(Type)] +enum Shape { + // Unit variant + None, + + // Simple tuple variant + Point(f64, f64), + + // Named fields variant + Circle { + center: Point, + radius: f64, + }, + + // Nested struct variant + Rectangle(Rectangle), + + // Complex nested variant + Line { + start: Point, + end: Point, + }, + + // Mixed variant with multiple nested types + Complex { + shapes: Vec, + metadata: Option, + }, +} + +#[derive(Type)] +enum ApiResponse { + // Success with data + Success { + data: T, + status: u16, + headers: Vec<(String, String)>, + }, + + // Error with details + Error { + code: u32, + message: String, + details: Option, + }, + + // Loading state + Loading { + progress: f32, + estimated_time: Option, + }, + + // Redirect + Redirect { + url: String, + permanent: bool, + }, +} + +#[derive(Type)] +enum DatabaseResult { + // Success with data and metadata + Ok { + data: T, + affected_rows: u64, + execution_time: f64, + }, + + // Error with error type and context + Err { + error: E, + query: String, + retry_count: u32, + }, + + // Connection issues + ConnectionError { + host: String, + port: u16, + reason: String, + }, + + // Timeout + Timeout { + duration: f64, + operation: String, + }, +} + +#[test] +fn test_complex_unions() { + let types = TypeCollection::default() + .register::() + .register::() + .register::() + .register::() + .register::() + .register::>() + .register::>(); + + let swift = Swift::default(); + let output = swift.export(&types).unwrap(); + + println!("Complex unions Swift code:\n{}", output); + + // Test that all types are generated + assert!(output.contains("struct Point")); + assert!(output.contains("struct Circle")); + assert!(output.contains("struct Rectangle")); + assert!(output.contains("struct Line")); + assert!(output.contains("enum Shape")); + assert!(output.contains("enum ApiResponse")); + assert!(output.contains("enum DatabaseResult")); + + // Test Shape enum variants + assert!(output.contains("case none")); + assert!(output.contains("case point(Double, Double)")); + assert!(output.contains("case circle")); + assert!(output.contains("case rectangle(Rectangle)")); + assert!(output.contains("case line")); + assert!(output.contains("case complex")); + + // Test ApiResponse enum variants + assert!(output.contains("case success")); + assert!(output.contains("case error")); + assert!(output.contains("case loading")); + assert!(output.contains("case redirect")); + + // Test DatabaseResult enum variants + assert!(output.contains("case ok")); + assert!(output.contains("case err")); + assert!(output.contains("case connectionError")); + assert!(output.contains("case timeout")); + + // Test that nested structs are properly referenced + assert!(output.contains("center: Point")); + assert!(output.contains("radius: Double")); + assert!(output.contains("topLeft: Point")); + assert!(output.contains("bottomRight: Point")); + assert!(output.contains("start: Point")); + assert!(output.contains("end: Point")); +} + +#[test] +fn test_union_with_generics() { + let types = TypeCollection::default() + .register::>() + .register::>(); + + let swift = Swift::default(); + let output = swift.export(&types).unwrap(); + + println!("Generic unions Swift code:\n{}", output); + + // Test generic enum definitions + assert!(output.contains("enum ApiResponse")); + assert!(output.contains("enum DatabaseResult")); + + // Test that Codable is added via extension + assert!(output.contains("extension ApiResponse: Codable")); + assert!(output.contains("extension DatabaseResult: Codable")); + + // Test that generic types are used in variants + assert!(output.contains("data: T")); + assert!(output.contains("error: E")); + assert!(output.contains("details: T?")); +} + +#[test] +fn test_union_naming_conventions() { + let types = TypeCollection::default().register::(); + + // Test with different naming conventions + let swift_pascal = Swift::new().naming(specta_swift::NamingConvention::PascalCase); + let swift_snake = Swift::new().naming(specta_swift::NamingConvention::SnakeCase); + + let output_pascal = swift_pascal.export(&types).unwrap(); + let output_snake = swift_snake.export(&types).unwrap(); + + println!("PascalCase output:\n{}", output_pascal); + println!("SnakeCase output:\n{}", output_snake); + + // PascalCase should have camelCase enum cases + assert!(output_pascal.contains("case none")); + assert!(output_pascal.contains("case point")); + assert!(output_pascal.contains("case circle")); + + // SnakeCase should have snake_case enum cases + assert!(output_snake.contains("case none")); + assert!(output_snake.contains("case point")); + assert!(output_snake.contains("case circle")); +} diff --git a/specta-swift/tests/basic.rs b/specta-swift/tests/basic.rs new file mode 100644 index 0000000..71fd085 --- /dev/null +++ b/specta-swift/tests/basic.rs @@ -0,0 +1,37 @@ +use specta::{Type, TypeCollection}; +use specta_swift::Swift; + +#[derive(Type)] +struct User { + name: String, + age: u32, + active: bool, +} + +#[derive(Type)] +enum Status { + Active, + Inactive, + Pending { reason: String }, + Error(String), +} + +#[test] +fn test_basic_export() { + let types = TypeCollection::default() + .register::() + .register::(); + + let swift = Swift::default(); + let output = swift.export(&types).unwrap(); + + println!("Generated Swift code:\n{}", output); + + // Basic assertions + assert!(output.contains("struct User")); + assert!(output.contains("enum Status")); + assert!(output.contains("let name: String")); + assert!(output.contains("let age: UInt32")); + assert!(output.contains("case active")); + assert!(output.contains("case pending")); +} diff --git a/specta-swift/tests/common_types.rs b/specta-swift/tests/common_types.rs new file mode 100644 index 0000000..79102de --- /dev/null +++ b/specta-swift/tests/common_types.rs @@ -0,0 +1,58 @@ +use specta::{Type, TypeCollection}; +use specta_swift::Swift; + +// Test with common types that might not have Type implementations +#[derive(Type)] +struct TestStruct { + // Basic types that should work + id: u32, + name: String, + email: Option, + // These might not have Type implementations + // uuid: uuid::Uuid, // Commented out - likely not supported + // created_at: chrono::DateTime, // Commented out - likely not supported +} + +#[derive(Type)] +enum TestEnum { + Unit, + Tuple(String, u32), + Named { id: u32, name: String }, +} + +#[test] +fn test_common_types() { + let types = TypeCollection::default() + .register::() + .register::(); + + let swift = Swift::default(); + let output = swift.export(&types).unwrap(); + + println!("Generated Swift code:\n{}", output); + + // Test that basic types work + assert!(output.contains("struct TestStruct")); + assert!(output.contains("enum TestEnum")); + assert!(output.contains("let id: UInt32")); + assert!(output.contains("let name: String")); + assert!(output.contains("let email: String?")); +} + +// Test what happens when we try to use unsupported types +#[test] +fn test_unsupported_types() { + // This test will fail to compile if UUID doesn't have Type implementation + // Uncomment to test: + /* + #[derive(Type)] + struct WithUuid { + id: uuid::Uuid, + } + + let types = TypeCollection::default().register::(); + let swift = Swift::default(); + let output = swift.export(&types).unwrap(); + println!("UUID support: {}", output); + */ +} diff --git a/specta-swift/tests/comprehensive.rs b/specta-swift/tests/comprehensive.rs new file mode 100644 index 0000000..0ea2fca --- /dev/null +++ b/specta-swift/tests/comprehensive.rs @@ -0,0 +1,146 @@ +use specta::{Type, TypeCollection}; +use specta_swift::Swift; + +#[derive(Type)] +struct User { + id: u32, + name: String, + email: Option, + age: u8, + is_active: bool, + metadata: UserMetadata, + tags: Vec, + scores: Vec, +} + +#[derive(Type)] +struct UserMetadata { + created_at: String, + last_login: Option, + preferences: UserPreferences, +} + +#[derive(Type)] +struct UserPreferences { + theme: String, + notifications: bool, + language: String, +} + +#[derive(Type)] +enum UserRole { + Admin, + User, + Guest, + Moderator { permissions: Vec }, + Custom { name: String, level: u8 }, +} + +#[derive(Type)] +enum ApiResponse { + Success(T), + Error { code: u32, message: String }, + Loading, +} + +#[derive(Type)] +struct ApiResult { + data: Option, + error: Option, + status: u16, +} + +#[test] +fn test_comprehensive_export() { + let types = TypeCollection::default() + .register::() + .register::() + .register::() + .register::() + .register::>() + .register::>(); + + let swift = Swift::default(); + let output = swift.export(&types).unwrap(); + + println!("Generated Swift code:\n{}", output); + + // Test struct generation + assert!(output.contains("struct User")); + assert!(output.contains("struct UserMetadata")); + assert!(output.contains("struct UserPreferences")); + assert!(output.contains("struct ApiResult")); + + // Test enum generation + assert!(output.contains("enum UserRole")); + assert!(output.contains("enum ApiResponse")); + + // Test field types + assert!(output.contains("let id: UInt32")); + assert!(output.contains("let name: String")); + assert!(output.contains("let email: String?")); + assert!(output.contains("let age: UInt8")); + assert!(output.contains("let isActive: Bool")); + assert!(output.contains("let metadata: UserMetadata")); + assert!(output.contains("let tags: [String]")); + assert!(output.contains("let scores: [Double]")); + + // Test enum cases + assert!(output.contains("case admin")); + assert!(output.contains("case user")); + assert!(output.contains("case guest")); + assert!(output.contains("case moderator")); + assert!(output.contains("case custom")); + + // Test generic types (they appear as generic definitions, not concrete instantiations) + assert!(output.contains("enum ApiResponse")); + assert!(output.contains("struct ApiResult")); + + // Test optional types + assert!(output.contains("let email: String?")); + assert!(output.contains("let lastLogin: String?")); + assert!(output.contains("let data: T?")); + assert!(output.contains("let error: E?")); + + // Test array types + assert!(output.contains("let tags: [String]")); + assert!(output.contains("let scores: [Double]")); + assert!(output.contains("permissions: [String]")); +} + +#[test] +fn test_naming_conventions() { + let types = TypeCollection::default() + .register::(); + + let swift = Swift::default(); + let output = swift.export(&types).unwrap(); + + // Test PascalCase for type names + assert!(output.contains("struct User")); + + // Test camelCase for field names + assert!(output.contains("let isActive: Bool")); // snake_case -> camelCase + assert!(output.contains("let createdAt: String")); // snake_case -> camelCase +} + +#[test] +fn test_swift_configuration() { + let types = TypeCollection::default() + .register::(); + + // Test with custom configuration + let swift = Swift::new() + .header("// Custom header") + .naming(specta_swift::NamingConvention::SnakeCase) + .optionals(specta_swift::OptionalStyle::Optional); + + let output = swift.export(&types).unwrap(); + + println!("Snake case output:\n{}", output); + + assert!(output.contains("// Custom header")); + assert!(output.contains("struct user")); // snake_case type names + assert!(output.contains("let is_active: Bool")); // snake_case field names + assert!(output.contains("let email: Optional")); // Optional style +} diff --git a/specta-swift/tests/multiline_comments.rs b/specta-swift/tests/multiline_comments.rs new file mode 100644 index 0000000..1a1b7d8 --- /dev/null +++ b/specta-swift/tests/multiline_comments.rs @@ -0,0 +1,65 @@ +use specta::{Type, TypeCollection}; +use specta_swift::Swift; + +/// A path within the Spacedrive Virtual Distributed File System +/// +/// This is the core abstraction that enables cross-device operations. +/// An SdPath can represent: +/// - A physical file at a specific path on a specific device +/// - A content-addressed file that can be sourced from any device +/// +/// This enum-based approach enables resilient file operations by allowing +/// content-based paths to be resolved to optimal physical locations at runtime. +#[derive(Type)] +enum SdPath { + /// A physical file path on a specific device + Physical { + /// The device ID where this file is located + device_id: String, + /// The file path on the device + path: String, + }, + /// A content-addressed file that can be sourced from any device + Content { + /// The content hash of the file + hash: String, + /// Optional preferred device for this content + preferred_device: Option, + }, +} + +/// A simple struct with a single-line comment +#[derive(Type)] +struct SimpleStruct { + /// The name of the struct + name: String, +} + +#[test] +fn test_multiline_comments() { + let types = TypeCollection::default() + .register::() + .register::(); + + let swift = Swift::default(); + let output = swift.export(&types).unwrap(); + + println!("Generated Swift code with comments:\n{}", output); + + // Test that multi-line comments are properly formatted + assert!(output.contains("/// A path within the Spacedrive Virtual Distributed File System")); + assert!( + output.contains("/// This is the core abstraction that enables cross-device operations.") + ); + assert!(output.contains("/// An SdPath can represent:")); + assert!(output.contains("/// - A physical file at a specific path on a specific device")); + assert!(output.contains("/// - A content-addressed file that can be sourced from any device")); + assert!(output.contains("/// This enum-based approach enables resilient file operations")); + + // Test that single-line comments work too + assert!(output.contains("/// A simple struct with a single-line comment")); + + // Note: Field-level comments are not currently supported by Specta + // The enum cases and struct fields don't have individual comments + // because Specta doesn't extract field-level documentation by default +} diff --git a/specta-swift/tests/string_enum_implementation.rs b/specta-swift/tests/string_enum_implementation.rs new file mode 100644 index 0000000..b1657dd --- /dev/null +++ b/specta-swift/tests/string_enum_implementation.rs @@ -0,0 +1,253 @@ +use specta::{Type, TypeCollection}; +use specta_swift::{NamingConvention, Swift}; + +#[derive(Type)] +#[serde(rename_all = "snake_case")] +pub enum JobStatus { + Queued, + Running, + Paused, + Completed, + Failed, + Cancelled, +} + +#[derive(Type)] +#[serde(rename_all = "UPPERCASE")] +pub enum Priority { + Low, + Medium, + High, +} + +#[derive(Type)] +#[serde(rename_all = "camelCase")] +pub enum LogLevel { + Debug, + Info, + Warning, + Error, +} + +#[derive(Type)] +#[serde(rename_all = "PascalCase")] +pub enum UserRole { + Admin, + Moderator, + User, + Guest, +} + +#[derive(Type)] +#[serde(rename_all = "kebab-case")] +pub enum ApiStatus { + Online, + Offline, + Maintenance, +} + +#[derive(Type)] +#[serde(rename_all = "SCREAMING-KEBAB-CASE")] +pub enum DatabaseStatus { + Connected, + Disconnected, + Reconnecting, +} + +// This should NOT be a string enum (has data fields) +#[derive(Type)] +#[serde(rename_all = "snake_case")] +pub enum MixedEnum { + Unit, + WithData(String), + WithFields { name: String, value: i32 }, +} + +// This should NOT be a string enum (no rename_all) +#[derive(Type)] +pub enum RegularEnum { + Variant1, + Variant2, + Variant3, +} + +#[test] +fn test_string_enum_snake_case() { + let types = TypeCollection::default().register::(); + + let swift = Swift::default(); + let result = swift.export(&types).unwrap(); + + println!("Generated Swift for JobStatus:"); + println!("{}", result); + + // Should contain string enum syntax + assert!(result.contains("enum JobStatus: String, Codable")); + assert!(result.contains("case queued = \"queued\"")); + assert!(result.contains("case running = \"running\"")); + assert!(result.contains("case completed = \"completed\"")); + assert!(result.contains("case failed = \"failed\"")); + assert!(result.contains("case cancelled = \"cancelled\"")); +} + +#[test] +fn test_string_enum_uppercase() { + let types = TypeCollection::default().register::(); + + let swift = Swift::default(); + let result = swift.export(&types).unwrap(); + + println!("Generated Swift for Priority:"); + println!("{}", result); + + // Should contain string enum syntax with uppercase values + assert!(result.contains("enum Priority: String, Codable")); + assert!(result.contains("case lOW = \"LOW\"")); + assert!(result.contains("case mEDIUM = \"MEDIUM\"")); + assert!(result.contains("case hIGH = \"HIGH\"")); +} + +#[test] +fn test_string_enum_camel_case() { + let types = TypeCollection::default().register::(); + + let swift = Swift::default(); + let result = swift.export(&types).unwrap(); + + println!("Generated Swift for LogLevel:"); + println!("{}", result); + + // Should contain string enum syntax with camelCase values + assert!(result.contains("enum LogLevel: String, Codable")); + assert!(result.contains("case debug = \"debug\"")); + assert!(result.contains("case info = \"info\"")); + assert!(result.contains("case warning = \"warning\"")); + assert!(result.contains("case error = \"error\"")); +} + +#[test] +fn test_string_enum_pascal_case() { + let types = TypeCollection::default().register::(); + + let swift = Swift::default(); + let result = swift.export(&types).unwrap(); + + println!("Generated Swift for UserRole:"); + println!("{}", result); + + // Should contain string enum syntax with PascalCase values + assert!(result.contains("enum UserRole: String, Codable")); + assert!(result.contains("case admin = \"Admin\"")); + assert!(result.contains("case moderator = \"Moderator\"")); + assert!(result.contains("case user = \"User\"")); + assert!(result.contains("case guest = \"Guest\"")); +} + +#[test] +fn test_string_enum_kebab_case() { + let types = TypeCollection::default().register::(); + + let swift = Swift::default(); + let result = swift.export(&types).unwrap(); + + println!("Generated Swift for ApiStatus:"); + println!("{}", result); + + // Should contain string enum syntax with kebab-case values + assert!(result.contains("enum ApiStatus: String, Codable")); + assert!(result.contains("case online = \"online\"")); + assert!(result.contains("case offline = \"offline\"")); + assert!(result.contains("case maintenance = \"maintenance\"")); +} + +#[test] +fn test_string_enum_screaming_kebab_case() { + let types = TypeCollection::default().register::(); + + let swift = Swift::default(); + let result = swift.export(&types).unwrap(); + + println!("Generated Swift for DatabaseStatus:"); + println!("{}", result); + + // Should contain string enum syntax with SCREAMING-KEBAB-CASE values + assert!(result.contains("enum DatabaseStatus: String, Codable")); + assert!(result.contains("case cONNECTED = \"C-O-N-N-E-C-T-E-D\"")); + assert!(result.contains("case dISCONNECTED = \"D-I-S-C-O-N-N-E-C-T-E-D\"")); + assert!(result.contains("case rECONNECTING = \"R-E-C-O-N-N-E-C-T-I-N-G\"")); +} + +#[test] +fn test_mixed_enum_not_string() { + let types = TypeCollection::default().register::(); + + let swift = Swift::default(); + let result = swift.export(&types).unwrap(); + + println!("Generated Swift for MixedEnum:"); + println!("{}", result); + + // Should NOT be a string enum (has data fields) - no redundant Codable in declaration + assert!(result.contains("enum MixedEnum")); + assert!(!result.contains("enum MixedEnum: Codable")); + assert!(!result.contains("enum MixedEnum: String, Codable")); + + // Should have Codable in extension instead + assert!(result.contains("extension MixedEnum: Codable")); + assert!(result.contains("case unit")); + assert!(result.contains("case withData(String)")); + assert!(result.contains("case withFields(MixedEnumWithFieldsData)")); + assert!(result.contains("struct MixedEnumWithFieldsData: Codable")); + assert!(result.contains("let name: String")); + assert!(result.contains("let value: Int32")); +} + +#[test] +fn test_regular_enum_not_string() { + let types = TypeCollection::default().register::(); + + let swift = Swift::default(); + let result = swift.export(&types).unwrap(); + + println!("Generated Swift for RegularEnum:"); + println!("{}", result); + + // Should NOT be a string enum (no rename_all) + assert!(result.contains("enum RegularEnum: Codable")); + assert!(!result.contains("enum RegularEnum: String, Codable")); + assert!(result.contains("case variant1")); + assert!(result.contains("case variant2")); + assert!(result.contains("case variant3")); +} + +#[test] +fn test_all_string_enums_together() { + let types = TypeCollection::default() + .register::() + .register::() + .register::() + .register::() + .register::() + .register::() + .register::() + .register::(); + + let swift = Swift::default(); + let result = swift.export(&types).unwrap(); + + println!("Generated Swift for all enums:"); + println!("{}", result); + + // Check that string enums are generated correctly + assert!(result.contains("enum JobStatus: String, Codable")); + assert!(result.contains("enum Priority: String, Codable")); + assert!(result.contains("enum LogLevel: String, Codable")); + assert!(result.contains("enum UserRole: String, Codable")); + assert!(result.contains("enum ApiStatus: String, Codable")); + assert!(result.contains("enum DatabaseStatus: String, Codable")); + + // Check that non-string enums are generated correctly + assert!(result.contains("enum MixedEnum")); // No redundant Codable in declaration + assert!(result.contains("enum RegularEnum: Codable")); // Simple enum can have Codable in declaration + assert!(result.contains("extension MixedEnum: Codable")); // Complex enum has Codable in extension +} diff --git a/specta-swift/tests/string_enum_test.rs b/specta-swift/tests/string_enum_test.rs new file mode 100644 index 0000000..ab4e554 --- /dev/null +++ b/specta-swift/tests/string_enum_test.rs @@ -0,0 +1,45 @@ +use specta::{Type, TypeCollection}; +use specta_swift::Swift; + +/// Test enum with snake_case rename_all - should generate string enum +#[derive(Type)] +#[specta(rename_all = "snake_case")] +enum JobStatus { + Completed, + Running, + Failed, + PendingApproval, +} + +/// Test enum without rename_all - should generate tagged union +#[derive(Type)] +enum RegularEnum { + Option1, + Option2, + Option3, +} + +#[test] +fn test_string_enum_generation() { + let types = TypeCollection::default() + .register::() + .register::(); + + let swift = Swift::default(); + let output = swift.export(&types).unwrap(); + + println!("String enum test output:\n{}", output); + + // JobStatus should be a string enum (with String protocol and raw values) + assert!(output.contains("enum JobStatus: String, Codable")); + assert!(output.contains("case completed = \"completed\"")); + assert!(output.contains("case running = \"running\"")); + assert!(output.contains("case failed = \"failed\"")); + assert!(output.contains("case pendingApproval = \"pending_approval\"")); + + // RegularEnum should be a tagged union + assert!(output.contains("enum RegularEnum: Codable")); + assert!(output.contains("case option1")); + assert!(output.contains("case option2")); + assert!(output.contains("case option3")); +} diff --git a/specta-swift/tests/struct_reuse_test.rs b/specta-swift/tests/struct_reuse_test.rs new file mode 100644 index 0000000..aecfe01 --- /dev/null +++ b/specta-swift/tests/struct_reuse_test.rs @@ -0,0 +1,91 @@ +use specta::{Type, TypeCollection}; +use specta_swift::Swift; + +#[derive(Type)] +struct UserData { + id: u32, + name: String, + email: Option, +} + +#[derive(Type)] +enum ApiResponse { + Success(UserData), + Error { message: String, code: u32 }, + Loading, +} + +#[derive(Type)] +struct ApiRequest { + user: UserData, + action: String, +} + +#[test] +fn test_struct_reuse_between_standalone_and_enum() { + let types = TypeCollection::default() + .register::() + .register::() + .register::(); + + let swift = Swift::default(); + let output = swift.export(&types).unwrap(); + + println!("Generated Swift for struct reuse test:"); + println!("{}", output); + + // Check that UserData is defined as a standalone struct + assert!(output.contains("public struct UserData: Codable")); + assert!(output.contains("public let id: UInt32")); + assert!(output.contains("public let name: String")); + assert!(output.contains("public let email: String?")); + + // Check that ApiResponse uses UserData directly (not a generated struct) + assert!(output.contains("case success(UserData)")); + + // Check that ApiRequest also uses UserData directly + assert!(output.contains("public struct ApiRequest: Codable")); + assert!(output.contains("public let user: UserData")); + assert!(output.contains("public let action: String")); + + // Ensure we don't have duplicate UserData definitions + let user_data_count = output.matches("public struct UserData: Codable").count(); + assert_eq!(user_data_count, 1, "UserData should be defined only once"); + + // Ensure we don't have any generated structs like ApiResponseSuccessData + assert!( + !output.contains("ApiResponseSuccessData"), + "Should not generate ApiResponseSuccessData when UserData is standalone" + ); +} + +#[test] +fn test_struct_reuse_with_different_ordering() { + // Test with different ordering - enum first, then standalone struct + let types = TypeCollection::default() + .register::() + .register::() + .register::(); + + let swift = Swift::default(); + let output = swift.export(&types).unwrap(); + + println!("Generated Swift for struct reuse test (enum first):"); + println!("{}", output); + + // Check that UserData is still defined as a standalone struct + assert!(output.contains("public struct UserData: Codable")); + + // Check that ApiResponse uses UserData directly + assert!(output.contains("case success(UserData)")); + + // Ensure we don't have duplicate UserData definitions + let user_data_count = output.matches("public struct UserData: Codable").count(); + assert_eq!(user_data_count, 1, "UserData should be defined only once"); + + // Ensure we don't have any generated structs like ApiResponseSuccessData + assert!( + !output.contains("ApiResponseSuccessData"), + "Should not generate ApiResponseSuccessData when UserData is standalone" + ); +} diff --git a/specta-swift/tests/struct_variants.rs b/specta-swift/tests/struct_variants.rs new file mode 100644 index 0000000..004ba0d --- /dev/null +++ b/specta-swift/tests/struct_variants.rs @@ -0,0 +1,83 @@ +use specta::{Type, TypeCollection}; +use specta_swift::Swift; + +/// Test enum with struct-like variants (named fields) +#[derive(Type)] +pub enum Event { + /// Job started event with named fields + JobStarted { job_id: String, job_type: String }, + /// Job completed event with named fields + JobCompleted { + job_id: String, + result: String, + duration: u64, + }, + /// Simple unit variant + JobCancelled, + /// Tuple variant (unnamed fields) + JobFailed(String, u32), +} + +/// Test enum with mixed variant types +#[derive(Type)] +pub enum ApiResponse { + /// Success with data + Success { data: String, status: u16 }, + /// Error with details + Error { + message: String, + code: u32, + details: Option, + }, + /// Loading state + Loading, + /// Tuple variant + Redirect(String), +} + +#[test] +fn test_struct_variants_generation() { + let types = TypeCollection::default() + .register::() + .register::(); + + let swift = Swift::default(); + let result = swift.export(&types).unwrap(); + + println!("Generated Swift for struct variants:"); + println!("{}", result); + + // Event enum should have struct-like cases + assert!(result.contains("enum Event")); + assert!(result.contains("case jobStarted(EventJobStartedData)")); + assert!(result.contains("case jobCompleted(EventJobCompletedData)")); + assert!(result.contains("case jobCancelled")); + assert!(result.contains("case jobFailed(String, UInt32)")); + + // Should generate structs for named field variants + assert!(result.contains("struct EventJobStartedData: Codable")); + assert!(result.contains("let jobId: String")); + assert!(result.contains("let jobType: String")); + + assert!(result.contains("struct EventJobCompletedData: Codable")); + assert!(result.contains("let jobId: String")); + assert!(result.contains("let result: String")); + assert!(result.contains("let duration: UInt64")); + + // ApiResponse enum should have struct-like cases + assert!(result.contains("enum ApiResponse")); + assert!(result.contains("case success(ApiResponseSuccessData)")); + assert!(result.contains("case error(ApiResponseErrorData)")); + assert!(result.contains("case loading")); + assert!(result.contains("case redirect(String)")); + + // Should generate structs for ApiResponse variants + assert!(result.contains("struct ApiResponseSuccessData: Codable")); + assert!(result.contains("let data: String")); + assert!(result.contains("let status: UInt16")); + + assert!(result.contains("struct ApiResponseErrorData: Codable")); + assert!(result.contains("let message: String")); + assert!(result.contains("let code: UInt32")); + assert!(result.contains("let details: String?")); +} diff --git a/specta-swift/tests/unions.rs b/specta-swift/tests/unions.rs new file mode 100644 index 0000000..466c713 --- /dev/null +++ b/specta-swift/tests/unions.rs @@ -0,0 +1,170 @@ +use specta::{Type, TypeCollection}; +use specta_swift::Swift; + +#[derive(Type)] +struct User { + id: u32, + name: String, + email: Option, +} + +#[derive(Type)] +struct Admin { + id: u32, + name: String, + permissions: Vec, + level: u8, +} + +#[derive(Type)] +struct Guest { + session_id: String, + expires_at: String, +} + +#[derive(Type)] +enum UserType { + // Unit variant + Anonymous, + + // Tuple variant + User(String, u32), + + // Named fields variant (should become a struct-like case) + Admin { + id: u32, + name: String, + permissions: Vec, + }, + + // Nested struct variant + Registered(User), + + // Complex nested struct variant + SuperAdmin(Admin), + + // Mixed variant with nested struct + Guest { + info: Guest, + created_at: String, + }, +} + +#[derive(Type)] +enum ApiResult { + Success { + data: T, + status: u16, + }, + Error { + error: E, + code: u32, + message: String, + }, + Loading { + progress: f32, + }, +} + +#[derive(Type)] +enum ComplexUnion { + // Simple unit + None, + + // Tuple with multiple types + Tuple(String, u32, bool), + + // Named fields + NamedFields { + id: u32, + name: String, + active: bool, + }, + + // Nested struct + UserStruct(User), + + // Nested enum + UserType(UserType), + + // Complex nested structure + Complex { + user: User, + metadata: Vec, + settings: Option, + }, +} + +#[test] +fn test_enum_with_nested_structs() { + let types = TypeCollection::default() + .register::() + .register::() + .register::() + .register::() + .register::>() + .register::(); + + let swift = Swift::default(); + let output = swift.export(&types).unwrap(); + + println!("Generated Swift code:\n{}", output); + + // Test enum generation + assert!(output.contains("enum UserType")); + assert!(output.contains("enum ApiResult")); + assert!(output.contains("enum ComplexUnion")); + + // Test unit variants + assert!(output.contains("case anonymous")); + assert!(output.contains("case none")); + + // Test tuple variants + assert!(output.contains("case user(String, UInt32)")); + assert!(output.contains("case tuple(String, UInt32, Bool)")); + + // Test named field variants (should be struct-like) + assert!(output.contains("case admin(UserTypeAdminData)")); + assert!(output.contains("case guest(UserTypeGuestData)")); + assert!(output.contains("case success(ApiResultSuccessData)")); + assert!(output.contains("case error(ApiResultErrorData)")); + assert!(output.contains("case loading(ApiResultLoadingData)")); + assert!(output.contains("case namedFields(ComplexUnionNamedFieldsData)")); + assert!(output.contains("case complex(ComplexUnionComplexData)")); + + // Test nested struct variants + assert!(output.contains("case registered(User)")); + assert!(output.contains("case superAdmin(Admin)")); + assert!(output.contains("case userStruct(User)")); + assert!(output.contains("case userType(UserType)")); + + // Test that nested structs are properly defined + assert!(output.contains("struct User")); + assert!(output.contains("struct Admin")); + assert!(output.contains("struct Guest")); +} + +#[test] +fn test_swift_union_syntax() { + let types = TypeCollection::default().register::(); + + let swift = Swift::default(); + let output = swift.export(&types).unwrap(); + + println!("UserType Swift code:\n{}", output); + + // Verify proper Swift enum syntax (no redundant Codable in declaration) + assert!(output.contains("enum UserType {")); + + // Unit variant + assert!(output.contains("case anonymous")); + + // Tuple variant + assert!(output.contains("case user(String, UInt32)")); + + // Named fields should be struct-like + assert!(output.contains("case admin(UserTypeAdminData)")); + assert!(output.contains("case registered(User)")); + assert!(output.contains("case superAdmin(Admin)")); + assert!(output.contains("case guest(UserTypeGuestData)")); +} diff --git a/specta-swift/tests/uuid_simple.rs b/specta-swift/tests/uuid_simple.rs new file mode 100644 index 0000000..2e8f49e --- /dev/null +++ b/specta-swift/tests/uuid_simple.rs @@ -0,0 +1,43 @@ +use specta::{Type, TypeCollection}; +use specta_swift::Swift; + +// Test with UUID - this should work now that we have the uuid feature enabled +#[derive(Type)] +struct WithUuid { + id: uuid::Uuid, + name: String, +} + +#[derive(Type)] +struct WithChrono { + created_at: chrono::DateTime, + updated_at: chrono::NaiveDateTime, + name: String, +} + +#[test] +fn test_uuid_support() { + let types = TypeCollection::default().register::(); + let swift = Swift::default(); + let output = swift.export(&types).unwrap(); + + println!("UUID support test:\n{}", output); + + // UUID should be converted to String in Swift + assert!(output.contains("let id: String")); + assert!(output.contains("let name: String")); +} + +#[test] +fn test_chrono_support() { + let types = TypeCollection::default().register::(); + let swift = Swift::default(); + let output = swift.export(&types).unwrap(); + + println!("Chrono support test:\n{}", output); + + // Chrono types should be converted to String in Swift + assert!(output.contains("let createdAt: String")); + assert!(output.contains("let updatedAt: String")); + assert!(output.contains("let name: String")); +} diff --git a/specta-swift/tests/uuid_test.rs b/specta-swift/tests/uuid_test.rs new file mode 100644 index 0000000..85e803a --- /dev/null +++ b/specta-swift/tests/uuid_test.rs @@ -0,0 +1,62 @@ +use specta::{Type, TypeCollection}; +use specta_swift::Swift; + +// Test with UUID - this should work if the uuid feature is enabled +#[cfg(feature = "uuid")] +#[derive(Type)] +struct WithUuid { + id: uuid::Uuid, + name: String, +} + +#[cfg(feature = "uuid")] +#[test] +fn test_uuid_support() { + let types = TypeCollection::default().register::(); + let swift = Swift::default(); + let output = swift.export(&types).unwrap(); + + println!("UUID support test:\n{}", output); + + // UUID should be converted to String in Swift + assert!(output.contains("let id: String")); + assert!(output.contains("let name: String")); +} + +#[cfg(not(feature = "uuid"))] +#[test] +fn test_uuid_not_available() { + println!("UUID feature not enabled - this is expected"); + // This test passes when UUID feature is not enabled +} + +// Test with chrono - this should work if the chrono feature is enabled +#[cfg(feature = "chrono")] +#[derive(Type)] +struct WithChrono { + created_at: chrono::DateTime, + updated_at: chrono::NaiveDateTime, + name: String, +} + +#[cfg(feature = "chrono")] +#[test] +fn test_chrono_support() { + let types = TypeCollection::default().register::(); + let swift = Swift::default(); + let output = swift.export(&types).unwrap(); + + println!("Chrono support test:\n{}", output); + + // Chrono types should be converted to String in Swift + assert!(output.contains("let createdAt: String")); + assert!(output.contains("let updatedAt: String")); + assert!(output.contains("let name: String")); +} + +#[cfg(not(feature = "chrono"))] +#[test] +fn test_chrono_not_available() { + println!("Chrono feature not enabled - this is expected"); + // This test passes when chrono feature is not enabled +} diff --git a/specta-typescript/src/legacy.rs b/specta-typescript/src/legacy.rs index 417c745..4b88717 100644 --- a/specta-typescript/src/legacy.rs +++ b/specta-typescript/src/legacy.rs @@ -575,6 +575,43 @@ pub(crate) fn enum_datatype( s } + (EnumRepr::String { rename_all }, Fields::Unit) => { + // Generate string literal for string enums + let string_value = match rename_all.as_deref() { + Some("snake_case") => variant_name.to_lowercase(), + Some("UPPERCASE") => variant_name.to_uppercase(), + Some("camelCase") => { + let mut chars = variant_name.chars(); + match chars.next() { + None => String::new(), + Some(first) => { + first.to_lowercase().chain(chars).collect() + } + } + } + Some("PascalCase") => { + let mut chars = variant_name.chars(); + match chars.next() { + None => String::new(), + Some(first) => { + first.to_uppercase().chain(chars).collect() + } + } + } + Some("kebab-case") => { + variant_name.to_lowercase().replace('_', "-") + } + _ => variant_name.to_lowercase(), + }; + format!(r#""{string_value}""#) + } + (EnumRepr::String { .. }, _) => { + // String enums should only have unit variants + return Err(Error::InvalidName { + path: format!("enum variant '{}'", variant_name), + name: "String enum variants cannot have fields".into(), + }); + } }, true, )) @@ -724,6 +761,8 @@ fn validate_type_for_tagged_intersection( }, // All of these repr's are always objects. EnumRepr::Internal { .. } | EnumRepr::Adjacent { .. } | EnumRepr::External => Ok(false), + // String enums are string literals, not objects + EnumRepr::String { .. } => Ok(false), } } DataType::Tuple(v) => { diff --git a/specta/src/datatype/enum.rs b/specta/src/datatype/enum.rs index 208fabe..2c584fa 100644 --- a/specta/src/datatype/enum.rs +++ b/specta/src/datatype/enum.rs @@ -46,6 +46,14 @@ impl Enum { pub fn variants_mut(&mut self) -> &mut Vec<(Cow<'static, str>, EnumVariant)> { &mut self.variants } + + /// Check if this enum should be serialized as a string enum. + /// This is true when all variants are unit variants (no fields). + pub fn is_string_enum(&self) -> bool { + self.variants() + .iter() + .all(|(_, variant)| matches!(variant.fields(), Fields::Unit)) + } } impl From for DataType { @@ -67,6 +75,25 @@ pub enum EnumRepr { tag: Cow<'static, str>, content: Cow<'static, str>, }, + /// String enum representation for unit-only enums with serde rename_all + String { + rename_all: Option>, + }, +} + +impl EnumRepr { + /// Check if this is a string enum representation + pub fn is_string(&self) -> bool { + matches!(self, EnumRepr::String { .. }) + } + + /// Get the rename_all inflection for string enums + pub fn rename_all(&self) -> Option<&str> { + match self { + EnumRepr::String { rename_all } => rename_all.as_deref(), + _ => None, + } + } } /// represents a variant of an enum. diff --git a/tests/tests/ts.rs b/tests/tests/ts.rs index dfab0c1..99f23f5 100644 --- a/tests/tests/ts.rs +++ b/tests/tests/ts.rs @@ -176,7 +176,9 @@ fn typescript_types() { assert_ts!((String, i32), "[string, number]"); assert_ts!((String, i32, bool), "[string, number, boolean]"); assert_ts!( - (bool, bool, bool, bool, bool, bool, bool, bool, bool, bool, bool, bool), + ( + bool, bool, bool, bool, bool, bool, bool, bool, bool, bool, bool, bool + ), "[boolean, boolean, boolean, boolean, boolean, boolean, boolean, boolean, boolean, boolean, boolean, boolean]" ); @@ -404,6 +406,9 @@ fn typescript_types() { // https://github.com/specta-rs/specta/issues/374 assert_ts!(Issue374, "{ foo?: boolean; bar?: boolean }"); + + // https://github.com/specta-rs/specta/issues/386 + assert_ts!(type_type::Type, "never"); } #[derive(Type)] @@ -774,7 +779,6 @@ struct Issue374 { bar: bool, } - // https://github.com/specta-rs/specta/issues/386 // We put this test in a separate module because the parent module has `use specta::Type`, // so it clashes with our user-defined `Type`. @@ -786,4 +790,4 @@ mod type_type { fn typescript_types() { assert_ts!(Type, "never"); } -} \ No newline at end of file +}