diff --git a/core/data/tests/can_handle_serde_transparent/input.rs b/core/data/tests/can_handle_serde_transparent/input.rs new file mode 100644 index 00000000..d0b7cd50 --- /dev/null +++ b/core/data/tests/can_handle_serde_transparent/input.rs @@ -0,0 +1,26 @@ +#[typeshare] +#[serde(transparent)] +pub struct Players { + players: Vec, +} + +#[typeshare] +pub struct Player { + name: String, +} + +#[typeshare] +#[serde(transparent)] +pub struct Pool { + cards: Cards, + #[serde(skip)] + queue: Vec, +} + +#[typeshare] +pub struct Cards(Vec); + +#[typeshare] +pub struct Card { + name: String, +} \ No newline at end of file diff --git a/core/data/tests/can_handle_serde_transparent/output.go b/core/data/tests/can_handle_serde_transparent/output.go new file mode 100644 index 00000000..347bc6ef --- /dev/null +++ b/core/data/tests/can_handle_serde_transparent/output.go @@ -0,0 +1,16 @@ +package proto + +import "encoding/json" + +type Card struct { + Name string `json:"name"` +} +type Cards []Card + +type Player struct { + Name string `json:"name"` +} +type Players []Player + +type Pool Cards + diff --git a/core/data/tests/can_handle_serde_transparent/output.kt b/core/data/tests/can_handle_serde_transparent/output.kt new file mode 100644 index 00000000..427762fe --- /dev/null +++ b/core/data/tests/can_handle_serde_transparent/output.kt @@ -0,0 +1,21 @@ +package com.agilebits.onepassword + +import kotlinx.serialization.Serializable +import kotlinx.serialization.SerialName + +@Serializable +data class Card ( + val name: String +) + +typealias Cards = List + +@Serializable +data class Player ( + val name: String +) + +typealias Players = List + +typealias Pool = Cards + diff --git a/core/data/tests/can_handle_serde_transparent/output.py b/core/data/tests/can_handle_serde_transparent/output.py new file mode 100644 index 00000000..410f5cdf --- /dev/null +++ b/core/data/tests/can_handle_serde_transparent/output.py @@ -0,0 +1,18 @@ +from __future__ import annotations + +from pydantic import BaseModel +from typing import List + + +class Card(BaseModel): + name: str + +Cards = List[Card] + +class Player(BaseModel): + name: str + +Players = List[Player] + +Pool = Cards + diff --git a/core/data/tests/can_handle_serde_transparent/output.scala b/core/data/tests/can_handle_serde_transparent/output.scala new file mode 100644 index 00000000..622a7180 --- /dev/null +++ b/core/data/tests/can_handle_serde_transparent/output.scala @@ -0,0 +1,22 @@ +package com.agilebits + +package object onepassword { + +type Cards = Vector[Card] + +type Players = Vector[Player] + +type Pool = Cards + +} +package onepassword { + +case class Card ( + name: String +) + +case class Player ( + name: String +) + +} diff --git a/core/data/tests/can_handle_serde_transparent/output.swift b/core/data/tests/can_handle_serde_transparent/output.swift new file mode 100644 index 00000000..f9ba9081 --- /dev/null +++ b/core/data/tests/can_handle_serde_transparent/output.swift @@ -0,0 +1,23 @@ +import Foundation + +public struct Card: Codable { + public let name: String + + public init(name: String) { + self.name = name + } +} + +public typealias Cards = [Card] + +public struct Player: Codable { + public let name: String + + public init(name: String) { + self.name = name + } +} + +public typealias Players = [Player] + +public typealias Pool = Cards diff --git a/core/data/tests/can_handle_serde_transparent/output.ts b/core/data/tests/can_handle_serde_transparent/output.ts new file mode 100644 index 00000000..c749060f --- /dev/null +++ b/core/data/tests/can_handle_serde_transparent/output.ts @@ -0,0 +1,14 @@ +export interface Card { + name: string; +} + +export type Cards = Card[]; + +export interface Player { + name: string; +} + +export type Players = Player[]; + +export type Pool = Cards; + diff --git a/core/src/parser.rs b/core/src/parser.rs index a87f1414..6ae94060 100644 --- a/core/src/parser.rs +++ b/core/src/parser.rs @@ -211,6 +211,33 @@ pub(crate) fn parse_struct( Ok(match &s.fields { // Structs Fields::Named(f) => { + // A struct with `#[serde(transparent)]` serializes as the value of + // its single non-skipped field, so generate a type alias to that + // field's type instead of a wrapper struct. + if serde_transparent(&s.attrs) { + let mut transparent_fields = f + .named + .iter() + .filter(|field| !is_skipped(&field.attrs, target_os)); + if let (Some(field), None) = (transparent_fields.next(), transparent_fields.next()) + { + let ty = if let Some(ty) = get_field_type_override(&field.attrs) { + ty.parse()? + } else { + RustType::try_from(&field.ty)? + }; + + return Ok(RustItem::Alias(RustTypeAlias { + id: get_ident(Some(&s.ident), &s.attrs, &None), + r#type: ty, + comments: parse_comment_attrs(&s.attrs), + generic_types, + decorators: get_decorators(&s.attrs), + is_redacted: is_redacted(&s.attrs), + })); + } + } + let fields = f .named .iter() @@ -759,6 +786,10 @@ fn serde_flatten(attrs: &[syn::Attribute]) -> bool { serde_attr(attrs, "flatten") } +fn serde_transparent(attrs: &[syn::Attribute]) -> bool { + serde_attr(attrs, "transparent") +} + /// Checks the struct or enum for decorators like `#[typeshare(typescript(readonly)]` /// Takes a slice of `syn::Attribute`, returns a `HashMap>`, where `language` is `SupportedLanguage` /// and `decorator` is `FieldDecorator`. Field decorators are ordered in a `BTreeSet` for consistent code generation. diff --git a/core/tests/snapshot_tests.rs b/core/tests/snapshot_tests.rs index 042684c7..40d8adb8 100644 --- a/core/tests/snapshot_tests.rs +++ b/core/tests/snapshot_tests.rs @@ -716,4 +716,5 @@ tests! { ]; no_mangle: [swift, kotlin, scala, typescript, go]; cfg_if_attribute_typeshare: [swift, kotlin, typescript]; + can_handle_serde_transparent: [kotlin, swift, typescript, scala, go, python]; } diff --git a/docs/src/usage/annotations.md b/docs/src/usage/annotations.md index 11ce4691..84e41b47 100644 --- a/docs/src/usage/annotations.md +++ b/docs/src/usage/annotations.md @@ -166,3 +166,20 @@ export interface MyStruct { c: number; } ``` + +### Transparent Structs + +A struct annotated with `#[serde(transparent)]` serializes as the value of its +single non-skipped field. Typeshare mirrors this by generating a type alias to +that field's type instead of a wrapper struct. For example, this Rust type +```rust +#[typeshare] +#[serde(transparent)] +pub struct Players { + players: Vec, +} +``` +becomes the following Typescript definition. +```typescript +export type Players = Player[]; +```