Fido Core contains several features that can help developers improve the quality of their code.
Below is a list of the most important features:
- Mappers
- JSON marshalling and unmarshalling
- Virtual database features
- Virtual Api clients
- Virtual Api servers
- Websockets
- Consul and Fabio support
- Boxes
- Events driven architecture
- Functional programming
- Currying
- Caching
- Channels
Unit: Fido.Mappers
.
Mappers are useful when it is required to transform from one type to another.
The mappings are registered into the global mappers
structure and can be registered as following:
Mappers.RegisterMapper<ISongRecord, ISong>(
procedure(Source: ISongRecord; Destination: ISong)
begin
Destination.SetId(Source.Id);
Destination.SetTitle(Source.Title);
end);
Please note that in the previous case, the registration is redundant, given that properties and methods in ISongRecord
and ISong
are of the same type.
In case you need to migrate to and from types that have these similarities, an auto-mapping functionality will be used, adopting the following rules:
- Only published methods and properties are used
- Only functions with no parameters are used
- Only readable properties will be used to extract data
- Only writable properties will be used to inject data
The mappings can be used as such:
try
Mappers.Map<ISongRecord, ISong>(SongRecord, Song);
except
on E: Exception do
Writeln(E.Message);
end;
Both parameters of the Map
method needs to be already instantiated.
Unit Fido.Json.Marshalling
.
By using the JSONUnmarshaller
and JSONMarshaller
tools, the developer can convert to and from JSON.
The following types are supported:
-
Primitives (
string, Int64, Integer, Smallint, TDateTime, Extended, Double, Currency, Boolean, TGuid
). -
Nullable of primitives (
Spring.Nullable<T>
where T is one of the supported Primitives). -
Objects (any descendent of
TObject
).Any published parameter-less function and readable property are marshalled.
Any published one-parameter method and writeable property are unmarshalled.
-
Records (other than
Spring.Nullable<T>
)Any public field, public parameter-less function and readable property are marshalled.
Any public field, public parameter-less function and readable property are unmarshalled.
-
Interfaces (any descendent of
IInterface
).Any public parameter-less function and readable property are marshalled.
Any public one-parameter method and writeable property are unmarshalled.
-
Lists of primitives (
Spring.Collections.IReadonlyList<T>
where T is one of the supported Primitives). -
Lists of interfaces (
Spring.Collections.IReadonlyList<T: IInterface>
).
It is worth noting that when it comes to the unmarshalling of interfaces, the developer doesn't need to implement the interfaces.
Fido library will use the TJSONVirtualDto
class to implement the interface virtually and expose the JSON values.
It is also worth noting that Fido library will process also aggregates and not only plain (as single level) JSON objects.
Let's say you have a JSON object as such:
{
"Id":1,
"Guid":"F76CD4D4-35E1-4C66-A30A-37C050C0B324",
"Title":"My Title",
"IsGood":false,
"ReleaseDate":"2021-09-16T16:59:07.096Z",
"Price":"15.95",
"Author":{
"Id":2,
"Name":"Author name"
},
"Years":[2001,2002],
"Singers":[
{
"Id":3,
"Name":"First singer"
},
{
"Id":4,
"Name":"Second singer"
}
],
"Media":2
}
then, you just provide an interface as such (again, no implementation):
ISinger = interface(IInvokable)
['{DC5320BB-1338-4745-B958-8A0BBD74DAB5}']
function Id: Integer;
function Name: string;
end;
IAuthor = interface(IInvokable)
['{2E7D194B-E2F4-4859-9330-8410164B4995}']
function Id: Integer;
function Name: string;
end;
ISong = interface(IInvokable)
['{8B681FE2-3C64-4D58-9DF6-0E9561B17E03}']
function Id: Integer;
function Guid: TGuid;
function Title: string;
function IsGood: Boolean;
function ReleaseDate: Nullable<TDateTime>;
function RetirementDate: Nullable<TDateTime>;
function Price: Currency;
function Author: IAuthor;
function Years: IReadOnlyList<Integer>;
function Singers: IReadOnlyList<ISinger>;
function Media: TMediaType;
end;
and by calling:
procedure UnmarshallSong(const SongJson: string);
var
Song: ISong;
begin
Song := JSONUnmarshaller.&To<ISong>(SongJson);
...
end;
You will have access to an interface instance that expresses the JSON. And that's what I mean when I say that the Fido library philosophy is "describe behaviour instead of coding it, whenever is possible".
The JSON mappings are registered into a global structure and can be added or overridden using the MappingsUtilities
.
Let's say you don't want to encode your datetimes in ISO8601 (the standard mapping), but in another way, you can simply call:
uses
Fido.JSON.Mapping;
function DateTomMyFormat(const Value: TDateTime): string;
begin
// Your own implementation
end;
function TryMyFormatToDate(const Value: string; out DateTimeValue: TDateTime): Boolean;
begin
// Your own implementation
end;
MappingsUtilities.RegisterPrimitive<TDateTime>(
function(const Value: TDateTime): string
begin
Result := DateToMyFormat(Value);
end,
function(const Value: string; const TypInfo: pTypeInfo): TDateTime
var
DateTimeValue: TDateTime;
begin
Result := 0;
if TryMyFormatToDate(Value, DateTimeValue) then
Result := DateTimeValue
end);
You can also override the default mapping for enumeratives (that by default uses the index) by calling:
uses
Fido.JSON.Mapping;
MappingsUtilities.RegisterEnumeratives(
function(const Value: TValue): string
begin
// Whatever implementation you like
end,
function(const Value: string; const TypInfo: pTypeInfo): TValue
begin
// whatever implementation you like
end);
Fido library will unmarshal classes and interfaces, adopting the following rules:
- Classes constructors must be parameter-less
- Only procedures called
Set<NAME_OF_JSON_VALUE>
with one parameter are used - Only writable properties will be used to inject data
Fido library will marshall interfaces, adopting the following rules:
- Only public parameter-less functions are used
- Only public readable properties will be used
Fido library will marshall classes, adopting the following rules:
- Only published parameter-less functions are used
- Only published readable properties will be used
It happens that you want to use your own marshalling/unmarshalling for your classes (although I don't understand why since Fido mechanism is amazing! :) ). In this case you can override the registration for a specific class:
uses
Fido.JSON.Mapping;
MappingsUtilities.RegisterType<TMyClass>(
function(const Value: TMyClass): string
begin
// Whatever implementation you like
end,
function(const Value: string; const TypInfo: pTypeInfo): TMyClass
begin
// whatever implementation you like
end);
IMPORTANT: The marshalling will use the mapping if the class is the same OR if it is a descendent of the registered class. Long story short you do not need to register all of the classes, but just the ancestor.
The usage is straightforward, to convert from JSON to type:
procedure UnmarshallSong(const SongJson: string);
var
Song: ISong;
begin
Song := JSONUnmarshaller.&To<ISong>(SongJson);
...
end;
And to convert from type to JSON:
procedure MarshallSong(const Song: ISong);
var
SongJson: string;
begin
SongJson := JSONMarshaller.From<ISong>(Song);
...
end;
FidoLib registers for you standard primitive type conversions rules, but you could have the need for supporting multiple configurations.
Let's say you connect to two separate APIs, and each of them has a different date and time format. In this case, it is enough to register mappings with the configuration name, for example:
uses
Fido.JSON.Mapping;
MappingsUtilities.RegisterEnumeratives(
function(const Value: TValue): string
begin
// Whatever implementation you like
end,
function(const Value: string; const TypInfo: pTypeInfo): TValue
begin
// whatever implementation you like
end,
'MySpecialConfiguration');
And the usage is again straightforward, to convert from JSON to type:
procedure UnmarshallSong(const SongJson: string);
var
Song: ISong;
begin
Song := JSONUnmarshaller.&To<ISong>(SongJson, 'MySpecialConfiguration');
...
end;
And to convert from type to JSON:
procedure MarshallSong(const Song: ISong);
var
SongJson: string;
begin
SongJson := JSONMarshaller.From<ISong>(Song, 'MySpecialConfiguration');
...
end;
The virtual database clients are interfaces that represent database statements (query, commands, sequences, stored procedures) and are enriched using attributes to allow the Fido library to work properly.
Fido library enables the connection to databases through implementations of the IStatementExecutor
interface.
In FidoFireDac the FireDAC implementation is already available through the classes TFireDacStatementExecutor
and TFireDacConnections
.
Ado and ZeosDB implementation are also available from the FidoAdo and FidoZeos packages.
The virtual database clients will use the IStatementExecutor
internally to reach the database. All of this is possible when registering the classes into the DI container (I said DI container, not global container...).
var
FireDacDatabaseParams: TStrings;
begin
FireDacDatabaseParams := TStringList.Create;
// Set FireDAC parameters
...
Container.RegisterType<TFireDacConnections>.DelegateTo(
function: TFireDacConnections
begin
Result := TFireDacPerThreadConnections.Create(FireDacDatabaseParams);
end).AsSingleton;
Container.RegisterType<IStatementExecutor, TFireDacStatementExecutor>;
...
end;
Fido library treats the SQL queries and commands as resources, and as such, you should provide those resources.
First of all, you need to add the resources to the dpr
file.
{$R 'Queries.res' '<PATH_TO_THE_DOMAIN>\Queries.rc'}
Then create the rc
file.
#include "Song.Repository.Data.rcs"
Then create the rcs
file.
SQL_SONG_GetList RCDATA "<PATH_TO_THE_QUERIES>\SQL_SONG_GetList.sql"
SQL_SONG_Get RCDATA "<PATH_TO_THE_QUERIES>\SQL_SONG_Get.sql"
SQL_SONG_Update RCDATA "<PATH_TO_THE_QUERIES>\SQL_SONG_Update.sql"
The folder structure will end up like this.
<DPR_LOCATION>\<PATH_TO_THE_DOMAIN>\<PATH_TO_THE_QUERIES>\sql files
This structure is because when you start working with a medium/large project and have 100s of resources, the dpr
file will become very slow when edited from the IDE.
Unit Fido.VirtualQuery.Intf
.
With a virtual query, you can abstract an SQL query and retrieve a Spring.Collections.IReadonlyList<T: IInvokable>
.
You just need to declare the DTO (Data Transfer Object) interface, and the query interface descendent from IVirtualQuery
and then use it.
ISongRecord = interface(IInvokable)
['{8B681FE2-3C64-4D58-9DF6-0E9561B17E03}']
function Id: Integer;
function Title: string;
end;
[SQLResource('SQL_SONG_GetList')]
ISongListQuery = interface(IVirtualQuery)
['{731740A3-5B8E-46B2-9E96-53A366CB7B54}']
function Open: IReadonlyList<ISongRecord>;
end;
[SQLResource('SQL_SONG_Get')]
ISongByIdQuery = interface(IVirtualQuery)
['{0A3B473B-C3A1-4473-A553-8F094927182F}']
function Open(const Id: Integer): IReadonlyList<ISongRecord>;
end;
Unit Fido.VirtualQuery.Attributes
.
It sets the resource that needs to be used
It sets the paging limit of the query, the number of records to return.
It sets the paging Offset of the query, the number of records to skip.
It tells Fido that the associated parameter (string
) will replace the tag in the sql.
Example:
[SQLResource('Q_AN_EXAMPLE_QUERY')]
function Open(const [SqlInject('ORDERBY')] OrderBy: string);
will replace the tag %ORDERBY%
of the sql resource called Q_AN_EXAMPLE_QUERY
with the content of the OrderBy
parameter.
select * from sometable order by %ORDERBY%
Containers.RegisterVirtualQuery<ISongRecord, ISongListQuery>(Container);
Containers.RegisterVirtualQuery<ISongRecord, ISongByIdQuery>(Container);
procedure DoSomethingWithSongListQuery(const SongListQuery: ISongListQuery);
var
Item: ISongRecord;
begin
for Item in SongListQuery.Open do
begin
//Do your magic here...
end;
end;
Unit Fido.VirtualStatement.Intf
.
With a virtual query, you can abstract an SQL command or call to stored procedure or sequence.
A virtual statement can use a fully qualified DB object name (i.e. when a procedure or sequence is needed) or a resource.
[Statement(stCommand, 'SQL_SONG_Update')]
ISongUpdateByIdCommand = interface(IVirtualStatement)
['{8ACAA604-8182-4FE3-B205-BBE2153B87B8}']
function Update(const Id: Integer; [Column('title')] const Name: string): Integer;
end;
unit Fido.VirtualStatement.Attributes
.
It sets the statement type and the resource that needs to be used. Values can be: stSequence, stFunction, stStoredProc, stCommand
.
For stCommand
it is necessary to provide a resource.
For stSequence, stFunction, stStoredProc
, it is necessary to provide the fully qualified database object name.
It sets the decorated method as the method to run to execute the resource.
If the method is already called Execute
or if the interface contains only one method, then the attribute is redundant.
It sets the database name of a specific parameter.
i.e. in the previous declaration, the second parameter of the Update
method is called Name
in the Delphi code, but title
in the related resource.
It sets the paging limit of the query, the number of records to return.
It sets the paging Offset of the query, the number of records to skip.
It tells Fido that the associated parameter (string
) will replace the tag in the sql.
Example:
[Statement(stQuery, 'Q_AN_EXAMPLE_QUERY')]
function Open(const [SqlInject('ORDERBY')] OrderBy: string);
will replace the tag %ORDERBY%
of the sql resource called Q_AN_EXAMPLE_QUERY
with the content of the OrderBy
parameter.
select * from sometable order by %ORDERBY%
Containers.RegisterVirtualStatement<ISongUpdateByIdCommand>(Container);
procedure TSongRepository.Update(const Song: ISong);
begin
Guard.CheckNotNull(Song, 'Song');
FSongUpdateByIdCommand.Update(Song.Id, Song.Title);
end;
If you need to connect to multiple databases in your application, just register the TXXXConnections
' and the IStatementExecutor
s using service names and then register the virtual statements and virtual query using the appropriate service name.
var
FireDacDatabaseParamsFirstDatabase: TStrings;
FireDacDatabaseParamsSecondDatabase: TStrings;
begin
FireDacDatabaseParamsFirstDatabase := TStringList.Create;
FireDacDatabaseParamsSecondDatabase := TStringList.Create;
// Set FireDAC parameters
...
Container.RegisterType<TFireDacConnections>('First_Database').DelegateTo(
function: TFireDacConnections
begin
Result := TFireDacPerThreadConnections.Create(FireDacDatabaseParamsFirstDatabase);
end).AsSingleton;
Container.RegisterType<IStatementExecutor>('First_Database_Connector').DelegateTo(
function: IStatementExecutor
begin
Result := TFireDacStatementExecutor.Create(Container.Resolve<TFireDacConnections>(First_Database));
end);
Container.RegisterType<TFireDacConnections>('Second_Database').DelegateTo(
function: TFireDacConnections
begin
Result := TFireDacPerThreadConnections.Create(FireDacDatabaseParamsSecondDatabase);
end).AsSingleton;
Container.RegisterType<IStatementExecutor>('Second_Database_Connector').DelegateTo(
function: IStatementExecutor
begin
Result := TFireDacStatementExecutor.Create(Container.Resolve<TFireDacConnections>(First_Database));
end);
...
Containers.RegisterVirtualQuery<ISongRecord, ISongListQuery>(Container, 'First_Database_Connector');
Containers.RegisterVirtualStatement<ISongUpdateByIdCommand>(Container, 'Second_Database_Connector');
end;
Virtual Api clients are interfaces that represent services endpoints and are enriched by means of attributes in order to allow the Fido library to work properly.
Example of a virtual Api client:
{$M+}
IIdentityApi = interface(IClientVirtualApi)
['{6825ED01-3041-4528-8CA2-993DDBBCB9A7}']
[Endpoint(rmPUT, '/account/{accountId}')]
[RequestParam('Request')]
function AccountByAccountIdPut(AccountId: Integer; Request: TUpdateIdentityRequest): TUpdateIdentityResponse;
[Endpoint(rmPOST, '/accounts')]
[RequestParam('Request')]
function AccountsPost(Request: TCreateIdentityRequest): TCreateIdentityResponse;
[Endpoint(rmDELETE, '/identity')]
[RequestParam('Request')]
procedure IdentityDelete(Request: TDeleteIdentityRequest);
end;
A virtual Api client is always linked to an IClientVirtualApiConfiguration
(that is injected into it), and it is able to extract information from it.
Every parameter-less function will be scanned in order to resolve the call parameters.
The EndPoint attribute defines what method and endpoint will be used during the call. The EndPoint variable can contain Mustache templates that will be resolved during the call.
Example:
[Endpoint(rmPUT, '/account/{accountId}')]
The Content attribute is used to override the content of both request and response. By default, the value is application/json
, so there's no need to define it if your service uses it.
Example:
[Content('application/json')]
The QueryParam, HeaderParam, FormParam and FileParam attributes are used to define parameters that need to be set in the Query, Header, Form or File parameters.
Examples:
[HeaderParam('ApiKey')]
[HeaderParam('ApiKey', 'Api-Key')]
The MethodParam
is the name of the parameter as it can be found in the Delphi code (call parameter or Configuration function name), while the RestParam
is the optional name of the parameter as is requested by the service. This is required usually only when, for example, the parameter name must be something-somethingelse
, and Delphi won't be able to find any parameter or method called that way (in case you are wondering why it is because Delphi does not allow to define names containing -
).
The Request attribute is used when the call requires JSON content to inform the Fido library which parameter is to be converted in JSON and become the body of the request.
If the body is an aggregate, we strongly suggest descending it from TOwningObject
so that the whole hierarchy of children objects will be freed upon the destruction of the aggregate.
Example
[RequestParam(''Request')]
The RawRequest attribute is used when the call requires raw content to inform the Fido library which parameter is to be converted in string and become the body of the request. Example
[RawRequestParam('Request')]
The RestParam attribute is used when a configuration function name or a call parameter does not have the same name of the actual parameter the service needs.
Declaration Example:
[RestParam('Api-Key')]
Usage Examples:
IIdentityApi = interface(IVirtualRestApi)
['{6825ED01-3041-4528-8CA2-993DDBBCB9A7}']
function AccountByAccountIdPut([RestParam('AccountId')] Account_Id: Integer; Request: TUpdateIdentityRequest): TUpdateIdentityResponse;
end;
IMySettings = interface(IBaseRestApiConfiguration)
['{D875BB18-F217-4208-8B41-17A867BA9F2B}']
[RestParam('Api-Key')]
function GetApiKey: string;
end;
The ResponseHeaderParam attribute is used when a POST call is expected to return a header parameter when the response code is a specific number (usually 201), as explained here.
If ParamName is not set then it assumes it has the same value as HeaderParam. The interface parameter must be out and of type string.
Declaration Example:
[ResponseHeaderParam(201, 'Location')]
Usage example:
IIdentityApi = interface(IVirtualRestApi)
['{6825ED01-3041-4528-8CA2-993DDBBCB9A7}']
[ResponseHeaderParam(201, 'Location')]
procedure SomethingPost(const Request: TUpdateIdentityRequest; out Location: string);
end;
Let's assume we have this Api definition:
{$M+}
IIdentityApi = interface(IVirtualRestApi)
['{6825ED01-3041-4528-8CA2-993DDBBCB9A7}']
[Endpoint(rmPUT, '/account/{accountId}')]
[RequestParam('Request')]
[HeaderParameter('ApiKey', 'Api-Key')]
function AccountByAccountIdPut([RestParam('AccountId')] Account_Id: Integer; Request: TUpdateIdentityRequest): TUpdateIdentityResponse;
end;
IMySettings = interface(IClientVirtualApiConfiguration)
['{D875BB18-F217-4208-8B41-17A867BA9F2B}']
[ApiParam('Api-Key')]
function GetApiKey: string;
procedure SetApiKey(const Value: string);
end;
When the AccountByAccountIdPut
method is called the Fido library executes the following steps:
- Replaces the
{accountId}
in/account/{accountId}
with the value of theAccount_Id
variable. - Converts the
Request
variable from aTUpdateIdentityRequest
to a JSON and puts it as the request body. - Reads the result from the
GetApiKey
function and saves it as a header parameter calledApi-Key
.
And finally calls the REST service.
Updating the ApiConfiguration
If an Api call outputs a parameter that is called as one of the ApiConfiguration parameters (using the convention Set<ParameterName>
) the library will automatically update it. This is useful, for example, in case of headers or tokens that can be updated by server (i.e. Access tokens).
The Fido library supports, out of the box, Apis based on JSON.
The unit Fido.Api.Client.VirtualApi.json.pas
contains the class TJSONClientVirtualApi<T: IClientVirtualApi; IConfiguration: IClientVirtualApiConfiguration>
that can be used to consume JSON based Apis.
If you need to support other flavours or you want to specialize the behaviour of the class, please feel free to inherit from TAbstractClientVirtualApi<T: IClientVirtualApi; IConfiguration: IClientVirtualApiConfiguration>
declared in Fido.Api.Client.VirtualApi.Abstract.pas
.
The inherited class will implement the following methods:
function ConvertTValueToString(const Value: TValue): string; virtual; abstract;
In this function you will convert a TValue into a string. The Virtual Api works with RTTI, so it required a way to convert a parameter from TValue to a string (JSON and XML end up writing strings).
function ConvertResponseToDto(const Response: string; const ConvertToClass: TClass): TValue; virtual; abstract;
In this conversion, you will convert from a string response (JSON or XML depending on the Service implementation) to a instance.
function ConvertRequestDtoToString(const Value: TValue): string; virtual; abstract;
In this function, you will convert the request from a TValue to a string.
procedure CallApi(
const Method: TRESTRequestMethod;
const Url: string;
const ContentType: string;
const QueryParams: IDictionary<string, string>;
const PostBody: string;
const HeaderParams: IDictionary<string, string>;
const FormParams: IDictionary<string, string>;
const FileParams: IDictionary<string, TStream>;
out ApiResponse: TRestApiResponse;
out ResponseHeaders: TStrings); virtual; abstract;
This is the core procedure that will perform the Api call. Is it important that you catch all the exceptions on this call because if you do not do that, you will raise a generic exception instead of getting the proper HTTP status code.
Virtual Apis servers classes allow you to create Api servers quickly.
A server is the engine that will respond to incoming requests. Fido library provides already an Indy and a Brook implementation, but feel free to implement your own, as long as it implements the IApiServer
interface:
IApiServer = interface(IInvokable)
['{AA282BB3-418E-4835-8752-73D8DCCD326A}']
function Port: Word;
function IsActive: Boolean;
procedure SetActive(const Value: Boolean);
procedure SetWebServer(const WebServer: IWebServer);
procedure RegisterResource(const Resource: TObject);
procedure RegisterRequestMiddleware(const Name: string; const Step: TApiRequestMiddlewareFunc);
procedure RegisterResponseMiddleware(const Name: string; const Step: TApiResponseMiddlewareProc);
procedure RegisterExceptionMiddleware(const MiddlewareProc: TApiExceptionMiddlewareProc);
procedure RegisterGlobalMiddleware(const MiddlewareProc: TApiGlobalMiddlewareProc);
procedure RegisterFormatExceptionToResponse(const FormatExceptionToResponseProc: TApiFormatExceptionToResponseProc);
end;
program FileRestServerExample;
{$APPTYPE CONSOLE}
{$R *.res}
uses
classes,
System.SysUtils,
System.IOUtils,
Spring,
Fido.Http.Types,
Fido.Api.Server.Intf,
Fido.Api.Server.Indy,
Fido.Web.Server.Files,
TestData in 'TestData.pas';
var
RestServer: IApiServer;
begin
ReportMemoryLeaksOnShutdown := True;
RestServer := TIndyApiServer.Create(
8080,
50,
TSSLCertData.CreateEmpty);
try
try
RestServer.RegisterResource(TTestResource.Create);
RestServer.SetWeBserver(TFileWebServer.Create(
'public',
'index.html'));
RestServer.SetActive(True);
Readln;
except
on E: Exception do
begin
Writeln(E.ClassName, ': ', E.Message);
Readln;
end;
end;
finally
RestServer.SetActive(False);
RestServer := nil;
end;
end.
Endpoints are added to the server by registering resources.
Example of a resource:
unit TestData;
interface
uses
Fido.Http.Types,
Fido.Api.Server.Resource.Attributes;
type
{$M+}
TData = class
private
FName: string;
FLastName: string;
FAge: Integer;
published
property Name: string read FName write FName;
property LastName: string read FLastName write FLastName;
property Age: Integer read FAge write FAge;
end;
{$M+}
[BaseUrl('/Test')]
[Consumes(mtJson)]
[Produces(mtJson)]
TTestResource = class(TObject)
[Path(rmGet, '/helloworld')]
function HelloWorld: string;
[Path(rmGet, '/helloworld/{name}')]
function HelloWorldName(const [PathParam]Name: string): string;
[Path(rmGet, '/Token')]
procedure GetToken(out [HeaderParam] Token: string);
[Path(rmPost, '/PostData')]
procedure PostData(const [BodyParam] Data: TData);
[Path(rmPost, '/OutData')]
procedure OutData(out [BodyParam] Data: TData);
[Path(rmGet, '/OutData')]
function Data: TData;
end;
implementation
{ TTestResource }
function TTestResource.Data: TData;
begin
Data := TData.Create;
Data.Name := 'Me';
Data.LastName := 'again me';
Data.Age := 22;
end;
procedure TTestResource.GetToken(out Token: string);
begin
Token := '{C6A68632-28E3-410F-8B6C-F8EB7133AF8B}';
end;
function TTestResource.HelloWorld: string;
begin
Result := 'Hello World';
end;
function TTestResource.HelloWorldName(const Name: string): string;
begin
Result := 'Hello World and ' + Name;
end;
procedure TTestResource.OutData(out Data: TData);
begin
Data := TData.Create;
Data.Name := 'Me';
Data.LastName := 'again me';
Data.Age := 12;
end;
procedure TTestResource.PostData(const Data: TData);
begin
WriteLn(Data.Name);
WriteLn(Data.LastName);
WriteLn(Data.Age);
end;
end.
Resources are decorated with attributes so that the server can understand how to process them.
The BaseUrl attribute defines the endpoint base URL.
Example:
[BaseUrl('/api')]
The Consumes attribute defines what mime type the endpoint consumes.
Example:
[Consumes(mtJson)]
The Produces attribute defines what mime type the endpoint produces.
Example:
[Produces(mtJson)]
The Path attribute defines to what method and endpoint the decorated method will respond. The Path variable can contain Mustache templates that will be resolved during the call.
Example:
[Endpoint(rmPUT, '/account/{accountId}')]
The ResponseCode attribute is used to override the standard response (200, 'OK') that the Server will apply to the response.
Example:
[ResponseCode(201, 'something')]
The QueryParam, HeaderParam, FormParam, PathParam and BodyParam attributes are used to define parameters that need to be set in or retrieved from the Query, Header, Form, Path parameters or the body.
Examples:
[HeaderParam('ApiKey')]
[HeaderParam('ApiKey', 'Api-Key')]
The MethodParam
is the name of the parameter as it can be found in the Delphi code (call parameter or Configuration function name), while the RestParam
is the optional name of the parameter as is requested by the service. This is required usually only when, for example, the parameter name must be something-somethingelse
, and Delphi won't be able to find any parameter or method called that way (in case you are wondering why, it is because Delphi does not allow to define names containing -
).
The WebSocketPath attribute is used to instruct the server to which path the websocket will respond to.
Example:
[WebSocketPath('/')]
The RequestMiddleware attribute is used to instruct the server that the decorated method will use the named middleware on the request before the method itself is called.
Example:
[RequestMiddleware('First decorator')]
The ResponseMiddleware attribute is used to instruct the server that the decorated method will use the named middleware on the response before the method itself is called.
Example:
[ResponseMiddleware('Last decorator')]
FidoLib supports websockets, both server side and client side, with an implementation based on Indy.
Units: Fido.Web.Server.WebSocket.Intf
, Fido.Web.Server.WebSocket
.
Setting up a server is pretty straight forward.
You can send bytes, strings, or an item (that will be translated into a JSON by means of the JSON marshalling).
var
Server: IWebSocketServer;
TypedServer: IWebSocketServer<ISong>;
Song: ISong;
begin
Server := TWebSocketServer.Create(8080, TSSLCertData.CreateEmpty);
Server.RegisterTopicCallback(
'atopic/{id}',
// Optional: Register a callback when a client connected to a topic sends a message.
procedure(const Client: TWebSocketClient; const Params: array of TNamedValue)
var
Message: string;
LParams: string;
begin
LParams := Params[0].Name + '=' + Params[0].Value.ToString;
Message := Client.WaitForMessage;
if Message.IsEmpty then
Exit;
TThread.Synchronize(nil, procedure
begin
mmMessages.Lines.Add(Client.Topic + ' ' + LParams + ' ' + Client.Host + ': ' + Message);
end);
end);
Server.Start;
Server.Send('atopic/100', [1, 2, 3, 4]);
Server.Send('atopic/100', 'This is a test message');
Song := // Instantiate the type here
// Go Typed to send "objects" across the wire
TypedServer := WebSocketServer.GetFor<ISong>(Server);
TypedServer.Send('atopic/100', Song);
end;
Units: Fido.Web.Client.WebSocket.Intf
, Fido.Web.Client.WebSocket
.
Setting up a client is even more straight forward for bytes and strings.
var
Client: IWebSocketClient;
CustomHeaders: Shared<TStringlist>;
begin
CustomHeaders := TStringlist.Create;
Client := TWebSocketClient.Create;
// Start and register the callback for the received messages
Client.Start('ws://127.0.0.1:8080/atopic/100', CustomHeaders,
procedure(const Message: string)
begin
TThread.Synchronize(nil, procedure
begin
ShowMessage(Message);
end);
end);
// Start and register the callback for the received data
Client.Start('ws://127.0.0.1:8080/atopic/100', CustomHeaders,
procedure(const Data: TArray<Byte>)
begin
// Do something with the data
end);
Client.Send('This is a test message');
Client.Send([1, 2, 3, 4]);
end;
You can also set up a typed client, that adds typed functionalities to IWebSocketClient
.
var
Client: IWebSocketClient<ISong>;
CustomHeaders: Shared<TStringlist>;
Song: ISong;
begin
CustomHeaders := TStringlist.Create;
Client := TWebSocketClient<ISong>.Create;
// Start and register the callback for the received messages
Client.Start('ws://127.0.0.1:8080/atopic/100', CustomHeaders,
procedure(const Item: ISong)
begin
// Do something with the received item
end);
Song := // Instantiate the type here.
Client.Send(Song);
end;
Units: Fido.Web.Client.WebSocket.SignalR
, Fido.Web.Client.WebSocket.SignalR.Types
.
FidoLib supports also SignalR.
var
SignalR: TSignalR;
begin
SignalR := TSignalR.Create(
'socket.somedummysite.com',
True,
procedure(const Channel: string; const Message: string)
begin
Writeln(Channel + ': ' + Message);
end,
TWebSocketClient.Create);
Result := SignalR.Send('c3', 'Channel', 'message', function(const Message: string): TSignalRMethodResult
var
Success: Boolean;
ErrorMessage: string;
begin
// Process result
Result := TSignalRMethodResult.Create(Success, ErrorMessage);
end);
end;
Unit: Fido.Api.Server.Consul
.
If you plan to develop a (micro)services architecture then Consul and Fabio will help you manage the whole mesh and present it as a single API reachable from a single IP address. Do you need to scale horizontally? No worries, spawn your new service instance, register it into Consul and Fabio will pick it up and do the load balancing for you.
FidoLib has a ready-to-go solution that will register your API server into Consul, with a little bit of configuration.
The code needs to know where Consul is and the token to use.
...
[Consul]
URL=http://192.168.178.11:8500
Token=3b12990b-b69f-3f9c-0fbc-5a627ee6a7be
...
After that the TConsulAwareApiServer
will simply decorate the IApiServer
and provide the registration and deregistration with Consul.
Fido.Consul.DI.Registration.Register(Container, IniFile);
Container.RegisterType<IApiServer>.DelegateTo(
function: IApiServer
begin
Result := TConsulAwareApiServer.Create(
TIndyApiServer.Create(
IniFile.ReadInteger('Server', 'Port', 8080),
IniFile.ReadInteger('Server', 'MaxConnections', 50),
TSSLCertData.CreateEmpty),
Container.Resolve<IConsulService>,
IniFile.ReadString('Server', 'ServiceName', 'Authentication'));
end);
The ConsulHealthCheck
attribute tells FidoLib that the endpoint will be used as healthcheck by Consul and Fabio.
type
[BaseUrl('/api')]
[Consumes(mtJson)]
[Produces(mtJson)]
THealthResource = class(TObject)
private
FLogger: ILogger;
public
constructor Create(const Logger: ILogger);
[Path(rmGet, '/health')]
[ConsulHealthCheck]
function Default: TResult<string>;
end;
The result is:
- FidoLib will make the api available under the
{fabiolburl}/{ServiceName.Lowercase}/{EndpointURI}
, in this case{fabiolburl}/authentication/api/health
- If more than one instances of the service run simultaneously Fabio will automatically load balance them.
- If one instance shuts down or dies Fabio will automatically stop using it.
FidoLib allows you to consume the Consul KVStore by means of the TConsulKVStore
class which implements the IKVStore
interface.
KVStore := Container.Resolve<IKVStore>;
PublicKeyContent := KVStore.Get('key');
Flag := KVStore.Put('key', 'a test value');
Flag := KVStore.Delete('key');
Unit Fido.Boxes
.
Boxes are, well, boxes...
Did you ever need to share a state between two or more pieces of code (possibly over different threads)?
You can achieve that in FidoLib using IBox<T>
and IReadonlyBox<T>
.
procedure Example;
var
Box: IBox<Boolean>;
ROBox: IReadonlyBox<Boolean>;
Updater: TBoxUpdater<Boolean>;
UpdaterProc: TBoxUpdaterProc<Boolean>;
begin
Box := Box<Boolean>.Setup(True);
TTask.Run(
procedure
begin
WriteLn(Box.Value); // This would be True
// Update the value passing a value directly
Box.UpdateValue(False);
end).Wait;
WriteLn(Box.Value); // This would be False;
Box.UpdateValue(True); // Back to True
TTask.Run(
procedure
begin
WriteLn(Box.Value); // This would be True
// Update the value passing a method that will set the value
// The context of the method is thread safe
Box.UpdateValue(procedure(var Value: Boolean)
begin
Value := False;
end);
end).Wait;
WriteLn(Box.Value); // This would be False;
ROBox := Box<Boolean>.Setup(True, Updater);
TTask.Run(
procedure
begin
WriteLn(ROBox.Value); // This would be True
// Update the value passing a value directly
Updater(False);
end).Wait;
WriteLn(ROBox.Value); //This would be False;
ROBox := Box<Boolean>.Setup(True, UpdaterProc);
Updater(True); // Back to True
TTask.Run(
procedure
begin
WriteLn(ROBox.Value); // This would be True
// Update the value passing a method that will set the value
// The context of the method is thread safe
UpdaterProc(procedure(var Value: Boolean)
begin
Value := False;
end);
end).Wait;
WriteLn(ROBox.Value); //This would be False;
end;
Units folder: EventsDriven
.
An event-driven architecture uses events to trigger and communicate between decoupled services and is common in modern applications built with microservices. An event is a change in state, or an update, like an item being placed in a shopping cart on an e-commerce website. Events can either carry the state (the item purchased, its price, and a delivery address) or events can be identifiers (a notification that an order was shipped).
Event-driven architectures have three key components: event producers, event routers, and event consumers. A producer publishes an event to the router, which filters and pushes the events to consumers. Producer services and consumer services are decoupled, which allows them to be scaled, updated, and deployed independently. Source
With FidoLib you can design your system adhering to the Events driven architecture.
Out of the box you can use:
- a Redis implementation that would use Queues, PubSub or an hybrid Queues + PubSub system to manage the events
- an intra-app memory based implementation that would use a PubSub system to manage the events.
But as always nothing stops you from implementing your own flavour and to contribute.
The events can be associated to a payload. Fidolib support out of the box two types of payload:
- string - Used by Redis, that does a JSON marshalling of the payload.
- TArray - Used by the intra-app memory implementation.
To add support for yet another payload type just add conversion by means of the TEventsDrivenUtilities.RegisterPayloadTypeMapper<PayloadType>
method. TEventsDrivenUtilities.Create
contains the code for the registration of the supported payload types, you can use them as a reference.
Depending on the system used the mechanism works as follow:
-
Queues
- Subscribers subscribe to channels and event names and start polling the queue for new events
- Producers publish events to channels with optional payloads
- One subscriber will eventually pop the event and payload and process it. In case of failure it will push it back at the end of the queue.
-
PubSub
- Subscribers subscribe to channels and event names
- Producers publish events to channels with optional payloads
- All active subscribers will get the event and the payload and process it.
-
Hybrid
- Subscribers subscribe to channels and event names
- Producers publish events to channels with optional payloads
- All active subscribers will get the notification of a new event and try to pop it from a queue
- The first one will get event and the payload and process it.
As you can see each flavour has its on peculiarities and behaviors. You can choose the system you find suitable for your own processes or implement a new one, if needed.
To use the Qeues system please use the following registration:
Container.RegisterType<IQueueEventsDrivenConsumer<string>, TRedisQueueEventsDrivenConsumer>;
Container.RegisterFactory<IQueueEventsDrivenConsumerFactory<string>>;
Container.RegisterType<IEventsDrivenProducer<string>, TRedisQueueEventsDrivenProducer>;
Container.RegisterFactory<IEventsDrivenProducerFactory<string>>;
Container.RegisterType<IEventsDrivenListener, TQueueEventsDrivenListener<string>>;
Container.RegisterType<IEventsDrivenPublisher<string>, TEventsDrivenPublisher<string>>;
Container.RegisterType<IEventsDrivenSubscriber, TEventsDrivenSubscriber>;
To use the PubSub system please use the following registration:
Container.RegisterType<IPubSubEventsDrivenConsumer<string>, TRedisPubSubEventsDrivenConsumer>;
Container.RegisterType<IEventsDrivenProducer<string>, TRedisPubSubEventsDrivenProducer>;
Container.RegisterFactory<IEventsDrivenProducerFactory<string>>;
Container.RegisterType<IEventsDrivenListener, TPubSubEventsDrivenListener<string>>;
Container.RegisterType<IEventsDrivenPublisher<string>, TEventsDrivenPublisher<string>>;
Container.RegisterType<IEventsDrivenSubscriber, TEventsDrivenSubscriber>;
To use the hybrid system please use the following registration:
Container.RegisterType<IPubSubEventsDrivenConsumer<string>, TRedisPubSubEventsDrivenQueueConsumer>;
Container.RegisterFactory<IPubSubEventsDrivenConsumerFactory<string>>;
Container.RegisterType<IEventsDrivenProducer<string>, TRedisPubSubEventsDrivenQueueProducer>;
Container.RegisterFactory<IEventsDrivenProducerFactory<string>>;
Container.RegisterType<IEventsDrivenListener, TPubSubEventsDrivenListener<string>>;
Container.RegisterType<IEventsDrivenPublisher<string>, TEventsDrivenPublisher<string>>;
Container.RegisterType<IEventsDrivenSubscriber, TEventsDrivenSubscriber>;
We tried to make the usage as straight forward as possible.
In the example below TAddUserConsumer
is a consumer for the event UserAdded
triggered in the Authentication
channel.
It is assumed that the payload of the event will be a Json compatible to the IUserCreatedDto
Dto.
The class is also publishing (via the IEventsDrivenPublisher
) events to the Users
channel.
type
IUserCreatedDto = interface(IInvokable)
function UserId: TGuid;
function FirstName: string;
function LastName: string;
end;
TAddUserConsumer = class
private
FLogger: ILogger;
FAddUseCase: IAddUseCase;
FDistribuitedEventPublisher: IEventsDrivenPublisher;
public
constructor Create(const Logger: ILogger; const AddUseCase: IAddUseCase; const DistribuitedEventPublisher: IEventsDrivenPublisher);
[TriggeredByEvent('Authentication', 'UserAdded')]
procedure Run(const UserCreatedDto: IUserCreatedDto);
end;
implementation
...
procedure TAddUserConsumer.Run(const UserCreatedDto: IUserCreatedDto);
begin
Logging.LogDuration(
FLogger,
Self.ClassName,
'Run',
procedure
begin
try
FAddUseCase.Run(UserCreatedDto.UserId, UserCreatedDto.FirstName, UserCreatedDto.LastName);
FDistribuitedEventPublisher.Trigger('Users', 'UserAdded', JSONMarshaller.From(UserCreatedDto.UserId).DeQuotedString('"'));
except
FDistribuitedEventPublisher.Trigger('Users', 'UserAddFailed', JSONMarshaller.From(UserCreatedDto.UserId).DeQuotedString('"'));
raise;
end;
end);
end;
The instance of TAddUserConsumer
is registered in the EventsDrivenSubscriber
. This way it will be able to receive the events notifications.
EventsDrivenSubscriber := Container.Value.Resolve<IEventsDrivenSubscriber>;
EventsDrivenSubscriber.RegisterConsumer(Container.Value.Resolve<TAddUserConsumer>);
Units: Fido.Functional
, Fido.Functional.Tries
, Fido.Functional.Retries
, Fido.Functional.Ifs
.
Can you guess what this little piece of code does?
Result := Context<TArray<TValue>>.New([OrderBy, Limit, Offset]).
Map<IReadOnlyList<IUserRecord>>(DoGetAll).
Map<IReadOnlyList<TUser>>(MapToEntity);
Well, it takes an input of type TArray<TValue>
, uses it as input for the function DoGetAll
, which returns an IReadOnlyList<IUserRecord>
, which is used as input for the function MapToEntity
that returns an IReadOnlyList<TUser>
.
Everybody is talking about functional programming nowadays.
Probably just a small fraction of them understands its basic components (functors, applicatives and monads) and definitely I am NOT among them.
Nonetheless, here I am with my take on facilitating functional programming with Delphi.
This implementation is inspired by this article.
The context
The context is a box that contains something. You can set up a new context using the following syntax:
const
TIMEOUT = 1000;
PAUSED = False;
var
ValueContext: Context<string>;
ConContext: Context<string>;
FutureContext: Context<string>;
FuncContext: Context<string>;
begin
ValueContext := Context<string>.New('A string');
ConContext := Context<string>.New(ValueContext);
FutureContext := Context<string>.New(function: string
begin
// Do something asyncronously
end, TIMEOUT, PAUSED);
FuncContext := Context<string>.New(function: string
begin
//Do something synchronously
end);
...
Once you have the context setup you can resolve it implicitly this way:
var
Value: string;
Func: TFunc<string>;
ValueContext: Context<string>;
begin
ValueContext := Context<string>.New('A string');
Value := ValueContext; // the value from the context is resolved
Func := ValueContext; // the value from the context is resolved as a function
...
Or, if you were lazy like me, you can setup the context by implicitly converting, as following:
var
Value: string;
Func: TFunc<string>;
ValueContext: Context<string>;
begin
Value := 'A string';
Func := function: string
begin
//Do something synchronously
end;
ValueContext := Value; // the context is implicitly constructed from the value
ValueContext := Func; // the context is implicitly constructed from the function
...
Pretty boring stuff, but wait, now it gets interesting. If you can map the contexed value and a function and get another context out containing the new calculated value:
const
TIMEOUT = 1000;
PAUSED = False;
var
Value: Integer;
MonadFunc: Context<string>.MonadFunc<Integer>;
begin
// System.SysUtil.StrToInt is a Context<string>.FunctorFunc<Integer>;
// Maps the initial value with System.SysUtil.StrToInt function and returns 100
Value := Context<string>.New('100').Map<Integer>(StrToInt);
// Maps asyncronously the initial value with System.SysUtil.StrToInt function and returns 100.
// If StrToInt takes more than one second, it raises an exception. Hey... never said my examples would make sense...
Value := Context<string>.New('100').MapAsync<Integer>(StrToInt, TIMEOUT, PAUSED);
MonadFunc := function(const Value: string): Context<Integer>
begin
// Some functionality that returns a Context<Integer>
end;
...
And you can map it again, and again, concatenating function one after the other. The implementation enforce lazy evaluations and the actual value will be only resolved either when explicitly calling Context.Value
or when implicitly assigning it to a variable.
The void
We love Pascal, but sometimes its peculiarities are a bit of a pain in the ass. In order to avoid duplicating most of the code I introduced the void type. The void type allows you to translate from procedures and functions and vice versa.
Examples:
var
Proc1: Context<Integer>.FunctorProc;
Func1: Context<T>.FunctorFunc<Void>;
Func2: Context<Void>.FunctorFunc<Void>;
Func3: TFunc<Integer>;
Func4: Context<Void>.FunctorFunc<Integer>;
begin
// Translates to procedure(const Value: Integer)
Proc1 := Void.MapProc<Integer>(function(const Value: Integer): Void
begin
end);
// Translates to function(const Value: Integer): Void
Func1 := Void.MapProc<Integer>(procedure(const Value: Integer)
begin
end);
// Translates to function(const Value:void): Void
Func2 := Void.MapProc(procedure
begin
end);
// Translates to function: Integer
Func3 := Void.MapFunc<Integer>(function(const Value: Void): Integer
begin
end);
// Translates to function(const Value: Void): Integer
Func4 := Void.MapFunc<Integer>(function: Integer
begin
end);
...
Tries
The &Try construct allows you to execute a function and manage the side effects in a functional way.
Examples:
var
Result: Context<Boolean>;
begin
//Try something and manage exceptions
&Try<string>.New('100s').Map<Integer>(StrToInt).Match(function(const E: Exception): Nullable<Integer>
begin
//Manage the exceptions
end);
//Try something and if there exceptions raise an exception
&Try<string>.New('100s').Map<Integer>(StrToInt).Match(EMyException, 'My error message. Original message: %s');
//Try something and finally do something
&Try<string>.New('100s').Map<Integer>(StrToInt).Match(procedure
begin
//Finally do something
end);
//Try something, manage exceptions and finally do something
&Try<string>.New('100s').Map<Integer>(StrToInt).Match(function(const E: Exception): Nullable<Integer>
begin
//Manage the exception
end,
procedure
begin
//Finally do something
end);
//Try something and returns if it succeded or not
Result := &Try<string>.New('100s').Map<Integer>(StrToInt).Match;
//Try something and manage exceptions
TryOut<Integer>.New(function: Integer
begin
// Get the value somehow
end).Match(function(const E: Exception): Nullable<Integer>
begin
//Manage the exceptions
end);
//Try something and if there exceptions raise an exception
TryOut<Integer>(function: Integer
begin
// Get the value somehow
end).Match(EMyException, 'My error message. Original message: %s');
//Try something and finally do something
TryOut<Integer>(function: Integer
begin
// Get the value somehow
end).Match(procedure
begin
//Finally do something
end);
//Try something, manage exceptions and finally do something
TryOut<Integer>(function: Integer
begin
// Get the value somehow
end).Match(function(const E: Exception): Nullable<Integer>
begin
//Manage the exception
end,
procedure
begin
//Finally do something
end);
...
Retries
The Retry construct allows you to try to execute a function for a certain amount of times, before giving up.
Examples:
const
TIMEOUT = 1000;
PAUSED = False;
begin
Retry<string>.New('100s').Map<Integer>(function(const Value: string): Integer
begin
//Do something that could fail due to external circumstances
end);
Retry<string>.New('100s').MapAsync<Integer>(function(const Value: string): Integer
begin
//Do something that could fail due to external circumstances
end, TIMEOUT, PAUSED);
Retry<string>.New('100s').Map<Integer>(function(const Value: string): Context<Integer>
begin
//Do something that could fail due to external circumstances
end);
Retry<string>.New('100s').MapAsync<Integer>(function(const Value: string): Context<Integer>
begin
//Do something that could fail due to external circumstances
end, TIMEOUT, PAUSED);
Retry.Map<Integer>(function: Context<Integer>
begin
//Do something that could fail due to external circumstances
end);
Retry.MapAsync<Integer>(function: Context<Integer>
begin
//Do something that could fail due to external circumstances
end, TIMEOUT, PAUSED);
...
Ifs
The If construct allows you to execute branching in a functional way.
Examples:
var
BoolContext: Context<Boolean>;
ContextFalse: Context<Integer>;
begin
&If<string>.New('100s').Map(function(const Value: string): Boolean
begin
//Test the value
end).&Then<Integer>(function(const Value: string): Integer
begin
//When true
end,
0 //When false
);
&If<string>.New('100s').Map(function(const Value: string): Context<Boolean>
begin
//Test the value
end).&Then<Integer>(function(const Value: string): Integer
begin
//When true
end,
0 //When false
);
BoolContext := //Some delayed calculation that returns a boolean
ContextFalse := //Some delayed calculation of the value when false;
ThenElse.New(BoolContext).&Then<Integer>(
100, //When true
ContextFalse
);
...
Units: Fido.Currying
.
Currying allows you to transform a function with multiple parameters in sequence of one-parameter functions.
FidoLib allows you to do that by using the Currying tool:
function Add(const P1: Integer; const P2: Integer): Integer;
begin
Result := P1 + P2;
end;
begin
Result := Curry.Cook<Integer, Integer, Integer>(Add)(1)(2); //This will return 3
end;
Units: Fido.caching.Intf
, Fido.caching
.
FidoLib support a "functional" caching. That means you can cache the results of functions... basically memoizing.
Since pure memoization is not always optimal, FidoLib supports three types of caching:
- Memoization. The cache has no limit.
- FIFO. The cache has a limited size. when the size is reached the first item that was added is removed.
- Usage. The cache has a limited size. when the size is reached the items that are added/accessed the last are kept.
When an item in the cache becomes stale its value can be forced by using the .ForceIt
method.
Caching is supported for functions up to four parameters:
IOneParamCache<P, R> = Interface(IInvokable)
['{60819F67-2B66-46BA-8F78-22C3597E4FEB}']
// It executes the function if it has not been cached yet and then caches it, otherwise retrieves the value from the cache
function It(const AFunction: TOneParamFunction<P, R>; const Param: P): R;
// ForceIt always executes the function and then caches it. Useful when you want to re-cache stale data
function ForceIt(const AFunction: TOneParamFunction<P, R>; const Param: P): R;
end;
ITwoParamsCache<P1, P2, R> = interface(IInvokable)
['{BCD8DB8D-25A8-4991-B1FB-B6213B03A556}']
function It(const AFunction: TTwoParamsFunction<P1, P2, R>; const Param1: P1; const Param2: P2): R;
function ForceIt(const AFunction: TTwoParamsFunction<P1, P2, R>; const Param1: P1; const Param2: P2): R;
end;
IThreeParamsCache<P1, P2, P3, R> = interface(IInvokable)
['{DDC1A3B3-8E6E-4F1B-BFC8-474D7F186237}']
function It(const AFunction: TThreeParamsFunction<P1, P2, P3, R>; const Param1: P1; const Param2: P2; const Param3: P3): R;
function ForceIt(const AFunction: TThreeParamsFunction<P1, P2, P3, R>; const Param1: P1; const Param2: P2; const Param3: P3): R;
end;
IFourParamsCache<P1, P2, P3, P4, R> = interface(IInvokable)
['{6C1E8BDD-9805-4095-8179-BC0230F9EC19}']
function It(const AFunction: TFourParamsFunction<P1, P2, P3, P4, R>; const Param1: P1; const Param2: P2; const Param3: P3; const Param4: P4): R;
function ForceIt(const AFunction: TFourParamsFunction<P1, P2, P3, P4, R>; const Param1: P1; const Param2: P2; const Param3: P3; const Param4: P4): R;
end;
An example of usage:
program CachingExample;
{$APPTYPE CONSOLE}
{$R *.res}
uses
Fido.Caching.Intf,
Fido.Caching;
type
TCalculator = class
private
FAddCache: ITwoParamsCache<Integer, Integer, Integer>;
function DoAdd(const Param1: Integer; const Param2: Integer): Integer;
public
constructor Create;
function Add(const Param1: Integer; const Param2: Integer): Integer;
end;
{ TCalculator }
constructor TCalculator.Create;
begin
inherited;
FAddCache := Caching.TwoParams.Memoize<Integer, Integer, Integer>;
end;
function TCalculator.DoAdd(const Param1, Param2: Integer): Integer;
begin
Result := Param1 + Param2;
end;
function TCalculator.Add(const Param1, Param2: Integer): Integer;
begin
Result := FAddCache.It(DoAdd, Param1, Param2);
end;
var
Calculator: TCalculator;
begin
Calculator := TCalculator.Create;
Writeln(Calculator.Add(1, 1));
Writeln(Calculator.Add(1, 2));
Writeln(Calculator.Add(1, 1));
Readln;
end.
Units: Fido.channels
.
Don't communicate by sharing memory; share memory by communicating.
Go koan.
Channels are the pipes that connect concurrent goroutines. You can send values into channels from one goroutine and receive those values into another goroutine.
from Go By Example
Channels are tools implemented in modern languages like Go and Rust, so for a decent description of what they are it's probably better if you read those languages documentation.
Long story short, they are one-directional pipes that are sized (allowing only some data to pass at any time) and blocking (as in, it blocks the sender if the pipe is full, and it blocks the receiver if the pipeline is empty).
Our implementation allows non-blocking behavior too.
Blocking behavior
begin
var Chan := Channels.Make<Boolean>;
TThread.CreateAnonymousThread(procedure
begin
Writeln('working...');
Sleep(1000);
Writeln('Done');
Chan.Send(True);
end).Start;
Chan.Receive;
Writeln('Received');
Readln;
end.
Output:
working...
Done
Received
Non-Blocking behavior
begin
var Chan := Channels.Make<string>;
var Value: string;
Chan.Send('test');
if Chan.TrySend('another test') then
Writeln('Sent again')
else
Writeln('Cannot send again. Channel is full');
if Chan.TryReceive(Value) then
Writeln('Received');
Readln;
end.
Output:
Cannot send again. Channel is full
Received
Buffered channels
begin
var Chan := Channels.Make<Boolean>(2);
var Value: Boolean;
Chan.Send(True);
Writeln('Sent');
Chan.Send(True);
Writeln('Sent again');
Value := Chan.Receive;
Writeln('Received');
Value := Chan.Receive;
Writeln('Received again');
if Chan.TryReceive(Value) then
Writeln('Received a third time')
else
Writeln('Channel is empty. Cannot receive any further');
Readln;
end.
Output:
Sent
Sent again
Received
Received again
Channel is empty. Cannot receive any further
Channel directions
When you pass the channel as a parameter, sometimes you want to make sure that it will be used only to send or receive.
begin
var Chan := Channels.Make<Boolean>;
var SendChan := Chan.AsSender;
var RecChan := Chan.AsReceiver;
end.
Select
Use Select to wait on multiple channels operations. The following example will wait until both channels are queued with data.
begin
var Stopwatch := TStopwatch.StartNew;
var C1 := Channels.Make<string>;
var C2 := Channels.Make<string>;
TThread.CreateAnonymousThread(procedure
begin
Sleep(2000);
if Assigned(C1) then
C1.Send('One');
end).Start;
TThread.CreateAnonymousThread(procedure
begin
Sleep(1000);
if Assigned(C2) then
C2.Send('Two');
end).Start;
var Select := Channels.Select;
Channels.Case<string>(Select, C1.AsReceiver, procedure(const Value: string)
begin
Writeln(Value);
end);
Channels.Case<string>(Select, C2.AsReceiver, procedure(const Value: string)
begin
Writeln(Value);
end);
Select.Run;
Writeln('Execution time: ', Stopwatch.Elapsed.TotalMilliseconds);
Readln;
end;
Output:
Two
One
Execution time: 2.09054930000000E+0003
Select with a timeout
You can set a timeout and a failed procedure, in case you are dealing with and external resource, or if you want to limit the execution time.
begin
var Stopwatch := TStopwatch.StartNew;
var C1 := Channels.Make<string>;
var C2 := Channels.Make<string>;
TThread.CreateAnonymousThread(procedure
begin
Sleep(2000);
if Assigned(C1) then
C1.Send('One');
end).Start;
TThread.CreateAnonymousThread(procedure
begin
Sleep(1000);
if Assigned(C2) then
C2.Send('Two');
end).Start;
var Select := Channels.Select;
Channels.Case<string>(Select, C1.AsReceiver, procedure(const Value: string)
begin
Writeln(Value);
end);
Channels.Case<string>(Select, C2.AsReceiver, procedure(const Value: string)
begin
Writeln(Value);
end);
Select.Run(1500, procedure
begin
Writeln('Failed');
end);
Writeln('Execution time: ', Stopwatch.Elapsed.TotalMilliseconds);
Readln;
end;
Output:
Two
Failed
Execution time: 1.53175820000000E+0003
Non-blocking select
Use Select with a default action to achieve a non-blocking select.
begin
var C1 := Channels.Make<string>;
var C2 := Channels.Make<string>;
C1.Send('One');
var Select := Channels.Select;
Channels.Case<string>(Select, C1.AsReceiver, procedure(const Value: string)
begin
Writeln(Value);
end);
Channels.Case<string>(Select, C2.AsReceiver, procedure(const Value: string)
begin
Writeln(Value);
end);
Select.Run(procedure
begin
Writeln('Default');
end);
Select := Channels.Select;
Channels.Case<string>(Select, C1.AsReceiver, procedure(const Value: string)
begin
Writeln(Value);
end);
Channels.Case<string>(Select, C2.AsReceiver, procedure(const Value: string)
begin
Writeln(Value);
end);
Select.Run(procedure
begin
Writeln('Default');
end);
Readln;
end;
Output:
One
Default
Closing channels
You can set a channel as closed. After a channel is closed sending will hang indefinitely, That is not a problem since it's the sender who's in control of the channel.
ATTENTION: that means you should check for Closed
, as sending through a closed channel will hang. The same for receiving on a empty closed channel.
begin
var Channel := Channels.Make<Boolean>(2);
Channel.Send(True);
Channel.Send(True);
Channel.Close;
// Channel.Send(True); <-- This would hang
if Channel.Closed then
Writeln('Closed')
else
Writeln('Not closed');
Channel.Receive;
Channel.Receive;
// Channel.Receive; <-- This would hang
if Channel.Closed then
Writeln('Closed')
else
Writeln('Not closed');
Readln;
end;
Output:
Not closed
Closed
Enumerating closed channels
You can enumerate a closed channel. If you try to enumerate an open channel you receive an EChannel
exception.
begin
var Channel := Channels.Make<string>(2);
Channel.Send('One');
Channel.Send('Two');
Channel.Close;
with Channel.GetEnumerator do
while MoveNext do
Writeln(Current);
Readln;
end;
Output:
One
Two