{ This file is part of restemplate. Copyright (C) 2015 Andreas Schneider restemplate is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. restemplate is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with restemplate. If not, see . } unit URestemplateApp; {$mode objfpc}{$H+} interface uses Classes, SysUtils, CustApp, IniFiles, jsonparser, JTemplate, UFilter, IdHTTP; type { TRestHTTP } TRestHTTP = class(TIdHTTP) public procedure Perform(AMethod, AURL: String; ARequest, AResponse: TStream); end; { TRestRequest } TRestRequest = class(TIdHTTPRequest) public procedure Prepare; end; { TRestemplateApplication } TRestemplateApplication = class(TCustomApplication) protected FConfigDir: String; FTemplateDir: String; FSessionIni: TIniFile; FTemplateName: String; FParser: TJTemplateParser; FHttp: TRestHTTP; FRequest: TRestRequest; FContent: TStringList; FFormFields: TStrings; FFilters: TFilterList; FBeautify: Boolean; FExpectations: TStringList; FURL: String; FMethod: String; //Main procedure DoRun; override; //Command processing function ProcessCommand(ALine: String): Boolean; procedure CmdAskUser(AName: String); procedure CmdHeader(AHeader: String); procedure CmdBasicAuth(AData: String); procedure CmdHighlight(AData: String); procedure CmdFormField(AData: String); procedure CmdProxy(AData: String); procedure CmdProxyAuth(AData: String); procedure CmdGenerate(AData: String); procedure CmdExpect(AData: String); procedure ProcessCall(AURL: String); //Helper procedure WriteHelp; procedure ListTemplates; procedure WriteContent; procedure CheckExpectations; public constructor Create; overload; destructor Destroy; override; end; implementation uses strutils, crt, UCRTHelper, fpjson, DOM, XMLRead, XMLWrite, vinfo, IdCompressorZLib, IdHeaderList, IdGlobalProtocols, xmlxsdparser; type TContentType = (ctOther, ctJSON, ctXML); function IdentifyContentType(AString: String; out ContentType: TContentType): Boolean; begin if Pos('application/json', AString) > 0 then ContentType := ctJSON else if Pos('application/xml', AString) > 0 then ContentType := ctXML else ContentType := ctOther; Result := (ContentType <> ctOther); end; { TRestRequest } procedure TRestRequest.Prepare; var originalHeaders: TIdHeaderList; i: Integer; begin //Parse RawHeaders, since they have precendence and should not be overridden! ProcessHeaders; //SetHeaders clears RawHeaders and therefore discards our custom headers. //We work around that by remembering what is set. originalHeaders := TIdHeaderList.Create(QuoteHTTP); try originalHeaders.Assign(FRawHeaders); //Now we call SetHeaders to be able to find out what was custom and what //was known. SetHeaders; for i := 0 to originalHeaders.Count - 1 do begin if FRawHeaders.IndexOfName(originalHeaders.Names[i]) < 0 then FCustomHeaders.Add(originalHeaders.Strings[i]); end; finally originalHeaders.Free; end; end; { TRestHTTP } procedure TRestHTTP.Perform(AMethod, AURL: String; ARequest, AResponse: TStream ); begin DoRequest(AMethod, AURL, ARequest, AResponse, []); end; { TRestemplateApplication } procedure TRestemplateApplication.DoRun; var templateFile: String; line: String; data: TextFile; commandMode: Boolean; begin if HasOption('l', 'list') then begin ListTemplates; Terminate; Exit; end; if HasOption('h', 'help') or (ParamCount < 1) or (ParamStr(ParamCount)[1] = '-') then begin WriteHelp; Terminate; Exit; end; templateFile := FTemplateDir + ParamStr(ParamCount) + '.rest'; if FileExists(ParamStr(ParamCount)) then begin AssignFile(data, ParamStr(ParamCount)); FTemplateName := ExtractFileName(ParamStr(ParamCount)); if AnsiEndsStr('.rest', FTemplateName) then FTemplateName := Copy(FTemplateName, 1, Length(FTemplateName) - 5); end else if FileExists(templateFile) then begin AssignFile(data, templateFile); FTemplateName := ParamStr(ParamCount); end else begin writeln('Template ', ParamStr(ParamCount), ' not found!'); Terminate; Exit; end; Reset(data); FBeautify := False; //By default, we don't want to manipulate output. commandMode := True; try while not EOF(data) do begin ReadLn(data, line); if commandMode and (line <> '') and (line[1] <> '#') and not ProcessCommand(line) then begin commandMode := False; end; if not commandMode then FContent.Add(line); end; if FURL <> '' then ProcessCall(FURL); finally CloseFile(data); end; Terminate; end; function TRestemplateApplication.ProcessCommand(ALine: String): Boolean; begin Result := False; //The first char after "Ask" is treated as "mode". "#" means password. if AnsiStartsStr('Ask', ALine) then begin Result := True; CmdAskUser(Copy(ALine, 4, Length(ALine))); end else if AnsiStartsStr('Header ', ALine) then begin Result := True; CmdHeader(Copy(ALine, 8, Length(ALine))); end else if AnsiStartsStr('Method ', ALine) then begin Result := True; FMethod := Copy(ALine, 8, Length(ALine)); end else // We don't include the space here, since we also interpret this following // char as separator for username and password! (In case the username itself // contains a space.) if AnsiStartsStr('BasicAuth', ALine) then begin Result := True; CmdBasicAuth(Copy(ALine, 10, Length(ALine))); end else if ALine = 'Beautify' then begin Result := True; FBeautify := True; end else if AnsiStartsStr('Highlight ', ALine) then begin Result := True; CmdHighlight(Copy(ALine, 11, Length(ALine))); end else if AnsiStartsStr('FormField ', ALine) then begin Result := True; CmdFormField(Copy(ALine, 11, Length(ALine))); end else if AnsiStartsStr('Compress', ALine) then begin Result := True; FHttp.Compressor := TIdCompressorZLib.Create(FHttp); end else if AnsiStartsStr('Proxy ', ALine) then begin Result := True; CmdProxy(Copy(ALine, 7, Length(ALine))); end else if AnsiStartsStr('ProxyAuth', ALine) then begin Result := True; CmdProxy(Copy(ALine, 10, Length(ALine))); end else if AnsiStartsStr('Generate ', ALine) then begin Result := True; CmdGenerate(Copy(ALine, 10, Length(ALine))); end else if AnsiStartsStr('Expect ', ALine) then begin Result := True; CmdExpect(Copy(ALine, 8, Length(ALine))); end else if AnsiStartsStr('Call ', ALine) then begin Result := True; FURL := Copy(ALine, 6, Length(ALine)); end; end; procedure TRestemplateApplication.CmdAskUser(AName: String); var mode: TPromptMode; value, default: String; begin if Length(AName) < 2 then raise Exception.Create('Asking without variable!'); if AName[1] <> ' ' then mode := pmPassword else mode := pmNormal; AName := Copy(AName, 2, Length(AName)); //Check if the variable has been set on the commandline; we do not //have to ask the user then. if HasOption('var:' + AName) then begin value := GetOptionValue('var:' + AName); end else begin if mode = pmNormal then begin default := FSessionIni.ReadString(FTemplateName, AName, ''); value := Prompt(AName, default); end else begin value := Prompt(AName, mode); end; if value = '' then Halt(3); //Cancelled end; FParser.Fields.Add(AName, value); if mode = pmNormal then FSessionIni.WriteString(FTemplateName, AName, value); end; procedure TRestemplateApplication.CmdHeader(AHeader: String); var i: Integer; varName, varValue: String; begin FParser.Content := AHeader; FParser.Replace; AHeader := FParser.Content; i := 1; while (i < Length(AHeader)) and (AHeader[i] <> ':') do Inc(i); varName := Trim(Copy(AHeader, 1, i - 1)); varValue := Trim(Copy(AHeader, i + 1, Length(AHeader))); FRequest.RawHeaders.AddValue(varName, varValue); end; procedure TRestemplateApplication.CmdBasicAuth(AData: String); var separator: Char; i: Integer; begin FParser.Content := AData; FParser.Replace; AData := FParser.Content; separator := AData[1]; i := 2; while (i < Length(AData)) and (AData[i] <> separator) do Inc(i); FRequest.Username := Copy(AData, 2, i - 2); FRequest.Password := Copy(AData, i + 1, Length(AData)); FRequest.BasicAuthentication := True; end; procedure TRestemplateApplication.CmdHighlight(AData: String); begin FFilters.Add(THighlightFilter.Create(AData)) end; procedure TRestemplateApplication.CmdFormField(AData: String); begin FParser.Content := AData; FParser.Replace; FFormFields.Add(FParser.Content); end; procedure TRestemplateApplication.CmdProxy(AData: String); var i: Integer; begin i := 1; while (i < Length(AData)) and (AData[i] <> ' ') do Inc(i); FHttp.ProxyParams.ProxyServer := Copy(AData, 1, i - 1); FHttp.ProxyParams.ProxyPort := StrToInt(Copy(AData, i + 1, Length(AData))); end; procedure TRestemplateApplication.CmdProxyAuth(AData: String); var separator: Char; i: Integer; begin separator := AData[1]; i := 2; while (i < Length(AData)) and (AData[i] <> separator) do Inc(i); FHttp.ProxyParams.ProxyUsername := Copy(AData, 2, i - 2); FHttp.ProxyParams.ProxyPassword := Copy(AData, i + 1, Length(AData)); end; procedure TRestemplateApplication.CmdGenerate(AData: String); var i: Integer; varName, generator: String; guid: TGuid; begin i := 1; while (i < Length(AData)) and (AData[i] <> ' ') do Inc(i); varName := Copy(AData, 1, i - 1); generator := LowerCase(Copy(AData, i + 1, Length(AData))); case generator of 'uuid': begin CreateGUID(guid); FParser.Fields.Add(varName, Copy(GUIDToString(guid), 2, 36)); end; 'unixtime': FParser.Fields.Add(varName, IntToStr(DateTimeToUnix(Now))); 'localtime': FParser.Fields.Add(varName, DateTimeToStr(Now)); 'isodatetime': FParser.Fields.Add(varName, xsdFormatDateTime(Now, nil)); 'isodate': FParser.Fields.Add(varName, xsdFormatDate(Now)); 'isotime': FParser.Fields.Add(varName, xsdFormatTime(Now)); else raise Exception.Create('Unknown generator: ' + generator); end; end; procedure TRestemplateApplication.CmdExpect(AData: String); begin FExpectations.Add(AData); end; procedure TRestemplateApplication.ProcessCall(AURL: String); var s, httpMethod: String; request, response: TStream; jsonParser: TJSONParser; jsonData: TJSONData; contentType: TContentType; xmlDoc: TXMLDocument; begin httpMethod := Trim(FMethod); if httpMethod = '' then if (FContent.Count > 0) or (FFormFields.Count > 0) then httpMethod := 'POST' else httpMethod := 'GET'; FParser.Content := AURL; FParser.Replace; AURL := FParser.Content; writeln(httpMethod, ' ', AURL); response := TMemoryStream.Create; request := nil; if FContent.Count > 0 then begin request := TMemoryStream.Create; // Variable replacement FParser.Content := FContent.Text; FParser.Replace; FContent.Text := FParser.Content; FContent.SaveToStream(request); end; if HasOption('showrequest') then begin //TODO Handle URL-Encoded FormFields here! writeln; writeln('Request:'); for s in FContent do writeln(' ', s); end; try FRequest.Prepare; FHttp.Request := FRequest; FHttp.HTTPOptions := FHttp.HTTPOptions + [hoNoProtocolErrorException]; if SameText('POST', httpMethod) and (FFormFields.Count > 0) then FHttp.Post(AURL, FFormFields, response) else FHttp.Perform(httpMethod, AURL, request, response); except on E: Exception do begin writeln; writeln('Failed! ', E.Message); Halt(1); end; end; writeln; writeln('Status: ', FHttp.ResponseCode, ' (', FHttp.ResponseText, ')'); writeln; writeln('Headers:'); for s in FHttp.Response.RawHeaders do writeln(' ', s); writeln; response.Position := 0; if FBeautify and IdentifyContentType(FHttp.Response.ContentType, contentType) then begin case contentType of ctJSON: begin jsonParser := TJSONParser.Create(response); jsonData := jsonParser.Parse; FContent.Text := jsonData.FormatJSON; jsonData.Free; jsonParser.Free; WriteContent; end; ctXML: begin ReadXMLFile(xmlDoc, response); response.Size := 0; WriteXMLFile(xmlDoc, response); response.Position := 0; FContent.LoadFromStream(response); WriteContent; end; end; end else begin FContent.LoadFromStream(response); WriteContent; end; response.Free; request.Free; try CheckExpectations; except on e: Exception do begin writeln; writeln('Expections failed:'); writeln(e.Message); ExitCode := 5; end; end; end; procedure TRestemplateApplication.WriteHelp; begin NormVideo; TextColor(Green); Writeln('restemplate ', VersionInfo.GetProductVersionString); Writeln('Copyright (c) ', VersionInfo.GetCopyright(False)); NormVideo; Writeln; Writeln('Usage: ', ExtractFileName(ExeName), ' [options]