Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@ public InputEnumTypeIntegerValue(string name, int integerValue, InputPrimitiveTy
IntegerValue = integerValue;
}

public int IntegerValue { get; }
public InputEnumTypeIntegerValue(string name, long integerValue, InputPrimitiveType valueType, string? summary, string? doc, InputEnumType? enumType = default)
: base(name, integerValue, valueType, summary, doc, enumType)
{
IntegerValue = integerValue;
}

public long IntegerValue { get; }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -59,9 +59,23 @@ internal static InputEnumTypeValue CreateEnumTypeValue(ref Utf8JsonReader reader
InputEnumTypeValue enumValue = valueType.Kind switch
{
InputPrimitiveTypeKind.String => new InputEnumTypeStringValue(name, rawValue.Value.GetString() ?? throw new JsonException(), valueType, summary, doc, enumType) { Decorators = decorators ?? [] },
InputPrimitiveTypeKind.Int32 => new InputEnumTypeIntegerValue(name, rawValue.Value.GetInt32(), valueType, summary, doc, enumType) { Decorators = decorators ?? [] },
InputPrimitiveTypeKind.Float32 => new InputEnumTypeFloatValue(name, rawValue.Value.GetSingle(), valueType, summary, doc, enumType) { Decorators = decorators ?? [] },
_ => throw new JsonException()
InputPrimitiveTypeKind.Integer or
InputPrimitiveTypeKind.Int8 or
InputPrimitiveTypeKind.Int16 or
InputPrimitiveTypeKind.Int32 or
InputPrimitiveTypeKind.UInt8 or
InputPrimitiveTypeKind.UInt16 => new InputEnumTypeIntegerValue(name, rawValue.Value.GetInt32(), valueType, summary, doc, enumType) { Decorators = decorators ?? [] },
InputPrimitiveTypeKind.Int64 or
InputPrimitiveTypeKind.UInt32 or
InputPrimitiveTypeKind.UInt64 or
InputPrimitiveTypeKind.SafeInt => new InputEnumTypeIntegerValue(name, rawValue.Value.GetInt64(), valueType, summary, doc, enumType) { Decorators = decorators ?? [] },
InputPrimitiveTypeKind.Float or
InputPrimitiveTypeKind.Float32 or
InputPrimitiveTypeKind.Float64 or
InputPrimitiveTypeKind.Numeric or
InputPrimitiveTypeKind.Decimal or
InputPrimitiveTypeKind.Decimal128 => new InputEnumTypeFloatValue(name, rawValue.Value.GetSingle(), valueType, summary, doc, enumType) { Decorators = decorators ?? [] },
_ => throw new JsonException($"Unsupported enum valueType kind '{valueType.Kind}' for enum '{enumType.Name}' value '{name}'.")
};
if (id != null)
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
{
"$id": "1",
"name": "TestNamespace",
"models": [],
"clients": [],
"enums": [
{
"$id": "2",
"kind": "enum",
"name": "WeatherIconCode",
"namespace": "TestNamespace",
"crossLanguageDefinitionId": "TestNamespace.WeatherIconCode",
"valueType": {
"$id": "3",
"kind": "integer",
"name": "integer",
"crossLanguageDefinitionId": "TypeSpec.integer",
"decorators": []
},
"values": [
{
"$id": "4",
"kind": "enumvalue",
"name": "Sunny",
"value": 1,
"valueType": { "$ref": "3" },
"enumType": { "$ref": "2" },
"doc": "Sunny.",
"decorators": []
},
{
"$id": "5",
"kind": "enumvalue",
"name": "MostlySunny",
"value": 2,
"valueType": { "$ref": "3" },
"enumType": { "$ref": "2" },
"doc": "Mostly sunny.",
"decorators": []
}
],
"isFixed": false,
"isFlags": false,
"usage": "Output,Json",
"decorators": []
},
{
"$id": "6",
"kind": "enum",
"name": "Int32WeatherCode",
"namespace": "TestNamespace",
"crossLanguageDefinitionId": "TestNamespace.Int32WeatherCode",
"valueType": {
"$id": "7",
"kind": "int32",
"name": "int32",
"crossLanguageDefinitionId": "TypeSpec.int32",
"decorators": []
},
"values": [
{
"$id": "8",
"kind": "enumvalue",
"name": "Cold",
"value": -10,
"valueType": { "$ref": "7" },
"enumType": { "$ref": "6" },
"decorators": []
},
{
"$id": "9",
"kind": "enumvalue",
"name": "Hot",
"value": 100,
"valueType": { "$ref": "7" },
"enumType": { "$ref": "6" },
"decorators": []
}
],
"isFixed": true,
"isFlags": false,
"usage": "Output,Json",
"decorators": []
},
{
"$id": "10",
"kind": "enum",
"name": "LongWeatherTimestamp",
"namespace": "TestNamespace",
"crossLanguageDefinitionId": "TestNamespace.LongWeatherTimestamp",
"valueType": {
"$id": "11",
"kind": "int64",
"name": "int64",
"crossLanguageDefinitionId": "TypeSpec.int64",
"decorators": []
},
"values": [
{
"$id": "12",
"kind": "enumvalue",
"name": "Epoch",
"value": 0,
"valueType": { "$ref": "11" },
"enumType": { "$ref": "10" },
"decorators": []
},
{
"$id": "13",
"kind": "enumvalue",
"name": "MaxValue",
"value": 9223372036854775807,
"valueType": { "$ref": "11" },
"enumType": { "$ref": "10" },
"decorators": []
}
],
"isFixed": true,
"isFlags": false,
"usage": "Output,Json",
"decorators": []
}
]
}
Original file line number Diff line number Diff line change
Expand Up @@ -690,6 +690,52 @@ public void LoadsModelWithExternalMetadataEndToEnd()
Assert.AreEqual("8.0.0", externalModel.External.MinVersion);
}

[Test]
public void LoadsEnumsWithIntegerAndLongValues()
{
var directory = Helpers.GetAssetFileOrDirectoryPath(false);
var content = File.ReadAllText(Path.Combine(directory, "tspCodeModel.json"));
var inputNamespace = TypeSpecSerialization.Deserialize(content);

Assert.IsNotNull(inputNamespace);
Assert.AreEqual(3, inputNamespace!.Enums.Count);

// 1) Base `integer` kind
var integerEnum = inputNamespace.Enums.SingleOrDefault(e => e.Name == "WeatherIconCode");
Assert.IsNotNull(integerEnum);
Assert.AreEqual(InputPrimitiveTypeKind.Integer, integerEnum!.ValueType.Kind);
Assert.AreEqual(2, integerEnum.Values.Count);
var sunny = integerEnum.Values[0] as InputEnumTypeIntegerValue;
Assert.IsNotNull(sunny);
Assert.AreEqual("Sunny", sunny!.Name);
Assert.AreEqual(1L, sunny.IntegerValue);
var mostlySunny = integerEnum.Values[1] as InputEnumTypeIntegerValue;
Assert.IsNotNull(mostlySunny);
Assert.AreEqual(2L, mostlySunny!.IntegerValue);

// 2) Explicit int32 kind, including a negative value.
var int32Enum = inputNamespace.Enums.SingleOrDefault(e => e.Name == "Int32WeatherCode");
Assert.IsNotNull(int32Enum);
Assert.AreEqual(InputPrimitiveTypeKind.Int32, int32Enum!.ValueType.Kind);
var cold = int32Enum.Values[0] as InputEnumTypeIntegerValue;
Assert.IsNotNull(cold);
Assert.AreEqual(-10L, cold!.IntegerValue);
var hot = int32Enum.Values[1] as InputEnumTypeIntegerValue;
Assert.IsNotNull(hot);
Assert.AreEqual(100L, hot!.IntegerValue);

// 3) int64 (long) kind, including long.MaxValue to confirm we use GetInt64.
var longEnum = inputNamespace.Enums.SingleOrDefault(e => e.Name == "LongWeatherTimestamp");
Assert.IsNotNull(longEnum);
Assert.AreEqual(InputPrimitiveTypeKind.Int64, longEnum!.ValueType.Kind);
var epoch = longEnum.Values[0] as InputEnumTypeIntegerValue;
Assert.IsNotNull(epoch);
Assert.AreEqual(0L, epoch!.IntegerValue);
var maxValue = longEnum.Values[1] as InputEnumTypeIntegerValue;
Assert.IsNotNull(maxValue);
Assert.AreEqual(long.MaxValue, maxValue!.IntegerValue);
}

[Test]
public void LoadsXmlOnlyModelDoesNotAddJsonUsage()
{
Expand Down Expand Up @@ -1131,5 +1177,110 @@ public void IgnoresUnknownPropertiesInSerializationOptions()
Assert.IsNotNull(result!.Json);
Assert.AreEqual("msg", result.Json!.Name);
}

private static JsonSerializerOptions CreateEnumValueTestOptions()
{
var referenceHandler = new TypeSpecReferenceHandler();
return new JsonSerializerOptions
{
AllowTrailingCommas = true,
Converters =
{
new JsonStringEnumConverter(JsonNamingPolicy.CamelCase),
new InputTypeConverter(referenceHandler),
new InputEnumTypeConverter(referenceHandler),
new InputEnumTypeValueConverter(referenceHandler),
new InputPrimitiveTypeConverter(referenceHandler),
}
};
}

private static string CreateEnumJson(string valueKindJson, string rawValueJson)
{
return $$"""
{
"$id": "1",
"kind": "enum",
"name": "TestEnum",
"namespace": "Test.Models",
"crossLanguageDefinitionId": "Test.Models.TestEnum",
"valueType": { "$id": "2", "kind": {{valueKindJson}}, "name": "valueType", "crossLanguageDefinitionId": "TypeSpec.numeric" },
"values": [
{
"$id": "3",
"kind": "enumvalue",
"name": "One",
"value": {{rawValueJson}},
"valueType": { "$ref": "2" },
"enumType": { "$ref": "1" }
}
],
"isFixed": true
}
""";
}

[TestCase("\"integer\"", "1", 1L)]
[TestCase("\"int8\"", "1", 1L)]
[TestCase("\"int16\"", "1", 1L)]
[TestCase("\"int32\"", "1", 1L)]
[TestCase("\"uint8\"", "1", 1L)]
[TestCase("\"uint16\"", "1", 1L)]
[TestCase("\"int64\"", "9223372036854775807", 9223372036854775807L)]
[TestCase("\"uint32\"", "4294967295", 4294967295L)]
[TestCase("\"uint64\"", "9223372036854775807", 9223372036854775807L)]
[TestCase("\"safeInt\"", "9007199254740991", 9007199254740991L)]
public void DeserializeEnumWithIntegerKind(string valueKindJson, string rawValueJson, long expected)
{
var json = CreateEnumJson(valueKindJson, rawValueJson);
var enumType = JsonSerializer.Deserialize<InputEnumType>(json, CreateEnumValueTestOptions());

Assert.IsNotNull(enumType);
Assert.AreEqual(1, enumType!.Values.Count);
var value = enumType.Values[0] as InputEnumTypeIntegerValue;
Assert.IsNotNull(value);
Assert.AreEqual(expected, value!.IntegerValue);
Assert.AreEqual("One", value.Name);
}

[TestCase("\"float\"", "1.5", 1.5f)]
[TestCase("\"float32\"", "1.5", 1.5f)]
[TestCase("\"float64\"", "1.5", 1.5f)]
[TestCase("\"numeric\"", "1.5", 1.5f)]
[TestCase("\"decimal\"", "1.5", 1.5f)]
[TestCase("\"decimal128\"", "1.5", 1.5f)]
public void DeserializeEnumWithFloatKind(string valueKindJson, string rawValueJson, float expected)
{
var json = CreateEnumJson(valueKindJson, rawValueJson);
var enumType = JsonSerializer.Deserialize<InputEnumType>(json, CreateEnumValueTestOptions());

Assert.IsNotNull(enumType);
Assert.AreEqual(1, enumType!.Values.Count);
var value = enumType.Values[0] as InputEnumTypeFloatValue;
Assert.IsNotNull(value);
Assert.AreEqual(expected, value!.FloatValue);
Assert.AreEqual("One", value.Name);
}

[Test]
public void DeserializeEnumWithStringKind()
{
var json = CreateEnumJson("\"string\"", "\"sunny\"");
var enumType = JsonSerializer.Deserialize<InputEnumType>(json, CreateEnumValueTestOptions());

Assert.IsNotNull(enumType);
Assert.AreEqual(1, enumType!.Values.Count);
var value = enumType.Values[0] as InputEnumTypeStringValue;
Assert.IsNotNull(value);
Assert.AreEqual("sunny", value!.StringValue);
}

[Test]
public void DeserializeEnumWithUnsupportedKindThrows()
{
var json = CreateEnumJson("\"boolean\"", "true");
Assert.Throws<JsonException>(() =>
JsonSerializer.Deserialize<InputEnumType>(json, CreateEnumValueTestOptions()));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,28 @@ protected override TypeProvider[] BuildSerializationProviders()

protected override TypeSignatureModifiers BuildDeclarationModifiers() => _modifiers;

// The set of types permitted as a C# enum's underlying type (CS1008):
// https://learn.microsoft.com/dotnet/csharp/misc/cs1008
private static readonly HashSet<Type> _allowedEnumUnderlyingTypes =
[
typeof(byte),
typeof(sbyte),
typeof(short),
typeof(ushort),
typeof(uint),
typeof(long),
typeof(ulong),
];
protected override CSharpType? BuildBaseType()
{
var underlying = EnumUnderlyingType;
if (!underlying.IsFrameworkType || !_allowedEnumUnderlyingTypes.Contains(underlying.FrameworkType))
{
return null;
}
return underlying;
}

// we have to build the values first, because the corresponding fieldDeclaration of the values might need all of the existing values to avoid name conflicts
protected override IReadOnlyList<EnumTypeMember> BuildEnumValues()
{
Expand Down
Loading
Loading