Interface Description Language

The interface description language of webwire is inspired by the Rust programming language.

The syntax is specified using Extended Backus-Naur Form (EBNF):

|   alternation
()  grouping
[]  option (zero or one time)
{}  repetition (any number of times)

Lexical elements

LETTER = "A" … "Z" | "a" ... "z"
DIGIT_DEC = "0" … "9"
DIGIT_HEX = DIGIT_DEC | "A" … "F" | "a" … "f"

Identifier

Identifiers must start with a letter. Subsequent characters may also include digits and the underscore "_" character.

identifier = LETTER { LETTER | DIGIT_DEC | "_" }

Values

Boolean

Booleans are either true or false.

boolean = "true" | "false"

Integer

Integers support both decimal and hexadecimal format.

integer_dec = [ "+" | "-" ] DIGIT_DEC { DIGIT_DEC }
integer_hex = [ "+" | "-" ] "0" ("X" | "x") DIGIT_HEX { DIGIT_HEX }
integer = integer_dec | integer_hex

Examples:

  • 57005
  • +3
  • -5
  • 0x539
  • +0xFF
  • -0x7FFF

Float

Floats must contain at least one digit before and after the decimal separator.

float = [ "+" | "-" ] DIGIT_DEC { DIGIT_DEC } "." DIGIT_DEC { DIGIT_DEC }

Examples:

  • 2.56
  • +5.3338
  • -0.5

String

Strings are quoted using double quotes '"' and the backspace character "\" is used to escape special characters.

Supported escapes are:

  • \\ → backspace "\"
  • \" → double quote '"'
  • \n → newline (linefeed)
string = '"' { char_escape | /[^\\]/ } '"'
char_escape = "\" ( "\" | '"' | "n" )

Examples:

  • "master"
  • "line1\nline2\nline3"
  • "backslash: \\"

Range

Ranges have an upper and lower bound which are separated via ".."

range = integer ".." integer

Examples:

  • 0..255
  • 0..0xFF
  • 1..
  • ..50

Types

Types can either be named or used as part of an array type or map type. The named form also supports passing generic parameters.

type = (type_named | type_array | type_map) [ type_options ]
type_named = identifier ["<" type { "," type } ">"]
type_array = "[" type "]"
type_map = "{" type ":" type "}"
type_options = "(" [ type_option { "," type_option } [ "," ] ] ")"
type_option = identifier "=" value

Examples:

  • FooBar
  • PaginatedResponse<Bar>
  • [ Integer ]
  • [ Integer ] (length=1..16)
  • { Integer: String }
  • String (length=0..50)
  • Integer (range=-0x80..0x7F)

Builtin types

Builtin types are reserved type names and can not be used as names for your own types. It is however possible to use them as identifiers though this SHOULD NOT be done on a regular basis.

Builtin types are:

  • Boolean
  • Integer
  • Float
  • String
  • Date
  • Time
  • DateTime
  • UUID
  • None
  • Nullable<T>
  • Result<T, E>

None is a special type that does only have one valid value which is None. This is useful for methods that don't have input or output types and when working with optional generics. None should not be used on its own for field types as it has no meaning there.

Nullable is a special type which wraps another type internally. It is similar to enum Nullable<T> { Some(T), Null } except that it maps to null or a similar values in programming languages that support this concept. It is typically used in APIs to clear values. This must not be confused with optional fields of structures.

Result is a special type which is an enum that can either be Ok(T) or Err(E). It is similar to enum Result<T, E> { Ok(T), Err(E) } except that it maps directly to the builtin Result type if supported by the target programming language.

Generics

Structures, Enumerations and some builtin types support generics.

generics = "<" identifier { "," identifier } ">"

Struct

Structures are a collection of fields of storing complex data structures.

struct = "struct" identifier [ generics ] "{" [ struct_fields ] "}"
struct_fields = struct_field "," struct_field
struct_field = identifier [ "?" ] ":" type

Examples:

  • struct Pet {
        name: String,
        age?: Integer
    }
    
  • struct Complex {
        r: Float,
        i: Float
    }
    
  • struct Person {
        first_name: String (length=1..50)
    }`
    
  • struct PaginatedResponse<T> {
        results: T,
        page: Integer(range=0..),
        count: Integer(range=0..),
    }
    

Optional fields and nullable types

By appending a ? to the field identifier a field is marked as optional. Optional means that the field can be absent from the structure.

There also exists the special Nullable<T> type which is used for something completely different. While optional means that the field can be missing from the serialized structure Nullable<T> means that instead of T a special Null value can be transferred instead. It is perfectly valid to use optional and nullable at the same time:

struct UpdateProfile {
    name?: String,
    age?: Nullable<Integer>,
}

Both name and age are optional in this example. Since Strings do have a special empty value (the empty string: "") it does not need to be Nullable. Integers on the other hand do not have such thing so Nullable<Integer> would make it possilbe to clear the age of a user profile. A JSON serialized structure containing an empty string for the name and Null for the age would look like this:

{
    "name": "",
    "age": null,
}

Some programming languages and serialization formats support this quite naturaly. JavaScript and JSON differenciate between undefined and null for object attributes. For languages that don't support this out of the box optional fields and nullable types are put in a special wrapper.

Fieldset

Fieldset is a special kind of structure that does not define its own fields but uses an existing structure and creates a subset of it. This feature is mainly for keeping the repetition for typical CRUD APIs as little as possible.

fieldset = "fieldset" identifier "for" identifier "{" struct_fields "}"
fieldset_fields = [ fieldset_field { "," fieldset_field } [ "," ] ]
fieldset_field = identifier [ "?" ]

Examples:

  • fieldset PersonUpdate for Person {
        id,
        first_name?,
        last_name?
    }
    

Enumerations

Enumerations in webwire fulfill two things. They can either be used as plain enumerations like in most programming languages. An optional type argument makes it possible to describe tagged unions.

This is especially handly for returning errors which might contain data which depends on the actual error code.

Enumerations can also extend existing enumerations.

enum = "enum" identifier [ generics ] [ enum_extends ] "{" enum_variants "}"
enum_extends = "extends" identifier [ generics ]
enum_variants = [ enum_variant { "," enum_variant } [ "," ] ]
enum_variant = identifier [ "(" type ")" ]

Examples:

  • enum Status {
        Enabled,
        Disabled,
        Error
    }
    
  • enum AuthError {
        Unauthenticated,
        PermissionDenied
    }
    
  • enum GetError extends AuthError {
        DoesNotExist
    }
    
  • enum Notification {
        UserJoined(User),
        UserLeft(User),
        Message(ChatMessage),
    }
    

Namespace

TODO

namespace = "namespace" identifier "{" namespace_parts "}"
namespace_parts = { namespace_part }
namespace_part = struct | fieldset | enum | namespace | service

Service

TODO

service = ["async" | "sync"] "service" identifier "{" methods "}"
methods = [ method { "," method } [ "," ] ]
method = identifier ":" ( type | "None" ) "->" ( type | "None" )