Docs / ArchitectureKit / Rust / Guides / Composing Subjects

Composing Subjects

So far, subjects have been composed by hand.

To define their structure once, create a SubjectScheme with a pattern, and use placeholders in braces for the variable parts. Parsing the pattern once is enough, so keep the scheme in a static:

use std::sync::LazyLock;

use architecturekit::SubjectScheme;

static BOOKS: LazyLock<SubjectScheme> = LazyLock::new(|| SubjectScheme::new("/books/{book_id}"));

Each segment of a subject may only contain the characters that EventSourcingDB allows: the letters A-Z and a-z, the digits 0-9, underscores, and hyphens (see Subjects). This applies to the literal segments of the pattern as well as to the values that fill its placeholders. The names of the placeholders follow the same rule, as book_id and license-agreement do, so that a typo, such as a brace too many in {book_id}}, is not taken for a name.

To compose a subject, call the build method with one value per placeholder, in the order in which they appear in the pattern. It returns a Subject, which is what a command names as its subject, so use it in every command that acts on a book:

let acquire_command = AcquireBook {
  book: BOOKS.build(&["42"]),
  title: "2001 – A Space Odyssey".to_owned(),
  author: "Arthur C. Clarke".to_owned(),
  isbn: "978-0756906788".to_owned(),
};

let borrow_command = BorrowBook {
  book: BOOKS.build(&["42"]),
  reader_id: "23".to_owned(),
  borrowed_until: "2026-10-24".to_owned(),
};

let return_command = ReturnBook { book: BOOKS.build(&["42"]) };

To take a subject apart, call the match_subject method. It returns the values by placeholder name, and None if the subject does not follow the pattern or has a value that build would refuse. It is called match_subject rather than match, which is a keyword in Rust:

let values = BOOKS.match_subject("/books/42");
// values == Some(HashMap::from([("book_id", "42")]))

To take a single value out of a subject, call the value method with the subject and the name of the placeholder. Like match_subject, it returns None if the subject does not follow the pattern. Both take a Subject as well as text:

let book_id = BOOKS.value("/books/42", "book_id");
// book_id == Some("42")

Note that value panics for a placeholder that the pattern does not have, such as one with a typo, whatever the subject is. The map that match_subject returns simply has no entry for such a name instead, so use value for a single value, and match_subject for several values of the same subject.

To get the pattern and the names of the placeholders, call the pattern and the placeholders method respectively.

To read or observe the events of all books, for example in a projection, start from the subject that all of them lie under. Call the root method to get it from the scheme, rather than writing it down a second time. It returns the literal segments before the first placeholder, here /books, or / if the pattern starts with a placeholder:

let run = store.start_projection(
  Subjects::tree(BOOKS.root()),
  catalog_projection,
  token.clone(),
  ProjectionOptions::default(),
);

Note that other subjects may lie under the same root, such as /books/42/reviews/7 under /books. Use match_subject or value in the projection to tell them apart.

Note that a malformed pattern panics, including one with a literal segment or a placeholder name that contains a character EventSourcingDB does not allow, as does calling build with the wrong number of values, with an empty value, or with a value that contains such a character, for example a slash, a dot, or a space.

Values that come from outside, such as an ID in a request, may well be empty or contain such characters, and that is not a programming error. So always check them before building a subject: call the check method with the same values as build. It returns a SubjectValuesError that says what is wrong, such as which characters a value may contain, instead of panicking. To check the values and compose the subject in one step, call the try_build method, which returns the subject or the same error. The error may reach the caller of an API, where ? answers it with 400 Bad Request (see Handling Commands over HTTP), so it names the placeholder of the value, but neither the crate nor the pattern, as in value for "book_id" must not be empty:

BOOKS.check(&[&book_id])?;