Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
130 changes: 125 additions & 5 deletions src/Yamlify.SourceGenerator/YamlSourceGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,22 @@ attrClass.TypeArguments[0] is INamedTypeSymbol mappingBaseType &&
polymorphicConfig = new PolymorphicInfo(typeDiscriminatorPropertyName, derivedTypeMappings);
}

typesToGenerate.Add(new TypeToGenerate(typeArg, typeOrdering, polymorphicConfig));
// Check if type has [YamlConverter] attribute for custom converter support
INamedTypeSymbol? customConverterType = null;
foreach (var typeAttr in typeArg.GetAttributes())
{
if (typeAttr.AttributeClass?.ToDisplayString() == "Yamlify.Serialization.YamlConverterAttribute")
{
if (typeAttr.ConstructorArguments.Length > 0 &&
typeAttr.ConstructorArguments[0].Value is INamedTypeSymbol converterType)
{
customConverterType = converterType;
}
break;
}
}

typesToGenerate.Add(new TypeToGenerate(typeArg, typeOrdering, polymorphicConfig, customConverterType));
}
else if (attrName == "Yamlify.Serialization.YamlSourceGenerationOptionsAttribute")
{
Expand Down Expand Up @@ -426,10 +441,26 @@ private static void GenerateTypeInfoMethod(StringBuilder sb, TypeToGenerate type
var propertyName = propertyNameMap[type.Symbol.ToDisplayString()];
var fullTypeName = type.Symbol.ToDisplayString();
var converterName = GetConverterName(type.Symbol);
var hasCustomConverter = type.CustomConverterType is not null;

sb.AppendLine($" private YamlTypeInfo<{fullTypeName}> Create{propertyName}TypeInfo()");
sb.AppendLine(" {");
sb.AppendLine($" var converter = new {converterName}();");

if (hasCustomConverter)
{
// Use the custom converter with GeneratedRead/GeneratedWrite delegates set via object initializer
var customConverterTypeName = type.CustomConverterType!.ToDisplayString();
sb.AppendLine($" var converter = new {customConverterTypeName}");
sb.AppendLine(" {");
sb.AppendLine($" GeneratedRead = {converterName}.ReadCore,");
sb.AppendLine($" GeneratedWrite = {converterName}.WriteCore");
sb.AppendLine(" };");
}
else
{
sb.AppendLine($" var converter = new {converterName}();");
}

sb.AppendLine($" var properties = new List<YamlPropertyInfo>();");
sb.AppendLine();

Expand Down Expand Up @@ -484,8 +515,18 @@ private static void GenerateTypeInfoMethod(StringBuilder sb, TypeToGenerate type
sb.AppendLine($" CreateInstance = static () => new {fullTypeName}(),");
}

sb.AppendLine($" SerializeAction = static (writer, value, options) => new {converterName}().Write(writer, value, options),");
sb.AppendLine($" DeserializeFunc = static (ref Utf8YamlReader reader, YamlSerializerOptions options) => new {converterName}().Read(ref reader, options)");
// For custom converters, use the custom converter's methods (which may delegate to generated code)
if (hasCustomConverter)
{
sb.AppendLine($" SerializeAction = (writer, value, options) => converter.Write(writer, value, options),");
sb.AppendLine($" DeserializeFunc = (ref Utf8YamlReader reader, YamlSerializerOptions options) => converter.Read(ref reader, options)");
}
else
{
sb.AppendLine($" SerializeAction = static (writer, value, options) => new {converterName}().Write(writer, value, options),");
sb.AppendLine($" DeserializeFunc = static (ref Utf8YamlReader reader, YamlSerializerOptions options) => new {converterName}().Read(ref reader, options)");
}

sb.AppendLine(" };");
sb.AppendLine(" }");
sb.AppendLine();
Expand All @@ -496,6 +537,13 @@ private static void GenerateConverterClass(StringBuilder sb, TypeToGenerate type
var converterName = GetConverterName(type.Symbol);
var fullTypeName = type.Symbol.ToDisplayString();

// Check if this type has a custom converter - if so, generate static methods instead
if (type.CustomConverterType is not null)
{
GenerateStaticConverterMethods(sb, type, allTypes, compilation, propertyOrdering, discriminatorPosition, ignoreEmptyObjects);
return;
}

sb.AppendLine($" private sealed class {converterName} : YamlConverter<{fullTypeName}>");
sb.AppendLine(" {");

Expand All @@ -521,6 +569,66 @@ private static void GenerateConverterClass(StringBuilder sb, TypeToGenerate type
sb.AppendLine();
}

/// <summary>
/// Generates static ReadCore and WriteCore methods for types with custom converters.
/// These methods are wired up to the GeneratedRead/GeneratedWrite delegates on the custom converter.
/// We generate a full converter class internally and wrap calls to it via static methods.
/// </summary>
private static void GenerateStaticConverterMethods(StringBuilder sb, TypeToGenerate type, IReadOnlyList<TypeToGenerate> allTypes, Compilation compilation, PropertyOrderingMode propertyOrdering, DiscriminatorPositionMode discriminatorPosition, bool ignoreEmptyObjects)
{
var converterName = GetConverterName(type.Symbol);
var fullTypeName = type.Symbol.ToDisplayString();
var isValueType = type.Symbol.IsValueType;
var nullableAnnotation = isValueType ? "" : "?";

// Check if this is a polymorphic base type
var polyInfo = GetPolymorphicInfoForType(type);
var isPolymorphicBase = polyInfo is not null && polyInfo.DerivedTypes.Count > 0;

sb.AppendLine($" /// <summary>");
sb.AppendLine($" /// Generated converter for {type.Symbol.Name}. Since a custom converter exists ({type.CustomConverterType!.ToDisplayString()}),");
sb.AppendLine($" /// static ReadCore/WriteCore methods are provided for delegation from the custom converter.");
sb.AppendLine($" /// </summary>");
sb.AppendLine($" private sealed class {converterName} : YamlConverter<{fullTypeName}>");
sb.AppendLine(" {");

// Instance field for lazy initialization
sb.AppendLine($" private static {converterName}? _instance;");
sb.AppendLine($" private static {converterName} Instance => _instance ??= new {converterName}();");
sb.AppendLine();

// Generate static ReadCore method that delegates to instance Read
sb.AppendLine($" public static {fullTypeName}{nullableAnnotation} ReadCore(ref Utf8YamlReader reader, YamlSerializerOptions options)");
sb.AppendLine(" {");
sb.AppendLine(" return Instance.Read(ref reader, options);");
sb.AppendLine(" }");
sb.AppendLine();

// Generate static WriteCore method that delegates to instance Write
sb.AppendLine($" public static void WriteCore(Utf8YamlWriter writer, {fullTypeName} value, YamlSerializerOptions options)");
sb.AppendLine(" {");
sb.AppendLine(" Instance.Write(writer, value, options);");
sb.AppendLine(" }");
sb.AppendLine();

// Read method
GenerateReadMethod(sb, type, allTypes, compilation);
sb.AppendLine();

// Write method - use polymorphic dispatch for types with [YamlPolymorphic] and derived types
if (isPolymorphicBase)
{
GeneratePolymorphicWriteMethod(sb, type, polyInfo!, allTypes, compilation);
}
else
{
GenerateWriteMethod(sb, type, allTypes, compilation, propertyOrdering, discriminatorPosition, ignoreEmptyObjects);
}

sb.AppendLine(" }");
sb.AppendLine();
}

private static void GenerateReadMethod(StringBuilder sb, TypeToGenerate type, IReadOnlyList<TypeToGenerate> allTypes, Compilation compilation)
{
var typeName = type.Symbol.Name;
Expand Down Expand Up @@ -3634,11 +3742,23 @@ internal sealed class TypeToGenerate
/// </summary>
public PolymorphicInfo? PolymorphicConfig { get; }

public TypeToGenerate(INamedTypeSymbol symbol, PropertyOrderingMode? propertyOrdering = null, PolymorphicInfo? polymorphicConfig = null)
/// <summary>
/// The custom converter type specified via [YamlConverter] attribute on the type.
/// When set, the generator creates static ReadCore/WriteCore methods and wires up
/// the GeneratedRead/GeneratedWrite delegates on the custom converter.
/// </summary>
public INamedTypeSymbol? CustomConverterType { get; }

public TypeToGenerate(
INamedTypeSymbol symbol,
PropertyOrderingMode? propertyOrdering = null,
PolymorphicInfo? polymorphicConfig = null,
INamedTypeSymbol? customConverterType = null)
{
Symbol = symbol;
PropertyOrdering = propertyOrdering;
PolymorphicConfig = polymorphicConfig;
CustomConverterType = customConverterType;
}
}

Expand Down
13 changes: 13 additions & 0 deletions src/Yamlify/PublicAPI.Shipped.txt
Original file line number Diff line number Diff line change
Expand Up @@ -428,6 +428,10 @@ Yamlify.Serialization.YamlConstructorAttribute.YamlConstructorAttribute() -> voi
Yamlify.Serialization.YamlConverter
Yamlify.Serialization.YamlConverter.YamlConverter() -> void
Yamlify.Serialization.YamlConverter<T>
Yamlify.Serialization.YamlConverter<T>.GeneratedRead.get -> Yamlify.Serialization.YamlDeserializeFunc<T>?
Yamlify.Serialization.YamlConverter<T>.GeneratedRead.init -> void
Yamlify.Serialization.YamlConverter<T>.GeneratedWrite.get -> Yamlify.Serialization.YamlSerializeAction<T>?
Yamlify.Serialization.YamlConverter<T>.GeneratedWrite.init -> void
Yamlify.Serialization.YamlConverter<T>.YamlConverter() -> void
Yamlify.Serialization.YamlConverterAttribute
Yamlify.Serialization.YamlConverterAttribute.ConverterType.get -> System.Type!
Expand All @@ -438,6 +442,13 @@ Yamlify.Serialization.YamlDerivedTypeAttribute
Yamlify.Serialization.YamlDerivedTypeAttribute.DerivedType.get -> System.Type!
Yamlify.Serialization.YamlDerivedTypeAttribute.TypeDiscriminator.get -> string?
Yamlify.Serialization.YamlDerivedTypeAttribute.YamlDerivedTypeAttribute(System.Type! derivedType, string? typeDiscriminator = null) -> void
Yamlify.Serialization.YamlDerivedTypeAttribute<T>
Yamlify.Serialization.YamlDerivedTypeAttribute<T>.YamlDerivedTypeAttribute(string? typeDiscriminator = null) -> void
Yamlify.Serialization.YamlDerivedTypeMappingAttribute<TBase, TDerived>
Yamlify.Serialization.YamlDerivedTypeMappingAttribute<TBase, TDerived>.BaseType.get -> System.Type!
Yamlify.Serialization.YamlDerivedTypeMappingAttribute<TBase, TDerived>.DerivedType.get -> System.Type!
Yamlify.Serialization.YamlDerivedTypeMappingAttribute<TBase, TDerived>.TypeDiscriminator.get -> string?
Yamlify.Serialization.YamlDerivedTypeMappingAttribute<TBase, TDerived>.YamlDerivedTypeMappingAttribute(string? typeDiscriminator = null) -> void
Yamlify.Serialization.YamlDeserializeFunc<T>
Yamlify.Serialization.YamlDiscriminatorMappingAttribute
Yamlify.Serialization.YamlDiscriminatorMappingAttribute.ConcreteType.get -> System.Type!
Expand Down Expand Up @@ -495,6 +506,8 @@ Yamlify.Serialization.YamlSerializableAttribute.Type.get -> System.Type!
Yamlify.Serialization.YamlSerializableAttribute.TypeDiscriminatorPropertyName.get -> string?
Yamlify.Serialization.YamlSerializableAttribute.TypeDiscriminatorPropertyName.set -> void
Yamlify.Serialization.YamlSerializableAttribute.YamlSerializableAttribute(System.Type! type) -> void
Yamlify.Serialization.YamlSerializableAttribute<T>
Yamlify.Serialization.YamlSerializableAttribute<T>.YamlSerializableAttribute() -> void
Yamlify.Serialization.YamlSerializeAction<T>
Yamlify.Serialization.YamlSerializer
Yamlify.Serialization.YamlSerializerContext
Expand Down
11 changes: 1 addition & 10 deletions src/Yamlify/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
@@ -1,10 +1 @@
#nullable enable
Yamlify.Serialization.YamlDerivedTypeAttribute<T>
Yamlify.Serialization.YamlDerivedTypeAttribute<T>.YamlDerivedTypeAttribute(string? typeDiscriminator = null) -> void
Yamlify.Serialization.YamlDerivedTypeMappingAttribute<TBase, TDerived>
Yamlify.Serialization.YamlDerivedTypeMappingAttribute<TBase, TDerived>.BaseType.get -> System.Type!
Yamlify.Serialization.YamlDerivedTypeMappingAttribute<TBase, TDerived>.DerivedType.get -> System.Type!
Yamlify.Serialization.YamlDerivedTypeMappingAttribute<TBase, TDerived>.TypeDiscriminator.get -> string?
Yamlify.Serialization.YamlDerivedTypeMappingAttribute<TBase, TDerived>.YamlDerivedTypeMappingAttribute(string? typeDiscriminator = null) -> void
Yamlify.Serialization.YamlSerializableAttribute<T>
Yamlify.Serialization.YamlSerializableAttribute<T>.YamlSerializableAttribute() -> void
#nullable enable
43 changes: 43 additions & 0 deletions src/Yamlify/Serialization/YamlConverter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,49 @@ public abstract class YamlConverter<T> : YamlConverter
/// <param name="value">The value to convert.</param>
/// <param name="options">The serializer options.</param>
public abstract void Write(Utf8YamlWriter writer, T value, YamlSerializerOptions options);

/// <summary>
/// Gets or sets a delegate to the source-generated deserialization logic.
/// This allows custom converters to delegate to the generated code for standard deserialization
/// while handling special cases (e.g., legacy format migration) themselves.
/// </summary>
/// <remarks>
/// This property is automatically set by the source generator when a custom converter is registered
/// via <see cref="YamlConverterAttribute"/>. It enables patterns like:
/// <code>
/// public override T? Read(ref Utf8YamlReader reader, YamlSerializerOptions options)
/// {
/// // Handle legacy format
/// if (reader.TokenType == YamlTokenType.Boolean)
/// return new MyType(reader.GetBoolean());
///
/// // Delegate to generated code for standard format
/// return GeneratedRead!(ref reader, options);
/// }
/// </code>
/// </remarks>
public YamlDeserializeFunc<T>? GeneratedRead { get; init; }

/// <summary>
/// Gets or sets a delegate to the source-generated serialization logic.
/// This allows custom converters to delegate to the generated code for standard serialization
/// while performing custom pre/post processing.
/// </summary>
/// <remarks>
/// This property is automatically set by the source generator when a custom converter is registered
/// via <see cref="YamlConverterAttribute"/>. It enables patterns like:
/// <code>
/// public override void Write(Utf8YamlWriter writer, T value, YamlSerializerOptions options)
/// {
/// // Pre-processing
/// LogWrite(value);
///
/// // Delegate to generated code
/// GeneratedWrite!(writer, value, options);
/// }
/// </code>
/// </remarks>
public YamlSerializeAction<T>? GeneratedWrite { get; init; }
}

/// <summary>
Expand Down
Loading