Skip to main content

Declarations

A Spine file consists of namespace blocks (optional nesting), imports, and declarations. Names declared inside a namespace are qualified with that namespace.

Namespace

namespace qualified.name { ... }
  • qualified.name is one or more identifiers separated by ..
  • Nested namespace blocks prefix the inner names with the outer dotted name.

Imports

Imports end with ;.

  1. Namespace alias
    import namespace com.example.api as Api;

  2. Glob
    import com.example.models.*;

  3. Entity (optionally aliased)
    import com.example.models.User;
    import com.example.models.User as Person;

For entity imports, the last segment is the type name; the rest is the namespace.

Struct

Struct Name [ @version ]? '{' properties conversion_clauses* '}'
  • Fields: optional annotations, then name : Type, or an anonymous nested group name ':'? '{' properties '}'.
  • Enum variant names may be spelled Struct, Enum, or Annotation where the grammar allows a variant identifier.
  • from clauses and into clauses appear after properties (see Conversions below).

Enum

Enum Name [ @version ]? '{' variant ( ',' variant )* ','? conversion_clauses* '}'

Variants:

  • Unit: VariantName
  • Tuple: VariantName '(' Type ( ',' Type )* ')'
  • Struct-like: VariantName '{' properties '}'
  • from / into clauses appear after variants (see Conversions below).

Annotation

Annotation Name [ @version ]? '{' properties '}'

Defines an annotation type for use as @AnnotationName(...) on fields.

Singleton (Singleton keyword form)

Singleton Name [ @version ]? ':' Type '=' expression

Alternate singleton (ingests, outlets, config)

Type Name [ @version ]? [ ':' Type ]? ( '=' expression | expression )
  • Leading type is any type reference (e.g. Ingest, Outlet, KeyValueApi<String, Item>).
  • Optional second : Type can refine the type when a value is given.
  • The value is = expr or a direct expression (often { ... }).

Wired ingest example:

Ingest EventIngest {
name: "EventIngest",
type: Event,
}

Constant (module-level)

identifier [ @version ]? '=' expression

At namespace scope, a bare name = expression (without the const keyword) declares a named constant. That is how flow = … and joined = … bindings are written: they are constants whose value is a stream expression. Inside functions and closures, use const or let statements instead (Statements).

Function

function Name [ @version ]? '(' parameters ')' [ ':' returnType ] '{' statements '}'
  • Parameters: name [ ':' Type ], comma-separated.
  • The body uses the same rules as a multi-statement closure (return, let, if, for, …).

Conversions

Conversion clauses appear inside Struct and Enum declarations, after the properties/variants list. They declare how a type can be converted to or from another type.

from clause (on the target type)

from SourceType [ @version ]? '(' binding ')' '{' statements '}'
  • binding receives a value of SourceType.
  • The body must evaluate to a value of the enclosing type.
  • Declares that the enclosing type can be constructed from SourceType.

Example — struct versioning:

Struct Event @2 {
id: String
payload: String

from com.example.Event @1 (v) {
Event @2 { id: v.id, payload: "" }
}
}

Example — enum versioning:

Enum Status @2 {
Active,
Inactive,
Archived

from com.example.Status @1 (v) {
match v {
Active -> Status @2.Active,
Disabled -> Status @2.Inactive,
}
}
}

into clause (on the source type)

into TargetType [ @version ]? '(' binding ')' '{' statements '}'
  • Used when the target type is a library type or any type you cannot add a from clause to.
  • binding receives a value of the enclosing type.
  • The body must evaluate to a value of TargetType.

Example:

Struct MyDto @1 {
count: UnsignedInteger32

into com.example.library.Counter (v) {
Counter.new(value: v.count)
}
}

Multiple clauses

A single type can declare multiple from / into clauses. Duplicate directed pairs (same source and target) are rejected.

Call site

Use .into(TargetType) to apply an explicit conversion, or .into() when the target can be inferred from context:

let newEvent: Event @2 = oldEvent.into(Event @2)
let newEvent: Event @2 = oldEvent.into() // inferred from type annotation

See Operators for precedence details.