Bosatsu packages

Yichus/Service

Builtin package (resource service.bosatsu). This is the complete package source used by this build. The export list names its public API; definitions below give the types and behavior.

New to the API? Start with Make a calculator or Build an API, then use this page to look up a definition.

package Yichus/Service

from Bosatsu/Predef import Int, List, String

export (
  Permission(),
  Request(),
  Response(),
  Reply(),
  OneOf(),
  Handler(),
  RouteAuthority(),
  Limit(),
  Route(),
  ServiceDef(),
  ok,
  handler,
  route,
  public_route,
  publishable_route,
  role_route,
  limited,
  limited_by,
  service_def
)

enum Permission:
  ReadPerm(resource: String)
  WritePerm(resource: String)
  CreatePerm(resource: String)
  DeletePerm(resource: String)
  QueryPerm(resource: String)

struct Request(path: String, body: String)

struct Response(status: Int, body: String)

# What a handler answers: its value (`Done`), or a refusal the engine sends
# with the HTTP status its constructor names and a JSON body of
# {"message", "status", "code"}. `e` says why a handler refuses: an enum
# the program declares with two or more constructors, none spelled like
# one of the engine's own codes (invalid, not_found, try_again, ...). A
# NotFound or Refused carries one of its values, whose constructor, in
# snake_case, is the body's `code`, and whose fields join the body beside
# it (KeyReused(key, requested) answers
# {"message", "status": 422, "code": "key_reused", "key", "requested"}).
# The generated OpenAPI document lists each status and each code, so a
# generated client raises the matching error and can branch on the code.
#   Done(value)                200, the value as JSON
#   Invalid(message)           400, code "invalid": the request is
#                              malformed or out of bounds
#   NotFound(message, reason)  404: what the request names does not exist
#                              for the caller
#   Refused(message, reason)   422: a valid request the current state
#                              refuses (a name already taken, a key reused
#                              for another request, a count that does not
#                              fit a saved row)
#   TryAgain(message)          503, code "try_again", with Retry-After:
#                              nothing was saved, or sending the same
#                              request again is safe
# A repeat with the same Idempotency-Key replays every answer but TryAgain.
# There is no 409: generated clients retry a 409 as if it were busy.
enum Reply[e, a]:
  Done(value: a)
  Invalid(message: String)
  NotFound(message: String, reason: e)
  Refused(message: String, reason: e)
  TryAgain(message: String)

# A value of one of two types. In a route's request or result it is
# written as the value alone, with no `type` property naming the
# constructor: a String or a List, say, as a Chat Completions message's
# `content` is. The engine refuses to generate a route whose OneOf has two
# types with overlapping JSON, or JSON that can be null, because JSON could
# not tell which one a value is. A OneOf stored in a table is tagged like
# any other union, so that check does not apply to it there.
enum OneOf[a, b]:
  First(first: a)
  Second(second: b)

# The handler's function type is existentially erased so one service can
# register routes with differently-typed handlers (a rails-like routing
# table). Static analysis never needs the type parameter: route-to-handler
# attribution reads the typed Global reference from the Matchless IR.
struct Handler(name: String, run: exists a. a)

# The authority the generated server must establish before invoking a route.
# `RequiresRoles` means every listed role. Roles come from the server's
# verified identity boundary; request bodies never construct a Principal.
# `Publishable` is `Authenticated`, and an account's publishable key may
# call it too: the key an API's customer puts in pages their own users
# load, as a payment provider's publishable key is. Under
# YICHUS_AUTH=clerk-api-key a publishable key is one whose scopes are
# exactly ["publishable"]; it acts as its account, and every other route
# that reads a credential refuses it (403), the engine's key-making route
# included. A public route reads no credential, so it answers anyone.
# Under every other mode a `Publishable` route is an ordinary
# authenticated one.
enum RouteAuthority:
  Authenticated
  RequiresRoles(roles: List[String])
  Anonymous
  Publishable

# A limit on how often one account may call a route: at most `requests`
# calls in each window of `seconds` seconds, the windows starting at
# multiples of `seconds` since 1970. Every key of one account, publishable
# ones included, counts against the same windows. A call over the limit
# answers 429 with Retry-After, and every answer to a call the engine
# counted says how much is left in RateLimit and RateLimit-Policy headers. The engine's
# HTTP listener counts every call with an account, a malformed one too,
# against the shortest window first; a call past one window is not counted
# against the longer ones. Counts live in
# the store when it keeps them (the Postgres adapter does), so every
# instance of the engine shares them, and in the engine's memory
# otherwise. A public route has no account to count, so it takes no
# limit. A limit `by` request fields counts each account's calls apart for
# each value of those top-level fields of the request body (a pull's
# token, say), so one learner's guesses at one pull run out without
# holding up the account's other pulls.
struct Limit(requests: Int, seconds: Int, by: List[String])

struct Route(
  path: String,
  handler: Handler,
  permissions: List[Permission],
  authority: RouteAuthority,
  limits: List[Limit]
)

struct ServiceDef(name: String, routes: List[Route])

def ok(body: String) -> Response:
  Response(200, body)

def handler[a](name: String, run: a) -> Handler:
  Handler(name, run)

def route(path: String, h: Handler, permissions: List[Permission]) -> Route:
  Route(path, h, permissions, Authenticated, [])

def public_route(path: String, h: Handler, permissions: List[Permission]) -> Route:
  Route(path, h, permissions, Anonymous, [])

def publishable_route(path: String, h: Handler, permissions: List[Permission]) -> Route:
  Route(path, h, permissions, Publishable, [])

def role_route(path: String, h: Handler, roles: List[String], permissions: List[Permission]) -> Route:
  Route(path, h, permissions, RequiresRoles(roles), [])

# `r` with one more limit, counted apart for each value of the named
# top-level request fields, which the route's body must have. Every limit
# on a route holds at once, so a short window can cap bursts while a long
# one caps the total.
def limited_by(r: Route, by: List[String], requests: Int, seconds: Int) -> Route:
  Route(path, h, permissions, authority, limits) = r
  Route(path, h, permissions, authority, [Limit(requests, seconds, by), *limits])

# `r` with one more limit, counted per account alone.
def limited(r: Route, requests: Int, seconds: Int) -> Route:
  limited_by(r, [], requests, seconds)

def service_def(name: String, routes: List[Route]) -> ServiceDef:
  ServiceDef(name, routes)