16.07.2013

Some concepts for a Delphi Webserver with API versioning

When I began programming at my first company in a practical semester, the very first application I wrote, used TCP/IP to communicate with the existing applications. Almose every application I developed since, was using some kind of network communication to control another application or to be controlled by other applications. Over the years, I added other platforms (iOS and Android), which controlled the delphi applications. In every new project, I tried to tune the server side a little bit, to make my life easier.

Recently, I extended the server part to support multiple versions of an API.

The idea is that there is one IWebAPI interface, which represents the public API. The TAbstractAPI "implements" this interface. The real API classes inherit from this class and implement the methods they want. Having an abstract class between the interface and the implementation releases the real API classes from having to implement every method from the interface.

The API classes register themselves in a TWebAPIFactory, which will instantiate these classes when needed.

An example of an API
unit WebAPI.Version1;

interface // empty interface section, I think Nick Hodges would like this ;-) (http://www.nickhodges.com/post/Getting-Giddy-with-Dependency-Injection-and-Delphi-Spring-5-Delphi-Spring-Basics.aspx)

implementation

uses
  JSON, WebAPI.Abstract, WebAPI.Factory;

type
  TWebAPIVersion1 = class(TAbstractAPI)
    function ReverseString(jo: IJSONObject): string; override;
  end;

{ TWebAPIVersion1 }

function TWebAPIVersion1.ReverseString(jo: IJSONObject): string;
var
  ch: char;
begin
  result := '';
  for ch in jo.GetString('value') do
  begin
    result := ch + result;
  end;
end;

initialization

begin
  TWebAPIFactory.RegisterAPI('1', TWebAPIVersion1);
end;

end.

Another concept, which has been used in my previous applications is that the methods of the api are not called directly. Instead, Rtti is used to invoke the right method.
class function TWebAPIInvoker.Invoke(const aAPI: IWebAPI; const aCommand: string; aParam: IJSONObject): string;
var
  o: TObject;
  ctx: TRttiContext;
  typ: TRttiType;
  m: TRttiMethod;
begin
  o := TObject(aAPI);
  typ := ctx.GetType(o.ClassInfo);
  m := typ.GetMethod(aCommand);
  if assigned(m) then
  begin
    result := m.Invoke(o, [TValue.From<IJSONObject>(aParam)]).AsString;
  end
  else
  raise Exception.Create('Unknown Command: "' + aCommand + '"');
end;
I used the IJSONObject from my JSON-Library as a parameter, because it has been proven to be a reliable format for passing complex data over the borders of applications.

These concepts are glued together in the datamodule, that holds an IdHTTPServer.
procedure TdmWebserver.IdHTTPServer1CommandGet(AContext: TIdContext; ARequestInfo: TIdHTTPRequestInfo; AResponseInfo: TIdHTTPResponseInfo);
begin
  if isAPIRequest(ARequestInfo.Document) then
    HandleAPIRequest(AContext, ARequestInfo, AResponseInfo)
  else // Is File Request
    HandleFileRequest(AContext, ARequestInfo, AResponseInfo);
end;

function TdmWebserver.isAPIRequest(const aDocument: string): boolean;
begin
  result := aDocument.StartsWith('/api/');
end;

procedure TdmWebserver.HandleAPIRequest(AContext: TIdContext; ARequestInfo: TIdHTTPRequestInfo; AResponseInfo: TIdHTTPResponseInfo);
var
  documentParts: TArray<string>;
  version, command: string;
  api: IWebAPI;
  i: integer;
begin
  // e.g. /api/1/ReverseString
  documentParts := ARequestInfo.Document.Split(['/'], TStringSplitOptions.ExcludeEmpty);
  version := documentParts[1];
  command := '';
  for i := 2 to high(documentParts) do
  begin
    command := command + documentParts[i];
  end;
  api := TWebAPIFactory.newAPI(version);
  AResponseInfo.ContentText := TWebAPIInvoker.Invoke(api, command, nil)
end;

It is a work in progess, so use it carefully!
Delphi XE3 is required.

You can get the source code from the git repository at http://code.google.com/p/delphi-webapi/

Keine Kommentare: