Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
add53ed
#3053 Fix by using custom bool parsing that if a string is found then…
simonsabin Jan 13, 2026
05b3586
Update src/Service.Tests/UnitTests/RuntimeConfigLoaderJsonDeserialize…
simonsabin Jan 13, 2026
0b6e7f3
Update src/Config/Converters/BooleanJsonConverterFactory.cs
simonsabin Jan 13, 2026
c1759af
Update src/Config/Converters/BooleanJsonConverterFactory.cs
simonsabin Jan 13, 2026
36621c6
Update src/Service.Tests/UnitTests/RuntimeConfigLoaderJsonDeserialize…
simonsabin Jan 13, 2026
76b0c41
Extra handling of numeric values and test coverage
simonsabin Jan 14, 2026
5c7f38a
rename file to match class
simonsabin Jan 14, 2026
7532d68
Add schema solution
simonsabin Jan 14, 2026
bcf15b2
Merge branch 'main' into 3053BooleanEnvSub
simonsabin Jan 14, 2026
e3eee4a
Add test to validate the pattern matching for string booleans
simonsabin Jan 14, 2026
17f1067
Apply flexible enabled to rest, graphql, health and application-insghts
simonsabin Jan 14, 2026
d531999
Merge branch 'main' into 3053BooleanEnvSub
simonsabin Jan 15, 2026
2edb0e4
fix formatting
simonsabin Jan 15, 2026
aba610e
Merge branch '3053BooleanEnvSub' of https://github.com/simonsabin/dat…
simonsabin Jan 15, 2026
856052c
Normalise line endings
simonsabin Jan 15, 2026
928bb0b
fix pre commit hook docs on formatting
simonsabin Jan 15, 2026
6b24404
update to all for any 3 letter prefix for substitution
simonsabin Jan 15, 2026
af5dee4
Better exception if null found
simonsabin Jan 15, 2026
2eaf189
Update src/Config/Converters/BooleanJsonConverter.cs
simonsabin Jan 15, 2026
a901275
remove tests no longer valid, 1 and 0 are valid for booleans now
simonsabin Jan 15, 2026
9edc9ee
Merge branch '3053BooleanEnvSub' of https://github.com/simonsabin/dat…
simonsabin Jan 15, 2026
17995ab
revert back to @env\\('.*'\\)|@akv\\('.*'\\)|
simonsabin Jan 16, 2026
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
2 changes: 2 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,5 @@
*.verified.txt text eol=lf working-tree-encoding=UTF-8
*.verified.xml text eol=lf working-tree-encoding=UTF-8
*.verified.json text eol=lf working-tree-encoding=UTF-8

*.cs text eol=lf
37 changes: 31 additions & 6 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ We use `dotnet format` to enforce code conventions. It is run automatically in C

#### Enforcing code style with git hooks

You can copy paste the following commands to install a git pre-commit hook. This will cause a commit to fail if you forgot to run `dotnet format`. If you have run on save enabled in your editor this is not necessary.
You can copy paste the following commands to install a git pre-commit hook (creates a pre-commit file in your .git folder, which isn't shown in vs code). This will cause a commit to fail if you forgot to run `dotnet format`. If you have run on save enabled in your editor this is not necessary.

```bash
cat > .git/hooks/pre-commit << __EOF__
Expand All @@ -112,17 +112,42 @@ if [ "\$(get_files)" = '' ]; then
fi

get_files |
xargs dotnet format src/Azure.DataApiBuilder.Service.sln \\
--check \\
--fix-whitespace --fix-style warn --fix-analyzers warn \\
xargs dotnet format src/Azure.DataApiBuilder.sln \\
--verify-no-changes
--include \\
|| {
get_files |
xargs dotnet format src/Azure.DataApiBuilder.Service.sln \\
--fix-whitespace --fix-style warn --fix-analyzers warn \\
xargs dotnet format src/Azure.DataApiBuilder.sln \\
--include
exit 1
}
__EOF__
chmod +x .git/hooks/pre-commit
```

The file should look like this

``` bash
#!/bin/bash
set -euo pipefail

get_files() {
git diff --cached --name-only --diff-filter=ACMR | \
grep '\.cs$'
}

if [ "$(get_files)" = '' ]; then
exit 0
fi

get_files |
xargs dotnet format src/Azure.DataApiBuilder.sln \
--verify-no-changes \
--include \
|| {
get_files |
xargs dotnet format src/Azure.DataApiBuilder.sln \
--include
exit 1
}
```
24 changes: 16 additions & 8 deletions schemas/dab.draft.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,8 @@
"additionalProperties": false,
"properties": {
"enabled": {
"type": "boolean",
"description": "Enable health check endpoint",
"$ref": "#/$defs/boolean-or-string",
"description": "Enable health check endpoint for something",
"default": true,
"additionalProperties": false
},
Expand Down Expand Up @@ -186,7 +186,7 @@
"type": "string"
},
"enabled": {
"type": "boolean",
"$ref": "#/$defs/boolean-or-string",
"description": "Allow enabling/disabling REST requests for all entities."
},
"request-body-strict": {
Expand All @@ -210,7 +210,7 @@
"type": "string"
},
"enabled": {
"type": "boolean",
"$ref": "#/$defs/boolean-or-string",
"description": "Allow enabling/disabling GraphQL requests for all entities."
},
"depth-limit": {
Expand Down Expand Up @@ -438,7 +438,7 @@
"description": "Application Insights connection string"
},
"enabled": {
"type": "boolean",
"$ref": "#/$defs/boolean-or-string",
"description": "Allow enabling/disabling Application Insights telemetry.",
"default": true
}
Expand Down Expand Up @@ -481,7 +481,7 @@
"additionalProperties": false,
"properties": {
"enabled": {
"type": "boolean",
"$ref": "#/$defs/boolean-or-string",
"description": "Allow enabling/disabling Azure Log Analytics.",
"default": false
},
Expand Down Expand Up @@ -618,7 +618,7 @@
"additionalProperties": false,
"properties": {
"enabled": {
"type": "boolean",
"$ref": "#/$defs/boolean-or-string",
"description": "Enable health check endpoint globally",
"default": true,
"additionalProperties": false
Expand Down Expand Up @@ -1391,7 +1391,15 @@
"type": "string"
}
},
"required": ["singular"]
"required": [ "singular" ]
}
]
},
"boolean-or-string": {
"oneOf":[
{
"type": [ "boolean", "string" ],
"pattern": "^(?:true|false|1|0|@env\\('.*'\\)|@akv\\('.*'\\))$"
}
]
},
Expand Down
61 changes: 61 additions & 0 deletions src/Config/Converters/BooleanJsonConverter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System.Text.Json;
using System.Text.Json.Serialization;

namespace Azure.DataApiBuilder.Config.Converters;

/// <summary>
/// JSON converter for boolean values that also supports string representations such as
/// "true", "false", "1", and "0". Any environment variable replacement is handled by
/// other converters (for example, the string converter) before the value is parsed here.
public class BoolJsonConverter : JsonConverter<bool>
{

public override bool Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType is JsonTokenType.Null)
{

throw new JsonException("Unexpected null JSON token. Expected a boolean literal or a valid @expression.");
}

if (reader.TokenType == JsonTokenType.String)
{

string? tempBoolean = JsonSerializer.Deserialize<string>(ref reader, options);

bool result = tempBoolean?.ToLower() switch
{
//numeric values have to be checked here as they may come from string replacement
"true" or "1" => true,
"false" or "0" => false,
_ => throw new JsonException($"Invalid boolean value: {tempBoolean}. Specify either true or false."),
};

return result;
}
else if (reader.TokenType == JsonTokenType.Number)
{
bool result = reader.GetInt32() switch
{
1 => true,
0 => false,
_ => throw new JsonException($"Invalid boolean value. Specify either true or false."),
};
return result;
}
else
{
return reader.GetBoolean();
}

throw new JsonException("Invalid JSON value. Expected a boolean literal or a valid @expression.");
}

public override void Write(Utf8JsonWriter writer, bool value, JsonSerializerOptions options)
{
writer.WriteBooleanValue(value);
}
}
1 change: 1 addition & 0 deletions src/Config/RuntimeConfigLoader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -327,6 +327,7 @@ public static JsonSerializerOptions GetSerializationOptions(
options.Converters.Add(new AKVRetryPolicyOptionsConverterFactory(replacementSettings));
options.Converters.Add(new AzureLogAnalyticsOptionsConverterFactory(replacementSettings));
options.Converters.Add(new AzureLogAnalyticsAuthOptionsConverter(replacementSettings));
options.Converters.Add(new BoolJsonConverter());
options.Converters.Add(new FileSinkConverter(replacementSettings));

// Add AzureKeyVaultOptionsConverterFactory to ensure AKV config is deserialized properly
Expand Down
2 changes: 0 additions & 2 deletions src/Service.Tests/Caching/CachingConfigProcessingTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -161,8 +161,6 @@ public void GlobalCacheOptionsDeserialization_ValidValues(
[DataRow(@",""cache"": { ""enabled"": true, ""ttl-seconds"": 0 }", DisplayName = "EntityCacheOptions.TtlSeconds set to zero is invalid configuration.")]
[DataRow(@",""cache"": { ""enabled"": true, ""ttl-seconds"": -1 }", DisplayName = "EntityCacheOptions.TtlSeconds set to negative number is invalid configuration.")]
[DataRow(@",""cache"": { ""enabled"": true, ""ttl-seconds"": 1.1 }", DisplayName = "EntityCacheOptions.TtlSeconds set to decimal is invalid configuration.")]
[DataRow(@",""cache"": { ""enabled"": 1 }", DisplayName = "EntityCacheOptions.Enabled property set to 1 should fail because not a boolean.")]
[DataRow(@",""cache"": { ""enabled"": 0 }", DisplayName = "EntityCacheOptions.Enabled property set to 0 should fail because not a boolean.")]
[DataRow(@",""cache"": 1", DisplayName = "EntityCacheOptions property set to 1 should fail because it's not a JSON object.")]
[DataRow(@",""cache"": 0", DisplayName = "EntityCacheOptions property set to 0 should fail because it's not a JSON object.")]
[DataRow(@",""cache"": true", DisplayName = "EntityCacheOptions property set to true should fail because it's not a JSON object.")]
Expand Down
80 changes: 78 additions & 2 deletions src/Service.Tests/Configuration/ConfigurationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -664,6 +664,46 @@ type Moon {
""entities"":{ }
}";

public const string CONFIG_FILE_WITH_BOOLEAN_AS_ENV = @"{
// Link for latest draft schema.
""$schema"":""https://github.com/Azure/data-api-builder/releases/download/vmajor.minor.patch-alpha/dab.draft.schema.json"",
""data-source"": {
""database-type"": ""mssql"",
""connection-string"": ""sample-conn-string"",
""health"": {
""enabled"": <REPLACE_VALUE>
}
},
""runtime"": {
""health"": {
""enabled"": <REPLACE_VALUE>
},
""rest"": {
""enabled"": <REPLACE_VALUE>,
""path"": ""/api""
},
""graphql"": {
""enabled"": <REPLACE_VALUE>,
""path"": ""/graphql"",
""allow-introspection"": true
},
""host"": {
""authentication"": {
""provider"": ""AppService""
}
},
""telemetry"": {
""application-insights"":{
""enabled"": <REPLACE_VALUE>,
""connection-string"":""sample-ai-connection-string""
}

}

},
""entities"":{ }
}";

[TestCleanup]
public void CleanupAfterEachTest()
{
Expand Down Expand Up @@ -1820,8 +1860,44 @@ public void TestBasicConfigSchemaWithNoOptionalFieldsIsValid(string jsonData)
JsonConfigSchemaValidator jsonSchemaValidator = new(schemaValidatorLogger.Object, new MockFileSystem());

JsonSchemaValidationResult result = jsonSchemaValidator.ValidateJsonConfigWithSchema(jsonSchema, jsonData);
Assert.IsTrue(result.IsValid);
Assert.AreEqual("", String.Join('\n', result.ValidationErrors?.Select(s => $"{s.Message} at {s.Path} {s.LineNumber} {s.LinePosition}") ?? []), "Expected no validation errors.");
Assert.IsTrue(EnumerableUtilities.IsNullOrEmpty(result.ValidationErrors));

Assert.IsTrue(result.IsValid);
schemaValidatorLogger.Verify(
x => x.Log(
LogLevel.Information,
It.IsAny<EventId>(),
It.Is<It.IsAnyType>((o, t) => o.ToString()!.Contains($"The config satisfies the schema requirements.")),
It.IsAny<Exception>(),
(Func<It.IsAnyType, Exception, string>)It.IsAny<object>()),
Times.Once);
}

[DataTestMethod]
[DataRow("true", DisplayName = "Validates variable boolean schema for true value")]
[DataRow("false", DisplayName = "Validates variable boolean schema for false value.")]
[DataRow("\"true\"", DisplayName = "Validates variable boolean schema for true as string.")]
[DataRow("\"false\"", DisplayName = "Validates variable boolean schema for false as string.")]
[DataRow("\"1\"", DisplayName = "Validates variable boolean schema for 1 as string.")]
[DataRow("\"0\"", DisplayName = "Validates variable boolean schema for 0as string.")]
[DataRow("\"@env('SAMPLE')\"", DisplayName = "Validates variable boolean schema for environment variables.")]
[DataRow("\"@akv('SAMPLE')\"", DisplayName = "Validates variable boolean schema for keyvaul variables.")]
public void TestBasicConfigSchemaWithFlexibleBoolean(string Value)
{
Mock<ILogger<JsonConfigSchemaValidator>> schemaValidatorLogger = new();

string jsonSchema = File.ReadAllText("dab.draft.schema.json");

JsonConfigSchemaValidator jsonSchemaValidator = new(schemaValidatorLogger.Object, new MockFileSystem());

string jsonData = CONFIG_FILE_WITH_BOOLEAN_AS_ENV.Replace("<REPLACE_VALUE>", Value);
JsonSchemaValidationResult result = jsonSchemaValidator.ValidateJsonConfigWithSchema(jsonSchema, jsonData);
Assert.AreEqual("", String.Join('\n', result.ValidationErrors?.Select(s => $"{s.Message} at {s.Path} {s.LineNumber} {s.LinePosition}") ?? []), "Expected no validation errors.");

Assert.IsTrue(EnumerableUtilities.IsNullOrEmpty(result.ValidationErrors), "Validation Erros null of empty");

Assert.IsTrue(result.IsValid, "Result should be valid");
schemaValidatorLogger.Verify(
x => x.Log(
LogLevel.Information,
Expand Down Expand Up @@ -3368,7 +3444,7 @@ public async Task ValidateStrictModeAsDefaultForRestRequestBody(bool includeExtr
HttpMethod httpMethod = SqlTestHelper.ConvertRestMethodToHttpMethod(SupportedHttpVerb.Post);
string requestBody = @"{
""title"": ""Harry Potter and the Order of Phoenix"",
""publisher_id"": 1234";
""publisher_id"": 1234 }";

if (includeExtraneousFieldInRequestBody)
{
Expand Down
Loading
Loading