From 4fd32b12b7a92dba90fd6a9ced6141140680d5bc Mon Sep 17 00:00:00 2001 From: Cerulan Lumina Date: Tue, 28 May 2024 16:08:59 -0400 Subject: [PATCH 01/24] chore: update changelog and bump version numbers for release v1.10.0-beta.3 --- CHANGELOG.md | 4 ++++ Cargo.lock | 4 ++-- cli/Cargo.toml | 2 +- core/Cargo.toml | 2 +- 4 files changed, 8 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 195e03b7..ff586b38 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Version 1.10.0-beta +## 1.10.0-beta.3 + +Fixed a bug involving `#[typeshare(skip)]` on fields in struct variants of enums. + ## 1.10.0-beta.2 Fixed a bug involving type aliases. diff --git a/Cargo.lock b/Cargo.lock index c7646153..73586e4b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -610,7 +610,7 @@ dependencies = [ [[package]] name = "typeshare-cli" -version = "1.10.0-beta.2" +version = "1.10.0-beta.3" dependencies = [ "anyhow", "clap", @@ -625,7 +625,7 @@ dependencies = [ [[package]] name = "typeshare-core" -version = "1.10.0-beta.2" +version = "1.10.0-beta.3" dependencies = [ "anyhow", "cool_asserts", diff --git a/cli/Cargo.toml b/cli/Cargo.toml index b9f05703..c8607480 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "typeshare-cli" -version = "1.10.0-beta.2" +version = "1.10.0-beta.3" edition = "2021" description = "Command Line Tool for generating language files with typeshare" license = "MIT OR Apache-2.0" diff --git a/core/Cargo.toml b/core/Cargo.toml index 2f06793b..5d599f49 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "typeshare-core" -version = "1.10.0-beta.2" +version = "1.10.0-beta.3" license = "MIT OR Apache-2.0" edition = "2021" description = "The code generator used by Typeshare's command line tool" From 2262a56a93f9dd41f133f733607d2c8947fdc87a Mon Sep 17 00:00:00 2001 From: Cerulan Lumina Date: Tue, 28 May 2024 16:11:29 -0400 Subject: [PATCH 02/24] chore: update changelog and bump version numbers for release v1.10.0-beta.4 --- CHANGELOG.md | 2 +- Cargo.lock | 4 ++-- cli/Cargo.toml | 4 ++-- core/Cargo.toml | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ff586b38..34c4bf17 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Version 1.10.0-beta -## 1.10.0-beta.3 +## 1.10.0-beta.4 Fixed a bug involving `#[typeshare(skip)]` on fields in struct variants of enums. diff --git a/Cargo.lock b/Cargo.lock index 73586e4b..858b49ec 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -610,7 +610,7 @@ dependencies = [ [[package]] name = "typeshare-cli" -version = "1.10.0-beta.3" +version = "1.10.0-beta.4" dependencies = [ "anyhow", "clap", @@ -625,7 +625,7 @@ dependencies = [ [[package]] name = "typeshare-core" -version = "1.10.0-beta.3" +version = "1.10.0-beta.4" dependencies = [ "anyhow", "cool_asserts", diff --git a/cli/Cargo.toml b/cli/Cargo.toml index c8607480..9d123591 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "typeshare-cli" -version = "1.10.0-beta.3" +version = "1.10.0-beta.4" edition = "2021" description = "Command Line Tool for generating language files with typeshare" license = "MIT OR Apache-2.0" @@ -22,5 +22,5 @@ once_cell = "1" rayon = "1.10" serde = { version = "1", features = ["derive"] } toml = "0.8" -typeshare-core = { path = "../core", version = "1.10.0-beta.2" } +typeshare-core = { path = "../core", version = "1.10.0-beta.4" } anyhow = "1" diff --git a/core/Cargo.toml b/core/Cargo.toml index 5d599f49..c0d16dc5 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "typeshare-core" -version = "1.10.0-beta.3" +version = "1.10.0-beta.4" license = "MIT OR Apache-2.0" edition = "2021" description = "The code generator used by Typeshare's command line tool" From 8e3423f8b63e34fca62d7fe05a940975e5ce5e76 Mon Sep 17 00:00:00 2001 From: Darrell Roberts Date: Tue, 4 Jun 2024 15:26:11 -0400 Subject: [PATCH 03/24] Allow swift decorator constraints to apply to generics --- .../input.rs | 17 +++++++++++ .../output.swift | 28 +++++++++++++++++++ core/src/language/swift.rs | 8 +++++- core/tests/snapshot_tests.rs | 1 + 4 files changed, 53 insertions(+), 1 deletion(-) create mode 100644 core/data/tests/generic_struct_with_constraints_and_decorators/input.rs create mode 100644 core/data/tests/generic_struct_with_constraints_and_decorators/output.swift diff --git a/core/data/tests/generic_struct_with_constraints_and_decorators/input.rs b/core/data/tests/generic_struct_with_constraints_and_decorators/input.rs new file mode 100644 index 00000000..962e5e4c --- /dev/null +++ b/core/data/tests/generic_struct_with_constraints_and_decorators/input.rs @@ -0,0 +1,17 @@ +#[typeshare(swift = "Equatable")] +pub struct Button { + /// Label of the button + pub label: String, + /// Accessibility label if it needed to be different than label + pub accessibility_label: Option, + /// Optional tooltips that provide extra explanation for a button + pub tooltip: Option, + /// Button action if there one + pub action: Option, + /// Icon if there is one + pub icon: Option, + /// Button state + pub state: ButtonState, + /// Button Mode + pub style: ButtonStyle, +} diff --git a/core/data/tests/generic_struct_with_constraints_and_decorators/output.swift b/core/data/tests/generic_struct_with_constraints_and_decorators/output.swift new file mode 100644 index 00000000..1d403024 --- /dev/null +++ b/core/data/tests/generic_struct_with_constraints_and_decorators/output.swift @@ -0,0 +1,28 @@ +import Foundation + +public struct Button: Codable, Equatable { + /// Label of the button + public let label: String + /// Accessibility label if it needed to be different than label + public let accessibility_label: String? + /// Optional tooltips that provide extra explanation for a button + public let tooltip: String? + /// Button action if there one + public let action: T? + /// Icon if there is one + public let icon: Icon? + /// Button state + public let state: ButtonState + /// Button Mode + public let style: ButtonStyle + + public init(label: String, accessibility_label: String?, tooltip: String?, action: T?, icon: Icon?, state: ButtonState, style: ButtonStyle) { + self.label = label + self.accessibility_label = accessibility_label + self.tooltip = tooltip + self.action = action + self.icon = icon + self.state = state + self.style = style + } +} diff --git a/core/src/language/swift.rs b/core/src/language/swift.rs index 83df5f17..f2fba105 100644 --- a/core/src/language/swift.rs +++ b/core/src/language/swift.rs @@ -278,7 +278,13 @@ impl Language for Swift { .for_each(|d| decs.push(d.clone())); } - let generic_constraint_string = default_generic_constraints.get_constraints().join(" & "); + // Include any decorator constraints on the generic types. + let generic_constraint_string = default_generic_constraints + .get_constraints() + .chain(decs.iter()) + .collect::>() + .into_iter() + .join(" & "); writeln!( w, diff --git a/core/tests/snapshot_tests.rs b/core/tests/snapshot_tests.rs index 6e119bfa..346d3b89 100644 --- a/core/tests/snapshot_tests.rs +++ b/core/tests/snapshot_tests.rs @@ -569,4 +569,5 @@ tests! { go ]; can_generate_anonymous_struct_with_skipped_fields: [swift, kotlin, scala, typescript, go]; + generic_struct_with_constraints_and_decorators: [swift]; } From 99504997947ddb3c41c8b2e22bf44d6feaf49a98 Mon Sep 17 00:00:00 2001 From: Darrell Roberts Date: Tue, 4 Jun 2024 17:00:00 -0400 Subject: [PATCH 04/24] Update generics for writing enum --- core/src/language/swift.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/core/src/language/swift.rs b/core/src/language/swift.rs index f2fba105..5b9983f2 100644 --- a/core/src/language/swift.rs +++ b/core/src/language/swift.rs @@ -452,10 +452,15 @@ impl Language for Swift { self.write_comments(w, 0, &shared.comments)?; let indirect = if shared.is_recursive { "indirect " } else { "" }; + let generic_constraint_string = self .default_generic_constraints .get_constraints() + .chain(decs.iter()) + .collect::>() + .into_iter() .join(" & "); + writeln!( w, "public {}enum {}{}: {} {{", From c5f47d523411583d07fb9555edb853c0332141ee Mon Sep 17 00:00:00 2001 From: Darrell Roberts Date: Wed, 5 Jun 2024 13:06:49 -0400 Subject: [PATCH 05/24] allow defining generic constraints via "#[typeshare(swiftGenericConstraints = "Constraint")]" --- .../input.rs | 9 +- .../output.swift | 6 +- core/src/language/kotlin.rs | 8 +- core/src/language/swift.rs | 138 +++++++++++------- core/src/parser.rs | 60 +++++--- core/src/rust_types.rs | 8 +- 6 files changed, 145 insertions(+), 84 deletions(-) diff --git a/core/data/tests/generic_struct_with_constraints_and_decorators/input.rs b/core/data/tests/generic_struct_with_constraints_and_decorators/input.rs index 962e5e4c..0ce78a36 100644 --- a/core/data/tests/generic_struct_with_constraints_and_decorators/input.rs +++ b/core/data/tests/generic_struct_with_constraints_and_decorators/input.rs @@ -1,5 +1,8 @@ -#[typeshare(swift = "Equatable")] -pub struct Button { +#[typeshare( + swift = "Equatable, Identifiable", + swiftGenericConstraints = "T: Equatable & SomeThingElse, V: Equatable" +)] +pub struct Button { /// Label of the button pub label: String, /// Accessibility label if it needed to be different than label @@ -9,7 +12,7 @@ pub struct Button { /// Button action if there one pub action: Option, /// Icon if there is one - pub icon: Option, + pub icon: Option, /// Button state pub state: ButtonState, /// Button Mode diff --git a/core/data/tests/generic_struct_with_constraints_and_decorators/output.swift b/core/data/tests/generic_struct_with_constraints_and_decorators/output.swift index 1d403024..46834b7e 100644 --- a/core/data/tests/generic_struct_with_constraints_and_decorators/output.swift +++ b/core/data/tests/generic_struct_with_constraints_and_decorators/output.swift @@ -1,6 +1,6 @@ import Foundation -public struct Button: Codable, Equatable { +public struct Button: Codable, Equatable, Identifiable { /// Label of the button public let label: String /// Accessibility label if it needed to be different than label @@ -10,13 +10,13 @@ public struct Button: Codable, Equatable { /// Button action if there one public let action: T? /// Icon if there is one - public let icon: Icon? + public let icon: V? /// Button state public let state: ButtonState /// Button Mode public let style: ButtonStyle - public init(label: String, accessibility_label: String?, tooltip: String?, action: T?, icon: Icon?, state: ButtonState, style: ButtonStyle) { + public init(label: String, accessibility_label: String?, tooltip: String?, action: T?, icon: V?, state: ButtonState, style: ButtonStyle) { self.label = label self.accessibility_label = accessibility_label self.tooltip = tooltip diff --git a/core/src/language/kotlin.rs b/core/src/language/kotlin.rs index ebd9f68e..1cbb0e7d 100644 --- a/core/src/language/kotlin.rs +++ b/core/src/language/kotlin.rs @@ -1,6 +1,6 @@ use super::{Language, ScopedCrateTypes}; use crate::language::SupportedLanguage; -use crate::parser::{remove_dash_from_identifier, ParsedData}; +use crate::parser::{remove_dash_from_identifier, ParsedData, KOTLIN_DECORATOR}; use crate::rust_types::{RustTypeFormatError, SpecialRustType}; use crate::{ rename::RenameExt, @@ -169,7 +169,11 @@ impl Language for Kotlin { writeln!(w)?; } write!(w, ")")?; - if let Some(kotlin_decorators) = rs.decorators.get(&SupportedLanguage::Kotlin) { + if let Some(kotlin_decorators) = rs + .decorators + .get(&SupportedLanguage::Kotlin) + .and_then(|d| d.get(KOTLIN_DECORATOR)) + { let redacted_decorator = String::from(REDACTED_TO_STRING); if kotlin_decorators.iter().any(|d| *d == redacted_decorator) { writeln!(w, " {{")?; diff --git a/core/src/language/swift.rs b/core/src/language/swift.rs index 5b9983f2..85c6dbed 100644 --- a/core/src/language/swift.rs +++ b/core/src/language/swift.rs @@ -1,9 +1,13 @@ use crate::{ language::{Language, SupportedLanguage}, - parser::{remove_dash_from_identifier, ParsedData}, + parser::{ + remove_dash_from_identifier, ParsedData, SWIFT_DECORATOR, + SWIFT_GENERIC_CONSTRAINTS_DECORATOR, + }, rename::RenameExt, rust_types::{ - RustEnum, RustEnumVariant, RustStruct, RustTypeAlias, RustTypeFormatError, SpecialRustType, + DecoratorMap, RustEnum, RustEnumVariant, RustStruct, RustTypeAlias, RustTypeFormatError, + SpecialRustType, }, GenerationError, }; @@ -266,9 +270,13 @@ impl Language for Swift { // If there are no decorators found for this struct, still write `Codable` and default decorators for structs let mut decs = self.get_default_decorators(); - let default_generic_constraints = self.default_generic_constraints.clone(); + // let default_generic_constraints = self.default_generic_constraints.clone(); // Check if this struct's decorators contains swift in the hashmap - if let Some(swift_decs) = rs.decorators.get(&SupportedLanguage::Swift) { + if let Some(swift_decs) = rs + .decorators + .get(&SupportedLanguage::Swift) + .and_then(|d| d.get(SWIFT_DECORATOR)) + { // For reach item in the received decorators in the typeshared struct add it to the original vector // this avoids duplicated of `Codable` without needing to `.sort()` then `.dedup()` // Note: the list received from `rs.decorators` is already deduped @@ -278,32 +286,14 @@ impl Language for Swift { .for_each(|d| decs.push(d.clone())); } - // Include any decorator constraints on the generic types. - let generic_constraint_string = default_generic_constraints - .get_constraints() - .chain(decs.iter()) - .collect::>() - .into_iter() - .join(" & "); + let generic_names_and_constraints = + self.generic_constraints(&rs.decorators, &rs.generic_types); writeln!( w, - "public struct {}{}: {} {{", - type_name, + "public struct {type_name}{}: {} {{", (!rs.generic_types.is_empty()) - .then(|| format!( - "<{}>", - rs.generic_types - .iter() - .map(|t| format!( - "{}{}", - t, - (!generic_constraint_string.is_empty()) - .then(|| format!(": {}", generic_constraint_string)) - .unwrap_or_default() - )) - .join(", ") - )) + .then(|| format!("<{generic_names_and_constraints}>",)) .unwrap_or_default(), decs.join(", ") )?; @@ -414,7 +404,12 @@ impl Language for Swift { .for_each(|dec| decs.push(dec)); // Check if this enum's decorators contains swift in the hashmap - if let Some(swift_decs) = e.shared().decorators.get(&SupportedLanguage::Swift) { + if let Some(swift_decs) = e + .shared() + .decorators + .get(&SupportedLanguage::Swift) + .and_then(|d| d.get(SWIFT_DECORATOR)) + { // Add any decorators from the typeshared enum decs.extend( // Note: `swift_decs` is already deduped @@ -453,34 +448,14 @@ impl Language for Swift { self.write_comments(w, 0, &shared.comments)?; let indirect = if shared.is_recursive { "indirect " } else { "" }; - let generic_constraint_string = self - .default_generic_constraints - .get_constraints() - .chain(decs.iter()) - .collect::>() - .into_iter() - .join(" & "); + let generic_names_and_constraints = + self.generic_constraints(&e.shared().decorators, &e.shared().generic_types); writeln!( w, - "public {}enum {}{}: {} {{", - indirect, - enum_name, + "public {indirect}enum {enum_name}{}: {} {{", (!e.shared().generic_types.is_empty()) - .then(|| format!( - "<{}>", - e.shared() - .generic_types - .iter() - .map(|t| format!( - "{}{}", - t, - (!generic_constraint_string.is_empty()) - .then(|| format!(": {}", generic_constraint_string)) - .unwrap_or_default() - )) - .join(", ") - )) + .then(|| format!("<{generic_names_and_constraints}>",)) .unwrap_or_default(), decs.join(", ") )?; @@ -812,6 +787,67 @@ impl Swift { writeln!(w, "public struct CodableVoid: {} {{}}", decs.join(", ")) } + + /// Build the generic constraints output. This checks for the `swiftGenericConstraints` typeshare attribute and combines + /// it with the `default_generic_constraints` configuration. If no `swiftGenericConstraints` is defined then we just use + /// `default_generic_constraints`. + fn generic_constraints<'a>( + &'a self, + decorator_map: &'a DecoratorMap, + generic_types: &'a [String], + ) -> String { + let swift_generic_contraints_annotated = decorator_map + .get(&SupportedLanguage::Swift) + .and_then(|d| d.get(SWIFT_GENERIC_CONSTRAINTS_DECORATOR)) + .map(|generic_constraints| { + generic_constraints + .iter() + .flat_map(|generic_constraint| { + let mut gen_name_val_iter = generic_constraint.split(':'); + let generic_name = gen_name_val_iter.next()?; + let generic_name_constraints = gen_name_val_iter + .next()? + .split('&') + .map(|s| s.trim()) + .collect::>(); + Some((generic_name, generic_name_constraints)) + }) + .map(|(gen_name, mut gen_constraints)| { + gen_constraints.extend( + self.default_generic_constraints + .get_constraints() + .map(|s| s.as_str()), + ); + (gen_name, gen_constraints) + }) + .collect::>() + }) + .unwrap_or_default(); + + if swift_generic_contraints_annotated.is_empty() { + generic_types + .iter() + .map(|type_name| { + format!( + "{type_name}: {}", + self.default_generic_constraints + .get_constraints() + .join(" & ") + ) + }) + .join(", ") + } else { + swift_generic_contraints_annotated + .iter() + .map(|(type_name, constraints)| { + format!( + "{type_name}: {constraints}", + constraints = constraints.iter().join(" & ") + ) + }) + .join(", ") + } + } } fn swift_keyword_aware_rename(name: &str) -> String { diff --git a/core/src/parser.rs b/core/src/parser.rs index a0be4cef..e13bee8a 100644 --- a/core/src/parser.rs +++ b/core/src/parser.rs @@ -2,8 +2,9 @@ use crate::{ language::{CrateName, SupportedLanguage}, rename::RenameExt, rust_types::{ - FieldDecorator, Id, RustEnum, RustEnumShared, RustEnumVariant, RustEnumVariantShared, - RustField, RustItem, RustStruct, RustType, RustTypeAlias, RustTypeParseError, + DecoratorMap, FieldDecorator, Id, RustEnum, RustEnumShared, RustEnumVariant, + RustEnumVariantShared, RustField, RustItem, RustStruct, RustType, RustTypeAlias, + RustTypeParseError, }, visitors::{ImportedType, TypeShareVisitor}, }; @@ -23,6 +24,16 @@ use thiserror::Error; const TYPESHARE: &str = "typeshare"; const SERDE: &str = "serde"; +/// The typeshare decorator name attribute. +pub type DecoratorName = &'static str; + +/// The typeshare attribute for swift type constraints. +pub const SWIFT_DECORATOR: DecoratorName = "swift"; +/// The typeshare attribute for kotlin. +pub const KOTLIN_DECORATOR: DecoratorName = "kotlin"; +/// The typeshare attribute for swift generic constraints. +pub const SWIFT_GENERIC_CONSTRAINTS_DECORATOR: DecoratorName = "swiftGenericConstraints"; + /// Errors that can occur while parsing Rust source input. #[derive(Debug, Error)] #[allow(missing_docs)] @@ -676,30 +687,33 @@ fn literal_to_string(lit: &syn::Lit) -> Option { /// Checks the struct or enum for decorators like `#[typeshare(swift = "Codable, Equatable")]` /// Takes a slice of `syn::Attribute`, returns a `HashMap>`, where `language` is `SupportedLanguage` and `decoration_words` is `String` -fn get_decorators(attrs: &[syn::Attribute]) -> HashMap> { +fn get_decorators(attrs: &[syn::Attribute]) -> DecoratorMap { // The resulting HashMap, Key is the language, and the value is a vector of decorators words that will be put onto structures - let mut out: HashMap> = HashMap::new(); - - for value in get_name_value_meta_items(attrs, "swift", TYPESHARE) { - let decorators: Vec = value.split(',').map(|s| s.trim().to_string()).collect(); - - // lastly, get the entry in the hashmap output and extend the value, or insert what we have already found - let decs = out.entry(SupportedLanguage::Swift).or_default(); - decs.extend(decorators); - // Sorting so all the added decorators will be after the normal ([`String`], `Codable`) in alphabetical order - decs.sort_unstable(); - decs.dedup(); //removing any duplicates just in case - } + let mut out: DecoratorMap = HashMap::new(); + + let add_decorators = + |name: &'static str, supported_lang: SupportedLanguage, map: &mut DecoratorMap| { + for value in get_name_value_meta_items(attrs, name, TYPESHARE) { + let constraints = || value.split(',').map(|s| s.trim().to_string()); + map.entry(supported_lang) + .and_modify(|dec_map| { + dec_map + .entry(name) + .and_modify(|dec_vals| dec_vals.extend(constraints())) + .or_insert(constraints().collect()); + }) + .or_insert(HashMap::from_iter([(name, constraints().collect())])); + } + }; - for value in get_name_value_meta_items(attrs, "kotlin", TYPESHARE) { - let decorators: Vec = value.split(',').map(|s| s.trim().to_string()).collect(); - let decs = out.entry(SupportedLanguage::Kotlin).or_default(); - decs.extend(decorators); - decs.sort_unstable(); - decs.dedup(); //removing any duplicates just in case - } + add_decorators(SWIFT_DECORATOR, SupportedLanguage::Swift, &mut out); + add_decorators( + SWIFT_GENERIC_CONSTRAINTS_DECORATOR, + SupportedLanguage::Swift, + &mut out, + ); + add_decorators(KOTLIN_DECORATOR, SupportedLanguage::Kotlin, &mut out); - //return our hashmap mapping of language -> Vec out } diff --git a/core/src/rust_types.rs b/core/src/rust_types.rs index 0d0afa75..88932509 100644 --- a/core/src/rust_types.rs +++ b/core/src/rust_types.rs @@ -6,8 +6,12 @@ use syn::{Expr, ExprLit, Lit, TypeArray, TypeSlice}; use thiserror::Error; use crate::language::SupportedLanguage; +use crate::parser::DecoratorName; use crate::visitors::accept_type; +/// Type level typeshare attributes are mapped by target language and a mapping of attribute. +pub type DecoratorMap = HashMap>>; + /// Identifier used in Rust structs, enums, and fields. It includes the `original` name and the `renamed` value after the transformation based on `serde` attributes. #[derive(Debug, Clone, PartialEq)] pub struct Id { @@ -43,7 +47,7 @@ pub struct RustStruct { /// so we need to collect them here. pub comments: Vec, /// Attributes that exist for this struct. - pub decorators: HashMap>, + pub decorators: DecoratorMap, } /// Rust type alias. @@ -566,7 +570,7 @@ pub struct RustEnumShared { /// Decorators applied to the enum for generation in other languages /// /// Example: `#[typeshare(swift = "Equatable, Comparable, Hashable")]`. - pub decorators: HashMap>, + pub decorators: DecoratorMap, /// True if this enum references itself in any field of any variant /// Swift needs the special keyword `indirect` for this case pub is_recursive: bool, From ee8c538f48f8e549df82b1604bdff26553a2f4f7 Mon Sep 17 00:00:00 2001 From: Darrell Roberts Date: Wed, 5 Jun 2024 15:01:48 -0400 Subject: [PATCH 06/24] always make CodableVoid Equatable --- core/data/tests/can_generate_generic_struct/output.swift | 2 +- core/data/tests/can_handle_unit_type/output.swift | 2 +- core/src/language/swift.rs | 3 +++ 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/core/data/tests/can_generate_generic_struct/output.swift b/core/data/tests/can_generate_generic_struct/output.swift index 0b8c79ae..f2a1005b 100644 --- a/core/data/tests/can_generate_generic_struct/output.swift +++ b/core/data/tests/can_generate_generic_struct/output.swift @@ -88,4 +88,4 @@ public enum CoreEnumUsingGenericStruct: Codable { } /// () isn't codable, so we use this instead to represent Rust's unit type -public struct CodableVoid: Codable {} +public struct CodableVoid: Codable, Equatable {} diff --git a/core/data/tests/can_handle_unit_type/output.swift b/core/data/tests/can_handle_unit_type/output.swift index c7407e99..41de9b9a 100644 --- a/core/data/tests/can_handle_unit_type/output.swift +++ b/core/data/tests/can_handle_unit_type/output.swift @@ -46,4 +46,4 @@ public enum EnumHasVoidType: Codable { } /// () isn't codable, so we use this instead to represent Rust's unit type -public struct CodableVoid: Codable {} +public struct CodableVoid: Codable, Equatable {} diff --git a/core/src/language/swift.rs b/core/src/language/swift.rs index 85c6dbed..d45008dc 100644 --- a/core/src/language/swift.rs +++ b/core/src/language/swift.rs @@ -780,6 +780,9 @@ impl Swift { let mut decs = self.get_default_decorators(); + // Unit type can be used as generic impl constrained to Equatable. + decs.push("Equatable".into()); + // If there are no decorators found for this struct, still write `Codable` and default decorators for structs if !decs.contains(&CODABLE.to_string()) { decs.push(CODABLE.to_string()); From e4970b2430193a2e368c8f43d89366f2bfa26e16 Mon Sep 17 00:00:00 2001 From: Darrell Roberts Date: Wed, 5 Jun 2024 17:27:26 -0400 Subject: [PATCH 07/24] No need for decorator language --- core/src/language/kotlin.rs | 8 ++--- core/src/language/swift.rs | 21 +++---------- core/src/parser.rs | 62 +++++++++++++++++++------------------ core/src/rust_types.rs | 4 +-- 4 files changed, 40 insertions(+), 55 deletions(-) diff --git a/core/src/language/kotlin.rs b/core/src/language/kotlin.rs index 1cbb0e7d..3a026f77 100644 --- a/core/src/language/kotlin.rs +++ b/core/src/language/kotlin.rs @@ -1,6 +1,6 @@ use super::{Language, ScopedCrateTypes}; use crate::language::SupportedLanguage; -use crate::parser::{remove_dash_from_identifier, ParsedData, KOTLIN_DECORATOR}; +use crate::parser::{remove_dash_from_identifier, DecoratorKind, ParsedData}; use crate::rust_types::{RustTypeFormatError, SpecialRustType}; use crate::{ rename::RenameExt, @@ -169,11 +169,7 @@ impl Language for Kotlin { writeln!(w)?; } write!(w, ")")?; - if let Some(kotlin_decorators) = rs - .decorators - .get(&SupportedLanguage::Kotlin) - .and_then(|d| d.get(KOTLIN_DECORATOR)) - { + if let Some(kotlin_decorators) = rs.decorators.get(&DecoratorKind::Kotlin) { let redacted_decorator = String::from(REDACTED_TO_STRING); if kotlin_decorators.iter().any(|d| *d == redacted_decorator) { writeln!(w, " {{")?; diff --git a/core/src/language/swift.rs b/core/src/language/swift.rs index d45008dc..490ce442 100644 --- a/core/src/language/swift.rs +++ b/core/src/language/swift.rs @@ -1,9 +1,6 @@ use crate::{ language::{Language, SupportedLanguage}, - parser::{ - remove_dash_from_identifier, ParsedData, SWIFT_DECORATOR, - SWIFT_GENERIC_CONSTRAINTS_DECORATOR, - }, + parser::{remove_dash_from_identifier, DecoratorKind, ParsedData}, rename::RenameExt, rust_types::{ DecoratorMap, RustEnum, RustEnumVariant, RustStruct, RustTypeAlias, RustTypeFormatError, @@ -272,11 +269,7 @@ impl Language for Swift { // let default_generic_constraints = self.default_generic_constraints.clone(); // Check if this struct's decorators contains swift in the hashmap - if let Some(swift_decs) = rs - .decorators - .get(&SupportedLanguage::Swift) - .and_then(|d| d.get(SWIFT_DECORATOR)) - { + if let Some(swift_decs) = rs.decorators.get(&DecoratorKind::Swift) { // For reach item in the received decorators in the typeshared struct add it to the original vector // this avoids duplicated of `Codable` without needing to `.sort()` then `.dedup()` // Note: the list received from `rs.decorators` is already deduped @@ -404,12 +397,7 @@ impl Language for Swift { .for_each(|dec| decs.push(dec)); // Check if this enum's decorators contains swift in the hashmap - if let Some(swift_decs) = e - .shared() - .decorators - .get(&SupportedLanguage::Swift) - .and_then(|d| d.get(SWIFT_DECORATOR)) - { + if let Some(swift_decs) = e.shared().decorators.get(&DecoratorKind::Swift) { // Add any decorators from the typeshared enum decs.extend( // Note: `swift_decs` is already deduped @@ -800,8 +788,7 @@ impl Swift { generic_types: &'a [String], ) -> String { let swift_generic_contraints_annotated = decorator_map - .get(&SupportedLanguage::Swift) - .and_then(|d| d.get(SWIFT_GENERIC_CONSTRAINTS_DECORATOR)) + .get(&DecoratorKind::SwiftGenericConstraints) .map(|generic_constraints| { generic_constraints .iter() diff --git a/core/src/parser.rs b/core/src/parser.rs index e13bee8a..e8c8131e 100644 --- a/core/src/parser.rs +++ b/core/src/parser.rs @@ -24,15 +24,27 @@ use thiserror::Error; const TYPESHARE: &str = "typeshare"; const SERDE: &str = "serde"; -/// The typeshare decorator name attribute. -pub type DecoratorName = &'static str; +/// Supported typeshare type level decorator attributes. +#[derive(PartialEq, Eq, Debug, Hash, Copy, Clone)] +pub enum DecoratorKind { + /// The typeshare attribute for swift type constraints "swift" + Swift, + /// The typeshare attribute for swift generic constraints "swiftGenericConstraints" + SwiftGenericConstraints, + /// The typeshare attribute for kotlin "kotlin" + Kotlin, +} -/// The typeshare attribute for swift type constraints. -pub const SWIFT_DECORATOR: DecoratorName = "swift"; -/// The typeshare attribute for kotlin. -pub const KOTLIN_DECORATOR: DecoratorName = "kotlin"; -/// The typeshare attribute for swift generic constraints. -pub const SWIFT_GENERIC_CONSTRAINTS_DECORATOR: DecoratorName = "swiftGenericConstraints"; +impl DecoratorKind { + /// This decorator as a str. + fn as_str(&self) -> &str { + match self { + DecoratorKind::Swift => "swift", + DecoratorKind::SwiftGenericConstraints => "swiftGenericConstraints", + DecoratorKind::Kotlin => "kotlin", + } + } +} /// Errors that can occur while parsing Rust source input. #[derive(Debug, Error)] @@ -691,28 +703,18 @@ fn get_decorators(attrs: &[syn::Attribute]) -> DecoratorMap { // The resulting HashMap, Key is the language, and the value is a vector of decorators words that will be put onto structures let mut out: DecoratorMap = HashMap::new(); - let add_decorators = - |name: &'static str, supported_lang: SupportedLanguage, map: &mut DecoratorMap| { - for value in get_name_value_meta_items(attrs, name, TYPESHARE) { - let constraints = || value.split(',').map(|s| s.trim().to_string()); - map.entry(supported_lang) - .and_modify(|dec_map| { - dec_map - .entry(name) - .and_modify(|dec_vals| dec_vals.extend(constraints())) - .or_insert(constraints().collect()); - }) - .or_insert(HashMap::from_iter([(name, constraints().collect())])); - } - }; - - add_decorators(SWIFT_DECORATOR, SupportedLanguage::Swift, &mut out); - add_decorators( - SWIFT_GENERIC_CONSTRAINTS_DECORATOR, - SupportedLanguage::Swift, - &mut out, - ); - add_decorators(KOTLIN_DECORATOR, SupportedLanguage::Kotlin, &mut out); + let add_decorators = |name: DecoratorKind, map: &mut DecoratorMap| { + for value in get_name_value_meta_items(attrs, name.as_str(), TYPESHARE) { + let constraints = || value.split(',').map(|s| s.trim().to_string()); + map.entry(name) + .and_modify(|dec_vals| dec_vals.extend(constraints())) + .or_insert(constraints().collect()); + } + }; + + add_decorators(DecoratorKind::Swift, &mut out); + add_decorators(DecoratorKind::SwiftGenericConstraints, &mut out); + add_decorators(DecoratorKind::Kotlin, &mut out); out } diff --git a/core/src/rust_types.rs b/core/src/rust_types.rs index 88932509..146849b0 100644 --- a/core/src/rust_types.rs +++ b/core/src/rust_types.rs @@ -6,11 +6,11 @@ use syn::{Expr, ExprLit, Lit, TypeArray, TypeSlice}; use thiserror::Error; use crate::language::SupportedLanguage; -use crate::parser::DecoratorName; +use crate::parser::DecoratorKind; use crate::visitors::accept_type; /// Type level typeshare attributes are mapped by target language and a mapping of attribute. -pub type DecoratorMap = HashMap>>; +pub type DecoratorMap = HashMap>; /// Identifier used in Rust structs, enums, and fields. It includes the `original` name and the `renamed` value after the transformation based on `serde` attributes. #[derive(Debug, Clone, PartialEq)] From a38d41dd38a78ee39eb05041a9faa05ada4d43db Mon Sep 17 00:00:00 2001 From: Darrell Roberts Date: Wed, 5 Jun 2024 17:36:52 -0400 Subject: [PATCH 08/24] refactor --- core/src/parser.rs | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/core/src/parser.rs b/core/src/parser.rs index e8c8131e..49fac29c 100644 --- a/core/src/parser.rs +++ b/core/src/parser.rs @@ -701,22 +701,23 @@ fn literal_to_string(lit: &syn::Lit) -> Option { /// Takes a slice of `syn::Attribute`, returns a `HashMap>`, where `language` is `SupportedLanguage` and `decoration_words` is `String` fn get_decorators(attrs: &[syn::Attribute]) -> DecoratorMap { // The resulting HashMap, Key is the language, and the value is a vector of decorators words that will be put onto structures - let mut out: DecoratorMap = HashMap::new(); - - let add_decorators = |name: DecoratorKind, map: &mut DecoratorMap| { - for value in get_name_value_meta_items(attrs, name.as_str(), TYPESHARE) { + let mut decorator_map: DecoratorMap = HashMap::new(); + + for decorator_kind in [ + DecoratorKind::Swift, + DecoratorKind::SwiftGenericConstraints, + DecoratorKind::Kotlin, + ] { + for value in get_name_value_meta_items(attrs, decorator_kind.as_str(), TYPESHARE) { let constraints = || value.split(',').map(|s| s.trim().to_string()); - map.entry(name) + decorator_map + .entry(decorator_kind) .and_modify(|dec_vals| dec_vals.extend(constraints())) .or_insert(constraints().collect()); } - }; - - add_decorators(DecoratorKind::Swift, &mut out); - add_decorators(DecoratorKind::SwiftGenericConstraints, &mut out); - add_decorators(DecoratorKind::Kotlin, &mut out); + } - out + decorator_map } fn get_tag_key(attrs: &[syn::Attribute]) -> Option { From cc894cda1603fcbef2aded4e40795af508d036a0 Mon Sep 17 00:00:00 2001 From: Darrell Roberts Date: Wed, 5 Jun 2024 17:38:49 -0400 Subject: [PATCH 09/24] Update code comment for changed implementation --- core/src/parser.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/core/src/parser.rs b/core/src/parser.rs index 49fac29c..7e2d549a 100644 --- a/core/src/parser.rs +++ b/core/src/parser.rs @@ -698,9 +698,8 @@ fn literal_to_string(lit: &syn::Lit) -> Option { } /// Checks the struct or enum for decorators like `#[typeshare(swift = "Codable, Equatable")]` -/// Takes a slice of `syn::Attribute`, returns a `HashMap>`, where `language` is `SupportedLanguage` and `decoration_words` is `String` +/// Takes a slice of `syn::Attribute`, returns a [`DecoratorMap`]. fn get_decorators(attrs: &[syn::Attribute]) -> DecoratorMap { - // The resulting HashMap, Key is the language, and the value is a vector of decorators words that will be put onto structures let mut decorator_map: DecoratorMap = HashMap::new(); for decorator_kind in [ From 42c9e704cbe710300177b31c05836479949a4e98 Mon Sep 17 00:00:00 2001 From: Darrell Roberts Date: Wed, 5 Jun 2024 21:30:16 -0400 Subject: [PATCH 10/24] Iterate over type generics and combine swiftGenericConstraints generics and non annotated generics --- core/src/language/swift.rs | 61 +++++++++++++++----------------------- 1 file changed, 24 insertions(+), 37 deletions(-) diff --git a/core/src/language/swift.rs b/core/src/language/swift.rs index 490ce442..0797b4d9 100644 --- a/core/src/language/swift.rs +++ b/core/src/language/swift.rs @@ -8,7 +8,7 @@ use crate::{ }, GenerationError, }; -use itertools::Itertools; +use itertools::{Either, Itertools}; use joinery::JoinableIterator; use lazy_format::lazy_format; use std::{ @@ -113,8 +113,8 @@ impl GenericConstraints { } } /// Get an iterator over all constraints. - pub fn get_constraints(&self) -> impl Iterator { - self.constraints.iter() + pub fn get_constraints(&self) -> impl Iterator { + self.constraints.iter().map(|s| s.as_str()) } fn split_constraints(constraints: String) -> Vec { @@ -792,51 +792,38 @@ impl Swift { .map(|generic_constraints| { generic_constraints .iter() - .flat_map(|generic_constraint| { + .filter_map(|generic_constraint| { let mut gen_name_val_iter = generic_constraint.split(':'); let generic_name = gen_name_val_iter.next()?; - let generic_name_constraints = gen_name_val_iter + let mut generic_name_constraints = gen_name_val_iter .next()? .split('&') .map(|s| s.trim()) .collect::>(); + // Merge default generic constraints with annotated constraints. + generic_name_constraints + .extend(self.default_generic_constraints.get_constraints()); Some((generic_name, generic_name_constraints)) }) - .map(|(gen_name, mut gen_constraints)| { - gen_constraints.extend( - self.default_generic_constraints - .get_constraints() - .map(|s| s.as_str()), - ); - (gen_name, gen_constraints) - }) - .collect::>() + .collect::>() }) .unwrap_or_default(); - if swift_generic_contraints_annotated.is_empty() { - generic_types - .iter() - .map(|type_name| { - format!( - "{type_name}: {}", - self.default_generic_constraints - .get_constraints() - .join(" & ") - ) - }) - .join(", ") - } else { - swift_generic_contraints_annotated - .iter() - .map(|(type_name, constraints)| { - format!( - "{type_name}: {constraints}", - constraints = constraints.iter().join(" & ") - ) - }) - .join(", ") - } + generic_types + .iter() + .map( + |type_name| match swift_generic_contraints_annotated.get(type_name.as_str()) { + // Use constraints from swiftGenericConstraints decorator. + Some(constraints) => (type_name, Either::Left(constraints.iter().copied())), + // Use the default generic constraints if it is not part of a swiftGenericConstraints decorator. + None => ( + type_name, + Either::Right(self.default_generic_constraints.get_constraints()), + ), + }, + ) + .map(|(type_name, mut constraints)| format!("{type_name}: {}", constraints.join(" & "))) + .join(", ") } } From becbe5f738346a1318dc2150244076d0c339047c Mon Sep 17 00:00:00 2001 From: Darrell Roberts Date: Thu, 6 Jun 2024 07:49:08 -0400 Subject: [PATCH 11/24] Update test to include generic not mentioned in swiftGenericConstraints --- .../input.rs | 10 ++++++++-- .../output.swift | 14 +++++++++++--- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/core/data/tests/generic_struct_with_constraints_and_decorators/input.rs b/core/data/tests/generic_struct_with_constraints_and_decorators/input.rs index 0ce78a36..ecc31e75 100644 --- a/core/data/tests/generic_struct_with_constraints_and_decorators/input.rs +++ b/core/data/tests/generic_struct_with_constraints_and_decorators/input.rs @@ -2,9 +2,9 @@ swift = "Equatable, Identifiable", swiftGenericConstraints = "T: Equatable & SomeThingElse, V: Equatable" )] -pub struct Button { +pub struct Button { /// Label of the button - pub label: String, + pub label: I, /// Accessibility label if it needed to be different than label pub accessibility_label: Option, /// Optional tooltips that provide extra explanation for a button @@ -18,3 +18,9 @@ pub struct Button { /// Button Mode pub style: ButtonStyle, } + +#[typeshare] +pub struct ButtonState; + +#[typeshare] +pub struct ButtonStyle; diff --git a/core/data/tests/generic_struct_with_constraints_and_decorators/output.swift b/core/data/tests/generic_struct_with_constraints_and_decorators/output.swift index 46834b7e..607f7875 100644 --- a/core/data/tests/generic_struct_with_constraints_and_decorators/output.swift +++ b/core/data/tests/generic_struct_with_constraints_and_decorators/output.swift @@ -1,8 +1,16 @@ import Foundation -public struct Button: Codable, Equatable, Identifiable { +public struct ButtonState: Codable { + public init() {} +} + +public struct ButtonStyle: Codable { + public init() {} +} + +public struct Button: Codable, Equatable, Identifiable { /// Label of the button - public let label: String + public let label: I /// Accessibility label if it needed to be different than label public let accessibility_label: String? /// Optional tooltips that provide extra explanation for a button @@ -16,7 +24,7 @@ public struct Button Date: Thu, 6 Jun 2024 08:15:22 -0400 Subject: [PATCH 12/24] Refactor --- core/src/parser.rs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/core/src/parser.rs b/core/src/parser.rs index 7e2d549a..0f6b0c2a 100644 --- a/core/src/parser.rs +++ b/core/src/parser.rs @@ -700,7 +700,7 @@ fn literal_to_string(lit: &syn::Lit) -> Option { /// Checks the struct or enum for decorators like `#[typeshare(swift = "Codable, Equatable")]` /// Takes a slice of `syn::Attribute`, returns a [`DecoratorMap`]. fn get_decorators(attrs: &[syn::Attribute]) -> DecoratorMap { - let mut decorator_map: DecoratorMap = HashMap::new(); + let mut decorator_map: DecoratorMap = DecoratorMap::new(); for decorator_kind in [ DecoratorKind::Swift, @@ -708,11 +708,10 @@ fn get_decorators(attrs: &[syn::Attribute]) -> DecoratorMap { DecoratorKind::Kotlin, ] { for value in get_name_value_meta_items(attrs, decorator_kind.as_str(), TYPESHARE) { - let constraints = || value.split(',').map(|s| s.trim().to_string()); decorator_map .entry(decorator_kind) - .and_modify(|dec_vals| dec_vals.extend(constraints())) - .or_insert(constraints().collect()); + .or_default() + .extend(value.split(',').map(|s| s.trim().to_string())); } } From f1a3b87aa4f904e3677ec87187170b7c8ef08bd2 Mon Sep 17 00:00:00 2001 From: Darrell Roberts Date: Thu, 6 Jun 2024 09:02:28 -0400 Subject: [PATCH 13/24] Make constraints on CodableVoid a config option. Remove excessive allocations. --- cli/src/config.rs | 2 ++ cli/src/main.rs | 1 + core/src/language/swift.rs | 35 +++++++++++++++++------------------ core/tests/snapshot_tests.rs | 5 +++-- 4 files changed, 23 insertions(+), 20 deletions(-) diff --git a/cli/src/config.rs b/cli/src/config.rs index eac8c5eb..caac7e54 100644 --- a/cli/src/config.rs +++ b/cli/src/config.rs @@ -34,6 +34,8 @@ pub struct SwiftParams { pub type_mappings: HashMap, pub default_decorators: Vec, pub default_generic_constraints: Vec, + /// The contraints to apply to `CodableVoid`. + pub codablevoid_constraints: Vec, } #[derive(Default, Serialize, Deserialize, PartialEq, Eq, Debug)] diff --git a/cli/src/main.rs b/cli/src/main.rs index c914f396..981c23a3 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -146,6 +146,7 @@ fn language( config.swift.default_generic_constraints, ), multi_file, + codablevoid_constraints: config.swift.codablevoid_constraints, ..Default::default() }), SupportedLanguage::Kotlin => Box::new(Kotlin { diff --git a/core/src/language/swift.rs b/core/src/language/swift.rs index 0797b4d9..84b5a242 100644 --- a/core/src/language/swift.rs +++ b/core/src/language/swift.rs @@ -151,6 +151,8 @@ pub struct Swift { pub no_version_header: bool, /// Are we generating mutliple modules? pub multi_file: bool, + /// The contraints to apply to `CodableVoid`. + pub codablevoid_constraints: Vec, } impl Language for Swift { @@ -276,7 +278,7 @@ impl Language for Swift { swift_decs .iter() .filter(|d| d.as_str() != CODABLE) - .for_each(|d| decs.push(d.clone())); + .for_each(|d| decs.push(d)); } let generic_names_and_constraints = @@ -387,14 +389,11 @@ impl Language for Swift { fn write_enum(&mut self, w: &mut dyn Write, e: &RustEnum) -> io::Result<()> { /// Determines the decorators needed for an enum given an array of decorators /// that should always be present - fn determine_decorators(always_present: &[String], e: &RustEnum) -> Vec { - let mut decs = vec![]; + fn determine_decorators<'a>(always_present: &'a [&str], e: &'a RustEnum) -> Vec<&'a str> { + let mut decs: Vec<&str> = vec![]; // Add the decorators that should always be present - always_present - .iter() - .cloned() - .for_each(|dec| decs.push(dec)); + decs.extend(always_present); // Check if this enum's decorators contains swift in the hashmap if let Some(swift_decs) = e.shared().decorators.get(&DecoratorKind::Swift) { @@ -404,8 +403,8 @@ impl Language for Swift { swift_decs .iter() // Avoids needing to sort / dedup - .filter(|d| !always_present.contains(d)) - .map(|d| d.to_owned()), + .filter(|d| !always_present.contains(&d.as_str())) + .map(|d| d.as_str()), ); } @@ -417,13 +416,13 @@ impl Language for Swift { swift_keyword_aware_rename(&format!("{}{}", self.prefix, shared.id.renamed)); let always_present = match e { RustEnum::Unit(_) => { - let mut always_present = vec!["String".into()]; + let mut always_present = vec!["String"]; always_present.append(&mut self.get_default_decorators()); always_present } RustEnum::Algebraic { .. } => self.get_default_decorators(), }; - let decs = determine_decorators(&always_present, e); + let decs = determine_decorators(&always_present, e).join(", "); // Make a suitable name for an anonymous struct enum variant let make_anonymous_struct_name = |variant_name: &str| format!("{}{}Inner", shared.id.renamed, variant_name); @@ -445,7 +444,7 @@ impl Language for Swift { (!e.shared().generic_types.is_empty()) .then(|| format!("<{generic_names_and_constraints}>",)) .unwrap_or_default(), - decs.join(", ") + decs )?; let coding_keys_info = self.write_enum_variants(w, e, make_anonymous_struct_name)?; @@ -745,9 +744,9 @@ impl Swift { } impl Swift { - fn get_default_decorators(&self) -> Vec { - let mut decs: Vec = vec![CODABLE.to_string()]; - decs.extend(self.default_decorators.iter().cloned()); + fn get_default_decorators(&self) -> Vec<&str> { + let mut decs = vec![CODABLE]; + decs.extend(self.default_decorators.iter().map(|s| s.as_str())); decs } @@ -769,11 +768,11 @@ impl Swift { let mut decs = self.get_default_decorators(); // Unit type can be used as generic impl constrained to Equatable. - decs.push("Equatable".into()); + decs.extend(self.codablevoid_constraints.iter().map(|s| s.as_str())); // If there are no decorators found for this struct, still write `Codable` and default decorators for structs - if !decs.contains(&CODABLE.to_string()) { - decs.push(CODABLE.to_string()); + if !decs.contains(&CODABLE) { + decs.push(CODABLE); } writeln!(w, "public struct CodableVoid: {} {{}}", decs.join(", ")) diff --git a/core/tests/snapshot_tests.rs b/core/tests/snapshot_tests.rs index 346d3b89..3bca4114 100644 --- a/core/tests/snapshot_tests.rs +++ b/core/tests/snapshot_tests.rs @@ -383,6 +383,7 @@ tests! { can_generate_generic_struct: [ swift { prefix: "Core".into(), + codablevoid_constraints: vec!["Equatable".into()] }, kotlin, scala, @@ -542,7 +543,7 @@ tests! { generate_types_with_keywords: [swift]; // TODO: how is this different from generates_empty_structs_and_initializers? use_correct_decoded_variable_name: [swift, kotlin, scala, typescript, go]; - can_handle_unit_type: [swift, kotlin, scala, typescript, go]; + can_handle_unit_type: [swift { codablevoid_constraints: vec!["Equatable".into()]} , kotlin, scala, typescript, go]; //3 tests for adding decorators to enums and structs const_enum_decorator: [ swift{ prefix: "OP".to_string(), } ]; @@ -569,5 +570,5 @@ tests! { go ]; can_generate_anonymous_struct_with_skipped_fields: [swift, kotlin, scala, typescript, go]; - generic_struct_with_constraints_and_decorators: [swift]; + generic_struct_with_constraints_and_decorators: [swift { codablevoid_constraints: vec!["Equatable".into()]}]; } From b9a22aec65a7ea39cec6bf540ad25e31b54a0df2 Mon Sep 17 00:00:00 2001 From: Darrell Roberts Date: Thu, 6 Jun 2024 09:33:33 -0400 Subject: [PATCH 14/24] reduce allocations --- core/src/language/swift.rs | 87 ++++++++++++++++++++------------------ 1 file changed, 45 insertions(+), 42 deletions(-) diff --git a/core/src/language/swift.rs b/core/src/language/swift.rs index 84b5a242..c0c7c3c4 100644 --- a/core/src/language/swift.rs +++ b/core/src/language/swift.rs @@ -267,19 +267,23 @@ impl Language for Swift { let type_name = swift_keyword_aware_rename(&format!("{}{}", self.prefix, rs.id.renamed)); // If there are no decorators found for this struct, still write `Codable` and default decorators for structs - let mut decs = self.get_default_decorators(); - - // let default_generic_constraints = self.default_generic_constraints.clone(); // Check if this struct's decorators contains swift in the hashmap - if let Some(swift_decs) = rs.decorators.get(&DecoratorKind::Swift) { + let decs = if let Some(swift_decs) = rs.decorators.get(&DecoratorKind::Swift) { // For reach item in the received decorators in the typeshared struct add it to the original vector // this avoids duplicated of `Codable` without needing to `.sort()` then `.dedup()` // Note: the list received from `rs.decorators` is already deduped - swift_decs - .iter() - .filter(|d| d.as_str() != CODABLE) - .for_each(|d| decs.push(d)); + Either::Left( + self.get_default_decorators().chain( + swift_decs + .iter() + .filter(|d| d.as_str() != CODABLE) + .map(|s| s.as_str()), + ), + ) + } else { + Either::Right(self.get_default_decorators()) } + .join(", "); let generic_names_and_constraints = self.generic_constraints(&rs.decorators, &rs.generic_types); @@ -290,7 +294,7 @@ impl Language for Swift { (!rs.generic_types.is_empty()) .then(|| format!("<{generic_names_and_constraints}>",)) .unwrap_or_default(), - decs.join(", ") + decs )?; for f in &rs.fields { @@ -389,40 +393,39 @@ impl Language for Swift { fn write_enum(&mut self, w: &mut dyn Write, e: &RustEnum) -> io::Result<()> { /// Determines the decorators needed for an enum given an array of decorators /// that should always be present - fn determine_decorators<'a>(always_present: &'a [&str], e: &'a RustEnum) -> Vec<&'a str> { - let mut decs: Vec<&str> = vec![]; - - // Add the decorators that should always be present - decs.extend(always_present); - - // Check if this enum's decorators contains swift in the hashmap - if let Some(swift_decs) = e.shared().decorators.get(&DecoratorKind::Swift) { - // Add any decorators from the typeshared enum - decs.extend( + fn determine_decorators<'a>( + always_present: &'a [&str], + e: &'a RustEnum, + ) -> impl Iterator { + always_present.iter().copied().chain( + if let Some(swift_decs) = e.shared().decorators.get(&DecoratorKind::Swift) { + // Add any decorators from the typeshared enum // Note: `swift_decs` is already deduped - swift_decs - .iter() - // Avoids needing to sort / dedup - .filter(|d| !always_present.contains(&d.as_str())) - .map(|d| d.as_str()), - ); - } - - decs + Either::Left( + swift_decs + .iter() + .map(|s| s.as_str()) + // Avoids needing to sort / dedup + .filter(|d| !always_present.contains(d)), + ) + } else { + Either::Right(std::iter::empty()) + }, + ) } let shared = e.shared(); let enum_name = swift_keyword_aware_rename(&format!("{}{}", self.prefix, shared.id.renamed)); let always_present = match e { - RustEnum::Unit(_) => { - let mut always_present = vec!["String"]; - always_present.append(&mut self.get_default_decorators()); - always_present - } - RustEnum::Algebraic { .. } => self.get_default_decorators(), + RustEnum::Unit(_) => ["String"] + .into_iter() + .chain(self.get_default_decorators()) + .collect::>(), + RustEnum::Algebraic { .. } => self.get_default_decorators().collect::>(), }; let decs = determine_decorators(&always_present, e).join(", "); + // Make a suitable name for an anonymous struct enum variant let make_anonymous_struct_name = |variant_name: &str| format!("{}{}Inner", shared.id.renamed, variant_name); @@ -744,10 +747,10 @@ impl Swift { } impl Swift { - fn get_default_decorators(&self) -> Vec<&str> { - let mut decs = vec![CODABLE]; - decs.extend(self.default_decorators.iter().map(|s| s.as_str())); - decs + fn get_default_decorators(&self) -> impl Iterator { + [CODABLE] + .into_iter() + .chain(self.default_decorators.iter().map(|s| s.as_str())) } /// When using mulitple file generation we write this into a separate module vs at the @@ -765,10 +768,10 @@ impl Swift { r"/// () isn't codable, so we use this instead to represent Rust's unit type" )?; - let mut decs = self.get_default_decorators(); - - // Unit type can be used as generic impl constrained to Equatable. - decs.extend(self.codablevoid_constraints.iter().map(|s| s.as_str())); + let mut decs = self + .get_default_decorators() + .chain(self.codablevoid_constraints.iter().map(|s| s.as_str())) + .collect::>(); // If there are no decorators found for this struct, still write `Codable` and default decorators for structs if !decs.contains(&CODABLE) { From 8ab96794592ccb5ac537880de364c37cc74d528b Mon Sep 17 00:00:00 2001 From: Darrell Roberts Date: Thu, 6 Jun 2024 16:55:55 -0400 Subject: [PATCH 15/24] Reduce allocations --- core/src/language/swift.rs | 69 ++++++++++++++++++++++++++------------ 1 file changed, 47 insertions(+), 22 deletions(-) diff --git a/core/src/language/swift.rs b/core/src/language/swift.rs index c0c7c3c4..32998de1 100644 --- a/core/src/language/swift.rs +++ b/core/src/language/swift.rs @@ -12,6 +12,7 @@ use itertools::{Either, Itertools}; use joinery::JoinableIterator; use lazy_format::lazy_format; use std::{ + borrow::Cow, collections::{BTreeSet, HashMap}, fs::File, io::{self, Write}, @@ -241,7 +242,8 @@ impl Language for Swift { self.write_comments(w, 0, &ty.comments)?; let swift_prefix = &self.prefix; - let type_name = swift_keyword_aware_rename(&format!("{}{}", swift_prefix, ty.id.renamed)); + let type_name = + swift_keyword_aware_rename(Cow::Owned(format!("{}{}", swift_prefix, ty.id.renamed))); writeln!( w, @@ -264,7 +266,8 @@ impl Language for Swift { writeln!(w)?; self.write_comments(w, 0, &rs.comments)?; - let type_name = swift_keyword_aware_rename(&format!("{}{}", self.prefix, rs.id.renamed)); + let type_name = + swift_keyword_aware_rename(Cow::Owned(format!("{}{}", self.prefix, rs.id.renamed))); // If there are no decorators found for this struct, still write `Codable` and default decorators for structs // Check if this struct's decorators contains swift in the hashmap @@ -307,7 +310,9 @@ impl Language for Swift { if f.id.renamed.chars().any(|c| c == '-') { coding_keys.push(format!( r##"{} = "{}""##, - remove_dash_from_identifier(&swift_keyword_aware_rename(&f.id.renamed)), + remove_dash_from_identifier( + swift_keyword_aware_rename(Cow::Borrowed(&f.id.renamed)).as_ref() + ), &f.id.renamed )); @@ -315,9 +320,9 @@ impl Language for Swift { // situation like this should_write_coding_keys = true; } else { - coding_keys.push(remove_dash_from_identifier(&swift_keyword_aware_rename( - &f.id.renamed, - ))); + coding_keys.push(remove_dash_from_identifier( + swift_keyword_aware_rename(Cow::Borrowed(&f.id.renamed)).as_ref(), + )); } let case_type: String = match f.type_override(SupportedLanguage::Swift) { @@ -330,7 +335,9 @@ impl Language for Swift { writeln!( w, "\tpublic let {}: {}{}", - remove_dash_from_identifier(&swift_keyword_aware_rename(&f.id.renamed)), + remove_dash_from_identifier( + swift_keyword_aware_rename(Cow::Borrowed(&f.id.renamed)).as_ref() + ), case_type, (f.has_default && !f.ty.is_optional()) .then_some("?") @@ -378,7 +385,9 @@ impl Language for Swift { w, "\n\t\tself.{} = {}", remove_dash_from_identifier(&f.id.renamed), - remove_dash_from_identifier(&swift_keyword_aware_rename(&f.id.renamed)) + remove_dash_from_identifier( + swift_keyword_aware_rename(Cow::Borrowed(&f.id.renamed)).as_ref() + ) )?; } if !rs.fields.is_empty() { @@ -416,7 +425,7 @@ impl Language for Swift { let shared = e.shared(); let enum_name = - swift_keyword_aware_rename(&format!("{}{}", self.prefix, shared.id.renamed)); + swift_keyword_aware_rename(Cow::Owned(format!("{}{}", self.prefix, shared.id.renamed))); let always_present = match e { RustEnum::Unit(_) => ["String"] .into_iter() @@ -551,13 +560,17 @@ impl Swift { self.write_comments(w, 1, &v.shared().comments)?; if v.shared().id.renamed == variant_name { // We don't need to handle any renaming - writeln!(w, "\tcase {}", &swift_keyword_aware_rename(&variant_name))?; + writeln!( + w, + "\tcase {}", + &swift_keyword_aware_rename(Cow::Borrowed(&variant_name)) + )?; } else { // We do need to handle renaming writeln!( w, "\tcase {} = {:?}", - swift_keyword_aware_rename(&variant_name), + swift_keyword_aware_rename(Cow::Borrowed(&variant_name)), &v.shared().id.renamed )?; } @@ -590,16 +603,20 @@ impl Swift { }; coding_keys.push(if variant_name == v.shared().id.renamed { - swift_keyword_aware_rename(&variant_name) + swift_keyword_aware_rename(Cow::Borrowed(&variant_name)).into_owned() } else { format!( r##"{} = "{}""##, - swift_keyword_aware_rename(&variant_name), + swift_keyword_aware_rename(Cow::Borrowed(&variant_name)), &v.shared().id.renamed ) }); - write!(w, "\tcase {}", swift_keyword_aware_rename(&variant_name))?; + write!( + w, + "\tcase {}", + swift_keyword_aware_rename(Cow::Borrowed(&variant_name)) + )?; match v { RustEnumVariant::Unit(_) => { @@ -616,7 +633,8 @@ impl Swift { case .{case_name}: try container.encode(CodingKeys.{case_name}, forKey: .{tag_key})", tag_key = tag_key, - case_name = swift_keyword_aware_rename(&variant_name), + case_name = + swift_keyword_aware_rename(Cow::Borrowed(&variant_name)), )); } RustEnumVariant::Tuple { ty, .. } => { @@ -624,7 +642,11 @@ impl Swift { let case_type = self .format_type(ty, e.shared().generic_types.as_slice()) .map_err(|e| io::Error::new(io::ErrorKind::Other, e))?; - write!(w, "({})", swift_keyword_aware_rename(&case_type))?; + write!( + w, + "({})", + swift_keyword_aware_rename(Cow::Borrowed(&case_type)) + )?; if content_optional { decoding_cases.push(format!( @@ -639,7 +661,8 @@ impl Swift { return }}", content_key = content_key, - case_type = swift_keyword_aware_rename(&case_type), + case_type = + swift_keyword_aware_rename(Cow::Borrowed(&case_type)), case_name = &variant_name )) } else { @@ -651,7 +674,8 @@ impl Swift { return }}", content_key = content_key, - case_type = swift_keyword_aware_rename(&case_type), + case_type = + swift_keyword_aware_rename(Cow::Borrowed(&case_type)), case_name = &variant_name, )); } @@ -829,9 +853,10 @@ impl Swift { } } -fn swift_keyword_aware_rename(name: &str) -> String { - if SWIFT_KEYWORDS.contains(&name) { - return format!("`{}`", name); +fn swift_keyword_aware_rename(name: Cow) -> Cow { + if SWIFT_KEYWORDS.contains(&name.as_ref()) { + Cow::Owned(format!("`{name}`")) + } else { + name } - name.to_string() } From db5dcae62f2e6daabbfa045be01a3b15ec0307e8 Mon Sep 17 00:00:00 2001 From: Darrell Roberts Date: Thu, 6 Jun 2024 18:31:35 -0400 Subject: [PATCH 16/24] Cleanup --- core/src/language/swift.rs | 62 +++++++++++++------------------------- 1 file changed, 21 insertions(+), 41 deletions(-) diff --git a/core/src/language/swift.rs b/core/src/language/swift.rs index 32998de1..984ebe14 100644 --- a/core/src/language/swift.rs +++ b/core/src/language/swift.rs @@ -242,8 +242,7 @@ impl Language for Swift { self.write_comments(w, 0, &ty.comments)?; let swift_prefix = &self.prefix; - let type_name = - swift_keyword_aware_rename(Cow::Owned(format!("{}{}", swift_prefix, ty.id.renamed))); + let type_name = swift_keyword_aware_rename(format!("{}{}", swift_prefix, ty.id.renamed)); writeln!( w, @@ -266,8 +265,7 @@ impl Language for Swift { writeln!(w)?; self.write_comments(w, 0, &rs.comments)?; - let type_name = - swift_keyword_aware_rename(Cow::Owned(format!("{}{}", self.prefix, rs.id.renamed))); + let type_name = swift_keyword_aware_rename(format!("{}{}", self.prefix, rs.id.renamed)); // If there are no decorators found for this struct, still write `Codable` and default decorators for structs // Check if this struct's decorators contains swift in the hashmap @@ -310,9 +308,7 @@ impl Language for Swift { if f.id.renamed.chars().any(|c| c == '-') { coding_keys.push(format!( r##"{} = "{}""##, - remove_dash_from_identifier( - swift_keyword_aware_rename(Cow::Borrowed(&f.id.renamed)).as_ref() - ), + remove_dash_from_identifier(swift_keyword_aware_rename(&f.id.renamed).as_ref()), &f.id.renamed )); @@ -321,7 +317,7 @@ impl Language for Swift { should_write_coding_keys = true; } else { coding_keys.push(remove_dash_from_identifier( - swift_keyword_aware_rename(Cow::Borrowed(&f.id.renamed)).as_ref(), + swift_keyword_aware_rename(&f.id.renamed).as_ref(), )); } @@ -335,9 +331,7 @@ impl Language for Swift { writeln!( w, "\tpublic let {}: {}{}", - remove_dash_from_identifier( - swift_keyword_aware_rename(Cow::Borrowed(&f.id.renamed)).as_ref() - ), + remove_dash_from_identifier(swift_keyword_aware_rename(&f.id.renamed).as_ref()), case_type, (f.has_default && !f.ty.is_optional()) .then_some("?") @@ -385,9 +379,7 @@ impl Language for Swift { w, "\n\t\tself.{} = {}", remove_dash_from_identifier(&f.id.renamed), - remove_dash_from_identifier( - swift_keyword_aware_rename(Cow::Borrowed(&f.id.renamed)).as_ref() - ) + remove_dash_from_identifier(swift_keyword_aware_rename(&f.id.renamed).as_ref()) )?; } if !rs.fields.is_empty() { @@ -424,8 +416,7 @@ impl Language for Swift { } let shared = e.shared(); - let enum_name = - swift_keyword_aware_rename(Cow::Owned(format!("{}{}", self.prefix, shared.id.renamed))); + let enum_name = swift_keyword_aware_rename(format!("{}{}", self.prefix, shared.id.renamed)); let always_present = match e { RustEnum::Unit(_) => ["String"] .into_iter() @@ -560,17 +551,13 @@ impl Swift { self.write_comments(w, 1, &v.shared().comments)?; if v.shared().id.renamed == variant_name { // We don't need to handle any renaming - writeln!( - w, - "\tcase {}", - &swift_keyword_aware_rename(Cow::Borrowed(&variant_name)) - )?; + writeln!(w, "\tcase {}", &swift_keyword_aware_rename(&variant_name))?; } else { // We do need to handle renaming writeln!( w, "\tcase {} = {:?}", - swift_keyword_aware_rename(Cow::Borrowed(&variant_name)), + swift_keyword_aware_rename(&variant_name), &v.shared().id.renamed )?; } @@ -603,20 +590,16 @@ impl Swift { }; coding_keys.push(if variant_name == v.shared().id.renamed { - swift_keyword_aware_rename(Cow::Borrowed(&variant_name)).into_owned() + swift_keyword_aware_rename(&variant_name).into_owned() } else { format!( r##"{} = "{}""##, - swift_keyword_aware_rename(Cow::Borrowed(&variant_name)), + swift_keyword_aware_rename(&variant_name), &v.shared().id.renamed ) }); - write!( - w, - "\tcase {}", - swift_keyword_aware_rename(Cow::Borrowed(&variant_name)) - )?; + write!(w, "\tcase {}", swift_keyword_aware_rename(&variant_name))?; match v { RustEnumVariant::Unit(_) => { @@ -633,8 +616,7 @@ impl Swift { case .{case_name}: try container.encode(CodingKeys.{case_name}, forKey: .{tag_key})", tag_key = tag_key, - case_name = - swift_keyword_aware_rename(Cow::Borrowed(&variant_name)), + case_name = swift_keyword_aware_rename(&variant_name), )); } RustEnumVariant::Tuple { ty, .. } => { @@ -642,11 +624,7 @@ impl Swift { let case_type = self .format_type(ty, e.shared().generic_types.as_slice()) .map_err(|e| io::Error::new(io::ErrorKind::Other, e))?; - write!( - w, - "({})", - swift_keyword_aware_rename(Cow::Borrowed(&case_type)) - )?; + write!(w, "({})", swift_keyword_aware_rename(&case_type))?; if content_optional { decoding_cases.push(format!( @@ -661,8 +639,7 @@ impl Swift { return }}", content_key = content_key, - case_type = - swift_keyword_aware_rename(Cow::Borrowed(&case_type)), + case_type = swift_keyword_aware_rename(&case_type), case_name = &variant_name )) } else { @@ -674,8 +651,7 @@ impl Swift { return }}", content_key = content_key, - case_type = - swift_keyword_aware_rename(Cow::Borrowed(&case_type)), + case_type = swift_keyword_aware_rename(&case_type), case_name = &variant_name, )); } @@ -853,7 +829,11 @@ impl Swift { } } -fn swift_keyword_aware_rename(name: Cow) -> Cow { +fn swift_keyword_aware_rename<'a, T>(name: T) -> Cow<'a, str> +where + T: Into>, +{ + let name = name.into(); if SWIFT_KEYWORDS.contains(&name.as_ref()) { Cow::Owned(format!("`{name}`")) } else { From 890786816fdb0dd9a756e0d1c759d487cd125b35 Mon Sep 17 00:00:00 2001 From: Charles Pierce Date: Wed, 12 Jun 2024 12:02:55 -0700 Subject: [PATCH 17/24] chore: update changelogs and bump versions for v1.10.0-beta.5 (#178) --- CHANGELOG.md | 7 ++++++- Cargo.lock | 4 ++-- cli/Cargo.toml | 4 ++-- core/Cargo.toml | 2 +- 4 files changed, 11 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 34c4bf17..722934ce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Version 1.10.0-beta +## 1.10.0-beta.5 + +- Added support for Swift generic constraints via `#[typeshare(swiftGenericConstraints)]` [#174](https://github.com/1Password/typeshare/pull/174) +- Added Swift config option for defining constraints on `CodableVoid` generated type [#174](https://github.com/1Password/typeshare/pull/174) + ## 1.10.0-beta.4 Fixed a bug involving `#[typeshare(skip)]` on fields in struct variants of enums. @@ -14,7 +19,7 @@ This release brings support for multiple file generation, allowing splitting gen files when used in large projects. This can dramatically increase compilation speed of the generated files and increase maintainability. -This is a *pre-release* version which may have bugs or break compatibility. +This is a _pre-release_ version which may have bugs or break compatibility. - Multiple file output [#166](https://github.com/1Password/typeshare/pull/166) diff --git a/Cargo.lock b/Cargo.lock index 858b49ec..185310d4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -610,7 +610,7 @@ dependencies = [ [[package]] name = "typeshare-cli" -version = "1.10.0-beta.4" +version = "1.10.0-beta.5" dependencies = [ "anyhow", "clap", @@ -625,7 +625,7 @@ dependencies = [ [[package]] name = "typeshare-core" -version = "1.10.0-beta.4" +version = "1.10.0-beta.5" dependencies = [ "anyhow", "cool_asserts", diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 9d123591..4e551f71 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "typeshare-cli" -version = "1.10.0-beta.4" +version = "1.10.0-beta.5" edition = "2021" description = "Command Line Tool for generating language files with typeshare" license = "MIT OR Apache-2.0" @@ -22,5 +22,5 @@ once_cell = "1" rayon = "1.10" serde = { version = "1", features = ["derive"] } toml = "0.8" -typeshare-core = { path = "../core", version = "1.10.0-beta.4" } +typeshare-core = { path = "../core", version = "1.10.0-beta.5" } anyhow = "1" diff --git a/core/Cargo.toml b/core/Cargo.toml index c0d16dc5..cb937233 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "typeshare-core" -version = "1.10.0-beta.4" +version = "1.10.0-beta.5" license = "MIT OR Apache-2.0" edition = "2021" description = "The code generator used by Typeshare's command line tool" From 49e402e6d16d1f019a1d0b30d71f123ae3bab431 Mon Sep 17 00:00:00 2001 From: Darrell Roberts <33698065+darrell-roberts@users.noreply.github.com> Date: Fri, 28 Jun 2024 13:24:22 -0400 Subject: [PATCH 18/24] Allow skipping fields/variants via target_os argument (#176) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Allow passing a target_os arg for applying filter to #[cfg(target_os = "target"] that do not match Co-authored-by: René Léveillé --- cli/src/args.rs | 7 + cli/src/config.rs | 2 + cli/src/main.rs | 14 +- cli/src/parse.rs | 83 ++++-------- cli/src/writer.rs | 5 +- core/Cargo.toml | 1 + .../data/tests/excluded_by_target_os/input.rs | 34 +++++ .../tests/excluded_by_target_os/output.go | 12 ++ .../tests/excluded_by_target_os/output.kt | 17 +++ .../tests/excluded_by_target_os/output.scala | 23 ++++ .../tests/excluded_by_target_os/output.swift | 9 ++ .../tests/excluded_by_target_os/output.ts | 8 ++ .../input.rs | 7 + .../output.swift | 1 + core/src/parser.rs | 122 ++++++++++++++---- core/src/visitors.rs | 69 +++++++--- core/tests/agnostic_tests.rs | 1 + core/tests/snapshot_tests.rs | 20 ++- 18 files changed, 332 insertions(+), 103 deletions(-) create mode 100644 core/data/tests/excluded_by_target_os/input.rs create mode 100644 core/data/tests/excluded_by_target_os/output.go create mode 100644 core/data/tests/excluded_by_target_os/output.kt create mode 100644 core/data/tests/excluded_by_target_os/output.scala create mode 100644 core/data/tests/excluded_by_target_os/output.swift create mode 100644 core/data/tests/excluded_by_target_os/output.ts create mode 100644 core/data/tests/excluded_by_target_os_full_module/input.rs create mode 100644 core/data/tests/excluded_by_target_os_full_module/output.swift diff --git a/cli/src/args.rs b/cli/src/args.rs index 8bf11d01..ed73124d 100644 --- a/cli/src/args.rs +++ b/cli/src/args.rs @@ -17,6 +17,7 @@ pub const ARG_GENERATE_CONFIG: &str = "generate-config-file"; pub const ARG_OUTPUT_FILE: &str = "output-file"; pub const ARG_OUTPUT_FOLDER: &str = "output-folder"; pub const ARG_FOLLOW_LINKS: &str = "follow-links"; +pub const ARG_TARGET_OS: &str = "target_os"; #[cfg(feature = "go")] const AVAILABLE_LANGUAGES: [&str; 5] = ["kotlin", "scala", "swift", "typescript", "go"]; @@ -147,5 +148,11 @@ pub(crate) fn build_command() -> Command<'static> { .help("Directories within which to recursively find and process rust files") .required_unless(ARG_GENERATE_CONFIG) .min_values(1), + ).arg( + Arg::new(ARG_TARGET_OS) + .long("target-os") + .help("Optional restrict to target_os") + .takes_value(true) + .required(false) ) } diff --git a/cli/src/config.rs b/cli/src/config.rs index caac7e54..c3d2dc5f 100644 --- a/cli/src/config.rs +++ b/cli/src/config.rs @@ -64,6 +64,8 @@ pub(crate) struct Config { pub scala: ScalaParams, #[cfg(feature = "go")] pub go: GoParams, + #[serde(skip)] + pub target_os: Option, } pub(crate) fn store_config(config: &Config, file_path: Option<&str>) -> anyhow::Result<()> { diff --git a/cli/src/main.rs b/cli/src/main.rs index 981c23a3..0b4ee57b 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -5,12 +5,13 @@ use anyhow::{anyhow, Context}; use args::{ build_command, ARG_CONFIG_FILE_NAME, ARG_FOLLOW_LINKS, ARG_GENERATE_CONFIG, ARG_JAVA_PACKAGE, ARG_KOTLIN_PREFIX, ARG_MODULE_NAME, ARG_OUTPUT_FOLDER, ARG_SCALA_MODULE_NAME, - ARG_SCALA_PACKAGE, ARG_SWIFT_PREFIX, ARG_TYPE, + ARG_SCALA_PACKAGE, ARG_SWIFT_PREFIX, ARG_TARGET_OS, ARG_TYPE, }; use clap::ArgMatches; use config::Config; use ignore::{overrides::OverrideBuilder, types::TypesBuilder, WalkBuilder}; use parse::{all_types, parse_input, parser_inputs}; +use rayon::iter::ParallelBridge; use std::collections::HashMap; #[cfg(feature = "go")] use typeshare_core::language::Go; @@ -103,6 +104,9 @@ fn main() -> anyhow::Result<()> { } let multi_file = options.value_of(ARG_OUTPUT_FOLDER).is_some(); + + let target_os = config.target_os.clone(); + let lang = language(language_type, config, multi_file); let ignored_types = lang.ignored_reference_types(); @@ -110,10 +114,15 @@ fn main() -> anyhow::Result<()> { // a git-ignored directory to be processed, add the specific directory to // the list of directories given to typeshare when it's invoked in the // makefiles + // TODO: The `ignore` walker supports parallel walking. We should use this + // and implement a `ParallelVisitor` that builds up the mapping of parsed + // data. That way both walking and parsing are in parallel. + // https://docs.rs/ignore/latest/ignore/struct.WalkParallel.html let crate_parsed_data = parse_input( - parser_inputs(walker_builder, language_type, multi_file), + parser_inputs(walker_builder, language_type, multi_file).par_bridge(), &ignored_types, multi_file, + target_os, )?; // Collect all the types into a map of the file name they @@ -211,6 +220,7 @@ fn override_configuration(mut config: Config, options: &ArgMatches) -> Config { config.go.package = go_package.to_string(); } + config.target_os = options.value_of(ARG_TARGET_OS).map(|s| s.to_string()); config } diff --git a/cli/src/parse.rs b/cli/src/parse.rs index 8edbff11..47ed56ae 100644 --- a/cli/src/parse.rs +++ b/cli/src/parse.rs @@ -4,7 +4,6 @@ use ignore::WalkBuilder; use rayon::iter::{IntoParallelIterator, ParallelIterator}; use std::{ collections::{hash_map::Entry, HashMap}, - ops::Not, path::PathBuf, }; use typeshare_core::{ @@ -28,12 +27,12 @@ pub fn parser_inputs( walker_builder: WalkBuilder, language_type: SupportedLanguage, multi_file: bool, -) -> Vec { +) -> impl Iterator { walker_builder .build() .filter_map(Result::ok) .filter(|dir_entry| !dir_entry.path().is_dir()) - .filter_map(|dir_entry| { + .filter_map(move |dir_entry| { let crate_name = if multi_file { CrateName::find_crate_name(dir_entry.path())? } else { @@ -47,7 +46,6 @@ pub fn parser_inputs( crate_name, }) }) - .collect() } /// The output file name to write to. @@ -90,74 +88,47 @@ pub fn all_types(file_mappings: &HashMap) -> CrateTypes { /// Collect all the parsed sources into a mapping of crate name to parsed data. pub fn parse_input( - inputs: Vec, + inputs: impl ParallelIterator, ignored_types: &[&str], multi_file: bool, + target_os: Option, ) -> anyhow::Result> { inputs .into_par_iter() .try_fold( HashMap::new, - |mut results: HashMap, + |mut parsed_crates: HashMap, ParserInput { file_path, file_name, crate_name, }| { - match std::fs::read_to_string(&file_path) - .context("Failed to read input") - .and_then(|data| { - typeshare_core::parser::parse( - &data, - crate_name.clone(), - file_name.clone(), - file_path, - ignored_types, - multi_file, - ) - .context("Failed to parse") - }) - .map(|parsed_data| { - parsed_data.and_then(|parsed_data| { - is_parsed_data_empty(&parsed_data) - .not() - .then_some((crate_name, parsed_data)) - }) - })? { - Some((crate_name, parsed_data)) => { - match results.entry(crate_name) { - Entry::Occupied(mut entry) => { - entry.get_mut().add(parsed_data); - } - Entry::Vacant(entry) => { - entry.insert(parsed_data); - } - } - Ok::<_, anyhow::Error>(results) - } - None => Ok(results), + let parsed_result = typeshare_core::parser::parse( + &std::fs::read_to_string(&file_path) + .with_context(|| format!("Failed to read input: {file_name}"))?, + crate_name.clone(), + file_name.clone(), + file_path, + ignored_types, + multi_file, + target_os.clone(), + ) + .with_context(|| format!("Failed to parse: {file_name}"))?; + + if let Some(parsed_data) = parsed_result { + parsed_crates + .entry(crate_name) + .or_default() + .add(parsed_data); } + + Ok(parsed_crates) }, ) - .try_reduce(HashMap::new, |mut file_maps, mapping| { - for (crate_name, parsed_data) in mapping { - match file_maps.entry(crate_name) { - Entry::Occupied(mut e) => { - e.get_mut().add(parsed_data); - } - Entry::Vacant(e) => { - e.insert(parsed_data); - } - } + .try_reduce(HashMap::new, |mut file_maps, parsed_crates| { + for (crate_name, parsed_data) in parsed_crates { + file_maps.entry(crate_name).or_default().add(parsed_data); } Ok(file_maps) }) } - -/// Check if we have not parsed any relavent typehsared types. -fn is_parsed_data_empty(parsed_data: &ParsedData) -> bool { - parsed_data.enums.is_empty() - && parsed_data.aliases.is_empty() - && parsed_data.structs.is_empty() - && parsed_data.errors.is_empty() -} diff --git a/cli/src/writer.rs b/cli/src/writer.rs index dae179cd..4fbbc968 100644 --- a/cli/src/writer.rs +++ b/cli/src/writer.rs @@ -67,13 +67,14 @@ fn check_write_file(outfile: &PathBuf, output: Vec) -> anyhow::Result<()> { if !output.is_empty() { let out_dir = outfile .parent() - .context(format!("Could not get parent for {outfile:?}"))?; + .with_context(|| format!("Could not get parent for {outfile:?}"))?; // If the output directory doesn't already exist, create it. if !out_dir.exists() { fs::create_dir_all(out_dir).context("failed to create output directory")?; } - fs::write(outfile, output).context("failed to write output")?; + fs::write(outfile, output) + .with_context(|| format!("failed to write output: {}", outfile.to_string_lossy()))?; } Ok(()) } diff --git a/core/Cargo.toml b/core/Cargo.toml index cb937233..783c7668 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -20,3 +20,4 @@ anyhow = "1" expect-test = "1.5" once_cell = "1" cool_asserts = "2" +syn = { version = "2", features = ["full", "visit", "extra-traits"] } diff --git a/core/data/tests/excluded_by_target_os/input.rs b/core/data/tests/excluded_by_target_os/input.rs new file mode 100644 index 00000000..5fb07c65 --- /dev/null +++ b/core/data/tests/excluded_by_target_os/input.rs @@ -0,0 +1,34 @@ +#![cfg(feature = "online")] +#![allow(dead_code)] + +use std::collection::HashMap; + +#[typeshare] +pub enum TestEnum { + Variant1, + #[cfg(target_os = "ios")] + Variant2, + #[cfg(any(target_os = "ios", feature = "test"))] + Variant3, + #[cfg(all(target_os = "ios", feature = "test"))] + Variant4, + #[cfg(target_os = "android")] + Variant5, +} + +#[typeshare] +#[cfg(target_os = "ios")] +pub struct TestStruct; + +#[typeshare] +#[cfg(target_os = "ios")] +type TypeAlias = String; + +#[typeshare] +#[cfg(any(target_os = "ios", feature = "test"))] +pub enum Test {} + +#[typeshare] +#[cfg(feature = "super")] +#[cfg(target_os = "android")] +pub enum SomeEnum {} diff --git a/core/data/tests/excluded_by_target_os/output.go b/core/data/tests/excluded_by_target_os/output.go new file mode 100644 index 00000000..bc7a64ec --- /dev/null +++ b/core/data/tests/excluded_by_target_os/output.go @@ -0,0 +1,12 @@ +package proto + +import "encoding/json" + +type TestEnum string +const ( + TestEnumVariant1 TestEnum = "Variant1" + TestEnumVariant5 TestEnum = "Variant5" +) +type SomeEnum string +const ( +) diff --git a/core/data/tests/excluded_by_target_os/output.kt b/core/data/tests/excluded_by_target_os/output.kt new file mode 100644 index 00000000..73f0c614 --- /dev/null +++ b/core/data/tests/excluded_by_target_os/output.kt @@ -0,0 +1,17 @@ +package com.agilebits.onepassword + +import kotlinx.serialization.Serializable +import kotlinx.serialization.SerialName + +@Serializable +enum class TestEnum(val string: String) { + @SerialName("Variant1") + Variant1("Variant1"), + @SerialName("Variant5") + Variant5("Variant5"), +} + +@Serializable +enum class SomeEnum(val string: String) { +} + diff --git a/core/data/tests/excluded_by_target_os/output.scala b/core/data/tests/excluded_by_target_os/output.scala new file mode 100644 index 00000000..6876c694 --- /dev/null +++ b/core/data/tests/excluded_by_target_os/output.scala @@ -0,0 +1,23 @@ +package com.agilebits + +package onepassword { + +sealed trait TestEnum { + def serialName: String +} +object TestEnum { + case object Variant1 extends TestEnum { + val serialName: String = "Variant1" + } + case object Variant5 extends TestEnum { + val serialName: String = "Variant5" + } +} + +sealed trait SomeEnum { + def serialName: String +} +object SomeEnum { +} + +} diff --git a/core/data/tests/excluded_by_target_os/output.swift b/core/data/tests/excluded_by_target_os/output.swift new file mode 100644 index 00000000..4a6c4e3a --- /dev/null +++ b/core/data/tests/excluded_by_target_os/output.swift @@ -0,0 +1,9 @@ +import Foundation + +public enum TestEnum: String, Codable { + case variant1 = "Variant1" + case variant5 = "Variant5" +} + +public enum SomeEnum: String, Codable { +} diff --git a/core/data/tests/excluded_by_target_os/output.ts b/core/data/tests/excluded_by_target_os/output.ts new file mode 100644 index 00000000..2f793b62 --- /dev/null +++ b/core/data/tests/excluded_by_target_os/output.ts @@ -0,0 +1,8 @@ +export enum TestEnum { + Variant1 = "Variant1", + Variant5 = "Variant5", +} + +export enum SomeEnum { +} + diff --git a/core/data/tests/excluded_by_target_os_full_module/input.rs b/core/data/tests/excluded_by_target_os_full_module/input.rs new file mode 100644 index 00000000..f6d27da8 --- /dev/null +++ b/core/data/tests/excluded_by_target_os_full_module/input.rs @@ -0,0 +1,7 @@ +#![cfg(feature = "online")] +#![allow(dead_code)] +#![cfg(any(target_os = "android", feature = "testing"))] +#![cfg(target_os = "wasm32")] + +#[typeshare] +pub struct IgnoredUnlessAndroid; diff --git a/core/data/tests/excluded_by_target_os_full_module/output.swift b/core/data/tests/excluded_by_target_os_full_module/output.swift new file mode 100644 index 00000000..fecc4ab4 --- /dev/null +++ b/core/data/tests/excluded_by_target_os_full_module/output.swift @@ -0,0 +1 @@ +import Foundation diff --git a/core/src/parser.rs b/core/src/parser.rs index 0f6b0c2a..b1a8edf2 100644 --- a/core/src/parser.rs +++ b/core/src/parser.rs @@ -8,6 +8,7 @@ use crate::{ }, visitors::{ImportedType, TypeShareVisitor}, }; +use itertools::Either; use proc_macro2::Ident; use std::{ collections::{BTreeSet, HashMap, HashSet}, @@ -16,7 +17,7 @@ use std::{ }; use syn::{ ext::IdentExt, parse::ParseBuffer, punctuated::Punctuated, visit::Visit, Attribute, Expr, - ExprLit, Fields, GenericParam, ItemEnum, ItemStruct, ItemType, LitStr, Meta, MetaList, + ExprLit, Fields, GenericParam, ItemEnum, ItemStruct, ItemType, Lit, LitStr, Meta, MetaList, MetaNameValue, Token, }; use thiserror::Error; @@ -127,6 +128,10 @@ impl ParsedData { self.import_types.extend(other.import_types); self.type_names.extend(other.type_names); self.errors.append(&mut other.errors); + + self.file_name = other.file_name; + self.crate_name = other.crate_name; + self.multi_file = other.multi_file; } pub(crate) fn push(&mut self, rust_thing: RustItem) { @@ -145,6 +150,14 @@ impl ParsedData { } } } + + /// If this file was skipped by the visitor. + pub fn is_empty(&self) -> bool { + self.structs.is_empty() + && self.enums.is_empty() + && self.aliases.is_empty() + && self.errors.is_empty() + } } /// Parse the given Rust source string into `ParsedData`. @@ -155,6 +168,7 @@ pub fn parse( file_path: PathBuf, ignored_types: &[&str], mult_file: bool, + target_os: Option, ) -> Result, ParseError> { // We will only produce output for files that contain the `#[typeshare]` // attribute, so this is a quick and easy performance win @@ -164,11 +178,17 @@ pub fn parse( // Parse and process the input, ensuring we parse only items marked with // `#[typeshare]` - let mut import_visitor = - TypeShareVisitor::new(crate_name, file_name, file_path, ignored_types, mult_file); + let mut import_visitor = TypeShareVisitor::new( + crate_name, + file_name, + file_path, + ignored_types, + mult_file, + target_os, + ); import_visitor.visit_file(&syn::parse_file(source_code)?); - Ok(Some(import_visitor.parsed_data())) + Ok(import_visitor.parsed_data()) } /// Parses a struct into a definition that more succinctly represents what @@ -176,7 +196,10 @@ pub fn parse( /// /// This function can currently return something other than a struct, which is a /// hack. -pub(crate) fn parse_struct(s: &ItemStruct) -> Result { +pub(crate) fn parse_struct( + s: &ItemStruct, + target_os: Option<&str>, +) -> Result { let serde_rename_all = serde_rename_all(&s.attrs); let generic_types = s @@ -207,7 +230,7 @@ pub(crate) fn parse_struct(s: &ItemStruct) -> Result { let fields = f .named .iter() - .filter(|field| !is_skipped(&field.attrs)) + .filter(|field| !is_skipped(&field.attrs, target_os)) .map(|f| { let ty = if let Some(ty) = get_field_type_override(&f.attrs) { ty.parse()? @@ -276,7 +299,7 @@ pub(crate) fn parse_struct(s: &ItemStruct) -> Result { /// /// This function can currently return something other than an enum, which is a /// hack. -pub(crate) fn parse_enum(e: &ItemEnum) -> Result { +pub(crate) fn parse_enum(e: &ItemEnum, target_os: Option<&str>) -> Result { let generic_types = e .generics .params @@ -311,8 +334,8 @@ pub(crate) fn parse_enum(e: &ItemEnum) -> Result { .variants .iter() // Filter out variants we've been told to skip - .filter(|v| !is_skipped(&v.attrs)) - .map(|v| parse_enum_variant(v, &serde_rename_all)) + .filter(|v| !is_skipped(&v.attrs, target_os)) + .map(|v| parse_enum_variant(v, &serde_rename_all, target_os)) .collect::, _>>()?; // Check if the enum references itself recursively in any of its variants @@ -374,6 +397,7 @@ pub(crate) fn parse_enum(e: &ItemEnum) -> Result { fn parse_enum_variant( v: &syn::Variant, enum_serde_rename_all: &Option, + target_os: Option<&str>, ) -> Result { let shared = RustEnumVariantShared { id: get_ident(Some(&v.ident), &v.attrs, enum_serde_rename_all), @@ -408,7 +432,7 @@ fn parse_enum_variant( fields: fields_named .named .iter() - .filter(|f| !is_skipped(&f.attrs)) + .filter(|f| !is_skipped(&f.attrs, target_os)) .map(|f| { let field_type = if let Some(ty) = get_field_type_override(&f.attrs) { ty.parse()? @@ -489,7 +513,6 @@ pub(crate) fn get_name_value_meta_items<'a>( ) -> impl Iterator + 'a { attrs.iter().flat_map(move |attr| { get_meta_items(attr, ident) - .iter() .filter_map(|arg| match arg { Meta::NameValue(name_value) if name_value.path.is_ident(name) => { expr_to_string(&name_value.value) @@ -501,15 +524,16 @@ pub(crate) fn get_name_value_meta_items<'a>( } /// Returns all arguments passed into `#[{ident}(...)]` where `{ident}` can be `serde` or `typeshare` attributes -fn get_meta_items(attr: &syn::Attribute, ident: &str) -> Vec { +#[inline(always)] +fn get_meta_items(attr: &syn::Attribute, ident: &str) -> impl Iterator { if attr.path().is_ident(ident) { - attr.parse_args_with(Punctuated::::parse_terminated) - .iter() - .flat_map(|meta| meta.iter()) - .cloned() - .collect() + Either::Left( + attr.parse_args_with(Punctuated::::parse_terminated) + .into_iter() + .flat_map(|punctuated| punctuated.into_iter()), + ) } else { - Vec::default() + Either::Right(std::iter::empty()) } } @@ -565,19 +589,69 @@ fn parse_comment_attrs(attrs: &[Attribute]) -> Vec { } // `#[typeshare(skip)]` or `#[serde(skip)]` -fn is_skipped(attrs: &[syn::Attribute]) -> bool { +fn is_skipped(attrs: &[syn::Attribute], target_os: Option<&str>) -> bool { attrs.iter().any(|attr| { get_meta_items(attr, SERDE) - .into_iter() .chain(get_meta_items(attr, TYPESHARE)) .any(|arg| matches!(arg, Meta::Path(path) if path.is_ident("skip"))) - }) + }) || target_os + .map(|target| attrs.iter().any(|attr| target_os_skip(attr, target))) + .unwrap_or(false) +} + +/// Check if we have a `target_os` cfg that dooes not match command line +/// argument `--target-os`. +#[inline] +pub(crate) fn target_os_skip(attr: &Attribute, target_os: &str) -> bool { + get_meta_items(attr, "cfg") + .find_map(|meta| match &meta { + // a single #[cfg(target_os = "target")] + Meta::NameValue(MetaNameValue { + path, + value: + Expr::Lit(ExprLit { + lit: Lit::Str(v), .. + }), + .. + }) if path.is_ident("target_os") => Some(v.value()), + // combined with any or all + // Ex: #[cfg(any(target_os = "target", feature = "test"))] + Meta::List(meta_list) + if meta_list.path.is_ident("any") || meta_list.path.is_ident("all") => + { + target_os_from_meta_list(meta_list) + } + _ => None, + }) + .map(|os| os != target_os) + .unwrap_or(false) +} + +/// Parses `target_os = "os"` value from `any` or `all` meta list. +#[inline] +fn target_os_from_meta_list(list: &MetaList) -> Option { + let name_values: Punctuated = + list.parse_args_with(Punctuated::parse_terminated).ok()?; + + name_values + .into_iter() + .find_map(|name_value| { + name_value + .path + .is_ident("target_os") + .then_some(name_value.value) + }) + .and_then(|val_expr| match val_expr { + Expr::Lit(ExprLit { + lit: Lit::Str(val), .. + }) => Some(val.value()), + _ => None, + }) } fn serde_attr(attrs: &[syn::Attribute], ident: &str) -> bool { attrs.iter().any(|attr| { get_meta_items(attr, SERDE) - .iter() .any(|arg| matches!(arg, Meta::Path(path) if path.is_ident(ident))) }) } @@ -601,14 +675,14 @@ fn get_field_decorators( attrs .iter() .flat_map(|attr| get_meta_items(attr, TYPESHARE)) - .flat_map(|meta| { + .filter_map(|meta| { if let Meta::List(list) = meta { Some(list) } else { None } }) - .flat_map(|list: MetaList| match list.path.get_ident() { + .filter_map(|list: MetaList| match list.path.get_ident() { Some(ident) if languages.contains(&ident.try_into().unwrap()) => { Some((ident.try_into().unwrap(), list)) } diff --git a/core/src/visitors.rs b/core/src/visitors.rs index 66d8dc49..3cb32ed4 100644 --- a/core/src/visitors.rs +++ b/core/src/visitors.rs @@ -2,13 +2,13 @@ use crate::{ language::CrateName, parser::{ - has_typeshare_annotation, parse_enum, parse_struct, parse_type_alias, ErrorInfo, - ParseError, ParsedData, + has_typeshare_annotation, parse_enum, parse_struct, parse_type_alias, target_os_skip, + ErrorInfo, ParseError, ParsedData, }, rust_types::{RustEnumVariant, RustItem}, }; -use std::{collections::HashSet, path::PathBuf}; -use syn::{visit::Visit, ItemUse, UseTree}; +use std::{collections::HashSet, ops::Not, path::PathBuf}; +use syn::{visit::Visit, Attribute, ItemUse, UseTree}; /// List of some popular crate names that we can ignore /// during import parsing. @@ -50,6 +50,7 @@ pub struct TypeShareVisitor<'a> { #[allow(dead_code)] file_path: PathBuf, ignored_types: &'a [&'a str], + target_os: Option, } impl<'a> TypeShareVisitor<'a> { @@ -60,25 +61,31 @@ impl<'a> TypeShareVisitor<'a> { file_path: PathBuf, ignored_types: &'a [&'a str], multi_file: bool, + target_os: Option, ) -> Self { Self { parsed_data: ParsedData::new(crate_name, file_name, multi_file), file_path, ignored_types, + target_os, } } + #[inline] /// Consume the visitor and return parsed data. - pub fn parsed_data(self) -> ParsedData { - if self.parsed_data.multi_file { - let mut s = self; - s.reconcile_referenced_types(); - s.parsed_data - } else { - self.parsed_data - } + pub fn parsed_data(self) -> Option { + self.parsed_data.is_empty().not().then(|| { + if self.parsed_data.multi_file { + let mut s = self; + s.reconcile_referenced_types(); + s.parsed_data + } else { + self.parsed_data + } + }) } + #[inline] fn collect_result(&mut self, result: Result) { match result { Ok(data) => self.parsed_data.push(data), @@ -181,6 +188,16 @@ impl<'a> TypeShareVisitor<'a> { self.parsed_data.import_types = diff; } + + /// Is this type annoted with at `#[cfg(target_os = "target")]` that does + /// not match `--target-os` argument? + #[inline(always)] + fn target_os_skipped(&self, attrs: &[Attribute]) -> bool { + self.target_os + .as_ref() + .map(|target_os| attrs.iter().any(|attr| target_os_skip(attr, target_os))) + .unwrap_or(false) + } } impl<'ast, 'a> Visit<'ast> for TypeShareVisitor<'a> { @@ -249,8 +266,8 @@ impl<'ast, 'a> Visit<'ast> for TypeShareVisitor<'a> { /// Collect rust structs. fn visit_item_struct(&mut self, i: &'ast syn::ItemStruct) { - if has_typeshare_annotation(&i.attrs) { - self.collect_result(parse_struct(i)); + if has_typeshare_annotation(&i.attrs) && !self.target_os_skipped(&i.attrs) { + self.collect_result(parse_struct(i, self.target_os.as_deref())); } syn::visit::visit_item_struct(self, i); @@ -258,8 +275,8 @@ impl<'ast, 'a> Visit<'ast> for TypeShareVisitor<'a> { /// Collect rust enums. fn visit_item_enum(&mut self, i: &'ast syn::ItemEnum) { - if has_typeshare_annotation(&i.attrs) { - self.collect_result(parse_enum(i)); + if has_typeshare_annotation(&i.attrs) && !self.target_os_skipped(&i.attrs) { + self.collect_result(parse_enum(i, self.target_os.as_deref())); } syn::visit::visit_item_enum(self, i); @@ -267,12 +284,29 @@ impl<'ast, 'a> Visit<'ast> for TypeShareVisitor<'a> { /// Collect rust type aliases. fn visit_item_type(&mut self, i: &'ast syn::ItemType) { - if has_typeshare_annotation(&i.attrs) { + if has_typeshare_annotation(&i.attrs) && !self.target_os_skipped(&i.attrs) { self.collect_result(parse_type_alias(i)); } syn::visit::visit_item_type(self, i); } + + /// Track potentially skipped modules. + fn visit_item_mod(&mut self, i: &'ast syn::ItemMod) { + if let Some(target_os) = self.target_os.as_ref() { + if i.attrs.iter().any(|attr| target_os_skip(attr, target_os)) { + println!("skip {}", i.ident); + } + }; + + syn::visit::visit_item_mod(self, i); + } + + fn visit_file(&mut self, i: &'ast syn::File) { + if !self.target_os_skipped(&i.attrs) { + syn::visit::visit_file(self, i); + } + } } /// Exclude popular crates that won't be typeshared. @@ -573,6 +607,7 @@ mod test { "file_path".into(), &[], true, + None, ); visitor.visit_file(&file); diff --git a/core/tests/agnostic_tests.rs b/core/tests/agnostic_tests.rs index a30bbf77..c299d68a 100644 --- a/core/tests/agnostic_tests.rs +++ b/core/tests/agnostic_tests.rs @@ -19,6 +19,7 @@ pub fn process_input( "file_path".into(), &[], false, + None, )? .unwrap(); diff --git a/core/tests/snapshot_tests.rs b/core/tests/snapshot_tests.rs index 3bca4114..7907de75 100644 --- a/core/tests/snapshot_tests.rs +++ b/core/tests/snapshot_tests.rs @@ -51,6 +51,7 @@ fn check( test_name: &str, file_name: impl AsRef, mut lang: Box, + target_os: Option<&str>, ) -> Result<(), anyhow::Error> { let _extension = file_name .as_ref() @@ -72,6 +73,7 @@ fn check( "file_path".into(), &[], false, + target_os.map(|s| s.to_owned()), )? .unwrap(); lang.generate_types(&mut typeshare_output, &HashMap::new(), parsed_data)?; @@ -221,6 +223,16 @@ macro_rules! language_instance { }; } +macro_rules! target_os { + ($target_os:literal) => { + Some($target_os) + }; + + () => { + None + }; +} + /// This macro removes the boilerplate involved in creating typeshare snapshot /// tests. Usage looks like: /// @@ -299,12 +311,13 @@ macro_rules! tests { })? ),+ $(,)? - ]; + ] $(target_os: $target_os:tt)?; )*) => {$( mod $test { use super::check; const TEST_NAME: &str = stringify!($test); + const TARGET_OS: Option<&str> = target_os!($($target_os)?); $( #[test] @@ -313,6 +326,7 @@ macro_rules! tests { TEST_NAME, output_file_for_ident!($language), language_instance!($language $({ $($lang_config)* })?), + TARGET_OS ) } )+ @@ -570,5 +584,7 @@ tests! { go ]; can_generate_anonymous_struct_with_skipped_fields: [swift, kotlin, scala, typescript, go]; - generic_struct_with_constraints_and_decorators: [swift { codablevoid_constraints: vec!["Equatable".into()]}]; + generic_struct_with_constraints_and_decorators: [swift { codablevoid_constraints: vec!["Equatable".into()] }]; + excluded_by_target_os: [ swift, kotlin, scala, typescript, go ] target_os: "android"; + // excluded_by_target_os_full_module: [swift] target_os: "ios"; } From 0284e32be7d993385f3ff83d8a380f1113a19c90 Mon Sep 17 00:00:00 2001 From: ComplexSpaces Date: Fri, 28 Jun 2024 13:12:48 -0500 Subject: [PATCH 19/24] chore: update changelogs and bump versions for v1.10.0-beta.6 (#179) --- CHANGELOG.md | 4 ++++ Cargo.lock | 4 ++-- cli/Cargo.toml | 4 ++-- core/Cargo.toml | 2 +- 4 files changed, 9 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 722934ce..4e359910 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Version 1.10.0-beta +## 1.10.0-beta.6 + +- Added support for skipping fields/variants via the `target_os` argument [#176](https://github.com/1Password/typeshare/pull/176) + ## 1.10.0-beta.5 - Added support for Swift generic constraints via `#[typeshare(swiftGenericConstraints)]` [#174](https://github.com/1Password/typeshare/pull/174) diff --git a/Cargo.lock b/Cargo.lock index 185310d4..739a35e6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -610,7 +610,7 @@ dependencies = [ [[package]] name = "typeshare-cli" -version = "1.10.0-beta.5" +version = "1.10.0-beta.6" dependencies = [ "anyhow", "clap", @@ -625,7 +625,7 @@ dependencies = [ [[package]] name = "typeshare-core" -version = "1.10.0-beta.5" +version = "1.10.0-beta.6" dependencies = [ "anyhow", "cool_asserts", diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 4e551f71..c6ea7bc6 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "typeshare-cli" -version = "1.10.0-beta.5" +version = "1.10.0-beta.6" edition = "2021" description = "Command Line Tool for generating language files with typeshare" license = "MIT OR Apache-2.0" @@ -22,5 +22,5 @@ once_cell = "1" rayon = "1.10" serde = { version = "1", features = ["derive"] } toml = "0.8" -typeshare-core = { path = "../core", version = "1.10.0-beta.5" } +typeshare-core = { path = "../core", version = "1.10.0-beta.6" } anyhow = "1" diff --git a/core/Cargo.toml b/core/Cargo.toml index 783c7668..116a8820 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "typeshare-core" -version = "1.10.0-beta.5" +version = "1.10.0-beta.6" license = "MIT OR Apache-2.0" edition = "2021" description = "The code generator used by Typeshare's command line tool" From a10393fb30fb63ed13c8010dfbe54455790106ee Mon Sep 17 00:00:00 2001 From: Jonathan Duran Date: Mon, 15 Jul 2024 09:22:32 -0400 Subject: [PATCH 20/24] Add support for Kotlin inline value classes --- core/data/tests/struct_decorator/input.rs | 5 +- core/data/tests/struct_decorator/output.kt | 8 ++ core/data/tests/struct_decorator/output.swift | 2 + core/src/language/kotlin.rs | 93 ++++++++++++++----- core/src/parser.rs | 4 + core/src/rust_types.rs | 2 + 6 files changed, 91 insertions(+), 23 deletions(-) diff --git a/core/data/tests/struct_decorator/input.rs b/core/data/tests/struct_decorator/input.rs index 77b8a1fc..c3700f13 100644 --- a/core/data/tests/struct_decorator/input.rs +++ b/core/data/tests/struct_decorator/input.rs @@ -15,7 +15,7 @@ pub struct BestHockeyTeams2 { Lies: String, } -#[typeshare(kotlin = "redacted_to_string")] +#[typeshare(kotlin = "Redacted")] pub struct BestHockeyTeams3 { PittsburghPenguins: u32, Lies: String, @@ -26,3 +26,6 @@ pub struct BestHockeyTeams4 { PittsburghPenguins: u32, Lies: String, } + +#[typeshare(kotlin = "JvmInline, Redacted")] +pub struct BestHockeyTeams5(String); diff --git a/core/data/tests/struct_decorator/output.kt b/core/data/tests/struct_decorator/output.kt index 2d025f04..69489943 100644 --- a/core/data/tests/struct_decorator/output.kt +++ b/core/data/tests/struct_decorator/output.kt @@ -3,6 +3,14 @@ package com.agilebits.onepassword import kotlinx.serialization.Serializable import kotlinx.serialization.SerialName +@Serializable +@JvmInline +value class BestHockeyTeams5( + val value: String +) { + override fun toString(): String = "***" +} + @Serializable data class BestHockeyTeams ( val PittsburghPenguins: UInt, diff --git a/core/data/tests/struct_decorator/output.swift b/core/data/tests/struct_decorator/output.swift index 767f4eee..6af92e27 100644 --- a/core/data/tests/struct_decorator/output.swift +++ b/core/data/tests/struct_decorator/output.swift @@ -1,5 +1,7 @@ import Foundation +public typealias OPBestHockeyTeams5 = String + public struct OPBestHockeyTeams: Codable { public let PittsburghPenguins: UInt32 public let Lies: String diff --git a/core/src/language/kotlin.rs b/core/src/language/kotlin.rs index 3a026f77..19ef594c 100644 --- a/core/src/language/kotlin.rs +++ b/core/src/language/kotlin.rs @@ -4,14 +4,15 @@ use crate::parser::{remove_dash_from_identifier, DecoratorKind, ParsedData}; use crate::rust_types::{RustTypeFormatError, SpecialRustType}; use crate::{ rename::RenameExt, - rust_types::{RustEnum, RustEnumVariant, RustField, RustStruct, RustTypeAlias}, + rust_types::{Id, RustEnum, RustEnumVariant, RustField, RustStruct, RustTypeAlias}, }; use itertools::Itertools; use joinery::JoinableIterator; use lazy_format::lazy_format; -use std::{collections::HashMap, io::Write}; +use std::{collections::BTreeSet, collections::HashMap, io::Write}; -const REDACTED_TO_STRING: &str = "redacted_to_string"; +const INLINE: &str = "JvmInline"; +const REDACTED: &str = "Redacted"; /// All information needed for Kotlin type-code #[derive(Default)] @@ -119,16 +120,50 @@ impl Language for Kotlin { self.write_comments(w, 0, &ty.comments)?; let type_name = format!("{}{}", &self.prefix, ty.id.original); - writeln!( - w, - "typealias {}{} = {}\n", - type_name, - (!ty.generic_types.is_empty()) - .then(|| format!("<{}>", ty.generic_types.join(", "))) - .unwrap_or_default(), - self.format_type(&ty.r#type, ty.generic_types.as_slice()) - .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))? - )?; + if self.is_inline(&ty.decorators) { + writeln!(w, "@Serializable")?; + writeln!(w, "@JvmInline")?; + writeln!(w, "value class {}{}(", self.prefix, ty.id.renamed)?; + + self.write_element( + w, + &RustField { + id: Id { + original: String::from("value"), + renamed: String::from("value"), + }, + ty: ty.r#type.clone(), + comments: vec![], + has_default: false, + decorators: HashMap::new(), + }, + &[], + false, + )?; + + writeln!(w)?; + + if self.is_redacted(&ty.decorators) { + writeln!(w, ") {{")?; + writeln!(w, "\toverride fun toString(): String = \"***\"")?; + writeln!(w, "}}")?; + } else { + writeln!(w, ")")?; + } + + writeln!(w)?; + } else { + writeln!( + w, + "typealias {}{} = {}\n", + type_name, + (!ty.generic_types.is_empty()) + .then(|| format!("<{}>", ty.generic_types.join(", "))) + .unwrap_or_default(), + self.format_type(&ty.r#type, ty.generic_types.as_slice()) + .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))? + )?; + } Ok(()) } @@ -168,16 +203,16 @@ impl Language for Kotlin { self.write_element(w, last, rs.generic_types.as_slice(), requires_serial_name)?; writeln!(w)?; } - write!(w, ")")?; - if let Some(kotlin_decorators) = rs.decorators.get(&DecoratorKind::Kotlin) { - let redacted_decorator = String::from(REDACTED_TO_STRING); - if kotlin_decorators.iter().any(|d| *d == redacted_decorator) { - writeln!(w, " {{")?; - writeln!(w, "\toverride fun toString(): String = {:?}", rs.id.renamed)?; - write!(w, "}}")?; - } + + if self.is_redacted(&rs.decorators) { + writeln!(w, ") {{")?; + writeln!(w, "\toverride fun toString(): String = {:?}", rs.id.renamed)?; + writeln!(w, "}}")?; + } else { + writeln!(w, ")")?; } - writeln!(w, "\n")?; + + writeln!(w)?; } Ok(()) } @@ -412,4 +447,18 @@ impl Kotlin { .iter() .try_for_each(|comment| self.write_comment(w, indent, comment)) } + + fn is_redacted(&self, decorators: &HashMap>) -> bool { + match decorators.get(&DecoratorKind::Kotlin) { + Some(kotlin_decorators) => kotlin_decorators.iter().contains(&String::from(REDACTED)), + _ => false, + } + } + + fn is_inline(&self, decorators: &HashMap>) -> bool { + match decorators.get(&DecoratorKind::Kotlin) { + Some(kotlin_decorators) => kotlin_decorators.iter().contains(&String::from(INLINE)), + _ => false, + } + } } diff --git a/core/src/parser.rs b/core/src/parser.rs index b1a8edf2..133aaf11 100644 --- a/core/src/parser.rs +++ b/core/src/parser.rs @@ -221,6 +221,7 @@ pub(crate) fn parse_struct( r#type: ty.parse()?, comments: parse_comment_attrs(&s.attrs), generic_types, + decorators: get_decorators(&s.attrs), })); } @@ -281,6 +282,7 @@ pub(crate) fn parse_struct( r#type: ty, comments: parse_comment_attrs(&s.attrs), generic_types, + decorators: get_decorators(&s.attrs), }) } // Unit structs or `None` @@ -320,6 +322,7 @@ pub(crate) fn parse_enum(e: &ItemEnum, target_os: Option<&str>) -> Result Result { r#type: ty, comments: parse_comment_attrs(&t.attrs), generic_types, + decorators: get_decorators(&t.attrs), })) } diff --git a/core/src/rust_types.rs b/core/src/rust_types.rs index 146849b0..7a5a4f64 100644 --- a/core/src/rust_types.rs +++ b/core/src/rust_types.rs @@ -64,6 +64,8 @@ pub struct RustTypeAlias { pub r#type: RustType, /// Comments that were in the type alias source. pub comments: Vec, + /// Attributes that exist for this struct. + pub decorators: DecoratorMap, } /// Rust field definition. From d72675201701bc89a4e247bf24b6bf2914af7b75 Mon Sep 17 00:00:00 2001 From: Horia Culea Date: Tue, 16 Jul 2024 12:24:46 +0200 Subject: [PATCH 21/24] Capitalize all variant names when configuration specifies so in Go --- core/src/language/go.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/core/src/language/go.rs b/core/src/language/go.rs index 2977a6a7..f6d24567 100644 --- a/core/src/language/go.rs +++ b/core/src/language/go.rs @@ -278,6 +278,7 @@ impl Go { )); if let Some(variant_type) = variant_type { + let variant_type = self.acronyms_to_uppercase(&variant_type); let (variant_pointer, variant_deref, variant_ref) = match (v, custom_structs.contains(&variant_type.as_str())) { (RustEnumVariant::AnonymousStruct { .. }, ..) | (.., true) => { From 104ae1a2eae9d6e01ec323a3464bbd444ba4fd0a Mon Sep 17 00:00:00 2001 From: Jonathan Duran Date: Tue, 16 Jul 2024 15:05:01 -0400 Subject: [PATCH 22/24] Introduce redacted typeshare parameter --- core/data/tests/struct_decorator/input.rs | 4 ++-- core/src/language/kotlin.rs | 12 ++---------- core/src/language/mod.rs | 1 + core/src/parser.rs | 15 +++++++++++++++ core/src/rust_types.rs | 6 ++++++ 5 files changed, 26 insertions(+), 12 deletions(-) diff --git a/core/data/tests/struct_decorator/input.rs b/core/data/tests/struct_decorator/input.rs index c3700f13..1ebbf5ac 100644 --- a/core/data/tests/struct_decorator/input.rs +++ b/core/data/tests/struct_decorator/input.rs @@ -15,7 +15,7 @@ pub struct BestHockeyTeams2 { Lies: String, } -#[typeshare(kotlin = "Redacted")] +#[typeshare(redacted)] pub struct BestHockeyTeams3 { PittsburghPenguins: u32, Lies: String, @@ -27,5 +27,5 @@ pub struct BestHockeyTeams4 { Lies: String, } -#[typeshare(kotlin = "JvmInline, Redacted")] +#[typeshare(kotlin = "JvmInline", redacted)] pub struct BestHockeyTeams5(String); diff --git a/core/src/language/kotlin.rs b/core/src/language/kotlin.rs index 19ef594c..4824b9b8 100644 --- a/core/src/language/kotlin.rs +++ b/core/src/language/kotlin.rs @@ -12,7 +12,6 @@ use lazy_format::lazy_format; use std::{collections::BTreeSet, collections::HashMap, io::Write}; const INLINE: &str = "JvmInline"; -const REDACTED: &str = "Redacted"; /// All information needed for Kotlin type-code #[derive(Default)] @@ -143,7 +142,7 @@ impl Language for Kotlin { writeln!(w)?; - if self.is_redacted(&ty.decorators) { + if ty.is_redacted { writeln!(w, ") {{")?; writeln!(w, "\toverride fun toString(): String = \"***\"")?; writeln!(w, "}}")?; @@ -204,7 +203,7 @@ impl Language for Kotlin { writeln!(w)?; } - if self.is_redacted(&rs.decorators) { + if rs.is_redacted { writeln!(w, ") {{")?; writeln!(w, "\toverride fun toString(): String = {:?}", rs.id.renamed)?; writeln!(w, "}}")?; @@ -448,13 +447,6 @@ impl Kotlin { .try_for_each(|comment| self.write_comment(w, indent, comment)) } - fn is_redacted(&self, decorators: &HashMap>) -> bool { - match decorators.get(&DecoratorKind::Kotlin) { - Some(kotlin_decorators) => kotlin_decorators.iter().contains(&String::from(REDACTED)), - _ => false, - } - } - fn is_inline(&self, decorators: &HashMap>) -> bool { match decorators.get(&DecoratorKind::Kotlin) { Some(kotlin_decorators) => kotlin_decorators.iter().contains(&String::from(INLINE)), diff --git a/core/src/language/mod.rs b/core/src/language/mod.rs index 666118de..671afce0 100644 --- a/core/src/language/mod.rs +++ b/core/src/language/mod.rs @@ -387,6 +387,7 @@ pub trait Language { &e.shared().id.original, )], decorators: e.shared().decorators.clone(), + is_redacted: e.shared().is_redacted, }, )?; } diff --git a/core/src/parser.rs b/core/src/parser.rs index 133aaf11..1c8351aa 100644 --- a/core/src/parser.rs +++ b/core/src/parser.rs @@ -222,6 +222,7 @@ pub(crate) fn parse_struct( comments: parse_comment_attrs(&s.attrs), generic_types, decorators: get_decorators(&s.attrs), + is_redacted: is_redacted(&s.attrs), })); } @@ -262,6 +263,7 @@ pub(crate) fn parse_struct( fields, comments: parse_comment_attrs(&s.attrs), decorators: get_decorators(&s.attrs), + is_redacted: is_redacted(&s.attrs), }) } // Tuple structs @@ -283,6 +285,7 @@ pub(crate) fn parse_struct( comments: parse_comment_attrs(&s.attrs), generic_types, decorators: get_decorators(&s.attrs), + is_redacted: is_redacted(&s.attrs), }) } // Unit structs or `None` @@ -292,6 +295,7 @@ pub(crate) fn parse_struct( fields: vec![], comments: parse_comment_attrs(&s.attrs), decorators: get_decorators(&s.attrs), + is_redacted: is_redacted(&s.attrs), }), }) } @@ -323,6 +327,7 @@ pub(crate) fn parse_enum(e: &ItemEnum, target_os: Option<&str>) -> Result) -> Result Result { comments: parse_comment_attrs(&t.attrs), generic_types, decorators: get_decorators(&t.attrs), + is_redacted: is_redacted(&t.attrs), })) } @@ -603,6 +610,14 @@ fn is_skipped(attrs: &[syn::Attribute], target_os: Option<&str>) -> bool { .unwrap_or(false) } +// `#[typeshare(redacted)]` +fn is_redacted(attrs: &[syn::Attribute]) -> bool { + attrs.iter().any(|attr| { + get_meta_items(attr, TYPESHARE) + .any(|arg| matches!(arg, Meta::Path(path) if path.is_ident("redacted"))) + }) +} + /// Check if we have a `target_os` cfg that dooes not match command line /// argument `--target-os`. #[inline] diff --git a/core/src/rust_types.rs b/core/src/rust_types.rs index 7a5a4f64..92d45e44 100644 --- a/core/src/rust_types.rs +++ b/core/src/rust_types.rs @@ -48,6 +48,8 @@ pub struct RustStruct { pub comments: Vec, /// Attributes that exist for this struct. pub decorators: DecoratorMap, + /// True if this struct contains data that needs to be redacted + pub is_redacted: bool, } /// Rust type alias. @@ -66,6 +68,8 @@ pub struct RustTypeAlias { pub comments: Vec, /// Attributes that exist for this struct. pub decorators: DecoratorMap, + /// True if this type alias contains data that needs to be redacted + pub is_redacted: bool, } /// Rust field definition. @@ -576,6 +580,8 @@ pub struct RustEnumShared { /// True if this enum references itself in any field of any variant /// Swift needs the special keyword `indirect` for this case pub is_recursive: bool, + /// True if this enum contains data that needs to be redacted + pub is_redacted: bool, } /// Parsed information about a Rust enum variant From ff2b13b3a10844f2cb54da8070c3f7b29700622b Mon Sep 17 00:00:00 2001 From: Darrell Roberts Date: Thu, 18 Jul 2024 14:14:03 -0400 Subject: [PATCH 23/24] keep the file output deterministic --- Cargo.lock | 1 + cli/Cargo.toml | 1 + cli/src/main.rs | 3 ++- cli/src/parse.rs | 11 ++++++----- cli/src/writer.rs | 9 +++++---- 5 files changed, 15 insertions(+), 10 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 739a35e6..e259578a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -616,6 +616,7 @@ dependencies = [ "clap", "clap_complete_command", "ignore", + "indexmap 2.2.6", "once_cell", "rayon", "serde", diff --git a/cli/Cargo.toml b/cli/Cargo.toml index c6ea7bc6..d123b8d3 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -24,3 +24,4 @@ serde = { version = "1", features = ["derive"] } toml = "0.8" typeshare-core = { path = "../core", version = "1.10.0-beta.6" } anyhow = "1" +indexmap = "2" diff --git a/cli/src/main.rs b/cli/src/main.rs index 0b4ee57b..312e1e6e 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -10,6 +10,7 @@ use args::{ use clap::ArgMatches; use config::Config; use ignore::{overrides::OverrideBuilder, types::TypesBuilder, WalkBuilder}; +use indexmap::IndexMap; use parse::{all_types, parse_input, parser_inputs}; use rayon::iter::ParallelBridge; use std::collections::HashMap; @@ -225,7 +226,7 @@ fn override_configuration(mut config: Config, options: &ArgMatches) -> Config { } /// Prints out all parsing errors if any and returns Err. -fn check_parse_errors(parsed_crates: &HashMap) -> anyhow::Result<()> { +fn check_parse_errors(parsed_crates: &IndexMap) -> anyhow::Result<()> { let mut errors_encountered = false; for data in parsed_crates .values() diff --git a/cli/src/parse.rs b/cli/src/parse.rs index 47ed56ae..5ff95a1a 100644 --- a/cli/src/parse.rs +++ b/cli/src/parse.rs @@ -1,6 +1,7 @@ //! Source file parsing. use anyhow::Context; use ignore::WalkBuilder; +use indexmap::IndexMap; use rayon::iter::{IntoParallelIterator, ParallelIterator}; use std::{ collections::{hash_map::Entry, HashMap}, @@ -66,7 +67,7 @@ fn output_file_name(language_type: SupportedLanguage, crate_name: &CrateName) -> /// Collect all the typeshared types into a mapping of crate names to typeshared types. This /// mapping is used to lookup and generated import statements for generated files. -pub fn all_types(file_mappings: &HashMap) -> CrateTypes { +pub fn all_types(file_mappings: &IndexMap) -> CrateTypes { file_mappings .iter() .map(|(crate_name, parsed_data)| (crate_name, parsed_data.type_names.clone())) @@ -92,12 +93,12 @@ pub fn parse_input( ignored_types: &[&str], multi_file: bool, target_os: Option, -) -> anyhow::Result> { +) -> anyhow::Result> { inputs .into_par_iter() .try_fold( - HashMap::new, - |mut parsed_crates: HashMap, + IndexMap::new, + |mut parsed_crates: IndexMap, ParserInput { file_path, file_name, @@ -125,7 +126,7 @@ pub fn parse_input( Ok(parsed_crates) }, ) - .try_reduce(HashMap::new, |mut file_maps, parsed_crates| { + .try_reduce(IndexMap::new, |mut file_maps, parsed_crates| { for (crate_name, parsed_data) in parsed_crates { file_maps.entry(crate_name).or_default().add(parsed_data); } diff --git a/cli/src/writer.rs b/cli/src/writer.rs index 4fbbc968..ab3696ee 100644 --- a/cli/src/writer.rs +++ b/cli/src/writer.rs @@ -2,6 +2,7 @@ use crate::args::{ARG_OUTPUT_FILE, ARG_OUTPUT_FOLDER}; use anyhow::Context; use clap::ArgMatches; +use indexmap::IndexMap; use std::{ collections::HashMap, fs, @@ -16,7 +17,7 @@ use typeshare_core::{ pub fn write_generated( options: ArgMatches, lang: Box, - crate_parsed_data: HashMap, + crate_parsed_data: IndexMap, import_candidates: CrateTypes, ) -> Result<(), anyhow::Error> { let output_folder = options.value_of(ARG_OUTPUT_FOLDER); @@ -35,7 +36,7 @@ pub fn write_generated( fn write_multiple_files( mut lang: Box, output_folder: &str, - crate_parsed_data: HashMap, + crate_parsed_data: IndexMap, import_candidates: CrateTypes, ) -> Result<(), anyhow::Error> { for (_crate_name, parsed_data) in crate_parsed_data { @@ -83,10 +84,10 @@ fn check_write_file(outfile: &PathBuf, output: Vec) -> anyhow::Result<()> { fn write_single_file( mut lang: Box, file_name: &str, - mut crate_parsed_data: HashMap, + mut crate_parsed_data: IndexMap, ) -> Result<(), anyhow::Error> { let parsed_data = crate_parsed_data - .remove(&SINGLE_FILE_CRATE_NAME) + .shift_remove(&SINGLE_FILE_CRATE_NAME) .context("Could not get parsed data for single file output")?; let mut output = Vec::new(); From ecc8ce7b12a0ebc79f47a57ac0b238b7e4d21ec8 Mon Sep 17 00:00:00 2001 From: Darrell Roberts Date: Thu, 18 Jul 2024 15:18:22 -0400 Subject: [PATCH 24/24] Deterministic output I want to use a parallel iterator while walking the input folders however in order to maintain a deterministic output I'll have to keep the crate to parsed data sorted along with the types within each parsed data. Consequently the tests have been updated since they did not have this sorting before. --- Cargo.lock | 1 - cli/Cargo.toml | 1 - cli/src/main.rs | 5 +- cli/src/parse.rs | 13 +- cli/src/writer.rs | 11 +- .../output.go | 4 +- .../output.kt | 4 +- .../output.scala | 4 +- .../output.swift | 4 +- .../output.ts | 4 +- .../tests/excluded_by_target_os/output.go | 6 +- .../tests/excluded_by_target_os/output.kt | 8 +- .../tests/excluded_by_target_os/output.scala | 12 +- .../tests/excluded_by_target_os/output.swift | 6 +- .../tests/excluded_by_target_os/output.ts | 6 +- .../generate_types_with_keywords/output.swift | 10 +- core/data/tests/orders_types/output.go | 6 +- core/data/tests/orders_types/output.kt | 10 +- core/data/tests/orders_types/output.swift | 16 +- .../tests/recursive_enum_decorator/output.go | 164 +++++++++--------- .../tests/recursive_enum_decorator/output.kt | 26 +-- .../recursive_enum_decorator/output.scala | 30 ++-- .../recursive_enum_decorator/output.swift | 110 ++++++------ .../tests/recursive_enum_decorator/output.ts | 10 +- .../data/tests/serialize_type_alias/output.go | 4 +- .../data/tests/serialize_type_alias/output.kt | 4 +- .../tests/serialize_type_alias/output.scala | 8 +- .../tests/serialize_type_alias/output.swift | 4 +- .../data/tests/serialize_type_alias/output.ts | 4 +- core/data/tests/smart_pointers/output.go | 24 +-- core/data/tests/smart_pointers/output.kt | 28 +-- core/data/tests/smart_pointers/output.scala | 28 +-- core/data/tests/smart_pointers/output.swift | 48 ++--- core/data/tests/smart_pointers/output.ts | 24 +-- .../tests/test_optional_type_alias/output.go | 4 +- .../tests/test_optional_type_alias/output.kt | 4 +- .../test_optional_type_alias/output.scala | 4 +- .../test_optional_type_alias/output.swift | 4 +- .../tests/test_optional_type_alias/output.ts | 4 +- core/src/parser.rs | 12 +- core/src/rust_types.rs | 66 ++++++- 41 files changed, 400 insertions(+), 345 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e259578a..739a35e6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -616,7 +616,6 @@ dependencies = [ "clap", "clap_complete_command", "ignore", - "indexmap 2.2.6", "once_cell", "rayon", "serde", diff --git a/cli/Cargo.toml b/cli/Cargo.toml index d123b8d3..c6ea7bc6 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -24,4 +24,3 @@ serde = { version = "1", features = ["derive"] } toml = "0.8" typeshare-core = { path = "../core", version = "1.10.0-beta.6" } anyhow = "1" -indexmap = "2" diff --git a/cli/src/main.rs b/cli/src/main.rs index 312e1e6e..9837bfe7 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -10,10 +10,9 @@ use args::{ use clap::ArgMatches; use config::Config; use ignore::{overrides::OverrideBuilder, types::TypesBuilder, WalkBuilder}; -use indexmap::IndexMap; use parse::{all_types, parse_input, parser_inputs}; use rayon::iter::ParallelBridge; -use std::collections::HashMap; +use std::collections::{BTreeMap, HashMap}; #[cfg(feature = "go")] use typeshare_core::language::Go; use typeshare_core::{ @@ -226,7 +225,7 @@ fn override_configuration(mut config: Config, options: &ArgMatches) -> Config { } /// Prints out all parsing errors if any and returns Err. -fn check_parse_errors(parsed_crates: &IndexMap) -> anyhow::Result<()> { +fn check_parse_errors(parsed_crates: &BTreeMap) -> anyhow::Result<()> { let mut errors_encountered = false; for data in parsed_crates .values() diff --git a/cli/src/parse.rs b/cli/src/parse.rs index 5ff95a1a..963926bb 100644 --- a/cli/src/parse.rs +++ b/cli/src/parse.rs @@ -1,10 +1,9 @@ //! Source file parsing. use anyhow::Context; use ignore::WalkBuilder; -use indexmap::IndexMap; use rayon::iter::{IntoParallelIterator, ParallelIterator}; use std::{ - collections::{hash_map::Entry, HashMap}, + collections::{hash_map::Entry, BTreeMap, HashMap}, path::PathBuf, }; use typeshare_core::{ @@ -67,7 +66,7 @@ fn output_file_name(language_type: SupportedLanguage, crate_name: &CrateName) -> /// Collect all the typeshared types into a mapping of crate names to typeshared types. This /// mapping is used to lookup and generated import statements for generated files. -pub fn all_types(file_mappings: &IndexMap) -> CrateTypes { +pub fn all_types(file_mappings: &BTreeMap) -> CrateTypes { file_mappings .iter() .map(|(crate_name, parsed_data)| (crate_name, parsed_data.type_names.clone())) @@ -93,12 +92,12 @@ pub fn parse_input( ignored_types: &[&str], multi_file: bool, target_os: Option, -) -> anyhow::Result> { +) -> anyhow::Result> { inputs .into_par_iter() .try_fold( - IndexMap::new, - |mut parsed_crates: IndexMap, + BTreeMap::new, + |mut parsed_crates: BTreeMap, ParserInput { file_path, file_name, @@ -126,7 +125,7 @@ pub fn parse_input( Ok(parsed_crates) }, ) - .try_reduce(IndexMap::new, |mut file_maps, parsed_crates| { + .try_reduce(BTreeMap::new, |mut file_maps, parsed_crates| { for (crate_name, parsed_data) in parsed_crates { file_maps.entry(crate_name).or_default().add(parsed_data); } diff --git a/cli/src/writer.rs b/cli/src/writer.rs index ab3696ee..457dc84a 100644 --- a/cli/src/writer.rs +++ b/cli/src/writer.rs @@ -2,9 +2,8 @@ use crate::args::{ARG_OUTPUT_FILE, ARG_OUTPUT_FOLDER}; use anyhow::Context; use clap::ArgMatches; -use indexmap::IndexMap; use std::{ - collections::HashMap, + collections::{BTreeMap, HashMap}, fs, path::{Path, PathBuf}, }; @@ -17,7 +16,7 @@ use typeshare_core::{ pub fn write_generated( options: ArgMatches, lang: Box, - crate_parsed_data: IndexMap, + crate_parsed_data: BTreeMap, import_candidates: CrateTypes, ) -> Result<(), anyhow::Error> { let output_folder = options.value_of(ARG_OUTPUT_FOLDER); @@ -36,7 +35,7 @@ pub fn write_generated( fn write_multiple_files( mut lang: Box, output_folder: &str, - crate_parsed_data: IndexMap, + crate_parsed_data: BTreeMap, import_candidates: CrateTypes, ) -> Result<(), anyhow::Error> { for (_crate_name, parsed_data) in crate_parsed_data { @@ -84,10 +83,10 @@ fn check_write_file(outfile: &PathBuf, output: Vec) -> anyhow::Result<()> { fn write_single_file( mut lang: Box, file_name: &str, - mut crate_parsed_data: IndexMap, + mut crate_parsed_data: BTreeMap, ) -> Result<(), anyhow::Error> { let parsed_data = crate_parsed_data - .shift_remove(&SINGLE_FILE_CRATE_NAME) + .remove(&SINGLE_FILE_CRATE_NAME) .context("Could not get parsed data for single file output")?; let mut output = Vec::new(); diff --git a/core/data/tests/can_recognize_types_inside_modules/output.go b/core/data/tests/can_recognize_types_inside_modules/output.go index f08ec778..b89511f4 100644 --- a/core/data/tests/can_recognize_types_inside_modules/output.go +++ b/core/data/tests/can_recognize_types_inside_modules/output.go @@ -5,10 +5,10 @@ import "encoding/json" type A struct { Field uint32 `json:"field"` } -type ABC struct { +type AB struct { Field uint32 `json:"field"` } -type AB struct { +type ABC struct { Field uint32 `json:"field"` } type OutsideOfModules struct { diff --git a/core/data/tests/can_recognize_types_inside_modules/output.kt b/core/data/tests/can_recognize_types_inside_modules/output.kt index a5bf8320..074d380a 100644 --- a/core/data/tests/can_recognize_types_inside_modules/output.kt +++ b/core/data/tests/can_recognize_types_inside_modules/output.kt @@ -9,12 +9,12 @@ data class A ( ) @Serializable -data class ABC ( +data class AB ( val field: UInt ) @Serializable -data class AB ( +data class ABC ( val field: UInt ) diff --git a/core/data/tests/can_recognize_types_inside_modules/output.scala b/core/data/tests/can_recognize_types_inside_modules/output.scala index 5a2e40b0..4847b5a3 100644 --- a/core/data/tests/can_recognize_types_inside_modules/output.scala +++ b/core/data/tests/can_recognize_types_inside_modules/output.scala @@ -14,11 +14,11 @@ case class A ( field: UInt ) -case class ABC ( +case class AB ( field: UInt ) -case class AB ( +case class ABC ( field: UInt ) diff --git a/core/data/tests/can_recognize_types_inside_modules/output.swift b/core/data/tests/can_recognize_types_inside_modules/output.swift index ffaebb38..04c199ec 100644 --- a/core/data/tests/can_recognize_types_inside_modules/output.swift +++ b/core/data/tests/can_recognize_types_inside_modules/output.swift @@ -8,7 +8,7 @@ public struct A: Codable { } } -public struct ABC: Codable { +public struct AB: Codable { public let field: UInt32 public init(field: UInt32) { @@ -16,7 +16,7 @@ public struct ABC: Codable { } } -public struct AB: Codable { +public struct ABC: Codable { public let field: UInt32 public init(field: UInt32) { diff --git a/core/data/tests/can_recognize_types_inside_modules/output.ts b/core/data/tests/can_recognize_types_inside_modules/output.ts index 00c101e9..c7ec397c 100644 --- a/core/data/tests/can_recognize_types_inside_modules/output.ts +++ b/core/data/tests/can_recognize_types_inside_modules/output.ts @@ -2,11 +2,11 @@ export interface A { field: number; } -export interface ABC { +export interface AB { field: number; } -export interface AB { +export interface ABC { field: number; } diff --git a/core/data/tests/excluded_by_target_os/output.go b/core/data/tests/excluded_by_target_os/output.go index bc7a64ec..613db1c2 100644 --- a/core/data/tests/excluded_by_target_os/output.go +++ b/core/data/tests/excluded_by_target_os/output.go @@ -2,11 +2,11 @@ package proto import "encoding/json" +type SomeEnum string +const ( +) type TestEnum string const ( TestEnumVariant1 TestEnum = "Variant1" TestEnumVariant5 TestEnum = "Variant5" ) -type SomeEnum string -const ( -) diff --git a/core/data/tests/excluded_by_target_os/output.kt b/core/data/tests/excluded_by_target_os/output.kt index 73f0c614..a177133a 100644 --- a/core/data/tests/excluded_by_target_os/output.kt +++ b/core/data/tests/excluded_by_target_os/output.kt @@ -3,6 +3,10 @@ package com.agilebits.onepassword import kotlinx.serialization.Serializable import kotlinx.serialization.SerialName +@Serializable +enum class SomeEnum(val string: String) { +} + @Serializable enum class TestEnum(val string: String) { @SerialName("Variant1") @@ -11,7 +15,3 @@ enum class TestEnum(val string: String) { Variant5("Variant5"), } -@Serializable -enum class SomeEnum(val string: String) { -} - diff --git a/core/data/tests/excluded_by_target_os/output.scala b/core/data/tests/excluded_by_target_os/output.scala index 6876c694..f8076058 100644 --- a/core/data/tests/excluded_by_target_os/output.scala +++ b/core/data/tests/excluded_by_target_os/output.scala @@ -2,6 +2,12 @@ package com.agilebits package onepassword { +sealed trait SomeEnum { + def serialName: String +} +object SomeEnum { +} + sealed trait TestEnum { def serialName: String } @@ -14,10 +20,4 @@ object TestEnum { } } -sealed trait SomeEnum { - def serialName: String -} -object SomeEnum { -} - } diff --git a/core/data/tests/excluded_by_target_os/output.swift b/core/data/tests/excluded_by_target_os/output.swift index 4a6c4e3a..fa30b52e 100644 --- a/core/data/tests/excluded_by_target_os/output.swift +++ b/core/data/tests/excluded_by_target_os/output.swift @@ -1,9 +1,9 @@ import Foundation +public enum SomeEnum: String, Codable { +} + public enum TestEnum: String, Codable { case variant1 = "Variant1" case variant5 = "Variant5" } - -public enum SomeEnum: String, Codable { -} diff --git a/core/data/tests/excluded_by_target_os/output.ts b/core/data/tests/excluded_by_target_os/output.ts index 2f793b62..d5fce3c6 100644 --- a/core/data/tests/excluded_by_target_os/output.ts +++ b/core/data/tests/excluded_by_target_os/output.ts @@ -1,8 +1,8 @@ +export enum SomeEnum { +} + export enum TestEnum { Variant1 = "Variant1", Variant5 = "Variant5", } -export enum SomeEnum { -} - diff --git a/core/data/tests/generate_types_with_keywords/output.swift b/core/data/tests/generate_types_with_keywords/output.swift index 673f5c75..3fdee40c 100644 --- a/core/data/tests/generate_types_with_keywords/output.swift +++ b/core/data/tests/generate_types_with_keywords/output.swift @@ -10,11 +10,6 @@ public struct `catch`: Codable { } } -public enum `throws`: String, Codable { - case `case` - case `default` -} - public enum `switch`: Codable { case `default`(`catch`) @@ -49,3 +44,8 @@ public enum `switch`: Codable { } } } + +public enum `throws`: String, Codable { + case `case` + case `default` +} diff --git a/core/data/tests/orders_types/output.go b/core/data/tests/orders_types/output.go index 278a605d..3ca7866f 100644 --- a/core/data/tests/orders_types/output.go +++ b/core/data/tests/orders_types/output.go @@ -11,10 +11,10 @@ type B struct { type C struct { DependsOn B `json:"dependsOn"` } +type E struct { + DependsOn D `json:"dependsOn"` +} type D struct { DependsOn C `json:"dependsOn"` AlsoDependsOn *E `json:"alsoDependsOn,omitempty"` } -type E struct { - DependsOn D `json:"dependsOn"` -} diff --git a/core/data/tests/orders_types/output.kt b/core/data/tests/orders_types/output.kt index dd8d1fe5..39f09f07 100644 --- a/core/data/tests/orders_types/output.kt +++ b/core/data/tests/orders_types/output.kt @@ -19,13 +19,13 @@ data class C ( ) @Serializable -data class D ( - val dependsOn: C, - val alsoDependsOn: E? = null +data class E ( + val dependsOn: D ) @Serializable -data class E ( - val dependsOn: D +data class D ( + val dependsOn: C, + val alsoDependsOn: E? = null ) diff --git a/core/data/tests/orders_types/output.swift b/core/data/tests/orders_types/output.swift index 3b5277a4..197f8628 100644 --- a/core/data/tests/orders_types/output.swift +++ b/core/data/tests/orders_types/output.swift @@ -24,20 +24,20 @@ public struct C: Codable { } } -public struct D: Codable { - public let dependsOn: C - public let alsoDependsOn: E? +public struct E: Codable { + public let dependsOn: D - public init(dependsOn: C, alsoDependsOn: E?) { + public init(dependsOn: D) { self.dependsOn = dependsOn - self.alsoDependsOn = alsoDependsOn } } -public struct E: Codable { - public let dependsOn: D +public struct D: Codable { + public let dependsOn: C + public let alsoDependsOn: E? - public init(dependsOn: D) { + public init(dependsOn: C, alsoDependsOn: E?) { self.dependsOn = dependsOn + self.alsoDependsOn = alsoDependsOn } } diff --git a/core/data/tests/recursive_enum_decorator/output.go b/core/data/tests/recursive_enum_decorator/output.go index 89f5f820..fde053a9 100644 --- a/core/data/tests/recursive_enum_decorator/output.go +++ b/core/data/tests/recursive_enum_decorator/output.go @@ -2,88 +2,6 @@ package proto import "encoding/json" -type OptionsTypes string -const ( - OptionsTypeVariantRed OptionsTypes = "Red" - OptionsTypeVariantBanana OptionsTypes = "Banana" - OptionsTypeVariantVermont OptionsTypes = "Vermont" -) -type Options struct{ - Type OptionsTypes `json:"type"` - content interface{} -} - -func (o *Options) UnmarshalJSON(data []byte) error { - var enum struct { - Tag OptionsTypes `json:"type"` - Content json.RawMessage `json:"content"` - } - if err := json.Unmarshal(data, &enum); err != nil { - return err - } - - o.Type = enum.Tag - switch o.Type { - case OptionsTypeVariantRed: - var res bool - o.content = &res - case OptionsTypeVariantBanana: - var res string - o.content = &res - case OptionsTypeVariantVermont: - var res Options - o.content = &res - - } - if err := json.Unmarshal(enum.Content, &o.content); err != nil { - return err - } - - return nil -} - -func (o Options) MarshalJSON() ([]byte, error) { - var enum struct { - Tag OptionsTypes `json:"type"` - Content interface{} `json:"content,omitempty"` - } - enum.Tag = o.Type - enum.Content = o.content - return json.Marshal(enum) -} - -func (o Options) Red() bool { - res, _ := o.content.(*bool) - return *res -} -func (o Options) Banana() string { - res, _ := o.content.(*string) - return *res -} -func (o Options) Vermont() Options { - res, _ := o.content.(*Options) - return *res -} - -func NewOptionsTypeVariantRed(content bool) Options { - return Options{ - Type: OptionsTypeVariantRed, - content: &content, - } -} -func NewOptionsTypeVariantBanana(content string) Options { - return Options{ - Type: OptionsTypeVariantBanana, - content: &content, - } -} -func NewOptionsTypeVariantVermont(content Options) Options { - return Options{ - Type: OptionsTypeVariantVermont, - content: &content, - } -} - // Generated type representing the anonymous struct variant `Exactly` of the `MoreOptions` Rust enum type MoreOptionsExactlyInner struct { Config string `json:"config"` @@ -174,3 +92,85 @@ func NewMoreOptionsTypeVariantBuilt(content *MoreOptionsBuiltInner) MoreOptions } } +type OptionsTypes string +const ( + OptionsTypeVariantRed OptionsTypes = "Red" + OptionsTypeVariantBanana OptionsTypes = "Banana" + OptionsTypeVariantVermont OptionsTypes = "Vermont" +) +type Options struct{ + Type OptionsTypes `json:"type"` + content interface{} +} + +func (o *Options) UnmarshalJSON(data []byte) error { + var enum struct { + Tag OptionsTypes `json:"type"` + Content json.RawMessage `json:"content"` + } + if err := json.Unmarshal(data, &enum); err != nil { + return err + } + + o.Type = enum.Tag + switch o.Type { + case OptionsTypeVariantRed: + var res bool + o.content = &res + case OptionsTypeVariantBanana: + var res string + o.content = &res + case OptionsTypeVariantVermont: + var res Options + o.content = &res + + } + if err := json.Unmarshal(enum.Content, &o.content); err != nil { + return err + } + + return nil +} + +func (o Options) MarshalJSON() ([]byte, error) { + var enum struct { + Tag OptionsTypes `json:"type"` + Content interface{} `json:"content,omitempty"` + } + enum.Tag = o.Type + enum.Content = o.content + return json.Marshal(enum) +} + +func (o Options) Red() bool { + res, _ := o.content.(*bool) + return *res +} +func (o Options) Banana() string { + res, _ := o.content.(*string) + return *res +} +func (o Options) Vermont() Options { + res, _ := o.content.(*Options) + return *res +} + +func NewOptionsTypeVariantRed(content bool) Options { + return Options{ + Type: OptionsTypeVariantRed, + content: &content, + } +} +func NewOptionsTypeVariantBanana(content string) Options { + return Options{ + Type: OptionsTypeVariantBanana, + content: &content, + } +} +func NewOptionsTypeVariantVermont(content Options) Options { + return Options{ + Type: OptionsTypeVariantVermont, + content: &content, + } +} + diff --git a/core/data/tests/recursive_enum_decorator/output.kt b/core/data/tests/recursive_enum_decorator/output.kt index b1c73aa4..1ed9fccb 100644 --- a/core/data/tests/recursive_enum_decorator/output.kt +++ b/core/data/tests/recursive_enum_decorator/output.kt @@ -3,19 +3,6 @@ package com.agilebits.onepassword import kotlinx.serialization.Serializable import kotlinx.serialization.SerialName -@Serializable -sealed class Options { - @Serializable - @SerialName("red") - data class Red(val content: Boolean): Options() - @Serializable - @SerialName("banana") - data class Banana(val content: String): Options() - @Serializable - @SerialName("vermont") - data class Vermont(val content: Options): Options() -} - /// Generated type representing the anonymous struct variant `Exactly` of the `MoreOptions` Rust enum @Serializable data class MoreOptionsExactlyInner ( @@ -41,3 +28,16 @@ sealed class MoreOptions { data class Built(val content: MoreOptionsBuiltInner): MoreOptions() } +@Serializable +sealed class Options { + @Serializable + @SerialName("red") + data class Red(val content: Boolean): Options() + @Serializable + @SerialName("banana") + data class Banana(val content: String): Options() + @Serializable + @SerialName("vermont") + data class Vermont(val content: Options): Options() +} + diff --git a/core/data/tests/recursive_enum_decorator/output.scala b/core/data/tests/recursive_enum_decorator/output.scala index fd6208bc..67254469 100644 --- a/core/data/tests/recursive_enum_decorator/output.scala +++ b/core/data/tests/recursive_enum_decorator/output.scala @@ -2,21 +2,6 @@ package com.agilebits package onepassword { -sealed trait Options { - def serialName: String -} -object Options { - case class Red(content: Boolean) extends Options { - val serialName: String = "red" - } - case class Banana(content: String) extends Options { - val serialName: String = "banana" - } - case class Vermont(content: Options) extends Options { - val serialName: String = "vermont" - } -} - // Generated type representing the anonymous struct variant `Exactly` of the `MoreOptions` Rust enum case class MoreOptionsExactlyInner ( config: String @@ -42,4 +27,19 @@ object MoreOptions { } } +sealed trait Options { + def serialName: String +} +object Options { + case class Red(content: Boolean) extends Options { + val serialName: String = "red" + } + case class Banana(content: String) extends Options { + val serialName: String = "banana" + } + case class Vermont(content: Options) extends Options { + val serialName: String = "vermont" + } +} + } diff --git a/core/data/tests/recursive_enum_decorator/output.swift b/core/data/tests/recursive_enum_decorator/output.swift index a1ae7109..abb4b1cc 100644 --- a/core/data/tests/recursive_enum_decorator/output.swift +++ b/core/data/tests/recursive_enum_decorator/output.swift @@ -1,60 +1,5 @@ import Foundation -public indirect enum Options: Codable { - case red(Bool) - case banana(String) - case vermont(Options) - - enum CodingKeys: String, CodingKey, Codable { - case red, - banana, - vermont - } - - private enum ContainerCodingKeys: String, CodingKey { - case type, content - } - - public init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: ContainerCodingKeys.self) - if let type = try? container.decode(CodingKeys.self, forKey: .type) { - switch type { - case .red: - if let content = try? container.decode(Bool.self, forKey: .content) { - self = .red(content) - return - } - case .banana: - if let content = try? container.decode(String.self, forKey: .content) { - self = .banana(content) - return - } - case .vermont: - if let content = try? container.decode(Options.self, forKey: .content) { - self = .vermont(content) - return - } - } - } - throw DecodingError.typeMismatch(Options.self, DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Wrong type for Options")) - } - - public func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: ContainerCodingKeys.self) - switch self { - case .red(let content): - try container.encode(CodingKeys.red, forKey: .type) - try container.encode(content, forKey: .content) - case .banana(let content): - try container.encode(CodingKeys.banana, forKey: .type) - try container.encode(content, forKey: .content) - case .vermont(let content): - try container.encode(CodingKeys.vermont, forKey: .type) - try container.encode(content, forKey: .content) - } - } -} - /// Generated type representing the anonymous struct variant `Exactly` of the `MoreOptions` Rust enum public struct MoreOptionsExactlyInner: Codable { @@ -127,3 +72,58 @@ public indirect enum MoreOptions: Codable { } } } + +public indirect enum Options: Codable { + case red(Bool) + case banana(String) + case vermont(Options) + + enum CodingKeys: String, CodingKey, Codable { + case red, + banana, + vermont + } + + private enum ContainerCodingKeys: String, CodingKey { + case type, content + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: ContainerCodingKeys.self) + if let type = try? container.decode(CodingKeys.self, forKey: .type) { + switch type { + case .red: + if let content = try? container.decode(Bool.self, forKey: .content) { + self = .red(content) + return + } + case .banana: + if let content = try? container.decode(String.self, forKey: .content) { + self = .banana(content) + return + } + case .vermont: + if let content = try? container.decode(Options.self, forKey: .content) { + self = .vermont(content) + return + } + } + } + throw DecodingError.typeMismatch(Options.self, DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Wrong type for Options")) + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: ContainerCodingKeys.self) + switch self { + case .red(let content): + try container.encode(CodingKeys.red, forKey: .type) + try container.encode(content, forKey: .content) + case .banana(let content): + try container.encode(CodingKeys.banana, forKey: .type) + try container.encode(content, forKey: .content) + case .vermont(let content): + try container.encode(CodingKeys.vermont, forKey: .type) + try container.encode(content, forKey: .content) + } + } +} diff --git a/core/data/tests/recursive_enum_decorator/output.ts b/core/data/tests/recursive_enum_decorator/output.ts index c237e978..e1367523 100644 --- a/core/data/tests/recursive_enum_decorator/output.ts +++ b/core/data/tests/recursive_enum_decorator/output.ts @@ -1,8 +1,3 @@ -export type Options = - | { type: "red", content: boolean } - | { type: "banana", content: string } - | { type: "vermont", content: Options }; - export type MoreOptions = | { type: "news", content: boolean } | { type: "exactly", content: { @@ -12,3 +7,8 @@ export type MoreOptions = top: MoreOptions; }}; +export type Options = + | { type: "red", content: boolean } + | { type: "banana", content: string } + | { type: "vermont", content: Options }; + diff --git a/core/data/tests/serialize_type_alias/output.go b/core/data/tests/serialize_type_alias/output.go index 87c436a6..eba948c4 100644 --- a/core/data/tests/serialize_type_alias/output.go +++ b/core/data/tests/serialize_type_alias/output.go @@ -2,12 +2,12 @@ package proto import "encoding/json" -type AlsoString string - type Uuid string // Unique identifier for an Account type AccountUuid Uuid +type AlsoString string + type ItemUuid string diff --git a/core/data/tests/serialize_type_alias/output.kt b/core/data/tests/serialize_type_alias/output.kt index a2f020c6..89012e5c 100644 --- a/core/data/tests/serialize_type_alias/output.kt +++ b/core/data/tests/serialize_type_alias/output.kt @@ -3,12 +3,12 @@ package com.agilebits.onepassword import kotlinx.serialization.Serializable import kotlinx.serialization.SerialName -typealias AlsoString = String - typealias Uuid = String /// Unique identifier for an Account typealias AccountUuid = Uuid +typealias AlsoString = String + typealias ItemUuid = String diff --git a/core/data/tests/serialize_type_alias/output.scala b/core/data/tests/serialize_type_alias/output.scala index 5a022bf3..156a8056 100644 --- a/core/data/tests/serialize_type_alias/output.scala +++ b/core/data/tests/serialize_type_alias/output.scala @@ -2,13 +2,13 @@ package com.agilebits package object onepassword { -type AlsoString = String - -type Uuid = String - // Unique identifier for an Account type AccountUuid = Uuid +type AlsoString = String + type ItemUuid = String +type Uuid = String + } diff --git a/core/data/tests/serialize_type_alias/output.swift b/core/data/tests/serialize_type_alias/output.swift index fe9ac9f4..3de6cfa3 100644 --- a/core/data/tests/serialize_type_alias/output.swift +++ b/core/data/tests/serialize_type_alias/output.swift @@ -1,10 +1,10 @@ import Foundation -public typealias AlsoString = String - public typealias Uuid = String /// Unique identifier for an Account public typealias AccountUuid = Uuid +public typealias AlsoString = String + public typealias ItemUuid = String diff --git a/core/data/tests/serialize_type_alias/output.ts b/core/data/tests/serialize_type_alias/output.ts index f3837004..c3c25030 100644 --- a/core/data/tests/serialize_type_alias/output.ts +++ b/core/data/tests/serialize_type_alias/output.ts @@ -1,9 +1,9 @@ -export type AlsoString = string; - export type Uuid = string; /** Unique identifier for an Account */ export type AccountUuid = Uuid; +export type AlsoString = string; + export type ItemUuid = string; diff --git a/core/data/tests/smart_pointers/output.go b/core/data/tests/smart_pointers/output.go index 1416d1b4..6dffe741 100644 --- a/core/data/tests/smart_pointers/output.go +++ b/core/data/tests/smart_pointers/output.go @@ -9,28 +9,28 @@ type ArcyColors struct { Green []string `json:"green"` } // This is a comment. -type MutexyColors struct { - Blue []string `json:"blue"` - Green string `json:"green"` -} -// This is a comment. -type RcyColors struct { +type CellyColors struct { Red string `json:"red"` Blue []string `json:"blue"` - Green string `json:"green"` } // This is a comment. -type CellyColors struct { - Red string `json:"red"` - Blue []string `json:"blue"` +type CowyColors struct { + Lifetime string `json:"lifetime"` } // This is a comment. type LockyColors struct { Red string `json:"red"` } // This is a comment. -type CowyColors struct { - Lifetime string `json:"lifetime"` +type MutexyColors struct { + Blue []string `json:"blue"` + Green string `json:"green"` +} +// This is a comment. +type RcyColors struct { + Red string `json:"red"` + Blue []string `json:"blue"` + Green string `json:"green"` } // This is a comment. type BoxyColorsTypes string diff --git a/core/data/tests/smart_pointers/output.kt b/core/data/tests/smart_pointers/output.kt index bb5f1f1e..cfb4886e 100644 --- a/core/data/tests/smart_pointers/output.kt +++ b/core/data/tests/smart_pointers/output.kt @@ -13,36 +13,36 @@ data class ArcyColors ( /// This is a comment. @Serializable -data class MutexyColors ( - val blue: List, - val green: String +data class CellyColors ( + val red: String, + val blue: List ) /// This is a comment. @Serializable -data class RcyColors ( - val red: String, - val blue: List, - val green: String +data class CowyColors ( + val lifetime: String ) /// This is a comment. @Serializable -data class CellyColors ( - val red: String, - val blue: List +data class LockyColors ( + val red: String ) /// This is a comment. @Serializable -data class LockyColors ( - val red: String +data class MutexyColors ( + val blue: List, + val green: String ) /// This is a comment. @Serializable -data class CowyColors ( - val lifetime: String +data class RcyColors ( + val red: String, + val blue: List, + val green: String ) /// This is a comment. diff --git a/core/data/tests/smart_pointers/output.scala b/core/data/tests/smart_pointers/output.scala index e035f1f0..6799a29f 100644 --- a/core/data/tests/smart_pointers/output.scala +++ b/core/data/tests/smart_pointers/output.scala @@ -18,22 +18,14 @@ case class ArcyColors ( ) // This is a comment. -case class MutexyColors ( - blue: Vector[String], - green: String -) - -// This is a comment. -case class RcyColors ( +case class CellyColors ( red: String, - blue: Vector[String], - green: String + blue: Vector[String] ) // This is a comment. -case class CellyColors ( - red: String, - blue: Vector[String] +case class CowyColors ( + lifetime: String ) // This is a comment. @@ -42,8 +34,16 @@ case class LockyColors ( ) // This is a comment. -case class CowyColors ( - lifetime: String +case class MutexyColors ( + blue: Vector[String], + green: String +) + +// This is a comment. +case class RcyColors ( + red: String, + blue: Vector[String], + green: String ) // This is a comment. diff --git a/core/data/tests/smart_pointers/output.swift b/core/data/tests/smart_pointers/output.swift index d9c58bf4..17b0befb 100644 --- a/core/data/tests/smart_pointers/output.swift +++ b/core/data/tests/smart_pointers/output.swift @@ -14,55 +14,55 @@ public struct ArcyColors: Codable { } /// This is a comment. -public struct MutexyColors: Codable { +public struct CellyColors: Codable { + public let red: String public let blue: [String] - public let green: String - public init(blue: [String], green: String) { + public init(red: String, blue: [String]) { + self.red = red self.blue = blue - self.green = green } } /// This is a comment. -public struct RcyColors: Codable { - public let red: String - public let blue: [String] - public let green: String +public struct CowyColors: Codable { + public let lifetime: String - public init(red: String, blue: [String], green: String) { - self.red = red - self.blue = blue - self.green = green + public init(lifetime: String) { + self.lifetime = lifetime } } /// This is a comment. -public struct CellyColors: Codable { +public struct LockyColors: Codable { public let red: String - public let blue: [String] - public init(red: String, blue: [String]) { + public init(red: String) { self.red = red - self.blue = blue } } /// This is a comment. -public struct LockyColors: Codable { - public let red: String +public struct MutexyColors: Codable { + public let blue: [String] + public let green: String - public init(red: String) { - self.red = red + public init(blue: [String], green: String) { + self.blue = blue + self.green = green } } /// This is a comment. -public struct CowyColors: Codable { - public let lifetime: String +public struct RcyColors: Codable { + public let red: String + public let blue: [String] + public let green: String - public init(lifetime: String) { - self.lifetime = lifetime + public init(red: String, blue: [String], green: String) { + self.red = red + self.blue = blue + self.green = green } } diff --git a/core/data/tests/smart_pointers/output.ts b/core/data/tests/smart_pointers/output.ts index b062e586..a3c0fee8 100644 --- a/core/data/tests/smart_pointers/output.ts +++ b/core/data/tests/smart_pointers/output.ts @@ -6,32 +6,32 @@ export interface ArcyColors { } /** This is a comment. */ -export interface MutexyColors { +export interface CellyColors { + red: string; blue: string[]; - green: string; } /** This is a comment. */ -export interface RcyColors { - red: string; - blue: string[]; - green: string; +export interface CowyColors { + lifetime: string; } /** This is a comment. */ -export interface CellyColors { +export interface LockyColors { red: string; - blue: string[]; } /** This is a comment. */ -export interface LockyColors { - red: string; +export interface MutexyColors { + blue: string[]; + green: string; } /** This is a comment. */ -export interface CowyColors { - lifetime: string; +export interface RcyColors { + red: string; + blue: string[]; + green: string; } /** This is a comment. */ diff --git a/core/data/tests/test_optional_type_alias/output.go b/core/data/tests/test_optional_type_alias/output.go index e791af8a..51018eb8 100644 --- a/core/data/tests/test_optional_type_alias/output.go +++ b/core/data/tests/test_optional_type_alias/output.go @@ -2,10 +2,10 @@ package proto import "encoding/json" -type OptionalU32 *uint32 - type OptionalU16 *int +type OptionalU32 *uint32 + type FooBar struct { Foo OptionalU32 `json:"foo"` Bar OptionalU16 `json:"bar"` diff --git a/core/data/tests/test_optional_type_alias/output.kt b/core/data/tests/test_optional_type_alias/output.kt index d69ef8c4..785dc98c 100644 --- a/core/data/tests/test_optional_type_alias/output.kt +++ b/core/data/tests/test_optional_type_alias/output.kt @@ -3,10 +3,10 @@ package com.agilebits.onepassword import kotlinx.serialization.Serializable import kotlinx.serialization.SerialName -typealias OptionalU32 = UInt? - typealias OptionalU16 = UShort? +typealias OptionalU32 = UInt? + @Serializable data class FooBar ( val foo: OptionalU32, diff --git a/core/data/tests/test_optional_type_alias/output.scala b/core/data/tests/test_optional_type_alias/output.scala index b3e30395..98afaa4b 100644 --- a/core/data/tests/test_optional_type_alias/output.scala +++ b/core/data/tests/test_optional_type_alias/output.scala @@ -7,10 +7,10 @@ type UShort = Short type UInt = Int type ULong = Int -type OptionalU32 = Option[UInt] - type OptionalU16 = Option[UShort] +type OptionalU32 = Option[UInt] + } package onepassword { diff --git a/core/data/tests/test_optional_type_alias/output.swift b/core/data/tests/test_optional_type_alias/output.swift index dd2a1266..c93a9b58 100644 --- a/core/data/tests/test_optional_type_alias/output.swift +++ b/core/data/tests/test_optional_type_alias/output.swift @@ -1,9 +1,9 @@ import Foundation -public typealias OptionalU32 = UInt32? - public typealias OptionalU16 = UInt16? +public typealias OptionalU32 = UInt32? + public struct FooBar: Codable { public let foo: OptionalU32 public let bar: OptionalU16 diff --git a/core/data/tests/test_optional_type_alias/output.ts b/core/data/tests/test_optional_type_alias/output.ts index c49e00c6..883ffb69 100644 --- a/core/data/tests/test_optional_type_alias/output.ts +++ b/core/data/tests/test_optional_type_alias/output.ts @@ -1,7 +1,7 @@ -export type OptionalU32 = number | undefined; - export type OptionalU16 = number | undefined; +export type OptionalU32 = number | undefined; + export interface FooBar { foo: OptionalU32; bar: OptionalU16; diff --git a/core/src/parser.rs b/core/src/parser.rs index 1c8351aa..b5ce6eba 100644 --- a/core/src/parser.rs +++ b/core/src/parser.rs @@ -90,11 +90,11 @@ pub struct ErrorInfo { #[derive(Default, Debug)] pub struct ParsedData { /// Structs defined in the source - pub structs: Vec, + pub structs: BTreeSet, /// Enums defined in the source - pub enums: Vec, + pub enums: BTreeSet, /// Type aliases defined in the source - pub aliases: Vec, + pub aliases: BTreeSet, /// Imports used by this file pub import_types: HashSet, /// Crate this belongs to. @@ -138,15 +138,15 @@ impl ParsedData { match rust_thing { RustItem::Struct(s) => { self.type_names.insert(s.id.renamed.clone()); - self.structs.push(s); + self.structs.insert(s); } RustItem::Enum(e) => { self.type_names.insert(e.shared().id.renamed.clone()); - self.enums.push(e); + self.enums.insert(e); } RustItem::Alias(a) => { self.type_names.insert(a.id.renamed.clone()); - self.aliases.push(a); + self.aliases.insert(a); } } } diff --git a/core/src/rust_types.rs b/core/src/rust_types.rs index 92d45e44..c670539e 100644 --- a/core/src/rust_types.rs +++ b/core/src/rust_types.rs @@ -34,7 +34,7 @@ impl std::fmt::Display for Id { } /// Rust struct. -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone)] pub struct RustStruct { /// The identifier for the struct. pub id: Id, @@ -52,11 +52,31 @@ pub struct RustStruct { pub is_redacted: bool, } +impl PartialEq for RustStruct { + fn eq(&self, other: &Self) -> bool { + self.id.original == other.id.original + } +} + +impl Eq for RustStruct {} + +impl PartialOrd for RustStruct { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for RustStruct { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + self.id.original.cmp(&other.id.original) + } +} + /// Rust type alias. /// ``` /// pub struct MasterPassword(String); /// ``` -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone)] pub struct RustTypeAlias { /// The identifier for the alias. pub id: Id, @@ -72,6 +92,26 @@ pub struct RustTypeAlias { pub is_redacted: bool, } +impl PartialEq for RustTypeAlias { + fn eq(&self, other: &Self) -> bool { + self.id.original == other.id.original + } +} + +impl Eq for RustTypeAlias {} + +impl Ord for RustTypeAlias { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + self.id.original.cmp(&other.id.original) + } +} + +impl PartialOrd for RustTypeAlias { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + /// Rust field definition. #[derive(Debug, Clone, PartialEq)] pub struct RustField { @@ -513,7 +553,7 @@ impl SpecialRustType { } /// Parsed information about a Rust enum definition -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone)] pub enum RustEnum { /// A unit enum /// @@ -553,6 +593,26 @@ pub enum RustEnum { }, } +impl PartialEq for RustEnum { + fn eq(&self, other: &Self) -> bool { + self.shared().id.original == other.shared().id.original + } +} + +impl Eq for RustEnum {} + +impl PartialOrd for RustEnum { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for RustEnum { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + self.shared().id.original.cmp(&other.shared().id.original) + } +} + impl RustEnum { /// Get a reference to the inner shared content pub fn shared(&self) -> &RustEnumShared {