/// generate cross-platform clients code and documentation from a mORMot server
// - this unit is a part of the freeware Synopse mORMot framework,
// licensed under a MPL/GPL/LGPL tri-license; version 1.18
unit mORMotWrappers;

{
    This file is part of Synopse mORMot framework.

    Synopse mORMot framework. Copyright (C) 2022 Arnaud Bouchez
      Synopse Informatique - https://synopse.info

  *** BEGIN LICENSE BLOCK *****
  Version: MPL 1.1/GPL 2.0/LGPL 2.1

  The contents of this file are subject to the Mozilla Public License Version
  1.1 (the "License"); you may not use this file except in compliance with
  the License. You may obtain a copy of the License at
  http://www.mozilla.org/MPL

  Software distributed under the License is distributed on an "AS IS" basis,
  WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
  for the specific language governing rights and limitations under the License.

  The Original Code is Synopse mORMot framework.

  The Initial Developer of the Original Code is Arnaud Bouchez.

  Portions created by the Initial Developer are Copyright (C) 2022
  the Initial Developer. All Rights Reserved.

  Contributor(s) (for this unit and the .mustache templates):
  - EMartin
  - Sabbiolina
  - Sevo
  - Stefan Diestelmann


  Alternatively, the contents of this file may be used under the terms of
  either the GNU General Public License Version 2 or later (the "GPL"), or
  the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
  in which case the provisions of the GPL or the LGPL are applicable instead
  of those above. If you wish to allow use of your version of this file only
  under the terms of either the GPL or the LGPL, and not to allow others to
  use your version of this file under the terms of the MPL, indicate your
  decision by deleting the provisions above and replace them with the notice
  and other provisions required by the GPL or the LGPL. If you do not delete
  the provisions above, a recipient may use your version of this file under
  the terms of any one of the MPL, the GPL or the LGPL.

  ***** END LICENSE BLOCK *****

}

{$I Synopse.inc} // define HASINLINE CPU32 CPU64 OWNNORMTOUPPER

interface

uses
  {$ifdef ISDELPHIXE2}System.SysUtils,{$else}SysUtils,{$endif}
  Classes,
  Contnrs,
  Variants,
  SynCommons,
  SynTable,
  mORMot,
  SynLZ,
  SynMustache;

/// compute the Model information, ready to be exported as JSON
// - will publish the ORM and SOA properties
// - to be used e.g. for client code generation via Mustache templates
// - optional aSourcePath parameter may be used to retrieve additional description
// from the comments of the source code of the unit - this text content may
// also be injected by WRAPPER_RESOURCENAME
// - you may specify a description file (as generated by FillDescriptionFromSource)
function ContextFromModel(aServer: TSQLRestServer;
  const aSourcePath: TFileName=''; const aDescriptions: TFileName=''): variant;

/// compute the information of an interface method, ready to be exported as JSON
// - to be used e.g. for the implementation of the MVC controller via interfaces
// - no description text will be included - use ContextFromModel() if needed
function ContextFromMethod(const method: TServiceMethod): variant;

/// compute the information of an interface, ready to be exported as JSON
// - to be used e.g. for the implementation of the MVC controller via interfaces
// - no description text will be included - use ContextFromModel() if needed
function ContextFromMethods(int: TInterfaceFactory): variant;

/// generate a code/doc wrapper for a given Model and Mustache template content
// - will use all ORM and SOA properties of the supplied server
// - aFileName will be transmitted as {{filename}}, e.g. 'mORMotClient'
// - you should also specify a "fake" HTTP port e.g. 888
// - the template content could be retrieved from a file via StringFromFile()
// - you may optionally retrieve a copy of the data context as TDocVariant
// - this function may be used to generate the client at build time, directly
// from a just built server, in an automated manner
// - you may specify custom helpers (e.g. via TSynMustache.HelpersGetStandardList)
// and retrieve the generated data context after generation (if aContext is
// a TDocVariant object, its fields would be added to the rendering context),
// or a custom description file (as generated by FillDescriptionFromSource)
function WrapperFromModel(aServer: TSQLRestServer;
  const aMustacheTemplate, aFileName: RawUTF8; aPort: integer;
  aHelpers: TSynMustacheHelpers=nil; aContext: PVariant=nil;
  const aDescriptions: TFileName=''): RawUTF8;

/// generate a code/doc wrapper for a given set of types and Mustache template content
// - will use aTables[] to define the ORM information, and supplied aSharedServices[]
// aSharedServicesContract[] for SOA definition of a shared API (expected to
// be called from TSQLRestClientURI.ServiceDefineSharedAPI)
// - aFileName will be transmitted as {{filename}}, e.g. 'mORMotClient'
// - you should also specify a "fake" HTTP port e.g. 888
// - the template content could be retrieved from a file via StringFromFile()
// - you may optionally retrieve a copy of the data context as TDocVariant
// - this function may be used to generate the client at build time, directly
// from a just built server, in an automated manner
// - you may specify custom helpers (e.g. via TSynMustache.HelpersGetStandardList)
// and retrieve the generated data context after generation (if aContext is
// a TDocVariant object, its fields would be added to the rendering context),
// or a custom description file (as generated by FillDescriptionFromSource)
function WrapperForPublicAPI(const aTables: array of TSQLRecordClass;
  const aRoot, aMustacheTemplate, aFileName: RawUTF8; const aSharedServices: array of TGUID;
  const aSharedServicesContract: array of RawUTF8; aResultAsJSONObjectWithoutResult: boolean;
  aPort: integer; aHelpers: TSynMustacheHelpers=nil; aContext: PVariant=nil;
  const aDescriptions: TFileName=''): RawUTF8;

/// instantiate a TSQLRest server instance, including supplied ORM and SOA definitions
// - will use aTables[] to define the ORM information, and supplied aSharedServices[]
// aSharedServicesContract[] for SOA definition of a shared API, implemented as
// abstract classes using TInterfaceStub
// - as used e.g. by WrapperForPublicAPI() to generate some code/doc wrappers
function WrapperFakeServer(const aTables: array of TSQLRecordClass; const aRoot: RawUTF8;
  const aSharedServices: array of TGUID; const aSharedServicesContract: array of RawUTF8;
  aResultAsJSONObjectWithoutResult: boolean): TSQLRestServerFullMemory;

/// you can call this procedure within a method-based service allow
// code-generation of an ORM and SOA client from a web browser
// - you have to specify one or several client *.mustache file paths
// - the first path containing any *.mustache file will be used as templates
// - for instance:
// ! procedure TCustomServer.Wrapper(Ctxt: TSQLRestServerURIContext);
// ! begin // search in the current path
// !   WrapperMethod(Ctxt,['.']);
// ! end;
// - optional SourcePath parameter may be used to retrieve additional description
// from the comments of the source code of the unit
// - you may specify a description file (as generated by FillDescriptionFromSource)
procedure WrapperMethod(Ctxt: TSQLRestServerURIContext; const Path: array of TFileName;
  const SourcePath: TFileName=''; const Descriptions: TFileName='');

/// you can call this procedure to add a 'Wrapper' method-based service
//  to a given server, to allow code-generation of an ORM and SOA client
// - you have to specify one or several client *.mustache file paths
// - the first path containing any *.mustache file will be used as templates
// - if no path is specified (i.e. as []), it will search in the .exe folder
// - the root/wrapper URI will be accessible without authentication (i.e.
// from any plain browser)
// - for instance:
// ! aServer := TSQLRestServerFullMemory.Create(aModel,'test.json',false,true);
// ! AddToServerWrapperMethod(aServer,['..']);
// - optional SourcePath parameter may be used to retrieve additional description
// from the comments of the source code of the unit
procedure AddToServerWrapperMethod(Server: TSQLRestServer; const Path: array of TFileName;
  const SourcePath: TFileName='');

/// you can call this procedure to generate the mORMotServer.pas unit needed
// to compile a given server source code using FPC
// - will locate FPCServer-mORMotServer.pas.mustache in the given Path[] array
// - will write the unit using specified file name or to mORMotServer.pas in the
// current directory if DestFileName is '', or to a sub-folder of the matching
// Path[] if DestFileName starts with '\' (to allow relative folder use)
// - the missing RTTI for records and interfaces would be defined, together
// with some patch comments for published record support (if any) for the ORM
procedure ComputeFPCServerUnit(Server: TSQLRestServer; const Path: array of TFileName;
  DestFileName: TFileName='');

/// you can call this procedure to generate the mORMotInterfaces.pas unit needed
// to register all needed interface RTTI for FPC
// - to circumvent http://bugs.freepascal.org/view.php?id=26774 unresolved issue
// - will locate FPC-mORMotInterfaces.pas.mustache in the given Path[] array
// - will write the unit using specified file name or to mORMotInterfaces.pas in
// the current directory if DestFileName is '', or to a sub-folder of the
// matching Path[] if DestFileName starts with '\' (to allow relative folder use)
// - all used interfaces will be exported, including SOA and mocking/stubing
// types: so you may have to run this function AFTER all process is done
procedure ComputeFPCInterfacesUnit(const Path: array of TFileName;
  DestFileName: TFileName='');

/// rough parsing of the supplied .pas unit, adding the /// commentaries
// into a TDocVariant content
procedure FillDescriptionFromSource(var Descriptions: TDocVariantData;
  const SourceFileName: TFileName);

/// rough parsing of the supplied .pas unit, adding the /// commentaries
// into a compressed binary resource
// - could be then compiled into a WRAPPER_RESOURCENAME resource, e.g. via the
// following .rc source file, assuming ResourceDestFileName='wrapper.desc':
// $ WrappersDescription 10 "wrapper.desc"
// - you may specify a .json file name, for debugging/validation purposes
// - calls internally FillDescriptionFromSource
// - returns the TDocVariant JSON object corresponding to all decriptions
function ResourceDescriptionFromSource(const ResourceDestFileName: TFileName;
  const SourceFileNames: array of TFileName;
  const JsonDestFileName: TFileName = ''): variant;

const
  /// internal Resource name used for bounded description
  // - as generated by FillDescriptionFromSource/ResourceDescriptionFromSource
  // - would be used e.g. by TWrapperContext.Create to inject the available
  // text description from any matching resource
  WRAPPER_RESOURCENAME = 'WrappersDescription';

var
  /// how FillDescriptionFromSource() handles trailing '-' in parsed comments
  // - default is [*], as expected by buggy AsciiDoc format
  DESCRIPTION_ITEM_PREFIX: RawUTF8 = ' [*]';

/// this function would generate a pascal unit defining asynchronous
// (non-blocking) types from a DDD's blocking dual-phase Select/Command service
// - you should specify the services to be converted, as an array - note that
// due to how RTTI is stored by the compiler, all "pure input" parameters should
// be defined explicitly as "const", otherwise the generated class won't match
// - optionally, the TCQRSServiceClass implementing the first Select() phase of
// the blocking service may be specified in queries array; a set of unit names
// in which those TCQRSServiceClass are defined may be specified
// - a Mustache template content should be provided - e.g. asynch.pas.mustache
// as published in SQLite3\DDD\dom folder of the source code repository
// - FileName would contain the resulting unit filename (without the .pas)
// - ProjectName would be written in the main unit comment
// - CallType should be the type used at Domain level to identify each
// asynchronous call - this type should be an integer, or a function may be
// supplied as CallFunction (matching VariantToInteger signature)
// - the first phase of the service should have set Key: KeyType, which would be
// used to create a single shared asynchronous service instance for all keys
// - ExceptionType may be customize, mainly to use a Domain-specific class
// - blocking execution may reach some timeout waiting for the asynchronous
// acknowledgement: a default delay (in ms) is to be supplied, and some custom
// delays may be specified as trios, e.g. ['IMyInterface', 'Method', 10000, ...]
function GenerateAsynchServices(const services: array of TGUID;
  const queries: array of TClass; const units: array of const;
  const additionalcontext: array of const; Template, FileName, ProjectName,
  CallType, CallFunction, Key, KeyType, ExceptionType: RawUTF8;
  DefaultDelay: integer; const CustomDelays: array of const): RawUTF8;


type
  /// the options retrieved during a ExecuteFromCommandLine() call
  TServiceClientCommandLineOptions = set of (cloPrompt,
    cloNoColor, cloPipe, cloHeaders, cloVerbose, cloNoExpand, cloNoBody);

  /// event handler to let ExecuteFromCommandLine call a remote server
  // - before call, aParams.InBody will be set with the expected JSON content
  TOnCommandLineCall = procedure(aOptions: TServiceClientCommandLineOptions;
    const aService: TInterfaceFactory; aMethod: PServiceMethod;
    var aParams: TSQLRestURIParams) of object;

const
  /// help information displayed by ExecuteFromCommandLine() with no command
  EXECUTEFROMCOMMANDLINEHELP =
   ' % help  -> show all services (interfaces)'#13#10 +
   ' % [service] [help]  -> show all methods of a given service'#13#10 +
   ' % [service] [method] help -> show parameters of a given method'#13#10 +
   ' % [options] [service] [method] [parameters] -> call a given method ' +
   {$ifdef MSWINDOWS}
   'with [parameters] being name=value or name=""value with spaces"" or ' +
   'name:={""some"":""json""}' +
   ' and [options] as /nocolor /pipe /headers /verbose /noexpand /nobody';
   {$else}
   'with [parameters] being name=value or name=''"value with spaces"'' or ' +
   'name:=''{"some":"json"}''' +
   ' and [options] as --nocolor --pipe --headers --verbose --noexpand --nobody';
   {$endif MSWINDOWS}

/// command-line SOA remote access to mORMot interface-based services
// - supports the following EXECUTEFROMCOMMANDLINEHELP commands
// - you shall have registered the aServices interface(s) by a previous call to
// the overloaded Get(TypeInfo(IMyInterface)) method or RegisterInterfaces()
// - you may specify an optional description file, as previously generated
// by mORMotWrappers' FillDescriptionFromSource function - a local
// 'WrappersDescription' resource will also be checked
// - to actually call the remote server, aOnGetClient should be supplied
procedure ExecuteFromCommandLine(const aServices: array of TGUID;
  const aOnCall: TOnCommandLineCall; const aDescriptions: TFileName = '');


implementation

type
  /// a cross-platform published property kind
  // - does not match mORMot.pas TSQLFieldType: here we recognize only types
  // which may expect a special behavior in SynCrossPlatformREST.pas unit
  // - should match TSQLFieldKind order in SynCrossPlatformREST.pas
  TCrossPlatformSQLFieldKind = (
    cpkDefault, cpkDateTime, cpkTimeLog, cpkBlob, cpkModTime, cpkCreateTime,
    cpkRecord, cpkVariant);

const
  /// those text values should match TSQLFieldKind in SynCrossPlatformREST.pas
  CROSSPLATFORMKIND_TEXT: array[TCrossPlatformSQLFieldKind] of RawUTF8 = (
    'sftUnspecified', 'sftDateTime', 'sftTimeLog', 'sftBlob', 'sftModTime',
    'sftCreateTime', 'sftRecord', 'sftVariant');

type
  /// types recognized and handled by this mORMotWrappers unit
  TWrapperType = (
    wUnknown,
    wBoolean, wEnum, wSet,
    wByte, wWord, wInteger, wCardinal,
    wInt64, wQWord, wID, wReference, wTimeLog, wModTime, wCreateTime,
    wCurrency, wSingle, wDouble, wDateTime,
    wRawUTF8, wString, wRawJSON, wBlob,
    wGUID, wCustomAnswer, wRecord, wArray, wVariant,
    wObject, wSQLRecord, wInterface, wRecordVersion);
  /// supported languages typesets
  TWrapperLanguage = (
    lngDelphi, lngPascal, lngCS, lngJava, lngTypeScript, lngSwagger);

const
  CROSSPLATFORM_KIND: array[TSQLFieldType] of TCrossPlatformSQLFieldKind = (
 // sftUnknown, sftAnsiText, sftUTF8Text, sftEnumerate, sftSet,    sftInteger,
    cpkDefault, cpkDefault,  cpkDefault,  cpkDefault,   cpkDefault,cpkDefault,
 // sftID,     sftRecord, sftBoolean,sftFloat,  sftDateTime,
    cpkDefault,cpkDefault,cpkDefault,cpkDefault,cpkDateTime,
 // sftTimeLog,sftCurrency,
    cpkTimeLog,cpkDefault,
 // sftObject,  sftVariant, sftNullable, sftBlob, sftBlobDynArray, sftBlobCustom,
    cpkDefault, cpkVariant, cpkVariant,  cpkBlob, cpkDefault,      cpkDefault,
 // sftUTF8Custom,sftMany, sftModTime, sftCreateTime, sftTID,   sftRecordVersion
    cpkRecord,  cpkDefault,cpkModTime, cpkCreateTime, cpkDefault, cpkDefault,
 // sftSessionUserID, sftDateTimeMS, sftUnixTime, sftUnixMStime
    cpkDefault, cpkDateTime, cpkDefault, cpkDefault);

  SIZETODELPHI: array[0..8] of string[7] = (
    'integer','byte','word','integer','integer','int64','int64','int64','int64');

  TYPES_SIZE: array[0..8] of TWrapperType = (
    wInteger,wByte,wWord,wInteger,wInteger,wInt64,wInt64,wInt64,wInt64);

  SWI32 = '{"type":"integer"}';
  SWI64 = '{"type":"integer","format":"int64"}';
  SWD32 = '{"type":"number","format":"float"}';
  SWD64 = '{"type":"number","format":"double"}';

  { TODO: refactor TID and Int64 for JavaScript (integers truncated to 53-bit) }
  TYPES_LANG: array[TWrapperLanguage,TWrapperType] of RawUTF8 = (
   // lngDelphi
   ('', 'Boolean', '', '', 'Byte', 'Word', 'Integer', 'Cardinal', 'Int64',
    'UInt64', 'TID', 'TRecordReference', 'TTimeLog', 'TModTime', 'TCreateTime',
    'Currency', 'Single', 'Double', 'TDateTime', 'RawUTF8','String', 'RawJSON',
    'TSQLRawBlob', 'TGUID', 'TServiceCustomAnswer', '', '', 'Variant', '', '', '',
    'TRecordVersion'),
   // lngPascal
   ('', 'Boolean', '', '', 'Byte', 'Word', 'Integer', 'Cardinal', 'Int64',
    'UInt64', 'TID', 'TRecordReference', 'TTimeLog', 'TModTime', 'TCreateTime',
    'Currency', 'Single', 'Double', 'TDateTime', 'String', 'String', 'Variant',
    'TSQLRawBlob', 'TGUID', 'THttpBody', '', '', 'Variant', '', 'TID', '',
    'TRecordVersion'),
   // lngCS
   ('', 'bool', '', '', 'byte', 'word', 'integer', 'uint', 'long', 'ulong',
    'TID', 'TRecordReference', 'TTimeLog', 'TModTime', 'TCreateTime',
    'decimal', 'single', 'double', 'double', 'string', 'string', 'dynamic',
    'byte[]', 'Guid', 'byte[]', '', '', 'dynamic', '', 'TID', '',
    'TRecordVersion'),
   // lngJava
   ('', 'boolean', '', '', 'byte', 'int', 'int', 'long', 'long', 'long', 'TID',
    'TRecordReference', 'TTimeLog', 'TModTime', 'TCreateTime', 'BigDecimal',
    'single', 'double', 'double', 'String', 'String', 'Object', 'byte[]',
    'String', 'byte[]', '', '', 'Object', '', 'TID', '', 'TRecordVersion'),
   // lngTypeScript
   ('', 'boolean', '', '', 'number', 'number', 'number', 'number', 'number',
    'number', 'mORMot.TID', 'mORMot.TRecordReference', 'mORMot.TTimeLog',
    'mORMot.TModTime', 'mORMot.TCreateTime', 'number', 'number', 'number',
    'mORMot.TDateTime', 'string', 'string', 'any', 'mORMot.TSQLRawBlob',
    'mORMot.TGUID', 'mORMot.THttpBody', '', '', 'any', '', '', '',
    'mORMot.TRecordVersion'),
   // lngSwagger
   ('', '{"type":"boolean"}', '', '', SWI32, SWI32, SWI32, SWI32, SWI64,
    SWI64, SWI64, SWI64, SWI64, SWI64,  SWI64,  SWD64, SWD32, SWD64,
    '{"type":"string","format":"date-time"}', // wDateTime
    '{"type":"string"}','{"type":"string"}',
    '{"type":"object"}', //FIXME! //wRawJSON
    '{"type":"string","format":"binary"}','{"type":"string"}', //wBlob,wGUID
    '', '', '', '', //wCustomAnswer, wRecord, wArray, wVariant
    '', SWI64, '', '' //wObject, wSQLRecord, wInterface, wRecordVersion
   ));
  TYPES_ORM: array[TSQLFieldType] of TWrapperType =
    (wUnknown,        // sftUnknown
     wString,         // sftAnsiText
     wRawUTF8,        // sftUTF8Text
     wEnum,           // sftEnumerate
     wSet,            // sftSet
     wUnknown,        // sftInteger - wUnknown to force exact type
     wSQLRecord,      // sftID
     wReference,      // sftRecord
     wBoolean,        // sftBoolean
     wUnknown,        // sftFloat - wUnknown to force exact type
     wDateTime,       // sftDateTime
     wTimeLog,        // sftTimeLog
     wCurrency,       // sftCurrency
     wObject,         // sftObject
     wVariant,        // sftVariant
     wVariant,        // sftNullable
     wBlob,           // sftBlob
     wBlob,           // sftBlobDynArray
     wRecord,         // sftBlobCustom
     wRecord,         // sftUTF8Custom
     wUnknown,        // sftMany
     wModTime,        // sftModTime
     wCreateTime,     // sftCreateTime
     wID,             // sftID
     wRecordVersion,  // sftRecordVersion
     wID,             // sftSessionUserID
     wDateTime,       // sftDateTimeMS
     wUnknown,        // sftUnixTime
     wUnknown);       // sftUnixMSTime

  TYPES_SIMPLE: array[TJSONCustomParserRTTIType] of TWrapperType = (
  //ptArray, ptBoolean, ptByte, ptCardinal, ptCurrency, ptDouble, ptExtended,
    wArray, wBoolean,   wByte,  wCardinal,  wCurrency,  wDouble,  wDouble,
  //ptInt64, ptInteger, ptQWord, ptRawByteString, ptRawJSON, ptRawUTF8, ptRecord,
    wInt64,  wInteger,  wQWord,  wBlob,           wRawJSON,  wRawUTF8,  wRecord,
  //ptSingle, ptString, ptSynUnicode, ptDateTime, ptDateTimeMS,
    wSingle,  wString,  wRawUTF8,     wDateTime,  wDateTime,
  //ptGUID, ptID, ptTimeLog, ptUnicodeString,
    wGUID,  wID, wTimeLog,   {$ifdef HASVARUSTRING} wRawUTF8, {$endif}
  //ptVariant, ptWideString, ptWord, ptCustom
    wVariant,  wRawUTF8,     wWord,  wUnknown);

  TYPES_SOA: array[TServiceMethodValueType] of TWrapperType = (
    wUnknown,wUnknown,wBoolean,wEnum,wSet,wUnknown,wUnknown,wUnknown,
    wDouble,wDateTime,wCurrency,wRawUTF8,wString,wRawUTF8,wRawUTF8,wRawUTF8,
    wRecord,wVariant,wObject,wRawJSON,wArray,
    wUnknown); // integers are wUnknown to force best type

type
  EWrapperContext = class(ESynException);

  TWrapperContext = class
  protected
    fServer: TSQLRestServer;
    fORM, fRecords,fEnumerates,fSets,fArrays,fUnits,fDescriptions: TDocVariantData;
    fSOA: variant;
    fSourcePath: TFileNameDynArray;
    fHasAnyRecord: boolean;
    function ContextFromInfo(typ: TWrapperType; typName: RawUTF8='';
      typInfo: PTypeInfo=nil): variant;
    function ContextNestedProperties(rtti: TJSONCustomParserRTTI): variant;
    function ContextOneProperty(prop: TJSONCustomParserRTTI): variant;
    function ContextFromMethods(int: TInterfaceFactory): variant;
    function ContextFromMethod(const meth: TServiceMethod): variant;
    function ContextArgsFromMethod(const meth: TServiceMethod): variant;
    procedure AddUnit(const aUnitName: ShortString; addAsProperty: PVariant);
  public
    constructor Create(const aDescriptions: TFileName);
    constructor CreateFromModel(aServer: TSQLRestServer; const aSourcePath: TFileName;
      const aDescriptions: TFileName);
    constructor CreateFromUsedInterfaces(const aDescriptions: TFileName);
    function Context: variant;
  end;

{ TWrapperContext }

constructor TWrapperContext.Create(const aDescriptions: TFileName);
var desc: RawByteString;
begin
  TDocVariant.NewFast([@fORM,@fRecords,@fEnumerates,@fSets,@fArrays,
    @fUnits,@fDescriptions]);
  if aDescriptions<>'' then
    desc := StringFromFile(aDescriptions);
  if desc='' then
    ResourceSynLZToRawByteString(WRAPPER_RESOURCENAME,desc);
  if desc<>'' then
    fDescriptions.InitJSONInPlace(Pointer(desc),JSON_OPTIONS_FAST);
end;

constructor TWrapperContext.CreateFromModel(aServer: TSQLRestServer;
  const aSourcePath, aDescriptions: TFileName);
var t,f,s,n: integer;
    nfoList: TSQLPropInfoList;
    nfo: TSQLPropInfo;
    nfoSQLFieldRTTITypeName: RawUTF8;
    kind: TCrossPlatformSQLFieldKind;
    hasRecord: boolean;
    fields,services: TDocVariantData;
    field,rec: variant;
    srv: TServiceFactoryServer;
    uri: RawUTF8;
    source: TFileName;
    src: PChar;
begin
  Create(aDescriptions);
  if aSourcePath<>'' then begin
    src := pointer(aSourcePath);
    n := 0;
    repeat
      source := GetNextItemString(src,';');
      if (source<>'') and DirectoryExists(source) then begin
        SetLength(fSourcePath,n+1);
        fSourcePath[n] := IncludeTrailingPathDelimiter(source);
        inc(n);
      end;
    until src=nil;
  end;
  fServer := aServer;
  TDocVariant.NewFast([@fields,@services]);
  // compute ORM information
  for t := 0 to fServer.Model.TablesMax do begin
    nfoList := fServer.Model.TableProps[t].Props.Fields;
    fields.Clear;
    fields.Init;
    hasRecord := false;
    for f := 0 to nfoList.Count-1 do begin
      nfo := nfoList.List[f];
      nfoSQLFieldRTTITypeName := nfo.SQLFieldRTTITypeName;
      if nfo.InheritsFrom(TSQLPropInfoRTTI) then
        field := ContextFromInfo(TYPES_ORM[nfo.SQLFieldType],nfoSQLFieldRTTITypeName,
          TSQLPropInfoRTTI(nfo).PropType) else
      if nfo.InheritsFrom(TSQLPropInfoRecordTyped) then begin
        hasRecord := true;
        fHasAnyRecord := true;
        field := ContextFromInfo(wRecord,nfoSQLFieldRTTITypeName,
          TSQLPropInfoRecordTyped(nfo).TypeInfo);
      end else
        raise EWrapperContext.CreateUTF8('Unexpected type % for %.%',
          [nfo,fServer.Model.Tables[t],nfo.Name]);
      kind := CROSSPLATFORM_KIND[nfo.SQLFieldType];
      _ObjAddProps(['index',f+1,'name',nfo.Name,'sql',ord(nfo.SQLFieldType),
        'sqlName',nfo.SQLFieldTypeName^,'typeKind',ord(kind),
        'typeKindName',CROSSPLATFORMKIND_TEXT[kind],'attr',byte(nfo.Attributes)],field);
      if aIsUnique in nfo.Attributes then
        _ObjAddProps(['unique',true],field);
      if nfo.FieldWidth>0 then
        _ObjAddProps(['width',nfo.FieldWidth],field);
      if f<nfoList.Count-1 then
        _ObjAddProps(['comma',','],field) else
        _ObjAddProps(['comma',null],field); // may conflict with rec.comma otherwise
      fields.AddItem(field);
    end;
    with fServer.Model.TableProps[t] do
      rec := _JsonFastFmt(
        '{tableName:?,className:?,classParent:?,fields:?,isInMormotPas:%,unitName:?,comma:%}',
        [NULL_OR_TRUE[(Props.Table=TSQLAuthGroup) or (Props.Table=TSQLAuthUser)],
         NULL_OR_COMMA[t<fServer.Model.TablesMax]],
        [Props.SQLTableName,Props.Table.ClassName,
         Props.Table.ClassParent.ClassName,Variant(fields),
         Props.TableClassType^.UnitName]);
    if hasRecord then
      rec.hasRecords := true;
    fORM.AddItem(rec);
  end;
  // compute SOA information
  if fServer.Services.Count>0 then begin
    for s := 0 to fServer.Services.Count-1 do begin
      srv := fServer.Services.Index(s) as TServiceFactoryServer;
      if fServer.Services.ExpectMangledURI then
        uri := srv.InterfaceMangledURI else
        uri := srv.InterfaceURI;
      with srv do
        rec := _ObjFast(['uri',uri,'interfaceURI',InterfaceURI,
          'interfaceMangledURI',InterfaceMangledURI,
          'interfaceName',InterfaceFactory.InterfaceTypeInfo^.Name,
          'GUID',GUIDToRawUTF8(InterfaceFactory.InterfaceIID),
          'contractExpected',UnQuoteSQLString(ContractExpected),
          'instanceCreation',ord(InstanceCreation),
          'instanceCreationName',GetEnumNameTrimed(
            TypeInfo(TServiceInstanceImplementation),InstanceCreation),
          'methods',ContextFromMethods(InterfaceFactory),
          'bypassAuthentication',ByPassAuthentication,
          'resultAsJSONObject',ResultAsJSONObject,
          'resultAsJSONObjectWithoutResult',ResultAsJSONObjectWithoutResult and
            (InstanceCreation in SERVICE_IMPLEMENTATION_NOID),
          'resultAsXMLObject',ResultAsXMLObject,
          'timeoutSec',TimeoutSec,
          'serviceDescription',fDescriptions.GetValueOrNull(InterfaceFactory.InterfaceName)]);
      if srv.InstanceCreation=sicClientDriven then
        rec.isClientDriven := true;
      services.AddItem(rec);
    end;
    fSOA := _ObjFast(['enabled',True,'services',variant(services),
      'expectMangledURI',fServer.Services.ExpectMangledURI]);
  end;
end;

constructor TWrapperContext.CreateFromUsedInterfaces(const aDescriptions: TFileName);
var interfaces: TSynObjectListLocked;
    services: TDocVariantData;
    i: Integer;
begin
  Create(aDescriptions);
  interfaces := TInterfaceFactory.GetUsedInterfaces;
  if interfaces=nil then
    exit;
  services.InitFast;
  for i := 0 to interfaces.Count-1 do
    services.AddItem(_ObjFast([
      'interfaceName',TInterfaceFactory(interfaces.List[i]).InterfaceTypeInfo^.Name,
      'methods',ContextFromMethods(interfaces.List[i])]));
  fSOA := _ObjFast(['enabled',True,'services',variant(services)]);
end;

function TWrapperContext.ContextArgsFromMethod(const meth: TServiceMethod): variant;
const
  DIRTODELPHI: array[TServiceMethodValueDirection] of string[7] = (
    'const','var','out','result');
  DIRTOSMS: array[TServiceMethodValueDirection] of string[7] = (
    'const','var','var','result');
var a,r: integer;
    arg: variant;
begin
  TDocVariant.NewFast(result);
  r := 0;
  for a := 1 to high(meth.Args) do begin
    with meth.Args[a] do begin
      arg := ContextFromInfo(TYPES_SOA[ValueType],'',ArgTypeInfo);
      arg.argName := ParamName^;
      arg.argType := ArgTypeName^;							  
      arg.dir := ord(ValueDirection);
      arg.dirName := DIRTODELPHI[ValueDirection];
      arg.dirNoOut := DIRTOSMS[ValueDirection]; // no OUT in DWS/SMS -> VAR instead
      if ValueDirection in [smdConst,smdVar] then
        arg.dirInput := true;
      if ValueDirection in [smdVar,smdOut,smdResult] then
        arg.dirOutput := true;
      if ValueDirection=smdResult then
        arg.dirResult := true;
    end;
    if a<meth.ArgsNotResultLast then
      _ObjAddProps(['commaArg','; '],arg);
    if a=high(meth.Args) then
      _ObjAddProps(['isArgLast',true],arg);
    if (meth.args[a].ValueDirection in [smdConst,smdVar]) and (a<meth.ArgsInLast) then
      _ObjAddProps(['commaInSingle',','],arg);
    if (meth.args[a].ValueDirection in [smdVar,smdOut]) and (a<meth.ArgsOutNotResultLast) then
      _ObjAddProps(['commaOut','; '],arg);
    if meth.args[a].ValueDirection in [smdVar,smdOut,smdResult] then begin
      _ObjAddProps(['indexOutResult',UInt32ToUtf8(r)+']'],arg);
      inc(r);
      if a<meth.ArgsOutLast then
        _ObjAddProps(['commaOutResult','; '],arg);
    end;
    TDocVariantData(result).AddItem(arg);
  end;
end;

function TWrapperContext.ContextFromMethod(const meth: TServiceMethod): variant;
const
  VERB_DELPHI: array[boolean] of string[9] = ('procedure','function');
var d: variant;
begin
  with meth do begin
    result := _ObjFast(['methodName',URI, 'methodIndex',ExecutionMethodIndex,
      'verb',VERB_DELPHI[ArgsResultIndex>=0],
      'args',ContextArgsFromMethod(meth),
      'argsOutputCount',ArgsOutputValuesCount]);
    if self<>nil then  begin// can be called as TWraperContext(nil).ContextFromMethod
      d := fDescriptions.GetValueOrNull(InterfaceDotMethodName);
      if VarIsEmptyOrNull(d) then
        RawUTF8ToVariant(InterfaceDotMethodName,d);
      result.methodDescription := d;
    end;
    if ArgsInFirst>=0 then
      result.hasInParams := true;
    if ArgsOutFirst>=0 then begin
      result.hasOutParams := true;
      if ArgsOutNotResultLast>0 then
        result.hasOutNotResultParams := true;
    end;
    if ArgsResultIsServiceCustomAnswer then
      result.resultIsServiceCustomAnswer := true;
    if IsInherited then
      result.isInherited := true;
  end;
end;

function ResourceDescriptionFromSource(const ResourceDestFileName: TFileName;
  const SourceFileNames: array of TFileName; const JsonDestFileName: TFileName): variant;
var desc: TDocVariantData absolute result;
    i: integer;
    json: RawUTF8;
begin
  VarClear(result);
  desc.InitFast;
  for i := 0 to high(SourceFileNames) do
    FillDescriptionFromSource(desc,SourceFileNames[i]);
  json := desc.ToJSON;
  if JsonDestFileName <> '' then
    JSONReformatToFile(json, JsonDestFileName);
  FileFromString(SynLZCompress(json),ResourceDestFileName);
end;

procedure FillDescriptionFromSource(var Descriptions: TDocVariantData;
  const SourceFileName: TFileName);
var desc,typeName,interfaceName: RawUTF8;
    P: PUTF8Char;
    withinCode: boolean;
begin
  P := pointer(StringFromFile(SourceFileName));
  if P=nil then
    exit;
  withinCode := false;
  repeat // rough parsing of the .pas unit file to extract /// description
    P := GotoNextNotSpace(P);
    if IdemPChar(P,'IMPLEMENTATION') then
      break; // only the "interface" section is parsed
    if IdemPChar(P,'{$IFDEF ') then begin
      repeat // just ignore any $ifdef ... $endif
        P := GotoNextLine(P);
        if P=nil then exit;
      until IdemPChar(GotoNextNotSpace(P),'{$ENDIF');
      P := GotoNextLine(P);
      P := GotoNextNotSpace(P);
    end;
    if (P[0]='/') and (P[1]='/') and (P[2]='/') then begin
      desc := GetNextLine(GotoNextNotSpace(P+3),P);
      if desc='' then
        break;
      desc[1] := UpCase(desc[1]);
      repeat
        if P=nil then
          exit;
        P := GotoNextNotSpace(P);
        if IdemPChar(P,'{$IFDEF ') then begin
          repeat // just ignore any $ifdef ... $endif
            P := GotoNextLine(P);
            if P=nil then exit;
          until IdemPChar(GotoNextNotSpace(P),'{$ENDIF');
          P := GotoNextLine(P);
        end else
        if (P[0]='/') and (P[1]='/') then begin
          if P[2]='/' then inc(P,3) else inc(P,2);
          P := GotoNextNotSpace(P);
          if P^ in ['$','!'] then begin
            if not withinCode then begin
              withinCode := true;
              desc := desc+#13#10#13#10'----'; // AsciiDoc source code block
            end;
            desc := desc+#13#10;
            inc(P);
          end else
          if P^='-' then begin
            desc := desc+#13#10#13#10'-'+ DESCRIPTION_ITEM_PREFIX;
            inc(P);
          end else
            desc := desc+' ';
          desc := desc+GetNextLine(P,P);
        end else
          break;
      until false;
      if withinCode then begin
        desc := desc+#13#10'----'; // code block should end the description
        withinCode := false;
      end;
      GetNextItem(P,' ',typeName);
      if P=nil then
        exit;
      if typeName<>'' then
        if P^='=' then begin // simple type (record, array, enumeration, set)
          if Descriptions.GetValueIndex(typeName)<0 then begin
            Descriptions.AddValue(typeName,RawUTF8ToVariant(desc));
            if typeName[1]='I' then
              interfaceName := Copy(typeName,2,128) else
              interfaceName := '';
          end;
        end else
          if interfaceName<>'' then
          if IdemPropNameU(typeName,'function') or
             IdemPropNameU(typeName,'procedure') then
            if GetNextFieldProp(P,typeName) then
              Descriptions.AddValue(interfaceName+'.'+typeName,RawUTF8ToVariant(desc));
    end else
      P := GotoNextLine(P);
  until (P=nil);
end;

procedure TWrapperContext.AddUnit(const aUnitName: ShortString;
  addAsProperty: PVariant);
var unitName: variant;
    i: Integer;
begin
  if (aUnitName='') or IdemPropName(aUnitName,'mORMot') then
    exit;
  RawUTF8ToVariant(@aUnitName[1],ord(aUnitName[0]),unitName);
  if addAsProperty<>nil then
    _ObjAddProps(['unitName',unitName],addAsProperty^);
  if (self=nil) or (fUnits.SearchItemByValue(unitName)>=0) then
    exit; // already registered
  fUnits.AddItem(unitName);
  if fSourcePath=nil then
    exit;
  for i := 0 to high(fSourcePath) do
    FillDescriptionFromSource(fDescriptions,format('%s%s.pas',[fSourcePath[i], aUnitName]));
end;

function TWrapperContext.ContextFromMethods(int: TInterfaceFactory): variant;
var m: integer;
    methods: TDocVariantData; // circumvent FPC -O2 memory leak
begin
  AddUnit(int.InterfaceTypeInfo^.InterfaceUnitName^,nil);
  methods.InitFast;
  for m := 0 to int.MethodsCount-1 do
    methods.AddItem(ContextFromMethod(int.Methods[m]));
  result := variant(methods);
end;

function TWrapperContext.ContextOneProperty(prop: TJSONCustomParserRTTI): variant;
var typ: pointer;
    l,level: integer;
begin
  if prop.InheritsFrom(TJSONCustomParserCustom) then begin
    typ := TJSONCustomParserCustom(prop).CustomTypeInfo;
    if (typ=nil) and (prop.ClassType=TJSONCustomParserCustomSimple) then
      case TJSONCustomParserCustomSimple(prop).KnownType of
        ktBinary: typ := TypeInfo(RawUTF8);
      end;
  end else
    typ := nil;
  result := ContextFromInfo(TYPES_SIMPLE[prop.PropertyType],prop.CustomTypeName,typ);
  if prop.PropertyName<>'' then
    _ObjAddProps(['propName',prop.PropertyName,'fullPropName',prop.FullPropertyName],result);
  level := 0;
  for l := 1 to length(prop.FullPropertyName) do
    if prop.FullPropertyName[l]='.' then
      inc(level);
  if level>0 then
    result.nestedIdentation := StringOfChar(' ',level*2);
  case prop.PropertyType of
  ptRecord: begin
    result.isSimple := null;
    result.nestedRecord := _ObjFast(
      ['nestedRecord',null,'fields',ContextNestedProperties(prop)]);
  end;
  ptArray: begin
    result.isSimple := null;
    if prop.NestedProperty[0].PropertyName='' then
      result.nestedSimpleArray := ContextOneProperty(prop.NestedProperty[0]) else
      result.nestedRecordArray := _ObjFast(
        ['nestedRecordArray',null,'fields',ContextNestedProperties(prop)]);
  end;
  else
    if TDocVariantData(result).GetValueIndex('toVariant')<0 then
      result.isSimple := true else
      result.isSimple := null;
  end;
end;

function TWrapperContext.ContextNestedProperties(rtti: TJSONCustomParserRTTI): variant;
var i: integer;
begin
  SetVariantNull(result);
  if rtti.PropertyType in [ptRecord,ptArray] then begin
    TDocVariant.NewFast(result);
    for i := 0 to high(rtti.NestedProperty) do
      TDocVariantData(result).AddItem(ContextOneProperty(rtti.NestedProperty[i]));
  end;
end;

function TWrapperContext.ContextFromInfo(typ: TWrapperType; typName: RawUTF8;
  typInfo: PTypeInfo): variant;
var typeWrapper: PShortString;
function VarName(lng: TWrapperLanguage): variant;
begin
  if TYPES_LANG[lng,typ]<>'' then
    RawUTF8ToVariant(TYPES_LANG[lng,typ],result) else
    if typName='' then
      SetVariantNull(result) else
      RawUTF8ToVariant(typName,result);
end;
procedure RegisterType(var list: TDocVariantData);
var info: variant;
    item: PTypeInfo;
    itemSize: integer;
    objArray: PClassInstance;
    objArrayType: TWrapperType;
    parser: TJSONRecordAbstract;
begin
  if list.SearchItemByProp('name',typName,false)>=0 then
   exit; // already registered
  if typInfo=nil then
    raise EWrapperContext.CreateUTF8('%.RegisterType(%): no RTTI',[typeWrapper^,typName]);
  case typ of
  wEnum: // full (untrimed) identifier: values[] may be trimmed at mustache level
    info := _JsonFastFmt('{name:?,values:%}',
      [typInfo^.EnumBaseType^.GetEnumNameAllAsJSONArray(false)],[typName]);
  wSet:  // full (untrimed) identifier: values[] may be trimmed at mustache level
    info := _JsonFastFmt('{name:?,values:%}',
      [typInfo^.SetEnumType^.GetEnumNameAllAsJSONArray(false)],[typName]);
  wRecord: begin
    parser := TTextWriter.RegisterCustomJSONSerializerFindParser(typInfo,true);
    if (parser<>nil) and (parser.Root<>nil) and (parser.Root.CustomTypeName<>'') then
      info := _ObjFast(['name',typName,'fields',ContextNestedProperties(parser.Root)]);
  end;
  wArray: begin
    item := typInfo^.DynArrayItemType(@itemSize);
    if item=nil then begin
      if itemSize=SizeOf(pointer) then begin
        objArray := TJSONSerializer.RegisterObjArrayFindType(typInfo);
        if objArray<>nil then begin
          // T*ObjArray -> retrieve item information
          if objArray.ItemCreate=cicTSQLRecord then
            objArrayType := wSQLRecord else
            objArrayType := wObject;
          info := ContextFromInfo(objArrayType,'',objArray^.ItemClass.ClassInfo);
          _ObjAddProps(['isObjArray',true],info);
        end;
      end;
      if VarIsEmptyOrNull(info) and DynArrayItemTypeIsSimpleBinary(typName) then
        info := ContextFromInfo(wRawUTF8);
      if VarIsEmptyOrNull(info) then
        info := ContextFromInfo(TYPES_SIZE[itemSize]);
    end else
      info := ContextFromInfo(wUnknown,'',item);
    info.name := typName;
  end;
  end;
  if not VarIsEmptyOrNull(info) then // null e.g. for a record without custom text definition
    list.AddItem(info);
end;
var siz, i: integer;
    enum: PEnumType;
begin
  if typ=wUnknown then begin
    if typInfo=nil then
      raise EWrapperContext.CreateUTF8('No RTTI nor typ for [%]',[typName]);
    typ := TYPES_ORM[typInfo.GetSQLFieldType];
    if typ=wUnknown then begin
      typ := TYPES_SIMPLE[TJSONCustomParserRTTI.TypeInfoToSimpleRTTIType(typInfo)];
      if typ=wUnknown then
        case typInfo^.Kind of
        tkRecord{$ifdef FPC},tkObject{$endif}:
          typ := wRecord;
        tkInterface:
          typ := wInterface;
        else
          raise EWrapperContext.CreateUTF8('Not enough RTTI for [%]',[typInfo^.Name]);
        end;
    end;
  end;
  if typName='' then begin
    if typInfo<>nil then begin
      TypeInfoToQualifiedName(typInfo,typName);
      i := PosExChar('.',typName);
      if i>0 then
        trimcopy(typName,i+1,maxInt,typName); // trim unit name
    end else
      typName := TYPES_LANG[lngDelphi,typ];
  end;
  if (typ=wRecord) and IdemPropNameU(typName,'TGUID') then
    typ := wGUID else
  if (typ=wRecord) and IdemPropNameU(typName,'TServiceCustomAnswer') then
    typ := wCustomAnswer;
  typeWrapper := GetEnumName(TypeInfo(TWrapperType),ord(typ));
  result := _ObjFast([
    'typeWrapper',typeWrapper^,      'typeSource',typName,
    'typeDelphi',VarName(lngDelphi), 'typePascal',VarName(lngPascal),
    'typeCS',VarName(lngCS),         'typeJava',VarName(lngJava),
    'typeTS',VarName(lngTypeScript), 'typeSwagger',VarName(lngSwagger)]);
  if self=nil then
    exit; // no need to have full info if called e.g. from MVC
  if typInfo<>nil then
  case typInfo^.Kind of
  tkClass:
    AddUnit(typInfo^.ClassType^.UnitName,@result);
  end;
  case typ of
  wBoolean,wByte,wWord,wInteger,wCardinal,wInt64,wQWord,wID,wReference,wTimeLog,
  wModTime,wCreateTime,wSingle,wDouble,wRawUTF8,wString:
    ; // simple types have no special marshalling
  wDateTime:
    _ObjAddProps(['isDateTime',true,'toVariant','DateTimeToIso8601',
      'fromVariant','Iso8601ToDateTime'],result);
  wRecordVersion:
    _ObjAddProps(['isRecordVersion',true],result);
  wCurrency:
    _ObjAddProps(['isCurrency',true],result);
  wVariant:
    _ObjAddProps(['isVariant',true],result);
  wRawJSON:
    _ObjAddProps(['isJson',true],result);
  wEnum: begin
    _ObjAddProps(['isEnum',true,'toVariant','ord','fromVariant','Variant2'+typName],result);
    if self<>nil then
      RegisterType(fEnumerates);
  end;
  wSet: begin
    enum := typInfo^.SetEnumType;
    if enum=nil then
      siz := 0 else
      siz := enum^.SizeInStorageAsSet;
    _ObjAddProps(['isSet',true,'toVariant',SIZETODELPHI[siz],'fromVariant',typName],result);
    if self<>nil then
      RegisterType(fSets);
  end;
  wGUID:
    _ObjAddProps(['toVariant','GUIDToVariant','fromVariant','VariantToGUID'],result);
  wCustomAnswer:
    _ObjAddProps(['toVariant','HttpBodyToVariant','fromVariant','VariantToHttpBody'],result);
  wRecord: begin
     _ObjAddProps(['isRecord',true],result);
     if typInfo<>nil then begin
      _ObjAddProps(['toVariant',typName+'2Variant','fromVariant','Variant2'+typName],result);
      if self<>nil then
        RegisterType(fRecords);
    end;
  end;
  wSQLRecord:
    if fServer.Model.GetTableIndexInheritsFrom(TSQLRecordClass(typInfo^.ClassType^.ClassType))<0 then
      raise EWrapperContext.CreateUTF8('% should be part of the model',[typName]) else
      _ObjAddProps(['isSQLRecord',true],result);
  wObject: begin
   _ObjAddProps(['isObject',true],result);
   if typInfo<>nil then
     _ObjAddProps(['toVariant','ObjectToVariant','fromVariant',typName+'.CreateFromVariant'],result);
  end;
  wArray: begin
    _ObjAddProps(['isArray',true],result);
    if typInfo<>nil then begin
      _ObjAddProps(['toVariant',typName+'2Variant','fromVariant','Variant2'+typName],result);
      if self<>nil then
        RegisterType(fArrays);
    end;
  end;
  wBlob:
    _ObjAddProps(['isBlob',true,
      'toVariant','BlobToVariant','fromVariant','VariantToBlob'],result);
  wInterface:
    _ObjAddProps(['isInterface',true],result);
  else raise EWrapperContext.CreateUTF8('Unexpected type % (%) for [%]',
    [typeWrapper^,ord(typ),typName]);
  end;
end;

function TWrapperContext.Context: variant;
procedure AddDescription(var list: TDocVariantData; const propName,descriptionName: RawUTF8);
var i: integer;
    propValue: RawUTF8;
begin
  if (list.Kind<>dvArray) or (fDescriptions.Count=0) then
    exit;
  for i := 0 to list.Count-1 do
    with _Safe(list.Values[i])^ do
    if GetAsRawUTF8(propName,propValue) then
      AddValue(descriptionName,fDescriptions.GetValueOrNull(propValue));
end;
var s: integer;
    authClass: TClass;
begin
  // compute the Model information as JSON
  result := _ObjFast(['time',NowToString, 'year',CurrentYear,
    'mORMotVersion',SYNOPSE_FRAMEWORK_VERSION,
    'exeVersion',VarStringOrNull(StringToUTF8(ExeVersion.Version.DetailedOrVoid)),
    'exeInfo',ExeVersion.Version.VersionInfo,
    'exeName',ExeVersion.ProgramName,
    'hasorm',fORM.Count>0, 'orm',variant(fORM),
    'soa',fSOA]);
  if fServer<>nil then
    _ObjAddProps(['root',fServer.Model.Root],result);
  if fHasAnyRecord then
    result.ORMWithRecords := true;
  if fRecords.Count>0 then begin
    AddDescription(fRecords,'name','recordDescription');
    result.records := variant(fRecords);
    result.withRecords := true;
    result.withHelpers := true;
  end;
  if fEnumerates.Count>0 then begin
    AddDescription(fEnumerates,'name','enumDescription');
    result.enumerates := variant(fEnumerates);
    result.withEnumerates := true;
    result.withHelpers := true;
  end;
  if fSets.Count>0 then begin
    AddDescription(fSets,'name','setDescription');
    result.sets := variant(fSets);
    result.withsets := true;
    result.withHelpers := true;
  end;
  if fArrays.Count>0 then begin
    result.arrays := variant(fArrays);
    result.withArrays := true;
    result.withHelpers := true;
  end;
  if fUnits.Count>0 then
    result.units := variant(fUnits);
  // add the first registered supported authentication class type as default
  if fServer<>nil then
    for s := 0 to fServer.AuthenticationSchemesCount-1 do begin
      authClass := fServer.AuthenticationSchemes[s].ClassType;
      if (authClass=TSQLRestServerAuthenticationDefault) or
         (authClass=TSQLRestServerAuthenticationNone) then begin
        result.authClass := authClass.ClassName;
        break;
      end;
    end;
end;

function ContextFromModel(aServer: TSQLRestServer;
  const aSourcePath, aDescriptions: TFileName): variant;
begin
  with TWrapperContext.CreateFromModel(aServer,aSourcePath,aDescriptions) do
  try
    result := Context;
  finally
    Free;
  end;
end;

function ContextFromMethod(const method: TServiceMethod): variant;
begin
  result := TWrapperContext(nil).ContextFromMethod(method);
end;

function ContextFromMethods(int: TInterfaceFactory): variant;
begin
  result := TWrapperContext(nil).ContextFromMethods(int);
end;


procedure WrapperMethod(Ctxt: TSQLRestServerURIContext; const Path: array of TFileName;
  const SourcePath, Descriptions: TFileName);
var root, templateName, templateTitle, savedName, templateExt, unitName, template,
    result, host, uri, head: RawUTF8;
    context: variant;
    SR: TSearchRec;
    i, templateFound, port: integer;
begin // URI is e.g. GET http://localhost:888/root/wrapper/Delphi/UnitName.pas
  if (Ctxt.Method<>mGET) or (high(Path)<0) then
    exit;
  templateFound := -1;
  for i := 0 to high(Path) do
    if FindFirst(Path[i]+PathDelim+'*.mustache',faAnyFile,SR)=0 then begin
      templateFound := i;
      break;
    end;
  if templateFound<0 then
    Ctxt.Error('Please copy some .mustache files in the expected folder (e.g. %)',[Path[0]]) else
  try
    context := ContextFromModel(Ctxt.Server,SourcePath,Descriptions);
    context.uri := Ctxt.URIWithoutSignature;
    if llfHttps in Ctxt.Call^.LowLevelFlags then begin
      context.protocol := 'https';
      context.https := true;
    end else
      context.protocol := 'http';
    host := Ctxt.InHeader['host'];
    if host<>'' then
      context.host := host;
    port := GetInteger(pointer(split(host,':',host)));
    if port=0 then
      port := 80;
    context.port := port;
    if IdemPropNameU(Ctxt.URIBlobFieldName,'context') then begin
      Ctxt.ReturnsJson(context,200,{304=}true,twNone,{humanreadable=}true);
      exit;
    end;
    root := Ctxt.Server.Model.Root;
    if Ctxt.URIBlobFieldName='' then begin
      result := '<html><title>mORMot Wrappers</title>'+
        '<body style="font-family:verdana;"><h1>Generated Code/Doc Wrappers</h1>'+
        '<hr><h2>Available Templates:</h2><ul>';
      repeat
        Split(StringToUTF8(SR.Name),'.',templateName,templateExt);
        templateTitle := templateName;
        i := PosExChar('-',templateName);
        if i>0 then begin
          SetLength(templateTitle,i-1);
          savedName := copy(templateName,i+1,maxInt);
        end else
          savedName := 'mORMotClient';
        Split(templateExt,'.',templateExt);
        uri := FormatUTF8('<a href=/%/wrapper/%/%.%',
          [root,templateName,savedName,templateExt]);
        result := FormatUTF8(
         '%<li><b>%</b><br><i>%.%</i>  -  %>download as file</a>  -  '+
         '%.txt>see as text</a> - %.mustache>see template</a></li><br>',
         [result,templateTitle,savedName,templateExt,uri,uri,uri]);
      until FindNext(SR)<>0;
      result := FormatUTF8('%</ul><p>You can also retrieve the corresponding '+
        '<a href=/%/wrapper/context>template context</a>.<hr><p>Generated by a '+
        '<a href=http://mormot.net>Synopse <i>mORMot</i> '+SYNOPSE_FRAMEWORK_VERSION+
        '</a> server.',[result,root]);
      Ctxt.Returns(result,HTTP_SUCCESS,HTML_CONTENT_TYPE_HEADER);
      exit;
    end;
  finally
    FindClose(SR);
  end;
  Split(Ctxt.URIBlobFieldName,'/',templateName,unitName);
  Split(unitName,'.',unitName,templateExt);
  if PosExChar('.',templateExt)>0 then begin // see as text
    if IdemPropNameU(Split(templateExt,'.',templateExt),'mustache') then
      unitName := ''; // force return .mustache
    head := TEXT_CONTENT_TYPE_HEADER;
  end else // download as file
    head := HEADER_CONTENT_TYPE+'application/'+LowerCase(templateExt);
  templateName := templateName+'.'+templateExt+'.mustache';
  template := AnyTextFileToRawUTF8(
    Path[templateFound]+PathDelim+UTF8ToString(templateName),true);
  if template='' then begin
    Ctxt.Error(templateName,HTTP_NOTFOUND);
    exit;
  end;
  if unitName='' then
    result := template else begin
    context.templateName := templateName;
    context.filename := unitName;
    result := TSynMustache.Parse(template).Render(context,nil,
      TSynMustache.HelpersGetStandardList,nil,true);
  end;
  Ctxt.Returns(result,HTTP_SUCCESS,head);
end;

function WrapperFromModel(aServer: TSQLRestServer; const aMustacheTemplate,
  aFileName: RawUTF8; aPort: integer; aHelpers: TSynMustacheHelpers;
  aContext: PVariant; const aDescriptions: TFileName): RawUTF8;
var context: variant;
begin // no context.uri nor context.host here
  context := ContextFromModel(aServer,'',aDescriptions);
  with _Safe(context)^ do begin
    if aPort<>0 then
      I['port'] := aPort;
    U['filename'] := aFileName;
    if aContext<>nil then begin
      AddFrom(aContext^);
      aContext^ := context;
    end;
  end;
  if aHelpers=nil then
    aHelpers := TSynMustache.HelpersGetStandardList;
  result := TSynMustache.Parse(aMustacheTemplate).Render(context,nil,aHelpers,nil,true);
end;

function WrapperFakeServer(const aTables: array of TSQLRecordClass; const aRoot: RawUTF8;
  const aSharedServices: array of TGUID; const aSharedServicesContract: array of RawUTF8;
  aResultAsJSONObjectWithoutResult: boolean): TSQLRestServerFullMemory;
var contract: RawUTF8;
    fake: IInterface;
    i: integer;
begin
  result := TSQLRestServerFullMemory.CreateWithOwnModel(aTables,false,aRoot);
  for i := 0 to high(aSharedServices) do begin
    if i<=high(aSharedServicesContract) then
      contract := aSharedServicesContract[i] else
      contract := '';
    result.ServiceDefine(TInterfaceStub.Create(aSharedServices[i], fake).
       LastInterfacedObjectFake,[aSharedServices[i]],contract).
      ResultAsJSONObjectWithoutResult := aResultAsJSONObjectWithoutResult;
  end;
end;

function WrapperForPublicAPI(const aTables: array of TSQLRecordClass;
  const aRoot, aMustacheTemplate, aFileName: RawUTF8; const aSharedServices: array of TGUID;
  const aSharedServicesContract: array of RawUTF8; aResultAsJSONObjectWithoutResult: boolean;
  aPort: integer; aHelpers: TSynMustacheHelpers; aContext: PVariant; const aDescriptions: TFileName): RawUTF8;
var server: TSQLRestServer;
begin
  server := WrapperFakeServer(aTables,aRoot,aSharedServices,aSharedServicesContract,
    aResultAsJSONObjectWithoutResult);
  try
    result := WrapperFromModel(server,aMustacheTemplate,aFileName,aPort,aHelpers,
      aContext,aDescriptions);
  finally
    server.Free;
  end;
end;



{ TWrapperMethodHook }

type
  TWrapperMethodHook = class(TPersistent)
  public
    SearchPath: TFileNameDynArray;
    SourcePath: TFileName;
  published
    procedure Wrapper(Ctxt: TSQLRestServerURIContext);
  end;

procedure TWrapperMethodHook.Wrapper(Ctxt: TSQLRestServerURIContext);
begin
  WrapperMethod(Ctxt,SearchPath,SourcePath);
end;

procedure ComputeSearchPath(const Path: array of TFileName;
  out SearchPath: TFileNameDynArray);
var i: integer;
begin
  if length(Path)=0 then begin
    SetLength(SearchPath,1);
    SearchPath[0] := ExeVersion.ProgramFilePath; // use .exe path
  end else begin
    SetLength(SearchPath,length(Path));
    for i := 0 to high(Path) do
      SearchPath[i] := ExpandFileName(Path[i]); // also convert \ if needed on FPC
  end;
end;

procedure AddToServerWrapperMethod(Server: TSQLRestServer; const Path: array of TFileName;
  const SourcePath: TFileName);
var hook: TWrapperMethodHook;
begin
  if Server=nil then
    exit;
  hook := TWrapperMethodHook.Create;
  Server.PrivateGarbageCollector.Add(hook); // Server.Free will call hook.Free
  ComputeSearchPath(Path,hook.SearchPath);
  hook.SourcePath := SourcePath;
  Server.ServiceMethodRegisterPublishedMethods('',hook);
  Server.ServiceMethodByPassAuthentication('wrapper');
end;


function FindTemplate(const TemplateName: TFileName; const Path: array of TFileName): TFileName;
var SearchPath: TFileNameDynArray;
    i: integer;
begin
  ComputeSearchPath(Path,SearchPath);
  for i := 0 to High(SearchPath) do begin
    result := IncludeTrailingPathDelimiter(SearchPath[i])+TemplateName;
    if FileExists(result) then
      exit;
  end;
  result := '';
end;

procedure ComputeFPCServerUnit(Server: TSQLRestServer; const Path: array of TFileName;
  DestFileName: TFileName);
var TemplateName: TFileName;
begin
  TemplateName := FindTemplate('FPCServer-mORMotServer.pas.mustache',Path);
  if TemplateName='' then
    exit;
  if DestFileName='' then
    DestFileName := 'mORMotServer.pas' else
    if DestFileName[1]=PathDelim then
      DestFileName := ExtractFilePath(TemplateName)+DestFileName;
  FileFromString(WrapperFromModel(Server,AnyTextFileToRawUTF8(TemplateName,true),
    StringToUTF8(ExtractFileName(DestFileName)),0),DestFileName);
end;

procedure ComputeFPCInterfacesUnit(const Path: array of TFileName;
  DestFileName: TFileName);
const TEMPLATE_NAME = 'FPC-mORMotInterfaces.pas.mustache';
var TemplateName: TFileName;
    ctxt: variant;
begin
  TemplateName := FindTemplate(TEMPLATE_NAME,Path);
  if TemplateName='' then
    exit;
  if DestFileName='' then
    DestFileName := 'mORMotInterfaces.pas' else
    if DestFileName[1]=PathDelim then
      DestFileName := ExtractFilePath(TemplateName)+DestFileName;
  with TWrapperContext.CreateFromUsedInterfaces('') do
  try
    ctxt := Context;
  finally
    Free;
  end;
  ctxt.fileName := GetFileNameWithoutExt(ExtractFileName(DestFileName));
  FileFromString(TSynMustache.Parse(AnyTextFileToRawUTF8(TemplateName,true)).
    Render(ctxt,nil,nil,nil,true),DestFileName);
end;

{$ifdef ISDELPHI20062007}
  {$WARNINGS OFF} // circument Delphi 2007 false positive warning
{$endif}

function GenerateAsynchServices(const services: array of TGUID;
  const queries: array of TClass; const units: array of const;
  const additionalcontext: array of const; Template, FileName, ProjectName,
  CallType, CallFunction, Key, KeyType, ExceptionType: RawUTF8;
  DefaultDelay: integer; const CustomDelays: array of const): RawUTF8;
var
  server: TSQLRestServerFullMemory;
  stub: IInvokable;
  context: variant;
  service, method: PDocVariantData;
  pas, intf, meth: RawUTF8;
  delay: Int64;
  i: integer;
begin
  result := '';
  if high(services) < 0 then
    exit;
  if FileName = '' then
    FileName := 'ServicesAsynch';
  if CallType = '' then
    CallType := 'TBlockingProcessPoolCall';
  if ExceptionType = '' then
    ExceptionType := 'EServiceException';
  server := TSQLRestServerFullMemory.CreateWithOwnModel([]);
  try
    for i := 0 to high(services) do
      server.ServiceDefine(TInterfaceStub.Create(services[i], stub).
        LastInterfacedObjectFake, [services[i]]);
    context := ContextFromModel(server);
    _Safe(context)^.AddNameValuesToObject(['filename', FileName,
      'projectname', ProjectName, 'exeName', ExeVersion.ProgramName,
      'User', ExeVersion.User, 'calltype', CallType, 'callfunction', CallFunction,
      'exception', ExceptionType, 'defaultdelay', DefaultDelay]);
    if high(units) >= 0 then
      _Safe(context.units)^.AddItems(units);
    if Key <> '' then
      _Safe(context)^.AddNameValuesToObject(['asynchkey', Key, 'asynchkeytype', KeyType]);
    _Safe(context)^.AddNameValuesToObject(additionalcontext);
    for i := 0 to high(services) do
      if i < length(queries) then begin
        intf := ToUTF8(TInterfaceFactory.GUID2TypeInfo(services[i])^.Name);
        if _Safe(context.soa.services)^.GetDocVariantByProp(
           'interfaceName', intf, false, service) then
          service^.AddValue('query', queries[i].ClassName)
        else
          raise EWrapperContext.CreateUTF8('CustomDelays: unknown %', [intf]);
      end;
    i := 0;
    while i + 2 <= high(CustomDelays) do begin
      if VarRecToUTF8IsString(CustomDelays[i], intf) and
         VarRecToUTF8IsString(CustomDelays[i + 1], meth) and
         VarRecToInt64(CustomDelays[i + 2], delay) then
        if _Safe(context.soa.services)^.GetDocVariantByProp(
            'interfaceName',intf, false, service) and
           service^.GetAsDocVariantSafe('methods')^.GetDocVariantByProp(
            'methodName', meth, false, method) then
          method^.I['asynchdelay'] := delay
        else
          raise EWrapperContext.CreateUTF8('CustomDelays: unknown %.%', [intf, meth]);
      inc(i, 3);
    end;
    pas := TSynMustache.Parse(Template).Render(context, nil, TSynMustache.HelpersGetStandardList);
    result := StringReplaceAll(pas, ['();', ';', '():', ':']);
//FileFromString(_Safe(context)^.ToJSON('','',jsonUnquotedPropName),FileName+'.json');
  finally
    server.Free;
  end;
end;

{$ifdef ISDELPHI20062007}
  {$WARNINGS ON} // circument Delphi 2007 false positive warning
{$endif}

{ TServiceClientCommandLine }

type
  /// a class implementing ExecuteFromCommandLine()
  TServiceClientCommandLine = class(TSynPersistent)
  protected
    fExe: RawUTF8;
    fOptions: TServiceClientCommandLineOptions;
    fServices: array of TInterfaceFactory;
    fDescriptions: TDocVariantData;
    fOnCall: TOnCommandLineCall;
    procedure ToConsole(const Fmt: RawUTF8; const Args: array of const;
      Color: TConsoleColor = ccLightGray; NoLineFeed: boolean = false);
    function Find(const name: RawUTF8; out service: TInterfaceFactory): boolean;
    procedure WriteDescription(desc: RawUTF8; color: TConsoleColor; firstline: boolean);
    procedure ShowHelp;
    procedure ShowAllServices;
    procedure ShowService(service: TInterfaceFactory);
    procedure ShowMethod(service: TInterfaceFactory; method: PServiceMethod);
    procedure ExecuteMethod(service: TInterfaceFactory; method: PServiceMethod;
      firstparam: integer);
  public
    constructor Create(const aServices: array of TGUID;
      const aOnCall: TOnCommandLineCall;
  const aDescriptions: TFileName); reintroduce;
    procedure Execute;
    destructor Destroy; override;
  end;

{$I-}

procedure TServiceClientCommandLine.ToConsole(const Fmt: RawUTF8;
  const Args: array of const; Color: TConsoleColor; NoLineFeed: boolean);
begin
  ConsoleWrite(FormatUTF8(Fmt, Args), Color, NoLineFeed, cloNoColor in fOptions);
end;

function TServiceClientCommandLine.Find(const name: RawUTF8;
  out service: TInterfaceFactory): boolean;
var
  s: integer;
begin
  for s := 0 to high(fServices) do
    if IdemPropNameU(fServices[s].InterfaceURI, name) then begin
      service := fServices[s];
      result := true;
      exit;
    end;
  result := false;
end;

procedure TServiceClientCommandLine.WriteDescription(desc: RawUTF8;
  color: TConsoleColor; firstline: boolean);
var
  line: RawUTF8;
  P: PUTF8Char;
  i, j, k, l: integer;
begin
  if not(cloNoColor in fOptions) then
    TextColor(color);
  if firstline then
    SetLength(desc, PosExChar(#13, desc) - 1);
  if desc = '' then
    exit;
  P := pointer(desc);
  repeat
    line := GetNextLine(P,P);
    if line = '' then
      continue;
    if line = '----' then begin
      if not(cloNoColor in fOptions) then
        TextColor(ccBrown);
    end
    else begin
      line := StringReplaceAll(line, ['`','', '<<','', '>>','']);
      i := 1;
      repeat
        j := PosEx('[', line, i);
        if j = 0 then
          break;
        k := PosEx('](', line, j + 1);
        if k = 0 then
          break;
        l := PosEx(')', line, k + 2);
        if l = 0 then
          break;
        delete(line, k, l - k + 1);
        delete(line, j, 1);
        i := k;
      until false;
      writeln(line);
    end;
  until P = nil;
end;

procedure TServiceClientCommandLine.ShowHelp;
begin
  ToConsole('% %'#13#10, [fExe, ExeVersion.Version.DetailedOrVoid], ccLightGreen);
  ToConsole(EXECUTEFROMCOMMANDLINEHELP, [fExe, fExe, fExe, fExe]);
end;

procedure TServiceClientCommandLine.ShowAllServices;
var
  i: integer;
begin
  for i := 0 to high(fServices) do begin
    ToConsole('% %', [fExe, fServices[i].InterfaceURI], ccWhite);
    WriteDescription(fDescriptions.U[fServices[i].InterfaceName], ccLightGray, true);
  end;
end;

procedure TServiceClientCommandLine.ShowService(service: TInterfaceFactory);
var
  m: integer;
begin
  ToConsole('% %', [fExe, service.InterfaceURI], ccWhite);
  WriteDescription(fDescriptions.U[service.InterfaceName], ccLightGray, false);
  for m := 0 to service.MethodsCount - 1 do
    with service.Methods[m] do begin
      ToConsole('% % % [parameters]', [fExe, service.InterfaceURI, URI], ccWhite);
      WriteDescription(fDescriptions.U[InterfaceDotMethodName], ccLightGray, true);
    end;
end;

procedure TServiceClientCommandLine.ShowMethod(service: TInterfaceFactory;
  method: PServiceMethod);

  procedure Arguments(input: boolean);
  const IN_OUT: array[boolean] of RawUTF8 = ('OUT', ' IN');
  var
    i: integer;
    line, typ: RawUTF8;
  begin
    ToConsole('%', [IN_OUT[input]], ccDarkGray, {nolinefeed=}true);
    if not input and method^.ArgsResultIsServiceCustomAnswer then
      line := ' is undefined'
    else begin
      line := ' { ';
      i := 0;
      while method^.ArgNext(i, input) do
        with method^.Args[i] do begin
          typ := TYPES_LANG[lngCS, TYPES_SOA[ValueType]];
          if typ = '' then
            ShortStringToAnsi7String(ArgTypeName^, typ);
          line := FormatUTF8('%"%":%, ', [line, ParamName^,  typ]);
        end;
      i := length(line);
      line[i - 1] := ' ';
      line[i] := '}';
    end;
    ToConsole('%', [line], ccDarkGray);
  end;

begin
  ToConsole('% % % [parameters]', [fExe, service.InterfaceURI, method.URI], ccWhite);
  WriteDescription(fDescriptions.U[method.InterfaceDotMethodName], ccLightGray, false);
  if method.ArgsInputValuesCount <> 0 then
    Arguments({input=}true);
  if method.ArgsOutputValuesCount <> 0 then
    Arguments({input=}false);
end;

procedure TServiceClientCommandLine.ExecuteMethod(service: TInterfaceFactory;
  method: PServiceMethod; firstparam: integer);
var
  params, result: RawUTF8;
  i: integer;
  cc: TConsoleColor;
  call: TSQLRestURIParams;
begin
  call.Init;
  if cloPipe in fOptions then
    call.InBody := ConsoleReadBody
  else begin
    for i := firstparam to ParamCount do
      params := FormatUTF8('% %', [params, ParamStr(i)]);
    //writeln(params); // for debugging
    call.InBody := method^.ArgsCommandLineToObject(pointer(params), {input=}true, true);
  end;
  // writeln(call.InBody); exit;
  if [cloVerbose, cloHeaders] * fOptions <> [] then
    ToConsole('POST %', [method.InterfaceDotMethodName], ccLightGray);
  if cloVerbose in fOptions then
    ToConsole('%', [call.InBody], ccLightBlue);
  if not Assigned(fOnCall) then
    raise EServiceException.CreateUTF8('No Client available to call %',
      [method.InterfaceDotMethodName]);
  fOnCall(fOptions, service, method, call); // will set URI + Bearer
  if [cloVerbose, cloHeaders] * fOptions <> [] then
    ToConsole('HTTP %'#13#10'%', [call.OutStatus, call.OutHead], ccLightGray);
  if (call.OutBody <> '') and (call.OutBody[1] = '[') then
    call.OutBody := method^.ArgsArrayToObject(pointer(call.OutBody), false);
  if cloNoBody in fOptions then
    FormatUTF8('% bytes received', [length(call.OutBody)], result)
  else if (cloNoExpand in fOptions) or not call.OutBodyTypeIsJson then
    result := call.OutBody
  else
    JSONBufferReformat(pointer(call.OutBody), result);
  cc := ccWhite;
  if not StatusCodeIsSuccess(call.OutStatus) then
    cc := ccLightRed;
  ToConsole('%', [result], cc, {nofeed=}true);
end;

constructor TServiceClientCommandLine.Create(const aServices: array of TGUID;
  const aOnCall: TOnCommandLineCall; const aDescriptions: TFileName);
var
  desc: RawByteString;
  n, s, i: integer;
begin
  inherited Create;
  fExe := {$ifndef MSWINDOWS}'./' + {$endif} ExeVersion.ProgramName;
  n := length(aServices);
  SetLength(fServices, n);
  s := 0;
  for i := 0 to n - 1 do begin
    fServices[s] := TInterfaceFactory.Get(aServices[i]);
    if fServices[s] <> nil then
      inc(s);
  end;
  if s = 0 then
    raise EServiceException.Create('ExecuteFromCommandLine: no service - did you call RegisterInterfaces()?');
  if s <> n then
    SetLength(fServices, s);
  fOnCall := aOnCall;
  TDocVariant.NewFast([@fDescriptions]);
  if aDescriptions <> '' then
    desc := StringFromFile(aDescriptions);
  if desc = '' then
    ResourceSynLZToRawByteString(WRAPPER_RESOURCENAME, desc);
  if desc <> '' then
    fDescriptions.InitJSONInPlace(pointer(desc), JSON_OPTIONS_FAST);
end;

procedure TServiceClientCommandLine.Execute;
var
  p: array[0..3] of RawUTF8;
  a: PUTF8Char;
  i, j, n, first: integer;
  s: TInterfaceFactory;
  m: PServiceMethod;
begin
  first := 3;
  n := 0;
  for i := 1 to ParamCount do begin
    StringToUTF8(ParamStr(i), p[n]);
    a := pointer(p[n]);
    if a^ in ['-', '/'] then begin
      inc(a);
      if a^ = '-' then
        inc(a);
      j := PTypeInfo(TypeInfo(TServiceClientCommandLineOptions))^.SetEnumType^.
        GetEnumNameTrimedValue(a);
      if j >= 0 then begin
        SetBitPtr(@fOptions, j);
        if n < high(p) then
          inc(first);
        continue;
      end;
      raise EServiceException.CreateUTF8('%.Execute: unknown option [%]', [self, p[n]]);
    end;
    if n < high(p) then
      inc(n);
  end;
  case n of
  0:
    ShowHelp;
  1:
    if Find(p[0], s) then
      ShowService(s)
    else
      ShowAllServices;
  else
    if Find(p[0], s) then begin
      m := s.FindMethod(p[1]);
      if m = nil then
        ShowService(s)
      else
        if IdemPropNameU(p[2], 'help') or
            ((m^.ArgsInputValuesCount <> 0) and (PosExChar('=', p[2]) = 0)) then
          ShowMethod(s, m)
        else
          ExecuteMethod(s, m, first);
    end
    else
      ShowAllServices;
  end;
  ToConsole('', [], ccDarkGray);
end;

destructor TServiceClientCommandLine.Destroy;
begin
  inherited Destroy;
end;

{$I+}

procedure ExecuteFromCommandLine(const aServices: array of TGUID;
  const aOnCall: TOnCommandLineCall; const aDescriptions: TFileName);
begin
  with TServiceClientCommandLine.Create(aServices, aOnCall, aDescriptions) do
    try
      try
        Execute;
      except
        on E: Exception do
          ConsoleShowFatalException(E, {waitforkey=}false);
      end;
    finally
      Free;
    end;
end;

end.

