cnocstream

From Free Pascal wiki
Revision as of 23:17, 8 September 2019 by Loesje (talk | contribs) (Section about modifying the behaviour using description added)
Jump to navigationJump to search

cnocStream

This package is a library to serialize objects to streamable contents. For example: stream an object into a JSON-format, or xml. And vice-versa. It is work in progress, and I'm seeking help. The goal is to make a library which will make all others obsolete. This mean that a good design is crucial. So if you can have had a look and have some ideas how to improve it, mail me: <joost@cnoc.nl>. (Ideas for another name are also welcome). The idea has also been discussed on the [fpc-pascal mailinglist](https://lists.freepascal.org/pipermail/fpc-pascal/2019-August/056486.html).

At this moment (aug 2019) it is all just work-in-progress. It might be that nothing works, or that everything will change in the future.

How to use

Using one of the helpers

There are several helpers to stream into several formats using several techniques. (Well, in theory, now there is only JSON and RTTI) For example, use the following to serialize a instance of TComponent into JSON format:

   uses
     Classes,
     csJSONRttiStreamHelper;
   var
     Comp: TComponent;
   begin
     Comp := TComponent.Create(nil);
     Comp.Name := 'MyComponent';
     writeln(TJSONRttiStreamHelper.ObjectToJSONString(Comp));
     Comp.Free;
   end.

This will print:

   { "Name" : "MyComponent", "Tag" : 0 }

The other way around looks like this:

   uses
     Classes,
     csJSONRttiStreamHelper;
   var
     Comp: TComponent;
   begin
     Comp := TComponent.Create(nil);
     TJSONRttiStreamHelper.JSONStringToObject('{"Name":"MyObject"}', Comp);
     writeln(Comp.Name); // Will return 'MyObject'
     Comp.Free;
   end.

Using a description

But what if you want to have some influence on how the class is streamed? For this there are descriptions (TcsStreamDescription). While it is possible to construct a description by yourself. It is easier to start with a description made by a describer. To obtain a description of TComponent based on the RTTI information, use the following:

   uses
     classes,
     csModel,
     csRttiModel,
     csJSONRttiStreamHelper;
   function GetTComponentDescription: TcsStreamDescription;
   var
     Describer: TcsRttiDescriber;
   begin
     Describer := TcsRttiDescriber.Create;
     Result :=Describer.ObtainDescriptionForClass(TComponent);
     Describer.Free;
   end;
   var
     Comp: TComponent;
     Description: TcsStreamDescription;
   begin
     Comp := TComponent.Create(nil);
     Comp.Name := 'MyComponent';
     Description := GetTComponentDescription();
     writeln(TJSONRttiStreamHelper.ObjectToJSONString(Comp));
     Description.Free;
     Comp.Free;
   end.

(Note that I left out proper exception/memory handling. This only confuscates the examples.)

The description is passed as parameter to ObjectToJSONString. Note that this example prints the same result as before. But now it is possible to alter the given description. We could, for example, emit the 'tag' from the result:

   function GetTComponentDescription: TcsStreamDescription;
   var
     Describer: TcsRttiDescriber;
     TagProp: TcsProperty;
   begin
     Describer := TcsRttiDescriber.Create;
     Result :=Describer.ObtainDescriptionForClass(TComponent);
     // Adapt the description by removing the tag-property:
     TagProp := Result.Properties.FindPropertyByName('Tag');
     Result.Properties.Remove(TagProp);
     Describer.Free;
   end;

With this version of GetTComponentDescription the results looks like:

   { "Name" : "MyComponent" }

Re-use descriptions

It could be quite tedious to create descriptions over and over again. One option to tacke this problem is by using a TcsStreamClass-descendant. They exist for several combinations of streaming-techniques and formats. They use a description-store to store the used descriptions. And it is possible to differenitate between different descriptions for one class by adding a description-tag. Take for example the following program:

   uses
     classes,
     csModel,
     csJSONRttiStreamHelper;
   var
     Comp: TComponent;
     Serializer: TJSONRttiStreamClass;
     TagProp: TcsProperty;
     Description: TcsStreamDescription;
   begin
     Serializer := TJSONRttiStreamClass.Create;
     try
       Description := Serializer.DescriptionStore.GetDescription(TComponent, 'without_tag');
       TagProp := Description.Properties.FindPropertyByName('Tag');
       Description.Properties.Remove(TagProp);
       Comp := TComponent.Create(nil);
       Comp.Name := 'MyComponent';
       writeln('With tag: ', Serializer.ObjectToJSONString(Comp));
       writeln('Without tag: ', Serializer.ObjectToJSONString(Comp, 'without_tag'));
       Comp.Free;
     finally
       Serializer.Free;
     end;
   end.

In the description-store of the (de)serializer there are two different description stored. One that does the serialization with and one without the tag. By calling ObjectToJSONString with the description-tag, it is possible to choose the used format (description).

Modifying the behaviour using descriptions

There are more ways to modify the behaviour of the serialization then omitting properties. Descriptions can have several settings. When one such setting is not set, the setting of a parent description is used.

This way, for example, it is possible to change the case of the property-names of all properties to lowercase, except for one property. This is done as follows:

 {$M+}
 uses
   csModel,
   csJSONRttiStreamHelper;
 type
   TMyType = class
   private
     FName: string;
     FDescription: string;
     FCamelCase: Integer;
   published
     property Name: string read FName write FName;
     property Description: string read FDescription write FDescription;
     property CamelCase: Integer read FCamelCase write FCamelCase;
   end;
 var
   Inst: TMyType;
   Serializer: TJSONRttiStreamClass;
   Description: TcsStreamDescription;
 begin
   Serializer := TJSONRttiStreamClass.Create;
   try
     Description := Serializer.DescriptionStore.GetDescription(TMyType);
     // Set the default behaviour to convert all property-names to lowercase
     Description.ExportNameStyle := tcsensLowerCase;
     // Make an exception for the property named CamelCase, this property
     // should only have the first character converted to lowercase.
     Description.Properties.FindPropertyByName('CamelCase').Describer.ExportNameStyle := tcsensLowerCaseFirstChar;
     Inst := TMyType.Create;
     Inst.Name := 'My instance';
     Inst.Description := 'The description';
     Inst.CamelCase := 101010;
     writeln('Case test: ', Serializer.ObjectToJSONString(Inst));
     Inst.Free;
   finally
     Serializer.Free;
   end;
 end.

Where to go from here

Please have a look at the unit-tests in the tests directory. These tests could also be used as examples on how to use the library. Just use the sources if you want to know more about how it works internally.

Where to find the latest version?

Good question. I'm still looking for a good place to host everything. Eventually it will be available in the fppkg-repository, I'll guess. For now I've uploaded it to a temporary location

License

Before I forget: LGPL v2, with the linking-exception. Just like FPC uses for it's packages.

Have fun,

Joost van der Sluis / <joost@cnoc.nl>