I know, you wait 5 years for a library then three come along at once! As well as Smoketest I also want to mention a couple of other libraries that have been published alongside it. They are wholly unrelated to Smoketest itself, so I decided to just quickly mention them in this separate post.
The first thing to mention is that as with all my code, these libraries support all versions of Delphi back to Delphi 7. It is only tested on Win32 VCL platforms (and Win64 where supported).
JSON
The first is a JSON implementation and parser that I wrote a long, long time ago though it has been updated a little recently. This is incorporated in a single unit in the rtl
library of the delphi.libs
project that also contains Smoketest.
One thing to note about this implementation is that all non-ASCII values are emitted as escaped Unicode (\Uxxxx
).
Duplicate value names are supported, taking a libteral approach to the interpretation of the specification (RFC-4267). The specification states that duplicate names SHOULD be supported but does not stipulate how.
My JSON parser supports duplicate values currently by simply creating a JSON structure with all named values. Where duplicate names exist in a JSON structure only the first will be found if accessed by name, so given the JSON data:
{ "duplicate": "first", "duplicate": "second" }
Accessing the “duplicate” value by name (the default property of a JSON object):
str := json['duplicated'].AsString; // returns 'first'
But all occurrences of a particular named value may be accessed by ordinal index just as with any other value:
str := json.ValueByIndex[0].AsString; // returns 'first' str := json.ValueByIndex[1].AsString; // returns 'second'
Similarly, if you explicitly add two values with the same name then that is what the JSON object will emit. If you change a value by name then you will replace the value of the first such named value, but again you can still change any value by ordinal index:
json.Add('duplicate', 'first'); json.Add('duplicate', 'second'); json['duplicate'].AsString := 'new first'; json.ValueByIndex[1].AsString := 'new second'; // result: // // { // "duplicate": "new first", // "duplicate": "new second" // }
In effect this means that the JSON object itself makes no decision about how to treat duplicate names where they exist, leaving it for the consumer code to make this decision and behave accordingly.
On that note I would add that the ambiguity of the specification in this area is a well known problem and I strongly recommend that in any JSON implementation you avoid any potential problems by simply not using duplicated value names at all. But if you are presented with JSON that does, at least this implementation allows you to choose how to work with them.
I am currently contemplating providing a mechanism to specify how the parser should treat duplicate names so that application code can delegate any particular implementation decision in this area to the JSON object.
A number of non-JSON data types are supporting on the API (GUID, TDateTime, enums etc), but these all resolve down to JSON core types underneath – usually string – and are simply provided as a convenience when using JSON as an interchange data structure being passed between two Delphi authored processes.
BONJOUR
The second is a far more sophisticated affair: A set of components to support the Apple Bonjour Service Discovery API.
This is also in the delphi.libs
project but is a library in it’s own right.
The components are intended primarily for the very simple advertisement and discovery of services, rather than being a complete DNS-SD framework (whatever that means – something I am still learning on an as-needed basis).
To advertise a service you use a TService component, simply set the service name, type and port and set it active. So for example, to advertise a (fake) iTunes shared library:
service := TService.Create(self); service.ServiceName := 'A Fake iTunes Library'; service.ServiceType := '_daap._tcp'; // the service type of an iTunes library service.Port := 3572; service.Active := TRUE;
You can also add TXT records for your advertised service if you need to.
Discovering services is a little bit more involved, though not much. For this you use a TListener. Set the service type you are interested in, assign a handler to respond to newly discovered services (and services that disappear, if you need to). When you discover a service you typically resolve it (in your ServiceFound handler) to obtain further information about the service so you will need a handler for the event that resolving a service will trigger. Then set the listener active:
procedure TMyForm.DoServiceFound(const aSender: TListener; const aService: TServiceInfo); begin aService.Resolve; end; procedure TMyForm.DoServiceResolved(const aSender: TListener; const aService: TServiceInfo); begin // service hostname, port and TXT records are now available... end; listener := TListener.Create(self); listener.ServiceType := '_daap._tcp'; listener.OnServiceFound := DoServiceFound; listener.OnServiceResolved := DoServiceResolved; listener.Active := TRUE;
Alternatively you can resolve a service synchronously by specifying a timeout (in milliseconds) to the Resolve
method, avoiding the need for a second event handler. In this case, the Resolve
method returns a boolean indicating whether the service was resolved within the time allowed (if it didn’t it may still eventually resolve):
procedure TMyForm.DoServiceFound(const aSender: TListener; const aService: TServiceInfo); begin if aService.Resolve(1000) then // service resolved - host, port and TXT etc now available end;
I hope to find time to blog more about these libraries in the coming weeks, but if you’re prepared to get your hands filthy and learn the hard way (not least by finding my bugs for me – thanks Stefan! :)) then the code is already up in the same github repository as Smoketest.
Both libraries have Smoketest test projects.
These are far from complete and are frankly more than a little scruffy, but should provide some initial guidance on how to use these libraries, if you are interested.
Once again, both of these libraries support all versions of Delphi back to Delphi 7.
You might want to check my libyaml wrapper for Delphi: https://bitbucket.org/OCTAGRAM/delphi-yaml
JSON should be recognized as YAML without changes (YAML is a superset of JSON). I didn’t implement JSON output, but it should be not very hard. It is a matter of using another formatting options (always flowed, always quoted and so on).
Hi Jolyon, a quick feedback: You may want to rename your component to be a bit less generic? Say, TBonjourService instead of TService? And if you intend to support more types of zero-conf networking techs in the future (Avahi, zcip, etc) you may want to make a generic Interface and make components that implement the Interface, or to make the zeroconf type as something pluggable. Just my 2 cents ๐
Thanks for the feedback.
I shall look at the impact of renaming the classes, though since I don’t use these components in a way that makes it necessary (and nobody else ever need do so either), this is a non-issue for me. If Bonjour service registration/discovery is compartmentalised in it’s own unit (as it should be), separate from any other code which may involve “services” or “listeners” then the possibility of collisions (occurring when components are placed on a form/data module with other components of a different type but with the same classname) doesn’t arise.
And no, I have no plans to implement support for other ZeroConf implementations that I would never use. Of course, someone else could create a pluggable ZeroConf framework and use these Bonjour components for the Bonjour plug-in part of it. ๐
Avahi on Windows? O’RLY ?
Why ?
Where did you get the idea that duplicates SHOULD be supported?!?
Quoting section 2.2 of the RFC:
“The names within an object SHOULD be unique”
— http://tools.ietf.org/html/rfc4627#section-2.2
…which sounds to me the opposite of what youโre saying.
“Names SHOULD be unique” means they do not have to be, and if they not have to be then they may also be non-unique – i.e. duplicated – to which the term SHOULD therefore equally applies (the RFC is describing what the JSON implementation is required to support, not how a particular application is to use that implementation).
Any implementation which enforces unique names is enforcing that “names MUST be unique“, which is not what the spec says (especially given that the terms MUST and SHOULD are specifically distinguished, as normal in RFC’s), even if with the benefit of hindsight this is perhaps regarded as a mistake.
And given that the RFC does not say how non-unique names should be treated, any implementation which supports them but imposes a particular strategy for dealing with duplicate names – other than simply allowing the application to decide – risks not being suitable for the the needs of a particular application when faced with JSON from some other source with an implementation which differs in this respect.
Of course! I was forgetting youโre talking about a library component here, in which case itโs not yet known whether it will be used to emit or receive JSON. ๐
(Apologies for the unconstructive comment in any case. I guess I was tired that day).
No worries, it wasn’t unconstructive at all. Apart from anything else it made me think and clarify my own thoughts, which is always welcome. ๐