diff --git a/README.md b/README.md index 327d0976..741f15e0 100644 --- a/README.md +++ b/README.md @@ -99,6 +99,7 @@ Are you getting weird deserialization issues? Did our procedural macro throw a c - Typescript - Go** - Python** (see list of limitations [here](https://github.com/1Password/typeshare/issues/217)) +- ReScript If there is a language that you want Typeshare to generate definitions for, you can either: diff --git a/cli/data/tests/default_config.toml b/cli/data/tests/default_config.toml index 1fe92952..2125a146 100644 --- a/cli/data/tests/default_config.toml +++ b/cli/data/tests/default_config.toml @@ -19,4 +19,6 @@ package = '' [kotlin.type_mappings] -[scala.type_mappings] \ No newline at end of file +[scala.type_mappings] + +[rescript] \ No newline at end of file diff --git a/cli/data/tests/rescript_header_config.toml b/cli/data/tests/rescript_header_config.toml new file mode 100644 index 00000000..84aa1e0e --- /dev/null +++ b/cli/data/tests/rescript_header_config.toml @@ -0,0 +1,2 @@ +[rescript] +header = "/* Custom ReScript header */" \ No newline at end of file diff --git a/cli/src/args.rs b/cli/src/args.rs index 05b79619..a165309f 100644 --- a/cli/src/args.rs +++ b/cli/src/args.rs @@ -12,6 +12,8 @@ pub enum AvailableLanguage { Go, #[cfg(feature = "python")] Python, + #[value(name = "rescript")] + ReScript, } #[derive(clap::Parser)] diff --git a/cli/src/config.rs b/cli/src/config.rs index e221648d..60a75899 100644 --- a/cli/src/config.rs +++ b/cli/src/config.rs @@ -62,6 +62,14 @@ pub struct GoParams { pub type_mappings: HashMap, } +#[derive(Default, Serialize, Deserialize, PartialEq, Eq, Debug)] +#[serde(default)] +pub struct ReScriptParams { + pub type_mappings: HashMap, + pub default_decorators: Vec, + pub header: Option, +} + /// The parameters that are used to configure the behaviour of typeshare /// from the configuration file `typeshare.toml` #[derive(Serialize, Deserialize, Default, Debug, PartialEq)] @@ -75,6 +83,7 @@ pub(crate) struct Config { pub python: PythonParams, #[cfg(feature = "go")] pub go: GoParams, + pub rescript: ReScriptParams, #[serde(skip)] pub target_os: Vec, } diff --git a/cli/src/main.rs b/cli/src/main.rs index 1062ed2b..6692ad25 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -27,7 +27,9 @@ use typeshare_core::language::Go; use typeshare_core::language::Python; use typeshare_core::{ context::ParseContext, - language::{CrateName, Kotlin, Language, Scala, SupportedLanguage, Swift, TypeScript}, + language::{ + CrateName, Kotlin, Language, ReScript, Scala, SupportedLanguage, Swift, TypeScript, + }, parser::ParsedData, reconcile::reconcile_aliases, }; @@ -95,6 +97,7 @@ fn generate_types(config_file: Option<&Path>, options: &Args) -> anyhow::Result< args::AvailableLanguage::Go => SupportedLanguage::Go, #[cfg(feature = "python")] args::AvailableLanguage::Python => SupportedLanguage::Python, + args::AvailableLanguage::ReScript => SupportedLanguage::ReScript, }, }; @@ -236,6 +239,12 @@ fn language( SupportedLanguage::Python => { panic!("python support is currently experimental and must be enabled as a feature flag for typeshare-cli") } + SupportedLanguage::ReScript => Box::new(ReScript { + type_mappings: config.rescript.type_mappings, + default_decorators: config.rescript.default_decorators, + header: config.rescript.header, + ..Default::default() + }), } } diff --git a/cli/src/parse.rs b/cli/src/parse.rs index 1a1aed54..87e8dd52 100644 --- a/cli/src/parse.rs +++ b/cli/src/parse.rs @@ -61,6 +61,7 @@ fn output_file_name(language_type: SupportedLanguage, crate_name: &CrateName) -> SupportedLanguage::Swift => pascal_case(), SupportedLanguage::TypeScript => snake_case(), SupportedLanguage::Python => snake_case(), + SupportedLanguage::ReScript => pascal_case(), } } diff --git a/core/data/tests/can_generate_algebraic_enum/output.res b/core/data/tests/can_generate_algebraic_enum/output.res new file mode 100644 index 00000000..4d65b8ad --- /dev/null +++ b/core/data/tests/can_generate_algebraic_enum/output.res @@ -0,0 +1,24 @@ +/** Struct comment */ +type itemDetailsFieldValue = { +} + +/** Enum comment */ +@tag("type") +type advancedColors = + /** This is a case comment */ + | @as("String") String({ content: string }) + | @as("Number") Number({ content: int }) + | @as("UnsignedNumber") UnsignedNumber({ content: int }) + | @as("NumberArray") NumberArray({ content: array }) + /** Comment on the last element */ + | @as("ReallyCoolType") ReallyCoolType({ content: itemDetailsFieldValue }) + +@tag("type") +type advancedColors2 = + /** This is a case comment */ + | @as("string") String({ content: string }) + | @as("number") Number({ content: int }) + | @as("number-array") NumberArray({ content: array }) + /** Comment on the last element */ + | @as("really-cool-type") ReallyCoolType({ content: itemDetailsFieldValue }) + diff --git a/core/data/tests/can_generate_algebraic_enum_with_skipped_variants/output.res b/core/data/tests/can_generate_algebraic_enum_with_skipped_variants/output.res new file mode 100644 index 00000000..7388d47b --- /dev/null +++ b/core/data/tests/can_generate_algebraic_enum_with_skipped_variants/output.res @@ -0,0 +1,5 @@ +@tag("type") +type SomeEnum = + | @as("A") A + | @as("C") C(int) + diff --git a/core/data/tests/can_generate_anonymous_struct_with_skipped_fields/output.res b/core/data/tests/can_generate_anonymous_struct_with_skipped_fields/output.res new file mode 100644 index 00000000..d6b02663 --- /dev/null +++ b/core/data/tests/can_generate_anonymous_struct_with_skipped_fields/output.res @@ -0,0 +1,20 @@ +/** Enum keeping track of who autofilled a field */ +/** This field was autofilled by us */ +type autofilledByUs = { + /** The UUID for the fill */ + uuid: string, +} + +/** Something else autofilled this field */ +type autofilledBySomethingElse = { + /** The UUID for the fill */ + uuid: string, +} + +@tag("type") +type autofilledBy = + /** This field was autofilled by us */ + | @as("Us") Us({ content: autofilledByUs }) + /** Something else autofilled this field */ + | @as("SomethingElse") SomethingElse({ content: autofilledBySomethingElse }) + diff --git a/core/data/tests/can_generate_bare_string_enum/output.res b/core/data/tests/can_generate_bare_string_enum/output.res new file mode 100644 index 00000000..49d3e747 --- /dev/null +++ b/core/data/tests/can_generate_bare_string_enum/output.res @@ -0,0 +1,7 @@ +/** This is a comment. */ +type colors = + | @as("Red") Red + | @as("Blue") Blue + | @as("Green") Green + + diff --git a/core/data/tests/can_generate_double_option_pattern/output.res b/core/data/tests/can_generate_double_option_pattern/output.res new file mode 100644 index 00000000..d45f9666 --- /dev/null +++ b/core/data/tests/can_generate_double_option_pattern/output.res @@ -0,0 +1,4 @@ +type someStruct = { + field_a?: option, +} + diff --git a/core/data/tests/can_generate_empty_algebraic_enum/output.res b/core/data/tests/can_generate_empty_algebraic_enum/output.res new file mode 100644 index 00000000..91727cd7 --- /dev/null +++ b/core/data/tests/can_generate_empty_algebraic_enum/output.res @@ -0,0 +1,8 @@ +type addressDetails = { +} + +@tag("type") +type address = + | @as("FixedAddress") FixedAddress({ content: addressDetails }) + | @as("NoFixedAddress") NoFixedAddress + diff --git a/core/data/tests/can_generate_generic_enum/output.res b/core/data/tests/can_generate_generic_enum/output.res new file mode 100644 index 00000000..f65b8e68 --- /dev/null +++ b/core/data/tests/can_generate_generic_enum/output.res @@ -0,0 +1,40 @@ +@tag("type") +type genericEnum<'a, 'b> = + | @as("VariantA") VariantA({ content: 'a }) + | @as("VariantB") VariantB({ content: 'b }) + +type structUsingGenericEnum = { + enum_field: genericEnum, +} + +@tag("type") +type genericEnumUsingGenericEnum<'t> = + | @as("VariantC") VariantC({ content: genericEnum<'t, 't> }) + | @as("VariantD") VariantD({ content: genericEnum> }) + | @as("VariantE") VariantE({ content: genericEnum }) + +type genericEnumsUsingStructVariantsVariantF<'t, 'u> = { + action: 't, +} + +type genericEnumsUsingStructVariantsVariantG<'t, 'u> = { + action: 't, + response: 'u, +} + +type genericEnumsUsingStructVariantsVariantH<'t, 'u> = { + non_generic: int, +} + +type genericEnumsUsingStructVariantsVariantI<'t, 'u> = { + vec: array<'t>, + action: myType<'t, 'u>, +} + +@tag("type") +type genericEnumsUsingStructVariants<'t, 'u> = + | @as("VariantF") VariantF({ content: genericEnumsUsingStructVariantsVariantF<'t, 'u> }) + | @as("VariantG") VariantG({ content: genericEnumsUsingStructVariantsVariantG<'t, 'u> }) + | @as("VariantH") VariantH({ content: genericEnumsUsingStructVariantsVariantH<'t, 'u> }) + | @as("VariantI") VariantI({ content: genericEnumsUsingStructVariantsVariantI<'t, 'u> }) + diff --git a/core/data/tests/can_generate_generic_struct/output.res b/core/data/tests/can_generate_generic_struct/output.res new file mode 100644 index 00000000..72b22ef4 --- /dev/null +++ b/core/data/tests/can_generate_generic_struct/output.res @@ -0,0 +1,18 @@ +type genericStruct<'a, 'b> = { + field_a: 'a, + field_b: array<'b>, +} + +type genericStructUsingGenericStruct<'t> = { + struct_field: genericStruct, + second_struct_field: genericStruct<'t, string>, + third_struct_field: genericStruct<'t, array<'t>>, +} + +@tag("type") +type enumUsingGenericStruct = + | @as("VariantA") VariantA({ content: genericStruct }) + | @as("VariantB") VariantB({ content: genericStruct }) + | @as("VariantC") VariantC({ content: genericStruct }) + | @as("VariantD") VariantD({ content: genericStructUsingGenericStruct }) + diff --git a/core/data/tests/can_generate_generic_type_alias/output.res b/core/data/tests/can_generate_generic_type_alias/output.res new file mode 100644 index 00000000..624d9978 --- /dev/null +++ b/core/data/tests/can_generate_generic_type_alias/output.res @@ -0,0 +1,4 @@ +type genericTypeAlias<'t> = array<'t> + +type nonGenericAlias = genericTypeAlias + diff --git a/core/data/tests/can_generate_simple_enum/output.res b/core/data/tests/can_generate_simple_enum/output.res new file mode 100644 index 00000000..ed0df6e5 --- /dev/null +++ b/core/data/tests/can_generate_simple_enum/output.res @@ -0,0 +1,11 @@ +/** + * This is a comment. + * Continued lovingly here + */ +type colors = + | @as("Red") Red + | @as("Blue") Blue + /** Green is a cool color */ + | @as("Green") Green + + diff --git a/core/data/tests/can_generate_simple_struct_with_a_comment/output.res b/core/data/tests/can_generate_simple_struct_with_a_comment/output.res new file mode 100644 index 00000000..7bee5c73 --- /dev/null +++ b/core/data/tests/can_generate_simple_struct_with_a_comment/output.res @@ -0,0 +1,13 @@ +type location = { +} + +/** This is a comment. */ +type person = { + /** This is another comment */ + name: string, + age: int, + info?: string, + emails: array, + location: location, +} + diff --git a/core/data/tests/can_generate_slice_of_user_type/output.res b/core/data/tests/can_generate_slice_of_user_type/output.res new file mode 100644 index 00000000..6402e82a --- /dev/null +++ b/core/data/tests/can_generate_slice_of_user_type/output.res @@ -0,0 +1,4 @@ +type video = { + tags: array, +} + diff --git a/core/data/tests/can_generate_struct_with_skipped_fields/output.res b/core/data/tests/can_generate_struct_with_skipped_fields/output.res new file mode 100644 index 00000000..624164af --- /dev/null +++ b/core/data/tests/can_generate_struct_with_skipped_fields/output.res @@ -0,0 +1,5 @@ +type myStruct = { + a: int, + c: int, +} + diff --git a/core/data/tests/can_handle_anonymous_struct/output.res b/core/data/tests/can_handle_anonymous_struct/output.res new file mode 100644 index 00000000..8867e560 --- /dev/null +++ b/core/data/tests/can_handle_anonymous_struct/output.res @@ -0,0 +1,41 @@ +/** Enum keeping track of who autofilled a field */ +/** This field was autofilled by us */ +type autofilledByUs = { + /** The UUID for the fill */ + uuid: string, +} + +/** Something else autofilled this field */ +type autofilledBySomethingElse = { + /** The UUID for the fill */ + uuid: string, + /** Some other thing */ + thing: int, +} + +@tag("type") +type autofilledBy = + /** This field was autofilled by us */ + | @as("Us") Us({ content: autofilledByUs }) + /** Something else autofilled this field */ + | @as("SomethingElse") SomethingElse({ content: autofilledBySomethingElse }) + +/** This is a comment (yareek sameek wuz here) */ +type enumWithManyVariantsAnonVariant = { + uuid: string, +} + +type enumWithManyVariantsAnotherAnonVariant = { + uuid: string, + thing: int, +} + +@tag("type") +type enumWithManyVariants = + | @as("UnitVariant") UnitVariant + | @as("TupleVariantString") TupleVariantString({ content: string }) + | @as("AnonVariant") AnonVariant({ content: enumWithManyVariantsAnonVariant }) + | @as("TupleVariantInt") TupleVariantInt({ content: int }) + | @as("AnotherUnitVariant") AnotherUnitVariant + | @as("AnotherAnonVariant") AnotherAnonVariant({ content: enumWithManyVariantsAnotherAnonVariant }) + diff --git a/core/data/tests/can_handle_quote_in_serde_rename/output.res b/core/data/tests/can_handle_quote_in_serde_rename/output.res new file mode 100644 index 00000000..416b5d02 --- /dev/null +++ b/core/data/tests/can_handle_quote_in_serde_rename/output.res @@ -0,0 +1,5 @@ +/** This is a comment. */ +type colors = + | @as("Green\"") Green + + diff --git a/core/data/tests/can_handle_serde_rename/output.res b/core/data/tests/can_handle_serde_rename/output.res new file mode 100644 index 00000000..5210cb6c --- /dev/null +++ b/core/data/tests/can_handle_serde_rename/output.res @@ -0,0 +1,13 @@ +type otherType = { +} + +/** This is a comment. */ +type person = { + name: string, + age: int, + extraSpecialFieldOne: int, + extraSpecialFieldTwo?: array, + nonStandardDataType: otherType, + nonStandardDataTypeInArray?: array, +} + diff --git a/core/data/tests/can_handle_serde_rename_all/output.res b/core/data/tests/can_handle_serde_rename_all/output.res new file mode 100644 index 00000000..feafa898 --- /dev/null +++ b/core/data/tests/can_handle_serde_rename_all/output.res @@ -0,0 +1,16 @@ +/** This is a Person struct with camelCase rename */ +type person = { + firstName: string, + lastName: string, + age: int, + extraSpecialField1: int, + extraSpecialField2?: array, +} + +/** This is a Person2 struct with UPPERCASE rename */ +type person2 = { + FIRST_NAME: string, + LAST_NAME: string, + AGE: int, +} + diff --git a/core/data/tests/can_handle_serde_rename_on_top_level/output.res b/core/data/tests/can_handle_serde_rename_on_top_level/output.res new file mode 100644 index 00000000..82b1c742 --- /dev/null +++ b/core/data/tests/can_handle_serde_rename_on_top_level/output.res @@ -0,0 +1,13 @@ +type otherType = { +} + +/** This is a comment. */ +type personTwo = { + name: string, + age: int, + extraSpecialFieldOne: int, + extraSpecialFieldTwo?: array, + nonStandardDataType: otherType, + nonStandardDataTypeInArray?: array, +} + diff --git a/core/data/tests/can_handle_unit_type/output.res b/core/data/tests/can_handle_unit_type/output.res new file mode 100644 index 00000000..7bd29223 --- /dev/null +++ b/core/data/tests/can_handle_unit_type/output.res @@ -0,0 +1,10 @@ +/** This struct has a unit field */ +type structHasVoidType = { + thisIsAUnit: unit, +} + +/** This enum has a variant associated with unit data */ +@tag("type") +type enumHasVoidType = + | @as("hasAUnit") HasAUnit({ content: unit }) + diff --git a/core/data/tests/can_override_types/input.rs b/core/data/tests/can_override_types/input.rs index 994cc9aa..1d9c9d36 100644 --- a/core/data/tests/can_override_types/input.rs +++ b/core/data/tests/can_override_types/input.rs @@ -5,8 +5,10 @@ struct OverrideStruct { #[typeshare( swift(type = "Int"), typescript(readonly, type = "any | undefined"), - kotlin(type = "Int"), go(type = "uint"), - scala(type = "Short") + kotlin(type = "Int"), + go(type = "uint"), + scala(type = "Short"), + rescript(type = "int") )] field_to_override: String, } @@ -21,9 +23,11 @@ enum OverrideEnum { #[typeshare( swift(type = "Int"), typescript(readonly, type = "any | undefined"), - kotlin(type = "Int"), go(type = "uint"), - scala(type = "Short") + kotlin(type = "Int"), + go(type = "uint"), + scala(type = "Short"), + rescript(type = "int") )] - field_to_override: String - } -} \ No newline at end of file + field_to_override: String, + }, +} diff --git a/core/data/tests/can_override_types/output.res b/core/data/tests/can_override_types/output.res new file mode 100644 index 00000000..660ea24c --- /dev/null +++ b/core/data/tests/can_override_types/output.res @@ -0,0 +1,14 @@ +type overrideStruct = { + fieldToOverride: int, +} + +type overrideEnumAnonymousStructVariant = { + fieldToOverride: int, +} + +@tag("type") +type overrideEnum = + | @as("UnitVariant") UnitVariant + | @as("TupleVariant") TupleVariant({ content: string }) + | @as("AnonymousStructVariant") AnonymousStructVariant({ content: overrideEnumAnonymousStructVariant }) + diff --git a/core/data/tests/can_recognize_types_inside_modules/output.res b/core/data/tests/can_recognize_types_inside_modules/output.res new file mode 100644 index 00000000..dc479144 --- /dev/null +++ b/core/data/tests/can_recognize_types_inside_modules/output.res @@ -0,0 +1,16 @@ +type a = { + field: int, +} + +type ab = { + field: int, +} + +type abc = { + field: int, +} + +type outsideOfModules = { + field: int, +} + diff --git a/core/data/tests/const_enum_decorator/input.rs b/core/data/tests/const_enum_decorator/input.rs index 2248020d..c1289777 100644 --- a/core/data/tests/const_enum_decorator/input.rs +++ b/core/data/tests/const_enum_decorator/input.rs @@ -21,3 +21,8 @@ pub enum BestHockeyTeams3 { pub enum BestHockeyTeams4 { PittsburghPenguins, } + +#[typeshare(rescript = "@unboxed")] +pub enum BestHockeyTeams5 { + PittsburghPenguins, +} diff --git a/core/data/tests/const_enum_decorator/output.res b/core/data/tests/const_enum_decorator/output.res new file mode 100644 index 00000000..77d02281 --- /dev/null +++ b/core/data/tests/const_enum_decorator/output.res @@ -0,0 +1,25 @@ +type bestHockeyTeams = + | @as("PittsburghPenguins") PittsburghPenguins + + +type bestHockeyTeams1 = + | @as("PittsburghPenguins") PittsburghPenguins + + +type bestHockeyTeams2 = + | @as("PittsburghPenguins") PittsburghPenguins + + +type bestHockeyTeams3 = + | @as("PittsburghPenguins") PittsburghPenguins + + +type bestHockeyTeams4 = + | @as("PittsburghPenguins") PittsburghPenguins + + +@unboxed +type bestHockeyTeams5 = + | @as("PittsburghPenguins") PittsburghPenguins + + diff --git a/core/data/tests/const_enum_decorator/output.swift b/core/data/tests/const_enum_decorator/output.swift index fbf15cb8..3fb4f04c 100644 --- a/core/data/tests/const_enum_decorator/output.swift +++ b/core/data/tests/const_enum_decorator/output.swift @@ -19,3 +19,7 @@ public enum OPBestHockeyTeams3: String, Codable { public enum OPBestHockeyTeams4: String, Codable, Equatable, Hashable { case pittsburghPenguins = "PittsburghPenguins" } + +public enum OPBestHockeyTeams5: String, Codable { + case pittsburghPenguins = "PittsburghPenguins" +} diff --git a/core/data/tests/enum_is_properly_named_with_serde_overrides/output.res b/core/data/tests/enum_is_properly_named_with_serde_overrides/output.res new file mode 100644 index 00000000..deef59bd --- /dev/null +++ b/core/data/tests/enum_is_properly_named_with_serde_overrides/output.res @@ -0,0 +1,11 @@ +/** + * This is a comment. + * Continued lovingly here + */ +type colors = + | @as("red") Red + | @as("blue") Blue + /** Green is a cool color */ + | @as("green-like") Green + + diff --git a/core/data/tests/excluded_by_target_os/output.res b/core/data/tests/excluded_by_target_os/output.res new file mode 100644 index 00000000..f56d4935 --- /dev/null +++ b/core/data/tests/excluded_by_target_os/output.res @@ -0,0 +1,50 @@ +/** + * A struct with no target_os. Should be generated when + * we use --target-os. + */ +type alwaysAccept = { +} + +type definedTwice = { + field1: string, +} + +type excluded = { +} + +type manyStruct = { +} + +type multipleTargets = { +} + +type nestedNotTarget1 = { +} + +type otherExcluded = { +} + +type alwaysAcceptEnum = + | @as("Variant1") Variant1 + | @as("Variant2") Variant2 + + +type someEnum + + +type testEnumVariant7 = { + field1: string, +} + +type testEnumVariant9 = { + field2: string, +} + +@tag("type") +type testEnum = + | @as("Variant1") Variant1 + | @as("Variant5") Variant5 + | @as("Variant7") Variant7({ content: testEnumVariant7 }) + | @as("Variant8") Variant8 + | @as("Variant9") Variant9({ content: testEnumVariant9 }) + diff --git a/core/data/tests/fully_qualified_decorator/input.rs b/core/data/tests/fully_qualified_decorator/input.rs new file mode 100644 index 00000000..b587f324 --- /dev/null +++ b/core/data/tests/fully_qualified_decorator/input.rs @@ -0,0 +1,4 @@ +#[typeshare::typeshare(rescript = "@unboxed")] +pub enum TestEnum { + Variant1, +} \ No newline at end of file diff --git a/core/data/tests/fully_qualified_decorator/output.res b/core/data/tests/fully_qualified_decorator/output.res new file mode 100644 index 00000000..d72ecf1b --- /dev/null +++ b/core/data/tests/fully_qualified_decorator/output.res @@ -0,0 +1,3 @@ +@unboxed +type testEnum = + | @as("Variant1") Variant1 \ No newline at end of file diff --git a/core/data/tests/generate_types/output.res b/core/data/tests/generate_types/output.res new file mode 100644 index 00000000..a0b4382c --- /dev/null +++ b/core/data/tests/generate_types/output.res @@ -0,0 +1,16 @@ +type customType = { +} + +type types = { + s: string, + static_s: string, + int8: int, + float: float, + double: float, + array: array, + fixed_length_array: array /* length: 4 */, + dictionary: Dict.t, + optional_dictionary?: Dict.t, + custom_type: customType, +} + diff --git a/core/data/tests/generate_types_with_keywords/output.res b/core/data/tests/generate_types_with_keywords/output.res new file mode 100644 index 00000000..00a47a97 --- /dev/null +++ b/core/data/tests/generate_types_with_keywords/output.res @@ -0,0 +1,14 @@ +type catch = { + default: string, + case: string, +} + +@tag("type") +type \"switch" = + | @as("default") Default({ content: catch }) + +type throws = + | @as("case") Case + | @as("default") Default + + diff --git a/core/data/tests/serialize_type_alias/output.res b/core/data/tests/serialize_type_alias/output.res new file mode 100644 index 00000000..b49d7d59 --- /dev/null +++ b/core/data/tests/serialize_type_alias/output.res @@ -0,0 +1,9 @@ +type uuid = string + +/** Unique identifier for an Account */ +type accountUuid = uuid + +type alsoString = string + +type itemUuid = string + diff --git a/core/data/tests/smart_pointers/output.res b/core/data/tests/smart_pointers/output.res new file mode 100644 index 00000000..7c1ef48b --- /dev/null +++ b/core/data/tests/smart_pointers/output.res @@ -0,0 +1,43 @@ +/** This is a comment. */ +type arcyColors = { + red: int, + blue: string, + green: array, +} + +/** This is a comment. */ +type cellyColors = { + red: string, + blue: array, +} + +/** This is a comment. */ +type cowyColors = { + lifetime: string, +} + +/** This is a comment. */ +type lockyColors = { + red: string, +} + +/** This is a comment. */ +type mutexyColors = { + blue: array, + green: string, +} + +/** This is a comment. */ +type rcyColors = { + red: string, + blue: array, + green: string, +} + +/** This is a comment. */ +@tag("type") +type boxyColors = + | @as("Red") Red + | @as("Blue") Blue + | @as("Green") Green({ content: string }) + diff --git a/core/data/tests/test_algebraic_enum_case_name_support/output.res b/core/data/tests/test_algebraic_enum_case_name_support/output.res new file mode 100644 index 00000000..e49d3486 --- /dev/null +++ b/core/data/tests/test_algebraic_enum_case_name_support/output.res @@ -0,0 +1,10 @@ +type itemDetailsFieldValue = { +} + +@tag("type") +type advancedColors = + | @as("string") String({ content: string }) + | @as("number") Number({ content: int }) + | @as("number-array") NumberArray({ content: array }) + | @as("reallyCoolType") ReallyCoolType({ content: itemDetailsFieldValue }) + diff --git a/core/data/tests/test_default_decorators/output.res b/core/data/tests/test_default_decorators/output.res new file mode 100644 index 00000000..1cab0d29 --- /dev/null +++ b/core/data/tests/test_default_decorators/output.res @@ -0,0 +1,4 @@ +@schema +type emptyType = { +} + diff --git a/core/data/tests/test_generate_char/output.res b/core/data/tests/test_generate_char/output.res new file mode 100644 index 00000000..e9888afb --- /dev/null +++ b/core/data/tests/test_generate_char/output.res @@ -0,0 +1,4 @@ +type myType = { + field: char, +} + diff --git a/core/data/tests/test_i54_u53_type/output.res b/core/data/tests/test_i54_u53_type/output.res new file mode 100644 index 00000000..f7317410 --- /dev/null +++ b/core/data/tests/test_i54_u53_type/output.res @@ -0,0 +1,5 @@ +type foo = { + a: int, + b: int, +} + diff --git a/core/data/tests/test_optional_type_alias/output.res b/core/data/tests/test_optional_type_alias/output.res new file mode 100644 index 00000000..a84c5b4f --- /dev/null +++ b/core/data/tests/test_optional_type_alias/output.res @@ -0,0 +1,9 @@ +type optionalU16 = option + +type optionalU32 = option + +type fooBar = { + foo: optionalU32, + bar: optionalU16, +} + diff --git a/core/data/tests/test_serde_default_struct/output.res b/core/data/tests/test_serde_default_struct/output.res new file mode 100644 index 00000000..08fd4a67 --- /dev/null +++ b/core/data/tests/test_serde_default_struct/output.res @@ -0,0 +1,4 @@ +type foo = { + bar?: bool, +} + diff --git a/core/data/tests/test_serde_iso8601/output.res b/core/data/tests/test_serde_iso8601/output.res new file mode 100644 index 00000000..f3ebeab2 --- /dev/null +++ b/core/data/tests/test_serde_iso8601/output.res @@ -0,0 +1,4 @@ +type foo = { + time: string, +} + diff --git a/core/data/tests/test_serde_url/output.res b/core/data/tests/test_serde_url/output.res new file mode 100644 index 00000000..8094133d --- /dev/null +++ b/core/data/tests/test_serde_url/output.res @@ -0,0 +1,4 @@ +type foo = { + url: string, +} + diff --git a/core/data/tests/test_serialized_as/output.res b/core/data/tests/test_serialized_as/output.res new file mode 100644 index 00000000..3cf73791 --- /dev/null +++ b/core/data/tests/test_serialized_as/output.res @@ -0,0 +1,5 @@ +type itemId = string + +/** Options that you could pick */ +type options = string + diff --git a/core/data/tests/test_serialized_as_tuple/output.res b/core/data/tests/test_serialized_as_tuple/output.res new file mode 100644 index 00000000..9b20a947 --- /dev/null +++ b/core/data/tests/test_serialized_as_tuple/output.res @@ -0,0 +1,2 @@ +type itemId = string + diff --git a/core/data/tests/test_simple_enum_case_name_support/output.res b/core/data/tests/test_simple_enum_case_name_support/output.res new file mode 100644 index 00000000..cb79bc57 --- /dev/null +++ b/core/data/tests/test_simple_enum_case_name_support/output.res @@ -0,0 +1,7 @@ +/** This is a comment. */ +type colors = + | @as("red") Red + | @as("blue-ish") Blue + | @as("Green") Green + + diff --git a/core/data/tests/test_type_alias/output.res b/core/data/tests/test_type_alias/output.res new file mode 100644 index 00000000..464dc320 --- /dev/null +++ b/core/data/tests/test_type_alias/output.res @@ -0,0 +1,6 @@ +type bar = string + +type foo = { + bar: bar, +} + diff --git a/core/data/tests/use_correct_decoded_variable_name/output.res b/core/data/tests/use_correct_decoded_variable_name/output.res new file mode 100644 index 00000000..81bf4ec3 --- /dev/null +++ b/core/data/tests/use_correct_decoded_variable_name/output.res @@ -0,0 +1,3 @@ +type myEmptyStruct = { +} + diff --git a/core/data/tests/use_correct_integer_types/output.res b/core/data/tests/use_correct_integer_types/output.res new file mode 100644 index 00000000..44f140a5 --- /dev/null +++ b/core/data/tests/use_correct_integer_types/output.res @@ -0,0 +1,10 @@ +/** This is a comment. */ +type foo = { + a: int, + b: int, + c: int, + e: int, + f: int, + g: int, +} + diff --git a/core/src/language/mod.rs b/core/src/language/mod.rs index e92dfc05..f1743c58 100644 --- a/core/src/language/mod.rs +++ b/core/src/language/mod.rs @@ -22,6 +22,7 @@ use std::{ mod go; mod kotlin; mod python; +mod rescript; mod scala; mod swift; mod typescript; @@ -29,6 +30,7 @@ mod typescript; pub use go::Go; pub use kotlin::Kotlin; pub use python::Python; +pub use rescript::ReScript; pub use scala::Scala; pub use swift::GenericConstraints; pub use swift::Swift; @@ -103,13 +105,14 @@ pub enum SupportedLanguage { Swift, TypeScript, Python, + ReScript, } impl SupportedLanguage { /// Returns an iterator over all supported language variants. pub fn all_languages() -> impl Iterator { use SupportedLanguage::*; - [Go, Kotlin, Scala, Swift, TypeScript, Python].into_iter() + [Go, Kotlin, Scala, Swift, TypeScript, Python, ReScript].into_iter() } /// Get the file name extension for the supported language. @@ -121,6 +124,7 @@ impl SupportedLanguage { SupportedLanguage::Swift => "swift", SupportedLanguage::TypeScript => "ts", SupportedLanguage::Python => "py", + SupportedLanguage::ReScript => "res", } } } @@ -136,6 +140,7 @@ impl FromStr for SupportedLanguage { "swift" => Ok(Self::Swift), "typescript" => Ok(Self::TypeScript), "python" => Ok(Self::Python), + "rescript" => Ok(Self::ReScript), _ => Err(ParseError::UnsupportedLanguage(s.into())), } } diff --git a/core/src/language/rescript.rs b/core/src/language/rescript.rs new file mode 100644 index 00000000..b8651954 --- /dev/null +++ b/core/src/language/rescript.rs @@ -0,0 +1,561 @@ +use crate::rust_types::Id; +use crate::RenameExt; +use crate::{ + language::{Language, SupportedLanguage}, + parser::{DecoratorKind, ParsedData}, + rust_types::{ + RustConst, RustConstExpr, RustEnum, RustEnumVariant, RustField, RustStruct, RustType, + RustTypeAlias, RustTypeFormatError, SpecialRustType, + }, +}; +use itertools::Itertools; +use std::{ + borrow::Cow, + collections::HashMap, + io::{self, Write}, +}; + +use super::ScopedCrateTypes; + +// Source: https://rescript-lang.org/docs/manual/reserved-keywords/ +const RESCRIPT_KEYWORDS: &[&str] = &[ + "and", + "as", + "assert", + "constraint", + "else", + "exception", + "external", + "false", + "for", + "if", + "in", + "include", + "lazy", + "let", + "module", + "mutable", + "of", + "open", + "rec", + "switch", + "true", + "try", + "type", + "when", + "while", + "with", +]; + +/// All information needed to generate ReScript type-code +#[derive(Default)] +pub struct ReScript { + /// Mappings from Rust type names to ReScript type names + pub type_mappings: HashMap, + /// Default decorators that will be applied to all typeshared types + pub default_decorators: Vec, + /// Whether or not to exclude the version header that normally appears at the top of generated code. + /// If you aren't generating a snapshot test, this setting can just be left as a default (false) + pub no_version_header: bool, + /// Custom header content to include at the beginning of generated files + pub header: Option, +} + +impl Language for ReScript { + fn type_map(&mut self) -> &HashMap { + &self.type_mappings + } + + fn end_file(&mut self, _w: &mut dyn Write) -> std::io::Result<()> { + Ok(()) + } + + /// Format a simple type with no generic parameters. + /// Note that we still need to take a list of generic types in case the implementors + /// need to differentiate between a user-defined type and a generic type (for example: Swift) + #[allow(clippy::ptr_arg)] + fn format_simple_type( + &mut self, + base: &String, + generic_types: &[String], + ) -> Result { + if let Some(mapped) = self.type_map().get(base) { + Ok(mapped.into()) + } else { + let base_camel = base.to_camel_case(); + // If this is a generic type parameter (i.e., it's in the generic_types list), + // prefix it with an apostrophe as required by ReScript + if generic_types.contains(base) { + Ok(format!("'{}", base_camel)) + } else { + Ok(base_camel) + } + } + } + + fn format_special_type( + &mut self, + special_ty: &SpecialRustType, + generic_types: &[String], + ) -> Result { + if let Some(mapped) = self.type_mappings.get(&special_ty.to_string()) { + // if self.custom_translations(mapped).is_some() { + // self.types_for_custom_json_translation + // .insert(mapped.to_string(), BTreeSet::new()); + // } + return Ok(mapped.to_owned()); + } + match special_ty { + SpecialRustType::Vec(rtype) => Ok(format!( + "array<{}>", + self.format_type(rtype, generic_types)? + )), + SpecialRustType::Array(rtype, len) => { + // ReScript doesn't have a fixed-length array type, so we just use a regular array type and add a comment about the length + let formatted_type = self.format_type(rtype, generic_types)?; + Ok(format!("array<{}> /* length: {} */", formatted_type, len)) + } + SpecialRustType::Slice(rtype) => Ok(format!( + "array<{}>", + self.format_type(rtype, generic_types)? + )), + // We add optionality above the type formatting level + SpecialRustType::Option(rtype) => self.format_type(rtype, generic_types), + SpecialRustType::HashMap(rtype1, rtype2) => { + let _hashtype = match rtype1.as_ref() { + RustType::Simple { id } if generic_types.contains(id) => { + return Err(RustTypeFormatError::GenericKeyForbiddenInTS(id.clone())); + } + _ => self.format_type(rtype1, generic_types)?, + }; + Ok(format!( + "Dict.t<{}>", + self.format_type(rtype2, generic_types)? + )) + } + SpecialRustType::Unit => Ok("unit".into()), + SpecialRustType::DateTime => Ok("Date.t".into()), + SpecialRustType::String => Ok("string".into()), + SpecialRustType::Char => Ok("char".into()), + SpecialRustType::I8 + | SpecialRustType::U8 + | SpecialRustType::I16 + | SpecialRustType::U16 + | SpecialRustType::I32 + | SpecialRustType::U32 + | SpecialRustType::I54 + | SpecialRustType::U53 => Ok("int".into()), + SpecialRustType::F32 | SpecialRustType::F64 => Ok("float".into()), + SpecialRustType::Bool => Ok("bool".into()), + SpecialRustType::U64 + | SpecialRustType::I64 + | SpecialRustType::ISize + | SpecialRustType::USize => { + panic!("64 bit types not allowed in Typeshare") + } + } + } + + fn begin_file(&mut self, w: &mut dyn Write, _parsed_data: &ParsedData) -> io::Result<()> { + if let Some(header) = &self.header { + writeln!(w, "{}", header)?; + writeln!(w)?; + } else if !self.no_version_header { + writeln!(w, "/*")?; + writeln!(w, " Generated by typeshare {}", env!("CARGO_PKG_VERSION"))?; + writeln!(w, "*/")?; + writeln!(w)?; + } + Ok(()) + } + + fn write_type_alias(&mut self, w: &mut dyn Write, ty: &RustTypeAlias) -> io::Result<()> { + self.write_comments(w, 0, &ty.comments)?; + + let r#type = self + .format_type(&ty.r#type, ty.generic_types.as_slice()) + .map_err(io::Error::other)?; + + // Apply default decorators if any + if !self.default_decorators.is_empty() { + writeln!(w, "{}", self.default_decorators.join(" "))?; + } + + writeln!( + w, + "type {}{} = {}\n", + rescript_keyword_aware_rename(ty.id.renamed.to_camel_case()), + if !ty.generic_types.is_empty() { + format!( + "<{}>", + ty.generic_types + .iter() + .map(|s| format!("'{}", s.to_camel_case())) + .join(", ") + ) + } else { + Default::default() + }, + if ty.r#type.is_optional() { + format!("option<{}>", r#type) + } else { + r#type + }, + )?; + + Ok(()) + } + + fn write_const(&mut self, w: &mut dyn Write, c: &RustConst) -> io::Result<()> { + match c.expr { + RustConstExpr::Int(val) => { + let const_type = self + .format_type(&c.r#type, &[]) + .map_err(std::io::Error::other)?; + writeln!( + w, + "export const {}: {} = {};", + c.id.renamed.to_snake_case().to_uppercase(), + const_type, + val + ) + } + } + } + + fn write_struct(&mut self, w: &mut dyn Write, rs: &RustStruct) -> io::Result<()> { + self.write_comments(w, 0, &rs.comments)?; + + // Apply default decorators if any + if !self.default_decorators.is_empty() { + writeln!(w, "{}", self.default_decorators.join(" "))?; + } + + let type_name = rs.id.renamed.to_camel_case(); + let generic_parameters = if !rs.generic_types.is_empty() { + format!( + "<{}>", + rs.generic_types + .iter() + .map(|s| format!("'{}", s.to_camel_case())) + .join(", ") + ) + } else { + Default::default() + }; + + writeln!( + w, + "type {}{} = {{", + rescript_keyword_aware_rename(type_name), + generic_parameters + )?; + // writeln!(w, "type t = {{")?; + + rs.fields + .iter() + .try_for_each(|f| self.write_field(w, f, rs.generic_types.as_slice()))?; + + // writeln!(w, "}}\n")?; + writeln!(w, "}}\n") + } + + fn write_enum(&mut self, w: &mut dyn Write, e: &RustEnum) -> io::Result<()> { + self.write_comments(w, 0, &e.shared().comments)?; + + let generic_parameters = if !e.shared().generic_types.is_empty() { + format!( + "<{}>", + e.shared() + .generic_types + .iter() + .map(|s| format!("'{}", s.to_camel_case())) + .join(", ") + ) + } else { + Default::default() + }; + + // 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 + let decs = if let Some(rescript_decs) = e.shared().decorators.get(&DecoratorKind::ReScript) + { + // 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 + let combined_decorators: Vec = self + .default_decorators + .iter() + .chain(rescript_decs.iter()) + .cloned() + .collect(); + combined_decorators.join(" ") + } else { + self.default_decorators.join(" ") + }; + + // Apply default decorators if any + if !decs.is_empty() { + writeln!(w, "{}", decs)?; + } + + match e { + RustEnum::Unit(shared) => { + if e.shared().variants.len() == 0 { + // Special case: an empty enum + write!( + w, + "type {}{}", + rescript_keyword_aware_rename(shared.id.renamed.to_camel_case()), + generic_parameters + )?; + } else { + write!( + w, + "type {}{} = ", + rescript_keyword_aware_rename(shared.id.renamed.to_camel_case()), + generic_parameters + )?; + + self.write_enum_variants(w, e)?; + } + writeln!(w, "\n\n") + } + RustEnum::Algebraic { + shared, tag_key, .. + } => { + let parent_name = shared.id.renamed.to_camel_case(); + // Write internal structs before the actual type because rescript does not allow nested structs inside enums/variants + shared.variants.iter().try_for_each(|v| { + // writeln!(w)?; + // self.write_comments(w, 1, &v.shared().comments)?; + match v { + RustEnumVariant::Unit(_shared) => { + // Do nothing + } + RustEnumVariant::Tuple { ty: _, shared: _ } => { + // Do nothing + } + RustEnumVariant::AnonymousStruct { fields, shared } => { + if !self.default_decorators.is_empty() { + writeln!(w, "{}", self.default_decorators.join(" "))?; + } + self.write_struct( + w, + &RustStruct { + id: Id { + original: rescript_keyword_aware_rename(format!( + "{}{}", + parent_name, + shared.id.original.to_pascal_case() + )) + .to_string(), + renamed: rescript_keyword_aware_rename(format!( + "{}{}", + parent_name, + shared.id.renamed.to_pascal_case() + )) + .to_string(), + serde_rename: shared.id.serde_rename, + }, + decorators: HashMap::new(), + is_redacted: false, + comments: shared.comments.clone(), + fields: fields.to_vec(), + generic_types: e.shared().generic_types.clone(), + }, + )?; + } + } + io::Result::Ok(()) + })?; + + writeln!(w, "@tag(\"{}\")", tag_key)?; + write!( + w, + "type {}{} = ", + rescript_keyword_aware_rename(shared.id.renamed.to_camel_case()), + generic_parameters + )?; + + self.write_enum_variants(w, e)?; + + writeln!(w)?; + writeln!(w) + } + } + } + + fn write_imports( + &mut self, + _w: &mut dyn Write, + _imports: ScopedCrateTypes<'_>, + ) -> std::io::Result<()> { + // ReScript does not require import statements for the generated types. + Ok(()) + } + + fn ignored_reference_types(&self) -> Vec<&str> { + self.type_mappings.keys().map(|s| s.as_str()).collect() + } +} + +impl ReScript { + fn write_enum_variants(&mut self, w: &mut dyn Write, e: &RustEnum) -> io::Result<()> { + let parent_name = e.shared().id.original.to_camel_case(); + match e { + // Write all the unit variants out (there can only be unit variants in + // this case) + RustEnum::Unit(shared) => shared.variants.iter().try_for_each(|v| match v { + RustEnumVariant::Unit(shared) => { + writeln!(w)?; + self.write_comments(w, 1, &shared.comments)?; + write!( + w, + "\t| @as({:?}) {}", + &shared.id.renamed, + shared.id.original.to_pascal_case() + ) + } + _ => unreachable!(), + }), + + // Write all the algebraic variants out (all three variant types are possible + // here) + RustEnum::Algebraic { + tag_key: _, + content_key, + shared, + } => shared.variants.iter().try_for_each(|v| { + writeln!(w)?; + self.write_comments(w, 1, &v.shared().comments)?; + match v { + RustEnumVariant::Unit(shared) => { + write!( + w, + "\t| @as(\"{}\") {}", + shared.id.renamed, + shared.id.original.to_pascal_case() + ) + } + RustEnumVariant::Tuple { ty, shared } => { + let r#type = self + .format_type(ty, e.shared().generic_types.as_slice()) + .map_err(io::Error::other)?; + write!( + w, + "\t| @as(\"{}\") {}({{ {}{}: {} }})", + shared.id.renamed, + shared.id.original.to_pascal_case(), + content_key, + if ty.is_optional() { + "?" + } else { + Default::default() + }, + r#type + ) + } + RustEnumVariant::AnonymousStruct { fields: _, shared } => { + let generic_str = if e.shared().generic_types.as_slice().len() > 0 { + format!( + "<{}>", + e.shared() + .generic_types + .as_slice() + .iter() + .map(|s| format!("'{}", s.to_camel_case())) + .join(", ") + ) + } else { + Default::default() + }; + write!( + w, + "\t| @as(\"{}\") {}({{ {}: {}{} }})", + shared.id.renamed, + shared.id.original.to_pascal_case(), + content_key, + format!("{}{}", parent_name, shared.id.original.to_pascal_case()), + generic_str + ) + } + } + }), + } + } + + fn write_field( + &mut self, + w: &mut dyn Write, + field: &RustField, + generic_types: &[String], + ) -> io::Result<()> { + self.write_comments(w, 1, &field.comments)?; + let ts_ty: String = match field.type_override(SupportedLanguage::ReScript) { + Some(type_override) => type_override.to_owned(), + None => self + .format_type(&field.ty, generic_types) + .map_err(io::Error::other)?, + }; + let optional = field.ty.is_optional() || field.has_default; + let double_optional = field.ty.is_double_optional(); + writeln!( + w, + "\t{}{}: {},", + rescript_keyword_aware_rename(&field.id.renamed), + if optional { "?" } else { Default::default() }, + if double_optional { + format!("option<{}>", ts_ty) + } else { + ts_ty + } + )?; + + Ok(()) + } + + fn write_comments( + &mut self, + w: &mut dyn Write, + indent: usize, + comments: &[String], + ) -> io::Result<()> { + // Only attempt to write a comment if there are some, otherwise we're Ok() + if !comments.is_empty() { + let comment: String = { + let tab_indent = "\t".repeat(indent); + // If there's only one comment then keep it on the same line, otherwise we'll make a nice multi-line comment + if comments.len() == 1 { + format!("{}/** {} */", tab_indent, comments.first().unwrap()) + } else { + let joined_comments = comments.join(&format!("\n{tab_indent} * ")); + format!( + "{tab_indent}/** +{tab_indent} * {joined_comments} +{tab_indent} */" + ) + } + }; + writeln!(w, "{comment}")?; + } + Ok(()) + } +} + +fn rescript_keyword_aware_rename<'a, T>(name: T) -> Cow<'a, str> +where + T: Into>, +{ + let name = name.into(); + if RESCRIPT_KEYWORDS.contains(&name.as_ref()) { + Cow::Owned(format!("\\\"{name}\"")) + } else { + // If name contains hyphen + if name.contains('-') { + Cow::Owned(format!("\\\"{name}\"")) + } else { + name + } + } +} diff --git a/core/src/parser.rs b/core/src/parser.rs index a87f1414..ee341aca 100644 --- a/core/src/parser.rs +++ b/core/src/parser.rs @@ -36,6 +36,7 @@ pub enum DecoratorKind { SwiftGenericConstraints, /// The typeshare attribute for kotlin "kotlin" Kotlin, + ReScript, } impl DecoratorKind { @@ -45,6 +46,7 @@ impl DecoratorKind { DecoratorKind::Swift => "swift", DecoratorKind::SwiftGenericConstraints => "swiftGenericConstraints", DecoratorKind::Kotlin => "kotlin", + DecoratorKind::ReScript => "rescript", } } } @@ -639,8 +641,15 @@ fn get_name_value_meta_items<'a>( /// Returns all arguments passed into `#[{ident}(...)]` where `{ident}` can be `serde` or `typeshare` attributes #[inline(always)] pub(crate) fn get_meta_items(attr: &syn::Attribute, ident: &str) -> impl Iterator { - attr.path() - .is_ident(ident) + // Check if the attribute path directly matches the ident, or if any of its segments match + let matches = attr.path().is_ident(ident) + || attr + .path() + .segments + .iter() + .any(|segment| segment.ident == ident); + + matches .then(|| { attr.parse_args_with(Punctuated::::parse_terminated) .into_iter() @@ -875,6 +884,7 @@ fn get_decorators(attrs: &[syn::Attribute]) -> DecoratorMap { DecoratorKind::Swift, DecoratorKind::SwiftGenericConstraints, DecoratorKind::Kotlin, + DecoratorKind::ReScript, ]; for (decorator_kind, value) in decorator_kinds.into_iter().flat_map(|decorator_kind| { @@ -914,7 +924,7 @@ mod test { rust_types::RustItem, }; use std::collections::BTreeSet; - use syn::{Attribute, ItemStruct}; + use syn::{Attribute, ItemEnum, ItemStruct}; #[test] fn test_rename_all_to_case() { @@ -1039,4 +1049,20 @@ mod test { assert!(has_typeshare_annotation(&item_struct.attrs)); } + + #[test] + fn test_fully_qualified_decorator() { + let item_enum: ItemEnum = syn::parse_quote! { + #[typeshare::typeshare(rescript = "@unboxed")] + pub enum TestEnum { + Variant1, + } + }; + + let decorators = get_decorators(&item_enum.attrs); + // Should find the rescript decorator + assert!(decorators.contains_key(&DecoratorKind::ReScript)); + let rescript_decorators = decorators.get(&DecoratorKind::ReScript).unwrap(); + assert!(rescript_decorators.contains(&"@unboxed".to_string())); + } } diff --git a/core/tests/snapshot_tests.rs b/core/tests/snapshot_tests.rs index 042684c7..0b22dac7 100644 --- a/core/tests/snapshot_tests.rs +++ b/core/tests/snapshot_tests.rs @@ -166,6 +166,9 @@ macro_rules! output_file_for_ident { (python) => { "output.py" }; + (rescript) => { + "output.res" + }; } /// Simplifies the construction of `Language` instances for each language. @@ -298,6 +301,22 @@ macro_rules! language_instance { ..Default::default() }) }; + + // Default ReScript + (rescript) => { + language_instance!(rescript { }) + }; + + // ReScript with configuration fields forwarded + (rescript {$($field:ident: $val:expr),* $(,)?}) => { + #[allow(clippy::needless_update)] + Box::new(typeshare_core::language::ReScript { + no_version_header: true, + $($field: $val,)* + ..Default::default() + }) + }; + } macro_rules! target_os { @@ -468,6 +487,17 @@ static PYTHON_MAPPINGS: Lazy> = Lazy::new(|| { .collect() }); +static RESCRIPT_MAPPINGS: Lazy> = Lazy::new(|| { + [ + ("Url", "string"), + ("DateTime", "string"), + ("Vec", "Uint8Array.t"), + ] + .iter() + .map(|(k, v)| (k.to_string(), v.to_string())) + .collect() +}); + tests! { /// Enums can_generate_algebraic_enum: [ @@ -484,7 +514,8 @@ tests! { }, typescript, go, - python + python, + rescript ]; can_generate_generic_enum: [ swift { @@ -492,7 +523,8 @@ tests! { }, kotlin, scala, - typescript + typescript, + rescript, ]; can_generate_generic_struct: [ swift { @@ -502,6 +534,7 @@ tests! { kotlin, scala, typescript, + rescript, ]; can_generate_generic_type_alias: [ swift { @@ -509,10 +542,11 @@ tests! { }, kotlin, scala, - typescript + typescript, + rescript ]; can_generate_const: [typescript, go, python]; - can_generate_slice_of_user_type: [swift, kotlin, scala, typescript, go, python]; + can_generate_slice_of_user_type: [swift, kotlin, scala, typescript, go, python, rescript]; can_generate_readonly_fields: [ typescript ]; @@ -524,16 +558,17 @@ tests! { scala, typescript, go, - python + python, + rescript ]; - can_generate_bare_string_enum: [swift, kotlin, scala, typescript, go, python ]; + can_generate_bare_string_enum: [swift, kotlin, scala, typescript, go, python, rescript ]; can_generate_double_option_pattern: [ - typescript + typescript, rescript ]; can_recognize_types_inside_modules: [ - swift, kotlin, scala, typescript, go, python + swift, kotlin, scala, typescript, go, python, rescript ]; - test_simple_enum_case_name_support: [swift, kotlin, scala, typescript, go, python ]; + test_simple_enum_case_name_support: [swift, kotlin, scala, typescript, go, python, rescript]; test_algebraic_enum_case_name_support: [ swift { prefix: "OP".to_string(), @@ -548,16 +583,17 @@ tests! { }, typescript, go, - python + python, + rescript ]; can_apply_prefix_correctly: [ swift { prefix: "OP".to_string(), }, kotlin { prefix: "OP".to_string(), }, scala, typescript, go, python ]; - can_generate_empty_algebraic_enum: [ swift { prefix: "OP".to_string(), }, kotlin { prefix: "OP".to_string(), }, scala, typescript, go, python ]; + can_generate_empty_algebraic_enum: [ swift { prefix: "OP".to_string(), }, kotlin { prefix: "OP".to_string(), }, scala, typescript, go, python, rescript ]; can_generate_algebraic_enum_with_skipped_variants: [swift, kotlin, scala, typescript, go, python]; - can_generate_struct_with_skipped_fields: [swift, kotlin, scala, typescript, go, python]; - enum_is_properly_named_with_serde_overrides: [swift, kotlin, scala, typescript, go, python]; - can_handle_quote_in_serde_rename: [swift, kotlin, scala, typescript, go, python]; - can_handle_anonymous_struct: [swift, kotlin, scala, typescript, go, python]; - test_generate_char: [swift, kotlin, scala, typescript, go, python]; + can_generate_struct_with_skipped_fields: [swift, kotlin, scala, typescript, go, python, rescript]; + enum_is_properly_named_with_serde_overrides: [swift, kotlin, scala, typescript, go, python, rescript]; + can_handle_quote_in_serde_rename: [swift, kotlin, scala, typescript, go, python, rescript]; + can_handle_anonymous_struct: [swift, kotlin, scala, typescript, go, python, rescript]; + test_generate_char: [swift, kotlin, scala, typescript, go, python, rescript]; anonymous_struct_with_rename: [ swift { prefix: "Core".to_string(), @@ -568,11 +604,11 @@ tests! { go, python ]; - can_override_types: [swift, kotlin, scala, typescript, go]; + can_override_types: [swift, kotlin, scala, typescript, go, rescript]; /// Structs - can_generate_simple_struct_with_a_comment: [kotlin, swift, typescript, scala, go, python]; - generate_types: [kotlin, swift, typescript, scala, go, python]; + can_generate_simple_struct_with_a_comment: [kotlin, swift, typescript, scala, go, python, rescript]; + generate_types: [kotlin, swift, typescript, scala, go, python, rescript]; can_handle_serde_rename: [ swift { prefix: "TypeShareX_".to_string(), @@ -581,14 +617,15 @@ tests! { scala, typescript, go, - python + python, + rescript ]; // TODO: kotlin and typescript don't appear to support this yet generates_empty_structs_and_initializers: [swift, kotlin, scala, typescript, go,python]; - test_default_decorators: [swift { default_decorators: vec!["Sendable".into(), "Identifiable".into()]}]; + test_default_decorators: [swift { default_decorators: vec!["Sendable".into(), "Identifiable".into()]}, rescript { default_decorators: vec!["@schema".into()] }]; test_default_generic_constraints: [swift { default_generic_constraints: typeshare_core::language::GenericConstraints::from_config(vec!["Sendable".into(), "Identifiable".into()]) }]; - test_i54_u53_type: [swift, kotlin, scala, typescript, go, python]; - test_serde_default_struct: [swift, kotlin, scala, typescript, go, python]; + test_i54_u53_type: [swift, kotlin, scala, typescript, go, python, rescript]; + test_serde_default_struct: [swift, kotlin, scala, typescript, go, python, rescript]; test_serde_iso8601: [ swift { prefix: String::new(), @@ -612,6 +649,9 @@ tests! { }, python { type_mappings: super::PYTHON_MAPPINGS.clone(), + }, + rescript { + type_mappings: super::RESCRIPT_MAPPINGS.clone(), } ]; test_serde_url: [ @@ -638,11 +678,14 @@ tests! { }, python{ type_mappings: super::PYTHON_MAPPINGS.clone() + }, + rescript { + type_mappings: super::RESCRIPT_MAPPINGS.clone(), } ]; - test_type_alias: [ swift { prefix: "OP".to_string(), }, kotlin, scala, typescript, go, python ]; - test_optional_type_alias: [swift, kotlin, scala, typescript, go, python]; - test_serialized_as: [ swift { prefix: "OP".to_string(), }, kotlin, scala, typescript, go, python ]; + test_type_alias: [ swift { prefix: "OP".to_string(), }, kotlin, scala, typescript, go, python, rescript ]; + test_optional_type_alias: [swift, kotlin, scala, typescript, go, python, rescript]; + test_serialized_as: [ swift { prefix: "OP".to_string(), }, kotlin, scala, typescript, go, python, rescript ]; test_serialized_as_tuple: [ swift { prefix: "OP".to_string(), @@ -653,10 +696,11 @@ tests! { go { uppercase_acronyms: vec!["ID".to_string()], }, - python + python, + rescript ]; - can_handle_serde_rename_all: [swift, kotlin, scala, typescript, go,python]; - can_handle_serde_rename_on_top_level: [swift { prefix: "OP".to_string(), }, kotlin, scala, typescript, go, python]; + can_handle_serde_rename_all: [swift, kotlin, scala, typescript, go, python, rescript]; + can_handle_serde_rename_on_top_level: [swift { prefix: "OP".to_string(), }, kotlin, scala, typescript, go, python, rescript]; can_generate_unit_structs: [swift, kotlin, scala, typescript, go, python]; kebab_case_rename: [swift, kotlin, scala, typescript, go, python]; @@ -664,21 +708,21 @@ tests! { orders_types: [swift, kotlin, go, python]; /// Other - use_correct_integer_types: [swift, kotlin, scala, typescript, go, python]; + use_correct_integer_types: [swift, kotlin, scala, typescript, go, python, rescript]; // Only swift supports generating types with keywords - generate_types_with_keywords: [swift]; + generate_types_with_keywords: [swift, rescript]; // TODO: how is this different from generates_empty_structs_and_initializers? - use_correct_decoded_variable_name: [swift, kotlin, scala, typescript, go, python]; - can_handle_unit_type: [swift { codablevoid_constraints: vec!["Equatable".into()]} , kotlin, scala, typescript, go, python]; + use_correct_decoded_variable_name: [swift, kotlin, scala, typescript, go, python, rescript]; + can_handle_unit_type: [swift { codablevoid_constraints: vec!["Equatable".into()]} , kotlin, scala, typescript, go, python, rescript]; //3 tests for adding decorators to enums and structs - const_enum_decorator: [ swift{ prefix: "OP".to_string(), } ]; + const_enum_decorator: [ swift{ prefix: "OP".to_string(), }, rescript ]; algebraic_enum_decorator: [ swift{ prefix: "OP".to_string(), } ]; struct_decorator: [ kotlin, swift{ prefix: "OP".to_string(), } ]; serialize_field_as: [kotlin, swift, typescript, scala, go, python]; - serialize_type_alias: [kotlin, swift, typescript, scala, go, python]; + serialize_type_alias: [kotlin, swift, typescript, scala, go, python, rescript]; serialize_anonymous_field_as: [kotlin, swift, typescript, scala, go, python]; - smart_pointers: [kotlin, swift, typescript, scala, go, python]; + smart_pointers: [kotlin, swift, typescript, scala, go, python, rescript]; recursive_enum_decorator: [kotlin, swift, typescript, scala, go, python]; uppercase_go_acronyms: [ @@ -696,9 +740,9 @@ tests! { go, python ]; - can_generate_anonymous_struct_with_skipped_fields: [swift, kotlin, scala, typescript, go, python]; + can_generate_anonymous_struct_with_skipped_fields: [swift, kotlin, scala, typescript, go, python, rescript]; generic_struct_with_constraints_and_decorators: [swift { codablevoid_constraints: vec!["Equatable".into()] }]; - excluded_by_target_os: [ swift, kotlin, scala, typescript, go,python ] target_os: ["android", "macos"]; + excluded_by_target_os: [ swift, kotlin, scala, typescript, go, python, rescript ] target_os: ["android", "macos"]; // excluded_by_target_os_full_module: [swift] target_os: "ios"; serde_rename_references: [ swift, kotlin, scala, typescript, go ]; test_custom_serialize_deserialize_functions: [ go