cnocstream
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>