diff --git a/cli/src/args.rs b/cli/src/args.rs index 05b79619..800ad719 100644 --- a/cli/src/args.rs +++ b/cli/src/args.rs @@ -12,6 +12,7 @@ pub enum AvailableLanguage { Go, #[cfg(feature = "python")] Python, + CSharp, } #[derive(clap::Parser)] @@ -49,6 +50,10 @@ pub struct Args { #[arg(long)] pub scala_package: Option, + /// CSharp namespace + #[arg(long)] + pub csharp_namespace: Option, + /// Scala serializer module name #[arg(long)] pub scala_module_name: Option, diff --git a/cli/src/config.rs b/cli/src/config.rs index 7529d556..53ac69ac 100644 --- a/cli/src/config.rs +++ b/cli/src/config.rs @@ -62,7 +62,15 @@ pub struct GoParams { pub type_mappings: HashMap, } -/// The parameters that are used to configure the behaviour of typeshare +#[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)] #[serde(default)] @@ -77,6 +85,7 @@ pub(crate) struct Config { pub go: GoParams, #[serde(skip)] pub target_os: Vec, + pub csharp: CSharpParams, } pub(crate) fn store_config(config: &Config, file_path: Option<&Path>) -> anyhow::Result<()> { diff --git a/cli/src/main.rs b/cli/src/main.rs index 7a702b4e..dc96d992 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -27,7 +27,7 @@ use typeshare_core::language::Go; use typeshare_core::language::Python; use typeshare_core::{ context::ParseContext, - language::{CrateName, Kotlin, Language, Scala, SupportedLanguage, Swift, TypeScript}, + language::{CSharp, CrateName, Kotlin, Language, Scala, SupportedLanguage, Swift, TypeScript}, parser::ParsedData, reconcile::reconcile_aliases, }; @@ -95,6 +95,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::CSharp => SupportedLanguage::CSharp, }, }; @@ -212,6 +213,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, @@ -262,6 +269,10 @@ fn override_configuration(mut config: Config, options: &Args) -> anyhow::Result< config.scala.module_name = scala_module_name.to_string(); } + if let Some(csharp_namespace) = options.csharp_namespace.as_ref() { + config.csharp.namespace = csharp_namespace.to_string(); + } + #[cfg(feature = "go")] { if let Some(go_package) = options.go_package.as_ref() { diff --git a/cli/src/parse.rs b/cli/src/parse.rs index 2ae38fe2..64c48d58 100644 --- a/cli/src/parse.rs +++ b/cli/src/parse.rs @@ -59,6 +59,7 @@ fn output_file_name(language_type: SupportedLanguage, crate_name: &CrateName) -> SupportedLanguage::Swift => pascal_case(), SupportedLanguage::TypeScript => snake_case(), SupportedLanguage::Python => 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..cd382f75 --- /dev/null +++ b/core/data/tests/anonymous_struct_with_rename/output.cs @@ -0,0 +1,45 @@ +#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 { + [JsonProperty(Required = Required.Always)] + public IEnumerable list { get; set; } +} + +/** Generated type representing the anonymous struct variant `LongFieldNames` of the `AnonymousStructWithRename` Rust enum */ +public class AnonymousStructWithRenameLongFieldNamesInner { + [JsonProperty(Required = Required.Always)] + public string some_long_field_name { get; set; } + [JsonProperty(Required = Required.Always)] + public bool and { get; set; } + [JsonProperty(Required = Required.Always)] + public IEnumerable but_one_more { get; set; } +} + +/** Generated type representing the anonymous struct variant `KebabCase` of the `AnonymousStructWithRename` Rust enum */ +public class AnonymousStructWithRenameKebabCaseInner { + [JsonProperty(Required = Required.Always)] + public IEnumerable another-list { get; set; } + [JsonProperty(Required = Required.Always)] + public string camelCaseStringField { get; set; } + [JsonProperty(Required = Required.Always)] + 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..c8129964 --- /dev/null +++ b/core/data/tests/can_apply_prefix_correctly/output.cs @@ -0,0 +1,30 @@ +#nullable enable + +using System.Reflection; +using JsonSubTypes; +using Newtonsoft.Json; +using System.Runtime.Serialization; + +public class ItemDetailsFieldValue { + [JsonProperty(Required = Required.Always)] + 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..8f787b0f --- /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), "number-array")] +[JsonSubtypes.KnownSubType(typeof(ReallyCoolType), "really-cool-type")] +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..6d3adab1 --- /dev/null +++ b/core/data/tests/can_generate_generic_enum/output.cs @@ -0,0 +1,76 @@ +#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 { + [JsonProperty(Required = Required.Always)] + 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 { + [JsonProperty(Required = Required.Always)] + public T Action { get; set; } +} + +/** Generated type representing the anonymous struct variant `VariantG` of the `GenericEnumsUsingStructVariants` Rust enum */ +public class GenericEnumsUsingStructVariantsVariantGInner { + [JsonProperty(Required = Required.Always)] + public T Action { get; set; } + [JsonProperty(Required = Required.Always)] + public TU Response { get; set; } +} + +/** Generated type representing the anonymous struct variant `VariantH` of the `GenericEnumsUsingStructVariants` Rust enum */ +public class GenericEnumsUsingStructVariantsVariantHInner { + [JsonProperty(Required = Required.Always)] + public int NonGeneric { get; set; } +} + +/** Generated type representing the anonymous struct variant `VariantI` of the `GenericEnumsUsingStructVariants` Rust enum */ +public class GenericEnumsUsingStructVariantsVariantIInner { + [JsonProperty(Required = Required.Always)] + public IEnumerable Vec { get; set; } + [JsonProperty(Required = Required.Always)] + 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..881d66ba --- /dev/null +++ b/core/data/tests/can_generate_readonly_fields/output.cs @@ -0,0 +1,12 @@ +#nullable enable + +using System.Reflection; +using JsonSubTypes; +using Newtonsoft.Json; +using System.Runtime.Serialization; + +public class SomeStruct { + [JsonProperty(Required = Required.Always)] + 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..76a6dbd9 --- /dev/null +++ b/core/data/tests/can_generate_simple_struct_with_a_comment/output.cs @@ -0,0 +1,24 @@ +#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 */ + [JsonProperty(Required = Required.Always)] + public string Name { get; set; } + [JsonProperty(Required = Required.Always)] + public ushort Age { get; set; } + public string? Info { get; set; } + [JsonProperty(Required = Required.Always)] + public IEnumerable Emails { get; set; } + [JsonProperty(Required = Required.Always)] + 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..95de631c --- /dev/null +++ b/core/data/tests/can_generate_slice_of_user_type/output.cs @@ -0,0 +1,12 @@ +#nullable enable + +using System.Reflection; +using JsonSubTypes; +using Newtonsoft.Json; +using System.Runtime.Serialization; + +public class Video { + [JsonProperty(Required = Required.Always)] + 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..da974387 --- /dev/null +++ b/core/data/tests/can_generate_struct_with_skipped_fields/output.cs @@ -0,0 +1,14 @@ +#nullable enable + +using System.Reflection; +using JsonSubTypes; +using Newtonsoft.Json; +using System.Runtime.Serialization; + +public class MyStruct { + [JsonProperty(Required = Required.Always)] + public int A { get; set; } + [JsonProperty(Required = Required.Always)] + 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..c93bb1bf --- /dev/null +++ b/core/data/tests/can_handle_anonymous_struct/output.cs @@ -0,0 +1,70 @@ +#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 */ + [JsonProperty(Required = Required.Always)] + 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 */ + [JsonProperty(Required = Required.Always)] + public string Uuid { get; set; } + /** Some other thing */ + [JsonProperty(Required = Required.Always)] + 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 { + [JsonProperty(Required = Required.Always)] + public string Uuid { get; set; } +} + +/** Generated type representing the anonymous struct variant `AnotherAnonVariant` of the `EnumWithManyVariants` Rust enum */ +public class EnumWithManyVariantsAnotherAnonVariantInner { + [JsonProperty(Required = Required.Always)] + public string Uuid { get; set; } + [JsonProperty(Required = Required.Always)] + 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..4385e58d --- /dev/null +++ b/core/data/tests/can_handle_serde_rename/output.cs @@ -0,0 +1,24 @@ +#nullable enable + +using System.Reflection; +using JsonSubTypes; +using Newtonsoft.Json; +using System.Runtime.Serialization; + +public class OtherType { +} + +/** This is a comment. */ +public class Person { + [JsonProperty(Required = Required.Always)] + public string name { get; set; } + [JsonProperty(Required = Required.Always)] + public ushort age { get; set; } + [JsonProperty(Required = Required.Always)] + public int extraSpecialFieldOne { get; set; } + public IEnumerable? extraSpecialFieldTwo { get; set; } + [JsonProperty(Required = Required.Always)] + 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..1d67137d --- /dev/null +++ b/core/data/tests/can_handle_serde_rename_all/output.cs @@ -0,0 +1,30 @@ +#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 { + [JsonProperty(Required = Required.Always)] + public string firstName { get; set; } + [JsonProperty(Required = Required.Always)] + public string lastName { get; set; } + [JsonProperty(Required = Required.Always)] + public ushort age { get; set; } + [JsonProperty(Required = Required.Always)] + public int extraSpecialField1 { get; set; } + public IEnumerable? extraSpecialField2 { get; set; } +} + +/** This is a Person2 struct with UPPERCASE rename */ +public class Person2 { + [JsonProperty(Required = Required.Always)] + public string FIRST_NAME { get; set; } + [JsonProperty(Required = Required.Always)] + public string LAST_NAME { get; set; } + [JsonProperty(Required = Required.Always)] + 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..0fc6b3e8 --- /dev/null +++ b/core/data/tests/can_handle_serde_rename_on_top_level/output.cs @@ -0,0 +1,24 @@ +#nullable enable + +using System.Reflection; +using JsonSubTypes; +using Newtonsoft.Json; +using System.Runtime.Serialization; + +public class OtherType { +} + +/** This is a comment. */ +public class PersonTwo { + [JsonProperty(Required = Required.Always)] + public string name { get; set; } + [JsonProperty(Required = Required.Always)] + public ushort age { get; set; } + [JsonProperty(Required = Required.Always)] + public int extraSpecialFieldOne { get; set; } + public IEnumerable? extraSpecialFieldTwo { get; set; } + [JsonProperty(Required = Required.Always)] + 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..21c53789 --- /dev/null +++ b/core/data/tests/can_override_types/output.cs @@ -0,0 +1,30 @@ +#nullable enable + +using System.Reflection; +using JsonSubTypes; +using Newtonsoft.Json; +using System.Runtime.Serialization; + +public class OverrideStruct { + [JsonProperty(Required = Required.Always)] + public char fieldToOverride { get; set; } +} + +/** Generated type representing the anonymous struct variant `AnonymousStructVariant` of the `OverrideEnum` Rust enum */ +public class OverrideEnumAnonymousStructVariantInner { + [JsonProperty(Required = Required.Always)] + 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..c3fe9447 --- /dev/null +++ b/core/data/tests/can_recognize_types_inside_modules/output.cs @@ -0,0 +1,27 @@ +#nullable enable + +using System.Reflection; +using JsonSubTypes; +using Newtonsoft.Json; +using System.Runtime.Serialization; + +public class A { + [JsonProperty(Required = Required.Always)] + public uint Field { get; set; } +} + +public class AB { + [JsonProperty(Required = Required.Always)] + public uint Field { get; set; } +} + +public class ABC { + [JsonProperty(Required = Required.Always)] + public uint Field { get; set; } +} + +public class OutsideOfModules { + [JsonProperty(Required = Required.Always)] + public uint Field { get; set; } +} + diff --git a/core/data/tests/csharp_without_naming_convention/input.rs b/core/data/tests/csharp_without_naming_convention/input.rs new file mode 100644 index 00000000..79cd6c16 --- /dev/null +++ b/core/data/tests/csharp_without_naming_convention/input.rs @@ -0,0 +1,24 @@ +#[typeshare] +#[serde(rename_all = "camelCase")] +pub struct ObjectNamedA { + depends_on: String, + age: i32, + some_string_value: String, +} + +#[typeshare] +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "kebab-case")] +pub enum DimensionFitValue { + WrapContent, + FitHeight, +} + +#[typeshare] +#[derive(Serialize, Deserialize)] +#[serde(tag = "type", content = "value", rename_all = "kebab-case")] +pub enum DimensionValue { + FixedSize(f32), + Percentage(f32), + Fit(DimensionFitValue), +} diff --git a/core/data/tests/csharp_without_naming_convention/output.cs b/core/data/tests/csharp_without_naming_convention/output.cs new file mode 100644 index 00000000..4f613117 --- /dev/null +++ b/core/data/tests/csharp_without_naming_convention/output.cs @@ -0,0 +1,38 @@ +#nullable enable + +using System.Reflection; +using JsonSubTypes; +using Newtonsoft.Json; +using System.Runtime.Serialization; + +public class ObjectNamedA { + [JsonProperty(Required = Required.Always)] + public string dependsOn { get; set; } + [JsonProperty(Required = Required.Always)] + public int age { get; set; } + [JsonProperty(Required = Required.Always)] + public string someStringValue { get; set; } +} + +public enum DimensionFitValue +{ + [EnumMember(Value = "wrap-content")] + WrapContent, + + [EnumMember(Value = "fit-height")] + FitHeight, + +} + +[JsonConverter(typeof(JsonSubtypes), "type")] +[JsonSubtypes.KnownSubType(typeof(FixedSize), "fixed-size")] +[JsonSubtypes.KnownSubType(typeof(Percentage), "percentage")] +[JsonSubtypes.KnownSubType(typeof(Fit), "fit")] +public abstract record DimensionValue +{ + public record FixedSize(float Value) : DimensionValue(); + public record Percentage(float Value) : DimensionValue(); + public record Fit(DimensionFitValue Value) : DimensionValue(); +} + + 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..690fdefe --- /dev/null +++ b/core/data/tests/generate_types/output.cs @@ -0,0 +1,32 @@ +#nullable enable + +using System.Reflection; +using JsonSubTypes; +using Newtonsoft.Json; +using System.Runtime.Serialization; + +public class CustomType { +} + +public class Types { + [JsonProperty(Required = Required.Always)] + public string S { get; set; } + [JsonProperty(Required = Required.Always)] + public string StaticS { get; set; } + [JsonProperty(Required = Required.Always)] + public short Int8 { get; set; } + [JsonProperty(Required = Required.Always)] + public float Float { get; set; } + [JsonProperty(Required = Required.Always)] + public double Double { get; set; } + [JsonProperty(Required = Required.Always)] + public IEnumerable Array { get; set; } + [JsonProperty(Required = Required.Always)] + public string[] FixedLengthArray { get; set; } + [JsonProperty(Required = Required.Always)] + public IDictionary Dictionary { get; set; } + public IDictionary? OptionalDictionary { get; set; } + [JsonProperty(Required = Required.Always)] + 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..2652b7c4 --- /dev/null +++ b/core/data/tests/kebab_case_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 class Things { + [JsonProperty(Required = Required.Always)] + 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..d9ddf7ef --- /dev/null +++ b/core/data/tests/recursive_enum_decorator/output.cs @@ -0,0 +1,43 @@ +#nullable enable + +using System.Reflection; +using JsonSubTypes; +using Newtonsoft.Json; +using System.Runtime.Serialization; + +/** Generated type representing the anonymous struct variant `Exactly` of the `MoreOptions` Rust enum */ +public class MoreOptionsExactlyInner { + [JsonProperty(Required = Required.Always)] + public string Config { get; set; } +} + +/** Generated type representing the anonymous struct variant `Built` of the `MoreOptions` Rust enum */ +public class MoreOptionsBuiltInner { + [JsonProperty(Required = Required.Always)] + 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(); +} + + +[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(); +} + + 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..ad1f9cd1 --- /dev/null +++ b/core/data/tests/resolves_qualified_type/output.cs @@ -0,0 +1,20 @@ +#nullable enable + +using System.Reflection; +using JsonSubTypes; +using Newtonsoft.Json; +using System.Runtime.Serialization; + +public class QualifiedTypes { + [JsonProperty(Required = Required.Always)] + public string Unqualified { get; set; } + [JsonProperty(Required = Required.Always)] + public string Qualified { get; set; } + [JsonProperty(Required = Required.Always)] + public IEnumerable QualifiedVec { get; set; } + [JsonProperty(Required = Required.Always)] + 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..048d0178 --- /dev/null +++ b/core/data/tests/serialize_field_as/output.cs @@ -0,0 +1,15 @@ +#nullable enable + +using System.Reflection; +using JsonSubTypes; +using Newtonsoft.Json; +using System.Runtime.Serialization; + +public class EditItemViewModelSaveRequest { + [JsonProperty(Required = Required.Always)] + public string Context { get; set; } + [JsonProperty(Required = Required.Always)] + 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..a40bf963 --- /dev/null +++ b/core/data/tests/smart_pointers/output.cs @@ -0,0 +1,68 @@ +#nullable enable + +using System.Reflection; +using JsonSubTypes; +using Newtonsoft.Json; +using System.Runtime.Serialization; + +/** This is a comment. */ +public class ArcyColors { + [JsonProperty(Required = Required.Always)] + public ushort Red { get; set; } + [JsonProperty(Required = Required.Always)] + public string Blue { get; set; } + [JsonProperty(Required = Required.Always)] + public IEnumerable Green { get; set; } +} + +/** This is a comment. */ +public class CellyColors { + [JsonProperty(Required = Required.Always)] + public string Red { get; set; } + [JsonProperty(Required = Required.Always)] + public IEnumerable Blue { get; set; } +} + +/** This is a comment. */ +public class CowyColors { + [JsonProperty(Required = Required.Always)] + public string Lifetime { get; set; } +} + +/** This is a comment. */ +public class LockyColors { + [JsonProperty(Required = Required.Always)] + public string Red { get; set; } +} + +/** This is a comment. */ +public class MutexyColors { + [JsonProperty(Required = Required.Always)] + public IEnumerable Blue { get; set; } + [JsonProperty(Required = Required.Always)] + public string Green { get; set; } +} + +/** This is a comment. */ +public class RcyColors { + [JsonProperty(Required = Required.Always)] + public string Red { get; set; } + [JsonProperty(Required = Required.Always)] + public IEnumerable Blue { get; set; } + [JsonProperty(Required = Required.Always)] + public string Green { 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..be6bfd36 --- /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), "number-array")] +[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..83c000b1 --- /dev/null +++ b/core/data/tests/test_generate_char/output.cs @@ -0,0 +1,12 @@ +#nullable enable + +using System.Reflection; +using JsonSubTypes; +using Newtonsoft.Json; +using System.Runtime.Serialization; + +public class MyType { + [JsonProperty(Required = Required.Always)] + 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..fd0defaa --- /dev/null +++ b/core/data/tests/test_i54_u53_type/output.cs @@ -0,0 +1,14 @@ +#nullable enable + +using System.Reflection; +using JsonSubTypes; +using Newtonsoft.Json; +using System.Runtime.Serialization; + +public class Foo { + [JsonProperty(Required = Required.Always)] + public long A { get; set; } + [JsonProperty(Required = Required.Always)] + 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..5bbe3772 --- /dev/null +++ b/core/data/tests/test_serde_iso8601/output.cs @@ -0,0 +1,12 @@ +#nullable enable + +using System.Reflection; +using JsonSubTypes; +using Newtonsoft.Json; +using System.Runtime.Serialization; + +public class Foo { + [JsonProperty(Required = Required.Always)] + 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..4b1f3f64 --- /dev/null +++ b/core/data/tests/test_serde_url/output.cs @@ -0,0 +1,12 @@ +#nullable enable + +using System.Reflection; +using JsonSubTypes; +using Newtonsoft.Json; +using System.Runtime.Serialization; + +public class Foo { + [JsonProperty(Required = Required.Always)] + 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..eb0690b6 --- /dev/null +++ b/core/data/tests/use_correct_integer_types/output.cs @@ -0,0 +1,23 @@ +#nullable enable + +using System.Reflection; +using JsonSubTypes; +using Newtonsoft.Json; +using System.Runtime.Serialization; + +/** This is a comment. */ +public class Foo { + [JsonProperty(Required = Required.Always)] + public short A { get; set; } + [JsonProperty(Required = Required.Always)] + public short B { get; set; } + [JsonProperty(Required = Required.Always)] + public int C { get; set; } + [JsonProperty(Required = Required.Always)] + public ushort E { get; set; } + [JsonProperty(Required = Required.Always)] + public ushort F { get; set; } + [JsonProperty(Required = Required.Always)] + 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..153a8503 --- /dev/null +++ b/core/src/language/csharp.rs @@ -0,0 +1,450 @@ +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, renamed) = match v { + RustEnumVariant::AnonymousStruct { shared, .. } => { + (&shared.id.original, &shared.id.renamed) + } + RustEnumVariant::Unit(shared) => (&shared.id.original, &shared.id.renamed), + RustEnumVariant::Tuple { shared, .. } => { + (&shared.id.original, &shared.id.renamed) + } + }; + format!( + "[JsonSubtypes.KnownSubType(typeof({0}), \"{1}\")]", + case_name, renamed + ) + }); + + 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{} {}): {}{}();", + if self.without_csharp_naming_convention { + &shared.id.renamed + } else { + &shared.id.original + }, + 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 4d007411..703ff6b3 100644 --- a/core/src/language/mod.rs +++ b/core/src/language/mod.rs @@ -19,6 +19,7 @@ use std::{ str::FromStr, }; +mod csharp; mod go; mod kotlin; mod python; @@ -26,6 +27,7 @@ mod scala; mod swift; mod typescript; +pub use csharp::CSharp; pub use go::Go; pub use kotlin::Kotlin; pub use python::Python; @@ -103,13 +105,14 @@ pub enum SupportedLanguage { Swift, TypeScript, Python, + CSharp, } 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, CSharp].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::CSharp => "cs", } } } @@ -136,6 +140,7 @@ impl FromStr for SupportedLanguage { "swift" => Ok(Self::Swift), "typescript" => Ok(Self::TypeScript), "python" => Ok(Self::Python), + "csharp" => Ok(Self::CSharp), _ => Err(ParseError::UnsupportedLanguage(s.into())), } } diff --git a/core/src/rust_types.rs b/core/src/rust_types.rs index cb231384..ad7042e8 100644 --- a/core/src/rust_types.rs +++ b/core/src/rust_types.rs @@ -493,6 +493,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 eb02798f..ac8e3a63 100644 --- a/core/tests/snapshot_tests.rs +++ b/core/tests/snapshot_tests.rs @@ -154,6 +154,9 @@ macro_rules! output_file_for_ident { (python) => { "output.py" }; + (csharp) => { + "output.cs" + }; } /// Simplifies the construction of `Language` instances for each language. @@ -286,6 +289,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() + }) + }; } macro_rules! target_os { @@ -443,6 +462,12 @@ static PYTHON_MAPPINGS: Lazy> = Lazy::new(|| { .map(|(k, v)| (k.to_string(), v.to_string())) .collect() }); +static CSHARP_MAPPINGS: Lazy> = Lazy::new(|| { + [("Url", "string"), ("DateTime", "string")] + .iter() + .map(|(k, v)| (k.to_string(), v.to_string())) + .collect() +}); tests! { /// Enums @@ -460,7 +485,10 @@ tests! { }, typescript, go, - python + python, + csharp { + namespace: "Company.Domain.Models".to_string(), + } ]; can_generate_generic_enum: [ swift { @@ -468,7 +496,8 @@ tests! { }, kotlin, scala, - typescript + typescript, + csharp, ]; can_generate_generic_struct: [ swift { @@ -488,9 +517,10 @@ tests! { typescript ]; 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, csharp]; can_generate_readonly_fields: [ - typescript + typescript, + csharp ]; can_generate_simple_enum: [ swift { @@ -500,16 +530,17 @@ tests! { scala, typescript, go, + csharp, python ]; - can_generate_bare_string_enum: [swift, kotlin, scala, typescript, go, python ]; + can_generate_bare_string_enum: [swift, kotlin, scala, typescript, go, python, csharp ]; can_generate_double_option_pattern: [ typescript ]; can_recognize_types_inside_modules: [ - swift, kotlin, scala, typescript, go, python + swift, kotlin, scala, typescript, go, python, csharp ]; - test_simple_enum_case_name_support: [swift, kotlin, scala, typescript, go, python ]; + test_simple_enum_case_name_support: [swift, kotlin, scala, typescript, go, python, csharp ]; test_algebraic_enum_case_name_support: [ swift { prefix: "OP".to_string(), @@ -524,16 +555,17 @@ tests! { }, typescript, go, + csharp, python ]; - 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_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_apply_prefix_correctly: [ swift { prefix: "OP".to_string(), }, kotlin { prefix: "OP".to_string(), }, scala, typescript, go, python, csharp ]; + can_generate_empty_algebraic_enum: [ swift { prefix: "OP".to_string(), }, kotlin { prefix: "OP".to_string(), }, scala, typescript, go, python, csharp ]; + can_generate_algebraic_enum_with_skipped_variants: [swift, kotlin, scala, typescript, go, python, csharp]; + can_generate_struct_with_skipped_fields: [swift, kotlin, scala, typescript, go, python, csharp]; + enum_is_properly_named_with_serde_overrides: [swift, kotlin, scala, typescript, go, python, csharp]; + can_handle_quote_in_serde_rename: [swift, kotlin, scala, typescript, go, python, csharp]; + can_handle_anonymous_struct: [swift, kotlin, scala, typescript, go, python, csharp]; + test_generate_char: [swift, kotlin, scala, typescript, go, python, csharp]; anonymous_struct_with_rename: [ swift { prefix: "Core".to_string(), @@ -542,13 +574,15 @@ tests! { scala, typescript, go, - python + python, + 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, python]; - generate_types: [kotlin, swift, typescript, scala, go, python]; + can_generate_simple_struct_with_a_comment: [kotlin, swift, typescript, scala, go, python, csharp]; + generate_types: [kotlin, swift, typescript, scala, go, python, csharp]; can_handle_serde_rename: [ swift { prefix: "TypeShareX_".to_string(), @@ -557,14 +591,17 @@ tests! { scala, typescript, go, + csharp { + without_csharp_naming_convention: true, + }, python ]; // TODO: kotlin and typescript don't appear to support this yet - generates_empty_structs_and_initializers: [swift, kotlin, scala, typescript, go,python]; + generates_empty_structs_and_initializers: [swift, kotlin, scala, typescript, go,python, 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, python]; - test_serde_default_struct: [swift, kotlin, scala, typescript, go, python]; + test_i54_u53_type: [swift, kotlin, scala, typescript, go, python, csharp]; + test_serde_default_struct: [swift, kotlin, scala, typescript, go, python, csharp]; test_serde_iso8601: [ swift { prefix: String::new(), @@ -583,9 +620,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 { @@ -609,6 +649,9 @@ tests! { type_mappings: super::GO_MAPPINGS.clone(), uppercase_acronyms: vec!["URL".to_string()], }, + csharp { + type_mappings: super::CSHARP_MAPPINGS.clone(), + }, python{ type_mappings: super::PYTHON_MAPPINGS.clone() } @@ -628,31 +671,31 @@ tests! { }, python ]; - 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_generate_unit_structs: [swift, kotlin, scala, typescript, go, python]; - kebab_case_rename: [swift, kotlin, scala, typescript, go, python]; + can_handle_serde_rename_all: [swift, kotlin, scala, typescript, go,python, csharp { without_csharp_naming_convention: true, }]; + can_handle_serde_rename_on_top_level: [swift { prefix: "OP".to_string(), }, kotlin, scala, typescript, go, python, csharp { without_csharp_naming_convention: true, }]; + can_generate_unit_structs: [swift, kotlin, scala, typescript, go, python, csharp]; + kebab_case_rename: [swift, kotlin, scala, typescript, go, python, csharp { without_csharp_naming_convention: true, }]; /// Globals get topologically sorted 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, 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, python]; + use_correct_decoded_variable_name: [swift, kotlin, scala, typescript, go, python, csharp]; can_handle_unit_type: [swift { codablevoid_constraints: vec!["Equatable".into()]} , kotlin, scala, typescript, go, python]; //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, python]; + serialize_field_as: [kotlin, swift, typescript, scala, go, python, csharp]; serialize_type_alias: [kotlin, swift, typescript, scala, go, python]; - serialize_anonymous_field_as: [kotlin, swift, typescript, scala, go, python]; - smart_pointers: [kotlin, swift, typescript, scala, go, python]; - recursive_enum_decorator: [kotlin, swift, typescript, scala, go, python]; + serialize_anonymous_field_as: [kotlin, swift, typescript, scala, go, python, csharp]; + smart_pointers: [kotlin, swift, typescript, scala, go, python, csharp]; + recursive_enum_decorator: [kotlin, swift, typescript, scala, go, python, csharp]; uppercase_go_acronyms: [ go { @@ -667,11 +710,16 @@ tests! { kotlin, scala, go, - python + python, + csharp ]; can_generate_anonymous_struct_with_skipped_fields: [swift, kotlin, scala, typescript, go, python]; 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"]; + csharp_without_naming_convention: [csharp { + without_csharp_naming_convention: true, + } + ]; + excluded_by_target_os: [ swift, kotlin, scala, typescript, go, python ] target_os: ["android", "macos"]; // excluded_by_target_os_full_module: [swift] target_os: "ios"; serde_rename_references: [ swift, kotlin, scala, typescript, go ]; }