#specification-language #interface #design #arc #path #analysis #url-path

bin+lib arc-isle

The Interface Specification Language for the Arc project: design and implementation

3 releases

0.1.2 Mar 10, 2024
0.1.1 Mar 7, 2024
0.1.0 Feb 7, 2024

#1169 in Network programming

Apache-2.0

75KB
2K SLoC

ArcISLE

-- The Interface Specification Language.

Its goal is to provide a lightweight way to describe virtually any type of interfaces in the software and provide more advanced tools for the analysis and governance.

Status

The project is in early development stage. At the moment, it is capable of parsing a document created according to the specification described below, and emit errors in places that does not match without failing all the parsing.

Nearest plans is to:

  • Add types declarations validation. Right now we collect declared types, but does not verify that all usages stict to that declarations and emit errors.
    • Still missing precise localisation of missing declaration, good to add at some point before completion.
  • Command-line interface.
  • Check speed on larger documents. So far it has been tested on a really small API specification, large and more real-world example is required to define if there optimizations to be done.
  • OpenAPI <> ArcISLE convertation.
  • Mix types and routes declaration
  • Different route versions
  • Default response code guess

Structure

All specification is written using YAML solely. It allows to write less, do this faster, and still have powerful tools to cover wide range of needs. JSON has inconvenient limitations, like requiring quotes almost everywhere, which make it harder to write manually.

Main sections are following:

  • types — describes list of objects used in an API.
  • interfaces — describes endpoints provided by an API.

By default, it is recommended to not maintain single document with description of everything, but start decomposing files into two groups: types and interfaces. This could be done by importing subset of the document:

types:
    _import: types.yml

Imports also can include list of files:

_import:
    - file1.yml
    - file2.yml
    - file3.yml
    - file4.yml

Path to the file is resolved relatively to the root document, from which an import is requested.

Types

API usually contains a lot of specific types, like user, or post, or transaction, etc. These entities appear in many places as we start defining interfaces. By defining them upfront and reusing we can simplify workflow in future. That’s what types for.

Types declarations is allowed inside types section (or separated files included inside that section). To declare a type, specify its name and set of fields. Each field has its name and type, defined after a semicolon.

user:
    id: uuid
    name: str
    role: str

You can also nest types up to 3 levels (this limitation is artificial and made for improved readability):

settings:
    version: str?
    flags:
        a: bool
        b: bool	
        c: bool

Types can be referenced inside other types:

post:
    title: str
    body: str
    author: user

Built-in

Specification supports a set of default types: primitives, containers, and formats. They serve to cover basic needs and allow to build custom types.

Primitives

All data we are working with has some basic type after all, most essential of them are supported:

  • int - represents integer numbers
  • double - represents double precision number
  • bool - represents boolean
  • str - unicode strings

Containers

To serve needs of collections or nesting JSON, there are two container type:

  • array[type] or array
  • dict[key, value] or dict

Square braces act as inner element definition, by specifying type inside of them you denote that it is expected, for example, an array of integers, or a dictionary keyed by string and containing an integer as value. There are no limitations of types that can be used.

No-braces notation removes typing of elements which might be useful in certain cases, but does not recommended in general use.

Specific Formats

Some types of data are frequently used, and specifying them simply as string might means loosing information. To improve that, specification offers some default types, identified as commonly used.

  • timestamp — acts as double, denotes that this double is expected to be a time in UNIX format.
  • date_iso8601 — acts as string, denotes and validates field to confirm ISO 8601 date standard.
  • uuid — acts as string, denotes and validates field to contain a valid UUID value.
  • url

Optionality

Fields inside types might be required or optional. To simplify declaration each field has optionality parameter represented by ? symbol. Adding it in the end of the field type denotes that this field is optional and might by omitted in the instance of this type.

title: str
description: str?
connections: array[uuid]?

Optionality only ****refers to a field, not a type, which means specifying, e.g. array[int?] is not a valid syntax.

JSON Schema

All types are compatible with JSON Schema by design. Any implementation of this specification has to provide a way to generate valid schema from types.

Interfaces


Now, heart of every REST API is actually endpoints, which called interfaces here as they are interface between server and client(s). Declaration of an interface a bit more complex, than of a type. Two mandatory fields of each interface is path and method:

- path: news
  method: get

Path could be parameterised using curly brackets:

- path: users/{user_id}

It is recommended to include type name in parameter name if possible to increase readability. Compare following versions:

  • users/{id}/posts/{id}/comments/{id}
  • users/{user_id}/posts/{post_id}/comments/{comment_id}

Leaving aside question of nesting decision here, second option is far easier to understand just by looking at it, since we clearly understand what each parameter represents.

Rest of options depend on details request, and follow HTTP standards.

Field Required Purpose Restrictions Possible values
query No Specifies query parameters of a request. Allowed only within GET and HEAD requests. Required to be a valid custom type.
body No Specifies body of a request. Allowed within POST, PUT, and PATCH requests. Required to be a valid custom type.
body_type No Specifies type of a body. Allowed only together with body present. Supports currently only form-data value.
response No Specifies response of a request. No restrictions. Any type is allowed in response.

This is enough to specify basic requirements for endpoints with success flow in mind, for example:

types:
  news_entry:
    id: str
    title: str
    link: url
interfaces:
    - path: news
      method: get
      query:
        search: str?
      response:
        items: array[news_entry]
        next_page_link: url?
    - path: news
      method: post
      body:
        title: str
        link: url
      response: news_entry
    - path: news/{entry_id}
      method: delete

Different responses

An advanced scenario is when we want to define custom responses for different cases, like success, failure, permissions error, etc. To support it, response field supports variable content in the following way:

- path: news/{entry_id}
  method: delete
  response:
    200: news_entry
    4xx: 
        code: int
        reason: str?
    5xx:
        message: str
    501:
        code: int
        reason: str?

You can specify either concrete status code, like 200 or 501 in example above, or pattern for a family of status codes, like 4xx and 5xx, and you can combine both styles.

By default 2xx family is assumed if response field does not specify any code.

Dependencies

~2.2–7MB
~59K SLoC