diff --git a/core/data/tests/can_override_disallowed_types/input.rs b/core/data/tests/can_override_disallowed_types/input.rs new file mode 100644 index 00000000..01548305 --- /dev/null +++ b/core/data/tests/can_override_disallowed_types/input.rs @@ -0,0 +1,10 @@ +#[typeshare] +struct DisallowedType { + #[typeshare(typescript(type = "bigint"))] + disallowed_type: u64, + #[typeshare(typescript(type = "number"))] + another_disallowed_type: i64, + #[typeshare(typescript(type = "string"))] + #[serde(with = "my_string_serde_impl")] + disallowed_type_serde_with: u64, +} diff --git a/core/data/tests/can_override_disallowed_types/output.go b/core/data/tests/can_override_disallowed_types/output.go new file mode 100644 index 00000000..69181b9c --- /dev/null +++ b/core/data/tests/can_override_disallowed_types/output.go @@ -0,0 +1,9 @@ +package proto + +import "encoding/json" + +type DisallowedType struct { + DisallowedType uint64 `json:"disallowed_type"` + AnotherDisallowedType int64 `json:"another_disallowed_type"` + DisallowedTypeSerdeWith uint64 `json:"disallowed_type_serde_with"` +} diff --git a/core/data/tests/can_override_disallowed_types/output.kt b/core/data/tests/can_override_disallowed_types/output.kt new file mode 100644 index 00000000..54159a8c --- /dev/null +++ b/core/data/tests/can_override_disallowed_types/output.kt @@ -0,0 +1,12 @@ +package com.agilebits.onepassword + +import kotlinx.serialization.Serializable +import kotlinx.serialization.SerialName + +@Serializable +data class DisallowedType ( + val disallowed_type: ULong, + val another_disallowed_type: Long, + val disallowed_type_serde_with: ULong +) + diff --git a/core/data/tests/can_override_disallowed_types/output.scala b/core/data/tests/can_override_disallowed_types/output.scala new file mode 100644 index 00000000..43e81a32 --- /dev/null +++ b/core/data/tests/can_override_disallowed_types/output.scala @@ -0,0 +1,19 @@ +package com.agilebits + +package object onepassword { + +type UByte = Byte +type UShort = Short +type UInt = Int +type ULong = Int + +} +package onepassword { + +case class DisallowedType ( + disallowed_type: ULong, + another_disallowed_type: Long, + disallowed_type_serde_with: ULong +) + +} diff --git a/core/data/tests/can_override_disallowed_types/output.swift b/core/data/tests/can_override_disallowed_types/output.swift new file mode 100644 index 00000000..385028a3 --- /dev/null +++ b/core/data/tests/can_override_disallowed_types/output.swift @@ -0,0 +1,13 @@ +import Foundation + +public struct DisallowedType: Codable { + public let disallowed_type: UInt64 + public let another_disallowed_type: Int64 + public let disallowed_type_serde_with: UInt64 + + public init(disallowed_type: UInt64, another_disallowed_type: Int64, disallowed_type_serde_with: UInt64) { + self.disallowed_type = disallowed_type + self.another_disallowed_type = another_disallowed_type + self.disallowed_type_serde_with = disallowed_type_serde_with + } +} diff --git a/core/data/tests/can_override_disallowed_types/output.ts b/core/data/tests/can_override_disallowed_types/output.ts new file mode 100644 index 00000000..87b53f36 --- /dev/null +++ b/core/data/tests/can_override_disallowed_types/output.ts @@ -0,0 +1,6 @@ +export interface DisallowedType { + disallowed_type: bigint; + another_disallowed_type: number; + disallowed_type_serde_with: string; +} + diff --git a/core/data/tests/can_override_pointer_sized_types/input.rs b/core/data/tests/can_override_pointer_sized_types/input.rs new file mode 100644 index 00000000..6514c434 --- /dev/null +++ b/core/data/tests/can_override_pointer_sized_types/input.rs @@ -0,0 +1,15 @@ +#[typeshare] +struct PointerSizedType { + #[typeshare(kotlin(type = "ULong"))] + #[typeshare(scala(type = "ULong"))] + #[typeshare(swift(type = "UInt64"))] + #[typeshare(typescript(type = "number"))] + #[typeshare(go(type = "uint64"))] + unsigned: usize, + #[typeshare(kotlin(type = "Long"))] + #[typeshare(scala(type = "Long"))] + #[typeshare(swift(type = "Int64"))] + #[typeshare(typescript(type = "number"))] + #[typeshare(go(type = "int64"))] + signed: isize, +} diff --git a/core/data/tests/can_override_pointer_sized_types/output.go b/core/data/tests/can_override_pointer_sized_types/output.go new file mode 100644 index 00000000..e63b6b63 --- /dev/null +++ b/core/data/tests/can_override_pointer_sized_types/output.go @@ -0,0 +1,8 @@ +package proto + +import "encoding/json" + +type PointerSizedType struct { + Unsigned uint64 `json:"unsigned"` + Signed int64 `json:"signed"` +} diff --git a/core/data/tests/can_override_pointer_sized_types/output.kt b/core/data/tests/can_override_pointer_sized_types/output.kt new file mode 100644 index 00000000..01d31c05 --- /dev/null +++ b/core/data/tests/can_override_pointer_sized_types/output.kt @@ -0,0 +1,11 @@ +package com.agilebits.onepassword + +import kotlinx.serialization.Serializable +import kotlinx.serialization.SerialName + +@Serializable +data class PointerSizedType ( + val unsigned: ULong, + val signed: Long +) + diff --git a/core/data/tests/can_override_pointer_sized_types/output.scala b/core/data/tests/can_override_pointer_sized_types/output.scala new file mode 100644 index 00000000..6c8ea907 --- /dev/null +++ b/core/data/tests/can_override_pointer_sized_types/output.scala @@ -0,0 +1,18 @@ +package com.agilebits + +package object onepassword { + +type UByte = Byte +type UShort = Short +type UInt = Int +type ULong = Int + +} +package onepassword { + +case class PointerSizedType ( + unsigned: ULong, + signed: Long +) + +} diff --git a/core/data/tests/can_override_pointer_sized_types/output.swift b/core/data/tests/can_override_pointer_sized_types/output.swift new file mode 100644 index 00000000..403ebc1c --- /dev/null +++ b/core/data/tests/can_override_pointer_sized_types/output.swift @@ -0,0 +1,11 @@ +import Foundation + +public struct PointerSizedType: Codable { + public let unsigned: UInt64 + public let signed: Int64 + + public init(unsigned: UInt64, signed: Int64) { + self.unsigned = unsigned + self.signed = signed + } +} diff --git a/core/data/tests/can_override_pointer_sized_types/output.ts b/core/data/tests/can_override_pointer_sized_types/output.ts new file mode 100644 index 00000000..83713936 --- /dev/null +++ b/core/data/tests/can_override_pointer_sized_types/output.ts @@ -0,0 +1,5 @@ +export interface PointerSizedType { + unsigned: number; + signed: number; +} + diff --git a/core/src/language/go.rs b/core/src/language/go.rs index 524564a2..e8f2ae42 100644 --- a/core/src/language/go.rs +++ b/core/src/language/go.rs @@ -156,9 +156,7 @@ impl Language for Go { | SpecialRustType::U8 | SpecialRustType::U16 | SpecialRustType::I32 - | SpecialRustType::I16 - | SpecialRustType::ISize - | SpecialRustType::USize => "int".into(), + | SpecialRustType::I16 => "int".into(), SpecialRustType::U32 => "uint32".into(), SpecialRustType::I54 | SpecialRustType::I64 => "int64".into(), SpecialRustType::U53 | SpecialRustType::U64 => "uint64".into(), @@ -169,6 +167,12 @@ impl Language for Go { self.add_import("time"); "time.Time".into() } + SpecialRustType::ISize | SpecialRustType::USize => { + panic!( + "Pointer-sized types require an explicit output type. \ + See: https://1password.github.io/typeshare/usage/annotations.html#special-note-on-pointer-sized-types for more information." + ) + } }) } diff --git a/core/src/language/kotlin.rs b/core/src/language/kotlin.rs index 0ee5ac2d..f8b1879e 100644 --- a/core/src/language/kotlin.rs +++ b/core/src/language/kotlin.rs @@ -79,12 +79,12 @@ impl Language for Kotlin { // https://kotlinlang.org/docs/basic-types.html#integer-types SpecialRustType::I8 => "Byte".into(), SpecialRustType::I16 => "Short".into(), - SpecialRustType::ISize | SpecialRustType::I32 => "Int".into(), + SpecialRustType::I32 => "Int".into(), SpecialRustType::I54 | SpecialRustType::I64 => "Long".into(), // https://kotlinlang.org/docs/basic-types.html#unsigned-integers SpecialRustType::U8 => "UByte".into(), SpecialRustType::U16 => "UShort".into(), - SpecialRustType::USize | SpecialRustType::U32 => "UInt".into(), + SpecialRustType::U32 => "UInt".into(), SpecialRustType::U53 | SpecialRustType::U64 => "ULong".into(), SpecialRustType::Bool => "Boolean".into(), SpecialRustType::F32 => "Float".into(), @@ -95,6 +95,12 @@ impl Language for Kotlin { special_ty.to_string(), )) } + SpecialRustType::ISize | SpecialRustType::USize => { + panic!( + "Pointer-sized types require an explicit output type. \ + See: https://1password.github.io/typeshare/usage/annotations.html#special-note-on-pointer-sized-types for more information." + ) + } }) } diff --git a/core/src/language/scala.rs b/core/src/language/scala.rs index 840e7aff..069da1d1 100644 --- a/core/src/language/scala.rs +++ b/core/src/language/scala.rs @@ -102,12 +102,12 @@ impl Language for Scala { SpecialRustType::String | SpecialRustType::Char => "String".into(), SpecialRustType::I8 => "Byte".into(), SpecialRustType::I16 => "Short".into(), - SpecialRustType::ISize | SpecialRustType::I32 => "Int".into(), + SpecialRustType::I32 => "Int".into(), SpecialRustType::I54 | SpecialRustType::I64 => "Long".into(), // Scala does not support unsigned integers, so upcast it to the closest one SpecialRustType::U8 => "UByte".into(), SpecialRustType::U16 => "UShort".into(), - SpecialRustType::USize | SpecialRustType::U32 => "UInt".into(), + SpecialRustType::U32 => "UInt".into(), SpecialRustType::U53 | SpecialRustType::U64 => "ULong".into(), SpecialRustType::Bool => "Boolean".into(), SpecialRustType::F32 => "Float".into(), @@ -118,6 +118,12 @@ impl Language for Scala { special_ty.to_string(), )) } + SpecialRustType::ISize | SpecialRustType::USize => { + panic!( + "Pointer-sized types require an explicit output type. \ + See: https://1password.github.io/typeshare/usage/annotations.html#special-note-on-pointer-sized-types for more information." + ) + } }) } diff --git a/core/src/language/swift.rs b/core/src/language/swift.rs index d2b22629..27c821e8 100644 --- a/core/src/language/swift.rs +++ b/core/src/language/swift.rs @@ -206,8 +206,6 @@ impl Language for Swift { SpecialRustType::U8 => "UInt8".into(), SpecialRustType::I16 => "Int16".into(), SpecialRustType::U16 => "UInt16".into(), - SpecialRustType::USize => "UInt".into(), - SpecialRustType::ISize => "Int".into(), SpecialRustType::I32 => "Int32".into(), SpecialRustType::U32 => "UInt32".into(), SpecialRustType::I54 | SpecialRustType::I64 => "Int64".into(), @@ -221,6 +219,12 @@ impl Language for Swift { special_ty.to_string(), )) } + SpecialRustType::ISize | SpecialRustType::USize => { + panic!( + "Pointer-sized types require an explicit output type. \ + See: https://1password.github.io/typeshare/usage/annotations.html#special-note-on-pointer-sized-types for more information." + ) + } }) } diff --git a/core/src/language/typescript.rs b/core/src/language/typescript.rs index c1f4f836..3cc7553f 100644 --- a/core/src/language/typescript.rs +++ b/core/src/language/typescript.rs @@ -128,11 +128,17 @@ export const ReplacerFunc = (key: string, value: unknown): unknown => {{ | SpecialRustType::F32 | SpecialRustType::F64 => Ok("number".into()), SpecialRustType::Bool => Ok("boolean".into()), - SpecialRustType::U64 - | SpecialRustType::I64 - | SpecialRustType::ISize - | SpecialRustType::USize => { - panic!("64 bit types not allowed in Typeshare") + SpecialRustType::U64 | SpecialRustType::I64 => { + panic!( + "64 bit integer types require an explicit output type for TypeScript. \ + See: https://1password.github.io/typeshare/usage/annotations.html#special-note-on-64-bit-integer-types for more information." + ) + } + SpecialRustType::ISize | SpecialRustType::USize => { + panic!( + "Pointer-sized types require an explicit output type. \ + See: https://1password.github.io/typeshare/usage/annotations.html#special-note-on-pointer-sized-types for more information." + ) } } } diff --git a/core/src/rust_types.rs b/core/src/rust_types.rs index fab294a9..f30645a3 100644 --- a/core/src/rust_types.rs +++ b/core/src/rust_types.rs @@ -387,13 +387,14 @@ impl TryFrom<&syn::Type> for RustType { "u16" => Self::Special(SpecialRustType::U16), "u32" => Self::Special(SpecialRustType::U32), "U53" => Self::Special(SpecialRustType::U53), - "u64" | "i64" | "usize" | "isize" => { - return Err(RustTypeParseError::UnsupportedType(vec![id])) - } + "u64" => Self::Special(SpecialRustType::U64), + "usize" => Self::Special(SpecialRustType::USize), "i8" => Self::Special(SpecialRustType::I8), "i16" => Self::Special(SpecialRustType::I16), "i32" => Self::Special(SpecialRustType::I32), "I54" => Self::Special(SpecialRustType::I54), + "i64" => Self::Special(SpecialRustType::I64), + "isize" => Self::Special(SpecialRustType::ISize), "f32" => Self::Special(SpecialRustType::F32), "f64" => Self::Special(SpecialRustType::F64), _ => { diff --git a/core/tests/agnostic_tests.rs b/core/tests/agnostic_tests.rs index 70f195b1..4cb67a50 100644 --- a/core/tests/agnostic_tests.rs +++ b/core/tests/agnostic_tests.rs @@ -3,7 +3,6 @@ use typeshare_core::{ context::{ParseContext, ParseFileContext}, language::{CrateTypes, Language, TypeScript}, parser::{self, ParseError}, - rust_types::RustTypeParseError, ProcessInputError, }; /// Parse and generate types for a single Rust input file. @@ -36,68 +35,6 @@ pub fn process_input( Ok(()) } -mod blocklisted_types { - use std::collections::HashMap; - - use super::*; - - fn assert_type_is_blocklisted(ty: &str, blocklisted_type: &str) { - let source = format!( - r##" - #[typeshare] - #[serde(default, rename_all = "camelCase")] - pub struct Foo {{ - pub bar: {ty}, - }} - "##, - ty = ty - ); - - let mut out: Vec = Vec::new(); - assert!(matches!( - process_input(&source, &mut TypeScript::default(), &HashMap::new(), &mut out), - Err(ProcessInputError::ParseError( - ParseError::RustTypeParseError(RustTypeParseError::UnsupportedType(contents)) - )) if contents == vec![blocklisted_type.to_owned()] - )); - } - - #[test] - fn test_i64_blocklisted_struct() { - assert_type_is_blocklisted("i64", "i64"); - } - - #[test] - fn test_u64_blocklisted_struct() { - assert_type_is_blocklisted("u64", "u64"); - } - - #[test] - fn test_isize_blocklisted_struct() { - assert_type_is_blocklisted("isize", "isize"); - } - - #[test] - fn test_usize_blocklisted_in_struct() { - assert_type_is_blocklisted("usize", "usize"); - } - - #[test] - fn test_optional_blocklisted_struct() { - assert_type_is_blocklisted("Option", "i64"); - } - - #[test] - fn test_vec_blocklisted_struct() { - assert_type_is_blocklisted("Vec", "i64"); - } - - #[test] - fn test_hashmap_blocklisted_struct() { - assert_type_is_blocklisted("HashMap", "i64"); - } -} - mod serde_attributes_on_enums { use std::collections::HashMap; diff --git a/core/tests/snapshot_tests.rs b/core/tests/snapshot_tests.rs index 11ac96e0..6bd00963 100644 --- a/core/tests/snapshot_tests.rs +++ b/core/tests/snapshot_tests.rs @@ -557,6 +557,8 @@ tests! { python ]; can_override_types: [swift, kotlin, scala, typescript, go]; + can_override_disallowed_types: [swift, kotlin, scala, typescript, go]; + can_override_pointer_sized_types: [swift, kotlin, scala, typescript, go]; /// Structs can_generate_simple_struct_with_a_comment: [kotlin, swift, typescript, scala, go, python]; diff --git a/docs/src/usage/annotations.md b/docs/src/usage/annotations.md index 7227619f..aeed7438 100644 --- a/docs/src/usage/annotations.md +++ b/docs/src/usage/annotations.md @@ -21,7 +21,7 @@ enum MyEnum { ## Annotation arguments -We can add arguments to the `#[typeshare]` annotation to modify the generated definitions. +We can add arguments to the `#[typeshare]` annotation to modify the generated definitions. ### Decorators @@ -103,6 +103,95 @@ This would generate the following Kotlin code: typealias Options = String ``` +### Override Type for a Field + +You can also use language-specific arguments to tell Typeshare to treat +a field as a type in a particular output language. For example, +```rust +#[typeshare] +struct MyStruct { + #[typeshare(typescript(type = "0 | 1"))] + oneOrZero: u8, +} +``` +would generate the following Typescript code: +```typescript +export interface MyStruct { + oneOrZero: 0 | 1; +} +``` +The `type` argument is supported for all output languages, however Typescript +also supports the optional `readonly` argument (e.g. `typescript(readonly, type= "0 | 1")`) +to make the output property readonly. + +### Special Note on 64 Bit Integer Types + +The default behavior for 64 bit integer types when outputting TypeScript is to +panic. The reasoning behind this is that in JavaScript runtimes integers are not +sufficient to fully represent the set of all 64 bit integers, that is, +`Number.MIN_SAFE_INTEGER` and `Number.MAX_SAFE_INTEGER` are less in magnitude +than `i64::MIN` and `u64::MAX`, respectively. There are a few ways one can still +use 64 bit integer types, however, and a Typeshare attribute to override the +field type can be applied to accommodate the particular approach one chooses to +take. Here are a few examples: + +**Serializing 64 bit integer fields to strings using `serde(with = ...)`** +```rust +struct MyStruct { + #[typeshare(typescript(type = "string"))] + #[serde(with = "my_string_serde_impl")] + my_field: u64 +} +``` + +**Using a third-party JSON parser that provides support for larger integer types via `bigint`** +```rust +struct MyStruct { + #[typeshare(typescript(type = "bigint"))] + my_field: u64 +} +``` + +**Throwing all caution to the wind and just using `number`** +```rust +struct MyStruct { + #[typeshare(typescript(type = "number"))] + my_field: u64 +} +``` + +### Special Note on Pointer-Sized Types + +The default behavior for pointer-sized integer types (e.g. `isize` and `usize`) +is to panic, regardless of target language. The reasoning behind this is that +pointer-sized types may be different sizes based on platform. One can still use +pointer-sized types by using a Typeshare attribute to override the type for the +target language. For example: + +**Basic Override** +```rust +struct MyStruct { + #[typeshare(kotlin(type = "ULong"))] + my_field: usize +} +``` + +**Conditional Compilation Based on Platform** +```rust +#[cfg(target_pointer_width = "64")] +#[typeshare] +struct PointerSizedType { + #[typeshare(kotlin(type = "ULong"))] + unsigned: usize, +} + +#[cfg(target_pointer_width = "32")] +#[typeshare] +struct PointerSizedType { + #[typeshare(kotlin(type = "UInt"))] + unsigned: usize, +} +``` ## The `#[serde]` Attribute