Skip to content

Commit cf7cd44

Browse files
committed
Added support for [Faker<T>] attribute
- That way, there's now a way to generate a faker that doesn't pollute the model itself
1 parent 79130fe commit cf7cd44

10 files changed

Lines changed: 134 additions & 38 deletions

File tree

.scripts/Bump.cs

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -52,13 +52,29 @@
5252
: version.Release;
5353
}
5454

55-
var newVersion = new SemanticVersion(
56-
version.Major + (part == "Major" ? 1 : 0),
57-
version.Minor + (part == "Minor" ? 1 : 0),
58-
version.Patch + (part == "Patch" ? 1 : 0),
59-
tag,
60-
null
61-
);
55+
var major = version.Major;
56+
var minor = version.Minor;
57+
var patch = version.Patch;
58+
59+
switch (part)
60+
{
61+
case "Major":
62+
major++;
63+
minor = 0;
64+
patch = 0;
65+
break;
66+
case "Minor":
67+
minor++;
68+
patch = 0;
69+
break;
70+
case "Patch":
71+
patch++;
72+
break;
73+
case "Build":
74+
break;
75+
}
76+
77+
var newVersion = new SemanticVersion(major, minor, patch, tag, null);
6278

6379
AnsiConsole.MarkupLine($"[green]Bumping to: [bold]{newVersion}[/][/]");
6480

Forged.Core/FakeAttribute.cs

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,4 @@ namespace Forged.Core;
44
/// Marker attribute for classes or structs that represent fake data models.
55
/// </summary>
66
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct)]
7-
public sealed class FakeAttribute : Attribute
8-
{
9-
}
7+
public sealed class FakeAttribute : Attribute;

Forged.Core/FakerAttribute.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
namespace Forged.Core;
2+
3+
/// <summary>
4+
/// Marker attribute for classes or structs that represent fakers.
5+
/// </summary>
6+
[AttributeUsage(AttributeTargets.Class)]
7+
public sealed class FakerAttribute<TModel> : Attribute;

Forged.Demo/Models.cs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,14 @@ public class Person
1616
public DateTime? DateOfBirth { get; set; }
1717
public string? Bio { get; set; }
1818
public required int Luck { get; init; }
19-
}
19+
}
20+
21+
public class Address
22+
{
23+
public required string StreetName { get; init; }
24+
public required string StreetNumber { get; init; }
25+
public required string City { get; init; }
26+
}
27+
28+
[Faker<Address>]
29+
public partial class AddressGenerator;

Forged.Generator/Forged.Generator.csproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,12 @@
1111

1212
<PropertyGroup>
1313
<MeziantouPolyfill_IncludedPolyfills>
14+
T:System.Runtime.CompilerServices.CompilerRequiredAttribute;
15+
T:System.Diagnostics.CodeAnalysis.DoesNotReturnAttribute;
1416
T:System.Diagnostics.CodeAnalysis.NotNullWhenAttribute;
1517
T:System.Runtime.CompilerServices.IsExternalInit;
18+
T:System.Index;
19+
T:System.Range;
1620
</MeziantouPolyfill_IncludedPolyfills>
1721
</PropertyGroup>
1822

Forged.Generator/ForgedGenerator.cs

Lines changed: 66 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -32,18 +32,39 @@ public class ForgedGenerator : IIncrementalGenerator
3232

3333
public void Initialize(IncrementalGeneratorInitializationContext context)
3434
{
35-
var typesToGenerate = context.SyntaxProvider
35+
var typesToFake = context.SyntaxProvider
3636
.ForAttributeWithMetadataName(
3737
"Forged.Core.FakeAttribute",
3838
predicate: static (s, _) => s is ClassDeclarationSyntax or RecordDeclarationSyntax or StructDeclarationSyntax,
39-
transform: static (ctx, _) => GetSemanticTargetForGeneration(ctx.SemanticModel, ctx.TargetNode)
39+
transform: static (ctx, _) => GetFakedTypes(ctx.SemanticModel, ctx.TargetNode)
4040
)
41-
.Where(static m => m is not null);
41+
.Where(static m => m is not null)
42+
.Select(static (m, _) => m!)
43+
.Collect();
4244

43-
context.RegisterSourceOutput(typesToGenerate, static (spc, source) => Execute(spc, source));
45+
var fakersToGenerate = context.SyntaxProvider
46+
.ForAttributeWithMetadataName(
47+
"Forged.Core.FakerAttribute`1",
48+
predicate: static (s, _) => s is ClassDeclarationSyntax,
49+
transform: static (ctx, _) => GetFakers(ctx.SemanticModel, ctx.TargetNode)
50+
)
51+
.Where(static m => m is not null)
52+
.Select(static (m, _) => m!)
53+
.Collect();
54+
55+
var combined = typesToFake
56+
.Combine(fakersToGenerate);
57+
58+
context.RegisterSourceOutput(combined, static (spc, source) => {
59+
var (types, fakers) = source;
60+
foreach (var ttg in types.Concat(fakers))
61+
{
62+
Execute(spc, ttg);
63+
}
64+
});
4465
}
4566

46-
private static TypeToGenerate? GetSemanticTargetForGeneration(SemanticModel semanticModel, SyntaxNode node)
67+
private static TypeToGenerate? GetFakedTypes(SemanticModel semanticModel, SyntaxNode node)
4768
{
4869
if (semanticModel.GetDeclaredSymbol(node) is not INamedTypeSymbol symbol)
4970
{
@@ -66,16 +87,46 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
6687
Properties: properties.ToEquatableReadOnlyList()
6788
);
6889
}
69-
70-
private static void Execute(SourceProductionContext context, TypeToGenerate? typeToGenerate)
90+
91+
private static TypeToGenerate? GetFakers(SemanticModel semanticModel, SyntaxNode node)
7192
{
72-
if (typeToGenerate is null) return;
93+
if (semanticModel.GetDeclaredSymbol(node) is not INamedTypeSymbol symbol)
94+
{
95+
return null;
96+
}
97+
98+
var attribute = symbol.GetAttributes().FirstOrDefault(a => a.AttributeClass?.Name == "FakerAttribute");
99+
if (attribute is not { AttributeClass: { IsGenericType: true, TypeArguments: [ INamedTypeSymbol targetType ] } })
100+
{
101+
return null;
102+
}
103+
104+
var properties = targetType.GetMembers()
105+
.OfType<IPropertySymbol>()
106+
.Where(p => p.SetMethod is not null && p.DeclaredAccessibility == Accessibility.Public)
107+
.Select(p => new PropertyToGenerate(
108+
Name: p.Name,
109+
Type: p.Type.ToDisplayString(),
110+
IsRequired: p.IsRequired
111+
))
112+
.ToList();
113+
114+
return new TypeToGenerate(
115+
Namespace: targetType.ContainingNamespace.IsGlobalNamespace ? string.Empty : targetType.ContainingNamespace.ToDisplayString(),
116+
Name: targetType.Name,
117+
Properties: properties.ToEquatableReadOnlyList(),
118+
FakerName: symbol.Name
119+
);
120+
}
73121

122+
private static void Execute(SourceProductionContext context, TypeToGenerate typeToGenerate)
123+
{
74124
var w = new IndentedWriter();
75125
w.WriteLine(Header);
76126
w.WriteLine("#nullable enable");
77-
78-
127+
128+
var fakerName = typeToGenerate.FakerName ?? $"{typeToGenerate.Name}Faker";
129+
79130
if (!string.IsNullOrEmpty(typeToGenerate.Namespace))
80131
{
81132
w.WriteLine();
@@ -85,7 +136,7 @@ private static void Execute(SourceProductionContext context, TypeToGenerate? typ
85136
w.WriteLine();
86137
w.WriteLine(Attributes);
87138

88-
w.WriteLine($"public partial class {typeToGenerate.Name}Faker : global::Forged.Core.Faker<{typeToGenerate.Name}>");
139+
w.WriteLine($"public partial class {fakerName} : global::Forged.Core.Faker<{typeToGenerate.Name}>");
89140
w.BodyBlock(() => {
90141

91142
foreach (var prop in typeToGenerate.Properties)
@@ -105,7 +156,7 @@ private static void Execute(SourceProductionContext context, TypeToGenerate? typ
105156

106157
w.WriteLine();
107158
w.WriteLine("// Constructor");
108-
w.WriteLine($"public {typeToGenerate.Name}Faker(global::System.Random? random = null, global::System.Globalization.CultureInfo? locale = null) : base(random, locale) {{ }}");
159+
w.WriteLine($"public {fakerName}(global::System.Random? random = null, global::System.Globalization.CultureInfo? locale = null) : base(random, locale) {{ }}");
109160

110161
w.WriteLine();
111162
w.WriteLine($"public override {typeToGenerate.Name} Get()");
@@ -132,16 +183,16 @@ private static void Execute(SourceProductionContext context, TypeToGenerate? typ
132183
});
133184
});
134185
});
135-
136-
context.AddSource($"{typeToGenerate.Name}Faker.g.cs", SourceText.From(w.ToString(), Encoding.UTF8));
186+
187+
context.AddSource($"{fakerName}.g.cs", SourceText.From(w.ToString(), Encoding.UTF8));
137188
}
138189

139190
private static string Decapitalize(string input)
140191
{
141192
var span = input.AsSpan();
142193
Span<char> result = stackalloc char[span.Length];
143194
result[0] = char.ToLowerInvariant(span[0]);
144-
span.Slice(1).CopyTo(result.Slice(1));
195+
span[1..].CopyTo(result[1..]);
145196
return result.ToString();
146197
}
147198
}

Forged.Generator/EquatableReadOnlyList.cs renamed to Forged.Generator/Helpers/EquatableReadOnlyList.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
namespace Forged.Generator;
2-
3-
using System.Collections;
1+
using System.Collections;
42
using System.Diagnostics.CodeAnalysis;
53

4+
namespace Forged.Generator.Helpers;
5+
66
[ExcludeFromCodeCoverage]
77
public static class EquatableReadOnlyList
88
{
Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
1-
namespace Forged.Generator.Models;
1+
using Forged.Generator.Helpers;
2+
3+
namespace Forged.Generator.Models;
24

35
public sealed record TypeToGenerate(
46
string Namespace,
5-
string Name,
6-
EquatableReadOnlyList<PropertyToGenerate> Properties
7+
string Name,
8+
EquatableReadOnlyList<PropertyToGenerate> Properties,
9+
string? FakerName = null
710
);

Forged/Forged.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
<Nullable>enable</Nullable>
77

88
<PackageId>Atulin.Forged</PackageId>
9-
<PackageVersion>0.4.0</PackageVersion>
9+
<PackageVersion>1.0.0</PackageVersion>
1010
<Title>Atulin.Forged</Title>
1111
<Description>A fast, strict, and strongly-typed data generator (faker) for C# powered by Source Generators.</Description>
1212
<Authors>Atulin</Authors>

README.md

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,6 @@ Forged allows you to declaratively define how your models should be faked, lever
2121
2. Decorate your model with `[Fake]`:
2222

2323
```csharp
24-
using Forged.Core;
25-
26-
namespace MyProject;
27-
2824
[Fake]
2925
public class Person
3026
{
@@ -37,6 +33,13 @@ public class Person
3733
}
3834
```
3935

36+
or create a partial class with `[Faker<T>]` attribute:
37+
38+
```csharp
39+
[Faker<Person>]
40+
public partial class PersonFaker;
41+
```
42+
4043
3. The source generator will automatically create a `{ModelName}Faker` class for you. Configure it and generate data!
4144

4245
```csharp
@@ -68,9 +71,13 @@ var person = faker.Get();
6871
var people = faker.Get(5);
6972
```
7073

74+
> [!INFO]
75+
> When using the `Faker<T>` attribute, the generated faker will have the same name,
76+
> not `{ModelName}Faker`.
77+
7178
### Deterministic Generation
7279

73-
If you need reproducible results (e.g., in unit tests), you can provide a seeded `Random` instance to the faker:
80+
If you need reproducible results (e.g. in unit tests), you can provide a seeded `Random` instance to the faker:
7481

7582
```csharp
7683
var faker = new PersonFaker(new Random(12345))

0 commit comments

Comments
 (0)