Skip to content

Commit

Permalink
Add support for typed constants
Browse files Browse the repository at this point in the history
Fixes #150
  • Loading branch information
kzu committed Sep 27, 2024
1 parent e49b23a commit eae9ec1
Show file tree
Hide file tree
Showing 7 changed files with 83 additions and 8 deletions.
10 changes: 8 additions & 2 deletions src/ThisAssembly.Constants/CSharp.sbntxt
Original file line number Diff line number Diff line change
Expand Up @@ -40,13 +40,19 @@
{{- summary value -}}
{{- remarks -}}
{{ obsolete }}
{{~ if RawStrings ~}}
{{~ if RawStrings && value.IsText ~}}
public const string {{ value.Name | string.replace "-" "_" | string.replace " " "_" }} =

"""
{{ value.Value }}
""";
{{~ else ~}}
public const string {{ value.Name | string.replace "-" "_" | string.replace " " "_" }} = @"{{ value.Value }}";
public const {{ value.Type }} {{ value.Name | string.replace "-" "_" | string.replace " " "_" }} =
{{~ if value.IsText ~}}
@"{{ value.Value }}";
{{~ else ~}}
{{ value.Value }};
{{~ end ~}}
{{~ end ~}}
{{~ end ~}}

Expand Down
9 changes: 5 additions & 4 deletions src/ThisAssembly.Constants/ConstantsGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
.Select((x, ct) =>
{
x.Right.GetOptions(x.Left).TryGetValue("build_metadata.Constant.Value", out var value);
x.Right.GetOptions(x.Left).TryGetValue("build_metadata.Constant.Type", out var type);
x.Right.GetOptions(x.Left).TryGetValue("build_metadata.Constant.Comment", out var comment);
x.Right.GetOptions(x.Left).TryGetValue("build_metadata.Constant.Root", out var root);

Expand All @@ -52,7 +53,7 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
}
}

return (name, value: value ?? "", comment: string.IsNullOrWhiteSpace(comment) ? null : comment, root!);
return (name, value: value ?? "", type: string.IsNullOrWhiteSpace(type) ? null : type, comment: string.IsNullOrWhiteSpace(comment) ? null : comment, root!);
});

// Read the ThisAssemblyNamespace property or default to null
Expand All @@ -68,9 +69,9 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
}

void GenerateConstant(SourceProductionContext spc,
(((string name, string value, string? comment, string root), (string? ns, ParseOptions parse)), StatusOptions options) args)
(((string name, string value, string? type, string? comment, string root), (string? ns, ParseOptions parse)), StatusOptions options) args)
{
var (((name, value, comment, root), (ns, parse)), options) = args;
var (((name, value, type, comment, root), (ns, parse)), options) = args;
var cs = (CSharpParseOptions)parse;

if (!string.IsNullOrWhiteSpace(ns) &&
Expand All @@ -89,7 +90,7 @@ void GenerateConstant(SourceProductionContext spc,
comment = "/// " + string.Join(Environment.NewLine + "/// ", value.Replace("\\n", Environment.NewLine).Trim(['\r', '\n']).Split([Environment.NewLine], StringSplitOptions.None));

// Revert normalization of newlines performed in MSBuild to workaround the limitation in editorconfig.
var rootArea = Area.Load([new(name, value.Replace("\\n", Environment.NewLine).Trim(['\r', '\n']), comment),], root);
var rootArea = Area.Load([new(name, value.Replace("\\n", Environment.NewLine).Trim(['\r', '\n']), comment, type ?? "string"),], root);
// For now, we only support C# though
var file = parse.Language.Replace("#", "Sharp") + ".sbntxt";
var template = Template.Parse(EmbeddedResource.GetContent(file), file);
Expand Down
5 changes: 4 additions & 1 deletion src/ThisAssembly.Constants/Model.cs
Original file line number Diff line number Diff line change
Expand Up @@ -105,4 +105,7 @@ static Area GetArea(Area area, IEnumerable<string> areaPath)
}

[DebuggerDisplay("{Name} = {Value}")]
record Constant(string Name, string? Value, string? Comment);
record Constant(string Name, string? Value, string? Comment, string Type = "string")
{
public bool IsText => Type.Equals("string", StringComparison.OrdinalIgnoreCase);
}
1 change: 1 addition & 0 deletions src/ThisAssembly.Constants/ThisAssembly.Constants.targets
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
<CompilerVisibleItemMetadata Include="Constant" MetadataName="ItemType" />
<CompilerVisibleItemMetadata Include="Constant" MetadataName="Comment" />
<CompilerVisibleItemMetadata Include="Constant" MetadataName="Value" />
<CompilerVisibleItemMetadata Include="Constant" MetadataName="Type" />
<CompilerVisibleItemMetadata Include="Constant" MetadataName="Root" />

<!-- Make sure we're always private to the referencing project.
Expand Down
41 changes: 40 additions & 1 deletion src/ThisAssembly.Constants/readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,48 @@ constants for `@(Constant)` MSBuild items in the project.
</ItemGroup>
```


![](https://raw.githubusercontent.com/devlooped/ThisAssembly/main/img/ThisAssembly.Constants.png)

These constants can use values from MSBuild properties, making compile-time values configurable
via environment variables or command line arguments. For example:

```xml
<PropertyGroup>
<HttpDefaultTimeoutSeconds>10</HttpDefaultTimeoutSeconds>
</PropertyGroup>
<ItemGroup>
<Constant Include="Http.TimeoutSeconds"
Value="$(HttpDefaultTimeoutSeconds)"
Type="int"
Comment="Default timeout in seconds for HTTP requests" />
</ItemGroup>
```

The C# code could consume this constant as follows:

```csharp

public HttpClient CreateHttpClient(string name, int? timeout = default)
{
HttpClient client = httpClientFactory.CreateClient(name);
client.Timeout = TimeSpan.FromSeconds(timeout ?? ThisAssembly.Constants.Http.TimeoutSeconds);
return client;
}
```

Note how the constant is typed to `int` as specified in the `Type` attribute in MSBuild.
The generated code uses the specified `Type` as-is, as well as the `Value` attribute in that
case, so it's up to the user to ensure they match and result in valid C# code. For example,
you can emit a boolean, long, double, etc., but a `DateTime` wouldn't be a valid constant even
if you set the `Value` to `DateTime.Now`. If no type is provided, `string` is assumed. Values
can also be multi-line and will use [C# raw string literals](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/tokens/raw-string)
if supported by the target language version (11+).

In this example, you could trivially change how your product behaves by setting the environment
variable `HttpDefaultTimeoutSeconds` in CI. This is particularly useful for test projects,
where you can easily change the behavior of the system under test without changing the code.


In addition to arbitrary constants via `<Constant ...>`, it's quite useful (in particular in test projects)
to generate constants for files in the project, so there's also a shorthand for those:

Expand Down
20 changes: 20 additions & 0 deletions src/ThisAssembly.Tests/Tests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,26 @@ with a newline and
public void CanUseConstants()
=> Assert.Equal("Baz", ThisAssembly.Constants.Foo.Bar);

[Fact]
public void CanUseTypedIntConstant()
=> Assert.Equal(123, ThisAssembly.Constants.TypedInt);

[Fact]
public void CanUseTypedInt64Constant()
=> Assert.Equal(123, ThisAssembly.Constants.TypedInt64);

[Fact]
public void CanUseTypedLongConstant()
=> Assert.Equal(123, ThisAssembly.Constants.TypedLong);

[Fact]
public void CanUseTypedBoolConstant()
=> Assert.True(ThisAssembly.Constants.TypedBoolean);

[Fact]
public void CanUseTypedDoubleConstant()
=> Assert.Equal(1.23, ThisAssembly.Constants.TypedDouble);

[Fact]
public void CanUseFileConstants()
=> Assert.Equal(ThisAssembly.Constants.Content.Docs.License, Path.Combine("Content", "Docs", "License.md"));
Expand Down
5 changes: 5 additions & 0 deletions src/ThisAssembly.Tests/ThisAssembly.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,11 @@
<AssemblyMetadata Include="Foo" Value="Bar" />
<AssemblyMetadata Include="Raw" Value="$(Multiline)" />
<AssemblyMetadata Include="Root.Foo.Bar" Value="Baz" Comment="Comment" />
<Constant Include="TypedInt" Value="123" Type="int" />
<Constant Include="TypedInt64" Value="123" Type="Int64" />
<Constant Include="TypedLong" Value="123" Type="long" />
<Constant Include="TypedDouble" Value="1.23" Type="double" />
<Constant Include="TypedBoolean" Value="true" Type="bool" />
</ItemGroup>

<ItemGroup>
Expand Down

0 comments on commit eae9ec1

Please sign in to comment.