diff --git a/repository/interface.go b/repository/interface.go index 83876c9..1c1cc74 100644 --- a/repository/interface.go +++ b/repository/interface.go @@ -47,7 +47,10 @@ func (m method) UnderlyingComment() Comment { return m.Comment } func (m method) internalMethod() {} // GetterMethod is a method that returns a regular value. These methods must not -// do any IO. An example of one would be ID() returning ID. +// do any IO. An example of one would be ID() returning ID. For methods that +// don't take in any parameters, The return values must be constant throughout +// the lifespan of the program. As such, the frontend can assume that the return +// values will never change and store them. type GetterMethod struct { method diff --git a/services/ipc/README.md b/services/ipc/README.md new file mode 100644 index 0000000..678a753 --- /dev/null +++ b/services/ipc/README.md @@ -0,0 +1,327 @@ +# rpc + +Here are the plans for cchat's backend Remote Procedure Call (RPC) protocols and +implementations. + +# TODO + +Move the majority of this into actual Go code. + +# Specifications + +## Preamble + +This reference documentation will use terms such as "GetterMethod" and +"IOMethod." These terms refer to the named structs inside the [repository +package][repository], which defines the categories of cchat types. + +It is recommended that the [reference documentation for the repository +package][repository] be used in conjunction with this reference documentation. + +Some of the protocols defined here are inspired by [Wayland's wire +protocol][wayland-wire-protocol]. + +[repository]: https://godoc.org/github.com/diamondburned/cchat/repository +[wayland-wire-protocol]: https://wayland-book.com/protocol-design/wire-protocol.html + +## Protocol + +This section specifies the communication encoding and such. It does not talk +about possible means of communication. + +The encoding of choice for this would be line-delimited JSON. That is, each +implementation would read until a literal new line. + +Note that this reference will mostly include a Go struct that define a JSON +structure. If `json.RawMessage` is used, then it implies different types to be +defined later in the documentation. + +### Payload + +Each JSON message is called a payload. Each payload must follow a certain +structure, except for the arbitrary data field. JSON fields must always be +["MixedCaps"-cased][MixedCaps] with ["Initialisms"][initialisms] to match +one-on-one with Go struct field names. + +Below is the structure of the JSON payload, represented in a Go struct. + +```go +type Payload struct { + OP OP `json:"OP"` + ID ObjectID `json:"ID"` + Data json.RawMessage `json:"Data"` // Any type. +} +``` + +[MixedCaps]: https://golang.org/doc/effective_go.html#mixed-caps +[initialisms]: https://github.com/golang/go/wiki/CodeReviewComments#initialisms + +### Payload Type + +The payload type separates one JSON message into two possible types: requests +and events. As it is defined, events are sent from the backend to the frontend, +and requests are sent from the frontend to the backend. + +Two examples of this would be: a request to call a method, in which the frontend +will reply with returns for said methods, and an event sent from the backend +when a container's method needs to be called. + +### Object ID + +// TODO: THIS DOES NOT COVER OBJECT CREATION. + +A payload must always have an object ID to denote the object that the payload +belongs to. The ID is always of type uint32 to be compatible with languages that +don't have unsigned types or any integer types in general. IDs will always be +generated by the backend, and ID generation implementations could be arbitrary +as long as it could guarantee uniqueness across the service globally. + +An example implementation of ID generation could be a library with a global +singleton, where a new ID is derived for each new object by atomically +incrementing it by 1. + +Implementations of ID generation are allowed to free up unused IDs, that is, for +discarded objects, their IDs can be reused. Note that this is only possible when +both sides agree on the disposal of those objects. + +As an example of discarding old IDs: if the backend overrides a list of Servers +with a completely new list of Servers, then the frontend must discard the old +list in favor of new ones. At this point, either side could reason that the IDs +of those old Servers are now unused, so the backend should be able to reuse that +ID. Note that this requires a proper job queue to be implemented in the frontend +as well as proper synchronization primitives in the backend. A job queue is +required to ensure that the IDs are properly discarded before new objects with +the same IDs are sent over. + +For concreteness, below is the ID type defined in Go syntax. + +```go +type ID uint32 +``` + +Here's a naive Go implementation of atomic ID generation without any reusing. + +```go +var currentID uint32 + +// GenerateID returns a new unique ID. This function is safe for concurrent use. +func GenerateID() uint32 { + return atomic.AddUint32(¤tID, 1) +} +``` + +### Documenting Payload Types and Examples + +For documentation purposes, when a stream of traffic is written down, JSON +objects that are prefixed with `<-` denotes an event, and `->` denotes a +request. + +Also, for brevity, JSON will mostly be formatted to span across multiple lines. +In reality, as the protocol strictly uses new lines (`\n`, LF only) to delimit +JSON messages, everything will all be squeezed into a single line. There will +be no indicative arrows. + +### Struct Messages + +The behavior for all defined structs is for them to be completely marshaled into +JSON. As all defined structs only have exported fields, this shouldn't be a +problem. + +### Method Behaviors + +All GetterMethods with no parameters and AsserterMethods of an interface will be +resolved immediately right before sending said interface. As such, these methods +cannot be manually called using requests. + +### OP Code + +Each payload will have an OP code, which is just a +[SCREAMING_SNAKE_CASE][case-lists] string that sits in the `"OP"` field. Each +payload type has its own set of possible OP codes. + +For concreteness and examples, below is a formal definition of the OP code type. + +```go +type OP string +``` + +[case-lists]: https://en.wikipedia.org/wiki/Naming_convention_(programming)#Examples_of_multiple-word_identifier_formats + +#### Event OP codes + +These OP codes are only sent from the backend to the frontend. + +```go +const ( + // OPSet is used on SetterMethods, which are the Set methods in Container + // interfaces. + OPSet = "SET" + // OPReturn is used for returns of IO methods. The structure of the data + // field is described in the Return Events section. + OPReturn = "RETURN" +) +``` + +#### Request OP codes + +These OP codes are only sent from the frontend to the backend. + +```go +const ( + // OPCallMethod is used when the frontend needs to call a method in an + // interface. + OPCallMethod = "CALL_METHOD" + // OPCallContainerMethod is used when the frontend needs to call a + // ContainerMethod. Although this OP is also used to call a method, it is + // separated because it's called differently. + OPCallContainerMethod = "CALL_CONTAINER_METHOD" +) +``` + +### Request Messages + +As mentioned above, all messages sent from the client are called requests. Each +request will ask the server to do something based on the given OP code, and the +given ID will be used to determine what object the request refers to. This same +ID will be used in replies from the backend to obtain the same object. As such, +instead of identifying individual requests with an ID, we identify individual +objects with an ID instead. + +#### Calling a Method + +The frontend could send a request to the backend to ask for a method to be +called. Note that, as the [Method Behaviors](#method-behaviors) section points +out, certain methods cannot be called, because they're already called +beforehand. Any other method type not listed in that section must be explicitly +called. + +This section does NOT apply to ContainerMethods. For this method type, refer to +[Calling a Container Method][#calling-a-container-method]. + +To call a method, a payload must use the `CALL_METHOD` OP, and the data field +must follow a concrete structure. The object ID must be a valid ID that points +to the right object to call the method on. + +Below concretely defines the structure of the Data field in Go struct. + +```go +type CallRequestData struct { + Parameters []json.RawMessage `json:"Parameters"` +} +``` + +Here's an example of the frontend requesting to call the `Do` method inside +`Actioner`, which is an IOMethod. For reference, its function signature is +`Do(string, cchat.ID) error`. + +For reference, this example assumes that the `Actioner` object has the object ID +ofo `31341`. + +```json +-> { + "OP": "CALL_METHOD", + "ID": 31341, + "Data": { + "Parameters": ["print_message_id", "767517199165292574"] + } +} +``` + +#### Calling a Container Method + +### Event Messages + +#### Return Messages from Events + +All incoming events that are replies of GetterMethod (with parameters) and +IOMethod calls will have OPReturn and a specific structure for the data field: + +```go +type ReturnData struct { + // Returns represent arbitrary values. Its length should be the same as + // the Returns field of GetterMethod, or length 1 if it's an IOMethod. + Returns []json.RawMessage `json:"Returns"` + Error string `json:"Error,omitempty"` // optional field +} +``` + +// TODO: THIS SECTION DOES NOT ELABORATE ON CONTAINER TYPES. + +Here's an example of `RawContent(cchat.ID, string) (string, error)` (a +GetterMethod) returning successfully with no error: + +```json +<- { + "OP": "RETURN", + "Data": { + "Returns": ["edited message content"] + } +} +``` + +Here's the same method returning an error: + +```json +<- { + "OP": "RETURN", + "Data": { + "Returns": null, + "Error": "failed to parse ID: given argument is not a valid integer" + } +} +``` + +Here's an example of Actioner's `Actions(cchat.ID) []string` (a GetterMethod) +returning successfully: + +```json +<- { + "OP": "RETURN", + "ID": 31341, + "Data": { + "Returns": [["delete_message", "print_message_id"]] + } +} +``` + +Here's an example of Actioner's `Do(string, cchat.ID) error` (an IOMethod) +returning successfully: + +```json +<- { + "OP": "RETURN", + "ID": 31341, + "Data": { + "Returns": null + } +} +``` + +Note that since this method does not return any value and only an error, the +value of `Returns` will always be `null`. This is different from a method +intentionally returning `nil`, as the JSON value will be `[null]` instead, +denoting that the type of `Returns` is indeed an array with `null` as a value. + +# Implementation + +## Backend + +1. Implement onto cchat interfaces normally. +2. Make a main entrypoint package that imports adapter. + +```go +func init() { + services.RegisterService(&discord.Services{}) +} + +func main() { + // Main is a shortcut to Start with []cchat.Service from services.Get(). + adapter.Main(os.Args) +} +``` + +3. Install this compiled main binary into `~/.config/cchat/services`. + +## Frontend + +1. Dash-import `frontend`. diff --git a/services/ipc/adapter/adapter.go b/services/ipc/adapter/adapter.go new file mode 100644 index 0000000..99b376d --- /dev/null +++ b/services/ipc/adapter/adapter.go @@ -0,0 +1,29 @@ +// Package adapter provides functions to convert typical cchat calls to +// RPC-compatible payloads. +package adapter + +import ( + "log" + + "github.com/diamondburned/cchat" + "github.com/diamondburned/cchat/services" +) + +// Main is a helper function that calls Start() wtih the services from +// services.Get(). +func Main() error { + s, errs := services.Get() + if errs != nil { + for _, err := range errs { + if err != nil { + log.Println(err) + } + } + } + + return Start(s) +} + +func Start(services []cchat.Service) error { + panic("Implement me") +} diff --git a/services/ipc/frontend/frontend.go b/services/ipc/frontend/frontend.go new file mode 100644 index 0000000..ffdba65 --- /dev/null +++ b/services/ipc/frontend/frontend.go @@ -0,0 +1,22 @@ +// Package frontend +package frontend + +import "github.com/diamondburned/cchat/services" + +func init() { + services.RegisterSource(loadPlugins) +} + +func loadPlugins() []error { + panic("Implement me") + + // d, err := ioutil.ReadDir("stuff") + // // err check + + // for _, info := range d { + // f, err := os.Open(filepath.Join("stuff", file.Name())) + // // err check + + // services.RegisterService(f) + // } +}