-
Notifications
You must be signed in to change notification settings - Fork 4
schema language
This is a draft page filled as we go with the language implementation. The
ultimate source of truth for the syntax is schema.bnf
.
Epigraph schema files have an .epigraph
extension and consist of a namespace
declaration followed by imports followed by type and resource declarations.
- Schema is case-sensitive
- White spaces have no semantics
- Most of the commas are optional, for instance between field or parameter declarations
- C++ style single and multi-line comments are supported
In the following sections angle brackets will be used for non-terminal parts of the syntax, and curved brackets for optional parts, for example:
list[〈 value type〉] 〈type name〉 ⧼extends clause⧽
Namespace declaration is mandatory and consists of a namespace
keyword
followed by a fully-qualified namespace. Fully-qualified namespace is a list of
dot-separated namespace names forming a hierarchy, similar to Java packages.
Namespace names must be lower-cased.
Namespaces are used to organize types and resources into logical groups. They may also be translated by the code generators into appropriate language constructs such as packages.
There are two kinds of imports: namespace imports and single type imports.
Single type import makes one single type from another package available:
import foo.bar.Baz
record R {
myField: Baz
}
Importing types with the same short name from different namespaces is not allowed:
import foo.bar.Baz
import qux.Baz // clash!
Namespace import brings given namespace into the scope, so that namespace prefix can be omitted, except for the last segment:
import foo.bar
record R {
myField: bar.Baz
}
Importing namespaces with the same last segment is not allowed:
import foo.bar
import qux.bar // clash!
The following imports are always implicitly present:
import epigraph.String
import epigraph.Integer
import epigraph.Long
import epigraph.Double
import epigraph.Boolean
Type references are resolved in the following order:
- Using explicit imports
- Using implicit (standard) imports
- Types from the same (current) namespace
See References implementation for more technical details.
Here's a full list of reserved keywords that can't be used as type names, field names, parameter names etc.
CREATE
CUSTOM
DELETE
GET
POST
PUT
READ
UPDATE
abstract
boolean
default
deleteProjection
double
enum
extends
forbidden
import
inputProjection
inputType
integer
integer
list
long
map
meta
method
namespace
outputProjection
outputType
override
path
projection
record
required
resource
string
supplement
supplements
transformer
vartype
with
todo
Type declarations allow to define new custom named types. Custom type can be one of the following supported kinds:
- entity
- record
- map
- list
- primitive
- enum (unimiplemented as of writing)
Further definitions will be using a few building blocks:
Type name is either a
- string that starts with an upper case letter, only contains letters or digits and is not one of the keywords
- or an arbitrary string of any characters except backticks, enclosed in backticks
Keep in mind that codegens may not be happy if you go too creative with the latter case.
Type reference is a type name that can be resolved, for instance
String
or foo.bar.Baz
Value type is an entity type reference or model value type. It describes a field, map entry or a list element type: it can be a either a type reference, or an inline anonymous map or list declaration.
Model value type is a model type reference or an anonymous map or an anonymous list. It is a subset of a value type that can't resolve to an entity type and is used for entity tags. This is important because entity types can't act as each others entity models.
Is an inline list declaration without a name. Syntax is
list[〈value type〉]
Examples:
list[String]
list[some.Type]
list[list[list[Boolean]]]
Is an inline map declaration without a name. Syntax is
map[〈model value type〉, 〈value type〉]
Notable limitation here is that only model types can act as map keys, entity types are not allowed.
Examples:
map[String, String]
map[some.UserId, some.UserRecord]
Entity types describe entities that may have multiple representations, or models. Models are identified by unique named tags and their values are model value types. This means that entity types can't act as other entity models.
Entity type declaration syntax:
entity 〈type name〉 ⧼extends⧽ ⧼supplements⧽ ⧼meta⧽ ⧼body⧽
Where extends
is extends clause, supplements
is [supplements clause](#supplements clause),
meta
is metadata clause.
Optional body is a list of annotations and tag declarations enclosed in curly braces.
Tag declaration syntax:
⧼override⧽ 〈tag name〉: 〈model value type〉 ⧼tag body⧽
tag name obeys same rules as type name, but must start with a lower-case letter.
Optional tag body is a list of annotations enclosed in curly braces.
Examples:
entity Creature
entity Person extends Creature {
@Doc "Person entity type"
id: PersonId
}
entity User extends Person {
override id: UserId { @Doc "User ID" }
}
Entity type inherits all the tags from it's parents and there should be no type conflicts between them. Type of the overriding tag must be a sub-type of the overriden tag (or be the same type). Annotations on overriding tags hide annotations coming from overriden tags.
override
modifier is optional, but a tag must override some other tag if it is present.
Primitive declaration syntax
〈primitive kind〉 〈type name〉 ⧼extends⧽ ⧼supplements⧽ ⧼meta⧽ ⧼body⧽
Where extends
is extends clause, supplements
is [supplements clause](#supplements clause),
meta
is metadata clause.
primitive kind
is one of string
, long
, integer
, boolean
, double
.
Optional body is a list of annotations in curly braces.
Primitive types can only inherit other primitive types of the same pimitive kind, i.e a long
can only extend
another long
and can't extend a double
.
Examples:
long PersonId
long UserId extends PersonId { @Doc "User ID" }
Record type declaration syntax:
record 〈type name〉 ⧼extends⧽ ⧼supplements⧽ ⧼meta⧽ ⧼body⧽
Where extends
is extends clause, supplements
is [supplements clause](#supplements clause),
meta
is metadata clause.
Optional body is a list of annotations and field declarations enclosed in curly braces.
Field declaration syntax:
⧼override⧽ 〈field name〉: 〈value type〉 ⧼field body⧽
field name obeys same rules as type name, but must start with a lower-case letter.
Optional field body is a list of annotations enclosed in curly braces.
Examples:
record PersonRecord {
@Doc "Person record type"
id: PersonId
name: String
}
record UserRecord extends PersonRecord {
override id: UserId
profile: Url { @Doc "User profile URL" }
}
Record type inherits all the fields from it's parents and there should be no type conflicts between them. Type of the overriding field must be a sub-type of the overriden field (or be the same type). Annotations on overriding fields hide annotations coming from overriden fields.
override
modifier is optional, but a tag must override some other tag if it is present.
Syntax:
map[〈model value type〉, 〈value type〉] 〈type name〉 ⧼extends⧽ ⧼supplements⧽ ⧼meta⧽ ⧼body⧽
Where extends
is extends clause, supplements
is [supplements clause](#supplements clause),
meta
is metadata clause.
Optional body is a list of annotations enclosed in curly braces.
Limitations:
- only model types can act as map keys, entity types not allowed
- maps can only inherit maps with the same key/value types
Examples:
map[UserId, UserRecord] UserMap meta Pagination {
@Doc "Users map"
}
Syntax:
list[〈value type〉] 〈type name〉 ⧼extends⧽ ⧼supplements⧽ ⧼meta⧽ ⧼body⧽
Where extends
is extends clause, supplements
is [supplements clause](#supplements clause),
meta
is metadata clause.
Optional body is a list of annotations enclosed in curly braces.
Lists can only inherit lists with the same element types.
Examples:
list[User] UsersList Pagination { @Doc "Users list" }
Most of custom types support inheritance. Common limitation is that only type of the same kind may be inherited, for instance a record can extend another record but can't extend a primitive. Specific kinds impose their own additional constraints which are discussed in corresponding sections.
Multiple inheritance is supported. Circular inheritance is not allowed. Linearization algorithm similar to Scala trait linearization is used to flatten out inheritance hierarchies and resolve diamond problem.
Inheritance relationship can be initiated by both child side (using extends
clause) and parent side
(using supplements
clause). It can also be declared using standalone supplement
statement. This means that
in addition to traditional way of defining subtypes, it is also possible to inject supertypes into already
existing types.
Any annotations specified for child types hide annotations of the same type coming from parent types.
Extends clause provides a list of types that are inherited by a given type. Every list element is a type reference of the same kind. Examples:
extends String
extends foo.Bar, bar.Baz
Supplements clause specifies a list of type that will inherit a given type. Every list element is a type reference of the same kind. Examples:
supplements some.UserRecord, some.other.PersonRecord
Supplement statement allows to establish inheritance between types defined somewhere else. General syntax is
supplement 〈comma-separated list of type references〉 with 〈type reference〉
For example
supplement some.UserRecord with twitter.WithTwitterAccount