#![expect(dead_code, reason = "test structs are only used via derive macro")]
use csharp_rs::{CSharp, CSharpVersion, Config, Serializer};
#[derive(CSharp)]
struct SimpleStruct {
name: String,
level: i32,
active: bool,
}
#[test]
fn simple_struct_name() {
let cfg = Config::default();
assert_eq!(SimpleStruct::csharp_name(&cfg), "SimpleStruct");
}
#[test]
fn simple_struct_definition_contains_record() {
let cfg = Config::default();
let def = SimpleStruct::csharp_definition(&cfg);
assert!(
def.contains("public sealed record SimpleStruct"),
"missing record declaration:\n{def}"
);
}
#[test]
fn simple_struct_definition_contains_properties() {
let cfg = Config::default();
let def = SimpleStruct::csharp_definition(&cfg);
assert!(
def.contains("public string Name { get; init; }"),
"missing Name property:\n{def}"
);
assert!(
def.contains("public int Level { get; init; }"),
"missing Level property:\n{def}"
);
assert!(
def.contains("public bool Active { get; init; }"),
"missing Active property:\n{def}"
);
}
#[test]
fn simple_struct_definition_has_auto_generated_comment() {
let cfg = Config::default();
let def = SimpleStruct::csharp_definition(&cfg);
assert!(
def.starts_with("// <auto-generated/>"),
"missing auto-generated comment:\n{def}"
);
}
#[test]
fn simple_struct_definition_has_stj_attributes() {
let cfg = Config::default();
let def = SimpleStruct::csharp_definition(&cfg);
assert!(
def.contains("using System.Text.Json.Serialization;"),
"missing using directive:\n{def}"
);
assert!(
def.contains("[JsonPropertyName(\"name\")]"),
"missing JsonPropertyName for name:\n{def}"
);
assert!(
def.contains("[JsonPropertyName(\"level\")]"),
"missing JsonPropertyName for level:\n{def}"
);
}
#[test]
fn simple_struct_default_namespace() {
let cfg = Config::default();
let def = SimpleStruct::csharp_definition(&cfg);
assert!(
def.contains("namespace Generated"),
"missing default namespace:\n{def}"
);
}
#[derive(CSharp)]
#[serde(rename_all = "camelCase")]
struct PlayerProfile {
player_id: String,
display_name: String,
max_health: i32,
current_score: f64,
}
#[test]
fn rename_all_camel_case_json_names() {
let cfg = Config::default();
let def = PlayerProfile::csharp_definition(&cfg);
assert!(
def.contains("[JsonPropertyName(\"playerId\")]"),
"missing camelCase playerId:\n{def}"
);
assert!(
def.contains("[JsonPropertyName(\"displayName\")]"),
"missing camelCase displayName:\n{def}"
);
assert!(
def.contains("[JsonPropertyName(\"maxHealth\")]"),
"missing camelCase maxHealth:\n{def}"
);
assert!(
def.contains("[JsonPropertyName(\"currentScore\")]"),
"missing camelCase currentScore:\n{def}"
);
}
#[test]
fn rename_all_keeps_pascal_case_properties() {
let cfg = Config::default();
let def = PlayerProfile::csharp_definition(&cfg);
assert!(
def.contains("public string PlayerId { get; init; }"),
"missing PascalCase PlayerId:\n{def}"
);
assert!(
def.contains("public string DisplayName { get; init; }"),
"missing PascalCase DisplayName:\n{def}"
);
}
#[derive(CSharp)]
#[csharp(namespace = "PulsarAnvil.Types")]
struct GameConfig {
difficulty: i32,
}
#[test]
fn namespace_override() {
let cfg = Config::default();
let def = GameConfig::csharp_definition(&cfg);
assert!(
def.contains("namespace PulsarAnvil.Types"),
"missing namespace override:\n{def}"
);
}
#[derive(CSharp)]
struct WithOptionals {
required_field: String,
optional_score: Option<f64>,
optional_name: Option<String>,
}
#[test]
fn optional_fields_are_nullable() {
let cfg = Config::default();
let def = WithOptionals::csharp_definition(&cfg);
assert!(
def.contains("public string RequiredField { get; init; }"),
"required field should not be nullable:\n{def}"
);
assert!(
def.contains("public double? OptionalScore { get; init; }"),
"optional field should be nullable:\n{def}"
);
assert!(
def.contains("public string? OptionalName { get; init; }"),
"optional string should be nullable:\n{def}"
);
}
use std::collections::HashMap;
#[derive(CSharp)]
struct WithCollections {
tags: Vec<String>,
scores: HashMap<String, i32>,
items: Vec<i64>,
}
#[test]
fn vec_fields_map_to_list() {
let cfg = Config::default();
let def = WithCollections::csharp_definition(&cfg);
assert!(
def.contains("public List<string> Tags { get; init; }"),
"Vec<String> should map to List<string>:\n{def}"
);
assert!(
def.contains("public List<long> Items { get; init; }"),
"Vec<i64> should map to List<long>:\n{def}"
);
}
#[test]
fn hashmap_fields_map_to_dictionary() {
let cfg = Config::default();
let def = WithCollections::csharp_definition(&cfg);
assert!(
def.contains("public Dictionary<string, int> Scores { get; init; }"),
"HashMap should map to Dictionary:\n{def}"
);
}
#[derive(CSharp)]
#[serde(rename_all = "PascalCase")]
#[csharp(namespace = "Game.Models")]
struct FullExample {
player_id: String,
level: i32,
score: Option<f64>,
inventory: Vec<String>,
metadata: HashMap<String, String>,
}
#[test]
fn full_example_output() {
let cfg = Config::default();
let def = FullExample::csharp_definition(&cfg);
assert!(def.contains("// <auto-generated/>"));
assert!(def.contains("using System.Text.Json.Serialization;"));
assert!(def.contains("namespace Game.Models"));
assert!(def.contains("public sealed record FullExample"));
assert!(
def.contains("[JsonPropertyName(\"PlayerId\")]"),
"JSON name should be PascalCase:\n{def}"
);
assert!(def.contains("[JsonPropertyName(\"Level\")]"));
assert!(def.contains("[JsonPropertyName(\"Score\")]"));
assert!(def.contains("public string PlayerId { get; init; }"));
assert!(def.contains("public int Level { get; init; }"));
assert!(def.contains("public double? Score { get; init; }"));
assert!(def.contains("public List<string> Inventory { get; init; }"));
assert!(def.contains("public Dictionary<string, string> Metadata { get; init; }"));
}
#[test]
fn dependencies_include_field_types() {
let cfg = Config::default();
let deps = FullExample::dependencies(&cfg);
assert!(deps.contains(&String::from("string")));
assert!(deps.contains(&String::from("int")));
assert!(deps.contains(&String::from("double")));
assert!(deps.contains(&String::from("List<string>")));
}
#[derive(CSharp)]
struct WithFieldRename {
#[serde(rename = "userId")]
user_id: String,
level: i32,
}
#[test]
fn field_rename_json_name() {
let cfg = Config::default();
let def = WithFieldRename::csharp_definition(&cfg);
assert!(
def.contains("[JsonPropertyName(\"userId\")]"),
"JSON name should be 'userId':\n{def}"
);
assert!(
def.contains("public string UserId { get; init; }"),
"C# property should be PascalCase UserId:\n{def}"
);
assert!(
def.contains("[JsonPropertyName(\"level\")]"),
"level should be unaffected:\n{def}"
);
}
#[derive(CSharp)]
#[serde(rename_all = "camelCase")]
struct RenameOverride {
#[serde(rename = "ID")]
player_id: String,
display_name: String,
}
#[test]
fn field_rename_overrides_rename_all() {
let cfg = Config::default();
let def = RenameOverride::csharp_definition(&cfg);
assert!(
def.contains("[JsonPropertyName(\"ID\")]"),
"field rename should override rename_all:\n{def}"
);
assert!(
def.contains("[JsonPropertyName(\"displayName\")]"),
"non-renamed field should use camelCase:\n{def}"
);
}
#[derive(CSharp)]
struct WithSkip {
visible: String,
#[serde(skip)]
hidden: String,
also_visible: i32,
}
#[test]
fn skip_excludes_field() {
let cfg = Config::default();
let def = WithSkip::csharp_definition(&cfg);
assert!(
def.contains("public string Visible { get; init; }"),
"visible field should be present:\n{def}"
);
assert!(
!def.contains("Hidden"),
"skipped field should be absent:\n{def}"
);
assert!(
def.contains("public int AlsoVisible { get; init; }"),
"also_visible field should be present:\n{def}"
);
}
#[derive(CSharp)]
struct WithSkipSerializing {
visible: String,
#[serde(skip_serializing)]
write_only: String,
}
#[test]
fn skip_serializing_excludes_field() {
let cfg = Config::default();
let def = WithSkipSerializing::csharp_definition(&cfg);
assert!(
def.contains("public string Visible { get; init; }"),
"visible field should be present:\n{def}"
);
assert!(
!def.contains("WriteOnly"),
"skip_serializing field should be absent:\n{def}"
);
}
#[derive(CSharp)]
struct WithSkipSerializingIf {
name: String,
#[serde(skip_serializing_if = "Option::is_none")]
nickname: Option<String>,
#[serde(skip_serializing_if = "String::is_empty")]
tag: String,
}
#[test]
fn skip_serializing_if_forces_nullable() {
let cfg = Config::default();
let def = WithSkipSerializingIf::csharp_definition(&cfg);
assert!(
def.contains("public string Name { get; init; }"),
"name should be required:\n{def}"
);
assert!(
def.contains("public string? Nickname { get; init; }"),
"nickname should be nullable (Option + skip_serializing_if):\n{def}"
);
assert!(
def.contains("public string? Tag { get; init; }"),
"tag should be forced nullable by skip_serializing_if:\n{def}"
);
}
#[derive(CSharp)]
struct WithDefault {
name: String,
#[serde(default)]
level: i32,
}
#[test]
fn serde_default_makes_field_nullable() {
let cfg = Config::default();
let def = WithDefault::csharp_definition(&cfg);
assert!(
def.contains("int? Level"),
"field with serde(default) should be nullable:\n{def}"
);
assert!(
!def.contains("int? Name") && def.contains("string Name"),
"field without default should not be affected:\n{def}"
);
}
#[derive(CSharp)]
struct WithTypeOverride {
name: String,
#[csharp(type = "JsonElement")]
data: String,
}
#[test]
fn csharp_type_override_replaces_csharp_type() {
let cfg = Config::default();
let def = WithTypeOverride::csharp_definition(&cfg);
assert!(
def.contains("JsonElement Data"),
"type override should replace C# type:\n{def}"
);
assert!(
!def.contains("string Data"),
"original type should not appear:\n{def}"
);
}
#[derive(CSharp)]
#[allow(clippy::option_option, reason = "testing nested Option type override")]
struct WithNestedOptionTypeOverride {
#[serde(skip_serializing_if = "Option::is_none")]
#[csharp(type = "string?")]
inherits_from: Option<Option<String>>,
}
#[test]
fn csharp_type_override_suppresses_double_nullable() {
let cfg = Config::default();
let def = WithNestedOptionTypeOverride::csharp_definition(&cfg);
assert!(
def.contains("string? InheritsFrom"),
"type override should produce string?, not string??:\n{def}"
);
assert!(
!def.contains("string??"),
"double nullable must not appear:\n{def}"
);
}
#[derive(CSharp)]
#[serde(rename_all = "camelCase")]
struct CombinedFieldAttrs {
#[serde(rename = "id")]
player_id: String,
#[serde(skip)]
internal: String,
#[serde(skip_serializing_if = "Option::is_none")]
bio: Option<String>,
display_name: String,
}
#[test]
fn combined_field_attrs() {
let cfg = Config::default();
let def = CombinedFieldAttrs::csharp_definition(&cfg);
assert!(
def.contains("[JsonPropertyName(\"id\")]"),
"player_id should be renamed to 'id':\n{def}"
);
assert!(
!def.contains("Internal"),
"internal should be skipped:\n{def}"
);
assert!(
def.contains("public string? Bio { get; init; }"),
"bio should be nullable:\n{def}"
);
assert!(
def.contains("[JsonPropertyName(\"displayName\")]"),
"display_name should use camelCase:\n{def}"
);
}
#[test]
fn simple_struct_newtonsoft_uses_json_property() {
let cfg = Config::default().with_serializer(Serializer::Newtonsoft);
let def = SimpleStruct::csharp_definition(&cfg);
assert!(
def.contains("[JsonProperty(\"name\")]"),
"Newtonsoft should use [JsonProperty]:\n{def}"
);
assert!(
def.contains("using Newtonsoft.Json;"),
"Newtonsoft should have Newtonsoft.Json using:\n{def}"
);
assert!(
!def.contains("JsonPropertyName"),
"Newtonsoft should NOT contain JsonPropertyName:\n{def}"
);
}
#[test]
fn simple_struct_stj_uses_json_property_name() {
let cfg = Config::default().with_serializer(Serializer::SystemTextJson);
let def = SimpleStruct::csharp_definition(&cfg);
assert!(
def.contains("[JsonPropertyName(\"name\")]"),
"STJ should use [JsonPropertyName]:\n{def}"
);
assert!(
def.contains("using System.Text.Json.Serialization;"),
"STJ should have System.Text.Json.Serialization using:\n{def}"
);
}
#[test]
fn simple_struct_csharp9_block_scoped_namespace() {
let cfg = Config::default();
let def = SimpleStruct::csharp_definition(&cfg);
assert!(
!def.contains("namespace Generated;"),
"C# 9 should NOT use file-scoped namespace:\n{def}"
);
assert!(
def.contains("namespace Generated\n{"),
"C# 9 should use block-scoped namespace:\n{def}"
);
}
#[test]
fn simple_struct_csharp10_file_scoped_namespace() {
let cfg = Config::default().with_target(CSharpVersion::CSharp10);
let def = SimpleStruct::csharp_definition(&cfg);
assert!(
def.contains("namespace Generated;"),
"C# 10 should use file-scoped namespace:\n{def}"
);
}
#[test]
fn simple_struct_csharp11_has_required_modifier() {
let cfg = Config::default().with_target(CSharpVersion::CSharp11);
let def = SimpleStruct::csharp_definition(&cfg);
assert!(
def.contains("public required string Name"),
"C# 11 non-optional properties should have required modifier:\n{def}"
);
}
#[test]
fn simple_struct_csharp9_no_required_modifier() {
let cfg = Config::default();
let def = SimpleStruct::csharp_definition(&cfg);
assert!(
!def.contains("required"),
"C# 9 should NOT have required modifier:\n{def}"
);
}
#[test]
fn simple_struct_config_namespace_override() {
let cfg = Config::default().with_namespace("Custom.Ns");
let def = SimpleStruct::csharp_definition(&cfg);
assert!(
def.contains("namespace Custom.Ns"),
"should use config namespace:\n{def}"
);
assert!(
!def.contains("Generated"),
"should NOT contain default namespace:\n{def}"
);
}
#[test]
fn csharp_namespace_attr_overrides_config_namespace() {
let cfg = Config::default().with_namespace("Custom.Ns");
let def = GameConfig::csharp_definition(&cfg);
assert!(
def.contains("namespace PulsarAnvil.Types"),
"attribute namespace should take precedence over config:\n{def}"
);
assert!(
!def.contains("Custom.Ns"),
"config namespace should NOT appear when attr overrides:\n{def}"
);
}
#[derive(CSharp)]
struct UserId(String);
#[test]
fn newtype_struct_name() {
let cfg = Config::default();
assert_eq!(UserId::csharp_name(&cfg), "UserId");
}
#[test]
fn newtype_struct_has_value_property() {
let cfg = Config::default();
let def = UserId::csharp_definition(&cfg);
assert!(
def.contains("public sealed record UserId"),
"newtype should generate sealed record:\n{def}"
);
assert!(
def.contains("string Value"),
"newtype should have Value property with correct type:\n{def}"
);
}
#[test]
fn newtype_struct_has_json_property_name() {
let cfg = Config::default();
let def = UserId::csharp_definition(&cfg);
assert!(
def.contains("JsonPropertyName"),
"non-transparent newtype should have JsonPropertyName:\n{def}"
);
}
#[derive(CSharp)]
#[serde(transparent)]
struct PlayerId(String);
#[test]
fn transparent_newtype_has_converter() {
let cfg = Config::default();
let def = PlayerId::csharp_definition(&cfg);
assert!(
def.contains("PlayerIdConverter"),
"transparent newtype should have converter:\n{def}"
);
assert!(
def.contains("[JsonConverter(typeof(PlayerIdConverter))]"),
"transparent newtype should have converter attribute:\n{def}"
);
}
#[test]
fn transparent_newtype_no_json_property_on_value() {
let cfg = Config::default();
let def = PlayerId::csharp_definition(&cfg);
assert!(
!def.contains("JsonPropertyName"),
"transparent newtype should NOT have JsonPropertyName:\n{def}"
);
}
#[test]
fn transparent_newtype_converter_reads_inner_type() {
let cfg = Config::default();
let def = PlayerId::csharp_definition(&cfg);
assert!(
def.contains("Deserialize<string>"),
"STJ converter should deserialize inner type:\n{def}"
);
}
#[test]
fn transparent_newtype_newtonsoft_converter() {
let cfg = Config::default().with_serializer(Serializer::Newtonsoft);
let def = PlayerId::csharp_definition(&cfg);
assert!(
def.contains("PlayerIdConverter"),
"Newtonsoft transparent newtype should have converter:\n{def}"
);
assert!(
def.contains("ReadJson"),
"Newtonsoft converter should have ReadJson:\n{def}"
);
assert!(
def.contains("WriteJson"),
"Newtonsoft converter should have WriteJson:\n{def}"
);
}
#[test]
fn transparent_newtype_csharp10_file_scoped() {
let cfg = Config::default().with_target(CSharpVersion::CSharp10);
let def = PlayerId::csharp_definition(&cfg);
assert!(
def.contains("namespace Generated;"),
"C# 10 transparent newtype should use file-scoped namespace:\n{def}"
);
}
#[test]
fn unity_struct_generates_class_not_record() {
let cfg = Config::default().with_target(CSharpVersion::Unity);
let def = SimpleStruct::csharp_definition(&cfg);
assert!(
def.contains("public sealed class SimpleStruct"),
"Unity should generate sealed class:\n{def}"
);
assert!(
!def.contains("sealed record"),
"Unity should NOT generate sealed record:\n{def}"
);
}
#[test]
fn unity_struct_uses_get_set() {
let cfg = Config::default().with_target(CSharpVersion::Unity);
let def = SimpleStruct::csharp_definition(&cfg);
assert!(
def.contains("{ get; set; }"),
"Unity should use get; set; accessors:\n{def}"
);
assert!(
!def.contains("get; init;"),
"Unity should NOT use get; init;:\n{def}"
);
}
#[test]
fn unity_struct_no_required_modifier() {
let cfg = Config::default().with_target(CSharpVersion::Unity);
let def = SimpleStruct::csharp_definition(&cfg);
assert!(
!def.contains("required"),
"Unity should NOT have required modifier:\n{def}"
);
}
#[test]
fn unity_struct_block_scoped_namespace() {
let cfg = Config::default().with_target(CSharpVersion::Unity);
let def = SimpleStruct::csharp_definition(&cfg);
assert!(
!def.contains("namespace Generated;"),
"Unity should NOT use file-scoped namespace:\n{def}"
);
assert!(
def.contains("namespace Generated\n{"),
"Unity should use block-scoped namespace:\n{def}"
);
}
#[test]
fn unity_transparent_newtype_uses_class() {
let cfg = Config::default().with_target(CSharpVersion::Unity);
let def = PlayerId::csharp_definition(&cfg);
assert!(
def.contains("public sealed class PlayerId"),
"Unity transparent newtype should use class:\n{def}"
);
}