File references
file <path> is the one keyword Screenplay points at code with. It appears in two places that look the same and mean slightly different things, and the construct it sits on is what tells them apart.
Two relationships, one word
Section titled “Two relationships, one word”On a construct that has an implementation, file stands in for the inline body — the implementation lives there, do not expect it here. It is an alternative to a code block, and it has always worked this way:
command RegisterInvoice invoiceId InvoiceId identifier handler file Invoicing/RegisterInvoice/RegisterInvoiceHandler.csThe constructs that carry it in this sense are a command’s handler and its validation rule predicates, a query’s performer, a reducer rule, a reaction trigger, a constraint and a screen.
On a pure declaration there is no body to delegate — a concept is its primitive, an event is its properties — so the same word can only say one other thing: this is the file that realizes the declaration.
concept InvoiceId : Uuid file Invoicing/InvoiceId.csThe declarations that carry it are concept, type, event, readmodel, projection, slice, specification and the top-level trigger.
One keyword covers both because the construct already decides which is meant. A second keyword would carry no information a reader or a tool does not already have from the node it is reading, and would be one more word to learn.
A worked example
Section titled “A worked example”slice StateChange RegisterInvoice description "Registers an invoice against a customer" file Invoicing/RegisterInvoice/RegisterInvoice.cs
command RegisterInvoice invoiceId InvoiceId identifier
produces InvoiceRegistered invoiceId = invoiceId
event InvoiceRegistered file Invoicing/RegisterInvoice/RegisterInvoice.cs invoiceId InvoiceId
readmodel Invoice file Invoicing/RegisterInvoice/Invoice.cs invoiceId InvoiceId
projection Invoices => Invoice file Invoicing/RegisterInvoice/Invoices.cs from InvoiceRegistered invoiceId = invoiceId
specification RegisteringAnInvoice file Invoicing/RegisterInvoice/when_registering_an_invoice.cs when RegisterInvoice invoiceId = "0f5f5f7f-0f6f-4f47-9f39-5c1f2f0a1a9f" then InvoiceRegisteredSeveral declarations naming the same file is normal and correct — a Cratis slice keeps its backend artifacts in one file by convention.
The file line is written directly under the header, after a description when there is one. That is where the printer puts it too, so a generated document and a hand-written one read the same.
The rules a path follows
Section titled “The rules a path follows”It is repository relative, never absolute. The same path then means the same thing wherever the document is read. An absolute path is reported as PLAY0264 — a warning, so a document carrying one still compiles.
It is never resolved. The compiler does not look for the file, at any severity. A .play document is read in a designer, in a build, and on a machine where the source tree is not present, so a path that has gone stale must not be what makes an otherwise valid document invalid. Whatever can resolve paths — an editor, a generator, a renderer — decides for itself what an unresolvable one means.
It never replaces the declaration. On a declaration file is additive: a projection still declares its blocks, an event still declares its properties, a type still declares at least one property. The language’s guarantee that a document is expressible with zero file references is unchanged — this adds a place to record where code ended up, not a way to leave the document unsaid.
Telling it from a property named file
Section titled “Telling it from a property named file”event, readmodel and type bodies also read property lines, and file is a legal property name. The two are told apart by shape, which is the rule description already follows in the same bodies: a type reference is a bare identifier, so a value carrying a separator or an extension is a path and nothing else.
type Upload file Attachment size IntThat declares a property named file of type Attachment — the property wins the tie, so a document written before the directive existed keeps meaning what it meant. A single-segment path with no extension (file Makefile) is read as a property for the same reason; write it as the repository relative path it is.
A trigger body has always reserved the word, so there the directive wins and a trigger value named after it is written @file:
trigger LedgerFileArrived description "A ledger export landed in the drop folder" file Integrations/LedgerFileTrigger.cs @file nameReading them from the syntax tree
Section titled “Reading them from the syntax tree”Every one of these is a FileReferenceSyntax on the node that declares it, reached through ScreenplaySyntaxWalker.VisitFileReference. Override that one method and you see every file a document names, whichever construct named it.
On a construct that has an implementation the reference is a constructor parameter, as it always was. On a declaration it is an init property named File, so a tool constructing one of those nodes sets it in an object initializer or a with expression rather than positionally - which is how every syntax node grows from here, for the reason syntax tree compatibility gives.
See also
Section titled “See also”- Grammar — the EBNF, and the declarative-first guarantee.
- Diagnostics —
PLAY0264, and why an unresolvable path is not one. - Visitors and traversal — walking a document to collect what it points at.