{ TRJ - JSON Simple Read and Write - v0.9.2 - 2024-09-06 by gale - https://github.com/higale/RJSON } unit rjson; interface uses System.IOUtils, System.Classes, System.SysUtils, System.StrUtils, System.JSON, System.Generics.Collections; type /// Alias for . TJObject = TJSONObject; /// Alias for . TJArray = TJSONArray; /// Alias for . TJValue = TJSONValue; /// Alias for . TJString = TJSONString; /// Alias for . TJNumber = TJSONNumber; /// Alias for . TJBool = TJSONBool; /// Alias for . TJNull = TJSONNull; /// /// Type alias for the class. /// TJVType = type of TJSONValue; /// /// Interface for the root of a JSON structure. /// IRJRoot = interface function GetData: TJSONValue; procedure SetData(const AValue: TJSONValue); function ForceJV(AType: TJVType): TJSONValue; property Data: TJSONValue read GetData write SetData; end; /// /// Class representing the root of a JSON structure, implementing the interface. /// TRJRoot = class(TInterfacedObject, IRJRoot) private FData: TJSONValue; function GetData: TJSONValue; procedure SetData(const AValue: TJSONValue); function ForceJV(AType: TJVType): TJSONValue; public constructor Create; overload; constructor Create(const AValue: TJSONValue); overload; destructor Destroy; override; end; TRJEnumerator = class; /// /// Encapsulating common JSON data operation functionalities.< /// TRJ = record private FRoot: IRJRoot; FPath: string; function LinkPath(const ALeft, ARight: string): string; function GetJSONValue: TJSONValue; inline; function GetItems(const APath: string): TRJ; overload; function GetItems(AIndex: Integer): TRJ; overload; inline; function GetPairs(AIndex: Integer): TRJ; procedure SetValue(const [ref] AValue: TRJ); procedure SetItems(const APath: string; const [ref] AValue: TRJ); overload; procedure SetItems(AIndex: Integer; const [ref] AValue: TRJ); overload; inline; function GetCount: Integer; function GetIndex: Integer; function GetKey: string; //function GetIsNil: Boolean; public function GetEnumerator(): TRJEnumerator; class operator Initialize(out Dest: TRJ); class operator Finalize(var Dest: TRJ); class operator Assign(var Dest: TRJ; const [ref] Src: TRJ); class operator Implicit(const Value: string): TRJ; class operator Implicit(const [ref] Value: TRJ): string; class operator Implicit(Value: Integer): TRJ; class operator Implicit(const [ref] Value: TRJ): Integer; class operator Implicit(Value: Int64): TRJ; class operator Implicit(const [ref] Value: TRJ): Int64; class operator Implicit(Value: Extended): TRJ; class operator Implicit(const [ref] Value: TRJ): Extended; class operator Implicit(Value: Boolean): TRJ; class operator Implicit(const [ref] Value: TRJ): Boolean; class operator Implicit(const Value: TJSONValue): TRJ; /// Attempts to convert an object to a string representation. /// A default value to return if the conversion fails. /// The converted string value, or the specified default value if the conversion fails. function ToStr(const ADefault: string = ''): string; /// Attempts to convert an object to an integer type. /// A default value to return if the conversion fails. /// The converted integer value, or the specified default value if the conversion fails. function ToInt(ADefault: Integer = 0): Integer; /// Attempts to convert an object to a 64-bit integer type. /// A default value to return if the conversion fails. /// The converted 64-bit integer value, or the specified default value if the conversion fails. function ToInt64(ADefault: Int64 = 0): Int64; /// Attempts to convert an object to a floating-point number. /// A default value to return if the conversion fails. /// The converted floating-point value, or the specified default value if the conversion fails. function ToFloat(ADefault: Extended = 0.0): Extended; /// Attempts to convert an object to a boolean value. /// A default value to return if the conversion fails. /// The converted boolean value, or the specified default value if the conversion fails. function ToBool(ADefault: Boolean = False): Boolean; /// Gets or sets the item at the specified path. /// The path of the item. /// The item at the specified path. property Items[const APath: string]: TRJ read GetItems write SetItems; default; /// Gets or sets the item at the specified index. /// The index of the item. /// The item at the specified index. property Items[AIndex: Integer]: TRJ read GetItems write SetItems; default; /// Gets the pair at the specified index. /// The index of the pair. /// The pair at the specified index. property Pairs[AIndex: Integer]: TRJ read GetPairs; /// Gets the count of items. /// The count of items. property Count: Integer read GetCount; /// Gets the current index. /// The current index. property Index: Integer read GetIndex; /// Gets the key of the current item. /// The key of the current item. property Key: string read GetKey; /// Gets the root of the JSON structure. /// The root of the JSON structure. property Root: IRJRoot read FRoot; /// Gets the current path in the JSON structure. /// The current path in the JSON structure. property Path: string read FPath; /// Determines whether the root is of the specified JSON value type. /// The type of JSON value to check against. /// true if the root is of the specified type; otherwise, false. function RootIs: Boolean; /// Determines whether the value is of the specified JSON value type. /// The type of JSON value to check against. /// true if the value is of the specified type; otherwise, false. function ValueIs: Boolean; /// Gets the JSON value. /// The JSON value. property JSONValue: TJSONValue read GetJSONValue; /// Creates a clone of the JSON value. /// A clone of the JSON value. function CloneJSONValue: TJSONValue; /// Resets the JSON value to its initial state. procedure Reset; /// Formats the JSON value as a string with optional indentation. /// The number of spaces to use for indentation. Defaults to 4. /// The formatted JSON string. function Format(Indentation: Integer = 4): string; /// Parses the given JSON data string into a JSON value. /// The JSON data string to parse. /// Indicates whether to use boolean values for parsing. Defaults to false. /// Indicates whether to raise an exception on parsing errors. Defaults to false. procedure ParseJSONValue(const AData: string; AUseBool: Boolean = False; ARaiseExc: Boolean = False); /// Loads JSON data from a file and parses it into a JSON value. /// The name of the file to load JSON data from. /// Indicates whether to use boolean values for parsing. Defaults to false. /// Indicates whether to raise an exception on parsing errors. Defaults to false. procedure LoadFromFile(const AFileName: string; AUseBool: Boolean = False; ARaiseExc: Boolean = False); /// Saves the JSON value to a file with optional formatting. /// The name of the file to save the JSON data to. /// The number of spaces to use for indentation. Defaults to 4. /// Indicates whether to write a byte order mark (BOM) at the beginning of the file. Defaults to false. /// Indicates whether to add a trailing line break at the end of the file. Defaults to false. procedure SaveToFile(const AFileName: string; AIndentation: Integer = 4; AWriteBOM: Boolean = False; ATrailingLineBreak: Boolean = False); end; { Iterators } TRJEnumerator = class private FPData: ^TRJ; FIndex: Integer; function GetCurrent: TRJ; public constructor Create(const [ref] AData: TRJ); function MoveNext: Boolean; property Current: TRJ read GetCurrent; end; implementation { ============================================================================ } { TRJRoot } constructor TRJRoot.Create; begin inherited; FData := nil; end; constructor TRJRoot.Create(const AValue: TJSONValue); begin inherited Create; FData := AValue; end; destructor TRJRoot.Destroy; begin FData.Free; inherited; end; function TRJRoot.GetData: TJSONValue; begin Result := FData; end; procedure TRJRoot.SetData(const AValue: TJSONValue); begin FData := AValue; end; function TRJRoot.ForceJV(AType: TJVType): TJSONValue; begin if not(FData is AType) then begin FData.Free; FData := AType.Create; end; Result := FData; end; { TRJRoot } { ============================================================================ } { TJSONValueHelper } type TJSONValueHelper = class helper for TJSONValue private procedure ObjSetItem(const AName: string; const AValue: TJSONValue); procedure ArrFill(ACount: Integer); procedure ArrInsert(const AIndex: Integer; const AValue: TJSONValue); procedure ArrSetItem(AIndex: Integer; const AValue: TJSONValue); function ToType(ADefault: T): T; function GetOrCreate(AName: string): T; procedure SetValue(const APath: string; const AValue: TJSONValue); end; procedure TJSONValueHelper.ObjSetItem(const AName: string; const AValue: TJSONValue); var pairTmp: TJSONPair; begin pairTmp := TJSONObject(self).Get(AName); if pairTmp = nil then TJSONObject(self).AddPair(AName, AValue) else pairTmp.JSONValue := AValue; end; procedure TJSONValueHelper.ArrFill(ACount: Integer); begin for var j := TJSONArray(self).Count to ACount do TJSONArray(self).AddElement(T.Create); end; procedure TJSONValueHelper.ArrInsert(const AIndex: Integer; const AValue: TJSONValue); begin TJSONArray(self).AddElement(AValue); for var i := AIndex to TJSONArray(self).Count - 2 do TJSONArray(self).AddElement(TJSONArray(self).Remove(AIndex)); end; procedure TJSONValueHelper.ArrSetItem(AIndex: Integer; const AValue: TJSONValue); begin ArrFill(AIndex - 1); if AIndex <= TJSONArray(self).Count - 1 then TJSONArray(self).Remove(AIndex).Free; ArrInsert(AIndex, AValue); end; procedure TJSONValueHelper.SetValue(const APath: string; const AValue: TJSONValue); var LParser: TJSONPathParser; preName: string; jv: TJSONValue; begin if APath.IsEmpty then raise Exception.Create('TJSONValueHelper.SetValue: path cannot be empty'); jv := self; LParser := TJSONPathParser.Create(APath); LParser.NextToken; while true do begin preName := LParser.TokenName; LParser.NextToken; case LParser.Token of TJSONPathParser.TToken.Name: jv := jv.GetOrCreate(preName); TJSONPathParser.TToken.ArrayIndex: jv := jv.GetOrCreate(preName); TJSONPathParser.TToken.Eof: begin if jv is TJSONObject then jv.ObjSetItem(preName, AValue) else jv.ArrSetItem(preName.ToInteger, AValue); break; end; else raise Exception.Create('TJSONValueHelper.SetValue, LParser.Token Error!'); end; end; end; function TJSONValueHelper.ToType(ADefault: T): T; begin if self = nil then Exit(ADefault); try Result := AsType; except Result := ADefault; end; end; function TJSONValueHelper.GetOrCreate(AName: string): T; begin if self is TJSONObject then begin Result := T(TJSONObject(self).GetValue(AName)); if not(Result is T) then begin Result := T.Create; ObjSetItem(AName, Result); end; end else if self is TJSONArray then begin ArrFill(AName.ToInteger); Result := T(TJSONArray(self).Items[AName.ToInteger]); if not(Result is T) then begin Result := T.Create; ArrSetItem(AName.ToInteger, Result); end; end else begin raise Exception.Create('GetOrCreate Error, self must be TJO or TJA'); end; end; { TJSONValueHelper } { ============================================================================ } { TRJEnumerator } constructor TRJEnumerator.Create(const [ref] AData: TRJ); begin inherited Create; FPData := @AData; FIndex := -1; end; function TRJEnumerator.GetCurrent: TRJ; var jvTmp: TJSONValue; begin Result.Reset; Result.FRoot := FPData^.FRoot; jvTmp := FPData^.GetJSONValue; if jvTmp is TJSONObject then begin if FPData^.FPath = '' then Result.FPath := TJSONObject(jvTmp).Pairs[FIndex].JsonString.Value else Result.FPath := FPData^.FPath + '.' + TJSONObject(jvTmp).Pairs[FIndex].JsonString.Value; end else if jvTmp is TJSONArray then begin Result.FPath := FPData^.FPath + '[' + FIndex.ToString + ']'; end; end; function TRJEnumerator.MoveNext: Boolean; begin Inc(FIndex); Exit(FIndex < FPData^.Count) end; { TRJEnumerator } { ============================================================================ } { TRJ } function TRJ.GetEnumerator(): TRJEnumerator; begin Result := TRJEnumerator.Create(self); end; class operator TRJ.Initialize(out Dest: TRJ); begin Dest.FRoot := TRJRoot.Create; Dest.FPath := ''; end; class operator TRJ.Finalize(var Dest: TRJ); begin Dest.FRoot := nil; end; function TRJ.LinkPath(const ALeft, ARight: string): string; begin if ALeft.IsEmpty then Result := ARight else if ARight.IsEmpty then Result := ALeft else if ARight.StartsWith('[') then Result := ALeft + ARight else Result := ALeft + '.' + ARight; end; function TRJ.GetJSONValue: TJSONValue; begin Result := FRoot.Data.FindValue(FPath); end; function TRJ.CloneJSONValue: TJSONValue; begin Result := GetJSONValue; if Result <> nil then Result := Result.Clone as TJSONValue else Result := TJSONNull.Create; end; class operator TRJ.Assign(var Dest: TRJ; const [ref] Src: TRJ); begin if Dest.FPath.IsEmpty then begin Dest.FRoot := Src.FRoot; Dest.FPath := Src.FPath; end else begin Dest.SetValue(Src); end; end; class operator TRJ.Implicit(const Value: string): TRJ; begin Result.FRoot.Data := TJSONString.Create(Value); end; class operator TRJ.Implicit(const [ref] Value: TRJ): string; begin Result := Value.ToStr(''); end; class operator TRJ.Implicit(Value: Integer): TRJ; begin Result.FRoot.Data := TJSONNumber.Create(Value); end; class operator TRJ.Implicit(const [ref] Value: TRJ): Integer; begin Result := Value.ToInt(0); end; class operator TRJ.Implicit(Value: Int64): TRJ; begin Result.FRoot.Data := TJSONNumber.Create(Value); end; class operator TRJ.Implicit(const [ref] Value: TRJ): Int64; begin Result := Value.ToInt64(0); end; class operator TRJ.Implicit(Value: Extended): TRJ; begin Result.FRoot.Data := TJSONNumber.Create(Value); end; class operator TRJ.Implicit(const [ref] Value: TRJ): Extended; begin Result := Value.ToFloat(0.0); end; class operator TRJ.Implicit(Value: Boolean): TRJ; begin Result.FRoot.Data := TJSONBool.Create(Value); end; class operator TRJ.Implicit(const [ref] Value: TRJ): Boolean; begin Result := Value.ToBool(False); end; class operator TRJ.Implicit(const Value: TJSONValue): TRJ; begin Result.FRoot.Data := Value; end; function TRJ.ToStr(const ADefault: string): string; begin Result := FRoot.Data.FindValue(FPath).ToType(ADefault); end; function TRJ.ToInt(ADefault: Integer = 0): Integer; begin Result := FRoot.Data.FindValue(FPath).ToType(ADefault); end; function TRJ.ToInt64(ADefault: Int64 = 0): Int64; begin Result := FRoot.Data.FindValue(FPath).ToType(ADefault); end; function TRJ.ToFloat(ADefault: Extended = 0.0): Extended; begin Result := FRoot.Data.FindValue(FPath).ToType(ADefault); end; function TRJ.ToBool(ADefault: Boolean = False): Boolean; begin Result := FRoot.Data.FindValue(FPath).ToType(ADefault); end; function TRJ.GetItems(const APath: string): TRJ; begin Result.FRoot := FRoot; Result.FPath := LinkPath(FPath, APath); end; function TRJ.GetItems(AIndex: Integer): TRJ; begin Result := GetItems('[' + AIndex.ToString + ']'); end; function TRJ.GetPairs(AIndex: Integer): TRJ; var jvTmp: TJSONValue; begin jvTmp := GetJSONValue; if (jvTmp is TJSONObject) then Result := GetItems(TJSONObject(jvTmp).Pairs[AIndex].JsonString.Value); end; procedure TRJ.SetValue(const [ref] AValue: TRJ); var LValue: TJSONValue; begin if FPath.IsEmpty then raise Exception.Create(' TRJ.SetValue: Path is empty'); LValue := AValue.CloneJSONValue; try if FPath.StartsWith('[') then FRoot.ForceJV(TJSONArray).SetValue(FPath, LValue) else FRoot.ForceJV(TJSONObject).SetValue(FPath, LValue); except on E: Exception do begin LValue.Free; raise Exception.Create(E.message); end; end; end; procedure TRJ.SetItems(const APath: string; const [ref] AValue: TRJ); var tmp: TRJ; begin tmp.FRoot := FRoot; tmp.FPath := LinkPath(FPath, APath); tmp.SetValue(AValue) end; procedure TRJ.SetItems(AIndex: Integer; const [ref] AValue: TRJ); begin SetItems('[' + AIndex.ToString + ']', AValue); end; function TRJ.GetCount: Integer; var jvTemp: TJSONValue; begin jvTemp := GetJSONValue; if jvTemp is TJSONArray then Result := TJSONArray(jvTemp).Count else if jvTemp is TJSONObject then Result := TJSONObject(jvTemp).Count else Result := 0; end; function TRJ.GetIndex: Integer; var strTmp: string; begin Result := -1; strTmp := FPath.Substring(FPath.LastIndexOf('[') + 1); if strTmp.EndsWith(']') then Result := StrToIntDef(strTmp.TrimRight([']']), -1); end; function TRJ.GetKey: string; begin Result := FPath.Substring(FPath.LastIndexOf('.') + 1); if Result.EndsWith(']') then Result := ''; end; function TRJ.RootIs: Boolean; begin Result := FRoot.Data is T; end; function TRJ.ValueIs: Boolean; begin Result := GetJSONValue is T; end; procedure TRJ.Reset; begin FRoot := TRJRoot.Create; FPath := ''; end; function TRJ.Format(Indentation: Integer): string; var LValue: TJSONValue; begin Result := ''; LValue := GetJSONValue; if LValue <> nil then begin if Indentation > 0 then Result := LValue.Format(Indentation) else Result := LValue.ToString; end; end; procedure TRJ.ParseJSONValue(const AData: string; AUseBool: Boolean; ARaiseExc: Boolean); begin Reset; FRoot.Data := TJSONValue.ParseJSONValue(AData, AUseBool, ARaiseExc); end; procedure TRJ.LoadFromFile(const AFileName: string; AUseBool: Boolean; ARaiseExc: Boolean); begin ParseJSONValue(TFile.ReadAllText(AFileName, TEncoding.UTF8), AUseBool, ARaiseExc); end; procedure TRJ.SaveToFile(const AFileName: string; AIndentation: Integer; AWriteBOM: Boolean; ATrailingLineBreak: Boolean); var strs: TStrings; begin strs := TStringList.Create; try strs.WriteBOM := AWriteBOM; strs.TrailingLineBreak := ATrailingLineBreak; strs.Text := Format(AIndentation); strs.SaveToFile(AFileName, TEncoding.UTF8); finally strs.Free; end; end; { TRJ } { ============================================================================ } end.