Repository: 1Password/typeshare
Pull request #89 (2023-04-06 23:59:41)

Add support for fixed length arrays
These have been a problem in a couple structures I'm hoping to translate into Typescript.

I'm unsure of the syntax for all the languages excepting typescript. For the majority of them I reused Vec (because a fixed length array is a specialised version of a dynamically sized array).

For go, I found something that suggested this syntax `[4]string`. I don't know if this is right, so I'd appreciate input on the PR.

I would appreciate input in the PR for go, kotlin, scala and swift.


Patch:
diff --git a/core/src/language/go.rs b/core/src/language/go.rs
index a260c2f..a5936d4 100644
--- a/core/src/language/go.rs
+++ b/core/src/language/go.rs
@@ -80,6 +80,9 @@ impl Language for Go {
     ) -> Result<String, RustTypeFormatError> {
         Ok(match special_ty {
             SpecialRustType::Vec(rtype) => format!("[]{}", self.format_type(rtype, generic_types)?),
+            SpecialRustType::Array(rtype, len) => {
+                format!("[{len}]{}", self.format_type(rtype, generic_types)?)
+            }
             SpecialRustType::Option(rtype) => {
                 format!("*{}", self.format_type(rtype, generic_types)?)
             }
diff --git a/core/src/language/kotlin.rs b/core/src/language/kotlin.rs
index 7587b78..fe9703d 100644
--- a/core/src/language/kotlin.rs
+++ b/core/src/language/kotlin.rs
@@ -38,6 +38,9 @@ impl Language for Kotlin {
             SpecialRustType::Vec(rtype) => {
                 format!("List<{}>", self.format_type(rtype, generic_types)?)
             }
+            SpecialRustType::Array(rtype, _) => {
+                format!("List<{}>", self.format_type(rtype, generic_types)?)
+            }
             SpecialRustType::Option(rtype) => {
                 format!("{}?", self.format_type(rtype, generic_types)?)
             }
diff --git a/core/src/language/scala.rs b/core/src/language/scala.rs
index d5afc50..be7ce99 100644
--- a/core/src/language/scala.rs
+++ b/core/src/language/scala.rs
@@ -79,6 +79,9 @@ impl Language for Scala {
             SpecialRustType::Vec(rtype) => {
                 format!("Vector[{}]", self.format_type(rtype, generic_types)?)
             }
+            SpecialRustType::Array(rtype, _) => {
+                format!("Vector[{}]", self.format_type(rtype, generic_types)?)
+            }
             SpecialRustType::Option(rtype) => {
                 format!("Option[{}]", self.format_type(rtype, generic_types)?)
             }
diff --git a/core/src/language/swift.rs b/core/src/language/swift.rs
index 24dd66a..936d3e1 100644
--- a/core/src/language/swift.rs
+++ b/core/src/language/swift.rs
@@ -127,6 +127,9 @@ impl Language for Swift {
     ) -> Result<String, RustTypeFormatError> {
         Ok(match special_ty {
             SpecialRustType::Vec(rtype) => format!("[{}]", self.format_type(rtype, generic_types)?),
+            SpecialRustType::Array(rtype, _) => {
+                format!("[{}]", self.format_type(rtype, generic_types)?)
+            }
             SpecialRustType::Option(rtype) => {
                 format!("{}?", self.format_type(rtype, generic_types)?)
             }
diff --git a/core/src/language/typescript.rs b/core/src/language/typescript.rs
index c337753..74879d7 100644
--- a/core/src/language/typescript.rs
+++ b/core/src/language/typescript.rs
@@ -1,3 +1,5 @@
+use joinery::JoinableIterator;
+
 use crate::rust_types::{RustType, RustTypeFormatError, SpecialRustType};
 use crate::{
     language::{Language, SupportedLanguage},
@@ -29,6 +31,15 @@ impl Language for TypeScript {
             SpecialRustType::Vec(rtype) => {
                 Ok(format!("{}[]", self.format_type(rtype, generic_types)?))
             }
+            SpecialRustType::Array(rtype, len) => {
+                let formatted_type = self.format_type(rtype, generic_types)?;
+                Ok(format!(
+                    "[{}]",
+                    std::iter::repeat(&formatted_type)
+                        .take(*len)
+                        .join_with(", ")
+                ))
+            }
             // We add optionality above the type formatting level
             SpecialRustType::Option(rtype) => self.format_type(rtype, generic_types),
             SpecialRustType::HashMap(rtype1, rtype2) => Ok(format!(
diff --git a/core/src/rust_types.rs b/core/src/rust_types.rs
index 2c2223b..49c5539 100644
--- a/core/src/rust_types.rs
+++ b/core/src/rust_types.rs
@@ -2,6 +2,7 @@ use quote::ToTokens;
 use std::collections::HashSet;
 use std::str::FromStr;
 use std::{collections::HashMap, convert::TryFrom};
+use syn::{Expr, ExprLit, Lit, TypeArray};
 use thiserror::Error;
 
 use crate::language::SupportedLanguage;
@@ -114,6 +115,8 @@ pub enum RustType {
 pub enum SpecialRustType {
     /// Represents `Vec<T>` from the standard library
     Vec(Box<RustType>),
+    /// Represents `[T; N]` from the standard library
+    Array(Box<RustType>, usize),
     /// Represents `HashMap<K, V>` from the standard library
     HashMap(Box<RustType>, Box<RustType>),
     /// Represents `Option<T>` from the standard library
@@ -163,6 +166,8 @@ pub enum RustTypeParseError {
     UnexpectedToken(String),
     #[error("Tuples are not allowed in typeshare types")]
     UnexpectedParameterizedTuple,
+    #[error("Could not parse numeric literal")]
+    NumericLiteral(syn::parse::Error),
 }
 
 impl FromStr for RustType {
@@ -243,6 +248,20 @@ impl TryFrom<&syn::Type> for RustType {
                     }
                 }
             }
+            syn::Type::Array(TypeArray {
+                elem,
+                len:
+                    Expr::Lit(ExprLit {
+                        lit: Lit::Int(count),
+                        ..
+                    }),
+                ..
+            }) => Self::Special(SpecialRustType::Array(
+                Self::try_from(elem.as_ref())?.into(),
+                count
+                    .base10_parse()
+                    .map_err(RustTypeParseError::NumericLiteral)?,
+            )),
             _ => {
                 return Err(RustTypeParseError::UnexpectedToken(
                     ty.to_token_stream().to_string(),
@@ -322,7 +341,7 @@ impl SpecialRustType {
     /// Check if this type is equivalent to or contains `ty` in one of its generic parameters.
     pub fn contains_type(&self, ty: &str) -> bool {
         match &self {
-            Self::Vec(rty) | Self::Option(rty) => rty.contains_type(ty),
+            Self::Vec(rty) | Self::Array(rty, _) | Self::Option(rty) => rty.contains_type(ty),
             Self::HashMap(rty1, rty2) => rty1.contains_type(ty) || rty2.contains_type(ty),
             Self::Unit
             | Self::String
@@ -351,6 +370,7 @@ impl SpecialRustType {
             Self::F64 => "f64",
             Self::F32 => "f32",
             Self::Vec(_) => "Vec",
+            Self::Array(_, _) => "[]",
             Self::Option(_) => "Option",
             Self::HashMap(_, _) => "HashMap",
             Self::String => "String",
@@ -373,7 +393,9 @@ impl SpecialRustType {
     /// if there are none.
     pub fn parameters(&self) -> Box<dyn Iterator<Item = &RustType> + '_> {
         match &self {
-            Self::Vec(rtype) | Self::Option(rtype) => Box::new(std::iter::once(rtype.as_ref())),
+            Self::Vec(rtype) | Self::Array(rtype, _) | Self::Option(rtype) => {
+                Box::new(std::iter::once(rtype.as_ref()))
+            }
             Self::HashMap(rtype1, rtype2) => {
                 Box::new([rtype1.as_ref(), rtype2.as_ref()].into_iter())
             }


Test patch:
diff --git a/core/data/tests/generate_types/input.rs b/core/data/tests/generate_types/input.rs
index df61af7..f470649 100644
--- a/core/data/tests/generate_types/input.rs
+++ b/core/data/tests/generate_types/input.rs
@@ -9,6 +9,7 @@ pub struct Types {
     pub float: f32,
     pub double: f64,
     pub array: Vec<String>,
+    pub fixed_length_array: [String; 4],
     pub dictionary: HashMap<String, i32>,
     pub optional_dictionary: Option<HashMap<String, i32>>,
     pub custom_type: CustomType,
diff --git a/core/data/tests/generate_types/output.go b/core/data/tests/generate_types/output.go
index 5707eaa..c8d9bde 100644
--- a/core/data/tests/generate_types/output.go
+++ b/core/data/tests/generate_types/output.go
@@ -11,6 +11,7 @@ type Types struct {
 	Float float32 `json:"float"`
 	Double float64 `json:"double"`
 	Array []string `json:"array"`
+	FixedLengthArray [4]string `json:"fixed_length_array"`
 	Dictionary map[string]int `json:"dictionary"`
 	OptionalDictionary *map[string]int `json:"optional_dictionary,omitempty"`
 	CustomType CustomType `json:"custom_type"`
diff --git a/core/data/tests/generate_types/output.kt b/core/data/tests/generate_types/output.kt
index 32a73d4..5152cad 100644
--- a/core/data/tests/generate_types/output.kt
+++ b/core/data/tests/generate_types/output.kt
@@ -16,6 +16,7 @@ data class Types (
 	val float: Float,
 	val double: Double,
 	val array: List<String>,
+	val fixed_length_array: List<String>,
 	val dictionary: HashMap<String, Int>,
 	val optional_dictionary: HashMap<String, Int>? = null,
 	val custom_type: CustomType
diff --git a/core/data/tests/generate_types/output.scala b/core/data/tests/generate_types/output.scala
index 6acb320..5eac377 100644
--- a/core/data/tests/generate_types/output.scala
+++ b/core/data/tests/generate_types/output.scala
@@ -11,6 +11,7 @@ case class Types (
 	float: Float,
 	double: Double,
 	array: Vector[String],
+	fixed_length_array: Vector[String],
 	dictionary: Map[String, Int],
 	optional_dictionary: Option[Map[String, Int]] = None,
 	custom_type: CustomType
diff --git a/core/data/tests/generate_types/output.swift b/core/data/tests/generate_types/output.swift
index 503c5c6..27423b7 100644
--- a/core/data/tests/generate_types/output.swift
+++ b/core/data/tests/generate_types/output.swift
@@ -11,17 +11,19 @@ public struct Types: Codable {
 	public let float: Float
 	public let double: Double
 	public let array: [String]
+	public let fixed_length_array: [String]
 	public let dictionary: [String: Int32]
 	public let optional_dictionary: [String: Int32]?
 	public let custom_type: CustomType
 
-	public init(s: String, static_s: String, int8: Int8, float: Float, double: Double, array: [String], dictionary: [String: Int32], optional_dictionary: [String: Int32]?, custom_type: CustomType) {
+	public init(s: String, static_s: String, int8: Int8, float: Float, double: Double, array: [String], fixed_length_array: [String], dictionary: [String: Int32], optional_dictionary: [String: Int32]?, custom_type: CustomType) {
 		self.s = s
 		self.static_s = static_s
 		self.int8 = int8
 		self.float = float
 		self.double = double
 		self.array = array
+		self.fixed_length_array = fixed_length_array
 		self.dictionary = dictionary
 		self.optional_dictionary = optional_dictionary
 		self.custom_type = custom_type
diff --git a/core/data/tests/generate_types/output.ts b/core/data/tests/generate_types/output.ts
index 9cef419..01111ea 100644
--- a/core/data/tests/generate_types/output.ts
+++ b/core/data/tests/generate_types/output.ts
@@ -8,6 +8,7 @@ export interface Types {
 	float: number;
 	double: number;
 	array: string[];
+	fixed_length_array: [string, string, string, string];
 	dictionary: Record<string, number>;
 	optional_dictionary?: Record<string, number>;
 	custom_type: CustomType;