delphimvcframework/lib/dmustache/mormot.core.json.pas

11905 lines
396 KiB
ObjectPascal
Raw Normal View History

2024-04-29 15:40:45 +02:00
/// Framework Core Low-Level JSON Processing
// - this unit is a part of the Open Source Synopse mORMot framework 2,
// licensed under a MPL/GPL/LGPL three license - see LICENSE.md
unit mormot.core.json;
{
*****************************************************************************
JSON functions shared by all framework units
- Low-Level JSON Processing Functions
- TJsonWriter class with proper JSON escaping and WriteObject() support
- JSON-aware TSynNameValue TSynPersistentStoreJson
- JSON-aware TSynDictionary Storage
- JSON Unserialization for any kind of Values
- JSON Serialization Wrapper Functions
- Abstract Classes with Auto-Create-Fields
*****************************************************************************
}
interface
{$I mormot.defines.inc}
uses
classes,
contnrs,
sysutils,
{$ifdef ISDELPHI}
typinfo, // for proper Delphi inlining
{$endif ISDELPHI}
mormot.core.base,
mormot.core.os,
mormot.core.unicode,
mormot.core.text,
mormot.core.datetime,
mormot.core.rtti,
mormot.core.buffers,
mormot.core.data;
{ ********** Low-Level JSON Processing Functions }
type
/// exception raised by this unit, in relation to raw JSON process
EJsonException = class(ESynException);
/// kind of first character used from JSON_TOKENS[] for efficient JSON parsing
TJsonToken = (
jtNone,
jtDoubleQuote,
jtFirstDigit,
jtNullFirstChar,
jtTrueFirstChar,
jtFalseFirstChar,
jtObjectStart,
jtArrayStart,
jtObjectStop,
jtArrayStop,
jtAssign,
jtComma,
jtSingleQuote,
jtEqual,
jtIdentifierFirstChar,
jtSlash,
jtEndOfBuffer);
/// defines a lookup table used for branch-less first char JSON parsing
TJsonTokens = array[AnsiChar] of TJsonToken;
/// points to a lookup table used for branch-less first char JSON parsing
PJsonTokens = ^TJsonTokens;
/// kind of character used from JSON_CHARS[] for efficient JSON parsing
// - using such a set compiles into TEST [MEM], IMM so is more efficient
// than a regular set of AnsiChar which generates much slower BT [MEM], IMM
// - the same 256-byte memory will also be reused from L1 CPU cache
// during the parsing of complex JSON input
// - TTestCoreProcess.JSONBenchmark shows around 900MB/s on my i5 notebook
TJsonChar = set of (
jcJsonIdentifierFirstChar,
jcJsonIdentifier,
jcEndOfJsonFieldOr0,
jcEndOfJsonFieldNotName,
jcEndOfJsonValueField,
jcJsonStringMarker,
jcDigitFirstChar,
jcDigitFloatChar);
/// defines a lookup table used for branch-less JSON parsing
TJsonCharSet = array[AnsiChar] of TJsonChar;
/// points to a lookup table used for branch-less JSON parsing
PJsonCharSet = ^TJsonCharSet;
const
/// JSON_ESCAPE[] lookup value: indicates no escape needed
JSON_ESCAPE_NONE = 0;
/// JSON_ESCAPE[] lookup value: indicates #0 (end of string)
JSON_ESCAPE_ENDINGZERO = 1;
/// JSON_ESCAPE[] lookup value: should be escaped as \u00xx
JSON_ESCAPE_UNICODEHEX = 2;
/// JSON_UNESCAPE[] lookup value: indicates #0 or unexpected control char
JSON_UNESCAPE_UNEXPECTED = #0;
/// JSON_UNESCAPE[] lookup value: indicates '\u0123' UTF-16 pattern
JSON_UNESCAPE_UTF16 = #1;
var
/// 256-byte lookup table for fast branchless initial character JSON parsing
JSON_TOKENS: TJsonTokens;
/// 256-byte lookup table for fast branchless JSON parsing
// - to be used e.g. as:
// ! if jvJsonIdentifier in JSON_CHARS[P^] then ...
JSON_CHARS: TJsonCharSet;
/// 256-byte lookup table for fast branchless JSON text escaping
// - 0 = JSON_ESCAPE_NONE indicates no escape needed
// - 1 = JSON_ESCAPE_ENDINGZERO indicates #0 (end of string)
// - 2 = JSON_ESCAPE_UNICODEHEX should be escaped as \u00xx
// - b,t,n,f,r,\," as escaped character for #8,#9,#10,#12,#13,\,"
JSON_ESCAPE: array[byte] of byte;
/// 256-byte lookup table for fast branchless JSON text un-escaping
// - #0 = JSON_UNESCAPE_UNEXPECTED for unexpected #0 or control char
// - #1 = JSON_UNESCAPE_UTF16 for '\u0123' UTF-16 pattern
// - #8,#9,#10,#12,#13 as unescaped char from b,t,n,f,r
// - other characters are litterals and should be written as such
JSON_UNESCAPE: array[AnsiChar] of AnsiChar;
/// how many initial chars of a JSON array are parsed for intial capacity
// - used e.g. by _JL_DynArray() and TDocVariantData.InitJsonInPlace()
// - 64KB was found out empirically as a good value - but you can tune it
JSON_PREFETCH: integer = 65536;
/// returns TRUE if the given text buffers would be escaped when written as JSON
// - e.g. if contains " or \ characters, as defined by
// http://www.ietf.org/rfc/rfc4627.txt
function NeedsJsonEscape(const Text: RawUtf8): boolean; overload;
/// returns TRUE if the given text buffers would be escaped when written as JSON
// - e.g. if contains " or \ characters, as defined by
// http://www.ietf.org/rfc/rfc4627.txt
function NeedsJsonEscape(P: PUtf8Char): boolean; overload;
/// returns TRUE if the given text buffers would be escaped when written as JSON
// - e.g. if contains " or \ characters, as defined by
// http://www.ietf.org/rfc/rfc4627.txt
function NeedsJsonEscape(P: PUtf8Char; PLen: integer): boolean; overload;
{$ifdef HASINLINE}inline;{$endif}
/// UTF-8 decode one or two \u#### JSON escaped codepoints into Dest
// - P^ should point at 'u1234' just after \u1234
// - return ending P position, maybe after another \u#### UTF-16 surrogate char
function JsonUnicodeEscapeToUtf8(var D: PUtf8Char; P: PUtf8Char): PUtf8Char;
// {$ifdef HASINLINE}inline;{$endif}
/// ensure all UTF-8 Unicode glyphs are escaped as \u#### UTF-16 JSON
// - this will work at raw UTF-8 text level: if your input is true JSON,
// consider using JsonReformat(s, jsonEscapeUnicode) instead
function JsonUnicodeEscape(const s: RawUtf8): RawUtf8;
/// ensure all \u#### UTF-16 JSON are decoded into plain UTF-8 content
// - this will work at raw UTF-8 text level: if your input is true JSON,
// consider using JsonReformat(s, jsonNoEscapeUnicode) instead
function JsonUnicodeUnEscape(const s: RawUtf8): RawUtf8;
/// encode one \u#### JSON escaped UTF-16 codepoint into Dest
procedure Utf16ToJsonUnicodeEscape(var B: PUtf8Char; c: PtrUint; tab: PByteToWord);
{$ifdef HASINLINE} inline; {$endif}
/// test if the supplied buffer is a "string" value or a numerical value
// (floating point or integer), according to the characters within
// - this version will recognize null/false/true as strings
// - e.g. IsString('0')=false, IsString('abc')=true, IsString('null')=true
function IsString(P: PUtf8Char): boolean;
/// test if the supplied buffer is a "string" value or a numerical value
// (floating or integer), according to the JSON encoding schema
// - this version will NOT recognize JSON null/false/true as strings
// - e.g. IsStringJson('0')=false, IsStringJson('abc')=true,
// but IsStringJson('null')=false
// - will follow the JSON definition of number, i.e. '0123' is a string (i.e.
// '0' is excluded at the begining of a number) and '123' is not a string
function IsStringJson(P: PUtf8Char): boolean;
/// test if the supplied text buffer seems to be a correct (extended) JSON value
// - will allow comments and extended MongoDB JSON syntax unless Strict=true
// - numbers, escaped strings and commas are wild guessed, for performance
function IsValidJson(P: PUtf8Char; len: PtrInt; strict: boolean = false): boolean; overload;
/// test if the supplied text seems to be a correct (extended) JSON value
// - will allow comments and extended MongoDB JSON syntax unless Strict=true
// - numbers, escaped strings and commas are wild guessed, for performance
function IsValidJson(const s: RawUtf8; strict: boolean = false): boolean; overload;
/// test if the supplied #0 ended buffer is a correct (extended) JSON value
// - will allow comments and extended MongoDB JSON syntax unless Strict=true
// - numbers, escaped strings and commas are wild guessed, for performance
function IsValidJsonBuffer(P: PUtf8Char; strict: boolean = false): boolean;
/// returns the JSON type of a supplied #0 ended buffer
// - will move to the first non-space char, then returns its JSON_TOKENS[] value
// - for valid JSON, is likely to return jtDoubleQuote, jtFirstDigit,
// jtNullFirstChar, jtTrueFirstChar, jtFalseFirstChar, jtObjectStart or jtArrayStart
function GetFirstJsonToken(P: PUtf8Char): TJsonToken;
/// validate the supplied #0 ended buffer and returns its JSON type
// - on parsing error, returns P=nil and jtNone
// - will move P to the next JSON item, and return the JSON token kind, e.g.
// jtArrayStart, jtObjectStart, jtDoubleQuote or jtFirstDigit
// - will allow comments and extended MongoDB JSON syntax unless Strict=true
// - optionally return the number of nested items for jtArrayStart/jtObjectStart
function GetNextJsonToken(var P: PUtf8Char; strict: boolean = false;
DocCount: PInteger = nil): TJsonToken;
/// simple method to go after the next ',' character
procedure IgnoreComma(var P: PUtf8Char);
{$ifdef HASINLINE}inline;{$endif}
/// returns TRUE if the given text buffer contains simple characters as
// recognized by JSON extended syntax
// - follow GetJsonPropName and GotoNextJsonObjectOrArray expectations
function JsonPropNameValid(P: PUtf8Char): boolean;
{$ifdef HASINLINE}inline;{$endif}
type
/// efficient JSON value parser / in-place decoder
// - as used by JsonDecode() and all internal JSON functions
{$ifdef USERECORDWITHMETHODS}
TGetJsonField = record
{$else}
TGetJsonField = object
{$endif USERECORDWITHMETHODS}
public
/// input/output JSON parsing buffer address
Json: PUtf8Char;
/// in-place output parsed JSON value, unescaped and #0 terminated
// - see associated WasString to find out its actual type
Value: PUtf8Char;
/// in-place output parsed JSON value length
ValueLen: integer;
/// set if the value was actually a JSON string
// - "strings" are decoded as 'strings', with WasString=true, properly JSON
// unescaped (e.g. any \u0123 pattern would be converted into UTF-8 content)
// - numbers are decoded as text, e.g. '1.234', with WasString=false
// - null is decoded as Value=nil and WasString=false
// - true/false are decoded as 'true'/'false' with WasString=false
WasString: boolean;
/// the ',' ':' or '}' separator just after the Value
// - may have been overwritten with a #0 termination in the input buffer
EndOfObject: AnsiChar;
/// decode a JSON field value in-place from an UTF-8 encoded text buffer
// - warning: will decode in the Json buffer memory itself (no memory copy
// nor allocation), for faster process - so take care that it is not shared
// - Value/ValueLen/WasString is set with the parsed value
// - EndOfObject is set to the JSON ending char (',' ':' or '}' e.g.)
// - Json points to the next field to be decoded, or nil on parsing error
procedure GetJsonField;
/// decode a JSON 64-bit integer value from an UTF-8 encoded text buffer
function GetJsonInt64: Int64;
{$ifdef HASINLINE} inline; {$endif}
/// decode a JSON field value in-place into a RawUtf8 string
procedure GetJsonValue(var Text: RawUtf8);
/// decode a JSON content from an UTF-8 encoded buffer
// - GetJsonField will only handle JSON "strings" or numbers - if
// HandleValuesAsObjectOrArray is TRUE, this function will process JSON {
// objects } or [ arrays ] and add a #0 at the end of it
// - warning: will decode in the Json buffer memory itself (no memory
// allocation or copy), for faster process - so take care that it is not shared
// - Value/ValueLen/WasString is set with the parsed JSON value
// - EndOfObject is set to the JSON ending char (',' ':' or '}' e.g.)
// - Json points to the next value to be decoded, or nil on parsing error
procedure GetJsonFieldOrObjectOrArray(
HandleValuesAsObjectOrArray: boolean = true; NormalizeBoolean: boolean = true);
end;
{$ifndef PUREMORMOT2}
/// decode a JSON field value in-place from an UTF-8 encoded text buffer
// - compatibility wrapper around TGetJsonField.GetJsonField method
function GetJsonField(P: PUtf8Char; out PDest: PUtf8Char;
WasString: PBoolean = nil; EndOfObject: PUtf8Char = nil;
Len: PInteger = nil): PUtf8Char;
{$ifdef HASINLINE} inline; {$endif}
/// decode a JSON content from an UTF-8 encoded buffer
// - compatibility wrapper around TGetJsonField.GetJsonFieldOrObjectOrArray
function GetJsonFieldOrObjectOrArray(var Json: PUtf8Char;
WasString: PBoolean = nil; EndOfObject: PUtf8Char = nil;
HandleValuesAsObjectOrArray: boolean = false;
NormalizeBoolean: boolean = true; Len: PInteger = nil): PUtf8Char;
{$ifdef HASINLINE} inline; {$endif}
{$endif PUREMORMOT2}
/// decode a JSON field name in an UTF-8 encoded buffer
// - this function decodes in the P^ buffer memory itself (no memory allocation
// or copy), for faster process - so take care that P^ is not shared
// - it will return the property name, with an ending #0, and "..." content
// properly unescaped unless NoJsonUnescape is set to true
// - returns nil on error
// - this function will handle strict JSON property name (i.e. a "string"), but
// also MongoDB extended syntax, e.g. {age:{$gt:18}} or {'people.age':{$gt:18}}
// see @http://docs.mongodb.org/manual/reference/mongodb-extended-json
function GetJsonPropName(var Json: PUtf8Char; Len: PInteger = nil;
NoJsonUnescape: boolean = false): PUtf8Char;
/// decode a JSON field name in an UTF-8 encoded ShortString variable
// - this function would left the P^ buffer memory untouched, so may be safer
// than the overloaded GetJsonPropName() function in some cases
// - it will return the property name as a local UTF-8 encoded ShortString,
// or PropName='' on error
// - this function won't unescape the property name, as strict JSON (i.e. a "st\"ring")
// - but it will handle MongoDB syntax, e.g. {age:{$gt:18}} or {'people.age':{$gt:18}}
// see @http://docs.mongodb.org/manual/reference/mongodb-extended-json
procedure GetJsonPropNameShort(var P: PUtf8Char; out PropName: ShortString);
/// decode a JSON object or array from an UTF-8 encoded buffer
// - as called by GetJsonFieldOrObjectOrArray() for HandleValuesAsObjectOrArray
// - return the position of the next JSON item (with EndOfObject and optionally
// Len^ properly set) or nil on parsing error
function GetJsonObjectOrArray(P: PUtf8Char;
EndOfObject: PUtf8Char; Len: PInteger = nil): PUtf8Char;
/// retrieve the next JSON item as a RawJson undecoded variable
// - P buffer can be either any JSON item, i.e. a string, a number or even a
// JSON array (ending with ]) or a JSON object (ending with })
// - EndOfObject (if not nil) is set to the JSON value end char (',' ':' or '}')
// - input buffer is not modified in-place, since result is directly copied
procedure GetJsonItemAsRawJson(var P: PUtf8Char; var result: RawJson;
EndOfObject: PAnsiChar = nil);
/// retrieve the next JSON item as a RawUtf8 decoded buffer
// - P buffer can be either any JSON item, i.e. a string, a number or even a
// JSON array (ending with ]) or a JSON object (ending with })
// - EndOfObject (if not nil) is set to the JSON value end char (',' ':' or '}')
// - just calls TGetJsonField, and create a new RawUtf8 from the returned value,
// after proper string unescape (with WasString^=true)
// - warning: input buffer is modified in-place during output value parsing
function GetJsonItemAsRawUtf8(var P: PUtf8Char; var output: RawUtf8;
WasString: PBoolean = nil; EndOfObject: PUtf8Char = nil): boolean;
/// get the next character after a quoted buffer
// - the first character in P^ must be "
// - it will return the latest " position, ignoring \" within
// - caller should check that return PUtf8Char is indeed a "
function GotoEndOfJsonString(P: PUtf8Char): PUtf8Char;
/// reach position just after the current JSON string in the supplied UTF-8 buffer
// - will first ensure that P^='"' then process like GotoEndJsonItem()
function GotoEndJsonItemString(P: PUtf8Char): PUtf8Char;
/// reach position just after the current JSON item in the supplied UTF-8 buffer
// - buffer can be either any JSON item, i.e. a string, a number or even a
// JSON array (ending with ]) or a JSON object (ending with })
// - returns nil if the specified buffer is not valid JSON content
// - returns the position in buffer just after the item excluding the separator
// character - i.e. result^ may be ',','}',']'
// - will allow comments and extended MongoDB JSON syntax - use
// GotoEndJsonItemStrict() if you expect a more standard JSON parsing
function GotoEndJsonItem(P: PUtf8Char; PMax: PUtf8Char = nil): PUtf8Char;
/// reach position just after the current JSON item in the supplied UTF-8 buffer
// - in respect to GotoEndJsonItem(), this function will validate for strict
// JSON simple values, i.e. real numbers or only true/false/null constants,
// and refuse commens or MongoDB extended syntax like {age:{$gt:18}}
// - numbers and escaped strings are not fully validated, just their charset
function GotoEndJsonItemStrict(P: PUtf8Char; PMax: PUtf8Char = nil): PUtf8Char;
/// reach the position of the next JSON item(s) in the supplied UTF-8 buffer
// - buffer can be either any JSON item, i.e. a string, a number or even a
// JSON array (ending with ]) or a JSON object (ending with })
// - returns nil if the specified number of items is not available in buffer
// - returns the position in buffer after the item including the separator
// character (optionally in EndOfObject) - i.e. result will be at the start of
// the next object, and EndOfObject may be ',','}',']'
function GotoNextJsonItem(P: PUtf8Char; NumberOfItemsToJump: cardinal = 1;
EndOfObject: PAnsiChar = nil; PMax: PUtf8Char = nil;
Strict: boolean = false): PUtf8Char; overload;
/// reach the position of the next JSON item in the supplied UTF-8 buffer
// - similar to the GotoNextJsonItem() with NumberOfItemsToJump=1
function GotoNextJsonItem(P: PUtf8Char; var EndOfObject: AnsiChar): PUtf8Char; overload;
{don't inline to reduce the stack size of the caller function}
/// search the EndOfObject of a JSON buffer, just like TGetJsonField does
function ParseEndOfObject(P: PUtf8Char; out EndOfObject: AnsiChar): PUtf8Char;
{$ifdef HASINLINE}inline;{$endif}
/// compute the number of elements of a JSON array
// - this will handle any kind of arrays, including those with nested
// JSON objects or arrays
// - incoming P^ should point to the first char AFTER the initial '[' (which
// may be a closing ']')
// - returns 0 if the supplied input is invalid, or the number of identified
// items in the JSON array buffer
// - if PMax is set, will abort after this position, and return the current
// counted number of items as negative, which could be used as initial allocation
// before the loop - typical use in this case is e.g.
// ! cap := abs(JsonArrayCount(P, P + JSON_PREFETCH));
// - some performance numbers on a Core i5-13400:
// $ JsonArrayCount(P) in 10.95ms i.e. 14.3M/s, 1.7 GB/s
// $ JsonArrayCount(P,PMax) in 11.05ms i.e. 14.1M/s, 1.7 GB/s
function JsonArrayCount(P: PUtf8Char; PMax: PUtf8Char = nil;
Strict: boolean = false): integer;
/// go to the #nth item of a JSON array
// - implemented via a fast SAX-like approach: the input buffer is not changed,
// nor no memory buffer allocated neither content copied
// - returns nil if the supplied index is out of range
// - returns a pointer to the index-nth item in the JSON array (first index=0)
// - this will handle any kind of arrays, including those with nested
// JSON objects or arrays
// - incoming P^ should point to the first initial '[' char
function JsonArrayItem(P: PUtf8Char; Index: integer): PUtf8Char;
/// retrieve the positions of all elements of a JSON array
// - this will handle any kind of arrays, including those with nested
// JSON objects or arrays
// - warning: incoming P^ should point to the first char AFTER the initial '['
// (which may be a closing ']') - calling e.g. NextNotSpaceCharIs()
// - returns false if the supplied input is invalid
// - returns true on success, with Values[] pointing to each unescaped value,
// may be a JSON string, object, array of constant
function JsonArrayDecode(P: PUtf8Char;
out Values: TPUtf8CharDynArray): boolean;
/// compute the number of fields in a JSON object
// - this will handle any kind of objects, including those with nested JSON
// documents, and also comments or MongoDB extended syntax (unless Strict=true)
// - warning: incoming P^ should point to the first char AFTER the initial '{'
// (which may be a closing '}')
// - will abort if P reaches PMax (if not nil), and return the current counted
// number of items as negative, which could be used as initial allocation before
// a parsing loop - typical use in this case is e.g.
// ! cap := abs(JsonObjectPropCount(P, P + JSON_PREFETCH));
function JsonObjectPropCount(P: PUtf8Char; PMax: PUtf8Char = nil;
Strict: boolean = false): PtrInt;
/// go to a named property of a JSON object
// - implemented via a fast SAX-like approach: the input buffer is not changed,
// nor no memory buffer allocated neither content copied
// - PropName is search case-insensitively as 'propertyname' or 'property*'
// - returns nil if the supplied property name does not exist
// - returns a pointer to the matching item value in the JSON object
// - this will handle any kind of objects, including those with nested
// JSON objects or arrays
// - incoming P^ should point to the first initial '{' char
function JsonObjectItem(P: PUtf8Char; const PropName: RawUtf8;
PropNameFound: PRawUtf8 = nil): PUtf8Char; overload;
/// go to a buffer-named property of a JSON object
// - as called by overloaded JsonObjectItem()
function JsonObjectItem(P: PUtf8Char; PropName: PUtf8Char; PropNameLen: PtrInt;
PropNameFound: PRawUtf8): PUtf8Char; overload;
/// go to a property of a JSON object, by its full path, e.g. 'parent.child'
// - implemented via a fast SAX-like approach: the input buffer is not changed,
// nor no memory buffer allocated neither content copied
// - PropPath is search case-insensitively as 'parent.child' or 'parent.ch*'
// - returns nil if the supplied property path does not exist
// - returns a pointer to the matching item value in the JSON object
// - this will handle any kind of objects, including those with nested
// JSON objects or arrays
// - incoming P^ should point to the first initial '{' char
function JsonObjectByPath(JsonObject, PropPath: PUtf8Char): PUtf8Char;
/// return all matching properties of a JSON object
// - here the PropPath could be a comma-separated list of case-insensitive full
// paths, e.g. 'Prop1,Prop2' or 'Obj1.Obj2.Prop*,Obj1.Prop1'
// - returns '' if no property did match
// - returns a JSON object of all matching properties
// - this will handle any kind of objects, including those with nested
// JSON objects or arrays
// - incoming P^ should point to the first initial '{' char
function JsonObjectsByPath(JsonObject, PropPath: PUtf8Char): RawUtf8;
/// convert one JSON object into two JSON arrays of keys and values
// - i.e. makes the following transformation:
// $ {key1:value1,key2,value2...} -> [key1,key2...] + [value1,value2...]
// - this function won't allocate any memory during its process, nor
// modify the JSON input buffer
// - is the reverse of the TJsonWriter.AddJsonArraysAsJsonObject() method
// - used e.g. by TSynDictionary.LoadFromJson
// - returns the number of items parsed and stored into keys/values, -1 on
// error parsing the input JSON buffer
function JsonObjectAsJsonArrays(Json: PUtf8Char;
out keys, values: RawUtf8): integer;
/// remove comments and trailing commas from a text buffer before passing
// it to a JSON parser
// - handle two types of comments: starting from // till end of line
// or /* ..... */ blocks anywhere in the text content
// - trailing commas is replaced by ' ', so resulting JSON is valid for parsers
// what not allows trailing commas (browsers for example)
// - may be used to prepare configuration files before loading;
// for example we store server configuration in file config.json and
// put some comments in this file then code for loading is:
// !var
// ! cfg: RawUtf8;
// ! ...
// ! cfg := StringFromFile(Executable.ProgramFilePath + 'config.json');
// ! RemoveCommentsFromJson(@cfg[1]);
// ! pLastChar := JsonToObject(obj, pointer(cfg), isvalid);
procedure RemoveCommentsFromJson(P: PUtf8Char); overload;
/// remove comments from a text buffer before passing it to JSON parser
// - won't remove the comments in-place, but allocate a new string
function RemoveCommentsFromJson(const s: RawUtf8): RawUtf8; overload;
/// helper to retrieve the bit mapped integer value of a set from its JSON text
// - Names and MaxValue should be retrieved from RTTI
// - if supplied P^ is a JSON integer number, will read it directly
// - if P^ maps some ["item1","item2"] content, would fill all matching bits
// - if P^ contains ['*'], would fill all bits
// - returns P=nil if reached prematurely the end of content, or returns
// the value separator (e.g. , or }) in EndOfObject (like GetJsonField)
function GetSetNameValue(Names: PShortString; MinValue, MaxValue: integer;
var P: PUtf8Char; out EndOfObject: AnsiChar): QWord; overload;
/// helper to retrieve the bit mapped integer value of a set from its JSON text
// - overloaded function using the RTTI
function GetSetNameValue(Info: PRttiInfo;
var P: PUtf8Char; out EndOfObject: AnsiChar): QWord; overload;
/// retrieve a pointer to JSON string field content, without unescaping it
// - returns either ':' for name field, or } , for value field
// - returns nil on JSON content error
// - this function won't touch the JSON buffer, so you can call it before
// using in-place escape process via JsonDecode() or TGetJsonField
function JsonRetrieveStringField(P: PUtf8Char; out Field: PUtf8Char;
out FieldLen: integer; ExpectNameField: boolean): PUtf8Char;
{$ifdef HASINLINE}inline;{$endif}
/// retrieve a class Rtti, as saved by ObjectToJson(...,[...,woStoreClassName,...]);
// - JSON input should be either 'null', either '{"ClassName":"TMyClass",...}'
// - calls IdemPropName/JsonRetrieveStringField so input buffer won't be
// modified, but caller should ignore this "ClassName" property later on
// - the corresponding class shall have been previously registered by
// Rtti.RegisterClass(), in order to retrieve the class type from it name -
// or, at least, by the RTL Classes.RegisterClass() function, if AndGlobalFindClass
// parameter is left to default true so that RTL Classes.FindClass() is called
function JsonRetrieveObjectRttiCustom(var Json: PUtf8Char;
AndGlobalFindClass: boolean): TRttiCustom;
/// encode a JSON object UTF-8 buffer into URI parameters
// - you can specify property names to ignore during the object decoding
// - you can omit the leading query delimiter ('?') by setting IncludeQueryDelimiter=false
// - warning: the ParametersJson input buffer will be modified in-place
function UrlEncodeJsonObject(const UriName: RawUtf8; ParametersJson: PUtf8Char;
const PropNamesToIgnore: array of RawUtf8;
IncludeQueryDelimiter: boolean = true): RawUtf8; overload;
/// encode a JSON object UTF-8 buffer into URI parameters
// - you can specify property names to ignore during the object decoding
// - you can omit the leading query delimiter ('?') by setting IncludeQueryDelimiter=false
// - overloaded function which will make a copy of the input JSON before parsing
function UrlEncodeJsonObject(const UriName, ParametersJson: RawUtf8;
const PropNamesToIgnore: array of RawUtf8;
IncludeQueryDelimiter: boolean = true): RawUtf8; overload;
/// formats and indents a JSON array or document to the specified layout
// - just a wrapper around TJsonWriter.AddJsonReformat() method
// - WARNING: the JSON buffer is decoded in-place, so P^ WILL BE modified
procedure JsonBufferReformat(P: PUtf8Char; out result: RawUtf8;
Format: TTextWriterJsonFormat = jsonHumanReadable);
/// formats and indents a JSON array or document to the specified layout
// - just a wrapper around TJsonWriter.AddJsonReformat, making a private
// of the supplied JSON buffer (so that JSON content would stay untouched)
function JsonReformat(const Json: RawUtf8;
Format: TTextWriterJsonFormat = jsonHumanReadable): RawUtf8;
/// formats and indents a JSON array or document as a file
// - just a wrapper around TJsonWriter.AddJsonReformat() method
// - WARNING: the JSON buffer is decoded in-place, so P^ WILL BE modified
function JsonBufferReformatToFile(P: PUtf8Char; const Dest: TFileName;
Format: TTextWriterJsonFormat = jsonHumanReadable): boolean;
/// formats and indents a JSON array or document as a file
// - just a wrapper around TJsonWriter.AddJsonReformat, making a private
// of the supplied JSON buffer (so that JSON content would stay untouched)
function JsonReformatToFile(const Json: RawUtf8; const Dest: TFileName;
Format: TTextWriterJsonFormat = jsonHumanReadable): boolean;
/// convert UTF-8 content into a JSON string
// - with proper escaping of the content, and surounding " characters
procedure QuotedStrJson(const aText: RawUtf8; var result: RawUtf8;
const aPrefix: RawUtf8 = ''; const aSuffix: RawUtf8 = ''); overload;
{$ifdef HASINLINE}inline;{$endif}
/// convert UTF-8 buffer into a JSON string
// - with proper escaping of the content, and surounding " characters
procedure QuotedStrJson(P: PUtf8Char; PLen: PtrInt; var result: RawUtf8;
const aPrefix: RawUtf8 = ''; const aSuffix: RawUtf8 = ''); overload;
/// convert UTF-8 content into a JSON string
// - with proper escaping of the content, and surounding " characters
function QuotedStrJson(const aText: RawUtf8): RawUtf8; overload;
{$ifdef HASINLINE}inline;{$endif}
const
FIELDCOUNT_PATTERN: PUtf8Char = '{"fieldCount":'; // PatternLen = 14 chars
ROWCOUNT_PATTERN: PUtf8Char = ',"rowCount":'; // PatternLen = 12 chars
VALUES_PATTERN: PUtf8Char = ',"values":['; // PatternLen = 11 chars
/// quickly check if an UTF-8 buffer start with the supplied Pattern
// - PatternLen is at least 8 bytes long, typically FIELDCOUNT_PATTERN,
// ROWCOUNT_PATTERN or VALUES_PATTERN constants
// - defined here for TDocVariantData.InitArrayFromResults
function Expect(var P: PUtf8Char; Pattern: PUtf8Char; PatternLen: PtrInt): boolean;
{$ifdef HASINLINE}inline;{$endif}
/// parse JSON content in not-expanded format
// - i.e. stored as
// $ {"fieldCount":2,"values":["f1","f2","1v1",1v2,"2v1",2v2...],"rowCount":20}
// - search and extract "fieldCount" and "rowCount" field information
// - defined here for TDocVariantData.InitArrayFromResults
function IsNotExpandedBuffer(var P: PUtf8Char; PEnd: PUtf8Char;
var FieldCount, RowCount: PtrInt): boolean;
/// efficient retrieval of the number of rows in non-expanded layout
// - search for "rowCount": at the end of the JSON buffer
function NotExpandedBufferRowCountPos(P, PEnd: PUtf8Char): PUtf8Char;
/// low-level prepare GetFieldCountExpanded() parsing returning '{' or ']'
function GotoFieldCountExpanded(P: PUtf8Char): PUtf8Char;
/// low-level parsing of the first expanded JSON object to guess fields count
function GetFieldCountExpanded(P: PUtf8Char): integer;
/// fast Format() function replacement, handling % and ? parameters
// - call rather FormatSql() and FormatJson() wrappers instead
// - resulting string has no length limit and uses fast concatenation
// - any supplied TObject instance will be written as their class name
procedure FormatParams(const Format: RawUtf8; const Args, Params: array of const;
JsonFormat: boolean; var Result: RawUtf8);
/// fast Format() function replacement, handling % but also ? inlined parameters
// - will include Args[] for every % in Format
// - will include Params[] for every ? in Format, as "inlined" ORM or DB values,
// e.g. :(1234): for numbers, and :('quoted '' string'): for text
// - note that, due to a Delphi compiler limitation, cardinal values should be
// type-casted to Int64() (otherwise the integer mapped value will be converted)
// - is a wrapper around FormatParams(Format, Args, Params, false, result);
function FormatSql(const Format: RawUtf8;
const Args, Params: array of const): RawUtf8;
/// fast Format() function replacement, handling % but also ? parameters as JSON
// - will include Args[] for every % in Format
// - will include Params[] for every ? in Format, as their JSON value, with
// proper JSON double quotes and escaping for strings
// - note that, due to a Delphi compiler limitation, cardinal values should be
// type-casted to Int64() (otherwise the integer mapped value will be converted)
// - is a wrapper around FormatParams(Format, Args, Params, true, result);
function FormatJson(const Format: RawUtf8;
const Args, Params: array of const): RawUtf8;
{$ifndef PUREMORMOT2} // rather call FormatSql() and FormatJson() functions
function FormatUtf8(const Format: RawUtf8; const Args, Params: array of const;
JsonFormat: boolean = false): RawUtf8; overload;
{$endif PUREMORMOT2}
{ ********** TJsonWriter class with proper JSON escaping and WriteObject() support }
type
/// JSON-capable TTextWriter/TTextDateWriter inherited class
// - in addition to TTextWriter/TTextDateWriter, will handle JSON
// serialization of any kind of value, including records, classes or arrays
TJsonWriter = class(TTextDateWriter)
protected
// used by AddCRAndIndent for enums, sets and T*ObjArray comment of values
fBlockComment: RawUtf8;
// used by WriteObjectAsString/AddDynArrayJsonAsString methods
fInternalJsonWriter: TJsonWriter;
procedure InternalAddFixedAnsi(Source: PAnsiChar; SourceChars: cardinal;
AnsiToWide: PWordArray; Escape: TTextWriterKind);
// called after TRttiCustomProp.GetValueDirect/GetValueGetter
procedure AddRttiVarData(const Value: TRttiVarData; Escape: TTextWriterKind;
WriteOptions: TTextWriterWriteObjectOptions);
public
/// release all internal structures
destructor Destroy; override;
/// gives access to a temporary TJsonWriter
// - returned instance is owned by this TJsonWriter, and voided
// - may be used to escape some JSON espaced value (i.e. escape it twice),
// in conjunction with AddJsonEscape(Source: TJsonWriter)
function GetTempJsonWriter: TJsonWriter;
/// append '[' or '{' with proper indentation
procedure BlockBegin(Starter: AnsiChar; Options: TTextWriterWriteObjectOptions);
{$ifdef HASINLINE}inline;{$endif}
/// append ',' with proper indentation
// - warning: this will break CancelLastComma, since CRLF+tabs are added
procedure BlockAfterItem(Options: TTextWriterWriteObjectOptions);
{$ifdef HASINLINE}inline;{$endif}
/// append ']' or '}' with proper indentation
procedure BlockEnd(Stopper: AnsiChar; Options: TTextWriterWriteObjectOptions);
{$ifdef HASINLINE}inline;{$endif}
/// used internally by WriteObject() when serializing a published property
// - will call AddCRAndIndent then append "PropName":
procedure WriteObjectPropNameHumanReadable(PropName: PUtf8Char; PropNameLen: PtrInt);
/// used internally by WriteObject() when serializing a published property
// - will call AddCRAndIndent then append "PropName":
procedure WriteObjectPropNameShort(const PropName: ShortString;
Options: TTextWriterWriteObjectOptions);
{$ifdef HASINLINE}inline;{$endif}
/// same as WriteObject(), but will double all internal " and bound with "
// - this implementation will avoid most memory allocations
procedure WriteObjectAsString(Value: TObject;
Options: TTextWriterWriteObjectOptions = [woDontStoreDefault]);
/// same as AddDynArrayJson(), but will double all internal " and bound with "
// - this implementation will avoid most memory allocations
procedure AddDynArrayJsonAsString(aTypeInfo: PRttiInfo; var aValue;
WriteOptions: TTextWriterWriteObjectOptions = []);
/// append a JSON field name, followed by an escaped UTF-8 JSON String and
// a comma (',')
procedure AddPropJsonString(const PropName: ShortString; const Text: RawUtf8);
/// append CR+LF (#13#10) chars and #9 indentation
// - will also flush any fBlockComment
procedure AddCRAndIndent; override;
/// write some #0 ended UTF-8 text, according to the specified format
// - if Escape is a constant, consider calling directly AddNoJsonEscape,
// AddJsonEscape or AddOnSameLine methods
procedure Add(P: PUtf8Char; Escape: TTextWriterKind); override;
/// write some #0 ended UTF-8 text, according to the specified format
// - if Escape is a constant, consider calling directly AddNoJsonEscape,
// AddJsonEscape or AddOnSameLine methods
procedure Add(P: PUtf8Char; Len: PtrInt; Escape: TTextWriterKind); override;
/// write some #0 ended Unicode text as UTF-8, according to the specified format
// - if Escape is a constant, consider calling directly AddNoJsonEscapeW,
// AddJsonEscapeW or AddOnSameLineW methods
procedure AddW(P: PWord; Len: PtrInt; Escape: TTextWriterKind);
{$ifdef HASINLINE}inline;{$endif}
/// append some UTF-8 encoded chars to the buffer, from the main AnsiString type
// - use the current system code page for AnsiString parameter
procedure AddAnsiString(const s: AnsiString; Escape: TTextWriterKind); overload;
/// append some UTF-8 encoded chars to the buffer, from any AnsiString value
// - if CodePage is left to its default value of -1, it will assume
// CurrentAnsiConvert.CodePage prior to Delphi 2009, but newer UNICODE
// versions of Delphi will retrieve the code page from string
// - if CodePage is defined to a >= 0 value, the encoding will take place
procedure AddAnyAnsiString(const s: RawByteString; Escape: TTextWriterKind;
CodePage: integer = -1);
/// append some UTF-8 encoded chars to the buffer, from any Ansi buffer
// - the codepage should be specified, e.g. CP_UTF8, CP_RAWBYTESTRING,
// CP_WINANSI, or any version supported by the Operating System
// - if codepage is 0, the current CurrentAnsiConvert.CodePage would be used
// - will use TSynAnsiConvert to perform the conversion to UTF-8
procedure AddAnyAnsiBuffer(P: PAnsiChar; Len: PtrInt;
Escape: TTextWriterKind; CodePage: integer);
/// write some data Base64 encoded
// - if withMagic is TRUE, will write as '"\uFFF0base64encodedbinary"'
procedure WrBase64(P: PAnsiChar; Len: PtrUInt; withMagic: boolean); override;
/// write some binary-saved data with Base64 encoding
// - if withMagic is TRUE, will write as '"\uFFF0base64encodedbinary"'
// - is a wrapper around BinarySave() and WrBase64()
procedure BinarySaveBase64(Data: pointer; Info: PRttiInfo;
Kinds: TRttiKinds; withMagic: boolean; withCrc: boolean = false);
/// append some values at once
// - text values (e.g. RawUtf8) will be escaped as JSON by default
procedure Add(const Values: array of const); overload;
/// append some values at once with custom escaping
procedure Add(const Values: array of const; Escape: TTextWriterKind); overload;
/// append an array of RawUtf8 as CSV of JSON strings
procedure AddCsvUtf8(const Values: array of RawUtf8);
/// append an array of const as CSV of JSON values
procedure AddCsvConst(const Values: array of const);
/// append a quoted string as JSON, with in-place decoding
// - if QuotedString does not start with ' or ", it will written directly
// (i.e. expects to be a number, or null/true/false constants)
// - as used e.g. by TJsonObjectDecoder.EncodeAsJson method and
// JsonEncodeNameSQLValue() function
procedure AddQuotedStringAsJson(const QuotedString: RawUtf8);
/// append strings or integers with a specified format
// - this overriden version will properly handle JSON escape
// - % = #37 marks a string, integer, floating-point, or class parameter
// to be appended as text (e.g. class name)
// - note that due to a limitation of the "array of const" format, cardinal
// values should be type-casted to Int64() - otherwise the integer mapped
// value will be transmitted, therefore wrongly
procedure Add(const Format: RawUtf8; const Values: array of const;
Escape: TTextWriterKind = twNone;
WriteObjectOptions: TTextWriterWriteObjectOptions = [woFullExpand]); override;
/// append a variant content as number or string
// - this overriden version will properly handle JSON escape
// - properly handle Value as a TRttiVarData from TRttiProp.GetValue
procedure AddVariant(const Value: variant; Escape: TTextWriterKind = twJsonEscape;
WriteOptions: TTextWriterWriteObjectOptions = []); override;
/// append complex types as JSON content using raw TypeInfo()
// - handle rkClass as WriteObject, rkEnumeration/rkSet with proper options,
// rkRecord, rkDynArray or rkVariant using proper JSON serialization
// - other types will append 'null'
procedure AddTypedJson(Value, TypeInfo: pointer;
WriteOptions: TTextWriterWriteObjectOptions = []); override;
/// serialize as JSON the given object
procedure WriteObject(Value: TObject;
WriteOptions: TTextWriterWriteObjectOptions = [woDontStoreDefault]); override;
/// append complex types as JSON content using TRttiCustom
// - called e.g. by TJsonWriter.AddVariant() for varAny / TRttiVarData
procedure AddRttiCustomJson(Value: pointer; RttiCustom: TObject;
Escape: TTextWriterKind; WriteOptions: TTextWriterWriteObjectOptions);
/// append a JSON value, array or document, in a specified format
// - this overriden version will properly handle JSON escape
function AddJsonReformat(Json: PUtf8Char; Format: TTextWriterJsonFormat;
EndOfObject: PUtf8Char): PUtf8Char; override;
/// append a JSON value, array or document as simple XML content
// - you can use JsonBufferToXML() and JsonToXML() functions as wrappers
// - this method is called recursively to handle all kind of JSON values
// - WARNING: the JSON buffer is decoded in-place, so will be changed
// - returns the end of the current JSON converted level, or nil if the
// supplied content was not correct JSON
function AddJsonToXML(Json: PUtf8Char; ArrayName: PUtf8Char = nil;
EndOfObject: PUtf8Char = nil): PUtf8Char;
/// append a record content as UTF-8 encoded JSON or custom serialization
// - default serialization will use Base64 encoded binary stream, or
// a custom serialization, in case of a previous registration via
// TRttiJson.RegisterCustomSerializer() class method - from a dynamic array
// handling this kind of records, or directly from TypeInfo() of the record
// - by default, custom serializers defined via RegisterCustomSerializer()
// would write enumerates and sets as integer numbers, unless
// twoEnumSetsAsTextInRecord or twoEnumSetsAsBooleanInRecord is set in
// the instance CustomOptions
// - returns the element size
function AddRecordJson(Value: pointer; RecordInfo: PRttiInfo;
WriteOptions: TTextWriterWriteObjectOptions = []): PtrInt;
/// append a void record content as UTF-8 encoded JSON or custom serialization
// - this method will first create a void record (i.e. filled with #0 bytes)
// then save its content with default or custom serialization
procedure AddVoidRecordJson(RecordInfo: PRttiInfo;
WriteOptions: TTextWriterWriteObjectOptions = []);
/// append a dynamic array content as UTF-8 encoded JSON array
// - typical content could be
// ! '[1,2,3,4]' or '["\uFFF0base64encodedbinary"]'
procedure AddDynArrayJson(var DynArray: TDynArray;
WriteOptions: TTextWriterWriteObjectOptions = []); overload;
/// append a dynamic array content as UTF-8 encoded JSON array
// - expect a dynamic array TDynArrayHashed wrapper as incoming parameter
procedure AddDynArrayJson(var DynArray: TDynArrayHashed;
WriteOptions: TTextWriterWriteObjectOptions = []); overload;
{$ifdef HASINLINE}inline;{$endif}
/// append a dynamic array content as UTF-8 encoded JSON array
// - returns the array element size
function AddDynArrayJson(Value: pointer; Info: TRttiCustom;
WriteOptions: TTextWriterWriteObjectOptions = []): PtrInt; overload;
/// append UTF-8 content as text
// - Text CodePage will be used (if possible) - assume RawUtf8 otherwise
// - will properly handle JSON escape between two " double quotes
procedure AddText(const Text: RawByteString; Escape: TTextWriterKind = twJsonEscape);
/// append UTF-16 content as text
// - P should be a #0 terminated PWideChar buffer
// - will properly handle JSON escape between two " double quotes
procedure AddTextW(P: PWord; Escape: TTextWriterKind = twJsonEscape);
/// append some UTF-8 encoded chars to the buffer
// - escapes chars according to the JSON RFC
// - if Len is 0, writing will stop at #0 (default Len = 0 is slightly faster
// than specifying Len>0 if you are sure P is zero-ended - e.g. from RawUtf8)
procedure AddJsonEscape(P: Pointer; Len: PtrInt = 0); overload;
/// append some Unicode encoded chars to the buffer
// - if Len is 0, Len is calculated from zero-ended widechar
// - escapes chars according to the JSON RFC
procedure AddJsonEscapeW(P: PWord; Len: PtrInt = 0);
/// append some UTF-8 encoded chars to the buffer, from a RTL string type
// - faster than AddJsonEscape(pointer(StringToUtf8(string))
// - escapes chars according to the JSON RFC
procedure AddJsonEscapeString(const s: string);
{$ifdef HASINLINE}inline;{$endif}
/// append some UTF-8 encoded chars to the buffer, from the main AnsiString type
// - escapes chars according to the JSON RFC
procedure AddJsonEscapeAnsiString(const s: AnsiString);
/// append an open array constant value to the buffer
// - "" will be added if necessary
// - escapes chars according to the JSON RFC
// - very fast (avoid most temporary storage)
procedure AddJsonEscape(const V: TVarRec); overload;
/// append a UTF-8 JSON string, JSON escaped between double quotes
// - "" will always be added, before calling AddJsonEscape()
procedure AddJsonString(const Text: RawUtf8);
/// flush a supplied TJsonWriter, and write pending data as JSON escaped text
// - may be used with InternalJsonWriter, as a faster alternative to
// ! AddJsonEscape(Pointer(fInternalJsonWriter.Text),0);
procedure AddJsonEscape(Source: TJsonWriter); overload;
/// flush a supplied TJsonWriter, and write pending data as JSON escaped text
// - may be used with InternalJsonWriter, as a faster alternative to
// ! AddNoJsonEscapeUtf8(Source.Text);
procedure AddNoJsonEscape(Source: TJsonWriter); overload;
/// append a UTF-8 already encoded JSON buffer forcing Unicode escape
// - don't escapes chars according to the JSON RFC but convert any 8-bit
// UTF-8 values as their UTF-16 \u#### escaped content
// - i.e. generate a pure ASCII output with no UTF-8 encoding involved
// - used for jsonEscapeUnicode to follow python default json.dumps() layout
procedure AddNoJsonEscapeForcedUnicode(P: PUtf8Char; Len: PtrInt);
/// append a UTF-8 encoded JSON buffer without any \u#### Unicode escape
// - i.e. \u#### patterns will be converted into pure UTF-8 output
// - as used for jsonNoEscapeUnicode transformation
procedure AddNoJsonEscapeForcedNoUnicode(P: PUtf8Char; Len: PtrInt);
/// append an open array constant value to the buffer
// - "" won't be added for string values
// - string values may be escaped, depending on the supplied parameter
// - very fast (avoid most temporary storage)
procedure Add(const V: TVarRec; Escape: TTextWriterKind = twNone;
WriteObjectOptions: TTextWriterWriteObjectOptions = [woFullExpand]); overload;
/// encode the supplied data as an UTF-8 valid JSON object content
// - data must be supplied two by two, as Name,Value pairs, e.g.
// ! aWriter.AddJsonEscape(['name','John','year',1972]);
// will append to the buffer:
// ! '{"name":"John","year":1972}'
// - or you can specify nested arrays or objects with '['..']' or '{'..'}':
// ! aWriter.AddJsonEscape(['doc','{','name','John','ab','[','a','b']','}','id',123]);
// will append to the buffer:
// ! '{"doc":{"name":"John","abc":["a","b"]},"id":123}'
// - note that, due to a Delphi compiler limitation, cardinal values should be
// type-casted to Int64() (otherwise the integer mapped value will be converted)
// - you can pass nil as parameter for a null JSON value
procedure AddJsonEscape(
const NameValuePairs: array of const); overload;
/// encode the supplied (extended) JSON content, with parameters,
// as an UTF-8 valid JSON object content
// - in addition to the JSON RFC specification strict mode, this method will
// handle some BSON-like extensions, e.g. unquoted field names:
// ! aWriter.AddJson('{id:?,%:{name:?,birthyear:?}}',['doc'],[10,'John',1982]);
// - you can use nested _Obj() / _Arr() instances
// ! aWriter.AddJson('{%:{$in:[?,?]}}',['type'],['food','snack']);
// ! aWriter.AddJson('{type:{$in:?}}',[],[_Arr(['food','snack'])]);
// ! // which are the same as:
// ! aWriter.AddShort('{"type":{"$in":["food","snack"]}}');
// - if the mormot.db.nosql.bson unit is used in the application, the MongoDB
// Shell syntax will also be recognized to create TBsonVariant, like
// ! new Date() ObjectId() MinKey MaxKey /<jRegex>/<jOptions>
// see @http://docs.mongodb.org/manual/reference/mongodb-extended-json
// ! aWriter.AddJson('{name:?,field:/%/i}',['acme.*corp'],['John']))
// ! // will write
// ! '{"name":"John","field":{"$regex":"acme.*corp","$options":"i"}}'
// - will call internally _JsonFastFmt() to create a temporary TDocVariant
// with all its features - so is slightly slower than other AddJson* methods
procedure AddJson(const Format: RawUtf8;
const Args, Params: array of const);
/// append two JSON arrays of keys and values as one JSON object
// - i.e. makes the following transformation:
// $ [key1,key2...] + [value1,value2...] -> {key1:value1,key2,value2...}
// - this method won't allocate any memory during its process, nor
// modify the keys and values input buffers
// - is the reverse of the JsonObjectAsJsonArrays() function
// - used e.g. by TSynDictionary.SaveToJson
procedure AddJsonArraysAsJsonObject(keys, values: PUtf8Char);
end;
{ ************ JSON-aware TSynNameValue TSynPersistentStoreJson }
type
/// store one Name/Value pair, as used by TSynNameValue class
TSynNameValueItem = record
/// the name of the Name/Value pair
// - this property is hashed by TSynNameValue for fast retrieval
Name: RawUtf8;
/// the value of the Name/Value pair
Value: RawUtf8;
/// any associated Pointer or numerical value
Tag: PtrInt;
end;
/// Name/Value pairs storage, as used by TSynNameValue class
TSynNameValueItemDynArray = array of TSynNameValueItem;
/// event handler used to convert on the fly some UTF-8 text content
TOnSynNameValueConvertRawUtf8 = function(
const text: RawUtf8): RawUtf8 of object;
/// callback event used by TSynNameValue
TOnSynNameValueNotify = procedure(
const Item: TSynNameValueItem; Index: PtrInt) of object;
/// pseudo-class used to store Name/Value RawUtf8 pairs
// - use internally a TDynArrayHashed instance for fast retrieval
// - is therefore faster than TRawUtf8List
// - is defined as an object, not as a class: you can use this in any
// class, without the need to destroy the content
// - Delphi "object" is buggy on stack -> also defined as record with methods
{$ifdef USERECORDWITHMETHODS}
TSynNameValue = record
private
{$else}
TSynNameValue = object
protected
{$endif USERECORDWITHMETHODS}
fOnAdd: TOnSynNameValueNotify;
function GetBlobData: RawByteString;
procedure SetBlobData(const aValue: RawByteString);
function GetStr(const aName: RawUtf8): RawUtf8;
{$ifdef HASINLINE}inline;{$endif}
function GetInt(const aName: RawUtf8): Int64;
{$ifdef HASINLINE}inline;{$endif}
public
/// the internal Name/Value storage
List: TSynNameValueItemDynArray;
/// the number of Name/Value pairs
Count: integer;
/// low-level access to the internal storage hasher
DynArray: TDynArrayHashed;
/// initialize the storage
// - will also reset the internal List[] and the internal hash array
procedure Init(aCaseSensitive: boolean);
/// add an element to the array
// - if aName already exists, its associated Value will be updated
procedure Add(const aName, aValue: RawUtf8; aTag: PtrInt = 0);
/// reset content, then add all name=value pairs from a supplied .ini file
// section content
// - will first call Init(false) to initialize the internal array
// - Section can be retrieved e.g. via FindSectionFirstLine()
procedure InitFromIniSection(Section: PUtf8Char;
const OnTheFlyConvert: TOnSynNameValueConvertRawUtf8 = nil;
const OnAdd: TOnSynNameValueNotify = nil);
/// reset content, then add all name=value; CSV pairs
// - will first call Init(false) to initialize the internal array
// - if ItemSep=#10, then any kind of line feed (CRLF or LF) will be handled
procedure InitFromCsv(Csv: PUtf8Char; NameValueSep: AnsiChar = '=';
ItemSep: AnsiChar = #10);
/// reset content, then add all fields from an JSON object
// - will first call Init() to initialize the internal array
// - then parse the incoming JSON object, storing all its field values
// as RawUtf8, and returning TRUE if the supplied content is correct
// - warning: the supplied JSON buffer will be decoded and modified in-place
function InitFromJson(Json: PUtf8Char; aCaseSensitive: boolean = false): boolean;
/// reset content, then add all name, value pairs
// - will first call Init(false) to initialize the internal array
procedure InitFromNamesValues(const Names, Values: array of RawUtf8);
/// search for a Name, return the index in List
// - using fast O(1) hash algoritm
function Find(const aName: RawUtf8): PtrInt;
/// search for the first chars of a Name, return the index in List
// - using O(n) calls of IdemPChar() function
// - here aUpperName should be already uppercase, as expected by IdemPChar()
function FindStart(const aUpperName: RawUtf8): PtrInt;
/// search for a Value, return the index in List
// - using O(n) brute force algoritm with case-sensitive aValue search
function FindByValue(const aValue: RawUtf8): PtrInt;
/// search for a Name, and delete its entry in the List if it exists
function Delete(const aName: RawUtf8): boolean;
/// search for a Value, and delete its entry in the List if it exists
// - returns the number of deleted entries
// - you may search for more than one match, by setting a >1 Limit value
function DeleteByValue(const aValue: RawUtf8; Limit: integer = 1): integer;
/// search for a Name, return the associated Value as a UTF-8 string
function Value(const aName: RawUtf8; const aDefaultValue: RawUtf8 = ''): RawUtf8;
/// search for a Name, return the associated Value as integer
function ValueInt(const aName: RawUtf8; const aDefaultValue: Int64 = 0): Int64;
/// search for a Name, return the associated Value as boolean
// - returns true only if the value is exactly '1' / 'true'
function ValueBool(const aName: RawUtf8): boolean;
{$ifdef HASINLINE}inline;{$endif}
/// search for a Name, return the associated Value as an enumerate
// - returns true and set aEnum if aName was found, and associated value
// matched an aEnumTypeInfo item
// - returns false if no match was found
function ValueEnum(const aName: RawUtf8; aEnumTypeInfo: PRttiInfo;
out aEnum; aEnumDefault: PtrUInt = 0): boolean; overload;
/// returns all values, as CSV or INI content
function AsCsv(const KeySeparator: RawUtf8 = '=';
const ValueSeparator: RawUtf8 = #13#10; const IgnoreKey: RawUtf8 = ''): RawUtf8;
/// returns all values as a JSON object of string fields
function AsJson: RawUtf8;
/// fill the supplied two arrays of RawUtf8 with the stored values
procedure AsNameValues(out Names, Values: TRawUtf8DynArray);
/// search for a Name, return the associated Value as variant
// - returns null if the name was not found
function ValueVariantOrNull(const aName: RawUtf8): variant;
/// compute a TDocVariant document from the stored values
// - output variant will be reset and filled as a TDocVariant instance,
// ready to be serialized as a JSON object
// - if there is no value stored (i.e. Count=0), set null
procedure AsDocVariant(out DocVariant: variant;
ExtendedJson: boolean = false; ValueAsString: boolean = true;
AllowVarDouble: boolean = false); overload;
/// compute a TDocVariant document from the stored values
function AsDocVariant(ExtendedJson: boolean = false;
ValueAsString: boolean = true): variant; overload;
{$ifdef HASINLINE}inline;{$endif}
/// merge the stored values into a TDocVariant document
// - existing properties would be updated, then new values will be added to
// the supplied TDocVariant instance, ready to be serialized as a JSON object
// - if ValueAsString is TRUE, values would be stored as string
// - if ValueAsString is FALSE, numerical values would be identified by
// IsString() and stored as such in the resulting TDocVariant
// - if you let ChangedProps point to a TDocVariantData, it would contain
// an object with the stored values, just like AsDocVariant
// - returns the number of updated values in the TDocVariant, 0 if
// no value was changed
function MergeDocVariant(var DocVariant: variant; ValueAsString: boolean;
ChangedProps: PVariant = nil; ExtendedJson: boolean = false;
AllowVarDouble: boolean = false): integer;
/// returns true if the Init() method has been called
function Initialized: boolean;
/// can be used to set all data from one BLOB memory buffer
procedure SetBlobDataPtr(aValue, aValueMax: pointer);
/// can be used to set or retrieve all stored data as one BLOB content
property BlobData: RawByteString
read GetBlobData write SetBlobData;
/// event triggerred after an item has just been added to the list
property OnAfterAdd: TOnSynNameValueNotify
read fOnAdd write fOnAdd;
/// search for a Name, return the associated Value as a UTF-8 string
// - returns '' if aName is not found in the stored keys
property Str[const aName: RawUtf8]: RawUtf8
read GetStr; default;
/// search for a Name, return the associated Value as integer
// - returns 0 if aName is not found, or not a valid Int64 in the stored keys
property Int[const aName: RawUtf8]: Int64
read GetInt;
/// search for a Name, return the associated Value as boolean
// - returns true if aName stores '1' / 'true' as associated value
property Bool[const aName: RawUtf8]: boolean
read ValueBool;
end;
/// a reference pointer to a Name/Value RawUtf8 pairs storage
PSynNameValue = ^TSynNameValue;
/// implement a cache of some key/value pairs, e.g. to improve reading speed
// - used e.g. by TSqlDataBase for caching the SELECT statements results in an
// internal JSON format (which is faster than a query to the SQLite3 engine)
// - internally make use of an efficient hashing algorithm for fast response
// (i.e. TSynNameValue will use the TDynArrayHashed wrapper mechanism)
TSynCache = class(TSynPersistent)
protected
fNameValue: TSynNameValue;
fRamUsed: cardinal;
fMaxRamUsed: cardinal;
fTimeoutSeconds: cardinal;
fTimeoutTix: cardinal;
fSafe: TRWLock; // writes should be reentrant for Reset
procedure ResetIfNeeded; // call Reset after TimeoutSeconds
public
/// initialize the internal storage
// - aMaxCacheRamUsed can set the maximum RAM to be used for values, in bytes
// (default is 16 MB), after which the cache is flushed
// - by default, key search is done case-insensitively, but you can specify
// another option here
// - by default, there is no timeout period, but you may specify a number of
// seconds of inactivity (i.e. no Add call) after which the cache is flushed
constructor Create(aMaxCacheRamUsed: cardinal = 16 shl 20;
aCaseSensitive: boolean = false; aTimeoutSeconds: cardinal = 0); reintroduce;
/// find a Key in the cache entries
// - return '' if nothing found: you may call Add() just after to insert
// the expected value in the cache
// - return the associated Value otherwise, and the associated integer tag
// if aResultTag address is supplied
// - this method is thread-safe, using the Safe R/W locker of this instance
function Find(const aKey: RawUtf8; aResultTag: PPtrInt = nil): RawUtf8;
/// add a Key/Value pair in the cache entries
// - returns true if aKey was not existing yet, and aValue has been stored
// - returns false if aKey did already exist in the internal cache, and
// its entry has been updated with the supplied aValue/aTag
// - this method is thread-safe, using the Safe R/W locker of this instance
function AddOrUpdate(const aKey, aValue: RawUtf8; aTag: PtrInt): boolean;
/// called after a write access to the database to flush the cache
// - set Count to 0
// - release all cache memory
// - returns TRUE if was flushed, i.e. if there was something in cache
// - this method is thread-safe, using the Safe R/W locker of this instance
function Reset: boolean;
/// access to the internal R/W locker, for thread-safe process
// - Find/AddOrUpdate methods are protected by this R/W lock
property Safe: TRWLock
read fSafe;
published
/// number of entries in the cache
property Count: integer
read fNameValue.Count;
/// the current global size of Values in RAM cache, in bytes
property RamUsed: cardinal
read fRamUsed;
/// the maximum RAM to be used for values, in bytes
// - the cache is flushed when ValueSize reaches this limit
// - default is 16 MB (16 shl 20)
property MaxRamUsed: cardinal
read fMaxRamUsed;
/// after how many seconds betwen Add() calls the cache should be flushed
// - equals 0 by default, meaning no time out
property TimeoutSeconds: cardinal
read fTimeoutSeconds;
end;
type
/// implement binary persistence and JSON serialization (not deserialization)
TSynPersistentStoreJson = class(TSynPersistentStore)
protected
// append "name" -> inherited should add properties to the JSON object
procedure AddJson(W: TJsonWriter); virtual;
public
/// serialize this instance as a JSON object
function SaveToJson(reformat: TTextWriterJsonFormat = jsonCompact): RawUtf8;
end;
{ *********** JSON-aware TSynDictionary Storage }
type
/// exception raised during TSynDictionary process
ESynDictionary = class(ESynException);
// internal flag, used only by TSynDictionary.InArray protected method
TSynDictionaryInArray = (
iaFind,
iaFindAndDelete,
iaFindAndUpdate,
iaFindAndAddIfNotExisting,
iaAdd,
iaAddForced);
/// event called by TSynDictionary.ForEach methods to iterate over stored items
// - if the implementation method returns TRUE, will continue the loop
// - if the implementation method returns FALSE, will stop values browsing
// - aOpaque is a custom value specified at ForEach() method call
TOnSynDictionary = function(const aKey; var aValue;
aIndex, aCount: integer; aOpaque: pointer): boolean of object;
/// event called by TSynDictionary.DeleteDeprecated
// - called just before deletion: return false to by-pass this item
TOnSynDictionaryCanDelete = function(const aKey, aValue;
aIndex: integer): boolean of object;
/// thread-safe dictionary to store some values from associated keys
// - will maintain a dynamic array of values, associated with a hash table
// for the keys, so that setting or retrieving values would be O(1)
// - thread-safe by default, since most methods are protected by a TSynLocker;
// use the ThreadUse option to tune thread-safety (e.g. disable or use a TRWLock)
// - TDynArray is a wrapper which does not store anything, whereas this class
// actually stores both keys and values, and provide convenient methods to
// access the stored data, including JSON serialization and binary storage
// - consider IKeyValue<> from mormot.core.collections.pas, for more robust
// generics-based code where TKey/TValue are propagated to all methods
TSynDictionary = class(TSynLocked)
protected
fKeys: TDynArrayHashed;
fValues: TDynArray;
fTimeOut: TCardinalDynArray;
fTimeOuts: TDynArray;
fCompressAlgo: TAlgoCompress;
fOnCanDelete: TOnSynDictionaryCanDelete;
function InternalAddUpdate(aKey, aValue: pointer; aUpdate: boolean): PtrInt;
function InArray(const aKey, aArrayValue; aAction: TSynDictionaryInArray;
aCompare: TDynArraySortCompare): boolean;
procedure SetTimeouts;
function ComputeNextTimeOut: cardinal;
{$ifdef HASINLINE} inline; {$endif}
function GetCapacity: integer;
procedure SetCapacity(const Value: integer);
function GetTimeOutSeconds: cardinal;
{$ifdef HASINLINE} inline; {$endif}
procedure SetTimeOutSeconds(Value: cardinal);
function GetThreadUse: TSynLockerUse;
{$ifdef HASINLINE} inline; {$endif}
procedure SetThreadUse(const Value: TSynLockerUse);
{$ifdef HASINLINE} inline; {$endif}
public
/// initialize the dictionary storage, specifying dynamic array keys/values
// - aKeyTypeInfo should be a dynamic array TypeInfo() RTTI pointer, which
// would store the keys within this TSynDictionary instance
// - aValueTypeInfo should be a dynamic array TypeInfo() RTTI pointer, which
// would store the values within this TSynDictionary instance
// - by default, string keys would be searched following exact case, unless
// aKeyCaseInsensitive is TRUE
// - you can set an optional timeout period, in seconds - you should call
// DeleteDeprecated periodically to search for deprecated items
constructor Create(aKeyTypeInfo, aValueTypeInfo: PRttiInfo;
aKeyCaseInsensitive: boolean = false; aTimeoutSeconds: cardinal = 0;
aCompressAlgo: TAlgoCompress = nil; aHasher: THasher = nil); reintroduce; virtual;
{$ifdef HASGENERICS}
/// initialize the dictionary storage, specifying keys/values as generic types
// - just a convenient wrapper around TSynDictionary.Create()
// - consider IKeyValue<> from mormot.core.collections.pas, for more robust
// generics-based code where TKey/TValue are propagated to all methods
class function New<TKey, TValue>(aKeyCaseInsensitive: boolean = false;
aTimeoutSeconds: cardinal = 0; aCompressAlgo: TAlgoCompress = nil;
aHasher: THasher = nil): TSynDictionary;
static; {$ifdef FPC} inline; {$endif}
{$endif HASGENERICS}
/// finalize the storage
// - would release all internal stored values
destructor Destroy; override;
/// try to add a value associated with a primary key
// - returns the index of the inserted item, -1 if aKey is already existing
// - this method is thread-safe, since it will lock the instance
function Add(const aKey, aValue): PtrInt;
/// store a value associated with a primary key
// - returns the index of the matching item
// - if aKey does not exist, a new entry is added
// - if aKey does exist, the existing entry is overriden with aValue
// - this method is thread-safe, since it will lock the instance
function AddOrUpdate(const aKey, aValue): PtrInt;
/// clear the value associated via aKey
// - does not delete the entry, but reset its value
// - returns the index of the matching item, -1 if aKey was not found
// - this method is thread-safe, since it will lock the instance
function Clear(const aKey): PtrInt;
/// delete all key/value stored in the current instance
procedure DeleteAll;
/// delete a key/value association from its supplied aKey
// - this would delete the entry, i.e. matching key and value pair
// - returns the index of the deleted item, -1 if aKey was not found
// - this method is thread-safe, since it will lock the instance
function Delete(const aKey): PtrInt;
/// delete a key/value association from its internal index
// - this method is not thread-safe: you should use fSafe.Lock/Unlock
// e.g. then Find/FindValue to retrieve the index value
function DeleteAt(aIndex: PtrInt): boolean;
/// search and delete all deprecated items according to TimeoutSeconds
// - returns how many items have been deleted
// - you can call this method very often: it will ensure that the
// search process will take place at most once every second
// - this method is thread-safe, but blocking during the process
function DeleteDeprecated(tix64: Int64 = 0): integer;
/// search of a primary key within the internal hashed dictionary
// - returns the index of the matching item, -1 if aKey was not found
// - if you want to access the value, you should use fSafe.Lock/Unlock:
// consider using Exists or FindAndCopy thread-safe methods instead
// - aUpdateTimeOut will update the associated timeout value of the entry
function Find(const aKey; aUpdateTimeOut: boolean = false): PtrInt;
/// search of a primary key within the internal hashed dictionary
// - returns a pointer to the matching item, nil if aKey was not found
// - if you want to access the value, you should use fSafe.Lock/Unlock:
// consider using Exists or FindAndCopy thread-safe methods instead
// - aUpdateTimeOut will update the associated timeout value of the entry
function FindValue(const aKey; aUpdateTimeOut: boolean = false;
aIndex: PPtrInt = nil): pointer;
/// search of a primary key within the internal hashed dictionary
// - returns a pointer to the matching or already existing value item
// - if you want to access the value, you should use fSafe.Lock/Unlock:
// consider using Exists or FindAndCopy thread-safe methods instead
// - will update the associated timeout value of the entry, if applying
function FindValueOrAdd(const aKey; var added: boolean;
aIndex: PPtrInt = nil): pointer;
/// search of a stored value by its primary key, and return a local copy
// - so this method is thread-safe
// - returns TRUE if aKey was found, FALSE if no match exists
// - will update the associated timeout value of the entry, unless
// aUpdateTimeOut is set to false
function FindAndCopy(const aKey;
var aValue; aUpdateTimeOut: boolean = true): boolean;
/// search of a stored value by its primary key, then delete and return it
// - returns TRUE if aKey was found, fill aValue with its content,
// and delete the entry in the internal storage
// - so this method is thread-safe
// - returns FALSE if no match exists
function FindAndExtract(const aKey; var aValue): boolean;
/// search of a stored value, and return seconds since its timeout was set
// - returns -1 if aKey was not found
// - this method is thread-safe
function FindAndGetElapsedSeconds(const aKey): integer;
/// search of a stored value, and delete it if its timeout was set too long ago
// - returns true if aKey was found and deleted
// - returns false if aKey was not found or the entry not deprecated
// - this method is thread-safe
function FindAndDeleteDeprecated(const aKey; aSeconds: integer): boolean;
/// search for a primary key presence
// - returns TRUE if aKey was found, FALSE if no match exists
// - this method is thread-safe
function Exists(const aKey): boolean;
/// search for a value presence
// - returns TRUE if aValue was found, FALSE if no match exists
// - this method is thread-safe, but will use O(n) slow browsing
function ExistsValue(const aValue; aCompare: TDynArraySortCompare = nil): boolean;
/// apply a specified event over all items stored in this dictionnary
// - would browse the list in the adding order
// - returns the number of times OnEach has been called
// - this method is thread-safe - if the callback modifies the data, set
// MayModify=true and call fSafe.Lock/UnLock when writing
function ForEach(const OnEach: TOnSynDictionary;
Opaque: pointer = nil; MayModify: boolean = true): integer; overload;
/// apply a specified event over matching items stored in this dictionnary
// - would browse the list in the adding order, comparing each key and/or
// value item with the supplied comparison functions and aKey/aValue content
// - returns the number of times OnMatch has been called, i.e. how many times
// KeyCompare(aKey,Keys[#])=0 or ValueCompare(aValue,Values[#])=0
// - this method is thread-safe - if the callback modifies the data, set
// MayModify=true and call fSafe.Lock/UnLock when writing
function ForEach(const OnMatch: TOnSynDictionary;
KeyCompare, ValueCompare: TDynArraySortCompare; const aKey, aValue;
Opaque: pointer = nil; MayModify: boolean = true): integer; overload;
/// touch the entry timeout field so that it won't be deprecated sooner
// - this method is not thread-safe, and is expected to be executed e.g.
// from a ForEach() TOnSynDictionary callback
procedure SetTimeoutAtIndex(aIndex: PtrInt);
/// search aArrayValue item in a dynamic-array value associated via aKey
// - expect the stored value to be a dynamic array itself
// - would search for aKey as primary key, then use TDynArray.Find
// to delete any aArrayValue match in the associated dynamic array
// - returns FALSE if Values is not a tkDynArray, or if aKey or aArrayValue
// were not found
// - this method is thread-safe, since it will lock the instance
function FindInArray(const aKey, aArrayValue;
aCompare: TDynArraySortCompare = nil): boolean;
/// search of a stored key by its associated key, and return a key local copy
// - won't use any hashed index but RTTI TDynArray.IndexOf search over
// over fValues() so is much slower than FindAndCopy() for huge arrays
// - will update the associated timeout value of the entry, unless
// aUpdateTimeOut is set to false
// - this method is thread-safe
// - returns TRUE if aValue was found, FALSE if no match exists
function FindKeyFromValue(const aValue; out aKey;
aUpdateTimeOut: boolean = true): boolean;
/// add aArrayValue item within a dynamic-array value associated via aKey
// - expect the stored value to be a dynamic array itself
// - would search for aKey as primary key, then use TDynArray.Add
// to add aArrayValue to the associated dynamic array
// - returns FALSE if Values is not a tkDynArray, or if aKey was not found
// - this method is thread-safe, since it will lock the instance
function AddInArray(const aKey, aArrayValue;
aCompare: TDynArraySortCompare = nil): boolean;
/// add aArrayValue item within a dynamic-array value associated via aKey
// - expect the stored value to be a dynamic array itself
// - would search for aKey as primary key, create the entry if not found,
// then use TDynArray.Add to add aArrayValue to the associated dynamic array
// - returns FALSE if Values is not a tkDynArray
// - this method is thread-safe, since it will lock the instance
function AddInArrayForced(const aKey, aArrayValue;
aCompare: TDynArraySortCompare = nil): boolean;
/// add once aArrayValue within a dynamic-array value associated via aKey
// - expect the stored value to be a dynamic array itself
// - would search for aKey as primary key, then use
// TDynArray.FindAndAddIfNotExisting to add once aArrayValue to the
// associated dynamic array
// - returns FALSE if Values is not a tkDynArray, or if aKey was not found
// - this method is thread-safe, since it will lock the instance
function AddOnceInArray(const aKey, aArrayValue;
aCompare: TDynArraySortCompare = nil): boolean;
/// clear aArrayValue item of a dynamic-array value associated via aKey
// - expect the stored value to be a dynamic array itself
// - would search for aKey as primary key, then use TDynArray.FindAndDelete
// to delete any aArrayValue match in the associated dynamic array
// - returns FALSE if Values is not a tkDynArray, or if aKey or aArrayValue
// were not found
// - this method is thread-safe, since it will lock the instance
function DeleteInArray(const aKey, aArrayValue;
aCompare: TDynArraySortCompare = nil): boolean;
/// replace aArrayValue item of a dynamic-array value associated via aKey
// - expect the stored value to be a dynamic array itself
// - would search for aKey as primary key, then use TDynArray.FindAndUpdate
// to delete any aArrayValue match in the associated dynamic array
// - returns FALSE if Values is not a tkDynArray, or if aKey or aArrayValue were
// not found
// - this method is thread-safe, since it will lock the instance
function UpdateInArray(const aKey, aArrayValue;
aCompare: TDynArraySortCompare = nil): boolean;
/// make a copy of the stored values
// - this method is thread-safe, since it will lock the instance during copy
// - resulting length(Dest) will match the exact values count
// - T*ObjArray will be reallocated and copied by content (using a temporary
// JSON serialization), unless ObjArrayByRef is true and pointers are copied
procedure CopyValues(out Dest; ObjArrayByRef: boolean = false);
/// serialize the content as a "key":value JSON object
procedure SaveToJson(W: TJsonWriter; EnumSetsAsText: boolean = false); overload;
/// serialize the content as a "key":value JSON object
function SaveToJson(EnumSetsAsText: boolean = false): RawUtf8; overload;
/// serialize the Values[] as a JSON array
function SaveValuesToJson(EnumSetsAsText: boolean = false;
ReFormat: TTextWriterJsonFormat = jsonCompact): RawUtf8;
/// unserialize the content from "key":value JSON object
// - if the JSON input may not be correct (i.e. if not coming from SaveToJson),
// you may set EnsureNoKeyCollision=TRUE for a slow but safe keys validation
function LoadFromJson(const Json: RawUtf8;
CustomVariantOptions: PDocVariantOptions = nil): boolean; overload;
/// unserialize the content from "key":value JSON object
// - note that input JSON buffer is not modified in place: no need to create
// a temporary copy if the buffer is about to be re-used
function LoadFromJson(Json: PUtf8Char;
CustomVariantOptions: PDocVariantOptions = nil): boolean; overload;
/// save the content as SynLZ-compressed raw binary data
// - warning: this format is tied to the values low-level RTTI, so if you
// change the value/key type definitions, LoadFromBinary() would fail
function SaveToBinary(NoCompression: boolean = false;
Algo: TAlgoCompress = nil): RawByteString;
/// load the content from SynLZ-compressed raw binary data
// - as previously saved by SaveToBinary method
function LoadFromBinary(const binary: RawByteString): boolean;
/// can be assigned to OnCanDeleteDeprecated to check
// TSynPersistentLock(aValue).Safe.IsLocked before actual deletion
class function OnCanDeleteSynPersistentLock(
const aKey, aValue; aIndex: PtrInt): boolean;
{$ifndef PUREMORMOT2}
class function OnCanDeleteSynPersistentLocked(
const aKey, aValue; aIndex: PtrInt): boolean;
{$endif PUREMORMOT2}
/// returns how many items are currently stored in this dictionary
// - this method is NOT thread-safe so should be protected by fSafe.Lock/UnLock
function Count: integer;
{$ifdef HASINLINE}inline;{$endif}
/// direct access to the primary key identifiers
// - if you want to access the keys, you should use fSafe.Lock/Unlock
property Keys: TDynArrayHashed
read fKeys;
/// direct access to the associated stored values
// - if you want to access the values, you should use fSafe.Lock/Unlock
property Values: TDynArray
read fValues;
/// defines how many items are currently stored in Keys/Values internal arrays
// - if you set a maximum size of this store (even a rough size), Add() are
// likely to be up to twice faster than letting the table grow by chunks
property Capacity: integer
read GetCapacity write SetCapacity;
/// direct low-level access to the internal access tick (GetTickCount64 shr 10)
// - may be nil if TimeOutSeconds=0
property TimeOut: TCardinalDynArray
read fTimeOut;
/// returns the aTimeOutSeconds parameter value, as specified to Create()
// - warning: setting a new timeout will clear all previous content
property TimeOutSeconds: cardinal
read GetTimeOutSeconds write SetTimeOutSeconds;
/// the compression algorithm used for binary serialization
property CompressAlgo: TAlgoCompress
read fCompressAlgo write fCompressAlgo;
/// callback to by-pass DeleteDeprecated deletion by returning false
// - can be assigned e.g. to OnCanDeleteSynPersistentLock if Value is a
// TSynPersistentLock instance, to avoid any potential access violation
property OnCanDeleteDeprecated: TOnSynDictionaryCanDelete
read fOnCanDelete write fOnCanDelete;
/// can tune TSynDictionary threading process depending on your use case
// - will redirect to the internal Safe TSynLocker instance
// - warning: to be set only before any process is done
// - advice: any performance impact should always be monitored, not guessed
property ThreadUse: TSynLockerUse
read GetThreadUse write SetThreadUse;
end;
const
// TSynDictionary.fSafe.Padding[DIC_*] place holders - defined here for inlining
DIC_KEYCOUNT = 0; // Keys.Count integer
DIC_KEY = 1; // Key.Value pointer
DIC_VALUECOUNT = 2; // Values.Count integer
DIC_VALUE = 3; // Values.Value pointer
DIC_TIMECOUNT = 4; // Timeouts.Count integer
DIC_TIMESEC = 5; // Timeouts Seconds integer
DIC_TIMETIX = 6; // last GetTickCount64 shr 10 integer
{ ********** Low-level JSON Serialization for any kind of Values }
type
/// internal stack-allocated structure for nested JSON serialization
// - defined here for low-level use within TRttiJsonSave functions
{$ifdef USERECORDWITHMETHODS}
TJsonSaveContext = record
{$else}
TJsonSaveContext = object
{$endif USERECORDWITHMETHODS}
public
/// the associated stream writer for the JSON output
W: TJsonWriter;
/// serialization options as specified for this process
// - as used by AddShort/Add64/AddDateTime methods
Options: TTextWriterWriteObjectOptions;
/// the RTTI information of the current serialized type
Info: TRttiCustom;
/// the RTTI information of the current serialized property
// - is likely to be nil outside of properties serialization
Prop: PRttiCustomProp;
/// initialize this low-level JSON serialization context
procedure Init(WR: TJsonWriter;
WriteOptions: TTextWriterWriteObjectOptions; Rtti: TRttiCustom);
{$ifdef HASINLINE}inline;{$endif}
/// some basic function to append a shorstring JSON value according to Options
procedure AddShort(PS: PShortString);
/// some basic function to append an Int64 JSON value according to Options
procedure Add64(Value: PInt64; UnSigned: boolean);
{$ifdef HASINLINE}inline;{$endif}
/// some basic function to append a TDateTime JSON value according to Options
procedure AddDateTime(Value: PDateTime; WithMS: boolean);
/// some basic function to append a "name":boolean JSON pair value
procedure AddShortBoolean(PS: PShortString; Value: boolean);
end;
/// internal function handler for JSON persistence of any TRttiParserType value
// - i.e. the kind of functions called via PT_JSONSAVE[] lookup table
TRttiJsonSave = procedure(Data: pointer; const Ctxt: TJsonSaveContext);
{ ********** Low-level JSON Unserialization for any kind of Values }
type
/// store one name/value pair of raw UTF-8 content, from a JSON buffer
// - used e.g. by JsonDecode() overloaded function or UrlEncodeJsonObject()
// to returns names/values
TNameValuePUtf8Char = record
/// pointer and length to the actual UTF-8 name text
Name: TValuePUtf8Char;
/// pointer and length to the actual UTF-8 value text
Value: TValuePUtf8Char;
end;
/// used e.g. by JsonDecode() overloaded function to returns name/value pairs
TNameValuePUtf8CharDynArray = array of TNameValuePUtf8Char;
/// decode the supplied UTF-8 JSON content for the supplied names
// - data will be set in Values, according to the Names supplied e.g.
// ! JsonDecode(JSON,['name','year'],@Values) -> Values[0].Value='John'; Values[1].Value='1972';
// - if any supplied name wasn't found its corresponding Values[] will be nil
// - this procedure will decode the JSON content in-memory, i.e. the PUtf8Char
// array is created inside JSON, which is therefore modified: make a private
// copy first if you want to reuse the JSON content
// - if HandleValuesAsObjectOrArray is TRUE, then this procedure will handle
// JSON arrays or objects
// - support enhanced JSON syntax, e.g. '{name:'"John",year:1972}' is decoded
// just like '{"name":'"John","year":1972}'
procedure JsonDecode(var Json: RawUtf8; const Names: array of RawUtf8;
Values: PValuePUtf8CharArray;
HandleValuesAsObjectOrArray: boolean = false); overload;
/// decode the supplied UTF-8 JSON content for the supplied names
// - an overloaded function when the JSON is supplied as a RawJson variable
procedure JsonDecode(var Json: RawJson; const Names: array of RawUtf8;
Values: PValuePUtf8CharArray;
HandleValuesAsObjectOrArray: boolean = false); overload;
/// decode the supplied UTF-8 JSON object for the supplied field names
// - data will be set in Values, according to the Names supplied e.g.
// ! JsonDecode(P,['name','year'],Values) -> Values[0]^='John'; Values[1]^='1972';
// - if any supplied name wasn't found its corresponding Values[] will be nil
// - this procedure will decode the JSON content in-memory, i.e. the PUtf8Char
// array is created inside P, which is therefore modified: make a private
// copy first if you want to reuse the JSON content
// - if HandleValuesAsObjectOrArray is TRUE, then this procedure will handle
// JSON arrays or objects
// - if ValuesLen is set, ValuesLen[] will contain the length of each Values[]
// - returns a pointer to the next content item in the JSON buffer
function JsonDecode(P: PUtf8Char; const Names: array of RawUtf8;
Values: PValuePUtf8CharArray;
HandleValuesAsObjectOrArray: boolean = false): PUtf8Char; overload;
/// decode the supplied UTF-8 JSON object for the supplied field names
// - overloaded function expecting the names supplied as a constant array
// - slightly faster than the one using "const Names: array of RawUtf8"
function JsonDecode(P: PUtf8Char; Names: PPUtf8CharArray; NamesCount: integer;
Values: PValuePUtf8CharArray;
HandleValuesAsObjectOrArray: boolean = false): PUtf8Char; overload;
/// decode the supplied UTF-8 JSON object into an array of name/value pairs
// - this procedure will decode the JSON content in-memory, i.e. the PUtf8Char
// array is created inside JSON, which is therefore modified: make a private
// copy first if you want to reuse the JSON content
// - the supplied JSON buffer should stay available until Name/Value pointers
// from returned Values[] are accessed
// - if HandleValuesAsObjectOrArray is TRUE, then this procedure will handle
// JSON arrays or objects
// - support enhanced JSON syntax, e.g. '{name:'"John",year:1972}' is decoded
// just like '{"name":'"John","year":1972}'
function JsonDecode(P: PUtf8Char; out Values: TNameValuePUtf8CharDynArray;
HandleValuesAsObjectOrArray: boolean = false): PUtf8Char; overload;
/// decode the supplied UTF-8 JSON object for the one supplied field name
// - this function will decode the JSON content in-memory, so will unescape it
// in-place: it must be called only once with the same JSON data
function JsonDecode(var Json: RawUtf8; const aName: RawUtf8 = 'result';
WasString: PBoolean = nil;
HandleValuesAsObjectOrArray: boolean = false): RawUtf8; overload;
{$ifdef HASINLINE} inline; {$endif}
/// decode the supplied UTF-8 JSON object for the one supplied field name
// - this function will decode and modify the input JSON buffer in-place
function JsonDecode(Json: PUtf8Char; const aName: RawUtf8;
WasString: PBoolean; HandleValuesAsObjectOrArray: boolean): RawUtf8; overload;
type
/// available options for JSON parsing process
// - by default, parsing will fail if a JSON field name is not part of the
// object published properties, unless jpoIgnoreUnknownProperty is defined -
// this option will also ignore read-only properties (i.e. with only a getter)
// - by default, function will check that the supplied JSON value will
// be a JSON string when the property is a string, unless jpoIgnoreStringType
// is defined and JSON numbers are accepted and stored as text
// - by default any unexpected value for enumerations will be marked as
// invalid, unless jpoIgnoreUnknownEnum is defined, so that in such case the
// ordinal 0 value is left, and loading continues
// - by default, only simple kind of variant types (string/numbers) are
// handled: set jpoHandleCustomVariants if you want to handle any custom -
// in this case , it will handle direct JSON [array] of {object}: but if you
// also define jpoHandleCustomVariantsWithinString, it will also try to
// un-escape a JSON string first, i.e. handle "[array]" or "{object}" content
// (may be used e.g. when JSON has been retrieved from a database TEXT column)
// - by default, a temporary instance will be created if a published field
// has a setter, and the instance is expected to be released later by the
// owner class: set jpoSetterExpectsToFreeTempInstance to let JsonParser
// (and TPropInfo.ClassFromJson) release it when the setter returns, and
// jpoSetterNoCreate to avoid the published field instance creation
// - set jpoAllowInt64Hex to let Int64/QWord fields accept hexadecimal string
// (as generated e.g. via the woInt64AsHex option)
// - by default, double values won't be stored as variant values, unless
// jpoAllowDouble is set - see also dvoAllowDoubleValue in TDocVariantOptions
// - jpoObjectListClassNameGlobalFindClass would also search for "ClassName":
// TObjectList serialized field with the global Classes.FindClass() function
// - null will release any class instance, unless jpoNullDontReleaseObjectInstance
// is set which will leave the instance untouched
// - values will be left untouched before parsing, unless jpoClearValues
// is defined, to void existing record fields or class published properties
TJsonParserOption = (
jpoIgnoreUnknownProperty,
jpoIgnoreStringType,
jpoIgnoreUnknownEnum,
jpoHandleCustomVariants,
jpoHandleCustomVariantsWithinString,
jpoSetterExpectsToFreeTempInstance,
jpoSetterNoCreate,
jpoAllowInt64Hex,
jpoAllowDouble,
jpoObjectListClassNameGlobalFindClass,
jpoNullDontReleaseObjectInstance,
jpoClearValues);
/// set of options for JsonParser() parsing process
TJsonParserOptions = set of TJsonParserOption;
/// efficient execution context of the JSON parser
// - defined here for low-level use of TRttiJsonLoad functions
// - inherit from TGetJsonField to include ParseNext/ParseNextAny unserialized
// Value/ValueLen and flags, and Json as current position in the JSON input
{$ifdef USERECORDWITHMETHODS}
TJsonParserContext = record
public
Get: TGetJsonField;
function Json: PUtf8Char; {$ifdef HASINLINE} inline; {$endif}
function Value: PUtf8Char; {$ifdef HASINLINE} inline; {$endif}
function ValueLen: PtrInt; {$ifdef HASINLINE} inline; {$endif}
function WasString: boolean; {$ifdef HASINLINE} inline; {$endif}
function EndOfObject: AnsiChar; {$ifdef HASINLINE} inline; {$endif}
{$else}
TJsonParserContext = object(TGetJsonField)
{$endif USERECORDWITHMETHODS}
public
/// true if the last parsing succeeded
Valid: boolean;
/// customize parsing
Options: TJsonParserOptions;
/// how TDocVariant should be created
CustomVariant: PDocVariantOptions;
/// contains the current value RTTI
Info: TRttiCustom;
/// contains the current property value RTTI
Prop: PRttiCustomProp;
/// force the item class when reading a TObjectList without "ClassName":...
ObjectListItem: TRttiCustom;
/// optional RawUtf8 values interning
Interning: TRawUtf8Interning;
/// TDocVariant initialization options
DVO: TDocVariantOptions;
/// initialize this unserialization context
procedure InitParser(P: PUtf8Char; Rtti: TRttiCustom; O: TJsonParserOptions;
CV: PDocVariantOptions; ObjectListItemClass: TClass;
RawUtf8Interning: TRawUtf8Interning);
/// call inherited GetJsonField() to retrieve the next JSON value
// - on success, return true and set Value/ValueLen and WasString fields
function ParseNext: boolean;
{$ifdef HASINLINE}inline;{$endif}
/// call inherited GetJsonFieldOrObjectOrArray() to retrieve the next JSON value
// - on success, return true and set Value/ValueLen and WasString fields
function ParseNextAny: boolean;
{$ifdef HASINLINE}inline;{$endif}
/// retrieve the next JSON value as UTF-8 text
function ParseUtf8: RawUtf8;
/// retrieve the next JSON value as RTL string text
function ParseString: string;
/// retrieve the next JSON value as integer
function ParseInteger: Int64;
/// set the EndOfObject field of a JSON buffer, just like GetJsonField() does
// - to be called whan a JSON object or JSON array has been manually parsed
procedure ParseEndOfObject;
{$ifdef HASINLINE}inline;{$endif}
/// parse a 'null' value from JSON buffer
function ParseNull: boolean;
{$ifdef HASINLINE}inline;{$endif}
/// parse initial '[' token from JSON buffer
// - once all the nested values have been read, call ParseEndOfObject
function ParseArray: boolean;
/// parse initial '{' token from JSON buffer
// - once all the nested values have been read, call ParseEndOfObject
function ParseObject: boolean; overload;
/// wrapper around JsonDecode() to easily get JSON object values
function ParseObject(const Names: array of RawUtf8;
Values: PValuePUtf8CharArray;
HandleValuesAsObjectOrArray: boolean = false): boolean; overload;
/// parse a JSON object from the buffer into a
// - if ObjectListItem was not defined, expect the JSON input to start as
// '{"ClassName":"TMyClass",...}'
function ParseNewObject: TObject;
{$ifdef HASINLINE}inline;{$endif}
/// parse a property value, properly calling any setter
procedure ParsePropComplex(Data: pointer);
end;
PJsonParserContext = ^TJsonParserContext;
/// internal function handler for JSON reading of any TRttiParserType value
TRttiJsonLoad = procedure(Data: pointer; var Ctxt: TJsonParserContext);
var
/// default options for the JSON parser
// - as supplied to LoadJson() with Tolerant=false
// - defined as var, not as const, to allow process-wide override
JSONPARSER_DEFAULTOPTIONS: TJsonParserOptions = [];
/// some open-minded options for the JSON parser
// - as supplied to LoadJson() with Tolerant=true
// - won't block JSON unserialization due to some minor unexpected values
// - used e.g. by TObjArraySerializer.CustomReader and
// TInterfacedObjectFake.FakeCall/TServiceMethodExecute.ExecuteJson methods
// - defined as var, not as const, to allow process-wide override
JSONPARSER_TOLERANTOPTIONS: TJsonParserOptions =
[jpoHandleCustomVariants, jpoIgnoreUnknownEnum,
jpoIgnoreUnknownProperty, jpoIgnoreStringType, jpoAllowInt64Hex];
/// access default (false) or tolerant (true) JSON parser options
// - to be used as JSONPARSER_DEFAULTORTOLERANTOPTIONS[tolerant]
JSONPARSER_DEFAULTORTOLERANTOPTIONS: array[boolean] of TJsonParserOptions = (
[],
[jpoHandleCustomVariants, jpoIgnoreUnknownEnum,
jpoIgnoreUnknownProperty, jpoIgnoreStringType, jpoAllowInt64Hex]);
// backward compatibility types redirections
{$ifndef PUREMORMOT2}
type
TJsonToObjectOption = TJsonParserOption;
TJsonToObjectOptions = TJsonParserOptions;
const
j2oSQLRawBlobAsBase64 = woRawBlobAsBase64;
j2oIgnoreUnknownProperty = jpoIgnoreUnknownProperty;
j2oIgnoreStringType = jpoIgnoreStringType;
j2oIgnoreUnknownEnum = jpoIgnoreUnknownEnum;
j2oHandleCustomVariants = jpoHandleCustomVariants;
j2oHandleCustomVariantsWithinString = jpoHandleCustomVariantsWithinString;
j2oSetterExpectsToFreeTempInstance = jpoSetterExpectsToFreeTempInstance;
j2oSetterNoCreate = jpoSetterNoCreate;
j2oAllowInt64Hex = jpoAllowInt64Hex;
const
JSONTOOBJECT_TOLERANTOPTIONS: TJsonParserOptions =
[jpoHandleCustomVariants, jpoIgnoreUnknownEnum,
jpoIgnoreUnknownProperty, jpoIgnoreStringType, jpoAllowInt64Hex];
{$endif PUREMORMOT2}
{ ********** Custom JSON Serialization }
type
/// the callback signature used by TRttiJson for serializing JSON data
// - Data^ should be written into W, with the supplied Options
TOnRttiJsonWrite = procedure(W: TJsonWriter; Data: pointer;
Options: TTextWriterWriteObjectOptions) of object;
/// the callback signature used by TRttiJson for unserializing JSON data
// - set Context.Valid=true if Context.JSON has been parsed into Data^
TOnRttiJsonRead = procedure(var Context: TJsonParserContext;
Data: pointer) of object;
/// the callback signature used by TRttiJson for serializing JSON classes
// - Instance should be written into W, with the supplied Options
// - is in fact a convenient alias to the TOnRttiJsonWrite callback
TOnClassJsonWrite = procedure(W: TJsonWriter; Instance: TObject;
Options: TTextWriterWriteObjectOptions) of object;
/// the callback signature used by TRttiJson for unserializing JSON classes
// - set Context.Valid=true if Context.JSON has been parsed into Instance
// - is in fact a convenient alias to the TOnRttiJsonRead callback
TOnClassJsonRead = procedure(var Context: TJsonParserContext;
Instance: TObject) of object;
/// JSON-aware TRttiCustom class - used for global RttiCustom: TRttiCustomList
TRttiJson = class(TRttiCustom)
protected
fCompare: array[{CaseInsens:}boolean] of TRttiCompare; // for ValueCompare
fIncludeReadOptions: TJsonParserOptions;
fIncludeWriteOptions: TTextWriterWriteObjectOptions;
// overriden for proper JSON process - set fJsonSave and fJsonLoad
function SetParserType(aParser: TRttiParserType;
aParserComplex: TRttiParserComplexType): TRttiCustom; override;
procedure SetValueClass(aClass: TClass; aInfo: PRttiInfo); override;
public
/// simple wrapper around TRttiJsonSave(fJsonSave)
procedure RawSaveJson(Data: pointer; const Ctxt: TJsonSaveContext);
{$ifdef HASINLINE}inline;{$endif}
/// simple wrapper around TRttiJsonLoad(fJsonLoad)
procedure RawLoadJson(Data: pointer; var Ctxt: TJsonParserContext);
{$ifdef HASINLINE}inline;{$endif}
/// create and parse a new TObject instance of this rkClass
function ParseNewInstance(var Context: TJsonParserContext): TObject;
/// compare two stored values of this type
function ValueCompare(Data, Other: pointer; CaseInsensitive: boolean): integer; override;
/// fill a variant with a stored value of this type
// - complex values can be returned as TDocVariant after JSON conversion,
// using e.g. @JSON_[mFast] as optional Options parameter
function ValueToVariant(Data: pointer; out Dest: TVarData;
Options: pointer{PDocVariantOptions} = nil): PtrInt; override;
/// unserialize some JSON input into Data^
// - as used by LoadJson() and similar high-level functions
procedure ValueLoadJson(Data: pointer; var Json: PUtf8Char; EndOfObject: PUtf8Char;
ParserOptions: TJsonParserOptions; CustomVariantOptions: PDocVariantOptions;
ObjectListItemClass: TClass; Interning: TRawUtf8Interning);
/// how many iterations could be done one a given value
// - returns -1 if the value is not iterable, or length(DynArray) or
// TRawUtf8List.Count or TList.Count or TSynList.Count
// - note that TStrings values are not supported, because they require a
// temporary string variable for their getter
function ValueIterateCount(Data: pointer): integer; override;
/// iterate over one sub-item of a given value
// - returns nil if the value is not iterable or Index is out of range
// - returns a pointer to the value, rkClass/rkLString kinds being already
// resolved (as the TList/TSynList/TRawUtf8List items are returned),
// so you can directly trans-type the result to TObject() or RawUtf8()
// - ResultRtti holds the type of the resolved result pointer
// - note that TStrings values are not supported, because they require a
// temporary string variable for their getter method
function ValueIterate(Data: pointer; Index: PtrUInt;
out ResultRtti: TRttiCustom): pointer; override;
/// lookup a value by a path name e.g. 'one.two.three' nested values
// - for a record/class, will search for a property name
// - for a TDocVariant/TBsonVariant, calls TSynInvokeableVariantType.IntGet
// - for an enumeration or set, will return true/false about the enum name
// - for a string, Data^ will be compared to the name
function ValueByPath(var Data: pointer; Path: PUtf8Char; var Temp: TVarData;
PathDelim: AnsiChar = '.'): TRttiCustom; override;
/// efficient search of TRttiJson from a given RTTI TypeInfo()
// - to be used instead of Rtti.Find() to return directly the TRttiJson instance
class function Find(Info: PRttiInfo): TRttiJson;
{$ifdef HASINLINE}inline;{$endif}
/// register a custom callback for JSON serialization of a given TypeInfo()
// - for a dynamic array, will customize the item serialization callbacks
// - replace deprecated TTextWriter.RegisterCustomJSONSerializer() method
class function RegisterCustomSerializer(Info: PRttiInfo;
const Reader: TOnRttiJsonRead; const Writer: TOnRttiJsonWrite): TRttiJson;
/// unregister any custom callback for JSON serialization of a given TypeInfo()
// - will also work after RegisterFromText()
class function UnRegisterCustomSerializer(Info: PRttiInfo): TRttiJson;
/// register a custom callback for JSON serialization of a given class
// - replace deprecated TTextWriter.RegisterCustomJSONSerializer() method
class function RegisterCustomSerializerClass(ObjectClass: TClass;
const Reader: TOnClassJsonRead; const Writer: TOnClassJsonWrite): TRttiJson;
/// unregister any custom callback for JSON serialization of a given class
class function UnRegisterCustomSerializerClass(ObjectClass: TClass): TRttiJson;
/// register TypeInfo() custom JSON serialization for a given dynamic
// array or record
// - to be used instead of homonomous Rtti.RegisterFromText() to supply
// an additional set of serialization/unserialization JSON options
class function RegisterFromText(DynArrayOrRecord: PRttiInfo;
const RttiDefinition: RawUtf8;
IncludeReadOptions: TJsonParserOptions;
IncludeWriteOptions: TTextWriterWriteObjectOptions): TRttiJson;
/// define an additional set of unserialization JSON options
// - is included for this type to the supplied TJsonParserOptions
property IncludeReadOptions: TJsonParserOptions
read fIncludeReadOptions write fIncludeReadOptions;
/// define an additional set of serialization JSON options
// - is included for this type to the supplied TTextWriterWriteObjectOptions
property IncludeWriteOptions: TTextWriterWriteObjectOptions
read fIncludeWriteOptions write fIncludeWriteOptions;
end;
{ ********** JSON Serialization Wrapper Functions }
/// encode the supplied data as an UTF-8 valid JSON object content
// - data must be supplied two by two, as Name,Value pairs, e.g.
// ! JsonEncode(['name','John','year',1972]) = '{"name":"John","year":1972}'
// - or you can specify nested arrays or objects with '['..']' or '{'..'}':
// ! J := JsonEncode(['doc','{','name','John','abc','[','a','b','c',']','}','id',123]);
// ! assert(J='{"doc":{"name":"John","abc":["a","b","c"]},"id":123}');
// - note that, due to a Delphi compiler limitation, cardinal values should be
// type-casted to Int64() (otherwise the integer mapped value will be converted)
// - you can pass nil as parameter for a null JSON value
function JsonEncode(const NameValuePairs: array of const): RawUtf8; overload;
/// encode the supplied (extended) JSON content, with parameters,
// as an UTF-8 valid JSON object content
// - in addition to the JSON RFC specification strict mode, this method will
// handle some BSON-like extensions, e.g. unquoted field names:
// ! aJson := JsonEncode('{id:?,%:{name:?,birthyear:?}}',['doc'],[10,'John',1982]);
// - you can use nested _Obj() / _Arr() instances
// ! aJson := JsonEncode('{%:{$in:[?,?]}}',['type'],['food','snack']);
// ! aJson := JsonEncode('{type:{$in:?}}',[],[_Arr(['food','snack'])]);
// ! // will both return
// ! '{"type":{"$in":["food","snack"]}}')
// - if the mormot.db.nosql.bson unit is used in the application, the MongoDB
// Shell syntax will also be recognized to create TBsonVariant, like
// ! new Date() ObjectId() MinKey MaxKey /<jRegex>/<jOptions>
// see @http://docs.mongodb.org/manual/reference/mongodb-extended-json
// ! aJson := JsonEncode('{name:?,field:/%/i}',['acme.*corp'],['John']))
// ! // will return
// ! '{"name":"John","field":{"$regex":"acme.*corp","$options":"i"}}'
// - will call internally _JsonFastFmt() to create a temporary TDocVariant with
// all its features - so is slightly slower than other JsonEncode* functions
function JsonEncode(const Format: RawUtf8;
const Args, Params: array of const): RawUtf8; overload;
/// encode the supplied RawUtf8 array data as an UTF-8 valid JSON array content
function JsonEncodeArrayUtf8(
const Values: array of RawUtf8): RawUtf8; overload;
/// encode the supplied integer array data as a valid JSON array
function JsonEncodeArrayInteger(
const Values: array of integer): RawUtf8; overload;
/// encode the supplied floating-point array data as a valid JSON array
function JsonEncodeArrayDouble(
const Values: array of double): RawUtf8; overload;
/// encode the supplied array data as a valid JSON array content
// - if WithoutBraces is TRUE, no [ ] will be generated
// - note that, due to a Delphi compiler limitation, cardinal values should be
// type-casted to Int64() (otherwise the integer mapped value will be converted)
function JsonEncodeArrayOfConst(const Values: array of const;
WithoutBraces: boolean = false): RawUtf8; overload;
/// encode the supplied array data as a valid JSON array content
// - if WithoutBraces is TRUE, no [ ] will be generated
// - note that, due to a Delphi compiler limitation, cardinal values should be
// type-casted to Int64() (otherwise the integer mapped value will be converted)
procedure JsonEncodeArrayOfConst(const Values: array of const;
WithoutBraces: boolean; var result: RawUtf8); overload;
/// encode as JSON {"name":value} object, from a potential SQL quoted value
// - will unquote the SQLValue using TJsonWriter.AddQuotedStringAsJson()
procedure JsonEncodeNameSQLValue(const Name, SQLValue: RawUtf8;
var result: RawUtf8);
var
/// the options used by TObjArraySerializer, TInterfacedObjectFake and
// TServiceMethodExecute when serializing values as JSON
// - used as DEFAULT_WRITEOPTIONS[DontStoreVoidJson]
// - you can modify this global variable to customize the whole process
DEFAULT_WRITEOPTIONS: array[boolean] of TTextWriterWriteObjectOptions = (
[woDontStoreDefault, woRawBlobAsBase64],
[woDontStoreDefault, woDontStoreVoid, woRawBlobAsBase64]);
/// the options used by TSynJsonFileSettings.SaveIfNeeded
// - you can modify this global variable to customize the whole process
SETTINGS_WRITEOPTIONS: TTextWriterWriteObjectOptions =
[woHumanReadable, woStoreStoredFalse, woHumanReadableFullSetsAsStar,
woHumanReadableEnumSetAsComment, woInt64AsHex];
/// the options used by TServiceFactoryServer.OnLogRestExecuteMethod
// - you can modify this global variable to customize the whole process
SERVICELOG_WRITEOPTIONS: TTextWriterWriteObjectOptions =
[woDontStoreDefault, woDontStoreVoid, woHideSensitivePersonalInformation];
/// serialize most kind of content as JSON, using its RTTI
// - is just a wrapper around TJsonWriter.AddTypedJson()
// - so would handle tkClass, tkEnumeration, tkSet, tkRecord, tkDynArray,
// tkVariant kind of content - other kinds would return 'null'
// - you can override serialization options if needed
procedure SaveJson(const Value; TypeInfo: PRttiInfo;
Options: TTextWriterOptions; var result: RawUtf8;
ObjectOptions: TTextWriterWriteObjectOptions = []); overload;
/// serialize most kind of content as JSON, using its RTTI
// - is just a wrapper around TJsonWriter.AddTypedJson()
function SaveJson(const Value; TypeInfo: PRttiInfo;
EnumSetsAsText: boolean): RawUtf8; overload;
{$ifdef HASINLINE}inline;{$endif}
/// serialize most kind of content as JSON, using its RTTI
function SaveJson(const Value; TypeInfo: PRttiInfo): RawUtf8; overload;
/// serialize most kind of content as JSON, using its RTTI and a type name
// - could be used if you know the type name and not the TypeInfo()
// - will use Rtti.RegisterTypeFromName() so the type should be known, i.e. be
// a simple type, or have been alredy registered
// - returns '' if TypeName is not recognized
function SaveJson(const Value; const TypeName: RawUtf8;
Options: TTextWriterOptions = []): RawUtf8; overload;
{$ifdef FPC}
/// special global function exported to Lazarus for runtime evaluation, within
// latest trunk fpdebug, of any variable as JSON, using mORMot RTTI
// - the "JsonForDebug" function name is recognized by recent fpdebug, and
// called and try to serialize a variable as JSON in Lazarus debug windows - see
// https://wiki.freepascal.org/IDE_Window:_Ide_Options_-_Backend_Value_Converter
// - this function will recognize 1) all type names registered to mORMot RTTI
// (using Rtti.Register*() methods), 2) T* class types guessing from their VMT,
// 3) I* types recognized as interface, and their associated "as TObject" class
// instance will be serialized
procedure JsonForDebug(Value: pointer; var TypeName: RawUtf8;
out JsonResultText: RawUtf8);
{$endif FPC}
/// save record into its JSON serialization as saved by TJsonWriter.AddRecordJson
// - will use default Base64 encoding over RecordSave() binary - or custom true
// JSON format (as set by Rtti.RegisterFromText/TRttiJson.RegisterCustomSerializer
// or via enhanced RTTI), if available (following EnumSetsAsText optional
// parameter for nested enumerates and sets)
function RecordSaveJson(const Rec; TypeInfo: PRttiInfo;
EnumSetsAsText: boolean = false): RawUtf8;
{$ifdef HASINLINE}inline;{$endif}
/// serialize a dynamic array content as JSON
// - Value shall be set to the source dynamic array field
// - is just a wrapper around TJsonWriter.AddDynArrayJson(), creating
// a temporary TDynArray wrapper on the stack
// - to be used e.g. for custom record JSON serialization, within a
// TDynArrayJsonCustomWriter callback or Rtti.RegisterFromText()
// (following EnumSetsAsText optional parameter for nested enumerates and sets)
function DynArraySaveJson(const Value; TypeInfo: PRttiInfo;
EnumSetsAsText: boolean = false): RawUtf8;
/// serialize a dynamic array content, supplied as raw binary buffer, as JSON
// - Value shall be set to the source dynamic array field
// - is just a wrapper around TJsonWriter.AddDynArrayJson(), creating
// a temporary TDynArray wrapper on the stack
// - to be used e.g. for custom record JSON serialization, within a
// TDynArrayJsonCustomWriter callback or Rtti.RegisterFromText()
function DynArrayBlobSaveJson(TypeInfo: PRttiInfo; BlobValue: pointer;
BlobLen: PtrInt): RawUtf8;
/// wrapper to serialize a T*ObjArray dynamic array as JSON
// - for proper serialization on Delphi 7-2009, use Rtti.RegisterObjArray()
function ObjArrayToJson(const aObjArray;
aOptions: TTextWriterWriteObjectOptions = [woDontStoreDefault]): RawUtf8;
/// will serialize set of TObject into its UTF-8 JSON representation
// - follows ObjectToJson()/TJsonWriter.WriterObject() functions output
// - if Names is not supplied, the corresponding class names would be used
function ObjectsToJson(const Names: array of RawUtf8; const Values: array of TObject;
Options: TTextWriterWriteObjectOptions = [woDontStoreDefault]): RawUtf8;
/// persist a class instance into a JSON file
// - returns TRUE on success, false on error (e.g. the file name is invalid
// or the file is existing and could not be overwritten)
// - see ObjectToJson() as defined in momrot.core.text.pas
function ObjectToJsonFile(Value: TObject; const JsonFile: TFileName;
Options: TTextWriterWriteObjectOptions = [woHumanReadable]): boolean;
/// get any (potentially nested) object property by path
// - complex values (e.g. dynamic array properties) will be returned as
// TDocVariant after JSON conversion
function GetValueObject(Instance: TObject; const Path: RawUtf8;
out Value: variant): boolean;
/// unserialize most kind of content as JSON, using its RTTI, as saved by
// TJsonWriter.AddRecordJson / RecordSaveJson
// - same implementation than GetDataFromJson() global low-level function
// - returns nil on error, or the end of buffer on success
// - warning: the JSON buffer will be modified in-place during process - use
// LoadJson() instead or a make temporary copy if you need to access it later
function LoadJsonInPlace(var Value; Json: PUtf8Char; TypeInfo: PRttiInfo;
EndOfObject: PUtf8Char = nil; CustomVariantOptions: PDocVariantOptions = nil;
Tolerant: boolean = true; Interning: TRawUtf8Interning = nil): PUtf8Char;
/// unserialize most kind of content as JSON, using its RTTI, as saved by
// TJsonWriter.AddRecordJson / RecordSaveJson
// - this overloaded function will make a private copy before parsing it,
// so is safe with a read/only or shared string - but slightly slower
function LoadJson(var Value; const Json: RawUtf8; TypeInfo: PRttiInfo;
EndOfObject: PUtf8Char = nil; CustomVariantOptions: PDocVariantOptions = nil;
Tolerant: boolean = true; Interning: TRawUtf8Interning = nil): boolean;
/// fill a record content from a JSON serialization as saved by
// TJsonWriter.AddRecordJson / RecordSaveJson
// - will use default Base64 encoding over RecordSave() binary - or custom
// JSON format (as set by Rtti.RegisterFromText/TRttiJson.RegisterCustomSerializer
// or via enhanced RTTI), if available
// - returns nil on error, or the end of buffer on success
// - warning: the JSON buffer will be modified in-place during process - use
// a temporary copy if you need to access it later or if the string comes from
// a constant (refcount=-1) - see e.g. the overloaded RecordLoadJson()
function RecordLoadJson(var Rec; Json: PUtf8Char; TypeInfo: PRttiInfo;
EndOfObject: PUtf8Char = nil; CustomVariantOptions: PDocVariantOptions = nil;
Tolerant: boolean = true; Interning: TRawUtf8Interning = nil): PUtf8Char; overload;
/// fill a record content from a JSON serialization as saved by
// TJsonWriter.AddRecordJson / RecordSaveJson
// - this overloaded function will make a private copy before parsing it,
// so is safe with a read/only or shared string - but slightly slower
function RecordLoadJson(var Rec; const Json: RawUtf8; TypeInfo: PRttiInfo;
CustomVariantOptions: PDocVariantOptions = nil;
Tolerant: boolean = true; Interning: TRawUtf8Interning = nil): boolean; overload;
/// fill a dynamic array content from a JSON serialization as saved by
// TJsonWriter.AddDynArrayJson with or without twoNonExpandedArrays layout
// - Value shall be set to the target dynamic array field
// - return a pointer at the end of the data read from JSON, nil in case
// of an invalid input buffer
// - could be used e.g. for custom record JSON unserialization, within a
// TDynArrayJsonCustomReader callback
// - warning: the JSON buffer will be modified in-place during process - use
// a temporary copy if you need to access it later or if the string comes from
// a constant (refcount=-1) - see e.g. the overloaded DynArrayLoadJson()
// - some numbers on a Core i5-13500, extracted from our regression tests:
// $ DynArrayLoadJson exp in 32.86ms i.e. 4.7M rows/s, 596.5 MB/s
// $ DynArrayLoadJson non exp in 22.46ms i.e. 6.9M rows/s, 383.7 MB/s
function DynArrayLoadJson(var Value; Json: PUtf8Char; TypeInfo: PRttiInfo;
EndOfObject: PUtf8Char = nil; CustomVariantOptions: PDocVariantOptions = nil;
Tolerant: boolean = true; Interning: TRawUtf8Interning = nil): PUtf8Char; overload;
/// fill a dynamic array content from a JSON serialization as saved by
// TJsonWriter.AddDynArrayJson, which won't be modified
// - this overloaded function will make a private copy before parsing it,
// so is safe with a read/only or shared string - but slightly slower
function DynArrayLoadJson(var Value; const Json: RawUtf8;
TypeInfo: PRttiInfo; CustomVariantOptions: PDocVariantOptions = nil;
Tolerant: boolean = true; Interning: TRawUtf8Interning = nil): boolean; overload;
/// read an object properties, as saved by ObjectToJson function
// - ObjectInstance must be an existing TObject instance
// - the data inside From^ is modified in-place (unescaped and transformed):
// calling JsonToObject(pointer(JSONRawUtf8)) will change the JSONRawUtf8
// variable content, which may not be what you expect - consider using the
// ObjectLoadJson() function instead
// - handle integer, Int64, enumerate (including boolean), set, floating point,
// TDateTime, TCollection, TStrings, TRawUtf8List, variant, and string properties
// (excluding ShortString, but including WideString and UnicodeString under
// Delphi 2009+)
// - TList won't be handled since it may leak memory when calling TList.Clear
// - won't handle TObjectList (even if ObjectToJson is able to serialize
// them) since has no way of knowing the object type to add (TCollection.Add
// is missing), unless: 1. you set the TObjectListItemClass property as expected,
// and provide a TObjectList object, or 2. woStoreClassName option has been
// used at ObjectToJson() call and the corresponding classes have been previously
// registered by Rtti.RegisterClass()
// - will clear any previous TCollection objects, and convert any null JSON
// basic type into nil - e.g. if From='null', will call FreeAndNil(Value)
// - you can add some custom (un)serializers for ANY class, via mormot.core.json
// TRttiJson.RegisterCustomSerializer() class method
// - set Valid=TRUE on success, Valid=FALSE on error, and the main function
// will point in From at the syntax error place (e.g. on any unknown property name)
// - caller should explicitly perform a SetDefaultValuesObject(Value) if
// the default values are expected to be set before JSON parsing
function JsonToObject(var ObjectInstance; From: PUtf8Char;
out Valid: boolean; TObjectListItemClass: TClass = nil;
Options: TJsonParserOptions = []; Interning: TRawUtf8Interning = nil): PUtf8Char;
/// parse the supplied JSON with some tolerance about Settings format
// - will make a TSynTempBuffer copy for parsing, and un-comment it
// - returns true if the supplied JSON was successfully retrieved
// - returns false on error
function JsonSettingsToObject(const JsonContent: RawUtf8;
Instance: TObject): boolean;
/// read an object properties, as saved by ObjectToJson function
// - ObjectInstance must be an existing TObject instance
// - this overloaded version will make a private copy of the supplied JSON
// content (via TSynTempBuffer), to ensure the original buffer won't be modified
// during process, before calling safely JsonToObject()
// - will return TRUE on success, or FALSE if the supplied JSON was invalid
function ObjectLoadJson(var ObjectInstance; const Json: RawUtf8;
TObjectListItemClass: TClass = nil; Options: TJsonParserOptions = [];
Interning: TRawUtf8Interning = nil): boolean;
/// create a new object instance, as saved by ObjectToJson(...,[...,woStoreClassName,...]);
// - JSON input should be either 'null', either '{"ClassName":"TMyClass",...}'
// - woStoreClassName option shall have been used at ObjectToJson() call
// - and the corresponding class shall have been previously registered by
// Rtti.RegisterClass() to retrieve the class type from it name
// - the data inside From^ is modified in-place (unescaped and transformed):
// don't call JsonToObject(pointer(JSONRawUtf8)) but makes a temporary copy of
// the JSON text buffer before calling this function, if want to reuse it later
function JsonToNewObject(var From: PUtf8Char; var Valid: boolean;
Options: TJsonParserOptions = []; Interning: TRawUtf8Interning = nil): TObject;
/// read an TObject published property, as saved by ObjectToJson() function
// - will use direct in-memory reference to the object, or call the corresponding
// setter method (if any), creating a temporary instance
// - unserialize the JSON input buffer via a call to JsonToObject()
// - by default, a temporary instance will be created if a published field
// has a setter, and the instance is expected to be released later by the
// owner class: you can set the j2oSetterExpectsToFreeTempInstance option
// to let this method release it when the setter returns
function PropertyFromJson(Prop: PRttiCustomProp; Instance: TObject;
From: PUtf8Char; var Valid: boolean; Options: TJsonParserOptions = [];
Interning: TRawUtf8Interning = nil): PUtf8Char;
/// decode a specified parameter compatible with URI encoding into its original
// object contents
// - ObjectInstance must be an existing TObject instance
// - will call internally JsonToObject() function to unserialize its content
// - UrlDecodeExtended('price=20.45&where=LastName%3D%27M%C3%B4net%27','PRICE=',P,@Next)
// will return Next^='where=...' and P=20.45
// - if Upper is not found, Value is not modified, and result is FALSE
// - if Upper is found, Value is modified with the supplied content, and result is TRUE
function UrlDecodeObject(U: PUtf8Char; Upper: PAnsiChar;
var ObjectInstance; Next: PPUtf8Char = nil;
Options: TJsonParserOptions = []): boolean;
/// fill the object properties from a JSON file content
// - ObjectInstance must be an existing TObject instance
// - this function will call RemoveCommentsFromJson() before process
function JsonFileToObject(const JsonFile: TFileName; var ObjectInstance;
TObjectListItemClass: TClass = nil; Options: TJsonParserOptions = [];
Interning: TRawUtf8Interning = nil): boolean;
const
/// standard header for an UTF-8 encoded XML file
XMLUTF8_HEADER = '<?xml version="1.0" encoding="UTF-8"?>'#13#10;
/// standard namespace for a generic XML File
XMLUTF8_NAMESPACE = '<contents xmlns="http://www.w3.org/2001/XMLSchema-instance">';
/// convert a JSON array or document into a simple XML content
// - just a wrapper around TJsonWriter.AddJsonToXML, with an optional
// header before the XML converted data (e.g. XMLUTF8_HEADER), and an optional
// name space content node which will nest the generated XML data (e.g.
// '<contents xmlns="http://www.w3.org/2001/XMLSchema-instance">') - the
// corresponding ending token will be appended after (e.g. '</contents>')
// - WARNING: the JSON buffer is decoded in-place, so P^ WILL BE modified
procedure JsonBufferToXML(P: PUtf8Char; const Header, NameSpace: RawUtf8;
out result: RawUtf8);
/// convert a JSON array or document into a simple XML content
// - just a wrapper around TJsonWriter.AddJsonToXML, making a private copy
// of the supplied JSON buffer using TSynTempBuffer (so that JSON content
// would stay untouched)
// - the optional header is added at the beginning of the resulting string
// - an optional name space content node could be added around the generated XML,
// e.g. '<content>'
function JsonToXML(const Json: RawUtf8; const Header: RawUtf8 = XMLUTF8_HEADER;
const NameSpace: RawUtf8 = ''): RawUtf8;
{ ********************* Abstract Classes with Auto-Create-Fields }
/// should be called by T*AutoCreateFields constructors
// - will also register this class type, if needed, so RegisterClass() is
// redundant to this method
function AutoCreateFields(ObjectInstance: TObject): TRttiJson;
{$ifdef HASINLINE}inline;{$endif}
/// should be called by T*AutoCreateFields destructors
// - constructor should have called AutoCreateFields()
procedure AutoDestroyFields(ObjectInstance: TObject; Info: TRttiJson = nil);
{$ifdef HASINLINE}inline;{$endif}
/// internal function called by AutoCreateFields() when inlined
// - do not call this internal function, but always AutoCreateFields()
function DoRegisterAutoCreateFields(ObjectInstance: TObject): TRttiJson;
type
/// abstract TPersistent class, which will instantiate all its nested class
// published properties, then release them (and any T*ObjArray) when freed
// - TSynAutoCreateFields is to be preferred in most cases, thanks to its
// lower overhead
// - note that non published (e.g. public) properties won't be instantiated,
// serialized, nor released - but may contain weak references to other classes
// - please take care that you will not create any endless recursion: you should
// ensure that at one level, nested published properties won't have any class
// instance refering to its owner (there is no weak reference - remember!)
// - since the destructor will release all nested properties, you should
// never store a reference to any of those nested instances if this owner
// may be freed before
TPersistentAutoCreateFields = class(TPersistentWithCustomCreate)
public
/// this overriden constructor will instantiate all its nested
// class or T*ObjArray published properties
constructor Create; override;
/// finalize the instance, and its class or T*ObjArray published properties
destructor Destroy; override;
end;
/// our own empowered TPersistentAutoCreateFields-like parent class
// - this class is a perfect parent to store any data by value, e.g. DDD Value
// Objects, Entities or Aggregates
// - is defined as an abstract class able with a virtual constructor, RTTI
// for published properties, and automatic memory management of all nested
// class published properties: any class defined as a published property will
// be owned by this instance - i.e. with strong reference
// - will also release any T*ObjArray dynamic array storage of persistents,
// previously registered via Rtti.RegisterObjArray() for Delphi 7-2009
// - nested published classes (or T*ObjArray) don't need to inherit from
// TSynAutoCreateFields: they may be from any TPersistent/TSynPersistent type
// - note that non published (e.g. public) properties won't be instantiated,
// serialized, nor released - but may contain weak references to other classes
// - please take care that you will not create any endless recursion: you should
// ensure that at one level, nested published properties won't have any class
// instance refering to its owner (there is no weak reference - remember!)
// - since the destructor will release all nested properties, you should
// never store a reference to any of those nested instances if this owner
// may be freed before
// - TPersistent/TPersistentAutoCreateFields have an unexpected speed overhead
// due a giant lock introduced to manage property name fixup resolution
// (which we won't use outside the UI) - this class is definitively faster
TSynAutoCreateFields = class(TSynPersistent)
public
/// this overriden constructor will instantiate all its nested
// class or T*ObjArray published properties
constructor Create; override;
/// finalize the instance, and its class or T*ObjArray published properties
destructor Destroy; override;
end;
/// meta-class definition of TSynAutoCreateFields
TSynAutoCreateFieldsClass = class of TSynAutoCreateFields;
/// adding locking methods to a TSynAutoCreateFields with virtual constructor
TSynAutoCreateFieldsLocked = class(TSynPersistentLock)
public
/// initialize the object instance, its associated lock, and its nested
// class or T*ObjArray published properties
constructor Create; override;
/// release the instance (including the locking resource) and nested classes
destructor Destroy; override;
end;
/// meta-class definition of TSynAutoCreateFieldsLocked
TSynAutoCreateFieldsLockedClass = class of TSynAutoCreateFieldsLocked;
/// abstract TInterfacedObject class, which will instantiate all its nested
// class published properties, then release them when freed
// - will handle automatic memory management of all nested class and T*ObjArray
// published properties: any class or T*ObjArray defined as a published
// property will be owned by this instance - i.e. with strong reference
// - non published properties (e.g. public) won't be instantiated, so may
// store weak class references
// - could be used for gathering of TCollectionItem properties, e.g. for
// Domain objects in DDD, especially for list of value objects, with some
// additional methods defined by an Interface
// - since the destructor will release all nested properties, you should
// never store a reference to any of those nested instances if this owner
// may be freed before
TInterfacedObjectAutoCreateFields = class(TInterfacedObjectWithCustomCreate)
public
/// this overriden constructor will instantiate all its nested
// class or T*ObjArray published properties
constructor Create; override;
/// finalize the instance, and release its published properties
destructor Destroy; override;
end;
/// meta-class definition of TInterfacedObjectAutoCreateFields
TInterfacedObjectAutoCreateFieldsClass = class of TInterfacedObjectAutoCreateFields;
/// abstract interface parent with common methods for JSON serialization
// - to implement this, you can inherit from TInterfacedSerializable
// or TInterfacedSerializableAutoCreateFields
ISerializable = interface
['{EA7F298D-06D7-4ADF-9F75-6598B75338B3}']
// methods used as getter/setter for the Json property
function GetJson: RawUtf8;
procedure SetJson(const value: RawUtf8);
/// serialize this instance into a JSON array/object specific format
function ToJson(format: TTextWriterJsonFormat;
options: TTextWriterWriteObjectOptions = []): RawUtf8; overload;
/// convert this instance into JSON array/object as RTL string
function ToString(format: TTextWriterJsonFormat = jsonCompact;
options: TTextWriterWriteObjectOptions = []): string;
/// raw unserialization of a JSON content into this instance
procedure FromJson(var Context: TJsonParserContext);
/// raw serialization of this instance into a JSON writer
procedure ToJson(W: TJsonWriter; options: TTextWriterWriteObjectOptions); overload;
/// unserialize/serialize this IDocList/IDocDict from/into a JSON array/object
// - use ToString if you want the result as RTL string
property Json: RawUtf8
read GetJson write SetJson;
end;
{$M+}
/// abstract class parent with ISerializable methods for JSON serialization
// - you need to override Create, ToJson and FromJson abstract methods
TInterfacedSerializable = class(TInterfacedObject, ISerializable)
protected
// methods used as getter/setter for the Json property
function GetJson: RawUtf8;
procedure SetJson(const value: RawUtf8); virtual;
// used internally for proper ISerializable instances serialization
class function SerializableInterface: TRttiCustom;
{$ifdef HASINLINE} inline; {$endif}
class procedure JS(W: TJsonWriter; data: pointer;
options: TTextWriterWriteObjectOptions);
class procedure JL(var context: TJsonParserContext; data: pointer);
public
/// factory of one class implementing a ISerializable interface
// - this abstract method must be overriden
constructor Create(options: PDocVariantOptions); reintroduce; virtual; abstract;
/// raw serialization of this instance into a JSON writer
// - this abstract method must be overriden
procedure ToJson(W: TJsonWriter;
options: TTextWriterWriteObjectOptions); overload; virtual; abstract;
/// raw unserialization of a JSON content into this instance
// - this abstract method must be overriden
procedure FromJson(var context: TJsonParserContext); virtual; abstract;
public
/// register this class to implement a given ISerializer sub-interface
class function RegisterToRtti(InterfaceInfo: PRttiInfo): TRttiJson;
/// return the associated ISerializer sub-interface TGuid
// - as registered by RegisterToRtti() class method
class function Guid: PGuid;
/// create a new instance as the associated ISerializer sub-interface
// - as registered by RegisterToRtti() class method
class procedure NewInterface(out Obj);
/// serialize this instance into a JSON array/object specific format
function ToJson(format: TTextWriterJsonFormat;
options: TTextWriterWriteObjectOptions): RawUtf8; overload; virtual;
/// convert this instance into JSON array/object as RTL string
function ToString(format: TTextWriterJsonFormat;
options: TTextWriterWriteObjectOptions): string; reintroduce; virtual;
/// unserialize/serialize this instance from/into a JSON array/object
// - use ToString if you want the result as RTL string
property Json: RawUtf8
read GetJson write SetJson;
end;
{$M-}
/// meta-class of the TInterfacedSerializable type
TInterfacedSerializableClass = class of TInterfacedSerializable;
/// points to a TInterfacedSerializable class instance
PInterfacedSerializable = ^TInterfacedSerializable;
/// abstract ISerializable class parent with auto-create published fields
// - you should inherit this class, associated with an interface inheriting
// from ISerializable (and propably with a method returning self to access the
// properties), then call once the RegisterToRtti() class function
// - could be used e.g. to implement a DDD/KDD Aggregate object with both
// ref-counted data and methods, ready to be serialized over SOA
TInterfacedSerializableAutoCreateFields = class(TInterfacedSerializable)
protected
fRttiJson: TRttiJson;
public
/// instantiate all nested class or T*ObjArray published properties
constructor Create(options: PDocVariantOptions = nil); override;
/// finalize the instance, and release its published properties
destructor Destroy; override;
/// raw JSON serialization of the published properties of this instance
procedure ToJson(W: TJsonWriter; options: TTextWriterWriteObjectOptions); override;
/// raw JSON unserialization into the published properties of this instance
procedure FromJson(var context: TJsonParserContext); override;
/// low-level access to the RTTI information associated with this class
property RttiJson: TRttiJson
read fRttiJson;
end;
/// abstract TCollectionItem class, which will instantiate all its nested class
// published properties, then release them (and any T*ObjArray) when freed
// - could be used for gathering of TCollectionItem properties, e.g. for
// Domain objects in DDD, especially for list of value objects
// - consider using T*ObjArray dynamic array published properties in your
// value types instead of TCollection storage: T*ObjArray have a lower overhead
// and are easier to work with, once Rtti.RegisterObjArray is called on Delphi
// 7-2009 to register the T*ObjArray type (not needed on FPC and Delphi 2010+)
// - note that non published (e.g. public) properties won't be instantiated,
// serialized, nor released - but may contain weak references to other classes
// - please take care that you will not create any endless recursion: you should
// ensure that at one level, nested published properties won't have any class
// instance refering to its owner (there is no weak reference - remember!)
// - since the destructor will release all nested properties, you should
// never store a reference to any of those nested instances if this owner
// may be freed before
TCollectionItemAutoCreateFields = class(TCollectionItem)
public
/// this overriden constructor will instantiate all its nested
// class or T*ObjArray published properties
constructor Create(Collection: TCollection); override;
/// finalize the instance, and release its published properties
destructor Destroy; override;
end;
/// customize TSynJsonFileSettings process
// - fsoDisableSaveIfNeeded will disable SaveIfNeeded method process
// - fsoReadIni will disable JSON loading, and expect INI file format
// - fsoWriteIni will force SaveIfNeeded to use the INI layout
TSynJsonFileSettingsOption = (
fsoDisableSaveIfNeeded,
fsoReadIni,
fsoWriteIni);
TSynJsonFileSettingsOptions = set of TSynJsonFileSettingsOption;
/// abstract parent class able to store settings as JSON file
// - would fallback and try to read as INI file if no valid JSON is found
TSynJsonFileSettings = class(TSynAutoCreateFields)
protected
fInitialJsonContent, fSectionName: RawUtf8;
fFileName: TFileName;
fLoadedAsIni: boolean;
fSettingsOptions: TSynJsonFileSettingsOptions;
// could be overriden to validate the content coherency and/or clean fields
function AfterLoad: boolean; virtual;
public
/// read existing settings from a JSON content
// - if the input is no JSON object, then a .INI structure is tried
function LoadFromJson(const aJson: RawUtf8;
const aSectionName: RawUtf8 = 'Main'): boolean;
/// read existing settings from a JSON or INI file file
function LoadFromFile(const aFileName: TFileName;
const aSectionName: RawUtf8 = 'Main'): boolean; virtual;
/// just a wrapper around ExtractFilePath(FileName);
function FolderName: TFileName;
/// persist the settings as a JSON file, named from LoadFromFile() parameter
// - will use the INI format if it was used at loading, or fsoWriteIni is set
procedure SaveIfNeeded; virtual;
/// optional persistence file name, as set by LoadFromFile()
property FileName: TFileName
read fFileName write fFileName;
/// allow to customize the storing process
property SettingsOptions: TSynJsonFileSettingsOptions
read fSettingsOptions write fSettingsOptions;
end;
/// meta-class definition of TSynJsonFileSettings
TSynJsonFileSettingsClass = class of TSynJsonFileSettings;
implementation
uses
mormot.core.variants;
{ ********** Low-Level JSON Processing Functions }
function NeedsJsonEscape(P: PUtf8Char; PLen: integer): boolean;
var
tab: PByteArray;
begin
result := true;
tab := @JSON_ESCAPE;
if PLen > 0 then
repeat
if tab[ord(P^)] <> JSON_ESCAPE_NONE then
exit;
inc(P);
dec(PLen);
until PLen = 0;
result := false;
end;
function NeedsJsonEscape(const Text: RawUtf8): boolean;
begin
result := NeedsJsonEscape(pointer(Text), length(Text));
end;
function NeedsJsonEscape(P: PUtf8Char): boolean;
var
tab: PByteArray;
esc: byte;
begin
result := false;
if P = nil then
exit;
tab := @JSON_ESCAPE;
repeat
esc := tab[ord(P^)];
if esc = JSON_ESCAPE_NONE then
inc(P)
else if esc = JSON_ESCAPE_ENDINGZERO then
exit
else
break;
until false;
result := true;
end;
function JsonUnicodeEscapeToUtf8(var D: PUtf8Char; P: PUtf8Char): PUtf8Char;
var
c, s: cardinal;
begin
// P^ points at 'u1234' just after \u0123
c := HexToWideChar(P + 1);
if c <= $7f then
if c >= 32 then
D^ := AnsiChar(c)
else if c = 0 then
D^ := '?' // \u0000 is an invalid value (at least in our framework)
else
begin
PInt64(D)^ := PInt64(P - 1)^; // control chars should always be escaped
inc(D, 5);
end
else if c < $7ff then
begin
D[0] := AnsiChar($C0 or (c shr 6));
D[1] := AnsiChar($80 or (c and $3F));
inc(D);
end
else if (c >= UTF16_HISURROGATE_MIN) and // decode from two UTF-16 surrogates
(c <= UTF16_LOSURROGATE_MAX) then
if PWord(P + 5)^ = ord('\') + ord('u') shl 8 then
begin
s := HexToWideChar(P + 7);
if s = 0 then
D^ := '?' // invalid surrogate
else
begin
case c of // inlined Utf16CharToUtf8()
UTF16_HISURROGATE_MIN..UTF16_HISURROGATE_MAX:
c := ((c - UTF16_SURROGATE_OFFSET) shl 10) or
(s xor UTF16_LOSURROGATE_MIN);
UTF16_LOSURROGATE_MIN..UTF16_LOSURROGATE_MAX:
c := ((s - UTF16_SURROGATE_OFFSET) shl 10) or
(c xor UTF16_LOSURROGATE_MIN);
end;
inc(D, Ucs4ToUtf8(c, D));
result := P + 11;
exit;
end;
end
else
D^ := '?' // the first \u#### expects a following \u#### surrogate
else
begin
D[0] := AnsiChar($E0 or (c shr 12));
D[1] := AnsiChar($80 or ((c shr 6) and $3F));
D[2] := AnsiChar($80 or (c and $3F));
inc(D,2);
end;
inc(D);
result := P + 5;
end;
procedure JsonDoUniEscape(const s: RawUtf8; var result: RawUtf8; esc: boolean);
var
tmp: TTextWriterStackBuffer;
begin
with TJsonWriter.CreateOwnedStream(tmp) do
try
if esc then
AddNoJsonEscapeForcedUnicode(pointer(s), length(s))
else
AddNoJsonEscapeForcedNoUnicode(pointer(s), length(s));
SetText(result);
finally
Free;
end;
end;
function JsonUnicodeEscape(const s: RawUtf8): RawUtf8;
begin
JsonDoUniEscape(s, result, true);
end;
function JsonUnicodeUnEscape(const s: RawUtf8): RawUtf8;
begin
JsonDoUniEscape(s, result, false);
end;
procedure Utf16ToJsonUnicodeEscape(var B: PUtf8Char; c: PtrUInt; tab: PByteToWord);
var
P: PUtf8Char;
begin
P := B;
PWord(P + 1)^ := ord('\') + ord('u') shl 8;
PWord(P + 3)^ := tab[c shr 8];
PWord(P + 5)^ := tab[c and $ff];
inc(B, 6);
end;
function IsString(P: PUtf8Char): boolean; // test if P^ is a "string" value
begin
if P = nil then
begin
result := false;
exit;
end;
while (P^ <= ' ') and
(P^ <> #0) do
inc(P);
if (P[0] in ['0'..'9']) or // is first char numeric?
((P[0] in ['-', '+']) and
(P[1] in ['0'..'9'])) then
begin
// check if P^ is a true numerical value
repeat
inc(P);
until not (P^ in ['0'..'9']); // check digits
if P^ = '.' then
repeat
inc(P);
until not (P^ in ['0'..'9']); // check fractional digits
if ((P^ = 'e') or
(P^ = 'E')) and
(P[1] in ['0'..'9', '+', '-']) then
begin
inc(P);
if P^ = '+' then
inc(P)
else if P^ = '-' then
inc(P);
while (P^ >= '0') and
(P^ <= '9') do
inc(P);
end;
while (P^ <= ' ') and
(P^ <> #0) do
inc(P);
result := (P^ <> #0);
exit;
end
else
result := true; // don't begin with a numerical value -> must be a string
end;
function IsStringJson(P: PUtf8Char): boolean; // test if P^ is a "string" value
var
c4: integer;
c: AnsiChar;
tab: PJsonCharSet;
begin
if P = nil then
begin
result := false;
exit;
end;
while (P^ <= ' ') and
(P^ <> #0) do
inc(P);
tab := @JSON_CHARS;
c4 := PInteger(P)^;
if (((c4 = NULL_LOW) or
(c4 = TRUE_LOW)) and
(jcEndOfJsonValueField in tab[P[4]])) or
((c4 = FALSE_LOW) and
(P[4] = 'e') and
(jcEndOfJsonValueField in tab[P[5]])) then
begin
result := false; // constants are no string
exit;
end;
c := P^;
if c = '-' then
begin
inc(P);
c := P^;
end;
if ((c >= '1') and (c <= '9')) or // is first char numeric?
((c = '0') and ((P[1] < '0') or (P[1] > '9'))) then // '012' not JSON
begin
// check if c is a true numerical value
repeat
inc(P);
until (P^ < '0') or
(P^ > '9'); // check digits
if P^ = '.' then
repeat
inc(P);
until (P^ < '0') or
(P^ > '9'); // check fractional digits
if ((P^ = 'e') or
(P^ = 'E')) and
(jcDigitFirstChar in tab[P[1]]) then
begin
inc(P);
c := P^;
if c = '+' then
inc(P)
else if c = '-' then
inc(P);
while (P^ >= '0') and
(P^ <= '9') do
inc(P);
end;
while (P^ <= ' ') and
(P^ <> #0) do
inc(P);
result := (P^ <> #0);
exit;
end
else
result := true; // don't begin with a numerical value -> must be a string
end;
type
TJsonGotoEndParserState = (
stObjectName,
stObjectValue,
stValue);
/// state machine for fast (900MB/s) parsing of (extended) JSON input
{$ifdef USERECORDWITHMETHODS}
TJsonGotoEndParser = record
{$else}
TJsonGotoEndParser = object
{$endif USERECORDWITHMETHODS}
public
{$ifdef CPUX86}
JsonSet: PJsonCharSet; // not enough registers in i386 mode
{$endif CPUX86}
State: TJsonGotoEndParserState;
ExpectStandard: boolean;
StackCount: integer;
JsonFirst: PJsonTokens;
Max: PUtf8Char; // checking Max after each comma is good enough
RootCount: integer;
// 500 nested documents seem enough in practice (SQlite3 uses 1000)
Stack: array[0..500] of TJsonGotoEndParserState;
procedure Init(Strict: boolean; PMax: PUtf8Char);
{$ifdef HASINLINE} inline; {$endif}
procedure InitCount(Strict: boolean; PMax: PUtf8Char;
First: TJsonGotoEndParserState);
// reusable method able to jump over any JSON value (up to Max)
function GotoEnd(P: PUtf8Char): PUtf8Char; overload;
function GotoEnd(P: PUtf8Char; var EndOfObject: AnsiChar): PUtf8Char; overload;
{$ifdef HASINLINE} inline; {$endif}
end;
procedure TJsonGotoEndParser.Init(Strict: boolean; PMax: PUtf8Char);
begin
{$ifdef CPUX86}
JsonSet := @JSON_CHARS;
{$endif CPUX86}
State := stValue;
ExpectStandard := Strict;
StackCount := 0;
JsonFirst := @JSON_TOKENS;
Max := PMax;
end; // RootCount is not initialized by default unless InitCount() is called
procedure TJsonGotoEndParser.InitCount(Strict: boolean; PMax: PUtf8Char;
First: TJsonGotoEndParserState);
begin
Init(Strict, PMax);
RootCount := 0;
Stack[0] := stValue; // emulate parsing of the opening [ or {
inc(StackCount);
State := First;
end;
function TJsonGotoEndParser.GotoEnd(P: PUtf8Char): PUtf8Char;
var
n: PtrInt;
{$ifndef CPUX86}
JsonSet: PJsonCharSet; // will use a register for this lookup table
{$endif CPUX86}
label
prop, stop, assign;
begin
result := nil; // to notify unexpected end
if P = nil then
exit;
{$ifndef CPUX86}
JsonSet := @JSON_CHARS;
{$endif CPUX86}
repeat
{$ifdef FPC}
while (P^ <= ' ') and
(P^ <> #0) do
inc(P);
{$else}
if P^ in [#1..' '] then
repeat
inc(P)
until not (P^ in [#1..' ']);
{$endif FPC}
case JsonFirst[P^] of // FPC and Delphi will use a jump table :)
jtNone:
exit;// unexpected character in JSON input
jtDoubleQuote: // '"'
begin
repeat // inlined GotoEndOfJsonString2()
inc(P);
if not (jcJsonStringMarker in JsonSet[P^]) then // [#0, '"', '\']
continue; // very fast parsing of most UTF-8 chars
if P^ = '"' then
break
else if P^ = #0 then
exit; // unexpected end of string/buffer
inc(P); // P^ was '\' -> ignore \# ou \u0123
if P^ = #0 then
exit; // buffer overflow detected as \#0
until false;
inc(P);
if (StackCount <> 0) or
(State = stObjectName) then
continue;
break;
end;
jtFirstDigit: // '-', '0'..'9'
begin
if (State = stObjectName) and
ExpectStandard then
exit;
// '0123' excluded by JSON, but not here
repeat
inc(P);
until not (jcDigitFloatChar in JsonSet[P^]);
// not ['-', '+', '0'..'9', '.', 'E', 'e']
if (StackCount <> 0) or
(State = stObjectName) then
continue;
break;
end;
jtNullFirstChar: // 'n'
if (PInteger(P)^ = NULL_LOW) and
(jcEndOfJsonValueField in JsonSet[P[4]]) then
inc(P, 3)
else
goto prop;
jtTrueFirstChar: // 't'
if (PInteger(P)^ = TRUE_LOW) and
(jcEndOfJsonValueField in JsonSet[P[4]]) then
inc(P, 3)
else
goto prop;
jtFalseFirstChar: // 'f'
if (PInteger(P + 1)^ = FALSE_LOW2) and
(jcEndOfJsonValueField in JsonSet[P[5]]) then
inc(P, 4)
else
goto prop;
jtObjectStart: // {
begin
n := StackCount;
if (State = stObjectName) or
(n > high(Stack)) then
exit; // too many nested documents
Stack[n] := State;
inc(StackCount);
State := stObjectName;
inc(P);
continue;
end;
jtArrayStart: // [
begin
n := StackCount;
if (State = stObjectName) or
(n > high(Stack)) then
exit; // too many nested documents
Stack[n] := State;
inc(StackCount);
State := stValue;
inc(P);
continue;
end;
jtObjectStop: // }
begin
if State = stValue then
exit;
stop: n := StackCount;
if n = 0 then
exit; // invalid input
dec(n);
inc(RootCount, ord(n = 0));
StackCount := n;
State := Stack[n];
end;
jtArrayStop: // ]
if State <> stValue then
exit
else
goto stop;
jtAssign: // :
begin
assign: if State <> stObjectName then
exit;
State := stObjectValue;
inc(P);
continue;
end;
jtComma: // ,
begin
if State = stObjectName then
exit;
dec(State, ord(State = stObjectValue)); // branchless update
inc(P);
inc(RootCount, ord(StackCount = 1));
if (Max = nil) or // checking Max after each comma is good enough
(P < Max) then
continue;
// reached end of allowed - but valid - input
if RootCount = 0 then
dec(RootCount) // first item may be huge -> at least -1
else
RootCount := -RootCount;
exit;
end;
jtSingleQuote: // '''' as single-quoted identifier or value
if ExpectStandard then
exit
else
repeat
inc(P);
if P^ <= ' ' then
exit;
until P^ = '''';
jtEqual: // =
if ExpectStandard then
exit
else
goto assign;
jtIdentifierFirstChar: // ['_', 'a'..'z', 'A'..'Z', '$']
begin
prop: if ExpectStandard then
exit;
repeat
repeat
inc(P);
until not (jcJsonIdentifier in JsonSet[P^]);
// not ['_', '0'..'9', 'a'..'z', 'A'..'Z', '.', '[', ']']
while (P^ <= ' ') and
(P^ <> #0) do
inc(P);
until not (jcJsonIdentifierFirstChar in JsonSet[P^]); // new date(...
while (P^ <= ' ') and
(P^ <> #0) do
inc(P);
if P^ = '(' then
begin
// handle e.g. "born":isodate("1969-12-31")
repeat
inc(P);
until (P^ > ' ') or
(P^ = #0);
if P^ = '"' then
begin
repeat
inc(P);
until jcJsonStringMarker in JsonSet[P^]; // [#0, '"', '\']
if P^ <> '"' then
exit;
inc(P);
end;
while (P^ <> ')') and
(P^ <> #0) do
inc(P);
if P^ <> #0 then
inc(P);
end
else if State <> stObjectName then
exit; // identifier values are functions like isodate() objectid()
continue;
end;
jtSlash: // '/' extended /regex/i or /*comment*/ or //comment
begin
if ExpectStandard then
exit;
inc(P);
if P^ = #0 then
exit
else if P^ = '*' then // ignore /* comment */
begin
repeat
inc(P);
if P^ = #0 then
exit;
until PWord(P)^ = ord('*') + ord('/') shl 8;
inc(P, 2);
continue;
end
else if P^ = '/' then // ignore // comment
begin
P := GotoNextLine(P + 1);
if P = nil then
exit;
continue;
end
else
begin
repeat // extended /regex/i syntax
inc(P);
if P^ = #0 then
exit;
until P^ = '/';
while not (jcEndOfJsonFieldNotName in JsonSet[P[1]]) do
inc(P);
end;
end;
jtEndOfBuffer: // #0
if StackCount <> 0 then
exit // unclosed array or object
else
break; // return #0
else
exit; // paranoid (every and each TJsonToken should be handled above)
end;
// if we are here we know this was an identifier or value
inc(P);
if (StackCount = 0) and
(State <> stObjectName) then
break;
until false;
while (P^ <= ' ') and
(P^ <> #0) do
inc(P);
result := P; // points to the next meaningful char
end;
function TJsonGotoEndParser.GotoEnd(P: PUtf8Char; var EndOfObject: AnsiChar): PUtf8Char;
var
c: AnsiChar;
begin
result := GotoEnd(P);
if result = nil then
exit;
c := result^; // return last jcEndOfJsonFieldOr0
EndOfObject := c;
if c <> #0 then
inc(result);
end;
function IsValidJson(const s: RawUtf8; strict: boolean): boolean;
begin
result := IsValidJson(pointer(s), length(s), strict);
end;
function IsValidJson(P: PUtf8Char; len: PtrInt; strict: boolean): boolean;
var
B: PUtf8Char;
parser: TJsonGotoEndParser;
begin
result := false;
if (P = nil) or
(len <= 0) then
exit;
B := P;
{%H-}parser.Init(strict, P + len);
P := parser.GotoEnd(P);
result := (P <> nil) and
(P - B = len);
end;
function GetFirstJsonToken(P: PUtf8Char): TJsonToken;
begin
if P <> nil then
result := JSON_TOKENS[GotoNextNotSpace(P)^]
else
result := jtNone;
end;
function GetNextJsonToken(var P: PUtf8Char; strict: boolean; DocCount: PInteger): TJsonToken;
var
parser: TJsonGotoEndParser;
begin
result := jtNone;
if DocCount <> nil then
DocCount^ := 0;
if P = nil then
exit;
P := GotoNextNotSpace(P);
result := JSON_TOKENS[P^];
if result in [jtNone, jtEndOfBuffer, jtAssign, jtEqual, jtComma] then
begin
P := nil;
result := jtNone;
exit;
end;
{%H-}parser.Init(strict, nil);
parser.RootCount := 0;
P := parser.GotoEnd(P);
if P = nil then
result := jtNone
else if DocCount <> nil then
DocCount^ := parser.RootCount;
end;
function IsValidJsonBuffer(P: PUtf8Char; strict: boolean): boolean;
var
parser: TJsonGotoEndParser;
begin
{%H-}parser.Init(strict, nil);
result := parser.GotoEnd(P) <> nil;
end;
procedure IgnoreComma(var P: PUtf8Char);
begin
if P <> nil then
begin
while (P^ <= ' ') and
(P^ <> #0) do
inc(P);
if P^ = ',' then
inc(P);
end;
end;
function JsonPropNameValid(P: PUtf8Char): boolean;
var
tab: PJsonCharSet;
begin
tab := @JSON_CHARS;
if (P <> nil) and
(jcJsonIdentifierFirstChar in tab[P^]) then
begin
// ['_', '0'..'9', 'a'..'z', 'A'..'Z', '$']
repeat
inc(P);
until not (jcJsonIdentifier in tab[P^]);
// not ['_', '0'..'9', 'a'..'z', 'A'..'Z', '.', '[', ']']
result := P^ = #0;
end
else
result := false;
end;
{$ifndef PUREMORMOT2}
function GetJsonField(P: PUtf8Char; out PDest: PUtf8Char; WasString: PBoolean;
EndOfObject: PUtf8Char; Len: PInteger): PUtf8Char;
var
info: TGetJsonField;
begin
info.Json := P;
info.GetJsonField;
PDest := info.Json;
if WasString <> nil then
WasString^ := info.WasString;
if EndOfObject <> nil then
EndOfObject^ := info.EndOfObject;
if Len <> nil then
Len^ := info.ValueLen;
result := info.Value;
end;
function GetJsonFieldOrObjectOrArray(var Json: PUtf8Char; WasString: PBoolean;
EndOfObject: PUtf8Char; HandleValuesAsObjectOrArray, NormalizeBoolean: boolean;
Len: PInteger): PUtf8Char;
var
info: TGetJsonField;
begin
info.Json := Json;
info.GetJsonFieldOrObjectOrArray(HandleValuesAsObjectOrArray, NormalizeBoolean);
Json := info.Json;
if WasString <> nil then
WasString^ := info.WasString;
if EndOfObject <> nil then
EndOfObject^ := info.EndOfObject;
if Len <> nil then
Len^ := info.ValueLen;
result := info.Value;
end;
{$endif PUREMORMOT2}
{ TGetJsonField }
procedure TGetJsonField.GetJsonValue(var Text: RawUtf8);
begin
GetJsonField;
FastSetString(Text, Value, ValueLen);
end;
function TGetJsonField.GetJsonInt64: Int64;
begin
GetJsonField;
result := GetInt64(Value);
end;
procedure TGetJsonField.GetJsonField;
var
P, D: PUtf8Char;
c4, surrogate, extra: PtrUInt;
c: AnsiChar;
{$ifdef CPUX86NOTPIC}
tab: TJsonCharSet absolute JSON_CHARS; // not enough registers
{$else}
tab: PJsonCharSet;
{$endif CPUX86NOTPIC}
begin
// see http://www.ietf.org/rfc/rfc4627.txt
P := Json;
Json := nil; // Json=nil indicates error or unexpected end (#0)
Value := nil;
ValueLen := 0; // ensure returns ValueLen=0 on invalid input (Json=nil)
WasString := false; // not a string by default
if P = nil then
exit;
while P^ <= ' ' do
begin
if P^ = #0 then
exit;
inc(P);
end;
{$ifndef CPUX86NOTPIC}
tab := @JSON_CHARS;
{$endif CPUX86NOTPIC}
case JSON_TOKENS[P^] of
jtFirstDigit: // '-', '0'..'9'
begin
// numerical value
Value := P;
if P^ = '0' then
if (P[1] >= '0') and
(P[1] <= '9') then
// 0123 excluded by JSON!
exit;
repeat // loop all '-', '+', '0'..'9', '.', 'E', 'e'
inc(P);
until not (jcDigitFloatChar in tab[P^]);
if P^ = #0 then
exit; // a JSON number value should be followed by , } or ]
ValueLen := P - Value;
if (P^ <= ' ') and
(P^ <> #0) then
begin
P^ := #0; // force numerical field with no trailing ' '
inc(P);
end;
end;
jtDoubleQuote: // '"'
begin
// " -> unescape P^ into D^
inc(P);
Value := P; // points to the unescaped JSON string
WasString := true;
while not (jcJsonStringMarker in tab[P^]) do
// not [#0, '"', '\']
inc(P); // very fast parsing of most UTF-8 chars within "string"
D := P;
if P^ <> '"' then
repeat
// escape needed -> in-place unescape from P^ into D^
c := P^;
if not (jcJsonStringMarker in tab[c]) then
begin
inc(P);
D^ := c;
inc(D);
continue; // very fast parsing of most UTF-8 chars within "string"
end;
// P^ is either #0, '"' or '\'
if c = '"' then
// end of string
break;
if c = #0 then
// premature ending (leaving Json=nil)
exit;
// unescape JSON text: process char after \
inc(P); // P^ was '\' here
c := JSON_UNESCAPE[P^];
if c > JSON_UNESCAPE_UTF16 then
begin
inc(P);
D^ := c;
inc(D);
continue; // direct un-escape of most \x values
end
else if c = JSON_UNESCAPE_UNEXPECTED then
exit; // avoid \#0 potential buffer overflow issue or control char
// JSON_UNESCAPE_UTF16: decode '\u0123' UTF-16 into UTF-8
// (inlined JsonUnicodeEscapeToUtf8() to optimize GetJsonField)
c4 := (ConvertHexToBin[ord(P[1])] shl 12) or
(ConvertHexToBin[ord(P[2])] shl 8) or
(ConvertHexToBin[ord(P[3])] shl 4) or
ConvertHexToBin[ord(P[4])]; // optimistic conversion (no check)
inc(P, 5);
case c4 of
0: // \u0000 is an invalid value (at least in our framework)
begin
D^ := '?';
inc(D);
end;
1..$7f:
begin
D^ := AnsiChar(c4);
inc(D);
end;
$80..$7ff:
begin
D[0] := AnsiChar($C0 or (c4 shr 6));
D[1] := AnsiChar($80 or (c4 and $3F));
inc(D, 2);
end;
UTF16_HISURROGATE_MIN..UTF16_LOSURROGATE_MAX:
if PWord(P)^ = ord('\') + ord('u') shl 8 then
begin
inc(P);
surrogate := (ConvertHexToBin[ord(P[1])] shl 12) or
(ConvertHexToBin[ord(P[2])] shl 8) or
(ConvertHexToBin[ord(P[3])] shl 4) or
ConvertHexToBin[ord(P[4])];
case c4 of // inlined Utf16CharToUtf8()
UTF16_HISURROGATE_MIN..UTF16_HISURROGATE_MAX:
c4 := ((c4 - UTF16_SURROGATE_OFFSET) shl 10) or
(surrogate xor UTF16_LOSURROGATE_MIN);
UTF16_LOSURROGATE_MIN..UTF16_LOSURROGATE_MAX:
c4 := ((surrogate - UTF16_SURROGATE_OFFSET) shl 10) or
(c4 xor UTF16_LOSURROGATE_MIN);
end;
if c4 <= $7ff then
c := #2
else if c4 <= $ffff then
c := #3
else if c4 <= $1FFFFF then
c := #4
else if c4 <= $3FFFFFF then
c := #5
else
c := #6;
extra := ord(c) - 1;
repeat
D[extra] := AnsiChar((c4 and $3f) or $80);
c4 := c4 shr 6;
dec(extra);
until extra = 0;
D^ := AnsiChar(byte(c4) or UTF8_TABLE.FirstByte[ord(c)]);
inc(D, ord(c));
inc(P, 5);
end
else
begin
// unexpected surrogate without its pair
D^ := '?';
inc(D);
end;
else
begin
D[0] := AnsiChar($E0 or (c4 shr 12));
D[1] := AnsiChar($80 or ((c4 shr 6) and $3F));
D[2] := AnsiChar($80 or (c4 and $3F));
inc(D, 3);
end;
end;
until false;
// here P^='"'
inc(P);
D^ := #0; // make zero-terminated
ValueLen := D - Value;
end;
jtSingleQuote: // extended/non-standard 'text' single quoted content
begin
inc(P);
Value := P; // points to the unquoted string
WasString := true;
D := P;
repeat
c := P^;
if c = #0 then
exit
else if c = '''' then
if P[1] = '''' then
inc(P) // unquote double quotes
else
break;
D^ := c;
inc(D);
inc(P);
until false;
inc(P);
D^ := #0; // make zero-terminated
ValueLen := D - Value;
end;
jtNullFirstChar: // 'n'
if (PInteger(P)^ = NULL_LOW) and
(jcEndOfJsonValueField in tab[P[4]]) then
// [#0, #9, #10, #13, ' ', ',', '}', ']']
// null -> returns nil and WasString=false
inc(P, 4)
else
exit;
jtFalseFirstChar: // 'f'
if (PInteger(P + 1)^ = FALSE_LOW2) and
(jcEndOfJsonValueField in tab[P[5]]) then
// [#0, #9, #10, #13, ' ', ',', '}', ']']
begin
// false -> returns 'false' and WasString=false
Value := P;
ValueLen := 5;
inc(P, 5);
end
else
exit;
jtTrueFirstChar: // 't'
if (PInteger(P)^ = TRUE_LOW) and
(jcEndOfJsonValueField in tab[P[4]]) then
// [#0, #9, #10, #13, ' ', ',', '}', ']']
begin
// true -> returns 'true' and WasString=false
Value := P;
ValueLen := 4;
inc(P, 4);
end
else
exit;
else
// leave Json=nil on error (e.g. if a {...} or [...] was supplied)
exit;
end;
while not (jcEndOfJsonFieldOr0 in tab[P^]) do
// loop until #0 , ] } : delimiter
inc(P);
EndOfObject := P^;
// ensure JSON value is zero-terminated, and continue after it
if P^ <> #0 then
begin
P^ := #0;
Json := P + 1;
end
else
Json := P;
end;
function TryGotoEndOfComment(P: PUtf8Char): PUtf8Char;
begin
repeat
result := P; // return input P^ = '/' if no comment was found
inc(P);
if P^ = '*' then // ignore /* comment */
begin
repeat
inc(P);
if P^ = #0 then
exit;
until PWord(P)^ = ord('*') + ord('/') shl 8;
result := GotoNextNotSpace(P + 2);
end
else if P^ = '/' then // ignore // comment
begin
P := GotoNextLine(P + 1);
if P = nil then
exit;
result := GotoNextNotSpace(P);
end
else
exit;
until P^ <> '/'; // there may be other subsequent comments ;)
end;
procedure TGetJsonField.GetJsonFieldOrObjectOrArray(
HandleValuesAsObjectOrArray, NormalizeBoolean: boolean);
var
P: PUtf8Char;
parser: TJsonGotoEndParser;
c: integer;
begin
P := Json;
Value := nil;
ValueLen := 0;
if P = nil then
exit;
while (P^ <= ' ') and
(P^ <> #0) do
inc(P);
if P^ = '/' then
P := TryGotoEndOfComment(P);
if HandleValuesAsObjectOrArray and
(P^ in ['{', '[']) then
begin
WasString := false;
Value := P;
{%H-}parser.Init({strict=}false, nil);
P := parser.GotoEnd(P);
if P = nil then
Value := nil
else
begin
ValueLen := P - Value;
while (P^ <= ' ') and
(P^ <> #0) do
inc(P);
EndOfObject := P^;
if P^ <> #0 then
begin
P^ := #0; // make zero-terminated as GetJsonField()
inc(P);
end;
end;
Json := P;
end
else
begin
Json := P;
GetJsonField;
if not WasString and
NormalizeBoolean and
(Value <> nil) then
begin
c := PInteger(Value)^;
if c = TRUE_LOW then
Value := pointer(SmallUInt32Utf8[1]) // normalize true -> 1
else if c = FALSE_LOW then
Value := pointer(SmallUInt32Utf8[0]) // normalize false -> 0
else
exit;
ValueLen := 1; // result = '0' or '1'
end;
end;
end;
function GotoEndOfJsonString2(P: PUtf8Char; tab: PJsonCharSet): PUtf8Char;
{$ifdef HASINLINE} inline; {$endif}
begin
// P[-1]='"' at function call
repeat
if not (jcJsonStringMarker in tab[P^]) then
begin
inc(P); // not [#0, '"', '\']
continue; // very fast parsing of most UTF-8 chars
end;
if (P^ = '"') or
(P^ = #0) or
(P[1] = #0) then
// end of string/buffer, or buffer overflow detected as \#0
break;
inc(P, 2); // P^ was '\' -> ignore \# ou \u0123
until false;
result := P;
// P^='"' at function return (if input was correct)
end;
function GotoEndOfJsonString(P: PUtf8Char): PUtf8Char;
begin
// P^='"' at function call and at successful function return
result := GotoEndOfJsonString2(P + 1, @JSON_CHARS);
end;
function GotoEndJsonItemString(P: PUtf8Char): PUtf8Char;
var
tab: PJsonCharSet;
begin
// see TOrmTableJson.ParseAndConvert and TDocVariantData.InitArrayFromResults
if P <> nil then
repeat
if P^ = '"' then
begin
inc(P);
tab := @JSON_CHARS;
repeat // inlined GotoEndOfJsonString2()
if not (jcJsonStringMarker in tab[P^]) then
begin
inc(P); // not [#0, '"', '\']
continue; // very fast parsing of most UTF-8 chars
end;
if P^ = '"' then
begin
repeat
inc(P);
until not (P^ in [#1..' ']);
result := P;
exit;
end
else if (P^ = #0) or
(P[1] = #0) then
// end of string/buffer, or buffer overflow detected as \#0
break;
inc(P, 2); // P^ was '\' -> ignore \# ou \u0123
until false;
break;
end
else if P^ <= ' ' then
begin
if P^ = #0 then
break;
inc(P);
continue;
end;
break;
until false;
result := nil;
end;
function GetJsonPropName(var Json: PUtf8Char; Len: PInteger;
NoJsonUnescape: boolean): PUtf8Char;
var
P, Name: PUtf8Char;
tab: PJsonCharSet;
info: TGetJsonField;
begin
// should match GotoNextJsonObjectOrArray() and JsonPropNameValid()
result := nil; // returns nil on invalid input
P := Json;
if P = nil then
exit;
while P^ <= ' ' do
begin
if P^ = #0 then
begin
Json := nil; // reached early end of input
exit;
end;
inc(P);
end;
if P^ = '/' then
P := TryGotoEndOfComment(P);
Name := P + 1;
tab := @JSON_CHARS;
if P^ = '"' then
begin
// handle very efficiently the most common case of unescaped double quotes
repeat
inc(P);
until jcJsonStringMarker in tab[P^]; // [#0, '"', '\']
if P^ <> '"' then
// we need to handle a complex property name (seldom encoutered)
if P^ = #0 then
exit
else if NoJsonUnescape then
P := GotoEndOfJsonString2(P, tab)
else
begin // should be unescaped
info.Json := Name - 1;
info.GetJsonField;
if (info.Value <> nil) and
info.WasString and
(info.EndOfObject = ':') then
begin
result := info.Value;
if Len <> nil then
Len^ := info.ValueLen;
end;
Json := info.Json;
exit;
end;
end
else if P^ = '''' then
// single quotes won't handle nested quote character
repeat
inc(P);
if P^ < ' ' then
exit;
until P^ = ''''
else
begin
// e.g. '{age:{$gt:18}}'
if not (jcJsonIdentifierFirstChar in tab[P^]) then
exit; // not ['_', '0'..'9', 'a'..'z', 'A'..'Z', '$']
repeat
inc(P);
until not (jcJsonIdentifier in tab[P^]);
// not ['_', '0'..'9', 'a'..'z', 'A'..'Z', '.', '[', ']']
if P^ = #0 then
exit;
dec(Name);
if Len <> nil then
Len^ := P - Name;
info.EndOfObject := P^;
P^ := #0; // Name should end with #0
if not (info.EndOfObject in [':', '=']) then // relaxed {age=10} syntax
repeat
inc(P);
if P^ = #0 then
exit;
until P^ in [':', '='];
Json := P + 1;
result := Name;
exit;
end;
if Len <> nil then
Len^ := P - Name;
P^ := #0; // ensure Name is #0 terminated
repeat
inc(P);
if P^ = #0 then
exit;
until P^ = ':';
Json := P + 1;
result := Name;
end;
procedure GetJsonPropNameShort(var P: PUtf8Char; out PropName: ShortString);
var
Name: PAnsiChar;
c: AnsiChar;
tab: PJsonCharSet;
label
ok;
begin
// match GotoNextJsonObjectOrArray() and overloaded GetJsonPropName()
PropName[0] := #0;
if P = nil then
exit;
while P^ <= ' ' do
begin
if P^ = #0 then
begin
P := nil;
exit;
end;
inc(P);
end;
Name := pointer(P);
c := P^;
if c = '/' then
begin
P := TryGotoEndOfComment(P);
c := P^;
end;
if c = '"' then
begin
inc(Name);
tab := @JSON_CHARS;
repeat
inc(P);
until jcJsonStringMarker in tab[P^]; // end at [#0, '"', '\']
if P^ <> '"' then
exit;
ok: SetString(PropName, Name, P - Name); // note: won't unescape JSON strings
repeat
inc(P)
until (P^ > ' ') or
(P^ = #0);
if P^ <> ':' then
begin
PropName[0] := #0;
exit;
end;
inc(P);
end
else if c = '''' then
begin
// single quotes won't handle nested quote character
inc(P);
inc(Name);
while P^ <> '''' do
if P^ < ' ' then
exit
else
inc(P);
goto ok;
end
else
begin
// e.g. '{age:{$gt:18}}'
tab := @JSON_CHARS;
if not (jcJsonIdentifierFirstChar in tab[c]) then
exit; // not ['_', '0'..'9', 'a'..'z', 'A'..'Z', '$']
repeat
inc(P);
until not (jcJsonIdentifier in tab[P^]);
// not ['_', '0'..'9', 'a'..'z', 'A'..'Z', '.', '[', ']']
SetString(PropName, Name, P - Name);
while (P^ <= ' ') and
(P^ <> #0) do
inc(P);
if (P^ <> ':') and
(P^ <> '=') then
begin
// allow both age:18 and age=18 pairs (very relaxed JSON syntax)
PropName[0] := #0;
exit;
end;
inc(P);
end;
end;
function JsonRetrieveStringField(P: PUtf8Char; out Field: PUtf8Char;
out FieldLen: integer; ExpectNameField: boolean): PUtf8Char;
var
tab: PJsonCharSet;
begin
result := nil;
// retrieve string field
if P = nil then
exit;
while (P^ <= ' ') and
(P^ <> #0) do
inc(P);
if P^ <> '"' then
exit;
inc(P);
Field := P;
tab := @JSON_CHARS;
while not (jcJsonStringMarker in tab[P^]) do
// not [#0, '"', '\']
inc(P); // very fast parsing of most UTF-8 chars within "string"
if P^ <> '"' then
exit; // here P^ should be '"'
FieldLen := P - Field;
// check valid JSON delimiter
repeat
inc(P)
until (P^ > ' ') or
(P^ = #0);
if ExpectNameField then
begin
if P^ <> ':' then
exit; // invalid name field
end
else if not (P^ in ['}', ',']) then
exit; // invalid value field
result := P; // return either ':' for name field, or } , for value field
end;
function GlobalFindClass(classname: PUtf8Char; classnamelen: integer): TRttiCustom;
var
name: string;
c: TClass;
begin
Utf8DecodeToString(classname, classnamelen, name);
c := FindClass(name);
if c = nil then
result := nil
else
result := Rtti.RegisterClass(c);
end;
function JsonRetrieveObjectRttiCustom(var Json: PUtf8Char;
AndGlobalFindClass: boolean): TRttiCustom;
var
tab: PNormTable;
P, classname: PUtf8Char;
classnamelen: integer;
begin
result := nil;
P := GotoNextNotSpace(Json + 1); // at input, Json^ = '{'
tab := @NormToUpperAnsi7;
if IdemPChar(P, '"CLASSNAME":', tab) then
inc(P, 12)
else if IdemPChar(P, 'CLASSNAME:', tab) then
inc(P, 10)
else
exit; // we expect woStoreClassName option to have been used
P := JsonRetrieveStringField(P, classname, classnamelen, false);
if P = nil then
exit; // invalid (maybe too complex) Json string value
Json := P; // Json^ is either } or ,
result := Rtti.FindName(classname, classnamelen, rkClass);
if (result = nil) and
AndGlobalFindClass then
result := GlobalFindClass(classname, classnamelen);
end;
procedure GetJsonItemAsRawJson(var P: PUtf8Char; var result: RawJson;
EndOfObject: PAnsiChar);
var
B: PUtf8Char;
begin
result := '';
if P = nil then
exit;
B := GotoNextNotSpace(P);
P := GotoEndJsonItem(B);
if P = nil then
exit;
FastSetString(RawUtf8(result), B, P - B);
while (P^ <= ' ') and
(P^ <> #0) do
inc(P);
if EndOfObject <> nil then
EndOfObject^ := P^;
if P^ <> #0 then //if P^=',' then
repeat
inc(P)
until (P^ > ' ') or
(P^ = #0);
end;
function GetJsonItemAsRawUtf8(var P: PUtf8Char; var output: RawUtf8;
WasString: PBoolean; EndOfObject: PUtf8Char): boolean;
var
info: TGetJsonField;
begin
info.Json := P;
info.GetJsonValue(output);
P := info.Json;
if WasString <> nil then
WasString^ := info.WasString;
if EndOfObject <> nil then
EndOfObject^ := info.EndOfObject;
result := info.Json <> nil;
end;
function GotoEndJsonItemStrict(P, PMax: PUtf8Char): PUtf8Char;
var
parser: TJsonGotoEndParser;
begin
{%H-}parser.Init({strict=}true, PMax);
result := parser.GotoEnd(P);
end;
function GotoEndJsonItem(P, PMax: PUtf8Char): PUtf8Char;
var
parser: TJsonGotoEndParser;
begin
{%H-}parser.Init({strict=}false, PMax);
result := parser.GotoEnd(P);
end;
function GotoNextJsonItem(P: PUtf8Char; NumberOfItemsToJump: cardinal;
EndOfObject: PAnsiChar; PMax: PUtf8Char; Strict: boolean): PUtf8Char;
var
parser: TJsonGotoEndParser;
begin
{%H-}parser.Init(Strict, PMax);
result := nil; // to notify unexpected end
if NumberOfItemsToJump <> 0 then
repeat
P := parser.GotoEnd(P);
if P = nil then
exit;
if EndOfObject <> nil then
EndOfObject^ := P^; // return last jcEndOfJsonFieldOr0
if P^ <> #0 then
inc(P);
dec(NumberOfItemsToJump);
until NumberOfItemsToJump = 0;
result := P;
end;
function GotoNextJsonItem(P: PUtf8Char; var EndOfObject: AnsiChar): PUtf8Char;
var
parser: TJsonGotoEndParser;
begin
{%H-}parser.Init(false, nil);
result := parser.GotoEnd(P, EndOfObject);
end;
function GetJsonObjectOrArray(P: PUtf8Char;
EndOfObject: PUtf8Char; Len: PInteger): PUtf8Char;
var
parser: TJsonGotoEndParser;
begin
{%H-}parser.Init({strict=}false, nil);
result := parser.GotoEnd(P);
if result = nil then
exit;
if Len <> nil then
Len^ := result - P;
while (result^ <= ' ') and
(result^ <> #0) do
inc(result);
if EndOfObject <> nil then
EndOfObject^ := result^;
if result^ <> #0 then
begin
result^ := #0; // make zero-terminated
inc(result);
end;
end;
function JsonArrayCount(P, PMax: PUtf8Char; Strict: boolean): integer;
var
parser: TJsonGotoEndParser;
begin
{%H-}parser.InitCount(Strict, PMax, stValue);
if (parser.GotoEnd(P) = nil) and
(parser.RootCount >= 0) then
result := 0 // invalid input
else
result := parser.RootCount; // negative if PMax was reached
end;
function JsonArrayDecode(P: PUtf8Char; out Values: TPUtf8CharDynArray): boolean;
var
n, max: PtrInt;
parser: TJsonGotoEndParser;
begin
result := false;
max := 0;
n := 0;
{%H-}parser.Init({strict=}false, nil);
P := GotoNextNotSpace(P);
if P^ <> ']' then
repeat
if max = n then
begin
max := NextGrow(max);
SetLength(Values, max);
end;
Values[n] := P;
inc(n);
P := parser.GotoEnd(P);
if P = nil then
exit; // invalid content, or #0 reached
if P^ <> ',' then
break;
inc(P);
until false;
if P^ = ']' then
begin
if n <> 0 then
DynArrayFakeLength(Values{%H-}, n);
result := true;
end
else
Values := nil;
end;
function JsonArrayItem(P: PUtf8Char; Index: integer): PUtf8Char;
var
parser: TJsonGotoEndParser;
begin
if P <> nil then
begin
P := GotoNextNotSpace(P);
if P^ = '[' then
begin
{%H-}parser.Init({strict=}false, nil);
P := GotoNextNotSpace(P + 1);
if P^ <> ']' then
repeat
if Index <= 0 then
begin
result := P;
exit;
end;
P := parser.GotoEnd(P);
if (P = nil) or
(P^ <> ',') then
break; // invalid content or #0 reached
inc(P);
dec(Index);
until false;
end;
end;
result := nil;
end;
function JsonObjectPropCount(P, PMax: PUtf8Char; Strict: boolean): PtrInt;
var
parser: TJsonGotoEndParser;
begin // is very efficiently inlined on FPC
{%H-}parser.InitCount(Strict, PMax, stObjectName);
P := parser.GotoEnd(P);
result := parser.RootCount;
if P = nil then
// <0 means aborted when PMax or #0 was reached
if result >= 0 then
result := 0; // the JSON input was invalid
end;
function JsonObjectItem(P: PUtf8Char; const PropName: RawUtf8;
PropNameFound: PRawUtf8): PUtf8Char;
begin
result := JsonObjectItem(P, pointer(PropName), length(PropName), PropNameFound);
end;
function JsonObjectItem(P: PUtf8Char; PropName: PUtf8Char; PropNameLen: PtrInt;
PropNameFound: PRawUtf8): PUtf8Char;
var
name: ShortString; // no memory allocation nor P^ modification
PropNameUpper: array[byte] of AnsiChar;
parser: TJsonGotoEndParser;
begin
if P <> nil then
begin
P := GotoNextNotSpace(P);
if PropNameLen > 0 then
begin
if PropName[PropNameLen - 1] = '*' then
begin
UpperCopy255Buf(PropNameUpper{%H-}, PropName, PropNameLen - 1)^ := #0;
PropNameLen := 0; // mark 'PropName*' search
end;
if P^ = '{' then
P := GotoNextNotSpace(P + 1);
if P^ <> '}' then
repeat
GetJsonPropNameShort(P, name);
if (name[0] = #0) or
(name[0] > #200) then
break;
while (P^ <= ' ') and
(P^ <> #0) do
inc(P);
if PropNameLen = 0 then // 'PropName*'
begin
name[ord(name[0]) + 1] := #0; // make ASCIIZ
if IdemPChar(@name[1], PropNameUpper) then
begin
if PropNameFound <> nil then
FastSetString(PropNameFound^, @name[1], ord(name[0]));
result := P;
exit;
end;
end
else if IdemPropName(name, PropName, PropNameLen) then
begin
result := P;
exit;
end;
{%H-}parser.Init({strict=}false, nil);
P := parser.GotoEnd(P);
if (P = nil) or
(P^ <> ',') then
break; // invalid content, or #0 reached
inc(P);
until false;
end;
end;
result := nil;
end;
function JsonObjectByPath(JsonObject, PropPath: PUtf8Char): PUtf8Char;
var
objName: ShortString;
begin
result := nil;
if (JsonObject = nil) or
(PropPath = nil) then
exit;
repeat
GetNextItemShortString(PropPath, @objName, '.');
if objName[0] = #0 then
exit;
JsonObject := JsonObjectItem(JsonObject, @objName[1], ord(objName[0]), nil);
if JsonObject = nil then
exit;
until PropPath = nil; // found full name scope
result := JsonObject;
end;
function JsonObjectsByPath(JsonObject, PropPath: PUtf8Char): RawUtf8;
var
itemName, objName, propNameFound, objPath: RawUtf8;
start, ending, obj: PUtf8Char;
WR: TTextWriter;
temp: TTextWriterStackBuffer;
procedure AddFromStart(const name: RawUtf8);
begin
start := GotoNextNotSpace(start);
ending := GotoEndJsonItem(start);
if ending = nil then
exit;
if WR = nil then
begin
WR := TTextWriter.CreateOwnedStream(temp);
WR.AddDirect('{');
end
else
WR.AddComma;
WR.AddFieldName(name);
while (ending > start) and
(ending[-1] <= ' ') do
dec(ending); // trim right
WR.AddNoJsonEscape(start, ending - start);
end;
begin
result := '';
if (JsonObject = nil) or
(PropPath = nil) then
exit;
WR := nil;
try
repeat
GetNextItem(PropPath, ',', itemName);
if itemName = '' then
break;
if itemName[length(itemName)] <> '*' then // single property lookup
begin
start := JsonObjectByPath(JsonObject, pointer(itemName));
if start <> nil then
AddFromStart(itemName);
end
else // 'propname*' may append several properties
begin
objPath := '';
obj := pointer(itemName);
repeat
GetNextItem(obj, '.', objName);
if objName = '' then
exit;
propNameFound := '';
JsonObject := JsonObjectItem(JsonObject, objName, @propNameFound);
if JsonObject = nil then
exit;
if obj = nil then
begin
// found full name scope
start := JsonObject;
repeat
AddFromStart(objPath + propNameFound);
ending := GotoNextNotSpace(ending);
if ending^ <> ',' then
break;
propNameFound := '';
start := JsonObjectItem(
GotoNextNotSpace(ending + 1), objName, @propNameFound);
until start = nil;
break;
end
else
objPath := objPath + objName + '.';
until false;
end;
until PropPath = nil;
if WR <> nil then
begin
WR.AddDirect('}');
WR.SetText(result);
end;
finally
WR.Free;
end;
end;
function JsonObjectAsJsonArrays(Json: PUtf8Char; out keys, values: RawUtf8): integer;
var
wk, wv: TTextWriter;
kb, ke, vb, ve: PUtf8Char;
temp1, temp2: TTextWriterStackBuffer;
parser: TJsonGotoEndParser;
n: integer;
begin
result := -1;
if (Json = nil) or
(Json^ <> '{') then
exit;
parser.Init({strict=}false, nil);
n := 0;
wk := TTextWriter.CreateOwnedStream(temp1);
wv := TTextWriter.CreateOwnedStream(temp2);
try
wk.AddDirect('[');
wv.AddDirect('[');
kb := Json + 1;
repeat
ke := parser.GotoEnd(kb);
if (ke = nil) or
(ke^ <> ':') then
exit; // invalid input content
vb := ke + 1;
ve := parser.GotoEnd(vb);
if (ve = nil) or
not (ve^ in [',', '}']) then
exit;
wk.AddNoJsonEscape(kb, ke - kb);
wk.AddComma;
wv.AddNoJsonEscape(vb, ve - vb);
wv.AddComma;
kb := ve + 1;
inc(n);
until ve^ = '}';
wk.CancelLastComma(']');
wk.SetText(keys);
wv.CancelLastComma(']');
wv.SetText(values);
result := n; // success
finally
wv.Free;
wk.Free;
end;
end;
function DoRemoveComment(P: PUtf8Char): PUtf8Char;
{$ifdef HASINLINE} inline; {$endif}
begin
result := P + 1;
case result^ of
'/':
begin // this is // comment - replace by ' '
dec(result);
repeat
result^ := ' ';
inc(result)
until result^ in [#0, #10, #13];
if result^ <> #0 then
inc(result);
end;
'*':
begin // this is /* comment - replace by ' ' but keep CRLF
result[-1] := ' ';
repeat
if not (result^ in [#10, #13]) then
result^ := ' '; // keep CRLF for line numbering (e.g. for error)
inc(result);
if PWord(result)^ = ord('*') + ord('/') shl 8 then
begin
PWord(result)^ := $2020;
inc(result, 2);
break;
end;
until result^ = #0;
end;
end;
end;
procedure RemoveCommentsFromJson(P: PUtf8Char);
var
PComma: PUtf8Char;
begin // replace comments by ' ' characters which will be ignored by parser
if P <> nil then
while P^ <> #0 do
begin
case P^ of
'"':
begin
P := GotoEndOfJsonString2(P + 1, @JSON_CHARS);
if P^ <> '"' then
exit;
inc(P);
end;
'/':
P := DoRemoveComment(P);
',':
begin
// replace trailing comma by space for strict JSON parsers
PComma := P;
repeat
inc(P)
until (P^ > ' ') or
(P^ = #0);
if P^ = '/' then
P := DoRemoveComment(P);
while (P^ <= ' ') and
(P^ <> #0) do
inc(P);
if P^ in ['}', ']'] then
PComma^ := ' '; // see https://github.com/synopse/mORMot/pull/349
end;
else
inc(P);
end;
end;
end;
function RemoveCommentsFromJson(const s: RawUtf8): RawUtf8;
begin
if PosExChar('/', s) = 0 then
result := s
else
begin
FastSetString(result, pointer(s), length(s));
RemoveCommentsFromJson(pointer(s)); // remove in-place
end;
end;
function ParseEndOfObject(P: PUtf8Char; out EndOfObject: AnsiChar): PUtf8Char;
var
tab: PJsonCharSet;
begin
if P <> nil then
begin
tab := @JSON_CHARS; // mimics GetJsonField()
while not (jcEndOfJsonFieldOr0 in tab[P^]) do
inc(P); // not #0 , ] } :
EndOfObject := P^;
if P^ <> #0 then
repeat
inc(P); // ignore trailing , ] } and any successive spaces
until (P^ > ' ') or
(P^ = #0);
end;
result := P;
end;
function GetSetNameValue(Names: PShortString; MinValue, MaxValue: integer;
var P: PUtf8Char; out EndOfObject: AnsiChar): QWord;
var
info: TGetJsonField;
tmp: shortstring;
begin
result := 0;
if (P = nil) or
(Names = nil) or
(MinValue < 0) or
(MaxValue < 0) then
exit;
while (P^ <= ' ') and
(P^ <> #0) do
inc(P);
if P^ = '[' then
begin // stored as JSON array
repeat
inc(P)
until (P^ > ' ') or
(P^ = #0);
if P^ = ']' then
inc(P)
else
begin
info.Json := P;
repeat
info.GetJsonField;
if (info.Value = nil) or
not info.WasString then
begin
P := nil; // invalid input (expects a JSON array of strings)
exit;
end;
SetNamesValue(Names, MinValue, MaxValue, info.Value, info.ValueLen, result);
until info.EndOfObject = ']';
P := info.Json;
if P = nil then
exit; // avoid GPF below if already reached the input end
end;
P := ParseEndOfObject(P, EndOfObject); // mimics GetJsonField()
if EndOfObject = #0 then
P := nil; // as in mORMot 1
end
else
begin
info.Json := P;
info.GetJsonField;
P := info.Json;
if info.WasString then // stored as CSV text (e.g. from a .INI file)
while info.Value <> nil do
begin
GetNextItemShortString(info.Value, @tmp);
SetNamesValue(Names, MinValue, MaxValue, @tmp[1], ord(tmp[0]), result);
end
else // stored as a 64-bit unsigned integer
SetQWord(info.Value, result);
EndOfObject := info.EndOfObject;
end;
end;
function GetSetNameValue(Info: PRttiInfo;
var P: PUtf8Char; out EndOfObject: AnsiChar): QWord;
var
Names: PShortString;
MinValue, MaxValue: integer;
begin
if (Info <> nil) and
(Info^.Kind = rkSet) and
(Info^.SetEnumType(Names, MinValue, MaxValue) <> nil) then
result := GetSetNameValue(Names, MinValue, MaxValue, P, EndOfObject)
else
result := 0;
end;
function UrlEncodeJsonObject(const UriName: RawUtf8; ParametersJson: PUtf8Char;
const PropNamesToIgnore: array of RawUtf8; IncludeQueryDelimiter: boolean): RawUtf8;
var
i, j: PtrInt;
sep: AnsiChar;
Params: TNameValuePUtf8CharDynArray;
temp: TTextWriterStackBuffer;
begin
if ParametersJson = nil then
result := UriName
else
with TTextWriter.CreateOwnedStream(temp) do
try
AddString(UriName);
if (JsonDecode(ParametersJson, Params, true) <> nil) and
(Params <> nil) then
begin
sep := '?';
for i := 0 to length(Params) - 1 do
with Params[i] do
begin
for j := 0 to high(PropNamesToIgnore) do
if IdemPropNameU(PropNamesToIgnore[j], Name.Text, Name.Len) then
begin
Name.Len := 0;
break;
end;
if Name.Len = 0 then
continue; // was within PropNamesToIgnore[]
if IncludeQueryDelimiter then
Add(sep);
AddShort(Name.Text, Name.Len);
Add('=');
AddString(UrlEncode(Value.Text));
sep := '&';
IncludeQueryDelimiter := true;
end;
end;
SetText(result);
finally
Free;
end;
end;
function UrlEncodeJsonObject(const UriName, ParametersJson: RawUtf8;
const PropNamesToIgnore: array of RawUtf8; IncludeQueryDelimiter: boolean): RawUtf8;
var
temp: TSynTempBuffer;
begin
temp.Init(ParametersJson);
try
result := UrlEncodeJsonObject(
UriName, temp.buf, PropNamesToIgnore, IncludeQueryDelimiter);
finally
temp.Done;
end;
end;
procedure QuotedStrJson(P: PUtf8Char; PLen: PtrInt; var result: RawUtf8;
const aPrefix, aSuffix: RawUtf8);
var
temp: TTextWriterStackBuffer;
Lp, Ls: PtrInt;
D: PUtf8Char;
begin
if ((P = nil) or
(PLen <= 0)) and
(aPrefix = '') and
(aSuffix = '') then
result := '""'
else if (pointer(result) = pointer(P)) or
NeedsJsonEscape(P, PLen) then
// use TJsonWriter.AddJsonEscape() for proper JSON escape
with TJsonWriter.CreateOwnedStream(temp) do
try
AddString(aPrefix);
AddDirect('"');
AddJsonEscape(P, PLen);
AddDirect('"');
AddString(aSuffix);
SetText(result);
exit;
finally
Free;
end
else
begin
// direct allocation if no JSON escape is needed
Lp := length(aPrefix);
Ls := length(aSuffix);
FastSetString(result, PLen + Lp + Ls + 2);
D := pointer(result); // we checked dest result <> source P above
if Lp > 0 then
begin
MoveFast(pointer(aPrefix)^, D^, Lp);
inc(D, Lp);
end;
D^ := '"';
MoveFast(P^, D[1], PLen);
inc(D, PLen);
D[1] := '"';
if Ls > 0 then
MoveFast(pointer(aSuffix)^, D[2], Ls);
end;
end;
procedure QuotedStrJson(const aText: RawUtf8; var result: RawUtf8;
const aPrefix, aSuffix: RawUtf8);
begin
QuotedStrJson(pointer(aText), Length(aText), result, aPrefix, aSuffix);
end;
function QuotedStrJson(const aText: RawUtf8): RawUtf8;
begin
QuotedStrJson(pointer(aText), Length(aText), result, '', '');
end;
procedure JsonBufferReformat(P: PUtf8Char; out result: RawUtf8;
Format: TTextWriterJsonFormat);
var
temp: array[word] of byte; // 64KB buffer
begin
if P <> nil then
with TJsonWriter.CreateOwnedStream(@temp, SizeOf(temp)) do
try
AddJsonReformat(P, Format, nil);
SetText(result);
finally
Free;
end;
end;
function JsonReformat(const Json: RawUtf8; Format: TTextWriterJsonFormat): RawUtf8;
var
tmp: TSynTempBuffer;
begin
tmp.Init(Json);
try
JsonBufferReformat(tmp.buf, result, Format);
finally
tmp.Done;
end;
end;
function JsonBufferReformatToFile(P: PUtf8Char; const Dest: TFileName;
Format: TTextWriterJsonFormat): boolean;
var
F: TStream;
temp: array[word] of word; // 128KB
begin
try
F := TFileStreamEx.Create(Dest, fmCreate);
try
with TJsonWriter.Create(F, @temp, SizeOf(temp)) do
try
AddJsonReformat(P, Format, nil);
FlushFinal;
finally
Free;
end;
result := true;
finally
F.Free;
end;
except
on Exception do
result := false;
end;
end;
function JsonReformatToFile(const Json: RawUtf8; const Dest: TFileName;
Format: TTextWriterJsonFormat): boolean;
var
tmp: TSynTempBuffer;
begin
tmp.Init(Json);
try
result := JsonBufferReformatToFile(tmp.buf, Dest, Format);
finally
tmp.Done;
end;
end;
function Expect(var P: PUtf8Char; Pattern: PUtf8Char; PatternLen: PtrInt): boolean;
var
i: PtrInt;
J: PUtf8Char;
begin
result := false;
J := P;
if J = nil then
exit;
while (J^ <= ' ') and
(J^ <> #0) do
inc(J);
if PPtrInt(J)^ = PPtrInt(Pattern)^ then // PatternLen is at least 8 bytes long
begin
for i := SizeOf(PtrInt) to PatternLen - 1 do
if J[i] <> Pattern[i] then
exit;
P := J + PatternLen;
result := true;
end;
end;
function IsNotExpandedBuffer(var P: PUtf8Char; PEnd: PUtf8Char;
var FieldCount, RowCount: PtrInt): boolean;
var
RowCountPos: PUtf8Char;
begin
if not Expect(P, FIELDCOUNT_PATTERN, 14) then
begin
result := false;
exit;
end;
FieldCount := GetNextItemCardinal(P, #0);
if Expect(P, ROWCOUNT_PATTERN, 12) then
RowCount := GetNextItemCardinal(P, #0) // initial "rowCount":xxxx
else
begin
if PEnd = nil then
PEnd := P + mormot.core.base.StrLen(P); // late search of ending
RowCountPos := NotExpandedBufferRowCountPos(P, PEnd);
if RowCountPos = nil then
RowCount := -1 // no "rowCount":xxxx
else
RowCount := GetCardinal(RowCountPos); // trailing "rowCount":xxxx
end;
result := (FieldCount <> 0) and
Expect(P, VALUES_PATTERN, 11);
if result and
(RowCount < 0) then
begin
RowCount := JsonArrayCount(P, PEnd) div FieldCount; // 900MB/s browse
if RowCount <= 0 then
RowCount := -1; // bad format -> no data
end;
end;
function NotExpandedBufferRowCountPos(P, PEnd: PUtf8Char): PUtf8Char;
var
i: PtrInt;
begin
// search for "rowCount": at the end of the JSON buffer
result := nil;
if (PEnd <> nil) and
(PEnd - P > 24) then
for i := 1 to 24 do
case PEnd[-i] of
']',
',':
exit;
':':
begin
if CompareMemFixed(PEnd - i - 11, pointer(ROWCOUNT_PATTERN), 11) then
result := PEnd - i + 1;
exit;
end;
end;
end;
function GotoFieldCountExpanded(P: PUtf8Char): PUtf8Char;
begin
result := nil;
while P^ <> '[' do
if P^ = #0 then
exit
else
inc(P); // need an array of objects
repeat
inc(P);
if P^ = #0 then
exit;
until P^ in ['{', ']']; // go to object beginning
result := P;
end;
function GetFieldCountExpanded(P: PUtf8Char): integer;
var
EndOfObject: AnsiChar;
parser: TJsonGotoEndParser;
begin
result := 0;
{%H-}parser.Init(false, nil);
repeat
P := parser.GotoEnd(P, EndOfObject{%H-}); // ignore Name
P := parser.GotoEnd(P, EndOfObject); // ignore Value
if P = nil then
begin // unexpected end
result := 0;
exit;
end;
inc(result);
if EndOfObject = '}' then
break; // end of object
until false;
end;
procedure FormatParams(const Format: RawUtf8; const Args, Params: array of const;
JsonFormat: boolean; var Result: RawUtf8);
var
A, P: PtrInt;
F, FDeb: PUtf8Char;
isParam: AnsiChar;
tmp: TTempUtf8;
wasString: boolean;
temp: TTextWriterStackBuffer;
begin
if (Format = '') or
((high(Args) < 0) and
(high(Params) < 0)) then
// no formatting to process, but may be a const
// -> make unique since e.g. _JsonFmt() will parse it in-place
FastSetString(Result, pointer(Format), length(Format))
else if high(Params) < 0 then
// faster function with no ?
FormatUtf8(Format, Args, Result)
else if Format = '%' then
// optimize raw conversion
VarRecToUtf8(Args[0], Result)
else
// handle any number of parameters with minimal memory allocations
with TJsonWriter.CreateOwnedStream(temp) do
try
A := 0;
P := 0;
F := pointer(Format);
while F^ <> #0 do
begin
if (F^ <> '%') and
(F^ <> '?') then
begin
// handle plain text between % ? markers
FDeb := F;
repeat
inc(F);
until F^ in [#0, '%', '?'];
AddNoJsonEscape(FDeb, F - FDeb);
if F^ = #0 then
break;
end;
isParam := F^;
inc(F); // jump '%' or '?'
if (isParam = '%') and
(A <= high(Args)) then
begin
// handle % substitution
if Args[A].VType = vtObject then
AddShort(ClassNameShort(Args[A].VObject)^)
else
Add(Args[A]);
inc(A);
end
else if (isParam = '?') and
(P <= high(Params)) then
begin
// handle ? substitution as JSON or SQL
if JsonFormat then
AddJsonEscape(Params[P]) // does the JSON magic including "quotes"
else
begin
Add(':', '('); // markup for SQL parameter binding
VarRecToTempUtf8(Params[P], tmp, @wasString);
if wasString then
AddQuotedStr(tmp.Text, tmp.Len, '''') // SQL quote
else
AddShort(tmp.Text, tmp.Len); // numbers
if tmp.TempRawUtf8 <> nil then
RawUtf8(tmp.TempRawUtf8) := ''; // release temp memory
Add(')', ':');
end;
inc(P);
end
else
begin
// no more available Args or Params -> add all remaining text
AddNoJsonEscape(F, length(Format) - (F - pointer(Format)));
break;
end;
end;
SetText(Result);
finally
Free;
end;
end;
function FormatSql(const Format: RawUtf8;
const Args, Params: array of const): RawUtf8;
begin
FormatParams(Format, Args, Params, {json=}false, result);
end;
function FormatJson(const Format: RawUtf8;
const Args, Params: array of const): RawUtf8;
begin
FormatParams(Format, Args, Params, {json=}true, result);
end;
{$ifndef PUREMORMOT2}
function FormatUtf8(const Format: RawUtf8; const Args, Params: array of const;
JsonFormat: boolean): RawUtf8;
begin
FormatParams(Format, Args, Params, JsonFormat, result);
end;
{$endif PUREMORMOT2}
{ ********** Low-Level JSON Serialization for all TRttiParserType }
// some methods defined here for proper inlining
procedure TJsonWriter.BlockAfterItem(Options: TTextWriterWriteObjectOptions);
begin
B[1] := ',';
inc(B);
if woHumanReadable in Options then
AddCRAndIndent;
end;
procedure TJsonWriter.BlockBegin(Starter: AnsiChar;
Options: TTextWriterWriteObjectOptions);
begin
if woHumanReadable in Options then
begin
AddCRAndIndent;
inc(fHumanReadableLevel);
end;
Add(Starter);
end;
procedure TJsonWriter.BlockEnd(Stopper: AnsiChar;
Options: TTextWriterWriteObjectOptions);
begin
if woHumanReadable in Options then
begin
dec(fHumanReadableLevel);
AddCRAndIndent;
end;
B[1] := Stopper;
inc(B);
end;
{ TJsonSaveContext }
procedure TJsonSaveContext.Init(WR: TJsonWriter;
WriteOptions: TTextWriterWriteObjectOptions; Rtti: TRttiCustom);
begin
W := WR;
if Rtti <> nil then
WriteOptions := WriteOptions + TRttiJson(Rtti).fIncludeWriteOptions;
Options := WriteOptions;
Info := Rtti;
Prop := nil;
end;
procedure TJsonSaveContext.AddShort(PS: PShortString);
begin
W.Add('"');
if twoTrimLeftEnumSets in W.CustomOptions then
W.AddTrimLeftLowerCase(PS)
else
W.AddShort(PS^);
W.AddDirect('"');
end;
procedure TJsonSaveContext.Add64(Value: PInt64; UnSigned: boolean);
begin
if woInt64AsHex in Options then
if Value^ = 0 then
W.Add('"', '"')
else
W.AddBinToHexDisplayLower(Value, SizeOf(Value^), '"')
else if UnSigned then
W.AddQ(PQWord(Value)^)
else
W.Add(Value^);
end;
procedure TJsonSaveContext.AddDateTime(Value: PDateTime; WithMS: boolean);
var
d: double;
begin
if woDateTimeWithMagic in Options then
W.AddShorter(JSON_SQLDATE_MAGIC_QUOTE_STR)
else
W.Add('"');
d := unaligned(Value^);
W.AddDateTime(d, WithMS);
if woDateTimeWithZSuffix in Options then
if not (twoDateTimeWithZ in W.CustomOptions) then // if not already done
if frac(d) = 0 then // FireFox can't decode short form "2017-01-01Z"
W.AddShort('T00:00:00Z') // the same pattern for date and dateTime
else
W.Add('Z');
W.AddDirect('"');
end;
procedure TJsonSaveContext.AddShortBoolean(PS: PShortString; Value: boolean);
begin
AddShort(PS);
W.Add(':');
W.Add(Value);
end;
procedure _JS_Null(Data: PBoolean; const Ctxt: TJsonSaveContext);
var
W: TJsonWriter;
begin
W := Ctxt.W;
W.AddNull;
end;
procedure _JS_Boolean(Data: PBoolean; const Ctxt: TJsonSaveContext);
begin
Ctxt.W.Add(Data^);
end;
procedure _JS_Byte(Data: PByte; const Ctxt: TJsonSaveContext);
var
W: TJsonWriter;
begin
W := Ctxt.W;
W.AddU(Data^);
end;
procedure _JS_SmallInt(Data: PSmallInt; const Ctxt: TJsonSaveContext);
var
W: TJsonWriter;
begin
W := Ctxt.W;
W.Add(Data^);
end;
procedure _JS_ShortInt(Data: PShortInt; const Ctxt: TJsonSaveContext);
var
W: TJsonWriter;
begin
W := Ctxt.W;
W.Add(Data^);
end;
procedure _JS_Cardinal(Data: PCardinal; const Ctxt: TJsonSaveContext);
var
W: TJsonWriter;
begin
W := Ctxt.W;
W.AddU(Data^);
end;
procedure _JS_Currency(Data: PInt64; const Ctxt: TJsonSaveContext);
begin
Ctxt.W.AddCurr64(Data);
end;
procedure _JS_Double(Data: PDouble; const Ctxt: TJsonSaveContext);
begin
Ctxt.W.AddDouble(unaligned(Data^));
end;
procedure _JS_Extended(Data: PSynExtended; const Ctxt: TJsonSaveContext);
begin
Ctxt.W.AddDouble({$ifndef TSYNEXTENDED80}unaligned{$endif}(Data^));
end;
procedure _JS_Int64(Data: PInt64; const Ctxt: TJsonSaveContext);
begin
Ctxt.Add64(Data, {unsigned=}false);
end;
procedure _JS_Integer(Data: PInteger; const Ctxt: TJsonSaveContext);
var
W: TJsonWriter;
begin
W := Ctxt.W;
W.Add(Data^);
end;
procedure _JS_QWord(Data: PInt64; const Ctxt: TJsonSaveContext);
begin
Ctxt.Add64(Data, {unsigned=}true);
end;
procedure _JS_RawByteString(Data: PRawByteString; const Ctxt: TJsonSaveContext);
begin
if (Data^ = '') or
((rcfIsRawBlob in Ctxt.Info.Cache.Flags) and
not (woRawBlobAsBase64 in Ctxt.Options)) then
Ctxt.W.AddNull
else
begin
Ctxt.W.Add('"'); // no magic trailer as with mORMot 1
Ctxt.W.WrBase64(pointer(Data^), length(Data^), {withmagic=}false);
Ctxt.W.AddDirect('"');
end;
end;
procedure _JS_RawJson(Data: PRawJson; const Ctxt: TJsonSaveContext);
begin
Ctxt.W.AddRawJson(Data^);
end;
procedure _JS_RawUtf8(Data: PAnsiChar; const Ctxt: TJsonSaveContext);
var
cp: cardinal;
begin
Ctxt.W.Add('"');
Data := PPointer(Data)^;
if Data <> nil then
begin
cp := Ctxt.Info.Cache.CodePage;
if cp = CP_UTF8 then
Ctxt.W.AddJsonEscape(Data, {len=}0)
else
Ctxt.W.AddAnyAnsiBuffer(Data, PStrLen(Data - _STRLEN)^, twJsonEscape, cp);
end;
Ctxt.W.AddDirect('"');
end;
procedure _JS_Ansi(Data: PAnsiChar; const Ctxt: TJsonSaveContext);
begin
Ctxt.W.Add('"');
Data := PPointer(Data)^;
if Data <> nil then
with PStrRec(Data - SizeOf(TStrRec))^ do
// will handle any AnsiString, WinAnsiString or other CP
Ctxt.W.AddAnyAnsiBuffer(Data, length, twJsonEscape,
{$ifdef HASCODEPAGE} codePage {$else} Ctxt.Info.Cache.CodePage {$endif});
Ctxt.W.AddDirect('"');
end;
procedure _JS_Single(Data: PSingle; const Ctxt: TJsonSaveContext);
begin
Ctxt.W.AddSingle(Data^);
end;
procedure _JS_Unicode(Data: PPWord; const Ctxt: TJsonSaveContext);
begin
Ctxt.W.Add('"');
Ctxt.W.AddJsonEscapeW(Data^);
Ctxt.W.AddDirect('"');
end;
procedure _JS_Char(Data: PAnsiChar; const Ctxt: TJsonSaveContext);
begin
Ctxt.W.Add('"');
if Data^ <> #0 then // #0 will be serialized as ""
Ctxt.W.AddJsonEscape(Data, 1);
Ctxt.W.AddDirect('"');
end;
procedure _JS_WideChar(Data: PWord; const Ctxt: TJsonSaveContext);
begin
Ctxt.W.Add('"');
if Data^ <> 0 then
Ctxt.W.AddJsonEscapeW(Data, 1);
Ctxt.W.AddDirect('"');
end;
procedure _JS_DateTime(Data: PDateTime; const Ctxt: TJsonSaveContext);
begin
Ctxt.AddDateTime(Data, {withms=}false);
end;
procedure _JS_DateTimeMS(Data: PDateTime; const Ctxt: TJsonSaveContext);
begin
Ctxt.AddDateTime(Data, {withms=}true);
end;
procedure _JS_GUID(Data: PGUID; const Ctxt: TJsonSaveContext);
begin
Ctxt.W.Add(Data, '"');
end;
procedure _JS_Hash(Data: pointer; const Ctxt: TJsonSaveContext);
begin
Ctxt.W.AddBinToHexDisplayLower(Data, Ctxt.Info.Size, '"');
end;
procedure _JS_Binary(Data: pointer; const Ctxt: TJsonSaveContext);
begin
if IsZeroSmall(Data, Ctxt.Info.BinarySize) then
Ctxt.W.Add('"', '"') // serialize "" for 0 value
else
Ctxt.W.AddBinToHexDisplayLower(Data, Ctxt.Info.BinarySize, '"');
end;
procedure _JS_TimeLog(Data: PInt64; const Ctxt: TJsonSaveContext);
begin
if woTimeLogAsText in Ctxt.Options then
Ctxt.W.AddTimeLog(Data, '"')
else
Ctxt.Add64(Data, true);
end;
procedure _JS_UnixTime(Data: PInt64; const Ctxt: TJsonSaveContext);
begin
if woTimeLogAsText in Ctxt.Options then
Ctxt.W.AddUnixTime(Data, '"')
else
Ctxt.Add64(Data, true);
end;
procedure _JS_UnixMSTime(Data: PInt64; const Ctxt: TJsonSaveContext);
begin
if woTimeLogAsText in Ctxt.Options then
Ctxt.W.AddUnixMSTime(Data, {withms=}true, '"')
else
Ctxt.Add64(Data, true);
end;
procedure _JS_WinAnsi(Data: PWinAnsiString; const Ctxt: TJsonSaveContext);
begin
Ctxt.W.Add('"');
Ctxt.W.AddAnyAnsiBuffer(pointer(Data^), length(Data^), twJsonEscape, CP_WINANSI);
Ctxt.W.AddDirect('"');
end;
procedure _JS_Word(Data: PWord; const Ctxt: TJsonSaveContext);
begin
Ctxt.W.AddU(Data^);
end;
procedure _JS_Interface(Data: PInterface; const Ctxt: TJsonSaveContext);
begin
{$ifdef HASINTERFACEASTOBJECT}
// interfaces can be saved/serialized as their own object instance,
// but not restored/unserialized in _JL_Interface()
if Data^ <> nil then
Ctxt.W.WriteObject(Data^ as TObject)
else
{$endif HASINTERFACEASTOBJECT}
Ctxt.W.AddNull;
end;
procedure _JS_PUtf8Char(Data: PPUtf8Char; const Ctxt: TJsonSaveContext);
begin
// PUtf8Char can be saved/serialized as their own UTF-8 content,
// but not restored/unserialized in _JL_PUtf8Char()
Ctxt.W.Add('"');
if Data^ <> nil then
Ctxt.W.AddJsonEscape(Data^, {len=}0);
Ctxt.W.AddDirect('"');
end;
procedure _JS_ID(Data: PInt64; const Ctxt: TJsonSaveContext);
var
_str: ShortString;
begin
Ctxt.W.Add(Data^);
if woIDAsIDstr in Ctxt.Options then
begin
Ctxt.W.BlockAfterItem(Ctxt.Options);
if (Ctxt.Prop <> nil) and
(Ctxt.Prop^.Name <> '') then
begin
Ansi7StringToShortString(Ctxt.Prop^.Name, _str);
AppendShort('_str', _str);
Ctxt.W.WriteObjectPropNameShort(_str, Ctxt.Options);
end
else
Ctxt.W.WriteObjectPropNameShort('ID_str', Ctxt.Options);
Ctxt.W.Add('"');
Ctxt.W.Add(Data^);
Ctxt.W.AddDirect('"');
end;
end;
procedure _JS_Enumeration(Data: PByte; const Ctxt: TJsonSaveContext);
var
o: TTextWriterOptions;
PS: PShortString;
begin
o := Ctxt.W.CustomOptions;
if (Ctxt.Options * [woFullExpand, woHumanReadable, woEnumSetsAsText] <> []) or
(o * [twoEnumSetsAsBooleanInRecord, twoEnumSetsAsTextInRecord] <> []) then
begin
PS := Ctxt.Info.Cache.EnumInfo^.GetEnumNameOrd(Data^);
if twoEnumSetsAsBooleanInRecord in o then
Ctxt.AddShortBoolean(PS, true)
else
Ctxt.AddShort(PS);
if woHumanReadableEnumSetAsComment in Ctxt.Options then
Ctxt.Info.Cache.EnumInfo^.GetEnumNameAll(Ctxt.W.fBlockComment, '', true);
end
else
Ctxt.W.AddU(Data^);
end;
procedure _JS_Set(Data: PCardinal; const Ctxt: TJsonSaveContext);
var
PS: PShortString;
i: cardinal;
v: QWord;
o: TTextWriterOptions;
begin
o := Ctxt.W.CustomOptions;
if twoEnumSetsAsBooleanInRecord in o then
begin
// { "set1": true/false, .... } with proper indentation
PS := Ctxt.Info.Cache.EnumList;
Ctxt.W.BlockBegin('{', Ctxt.Options);
i := 0;
repeat
if i >= Ctxt.Info.Cache.EnumMin then
Ctxt.AddShortBoolean(PS, GetBitPtr(Data, i));
if i = Ctxt.Info.Cache.EnumMax then
break;
inc(i);
Ctxt.W.BlockAfterItem(Ctxt.Options);
inc(PByte(PS), PByte(PS)^ + 1); // next
until false;
Ctxt.W.BlockEnd('}', Ctxt.Options);
end
else if (Ctxt.Options * [woFullExpand, woHumanReadable, woEnumSetsAsText] <> []) or
(twoEnumSetsAsTextInRecord in o) then
begin
// [ "set1", "set4", .... } on same line
Ctxt.W.Add('[');
if ((twoFullSetsAsStar in o) or
(woHumanReadableFullSetsAsStar in Ctxt.Options)) and
GetAllBits(Data^, Ctxt.Info.Cache.EnumMax + 1) then
Ctxt.W.AddShorter('"*"')
else
begin
PS := Ctxt.Info.Cache.EnumList;
for i := 0 to Ctxt.Info.Cache.EnumMax do
begin
if (i >= Ctxt.Info.Cache.EnumMin) and
GetBitPtr(Data, i) then
begin
Ctxt.AddShort(PS);
Ctxt.W.AddComma;
end;
inc(PByte(PS), PByte(PS)^ + 1); // next
end;
Ctxt.W.CancelLastComma;
end;
Ctxt.W.AddDirect(']');
if woHumanReadableEnumSetAsComment in Ctxt.Options then
Ctxt.Info.Cache.EnumInfo^.GetEnumNameAll(
Ctxt.W.fBlockComment, '"*" or a set of ', true);
end
else
begin
// standard serialization as unsigned integer (up to 64 items)
v := 0;
MoveFast(Data^, v, Ctxt.Info.Size);
Ctxt.W.AddQ(v);
end;
end;
procedure _JS_Array(Data: PAnsiChar; const Ctxt: TJsonSaveContext);
var
n: integer;
jsonsave: TRttiJsonSave;
c: TJsonSaveContext;
begin
{%H-}c.Init(Ctxt.W, Ctxt.Options, Ctxt.Info.ArrayRtti);
c.W.BlockBegin('[', c.Options);
jsonsave := c.Info.JsonSave; // e.g. PT_JSONSAVE/PTC_JSONSAVE
if Assigned(jsonsave) then
begin
// efficient JSON serialization
n := Ctxt.Info.Cache.ItemCount;
repeat
jsonsave(Data, c);
dec(n);
if n = 0 then
break;
c.W.BlockAfterItem(c.Options);
inc(Data, c.Info.Cache.Size);
until false;
end
else
// fallback to raw RTTI binary serialization with Base64 encoding
c.W.BinarySaveBase64(Data, Ctxt.Info.Info, [rkArray],
{withMagic=}true, {withcrc=}false);
c.W.BlockEnd(']', c.Options);
end;
procedure _JS_DynArray_Custom(Data: pointer; const Ctxt: TJsonSaveContext);
begin
// TRttiJson.RegisterCustomSerializer() custom callback for each item
TOnRttiJsonWrite(TRttiJson(Ctxt.Info).fJsonWriter)(
Ctxt.W, Data, Ctxt.Options);
end;
procedure _JS_OneProp(var c: TJsonSaveContext; p: PRttiCustomProp; Data: PAnsiChar);
{$ifdef HASINLINE} inline; {$endif}
begin
if (woHideSensitivePersonalInformation in c.Options) and
(rcfSpi in p^.Value.Flags) then
c.W.AddShorter('"***"')
else if p^.OffsetGet >= 0 then
begin
// direct value write (record field or plain class property)
c.Info := p^.Value;
c.Prop := p;
TRttiJsonSave(c.Info.JsonSave)(Data + p^.OffsetGet, c);
end
else
// need to call a getter method
p^.AddValueJson(c.W, Data, c.Options);
end;
type
TCCHook = class(TObjectWithCustomCreate); // to access its protected methods
procedure _JS_NonExpanded(var c: TJsonSaveContext; Data: PAnsiChar; n: integer);
var
v: PAnsiChar;
item: TRttiCustom;
p: PRttiCustomProp;
f: integer;
begin
// {"fieldCount":2,"rowCount":20,"values":["f1","f2","1v1",1v2,"2v1",2v2...]}
item := c.Info;
c.W.BlockBegin('{', c.Options);
c.W.AddShort('"fieldCount":');
c.W.AddU(item.Props.CountNonVoid);
c.W.AddShort(',"rowCount":');
c.W.AddU(n);
c.W.AddShort(',"values":[');
c.W.AddString(item.Props.NamesAsJsonArray); // pre-computed - with trailing ,
if n <> 0 then
repeat
if item.Kind = rkClass then
v := PPointer(Data)^
else
v := Data;
p := pointer(item.Props.List);
f := item.Props.Count;
repeat
if p^.Name <> '' then
begin
if not (rcfHookWriteProperty in item.Flags) or
not TCCHook(v).RttiWritePropertyValue(c.W, p, c.Options) then
_JS_OneProp(c, p, v);
c.W.AddComma; // no c.W.BlockAfterItem() if non-expanded
end;
inc(p);
dec(f);
until f = 0;
inc(Data, item.Cache.Size);
dec(n);
until n = 0;
c.W.CancelLastComma(']');
c.W.BlockEnd('}', c.Options);
end;
procedure _JS_DynArray(Data: PPointer; const Ctxt: TJsonSaveContext);
var
n, s: PtrInt;
jsonsave: TRttiJsonSave;
P: PAnsiChar;
c: TJsonSaveContext;
begin
{%H-}c.Init(Ctxt.W, Ctxt.Options, Ctxt.Info.ArrayRtti);
if (twoNonExpandedArrays in c.W.CustomOptions) and
(c.Info <> nil) and
(c.Info.Props.CountNonVoid > 0) and
(Data^ <> nil) then
begin
// non-expanded format efficient serialization
n := PDALen(PAnsiChar(Data^) - _DALEN)^ + _DAOFF; // length(Data)
if n <> 1 then // expanded is fine for a single object array
begin
_JS_NonExpanded(c, Data^, n);
exit;
end;
end;
c.W.BlockBegin('[', c.Options);
if Data^ <> nil then
begin
if TRttiJson(Ctxt.Info).fJsonWriter.Code <> nil then
begin
c.Info := Ctxt.Info;
jsonsave := @_JS_DynArray_Custom; // redirect to custom callback
end
else if c.Info = nil then
jsonsave := nil
else
jsonsave := c.Info.JsonSave; // e.g. PT_JSONSAVE/PTC_JSONSAVE
if Assigned(jsonsave) then
begin
// efficient JSON serialization
P := Data^;
n := PDALen(P - _DALEN)^ + _DAOFF; // length(Data)
s := Ctxt.Info.Cache.ItemSize; // c.Info may be nil
repeat
jsonsave(P, c);
dec(n);
if n = 0 then
break;
c.W.BlockAfterItem(c.Options);
inc(P, s);
until false;
end
else
// fallback to raw RTTI binary serialization with Base64 encoding
c.W.BinarySaveBase64(Data, Ctxt.Info.Info, [rkDynArray],
{withMagic=}true, {withcrc=}false);
end
else if (woHumanReadableEnumSetAsComment in Ctxt.Options) and
(c.Info <> nil) and
(rcfHasNestedProperties in c.Info.Flags) then
// void dynarray should include record/T*ObjArray fields as comment
c.Info.Props.AsText(c.W.fBlockComment, true, 'array of {', '}');
c.W.BlockEnd(']', c.Options);
end;
procedure _JS_Variant(Data: PVarData; const Ctxt: TJsonSaveContext); forward;
/// use pointer to allow any kind of Data^ type in _JS_*() functions
// - typecast to TRttiJsonSave for proper function call
const
VARIANT_JSONSAVE: array[varEmpty .. varOleUInt] of pointer = (
{0} @_JS_Null, @_JS_Null, @_JS_SmallInt, @_JS_Integer, @_JS_Single,
{5} @_JS_Double, @_JS_Currency, @_JS_DateTime, nil, nil,
{10} nil, @_JS_Boolean, nil, nil, nil,
{15} nil, @_JS_ShortInt, @_JS_Byte, @_JS_Word, @_JS_Cardinal,
{20} @_JS_Int64, @_JS_QWord, @_JS_Integer, @_JS_Cardinal);
// rkRecord and rkClass are handled in TRttiJson.SetParserType
PT_JSONSAVE: array[TRttiParserType] of pointer = (
nil, @_JS_Array, @_JS_Boolean, @_JS_Byte, @_JS_Cardinal, @_JS_Currency,
@_JS_Double, @_JS_Extended, @_JS_Int64, @_JS_Integer, @_JS_QWord,
@_JS_RawByteString, @_JS_RawJson, @_JS_RawUtf8, nil, @_JS_Single,
{$ifdef UNICODE} @_JS_Unicode {$else} @_JS_Ansi {$endif},
@_JS_Unicode, @_JS_DateTime, @_JS_DateTimeMS, @_JS_GUID, @_JS_Hash,
@_JS_Hash, @_JS_Hash, nil, @_JS_TimeLog, @_JS_Unicode, @_JS_UnixTime,
@_JS_UnixMSTime, @_JS_Variant, @_JS_Unicode, @_JS_WinAnsi, @_JS_Word,
@_JS_Enumeration, @_JS_Set, nil, @_JS_DynArray, @_JS_Interface,
@_JS_PUtf8Char, nil);
PTC_JSONSAVE: array[TRttiParserComplexType] of pointer = (
nil, nil, nil, nil, @_JS_ID, @_JS_ID, @_JS_QWord, @_JS_QWord, @_JS_QWord);
procedure _JS_Variant(Data: PVarData; const Ctxt: TJsonSaveContext);
var
vt: cardinal;
cv: TSynInvokeableVariantType;
save: TRttiJsonSave;
begin
repeat
vt := Data^.VType;
if vt <> varVariantByRef then
break;
Data := Data^.VPointer;
until false;
if vt <= high(VARIANT_JSONSAVE) then
begin
save := VARIANT_JSONSAVE[vt];
if Assigned(save) then
begin
save(@Data^.VAny, Ctxt);
exit;
end;
end;
case vt of // most common strings
varString:
{$ifdef HASCODEPAGE}
_JS_Ansi(@Data^.VAny, Ctxt);
{$else} // old Delphi can't use Ctxt.Info.Cache.CodePage
Ctxt.W.AddText(RawByteString(Data^.VString), twJsonEscape);
{$endif HASCODEPAGE}
{$ifdef HASVARUSTRING} varUString, {$endif} varOleStr:
_JS_Unicode(@Data^.VAny, Ctxt);
else
begin
cv := FindSynVariantType(vt); // our custom types
if cv <> nil then
cv.ToJson(Ctxt.W, Data)
else // unsupported or seldom used
Ctxt.W.AddVariant(PVariant(Data)^, twJsonEscape, Ctxt.Options);
end;
end;
end;
procedure AppendExceptionLocation(w: TJsonWriter; e: ESynException);
begin // call TDebugFile.FindLocationShort if mormot.core.log is used
w.Add('"');
w.AddShort(GetExecutableLocation(e.RaisedAt));
w.Add('"');
end;
// serialization of properties for both records and classes
procedure _JS_RttiCustom(Data: PAnsiChar; const Ctxt: TJsonSaveContext);
var
nfo: TRttiJson;
p: PRttiCustomProp;
t: TClass;
n: integer;
flags: set of (isNotFirst, noStored, noDefault, noHook, noVoid, isHumanReadable);
c: TJsonSaveContext; // dedicated context used for fields/properties
begin
c.W := Ctxt.W;
c.Options := Ctxt.Options;
nfo := TRttiJson(Ctxt.Info);
if nfo.Kind = rkClass then
begin
if Data <> nil then
Data := PPointer(Data)^; // class instances are accessed by reference
if Data = nil then
begin
c.W.AddNull; // append 'null' for nil class instance
exit;
end;
t := PClass(Data)^; // actual class of this instance
if t <> nfo.ValueClass then
nfo := TRttiJson(Rtti.RegisterClass(t)); // work on proper inherited class
flags := [];
if (woStoreStoredFalse in c.Options) or
(rcfDisableStored in nfo.Flags) then
include(flags, noStored);
if not (woDontStoreDefault in c.Options) then
include(flags, noDefault);
if not (rcfHookWriteProperty in nfo.Flags) then
include(flags, noHook);
end
else
begin
exclude(c.Options, woFullExpand); // not available for null or records
flags := [noStored, noDefault, noHook];
end;
if nfo.fJsonWriter.Code <> nil then // TRttiJson.RegisterCustomSerializer()
begin // e.g. TOrm.RttiJsonWrite
TOnRttiJsonWrite(nfo.fJsonWriter)(c.W, Data, c.Options);
exit;
end;
if not (rcfHookWrite in nfo.Flags) or
not TCCHook(Data).RttiBeforeWriteObject(c.W, c.Options) then
begin
// regular JSON serialization using nested fields/properties
if not ((woDontStoreVoid in c.Options) or
(twoIgnoreDefaultInRecord in c.W.CustomOptions)) then
include(flags, noVoid);
if woHumanReadable in c.Options then
begin
include(flags, isHumanReadable);
c.W.BlockBegin('{', c.Options)
end
else
c.W.Add('{');
c.Prop := pointer(nfo.Props.List);
n := nfo.Props.Count;
if (nfo.Kind = rkClass) and
(c.Options * [woFullExpand, woStoreClassName, woStorePointer,
woDontStoreInherited] <> []) then
begin
if woFullExpand in c.Options then
begin
c.W.AddInstanceName(TObject(Data), ':');
c.W.BlockBegin('{', c.Options);
end;
if woStoreClassName in c.Options then
begin
c.W.WriteObjectPropNameShort('ClassName', c.Options);
c.W.AddDirect('"');
c.W.AddShort(ClassNameShort(PClass(Data)^)^);
c.W.AddDirect('"');
if (c.Prop <> nil) or
(woStorePointer in c.Options) then
c.W.BlockAfterItem(c.Options);
end;
if woStorePointer in c.Options then
begin
c.W.WriteObjectPropNameShort('Address', c.Options);
if nfo.ValueRtlClass = vcESynException then
AppendExceptionLocation(c.W, ESynException(Data))
else
c.W.AddPointer(PtrUInt(Data), '"');
if c.Prop <> nil then
c.W.BlockAfterItem(c.Options);
end;
if woDontStoreInherited in c.Options then
with nfo.Props do
if NotInheritedIndex <> 0 then
begin
// List[NotInheritedIndex]..List[Count-1] is the last class level
inc(c.Prop, NotInheritedIndex);
dec(n, NotInheritedIndex);
end;
end;
if n > 0 then
begin
// this is the main loop serializing Info.Props[]
p := c.Prop;
repeat
if // handle Props.NameChange() set to Name='' to ignore this field
(p^.Name <> '') and
// handle woStoreStoredFalse flag and "stored" attribute in code
((p^.Stored = rpsTrue) or // most common case
(noStored in flags) or
((p^.Stored = rpsGetter) and
(p^.Prop.IsStoredGetter(pointer(Data))))) and
// handle woDontStoreDefault flag over "default" attribute in code
((noDefault in flags) or
(p^.OrdinalDefault = NO_DEFAULT) or
not p^.ValueIsDefault(Data)) and
// detect 0 numeric values and empty strings
((noVoid in flags) or
not p^.ValueIsVoid(Data)) then
begin
// if we reached here, we should serialize this property
if isNotFirst in flags then
// append ',' and proper indentation if a field was just appended
c.W.BlockAfterItem(c.Options);
if isHumanReadable in flags then
c.W.WriteObjectPropNameHumanReadable(pointer(p^.Name), length(p^.Name))
else
c.W.AddProp(pointer(p^.Name), length(p^.Name));
if (noHook in flags) or
not TCCHook(Data).RttiWritePropertyValue(c.W, p, c.Options) then
_JS_OneProp(c, p, Data);
include(flags, isNotFirst);
end;
dec(n);
if n = 0 then
break;
inc(p);
until false;
end;
if rcfHookWrite in nfo.Flags then
TCCHook(Data).RttiAfterWriteObject(c.W, c.Options);
if isHumanReadable in flags then
c.W.BlockEnd('}', c.Options)
else
c.W.AddDirect('}');
if woFullExpand in c.Options then
c.W.BlockEnd('}', c.Options);
end;
end;
// most known RTL classes custom serialization
procedure _JS_Objects(W: TJsonWriter; Value: PObject; Count: integer;
Options: TTextWriterWriteObjectOptions);
var
ctxt: TJsonSaveContext;
save: TRttiJsonSave;
c, v: pointer; // reuse ctxt.Info if classes are the same (very likely)
begin
c := nil;
save := nil;
{%H-}ctxt.Init(W, Options, nil);
W.BlockBegin('[', Options);
if Count > 0 then
repeat
v := Value^;
if v = nil then
W.AddNull
else
begin
v := PPointer(v)^; // check Value class
if v <> c then
begin
// need to retrieve the RTTI
c := v;
ctxt.Info := Rtti.RegisterClass(TClass(v));
save := ctxt.Info.JsonSave;
end;
// this is where each object is serialized
save(pointer(Value), ctxt);
end;
dec(Count);
if Count = 0 then
break;
W.BlockAfterItem(Options);
inc(Value);
until false;
W.BlockEnd(']', Options);
end;
procedure _JS_TList(Data: PList; const Ctxt: TJsonSaveContext);
begin
if Data^ = nil then
Ctxt.W.AddNull
else
_JS_Objects(Ctxt.W, pointer(Data^.List), Data^.Count, Ctxt.Options);
end;
procedure _JS_TObjectList(Data: PObjectList; const Ctxt: TJsonSaveContext);
var
o: TTextWriterWriteObjectOptions;
begin
if Data^ = nil then
begin
Ctxt.W.AddNull;
exit;
end;
o := Ctxt.Options;
if not (woObjectListWontStoreClassName in o) then
include(o, woStoreClassName);
_JS_Objects(Ctxt.W, pointer(Data^.List), Data^.Count, o);
end;
procedure _JS_TCollection(Data: PCollection; const Ctxt: TJsonSaveContext);
var
item: TCollectionItem;
i, last: PtrInt;
c: TJsonSaveContext; // reuse same context for all collection items
begin
if Data^ = nil then
begin
Ctxt.W.AddNull;
exit;
end;
// can't use AddObjects() since we don't have access to the TCollection list
{%H-}c.Init(Ctxt.W, Ctxt.Options, Rtti.RegisterClass(Data^.ItemClass));
c.W.BlockBegin('[', c.Options);
i := 0;
last := Data^.Count - 1;
if last >= 0 then
repeat
item := Data^.Items[i];
TRttiJsonSave(c.Info.JsonSave)(@item, c);
if i = last then
break;
c.W.BlockAfterItem(c.Options);
inc(i);
until false;
c.W.BlockEnd(']', c.Options);
end;
procedure _JS_TStrings(Data: PStrings; const Ctxt: TJsonSaveContext);
var
i, last: PtrInt;
begin
if Data^ = nil then
begin
Ctxt.W.AddNull;
exit;
end;
Ctxt.W.BlockBegin('[', Ctxt.Options);
i := 0;
last := Data^.Count - 1;
if last >= 0 then
repeat
Ctxt.W.Add('"');
Ctxt.W.AddJsonEscapeString(Data^.Strings[i]);
Ctxt.W.AddDirect('"');
if i = last then
break;
Ctxt.W.BlockAfterItem(Ctxt.Options);
inc(i);
until false;
Ctxt.W.BlockEnd(']', Ctxt.Options);
end;
procedure _JS_TRawUtf8List(Data: PRawUtf8List; const Ctxt: TJsonSaveContext);
var
i, last: PtrInt;
u: PPUtf8CharArray;
begin
if Data^ = nil then
begin
Ctxt.W.AddNull;
exit;
end;
Ctxt.W.BlockBegin('[', Ctxt.Options);
i := 0;
u := Data^.TextPtr;
last := Data^.Count - 1;
if last >= 0 then
repeat
Ctxt.W.Add('"');
Ctxt.W.AddJsonEscape(u[i]);
Ctxt.W.AddDirect('"');
if i = last then
break;
Ctxt.W.BlockAfterItem(Ctxt.Options);
inc(i);
until false;
Ctxt.W.BlockEnd(']', Ctxt.Options);
end;
procedure _JS_TSynList(Data: PSynList; const Ctxt: TJsonSaveContext);
begin
if Data^ = nil then
Ctxt.W.AddNull
else
_JS_Objects(Ctxt.W, pointer(Data^.List), Data^.Count, Ctxt.Options);
end;
procedure _JS_TSynObjectList(Data: PSynObjectList; const Ctxt: TJsonSaveContext);
var
o: TTextWriterWriteObjectOptions;
begin
if Data^ = nil then
begin
Ctxt.W.AddNull;
exit;
end;
o := Ctxt.Options;
if not (woObjectListWontStoreClassName in o) then
include(o, woStoreClassName);
_JS_Objects(Ctxt.W, pointer(Data^.List), Data^.Count, o);
end;
{ ********** TJsonWriter class with proper JSON escaping and WriteObject() support }
{ TJsonWriter }
procedure TJsonWriter.WriteObjectPropNameHumanReadable(
PropName: PUtf8Char; PropNameLen: PtrInt);
begin
AddCRAndIndent; // won't do anything if has already been done
AddProp(PropName, PropNameLen); // handle twoForceJsonExtended
Add(' ');
end;
procedure TJsonWriter.WriteObjectPropNameShort(const PropName: ShortString;
Options: TTextWriterWriteObjectOptions);
begin
if woHumanReadable in Options then
WriteObjectPropNameHumanReadable(@PropName[1], ord(PropName[0]))
else
AddProp(@PropName[1], ord(PropName[0]));
end;
procedure TJsonWriter.WriteObjectAsString(Value: TObject;
Options: TTextWriterWriteObjectOptions);
var
W: TJsonWriter;
begin
Add('"');
W := GetTempJsonWriter;
W.WriteObject(Value, Options);
AddJsonEscape(W);
Add('"');
end;
procedure TJsonWriter.AddDynArrayJsonAsString(aTypeInfo: PRttiInfo; var aValue;
WriteOptions: TTextWriterWriteObjectOptions);
var
temp: TDynArray;
W: TJsonWriter;
begin
Add('"');
temp.Init(aTypeInfo, aValue);
W := GetTempJsonWriter;
W.AddDynArrayJson(temp, WriteOptions);
AddJsonEscape(W);
Add('"');
end;
procedure TJsonWriter.AddCRAndIndent;
begin
if fBlockComment <> '' then
begin
AddShorter(' // ');
AddString(fBlockComment);
fBlockComment := '';
end;
inherited AddCRAndIndent;
end;
procedure TJsonWriter.AddPropJsonString(const PropName: ShortString;
const Text: RawUtf8);
begin
AddProp(@PropName[1], ord(PropName[0]));
AddJsonString(Text); // " + AddJsonEscape(Text) + "
AddComma;
end;
procedure TJsonWriter.InternalAddFixedAnsi(Source: PAnsiChar; SourceChars: cardinal;
AnsiToWide: PWordArray; Escape: TTextWriterKind);
var
c: cardinal;
esc: byte;
begin
if SourceChars > 0 then
repeat
case Escape of // twJsonEscape or twOnSameLine only occur on c <= $7f
twNone:
repeat
if B >= BEnd then
FlushToStream;
c := byte(Source^);
inc(Source);
if c > $7F then
break;
if c = 0 then
exit;
inc(B);
B^ := AnsiChar(c);
dec(SourceChars);
if SourceChars = 0 then
exit;
until false;
twJsonEscape:
repeat
if B >= BEnd then
FlushToStream;
c := byte(Source^);
inc(Source);
if c > $7F then
break;
if c = 0 then
exit;
esc := JSON_ESCAPE[c]; // c<>0 -> esc<>JSON_ESCAPE_ENDINGZERO
if esc = JSON_ESCAPE_NONE then
begin
// no escape needed
inc(B);
B^ := AnsiChar(c);
end
else if esc = JSON_ESCAPE_UNICODEHEX then
begin
// characters below ' ', #7 e.g. -> \u0007
AddShorter('\u00');
AddByteToHex(c);
end
else
Add('\', AnsiChar(esc)); // escaped as \ + b,t,n,f,r,\,"
dec(SourceChars);
if SourceChars = 0 then
exit;
until false;
else //twOnSameLine:
repeat
if B >= BEnd then
FlushToStream;
c := byte(Source^);
inc(Source);
if c > $7F then
break;
if c = 0 then
exit;
inc(B);
if c < 32 then
B^ := ' ' // on same line
else
B^ := AnsiChar(c);
dec(SourceChars);
if SourceChars = 0 then
exit;
until false;
end;
// handle c > $7F (no surrogate is expected in TSynAnsiFixedWidth charsets)
c := AnsiToWide[c]; // convert FixedAnsi char into Unicode char
if c > $7ff then
begin
B[1] := AnsiChar($E0 or (c shr 12));
B[2] := AnsiChar($80 or ((c shr 6) and $3F));
B[3] := AnsiChar($80 or (c and $3F));
inc(B, 3);
end
else
begin
B[1] := AnsiChar($C0 or (c shr 6));
B[2] := AnsiChar($80 or (c and $3F));
inc(B, 2);
end;
dec(SourceChars);
until SourceChars = 0;
end;
destructor TJsonWriter.Destroy;
begin
inherited Destroy;
fInternalJsonWriter.Free;
end;
function TJsonWriter.GetTempJsonWriter: TJsonWriter;
begin
if fInternalJsonWriter = nil then
fInternalJsonWriter := TJsonWriter.CreateOwnedStream(4096, {noshare=}true)
else
fInternalJsonWriter.CancelAllAsNew;
result := fInternalJsonWriter;
end;
procedure TJsonWriter.Add(P: PUtf8Char; Escape: TTextWriterKind);
begin
if P <> nil then
case Escape of
twNone:
AddNoJsonEscape(P, StrLen(P));
twJsonEscape:
AddJsonEscape(P);
twOnSameLine:
AddOnSameLine(P);
end;
end;
procedure TJsonWriter.Add(P: PUtf8Char; Len: PtrInt; Escape: TTextWriterKind);
begin
if P <> nil then
case Escape of
twNone:
AddNoJsonEscape(P, Len);
twJsonEscape:
AddJsonEscape(P, Len);
twOnSameLine:
AddOnSameLine(P, Len);
end;
end;
procedure TJsonWriter.AddW(P: PWord; Len: PtrInt; Escape: TTextWriterKind);
begin
if P <> nil then
case Escape of
twNone:
AddNoJsonEscapeW(P, Len);
twJsonEscape:
AddJsonEscapeW(P, Len);
twOnSameLine:
AddOnSameLineW(P, Len);
end;
end;
procedure TJsonWriter.AddAnsiString(const s: AnsiString; Escape: TTextWriterKind);
begin
AddAnyAnsiBuffer(pointer(s), length(s), Escape, 0);
end;
procedure TJsonWriter.AddAnyAnsiString(const s: RawByteString;
Escape: TTextWriterKind; CodePage: integer);
var
L: integer;
begin
L := length(s);
if L = 0 then
exit;
if (L > 2) and
(PInteger(s)^ and $ffffff = JSON_BASE64_MAGIC_C) then
begin
AddNoJsonEscape(pointer(s), L); // was marked as a BLOB content
exit;
end;
if CodePage < 0 then
{$ifdef HASCODEPAGE}
CodePage := GetCodePage(s);
{$else}
CodePage := CP_ACP; // TSynAnsiConvert.Engine(0)=CurrentAnsiConvert
{$endif HASCODEPAGE}
AddAnyAnsiBuffer(pointer(s), L, Escape, CodePage);
end;
procedure EngineAppendUtf8(W: TJsonWriter; Engine: TSynAnsiConvert;
P: PAnsiChar; Len: PtrInt; Escape: TTextWriterKind);
var
tmp: TSynTempBuffer;
begin
// explicit conversion using a temporary UTF-16 buffer on stack
Engine.AnsiBufferToUnicode(tmp.Init(Len * 3), P, Len); // includes ending #0
W.AddW(tmp.buf, 0, Escape);
tmp.Done;
end;
procedure TJsonWriter.AddAnyAnsiBuffer(P: PAnsiChar; Len: PtrInt;
Escape: TTextWriterKind; CodePage: integer);
var
B: PUtf8Char;
engine: TSynAnsiConvert;
label
utf8;
begin
if (P = nil) or
(Len <= 0) then
exit;
if CodePage = CP_ACP then // CP_UTF8 is very likely on POSIX or LCL
CodePage := Unicode_CodePage; // = CurrentAnsiConvert.CodePage
case CodePage of
CP_UTF8: // direct write of RawUtf8 content
begin
if Escape = twJsonEscape then
Len := 0; // faster with no Len
utf8: Add(PUtf8Char(P), Len, Escape);
end;
CP_RAWBYTESTRING: // direct write of RawByteString content as UTF-8
goto utf8;
CP_UTF16: // direct write of UTF-16 content
AddW(PWord(P), 0, Escape);
CP_RAWBLOB: // RawBlob written with Base64 encoding
begin
AddShorter(JSON_BASE64_MAGIC_S); // \uFFF0
WrBase64(P, Len, {withMagic=}false);
end;
else
begin
// first handle trailing 7-bit ASCII chars, by quad
B := pointer(P);
if Len >= 4 then
repeat
if PCardinal(P)^ and $80808080 <> 0 then
break; // break on first non ASCII quad
inc(P, 4);
dec(Len, 4);
until Len < 4;
if (Len > 0) and
(P^ <= #127) then
repeat
inc(P);
dec(Len);
until (Len = 0) or
(P^ > #127);
if P <> pointer(B) then
Add(B, P - B, Escape);
if Len <= 0 then
exit;
// rely on explicit conversion for all remaining ASCII characters
engine := TSynAnsiConvert.Engine(CodePage);
if PClass(engine)^ = TSynAnsiFixedWidth then
InternalAddFixedAnsi(P, Len,
pointer(TSynAnsiFixedWidth(engine).AnsiToWide), Escape)
else
EngineAppendUtf8(self, engine, P, Len, Escape);
end;
end;
end;
procedure TJsonWriter.WrBase64(P: PAnsiChar; Len: PtrUInt; withMagic: boolean);
var
trailing, main, n: PtrUInt;
begin
if P = nil then
Len := 0;
if withMagic then
if Len <= 0 then
begin
AddNull; // JSON null is better than "" for BLOBs
exit;
end
else
AddShorter(JSON_BASE64_MAGIC_QUOTE_S); // "\uFFF0
if Len > 0 then
begin
n := Len div 3;
trailing := Len - n * 3;
dec(Len, trailing);
if BEnd - B > integer(n + 1) shl 2 then
begin
// will fit in available space in Buf -> fast in-buffer Base64 encoding
n := Base64EncodeMain(@B[1], P, Len); // may use AVX2 on FPC x86_64
inc(B, n * 4);
inc(P, n * 3);
end
else
begin
// bigger than available space in Buf -> do it per chunk
FlushToStream;
while Len > 0 do
begin
n := ((fTempBufSize - 4) shr 2) * 3;
if Len < n then
n := Len;
main := Base64EncodeMain(PAnsiChar(fTempBuf), P, n);
n := main * 4;
if n < cardinal(fTempBufSize) - 4 then
inc(B, n)
else
WriteToStream(fTempBuf, n);
n := main * 3;
inc(P, n);
dec(Len, n);
end;
end;
if trailing > 0 then
begin
Base64EncodeTrailing(@B[1], P, trailing);
inc(B, 4);
end;
end;
if withMagic then
Add('"');
end;
procedure TJsonWriter.BinarySaveBase64(Data: pointer; Info: PRttiInfo;
Kinds: TRttiKinds; withMagic, withCrc: boolean);
var
temp: TSynTempBuffer;
begin
BinarySave(Data, temp, Info, Kinds, withCrc);
WrBase64(temp.buf, temp.len, withMagic);
temp.Done;
end;
procedure TJsonWriter.Add(const Format: RawUtf8; const Values: array of const;
Escape: TTextWriterKind; WriteObjectOptions: TTextWriterWriteObjectOptions);
var
ValuesIndex: integer;
S, F: PUtf8Char;
begin
if Format = '' then
exit;
if (Format = '%') and
(high(Values) >= 0) then
begin
Add(Values[0], Escape);
exit;
end;
ValuesIndex := 0;
F := pointer(Format);
repeat
S := F;
repeat
if (F^ = #0) or
(F^ = '%') then
break;
inc(F);
until false;
AddNoJsonEscape(S, F - S);
if F^ = #0 then
exit;
// add next value as text instead of F^='%' placeholder
if ValuesIndex <= high(Values) then // missing value will display nothing
Add(Values[ValuesIndex], Escape, WriteObjectOptions);
inc(F);
inc(ValuesIndex);
until false;
end;
procedure TJsonWriter.AddCsvUtf8(const Values: array of RawUtf8);
var
i: PtrInt;
begin
if length(Values) = 0 then
exit;
for i := 0 to high(Values) do
begin
Add('"');
AddJsonEscape(pointer(Values[i]));
AddDirect('"', ',');
end;
CancelLastComma;
end;
procedure TJsonWriter.AddCsvConst(const Values: array of const);
var
i: PtrInt;
begin
if length(Values) = 0 then
exit;
for i := 0 to high(Values) do
begin
AddJsonEscape(Values[i]);
AddComma;
end;
CancelLastComma;
end;
procedure TJsonWriter.Add(const Values: array of const);
var
i: PtrInt;
begin
for i := 0 to high(Values) do
AddJsonEscape(Values[i]);
end;
procedure TJsonWriter.Add(const Values: array of const; Escape: TTextWriterKind);
var
i: PtrInt;
begin
for i := 0 to high(Values) do
Add(Values[i], Escape);
end;
procedure TJsonWriter.AddQuotedStringAsJson(const QuotedString: RawUtf8);
var
L: integer;
P, B: PUtf8Char;
quote: AnsiChar;
begin
L := length(QuotedString);
if L = 0 then
exit;
quote := QuotedString[1];
if (quote in ['''', '"']) and
(QuotedString[L] = quote) then
begin
Add('"');
P := pointer(QuotedString);
inc(P);
repeat
B := P;
while P[0] <> quote do
inc(P);
if P[1] <> quote then
break; // end quote
inc(P);
AddJsonEscape(B, P - B);
inc(P); // ignore double quote
until false;
if P - B <> 0 then
AddJsonEscape(B, P - B);
Add('"');
end
else // was not a quoted string
AddNoJsonEscape(pointer(QuotedString), length(QuotedString));
end;
procedure TJsonWriter.AddVariant(const Value: variant; Escape: TTextWriterKind;
WriteOptions: TTextWriterWriteObjectOptions);
var
ctxt: TJsonSaveContext;
cv: TSynInvokeableVariantType;
v: PVarData;
vt: cardinal;
save: TRttiJsonSave;
begin
v := @Value;
repeat
vt := v^.VType;
if vt <> varVariantByRef then
break;
v := v^.VPointer;
until false;
if vt <= high(VARIANT_JSONSAVE) then
begin
ctxt.W := self;
ctxt.Options := WriteOptions; // other fields are just ignored
save := VARIANT_JSONSAVE[vt];
if Assigned(save) then
begin
save(@v^.VAny, ctxt);
exit;
end;
end;
if vt = varString then
AddText(RawByteString(v^.VString), Escape)
else
case vt of
varOleStr {$ifdef HASVARUSTRING}, varUString{$endif}:
AddTextW(v^.VAny, Escape);
varAny:
// rkEnumeration,rkSet,rkDynArray,rkClass,rkInterface,rkRecord,rkObject
// from TRttiCustomProp.GetValueDirect/GetValueGetter
AddRttiVarData(PRttiVarData(v)^, Escape, WriteOptions);
varVariantByRef:
AddVariant(PVariant(v^.VPointer)^, Escape, WriteOptions);
varStringByRef:
AddText(PRawByteString(v^.VAny)^, Escape);
{$ifdef HASVARUSTRING} varUStringByRef, {$endif}
varOleStrByRef:
AddTextW(PPointer(v^.VAny)^, Escape)
else
begin
cv := FindSynVariantType(vt); // our custom types
if cv <> nil then
cv.ToJson(self, v)
else if not CustomVariantToJson(self, v, Escape) then // other custom
raise EJsonException.CreateUtf8('%.AddVariant VType=%', [self, vt]);
end;
end;
end;
procedure TJsonWriter.AddTypedJson(Value, TypeInfo: pointer;
WriteOptions: TTextWriterWriteObjectOptions);
var
ctxt: TJsonSaveContext;
save: TRttiJsonSave;
begin
{%H-}ctxt.Init(self, WriteOptions, Rtti.RegisterType(TypeInfo));
if ctxt.Info <> nil then
begin
save := ctxt.Info.JsonSave;
if Assigned(save) then
save(Value, ctxt)
else
BinarySaveBase64(Value, TypeInfo, rkRecordTypes, {withMagic=}true);
end
else
AddNull; // paranoid check
end;
procedure TJsonWriter.WriteObject(Value: TObject;
WriteOptions: TTextWriterWriteObjectOptions);
var
ctxt: TJsonSaveContext;
save: TRttiJsonSave;
begin
if Value <> nil then
begin
// Rtti.RegisterClass() may create fake RTTI if {$M+} was not used
{%H-}ctxt.Init(self, WriteOptions, Rtti.RegisterClass(PClass(Value)^));
save := ctxt.Info.JsonSave;
if Assigned(save) then
begin
save(@Value, ctxt);
exit;
end;
end;
AddNull;
end;
procedure TJsonWriter.AddRttiCustomJson(Value: pointer; RttiCustom: TObject;
Escape: TTextWriterKind; WriteOptions: TTextWriterWriteObjectOptions);
var
ctxt: TJsonSaveContext;
save: TRttiJsonSave;
begin
{%H-}ctxt.Init(self, WriteOptions, TRttiCustom(RttiCustom));
save := ctxt.Info.JsonSave;
if Assigned(save) then
save(Value, ctxt)
else
BinarySaveBase64(Value, ctxt.Info.Info, rkAllTypes,
{magic=}Escape <> twOnSameLine);
end;
procedure TJsonWriter.AddRttiVarData(const Value: TRttiVarData;
Escape: TTextWriterKind; WriteOptions: TTextWriterWriteObjectOptions);
var
V64: Int64;
begin
if Value.PropValueIsInstance then
begin
// from TRttiCustomProp.GetValueGetter
if rcfGetOrdProp in Value.Prop.Value.Cache.Flags then
begin
// rkEnumeration,rkSet,rkDynArray,rkClass,rkInterface
V64 := Value.Prop.Prop.GetOrdProp(Value.PropValue);
AddRttiCustomJson(@V64, Value.Prop.Value, Escape, WriteOptions);
end
else
// rkRecord,rkObject have no getter methods
raise EJsonException.CreateUtf8('%.AddRttiVarData: unsupported % (%)',
[self, Value.Prop.Value.Name, ToText(Value.Prop.Value.Kind)^]);
end
else
// from TRttiCustomProp.GetValueDirect
AddRttiCustomJson(Value.PropValue, Value.Prop.Value, Escape, WriteOptions);
end;
procedure TJsonWriter.AddText(const Text: RawByteString; Escape: TTextWriterKind);
begin
if Escape = twJsonEscape then
Add('"');
{$ifdef HASCODEPAGE}
AddAnyAnsiString(Text, Escape);
{$else}
Add(pointer(Text), length(Text), Escape);
{$endif HASCODEPAGE}
if Escape <> twJsonEscape then
exit;
B[1] := '"';
inc(B);
end;
procedure TJsonWriter.AddTextW(P: PWord; Escape: TTextWriterKind);
begin
if Escape = twJsonEscape then
Add('"');
AddW(P, 0, Escape);
if Escape <> twJsonEscape then
exit;
B[1] := '"';
inc(B);
end;
function TJsonWriter.AddJsonReformat(Json: PUtf8Char; Format: TTextWriterJsonFormat;
EndOfObject: PUtf8Char): PUtf8Char;
var
objEnd: AnsiChar;
Name, Value: PUtf8Char;
NameLen: integer;
ValueLen: PtrInt;
tab: PJsonCharSet;
begin
result := nil;
if Json = nil then
exit;
while (Json^ <= ' ') and
(Json^ <> #0) do
inc(Json);
case Json^ of
'[':
begin
// array
repeat
inc(Json)
until (Json^ = #0) or
(Json^ > ' ');
if Json^ = ']' then
begin
Add('[');
inc(Json);
end
else
begin
if Format in [jsonHumanReadable, jsonUnquotedPropName] then
AddCRAndIndent;
inc(fHumanReadableLevel);
Add('[');
repeat
if Json = nil then
exit;
if Format in [jsonHumanReadable, jsonUnquotedPropName] then
AddCRAndIndent;
Json := AddJsonReformat(Json, Format, @objEnd);
if objEnd = ']' then
break;
AddDirect(objEnd);
until false;
dec(fHumanReadableLevel);
if Format in [jsonHumanReadable, jsonUnquotedPropName] then
AddCRAndIndent;
end;
AddDirect(']');
end;
'{':
begin
// object
repeat
inc(Json)
until (Json^ = #0) or
(Json^ > ' ');
Add('{');
inc(fHumanReadableLevel);
if Format in [jsonHumanReadable, jsonUnquotedPropName] then
AddCRAndIndent;
if Json^ = '}' then
repeat
inc(Json)
until (Json^ = #0) or
(Json^ > ' ')
else
repeat
// processs property name
Name := GetJsonPropName(Json, @NameLen, {nounescape=}true);
if Name = nil then
exit;
if (Format in [jsonUnquotedPropName, jsonUnquotedPropNameCompact]) and
JsonPropNameValid(Name) then
AddNoJsonEscape(Name, NameLen)
else
begin
AddDirect('"');
if Format < jsonEscapeUnicode then
AddNoJsonEscape(Name, NameLen)
else if Format = jsonNoEscapeUnicode then
AddNoJsonEscapeForcedNoUnicode(Name, NameLen)
else
AddNoJsonEscapeForcedUnicode(Name, NameLen);
AddDirect('"');
end;
if Format in [jsonHumanReadable, jsonUnquotedPropName] then
AddDirect(':', ' ')
else
AddDirect(':');
// recurcisvely process value
while (Json^ <= ' ') and
(Json^ <> #0) do
inc(Json);
Json := AddJsonReformat(Json, Format, @objEnd);
if objEnd = '}' then
break;
Add(objEnd);
if Format in [jsonHumanReadable, jsonUnquotedPropName] then
AddCRAndIndent;
until false;
dec(fHumanReadableLevel);
if Format in [jsonHumanReadable, jsonUnquotedPropName] then
AddCRAndIndent;
AddDirect('}');
end;
'"':
begin
// string
Value := Json;
Json := GotoEndOfJsonString2(Json + 1, @JSON_CHARS);
if Json^ <> '"' then
exit;
inc(Json);
if Format < jsonEscapeUnicode then
AddNoJsonEscape(Value, Json - Value)
else if Format = jsonNoEscapeUnicode then
AddNoJsonEscapeForcedNoUnicode(Value, Json - Value)
else
AddNoJsonEscapeForcedUnicode(Value, Json - Value);
end;
else
begin
// numeric value or true/false/null constant or MongoDB extended
tab := @JSON_CHARS;
if jcEndOfJsonFieldOr0 in tab[Json^] then
exit; // #0 , ] } :
Value := Json;
ValueLen := 0;
repeat
inc(ValueLen);
until jcEndOfJsonFieldOr0 in tab[Json[ValueLen]];
inc(Json, ValueLen);
while (ValueLen > 0) and
(Value[ValueLen - 1] <= ' ') do
dec(ValueLen);
AddShort(Value, ValueLen);
end;
end;
if Json = nil then
exit;
while (Json^ <= ' ') and
(Json^ <> #0) do
inc(Json);
if EndOfObject <> nil then
EndOfObject^ := Json^;
if Json^ <> #0 then
repeat
inc(Json)
until (Json^ = #0) or
(Json^ > ' ');
result := Json;
end;
function TJsonWriter.AddJsonToXML(Json: PUtf8Char;
ArrayName, EndOfObject: PUtf8Char): PUtf8Char;
var
info: TGetJsonField;
Name: PUtf8Char;
n, c: integer;
begin
result := nil;
if Json = nil then
exit;
while (Json^ <= ' ') and
(Json^ <> #0) do
inc(Json);
if Json^ = '/' then
Json := TryGotoEndOfComment(Json);
case Json^ of
'[':
begin
repeat
inc(Json);
until (Json^ = #0) or
(Json^ > ' ');
if Json^ = ']' then
Json := GotoNextNotSpace(Json + 1)
else
begin
n := 0;
repeat
if Json = nil then
exit;
Add('<');
if ArrayName = nil then
Add(n)
else
AddXmlEscape(ArrayName);
Add('>');
Json := AddJsonToXML(Json, nil, @info.EndOfObject);
Add('<', '/');
if ArrayName = nil then
Add(n)
else
AddXmlEscape(ArrayName);
Add('>');
inc(n);
until info.EndOfObject = ']';
end;
end;
'{':
begin
repeat
inc(Json);
until (Json^ = #0) or
(Json^ > ' ');
if Json^ = '}' then
Json := GotoNextNotSpace(Json + 1)
else
begin
repeat
Name := GetJsonPropName(Json);
if Name = nil then
exit;
while (Json^ <= ' ') and
(Json^ <> #0) do
inc(Json);
if Json^ = '[' then // arrays are written as list of items, without root
Json := AddJsonToXML(Json, Name, @info.EndOfObject)
else
begin
Add('<');
AddXmlEscape(Name);
Add('>');
Json := AddJsonToXML(Json, Name, @info.EndOfObject);
Add('<', '/');
AddXmlEscape(Name);
Add('>');
end;
until info.EndOfObject = '}';
end;
end;
else
begin // unescape the JSON content and write as UTF-8 escaped XML
info.Json := Json;
info.GetJsonField;
if info.Value = nil then
AddNull
else
begin
c := PInteger(info.Value)^ and $ffffff;
if (c = JSON_BASE64_MAGIC_C) or
(c = JSON_SQLDATE_MAGIC_C) then
inc(info.Value, 3); // ignore the Magic codepoint encoded as UTF-8
AddXmlEscape(info.Value);
end;
if EndOfObject <> nil then
EndOfObject^ := info.EndOfObject;
result := info.Json;
exit;
end;
end;
if Json <> nil then
begin
while (Json^ <= ' ') and
(Json^ <> #0) do
inc(Json);
if EndOfObject <> nil then
EndOfObject^ := Json^;
if Json^ <> #0 then
repeat
inc(Json);
until (Json^ = #0) or
(Json^ > ' ');
end;
result := Json;
end;
procedure TJsonWriter.AddJsonEscape(P: Pointer; Len: PtrInt);
var
i, start: PtrInt;
{$ifdef CPUX86NOTPIC}
tab: TNormTableByte absolute JSON_ESCAPE;
{$else}
tab: PByteArray;
{$endif CPUX86NOTPIC}
label
noesc;
begin
if P = nil then
exit;
if Len = 0 then
dec(Len); // -1 = no end = AddJsonEscape(P, 0)
i := 0;
{$ifndef CPUX86NOTPIC}
tab := @JSON_ESCAPE;
{$endif CPUX86NOTPIC}
if tab[PByteArray(P)[i]] = JSON_ESCAPE_NONE then
begin
noesc:
start := i;
if Len < 0 then // fastest loop is with AddJsonEscape(P, 0)
repeat
inc(i);
until tab[PByteArray(P)[i]] <> JSON_ESCAPE_NONE
else
repeat
inc(i);
until (i >= Len) or
(tab[PByteArray(P)[i]] <> JSON_ESCAPE_NONE);
inc(PByte(P), start);
dec(i, start);
if Len >= 0 then
dec(Len, start);
if BEnd - B <= i then
AddNoJsonEscapeBig(P, i)
else
begin
MoveFast(P^, B[1], i);
inc(B, i);
end;
if (Len >= 0) and
(i >= Len) then
exit;
end;
repeat
if B >= BEnd then
FlushToStream;
case tab[PByteArray(P)[i]] of // better codegen with no temp var
JSON_ESCAPE_NONE:
goto noesc;
JSON_ESCAPE_ENDINGZERO:
// #0
exit;
JSON_ESCAPE_UNICODEHEX:
begin
// characters below ' ', #7 e.g. -> // 'u0007'
PCardinal(B + 1)^ :=
ord('\') + ord('u') shl 8 + ord('0') shl 16 + ord('0') shl 24;
inc(B, 4);
PWord(B + 1)^ := TwoDigitsHexWB[PByteArray(P)[i]];
end;
else
// escaped as \ + b,t,n,f,r,\,"
PWord(B + 1)^ := (integer(tab[PByteArray(P)[i]]) shl 8) or ord('\');
end;
inc(i);
inc(B, 2);
until (Len >= 0) and
(i >= Len);
end;
procedure TJsonWriter.AddJsonEscapeString(const s: string);
begin
if s <> '' then
{$ifdef UNICODE}
AddJsonEscapeW(pointer(s), Length(s));
{$else}
AddAnyAnsiString(s, twJsonEscape, 0);
{$endif UNICODE}
end;
procedure TJsonWriter.AddJsonEscapeAnsiString(const s: AnsiString);
begin
AddAnyAnsiString(s, twJsonEscape, 0);
end;
procedure TJsonWriter.AddJsonEscapeW(P: PWord; Len: PtrInt);
var
i, c, s: PtrInt;
esc: byte;
tab: PByteArray;
begin
if P = nil then
exit;
if Len = 0 then
Len := MaxInt;
i := 0;
while i < Len do
begin
s := i;
tab := @JSON_ESCAPE;
repeat
c := PWordArray(P)[i];
if (c <= 127) and
(tab[c] <> JSON_ESCAPE_NONE) then
break;
inc(i);
until i >= Len;
if i <> s then
AddNoJsonEscapeW(@PWordArray(P)[s], i - s);
if i >= Len then
exit;
c := PWordArray(P)[i];
if c = 0 then
exit;
esc := tab[c];
if esc = JSON_ESCAPE_ENDINGZERO then // #0
exit
else if esc = JSON_ESCAPE_UNICODEHEX then
begin
// characters below ' ', #7 e.g. -> \u0007
AddShorter('\u00');
AddByteToHex(c);
end
else
Add('\', AnsiChar(esc)); // escaped as \ + b,t,n,f,r,\,"
inc(i);
end;
end;
procedure TJsonWriter.AddJsonEscape(const V: TVarRec);
begin
with V do
case VType of
vtPointer:
AddNull;
vtString,
vtAnsiString,
{$ifdef HASVARUSTRING}vtUnicodeString, {$endif}
vtPChar,
vtChar,
vtWideChar,
vtWideString,
vtClass:
begin
Add('"');
case VType of
vtString:
if (VString <> nil) and
(VString^[0] <> #0) then
AddJsonEscape(@VString^[1], ord(VString^[0]));
vtAnsiString:
AddJsonEscape(VAnsiString);
{$ifdef HASVARUSTRING}
vtUnicodeString:
AddJsonEscapeW(pointer(UnicodeString(VUnicodeString)),
length(UnicodeString(VUnicodeString)));
{$endif HASVARUSTRING}
vtPChar:
AddJsonEscape(VPChar);
vtChar:
AddJsonEscape(@VChar, 1);
vtWideChar:
AddJsonEscapeW(@VWideChar, 1);
vtWideString:
AddJsonEscapeW(VWideString);
vtClass:
AddClassName(VClass);
end;
AddDirect('"');
end;
vtBoolean:
Add(VBoolean); // 'true'/'false'
vtInteger:
Add(VInteger);
vtInt64:
Add(VInt64^);
{$ifdef FPC}
vtQWord:
AddQ(V.VQWord^);
{$endif FPC}
vtExtended:
AddDouble(VExtended^);
vtCurrency:
AddCurr64(VInt64);
vtObject:
WriteObject(VObject);
vtVariant:
AddVariant(VVariant^, twJsonEscape);
end;
end;
procedure TJsonWriter.AddJsonEscape(Source: TJsonWriter);
begin
if Source.fTotalFileSize = 0 then
AddJsonEscape(Source.fTempBuf, Source.B - Source.fTempBuf + 1)
else
AddJsonEscape(Pointer(Source.Text));
end;
procedure TJsonWriter.AddNoJsonEscape(Source: TJsonWriter);
begin
if Source.fTotalFileSize = 0 then
AddNoJsonEscape(Source.fTempBuf, Source.B - Source.fTempBuf + 1)
else
AddNoJsonEscapeUtf8(Source.Text);
end;
procedure TJsonWriter.AddNoJsonEscapeForcedUnicode(P: PUtf8Char; Len: PtrInt);
var
S, P2: PUtf8Char;
c: cardinal;
tab: PByteToWord;
label
nxt;
begin
if Len > 0 then
repeat
// handle 7-bit ASCII chars, by quad if possible
S := P;
if Len >= 4 then
repeat
if PCardinal(S)^ and $80808080 <> 0 then
break; // break on first non ASCII quad
inc(S, 4);
dec(Len, 4);
until Len < 4;
if (Len > 0) and
(S^ <= #127) then // some 1..3 trailing ASCII chars
repeat
inc(S);
dec(Len);
until (Len = 0) or
(S^ > #127);
P2 := P;
P := S;
dec(S, PtrUInt(P2));
if S <> nil then
AddNoJsonEscape(P2, PtrUInt(S));
nxt:if Len = 0 then
exit;
// some characters needs UTF-16 \u#### Unicode encoding
if B >= BEnd then
FlushToStream;
P2 := P;
c := UTF8_TABLE.GetHighUtf8Ucs4(P);
dec(Len, P - P2);
if (Len < 0) or
(c = 0) then
break;
tab := @TwoDigitsHexWBLower;
if c <= $ffff then
Utf16ToJsonUnicodeEscape(B, c, tab)
else
begin
dec(c, $10000); // store as UTF-16 surrogates
Utf16ToJsonUnicodeEscape(B, (c shr 10) or UTF16_HISURROGATE_MIN, tab);
Utf16ToJsonUnicodeEscape(B, (c and $3FF) or UTF16_LOSURROGATE_MIN, tab);
end;
if P^ > #127 then
goto nxt;
until false;
end;
procedure TJsonWriter.AddNoJsonEscapeForcedNoUnicode(P: PUtf8Char; Len: PtrInt);
var
P2: PUtf8Char;
begin
if Len > 0 then
repeat
P2 := P;
repeat
if P^ <> '\' then // quickly search for \### escape marker
begin
inc(P);
dec(Len);
if Len = 0 then
break;
continue;
end;
if P[1] = 'u' then // found a \u#### pattern
break;
inc(P, 2); // ignore this \# two-chars escape block
dec(Len, 2);
if Len = 0 then
break;
if Len < 0 then
exit;
until false;
if P <> P2 then
AddNoJsonEscape(P2, P - P2);
if Len <= 0 then
exit;
// some characters needs UTF-16 \u#### Unicode decoding
if B >= BEnd then
FlushToStream;
P2 := P;
inc(P); // P^ should point at 'u1234' just after \u1234
inc(B);
P := JsonUnicodeEscapeToUtf8(B, P); // decode up to two UTF-16 surrogates
dec(B);
dec(Len, P - P2);
until Len <= 0;
end;
procedure TJsonWriter.AddJsonString(const Text: RawUtf8);
begin
Add('"');
AddJsonEscape(pointer(Text));
B[1] := '"';
inc(B);
end;
procedure TJsonWriter.Add(const V: TVarRec; Escape: TTextWriterKind;
WriteObjectOptions: TTextWriterWriteObjectOptions);
begin
with V do
case VType of
vtInteger:
Add(VInteger);
vtBoolean:
if VBoolean then // normalize
Add('1')
else
Add('0');
vtChar:
Add(@VChar, 1, Escape);
vtExtended:
AddDouble(VExtended^);
vtCurrency:
AddCurr64(VInt64);
vtInt64:
Add(VInt64^);
{$ifdef FPC}
vtQWord:
AddQ(VQWord^);
{$endif FPC}
vtVariant:
AddVariant(VVariant^, Escape);
vtString:
if (VString <> nil) and
(VString^[0] <> #0) then
Add(@VString^[1], ord(VString^[0]), Escape);
vtInterface,
vtPointer:
AddPointer(PtrUInt(VPointer));
vtPChar:
Add(PUtf8Char(VPChar), Escape);
vtObject:
WriteObject(VObject, WriteObjectOptions);
vtClass:
AddClassName(VClass);
vtWideChar:
AddW(@VWideChar, 1, Escape);
vtPWideChar:
AddW(pointer(VPWideChar), StrLenW(VPWideChar), Escape);
vtAnsiString:
if VAnsiString <> nil then // expect RawUtf8
Add(VAnsiString, length(RawUtf8(VAnsiString)), Escape);
vtWideString:
if VWideString <> nil then
AddW(VWideString, length(WideString(VWideString)), Escape);
{$ifdef HASVARUSTRING}
vtUnicodeString:
if VUnicodeString <> nil then // convert to UTF-8
AddW(VUnicodeString, length(UnicodeString(VUnicodeString)), Escape);
{$endif HASVARUSTRING}
end;
end;
procedure TJsonWriter.AddJson(const Format: RawUtf8; const Args, Params: array of const);
var
temp: variant;
begin
_JsonFmt(Format, Args, Params, JSON_FAST, temp);
AddVariant(temp, twJsonEscape);
end;
procedure TJsonWriter.AddJsonArraysAsJsonObject(keys, values: PUtf8Char);
var
k, v: PUtf8Char;
parser: TJsonGotoEndParser;
begin
if (keys = nil) or
(keys[0] <> '[') or
(values = nil) or
(values[0] <> '[') or
(keys[1] = ']') or
(values[1] = ']') then
begin
AddNull;
exit;
end;
inc(keys); // jump initial [
inc(values);
Add('{');
{%H-}parser.Init({strict=}false, nil);
repeat
k := parser.GotoEnd(keys);
v := parser.GotoEnd(values);
if (k = nil) or
(v = nil) then
break; // invalid JSON input
AddNoJsonEscape(keys, k - keys);
AddDirect(':');
AddNoJsonEscape(values, v - values);
AddComma;
if (k^ <> ',') or
(v^ <> ',') then
break; // reached the end of the input JSON arrays
keys := k + 1;
values := v + 1;
until false;
CancelLastComma('}');
end;
procedure TJsonWriter.AddJsonEscape(const NameValuePairs: array of const);
var
a: integer;
procedure WriteValue;
begin
case VarRecAsChar(NameValuePairs[a]) of
ord('['):
begin
Add('[');
while a < high(NameValuePairs) do
begin
inc(a);
if VarRecAsChar(NameValuePairs[a]) = ord(']') then
break;
WriteValue;
end;
CancelLastComma(']');
end;
ord('{'):
begin
Add('{');
while a < high(NameValuePairs) do
begin
inc(a);
if VarRecAsChar(NameValuePairs[a]) = ord('}') then
break;
AddJsonEscape(NameValuePairs[a]);
Add(':');
inc(a);
WriteValue;
end;
CancelLastComma('}');
end
else
AddJsonEscape(NameValuePairs[a]);
end;
AddComma;
end;
begin
Add('{');
a := 0;
while a < high(NameValuePairs) do
begin
AddJsonEscape(NameValuePairs[a]);
inc(a);
AddDirect(':');
WriteValue;
inc(a);
end;
CancelLastComma('}');
end;
function TJsonWriter.AddRecordJson(Value: pointer; RecordInfo: PRttiInfo;
WriteOptions: TTextWriterWriteObjectOptions): PtrInt;
var
ctxt: TJsonSaveContext;
begin
{%H-}ctxt.Init(self, WriteOptions, Rtti.RegisterType(RecordInfo));
if rcfHasNestedProperties in ctxt.Info.Flags then
// we know the fields from text definition
TRttiJsonSave(ctxt.Info.JsonSave)(Value, ctxt)
else
// fallback to binary serialization, trailing crc32c and Base64 encoding
BinarySaveBase64(Value, RecordInfo, rkRecordTypes, {magic=}true);
result := ctxt.Info.Size;
end;
procedure TJsonWriter.AddVoidRecordJson(RecordInfo: PRttiInfo;
WriteOptions: TTextWriterWriteObjectOptions);
var
tmp: TSynTempBuffer;
begin
tmp.InitZero(RecordInfo.RecordSize);
AddRecordJson(tmp.buf, RecordInfo, WriteOptions);
tmp.Done;
end;
procedure TJsonWriter.AddDynArrayJson(var DynArray: TDynArray;
WriteOptions: TTextWriterWriteObjectOptions);
var
ctxt: TJsonSaveContext;
len, backup: PtrInt;
hacklen: PDALen;
begin
len := DynArray.Count;
if len = 0 then
Add('[', ']')
else
begin
{%H-}ctxt.Init(self, WriteOptions, DynArray.Info);
hacklen := PDALen(PAnsiChar(DynArray.Value^) - _DALEN);
backup := hacklen^;
hacklen^ := len - _DAOFF; // may use ExternalCount -> ovewrite length(Array)
_JS_DynArray(DynArray.Value, ctxt);
hacklen^ := backup; // restore original length/capacity
end;
end;
procedure TJsonWriter.AddDynArrayJson(var DynArray: TDynArrayHashed;
WriteOptions: TTextWriterWriteObjectOptions);
begin
// needed if UNDIRECTDYNARRAY is defined (Delphi 2009+)
AddDynArrayJson(PDynArray(@DynArray)^, WriteOptions);
end;
function TJsonWriter.AddDynArrayJson(Value: pointer; Info: TRttiCustom;
WriteOptions: TTextWriterWriteObjectOptions): PtrInt;
var
temp: TDynArray;
begin
if Info.Kind <> rkDynArray then
raise EDynArray.CreateUtf8('%.AddDynArrayJson: % is %, expected rkDynArray',
[self, Info.Name, ToText(Info.Kind)^]);
temp.InitRtti(Info, Value^);
AddDynArrayJson(temp, WriteOptions);
result := temp.Info.Cache.ItemSize;
end;
{ ********** Low-Level JSON UnSerialization for all TRttiParserType }
{ TJsonParserContext }
procedure TJsonParserContext.InitParser(P: PUtf8Char; Rtti: TRttiCustom;
O: TJsonParserOptions; CV: PDocVariantOptions; ObjectListItemClass: TClass;
RawUtf8Interning: TRawUtf8Interning);
begin
{$ifdef USERECORDWITHMETHODS}Get.{$endif}Json := P;
Valid := true;
Interning := RawUtf8Interning;
if Rtti <> nil then
O := O + TRttiJson(Rtti).fIncludeReadOptions;
Options := O;
if CV <> nil then
begin
DVO := CV^;
CustomVariant := @DVO;
end
else if jpoHandleCustomVariants in O then
begin
if jpoAllowDouble in o then
DVO := JSON_FAST_FLOAT
else
DVO := JSON_FAST;
CustomVariant := @DVO;
end
else
CustomVariant := nil;
if jpoHandleCustomVariantsWithinString in O then
include(DVO, dvoJsonObjectParseWithinString);
Info := Rtti;
Prop := nil;
if ObjectListItemClass = nil then
ObjectListItem := nil
else
ObjectListItem := mormot.core.rtti.Rtti.RegisterClass(ObjectListItemClass);
end;
{$ifdef USERECORDWITHMETHODS}
function TJsonParserContext.Json: PUtf8Char;
begin
result := Get.Json;
end;
function TJsonParserContext.Value: PUtf8Char;
begin
result := Get.Value;
end;
function TJsonParserContext.ValueLen: PtrInt;
begin
result := Get.ValueLen;
end;
function TJsonParserContext.WasString: boolean;
begin
result := Get.WasString;
end;
function TJsonParserContext.EndOfObject: AnsiChar;
begin
result := Get.EndOfObject;
end;
{$endif USERECORDWITHMETHODS}
function TJsonParserContext.ParseNext: boolean;
begin
{$ifdef USERECORDWITHMETHODS}Get.{$endif}GetJsonField;
result := Json <> nil;
Valid := result;
end;
function TJsonParserContext.ParseNextAny: boolean;
begin
{$ifdef USERECORDWITHMETHODS}Get.{$endif}GetJsonFieldOrObjectOrArray;
result := Json <> nil;
Valid := result;
end;
function TJsonParserContext.ParseUtf8: RawUtf8;
begin
{$ifdef USERECORDWITHMETHODS}Get.{$endif}GetJsonField;
Valid := Json <> nil;
Interning.Unique(result, Value, ValueLen)
end;
function TJsonParserContext.ParseString: string;
begin
{$ifdef USERECORDWITHMETHODS}Get.{$endif}GetJsonField;
Valid := Json <> nil;
Utf8DecodeToString(Value, ValueLen, result);
end;
function TJsonParserContext.ParseInteger: Int64;
begin
if ParseNext then
SetInt64(Value, result{%H-})
else
result := 0;
end;
procedure TJsonParserContext.ParseEndOfObject;
var
P: PUtf8Char;
begin
if Valid then
begin
P := Json;
if P^ <> #0 then
P := mormot.core.json.ParseEndOfObject(
P, {$ifdef USERECORDWITHMETHODS}Get.{$endif}EndOfObject);
{$ifdef USERECORDWITHMETHODS}Get.{$endif}Json := P;
Valid := P <> nil;
end;
end;
function TJsonParserContext.ParseNull: boolean;
var
P: PUtf8Char;
begin
result := false;
if Valid then
if Json <> nil then
begin
P := GotoNextNotSpace(Json);
{$ifdef USERECORDWITHMETHODS}Get.{$endif}Json := P;
if PCardinal(P)^ = NULL_LOW then
begin
P := mormot.core.json.ParseEndOfObject(
P + 4, {$ifdef USERECORDWITHMETHODS}Get.{$endif}EndOfObject);
if P <> nil then
begin
{$ifdef USERECORDWITHMETHODS}Get.{$endif}Json := P;
result := true;
end
else
Valid := false;
end;
end
else
result := true; // nil -> null
end;
function TJsonParserContext.ParseArray: boolean;
var
P: PUtf8Char;
begin
result := false; // no need to parse
P := GotoNextNotSpace(Json);
{$ifdef USERECORDWITHMETHODS}Get.{$endif}Json := P;
if P^ = '[' then
begin
P := GotoNextNotSpace(P + 1); // ignore trailing [
if P^ = ']' then
begin
// void but valid array
P := mormot.core.json.ParseEndOfObject(
P + 1, {$ifdef USERECORDWITHMETHODS}Get.{$endif}EndOfObject);
Valid := P <> nil;
{$ifdef USERECORDWITHMETHODS}Get.{$endif}Json := P;
end
else
begin
// we have a non void [...] array -> caller should parse it
result := true;
{$ifdef USERECORDWITHMETHODS}Get.{$endif}Json := P;
end;
end
else
Valid := ParseNull; // only not [...] value allowed is null
end;
function TJsonParserContext.ParseObject: boolean;
var
P: PUtf8Char;
begin
result := false; // no need to parse
P := GotoNextNotSpace(Json);
{$ifdef USERECORDWITHMETHODS}Get.{$endif}Json := P;
if P^ = '{' then
begin
P := GotoNextNotSpace(P + 1); // ignore trailing {
if P^ = '}' then
begin
// void but valid array
P := mormot.core.json.ParseEndOfObject(
P + 1, {$ifdef USERECORDWITHMETHODS}Get.{$endif}EndOfObject);
Valid := P <> nil;
{$ifdef USERECORDWITHMETHODS}Get.{$endif}Json := P;
end
else
begin
// we have a non void {...} array -> caller should parse it
result := true;
{$ifdef USERECORDWITHMETHODS}Get.{$endif}Json := P;
end;
end
else
Valid := ParseNull; // only not {...} value allowed is null
end;
function TJsonParserContext.ParseNewObject: TObject;
begin
if ObjectListItem = nil then
begin
Info := JsonRetrieveObjectRttiCustom({$ifdef USERECORDWITHMETHODS}Get.{$endif}Json,
jpoObjectListClassNameGlobalFindClass in Options);
if (Info <> nil) and
(Json^ = ',') then
Json^ := '{' // to parse other properties as a regular Json object
else
begin
Valid := false;
result := nil;
exit;
end;
end;
result := TRttiJson(Info).ParseNewInstance(self);
end;
function TJsonParserContext.ParseObject(const Names: array of RawUtf8;
Values: PValuePUtf8CharArray; HandleValuesAsObjectOrArray: boolean): boolean;
begin
{$ifdef USERECORDWITHMETHODS}Get.{$endif}Json := JsonDecode(
Json, Names, Values, HandleValuesAsObjectOrArray);
if Json = nil then
Valid := false
else
ParseEndOfObject;
result := Valid;
end;
procedure _JL_Boolean(Data: PBoolean; var Ctxt: TJsonParserContext);
begin
if Ctxt.ParseNext then
Data^ := GetBoolean(Ctxt.Value);
end;
procedure _JL_Byte(Data: PByte; var Ctxt: TJsonParserContext);
begin
if Ctxt.ParseNext then
Data^ := GetCardinal(Ctxt.Value);
end;
procedure _JL_Cardinal(Data: PCardinal; var Ctxt: TJsonParserContext);
begin
if Ctxt.ParseNext then
Data^ := GetCardinal(Ctxt.Value);
end;
procedure _JL_Integer(Data: PInteger; var Ctxt: TJsonParserContext);
begin
if Ctxt.ParseNext then
Data^ := GetInteger(Ctxt.Value);
end;
procedure _JL_Currency(Data: PInt64; var Ctxt: TJsonParserContext);
begin
if Ctxt.ParseNext then
Data^ := StrToCurr64(Ctxt.Value);
end;
procedure _JL_Double(Data: PDouble; var Ctxt: TJsonParserContext);
var
err: integer;
begin
if Ctxt.ParseNext then
begin
unaligned(Data^) := GetExtended(Ctxt.Value, err);
Ctxt.Valid := (Ctxt.Value = nil) or (err = 0);
end;
end;
procedure _JL_Extended(Data: PSynExtended; var Ctxt: TJsonParserContext);
var
err: integer;
begin
if Ctxt.ParseNext then
begin
Data^ := GetExtended(Ctxt.Value, err);
Ctxt.Valid := (Ctxt.Value = nil) or (err = 0);
end;
end;
procedure _JL_Int64(Data: PInt64; var Ctxt: TJsonParserContext);
begin
if Ctxt.ParseNext then
if Ctxt.WasString and
(Ctxt.ValueLen = SizeOf(Data^) * 2) then
Ctxt.Valid := (jpoAllowInt64Hex in Ctxt.Options) and
HexDisplayToBin(PAnsiChar(Ctxt.Value), pointer(Data), SizeOf(Data^))
else
SetInt64(Ctxt.Value, Data^);
end;
procedure _JL_QWord(Data: PQWord; var Ctxt: TJsonParserContext);
begin
if Ctxt.ParseNext then
if Ctxt.WasString and
(Ctxt.ValueLen = SizeOf(Data^) * 2) then
Ctxt.Valid := (jpoAllowInt64Hex in Ctxt.Options) and
HexDisplayToBin(PAnsiChar(Ctxt.Value), pointer(Data), SizeOf(Data^))
else
SetQWord(Ctxt.Value, Data^);
end;
procedure _JL_RawByteString(Data: PRawByteString; var Ctxt: TJsonParserContext);
begin
if Ctxt.ParseNext then
if Ctxt.Value = nil then // null
Data^ := ''
else if not Ctxt.WasString then
Ctxt.Valid := false
else if Base64MagicTryAndDecode(Ctxt.Value, Ctxt.ValueLen, Data^) then
exit // base64-encoded, with magic or not
else
FastSetRawByteString(Data^, Ctxt.Value, Ctxt.ValueLen); // fallback as text
end;
procedure _JL_RawJson(Data: PRawJson; var Ctxt: TJsonParserContext);
begin
GetJsonItemAsRawJson(Ctxt.{$ifdef USERECORDWITHMETHODS}Get.{$endif}Json,
Data^, @Ctxt.{$ifdef USERECORDWITHMETHODS}Get.{$endif}EndOfObject);
Ctxt.Valid := Ctxt.Json <> nil;
end;
procedure _JL_RawUtf8(Data: PRawByteString; var Ctxt: TJsonParserContext);
begin
if Ctxt.ParseNext then
// will handle RawUtf8 but also AnsiString, WinAnsiString or other CP
if Ctxt.Info.Cache.CodePage = CP_UTF8 then
Ctxt.Interning.Unique(RawUtf8(Data^), Ctxt.Value, Ctxt.ValueLen)
else if Ctxt.Info.Cache.CodePage >= CP_RAWBLOB then
Ctxt.Valid := false // paranoid check (RawByteString should handle it)
else
Ctxt.Info.Cache.Engine.Utf8BufferToAnsi(Ctxt.Value, Ctxt.ValueLen, Data^);
end;
procedure _JL_Single(Data: PSingle; var Ctxt: TJsonParserContext);
begin
if Ctxt.ParseNext then
Data^ := GetExtended(Ctxt.Value);
end;
procedure _JL_String(Data: PString; var Ctxt: TJsonParserContext);
begin
if Ctxt.ParseNext then
Utf8DecodeToString(Ctxt.Value, Ctxt.ValueLen, Data^);
end;
procedure _JL_SynUnicode(Data: PSynUnicode; var Ctxt: TJsonParserContext);
begin
if Ctxt.ParseNext then
Utf8ToSynUnicode(Ctxt.Value, Ctxt.ValueLen, Data^);
end;
procedure _JL_Char(Data: PByte; var Ctxt: TJsonParserContext);
begin
if Ctxt.ParseNext then
if Ctxt.WasString then
if Ctxt.ValueLen <> 0 then
Data^ := ord(Ctxt.Value[0]) // get the first char of the input string
else
Data^ := 0 // _JS_Char serializes #0 as ""
else
Data^ := GetCardinal(Ctxt.Value); // allow serialization as integer
end;
procedure _JL_WideChar(Data: PWord; var Ctxt: TJsonParserContext);
begin
if Ctxt.ParseNext then
if Ctxt.WasString then
Data^ := GetUtf8WideChar(Ctxt.Value)
else
Data^ := GetCardinal(Ctxt.Value);
end;
procedure _JL_DateTime(Data: PDateTime; var Ctxt: TJsonParserContext);
begin
if Ctxt.ParseNext then
if Ctxt.WasString then
Iso8601ToDateTimePUtf8CharVar(Ctxt.Value, Ctxt.ValueLen, Data^)
else
Data^ := GetExtended(Ctxt.Value); // was propbably stored as double
end;
procedure _JL_GUID(Data: PByteArray; var Ctxt: TJsonParserContext);
begin
if Ctxt.ParseNext then
Ctxt.Valid := TextToGuid(Ctxt.Value, Data) <> nil;
end;
procedure _JL_Hash(Data: PByte; var Ctxt: TJsonParserContext);
begin
if Ctxt.ParseNext then
Ctxt.Valid := (Ctxt.ValueLen = Ctxt.Info.Size * 2) and
HexDisplayToBin(PAnsiChar(Ctxt.Value), Data, Ctxt.Info.Size);
end;
procedure _JL_Binary(Data: PByte; var Ctxt: TJsonParserContext);
var
v: QWord;
begin
if Ctxt.ParseNext then
if Ctxt.WasString then
begin
FillZeroSmall(Data, Ctxt.Info.Size); // BinarySize may be < Size
if Ctxt.ValueLen > 0 then // "" -> is valid 0
Ctxt.Valid := (Ctxt.ValueLen = Ctxt.Info.BinarySize * 2) and
HexDisplayToBin(PAnsiChar(Ctxt.Value), Data, Ctxt.Info.BinarySize);
end
else
begin
SetQWord(Ctxt.Value, v{%H-});
MoveFast(v, Data^, Ctxt.Info.Size);
end;
end;
procedure _JL_TimeLog(Data: PQWord; var Ctxt: TJsonParserContext);
begin
if Ctxt.ParseNext then
if Ctxt.WasString then
Data^ := Iso8601ToTimeLogPUtf8Char(Ctxt.Value, Ctxt.ValueLen)
else
SetQWord(Ctxt.Value, Data^);
end;
procedure _JL_UnicodeString(Data: pointer; var Ctxt: TJsonParserContext);
begin
Ctxt.ParseNext;
{$ifdef HASVARUSTRING}
if Ctxt.Valid then
Utf8DecodeToUnicodeString(Ctxt.Value, Ctxt.ValueLen, PUnicodeString(Data)^);
{$endif HASVARUSTRING}
end;
procedure _JL_UnixTime(Data: PQWord; var Ctxt: TJsonParserContext);
begin
if Ctxt.ParseNext then
if Ctxt.WasString then
Data^ := TimeLogToUnixTime(Iso8601ToTimeLogPUtf8Char(
Ctxt.Value, Ctxt.ValueLen))
else
SetQWord(Ctxt.Value, Data^);
end;
procedure _JL_UnixMSTime(Data: PQWord; var Ctxt: TJsonParserContext);
var
dt: TDateTime; // for ms resolution
begin
if Ctxt.ParseNext then
if Ctxt.WasString then
begin
Iso8601ToDateTimePUtf8CharVar(Ctxt.Value, Ctxt.ValueLen, dt);
Data^ := DateTimeToUnixMSTime(dt);
end
else
SetQWord(Ctxt.Value, Data^);
end;
procedure _JL_Variant(Data: PVariant; var Ctxt: TJsonParserContext);
begin
JsonToAnyVariant(Data^, Ctxt{$ifdef USERECORDWITHMETHODS}.Get{$endif},
Ctxt.CustomVariant, jpoAllowDouble in Ctxt.Options);
Ctxt.Valid := Ctxt.Json <> nil;
end;
procedure _JL_WideString(Data: PWideString; var Ctxt: TJsonParserContext);
begin
if Ctxt.ParseNext then
Utf8ToWideString(Ctxt.Value, Ctxt.ValueLen, Data^);
end;
procedure _JL_WinAnsi(Data: PRawByteString; var Ctxt: TJsonParserContext);
begin
if Ctxt.ParseNext then
WinAnsiConvert.Utf8BufferToAnsi(Ctxt.Value, Ctxt.ValueLen, Data^);
end;
procedure _JL_Word(Data: PWord; var Ctxt: TJsonParserContext);
begin
if Ctxt.ParseNext then
Data^ := GetCardinal(Ctxt.Value);
end;
procedure _JL_Enumeration(Data: pointer; var Ctxt: TJsonParserContext);
var
v: PtrInt;
err: integer;
begin
if Ctxt.ParseNext then
begin
if Ctxt.WasString then
v := Ctxt.Info.Cache.EnumInfo.GetEnumNameValue(Ctxt.Value, Ctxt.ValueLen)
else
begin
v := GetInteger(Ctxt.Value, err);
if (err <> 0) or
(PtrUInt(v) > Ctxt.Info.Cache.EnumMax) or
(PtrUInt(v) < Ctxt.Info.Cache.EnumMin) then
v := -1;
end;
if v < 0 then
if jpoIgnoreUnknownEnum in Ctxt.Options then
v := 0
else
Ctxt.Valid := false;
MoveFast(v, Data^, Ctxt.Info.Size);
end;
end;
procedure _JL_Set(Data: pointer; var Ctxt: TJsonParserContext);
var
v: QWord;
begin
with Ctxt.Info.Cache do
v := GetSetNameValue(EnumList, EnumMin, EnumMax,
Ctxt.{$ifdef USERECORDWITHMETHODS}Get.{$endif}Json,
Ctxt.{$ifdef USERECORDWITHMETHODS}Get.{$endif}EndOfObject);
Ctxt.Valid := Ctxt.Json <> nil;
MoveFast(v, Data^, Ctxt.Info.Size);
end;
function JsonLoadProp(Data: PAnsiChar; Prop: PRttiCustomProp;
var Ctxt: TJsonParserContext): boolean; {$ifdef HASINLINE} inline; {$endif}
var
load: TRttiJsonLoad;
begin
Ctxt.Info := Prop^.Value; // caller will restore it afterwards
Ctxt.Prop := Prop;
load := Ctxt.Info.JsonLoad;
if not Assigned(load) then
Ctxt.Valid := false
else if Prop^.OffsetSet >= 0 then
if (rcfHookReadProperty in Ctxt.Info.Flags) and
TCCHook(Data).RttiBeforeReadPropertyValue(@Ctxt, Prop) then
// custom parsing method (e.g. TOrm nested TOrm properties)
else
// default fast parsing into the property/field memory
load(Data + Prop^.OffsetSet, Ctxt)
else
// we need to call a setter
Ctxt.ParsePropComplex(Data);
Ctxt.Prop := nil;
result := Ctxt.Valid;
end;
procedure _JL_RttiCustomProps(Data: PAnsiChar; var Ctxt: TJsonParserContext);
var
j: PUtf8Char;
root: TRttiJson;
prop: PRttiCustomProp;
propname: PUtf8Char;
p, propnamelen: integer;
label
no, nxt, any;
begin
// regular JSON unserialization using nested fields/properties
j := GotoNextNotSpace(Ctxt.Json);
if j^ <> '{' then
begin
no: Ctxt.Valid := false;
exit;
end;
repeat
inc(j);
until not (j^ in [#1..' ']);
if j^ <> '}' then
begin
Ctxt.{$ifdef USERECORDWITHMETHODS}Get.{$endif}Json := j;
root := pointer(Ctxt.Info); // Ctxt.Info overriden in JsonLoadProp()
prop := pointer(root.Props.List);
for p := 1 to root.Props.Count do
begin
nxt: propname := GetJsonPropName(
Ctxt.{$ifdef USERECORDWITHMETHODS}Get.{$endif}Json, @propnamelen);
if (Ctxt.Json = nil) or
(propname = nil) then
goto no;
// O(1) optimistic process of the property name, following RTTI order
if prop^.NameMatch(propname, propnamelen) then
if JsonLoadProp(Data, prop, Ctxt) then
if Ctxt.EndOfObject = '}' then
break
else
inc(prop)
else
break
else if (Ctxt.Info.Kind = rkClass) and
(propnamelen = 9) and // fast "ClassName" case sensitive match
(PIntegerArray(propname)[0] =
ord('C') + ord('l') shl 8 + ord('a') shl 16 + ord('s') shl 24) and
(PIntegerArray(propname)[1] =
ord('s') + ord('N') shl 8 + ord('a') shl 16 + ord('m') shl 24) and
(propname[8] = 'e') then
// woStoreClassName was used -> just ignore the class name
begin
Ctxt.{$ifdef USERECORDWITHMETHODS}Get.{$endif}Json := GotoNextJsonItem(
Ctxt.Json, Ctxt.{$ifdef USERECORDWITHMETHODS}Get.{$endif}EndOfObject);
if Ctxt.Json <> nil then
goto nxt;
goto no;
end
else
begin
// we didn't find the property in its natural place -> full lookup
repeat
prop := FindCustomProp(pointer(root.Props.List),
propname, propnamelen, root.Props.Count);
if prop = nil then
// unexpected "prop": value
if (rcfReadIgnoreUnknownFields in root.Flags) or
(jpoIgnoreUnknownProperty in Ctxt.Options) then
begin
Ctxt.{$ifdef USERECORDWITHMETHODS}Get.{$endif}Json := GotoNextJsonItem(
Ctxt.Json, Ctxt.{$ifdef USERECORDWITHMETHODS}Get.{$endif}EndOfObject);
if Ctxt.Json = nil then
goto no;
end
else
goto no
else if not JsonLoadProp(Data, prop, Ctxt) then
goto no;
if Ctxt.EndOfObject = '}' then
break;
any: propname := GetJsonPropName(
Ctxt.{$ifdef USERECORDWITHMETHODS}Get.{$endif}Json, @propnamelen);
if (Ctxt.Json = nil) or
(propname = nil) then
goto no;
until false;
break;
end;
end;
if Ctxt.Valid and
(Ctxt.EndOfObject = ',') and
((rcfReadIgnoreUnknownFields in root.Flags) or
(jpoIgnoreUnknownProperty in Ctxt.Options)) then
goto any;
Ctxt.Info := root; // restore
end
else // {}
Ctxt.{$ifdef USERECORDWITHMETHODS}Get.{$endif}Json := j + 1;
Ctxt.ParseEndOfObject; // mimics GetJsonField() - set Ctxt.EndOfObject
end;
procedure _JL_RttiCustom(Data: PAnsiChar; var Ctxt: TJsonParserContext);
begin
if Ctxt.Json <> nil then
Ctxt.{$ifdef USERECORDWITHMETHODS}Get.{$endif}Json := GotoNextNotSpace(Ctxt.Json);
if TRttiJson(Ctxt.Info).fJsonReader.Code <> nil then
begin // TRttiJson.RegisterCustomSerializer() - e.g. TOrm.RttiJsonRead
if Ctxt.Info.Kind = rkClass then
begin
if PPointer(Data)^ = nil then // e.g. from _JL_DynArray for T*ObjArray
PPointer(Data)^ := TRttiJson(Ctxt.Info).fNewInstance(Ctxt.Info);
Data := PPointer(Data)^; // as expected by the callback
end;
TOnRttiJsonRead(TRttiJson(Ctxt.Info).fJsonReader)(Ctxt, Data)
end
else
begin
// always finalize and reset the existing values (in case of missing props)
if Ctxt.Info.Kind = rkClass then
begin
if Ctxt.ParseNull then
begin
if not (jpoNullDontReleaseObjectInstance in Ctxt.Options) then
FreeAndNil(PObject(Data)^);
exit;
end;
if PPointer(Data)^ = nil then // e.g. from _JL_DynArray for T*ObjArray
PPointer(Data)^ := TRttiJson(Ctxt.Info).fNewInstance(Ctxt.Info)
else if (jpoClearValues in Ctxt.Options) and
not (rcfClassMayBeID in Ctxt.Info.Flags) then
Ctxt.Info.Props.FinalizeAndClearPublishedProperties(PPointer(Data)^);
// class instances are accessed by reference, records are stored by value
Data := PPointer(Data)^;
if (rcfHookRead in Ctxt.Info.Flags) and
TCCHook(Data).RttiBeforeReadObject(@Ctxt) then
exit;
end
else
begin
if jpoClearValues in Ctxt.Options then
Ctxt.Info.ValueFinalizeAndClear(Data);
if Ctxt.ParseNull then
exit;
end;
// regular JSON unserialization using nested fields/properties
_JL_RttiCustomProps(Data, Ctxt);
if rcfHookRead in Ctxt.Info.Flags then
TCCHook(Data).RttiAfterReadObject;
end;
end;
procedure _JL_RttiObjectWithID(Data: PAnsiChar; var Ctxt: TJsonParserContext);
var
P: PUtf8Char;
begin
P := Ctxt.Json;
if P <> nil then // in-place replace trailing RowID -> ID for unserialization
begin
while (P^ <= ' ') and
(P^ <> #0) do
inc(P);
if P^ = '{' then
begin
repeat
inc(P);
until (P^ > ' ') or
(P^ = #0);
if PInt64(P)^ and $00ffdfdfdfdfdfff = // case insensitive search
ord('"') + ord('R') shl 8 + ord('O') shl 16 + ord('W') shl 24 +
Int64(ord('I')) shl 32 + Int64(ord('D')) shl 40 + Int64(ord('"')) shl 48 then
begin // "RowID" -> __{"ID"
PCardinal(P)^ := $2020 + ord('{') shl 16 + ord('"') shl 24;
Ctxt.{$ifdef USERECORDWITHMETHODS}Get.{$endif}Json := P + 2;
end
else if PInt64(P)^ and $0000ffdfdfdfdfdf =
ord('R') + ord('O') shl 8 + ord('W') shl 16 + ord('I') shl 24 +
Int64(ord('D')) shl 32 + Int64(ord(':')) shl 40 then
begin // RowID: -> __{ID:
PCardinal(P)^ := $2020 + ord('{') shl 16 + ord('I') shl 24;
Ctxt.{$ifdef USERECORDWITHMETHODS}Get.{$endif}Json := P + 2;
end;
end;
end;
_JL_RttiCustom(Data, Ctxt); // use default serialization
end;
procedure _JL_Array(Data: PAnsiChar; var Ctxt: TJsonParserContext);
var
n: integer;
arrinfo: TRttiCustom;
begin
if not Ctxt.ParseArray then
// detect void (i.e. []) or invalid array
exit;
if PCardinal(Ctxt.Json)^ = JSON_BASE64_MAGIC_QUOTE_C then
// raw RTTI binary layout with a single Base64 encoded item
Ctxt.Valid := Ctxt.ParseNext and
(Ctxt.EndOfObject = ']') and
(Ctxt.Value <> nil) and
(PCardinal(Ctxt.Value)^ and $ffffff = JSON_BASE64_MAGIC_C) and
BinaryLoadBase64(pointer(Ctxt.Value + 3), Ctxt.ValueLen - 3,
Data, Ctxt.Info.Info, {uri=}false, [rkArray], {withcrc=}false)
else
begin
// efficient load of all JSON items
arrinfo := Ctxt.Info;
Ctxt.Info := arrinfo.ArrayRtti; // nested context = item
n := arrInfo.Cache.ItemCount;
repeat
TRttiJsonLoad(Ctxt.Info.JsonLoad)(Data, Ctxt);
dec(n);
if Ctxt.Valid then
if (n > 0) and
(Ctxt.EndOfObject = ',') then
begin
// continue with the next item
inc(Data, arrinfo.Cache.ItemSize);
continue;
end
else if (n = 0) and
(Ctxt.EndOfObject = ']') then
// reached end of arrray
break;
Ctxt.Valid := false; // unexpected end
exit;
until false;
Ctxt.Info := arrinfo;
end;
Ctxt.ParseEndOfObject; // mimics GetJsonField() / Ctxt.ParseNext
end;
procedure _JL_DynArray_Custom(Data: PAnsiChar; var Ctxt: TJsonParserContext);
begin
// TRttiJson.RegisterCustomSerializer() custom callback for each item
TOnRttiJsonRead(TRttiJson(Ctxt.Info).fJsonReader)(Ctxt, Data);
end;
function _JL_DynArray_FromResults(Data: PPointer; var Ctxt: TJsonParserContext): boolean;
var
fieldcount, rowcount, r, f: PtrInt;
arrinfo, iteminfo: TRttiCustom;
item: PAnsiChar;
prop: PRttiCustomProp;
props: PRttiCustomPropDynArray;
begin
// Not Expanded (more optimized) format as array of values
// {"fieldCount":2,"values":["f1","f2","1v1",1v2,"2v1",2v2...],"rowCount":20}
result := IsNotExpandedBuffer(Ctxt.{$ifdef USERECORDWITHMETHODS}Get.{$endif}Json,
nil, fieldcount, rowcount);
if not result then
exit; // indicates not the expected format: caller will try Ctxt.ParseArray
// 1. check rowcount and fieldcount
Ctxt.Valid := false;
if (rowcount < 0) or
(fieldcount = 0) then
exit;
// 2. initialize the items lookup from the trailing field names
arrinfo := Ctxt.Info;
iteminfo := arrinfo.ArrayRtti;
if (iteminfo = nil) or
(iteminfo.Props.CountNonVoid = 0) then
exit; // expect an array of objects (classes or records)
SetLength(props, fieldcount);
for f := 0 to fieldcount - 1 do
begin
if not Ctxt.ParseNext or
not Ctxt.WasString then
exit; // should start with field names
prop := nil;
if Ctxt.ValueLen <> 0 then
begin
prop := FindCustomProp(pointer(iteminfo.Props.List),
Ctxt.Value, Ctxt.ValueLen, iteminfo.Props.Count);
if (prop = nil) and
(itemInfo.ValueRtlClass = vcObjectWithID) and
(PInteger(Ctxt.Value)^ and $dfdfdfdf =
ord('R') + ord('O') shl 8 + ord('W') shl 16 + ord('I') shl 24) and
(PWord(Ctxt.Value + 4)^ and $ffdf = ord('D')) then
prop := @iteminfo.Props.List[0]; // 'RowID' = first TObjectWithID field
end;
if (prop = nil) and
not (jpoIgnoreUnknownProperty in Ctxt.Options) then
exit;
props[f] := prop;
end;
// 3. fill all nested items from incoming values
Data := DynArrayNew(Data, rowcount, arrinfo.Cache.ItemSize); // alloc
for r := 1 to rowcount do
begin
if iteminfo.Kind = rkClass then
begin
Ctxt.Info := iteminfo; // as in _JL_RttiCustom()
Data^ := TRttiJson(iteminfo).fNewInstance(iteminfo);
item := Data^; // class are accessed by reference
if (rcfHookRead in iteminfo.Flags) and
TCCHook(item).RttiBeforeReadObject(@Ctxt) then
begin
inc(Data);
if Ctxt.Valid then
continue
else
break;
end;
end
else
item := pointer(Data); // record (or object) are stored by value
for f := 0 to fieldcount - 1 do
if props[f] = nil then // skip jpoIgnoreUnknownProperty
Ctxt.{$ifdef USERECORDWITHMETHODS}Get.{$endif}Json := GotoNextJsonItem(
Ctxt.Json, Ctxt.{$ifdef USERECORDWITHMETHODS}Get.{$endif}EndOfObject)
else if not JsonLoadProp(item, props[f], Ctxt) then
begin
Ctxt.{$ifdef USERECORDWITHMETHODS}Get.{$endif}Json := nil;
break;
end
else if Ctxt.EndOfObject = '}' then
break;
if Ctxt.Json = nil then
break;
if rcfHookRead in iteminfo.Flags then
TCCHook(item).RttiAfterReadObject;
inc(PAnsiChar(Data), arrinfo.Cache.ItemSize);
end;
Ctxt.Valid := false;
if Ctxt.Json <> nil then
begin
while not (Ctxt.Json^ in [#0, '}']) do
inc(Ctxt.{$ifdef USERECORDWITHMETHODS}Get.{$endif}Json);
if Ctxt.Json^ = '}' then
begin // reached final ..],"rowCount":20}
inc(Ctxt.{$ifdef USERECORDWITHMETHODS}Get.{$endif}Json);
Ctxt.Valid := true;
end;
end;
Ctxt.Info := arrinfo; // restore
end;
procedure _JL_DynArray(Data: PAnsiChar; var Ctxt: TJsonParserContext);
var
load: TRttiJsonLoad;
n, cap: PtrInt;
arr: PPointer;
arrinfo: TRttiCustom;
begin
arr := pointer(Data);
if arr^ <> nil then
Ctxt.Info.ValueFinalize(arr); // reset whole array variable
if Ctxt.Json = nil then
begin
Ctxt.Valid := false;
exit;
end;
Ctxt.{$ifdef USERECORDWITHMETHODS}Get.{$endif}Json := GotoNextNotSpace(Ctxt.Json);
if (PCardinal(Ctxt.Json)^ <> ord('{') + ord('"') shl 8 + ord('f') shl 16 +
ord('i') shl 24) or // FIELDCOUNT_PATTERN = '{"fieldCount":...
not _JL_DynArray_FromResults(arr, Ctxt) then
if not Ctxt.ParseArray then
// detect void (i.e. []) or invalid array
exit
else if PCardinal(Ctxt.Json)^ = JSON_BASE64_MAGIC_QUOTE_C then
// raw RTTI binary layout with a single Base64 encoded item
Ctxt.Valid := Ctxt.ParseNext and
(Ctxt.EndOfObject = ']') and
(Ctxt.Value <> nil) and
(PCardinal(Ctxt.Value)^ and $ffffff = JSON_BASE64_MAGIC_C) and
BinaryLoadBase64(pointer(Ctxt.Value + 3), Ctxt.ValueLen - 3,
Data, Ctxt.Info.Info, {uri=}false, [rkDynArray], {withcrc=}false)
else
begin
// efficient load of all JSON items
arrinfo := Ctxt.Info;
if TRttiJson(arrinfo).fJsonReader.Code <> nil then
load := @_JL_DynArray_Custom // custom callback
else
begin
Ctxt.Info := arrinfo.ArrayRtti;
if Ctxt.Info = nil then
load := nil
else
begin
load := Ctxt.Info.JsonLoad;
if (@load = @_JL_RttiCustom) and
(TRttiJson(Ctxt.Info).fJsonReader.Code = nil) and
(Ctxt.Info.Kind <> rkClass) and
(not (jpoClearValues in Ctxt.Options)) then
load := @_JL_RttiCustomProps; // somewhat faster direct record load
end;
end;
// initial guess of the JSON array count - will browse up to 64KB of input
cap := abs(JsonArrayCount(Ctxt.Json, Ctxt.Json + JSON_PREFETCH));
if (cap = 0) or
(not Assigned(load)) then
begin
Ctxt.Valid := false;
exit;
end;
Data := DynArrayNew(arr, cap, arrinfo.Cache.ItemSize); // alloc zeroed mem
// main JSON unserialization loop
n := 0;
repeat
if n = cap then
begin
// grow if our initial guess was aborted due to huge input
cap := NextGrow(cap);
Data := DynArrayGrow(arr, cap, arrinfo.Cache.ItemSize) +
(n * arrinfo.Cache.ItemSize);
end;
// unserialize one item
load(Data, Ctxt); // will call _JL_RttiCustom() for T*ObjArray
inc(n);
if Ctxt.Valid then
if Ctxt.EndOfObject = ',' then
begin
// continue with the next item
inc(Data, arrinfo.Cache.ItemSize);
continue;
end
else if Ctxt.EndOfObject = ']' then
// reached end of arrray
break;
Ctxt.Valid := false; // unexpected end
arrinfo.ValueFinalize(arr); // whole array clear on error
exit;
until false;
if n <> cap then
if n = 0 then
FastDynArrayClear(arr^, arrinfo.Cache.ItemInfo)
else
DynArrayFakeLength(arr^, n); // faster than SetLength()
Ctxt.Info := arrinfo; // restore
end;
Ctxt.ParseEndOfObject; // mimics GetJsonField() / Ctxt.ParseNext
end;
procedure _JL_Interface(Data: PInterface; var Ctxt: TJsonParserContext);
begin
// _JS_Interface() may have serialized the object instance properties, but we
// can't unserialize it since we don't know which class to create
Ctxt.Valid := Ctxt.ParseNull;
Data^ := nil;
end;
procedure _JL_PUtf8Char(Data: PPUtf8Char; var Ctxt: TJsonParserContext);
begin
// _JS_PUtf8Char() may have been serialized as JSON string, but we can't
// unserialize it since we can't allocate the memory and Ctxt.Json
// input is transient by definition
Ctxt.Valid := Ctxt.ParseNull;
Data^ := nil;
end;
// defined here to have _JL_RawJson and _JL_Variant known
procedure TJsonParserContext.ParsePropComplex(Data: pointer);
var
v: TRttiVarData;
tmp: TObject;
begin
// handle special cases of a setter method
case Info.Parser of
ptClass: // for a class property: use a temp instance for the setter call
begin
if jpoSetterNoCreate in Options then
Valid := false
else
begin
tmp := TRttiJson(Info).fNewInstance(Info);
try
v.Prop := Prop; // JsonLoad() would reset Prop := nil
TRttiJsonLoad(Info.JsonLoad)(@tmp, self); // JsonToObject(tmp)
if not Valid then
FreeAndNil(tmp)
else
begin
v.Prop.Prop.SetOrdProp(Data, PtrInt(tmp));
if jpoSetterExpectsToFreeTempInstance in Options then
FreeAndNil(tmp);
end;
except
on Exception do
tmp.Free;
end;
end;
end;
ptRawJson: // TRttiProp.SetValue() assume RawUtf8 -> dedicated RawJson code
begin
v.Data.VAny := nil;
try
_JL_RawJson(@v.Data.VAny, self);
if Valid then
Prop^.Prop.SetLongStrProp(Data, RawJson(v.Data.VAny));
finally
FastAssignNew(v.Data.VAny);
end;
end;
ptSet: // use a local temp variable before calling the setter
begin
v.Data.VInt64 := 0;
_JL_Set(@v.Data.VInt64, self);
if Valid then
Prop^.Prop.SetOrdProp(Data, v.Data.VInt64);
end;
else // call the getter via TRttiProp.SetValue() of a transient TRttiVarData
begin
v.VType := 0;
try
_JL_Variant(@v, self); // VariantLoadJson() over Ctxt
Valid := Valid and Prop^.Prop.SetValue(Data, variant(v));
finally
VarClearProc(v.Data);
end;
end;
end;
end;
procedure _JL_TObjectList(Data: PObjectList; var Ctxt: TJsonParserContext);
var
root: TRttiCustom;
item: TObject;
begin
if Data^ = nil then
begin
Ctxt.Valid := Ctxt.ParseNull;
exit;
end;
Data^.Clear;
if Ctxt.ParseNull or
not Ctxt.ParseArray then
exit;
root := Ctxt.Info;
Ctxt.Info := Ctxt.ObjectListItem;
repeat
item := Ctxt.ParseNewObject;
if item = nil then
break;
Data^.Add(item);
until Ctxt.EndOfObject = ']';
Ctxt.Info := root;
Ctxt.ParseEndOfObject;
end;
procedure _JL_TCollection(Data: PCollection; var Ctxt: TJsonParserContext);
var
root: TRttiJson;
load: TRttiJsonLoad;
item: TCollectionItem;
begin
if Data^ = nil then
begin
Ctxt.Valid := Ctxt.ParseNull;
exit;
end;
Data^.BeginUpdate;
try
Data^.Clear;
if Ctxt.ParseNull or
not Ctxt.ParseArray then
exit;
root := TRttiJson(Ctxt.Info);
load := nil;
repeat
item := Data^.Add;
if not Assigned(load) then
begin
if root.fCollectionItemRtti = nil then
begin
// RegisterCollection() was not called -> compute after Data^.Add
root.fCollectionItem := PPointer(item)^;
root.fCollectionItemRtti := Rtti.RegisterClass(PClass(item)^);
end;
Ctxt.Info := root.fCollectionItemRtti;
load := Ctxt.Info.JsonLoad;
end;
load(@item, Ctxt);
until (not Ctxt.Valid) or
(Ctxt.EndOfObject = ']');
Ctxt.Info := root;
Ctxt.ParseEndOfObject;
finally
Data^.EndUpdate;
end;
end;
procedure _JL_TSynObjectList(Data: PSynObjectList; var Ctxt: TJsonParserContext);
var
root: TRttiCustom;
item: TObject;
begin
if Data^ = nil then
begin
Ctxt.Valid := Ctxt.ParseNull;
exit;
end;
Data^.Clear;
if Ctxt.ParseNull or
not Ctxt.ParseArray then
exit;
root := Ctxt.Info;
Ctxt.Info := Ctxt.ObjectListItem;
if (Ctxt.Info = nil) and
(Data^.ItemClass <> nil) then
Ctxt.Info := Rtti.RegisterClass(Data^.ItemClass);
repeat
item := Ctxt.ParseNewObject;
if item = nil then
break;
Data^.Add(item);
until Ctxt.EndOfObject = ']';
Ctxt.Info := root;
Ctxt.ParseEndOfObject;
end;
procedure _JL_TStrings(Data: PStrings; var Ctxt: TJsonParserContext);
var
item: string;
begin
if Data^ = nil then
begin
Ctxt.Valid := Ctxt.ParseNull;
exit;
end;
Data^.BeginUpdate;
try
Data^.Clear;
if Ctxt.ParseNull or
not Ctxt.ParseArray then
exit;
repeat
if Ctxt.ParseNext then
begin
Utf8DecodeToString(Ctxt.Value, Ctxt.ValueLen, item);
Data^.Add(item);
end;
until (not Ctxt.Valid) or
(Ctxt.EndOfObject = ']');
finally
Data^.EndUpdate;
end;
Ctxt.ParseEndOfObject;
end;
procedure _JL_TRawUtf8List(Data: PRawUtf8List; var Ctxt: TJsonParserContext);
var
item: RawUtf8;
begin
if Data^ = nil then
begin
Ctxt.Valid := Ctxt.ParseNull;
exit;
end;
Data^.BeginUpdate;
try
Data^.Clear;
if Ctxt.ParseNull or
not Ctxt.ParseArray then
exit;
repeat
if Ctxt.ParseNext then
begin
Ctxt.Interning.Unique(item, Ctxt.Value, Ctxt.ValueLen);
Data^.AddObject(item, nil);
end;
until (not Ctxt.Valid) or
(Ctxt.EndOfObject = ']');
finally
Data^.EndUpdate;
end;
Ctxt.ParseEndOfObject;
end;
var
/// use pointer to allow any kind of Data^ type in above functions
// - typecast to TRttiJsonSave for proper function call
// - rkRecord and rkClass are set in TRttiJson.SetParserType
PT_JSONLOAD: array[TRttiParserType] of pointer = (
nil, @_JL_Array, @_JL_Boolean, @_JL_Byte, @_JL_Cardinal, @_JL_Currency,
@_JL_Double, @_JL_Extended, @_JL_Int64, @_JL_Integer, @_JL_QWord,
@_JL_RawByteString, @_JL_RawJson, @_JL_RawUtf8, nil,
@_JL_Single, @_JL_String, @_JL_SynUnicode, @_JL_DateTime, @_JL_DateTime,
@_JL_GUID, @_JL_Hash, @_JL_Hash, @_JL_Hash, @_JL_Int64, @_JL_TimeLog,
@_JL_UnicodeString, @_JL_UnixTime, @_JL_UnixMSTime, @_JL_Variant,
@_JL_WideString, @_JL_WinAnsi, @_JL_Word, @_JL_Enumeration, @_JL_Set,
nil, @_JL_DynArray, @_JL_Interface, @_JL_PUtf8Char, nil);
procedure JsonDecode(var Json: RawUtf8; const Names: array of RawUtf8;
Values: PValuePUtf8CharArray; HandleValuesAsObjectOrArray: boolean);
begin
JsonDecode(UniqueRawUtf8(Json), Names, Values, HandleValuesAsObjectOrArray);
end;
procedure JsonDecode(var Json: RawJson; const Names: array of RawUtf8;
Values: PValuePUtf8CharArray; HandleValuesAsObjectOrArray: boolean);
begin
JsonDecode(UniqueRawUtf8(RawUtf8(Json)), Names, Values, HandleValuesAsObjectOrArray);
end;
function JsonDecode(P: PUtf8Char; Names: PPUtf8CharArray; NamesCount: integer;
Values: PValuePUtf8CharArray; HandleValuesAsObjectOrArray: boolean): PUtf8Char;
var
v: PValuePUtf8Char;
name: PUtf8Char;
namelen, i: integer;
info: TGetJsonField;
begin
result := nil;
if (Values = nil) or
(NamesCount <= 0) then
exit; // avoid GPF
FillCharFast(Values[0], NamesCount * SizeOf(Values[0]), 0);
dec(NamesCount);
if P = nil then
exit;
while P^ <> '{' do
if P^ = #0 then
exit
else
inc(P);
info.Json := P + 1; // jump {
repeat
name := GetJsonPropName(info.Json, @namelen);
if name = nil then
exit; // invalid Json content
info.GetJsonFieldOrObjectOrArray(HandleValuesAsObjectOrArray);
if not (info.EndOfObject in [',', '}']) then
exit; // invalid item separator
v := pointer(Values);
for i := 0 to NamesCount do
if (v^.Text = nil) and
IdemPropNameU(Names[i], name, namelen) then
begin
v^.Text := info.Value;
v^.Len := info.ValueLen;
break;
end
else
inc(v);
until (info.Json = nil) or
(info.EndOfObject = '}');
if info.Json = nil then // result=nil indicates failure -> points to #0
result := @NULCHAR
else
result := info.Json;
end;
function JsonDecode(P: PUtf8Char; const Names: array of RawUtf8;
Values: PValuePUtf8CharArray; HandleValuesAsObjectOrArray: boolean): PUtf8Char;
begin
result := JsonDecode(P, @Names[0], high(Names) + 1,
Values, HandleValuesAsObjectOrArray);
end;
function JsonDecode(var Json: RawUtf8; const aName: RawUtf8; WasString: PBoolean;
HandleValuesAsObjectOrArray: boolean): RawUtf8;
begin
result := JsonDecode(pointer(Json), aName, WasString, HandleValuesAsObjectOrArray);
end;
function JsonDecode(Json: PUtf8Char; const aName: RawUtf8;
WasString: PBoolean; HandleValuesAsObjectOrArray: boolean): RawUtf8;
var
info: TGetJsonField;
begin
result := '';
if Json = nil then
exit;
while Json^ <> '{' do
if Json^ = #0 then
exit
else
inc(Json);
info.Json := Json + 1; // jump {
repeat
info.Value := GetJsonPropName(info.Json, @info.ValueLen);
if info.Value = nil then
exit; // invalid Json content
if IdemPropNameU(aName, info.Value, info.ValueLen) then
begin
info.GetJsonFieldOrObjectOrArray(HandleValuesAsObjectOrArray);
if info.Json <> nil then
FastSetString(result, info.Value, info.ValueLen);
exit;
end;
info.Json := GotoNextJsonItem(info.Json, info.EndOfObject);
if not (info.EndOfObject in [',', '}']) then
exit; // invalid item separator
until (info.Json = nil) or
(info.EndOfObject = '}');
end;
function JsonDecode(P: PUtf8Char; out Values: TNameValuePUtf8CharDynArray;
HandleValuesAsObjectOrArray: boolean): PUtf8Char;
var
n: PtrInt;
info: TGetJsonField;
nametext: PUtf8Char;
namelen: integer;
begin
{$ifdef FPC}
Values := nil;
{$endif FPC}
result := nil;
n := 0;
if P <> nil then
begin
while P^ <> '{' do
if P^ = #0 then
exit
else
inc(P);
inc(P); // jump {
info.Json := P;
repeat
nametext := GetJsonPropName(info.Json, @nameLen);
if nametext = nil then
exit; // invalid JSON content
info.GetJsonFieldOrObjectOrArray(HandleValuesAsObjectOrArray);
if not (info.EndOfObject in [',', '}']) then
exit; // invalid item separator
if n = length(Values) then
SetLength(Values, NextGrow(n));
with Values[n] do
begin
Name.Text := nametext;
Name.Len := namelen;
Value.Text := info.Value;
Value.Len := info.ValueLen;
end;
inc(n);
until (info.Json = nil) or
(info.EndOfObject = '}');
P := info.Json;
end;
if n <> 0 then
DynArrayFakeLength(Values, n); // SetLength() would have made a realloc()
if P = nil then // result=nil indicates failure -> points to #0
result := @NULCHAR
else
result := P;
end;
{ ************ JSON-aware TSynNameValue TSynPersistentStoreJson }
{ TSynNameValue }
procedure TSynNameValue.Add(const aName, aValue: RawUtf8; aTag: PtrInt);
var
added: boolean;
i: PtrInt;
begin
i := DynArray.FindHashedForAdding(aName, added);
with List[i] do
begin
if added then
Name := aName;
Value := aValue;
Tag := aTag;
end;
if Assigned(fOnAdd) then
fOnAdd(List[i], i);
end;
procedure TSynNameValue.InitFromIniSection(Section: PUtf8Char;
const OnTheFlyConvert: TOnSynNameValueConvertRawUtf8;
const OnAdd: TOnSynNameValueNotify);
var
s: RawUtf8;
i: PtrInt;
begin
Init(false);
fOnAdd := OnAdd;
while (Section <> nil) and
(Section^ <> '[') do
begin
s := GetNextLine(Section, Section);
i := PosExChar('=', s);
if (i > 1) and
not (s[1] in [';', '[']) then
if Assigned(OnTheFlyConvert) then
Add(copy(s, 1, i - 1), OnTheFlyConvert(copy(s, i + 1, 1000)))
else
Add(copy(s, 1, i - 1), copy(s, i + 1, 1000));
end;
end;
procedure TSynNameValue.InitFromCsv(Csv: PUtf8Char; NameValueSep, ItemSep: AnsiChar);
var
n, v: RawUtf8;
begin
Init(false);
while Csv <> nil do
begin
GetNextItem(Csv, NameValueSep, n);
if ItemSep = #10 then
GetNextItemTrimedCRLF(Csv, v)
else
GetNextItem(Csv, ItemSep, v);
if n = '' then
break;
Add(n, v);
end;
end;
procedure TSynNameValue.InitFromNamesValues(const Names, Values: array of RawUtf8);
var
i: PtrInt;
begin
Init(false);
if high(Names) <> high(Values) then
exit;
DynArray.Capacity := length(Names);
for i := 0 to high(Names) do
Add(Names[i], Values[i]);
end;
function TSynNameValue.InitFromJson(Json: PUtf8Char; aCaseSensitive: boolean): boolean;
var
N: PUtf8Char;
nam, val: RawUtf8;
Nlen, c: integer;
info: TGetJsonField;
begin
result := false;
Init(aCaseSensitive);
if Json = nil then
exit;
while (Json^ <= ' ') and
(Json^ <> #0) do
inc(Json);
if Json^ <> '{' then
exit;
repeat
inc(Json)
until (Json^ = #0) or
(Json^ > ' ');
info.Json := Json;
c := JsonObjectPropCount(Json); // fast 900MB/s parsing
if c <= 0 then
exit;
DynArray.Capacity := c;
repeat
N := GetJsonPropName(info.Json, @Nlen);
if N = nil then
exit;
info.GetJsonFieldOrObjectOrArray;
if info.Json = nil then
exit;
FastSetString(nam, N, Nlen);
FastSetString(val, info.Value, info.Valuelen);
Add(nam, val);
until info.EndOfObject = '}';
result := true;
end;
procedure TSynNameValue.Init(aCaseSensitive: boolean);
begin
// release dynamic arrays memory before FillcharFast()
List := nil;
Finalize(PDynArrayHasher(@DynArray.Hasher)^);
// initialize hashed storage
FillCharFast(self, SizeOf(self), 0);
DynArray.InitSpecific(TypeInfo(TSynNameValueItemDynArray), List,
ptRawUtf8, @Count, not aCaseSensitive);
end;
function TSynNameValue.Find(const aName: RawUtf8): PtrInt;
begin
result := DynArray.FindHashed(aName);
end;
function TSynNameValue.FindStart(const aUpperName: RawUtf8): PtrInt;
begin
for result := 0 to Count - 1 do
if IdemPChar(pointer(List[result].Name), pointer(aUpperName)) then
exit;
result := -1;
end;
function TSynNameValue.FindByValue(const aValue: RawUtf8): PtrInt;
begin
for result := 0 to Count - 1 do
if List[result].Value = aValue then
exit;
result := -1;
end;
function TSynNameValue.Delete(const aName: RawUtf8): boolean;
begin
result := DynArray.FindHashedAndDelete(aName) >= 0;
end;
function TSynNameValue.DeleteByValue(const aValue: RawUtf8; Limit: integer): integer;
var
ndx: PtrInt;
begin
result := 0;
if Limit < 1 then
exit;
for ndx := Count - 1 downto 0 do
if List[ndx].Value = aValue then
begin
DynArray.Delete(ndx);
inc(result);
if result >= Limit then
break;
end;
if result > 0 then
DynArray.ForceReHash;
end;
function TSynNameValue.Value(const aName: RawUtf8; const aDefaultValue: RawUtf8): RawUtf8;
var
i: PtrInt;
begin
if @self = nil then
i := -1
else
i := DynArray.FindHashed(aName);
if i < 0 then
result := aDefaultValue
else
result := List[i].Value;
end;
function TSynNameValue.ValueInt(const aName: RawUtf8; const aDefaultValue: Int64): Int64;
var
i: PtrInt;
err: integer;
begin
i := DynArray.FindHashed(aName);
if i < 0 then
result := aDefaultValue
else
begin
result := GetInt64(pointer(List[i].Value), err);
if err <> 0 then
result := aDefaultValue;
end;
end;
function TSynNameValue.ValueBool(const aName: RawUtf8): boolean;
begin
result := GetBoolean(pointer(Value(aName)));
end;
function TSynNameValue.ValueEnum(const aName: RawUtf8; aEnumTypeInfo: PRttiInfo;
out aEnum; aEnumDefault: PtrUInt): boolean;
var
rtti: PRttiEnumType;
v: RawUtf8;
err: integer;
i: PtrInt;
begin
result := false;
rtti := aEnumTypeInfo.EnumBaseType;
if rtti = nil then
exit;
RTTI_TO_ORD[rtti.RttiOrd](@aEnum, aEnumDefault); // always set default value
v := Value(aName, '');
TrimSelf(v);
if v = '' then
exit;
i := GetInteger(pointer(v), err);
if (err <> 0) or
(PtrUInt(i) > PtrUInt(rtti.MaxValue)) then
i := rtti.GetEnumNameValue(pointer(v), length(v), {alsotrimleft=}true);
if i >= 0 then
begin
RTTI_TO_ORD[rtti.RttiOrd](@aEnum, i); // we found a proper value
result := true;
end;
end;
function TSynNameValue.Initialized: boolean;
begin
result := DynArray.Value = @List;
end;
function TSynNameValue.GetBlobData: RawByteString;
begin
result := DynArray.SaveTo;
end;
procedure TSynNameValue.SetBlobDataPtr(aValue, aValueMax: pointer);
begin
DynArray.LoadFrom(aValue, aValueMax);
DynArray.ForceReHash;
end;
procedure TSynNameValue.SetBlobData(const aValue: RawByteString);
begin
DynArray.LoadFromBinary(aValue);
DynArray.ForceReHash;
end;
function TSynNameValue.GetStr(const aName: RawUtf8): RawUtf8;
begin
result := Value(aName, '');
end;
function TSynNameValue.GetInt(const aName: RawUtf8): Int64;
begin
result := ValueInt(aName, 0);
end;
function TSynNameValue.AsCsv(const KeySeparator, ValueSeparator, IgnoreKey: RawUtf8): RawUtf8;
var
i: PtrInt;
temp: TTextWriterStackBuffer;
begin
with TTextWriter.CreateOwnedStream(temp) do
try
for i := 0 to Count - 1 do
if (IgnoreKey = '') or
(List[i].Name <> IgnoreKey) then
begin
AddNoJsonEscapeUtf8(List[i].Name);
AddNoJsonEscapeUtf8(KeySeparator);
AddNoJsonEscapeUtf8(List[i].Value);
AddNoJsonEscapeUtf8(ValueSeparator);
end;
SetText(result);
finally
Free;
end;
end;
function TSynNameValue.AsJson: RawUtf8;
var
i: PtrInt;
temp: TTextWriterStackBuffer;
begin
with TJsonWriter.CreateOwnedStream(temp) do
try
Add('{');
for i := 0 to Count - 1 do
with List[i] do
begin
AddProp(pointer(Name), length(Name));
AddDirect('"');
AddJsonEscape(pointer(Value));
AddDirect('"', ',');
end;
CancelLastComma('}');
SetText(result);
finally
Free;
end;
end;
procedure TSynNameValue.AsNameValues(out Names, Values: TRawUtf8DynArray);
var
i: PtrInt;
begin
SetLength(Names, Count);
SetLength(Values, Count);
for i := 0 to Count - 1 do
begin
Names[i] := List[i].Name;
Values[i] := List[i].Value;
end;
end;
function TSynNameValue.ValueVariantOrNull(const aName: RawUtf8): variant;
var
i: PtrInt;
begin
i := Find(aName);
if i < 0 then
SetVariantNull(result{%H-})
else
RawUtf8ToVariant(List[i].Value, result);
end;
procedure TSynNameValue.AsDocVariant(out DocVariant: variant;
ExtendedJson, ValueAsString, AllowVarDouble: boolean);
var
ndx: PtrInt;
dv: TDocVariantData absolute DocVariant;
begin
if Count > 0 then
begin
dv.Init(JSON_NAMEVALUE[ExtendedJson], dvObject);
dv.SetCount(Count);
dv.Capacity := Count;
for ndx := 0 to Count - 1 do
begin
dv.Names[ndx] := List[ndx].Name;
if ValueAsString or
not GetVariantFromNotStringJson(pointer(List[ndx].Value),
TVarData(dv.Values[ndx]), AllowVarDouble) then
RawUtf8ToVariant(List[ndx].Value, dv.Values[ndx]);
end;
end
else
TVarData(DocVariant).VType := varNull;
end;
function TSynNameValue.AsDocVariant(ExtendedJson, ValueAsString: boolean): variant;
begin
AsDocVariant(result, ExtendedJson, ValueAsString);
end;
function TSynNameValue.MergeDocVariant(var DocVariant: variant;
ValueAsString: boolean; ChangedProps: PVariant; ExtendedJson,
AllowVarDouble: boolean): integer;
var
dv: TDocVariantData absolute DocVariant;
i, ndx: PtrInt;
v: variant;
intvalues: TRawUtf8Interning;
begin
if dv.VarType <> DocVariantVType then
TDocVariant.New(DocVariant, JSON_NAMEVALUE[ExtendedJson]);
if ChangedProps <> nil then
TDocVariant.New(ChangedProps^, dv.Options);
if dvoInternValues in dv.Options then
intvalues := DocVariantType.InternValues
else
intvalues := nil;
result := 0; // returns number of changed values
for i := 0 to Count - 1 do
if List[i].Name <> '' then
begin
VarClear(v{%H-});
if ValueAsString or
not GetVariantFromNotStringJson(
pointer(List[i].Value), TVarData(v), AllowVarDouble) then
RawUtf8ToVariant(List[i].Value, v);
ndx := dv.GetValueIndex(List[i].Name);
if ndx < 0 then
ndx := dv.InternalAdd(List[i].Name)
else if FastVarDataComp(@v, @dv.Values[ndx], false) = 0 then
continue; // value not changed -> skip
if ChangedProps <> nil then
PDocVariantData(ChangedProps)^.AddValue(List[i].Name, v);
SetVariantByValue(v, dv.Values[ndx]);
if intvalues <> nil then
intvalues.UniqueVariant(dv.Values[ndx]);
inc(result);
end;
end;
{ TSynPersistentStoreJson }
procedure TSynPersistentStoreJson.AddJson(W: TJsonWriter);
begin
W.AddPropJsonString('name', fName);
end;
function TSynPersistentStoreJson.SaveToJson(reformat: TTextWriterJsonFormat): RawUtf8;
var
W: TJsonWriter;
begin
W := TJsonWriter.CreateOwnedStream(65536);
try
W.Add('{');
AddJson(W);
W.CancelLastComma('}');
W.SetText(result, reformat);
finally
W.Free;
end;
end;
{ TSynCache }
constructor TSynCache.Create(aMaxCacheRamUsed: cardinal;
aCaseSensitive: boolean; aTimeoutSeconds: cardinal);
begin
inherited Create; // may have been overriden
fNameValue.Init(aCaseSensitive);
fMaxRamUsed := aMaxCacheRamUsed;
fTimeoutSeconds := aTimeoutSeconds;
end;
procedure TSynCache.ResetIfNeeded;
var
tix: cardinal;
begin
if fRamUsed > fMaxRamUsed then
Reset;
if fTimeoutSeconds > 0 then
begin
tix := GetTickCount64 shr 10;
if fTimeoutTix > tix then
Reset;
fTimeoutTix := tix + fTimeoutSeconds;
end;
end;
function TSynCache.Find(const aKey: RawUtf8; aResultTag: PPtrInt): RawUtf8;
var
ndx: PtrInt;
begin
result := '';
if (self = nil) or
(aKey = '') then
exit;
fSafe.ReadOnlyLock;
{$ifdef HASFASTTRYFINALLY}
try
{$else}
begin
{$endif HASFASTTRYFINALLY}
ndx := fNameValue.Find(aKey);
if ndx >= 0 then
with fNameValue.List[ndx] do
begin
result := Value;
if aResultTag <> nil then
aResultTag^ := Tag;
end;
{$ifdef HASFASTTRYFINALLY}
finally
{$endif HASFASTTRYFINALLY}
fSafe.ReadOnlyUnLock;
end;
end;
function TSynCache.AddOrUpdate(const aKey, aValue: RawUtf8; aTag: PtrInt): boolean;
var
ndx: PtrInt;
begin
result := false;
if self = nil then
exit; // avoid GPF
fSafe.WriteLock;
try
ResetIfNeeded;
ndx := fNameValue.DynArray.FindHashedForAdding(aKey, result);
with fNameValue.List[ndx] do
begin
Name := aKey;
dec(fRamUsed, length(Value));
Value := aValue;
inc(fRamUsed, length(Value));
Tag := aTag;
end;
finally
fSafe.WriteUnlock;
end;
end;
function TSynCache.Reset: boolean;
begin
result := false;
if (self = nil) or
(fNameValue.Count = 0) then
exit; // avoid GPF or a lock for nothing
fSafe.WriteLock;
try
if fNameValue.Count <> 0 then
begin
fNameValue.DynArray.Clear;
fNameValue.DynArray.ForceReHash;
result := true; // mark something was flushed
end;
fRamUsed := 0;
fTimeoutTix := 0;
finally
fSafe.WriteUnlock;
end;
end;
{ *********** JSON-aware TSynDictionary Storage }
{ TSynDictionary }
constructor TSynDictionary.Create(aKeyTypeInfo, aValueTypeInfo: PRttiInfo;
aKeyCaseInsensitive: boolean; aTimeoutSeconds: cardinal;
aCompressAlgo: TAlgoCompress; aHasher: THasher);
begin
inherited Create;
fSafe.Padding[DIC_KEYCOUNT].VType := varInteger; // Keys.Count
fSafe.Padding[DIC_KEY].VType := varUnknown; // Key.Value
fSafe.Padding[DIC_VALUECOUNT].VType := varInteger; // Values.Count
fSafe.Padding[DIC_VALUE].VType := varUnknown; // Values.Value
fSafe.Padding[DIC_TIMECOUNT].VType := varInteger; // Timeouts.Count
fSafe.Padding[DIC_TIMESEC].VType := varInteger; // Timeouts Seconds
fSafe.Padding[DIC_TIMETIX].VType := varInteger; // GetTickCount64 shr 10
fSafe.PaddingUsedCount := DIC_TIMETIX + 1; // manual registration
fKeys.Init(aKeyTypeInfo, fSafe.Padding[DIC_KEY].VAny, nil, nil, aHasher,
@fSafe.Padding[DIC_KEYCOUNT].VInteger, aKeyCaseInsensitive);
fValues.Init(aValueTypeInfo, fSafe.Padding[DIC_VALUE].VAny,
@fSafe.Padding[DIC_VALUECOUNT].VInteger);
fValues.Compare := DynArraySortOne(fValues.Info.ArrayFirstField, aKeyCaseInsensitive);
fTimeouts.Init(TypeInfo(TIntegerDynArray), fTimeOut,
@fSafe.Padding[DIC_TIMECOUNT].VInteger);
if aCompressAlgo = nil then
aCompressAlgo := AlgoSynLZ;
fCompressAlgo := aCompressAlgo;
fSafe.Padding[DIC_TIMESEC].VInteger := aTimeoutSeconds;
end;
{$ifdef HASGENERICS}
class function TSynDictionary.New<TKey, TValue>(aKeyCaseInsensitive: boolean;
aTimeoutSeconds: cardinal; aCompressAlgo: TAlgoCompress;
aHasher: THasher): TSynDictionary;
begin
result := TSynDictionary.Create(TypeInfo(TArray<TKey>), TypeInfo(TArray<TValue>),
aKeyCaseInsensitive, aTimeoutSeconds, aCompressAlgo, aHasher);
end;
{$endif HASGENERICS}
function TSynDictionary.ComputeNextTimeOut: cardinal;
begin
result := fSafe.Padding[DIC_TIMESEC].VInteger;
if result <> 0 then
result := cardinal(GetTickCount64 shr 10) + result;
end;
function TSynDictionary.GetCapacity: integer;
begin
result := fKeys.Capacity; // no need to lock for an evolving value
end;
procedure TSynDictionary.SetCapacity(const Value: integer);
begin
fSafe.Lock; // = RWLock(cWrite);
try
fKeys.Capacity := Value;
fValues.Capacity := Value;
if fSafe.Padding[DIC_TIMESEC].VInteger > 0 then
fTimeOuts.Capacity := Value;
finally
fSafe.UnLock;
end;
end;
function TSynDictionary.GetTimeOutSeconds: cardinal;
begin
result := fSafe.Padding[DIC_TIMESEC].VInteger;
end;
procedure TSynDictionary.SetTimeOutSeconds(Value: cardinal);
begin
// no fSafe.Lock because RWLock(cWrite) in DeleteAll is enough
DeleteAll;
fSafe.Padding[DIC_TIMESEC].VInteger := Value;
end;
procedure TSynDictionary.SetTimeouts;
var
i: PtrInt;
timeout: cardinal;
begin
if fSafe.Padding[DIC_TIMESEC].VInteger = 0 then
exit;
fTimeOuts.Count := fSafe.Padding[DIC_KEYCOUNT].VInteger;
timeout := ComputeNextTimeOut;
for i := 0 to fSafe.Padding[DIC_TIMECOUNT].VInteger - 1 do
fTimeOut[i] := timeout;
end;
function TSynDictionary.DeleteDeprecated(tix64: Int64): integer;
var
i: PtrInt;
now: cardinal;
begin
result := 0;
if (self = nil) or
(fSafe.Padding[DIC_TIMECOUNT].VInteger = 0) or // no entry
(fSafe.Padding[DIC_TIMESEC].VInteger = 0) then // nothing in fTimeOut[]
exit;
if tix64 = 0 then
tix64 := GetTickCount64;
now := tix64 shr 10;
if fSafe.Padding[DIC_TIMETIX].VInteger = integer(now) then
exit; // no need to search more often than every second
fSafe.ReadWriteLock; // would upgrade to cWrite only if needed
try
fSafe.Padding[DIC_TIMETIX].VInteger := now;
for i := fSafe.Padding[DIC_TIMECOUNT].VInteger - 1 downto 0 do
if (now > fTimeOut[i]) and
(fTimeOut[i] <> 0) and
(not Assigned(fOnCanDelete) or
fOnCanDelete(fKeys.ItemPtr(i)^, fValues.ItemPtr(i)^, i)) then
begin
if result = 0 then
fSafe.Lock; // = cWrite
fKeys.Delete(i);
fValues.Delete(i);
fTimeOuts.Delete(i);
inc(result);
end;
if result > 0 then
fKeys.ForceReHash; // mandatory after manual fKeys.Delete(i)
finally
if result > 0 then
fSafe.UnLock; // = cWrite
fSafe.ReadWriteUnLock;
end;
end;
procedure TSynDictionary.DeleteAll;
begin
if self = nil then
exit;
fSafe.Lock; // = RWLock(cWrite);
try
fKeys.Clear;
fKeys.Hasher.ForceReHash(nil); // mandatory to avoid GPF
fValues.Clear;
if fSafe.Padding[DIC_TIMESEC].VInteger > 0 then
fTimeOuts.Clear;
finally
fSafe.UnLock;
end;
end;
destructor TSynDictionary.Destroy;
begin
fKeys.Clear;
fValues.Clear;
inherited Destroy;
end;
function TSynDictionary.GetThreadUse: TSynLockerUse;
begin
result := fSafe^.RWUse;
end;
procedure TSynDictionary.SetThreadUse(const Value: TSynLockerUse);
begin
fSafe^.RWUse := Value;
end;
function TSynDictionary.InternalAddUpdate(
aKey, aValue: pointer; aUpdate: boolean): PtrInt;
var
added: boolean;
tim: cardinal;
begin
tim := ComputeNextTimeOut;
fSafe.Lock; // = RWLock(cWrite) - cReadWrite is not possible here
try
result := fKeys.FindHashedForAdding(aKey^, added);
if added then
begin // fKey[result] := aKey;
with fKeys{$ifdef UNDIRECTDYNARRAY}.InternalDynArray{$endif} do
ItemCopy(aKey, PAnsiChar(Value^) + (result * Info.Cache.ItemSize));
if fValues.Add(aValue^) <> result then
raise ESynDictionary.CreateUtf8('%.Add fValues.Add', [self]);
if tim <> 0 then
fTimeOuts.Add(tim);
end
else if aUpdate then
begin
fValues.ItemCopyFrom(aValue, result, {ClearBeforeCopy=}true);
if tim <> 0 then
fTimeOut[result] := tim;
end
else
result := -1;
finally
fSafe.UnLock;
end;
end;
function TSynDictionary.Add(const aKey, aValue): PtrInt;
begin
result := InternalAddUpdate(@aKey, @aValue, {update=}false)
end;
function TSynDictionary.AddOrUpdate(const aKey, aValue): PtrInt;
begin
result := InternalAddUpdate(@aKey, @aValue, {update=}true)
end;
function TSynDictionary.Clear(const aKey): PtrInt;
begin
fSafe.ReadWriteLock;
try
result := fKeys.FindHashed(aKey);
if result >= 0 then
begin
fSafe.Lock;
fValues.ItemClear(fValues.ItemPtr(result));
if fSafe.Padding[DIC_TIMESEC].VInteger > 0 then
fTimeOut[result] := 0;
fSafe.UnLock;
end;
finally
fSafe.ReadWriteUnLock;
end;
end;
function TSynDictionary.Delete(const aKey): PtrInt;
begin
fSafe.Lock;
try
result := fKeys.FindHashedAndDelete(aKey);
if result >= 0 then
begin
fValues.Delete(result);
if fSafe.Padding[DIC_TIMESEC].VInteger > 0 then
fTimeOuts.Delete(result);
end;
finally
fSafe.UnLock;
end;
end;
function TSynDictionary.DeleteAt(aIndex: PtrInt): boolean;
begin
if cardinal(aIndex) < cardinal(fSafe.Padding[DIC_KEYCOUNT].VInteger) then
// use Delete(aKey) to have efficient hash table update
result := Delete(fKeys.ItemPtr(aIndex)^) = aIndex
else
result := false;
end;
function TSynDictionary.InArray(const aKey, aArrayValue;
aAction: TSynDictionaryInArray; aCompare: TDynArraySortCompare): boolean;
var
nested: TDynArray;
ndx: PtrInt;
new: pointer;
begin
result := false;
if (fValues.Info.ArrayRtti = nil) or
(fValues.Info.ArrayRtti.Kind <> rkDynArray) then
raise ESynDictionary.CreateUtf8('%.Values: % items are not dynamic arrays',
[self, fValues.Info.Name]);
if aAction = iaFind then
fSafe.ReadLock
else
fSafe.Lock; // other actions may need to write the internal data
try
ndx := fKeys.FindHashed(aKey);
if ndx < 0 then
if aAction <> iaAddForced then
exit
else
begin
new := nil;
ndx := Add(aKey, new);
end;
nested.InitRtti(fValues.Info.ArrayRtti, fValues.ItemPtr(ndx)^);
nested.Compare := aCompare;
case aAction of
iaFind:
result := nested.Find(aArrayValue) >= 0;
iaFindAndDelete:
result := nested.FindAndDelete(aArrayValue) >= 0;
iaFindAndUpdate:
result := nested.FindAndUpdate(aArrayValue) >= 0;
iaFindAndAddIfNotExisting:
result := nested.FindAndAddIfNotExisting(aArrayValue) >= 0;
iaAdd,
iaAddForced:
result := nested.Add(aArrayValue) >= 0;
end;
finally
if aAction = iaFind then
fSafe.ReadUnLock
else
fSafe.UnLock;
end;
end;
function TSynDictionary.FindInArray(const aKey, aArrayValue;
aCompare: TDynArraySortCompare): boolean;
begin
result := InArray(aKey, aArrayValue, iaFind, aCompare);
end;
function TSynDictionary.FindKeyFromValue(const aValue;
out aKey; aUpdateTimeOut: boolean): boolean;
var
ndx: PtrInt;
begin
fSafe.ReadLock; // cReadOnly is good enough for SetTimeoutAtIndex()
try
ndx := fValues.IndexOf(aValue); // use fast RTTI for value search
result := ndx >= 0;
if result then
begin
fKeys.ItemCopyAt(ndx, @aKey);
if aUpdateTimeOut then
SetTimeoutAtIndex(ndx); // no cWrite lock needed
end;
finally
fSafe.ReadUnLock;
end;
end;
function TSynDictionary.DeleteInArray(const aKey, aArrayValue;
aCompare: TDynArraySortCompare): boolean;
begin
result := InArray(aKey, aArrayValue, iaFindAndDelete, aCompare);
end;
function TSynDictionary.UpdateInArray(const aKey, aArrayValue;
aCompare: TDynArraySortCompare): boolean;
begin
result := InArray(aKey, aArrayValue, iaFindAndUpdate, aCompare);
end;
function TSynDictionary.AddInArray(const aKey, aArrayValue;
aCompare: TDynArraySortCompare): boolean;
begin
result := InArray(aKey, aArrayValue, iaAdd, aCompare);
end;
function TSynDictionary.AddInArrayForced(const aKey, aArrayValue;
aCompare: TDynArraySortCompare): boolean;
begin
result := InArray(aKey, aArrayValue, iaAddForced, aCompare);
end;
function TSynDictionary.AddOnceInArray(const aKey, aArrayValue;
aCompare: TDynArraySortCompare): boolean;
begin
result := InArray(aKey, aArrayValue, iaFindAndAddIfNotExisting, aCompare);
end;
function TSynDictionary.Find(const aKey; aUpdateTimeOut: boolean): PtrInt;
var
tim: cardinal;
begin
// caller is expected to call fSafe.Lock/Unlock
if self = nil then
result := -1
else
begin
result := fKeys.Hasher.FindOrNew(fKeys.Hasher.HashOne(@aKey), @aKey, nil);
if result < 0 then
result := -1
else if aUpdateTimeOut then
begin
tim := fSafe.Padding[DIC_TIMESEC].VInteger;
if tim > 0 then // inlined fTimeout[result] := GetTimeout
fTimeout[result] := cardinal(GetTickCount64 shr 10) + tim;
end;
end;
end;
function TSynDictionary.FindValue(const aKey; aUpdateTimeOut: boolean;
aIndex: PPtrInt): pointer;
var
ndx: PtrInt;
begin
ndx := Find(aKey, aUpdateTimeOut);
if aIndex <> nil then
aIndex^ := ndx;
if ndx < 0 then
result := nil
else
result := PAnsiChar(fValues.Value^) + ndx * fValues.Info.Cache.ItemSize;
end;
function TSynDictionary.FindValueOrAdd(const aKey; var added: boolean;
aIndex: PPtrInt): pointer;
var
ndx: PtrInt;
tim: cardinal;
begin
tim := fSafe.Padding[DIC_TIMESEC].VInteger; // inlined tim := GetTimeout
if tim <> 0 then
tim := cardinal(GetTickCount64 shr 10) + tim;
ndx := fKeys.FindHashedForAdding(aKey, added);
if added then
begin
fKeys{$ifdef UNDIRECTDYNARRAY}.InternalDynArray{$endif}.
ItemCopyFrom(@aKey, ndx); // fKey[i] := aKey
fValues.Count := ndx + 1; // reserve new place for associated value
if tim > 0 then
fTimeOuts.Add(tim);
end
else if tim > 0 then
fTimeOut[ndx] := tim;
if aIndex <> nil then
aIndex^ := ndx;
result := PAnsiChar(fValues.Value^) + ndx * fValues.Info.Cache.ItemSize;
end;
function TSynDictionary.FindAndCopy(const aKey;
var aValue; aUpdateTimeOut: boolean): boolean;
var
ndx: PtrInt;
begin
result := false;
if self = nil then
exit;
fSafe.ReadLock;
{$ifdef HASFASTTRYFINALLY}
try
{$else}
begin
{$endif HASFASTTRYFINALLY}
ndx := Find(aKey, aUpdateTimeOut);
if ndx >= 0 then
begin
fValues.ItemCopy( // inlined ItemCopyAt(ndx, @aValue)
PAnsiChar(fValues.Value^) + ndx * fValues.Info.Cache.ItemSize, @aValue);
result := true;
end;
{$ifdef HASFASTTRYFINALLY}
finally
{$endif HASFASTTRYFINALLY}
fSafe.ReadUnLock;
end;
end;
function TSynDictionary.FindAndGetElapsedSeconds(const aKey): integer;
var
tim: cardinal;
ndx: PtrInt;
begin
result := -1;
if self = nil then
exit;
tim := ComputeNextTimeOut;
if tim = 0 then
exit;
fSafe.ReadLock;
try
ndx := Find(aKey, {aUpdateTimeOut=}false);
if ndx >= 0 then
result := tim - fTimeOut[ndx];
finally
fSafe.ReadUnLock;
end;
end;
function TSynDictionary.FindAndDeleteDeprecated(const aKey; aSeconds: integer): boolean;
var
tim: cardinal;
ndx: PtrInt;
begin
result := false;
if (self = nil) or
(aSeconds <= 0) then
exit;
tim := ComputeNextTimeOut;
if tim = 0 then
exit;
fSafe.ReadWriteLock;
try
ndx := Find(aKey, {aUpdateTimeOut=}false);
if (ndx >= 0) and
(tim - fTimeOut[ndx] > cardinal(aSeconds)) then
result := DeleteAt(ndx);
finally
fSafe.ReadWriteUnLock;
end;
end;
function TSynDictionary.FindAndExtract(const aKey; var aValue): boolean;
var
ndx: PtrInt;
begin
result := false;
if self = nil then
exit;
fSafe.ReadWriteLock;
try
ndx := fKeys.FindHashedAndDelete(aKey);
if ndx >= 0 then
begin
fSafe.Lock;
fValues.ItemMoveTo(ndx, @aValue); // faster than ItemCopy()
fValues.Delete(ndx);
if fSafe.Padding[DIC_TIMESEC].VInteger > 0 then
fTimeOuts.Delete(ndx);
fSafe.UnLock;
result := true;
end;
finally
fSafe.ReadWriteUnLock;
end;
end;
function TSynDictionary.Exists(const aKey): boolean;
begin
result := false;
if self = nil then
exit;
fSafe.ReadLock;
{$ifdef HASFASTTRYFINALLY}
try
{$else}
begin
{$endif HASFASTTRYFINALLY}
result := fKeys.FindHashed(aKey) >= 0;
{$ifdef HASFASTTRYFINALLY}
finally
{$endif HASFASTTRYFINALLY}
fSafe.ReadUnLock;
end;
end;
function TSynDictionary.ExistsValue(
const aValue; aCompare: TDynArraySortCompare): boolean;
begin
result := false;
if self = nil then
exit;
fSafe.ReadLock;
try
result := fValues.Find(aValue, aCompare) >= 0;
finally
fSafe.ReadUnLock;
end;
end;
procedure TSynDictionary.CopyValues(out Dest; ObjArrayByRef: boolean);
begin
fSafe.ReadLock;
try
fValues.CopyTo(Dest, ObjArrayByRef);
finally
fSafe.ReadUnLock;
end;
end;
function TSynDictionary.ForEach(const OnEach: TOnSynDictionary;
Opaque: pointer; MayModify: boolean): integer;
var
k, v: PAnsiChar;
i, n, ks, vs: PtrInt;
begin
result := 0;
if MayModify then
fSafe.ReadWriteLock
else
fSafe.ReadLock;
try
n := fSafe.Padding[DIC_KEYCOUNT].VInteger;
if (n = 0) or
(not Assigned(OnEach)) then
exit;
k := fKeys.Value^;
ks := fKeys.Info.Cache.ItemSize;
v := fValues.Value^;
vs := fValues.Info.Cache.ItemSize;
for i := 0 to n - 1 do
begin
inc(result);
if not OnEach(k^, v^, i, n, Opaque) then
break;
inc(k, ks);
inc(v, vs);
end;
finally
if MayModify then
fSafe.ReadWriteUnLock
else
fSafe.ReadUnLock;
end;
end;
function TSynDictionary.ForEach(const OnMatch: TOnSynDictionary;
KeyCompare, ValueCompare: TDynArraySortCompare; const aKey, aValue;
Opaque: pointer; MayModify: boolean): integer;
var
k, v: PAnsiChar;
i, n, ks, vs: PtrInt;
begin
if MayModify then
fSafe.ReadWriteLock
else
fSafe.ReadLock;
try
result := 0;
if (not Assigned(OnMatch)) or
(not (Assigned(KeyCompare) or
Assigned(ValueCompare))) then
exit;
n := fSafe.Padding[DIC_KEYCOUNT].VInteger;
k := fKeys.Value^;
ks := fKeys.Info.Cache.ItemSize;
v := fValues.Value^;
vs := fValues.Info.Cache.ItemSize;
for i := 0 to n - 1 do
begin
if (Assigned(KeyCompare) and
(KeyCompare(k^, aKey) = 0)) or
(Assigned(ValueCompare) and
(ValueCompare(v^, aValue) = 0)) then
begin
inc(result);
if not OnMatch(k^, v^, i, n, Opaque) then
break;
end;
inc(k, ks);
inc(v, vs);
end;
finally
if MayModify then
fSafe.ReadWriteUnLock
else
fSafe.ReadUnLock;
end;
end;
procedure TSynDictionary.SetTimeoutAtIndex(aIndex: PtrInt);
var
tim: cardinal;
begin
if cardinal(aIndex) >= cardinal(fSafe.Padding[DIC_KEYCOUNT].VInteger) then
exit;
tim := fSafe.Padding[DIC_TIMESEC].VInteger;
if tim > 0 then
fTimeOut[aIndex] := cardinal(GetTickCount64 shr 10) + tim;
end;
function TSynDictionary.Count: integer;
begin
result := fSafe.Padding[DIC_KEYCOUNT].VInteger;
end;
procedure TSynDictionary.SaveToJson(W: TJsonWriter; EnumSetsAsText: boolean);
var
k, v: RawUtf8;
begin
fSafe.ReadLock;
try
if fSafe.Padding[DIC_KEYCOUNT].VInteger > 0 then
begin
fKeys{$ifdef UNDIRECTDYNARRAY}.InternalDynArray{$endif}.
SaveToJson(k, EnumSetsAsText);
fValues.SaveToJson(v, EnumSetsAsText);
end;
finally
fSafe.ReadUnLock;
end;
W.AddJsonArraysAsJsonObject(pointer(k), pointer(v));
end;
function TSynDictionary.SaveToJson(EnumSetsAsText: boolean): RawUtf8;
var
W: TJsonWriter;
temp: TTextWriterStackBuffer;
begin
W := TJsonWriter.CreateOwnedStream(temp) as TJsonWriter;
try
SaveToJson(W, EnumSetsAsText);
W.SetText(result);
finally
W.Free;
end;
end;
function TSynDictionary.SaveValuesToJson(EnumSetsAsText: boolean;
ReFormat: TTextWriterJsonFormat): RawUtf8;
begin
if self = nil then
begin
result := '';
exit;
end;
fSafe.ReadLock;
try
fValues.SaveToJson(result, EnumSetsAsText, ReFormat);
finally
fSafe.ReadUnLock;
end;
end;
function TSynDictionary.LoadFromJson(const Json: RawUtf8;
CustomVariantOptions: PDocVariantOptions): boolean;
begin
// pointer(Json) is not modified in-place thanks to JsonObjectAsJsonArrays()
result := LoadFromJson(pointer(Json), CustomVariantOptions);
end;
function TSynDictionary.LoadFromJson(Json: PUtf8Char;
CustomVariantOptions: PDocVariantOptions): boolean;
var
k, v: RawUtf8; // private copy of the Json input, expanded as Keys/Values arrays
n: integer;
begin
result := false;
n := JsonObjectAsJsonArrays(Json, k, v);
if n <= 0 then
exit;
fSafe.Lock;
try
if (fKeys.LoadFromJson(pointer(k), nil, CustomVariantOptions) <> nil) and
(fKeys.Count = n) and
(fValues.LoadFromJson(pointer(v), nil, CustomVariantOptions) <> nil) and
(fValues.Count = n) then
begin
SetTimeouts;
fKeys.ForceRehash; // warning: duplicated keys won't be identified
result := true;
end;
finally
fSafe.UnLock;
end;
end;
function TSynDictionary.LoadFromBinary(const binary: RawByteString): boolean;
var
plain: RawByteString;
rdr: TFastReader;
n: integer;
begin
result := false;
plain := fCompressAlgo.Decompress(binary);
if plain = '' then
exit;
rdr.Init(plain);
fSafe.Lock;
try
try
RTTI_BINARYLOAD[rkDynArray](fKeys.Value, rdr, fKeys.Info.Info);
RTTI_BINARYLOAD[rkDynArray](fValues.Value, rdr, fValues.Info.Info);
n := fKeys.Capacity;
if n = fValues.Capacity then
begin
// RTTI_BINARYLOAD[rkDynArray]() did not set the external count
fSafe.Padding[DIC_KEYCOUNT].VInteger := n;
fSafe.Padding[DIC_VALUECOUNT].VInteger := n;
SetTimeouts; // set ComputeNextTimeOut for all items
fKeys.ForceReHash; // optimistic: input from TSynDictionary.SaveToBinary
result := true;
end;
except
result := false;
end;
finally
fSafe.UnLock;
end;
end;
class function TSynDictionary.OnCanDeleteSynPersistentLock(
const aKey, aValue; aIndex: PtrInt): boolean;
begin
result := not TSynPersistentLock(aValue).Safe^.IsLocked;
end;
{$ifndef PUREMORMOT2}
class function TSynDictionary.OnCanDeleteSynPersistentLocked(
const aKey, aValue; aIndex: PtrInt): boolean;
begin
result := not TSynPersistentLocked(aValue).Safe^.IsLocked;
end;
{$endif PUREMORMOT2}
function TSynDictionary.SaveToBinary(
NoCompression: boolean; Algo: TAlgoCompress): RawByteString;
var
tmp: TTextWriterStackBuffer;
W: TBufferWriter;
begin
result := '';
if fSafe.Padding[DIC_KEYCOUNT].VInteger = 0 then
exit;
W := TBufferWriter.Create(tmp{%H-});
try
fSafe.ReadLock;
try
if fSafe.Padding[DIC_KEYCOUNT].VInteger = 0 then
exit;
DynArraySave(pointer(fKeys.Value),
@fSafe.Padding[DIC_KEYCOUNT].VInteger, W, fKeys.Info.Info);
DynArraySave(pointer(fValues.Value),
@fSafe.Padding[DIC_VALUECOUNT].VInteger, W, fValues.Info.Info);
finally
fSafe.ReadUnLock;
end;
result := W.FlushAndCompress(NoCompression, Algo);
finally
W.Free;
end;
end;
{ ********** Custom JSON Serialization }
{ TRttiJson }
function _New_ObjectList(Rtti: TRttiCustom): pointer;
begin
result := TObjectListClass(Rtti.ValueClass).Create;
end;
function _New_InterfacedObjectWithCustomCreate(Rtti: TRttiCustom): pointer;
begin
result := TInterfacedObjectWithCustomCreateClass(Rtti.ValueClass).Create;
end;
function _New_PersistentWithCustomCreate(Rtti: TRttiCustom): pointer;
begin
result := TPersistentWithCustomCreateClass(Rtti.ValueClass).Create;
end;
function _New_Component(Rtti: TRttiCustom): pointer;
begin
result := TComponentClass(Rtti.ValueClass).Create(nil);
end;
function _New_ObjectWithCustomCreate(Rtti: TRttiCustom): pointer;
begin
result := TObjectWithCustomCreateClass(Rtti.ValueClass).Create;
end;
function _New_SynObjectList(Rtti: TRttiCustom): pointer;
begin
result := TSynObjectListClass(Rtti.ValueClass).Create({ownobjects=}true);
end;
function _New_SynLocked(Rtti: TRttiCustom): pointer;
begin
result := TSynLockedClass(Rtti.ValueClass).Create;
end;
function _New_InterfacedCollection(Rtti: TRttiCustom): pointer;
begin
result := TInterfacedCollectionClass(Rtti.ValueClass).Create;
end;
function _New_Collection(Rtti: TRttiCustom): pointer;
begin
if Rtti.CollectionItem = nil then
raise ERttiException.CreateUtf8('% with CollectionItem=nil: please call ' +
'Rtti.RegisterCollection()', [Rtti.ValueClass]);
result := TCollectionClass(Rtti.ValueClass).Create(Rtti.CollectionItem);
end;
function _New_CollectionItem(Rtti: TRttiCustom): pointer;
begin
result := TCollectionItemClass(Rtti.ValueClass).Create(nil);
end;
function _New_List(Rtti: TRttiCustom): pointer;
begin
result := TListClass(Rtti.ValueClass).Create;
end;
function _New_Object(Rtti: TRttiCustom): pointer;
begin
result := Rtti.ValueClass.Create; // non-virtual TObject.Create constructor
end;
function _BC_RawByteString(A, B: PPUtf8Char; Info: PRttiInfo;
out Compared: integer): PtrInt;
begin
{$ifdef CPUINTEL}
compared := SortDynArrayAnsiString(A^, B^); // i386/x86_64 asm uses length
{$else}
compared := SortDynArrayRawByteString(A^, B^); // will use length not #0
{$endif CPUINTEL}
result := SizeOf(pointer);
end;
function _BC_PUtf8Char(A, B: PPUtf8Char; Info: PRttiInfo; out Compared: integer): PtrInt;
begin
compared := StrComp(A^, B^);
result := SizeOf(pointer);
end;
function _BCI_PUtf8Char(A, B: PPUtf8Char; Info: PRttiInfo; out Compared: integer): PtrInt;
begin
compared := StrIComp(A^, B^);
result := SizeOf(pointer);
end;
function _BC_Default(A, B: pointer; Info: PRttiInfo; out Compared: integer): PtrInt;
begin
Compared := ComparePointer(A, B); // weak fallback
result := 0; // not used in TRttiJson.ValueCompare / fCompare[]
end;
function TRttiJson.SetParserType(aParser: TRttiParserType;
aParserComplex: TRttiParserComplexType): TRttiCustom;
var
C: TClass;
n: integer;
begin
// set Name and Flags from Props[]
inherited SetParserType(aParser, aParserComplex);
// set comparison functions
fCompare[true] := RTTI_COMPARE[true][Kind]; // generic comparison
fCompare[false] := RTTI_COMPARE[false][Kind];
if rcfHasRttiOrd in fCache.Flags then
begin
fCompare[true] := @RTTI_ORD_COMPARE[fCache.RttiOrd]; // tuned compare
fCompare[false] := fCompare[true];
end
else if rcfGetInt64Prop in fCache.Flags then
begin
if rcfQWord in fCache.Flags then
fCompare[true] := @_BC_UQWord // QWord compare
else
fCompare[true] := @_BC_SQWord; // Int64 compare
fCompare[false] := fCompare[true];
end
else if Kind = rkFloat then
begin
fCompare[true] := @RTTI_FLOAT_COMPARE[fCache.RttiFloat]; // tuned compare
fCompare[false] := fCompare[true];
end
else if rcfObjArray in fFlags then
begin
fCompare[true] := _BCI_ObjArray; // direct compare
fCompare[false] := _BC_ObjArray;
end
else if aParser = ptPUtf8Char then
begin
fCompare[true] := @_BCI_PUtf8Char; // rkPointer with no RTTI
fCompare[false] := @_BC_PUtf8Char;
end
else if Kind = rkLString then // override default StrComp/StrIComp
if Cache.CodePage >= CP_RAWBLOB then
begin
fCompare[true] := @_BC_RawByteString; // ignore #0 or CaseInsensitive
fCompare[false] := @_BC_RawByteString;
end
else if Cache.CodePage = CP_UTF16 then
begin
fCompare[true] := RTTI_COMPARE[true][rkWString]; // StrCompW
fCompare[false] := RTTI_COMPARE[false][rkWString]; // StrICompW
end;
if not Assigned(fCompare[true]) then
begin
// fallback to ComparePointer(A, B) if not enough RTTI
fCompare[true] := @_BC_Default;
fCompare[false] := @_BC_Default;
end;
// set class serialization and initialization
if aParser = ptClass then
begin
// default JSON serialization of published props
fJsonSave := @_JS_RttiCustom;
fJsonLoad := @_JL_RttiCustom;
// prepare efficient ClassNewInstance() and recognize most parents
C := fValueClass;
repeat
if C = TObjectList then // any branch taken will break below
begin
fNewInstance := @_New_ObjectList;
fJsonSave := @_JS_TObjectList;
fJsonLoad := @_JL_TObjectList;
end
else if C = TInterfacedObjectWithCustomCreate then
fNewInstance := @_New_InterfacedObjectWithCustomCreate
else if C = TPersistentWithCustomCreate then
fNewInstance := @_New_PersistentWithCustomCreate
else if C = TObjectWithCustomCreate then
begin
fNewInstance := @_New_ObjectWithCustomCreate;
// allow any kind of customization for TObjectWithCustomCreate children
// - is used e.g. by TOrm or TObjectWithID
n := Props.Count;
TObjectWithCustomCreateRttiCustomSetParser(
TObjectWithCustomCreateClass(fValueClass), self);
if n <> Props.Count then
fFlags := fFlags + fProps.AdjustAfterAdded; // added a prop
end
else if C = TSynObjectList then
begin
fNewInstance := @_New_SynObjectList;
fJsonSave := @_JS_TSynObjectList;
fJsonLoad := @_JL_TSynObjectList;
end
else if C = TSynLocked then
fNewInstance := @_New_SynLocked
else if C = TComponent then
fNewInstance := @_New_Component
else if C = TInterfacedCollection then
begin
if fValueClass <> C then
begin
fCollectionItem := TInterfacedCollectionClass(fValueClass).GetClass;
fCollectionItemRtti := Rtti.RegisterClass(fCollectionItem);
end;
fNewInstance := @_New_InterfacedCollection;
fJsonSave := @_JS_TCollection;
fJsonLoad := @_JL_TCollection;
end
else if C = TCollection then
begin
fNewInstance := @_New_Collection;
fJsonSave := @_JS_TCollection;
fJsonLoad := @_JL_TCollection;
end
else if C = TCollectionItem then
fNewInstance := @_New_CollectionItem
else if C = TList then
fNewInstance := @_New_List
else if C = TObject then
fNewInstance := @_New_Object
else
begin
// customize JSON serialization
if C = TSynList then
fJsonSave := @_JS_TSynList
else if C = TObjectWithID then
fJsonLoad := @_JL_RttiObjectWithID; // also accepts "RowID" field
C := C.ClassParent; // continue with the parent class
continue;
end;
break; // we reached the root supported class
until false;
case fValueRtlClass of
vcStrings:
begin
fJsonSave := @_JS_TStrings;
fJsonLoad := @_JL_TStrings;
end;
vcList:
fJsonSave := @_JS_TList;
vcRawUtf8List:
begin
fJsonSave := @_JS_TRawUtf8List;
fJsonLoad := @_JL_TRawUtf8List;
end;
end;
end
else if rcfBinary in Flags then
begin
fJsonSave := @_JS_Binary;
fJsonLoad := @_JL_Binary;
end
else
case Kind of
rkChar:
begin
fJsonSave := @_JS_Char;
fJsonLoad := @_JL_Char;
include(fFlags, rcfJsonString);
end;
rkWChar {$ifdef FPC}, rkUChar {$endif}:
begin
fJsonSave := @_JS_WideChar;
fJsonLoad := @_JL_WideChar;
include(fFlags, rcfJsonString);
end;
else
begin
// default well-known serialization
fJsonSave := PTC_JSONSAVE[aParserComplex];
if not Assigned(fJsonSave) then
fJsonSave := PT_JSONSAVE[aParser];
fJsonLoad := PT_JSONLOAD[aParser];
// rkRecordTypes serialization with proper fields RTTI
if (not Assigned(fJsonSave)) and
(Flags * [rcfWithoutRtti, rcfHasNestedProperties] <> []) then
fJsonSave := @_JS_RttiCustom;
if (not Assigned(fJsonLoad)) and
(Flags * [rcfWithoutRtti, rcfHasNestedProperties] <> []) then
fJsonLoad := @_JL_RttiCustom
end;
end;
// TRttiJson.RegisterCustomSerializer() custom callbacks have priority
if Assigned(fJsonWriter.Code) then
fJsonSave := @_JS_RttiCustom;
if Assigned(fJsonReader.Code) then
fJsonLoad := @_JL_RttiCustom;
result := self;
end;
procedure TRttiJson.SetValueClass(aClass: TClass; aInfo: PRttiInfo);
begin
inherited SetValueClass(aClass, aInfo);
if aClass.InheritsFrom(TSynList) then
fValueRtlClass := vcSynList
else if aClass.InheritsFrom(TRawUtf8List) then
fValueRtlClass := vcRawUtf8List;
end;
function TRttiJson.ParseNewInstance(var Context: TJsonParserContext): TObject;
begin
result := fNewInstance(self);
TRttiJsonLoad(fJsonLoad)(@result, Context);
if not Context.Valid then
FreeAndNil(result);
end;
function TRttiJson.ValueCompare(Data, Other: pointer; CaseInsensitive: boolean): integer;
begin
fCompare[CaseInsensitive](Data, Other, Info, result); // at least _BC_Default
end;
function TRttiJson.ValueToVariant(Data: pointer; out Dest: TVarData;
Options: pointer{PDocVariantOptions}): PtrInt;
var
tmp: pointer;
vt: cardinal;
ctx: TGetJsonField;
begin
// see TRttiCustomProp.GetValueDirect
vt := Cache.VarDataVType;
TRttiVarData(Dest).VType := vt;
case vt of
varInt64,
varBoolean:
// rkInteger,rkBool,rkEnumeration,rkSet using VInt64 for unsigned 32-bit
Dest.VInt64 := RTTI_FROM_ORD[Cache.RttiOrd](Data);
varWord64:
// rkInt64, rkQWord
begin
if not (rcfQWord in Cache.Flags) then
TRttiVarData(Dest).VType := varInt64; // fix VType
Dest.VInt64 := PInt64(Data)^;
end;
varSingle:
Dest.VInteger := PInteger(Data)^;
varDate,
varDouble,
varCurrency:
Dest.VInt64 := PInt64(Data)^;
varString:
// rkString
begin
Dest.VAny := nil; // avoid GPF
RawByteString(Dest.VAny) := PRawByteString(Data)^;
end;
varOleStr:
// rkWString
begin
Dest.VAny := nil; // avoid GPF
WideString(Dest.VAny) := PWideString(Data)^;
end;
{$ifdef HASVARUSTRING}
varUString:
// rkUString
begin
Dest.VAny := nil; // avoid GPF
UnicodeString(Dest.VAny) := PUnicodeString(Data)^;
end;
{$endif HASVARUSTRING}
varVariant:
// rkVariant
SetVariantByValue(PVariant(Data)^, PVariant(@Dest)^);
varUnknown:
// rkChar, rkWChar, rkSString converted into temporary RawUtf8
begin
TRttiVarData(Dest).VType := varString;
Dest.VAny := nil; // avoid GPF
Info.StringToUtf8(Data, RawUtf8(Dest.VAny));
end;
else
begin
tmp := nil; // use temporary JSON conversion
SaveJson(Data^, Info, [], RawUtf8(tmp)); // =TJsonWriter.AddTypedJson()
TRttiVarData(Dest).VType := varEmpty;
ctx.Json := tmp;
JsonToAnyVariant(variant(Dest), ctx, Options, true);
FastAssignNew(tmp);
end;
end;
result := Cache.ItemSize;
end;
procedure TRttiJson.ValueLoadJson(Data: pointer; var Json: PUtf8Char;
EndOfObject: PUtf8Char; ParserOptions: TJsonParserOptions;
CustomVariantOptions: PDocVariantOptions; ObjectListItemClass: TClass;
Interning: TRawUtf8Interning);
var
ctxt: TJsonParserContext;
begin
if Assigned(self) then
begin
ctxt.InitParser(Json, self, ParserOptions,
CustomVariantOptions, ObjectListItemClass, Interning);
if Assigned(fJsonLoad) then
// efficient direct Json parsing
TRttiJsonLoad(fJsonLoad)(Data, ctxt)
else
// try if binary serialization was used
ctxt.Valid := ctxt.ParseNext and
(Ctxt.Value <> nil) and
(PCardinal(Ctxt.Value)^ and $ffffff = JSON_BASE64_MAGIC_C) and
BinaryLoadBase64(pointer(Ctxt.Value + 3), Ctxt.ValueLen - 3,
Data, Ctxt.Info.Info, {uri=}false, rkAllTypes, {withcrc=}false);
if ctxt.Valid then
Json := ctxt.Json
else
Json := nil;
end
else
Json := nil;
end;
function TRttiJson.ValueIterateCount(Data: pointer): integer;
begin
result := -1; // unsupported
if Data <> nil then
case Kind of
rkDynArray:
result := length(PByteDynArray(Data)^); // length() is for all types
rkClass:
begin
Data := PPointer(Data)^; // TObject are stored by reference
if Data <> nil then
case ValueRtlClass of
// vcStrings can't be supported since TStrings.Items[] is a getter
vcCollection:
result := TCollection(Data).Count;
vcObjectList,
vcList:
result := TList(Data).Count;
vcSynList:
result := TSynList(Data).Count;
vcRawUtf8List:
result := TRawUtf8List(Data).Count;
end;
end;
end;
end;
function TRttiJson.ValueIterate(Data: pointer; Index: PtrUInt;
out ResultRtti: TRttiCustom): pointer;
begin
result := nil;
if Data <> nil then
case Kind of
rkDynArray:
if Index < PtrUInt(length(PByteDynArray(Data)^)) then
begin
result := PPAnsiChar(Data)^ + (Index * PtrUInt(ArrayRtti.Size));
ResultRtti := ArrayRtti; // also available for (most) unmanaged types
if ArrayRtti.Kind in [rkClass, rkLString] then
result := PPointer(result)^; // resolved as for rkClass below
end;
rkClass:
begin
Data := PPointer(Data)^; // TObject are stored by reference
if Data <> nil then
case ValueRtlClass of
// getter methods do require resolved results
vcCollection:
if Index < PtrUInt(TCollection(Data).Count) then
begin
result := TCollection(Data).Items[Index];
ResultRtti := fCollectionItemRtti;
end;
vcObjectList,
vcList:
if Index < PtrUInt(TList(Data).Count) then
begin
result := TList(Data).List[Index];
if result <> nil then
ResultRtti := Rtti.RegisterClass(PClass(result)^);
end;
vcSynList:
if Index < PtrUInt(TSynList(Data).Count) then
begin
result := TSynList(Data).List[Index];
if result <> nil then
ResultRtti := Rtti.RegisterClass(PClass(result)^);
end;
vcRawUtf8List:
if Index < PtrUInt(TRawUtf8List(Data).Count) then
begin
result := TRawUtf8List(Data).TextPtr[Index];
ResultRtti := PT_RTTI[ptRawUtf8];
exit;
end;
end;
end;
end;
end;
function StrEquA(n, str: PByte): boolean;
var
c: byte;
begin
result := false;
if str = nil then
exit;
repeat
c := n^;
if c <> str^ then // UTF-8 case-sensitive search
exit
else if c = 0 then
break; // n = str
inc(n);
inc(str);
until false;
result := true;
end;
function StrEquAW(n: PByte; str: PWord): boolean;
var
c: cardinal;
begin
result := false;
if str = nil then
exit;
repeat
c := n^;
if c <> str^ then // 7-bit ASCII case-sensitive search
exit
else if c = 0 then
break; // n = str
inc(n);
inc(str);
until false;
result := true;
end;
function TRttiJson.ValueByPath(var Data: pointer; Path: PUtf8Char;
var Temp: TVarData; PathDelim: AnsiChar): TRttiCustom;
var
vt: TSynInvokeableVariantType;
p: PRttiCustomProp;
v: TVarData;
i: PtrInt;
n: ShortString;
begin
result := self;
if (self <> nil) and
(Data <> nil) then
repeat
GetNextItemShortString(Path, @n, PathDelim);
if n[0] = #0 then
break;
if result.Props.CountNonVoid <> 0 then
begin
// search name in rkRecord/rkObject or rkClass properties
p := FindCustomProp(
pointer(result.Props.List), @n[1], ord(n[0]), result.Props.Count);
if (p = nil) or
(p^.OffsetGet < 0) then // we don't support getters yet
break;
result := p^.Value;
inc(PAnsiChar(Data), p.OffsetGet);
if Path = nil then
exit; // reach last path
if result.Kind = rkClass then // stored by reference
Data := PPointer(PAnsiChar(Data) + p.OffsetGet)^;
continue;
end
else
case result.Kind of
rkVariant:
// try TDocVariant/TBsonVariant name lookup
if DocVariantType.FindSynVariantType(PVarData(Data)^.VType, vt) then
begin
TRttiVarData(v).VType := varEmpty; // IntGet() would clear it
vt.IntGet(v, PVarData(Data)^, @n[1], ord(n[0]), {noexc=}true);
if v.VType = varEmpty then
break;
Temp := v;
Data := @Temp;
result := PT_RTTI[ptVariant];
if Path = nil then
exit;
continue;
end;
rkEnumeration,
rkSet:
// check enumeration/set name against the stored value
if Path = nil then // last path only
begin
i := result.Cache.EnumInfo^.GetEnumNameValue(@n[1], ord(n[0]));
if i < 0 then
break;
// enum name match: return a boolean to stop searching
if result.Kind = rkEnumeration then
begin
// true = enum name matches the stored enum value
result.ValueToVariant(Data, v); // calls RTTI_FROM_ORD[]
PBoolean(@Temp)^ := v.VInt64 = i;
end
else
// true = enum name is part of the set value
PBoolean(@Temp)^ := GetBitPtr(Data, i);
Data := @Temp;
result := PT_RTTI[ptBoolean]; // true/false if enum name found
exit;
end;
rkLString:
// case-sensitive comparison of a UTF-8 value with the name
if Path = nil then // last path only
if StrEquA(@n[1], PPByte(Data)^) then // n[1] ends with #0
exit; // return self as non nil value
{$ifdef HASVARUSTRING}
rkUstring,
{$endif HASVARUSTRING}
rkWString:
// case-sensitive comparison of a UTF-16 value with the name
if Path = nil then // last path only
if StrEquAW(@n[1], PPWord(Data)^) then
exit;
end;
break;
until false;
result := nil; // path not found
end;
procedure TRttiJson.RawSaveJson(Data: pointer; const Ctxt: TJsonSaveContext);
begin
TRttiJsonSave(fJsonSave)(Data, Ctxt);
end;
procedure TRttiJson.RawLoadJson(Data: pointer; var Ctxt: TJsonParserContext);
begin
TRttiJsonLoad(fJsonLoad)(Data, Ctxt);
end;
class function TRttiJson.Find(Info: PRttiInfo): TRttiJson;
begin
result := pointer(Rtti.FindType(Info));
end;
class function TRttiJson.RegisterCustomSerializer(Info: PRttiInfo;
const Reader: TOnRttiJsonRead; const Writer: TOnRttiJsonWrite): TRttiJson;
begin
result := Rtti.RegisterType(Info) as TRttiJson;
// (re)set fJsonSave/fJsonLoad
result.fJsonWriter := TMethod(Writer);
result.fJsonReader := TMethod(Reader);
if result.Kind <> rkDynArray then // Reader/Writer are for items, not array
result.SetParserType(result.Parser, result.ParserComplex);
end;
class function TRttiJson.RegisterCustomSerializerClass(ObjectClass: TClass;
const Reader: TOnClassJsonRead; const Writer: TOnClassJsonWrite): TRttiJson;
begin
// without {$M+} ObjectClasss.ClassInfo=nil -> ensure fake RTTI is available
result := Rtti.RegisterClass(ObjectClass) as TRttiJson;
result.fJsonWriter := TMethod(Writer);
result.fJsonReader := TMethod(Reader);
result.SetParserType(ptClass, pctNone);
end;
class function TRttiJson.UnRegisterCustomSerializer(Info: PRttiInfo): TRttiJson;
begin
result := Rtti.RegisterType(Info) as TRttiJson;
result.fJsonWriter.Code := nil; // force reset of the JSON serialization
result.fJsonReader.Code := nil;
if result.Kind <> rkDynArray then // Reader/Writer are for items, not array
result.SetParserType(result.Parser, result.ParserComplex);
end;
class function TRttiJson.UnRegisterCustomSerializerClass(ObjectClass: TClass): TRttiJson;
begin
// without {$M+} ObjectClasss.ClassInfo=nil -> ensure fake RTTI is available
result := Rtti.RegisterClass(ObjectClass) as TRttiJson;
result.fJsonWriter.Code := nil; // force reset of the JSON serialization
result.fJsonReader.Code := nil;
result.SetParserType(result.Parser, result.ParserComplex);
end;
class function TRttiJson.RegisterFromText(DynArrayOrRecord: PRttiInfo;
const RttiDefinition: RawUtf8;
IncludeReadOptions: TJsonParserOptions;
IncludeWriteOptions: TTextWriterWriteObjectOptions): TRttiJson;
begin
result := Rtti.RegisterFromText(DynArrayOrRecord, RttiDefinition) as TRttiJson;
result.fIncludeReadOptions := IncludeReadOptions;
result.fIncludeWriteOptions := IncludeWriteOptions;
end;
procedure _GetDataFromJson(Data: pointer; var Json: PUtf8Char;
EndOfObject: PUtf8Char; Rtti: TRttiCustom;
CustomVariantOptions: PDocVariantOptions; Tolerant: boolean;
Interning: TRawUtf8InterningAbstract);
begin
(Rtti as TRttiJson).ValueLoadJson(Data, Json, EndOfObject,
JSONPARSER_DEFAULTORTOLERANTOPTIONS[Tolerant],
CustomVariantOptions, nil, TRawUtf8Interning(Interning));
end;
{ ********** JSON Serialization Wrapper Functions }
function JsonEncode(const NameValuePairs: array of const): RawUtf8;
var
temp: TTextWriterStackBuffer;
begin
if high(NameValuePairs) < 1 then
// return void JSON object on error
result := '{}'
else
with TJsonWriter.CreateOwnedStream(temp) do
try
AddJsonEscape(NameValuePairs);
SetText(result);
finally
Free
end;
end;
function JsonEncode(const Format: RawUtf8;
const Args, Params: array of const): RawUtf8;
var
temp: TTextWriterStackBuffer;
begin
with TJsonWriter.CreateOwnedStream(temp) do
try
AddJson(Format, Args, Params);
SetText(result);
finally
Free
end;
end;
function JsonEncodeArrayDouble(const Values: array of double): RawUtf8;
var
W: TJsonWriter;
temp: TTextWriterStackBuffer;
begin
W := TJsonWriter.CreateOwnedStream(temp);
try
W.Add('[');
W.AddCsvDouble(Values);
W.Add(']');
W.SetText(result);
finally
W.Free
end;
end;
function JsonEncodeArrayUtf8(const Values: array of RawUtf8): RawUtf8;
var
W: TJsonWriter;
temp: TTextWriterStackBuffer;
begin
W := TJsonWriter.CreateOwnedStream(temp);
try
W.Add('[');
W.AddCsvUtf8(Values);
W.Add(']');
W.SetText(result);
finally
W.Free
end;
end;
function JsonEncodeArrayInteger(const Values: array of integer): RawUtf8;
var
W: TJsonWriter;
temp: TTextWriterStackBuffer;
begin
W := TJsonWriter.CreateOwnedStream(temp);
try
W.Add('[');
W.AddCsvInteger(Values);
W.Add(']');
W.SetText(result);
finally
W.Free
end;
end;
function JsonEncodeArrayOfConst(const Values: array of const;
WithoutBraces: boolean): RawUtf8;
begin
JsonEncodeArrayOfConst(Values, WithoutBraces, result);
end;
procedure JsonEncodeArrayOfConst(const Values: array of const;
WithoutBraces: boolean; var result: RawUtf8);
var
temp: TTextWriterStackBuffer;
begin
if length(Values) = 0 then
if WithoutBraces then
result := ''
else
result := '[]'
else
with TJsonWriter.CreateOwnedStream(temp) do
try
if not WithoutBraces then
Add('[');
AddCsvConst(Values);
if not WithoutBraces then
Add(']');
SetText(result);
finally
Free
end;
end;
procedure JsonEncodeNameSQLValue(const Name, SQLValue: RawUtf8;
var result: RawUtf8);
var
temp: TTextWriterStackBuffer;
begin
if (SQLValue <> '') and
(SQLValue[1] in ['''', '"']) then
// unescape SQL quoted string value into a valid JSON string
with TJsonWriter.CreateOwnedStream(temp) do
try
Add('{', '"');
AddNoJsonEscapeUtf8(Name);
Add('"', ':');
AddQuotedStringAsJson(SQLValue);
Add('}');
SetText(result);
finally
Free;
end
else
// Value is a number or null/true/false
result := '{"' + Name + '":' + SQLValue + '}';
end;
procedure SaveJson(const Value; TypeInfo: PRttiInfo; Options: TTextWriterOptions;
var result: RawUtf8; ObjectOptions: TTextWriterWriteObjectOptions);
var
temp: TTextWriterStackBuffer;
begin
with TJsonWriter.CreateOwnedStream(temp, twoNoSharedStream in Options) do
try
CustomOptions := CustomOptions + Options;
AddTypedJson(@Value, TypeInfo, ObjectOptions);
SetText(result);
finally
Free;
end;
end;
function SaveJson(const Value; TypeInfo: PRttiInfo; EnumSetsAsText: boolean): RawUtf8;
begin
SaveJson(Value, TypeInfo, TEXTWRITEROPTIONS_SETASTEXT[EnumSetsAsText], result);
end;
function SaveJson(const Value; TypeInfo: PRttiInfo): RawUtf8;
begin
SaveJson(Value, TypeInfo, [], Result, []);
end;
function SaveJson(const Value; const TypeName: RawUtf8;
Options: TTextWriterOptions): RawUtf8;
var
nfo: TRttiCustom;
begin
nfo := Rtti.RegisterTypeFromName(TypeName);
if nfo = nil then
result := ''
else
SaveJson(Value, nfo.Cache.Info, Options, result);
end;
{$ifdef FPC}
procedure JsonForDebug(Value: pointer; var TypeName: RawUtf8;
out JsonResultText: RawUtf8);
var
nfo: TRttiCustom;
vmt: PAnsiChar;
begin
if (TypeName <> '') and
(Value <> nil) then
try
nfo := Rtti.RegisterTypeFromName(TypeName); // from Rtti.Register*() functions
{$ifdef HASINTERFACEASTOBJECT} // we target FPC/Lazarus anyway
if (nfo = nil) and
(TypeName[1] = 'I') then // guess class instance from interface variable
nfo := Rtti.RegisterClass(PInterface(Value)^ as TObject);
{$endif HASINTERFACEASTOBJECT}
if (nfo = nil) and
(TypeName[1] = 'T') then
begin
vmt := PPointer(Value)^; // guess if seems to be a real TObject instance
if (vmt <> nil) and
SeemsRealPointer(vmt) and
(PPtrInt(vmt + vmtInstanceSize)^ >= sizeof(vmt)) and
SeemsRealPointer(PPointer(vmt + vmtClassName)^) and
IdemPropName(PShortString(vmt + vmtClassName)^,
pointer(TypeName), length(TypeName)) then
nfo := Rtti.RegisterClass(TClass(pointer(vmt)));
end;
if nfo <> nil then
begin
SaveJson(Value^, nfo.Cache.Info, [twoEnumSetsAsBooleanInRecord],
JsonResultText, [woEnumSetsAsText]);
exit;
end;
except // especially if Value is no class
JsonResultText := ''; // impossible to serialization this value
end;
end;
{$endif FPC}
function RecordSaveJson(const Rec; TypeInfo: PRttiInfo;
EnumSetsAsText: boolean): RawUtf8;
begin
if (TypeInfo <> nil) and
(TypeInfo^.Kind in rkRecordTypes) then
SaveJson(Rec, TypeInfo, TEXTWRITEROPTIONS_SETASTEXT[EnumSetsAsText], result)
else
result := NULL_STR_VAR;
end;
function DynArraySaveJson(const Value; TypeInfo: PRttiInfo;
EnumSetsAsText: boolean): RawUtf8;
begin
if (TypeInfo = nil) or
(TypeInfo^.Kind <> rkDynArray) then
result := NULL_STR_VAR
else if pointer(Value) = nil then
result := '[]'
else
SaveJson(Value, TypeInfo, TEXTWRITEROPTIONS_SETASTEXT[EnumSetsAsText], result);
end;
function DynArrayBlobSaveJson(TypeInfo: PRttiInfo;
BlobValue: pointer; BlobLen: PtrInt): RawUtf8;
var
DynArray: TDynArray;
Value: pointer; // decode BlobValue into a temporary dynamic array
temp: TTextWriterStackBuffer;
begin
Value := nil;
DynArray.Init(TypeInfo, Value);
try
if DynArray.LoadFrom(BlobValue, PAnsiChar(BlobValue) + BlobLen) = nil then
result := ''
else
with TJsonWriter.CreateOwnedStream(temp) do
try
AddDynArrayJson(DynArray);
SetText(result);
finally
Free;
end;
finally
DynArray.Clear; // release temporary memory
end;
end;
function ObjArrayToJson(const aObjArray;
aOptions: TTextWriterWriteObjectOptions): RawUtf8;
var
temp: TTextWriterStackBuffer;
begin
with TJsonWriter.CreateOwnedStream(temp) do
try
if woEnumSetsAsText in aOptions then
CustomOptions := CustomOptions + [twoEnumSetsAsTextInRecord];
AddObjArrayJson(aObjArray, aOptions);
SetText(result);
finally
Free;
end;
end;
function ObjectsToJson(const Names: array of RawUtf8;
const Values: array of TObject;
Options: TTextWriterWriteObjectOptions): RawUtf8;
var
i, n: PtrInt;
temp: TTextWriterStackBuffer;
begin
with TJsonWriter.CreateOwnedStream(temp) do
try
n := high(Names);
BlockBegin('{', Options);
i := 0;
if i <= high(Values) then
repeat
if i <= n then
AddFieldName(Names[i])
else if Values[i] = nil then
AddFieldName(SmallUInt32Utf8[i])
else
AddPropName(ClassNameShort(Values[i])^);
WriteObject(Values[i], Options);
if i = high(Values) then
break;
BlockAfterItem(Options);
inc(i);
until false;
CancelLastComma;
BlockEnd('}', Options);
SetText(result);
finally
Free;
end;
end;
function ObjectToJsonFile(Value: TObject; const JsonFile: TFileName;
Options: TTextWriterWriteObjectOptions): boolean;
var
humanread: boolean;
json: RawUtf8;
begin
humanread := woHumanReadable in Options;
if humanread and
(woHumanReadableEnumSetAsComment in Options) then
humanread := false
else
// JsonReformat() erases comments
exclude(Options, woHumanReadable);
json := ObjectToJson(Value, Options);
if humanread then
// woHumanReadable not working with custom JSON serializers, e.g. T*ObjArray
// TODO: check if this is always the case with our mORMot2 new serialization
result := JsonBufferReformatToFile(pointer(json), JsonFile)
else
result := FileFromString(json, JsonFile);
end;
function GetValueObject(Instance: TObject; const Path: RawUtf8;
out Value: variant): boolean;
var
p: PRttiCustomProp;
begin
result := GetInstanceByPath(Instance, Path, p);
if result then
p^.GetValueVariant(Instance, TVarData(Value), @JSON_[mFastFloat]);
end;
function LoadJsonInPlace(var Value; Json: PUtf8Char; TypeInfo: PRttiInfo;
EndOfObject: PUtf8Char; CustomVariantOptions: PDocVariantOptions;
Tolerant: boolean; Interning: TRawUtf8Interning): PUtf8Char;
begin
TRttiJson(Rtti.RegisterType(TypeInfo)).ValueLoadJson(
@Value, Json, EndOfObject, JSONPARSER_DEFAULTORTOLERANTOPTIONS[Tolerant],
CustomVariantOptions, nil, Interning);
result := Json;
end;
function LoadJson(var Value; const Json: RawUtf8; TypeInfo: PRttiInfo;
EndOfObject: PUtf8Char; CustomVariantOptions: PDocVariantOptions;
Tolerant: boolean; Interning: TRawUtf8Interning): boolean;
var
tmp: TSynTempBuffer;
begin
tmp.Init(Json); // make private copy before in-place decoding
try
result := LoadJsonInPlace(Value, tmp.buf, TypeInfo, EndOfObject,
CustomVariantOptions, Tolerant, Interning) <> nil;
finally
tmp.Done;
end;
end;
function RecordLoadJson(var Rec; Json: PUtf8Char; TypeInfo: PRttiInfo;
EndOfObject: PUtf8Char; CustomVariantOptions: PDocVariantOptions;
Tolerant: boolean; Interning: TRawUtf8Interning): PUtf8Char;
begin
if (TypeInfo = nil) or
not (TypeInfo.Kind in rkRecordTypes) then
raise EJsonException.CreateUtf8('RecordLoadJson: % is not a record',
[TypeInfo.Name]);
TRttiJson(Rtti.RegisterType(TypeInfo)).ValueLoadJson(
@Rec, Json, EndOfObject, JSONPARSER_DEFAULTORTOLERANTOPTIONS[Tolerant],
CustomVariantOptions, nil, Interning);
result := Json;
end;
function RecordLoadJson(var Rec; const Json: RawUtf8; TypeInfo: PRttiInfo;
CustomVariantOptions: PDocVariantOptions; Tolerant: boolean;
Interning: TRawUtf8Interning): boolean;
var
tmp: TSynTempBuffer;
begin
tmp.Init(Json); // make private copy before in-place decoding
try
result := RecordLoadJson(Rec, tmp.buf, TypeInfo, nil,
CustomVariantOptions, Tolerant, Interning) <> nil;
finally
tmp.Done;
end;
end;
function DynArrayLoadJson(var Value; Json: PUtf8Char; TypeInfo: PRttiInfo;
EndOfObject: PUtf8Char; CustomVariantOptions: PDocVariantOptions;
Tolerant: boolean; Interning: TRawUtf8Interning): PUtf8Char;
begin
if (TypeInfo = nil) or
(TypeInfo.Kind <> rkDynArray) then
raise EJsonException.CreateUtf8('DynArrayLoadJson: % is not a dynamic array',
[TypeInfo.Name]);
TRttiJson(Rtti.RegisterType(TypeInfo)).ValueLoadJson(
@Value, Json, EndOfObject, JSONPARSER_DEFAULTORTOLERANTOPTIONS[Tolerant],
CustomVariantOptions, nil, Interning);
result := Json;
end;
function DynArrayLoadJson(var Value; const Json: RawUtf8; TypeInfo: PRttiInfo;
CustomVariantOptions: PDocVariantOptions; Tolerant: boolean;
Interning: TRawUtf8Interning): boolean;
var
tmp: TSynTempBuffer;
begin
tmp.Init(Json); // make private copy before in-place decoding
try
result := DynArrayLoadJson(Value, tmp.buf, TypeInfo, nil,
CustomVariantOptions, Tolerant, Interning) <> nil;
finally
tmp.Done;
end;
end;
function JsonToObject(var ObjectInstance; From: PUtf8Char; out Valid: boolean;
TObjectListItemClass: TClass; Options: TJsonParserOptions;
Interning: TRawUtf8Interning): PUtf8Char;
var
ctxt: TJsonParserContext;
begin
if pointer(ObjectInstance) = nil then
raise ERttiException.Create('JsonToObject(nil)');
ctxt.InitParser(From, Rtti.RegisterClass(TObject(ObjectInstance)), Options,
nil, TObjectListItemClass, Interning);
TRttiJsonLoad(Ctxt.Info.JsonLoad)(@ObjectInstance, ctxt);
Valid := ctxt.Valid;
result := ctxt.Json;
end;
function JsonSettingsToObject(const JsonContent: RawUtf8;
Instance: TObject): boolean;
var
tmp: TSynTempBuffer;
begin
result := false;
if JsonContent = '' then
exit;
tmp.Init(JsonContent); // copy for in-place comment removal and JSON parsing
try
RemoveCommentsFromJson(tmp.buf);
JsonToObject(Instance, tmp.buf, result, nil, JSONPARSER_TOLERANTOPTIONS);
finally
tmp.Done;
end;
end;
function ObjectLoadJson(var ObjectInstance; const Json: RawUtf8;
TObjectListItemClass: TClass; Options: TJsonParserOptions;
Interning: TRawUtf8Interning): boolean;
var
tmp: TSynTempBuffer;
begin
tmp.Init(Json);
if tmp.len <> 0 then
try
JsonToObject(ObjectInstance,
tmp.buf, result, TObjectListItemClass, Options, Interning);
finally
tmp.Done;
end
else
result := false;
end;
function JsonToNewObject(var From: PUtf8Char; var Valid: boolean;
Options: TJsonParserOptions; Interning: TRawUtf8Interning): TObject;
var
ctxt: TJsonParserContext;
begin
ctxt.InitParser(From, nil, Options, nil, nil, Interning);
result := ctxt.ParseNewObject;
Valid := ctxt.Valid;
end;
function PropertyFromJson(Prop: PRttiCustomProp; Instance: TObject;
From: PUtf8Char; var Valid: boolean; Options: TJsonParserOptions;
Interning: TRawUtf8Interning): PUtf8Char;
var
ctxt: TJsonParserContext;
begin
Valid := false;
result := nil;
if (Prop = nil) or
(Prop^.Value.Kind <> rkClass) or
(Instance = nil) then
exit;
ctxt.InitParser(From, Prop^.Value, Options, nil, nil, Interning);
if not JsonLoadProp(pointer(Instance), Prop, ctxt) then
exit;
Valid := true;
result := ctxt.Json;
end;
function UrlDecodeObject(U: PUtf8Char; Upper: PAnsiChar;
var ObjectInstance; Next: PPUtf8Char; Options: TJsonParserOptions): boolean;
var
tmp: RawUtf8;
begin
result := UrlDecodeValue(U, Upper, tmp, Next);
if result then
JsonToObject(ObjectInstance, Pointer(tmp), result, nil, Options);
end;
function JsonFileToObject(const JsonFile: TFileName; var ObjectInstance;
TObjectListItemClass: TClass; Options: TJsonParserOptions;
Interning: TRawUtf8Interning): boolean;
var
tmp: RawUtf8;
begin
tmp := RawUtf8FromFile(JsonFile);
if tmp = '' then
result := false
else
begin
RemoveCommentsFromJson(pointer(tmp));
JsonToObject(ObjectInstance,
pointer(tmp), result, TObjectListItemClass, Options, Interning);
end;
end;
procedure JsonBufferToXML(P: PUtf8Char; const Header, NameSpace: RawUtf8;
out result: RawUtf8);
var
i, j, L: PtrInt;
temp: TTextWriterStackBuffer;
begin
if P = nil then
result := Header
else
with TJsonWriter.CreateOwnedStream(temp) do
try
AddNoJsonEscape(pointer(Header), length(Header));
L := length(NameSpace);
if L <> 0 then
AddNoJsonEscape(pointer(NameSpace), L);
AddJsonToXML(P);
if L <> 0 then
for i := 1 to L do
if NameSpace[i] = '<' then
begin
for j := i + 1 to L do
if NameSpace[j] in [' ', '>'] then
begin
Add('<', '/');
AddStringCopy(NameSpace, i + 1, j - i - 1);
Add('>');
break;
end;
break;
end;
SetText(result);
finally
Free;
end;
end;
function JsonToXML(const Json, Header, NameSpace: RawUtf8): RawUtf8;
var
tmp: TSynTempBuffer;
begin
tmp.Init(Json);
try
JsonBufferToXML(tmp.buf, Header, NameSpace, result);
finally
tmp.Done;
end;
end;
{ ********************* Abstract Classes with Auto-Create-Fields }
function DoRegisterAutoCreateFields(ObjectInstance: TObject): TRttiJson;
begin // sub procedure for smaller code generation in AutoCreateFields/Create
result := Rtti.RegisterAutoCreateFieldsClass(PClass(ObjectInstance)^) as TRttiJson;
end;
function AutoCreateFields(ObjectInstance: TObject): TRttiJson;
var
n: integer;
p: PPRttiCustomProp;
begin
// inlined Rtti.RegisterClass()
{$ifdef NOPATCHVMT}
result := pointer(Rtti.FindType(PPointer(PPAnsiChar(ObjectInstance)^ + vmtTypeInfo)^));
{$else}
result := PPointer(PPAnsiChar(ObjectInstance)^ + vmtAutoTable)^;
{$endif NOPATCHVMT}
if (result = nil) or
not (rcfAutoCreateFields in result.Flags) then
result := DoRegisterAutoCreateFields(ObjectInstance);
p := pointer(result.fAutoCreateInstances);
if p = nil then
exit;
// create all published class (or IDocList/IDocDict) fields
n := PDALen(PAnsiChar(p) - _DALEN)^ + _DAOFF; // length(AutoCreateClasses)
repeat
with p^^ do
PPointer(PAnsiChar(ObjectInstance) + OffsetGet)^ :=
TRttiJson(Value).fNewInstance(Value); // class or interface
inc(p);
dec(n);
until n = 0;
end;
procedure AutoDestroyFields(ObjectInstance: TObject; Info: TRttiJson);
var
n: integer;
p: PPRttiCustomProp;
arr: pointer;
o: TObject;
begin
if Info = nil then
{$ifdef NOPATCHVMT}
Info := pointer(Rtti.FindType(PPointer(PPAnsiChar(ObjectInstance)^ + vmtTypeInfo)^));
{$else}
Info := PPointer(PPAnsiChar(ObjectInstance)^ + vmtAutoTable)^;
{$endif NOPATCHVMT}
// free all published class fields
p := pointer(Info.fAutoDestroyClasses);
if p <> nil then
begin
n := PDALen(PAnsiChar(p) - _DALEN)^ + _DAOFF;
repeat
o := PObject(PAnsiChar(ObjectInstance) + p^^.OffsetGet)^;
if o <> nil then
// inlined o.Free
o.Destroy;
inc(p);
dec(n);
until n = 0;
end;
// release all published T*ObjArray fields
p := pointer(Info.fAutoCreateObjArrays);
if p = nil then
exit;
n := PDALen(PAnsiChar(p) - _DALEN)^ + _DAOFF;
repeat
arr := PPointer(PAnsiChar(ObjectInstance) + p^^.OffsetGet)^;
if arr <> nil then
// inlined ObjArrayClear()
RawObjectsClear(arr, PDALen(PAnsiChar(arr) - _DALEN)^ + _DAOFF);
inc(p);
dec(n);
until n = 0;
end;
{ TPersistentAutoCreateFields }
constructor TPersistentAutoCreateFields.Create;
begin
AutoCreateFields(self);
end; // no need to call the void inherited TPersistentWithCustomCreate
destructor TPersistentAutoCreateFields.Destroy;
begin
AutoDestroyFields(self);
inherited Destroy;
end;
{ TSynAutoCreateFields }
constructor TSynAutoCreateFields.Create;
begin
AutoCreateFields(self);
end; // no need to call the void inherited TSynPersistent
destructor TSynAutoCreateFields.Destroy;
begin
AutoDestroyFields(self);
inherited Destroy;
end;
{ TSynAutoCreateFieldsLocked }
constructor TSynAutoCreateFieldsLocked.Create;
begin
AutoCreateFields(self);
inherited Create; // initialize fSafe := NewSynLocker
end;
destructor TSynAutoCreateFieldsLocked.Destroy;
begin
AutoDestroyFields(self);
inherited Destroy;
end;
{ TInterfacedObjectAutoCreateFields }
constructor TInterfacedObjectAutoCreateFields.Create;
begin
AutoCreateFields(self);
end; // no need to call TInterfacedObjectWithCustomCreate.Create
destructor TInterfacedObjectAutoCreateFields.Destroy;
begin
AutoDestroyFields(self);
inherited Destroy;
end;
{ TInterfacedSerializable }
class function TInterfacedSerializable.SerializableInterface: TRttiCustom;
begin
result := Rtti.FindClass(self).Cache.SerializableInterface;
end;
class function TInterfacedSerializable.Guid: PGuid;
begin
result := SerializableInterface.Cache.InterfaceGuid;
end;
function _New_ISerializable(Rtti: TRttiCustom): pointer;
begin
result := TInterfacedSerializableClass(Rtti.Cache.SerializableClass).Create(nil);
TInterfacedSerializable(result).fRefCount := 1; // inlined GetInterface()
inc(PByte(result), Rtti.Cache.SerializableInterfaceEntryOffset);
end;
class procedure TInterfacedSerializable.NewInterface(out Obj);
begin
pointer(Obj) := _New_ISerializable(SerializableInterface);
end;
class function TInterfacedSerializable.RegisterToRtti(
InterfaceInfo: PRttiInfo): TRttiJson;
var
ent: PInterfaceEntry;
begin
ent := nil;
if (self <> nil) and
InterfaceInfo^.InterfaceImplements(ISerializable) then
ent := GetInterfaceEntry(InterfaceInfo^.InterfaceGuid^); // resolve TGuid
if (ent = nil) or
not InterfaceEntryIsStandard(ent) then
raise ERttiException.CreateUtf8('Unexpected %.RegisterToRtti(%)',
[self, InterfaceInfo^.Name^]);
result := Rtti.RegisterType(InterfaceInfo) as TRttiJson;
result.fCache.SerializableClass := self;
result.fCache.SerializableInterfaceEntryOffset := ent^.IOffset; // get once
TOnRttiJsonRead(result.fJsonReader) := JL;
TOnRttiJsonWrite(result.fJsonWriter) := JS;
result.SetParserType(result.Parser, result.ParserComplex); // needed
result.fNewInstance := @_New_ISerializable;
TRttiJson(Rtti.RegisterClass(self)).fCache.SerializableInterface := result;
end;
procedure TInterfacedSerializable.SetJson(const value: RawUtf8);
var
tmp: TSynTempBuffer;
ctx: TJsonParserContext;
begin
tmp.Init(value);
try
ctx.InitParser(tmp.buf, SerializableInterface, [], nil, nil, nil);
FromJson(ctx);
finally
tmp.Done;
end;
end;
class procedure TInterfacedSerializable.JS(W: TJsonWriter; data: pointer;
options: TTextWriterWriteObjectOptions);
begin
data := PPointer(data)^;
if data = nil then
W.AddNull // avoid GPF if ISerializable = nil
else
ISerializable(data).ToJson(W, options);
end;
class procedure TInterfacedSerializable.JL(var context: TJsonParserContext;
data: pointer);
var
o: TInterfacedSerializable;
i: ^ISerializable absolute data;
begin
if not Assigned(i^) then
begin // inlined Create + GetInterface()
o := Create(context.CustomVariant);
o.fRefCount := 1;
inc(PByte(o), context.Info.Cache.SerializableInterfaceEntryOffset);
PPointer(data)^ := o;
end;
i^.FromJson(context)
end;
function TInterfacedSerializable.GetJson: RawUtf8;
begin
result := ToJson(jsonCompact, []);
end;
function TInterfacedSerializable.ToJson(format: TTextWriterJsonFormat;
options: TTextWriterWriteObjectOptions): RawUtf8;
var
W: TJsonWriter;
temp: TTextWriterStackBuffer;
begin
W := TJsonWriter.CreateOwnedStream(temp);
try
ToJson(W, options);
W.SetText(result, Format);
finally
W.Free;
end;
end;
function TInterfacedSerializable.ToString(format: TTextWriterJsonFormat;
options: TTextWriterWriteObjectOptions): string;
begin
Utf8ToStringVar(ToJson(format, options), result);
end;
{ TInterfacedSerializableAutoCreateFields }
constructor TInterfacedSerializableAutoCreateFields.Create(
options: PDocVariantOptions);
begin
fRttiJson := AutoCreateFields(self);
end;
destructor TInterfacedSerializableAutoCreateFields.Destroy;
begin
AutoDestroyFields(self, fRttiJson);
inherited Destroy;
end;
procedure TInterfacedSerializableAutoCreateFields.ToJson(W: TJsonWriter;
options: TTextWriterWriteObjectOptions);
var
ctx: TJsonSaveContext;
begin
ctx.W := W;
ctx.Info := fRttiJson;
ctx.Options := options + fRttiJson.IncludeWriteOptions;
_JS_RttiCustom(@self, ctx); // all done via known RTTI
end;
procedure TInterfacedSerializableAutoCreateFields.FromJson(
var context: TJsonParserContext);
begin
context.Info := fRttiJson; // from interface RTTI to class RTTI
_JL_RttiCustom(@self, context);
end;
{ TCollectionItemAutoCreateFields }
constructor TCollectionItemAutoCreateFields.Create(Collection: TCollection);
begin
AutoCreateFields(self);
inherited Create(Collection);
end;
destructor TCollectionItemAutoCreateFields.Destroy;
begin
AutoDestroyFields(self);
inherited Destroy;
end;
{ TSynJsonFileSettings }
function TSynJsonFileSettings.AfterLoad: boolean;
begin
result := true; // success
end;
function TSynJsonFileSettings.LoadFromJson(const aJson: RawUtf8;
const aSectionName: RawUtf8): boolean;
begin
if fsoReadIni in fSettingsOptions then
begin
fSectionName := aSectionName;
result := false;
end
else
result := JsonSettingsToObject(aJson, self);
if not result then
begin
result := IniToObject(aJson, self, aSectionName, @JSON_[mFastFloat]);
if result then
begin
fSectionName := aSectionName;
include(fSettingsOptions, fsoWriteIni); // save back as INI
end;
end;
if result then
result := AfterLoad;
end;
function TSynJsonFileSettings.LoadFromFile(const aFileName: TFileName;
const aSectionName: RawUtf8): boolean;
begin
fFileName := aFileName;
fInitialJsonContent := RawUtf8FromFile(aFileName); // may detect BOM
result := LoadFromJson(fInitialJsonContent, aSectionName);
if not result then
fInitialJsonContent := ''; // file was neither valid JSON nor INI: ignore
end;
function TSynJsonFileSettings.FolderName: TFileName;
begin
if self = nil then
result := ''
else
result := ExtractFilePath(fFileName);
end;
procedure TSynJsonFileSettings.SaveIfNeeded;
var
saved: RawUtf8;
begin
if (self = nil) or
(fFileName = '') or
(fsoDisableSaveIfNeeded in fSettingsOptions) then
exit;
if fsoWriteIni in fSettingsOptions then
saved := ObjectToIni(self, fSectionName)
else
saved := ObjectToJson(self, SETTINGS_WRITEOPTIONS);
if saved = fInitialJsonContent then
exit;
FileFromString(saved, fFileName);
fInitialJsonContent := saved;
end;
type // local type definitions for their own RTTI to be found by name
RawUtf8 = type Utf8String;
{$ifdef CPU64}
PtrInt = type Int64;
PtrUInt = type QWord;
{$else}
PtrInt = type integer;
PtrUInt = type cardinal;
{$endif CPU64}
procedure InitializeUnit;
var
i: integer; // not PtrInt since has just been overriden
c: AnsiChar;
{$ifdef FPC} dummy: RawUtf8; {$endif}
begin
// branchless JSON escaping - JSON_ESCAPE_NONE=0 if no JSON escape needed
JSON_ESCAPE[0] := JSON_ESCAPE_ENDINGZERO; // 1 for #0 end of input
for i := 1 to 31 do
JSON_ESCAPE[i] := JSON_ESCAPE_UNICODEHEX; // 2 to escape #1..#31 as \u00xx
JSON_ESCAPE[8] := ord('b'); // others contain the escaped character
JSON_ESCAPE[9] := ord('t');
JSON_ESCAPE[10] := ord('n');
JSON_ESCAPE[12] := ord('f');
JSON_ESCAPE[13] := ord('r');
JSON_ESCAPE[ord('\')] := ord('\');
JSON_ESCAPE[ord('"')] := ord('"');
for c := #32 to #127 do
JSON_UNESCAPE[c] := c;
JSON_UNESCAPE['b'] := #8;
JSON_UNESCAPE['t'] := #9;
JSON_UNESCAPE['n'] := #10;
JSON_UNESCAPE['f'] := #12;
JSON_UNESCAPE['r'] := #13;
JSON_UNESCAPE['u'] := JSON_UNESCAPE_UTF16;
for c := low(c) to high(c) do
begin
if c in [#0, ',', ']', '}', ':'] then
include(JSON_CHARS[c], jcEndOfJsonFieldOr0);
if c in [#0, ',', ']', '}'] then
include(JSON_CHARS[c], jcEndOfJsonFieldNotName);
if c in [#0, #9, #10, #13, ' ', ',', '}', ']'] then
include(JSON_CHARS[c], jcEndOfJsonValueField);
if c in [#0, '"', '\'] then
include(JSON_CHARS[c], jcJsonStringMarker);
if c in ['-', '0'..'9'] then
begin
include(JSON_CHARS[c], jcDigitFirstChar);
JSON_TOKENS[c] := jtFirstDigit;
end;
if c in ['-', '+', '0'..'9', '.', 'E', 'e'] then
include(JSON_CHARS[c], jcDigitFloatChar);
if c in ['_', '0'..'9', 'a'..'z', 'A'..'Z', '$'] then
include(JSON_CHARS[c], jcJsonIdentifierFirstChar);
if c in ['_', '0'..'9', 'a'..'z', 'A'..'Z', '.', '[', ']'] then
include(JSON_CHARS[c], jcJsonIdentifier);
if c in ['_', 'a'..'z', 'A'..'Z', '$'] then
// exclude '0'..'9' as already in jcDigitFirstChar
JSON_TOKENS[c] := jtIdentifierFirstChar;
end;
JSON_TOKENS[#0 ] := jtEndOfBuffer;
JSON_TOKENS['{'] := jtObjectStart;
JSON_TOKENS['}'] := jtObjectStop;
JSON_TOKENS['['] := jtArrayStart;
JSON_TOKENS[']'] := jtArrayStop;
JSON_TOKENS[':'] := jtAssign;
JSON_TOKENS['='] := jtEqual;
JSON_TOKENS[','] := jtComma;
JSON_TOKENS[''''] := jtSingleQuote;
JSON_TOKENS['"'] := jtDoubleQuote;
JSON_TOKENS['t'] := jtTrueFirstChar;
JSON_TOKENS['f'] := jtFalseFirstChar;
JSON_TOKENS['n'] := jtNullFirstChar;
JSON_TOKENS['/'] := jtSlash;
// initialize JSON serialization
Rtti.GlobalClass := TRttiJson; // will ensure Rtti.Count = 0
// now we can register some local type alias to be found by name or ASAP
Rtti.RegisterTypes([TypeInfo(RawUtf8), TypeInfo(PtrInt), TypeInfo(PtrUInt),
TypeInfo(TRawUtf8DynArray), TypeInfo(TIntegerDynArray)]);
// prepare some JSON wrappers
GetDataFromJson := _GetDataFromJson;
InitializeVariantsJson; // from mormot.core.variants
{$ifdef FPC} // we need to call it once so that it is linked to the executable
JsonForDebug(nil, dummy, dummy);
{$endif FPC}
end;
initialization
InitializeUnit;
DefaultJsonWriter := TJsonWriter;
end.