api.fabaccess-api/resource.capnp
Nadja Reitzenstein f713df2221 More fragmentation
2021-10-28 00:32:25 +02:00

260 lines
11 KiB
Cap'n Proto
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

@0x8c2f829df1930cd5;
using CSharp = import "programming_language/csharp.capnp";
$CSharp.namespace("FabAccessAPI.Schema");
using Persistent = import "/capnp/persistent.capnp".Persistent;
using Value = import "/capnp/schema.capnp".Value;
using User = import "user.capnp".User;
using L10NString = import "utils.capnp".L10NString;
using UUID = import "utils.capnp".UUID;
struct Resource {
# BFFH's smallest unit of a physical or abstract "thing".
# A resource can be as simple and physical as a table, as complex as a PCB production line or as
# abstract as "people with specific know-how are present".
uuid @0 :UUID;
# An stable, globally unique descriptor for a resource. Two resources with the same UUID are
# (almost¹) guaranteed to be the same instance, and the UUID of a resource will survive through
# server restarts, renaming, reconfiguration etc.
#
# [¹]: UUID are 128-bit integer. A collision is *possible*, just *very* unlikely. If you
# generate 1 billion UUID every second for the next 100 years you have a chance for a collision
# of about 50%.
id @1 :Text;
# Every resource in BFFH has a human-readable "name" that is locally unique, but not persistent.
# That is a resource called "hello" today may be called "bye" tomorrow and a resource called
# "hi~~" may not be the same resource as the resource called "hi~~" yesterday. This name is
# canonical and thus identifying. There is exactly *one* valid representation of this name at
# any given point in time. Thus this name can also not be translated.
name @2 :L10NString;
# A resource may also have a human-meaningful name that is designed to be shown to users. This
# name does not have to be unique or identifiable or canonical, its main use is to be
# human-meaningful. For example a "name" could be the translations:
# - (en, "Prusa SL1 SLA-Printer")
# - (de, "Prusa SL1 SLA-Drucker")
# - (es, "Impresora 3D de SLA Prusa SL1")
description @3 :L10NString;
# A resource may have a description attached to tell an user some more information on a resource
# in a free-form format.
# Similar to the human-meaningful name this description can be translated.
notify @4 :Notify;
# Readonly access to the state of a resource.
# A resource can have "state". State are values attached to a resource that describe a specific
# state that users or administrators want this resource to be in. Usually this state consists of
# a number of primitive values encoding for example "turned on" or "turned off".
# Users with the required permissions may change this state.
claimable @5 :Claimable;
# Writeable access to the state of a resource.
# Resources are semaphores. They allow writeable access for n ∈ \{0} clients, depending on the
# exact resource in question. In some cases n ≔ 1, and the only write access is exclusive.
# "Claims" model this by requiring a client to first assert a claim, thus reserving a semaphore
# slot or failing if no more are available, and then using this claim to write to a resources
# state.
interestable @6 :Interestable;
# Sometimes clients are not just interested in the state of a resource but rather want a
# resource to stay in a specific state. e.g. somebody working in a makerspace wants the space to
# stay open, even though they themselves may not have permission to keep the makerspace open.
# "Interest" represents this. Specifically right now it tells BFFH that the client wants at
# least one `Claim` to remain.
}
struct Map(Key, Value) {
# A serialized key-value map represented as a list of (k,v) tuples.
entries @0 :List(Entry);
struct Entry {
key @0 :Key;
val @1 :Value;
}
}
using State = Map(Text, Value);
# Update state provided to a resource via a claim is represented as a Map of human-readable
# identifiers to Cap'n Proto Values. These Values can be either primitive types such as Uint8,
# Float64 or more complex types such as structs, lists, or enums.
# The resulting state of a resource, which is the output of whatever internal logic the resource
# implements, is also represented in this form, but the keys and also values may be different.
#
# Later on very common cases (use, register, return, etc.) can get shortcut functions in the Claim
# interface that pre-emptively check permissions and ability (so you get the respective cap iff the
# resource supports that update and if you're allowed to do that) but these functions only serve to
# make the update more efficient than calling `update` with the string identifier and dynamic typed
# value but do the exact same serverside as an `update` call would. This way we can make future
# versions of the API more efficient and easier to use while not breaking compatibility with old
# clients.
#
# TODO: This has the potential problem that a newer client can not distinguish between a server
# using an old version of the API and a client simply not being allowed to call a specific shortcut
# method because in both cases that cap will be a nullptr. Could be solved by making `Claim` a
# struct and indicating which shortcut methods it knows of.
# Not sure if this is a big problem, we optimize for old clients and up-to-date servers.
#
# TODO: We should provide a number of sensible implementations for common complex `Value` types such
# as "colour", "temperature", etc. and define identifiers for common values.
interface Access {
# Allow syncronous read access to a resource's output state. You're not given this capability
# directly but instead Notify, Interest and Claim all extend it, allowing you to call these
# methods on any of those.
readOutput @0 State;
# TODO: There should probably be a more efficient approach for reading state than "read *all*
# state".
}
interface Notify extends(Access) {
# The Notify interface allows clients to be informed about state changes asyncronously. It is
# mainly designed around the `register` function which allows a client to register a callback
# that is called every time state changes happen to the resource in question.
#
# Notify are ephermal. If the connection to the server is lost all `Notify` from that client are
# unregistered.
register @0 ( cb :Callback );
# Register a given callback to be called on every state update. If this client already has a
# callback registered for this resource the old callback is replaced.
unregister @1 ();
# Unregister this callback
interface Callback {
# This callback interface needs to be implemented on the client
newState @0 State;
# A server will call newState() with the updated output state. However a server will only
# allow one in-flight call, so as long as the previous call to newState() hasn't completed
# the server will drop intermediary updates as to not overload a client.
# Specifically, example timeline:
# 1. Update A
# 2. Server calls newState(A)
# 3. Update B
# 4. Update C
# 5. Call to newState(A) completes
# 6. Server calls newState(C)
# So Update B was never sent to the client but the client will eventually always end up with
# the latest state.
# TODO: There should probably be a more efficient approach here too, something along the
# lines of server-side filtering.
}
}
interface Interestable {
# "Interest" right now it tells BFFH that the client wants at least one `Claim` to remain.
register @0 ( cb :Callback ) -> ( handle :Handle );
# Register a callback that BFFH will use to send notifications back to the client asyncronously.
# This creates an "Interest" on this resource. Setting the callback to a `nullptr` will still
# register an interest but the server will not be able to inform a client about an impeding
# claim drop.
blocking @1 ();
# As an alternative to the `register`/`Callback` system you can also call `blocking` which will
# — as the name suggests — block until the last claim is being dropped. This will register an
# ephermal Interest that can not survive a disconnect.
interface Callback {
drop @0 ();
# The last claim on the resource this Interest is registered is being dropped, invalidating
# the Interest.
}
interface Handle {
# A Handle back to the server side Interest registered. Destroying this capability will also
# inform the server and remove the Interest again.
# TODO: `extends (Persistance)` so that clients can `save` this capability and thus make the
# Interest survive disconnects.
}
}
interface Claimable {
# Having this capability set (i.e. not be a `nullptr`) means the user has at least writeable
# access to a resource and the resource is claimable (n > 0).
claim @0 () -> ClaimResponse;
# Assert a claim on a resource.
struct ClaimResponse {
enum Error {
# Error describing why a claim failed.
exhausted @0;
# There are no more free Claim slots
locked @1;
# The resource was locked
precondition @2;
# Some precondition was not met
dependencies @3;
# Resource failed to secure dependencies
}
union {
failed :group {
error @0 :Error;
reason @1 :L10NString;
}
success @2 :Claim;
}
}
}
interface Claim extends(Access) {
# TODO: extend Persistance. Claims and Interests need to be able to survive a connection loss,
# which is exactly what `SturdyRef`/Persistance are designed to provide. The Persistance
# interface only provides one method, `save`, returning a `SturdyRef`. A SturdyRef is a generic
# and generally speaking opaque type that can be restored to a live capability using some sort
# of `Restorer` service.
# In this case the `Restorer` service could be `Claimable` / `Interestable` providing a
# `restore( ref: SturdyRef )` method.
readInput @0 () -> State;
# Get the current *input* state. This is not the output state that `Notify` or Actors get access
# to but instead the currently stored input state of a resource.
update @1 State -> UpdateResult;
# Update the State of the claimed resource with the given one
struct UpdateResult {
enum Error {
# Reason why the update failed
denied @0;
# Update was denied beause user is missing an required permission
precondition @1;
# Some other precondition failed, e.g. because a required field is not set
invalid @2;
# The update is invalid, e.g. because an unknown field was set.
typeError @3;
# A field in the update has a known identifier but a bad type for that identifier
}
union {
failed :group {
error @0 :Error;
field @1 :Text;
reason @2 :L10NString;
}
success @3 :Void;
}
}
}