Skip to content

Commit ee6cb37

Browse files
authored
feat: add generic support for YamlSerializable, YamlDerivedType and YamlDerivedTypeMapping (#3)
1 parent 45abe2a commit ee6cb37

File tree

8 files changed

+445
-51
lines changed

8 files changed

+445
-51
lines changed

src/Yamlify.SourceGenerator/YamlSourceGenerator.cs

Lines changed: 121 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ namespace Yamlify.SourceGenerator;
2222
public sealed class YamlSourceGenerator : IIncrementalGenerator
2323
{
2424
private const string YamlSerializableAttribute = "Yamlify.Serialization.YamlSerializableAttribute";
25+
private const string YamlSerializableAttributeGeneric = "Yamlify.Serialization.YamlSerializableAttribute<T>";
26+
private const string YamlDerivedTypeMappingAttributeGeneric = "Yamlify.Serialization.YamlDerivedTypeMappingAttribute<TBase, TDerived>";
2527
private const string YamlSerializerContextBase = "Yamlify.Serialization.YamlSerializerContext";
2628

2729
public void Initialize(IncrementalGeneratorInitializationContext context)
@@ -86,66 +88,137 @@ private static bool IsCandidateClass(SyntaxNode node)
8688
var ignoreEmptyObjects = false;
8789
var discriminatorPosition = DiscriminatorPositionMode.PropertyOrder;
8890

91+
// First pass: collect YamlDerivedTypeMapping attributes
92+
// Key: base type display string, Value: list of (discriminator, derivedType)
93+
var derivedTypeMappingsFromAttrs = new Dictionary<string, List<(string Discriminator, INamedTypeSymbol DerivedType)>>();
94+
95+
foreach (var attributeData in classSymbol.GetAttributes())
96+
{
97+
var attrOriginalDef = attributeData.AttributeClass?.OriginalDefinition?.ToDisplayString();
98+
99+
if (attrOriginalDef == YamlDerivedTypeMappingAttributeGeneric)
100+
{
101+
// [YamlDerivedTypeMapping<TBase, TDerived>("discriminator")]
102+
if (attributeData.AttributeClass is { IsGenericType: true, TypeArguments.Length: 2 } attrClass &&
103+
attrClass.TypeArguments[0] is INamedTypeSymbol mappingBaseType &&
104+
attrClass.TypeArguments[1] is INamedTypeSymbol mappingDerivedType)
105+
{
106+
var baseTypeKey = mappingBaseType.ToDisplayString();
107+
108+
// Get discriminator from constructor argument (optional)
109+
string? discriminator = null;
110+
if (attributeData.ConstructorArguments.Length > 0 &&
111+
attributeData.ConstructorArguments[0].Value is string discValue)
112+
{
113+
discriminator = discValue;
114+
}
115+
discriminator ??= mappingDerivedType.Name;
116+
117+
if (!derivedTypeMappingsFromAttrs.TryGetValue(baseTypeKey, out var mappings))
118+
{
119+
mappings = new List<(string, INamedTypeSymbol)>();
120+
derivedTypeMappingsFromAttrs[baseTypeKey] = mappings;
121+
}
122+
mappings.Add((discriminator, mappingDerivedType));
123+
}
124+
}
125+
}
126+
127+
// Second pass: process YamlSerializable attributes
89128
foreach (var attributeData in classSymbol.GetAttributes())
90129
{
91130
var attrName = attributeData.AttributeClass?.ToDisplayString();
131+
var attrOriginalDef = attributeData.AttributeClass?.OriginalDefinition?.ToDisplayString();
132+
133+
// Support both [YamlSerializable(typeof(T))] and [YamlSerializable<T>]
134+
INamedTypeSymbol? typeArg = null;
135+
var isYamlSerializableAttribute = false;
136+
92137
if (attrName == YamlSerializableAttribute)
93138
{
139+
// Non-generic: [YamlSerializable(typeof(T))]
94140
if (attributeData.ConstructorArguments.Length > 0 &&
95-
attributeData.ConstructorArguments[0].Value is INamedTypeSymbol typeArg)
141+
attributeData.ConstructorArguments[0].Value is INamedTypeSymbol ctorArg)
96142
{
97-
// Check for per-type PropertyOrdering override
98-
PropertyOrderingMode? typeOrdering = null;
99-
string? typeDiscriminatorPropertyName = null;
100-
List<INamedTypeSymbol>? derivedTypes = null;
101-
List<string>? derivedTypeDiscriminators = null;
102-
103-
foreach (var namedArg in attributeData.NamedArguments)
143+
typeArg = ctorArg;
144+
isYamlSerializableAttribute = true;
145+
}
146+
}
147+
else if (attrOriginalDef == YamlSerializableAttributeGeneric)
148+
{
149+
// Generic: [YamlSerializable<T>]
150+
if (attributeData.AttributeClass is { IsGenericType: true, TypeArguments.Length: > 0 } attrClass &&
151+
attrClass.TypeArguments[0] is INamedTypeSymbol genericArg)
152+
{
153+
typeArg = genericArg;
154+
isYamlSerializableAttribute = true;
155+
}
156+
}
157+
158+
if (isYamlSerializableAttribute && typeArg is not null)
159+
{
160+
// Check for per-type PropertyOrdering override
161+
PropertyOrderingMode? typeOrdering = null;
162+
string? typeDiscriminatorPropertyName = null;
163+
List<INamedTypeSymbol>? derivedTypes = null;
164+
List<string>? derivedTypeDiscriminators = null;
165+
166+
foreach (var namedArg in attributeData.NamedArguments)
167+
{
168+
if (namedArg.Key == "PropertyOrdering" && namedArg.Value.Value is int orderingValue && orderingValue >= 0)
104169
{
105-
if (namedArg.Key == "PropertyOrdering" && namedArg.Value.Value is int orderingValue && orderingValue >= 0)
106-
{
107-
// Only set if not Inherit (-1)
108-
typeOrdering = (PropertyOrderingMode)orderingValue;
109-
}
110-
else if (namedArg.Key == "TypeDiscriminatorPropertyName" && namedArg.Value.Value is string discPropName)
111-
{
112-
typeDiscriminatorPropertyName = discPropName;
113-
}
114-
else if (namedArg.Key == "DerivedTypes" && !namedArg.Value.IsNull)
115-
{
116-
derivedTypes = namedArg.Value.Values
117-
.Where(v => v.Value is INamedTypeSymbol)
118-
.Select(v => (INamedTypeSymbol)v.Value!)
119-
.ToList();
120-
}
121-
else if (namedArg.Key == "DerivedTypeDiscriminators" && !namedArg.Value.IsNull)
122-
{
123-
derivedTypeDiscriminators = namedArg.Value.Values
124-
.Where(v => v.Value is string)
125-
.Select(v => (string)v.Value!)
126-
.ToList();
127-
}
170+
// Only set if not Inherit (-1)
171+
typeOrdering = (PropertyOrderingMode)orderingValue;
128172
}
129-
130-
// Build PolymorphicInfo if polymorphic configuration is specified
131-
PolymorphicInfo? polymorphicConfig = null;
132-
if (typeDiscriminatorPropertyName is not null && derivedTypes is not null && derivedTypes.Count > 0)
173+
else if (namedArg.Key == "TypeDiscriminatorPropertyName" && namedArg.Value.Value is string discPropName)
133174
{
134-
var derivedTypeMappings = new List<(string Discriminator, INamedTypeSymbol DerivedType)>();
135-
for (int i = 0; i < derivedTypes.Count; i++)
136-
{
137-
var derivedType = derivedTypes[i];
138-
// Use explicit discriminator if provided, otherwise use type name
139-
var discriminator = (derivedTypeDiscriminators is not null && i < derivedTypeDiscriminators.Count)
140-
? derivedTypeDiscriminators[i]
141-
: derivedType.Name;
142-
derivedTypeMappings.Add((discriminator, derivedType));
143-
}
144-
polymorphicConfig = new PolymorphicInfo(typeDiscriminatorPropertyName, derivedTypeMappings);
175+
typeDiscriminatorPropertyName = discPropName;
145176
}
146-
147-
typesToGenerate.Add(new TypeToGenerate(typeArg, typeOrdering, polymorphicConfig));
177+
else if (namedArg.Key == "DerivedTypes" && !namedArg.Value.IsNull)
178+
{
179+
derivedTypes = namedArg.Value.Values
180+
.Where(v => v.Value is INamedTypeSymbol)
181+
.Select(v => (INamedTypeSymbol)v.Value!)
182+
.ToList();
183+
}
184+
else if (namedArg.Key == "DerivedTypeDiscriminators" && !namedArg.Value.IsNull)
185+
{
186+
derivedTypeDiscriminators = namedArg.Value.Values
187+
.Where(v => v.Value is string)
188+
.Select(v => (string)v.Value!)
189+
.ToList();
190+
}
191+
}
192+
193+
// Build PolymorphicInfo if polymorphic configuration is specified
194+
PolymorphicInfo? polymorphicConfig = null;
195+
var typeKey = typeArg.ToDisplayString();
196+
197+
// Check for derived type mappings from YamlDerivedTypeMappingAttribute
198+
if (typeDiscriminatorPropertyName is not null &&
199+
derivedTypeMappingsFromAttrs.TryGetValue(typeKey, out var mappingsFromAttr) &&
200+
mappingsFromAttr.Count > 0)
201+
{
202+
// Use mappings from YamlDerivedTypeMappingAttribute
203+
polymorphicConfig = new PolymorphicInfo(typeDiscriminatorPropertyName, mappingsFromAttr);
148204
}
205+
else if (typeDiscriminatorPropertyName is not null && derivedTypes is not null && derivedTypes.Count > 0)
206+
{
207+
// Use inline DerivedTypes/DerivedTypeDiscriminators arrays
208+
var derivedTypeMappings = new List<(string Discriminator, INamedTypeSymbol DerivedType)>();
209+
for (int i = 0; i < derivedTypes.Count; i++)
210+
{
211+
var derivedType = derivedTypes[i];
212+
// Use explicit discriminator if provided, otherwise use type name
213+
var discriminator = (derivedTypeDiscriminators is not null && i < derivedTypeDiscriminators.Count)
214+
? derivedTypeDiscriminators[i]
215+
: derivedType.Name;
216+
derivedTypeMappings.Add((discriminator, derivedType));
217+
}
218+
polymorphicConfig = new PolymorphicInfo(typeDiscriminatorPropertyName, derivedTypeMappings);
219+
}
220+
221+
typesToGenerate.Add(new TypeToGenerate(typeArg, typeOrdering, polymorphicConfig));
149222
}
150223
else if (attrName == "Yamlify.Serialization.YamlSourceGenerationOptionsAttribute")
151224
{
Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,10 @@
1-
#nullable enable
1+
#nullable enable
2+
Yamlify.Serialization.YamlDerivedTypeAttribute<T>
3+
Yamlify.Serialization.YamlDerivedTypeAttribute<T>.YamlDerivedTypeAttribute(string? typeDiscriminator = null) -> void
4+
Yamlify.Serialization.YamlDerivedTypeMappingAttribute<TBase, TDerived>
5+
Yamlify.Serialization.YamlDerivedTypeMappingAttribute<TBase, TDerived>.BaseType.get -> System.Type!
6+
Yamlify.Serialization.YamlDerivedTypeMappingAttribute<TBase, TDerived>.DerivedType.get -> System.Type!
7+
Yamlify.Serialization.YamlDerivedTypeMappingAttribute<TBase, TDerived>.TypeDiscriminator.get -> string?
8+
Yamlify.Serialization.YamlDerivedTypeMappingAttribute<TBase, TDerived>.YamlDerivedTypeMappingAttribute(string? typeDiscriminator = null) -> void
9+
Yamlify.Serialization.YamlSerializableAttribute<T>
10+
Yamlify.Serialization.YamlSerializableAttribute<T>.YamlSerializableAttribute() -> void

src/Yamlify/Serialization/YamlDerivedTypeAttribute.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ namespace Yamlify.Serialization;
44
/// Specifies polymorphic type information.
55
/// </summary>
66
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Interface, AllowMultiple = true)]
7-
public sealed class YamlDerivedTypeAttribute : Attribute
7+
public class YamlDerivedTypeAttribute : Attribute
88
{
99
/// <summary>
1010
/// Gets the derived type.
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
namespace Yamlify.Serialization;
2+
3+
/// <summary>
4+
/// Generic version of <see cref="YamlDerivedTypeAttribute"/> for a more type-safe API.
5+
/// </summary>
6+
/// <typeparam name="T">The derived type.</typeparam>
7+
/// <example>
8+
/// <code>
9+
/// [YamlPolymorphic(TypeDiscriminatorPropertyName = "type")]
10+
/// [YamlDerivedType&lt;Dog&gt;("dog")]
11+
/// [YamlDerivedType&lt;Cat&gt;("cat")]
12+
/// public abstract class Animal { }
13+
/// </code>
14+
/// </example>
15+
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Interface, AllowMultiple = true)]
16+
public sealed class YamlDerivedTypeAttribute<T> : YamlDerivedTypeAttribute
17+
{
18+
/// <summary>
19+
/// Initializes a new instance of the <see cref="YamlDerivedTypeAttribute{T}"/> class.
20+
/// </summary>
21+
/// <param name="typeDiscriminator">The type discriminator value. If null, the type name is used.</param>
22+
public YamlDerivedTypeAttribute(string? typeDiscriminator = null) : base(typeof(T), typeDiscriminator)
23+
{
24+
}
25+
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
namespace Yamlify.Serialization;
2+
3+
/// <summary>
4+
/// Specifies a polymorphic type mapping between a base type and a derived type for the serializer context.
5+
/// This attribute provides a type-safe way to declare derived type mappings on the context class.
6+
/// </summary>
7+
/// <typeparam name="TBase">The base type or interface for the polymorphic hierarchy.</typeparam>
8+
/// <typeparam name="TDerived">The derived type that implements or extends the base type.</typeparam>
9+
/// <remarks>
10+
/// <para>
11+
/// Use this attribute together with <see cref="YamlSerializableAttribute{T}"/> to configure
12+
/// polymorphic serialization directly on the context class.
13+
/// </para>
14+
/// <para>
15+
/// When using this attribute, you must also register the base type with
16+
/// <see cref="YamlSerializableAttribute{T}"/> and set its <see cref="YamlSerializableAttribute.TypeDiscriminatorPropertyName"/>.
17+
/// </para>
18+
/// </remarks>
19+
/// <example>
20+
/// <code>
21+
/// [YamlSerializable&lt;IAnimal&gt;(TypeDiscriminatorPropertyName = "type")]
22+
/// [YamlDerivedTypeMapping&lt;IAnimal, Dog&gt;("dog")]
23+
/// [YamlDerivedTypeMapping&lt;IAnimal, Cat&gt;("cat")]
24+
/// [YamlSerializable&lt;Dog&gt;]
25+
/// [YamlSerializable&lt;Cat&gt;]
26+
/// public partial class MyContext : YamlSerializerContext { }
27+
/// </code>
28+
/// </example>
29+
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
30+
public sealed class YamlDerivedTypeMappingAttribute<TBase, TDerived> : Attribute
31+
where TDerived : TBase
32+
{
33+
/// <summary>
34+
/// Gets the base type for the polymorphic hierarchy.
35+
/// </summary>
36+
public Type BaseType => typeof(TBase);
37+
38+
/// <summary>
39+
/// Gets the derived type.
40+
/// </summary>
41+
public Type DerivedType => typeof(TDerived);
42+
43+
/// <summary>
44+
/// Gets the type discriminator value used in YAML to identify this derived type.
45+
/// </summary>
46+
public string? TypeDiscriminator { get; }
47+
48+
/// <summary>
49+
/// Initializes a new instance of the <see cref="YamlDerivedTypeMappingAttribute{TBase, TDerived}"/> class.
50+
/// </summary>
51+
/// <param name="typeDiscriminator">
52+
/// The type discriminator value. If null, the type name of <typeparamref name="TDerived"/> is used.
53+
/// </param>
54+
public YamlDerivedTypeMappingAttribute(string? typeDiscriminator = null)
55+
{
56+
TypeDiscriminator = typeDiscriminator;
57+
}
58+
}

src/Yamlify/Serialization/YamlSerializableAttribute.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ namespace Yamlify.Serialization;
3131
/// </code>
3232
/// </example>
3333
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
34-
public sealed class YamlSerializableAttribute : Attribute
34+
public class YamlSerializableAttribute : Attribute
3535
{
3636
/// <summary>
3737
/// Gets the type for which to generate serialization metadata.
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
namespace Yamlify.Serialization;
2+
3+
/// <summary>
4+
/// Generic version of <see cref="YamlSerializableAttribute"/> for a more type-safe API.
5+
/// </summary>
6+
/// <typeparam name="T">The type for which to generate serialization metadata.</typeparam>
7+
/// <example>
8+
/// <code>
9+
/// [YamlSerializable&lt;Person&gt;]
10+
/// [YamlSerializable&lt;Address&gt;]
11+
/// [YamlSerializable&lt;IAnimal&gt;(
12+
/// TypeDiscriminatorPropertyName = "type",
13+
/// DerivedTypes = new[] { typeof(Dog), typeof(Cat) },
14+
/// DerivedTypeDiscriminators = new[] { "dog", "cat" })]
15+
/// public partial class MyContext : YamlSerializerContext { }
16+
/// </code>
17+
/// </example>
18+
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
19+
public sealed class YamlSerializableAttribute<T> : YamlSerializableAttribute
20+
{
21+
/// <summary>
22+
/// Initializes a new instance of the <see cref="YamlSerializableAttribute{T}"/> class.
23+
/// </summary>
24+
public YamlSerializableAttribute() : base(typeof(T))
25+
{
26+
}
27+
}

0 commit comments

Comments
 (0)