StreamDevice: Format Converter API

Converter Class

A user defined converter class inherits public from StreamFormatConverter and handles one or more conversion characters. It is not necessary that a given conversion character supports both, printing and scanning. But if it does, both must be handled by the same converter class.

Any conversion corresponds to one data type. The converter class must implement print and/or scan methods for this data type. It must also implement a parse method to analyse the format string.

A converter class must be registered with a call to RegisterConverter() in the global file context.

The converter must not contain any class variables, because there will be only one global instance for each conversion character - not one for each format string!

Example: LONG converter for %Q

#include "StreamFormatConverter.h"

class MyConverter : public StreamFormatConverter
{
    int parse(const StreamFormat&, StreamBuffer&, const char*&, bool);
    bool printLong(const StreamFormat&, StreamBuffer&, long);
    int scanLong(const StreamFormat&, const char*, long&);
};

RegisterConverter(MyConverter,"Q");

// ... (implementation)

Theroy of Operation

Registration

RegisterConverter(converterClass, "characters");

This macro registers the converter class for all given conversion characters. In most cases, you will give only one character. The macro must be called once for each class in the global file context.

HINT: Do not branch depending on the conversion character. Provide multiple classes, that's more efficient.

Parsing

int parse (const StreamFormat& fmt, StreamBuffer& info, const char*& source, bool scanFormat);
struct StreamFormat { char conv; StreamFormatType type; unsigned char flags; short prec; unsigned short width; unsigned short infolen; const char* info; };

During initialization, parse() is called whenever one of the conversion characters handled by your converter class is found in a protocol. The fields fmt.conv, fmt.flags, fmt.prec, and fmt.width have already been filled in. If a scan format is parsed, scanFormat is true. If a print format is parsed, scanFormat is false.

The fmt.flags field is a bitset and can have any of the following flags set:

It is not necessary that these flags have exactly the same meaning in your formats, but a similar and intuitive meaning helpful for the user.

There are two additional flags, default_flag indicating a ? and compare_flag indicating a = int the format, that are handled internally by StreamDevice and are not of interest to the converter class.

The source pointer points to the character of the format string just after the conversion character. You can parse additional characters if they belong to the format string handled by your class. Move the source pointer so that is points to the first character after your format string. This is done for example in the builtin formats %[charset] or %{enum0|enum1}. However, many formats don't need additional characters.

Example

 source       source
 before       after
 parse()      parse()
     |         |
"%39[0-9a-zA-Z]constant text"
    |
 conversion
 character

You can write any data you may need later in print*() or scan*() to the Streambuffer info. This will probably be necessary if you have parsed additional characters from the format string as in the above example

Return long_format, double_format, string_format, or enum_format depending on the datatype associated with the conversion character. It is not necessary to return the same value for print and for scan formats. You can even return different values depending on the format string, but I can't imagine why anyone should do that.

If the format is not a real data conversion but does other things with the data (append or check a checksum, encode or decode the data,...), return pseudo_format.

Return false if there is any parse error or if print or scan is requested but not supported by this conversion.

Printing and Scanning

Provide a print[Long|Double|String|Pseudo]() and/or scan[Long|Double|String|Pseudo]() method appropriate for the data type you have returned in the parse() method. That method is called whenever the conversion appears in an output or input, respectively. You only need to implement the flavour of print and/or scan suitable for the datatype returned by parse().

Now, fmt.type contains the value returned by parse(). With fmt.info() get access to the string you have written to info in parse() (null terminated).

The length of the info string can be found in fmt.infolen.

In print*(), append the converted value to output. Do not modify what is already in output (unless you really know what you're doing). Return true on success, false on failure.

In scan*(), read the value from input and return the number of consumed bytes. In the string version, don't write more bytes than maxlen! If the skip_flag is set, you don't need to write to value, since the value will be discarded anyway. Return -1 on failure.


Dirk Zimoch, 2007