Skip to content

Commit 5728a85

Browse files
Name templates
1 parent 3744a92 commit 5728a85

File tree

10 files changed

+443
-51
lines changed

10 files changed

+443
-51
lines changed

readme/builders.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
[![CSharp](https://img.shields.io/badge/C%23-code-blue.svg)](../tests/Pure.DI.UsageTests/Basics/BuildersScenario.cs)
44

5-
Sometimes you need to build up an existing composition root and inject all of its dependencies, in which case the `Builder` method will be useful, as in the example below:
5+
Sometimes you need builders for all types inherited from <see cref=“T”/> available at compile time at the point where the method is called.
66

77

88
```c#

readme/roots.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
[![CSharp](https://img.shields.io/badge/C%23-code-blue.svg)](../tests/Pure.DI.UsageTests/Basics/RootsScenario.cs)
44

5+
Sometimes you need roots for all types inherited from <see cref="T"/> available at compile time at the point where the method is called.
6+
57

68
```c#
79
using Shouldly;

src/Pure.DI.Core/Components/Api.g.cs

Lines changed: 198 additions & 25 deletions
Large diffs are not rendered by default.

src/Pure.DI.Core/Core/ApiInvocationProcessor.cs

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -674,9 +674,15 @@ private void VisitArg(
674674
if (genericName.TypeArgumentList.Arguments is [{ } argTypeSyntax]
675675
&& invocation.ArgumentList.Arguments is [{ Expression: { } nameArgExpression }, ..] args)
676676
{
677-
var name = semantic.GetRequiredConstantValue<string>(semanticModel, nameArgExpression).Trim();
677+
var argType = semantic.GetTypeSymbol<INamedTypeSymbol>(semanticModel, argTypeSyntax);
678678
var tags = BuildTags(semanticModel, args.Skip(1));
679-
var argType = semantic.GetTypeSymbol<ITypeSymbol>(semanticModel, argTypeSyntax);
679+
var name = GetName(
680+
nameArgExpression,
681+
semanticModel,
682+
semantic.GetRequiredConstantValue<string>(semanticModel, nameArgExpression),
683+
argType,
684+
tags.IsEmpty ? null : tags[0].Value) ?? "";
685+
680686
metadataVisitor.VisitContract(new MdContract(semanticModel, invocation, argType, ContractKind.Explicit, tags.ToImmutableArray()));
681687
metadataVisitor.VisitArg(new MdArg(semanticModel, argTypeSyntax, argType, name, kind, false, argComments));
682688
}
@@ -935,12 +941,7 @@ private static CompositionName CreateCompositionName(
935941
return nameTemplate;
936942
}
937943

938-
var name = nameFormatter.Format(semanticModel, nameTemplate, type, tag);
939-
if (string.IsNullOrWhiteSpace(name))
940-
{
941-
return name;
942-
}
943-
944+
var name = nameFormatter.Format(semanticModel, nameTemplate!, type, tag);
944945
if (!SyntaxFacts.IsValidIdentifier(name))
945946
{
946947
throw new CompileErrorException($"Invalid identifier \"{name}\".", source.GetLocation(), LogId.ErrorInvalidMetadata);

src/Pure.DI.Core/Core/INameFormatter.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@
22

33
internal interface INameFormatter
44
{
5-
string? Format(
5+
string Format(
66
SemanticModel semanticModel,
7-
string? nameTemplate,
7+
string nameTemplate,
88
INamedTypeSymbol? type,
99
object? tag);
1010
}

src/Pure.DI.Core/Core/NameFormatter.cs

Lines changed: 52 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -16,34 +16,72 @@ internal class NameFormatter : INameFormatter
1616
miscellaneousOptions: SymbolDisplayMiscellaneousOptions.UseSpecialTypes | SymbolDisplayMiscellaneousOptions.CollapseTupleTypes | SymbolDisplayMiscellaneousOptions.RemoveAttributeSuffix
1717
);
1818

19-
public string? Format(
19+
public string Format(
2020
SemanticModel semanticModel,
21-
string? nameTemplate,
21+
string nameTemplate,
2222
INamedTypeSymbol? type,
2323
object? tag)
2424
{
25-
if (string.IsNullOrWhiteSpace(nameTemplate))
25+
if (SyntaxFacts.IsValidIdentifier(nameTemplate))
2626
{
2727
return nameTemplate;
2828
}
2929

30-
if (!nameTemplate.Contains('{'))
31-
{
32-
return nameTemplate;
33-
}
30+
var name = nameTemplate!
31+
.Replace("{type}", type is not null ? type.ToDisplayString(NullableFlowState.NotNull, TypeFormat) : "")
32+
.Replace("{TYPE}", type is not null ? type.ToDisplayString(NullableFlowState.NotNull, FullTypeFormat) : "")
33+
.Replace("{tag}", tag is not null ?tag.ToString() : "");
3434

35-
var name = nameTemplate!.Trim();
36-
if (type is not null)
35+
return ToValidIdentifier(name);
36+
}
37+
38+
internal static string ToValidIdentifier(string text)
39+
{
40+
if (SyntaxFacts.IsValidIdentifier(text))
3741
{
38-
name = name.Replace("{type}", type.ToDisplayString(NullableFlowState.NotNull, TypeFormat));
39-
name = name.Replace("{TYPE}", type.ToDisplayString(NullableFlowState.NotNull, FullTypeFormat)).Replace(".", "");
42+
return text;
4043
}
4144

42-
if (tag is not null)
45+
var chars = text.ToArray();
46+
var size = 0;
47+
for (var i = 0; i < chars.Length; i++)
4348
{
44-
name = name.Replace("{tag}", tag.ToString());
49+
ref var ch = ref chars[i];
50+
if (i == 0)
51+
{
52+
if (!SyntaxFacts.IsIdentifierStartCharacter(ch))
53+
{
54+
chars[size++] = '_';
55+
}
56+
else
57+
{
58+
chars[size++] = ch;
59+
}
60+
}
61+
else
62+
{
63+
if (!SyntaxFacts.IsIdentifierPartCharacter(ch))
64+
{
65+
switch (ch)
66+
{
67+
case '.':
68+
case '`':
69+
case ' ':
70+
case ':':
71+
break;
72+
73+
default:
74+
chars[size++] = '_';
75+
break;
76+
}
77+
}
78+
else
79+
{
80+
chars[size++] = ch;
81+
}
82+
}
4583
}
4684

47-
return name;
85+
return new string(chars, 0, size);
4886
}
4987
}

tests/Pure.DI.IntegrationTests/ArgsTests.cs

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,70 @@ public static void Main()
106106
result.StdOut.ShouldBe(["Some Name"], result);
107107
}
108108

109+
[Fact]
110+
public async Task ShouldSupportArgWithNameTemplate()
111+
{
112+
// Given
113+
114+
// When
115+
var result = await """
116+
using System;
117+
using Pure.DI;
118+
119+
namespace Sample
120+
{
121+
interface IDependency {}
122+
123+
class Dependency: IDependency {}
124+
125+
interface IService
126+
{
127+
IDependency Dep { get; }
128+
129+
string Name { get; }
130+
}
131+
132+
class Service: IService
133+
{
134+
public Service(IDependency dep, string name)
135+
{
136+
Dep = dep;
137+
Name = name;
138+
}
139+
140+
public IDependency Dep { get; }
141+
142+
public string Name { get; private set; }
143+
}
144+
145+
static class Setup
146+
{
147+
private static void SetupComposition()
148+
{
149+
DI.Setup("Composition")
150+
.Bind<IDependency>().As(Lifetime.Singleton).To<Dependency>()
151+
.Bind<IService>().To<Service>()
152+
.Arg<string>("serviceName_{type}")
153+
.Root<IService>("Service");
154+
}
155+
}
156+
157+
public class Program
158+
{
159+
public static void Main()
160+
{
161+
var composition = new Composition(serviceName_string: "Some Name");
162+
Console.WriteLine(composition.Service.Name);
163+
}
164+
}
165+
}
166+
""".RunAsync();
167+
168+
// Then
169+
result.Success.ShouldBeTrue(result);
170+
result.StdOut.ShouldBe(["Some Name"], result);
171+
}
172+
109173
[Fact]
110174
public async Task ShouldSupportArgWhenFewDeps()
111175
{
@@ -401,6 +465,92 @@ public static void Main()
401465
result.StdOut.ShouldBe(["Some Name_99"], result);
402466
}
403467

468+
[Theory]
469+
[InlineData("\"my_id\"", "id_int_my_id")]
470+
[InlineData("null", "id_int_")]
471+
[InlineData("\"\"", "id_int_")]
472+
[InlineData("\"1my_id\"", "id_int_1my_id")]
473+
[InlineData("\"my Id\"", "id_int_myId")]
474+
[InlineData("\"my.Id\"", "id_int_myId")]
475+
[InlineData("\"my<Id\"", "id_int_my_Id")]
476+
[InlineData("\"my>Id\"", "id_int_my_Id")]
477+
[InlineData("\"my[Id\"", "id_int_my_Id")]
478+
[InlineData("\"my]Id\"", "id_int_my_Id")]
479+
[InlineData("\"my`Id\"", "id_int_myId")]
480+
public async Task ShouldSupportRootArgWithNameTemplate(string tag, string argName)
481+
{
482+
// Given
483+
484+
// When
485+
var result = await """
486+
using System;
487+
using Pure.DI;
488+
489+
namespace Sample
490+
{
491+
interface IDependency {}
492+
493+
class Dependency: IDependency {}
494+
495+
class Dependency2
496+
{
497+
public Dependency2([Tag(#tag#)] int id) {}
498+
}
499+
500+
interface IService
501+
{
502+
IDependency Dep { get; }
503+
504+
string Name { get; }
505+
}
506+
507+
class Service: IService
508+
{
509+
public Service(IDependency dep, [Tag(#tag#)] int id, string name)
510+
{
511+
Dep = dep;
512+
Name = name + "_" + id;
513+
}
514+
515+
public IDependency Dep { get; }
516+
517+
public string Name { get; private set; }
518+
}
519+
520+
static class Setup
521+
{
522+
private static void SetupComposition()
523+
{
524+
// ToString = On
525+
DI.Setup("Composition")
526+
.Bind<IDependency>().As(Lifetime.Singleton).To<Dependency>()
527+
.Bind<IService>().To<Service>()
528+
.Root<Dependency2>()
529+
.RootArg<string>("serviceName_{type}")
530+
.RootArg<int>("id_{type}_{tag}", #tag#)
531+
.Root<IService>("GetService");
532+
}
533+
}
534+
535+
public class Program
536+
{
537+
public static void Main()
538+
{
539+
var composition = new Composition();
540+
Console.WriteLine(composition.GetService(serviceName_string: "Some Name", #argName#: 99).Name);
541+
}
542+
}
543+
}
544+
""".Replace("#tag#", tag).Replace("#argName#", argName).RunAsync();
545+
546+
// Then
547+
result.Success.ShouldBeFalse(result);
548+
result.Errors.Count.ShouldBe(0, result);
549+
result.Warnings.Count.ShouldBe(2, result);
550+
result.Warnings.Count(i => i.Id == LogId.WarningRootArgInResolveMethod).ShouldBe(2, result);
551+
result.StdOut.ShouldBe(["Some Name_99"], result);
552+
}
553+
404554
[Fact]
405555
public async Task ShouldNotShowWaningAboutRootArgWhenResolveMethodsAreNotGenerated()
406556
{
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
namespace Pure.DI.Tests;
2+
3+
using Core;
4+
5+
public class NameFormatterTests
6+
{
7+
[Theory]
8+
[InlineData("Abc", "Abc")]
9+
[InlineData("Xyz.Abc", "XyzAbc")]
10+
[InlineData("", "")]
11+
[InlineData(".", "_")]
12+
[InlineData("Xyz.Abc<T>", "XyzAbc_T_")]
13+
[InlineData("Xyz.Abc`[T]", "XyzAbc_T_")]
14+
[InlineData("Xyz.Abc`[T,T1]", "XyzAbc_T_T1_")]
15+
[InlineData("Xyz.Abc`[T, T1]", "XyzAbc_T_T1_")]
16+
[InlineData("global::Xyz.Abc`[T, T1]", "globalXyzAbc_T_T1_")]
17+
public void ShouldConvertToValidIdentifier(string text, string expectedResult)
18+
{
19+
// Given
20+
21+
// When
22+
var actualResult = NameFormatter.ToValidIdentifier(text);
23+
24+
// Then
25+
actualResult.ShouldBe(expectedResult);
26+
}
27+
}

tests/Pure.DI.UsageTests/Basics/BuildersScenario.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
$v=true
33
$p=9
44
$d=Builders
5-
$h=Sometimes you need to build up an existing composition root and inject all of its dependencies, in which case the `Builder` method will be useful, as in the example below:
5+
$h=Sometimes you need builders for all types inherited from <see cref=“T”/> available at compile time at the point where the method is called.
66
$f=The default builder method name is `BuildUp`. The first argument to this method will always be the instance to be built.
77
$r=Shouldly
88
*/

tests/Pure.DI.UsageTests/Basics/RootsScenario.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
$v=true
33
$p=20
44
$d=Roots
5+
$h=Sometimes you need roots for all types inherited from <see cref="T"/> available at compile time at the point where the method is called.
56
$r=Shouldly
67
*/
78

0 commit comments

Comments
 (0)