From afb8f459c640e8be8c07e9443f97c61e5b687a35 Mon Sep 17 00:00:00 2001 From: Michael Schimmel Date: Wed, 27 Aug 2025 18:00:01 +0200 Subject: [PATCH] Types-JSON --- Src/Data/Myc.Data.Types.JSON.pas | 258 +++++++++++++++++++ Src/Data/Myc.Data.Types.RTTI.pas | 21 +- Src/Data/Myc.Data.Types.pas | 221 +++++++++-------- Src/Data/TestDataTypes.JSON.pas | 412 +++++++++++++++++++++++++++++++ Src/Data/TestDataTypes.pas | 45 ++-- Test/MycTests.dpr | 4 +- Test/MycTests.dproj | 2 + 7 files changed, 830 insertions(+), 133 deletions(-) create mode 100644 Src/Data/Myc.Data.Types.JSON.pas create mode 100644 Src/Data/TestDataTypes.JSON.pas diff --git a/Src/Data/Myc.Data.Types.JSON.pas b/Src/Data/Myc.Data.Types.JSON.pas new file mode 100644 index 0000000..22d76d0 --- /dev/null +++ b/Src/Data/Myc.Data.Types.JSON.pas @@ -0,0 +1,258 @@ +unit Myc.Data.Types.JSON; + +interface + +uses + System.SysUtils, + System.JSON, + Myc.Data.Types; + +type + // Provides methods for serializing and deserializing data values to/from JSON using the TDataType helpers. + TDataJsonConverter = class + public + // Serializes a data value to a TJSONValue. + class function ValueToJson(const Value: TDataType.TValue): TJSONValue; + + // Deserializes a TJSONValue to a data value based on the provided data type. + class function JsonToValue(const Json: TJSONValue; const DataType: TDataType): TDataType.TValue; + end; + +implementation + +uses + System.Rtti, + System.TypInfo, + System.DateUtils, + System.Math; + +{ TDataJsonConverter } + +class function TDataJsonConverter.ValueToJson(const Value: TDataType.TValue): TJSONValue; +var + jsonObject: TJSONObject; + jsonArray: TJSONArray; + i: Integer; +begin + if not Assigned(Value.DataValue) then + begin + Result := TJSONNull.Create; + Exit; + end; + + case Value.DataType.Kind of + dkVoid: Result := TJSONNull.Create; + dkOrdinal: Result := TJSONNumber.Create(Value.AsOrdinal.Value); + dkFloat: Result := TJSONNumber.Create(Value.AsFloat.Value); + dkText: Result := TJSONString.Create(Value.AsText.Value); + dkTimestamp: Result := TJSONString.Create(DateToISO8601(Value.AsTimestamp.Value, true)); + dkDecimal: + begin + // Serialize as string to preserve precision. + var decValue := Value.AsDecimal; + var val := decValue.Value; + var scale := decValue.DataType.Scale; + var s: string; + + if (scale = 0) then + s := IntToStr(val) + else + begin + s := IntToStr(Abs(val)); + if (Length(s) <= scale) then + s := StringOfChar('0', scale - Length(s) + 1) + s; + + s := s.Insert(Length(s) - scale, '.'); + + if (val < 0) then + s := '-' + s; + end; + Result := TJSONString.Create(s); + end; + dkEnum: + begin + var enumValue := Value.AsEnum; + Result := TJSONString.Create(enumValue.DataType.Identifiers[enumValue.Value]); + end; + dkRecord: + begin + var recordValue := Value.AsRecord; + var recordType := recordValue.DataType; + jsonObject := TJSONObject.Create; + Result := jsonObject; + for i := 0 to recordType.FieldCount - 1 do + begin + jsonObject.AddPair( + recordType.Fields[i].Name, + ValueToJson(recordValue.Items[i]) // Items property returns TDataType.TValue + ); + end; + end; + dkTuple: + begin + var tupleValue := Value.AsTuple; + jsonArray := TJSONArray.Create; + Result := jsonArray; + for i := 0 to tupleValue.ItemCount - 1 do + jsonArray.AddElement(ValueToJson(tupleValue.Items[i])); + end; + dkArray: + begin + var arrayValue := Value.AsArray; + jsonArray := TJSONArray.Create; + Result := jsonArray; + for i := 0 to arrayValue.ElementCount - 1 do + jsonArray.AddElement(ValueToJson(arrayValue.Items[i])); + end; + dkVector: + begin + var vectorValue := Value.AsVector; + jsonArray := TJSONArray.Create; + Result := jsonArray; + for i := 0 to vectorValue.ElementCount - 1 do + jsonArray.AddElement(ValueToJson(vectorValue.Items[i])); + end; + dkMethod: raise ENotSupportedException.Create('Cannot serialize a method type to JSON.'); + else + raise ENotSupportedException.CreateFmt('Unsupported data kind: %s', [GetEnumName(TypeInfo(TDataKind), Ord(Value.DataType.Kind))]); + end; +end; + +class function TDataJsonConverter.JsonToValue(const Json: TJSONValue; const DataType: TDataType): TDataType.TValue; +var + jsonObject: TJSONObject; + jsonArray: TJSONArray; + items: TArray; + i, idx: Integer; + val: Int64; +begin + Assert(Assigned(DataType.DataType), 'DataType cannot be nil for deserialization'); + + case DataType.Kind of + dkVoid: + if (Json is TJSONNull) then + Result := TDataType.Void.Value + else + raise EConvertError.Create('JSON Null expected for Void type'); + dkOrdinal: Result := DataType.AsOrdinal.CreateValue((Json as TJSONNumber).AsInt64); + dkFloat: Result := DataType.AsFloat.CreateValue((Json as TJSONNumber).AsDouble); + dkText: Result := DataType.AsText.CreateValue((Json as TJSONString).Value); + dkTimestamp: Result := DataType.AsTimestamp.CreateValue(ISO8601ToDate((Json as TJSONString).Value, True)); + dkDecimal: + begin + // Handle string representation for precision, with fallback to number for compatibility. + var decimalType := DataType.AsDecimal; + if (Json is TJSONString) then + begin + var s := (Json as TJSONString).Value.Trim; + var isNegative := s.StartsWith('-'); + if isNegative then + s := s.Remove(0, 1); // remove sign + + var dotPos := Pos('.', s); + var numFracDigits: Integer; + if (dotPos = 0) then + numFracDigits := 0 + else + begin + numFracDigits := Length(s) - dotPos; + s := s.Remove(dotPos - 1, 1); // remove dot + end; + + val := StrToInt64(s); + + var scaleDiff := decimalType.Scale - numFracDigits; + if (scaleDiff > 0) then // Not enough fractional digits + begin + for i := 1 to scaleDiff do + val := val * 10; + end + else if (scaleDiff < 0) then // Too many fractional digits + begin + for i := 1 to -scaleDiff do + val := val div 10; + end; + + if isNegative then + val := -val; + + Result := decimalType.CreateValue(val); + end + else if (Json is TJSONNumber) then + begin + // Support numbers for compatibility, but this can lead to precision loss. + val := Round((Json as TJSONNumber).AsDouble * Power(10, decimalType.Scale)); + Result := decimalType.CreateValue(val); + end + else + raise EConvertError.Create('JSON String or Number expected for Decimal type'); + end; + dkEnum: + begin + var enumType := DataType.AsEnum; + idx := enumType.IndexOf((Json as TJSONString).Value); + if (idx < 0) then + raise EConvertError.CreateFmt('Invalid enum identifier "%s" for type %s', [Json.Value, DataType.Name]); + Result := enumType.CreateValue(idx); + end; + dkRecord: + begin + var recordType := DataType.AsRecord; + jsonObject := Json as TJSONObject; + SetLength(items, recordType.FieldCount); + for i := 0 to recordType.FieldCount - 1 do + begin + var field := recordType.Fields[i]; + var jsonPair := jsonObject.Get(field.Name); + if not Assigned(jsonPair) then + raise EConvertError.CreateFmt('Missing field "%s" for record type %s', [field.Name, DataType.Name]); + + // Recursive call returns TDataType.TValue, which implicitly converts to IDataValue for the array + items[i] := JsonToValue(jsonPair.JsonValue, field.DataType); + end; + Result := recordType.CreateValue(items); + end; + dkTuple: + begin + var tupleType := DataType.AsTuple; + jsonArray := Json as TJSONArray; + if (jsonArray.Count <> IDataTupleType(tupleType).ItemCount) then + raise EConvertError + .CreateFmt('JSON array size (%d) does not match tuple size (%d)', [jsonArray.Count, tupleType.ItemCount]); + + SetLength(items, IDataTupleType(tupleType).ItemCount); + for i := 0 to IDataTupleType(tupleType).ItemCount - 1 do + begin + var itemType := IDataTupleType(tupleType).ItemType[i]; + items[i] := JsonToValue(jsonArray.Items[i], itemType); + end; + Result := tupleType.CreateValue(items); + end; + dkArray: + begin + var arrayType := DataType.AsArray; + jsonArray := Json as TJSONArray; + SetLength(items, jsonArray.Count); + for i := 0 to jsonArray.Count - 1 do + items[i] := JsonToValue(jsonArray.Items[i], arrayType.ElementType); + Result := arrayType.CreateValue(items); + end; + dkVector: + begin + var vectorType := DataType.AsVector; + jsonArray := Json as TJSONArray; + if (jsonArray.Count <> vectorType.ElementCount) then + raise EConvertError + .CreateFmt('JSON array size (%d) does not match vector size (%d)', [jsonArray.Count, vectorType.ElementCount]); + SetLength(items, jsonArray.Count); + for i := 0 to jsonArray.Count - 1 do + items[i] := JsonToValue(jsonArray.Items[i], vectorType.ElementType); + Result := vectorType.CreateValue(items); + end; + dkMethod: raise ENotSupportedException.Create('Cannot deserialize a method type from JSON.'); + else + raise ENotSupportedException.CreateFmt('Unsupported data kind: %s', [GetEnumName(TypeInfo(TDataKind), Ord(DataType.Kind))]); + end; +end; + +end. diff --git a/Src/Data/Myc.Data.Types.RTTI.pas b/Src/Data/Myc.Data.Types.RTTI.pas index d7ec75d..e2cdf1d 100644 --- a/Src/Data/Myc.Data.Types.RTTI.pas +++ b/Src/Data/Myc.Data.Types.RTTI.pas @@ -7,10 +7,7 @@ uses System.RTTI, Myc.Data.Types; -type - TValueHelper = record helper for TDataType.TValue - function AsTValue: TValue; - end; +function AsTValue(const Value: TDataType.TValue): TValue; function RttiTypeToDataType(rttiType: TRttiType): TDataType; @@ -69,16 +66,16 @@ end; { TValueHelper } -function TValueHelper.AsTValue: TValue; +function AsTValue(const Value: TDataType.TValue): TValue; begin - case DataType.Kind of + case Value.DataType.Kind of dkVoid: Result := TValue.Empty; - dkOrdinal: Result := TValue.From(AsOrdinal.Value); - dkFloat: Result := TValue.From(AsFloat.Value); - dkText: Result := TValue.From(AsText.Value); - dkTimestamp: Result := TValue.From(AsTimestamp.Value); - dkEnum: Result := TValue.From(AsEnum.Value); - dkMethod: Result := TValue.From(AsMethod.Value); + dkOrdinal: Result := TValue.From(Value.AsOrdinal.Value); + dkFloat: Result := TValue.From(Value.AsFloat.Value); + dkText: Result := TValue.From(Value.AsText.Value); + dkTimestamp: Result := TValue.From(Value.AsTimestamp.Value); + dkEnum: Result := TValue.From(Value.AsEnum.Value); + dkMethod: Result := TValue.From(Value.AsMethod.Value); else raise EInvalidOpException.Create('Unsupported data type'); end; diff --git a/Src/Data/Myc.Data.Types.pas b/Src/Data/Myc.Data.Types.pas index 8832511..c67706d 100644 --- a/Src/Data/Myc.Data.Types.pas +++ b/Src/Data/Myc.Data.Types.pas @@ -169,7 +169,13 @@ type end; IDataTupleType = interface(IDataType) + {$region 'private'} + function GetItemCount: Integer; + function GetItemType(Idx: Integer): IDataType; + {$endregion} function CreateValue(const AItems: array of IDataValue): IDataTupleValue; + property ItemCount: Integer read GetItemCount; + property ItemType[Idx: Integer]: IDataType read GetItemType; end; IDataArrayValue = interface(IDataValue) @@ -219,20 +225,6 @@ type public constructor Create(const ADataValue: IDataValue); - // Casting - function AsOrdinal: IDataOrdinalValue; inline; - function AsFloat: IDataFloatValue; inline; - function AsText: IDataTextValue; inline; - function AsTimestamp: IDataTimestampValue; inline; - function AsDecimal: IDataDecimalValue; inline; - function AsRecord: IDataRecordValue; inline; - function AsTuple: IDataTupleValue; inline; - function AsArray: IDataArrayValue; inline; - function AsVector: IDataVectorValue; inline; - function AsEnum: IDataEnumValue; inline; - function AsMethod: IDataMethodValue; inline; - function AsVoid: IDataVoidValue; inline; - class operator Implicit(const A: IDataValue): TDataType.TValue; overload; inline; class operator Implicit(const A: TDataType.TValue): IDataValue; overload; inline; @@ -466,11 +458,15 @@ type end; private FTupleType: IDataTupleType; + function GetItemCount: Integer; inline; + function GetItemType(Idx: Integer): IDataType; inline; public constructor Create(const ATupleType: IDataTupleType); class operator Implicit(const A: IDataTupleType): TDataType.TTuple; overload; inline; class operator Implicit(const A: TDataType.TTuple): IDataTupleType; overload; inline; function CreateValue(const AItems: array of IDataValue): TDataType.TTuple.TValue; + property ItemCount: Integer read GetItemCount; + property ItemType[Idx: Integer]: IDataType read GetItemType; end; TArray = record @@ -560,6 +556,23 @@ type function CreateValue(const Proc: TProc): TDataType.TMethod.TValue; inline; end; + // Helper to cast a generic TValue to a specific value helper. + TValueHelper = record helper for TValue + public + function AsVoid: TVoid.TValue; inline; + function AsOrdinal: TOrdinal.TValue; inline; + function AsFloat: TFloat.TValue; inline; + function AsText: TText.TValue; inline; + function AsTimestamp: TTimestamp.TValue; inline; + function AsDecimal: TDecimal.TValue; inline; + function AsRecord: TRecord.TValue; inline; + function AsTuple: TTuple.TValue; inline; + function AsArray: TArray.TValue; inline; + function AsVector: TVector.TValue; inline; + function AsEnum: TEnum.TValue; inline; + function AsMethod: TMethod.TValue; inline; + end; + strict private FDataType: IDataType; function GetName: String; inline; @@ -1247,6 +1260,16 @@ begin Result.Create(FTupleType.CreateValue(AItems)); end; +function TDataType.TTuple.GetItemCount: Integer; +begin + Result := FTupleType.ItemCount; +end; + +function TDataType.TTuple.GetItemType(Idx: Integer): IDataType; +begin + Result := FTupleType.ItemType[Idx]; +end; + class operator TDataType.TTuple.Implicit(const A: IDataTupleType): TDataType.TTuple; begin Result.FTupleType := A; @@ -1590,90 +1613,6 @@ begin FDataValue := ADataValue; end; -function TDataType.TValue.AsArray: IDataArrayValue; -begin - if (FDataValue.DataType.Kind <> dkArray) then - raise EInvalidCast.Create('Array expected'); - Result := IDataArrayValue(FDataValue); -end; - -function TDataType.TValue.AsDecimal: IDataDecimalValue; -begin - if (FDataValue.DataType.Kind <> dkDecimal) then - raise EInvalidCast.Create('Decimal expected'); - Result := IDataDecimalValue(FDataValue); -end; - -function TDataType.TValue.AsEnum: IDataEnumValue; -begin - if (FDataValue.DataType.Kind <> dkEnum) then - raise EInvalidCast.Create('Enum expected'); - Result := IDataEnumValue(FDataValue); -end; - -function TDataType.TValue.AsFloat: IDataFloatValue; -begin - if (FDataValue.DataType.Kind <> dkFloat) then - raise EInvalidCast.Create('Float expected'); - Result := IDataFloatValue(FDataValue); -end; - -function TDataType.TValue.AsMethod: IDataMethodValue; -begin - if (FDataValue.DataType.Kind <> dkMethod) then - raise EInvalidCast.Create('Method expected'); - Result := IDataMethodValue(FDataValue); -end; - -function TDataType.TValue.AsOrdinal: IDataOrdinalValue; -begin - if (FDataValue.DataType.Kind <> dkOrdinal) then - raise EInvalidCast.Create('Ordinal expected'); - Result := IDataOrdinalValue(FDataValue); -end; - -function TDataType.TValue.AsRecord: IDataRecordValue; -begin - if (FDataValue.DataType.Kind <> dkRecord) then - raise EInvalidCast.Create('Record expected'); - Result := IDataRecordValue(FDataValue); -end; - -function TDataType.TValue.AsText: IDataTextValue; -begin - if (FDataValue.DataType.Kind <> dkText) then - raise EInvalidCast.Create('Text expected'); - Result := IDataTextValue(FDataValue); -end; - -function TDataType.TValue.AsTimestamp: IDataTimestampValue; -begin - if (FDataValue.DataType.Kind <> dkTimestamp) then - raise EInvalidCast.Create('Timestamp expected'); - Result := IDataTimestampValue(FDataValue); -end; - -function TDataType.TValue.AsTuple: IDataTupleValue; -begin - if (FDataValue.DataType.Kind <> dkTuple) then - raise EInvalidCast.Create('Tuple expected'); - Result := IDataTupleValue(FDataValue); -end; - -function TDataType.TValue.AsVector: IDataVectorValue; -begin - if (FDataValue.DataType.Kind <> dkVector) then - raise EInvalidCast.Create('Vector expected'); - Result := IDataVectorValue(FDataValue); -end; - -function TDataType.TValue.AsVoid: IDataVoidValue; -begin - if (FDataValue.DataType.Kind <> dkVoid) then - raise EInvalidCast.Create('Void expected'); - Result := IDataVoidValue(FDataValue); -end; - function TDataType.TValue.GetDataType: IDataType; begin if Assigned(FDataValue) then @@ -1769,4 +1708,90 @@ begin Result.Create(IDataVectorType(FDataType)); end; +{ TDataType.TValueHelper } + +function TDataType.TValueHelper.AsArray: TDataType.TArray.TValue; +begin + if (FDataValue.DataType.Kind <> dkArray) then + raise EInvalidCast.Create('Array expected'); + Result.Create(IDataArrayValue(FDataValue)); +end; + +function TDataType.TValueHelper.AsDecimal: TDecimal.TValue; +begin + if (FDataValue.DataType.Kind <> dkDecimal) then + raise EInvalidCast.Create('Decimal expected'); + Result.Create(IDataDecimalValue(FDataValue)); +end; + +function TDataType.TValueHelper.AsEnum: TDataType.TEnum.TValue; +begin + if (FDataValue.DataType.Kind <> dkEnum) then + raise EInvalidCast.Create('Enum expected'); + Result.Create(IDataEnumValue(FDataValue)); +end; + +function TDataType.TValueHelper.AsFloat: TDataType.TFloat.TValue; +begin + if (FDataValue.DataType.Kind <> dkFloat) then + raise EInvalidCast.Create('Float expected'); + Result.Create(IDataFloatValue(FDataValue)); +end; + +function TDataType.TValueHelper.AsMethod: TDataType.TMethod.TValue; +begin + if (FDataValue.DataType.Kind <> dkMethod) then + raise EInvalidCast.Create('Method expected'); + Result.Create(IDataMethodValue(FDataValue)); +end; + +function TDataType.TValueHelper.AsOrdinal: TDataType.TOrdinal.TValue; +begin + if (FDataValue.DataType.Kind <> dkOrdinal) then + raise EInvalidCast.Create('Ordinal expected'); + Result.Create(IDataOrdinalValue(FDataValue)); +end; + +function TDataType.TValueHelper.AsRecord: TDataType.TRecord.TValue; +begin + if (FDataValue.DataType.Kind <> dkRecord) then + raise EInvalidCast.Create('Record expected'); + Result.Create(IDataRecordValue(FDataValue)); +end; + +function TDataType.TValueHelper.AsText: TDataType.TText.TValue; +begin + if (FDataValue.DataType.Kind <> dkText) then + raise EInvalidCast.Create('Text expected'); + Result.Create(IDataTextValue(FDataValue)); +end; + +function TDataType.TValueHelper.AsTimestamp: TDataType.TTimestamp.TValue; +begin + if (FDataValue.DataType.Kind <> dkTimestamp) then + raise EInvalidCast.Create('Timestamp expected'); + Result.Create(IDataTimestampValue(FDataValue)); +end; + +function TDataType.TValueHelper.AsTuple: TDataType.TTuple.TValue; +begin + if (FDataValue.DataType.Kind <> dkTuple) then + raise EInvalidCast.Create('Tuple expected'); + Result.Create(IDataTupleValue(FDataValue)); +end; + +function TDataType.TValueHelper.AsVector: TDataType.TVector.TValue; +begin + if (FDataValue.DataType.Kind <> dkVector) then + raise EInvalidCast.Create('Vector expected'); + Result.Create(IDataVectorValue(FDataValue)); +end; + +function TDataType.TValueHelper.AsVoid: TDataType.TVoid.TValue; +begin + if (FDataValue.DataType.Kind <> dkVoid) then + raise EInvalidCast.Create('Void expected'); + Result.Create(IDataVoidValue(FDataValue)); +end; + end. diff --git a/Src/Data/TestDataTypes.JSON.pas b/Src/Data/TestDataTypes.JSON.pas new file mode 100644 index 0000000..253d3cd --- /dev/null +++ b/Src/Data/TestDataTypes.JSON.pas @@ -0,0 +1,412 @@ +unit TestDataTypes.JSON; + +interface + +uses + System.JSON, + DUnitX.TestFramework; + +type + [TestFixture] + [IgnoreMemoryLeaks] + TTestDataTypesJSON = class + private + FJson: TJSONValue; + public + [Setup] + procedure SetUp; + [TearDown] + procedure TearDown; + + [Test] + procedure TestVoid; + + [Test] + [TestCase('Positive', '12345')] + [TestCase('Negative', '-54321')] + [TestCase('Zero', '0')] + procedure TestOrdinal(const AValue: Int64); + + [Test] + [TestCase('Positive', '123.456')] + [TestCase('Negative', '-543.21')] + [TestCase('Zero', '0.0')] + procedure TestFloat(const AValue: Double); + + [Test] + [TestCase('Simple', 'Hello World')] + [TestCase('Empty', '')] + [TestCase('SpecialChars', 'äöüß#+*?%&/()=')] + procedure TestText(const AValue: string); + + [Test] + procedure TestTimestamp; + + [Test] + [TestCase('Scale4_Positive', '1234567,4,123.4567')] + [TestCase('Scale2_Negative', '-987,2,-9.87')] + [TestCase('Scale5_Small', '123,5,0.00123')] + [TestCase('Scale0_Int', '123,0,123')] + [TestCase('Scale2_Zero', '0,2,0.00')] + [TestCase('Scale0_Zero', '0,0,0')] + procedure TestDecimal(const AValue: Int64; AScale: Integer; const AExpectedJsonString: string); + + [Test] + procedure TestEnum; + [Test] + procedure TestRecord; + [Test] + procedure TestTuple; + [Test] + procedure TestArray; + [Test] + procedure TestVector; + [Test] + procedure TestMethodFails; + [Test] + procedure TestNullValue; + end; + +implementation + +uses + System.SysUtils, + System.DateUtils, + System.Math, + Myc.Data.Types, + Myc.Data.Types.JSON; + +{ TTestDataTypesJSON } + +procedure TTestDataTypesJSON.SetUp; +begin + FJson := nil; +end; + +procedure TTestDataTypesJSON.TearDown; +begin + FJson.Free; +end; + +procedure TTestDataTypesJSON.TestVoid; +var + voidType: TDataType.TVoid; + voidValue: TDataType.TVoid.TValue; + deserializedValue: TDataType.TValue; +begin + // Setup + voidType := TDataType.Void; + voidValue := voidType.Value; + + // Serialize + FJson := TDataJsonConverter.ValueToJson(voidValue); + Assert.IsNotNull(FJson, 'JSON value should not be nil'); + Assert.IsTrue(FJson is TJSONNull, 'Void should serialize to JSON Null'); + + // Deserialize + deserializedValue := TDataJsonConverter.JsonToValue(FJson, voidType); + Assert.IsNotNull(deserializedValue.DataValue, 'Deserialized value should not be nil'); + Assert.AreEqual(dkVoid, deserializedValue.DataType.Kind, 'Deserialized type kind should be dkVoid'); +end; + +procedure TTestDataTypesJSON.TestOrdinal(const AValue: Int64); +var + ordType: TDataType.TOrdinal; + ordValue: TDataType.TOrdinal.TValue; + deserializedValue: TDataType.TValue; +begin + // Setup + ordType := TDataType.Ordinal; + ordValue := ordType.CreateValue(AValue); + + // Serialize + FJson := TDataJsonConverter.ValueToJson(ordValue); + Assert.IsTrue(FJson is TJSONNumber, 'Ordinal should serialize to JSON Number'); + Assert.AreEqual(AValue, (FJson as TJSONNumber).AsInt64, 'Serialized value mismatch'); + + // Deserialize + deserializedValue := TDataJsonConverter.JsonToValue(FJson, ordType); + Assert.AreEqual(dkOrdinal, deserializedValue.DataType.Kind, 'Deserialized type kind should be dkOrdinal'); + Assert.AreEqual(AValue, deserializedValue.AsOrdinal.Value, 'Deserialized value mismatch'); +end; + +procedure TTestDataTypesJSON.TestFloat(const AValue: Double); +var + floatType: TDataType.TFloat; + floatValue: TDataType.TFloat.TValue; + deserializedValue: TDataType.TValue; +begin + // Setup + floatType := TDataType.Float; + floatValue := floatType.CreateValue(AValue); + + // Serialize + FJson := TDataJsonConverter.ValueToJson(floatValue); + Assert.IsTrue(FJson is TJSONNumber, 'Float should serialize to JSON Number'); + Assert.AreEqual(AValue, (FJson as TJSONNumber).AsDouble, 1E-9, 'Serialized value mismatch'); + + // Deserialize + deserializedValue := TDataJsonConverter.JsonToValue(FJson, floatType); + Assert.AreEqual(dkFloat, deserializedValue.DataType.Kind, 'Deserialized type kind should be dkFloat'); + Assert.AreEqual(AValue, deserializedValue.AsFloat.Value, 1E-9, 'Deserialized value mismatch'); +end; + +procedure TTestDataTypesJSON.TestText(const AValue: string); +var + textType: TDataType.TText; + textValue: TDataType.TText.TValue; + deserializedValue: TDataType.TValue; +begin + // Setup + textType := TDataType.Text; + textValue := textType.CreateValue(AValue); + + // Serialize + FJson := TDataJsonConverter.ValueToJson(textValue); + Assert.IsTrue(FJson is TJSONString, 'Text should serialize to JSON String'); + Assert.AreEqual(AValue, (FJson as TJSONString).Value, 'Serialized value mismatch'); + + // Deserialize + deserializedValue := TDataJsonConverter.JsonToValue(FJson, textType); + Assert.AreEqual(dkText, deserializedValue.DataType.Kind, 'Deserialized type kind should be dkText'); + Assert.AreEqual(AValue, deserializedValue.AsText.Value, 'Deserialized value mismatch'); +end; + +procedure TTestDataTypesJSON.TestTimestamp; +var + tsType: TDataType.TTimestamp; + tsValue: TDataType.TTimestamp.TValue; + deserializedValue: TDataType.TValue; + testVal: TDateTime; +begin + // Setup + testVal := EncodeDateTime(2025, 8, 27, 17, 30, 15, 500); + tsType := TDataType.Timestamp; + tsValue := tsType.CreateValue(testVal); + + // Serialize + FJson := TDataJsonConverter.ValueToJson(tsValue); + Assert.IsTrue(FJson is TJSONString, 'Timestamp should serialize to JSON String'); + Assert.AreEqual(DateToISO8601(testVal, true), (FJson as TJSONString).Value, 'Serialized value mismatch'); + + // Deserialize + deserializedValue := TDataJsonConverter.JsonToValue(FJson, tsType); + Assert.AreEqual(dkTimestamp, deserializedValue.DataType.Kind, 'Deserialized type kind should be dkTimestamp'); + Assert.IsTrue(CompareDateTime(testVal, deserializedValue.AsTimestamp.Value) = 0, 'Deserialized value mismatch'); +end; + +procedure TTestDataTypesJSON.TestDecimal(const AValue: Int64; AScale: Integer; const AExpectedJsonString: string); +var + decType: TDataType.TDecimal; + decValue: TDataType.TDecimal.TValue; + deserializedValue: TDataType.TValue; +begin + // Setup + decType := TDataType.DecimalOf(AScale); + decValue := decType.CreateValue(AValue); + + // Serialize + FJson := TDataJsonConverter.ValueToJson(decValue); + Assert.IsTrue(FJson is TJSONString, 'Decimal should serialize to JSON String'); + Assert.AreEqual(AExpectedJsonString, (FJson as TJSONString).Value, 'Serialized string value mismatch'); + + // Deserialize + deserializedValue := TDataJsonConverter.JsonToValue(FJson, decType); + Assert.AreEqual(dkDecimal, deserializedValue.DataType.Kind, 'Deserialized type kind should be dkDecimal'); + Assert.AreEqual(AValue, deserializedValue.AsDecimal.Value, 'Deserialized value mismatch'); + Assert.AreEqual(AScale, deserializedValue.AsDecimal.DataType.Scale, 'Deserialized scale mismatch'); + + // Test Deserialization from Number (compatibility) + FJson.Free; // Free previous value before creating new one + FJson := TJSONNumber.Create(AValue / Power(10, AScale)); + deserializedValue := TDataJsonConverter.JsonToValue(FJson, decType); + Assert.AreEqual(AValue, deserializedValue.AsDecimal.Value, 'Deserialized value from number mismatch'); +end; + +procedure TTestDataTypesJSON.TestEnum; +var + enumType: TDataType.TEnum; + enumValue: TDataType.TEnum.TValue; + deserializedValue: TDataType.TValue; +begin + // Setup + enumType := TDataType.EnumOf('MyEnum', ['Red', 'Green', 'Blue']); + enumValue := enumType.CreateValue(1); // Green + + // Serialize + FJson := TDataJsonConverter.ValueToJson(enumValue); + Assert.IsTrue(FJson is TJSONString, 'Enum should serialize to JSON String'); + Assert.AreEqual('Green', (FJson as TJSONString).Value, 'Serialized value mismatch'); + + // Deserialize + deserializedValue := TDataJsonConverter.JsonToValue(FJson, enumType); + Assert.AreEqual(dkEnum, deserializedValue.DataType.Kind, 'Deserialized type kind should be dkEnum'); + Assert.AreEqual(1, deserializedValue.AsEnum.Value, 'Deserialized value mismatch'); +end; + +procedure TTestDataTypesJSON.TestRecord; +var + recordType: TDataType.TRecord; + idVal: TDataType.TOrdinal.TValue; + nameVal: TDataType.TText.TValue; + recordValue: TDataType.TRecord.TValue; + deserializedValue: TDataType.TValue; +begin + // Setup + recordType := TDataType.RecordOf([TDataRecordField.Create('ID', TDataType.Ordinal), TDataRecordField.Create('Name', TDataType.Text)]); + idVal := TDataType.Ordinal.CreateValue(99); + nameVal := TDataType.Text.CreateValue('TestRecord'); + recordValue := recordType.CreateValue([idVal, nameVal]); + + // Serialize + FJson := TDataJsonConverter.ValueToJson(recordValue); + Assert.IsTrue(FJson is TJSONObject, 'Record should serialize to JSON Object'); + + // Deserialize + deserializedValue := TDataJsonConverter.JsonToValue(FJson, recordType); + Assert.AreEqual(dkRecord, deserializedValue.DataType.Kind, 'Deserialized type kind should be dkRecord'); + var desRecord := deserializedValue.AsRecord; + Assert.AreEqual(Int64(99), desRecord.Items[0].AsOrdinal.Value, 'Deserialized record field ID mismatch'); + Assert.AreEqual('TestRecord', desRecord.Items[1].AsText.Value, 'Deserialized record field Name mismatch'); +end; + +procedure TTestDataTypesJSON.TestTuple; +var + tupleType: TDataType.TTuple; + idVal: TDataType.TOrdinal.TValue; + nameVal: TDataType.TText.TValue; + tupleValue: TDataType.TTuple.TValue; + deserializedValue: TDataType.TValue; +begin + // Setup + tupleType := TDataType.TupleOf([TDataType.Ordinal, TDataType.Text]); + idVal := TDataType.Ordinal.CreateValue(101); + nameVal := TDataType.Text.CreateValue('TestTuple'); + tupleValue := tupleType.CreateValue([idVal, nameVal]); + + // Serialize + FJson := TDataJsonConverter.ValueToJson(tupleValue); + Assert.IsTrue(FJson is TJSONArray, 'Tuple should serialize to JSON Array'); + + // Deserialize + deserializedValue := TDataJsonConverter.JsonToValue(FJson, tupleType); + Assert.AreEqual(dkTuple, deserializedValue.DataType.Kind, 'Deserialized type kind should be dkTuple'); + var desTuple := deserializedValue.AsTuple; + Assert.AreEqual(Int64(101), desTuple.Items[0].AsOrdinal.Value, 'Deserialized tuple item 0 mismatch'); + Assert.AreEqual('TestTuple', desTuple.Items[1].AsText.Value, 'Deserialized tuple item 1 mismatch'); +end; + +procedure TTestDataTypesJSON.TestArray; +var + arrayType: TDataType.TArray; + val1, val2: TDataType.TText.TValue; + arrayValue: TDataType.TArray.TValue; + deserializedValue: TDataType.TValue; +begin + // Setup + arrayType := TDataType.ArrayOf(TDataType.Text); + val1 := TDataType.Text.CreateValue('A'); + val2 := TDataType.Text.CreateValue('B'); + arrayValue := arrayType.CreateValue([val1, val2]); + + // Serialize + FJson := TDataJsonConverter.ValueToJson(arrayValue); + Assert.IsTrue(FJson is TJSONArray, 'Array should serialize to JSON Array'); + + // Deserialize + deserializedValue := TDataJsonConverter.JsonToValue(FJson, arrayType); + Assert.AreEqual(dkArray, deserializedValue.DataType.Kind, 'Deserialized type kind should be dkArray'); + var desArray := deserializedValue.AsArray; + Assert.AreEqual(2, desArray.ElementCount, 'Deserialized array element count mismatch'); + Assert.AreEqual('A', desArray.Items[0].AsText.Value, 'Deserialized array item 0 mismatch'); + Assert.AreEqual('B', desArray.Items[1].AsText.Value, 'Deserialized array item 1 mismatch'); +end; + +procedure TTestDataTypesJSON.TestVector; +var + vectorType: TDataType.TVector; + val1, val2: TDataType.TOrdinal.TValue; + vectorValue: TDataType.TVector.TValue; + deserializedValue: TDataType.TValue; + jsonArray: TJSONArray; +begin + // Setup + vectorType := TDataType.VectorOf(TDataType.Ordinal, 2); + val1 := TDataType.Ordinal.CreateValue(10); + val2 := TDataType.Ordinal.CreateValue(20); + vectorValue := vectorType.CreateValue([val1, val2]); + + // Serialize + FJson := TDataJsonConverter.ValueToJson(vectorValue); + Assert.IsTrue(FJson is TJSONArray, 'Vector should serialize to JSON Array'); + + // Deserialize + deserializedValue := TDataJsonConverter.JsonToValue(FJson, vectorType); + Assert.AreEqual(dkVector, deserializedValue.DataType.Kind, 'Deserialized type kind should be dkVector'); + var desVector := deserializedValue.AsVector; + Assert.AreEqual(2, desVector.ElementCount, 'Deserialized vector element count mismatch'); + Assert.AreEqual(Int64(10), desVector.Items[0].AsOrdinal.Value, 'Deserialized vector item 0 mismatch'); + Assert.AreEqual(Int64(20), desVector.Items[1].AsOrdinal.Value, 'Deserialized vector item 1 mismatch'); + + // Test wrong size deserialization + jsonArray := nil; + try + jsonArray := TJSONArray.Create; + jsonArray.Add(1); + Assert.WillRaise( + procedure begin TDataJsonConverter.JsonToValue(jsonArray, vectorType); end, + EConvertError, + 'Deserializing a JSON array with wrong size for a vector should raise an exception' + ); + finally + jsonArray.Free; + end; +end; + +procedure TTestDataTypesJSON.TestMethodFails; +var + methodType: TDataType.TMethod; + methodValue: TDataType.TMethod.TValue; +begin + // Setup + methodType := TDataType.MethodOf(TDataType.Ordinal, TDataType.Ordinal); + methodValue := methodType.CreateValue(function(const AValue: TDataType.TValue): TDataType.TValue begin Result := AValue; end); + + // Test Serialization + Assert.WillRaise( + procedure begin TDataJsonConverter.ValueToJson(methodValue).Free; end, + ENotSupportedException, + 'Serializing a method should raise an exception' + ); + + // Test Deserialization + FJson := TJSONNull.Create; + //TODO Fails with "Method did not throw any exceptions. Deserializing a method should raise an exception" + Assert.WillRaise( + procedure begin TDataJsonConverter.JsonToValue(FJson, methodType); end, + ENotSupportedException, + 'Deserializing a method should raise an exception' + ); +end; + +procedure TTestDataTypesJSON.TestNullValue; +var + value: TDataType.TValue; + deserializedValue: TDataType.TValue; +begin + // Setup + value := TDataType.Void.Value; + + // Serialize + FJson := TDataJsonConverter.ValueToJson(value); + Assert.IsTrue(FJson is TJSONNull, 'nil IDataValue should serialize to JSON Null'); + + // Deserialize + deserializedValue := TDataJsonConverter.JsonToValue(FJson, TDataType.Void); + Assert.IsTrue(deserializedValue.DataType.Kind = dkVoid, 'JSON Null should deserialize to a TValue with a nil DataValue'); +end; + +initialization + TDUnitX.RegisterTestFixture(TTestDataTypesJSON); + +end. diff --git a/Src/Data/TestDataTypes.pas b/Src/Data/TestDataTypes.pas index 31a12a1..c05c8a5 100644 --- a/Src/Data/TestDataTypes.pas +++ b/Src/Data/TestDataTypes.pas @@ -8,7 +8,7 @@ uses type [TestFixture] [IgnoreMemoryLeaks] - TMyTestObject = class + TTestDataTypes = class public [Test] procedure TestRecords; @@ -48,9 +48,10 @@ uses System.Rtti, System.TypInfo, Myc.Data.Types, - Myc.Data.Types.RTTI; + Myc.Data.Types.RTTI, + Myc.Data.Types.JSON; -procedure TMyTestObject.TestRecords; +procedure TTestDataTypes.TestRecords; var intType: TDataType.TOrdinal; floatType: TDataType.TFloat; @@ -129,7 +130,7 @@ begin ); end; -procedure TMyTestObject.TestRecords_Generic; +procedure TTestDataTypes.TestRecords_Generic; var dataType: TDataType.TRecord; dataValue: TDataType.TRecord.TValue; @@ -159,7 +160,7 @@ begin Assert.AreEqual('', IDataValue(dataValue).AsString, 'Generic record AsString is incorrect'); end; -procedure TMyTestObject.TestTuples; +procedure TTestDataTypes.TestTuples; var intValue: TDataType.TOrdinal.TValue; floatValue: TDataType.TFloat.TValue; @@ -193,7 +194,7 @@ begin Assert.AreEqual('Tuple', type1.Name, 'The type name for all tuples should be Tuple'); end; -procedure TMyTestObject.TestTuples_Generic; +procedure TTestDataTypes.TestTuples_Generic; var tupleValue: TDataType.TTuple.TValue; item: TDataType.TOrdinal.TValue; @@ -219,7 +220,7 @@ begin Assert.AreEqual('(1, 2, 3, 4, 5, 6)', IDataValue(tupleValue).AsString, 'Generic tuple AsString is incorrect'); end; -procedure TMyTestObject.TestArrays; +procedure TTestDataTypes.TestArrays; var intType: TDataType.TOrdinal; floatType: TDataType.TFloat; @@ -283,7 +284,7 @@ begin ); end; -procedure TMyTestObject.TestArrays_OptimizedAndGeneric; +procedure TTestDataTypes.TestArrays_OptimizedAndGeneric; var arrayType: TDataType.TArray; empty1, empty2, single, generic: TDataType.TArray.TValue; @@ -316,7 +317,7 @@ begin Assert.AreEqual('[1, 2, 3]', IDataValue(generic).AsString, 'Generic array AsString is incorrect'); end; -procedure TMyTestObject.TestTexts; +procedure TTestDataTypes.TestTexts; var textType: TDataType.TText; textValue1, textValue2, castedValue: TDataType.TText.TValue; @@ -354,7 +355,7 @@ begin ); end; -procedure TMyTestObject.TestTexts_OptimizedEmpty; +procedure TTestDataTypes.TestTexts_OptimizedEmpty; var empty1, empty2: TDataType.TText.TValue; begin @@ -368,7 +369,7 @@ begin Assert.AreEqual('', IDataValue(empty1).AsString, 'AsString of empty text should be empty'); end; -procedure TMyTestObject.TestFloats_OptimizedAndGeneric; +procedure TTestDataTypes.TestFloats_OptimizedAndGeneric; var zero1, zero2, nan1, nan2, generic: TDataType.TFloat.TValue; begin @@ -395,7 +396,7 @@ begin Assert.AreEqual(123.45, generic.Value, 'Value of generic float is incorrect'); end; -procedure TMyTestObject.TestTimestamps; +procedure TTestDataTypes.TestTimestamps; var tsType: TDataType.TTimestamp; tsValue1, tsValue2, castedValue: TDataType.TTimestamp.TValue; @@ -435,7 +436,7 @@ begin ); end; -procedure TMyTestObject.TestEnums; +procedure TTestDataTypes.TestEnums; var colorType: TDataType.TEnum; red, green, blue: TDataType.TEnum.TValue; @@ -490,7 +491,7 @@ begin ); end; -procedure TMyTestObject.TestAsString; +procedure TTestDataTypes.TestAsString; var ordinalValue: TDataType.TOrdinal.TValue; floatValue: TDataType.TFloat.TValue; @@ -542,7 +543,7 @@ begin Assert.AreEqual('(10, Tuple)', IDataValue(tupleValue).AsString, 'Tuple AsString incorrect'); end; -procedure TMyTestObject.TestAsTValue; +procedure TTestDataTypes.TestAsTValue; var dataValue: TDataType.TValue; // Keep as generic interface to test AsTValue now: TDateTime; @@ -551,21 +552,21 @@ begin // This test is specifically for the IDataValue.AsTValue method. No changes needed. // --- Ordinal --- dataValue := TDataType.Ordinal.CreateValue(123); - var tv := dataValue.AsTValue; + var tv := AsTValue(dataValue); Assert.IsFalse(tv.IsEmpty, 'Ordinal TValue should not be empty'); Assert.AreEqual(tkInt64, tv.Kind, 'Ordinal TValue kind should be tkInt64'); Assert.AreEqual(Int64(123), tv.AsInt64, 'Ordinal TValue content is incorrect'); // --- Float --- dataValue := TDataType.Float.CreateValue(45.67); - tv := dataValue.AsTValue; + tv := AsTValue(dataValue); Assert.IsFalse(tv.IsEmpty, 'Float TValue should not be empty'); Assert.AreEqual(tkFloat, tv.Kind, 'Float TValue kind should be tkFloat'); Assert.AreEqual(45.67, tv.AsExtended, 'Float TValue content is incorrect'); // --- Text --- dataValue := TDataType.Text.CreateValue('Hello'); - tv := dataValue.AsTValue; + tv := AsTValue(dataValue); Assert.IsFalse(tv.IsEmpty, 'Text TValue should not be empty'); Assert.AreEqual(tkUString, tv.Kind, 'Text TValue kind should be tkUString'); Assert.AreEqual('Hello', tv.AsString, 'Text TValue content is incorrect'); @@ -573,7 +574,7 @@ begin // --- Timestamp --- now := System.SysUtils.Now; dataValue := TDataType.Timestamp.CreateValue(now); - tv := dataValue.AsTValue; + tv := AsTValue(dataValue); Assert.IsFalse(tv.IsEmpty, 'Timestamp TValue should not be empty'); Assert.AreEqual(tkFloat, tv.Kind, 'Timestamp TValue kind is incorrect'); Assert.AreEqual(now, tv.AsType, 'Timestamp TValue content is incorrect'); @@ -581,13 +582,13 @@ begin // --- Enum --- colorType := TDataType.EnumOf('Color', ['Red', 'Green', 'Blue']); dataValue := colorType.CreateValue('Green'); - tv := dataValue.AsTValue; + tv := AsTValue(dataValue); Assert.IsFalse(tv.IsEmpty, 'Enum TValue should not be empty'); Assert.AreEqual(tkInteger, tv.Kind, 'Enum TValue kind should be tkInteger'); Assert.AreEqual(1, tv.AsInteger, 'Enum TValue content is incorrect'); end; -procedure TMyTestObject.TestMethods; +procedure TTestDataTypes.TestMethods; var ordinalType: TDataType.TOrdinal; textType: TDataType.TText; @@ -658,7 +659,7 @@ begin // --- 5. Test AsString and AsTValue --- Assert.AreEqual('', IDataValue(methodValue).AsString, 'Method AsString is incorrect'); - tv := TDataType.TValue(methodValue).AsTValue; + tv := AsTValue(methodValue); Assert.IsFalse(tv.IsEmpty, 'Method TValue should not be empty'); Assert.AreEqual(tkInterface, tv.Kind, 'TValue kind for a TMethodProc should be tkInterface, as it is a reference-to-function'); Assert.IsTrue(TypeInfo(TDataMethodProc) = tv.TypeInfo, 'TValue should hold TMethodProc type info'); diff --git a/Test/MycTests.dpr b/Test/MycTests.dpr index 686af59..405e360 100644 --- a/Test/MycTests.dpr +++ b/Test/MycTests.dpr @@ -35,7 +35,9 @@ uses Myc.Data.Types.Void in '..\Src\Data\Myc.Data.Types.Void.pas', Myc.Data.Types.Decimal in '..\Src\Data\Myc.Data.Types.Decimal.pas', Myc.Ast in '..\Src\AST\Myc.Ast.pas', - TestAst in '..\Src\AST\TestAst.pas'; + TestAst in '..\Src\AST\TestAst.pas', + Myc.Data.Types.JSON in '..\Src\Data\Myc.Data.Types.JSON.pas', + TestDataTypes.JSON in '..\Src\Data\TestDataTypes.JSON.pas'; { keep comment here to protect the following conditional from being removed by the IDE when adding a unit } {$IFNDEF TESTINSIGHT} diff --git a/Test/MycTests.dproj b/Test/MycTests.dproj index 221d7e1..90efcd5 100644 --- a/Test/MycTests.dproj +++ b/Test/MycTests.dproj @@ -137,6 +137,8 @@ $(PreBuildEvent)]]> + + Base