From 3268748c0369d3a479b83a66993199d7c7be85b5 Mon Sep 17 00:00:00 2001 From: Michael Schimmel Date: Wed, 27 Aug 2025 18:33:25 +0200 Subject: [PATCH] Types-JSON --- Src/Data/Myc.Data.Types.JSON.pas | 520 +++++++++++++++++++------------ Src/Data/TestDataTypes.JSON.pas | 1 - 2 files changed, 313 insertions(+), 208 deletions(-) diff --git a/Src/Data/Myc.Data.Types.JSON.pas b/Src/Data/Myc.Data.Types.JSON.pas index 22d76d0..e46a39c 100644 --- a/Src/Data/Myc.Data.Types.JSON.pas +++ b/Src/Data/Myc.Data.Types.JSON.pas @@ -10,6 +10,31 @@ uses type // Provides methods for serializing and deserializing data values to/from JSON using the TDataType helpers. TDataJsonConverter = class + private + // Serialization helpers for specific data types + class function OrdinalToJson(const Value: TDataType.TOrdinal.TValue): TJSONValue; static; + class function FloatToJson(const Value: TDataType.TFloat.TValue): TJSONValue; static; + class function TextToJson(const Value: TDataType.TText.TValue): TJSONValue; static; + class function TimestampToJson(const Value: TDataType.TTimestamp.TValue): TJSONValue; static; + class function DecimalToJson(const Value: TDataType.TDecimal.TValue): TJSONValue; static; + class function EnumToJson(const Value: TDataType.TEnum.TValue): TJSONValue; static; + class function RecordToJson(const Value: TDataType.TRecord.TValue): TJSONObject; static; + class function TupleToJson(const Value: TDataType.TTuple.TValue): TJSONArray; static; + class function ArrayToJson(const Value: TDataType.TArray.TValue): TJSONArray; static; + class function VectorToJson(const Value: TDataType.TVector.TValue): TJSONArray; static; + + // Deserialization helpers for specific data types + class function JsonToOrdinal(const Json: TJSONValue; const DataType: TDataType): TDataType.TOrdinal.TValue; static; + class function JsonToFloat(const Json: TJSONValue; const DataType: TDataType): TDataType.TFloat.TValue; static; + class function JsonToText(const Json: TJSONValue; const DataType: TDataType): TDataType.TText.TValue; static; + class function JsonToTimestamp(const Json: TJSONValue; const DataType: TDataType): TDataType.TTimestamp.TValue; static; + class function JsonToDecimal(const Json: TJSONValue; const DataType: TDataType): TDataType.TDecimal.TValue; static; + class function JsonToEnum(const Json: TJSONValue; const DataType: TDataType): TDataType.TEnum.TValue; static; + class function JsonToRecord(const Json: TJSONObject; const DataType: TDataType): TDataType.TRecord.TValue; static; + class function JsonToTuple(const Json: TJSONArray; const DataType: TDataType): TDataType.TTuple.TValue; static; + class function JsonToArray(const Json: TJSONArray; const DataType: TDataType): TDataType.TArray.TValue; static; + class function JsonToVector(const Json: TJSONArray; const DataType: TDataType): TDataType.TVector.TValue; static; + public // Serializes a data value to a TJSONValue. class function ValueToJson(const Value: TDataType.TValue): TJSONValue; @@ -26,92 +51,290 @@ uses System.DateUtils, System.Math; -{ TDataJsonConverter } +class function TDataJsonConverter.OrdinalToJson(const Value: TDataType.TOrdinal.TValue): TJSONValue; +begin + Result := TJSONNumber.Create(Value.Value); +end; -class function TDataJsonConverter.ValueToJson(const Value: TDataType.TValue): TJSONValue; +class function TDataJsonConverter.FloatToJson(const Value: TDataType.TFloat.TValue): TJSONValue; +begin + Result := TJSONNumber.Create(Value.Value); +end; + +class function TDataJsonConverter.TextToJson(const Value: TDataType.TText.TValue): TJSONValue; +begin + Result := TJSONString.Create(Value.Value); +end; + +class function TDataJsonConverter.TimestampToJson(const Value: TDataType.TTimestamp.TValue): TJSONValue; +begin + Result := TJSONString.Create(DateToISO8601(Value.Value, true)); +end; + +class function TDataJsonConverter.DecimalToJson(const Value: TDataType.TDecimal.TValue): TJSONValue; +var + val: Int64; + scale: Integer; + s: string; +begin + // Serialize as string to preserve precision. + val := Value.Value; + scale := Value.DataType.Scale; + + 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; + +class function TDataJsonConverter.EnumToJson(const Value: TDataType.TEnum.TValue): TJSONValue; +begin + Result := TJSONString.Create(Value.DataType.Identifiers[Value.Value]); +end; + +class function TDataJsonConverter.RecordToJson(const Value: TDataType.TRecord.TValue): TJSONObject; +var + i: Integer; + recordType: TDataType.TRecord; +begin + recordType := Value.DataType; + Result := TJSONObject.Create; + for i := 0 to recordType.FieldCount - 1 do + Result.AddPair(recordType.Fields[i].Name, ValueToJson(Value.Items[i])); +end; + +class function TDataJsonConverter.TupleToJson(const Value: TDataType.TTuple.TValue): TJSONArray; var - jsonObject: TJSONObject; - jsonArray: TJSONArray; i: Integer; begin - if not Assigned(Value.DataValue) then - begin - Result := TJSONNull.Create; - Exit; - end; + Result := TJSONArray.Create; + for i := 0 to Value.ItemCount - 1 do + Result.AddElement(ValueToJson(Value.Items[i])); +end; +class function TDataJsonConverter.ArrayToJson(const Value: TDataType.TArray.TValue): TJSONArray; +var + i: Integer; +begin + Result := TJSONArray.Create; + for i := 0 to Value.ElementCount - 1 do + Result.AddElement(ValueToJson(Value.Items[i])); +end; + +class function TDataJsonConverter.VectorToJson(const Value: TDataType.TVector.TValue): TJSONArray; +var + i: Integer; +begin + Result := TJSONArray.Create; + for i := 0 to Value.ElementCount - 1 do + Result.AddElement(ValueToJson(Value.Items[i])); +end; + +{ TDataJsonConverter - Deserialization Helpers } + +class function TDataJsonConverter.JsonToOrdinal(const Json: TJSONValue; const DataType: TDataType): TDataType.TOrdinal.TValue; +var + ordinalType: TDataType.TOrdinal; +begin + Assert(DataType.Kind = dkOrdinal); + ordinalType := DataType.AsOrdinal; + Result := ordinalType.CreateValue((Json as TJSONNumber).AsInt64); +end; + +class function TDataJsonConverter.JsonToFloat(const Json: TJSONValue; const DataType: TDataType): TDataType.TFloat.TValue; +var + floatType: TDataType.TFloat; +begin + Assert(DataType.Kind = dkFloat); + floatType := DataType.AsFloat; + Result := floatType.CreateValue((Json as TJSONNumber).AsDouble); +end; + +class function TDataJsonConverter.JsonToText(const Json: TJSONValue; const DataType: TDataType): TDataType.TText.TValue; +var + textType: TDataType.TText; +begin + Assert(DataType.Kind = dkText); + textType := DataType.AsText; + Result := textType.CreateValue((Json as TJSONString).Value); +end; + +class function TDataJsonConverter.JsonToTimestamp(const Json: TJSONValue; const DataType: TDataType): TDataType.TTimestamp.TValue; +var + timestampType: TDataType.TTimestamp; +begin + Assert(DataType.Kind = dkTimestamp); + timestampType := DataType.AsTimestamp; + Result := timestampType.CreateValue(ISO8601ToDate((Json as TJSONString).Value, True)); +end; + +class function TDataJsonConverter.JsonToDecimal(const Json: TJSONValue; const DataType: TDataType): TDataType.TDecimal.TValue; +var + val: Int64; + i: Integer; + decimalType: TDataType.TDecimal; +begin + Assert(DataType.Kind = dkDecimal); + decimalType := DataType.AsDecimal; + + // Handle string representation for precision, with fallback to number for compatibility. + 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; + +class function TDataJsonConverter.JsonToEnum(const Json: TJSONValue; const DataType: TDataType): TDataType.TEnum.TValue; +var + idx: Integer; + enumType: TDataType.TEnum; +begin + Assert(DataType.Kind = dkEnum); + 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; + +class function TDataJsonConverter.JsonToRecord(const Json: TJSONObject; const DataType: TDataType): TDataType.TRecord.TValue; +var + items: TArray; + i: Integer; + recordType: TDataType.TRecord; +begin + Assert(DataType.Kind = dkRecord); + recordType := DataType.AsRecord; + + SetLength(items, recordType.FieldCount); + for i := 0 to recordType.FieldCount - 1 do + begin + var field := recordType.Fields[i]; + var jsonPair := Json.Get(field.Name); + if Assigned(jsonPair) then + items[i] := JsonToValue(jsonPair.JsonValue, field.DataType); + end; + Result := recordType.CreateValue(items); +end; + +class function TDataJsonConverter.JsonToTuple(const Json: TJSONArray; const DataType: TDataType): TDataType.TTuple.TValue; +var + items: TArray; + i: Integer; + tupleType: TDataType.TTuple; +begin + Assert(DataType.Kind = dkTuple); + tupleType := DataType.AsTuple; + + if (Json.Count <> tupleType.ItemCount) then + raise EConvertError.CreateFmt('JSON array size (%d) does not match tuple size (%d)', [Json.Count, tupleType.ItemCount]); + + SetLength(items, tupleType.ItemCount); + for i := 0 to tupleType.ItemCount - 1 do + items[i] := JsonToValue(Json.Items[i], tupleType.ItemType[i]); + Result := tupleType.CreateValue(items); +end; + +class function TDataJsonConverter.JsonToArray(const Json: TJSONArray; const DataType: TDataType): TDataType.TArray.TValue; +var + items: TArray; + i: Integer; + arrayType: TDataType.TArray; +begin + Assert(DataType.Kind = dkArray); + arrayType := DataType.AsArray; + + SetLength(items, Json.Count); + for i := 0 to Json.Count - 1 do + items[i] := JsonToValue(Json.Items[i], arrayType.ElementType); + Result := arrayType.CreateValue(items); +end; + +class function TDataJsonConverter.JsonToVector(const Json: TJSONArray; const DataType: TDataType): TDataType.TVector.TValue; +var + items: TArray; + i: Integer; + vectorType: TDataType.TVector; +begin + Assert(DataType.Kind = dkVector); + vectorType := DataType.AsVector; + + if (Json.Count <> vectorType.ElementCount) then + raise EConvertError.CreateFmt('JSON array size (%d) does not match vector size (%d)', [Json.Count, vectorType.ElementCount]); + + SetLength(items, Json.Count); + for i := 0 to Json.Count - 1 do + items[i] := JsonToValue(Json.Items[i], vectorType.ElementType); + Result := vectorType.CreateValue(items); +end; + +{ TDataJsonConverter - Public Methods } + +class function TDataJsonConverter.ValueToJson(const Value: TDataType.TValue): TJSONValue; +begin 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; + dkOrdinal: Result := OrdinalToJson(Value.AsOrdinal); + dkFloat: Result := FloatToJson(Value.AsFloat); + dkText: Result := TextToJson(Value.AsText); + dkTimestamp: Result := TimestampToJson(Value.AsTimestamp); + dkDecimal: Result := DecimalToJson(Value.AsDecimal); + dkEnum: Result := EnumToJson(Value.AsEnum); + dkRecord: Result := RecordToJson(Value.AsRecord); + dkTuple: Result := TupleToJson(Value.AsTuple); + dkArray: Result := ArrayToJson(Value.AsArray); + dkVector: Result := VectorToJson(Value.AsVector); 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))]); @@ -119,136 +342,19 @@ begin 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; + dkVoid: Result := TDataType.Void.Value; + dkOrdinal: Result := JsonToOrdinal(Json, DataType); + dkFloat: Result := JsonToFloat(Json, DataType); + dkText: Result := JsonToText(Json, DataType); + dkTimestamp: Result := JsonToTimestamp(Json, DataType); + dkDecimal: Result := JsonToDecimal(Json, DataType); + dkEnum: Result := JsonToEnum(Json, DataType); + dkRecord: Result := JsonToRecord(Json as TJSONObject, DataType); + dkTuple: Result := JsonToTuple(Json as TJSONArray, DataType); + dkArray: Result := JsonToArray(Json as TJSONArray, DataType); + dkVector: Result := JsonToVector(Json as TJSONArray, DataType); 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))]); diff --git a/Src/Data/TestDataTypes.JSON.pas b/Src/Data/TestDataTypes.JSON.pas index 253d3cd..38d1424 100644 --- a/Src/Data/TestDataTypes.JSON.pas +++ b/Src/Data/TestDataTypes.JSON.pas @@ -381,7 +381,6 @@ begin // 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,