An entity is a shortcut for a number of pre-existing elements, but enforcing strict convention.
Entities work with the pentops/protostate repo to implement event-driven state machines.
An entity consists of:
Primary, foreign and natural, all immutable.
Specified as the collection of key fields in the entity definition
entity.
These become the FooKeys message.
An object representing the mutable data in the entity.
Specified as the collection of data fields in the entity definition.
These become the FooData message.
An Enum representing the fixed statuses the entity can be in.
This becomes the FooStatus enum.
entity Foo {
...
status ACTIVE
status INACTIVE
}
enum FooStatus {
FOO_STATUS_UNSPECIFIED = 0;
FOO_STATUS_ACTIVE = 1;
FOO_STATUS_INACTIVE = 2;
}The event shapes which modify the state.
Each event is specified as an object.
event Create {
field name string
}
The event types are represented as a single oneof 'FooEventType'.
The entity definition will automatically produce the 'query' service, which has a standard 'Get', 'List' and 'ListEvents' endpoint for interacting with the entity.
In addition, 'command' or alternate query services can be attached to the entity. The commands and queries defined within the entity should only interact with the entity itself, for any cross-cutting concerns, use a detached service. Attached services should be seen as 'methods' of the entity.
The entity definition will automatically produce a 'Publish' topic, which publishes all events (state transitions) for the entity.
Custom topics can not be defined within an entity block (yet).
package foo.v1
entity Foo {
| Foo is lorem ipsum
key fooId key:id62
data name string
status ACTIVE
status INACTIVE
event Create {
field name string
}
event Archive {
}
}
Produces the working objects:
The wrapper for all key fields
message FooKeys {
option (j5.ext.v1.psm) = {
entity_name: "foo"
entity_part: ENTITY_PART_KEYS
};
option (j5.ext.v1.message).object = {};
string foo_id = 1 [
(buf.validate.field).string.pattern = "^[0-9A-Za-z]{22}$",
(j5.ext.v1.field).key = {}
];
}The mutable data fields
message FooData {
option (j5.ext.v1.psm) = {
entity_name: "foo"
entity_part: ENTITY_PART_DATA
};
option (j5.ext.v1.message).object = {};
string name = 1 [(j5.ext.v1.field).string = {}];
}The wrapper state message
message FooState {
option (j5.ext.v1.psm) = {
entity_name: "foo"
entity_part: ENTITY_PART_STATE
};
option (j5.ext.v1.message).object = {};
j5.state.v1.StateMetadata metadata = 1 [
(buf.validate.field).required = true,
(j5.ext.v1.field).object = {}
];
FooKeys keys = 2 [
(buf.validate.field).required = true,
(j5.ext.v1.field).object.flatten = true
];
FooData data = 3 [
(buf.validate.field).required = true,
(j5.ext.v1.field).object = {}
];
FooStatus status = 4 [
(buf.validate.field) = {
required: true
enum: {
defined_only: true
}
},
(j5.ext.v1.field).enum = {}
];
}The oneof wrapper for all events
message FooEventType {
option (j5.ext.v1.message).oneof = {};
oneof type {
Create create = 1 [(j5.ext.v1.field).object = {}];
Archive archive = 2 [(j5.ext.v1.field).object = {}];
}
message Create {
option (j5.ext.v1.message).object = {};
string name = 1 [(j5.ext.v1.field).string = {}];
}
message Archive {
option (j5.ext.v1.message).object = {};
}
}A wrapper for the event itself, taking the type and metadata.
message FooEvent {
option (j5.ext.v1.psm) = {
entity_name: "foo"
entity_part: ENTITY_PART_EVENT
};
option (j5.ext.v1.message).object = {};
j5.state.v1.EventMetadata metadata = 1 [
(buf.validate.field).required = true,
(j5.ext.v1.field).object = {}
];
FooKeys keys = 2 [
(buf.validate.field).required = true,
(j5.ext.v1.field).object.flatten = true
];
FooEventType event = 3 [
(buf.validate.field).required = true,
(j5.ext.v1.field).oneof = {}
];
}The enum to store the status.
enum FooStatus {
FOO_STATUS_UNSPECIFIED = 0;
FOO_STATUS_ACTIVE = 1;
FOO_STATUS_INACTIVE = 2;
}The query service, with Get, List and ListEvents methods, produced into the
sub-package foo.v1.service.
service FooQueryService {
option (j5.ext.v1.service).state_query.entity = "foo";
rpc FooGet(FooGetRequest) returns (FooGetResponse) {
option (google.api.http) = {get: "/foo/v1/foo/q"};
option (j5.ext.v1.method).state_query.get = true;
}
rpc FooList(FooListRequest) returns (FooListResponse) {
option (google.api.http) = {get: "/foo/v1/foo/q"};
option (j5.ext.v1.method).state_query.list = true;
}
rpc FooEvents(FooEventsRequest) returns (FooEventsResponse) {
option (google.api.http) = {get: "/foo/v1/foo/q/events"};
option (j5.ext.v1.method).state_query.list_events = true;
}
}
//... plus implementation messages
The publish topic, for sending events to the entity, produced into the
sub-package foo.v1.topic.
service FooPublishTopic {
option (j5.messaging.v1.service) = {
topic_name: "foo_publish"
publish: {
}
};
rpc FooEvent(FooEventMessage) returns (google.protobuf.Empty) {}
}
message FooEventMessage {
option (j5.ext.v1.message).object = {};
j5.state.v1.EventPublishMetadata metadata = 1 [
(buf.validate.field).required = true,
(j5.ext.v1.field).object = {}
];
foo.v1.FooKeys keys = 2 [
(buf.validate.field).required = true,
(j5.ext.v1.field).object = {}
];
foo.v1.FooEventType event = 3 [
(buf.validate.field).required = true,
(j5.ext.v1.field).oneof = {}
];
foo.v1.FooData data = 4 [
(buf.validate.field).required = true,
(j5.ext.v1.field).object = {}
];
foo.v1.FooStatus status = 5 [
(buf.validate.field) = {
required: true
enum: {
defined_only: true
}
},
(j5.ext.v1.field).enum = {}
];
}