Skip to content

Latest commit

 

History

History
919 lines (715 loc) · 36.6 KB

File metadata and controls

919 lines (715 loc) · 36.6 KB

API Reference

This document describes in details the exchange format (or language) used by the Unitag Engine API.

Table of Contents generated with DocToc

Introduction

The Unitag Engine API allows to easily create and manage data-driven scenarios, called operations. These scenarios are expressed using structured JSON documents, whose format is detailed below.

As a lightweight and efficient serialization format, JSON is a perfect solution for describing complex structured documents like operations. Yet it does not suit well for describing common expressions such as interpreting variables or computing values.

To fill this gap, the Unitag Engine is able to parse and evaluate a markup syntax within JSON strings, for instance: <((env.connection.ip))>. This allows to embed a special expression language, which is largely inspired by JavaScript expressions, but with several useful specificities (see the expression language for more details). In the bellow documentation, the fields descriptions are often accompanied by a "Markup" column, which indicates whether a given field is allowed to embed some markup.

The Operation object

An Operation is a standalone document describing an HTTP application. It is expressed as a JSON object, which allows to specify:

  • how the application can be reached
  • which routes are exposed
  • which input data are needed
  • how input data are processed
  • which output data are produced
  • which external actions are triggered

The root object looks like the following:

Operation = {
    "url": Resolver | [Resolver],
    "state": String,
    "label": String,
    "needs": String | [String],
    "input": Input | [Input],
    "entryPoints": {Step},
    "index": Step
}
Field Description Markup
url Describes how this operation can be reached. Can be a single value or an array (but always returned as an array). In its simplest form, this field can contain a single string, which is sufficient to make the operation reachable through an URL of the following form: http://e.unitag.io/r/{YOUR PATH HERE}. See the Resolver object for more details. No
state Describes the publication state of this operation. Possible values are NOT_PUBLISHED, PUBLISHED, UNPUBLISHED and BLOCKED. The PUBLISHED and UNPUBLISHED states can be written, the others are read-only. No
label Defines an arbitrary name for this operation (generated by default). No
needs Defines the environment values needed by this operation. TODO: more details needed here. No
inputs Defines which data is needed globally for this operation. Can be a single value or an array. See the Input object for more details. No
entryPoints Defines the set of routes implemented by this operation. Each entry point is a key-value pair: the key defines an URL path, while the value defines the associated processing as a hierarchy of steps. See the Step object for more details. No
index Stands as a shortcut for entryPoints.index. This is because the index step has a special meaning: it is the default step, which is accessed when no entry point is specified in the URL. No

The publication state

The publication state controls whether an operation is publicly available or not. This state can have the following values:

State Description
NOT_PUBLISHED The operation has never been published (default).
PUBLISHED The operation is currently published. This is the only state where it can be accessed from the URL(s) defined by its resolver(s).
UNPUBLISHED The operation has already been published, but is not published anymore.
BLOCKED The operation was published, but has been blocked for some reason by an administrator.

Examples

The following minimal example, which creates a simple redirection to Google, can be considered as a starting point:

{
    "url": "{YOUR PATH HERE}",
    "state": "PUBLISHED",
    "index": "http://www.google.com"
}

Additionnal examples are available in the API documentation.

The Input object

An input object allows to define a set of data to be injected in the evaluation context. It definition looks like the following:

Input = {Connector} + {
    "$then": Input | [Input]
}

Connector = DataConnector | CsvConnector | FileConnector |
            VisitsConnector | LastVisitConnector | ParamsConnector |
            ActionLogResultConnector | ActionLogCountConnector

So, an input object is basically a map of connectors, where the key defines the name of the resulting value (accessible through <((io.name))> after evaluation), and the value defines how to produce the resulting value.

The value can be either:

  • a typed connector, which is expressed as an object containing a single $-prefixed key. The key denotes the connector's type, and its associated value defines the connector's parameters.
  • a raw value, which is any value that does not match the format of typed connectors. It is directly used as the resulting value.

The special $then field allows to define another set of connectors, which is evaluated only once all the other connectors are loaded. This can be used to load intermediate results, and then compute complex values based on them. See the evaluation order for more details.

The data connector

The data connector is the simplest connector: it allows to inject some arbitrary data into the evaluation context. Such a connector looks like the following:

DataConnector = Boolean | Number | String | Object | Array | {
    "$data": Boolean | Number | String | Object | Array
}

As stated above, any raw value is implicitly interpreted as a data connector. But, although rarely useful, a data connector can also be expressed as a typed connector (using the $data key).

The CSV connector

The CSV connector allows to retrieve a remote CSV file and to inject its parsed content into the evaluation context. Such a connector looks like the following:

CsvConnector = {
    "$csv": String | {
        "url": String | Url
    }
}

When specified as a single String, the connector definition is directly interpreted as the url field.

Field Description Markup
url Defines the URL of the remote CSV file. It can be any valid URL string, or a parsed URL object. TODO: more details here. Yes, but only the string format is accepted

The file connector

The file connector allows to load a file and to inject its content into the evaluation context. Such a connector looks like the following:

FileConnector = {
    "$file": String | RemoteFile | UploadedFile
}

RemoteFile = {
    "url": String | Object,
    "encoding": String
}

UploadedFile = {
    "filename": String,
    "encoding": String
}

When specified as a single String, the connector definition is directly interpreted as the url field of a RemoteFile.

Field Description Markup
url Defines the URL of a remote file. It can be any valid URL string, or a parsed URL object. TODO: more details here. Yes, but only the string format is accepted
filename Defines the name of an uploaded file, which can be obtained from the <((params.file))> object. Yes
encoding Optionally specifies the encoding of the loaded file: utf8, base64, hex or ascii. Default is base64. Yes

The visits connector

The visits connector allows to retrieve the number of times a given step or this operation has been accessed.

VisitsConnector = {
    "$visits": String | {
        "step": String,
        "from": Date,
        "to": Date,
        "duration": Number,
        "unit": String
    }
}

When specified as a single String, the connector definition is directly interpreted as the step field.

Field Description Markup
step Defines the step to consider. By default, consider all accesses to this operation. Yes
from Defines the lower bound of the time period taken into account. Unlimited by default. Yes, but only a numeric timestamp is accepted
to Defines the upper bound of the time period taken into account. Unlimited by default. Yes, but only a numeric timestamp is accepted
duration Defines the length of the time period taken into account. Default is unlimited if no unit is specified, 1 otherwise. Yes
unit Defines the unit of the duration field. Can be millisecond, second, minute, hour or day. Default is millisecond. Yes

There are several ways for specifying the time period:

  • When both from and to are specified, they define the exact bounds of the time period.
  • When either from or to is specified, the time period is defined by that bound, and an eventual duration.
  • When neither from nor to nor a duration is specified, the time period is unlimited.
  • When only a duration is specified, a time period of this duration is automatically selected around the current time. This period is always a slice of the upper time unit. For example, if the duration is 10 minutes, each hour is splitted into 6 slices of 10 minutes. Then, the selected time period is the slice that contain the current time. If the upper unit is not dividable by the requested duration, an incomplete slice is produced. For example, a duration of 25 minutes splits each hour into 2 slices of 25 minutes and a slice of 10 minutes.

The last visit connector

The last visit connector allows to retrieve the date (and time) of the last access to a given step or to this operation.

LastVisitConnector = {
    "$lastVisit": String | {
        "step": String,
        "from": Date,
        "to": Date
    }
}

When specified as a single String, the connector definition is directly interpreted as the step field.

Field Description Markup
step Defines the step to consider. By default, consider all accesses to this operation. Yes
from Defines the lower bound of the time period taken into account. Unlimited by default. Yes, but only a numeric timestamp is accepted
to Defines the upper bound of the time period taken into account. Unlimited by default. Yes, but only a numeric timestamp is accepted

The parameters connector

The parameters connector allows to retrieve all input parameters supplied upon access to a given step.

ParamsConnector = {
    "$params": String | {
        "step": String,
        "from": Date,
        "to": Date,
        "output": String | [String] | {String},
        "sort": Number,
        "skip": Number,
        "limit": Number
    }
}

When specified as a single String, the connector definition is directly interpreted as the step field.

Field Description Markup
step Defines the step to consider. "index" by default. Yes
from Defines the lower bound of the time period taken into account. Unlimited by default. Yes, but only a numeric timestamp is accepted
to Defines the upper bound of the time period taken into account. Unlimited by default. Yes, but only a numeric timestamp is accepted
output Defines how the results are generated from the HTTP parameters. See below for details on how to specify this field. Default is "params". Yes
sort Defines the sorting order of results. Can be either -1 (newest first), 1 (oldest first) or 0 (unordered). Unordered by default. Yes
skip Defines a number of results to be skipped. Especially useful in combination with sort and limit for pagination. No result is skipped by default. Yes
limit Defines the maximal number of results to be returned. All results are returned by default. Yes

For each access, the following data is available:

{
    "creationTime": Date,
    "params": {
        "url": {},
        "body": {},
        "cookie": {},
        "files": {}
    }
}
Field Description
creationTime The date (and time) of the access.
params The HTTP parameters. Reflects what was injectable through <((io.params))> at runtime.

The connector produces an array in which each item has been built from the input parameters supplied upon an access. The output field allows to define how to build these items. It consists in one or more strings that reference fields of the above object using the dot-notation. The structure of the output field (single value, array or map) is then replicated for each resulting item, with all references replaced by the targeted values.

For example, let's consider the following output field:

{
    "date": "creationTime",
    "form": "params.body"
}

Assuming that the connector was configured on a step that accepts an HTTP form, we could obtain this kind of result:

[
    {
        "date": Date("Thu, 01 Jan 1970 00:00:00 GMT"),
        "form": {
            "firstname": "Chuck",
            "lastname": "Norris"
        }
    },
    {
        "date": Date:("Sun, 25 Aug 1991 20:57:08 GMT"),
        "form": {
            "firstname": "Linus",
            "lastname": "Torvalds"
        }
    }
]

The action log result connector

The action log result connector allows to retrieve the results produced by the given action(s). Note that only results of actions with the log flag can be retrieved.

ActionLogResultConnector = {
    "$actionLogResult": String | {
        "actionName": String,
        "actionType": String,
        "step": String,
        "from": Date,
        "to": Date,
        "sort": Number,
        "skip": Number,
        "limit": Number
    }
}

When specified as a single String, the connector definition is directly interpreted as the actionName field.

Field Description Markup
actionName Defines the name of the action(s) to consider. By default, consider actions with any name. Yes
actionType Defines the type of the action(s) to consider. By default, consider actions with any type. Yes
step Defines the step to consider. By default, consider all accesses to this operation. Yes
from Defines the lower bound of the time period taken into account. Unlimited by default. Yes, but only a numeric timestamp is accepted
to Defines the upper bound of the time period taken into account. Unlimited by default. Yes, but only a numeric timestamp is accepted
sort Defines the sorting order of results. Can be either -1 (newest first), 1 (oldest first) or 0 (unordered). Unordered by default. Yes
skip Defines a number of results to be skipped. Especially useful in combination with sort and limit for pagination. No result is skipped by default. Yes
limit Defines the maximal number of results to be returned. All results are returned by default. Yes

The action log count connector

The action log count connector allows to retrieve the number of times the given action(s) has been triggered. Note that only actions with the log flag can be counted.

ActionLogCountConnector = {
    "$actionLogCount": String | {
        "actionName": String,
        "actionType": String,
        "step": String,
        "from": Date,
        "to": Date
    }
}

When specified as a single String, the connector definition is directly interpreted as the actionName field.

Field Description Markup
actionName Defines the name of the action(s) to consider. By default, consider actions with any name. Yes
actionType Defines the type of the action(s) to consider. By default, consider actions with any type. Yes
step Defines the step to consider. By default, consider all accesses to this operation. Yes
from Defines the lower bound of the time period taken into account. Unlimited by default. Yes, but only a numeric timestamp is accepted
to Defines the upper bound of the time period taken into account. Unlimited by default. Yes, but only a numeric timestamp is accepted

Evaluation order

The connectors defined in the same input object are implicitly evaluated in parallel. Thus, a connector cannot use a value produced by one of its siblings.

However, there are two ways for controlling the evaluation order of connectors:

  • An input object can define a special $then key. It allows to define another input object (or an array) which is processed only once all the connectors of the current object have been evaluated. The resulting values of these connectors can be injected in the $then field using the markup syntax.
  • When an array of input objects is specified, its items are evaluated in parallel. When coupled with $then, this allows to define several independent evaluation chains.

As shown in the following example, these techniques can cohabit in order to define complex and/or deep dependency graphs. However, most use cases will involve implicit parallel definitions, which are easy to express and evaluated efficiently.

Example of dependency graph

Caution: this example addresses complex situations. Beginners may skip this part at first.

To illustrate the previous section, let's consider the following input object (for the sake of simplicity, connector definitions have voluntary been replaced by empty objects {}):

[
    {
        "a": {},
        "b": {},
        "$then": [
            {
                "c": {}
            },
            {
                "d": {},
                "e": {},
                "$then": {
                    "f": {}
                }
            }
        ]
    },
    {
        "g": {}
    }
]

This declaration can mentally be represented as follows:

                             +---+
                        ,----| c |
            +---+      /     +---+
            | a |     /
       ,----|   |----:
      /     | b |     \      +---+
     /      +---+      \     | d |    +---+
----:                   `----|   |----| f |
     \                       | e |    +---+
      \     +---+            +---+
       `----| g |
            +---+

Now, more verbosely, here is what would happen at evaluation time:

  1. Two evaluation chains start in parallel (two items in the top-level array).
  • The first chain follows the following steps:
    1. Connectors a and b are evaluated in parallel.
    2. When a and b have completed, two new evaluation chains start in parallel (two items in the $then field):
    • The first chain simply evaluates the c connector.
    • The second chain follows the following steps:
      1. Connectors d and e are evaluated in parallel.
      2. When d and e have completed, the f connector is evaluated.
  • The second chain simply evaluates the g connector.
  1. All connectors have properly been evaluated, and the resulting values can now been accessed using the markup syntax (<((io.a))> ... <((io.g))>).

This example just aims at providing an overview of ordering capabilities. It is voluntarily complex and does not reflect a particular use case (see below for a more realistic example). However, it illustrates well how evaluation chains can be powerful. For instance, if evaluating the g connector is particularly expensive, it will not block nor affect in any way the other evaluation chain. Both will progress concurrently, and will be waited transparently for processing the following tasks.

Complete example

TODO

The Step object

A step is fundamentally a piece of logic. There are two kinds of step:

  • A transitional step allow to take decisions by conditionally redirecting to another step.
  • A terminal steps allows to generate an HTTP response, thus ending a chain of transitional steps.

Steps can be seen as statements in a standard programming language: transitional steps mimic control flow structures, while terminal steps mimic regular instructions (with potential side-effects).

Step = RedirectStep | VcardStep | ResponseStep | UmeStep |
       SwitchStep | IfStep | GotoStep | TryStep

Base fields of steps

A step generally looks like the following:

BaseStep = {
    "label": String,
    "input": Input | [Input],
    "trigger": Actions | [Actions]
}
Field Description Markup
label Defines a name for this step. It is mandatory when the step needs to be referenced (connectors, goto steps and analytics). No
input Defines which data is needed locally for this step and its potential descendants. Can be a single value or an array. See the Input object for more details. No
trigger Defines the action(s) to be triggered when accessing this step. Can be a single value or an array. See the Actions for more details. No

The redirection step

A redirection step is a terminal step that allows to send an HTTP redirection.

RedirectStep = String | BaseStep + {
    "redirect": String | Url
}

When the whole step definition is given as a single string, it is implicitly interpreted as the redirect field of a RedirectStep.

Field Description Markup
redirect Defines the destination of the HTTP redirection. It can be any valid URL string, or a parsed URL object. TODO: more details here. Yes, but only the string format is accepted

The vCard step

A vCard step is a terminal step that allows to send a vCard file.

VcardStep = BaseStep + {
    "vcard": Vcard
}
Field Description Markup
vcard Defines the vCard content. See the vCard object for more details. Yes

The response step

A response step is a terminal step that allows to send an arbitrary textual or JSON response.

ResponseStep = BaseStep + {
    "response": Boolean | Number | String | Array | {
        "status": Number,
        "body": Boolean | Number | String | Object
    }
}
Field Description Markup
response Defines the HTTP response. If a primitive type or an array is given, it is implicitly interpreted as the body field. Yes, but interpreted as the body field
response.status Defines the HTTP status code. Defaults to 200 (OK). No
response.body Defines the response data. A string is sent as a textual response (text/plain) while any other data type is sent as JSON (application/json). Yes

The U.me step

An U.me step is a terminal step that allows to render an HTML page.

UmeStep = BaseStep + {
    "ume": Ume,
    "mode": String
}
Field Description Markup
ume Defines the structure of the rendered page. See the U.me documentation for more details. Note that a single-page U.me is expected. Yes
mode Defines the rendering mode, which defaults to components. The classic mode may be used for backward compatibility. No

The switch/cases/default step

A switch/cases/default step is a transitional step that allows to test a given value against multiple conditions in order to select the following step.

SwitchStep = BaseStep + {
    "switch": Boolean | Number | String | Object,
    "cases": {Step} | [Option],
    "default": Step
}
Field Description Markup
switch Defines the value to be tested. Yes
cases Defines the tests to be performed, along with their respective destination steps. In its canonical form, this field is expressed as an array of options. The first encountered matching option (if any) triggers the evaluation of its destination step (see the Option object for more details). In addition, a simpler alternative allows to express this field as a key-value map: if a key is equal to the tested value, then its associated step is directly evaluated. No
default Defines the default step, which is evaluated if no match is found in the cases field. No

The if/then/else step

A if/then/else step is a transitional step that allows to perform a boolean test in order to select the following step. In fact, it is a simplified form of the switch/cases/default step.

IfStep = BaseStep + {
    "if": Boolean,
    "then": Step,
    "else": Step
}
Field Description Markup
if Defines the tested boolean. This value is likely to be obtained from a markup expression. Yes
then Defines the step that is evaluated if the tested value is true. No
else Defines the step that is evaluated if the tested value is not true (this includes non-boolean values). No

The goto step

A goto step is a transitional step that allows to unconditionnaly evaluate a specific step.

GotoStep = BaseStep + {
    "goto": String | Step
}
Field Description Markup
goto Defines the step to be evaluated. When expressed as a string, it allows to jump to the step that defines this specific label. This should be avoided, and reserved for situations where it is absolutely necessary. No

The try/catch/then step

A try/catch/then step is a transitional step that allows to...

TryStep = BaseStep + {
    "try": Test | [Test],
    "catch": Step,
    "then": Step
}

The Test object

Test = {
    "enabled": Boolean,
    "label": String,
    "input": Input | [Input],
    "trigger": Actions | [Actions],
    "value": Boolean | Number | String | Object,
    "assert": Boolean | Number | String | Assertion | [Assertion],
    "catch": Step
}

The Actions object

An Actions object allows to define a set of actions that are triggered upon step evaluation.

Actions = {
    "enabled": Boolean,
    "logged": Boolean,
    "waited": Boolean,

    "data": DataAction | [DataAction],
    "email": EmailAction | [EmailAction],
    "sms": SmsAction | [SmsAction],
    "cookie": CookieAction | [CookieAction],
    "upload": UploadAction | [UploadAction]
}
Field Description Markup
enabled Defines whether these actions are enabled. Can be overridden on a per action basis, and defaults to true. Yes
logged Defines whether these actions are expected to be logged in the database. Can be overridden on a per action basis. The default value depends on the action type. Some action types may also force this flag to a fixed value. Yes
waited Defines whether these actions are expected to be waited before evaluating the rest of the step. Can be overridden on a per action basis. The default value depends on the action type. Some action types may also force this flag to a fixed value. Yes
data Defines the data action(s), if any. No
email Defines the email action(s), if any. No
sms Defines the SMS action(s), if any. No
cookie Defines the cookie action(s), if any. No
upload Defines the upload action(s), if any. No

Base fields of actions

An action generally looks like the following:

BaseAction = {
    "enabled": Boolean,
    "logged": Boolean,
    "waited": Boolean,

    "output": String
}
Field Description Markup
enabled Defines whether this action is enabled. Defaults to the common enabled flag (from the Actions object). Yes
logged Defines whether this action is expected to be logged in the database. Defaults to the common logged flag (from the Actions object). Some action types may also force this flag to a fixed value. Yes
waited Defines whether this action is expected to be waited before evaluating the rest of the step. Defaults to the common waited flag (from the Actions object). Some action types may also force this flag to a fixed value. Yes
output Defines the output name of this action. This makes the action result accessible like this: <((out.{OUTPUT_NAME}))>. It may also be useful for selecting actions by their name in the action log connectors (using the actionName field). No

The data action

A data action allows to record some arbitrary data. No external action is actually performed. However, along with the output field, this can be useful in the following situations:

  • Thanks to the output field, it is possible to assign – and even reassign – an arbitrary value to a custom variable within the <((out))> object.
  • By enabling the logged flag, it is possible to permanently store some arbitrary data, in order to process them later using the action log connectors.
Flag Behavior
logged Defaults to false
waited Defaults to true
DataAction = BaseAction + {
    "value": Boolean | Number | String | Object
}
Field Description Markup
value Defines the arbitrary value produced by this action. Yes

The email action

An email action allows to send a custom email.

Flag Behavior
logged Forced to true
waited Defaults to false
EmailAction = BaseAction + {
    "from": String | [String] | Object | [Object],
    "to": String | [String] | Object | [Object],
    "cc": String | [String] | Object | [Object],
    "bcc": String | [String] | Object | [Object],
    "subject": Boolean | Number | String | Object,
    "body": Boolean | Number | String | Object,
    "attachment": Attachment | [Attachment]
}
Attachment = {
    "text": TextAttachement | [TextAttachement],
    "vcard": Vcard | [Vcard],
    "upload": File | [File]
}
TextAttachement = String | {
    "filename": String,
    "content": String
}

The SMS action

An SMS action allows to send a custom text message.

Flag Behavior
logged Forced to true
waited Defaults to false
SmsAction = BaseAction + {
    "destination": String,
    "text": Boolean | Number | String | Object
}

The cookie action

An cookie action allows to set a custom HTTP cookie.

Flag Behavior
logged Defaults to false
waited Forced to true
CookieAction = BaseAction + {
    "key": Boolean | Number | String,
    "value": Boolean | Number | String | Object,
    "age": Number
}

The upload action

An upload action allows to permanently store a file.

Flag Behavior
logged Defaults to true
waited Defaults to false
UploadAction = BaseAction + {
    "file": String | File,
    "alterations": [Alteration]
}

File = {
    "path": String,
    "filename": String,
    "contentType": String
}

Alteration = /TODO/

The Resolver object

A Resolver allows to specify how the operation can be reached. Each resolver basically defines an URL from which the underlying operation is made accessible. Additionally, it is possible to create a typed resolver, which allows to associate one ore more communication media to the access URL.

Resolver = String | RawResolver | QrcodeResolver | ImageResolver

Base fields of resolvers

A Resolver generally looks like the following:

RawResolver = {
    "host": String,
    "name": String,
    "label": String
}

When a resolver is specified as single String, it is interpreted as the name of a RawResolver.

Field Description Markup
host Defines the host on which the resolver is created (optional). Public hosts are available by default, but custom domains can be added upon request. No
name Defines the name of this resolver, which is used as the URL path. No
label Specifies an arbitrary label for this resolver (optional). No

QR code channel

A resolver can be used as a QR code channel. This means that one or more QR code(s) can be associated to the resolver in order to help the diffusion of the underlying operation. A QR code resolver looks like the following:

QrcodeResolver = RawResolver + {
    "type": "qrcode",
    "design": QrcodeDesign | [QrcodeDesign]
}
Field Description Markup
type Defines the type of channel (QR code here). No
design Describes the design(s) of the generated QR code(s). TODO: more details needed here. No

Image channel

A resolver can be used as an image recognition channel. This means that one or more interactive image(s) can be associated to the resolver in order to make them recognizable by Unitag QR Code Scanner. When such an image is scanned by this application, the user is redirected to the underlying operation. An image resolver looks like the following:

ImageResolver = RawResolver + {
    "type": "image",
    "image": Number | [Number]
}
Field Description Markup
type Defines the type of channel (image recognition here). No
image Describes the uploaded image(s) that should be associated to this resolver. TODO: more details needed here. No

Public hosts

By default, the following domain names can be used in the host field of a resolver:

Domain name Path prefix Description
e.unitag.io /r This is the default domain name, which is used when no host is specified.
opn.to /r This is a freely usable white-label domain.

Access URL

Once create and published, an operation can be reached from the web through an URL of the following form:

       +--> Targets an operation <-+
       |                           |
http://:resolverHost[/:pathPrefix]/:resolverPath/:entryPoint
                      |                          |
                      +--> Fixed by the host     +--> Targets an entry point

The entryPoint is optional, and defaults to index. The other URL components are defined by the resolver(s).

The expression language

Miscellaneous

Predicate testing

Predicate = {
    "eq": Boolean | Number | String | Object,
    "ne": Boolean | Number | String | Object,
    "lt": Boolean | Number | String | Object,
    "gt": Boolean | Number | String | Object,
    "lte": Boolean | Number | String | Object,
    "gte": Boolean | Number | String | Object

    // Other tests
}

The vCard object

Vcard = /TODO/