diff --git a/cli/src/args.rs b/cli/src/args.rs index 8bf11d01..8836e351 100644 --- a/cli/src/args.rs +++ b/cli/src/args.rs @@ -12,6 +12,7 @@ pub const ARG_SCALA_PACKAGE: &str = "SCALAPACKAGE"; pub const ARG_SCALA_MODULE_NAME: &str = "SCALAMODULENAME"; #[cfg(feature = "go")] pub const ARG_GO_PACKAGE: &str = "GOPACKAGE"; +pub const ARG_CSHARP_NAMESPACE: &str = "CSHARP_NAMESPACE"; pub const ARG_CONFIG_FILE_NAME: &str = "CONFIGFILENAME"; pub const ARG_GENERATE_CONFIG: &str = "generate-config-file"; pub const ARG_OUTPUT_FILE: &str = "output-file"; @@ -19,10 +20,10 @@ pub const ARG_OUTPUT_FOLDER: &str = "output-folder"; pub const ARG_FOLLOW_LINKS: &str = "follow-links"; #[cfg(feature = "go")] -const AVAILABLE_LANGUAGES: [&str; 5] = ["kotlin", "scala", "swift", "typescript", "go"]; +const AVAILABLE_LANGUAGES: [&str; 6] = ["kotlin", "scala", "swift", "typescript", "go", "csharp"]; #[cfg(not(feature = "go"))] -const AVAILABLE_LANGUAGES: [&str; 4] = ["kotlin", "scala", "swift", "typescript"]; +const AVAILABLE_LANGUAGES: [&str; 5] = ["kotlin", "scala", "swift", "typescript", "csharp"]; /// Parse command line arguments. pub(crate) fn build_command() -> Command<'static> { @@ -96,7 +97,13 @@ pub(crate) fn build_command() -> Command<'static> { .takes_value(true) .required(false), ) - + .arg( + Arg::new(ARG_CSHARP_NAMESPACE) + .long("namespace") + .help("C# namespace") + .takes_value(true) + .required(false) + ) .arg( Arg::new(ARG_CONFIG_FILE_NAME) .short('c') diff --git a/cli/src/config.rs b/cli/src/config.rs index caac7e54..1927e1ef 100644 --- a/cli/src/config.rs +++ b/cli/src/config.rs @@ -53,6 +53,14 @@ pub struct GoParams { pub uppercase_acronyms: Vec, } +#[derive(Default, Serialize, Deserialize, PartialEq, Eq, Debug)] +#[serde(default)] +pub struct CSharpParams { + pub type_mappings: HashMap, + pub namespace: String, + pub without_csharp_naming_convention: bool, +} + /// The paramters that are used to configure the behaviour of typeshare /// from the configuration file `typeshare.toml` #[derive(Serialize, Deserialize, Default, Debug, PartialEq)] @@ -64,6 +72,7 @@ pub(crate) struct Config { pub scala: ScalaParams, #[cfg(feature = "go")] pub go: GoParams, + pub csharp: CSharpParams, } 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..c1dcb538 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -3,9 +3,9 @@ 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, + build_command, ARG_CONFIG_FILE_NAME, ARG_CSHARP_NAMESPACE, 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, }; use clap::ArgMatches; use config::Config; @@ -16,7 +16,7 @@ use std::collections::HashMap; use typeshare_core::language::Go; use typeshare_core::{ language::{ - CrateName, GenericConstraints, Kotlin, Language, Scala, SupportedLanguage, Swift, + CSharp, CrateName, GenericConstraints, Kotlin, Language, Scala, SupportedLanguage, Swift, TypeScript, }, parser::ParsedData, @@ -166,6 +166,12 @@ fn language( type_mappings: config.typescript.type_mappings, ..Default::default() }), + SupportedLanguage::CSharp => Box::new(CSharp { + namespace: config.csharp.namespace, + type_mappings: config.csharp.type_mappings, + without_csharp_naming_convention: config.csharp.without_csharp_naming_convention, + ..Default::default() + }), #[cfg(feature = "go")] SupportedLanguage::Go => Box::new(Go { package: config.go.package, @@ -206,6 +212,10 @@ fn override_configuration(mut config: Config, options: &ArgMatches) -> Config { config.scala.module_name = scala_module_name.to_string(); } + if let Some(csharp_namespace) = options.value_of(ARG_CSHARP_NAMESPACE) { + config.csharp.namespace = csharp_namespace.to_string(); + } + #[cfg(feature = "go")] if let Some(go_package) = options.value_of(args::ARG_GO_PACKAGE) { config.go.package = go_package.to_string(); diff --git a/cli/src/parse.rs b/cli/src/parse.rs index 8edbff11..0b8805fe 100644 --- a/cli/src/parse.rs +++ b/cli/src/parse.rs @@ -63,6 +63,7 @@ fn output_file_name(language_type: SupportedLanguage, crate_name: &CrateName) -> SupportedLanguage::Scala => snake_case(), SupportedLanguage::Swift => pascal_case(), SupportedLanguage::TypeScript => snake_case(), + SupportedLanguage::CSharp => pascal_case(), } } diff --git a/core/data/tests/anonymous_struct_with_rename/output.cs b/core/data/tests/anonymous_struct_with_rename/output.cs new file mode 100644 index 00000000..d4e2f2da --- /dev/null +++ b/core/data/tests/anonymous_struct_with_rename/output.cs @@ -0,0 +1,38 @@ +#nullable enable + +using System.Reflection; +using JsonSubTypes; +using Newtonsoft.Json; +using System.Runtime.Serialization; + +/** Generated type representing the anonymous struct variant `List` of the `AnonymousStructWithRename` Rust enum */ +public class AnonymousStructWithRenameListInner { + public IEnumerable list { get; set; } +} + +/** Generated type representing the anonymous struct variant `LongFieldNames` of the `AnonymousStructWithRename` Rust enum */ +public class AnonymousStructWithRenameLongFieldNamesInner { + public string some_long_field_name { get; set; } + public bool and { get; set; } + public IEnumerable but_one_more { get; set; } +} + +/** Generated type representing the anonymous struct variant `KebabCase` of the `AnonymousStructWithRename` Rust enum */ +public class AnonymousStructWithRenameKebabCaseInner { + public IEnumerable another-list { get; set; } + public string camelCaseStringField { get; set; } + public bool something-else { get; set; } +} + +[JsonConverter(typeof(JsonSubtypes), "type")] +[JsonSubtypes.KnownSubType(typeof(List), "List")] +[JsonSubtypes.KnownSubType(typeof(LongFieldNames), "LongFieldNames")] +[JsonSubtypes.KnownSubType(typeof(KebabCase), "KebabCase")] +public abstract record AnonymousStructWithRename +{ + public record list(AnonymousStructWithRenameListInner Content): AnonymousStructWithRename(); + public record longFieldNames(AnonymousStructWithRenameLongFieldNamesInner Content): AnonymousStructWithRename(); + public record kebabCase(AnonymousStructWithRenameKebabCaseInner Content): AnonymousStructWithRename(); +} + + diff --git a/core/data/tests/can_apply_prefix_correctly/output.cs b/core/data/tests/can_apply_prefix_correctly/output.cs new file mode 100644 index 00000000..4608b126 --- /dev/null +++ b/core/data/tests/can_apply_prefix_correctly/output.cs @@ -0,0 +1,29 @@ +#nullable enable + +using System.Reflection; +using JsonSubTypes; +using Newtonsoft.Json; +using System.Runtime.Serialization; + +public class ItemDetailsFieldValue { + public string Hello { get; set; } +} + +[JsonConverter(typeof(JsonSubtypes), "t")] +[JsonSubtypes.KnownSubType(typeof(String), "String")] +[JsonSubtypes.KnownSubType(typeof(Number), "Number")] +[JsonSubtypes.KnownSubType(typeof(NumberArray), "NumberArray")] +[JsonSubtypes.KnownSubType(typeof(ReallyCoolType), "ReallyCoolType")] +[JsonSubtypes.KnownSubType(typeof(ArrayReallyCoolType), "ArrayReallyCoolType")] +[JsonSubtypes.KnownSubType(typeof(DictionaryReallyCoolType), "DictionaryReallyCoolType")] +public abstract record AdvancedColors +{ + public record String(string C) : AdvancedColors(); + public record Number(int C) : AdvancedColors(); + public record NumberArray(IEnumerable C) : AdvancedColors(); + public record ReallyCoolType(ItemDetailsFieldValue C) : AdvancedColors(); + public record ArrayReallyCoolType(IEnumerable C) : AdvancedColors(); + public record DictionaryReallyCoolType(IDictionary C) : AdvancedColors(); +} + + diff --git a/core/data/tests/can_generate_algebraic_enum/output.cs b/core/data/tests/can_generate_algebraic_enum/output.cs new file mode 100644 index 00000000..86991424 --- /dev/null +++ b/core/data/tests/can_generate_algebraic_enum/output.cs @@ -0,0 +1,48 @@ +#nullable enable + +using System.Reflection; +using JsonSubTypes; +using Newtonsoft.Json; +using System.Runtime.Serialization; + +namespace Company.Domain.Models; + +/** Struct comment */ +public class ItemDetailsFieldValue { +} + +/** Enum comment */ +[JsonConverter(typeof(JsonSubtypes), "type")] +[JsonSubtypes.KnownSubType(typeof(String), "String")] +[JsonSubtypes.KnownSubType(typeof(Number), "Number")] +[JsonSubtypes.KnownSubType(typeof(UnsignedNumber), "UnsignedNumber")] +[JsonSubtypes.KnownSubType(typeof(NumberArray), "NumberArray")] +[JsonSubtypes.KnownSubType(typeof(ReallyCoolType), "ReallyCoolType")] +public abstract record AdvancedColors +{ + /** This is a case comment */ + public record String(string Content) : AdvancedColors(); + public record Number(int Content) : AdvancedColors(); + public record UnsignedNumber(uint Content) : AdvancedColors(); + public record NumberArray(IEnumerable Content) : AdvancedColors(); + /** Comment on the last element */ + public record ReallyCoolType(ItemDetailsFieldValue Content) : AdvancedColors(); +} + + +[JsonConverter(typeof(JsonSubtypes), "type")] +[JsonSubtypes.KnownSubType(typeof(String), "String")] +[JsonSubtypes.KnownSubType(typeof(Number), "Number")] +[JsonSubtypes.KnownSubType(typeof(NumberArray), "NumberArray")] +[JsonSubtypes.KnownSubType(typeof(ReallyCoolType), "ReallyCoolType")] +public abstract record AdvancedColors2 +{ + /** This is a case comment */ + public record String(string Content) : AdvancedColors2(); + public record Number(int Content) : AdvancedColors2(); + public record NumberArray(IEnumerable Content) : AdvancedColors2(); + /** Comment on the last element */ + public record ReallyCoolType(ItemDetailsFieldValue Content) : AdvancedColors2(); +} + + diff --git a/core/data/tests/can_generate_algebraic_enum_with_skipped_variants/output.cs b/core/data/tests/can_generate_algebraic_enum_with_skipped_variants/output.cs new file mode 100644 index 00000000..49876fe0 --- /dev/null +++ b/core/data/tests/can_generate_algebraic_enum_with_skipped_variants/output.cs @@ -0,0 +1,17 @@ +#nullable enable + +using System.Reflection; +using JsonSubTypes; +using Newtonsoft.Json; +using System.Runtime.Serialization; + +[JsonConverter(typeof(JsonSubtypes), "type")] +[JsonSubtypes.KnownSubType(typeof(A), "A")] +[JsonSubtypes.KnownSubType(typeof(C), "C")] +public abstract record SomeEnum +{ + public record A(): SomeEnum(); + public record C(int Content) : SomeEnum(); +} + + diff --git a/core/data/tests/can_generate_bare_string_enum/output.cs b/core/data/tests/can_generate_bare_string_enum/output.cs new file mode 100644 index 00000000..d68e4830 --- /dev/null +++ b/core/data/tests/can_generate_bare_string_enum/output.cs @@ -0,0 +1,18 @@ +#nullable enable + +using System.Reflection; +using JsonSubTypes; +using Newtonsoft.Json; +using System.Runtime.Serialization; + +/** This is a comment. */ +public enum Colors +{ + Red, + + Blue, + + Green, + +} + diff --git a/core/data/tests/can_generate_empty_algebraic_enum/output.cs b/core/data/tests/can_generate_empty_algebraic_enum/output.cs new file mode 100644 index 00000000..8c023872 --- /dev/null +++ b/core/data/tests/can_generate_empty_algebraic_enum/output.cs @@ -0,0 +1,20 @@ +#nullable enable + +using System.Reflection; +using JsonSubTypes; +using Newtonsoft.Json; +using System.Runtime.Serialization; + +public class AddressDetails { +} + +[JsonConverter(typeof(JsonSubtypes), "type")] +[JsonSubtypes.KnownSubType(typeof(FixedAddress), "FixedAddress")] +[JsonSubtypes.KnownSubType(typeof(NoFixedAddress), "NoFixedAddress")] +public abstract record Address +{ + public record FixedAddress(AddressDetails Content) : Address(); + public record NoFixedAddress(): Address(); +} + + diff --git a/core/data/tests/can_generate_generic_enum/output.cs b/core/data/tests/can_generate_generic_enum/output.cs new file mode 100644 index 00000000..d7e26d9f --- /dev/null +++ b/core/data/tests/can_generate_generic_enum/output.cs @@ -0,0 +1,69 @@ +#nullable enable + +using System.Reflection; +using JsonSubTypes; +using Newtonsoft.Json; +using System.Runtime.Serialization; + +[JsonConverter(typeof(JsonSubtypes), "type")] +[JsonSubtypes.KnownSubType(typeof(VariantA), "VariantA")] +[JsonSubtypes.KnownSubType(typeof(VariantB), "VariantB")] +public abstract record GenericEnum +{ + public record VariantA(TA Content) : GenericEnum(); + public record VariantB(TB Content) : GenericEnum(); +} + + +public class StructUsingGenericEnum { + public GenericEnum EnumField { get; set; } +} + +[JsonConverter(typeof(JsonSubtypes), "type")] +[JsonSubtypes.KnownSubType(typeof(VariantC), "VariantC")] +[JsonSubtypes.KnownSubType(typeof(VariantD), "VariantD")] +[JsonSubtypes.KnownSubType(typeof(VariantE), "VariantE")] +public abstract record GenericEnumUsingGenericEnum +{ + public record VariantC(GenericEnum Content) : GenericEnumUsingGenericEnum(); + public record VariantD(GenericEnum> Content) : GenericEnumUsingGenericEnum(); + public record VariantE(GenericEnum Content) : GenericEnumUsingGenericEnum(); +} + + +/** Generated type representing the anonymous struct variant `VariantF` of the `GenericEnumsUsingStructVariants` Rust enum */ +public class GenericEnumsUsingStructVariantsVariantFInner { + public T Action { get; set; } +} + +/** Generated type representing the anonymous struct variant `VariantG` of the `GenericEnumsUsingStructVariants` Rust enum */ +public class GenericEnumsUsingStructVariantsVariantGInner { + public T Action { get; set; } + public TU Response { get; set; } +} + +/** Generated type representing the anonymous struct variant `VariantH` of the `GenericEnumsUsingStructVariants` Rust enum */ +public class GenericEnumsUsingStructVariantsVariantHInner { + public int NonGeneric { get; set; } +} + +/** Generated type representing the anonymous struct variant `VariantI` of the `GenericEnumsUsingStructVariants` Rust enum */ +public class GenericEnumsUsingStructVariantsVariantIInner { + public IEnumerable Vec { get; set; } + public MyType Action { get; set; } +} + +[JsonConverter(typeof(JsonSubtypes), "type")] +[JsonSubtypes.KnownSubType(typeof(VariantF), "VariantF")] +[JsonSubtypes.KnownSubType(typeof(VariantG), "VariantG")] +[JsonSubtypes.KnownSubType(typeof(VariantH), "VariantH")] +[JsonSubtypes.KnownSubType(typeof(VariantI), "VariantI")] +public abstract record GenericEnumsUsingStructVariants +{ + public record VariantF(GenericEnumsUsingStructVariantsVariantFInner Content): GenericEnumsUsingStructVariants(); + public record VariantG(GenericEnumsUsingStructVariantsVariantGInner Content): GenericEnumsUsingStructVariants(); + public record VariantH(GenericEnumsUsingStructVariantsVariantHInner Content): GenericEnumsUsingStructVariants(); + public record VariantI(GenericEnumsUsingStructVariantsVariantIInner Content): GenericEnumsUsingStructVariants(); +} + + diff --git a/core/data/tests/can_generate_readonly_fields/output.cs b/core/data/tests/can_generate_readonly_fields/output.cs new file mode 100644 index 00000000..585b0e43 --- /dev/null +++ b/core/data/tests/can_generate_readonly_fields/output.cs @@ -0,0 +1,11 @@ +#nullable enable + +using System.Reflection; +using JsonSubTypes; +using Newtonsoft.Json; +using System.Runtime.Serialization; + +public class SomeStruct { + public uint FieldA { get; set; } +} + diff --git a/core/data/tests/can_generate_simple_enum/output.cs b/core/data/tests/can_generate_simple_enum/output.cs new file mode 100644 index 00000000..e4379f3b --- /dev/null +++ b/core/data/tests/can_generate_simple_enum/output.cs @@ -0,0 +1,22 @@ +#nullable enable + +using System.Reflection; +using JsonSubTypes; +using Newtonsoft.Json; +using System.Runtime.Serialization; + +/** + * This is a comment. + * Continued lovingly here + */ +public enum Colors +{ + Red, + + Blue, + + /** Green is a cool color */ + Green, + +} + diff --git a/core/data/tests/can_generate_simple_struct_with_a_comment/output.cs b/core/data/tests/can_generate_simple_struct_with_a_comment/output.cs new file mode 100644 index 00000000..744de2df --- /dev/null +++ b/core/data/tests/can_generate_simple_struct_with_a_comment/output.cs @@ -0,0 +1,20 @@ +#nullable enable + +using System.Reflection; +using JsonSubTypes; +using Newtonsoft.Json; +using System.Runtime.Serialization; + +public class Location { +} + +/** This is a comment. */ +public class Person { + /** This is another comment */ + public string Name { get; set; } + public ushort Age { get; set; } + public string? Info { get; set; } + public IEnumerable Emails { get; set; } + public Location Location { get; set; } +} + diff --git a/core/data/tests/can_generate_slice_of_user_type/output.cs b/core/data/tests/can_generate_slice_of_user_type/output.cs new file mode 100644 index 00000000..8696141c --- /dev/null +++ b/core/data/tests/can_generate_slice_of_user_type/output.cs @@ -0,0 +1,11 @@ +#nullable enable + +using System.Reflection; +using JsonSubTypes; +using Newtonsoft.Json; +using System.Runtime.Serialization; + +public class Video { + public IEnumerable Tags { get; set; } +} + diff --git a/core/data/tests/can_generate_struct_with_skipped_fields/output.cs b/core/data/tests/can_generate_struct_with_skipped_fields/output.cs new file mode 100644 index 00000000..a469a34b --- /dev/null +++ b/core/data/tests/can_generate_struct_with_skipped_fields/output.cs @@ -0,0 +1,12 @@ +#nullable enable + +using System.Reflection; +using JsonSubTypes; +using Newtonsoft.Json; +using System.Runtime.Serialization; + +public class MyStruct { + public int A { get; set; } + public int C { get; set; } +} + diff --git a/core/data/tests/can_generate_unit_structs/output.cs b/core/data/tests/can_generate_unit_structs/output.cs new file mode 100644 index 00000000..0affd99b --- /dev/null +++ b/core/data/tests/can_generate_unit_structs/output.cs @@ -0,0 +1,10 @@ +#nullable enable + +using System.Reflection; +using JsonSubTypes; +using Newtonsoft.Json; +using System.Runtime.Serialization; + +public class UnitStruct { +} + diff --git a/core/data/tests/can_handle_anonymous_struct/output.cs b/core/data/tests/can_handle_anonymous_struct/output.cs new file mode 100644 index 00000000..3ed38536 --- /dev/null +++ b/core/data/tests/can_handle_anonymous_struct/output.cs @@ -0,0 +1,64 @@ +#nullable enable + +using System.Reflection; +using JsonSubTypes; +using Newtonsoft.Json; +using System.Runtime.Serialization; + +/** Generated type representing the anonymous struct variant `Us` of the `AutofilledBy` Rust enum */ +public class AutofilledByUsInner { + /** The UUID for the fill */ + public string Uuid { get; set; } +} + +/** Generated type representing the anonymous struct variant `SomethingElse` of the `AutofilledBy` Rust enum */ +public class AutofilledBySomethingElseInner { + /** The UUID for the fill */ + public string Uuid { get; set; } + /** Some other thing */ + public int Thing { get; set; } +} + +/** Enum keeping track of who autofilled a field */ +[JsonConverter(typeof(JsonSubtypes), "type")] +[JsonSubtypes.KnownSubType(typeof(Us), "Us")] +[JsonSubtypes.KnownSubType(typeof(SomethingElse), "SomethingElse")] +public abstract record AutofilledBy +{ + /** This field was autofilled by us */ + public record Us(AutofilledByUsInner Content): AutofilledBy(); + /** Something else autofilled this field */ + public record SomethingElse(AutofilledBySomethingElseInner Content): AutofilledBy(); +} + + +/** Generated type representing the anonymous struct variant `AnonVariant` of the `EnumWithManyVariants` Rust enum */ +public class EnumWithManyVariantsAnonVariantInner { + public string Uuid { get; set; } +} + +/** Generated type representing the anonymous struct variant `AnotherAnonVariant` of the `EnumWithManyVariants` Rust enum */ +public class EnumWithManyVariantsAnotherAnonVariantInner { + public string Uuid { get; set; } + public int Thing { get; set; } +} + +/** This is a comment (yareek sameek wuz here) */ +[JsonConverter(typeof(JsonSubtypes), "type")] +[JsonSubtypes.KnownSubType(typeof(UnitVariant), "UnitVariant")] +[JsonSubtypes.KnownSubType(typeof(TupleVariantString), "TupleVariantString")] +[JsonSubtypes.KnownSubType(typeof(AnonVariant), "AnonVariant")] +[JsonSubtypes.KnownSubType(typeof(TupleVariantInt), "TupleVariantInt")] +[JsonSubtypes.KnownSubType(typeof(AnotherUnitVariant), "AnotherUnitVariant")] +[JsonSubtypes.KnownSubType(typeof(AnotherAnonVariant), "AnotherAnonVariant")] +public abstract record EnumWithManyVariants +{ + public record UnitVariant(): EnumWithManyVariants(); + public record TupleVariantString(string Content) : EnumWithManyVariants(); + public record AnonVariant(EnumWithManyVariantsAnonVariantInner Content): EnumWithManyVariants(); + public record TupleVariantInt(int Content) : EnumWithManyVariants(); + public record AnotherUnitVariant(): EnumWithManyVariants(); + public record AnotherAnonVariant(EnumWithManyVariantsAnotherAnonVariantInner Content): EnumWithManyVariants(); +} + + diff --git a/core/data/tests/can_handle_quote_in_serde_rename/output.cs b/core/data/tests/can_handle_quote_in_serde_rename/output.cs new file mode 100644 index 00000000..4a9b4b81 --- /dev/null +++ b/core/data/tests/can_handle_quote_in_serde_rename/output.cs @@ -0,0 +1,15 @@ +#nullable enable + +using System.Reflection; +using JsonSubTypes; +using Newtonsoft.Json; +using System.Runtime.Serialization; + +/** This is a comment. */ +public enum Colors +{ + [EnumMember(Value = "Green\"")] + Green, + +} + diff --git a/core/data/tests/can_handle_serde_rename/output.cs b/core/data/tests/can_handle_serde_rename/output.cs new file mode 100644 index 00000000..4d6440d6 --- /dev/null +++ b/core/data/tests/can_handle_serde_rename/output.cs @@ -0,0 +1,20 @@ +#nullable enable + +using System.Reflection; +using JsonSubTypes; +using Newtonsoft.Json; +using System.Runtime.Serialization; + +public class OtherType { +} + +/** This is a comment. */ +public class Person { + public string name { get; set; } + public ushort age { get; set; } + public int extraSpecialFieldOne { get; set; } + public IEnumerable? extraSpecialFieldTwo { get; set; } + public OtherType nonStandardDataType { get; set; } + public IEnumerable? nonStandardDataTypeInArray { get; set; } +} + diff --git a/core/data/tests/can_handle_serde_rename_all/output.cs b/core/data/tests/can_handle_serde_rename_all/output.cs new file mode 100644 index 00000000..584912f7 --- /dev/null +++ b/core/data/tests/can_handle_serde_rename_all/output.cs @@ -0,0 +1,23 @@ +#nullable enable + +using System.Reflection; +using JsonSubTypes; +using Newtonsoft.Json; +using System.Runtime.Serialization; + +/** This is a Person struct with camelCase rename */ +public class Person { + public string firstName { get; set; } + public string lastName { get; set; } + public ushort age { get; set; } + public int extraSpecialField1 { get; set; } + public IEnumerable? extraSpecialField2 { get; set; } +} + +/** This is a Person2 struct with UPPERCASE rename */ +public class Person2 { + public string FIRST_NAME { get; set; } + public string LAST_NAME { get; set; } + public ushort AGE { get; set; } +} + diff --git a/core/data/tests/can_handle_serde_rename_on_top_level/output.cs b/core/data/tests/can_handle_serde_rename_on_top_level/output.cs new file mode 100644 index 00000000..1043375d --- /dev/null +++ b/core/data/tests/can_handle_serde_rename_on_top_level/output.cs @@ -0,0 +1,20 @@ +#nullable enable + +using System.Reflection; +using JsonSubTypes; +using Newtonsoft.Json; +using System.Runtime.Serialization; + +public class OtherType { +} + +/** This is a comment. */ +public class PersonTwo { + public string name { get; set; } + public ushort age { get; set; } + public int extraSpecialFieldOne { get; set; } + public IEnumerable? extraSpecialFieldTwo { get; set; } + public OtherType nonStandardDataType { get; set; } + public IEnumerable? nonStandardDataTypeInArray { get; set; } +} + diff --git a/core/data/tests/can_override_types/input.rs b/core/data/tests/can_override_types/input.rs index 994cc9aa..a2bade53 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"), + csharp(type = "char") )] 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"), + csharp(type = "char") )] - field_to_override: String - } -} \ No newline at end of file + field_to_override: String, + }, +} diff --git a/core/data/tests/can_override_types/output.cs b/core/data/tests/can_override_types/output.cs new file mode 100644 index 00000000..836a61ca --- /dev/null +++ b/core/data/tests/can_override_types/output.cs @@ -0,0 +1,28 @@ +#nullable enable + +using System.Reflection; +using JsonSubTypes; +using Newtonsoft.Json; +using System.Runtime.Serialization; + +public class OverrideStruct { + public char fieldToOverride { get; set; } +} + +/** Generated type representing the anonymous struct variant `AnonymousStructVariant` of the `OverrideEnum` Rust enum */ +public class OverrideEnumAnonymousStructVariantInner { + public char fieldToOverride { get; set; } +} + +[JsonConverter(typeof(JsonSubtypes), "type")] +[JsonSubtypes.KnownSubType(typeof(UnitVariant), "UnitVariant")] +[JsonSubtypes.KnownSubType(typeof(TupleVariant), "TupleVariant")] +[JsonSubtypes.KnownSubType(typeof(AnonymousStructVariant), "AnonymousStructVariant")] +public abstract record OverrideEnum +{ + public record UnitVariant(): OverrideEnum(); + public record TupleVariant(string Content) : OverrideEnum(); + public record AnonymousStructVariant(OverrideEnumAnonymousStructVariantInner Content): OverrideEnum(); +} + + diff --git a/core/data/tests/can_recognize_types_inside_modules/output.cs b/core/data/tests/can_recognize_types_inside_modules/output.cs new file mode 100644 index 00000000..feacde35 --- /dev/null +++ b/core/data/tests/can_recognize_types_inside_modules/output.cs @@ -0,0 +1,23 @@ +#nullable enable + +using System.Reflection; +using JsonSubTypes; +using Newtonsoft.Json; +using System.Runtime.Serialization; + +public class A { + public uint Field { get; set; } +} + +public class ABC { + public uint Field { get; set; } +} + +public class AB { + public uint Field { get; set; } +} + +public class OutsideOfModules { + public uint Field { get; set; } +} + diff --git a/core/data/tests/enum_is_properly_named_with_serde_overrides/output.cs b/core/data/tests/enum_is_properly_named_with_serde_overrides/output.cs new file mode 100644 index 00000000..e7dc9e1e --- /dev/null +++ b/core/data/tests/enum_is_properly_named_with_serde_overrides/output.cs @@ -0,0 +1,25 @@ +#nullable enable + +using System.Reflection; +using JsonSubTypes; +using Newtonsoft.Json; +using System.Runtime.Serialization; + +/** + * This is a comment. + * Continued lovingly here + */ +public enum Colors +{ + [EnumMember(Value = "red")] + Red, + + [EnumMember(Value = "blue")] + Blue, + + /** Green is a cool color */ + [EnumMember(Value = "green-like")] + Green, + +} + diff --git a/core/data/tests/generate_types/output.cs b/core/data/tests/generate_types/output.cs new file mode 100644 index 00000000..5c93be46 --- /dev/null +++ b/core/data/tests/generate_types/output.cs @@ -0,0 +1,23 @@ +#nullable enable + +using System.Reflection; +using JsonSubTypes; +using Newtonsoft.Json; +using System.Runtime.Serialization; + +public class CustomType { +} + +public class Types { + public string S { get; set; } + public string StaticS { get; set; } + public short Int8 { get; set; } + public float Float { get; set; } + public double Double { get; set; } + public IEnumerable Array { get; set; } + public string[] FixedLengthArray { get; set; } + public IDictionary Dictionary { get; set; } + public IDictionary? OptionalDictionary { get; set; } + public CustomType CustomType { get; set; } +} + diff --git a/core/data/tests/generates_empty_structs_and_initializers/output.cs b/core/data/tests/generates_empty_structs_and_initializers/output.cs new file mode 100644 index 00000000..da8babc1 --- /dev/null +++ b/core/data/tests/generates_empty_structs_and_initializers/output.cs @@ -0,0 +1,10 @@ +#nullable enable + +using System.Reflection; +using JsonSubTypes; +using Newtonsoft.Json; +using System.Runtime.Serialization; + +public class MyEmptyStruct { +} + diff --git a/core/data/tests/kebab_case_rename/output.cs b/core/data/tests/kebab_case_rename/output.cs new file mode 100644 index 00000000..b79628e0 --- /dev/null +++ b/core/data/tests/kebab_case_rename/output.cs @@ -0,0 +1,14 @@ +#nullable enable + +using System.Reflection; +using JsonSubTypes; +using Newtonsoft.Json; +using System.Runtime.Serialization; + +/** This is a comment. */ +public class Things { + public string bla { get; set; } + public string? label { get; set; } + public string? label-left { get; set; } +} + diff --git a/core/data/tests/recursive_enum_decorator/output.cs b/core/data/tests/recursive_enum_decorator/output.cs new file mode 100644 index 00000000..f309cdaf --- /dev/null +++ b/core/data/tests/recursive_enum_decorator/output.cs @@ -0,0 +1,41 @@ +#nullable enable + +using System.Reflection; +using JsonSubTypes; +using Newtonsoft.Json; +using System.Runtime.Serialization; + +[JsonConverter(typeof(JsonSubtypes), "type")] +[JsonSubtypes.KnownSubType(typeof(Red), "Red")] +[JsonSubtypes.KnownSubType(typeof(Banana), "Banana")] +[JsonSubtypes.KnownSubType(typeof(Vermont), "Vermont")] +public abstract record Options +{ + public record Red(bool Content) : Options(); + public record Banana(string Content) : Options(); + public record Vermont(Options Content) : Options(); +} + + +/** Generated type representing the anonymous struct variant `Exactly` of the `MoreOptions` Rust enum */ +public class MoreOptionsExactlyInner { + public string Config { get; set; } +} + +/** Generated type representing the anonymous struct variant `Built` of the `MoreOptions` Rust enum */ +public class MoreOptionsBuiltInner { + public MoreOptions Top { get; set; } +} + +[JsonConverter(typeof(JsonSubtypes), "type")] +[JsonSubtypes.KnownSubType(typeof(News), "News")] +[JsonSubtypes.KnownSubType(typeof(Exactly), "Exactly")] +[JsonSubtypes.KnownSubType(typeof(Built), "Built")] +public abstract record MoreOptions +{ + public record News(bool Content) : MoreOptions(); + public record exactly(MoreOptionsExactlyInner Content): MoreOptions(); + public record built(MoreOptionsBuiltInner Content): MoreOptions(); +} + + diff --git a/core/data/tests/resolves_qualified_type/output.cs b/core/data/tests/resolves_qualified_type/output.cs new file mode 100644 index 00000000..1fb82657 --- /dev/null +++ b/core/data/tests/resolves_qualified_type/output.cs @@ -0,0 +1,16 @@ +#nullable enable + +using System.Reflection; +using JsonSubTypes; +using Newtonsoft.Json; +using System.Runtime.Serialization; + +public class QualifiedTypes { + public string Unqualified { get; set; } + public string Qualified { get; set; } + public IEnumerable QualifiedVec { get; set; } + public IDictionary QualifiedHashmap { get; set; } + public string? QualifiedOptional { get; set; } + public IDictionary>? QualfiedOptionalHashmapVec { get; set; } +} + diff --git a/core/data/tests/serialize_anonymous_field_as/output.cs b/core/data/tests/serialize_anonymous_field_as/output.cs new file mode 100644 index 00000000..11592596 --- /dev/null +++ b/core/data/tests/serialize_anonymous_field_as/output.cs @@ -0,0 +1,18 @@ +#nullable enable + +using System.Reflection; +using JsonSubTypes; +using Newtonsoft.Json; +using System.Runtime.Serialization; + +[JsonConverter(typeof(JsonSubtypes), "type")] +[JsonSubtypes.KnownSubType(typeof(Context), "Context")] +[JsonSubtypes.KnownSubType(typeof(Other), "Other")] +public abstract record SomeEnum +{ + /** The associated String contains some opaque context */ + public record Context(string Content) : SomeEnum(); + public record Other(int Content) : SomeEnum(); +} + + diff --git a/core/data/tests/serialize_field_as/output.cs b/core/data/tests/serialize_field_as/output.cs new file mode 100644 index 00000000..038a5506 --- /dev/null +++ b/core/data/tests/serialize_field_as/output.cs @@ -0,0 +1,13 @@ +#nullable enable + +using System.Reflection; +using JsonSubTypes; +using Newtonsoft.Json; +using System.Runtime.Serialization; + +public class EditItemViewModelSaveRequest { + public string Context { get; set; } + public IEnumerable Values { get; set; } + public AutoFillItemActionRequest? FillAction { get; set; } +} + diff --git a/core/data/tests/smart_pointers/output.cs b/core/data/tests/smart_pointers/output.cs new file mode 100644 index 00000000..b3e8470f --- /dev/null +++ b/core/data/tests/smart_pointers/output.cs @@ -0,0 +1,56 @@ +#nullable enable + +using System.Reflection; +using JsonSubTypes; +using Newtonsoft.Json; +using System.Runtime.Serialization; + +/** This is a comment. */ +public class ArcyColors { + public ushort Red { get; set; } + public string Blue { get; set; } + public IEnumerable Green { get; set; } +} + +/** This is a comment. */ +public class MutexyColors { + public IEnumerable Blue { get; set; } + public string Green { get; set; } +} + +/** This is a comment. */ +public class RcyColors { + public string Red { get; set; } + public IEnumerable Blue { get; set; } + public string Green { get; set; } +} + +/** This is a comment. */ +public class CellyColors { + public string Red { get; set; } + public IEnumerable Blue { get; set; } +} + +/** This is a comment. */ +public class LockyColors { + public string Red { get; set; } +} + +/** This is a comment. */ +public class CowyColors { + public string Lifetime { get; set; } +} + +/** This is a comment. */ +[JsonConverter(typeof(JsonSubtypes), "type")] +[JsonSubtypes.KnownSubType(typeof(Red), "Red")] +[JsonSubtypes.KnownSubType(typeof(Blue), "Blue")] +[JsonSubtypes.KnownSubType(typeof(Green), "Green")] +public abstract record BoxyColors +{ + public record Red(): BoxyColors(); + public record Blue(): BoxyColors(); + public record Green(string Content) : BoxyColors(); +} + + diff --git a/core/data/tests/test_algebraic_enum_case_name_support/output.cs b/core/data/tests/test_algebraic_enum_case_name_support/output.cs new file mode 100644 index 00000000..b465ad9c --- /dev/null +++ b/core/data/tests/test_algebraic_enum_case_name_support/output.cs @@ -0,0 +1,24 @@ +#nullable enable + +using System.Reflection; +using JsonSubTypes; +using Newtonsoft.Json; +using System.Runtime.Serialization; + +public class ItemDetailsFieldValue { +} + +[JsonConverter(typeof(JsonSubtypes), "type")] +[JsonSubtypes.KnownSubType(typeof(String), "String")] +[JsonSubtypes.KnownSubType(typeof(Number), "Number")] +[JsonSubtypes.KnownSubType(typeof(NumberArray), "NumberArray")] +[JsonSubtypes.KnownSubType(typeof(ReallyCoolType), "ReallyCoolType")] +public abstract record AdvancedColors +{ + public record String(string Content) : AdvancedColors(); + public record Number(int Content) : AdvancedColors(); + public record NumberArray(IEnumerable Content) : AdvancedColors(); + public record ReallyCoolType(ItemDetailsFieldValue Content) : AdvancedColors(); +} + + diff --git a/core/data/tests/test_generate_char/output.cs b/core/data/tests/test_generate_char/output.cs new file mode 100644 index 00000000..cb2c1677 --- /dev/null +++ b/core/data/tests/test_generate_char/output.cs @@ -0,0 +1,11 @@ +#nullable enable + +using System.Reflection; +using JsonSubTypes; +using Newtonsoft.Json; +using System.Runtime.Serialization; + +public class MyType { + public char Field { get; set; } +} + diff --git a/core/data/tests/test_i54_u53_type/output.cs b/core/data/tests/test_i54_u53_type/output.cs new file mode 100644 index 00000000..de49db18 --- /dev/null +++ b/core/data/tests/test_i54_u53_type/output.cs @@ -0,0 +1,12 @@ +#nullable enable + +using System.Reflection; +using JsonSubTypes; +using Newtonsoft.Json; +using System.Runtime.Serialization; + +public class Foo { + public long A { get; set; } + public ulong B { get; set; } +} + diff --git a/core/data/tests/test_serde_default_struct/output.cs b/core/data/tests/test_serde_default_struct/output.cs new file mode 100644 index 00000000..c670bec2 --- /dev/null +++ b/core/data/tests/test_serde_default_struct/output.cs @@ -0,0 +1,11 @@ +#nullable enable + +using System.Reflection; +using JsonSubTypes; +using Newtonsoft.Json; +using System.Runtime.Serialization; + +public class Foo { + public bool? Bar { get; set; } +} + diff --git a/core/data/tests/test_serde_iso8601/output.cs b/core/data/tests/test_serde_iso8601/output.cs new file mode 100644 index 00000000..0a8c5123 --- /dev/null +++ b/core/data/tests/test_serde_iso8601/output.cs @@ -0,0 +1,11 @@ +#nullable enable + +using System.Reflection; +using JsonSubTypes; +using Newtonsoft.Json; +using System.Runtime.Serialization; + +public class Foo { + public string Time { get; set; } +} + diff --git a/core/data/tests/test_serde_url/output.cs b/core/data/tests/test_serde_url/output.cs new file mode 100644 index 00000000..dde28904 --- /dev/null +++ b/core/data/tests/test_serde_url/output.cs @@ -0,0 +1,11 @@ +#nullable enable + +using System.Reflection; +using JsonSubTypes; +using Newtonsoft.Json; +using System.Runtime.Serialization; + +public class Foo { + public string Url { get; set; } +} + diff --git a/core/data/tests/test_simple_enum_case_name_support/output.cs b/core/data/tests/test_simple_enum_case_name_support/output.cs new file mode 100644 index 00000000..c612e8ae --- /dev/null +++ b/core/data/tests/test_simple_enum_case_name_support/output.cs @@ -0,0 +1,20 @@ +#nullable enable + +using System.Reflection; +using JsonSubTypes; +using Newtonsoft.Json; +using System.Runtime.Serialization; + +/** This is a comment. */ +public enum Colors +{ + [EnumMember(Value = "red")] + Red, + + [EnumMember(Value = "blue-ish")] + Blue, + + Green, + +} + diff --git a/core/data/tests/use_correct_decoded_variable_name/output.cs b/core/data/tests/use_correct_decoded_variable_name/output.cs new file mode 100644 index 00000000..da8babc1 --- /dev/null +++ b/core/data/tests/use_correct_decoded_variable_name/output.cs @@ -0,0 +1,10 @@ +#nullable enable + +using System.Reflection; +using JsonSubTypes; +using Newtonsoft.Json; +using System.Runtime.Serialization; + +public class MyEmptyStruct { +} + diff --git a/core/data/tests/use_correct_integer_types/output.cs b/core/data/tests/use_correct_integer_types/output.cs new file mode 100644 index 00000000..3ba7c226 --- /dev/null +++ b/core/data/tests/use_correct_integer_types/output.cs @@ -0,0 +1,17 @@ +#nullable enable + +using System.Reflection; +using JsonSubTypes; +using Newtonsoft.Json; +using System.Runtime.Serialization; + +/** This is a comment. */ +public class Foo { + public short A { get; set; } + public short B { get; set; } + public int C { get; set; } + public ushort E { get; set; } + public ushort F { get; set; } + public uint G { get; set; } +} + diff --git a/core/src/language/csharp.rs b/core/src/language/csharp.rs new file mode 100644 index 00000000..171d31c1 --- /dev/null +++ b/core/src/language/csharp.rs @@ -0,0 +1,442 @@ +use crate::parser::{remove_dash_from_identifier, ParsedData}; +use crate::rename::RenameExt; +use crate::rust_types::{RustType, RustTypeFormatError, SpecialRustType}; +use crate::{ + language::{Language, SupportedLanguage}, + rust_types::{RustEnum, RustEnumVariant, RustField, RustStruct, RustTypeAlias}, +}; +use itertools::Itertools; +use joinery::JoinableIterator; +use lazy_format::lazy_format; +use std::io; +use std::{collections::HashMap, io::Write}; + +/// All information needed to generate C# type-code +#[derive(Default)] +pub struct CSharp { + /// Mappings from Rust type names to C# type names + pub type_mappings: HashMap, + /// 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, + /// Namespace to use in the generated file + pub namespace: String, + /// Disable C# property naming convention and follow Serde renaming rules on properties + pub without_csharp_naming_convention: bool, +} + +impl Language for CSharp { + fn type_map(&mut self) -> &HashMap { + &self.type_mappings + } + + fn format_type( + &mut self, + ty: &RustType, + generic_types: &[String], + ) -> Result { + match ty { + RustType::Simple { id } => self.format_simple_type(id, generic_types), + RustType::Generic { id, parameters } => { + self.format_generic_type(id, parameters.as_slice(), generic_types) + } + RustType::Special(special) => self.format_special_type(special, generic_types), + } + .map(|name| with_generic_naming_convention(generic_types, &name)) + } + + fn format_special_type( + &mut self, + special_ty: &SpecialRustType, + generic_types: &[String], + ) -> Result { + match special_ty { + SpecialRustType::Vec(rtype) => Ok(format!( + "IEnumerable<{}>", + self.format_type(rtype, generic_types)? + )), + SpecialRustType::Array(rtype, _len) => { + Ok(format!("{}[]", self.format_type(rtype, generic_types)?)) + } + SpecialRustType::Slice(rtype) => Ok(format!( + "IEnumerable<{}>", + self.format_type(rtype, generic_types)? + )), + SpecialRustType::Option(rtype) => self.format_type(rtype, generic_types), + SpecialRustType::HashMap(rtype1, rtype2) => Ok(format!( + "IDictionary<{}, {}>", + match rtype1.as_ref() { + RustType::Simple { id } if generic_types.contains(id) => { + return Err(RustTypeFormatError::GenericKeyForbiddenInTS(id.clone())); + } + _ => self.format_type(rtype1, generic_types)?, + }, + self.format_type(rtype2, generic_types)? + )), + SpecialRustType::Unit => Err(RustTypeFormatError::TypeUnitInCS()), + SpecialRustType::String => Ok("string".into()), + SpecialRustType::Char => Ok("char".into()), + SpecialRustType::I8 => Ok("short".into()), + SpecialRustType::I16 => Ok("short".into()), + SpecialRustType::U8 => Ok("ushort".into()), + SpecialRustType::U16 => Ok("ushort".into()), + SpecialRustType::I32 => Ok("int".into()), + SpecialRustType::U32 => Ok("uint".into()), + SpecialRustType::I54 => Ok("long".into()), + SpecialRustType::I64 => Ok("long".into()), + SpecialRustType::U53 => Ok("ulong".into()), + SpecialRustType::U64 => Ok("ulong".into()), + SpecialRustType::F32 => Ok("float".into()), + SpecialRustType::F64 => Ok("double".into()), + SpecialRustType::Bool => Ok("bool".into()), + SpecialRustType::ISize => Ok("nint".into()), + SpecialRustType::USize => Ok("nuint".into()), + } + } + + fn begin_file(&mut self, w: &mut dyn Write, _parsed_data: &ParsedData) -> io::Result<()> { + if !self.no_version_header { + writeln!(w, "/*")?; + writeln!(w, " Generated by typeshare {}", env!("CARGO_PKG_VERSION"))?; + writeln!(w, "*/")?; + } + + writeln!(w, "#nullable enable")?; + writeln!(w)?; + + writeln!(w, "using System.Reflection;")?; + writeln!(w, "using JsonSubTypes;")?; + writeln!(w, "using Newtonsoft.Json;")?; + writeln!(w, "using System.Runtime.Serialization;")?; + writeln!(w)?; + + if !self.namespace.is_empty() { + writeln!(w, "namespace {};", self.namespace)?; + writeln!(w)?; + } + + Ok(()) + } + + fn write_type_alias(&mut self, _w: &mut dyn Write, ty: &RustTypeAlias) -> io::Result<()> { + Err(io::Error::new( + io::ErrorKind::Unsupported, + RustTypeFormatError::TypeAliasesForbiddenInCS(format!( + "C# 11 does not support type aliases. At type alias \"{}\".", + ty.id.original + )), + )) + } + + fn write_struct(&mut self, w: &mut dyn Write, rs: &RustStruct) -> io::Result<()> { + self.write_comments(w, 0, &rs.comments)?; + writeln!( + w, + "public class {}{} {{", + rs.id.renamed, + (!rs.generic_types.is_empty()) + .then(|| { + format!( + "<{}>", + with_generic_definition_naming_convention(rs.generic_types.as_slice()) + ) + }) + .unwrap_or_default() + )?; + + rs.fields + .iter() + .try_for_each(|f| self.write_field(w, f, &rs.generic_types.as_slice()))?; + + writeln!(w, "}}\n") + } + + fn write_enum(&mut self, w: &mut dyn Write, e: &RustEnum) -> io::Result<()> { + self.write_types_for_anonymous_structs(w, e, &|variant_name| { + format!("{}{}Inner", &e.shared().id.renamed, variant_name) + })?; + self.write_comments(w, 0, &e.shared().comments)?; + + let generic_parameters = (!e.shared().generic_types.is_empty()) + .then(|| { + format!( + "<{}>", + with_generic_definition_naming_convention(&e.shared().generic_types) + ) + }) + .unwrap_or_default(); + + match e { + RustEnum::Unit(shared) => { + write!( + w, + "public enum {}{}\n{{", + shared.id.renamed, generic_parameters + )?; + + self.write_enum_variants(w, e)?; + + writeln!(w, "\n}}\n") + } + RustEnum::Algebraic { shared, .. } => { + write_discriminated_union_json_attributes(w, e)?; + write!( + w, + "public abstract record {}{} \n{{", + shared.id.renamed, generic_parameters + )?; + + self.write_enum_variants(w, e)?; + + writeln!(w, "\n}}\n")?; + writeln!(w) + } + } + } + + fn write_imports( + &mut self, + _writer: &mut dyn Write, + _imports: super::ScopedCrateTypes<'_>, + ) -> std::io::Result<()> { + todo!() + } +} + +fn write_discriminated_union_json_attributes(w: &mut dyn Write, e: &RustEnum) -> io::Result<()> { + match e { + RustEnum::Algebraic { + tag_key, + content_key: _content_key, + shared, + } => { + writeln!(w, "[JsonConverter(typeof(JsonSubtypes), \"{}\")]", tag_key)?; + let case_attributes = shared.variants.iter().map(|v| { + let case_name = match v { + RustEnumVariant::AnonymousStruct { shared, .. } => &shared.id.original, + RustEnumVariant::Unit(shared) => &shared.id.original, + RustEnumVariant::Tuple { shared, .. } => &shared.id.original, + }; + format!( + "[JsonSubtypes.KnownSubType(typeof({0}), \"{0}\")]", + case_name, + ) + }); + + writeln!(w, "{}", case_attributes.join_with("\n")) + } + _ => Ok(()), + } +} + +impl CSharp { + fn write_enum_variants(&mut self, w: &mut dyn Write, e: &RustEnum) -> io::Result<()> { + 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)?; + if shared.id.renamed != shared.id.original { + writeln!(w, "\t[EnumMember(Value = {:?})]", &shared.id.renamed)?; + } + writeln!(w, "\t{},", shared.id.original) + } + _ => unreachable!(), + }), + + // Write all the algebraic variants out (all three variant types are possible + // here) + RustEnum::Algebraic { + tag_key: _tag_key, + content_key, + shared, + } => shared.variants.iter().try_for_each(|v| { + writeln!(w)?; + self.write_comments(w, 1, &v.shared().comments)?; + let generic_types = &e.shared().generic_types; + let base_type = &e.shared().id.original; + let base_generics_names = generic_types + .iter() + .map(|name| with_generic_naming_convention(generic_types, &name)) + .collect_vec(); + + let base_generic_types = match base_generics_names.as_slice() { + [] => String::new(), + values => format!("<{}>", values.iter().join_with(", ")), + }; + + match v { + RustEnumVariant::Unit(shared) => write!( + w, + "\tpublic record {}(): {}{}();", + shared.id.original, base_type, base_generic_types, + ), + RustEnumVariant::Tuple { ty, shared } => { + let r#type = self + .format_type(&ty, e.shared().generic_types.as_slice()) + .map_err(|e| io::Error::new(io::ErrorKind::Other, e))?; + write!( + w, + "\tpublic record {}({}{} {}) : {}{}();", + shared.id.original, + r#type, + ty.is_optional().then(|| "?").unwrap_or_default(), + content_key.to_pascal_case(), + base_type, + base_generic_types, + ) + } + RustEnumVariant::AnonymousStruct { fields, shared } => { + let generics = fields + .iter() + .flat_map(|field| { + generic_types + .iter() + .filter(|g| field.ty.contains_type(g)) + .map(|name| { + with_generic_naming_convention(generic_types, &name) + }) + }) + .unique() + .collect_vec(); + + let generics = lazy_format!(match (generics.is_empty()) { + false => ("<{}>", generics.iter().join_with(", ")), + true => (""), + }); + + write!( + w, + "\tpublic record {}({}{}Inner{} {}): {}{}();", + shared.id.renamed, + e.shared().id.original, + shared.id.original, + generics, + content_key.to_pascal_case(), + base_type, + base_generic_types, + ) + } + } + }), + } + } + + fn write_field( + &mut self, + w: &mut dyn Write, + field: &RustField, + generic_types: &[String], + ) -> io::Result<()> { + self.write_comments(w, 1, &field.comments)?; + let cs_ty: String = match field.type_override(SupportedLanguage::CSharp) { + Some(type_override) => type_override.to_owned(), + None => self + .format_type(&field.ty, generic_types) + .map_err(|e| io::Error::new(io::ErrorKind::Other, e))?, + }; + + let optional = field.ty.is_optional() || field.has_default; + let is_readonly = field + .decorators + .get(&SupportedLanguage::CSharp) + .filter(|v| v.iter().any(|dec| dec.name() == "readonly")) + .is_some(); + + let property_name = if self.without_csharp_naming_convention { + field.id.renamed.clone() + } else { + csharp_property_aware_rename(&field.id.renamed) + }; + + writeln!( + w, + "{}\tpublic {}{} {} {{ get;{} }}", + if !optional { + "\t[JsonProperty(Required = Required.Always)]\n" + } else { + "" + }, + cs_ty, + optional.then(|| "?").unwrap_or_default(), + property_name, + if !is_readonly { " set;" } else { "" }, + )?; + + 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}/** +{tab} * {comment} +{tab} */", + tab = tab_indent, + comment = joined_comments + ) + } + }; + writeln!(w, "{}", comment)?; + } + Ok(()) + } +} + +fn with_generic_definition_naming_convention(generic_types: &[String]) -> String { + generic_types + .into_iter() + .map(|name| { + if name == "T" { + return name.clone(); + } else { + format!("T{}", name.to_string()) + } + }) + .collect::>() + .join(", ") +} + +fn with_generic_naming_convention<'a, 'b>(generic_types: &'a [String], name: &'b String) -> String { + if generic_types + .into_iter() + .any(|generic_name| generic_name == name && generic_name != "T") + { + format!("T{}", name.to_string()) + } else { + name.clone() + } +} + +fn csharp_property_aware_rename(name: &str) -> String { + remove_dash_from_identifier(name) + .to_pascal_case() + .to_string() +} + +#[cfg(test)] +mod tests { + #[test] + fn rename_property() { + let input = "open_links"; + let expected = "OpenLinks"; + let actual = super::csharp_property_aware_rename(input); + + assert_eq!(actual, expected) + } +} diff --git a/core/src/language/mod.rs b/core/src/language/mod.rs index 666118de..9219b73c 100644 --- a/core/src/language/mod.rs +++ b/core/src/language/mod.rs @@ -18,12 +18,14 @@ use std::{ str::FromStr, }; +mod csharp; mod go; mod kotlin; mod scala; mod swift; mod typescript; +pub use csharp::CSharp; pub use go::Go; pub use kotlin::Kotlin; pub use scala::Scala; @@ -99,13 +101,14 @@ pub enum SupportedLanguage { Scala, Swift, TypeScript, + CSharp, } impl SupportedLanguage { /// Returns an iterator over all supported language variants. pub fn all_languages() -> impl Iterator { use SupportedLanguage::*; - [Go, Kotlin, Scala, Swift, TypeScript].into_iter() + [Go, Kotlin, Scala, Swift, TypeScript, CSharp].into_iter() } /// Get the file name extension for the supported language. @@ -116,6 +119,7 @@ impl SupportedLanguage { SupportedLanguage::Scala => "scala", SupportedLanguage::Swift => "swift", SupportedLanguage::TypeScript => "ts", + SupportedLanguage::CSharp => "cs", } } } @@ -130,6 +134,7 @@ impl FromStr for SupportedLanguage { "scala" => Ok(Self::Scala), "swift" => Ok(Self::Swift), "typescript" => Ok(Self::TypeScript), + "csharp" => Ok(Self::CSharp), _ => Err(ParseError::UnsupportedLanguage(s.into())), } } diff --git a/core/src/rust_types.rs b/core/src/rust_types.rs index 146849b0..2c77a3a1 100644 --- a/core/src/rust_types.rs +++ b/core/src/rust_types.rs @@ -415,6 +415,10 @@ pub enum RustTypeFormatError { GenericsForbiddenInGo(String), #[error("Generic type `{0}` cannot be used as a map key in Typescript")] GenericKeyForbiddenInTS(String), + #[error("Type aliases are not supported in C# 11 or lower")] + TypeAliasesForbiddenInCS(String), + #[error("Type Unit is not supported in C#")] + TypeUnitInCS(), } impl SpecialRustType { diff --git a/core/tests/snapshot_tests.rs b/core/tests/snapshot_tests.rs index 3bca4114..ce0ccc5c 100644 --- a/core/tests/snapshot_tests.rs +++ b/core/tests/snapshot_tests.rs @@ -102,6 +102,9 @@ macro_rules! output_file_for_ident { (go) => { "output.go" }; + (csharp) => { + "output.cs" + }; } /// Simplifies the construction of `Language` instances for each language. @@ -219,6 +222,22 @@ macro_rules! language_instance { ..Default::default() }) }; + + // Default C# + (csharp) => { + language_instance!(csharp { + without_csharp_naming_convention: false, + }) + }; + // C# with configuration fields forwarded + (csharp {$($field:ident: $val:expr),* $(,)?}) => { + #[allow(clippy::needless_update)] + Box::new(typeshare_core::language::CSharp { + no_version_header: true, + $($field: $val,)* + ..Default::default() + }) + }; } /// This macro removes the boilerplate involved in creating typeshare snapshot @@ -355,6 +374,13 @@ static GO_MAPPINGS: Lazy> = Lazy::new(|| { .collect() }); +static CSHARP_MAPPINGS: Lazy> = Lazy::new(|| { + [("Url", "string"), ("DateTime", "string")] + .iter() + .map(|(k, v)| (k.to_string(), v.to_string())) + .collect() +}); + tests! { /// Enums can_generate_algebraic_enum: [ @@ -370,7 +396,10 @@ tests! { module_name: "colorsModule".to_string(), }, typescript, - go + go, + csharp { + namespace: "Company.Domain.Models".to_string(), + } ]; can_generate_generic_enum: [ swift { @@ -378,7 +407,8 @@ tests! { }, kotlin, scala, - typescript + typescript, + csharp, ]; can_generate_generic_struct: [ swift { @@ -387,7 +417,7 @@ tests! { }, kotlin, scala, - typescript + typescript, ]; can_generate_generic_type_alias: [ swift { @@ -397,9 +427,10 @@ tests! { scala, typescript ]; - can_generate_slice_of_user_type: [swift, kotlin, scala, typescript, go]; + can_generate_slice_of_user_type: [swift, kotlin, scala, typescript, go, csharp]; can_generate_readonly_fields: [ - typescript + typescript, + csharp ]; can_generate_simple_enum: [ swift { @@ -408,16 +439,17 @@ tests! { kotlin, scala, typescript, - go + go, + csharp ]; - can_generate_bare_string_enum: [swift, kotlin, scala, typescript, go ]; + can_generate_bare_string_enum: [swift, kotlin, scala, typescript, go, csharp ]; can_generate_double_option_pattern: [ typescript ]; can_recognize_types_inside_modules: [ - swift, kotlin, scala, typescript, go + swift, kotlin, scala, typescript, go, csharp ]; - test_simple_enum_case_name_support: [swift, kotlin, scala, typescript, go ]; + test_simple_enum_case_name_support: [swift, kotlin, scala, typescript, go, csharp ]; test_algebraic_enum_case_name_support: [ swift { prefix: "OP".to_string(), @@ -431,16 +463,24 @@ tests! { module_name: "colorModule".to_string(), }, typescript, - go + go, + csharp ]; - can_apply_prefix_correctly: [ swift { prefix: "OP".to_string(), }, kotlin { prefix: "OP".to_string(), }, scala, typescript, go ]; - can_generate_empty_algebraic_enum: [ swift { prefix: "OP".to_string(), }, kotlin { prefix: "OP".to_string(), }, scala, typescript, go ]; - can_generate_algebraic_enum_with_skipped_variants: [swift, kotlin, scala, typescript, go]; - can_generate_struct_with_skipped_fields: [swift, kotlin, scala, typescript, go]; - enum_is_properly_named_with_serde_overrides: [swift, kotlin, scala, typescript, go]; - can_handle_quote_in_serde_rename: [swift, kotlin, scala, typescript, go]; - can_handle_anonymous_struct: [swift, kotlin, scala, typescript, go]; - test_generate_char: [swift, kotlin, scala, typescript, go]; + can_apply_prefix_correctly: [ swift { prefix: "OP".to_string(), }, kotlin { prefix: "OP".to_string(), }, scala, typescript, go, csharp ]; + can_generate_empty_algebraic_enum: [ swift { prefix: "OP".to_string(), }, kotlin { prefix: "OP".to_string(), }, scala, typescript, go , csharp]; + can_generate_algebraic_enum_with_skipped_variants: [swift, kotlin, scala, typescript, go, csharp]; + can_generate_struct_with_skipped_fields: [swift, kotlin, scala, typescript, go, csharp]; + enum_is_properly_named_with_serde_overrides: [swift, kotlin, scala, typescript, go, csharp]; + can_handle_quote_in_serde_rename: [swift, kotlin, scala, typescript, go, csharp]; + can_handle_anonymous_struct: [ + swift, + kotlin, + scala, + typescript, + go, + csharp, + ]; + test_generate_char: [swift, kotlin, scala, typescript, go, csharp]; anonymous_struct_with_rename: [ swift { prefix: "Core".to_string(), @@ -448,13 +488,25 @@ tests! { kotlin, scala, typescript, - go + go, + csharp { + without_csharp_naming_convention: true, + }, + ]; + can_override_types: [ + swift, + kotlin, + scala, + typescript, + go, + csharp { + without_csharp_naming_convention: true, + }, ]; - can_override_types: [swift, kotlin, scala, typescript, go]; /// Structs - can_generate_simple_struct_with_a_comment: [kotlin, swift, typescript, scala, go]; - generate_types: [kotlin, swift, typescript, scala, go]; + can_generate_simple_struct_with_a_comment: [kotlin, swift, typescript, scala, go, csharp]; + generate_types: [kotlin, swift, typescript, scala, go, csharp]; can_handle_serde_rename: [ swift { prefix: "TypeShareX_".to_string(), @@ -462,14 +514,17 @@ tests! { kotlin, scala, typescript, - go + go, + csharp { + without_csharp_naming_convention: true, + }, ]; // TODO: kotlin and typescript don't appear to support this yet - generates_empty_structs_and_initializers: [swift, kotlin, scala, typescript, go]; + generates_empty_structs_and_initializers: [swift, kotlin, scala, typescript, go, csharp]; test_default_decorators: [swift { default_decorators: vec!["Sendable".into(), "Identifiable".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]; - test_serde_default_struct: [swift, kotlin, scala, typescript, go]; + test_i54_u53_type: [swift, kotlin, scala, typescript, go, csharp]; + test_serde_default_struct: [swift, kotlin, scala, typescript, go, csharp]; test_serde_iso8601: [ swift { prefix: String::new(), @@ -488,9 +543,12 @@ tests! { typescript { type_mappings: super::TYPESCRIPT_MAPPINGS.clone(), }, - go { + go { type_mappings: super::GO_MAPPINGS.clone(), }, + csharp { + type_mappings: super::CSHARP_MAPPINGS.clone(), + }, ]; test_serde_url: [ swift { @@ -514,10 +572,19 @@ tests! { type_mappings: super::GO_MAPPINGS.clone(), uppercase_acronyms: vec!["URL".to_string()], }, + csharp { + type_mappings: super::CSHARP_MAPPINGS.clone(), + }, ]; test_type_alias: [ swift { prefix: "OP".to_string(), }, kotlin, scala, typescript, go ]; test_optional_type_alias: [swift, kotlin, scala, typescript, go]; - test_serialized_as: [ swift { prefix: "OP".to_string(), }, kotlin, scala, typescript, go ]; + test_serialized_as: [ + swift { prefix: "OP".to_string(), }, + kotlin, + scala, + typescript, + go, + ]; test_serialized_as_tuple: [ swift { prefix: "OP".to_string(), @@ -529,31 +596,65 @@ tests! { uppercase_acronyms: vec!["ID".to_string()], }, ]; - can_handle_serde_rename_all: [swift, kotlin, scala, typescript, go]; - can_handle_serde_rename_on_top_level: [swift { prefix: "OP".to_string(), }, kotlin, scala, typescript, go]; - can_generate_unit_structs: [swift, kotlin, scala, typescript, go]; - kebab_case_rename: [swift, kotlin, scala, typescript, go]; + can_handle_serde_rename_all: [ + swift, + kotlin, + scala, + typescript, + go, + csharp { + without_csharp_naming_convention: true, + }, + ]; + can_handle_serde_rename_on_top_level: [ + swift { prefix: "OP".to_string(), }, + kotlin, + scala, + typescript, + go, + csharp { + without_csharp_naming_convention: true, + }, + ]; + can_generate_unit_structs: [swift, kotlin, scala, typescript, go, csharp]; + kebab_case_rename: [ + swift, + kotlin, + scala, + typescript, + go, + csharp { + without_csharp_naming_convention: true, + }, + ]; /// Globals get topologically sorted orders_types: [swift, kotlin, go]; /// Other - use_correct_integer_types: [swift, kotlin, scala, typescript, go]; + use_correct_integer_types: [swift, kotlin, scala, typescript, go, csharp]; // Only swift supports generating types with keywords 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]; + use_correct_decoded_variable_name: [swift, kotlin, scala, typescript, go, csharp]; 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(), } ]; algebraic_enum_decorator: [ swift{ prefix: "OP".to_string(), } ]; struct_decorator: [ kotlin, swift{ prefix: "OP".to_string(), } ]; - serialize_field_as: [kotlin, swift, typescript, scala, go]; + serialize_field_as: [kotlin, swift, typescript, scala, go, csharp]; serialize_type_alias: [kotlin, swift, typescript, scala, go]; - serialize_anonymous_field_as: [kotlin, swift, typescript, scala, go]; - smart_pointers: [kotlin, swift, typescript, scala, go]; - recursive_enum_decorator: [kotlin, swift, typescript, scala, go]; + serialize_anonymous_field_as: [kotlin, swift, typescript, scala, go, csharp]; + smart_pointers: [kotlin, swift, typescript, scala, go, csharp]; + recursive_enum_decorator: [ + kotlin, + swift, + typescript, + scala, + go, + csharp, + ]; uppercase_go_acronyms: [ go { @@ -567,7 +668,8 @@ tests! { typescript, kotlin, scala, - go + go, + csharp ]; can_generate_anonymous_struct_with_skipped_fields: [swift, kotlin, scala, typescript, go]; generic_struct_with_constraints_and_decorators: [swift { codablevoid_constraints: vec!["Equatable".into()]}];