From 8e9321928bd403d6893f48b7365ffef271372a16 Mon Sep 17 00:00:00 2001 From: diamondburned Date: Sat, 26 Sep 2020 18:24:56 -0700 Subject: [PATCH] Added a repository for API source of truth This commit adds a new package repository containing every single cchat types that the package provides. Its goal is to be the source of truth for the cchat files to be generated from. A huge advantage of this is having types in an easily representable format. This means that other languages can easily parse the repository and generate its own types that are similar to the original ones. Having a repository also allows easier code generation. For example, this commit will allow generating the "empty" package in the future, which would contain empty implementations of cchat databases that would return nil for asserter methods. --- go.mod | 6 +- go.sum | 4 + repository/comment.go | 68 ++ repository/comment_test.go | 41 ++ repository/enum.go | 25 + repository/error.go | 17 + repository/gob/generate.go | 3 + repository/gob/generator.go | 24 + repository/gob/repository.gob | Bin 0 -> 30300 bytes repository/interface.go | 21 + repository/main.go | 1207 +++++++++++++++++++++++++++++++++ repository/main_test.go | 29 + repository/method.go | 91 +++ repository/repository.go | 34 + repository/struct.go | 12 + repository/tmplstring.go | 21 + repository/type.go | 8 + 17 files changed, 1610 insertions(+), 1 deletion(-) create mode 100644 repository/comment.go create mode 100644 repository/comment_test.go create mode 100644 repository/enum.go create mode 100644 repository/error.go create mode 100644 repository/gob/generate.go create mode 100644 repository/gob/generator.go create mode 100644 repository/gob/repository.gob create mode 100644 repository/interface.go create mode 100644 repository/main.go create mode 100644 repository/main_test.go create mode 100644 repository/method.go create mode 100644 repository/repository.go create mode 100644 repository/struct.go create mode 100644 repository/tmplstring.go create mode 100644 repository/type.go diff --git a/go.mod b/go.mod index 47d3717..1f18ff2 100644 --- a/go.mod +++ b/go.mod @@ -2,4 +2,8 @@ module github.com/diamondburned/cchat go 1.14 -require github.com/pkg/errors v0.9.1 +require ( + github.com/dave/jennifer v1.4.1 + github.com/go-test/deep v1.0.7 + github.com/pkg/errors v0.9.1 +) diff --git a/go.sum b/go.sum index 7c401c3..5b12007 100644 --- a/go.sum +++ b/go.sum @@ -1,2 +1,6 @@ +github.com/dave/jennifer v1.4.1 h1:XyqG6cn5RQsTj3qlWQTKlRGAyrTcsk1kUmWdZBzRjDw= +github.com/dave/jennifer v1.4.1/go.mod h1:7jEdnm+qBcxl8PC0zyp7vxcpSRnzXSt9r39tpTVGlwA= +github.com/go-test/deep v1.0.7 h1:/VSMRlnY/JSyqxQUzQLKVMAskpY/NZKFA5j2P+0pP2M= +github.com/go-test/deep v1.0.7/go.mod h1:QV8Hv/iy04NyLBxAdO9njL0iVPN1S4d/A3NVv1V36o8= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= diff --git a/repository/comment.go b/repository/comment.go new file mode 100644 index 0000000..cad4436 --- /dev/null +++ b/repository/comment.go @@ -0,0 +1,68 @@ +package repository + +import ( + "bytes" + "go/doc" + "regexp" + "strings" +) + +var ( + // commentTrimSurrounding is a regex to trim surrounding new line and tabs. + // This is needed to find the correct level of indentation. + commentTrimSurrounding = regexp.MustCompile(`(^\n)|(\n\t+$)`) +) + +// Comment represents a raw comment string. Most use cases should use GoString() +// to get the comment's content. +type Comment struct { + RawText string +} + +// GoString formats the documentation string in 80 columns wide paragraphs. +func (c Comment) GoString() string { + return c.WrapText(80) +} + +// WrapText wraps the raw text in n columns wide paragraphs. +func (c Comment) WrapText(column int) string { + var buf bytes.Buffer + doc.ToText(&buf, c.Unindent(), "", "\t", column) + return buf.String() +} + +// Unindent removes the indentations that were there for the sake of syntax in +// RawText. It gets the lowest indentation level from each line and trim it. +func (c Comment) Unindent() string { + // Trim new lines. + txt := commentTrimSurrounding.ReplaceAllString(c.RawText, "") + + // Split the lines and rejoin them later to trim the indentation. + var lines = strings.Split(txt, "\n") + var indent = 0 + + // Get the minimum indentation count. + for _, line := range lines { + linedent := strings.Count(line, "\t") + if linedent < 0 { + continue + } + if linedent < indent || indent == 0 { + indent = linedent + } + } + + // Trim the indentation. + if indent > 0 { + for i, line := range lines { + if len(line) > 0 { + lines[i] = line[indent-1:] + } + } + } + + // Rejoin. + txt = strings.Join(lines, "\n") + + return txt +} diff --git a/repository/comment_test.go b/repository/comment_test.go new file mode 100644 index 0000000..2a3c871 --- /dev/null +++ b/repository/comment_test.go @@ -0,0 +1,41 @@ +package repository + +import ( + "testing" + + "github.com/go-test/deep" +) + +const _comment = ` +The authenticator interface allows for a multistage initial authentication API +that the backend could use. Multistage is done by calling AuthenticateForm then +Authenticate again forever until no errors are returned. + + var s *cchat.Session + var err error + + for { + // Pseudo-function to render the form and return the results of those + // forms when the user confirms it. + outputs := renderAuthForm(svc.AuthenticateForm()) + + s, err = svc.Authenticate(outputs) + if err != nil { + renderError(errors.Wrap(err, "Error while authenticating")) + continue // retry + } + + break // success + }` + +// Trim away the prefix new line. +var comment = _comment[1:] + +func TestComment(t *testing.T) { + var authenticator = Main["cchat"].Interface("Authenticator") + var authDoc = authenticator.Comment.GoString() + + if eq := deep.Equal(comment, authDoc); eq != nil { + t.Fatal("Comment inequality:", eq) + } +} diff --git a/repository/enum.go b/repository/enum.go new file mode 100644 index 0000000..0eedab1 --- /dev/null +++ b/repository/enum.go @@ -0,0 +1,25 @@ +package repository + +type Enumeration struct { + Comment + Name string + Values []EnumValue + Bitwise bool +} + +type EnumValue struct { + Comment + Name string // also return value from String() +} + +// IsPlaceholder returns true if the enumeration value is meant to be a +// placeholder. In Go, it would look like this: +// +// const ( +// _ EnumType = iota // IsPlaceholder() == true +// V1 +// ) +// +func (v EnumValue) IsPlaceholder() bool { + return v.Name == "" +} diff --git a/repository/error.go b/repository/error.go new file mode 100644 index 0000000..b32ce91 --- /dev/null +++ b/repository/error.go @@ -0,0 +1,17 @@ +package repository + +type ErrorType struct { + Struct + ErrorString TmplString // used for Error() +} + +// Wrapped returns true if the error struct contains a field with the error +// type. +func (t ErrorType) Wrapped() bool { + for _, ret := range t.Struct.Fields { + if ret.Type == "error" { + return true + } + } + return false +} diff --git a/repository/gob/generate.go b/repository/gob/generate.go new file mode 100644 index 0000000..4dcf1d6 --- /dev/null +++ b/repository/gob/generate.go @@ -0,0 +1,3 @@ +package xml + +//go:generate go run ./generator.go diff --git a/repository/gob/generator.go b/repository/gob/generator.go new file mode 100644 index 0000000..ccedddd --- /dev/null +++ b/repository/gob/generator.go @@ -0,0 +1,24 @@ +// +build ignore + +package main + +import ( + "encoding/gob" + "log" + "os" + + "github.com/diamondburned/cchat/repository" +) + +func main() { + f, err := os.Create("repository.gob") + if err != nil { + log.Fatalln("Failed to create file:", err) + } + defer f.Close() + + if err := gob.NewEncoder(f).Encode(repository.Main); err != nil { + os.Remove("repository.gob") + log.Fatal("Failed to gob encode:", err) + } +} diff --git a/repository/gob/repository.gob b/repository/gob/repository.gob new file mode 100644 index 0000000000000000000000000000000000000000..005f2c9c3e6679329a86abac05717b3d070a7e28 GIT binary patch literal 30300 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" in Markdown. Frontends could + use this information to format the quote properly. + `}, + Name: "QuotePrefix", + }, + Returns: []NamedType{{Name: "prefix", Type: "string"}}, + }, + }, + }}, + }, + "cchat": { + Enums: []Enumeration{{ + Comment: Comment{` + Status represents a user's status. This might be used by the + frontend to visually display the status. + `}, + Name: "Status", + Values: []EnumValue{ + {Comment{""}, "Unknown"}, + {Comment{""}, "Online"}, + {Comment{""}, "Idle"}, + {Comment{""}, "Busy"}, + {Comment{""}, "Away"}, + {Comment{""}, "Offline"}, + {Comment{"Invisible is reserved."}, "Invisible"}, + }, + }}, + TypeAliases: []TypeAlias{{ + Comment: Comment{` + ID is the type alias for an ID string. This type is used for + clarification and documentation purposes only. Implementations + could either use this type or a string type. + `}, + Name: "ID", + Type: "string", + }}, + Structs: []Struct{{ + Comment: Comment{` + AuthenticateEntry represents a single authentication entry, + usually an email or password prompt. Passwords or similar + entries should have Secrets set to true, which should imply to + frontends that the fields be masked. + `}, + Name: "AuthenticateEntry", + Fields: []StructField{ + {NamedType: NamedType{"Name", "string"}}, + {NamedType: NamedType{"Placeholder", "string"}}, + {NamedType: NamedType{"Description", "string"}}, + {NamedType: NamedType{"Secret", "bool"}}, + {NamedType: NamedType{"Multiline", "bool"}}, + }, + }, { + Comment: Comment{` + CompletionEntry is a single completion entry returned by + CompleteMessage. The icon URL field is optional. + `}, + Name: "CompletionEntry", + Fields: []StructField{{ + Comment: Comment{` + Raw is the text to be replaced in the input box. + `}, + NamedType: NamedType{"Raw", "string"}, + }, { + Comment: Comment{` + Text is the label to be displayed. + `}, + NamedType: NamedType{"Text", "text.Rich"}, + }, { + Comment: Comment{` + Secondary is the label to be displayed on the second line, + on the right of Text, or not displayed at all. This should + be optional. This text may be dimmed out as styling. + `}, + NamedType: NamedType{"Secondary", "text.Rich"}, + }, { + Comment: Comment{` + IconURL is the URL to the icon that will be displayed on the + left of the text. This field is optional. + `}, + NamedType: NamedType{"IconURL", "string"}, + }, { + Comment: Comment{` + Image returns whether or not the icon URL is actually an + image, which indicates that the frontend should not do + rounded corners. + `}, + NamedType: NamedType{"Image", "bool"}, + }}, + }}, + ErrorTypes: []ErrorType{{ + Struct: Struct{ + Comment: Comment{` + ErrInvalidConfigAtField is the structure for an error at a + specific configuration field. Frontends can use this and + highlight fields if the backends support it. + `}, + Name: "ErrInvalidConfigAtField", + Fields: []StructField{ + {NamedType: NamedType{"Key", "string"}}, + {NamedType: NamedType{"Err", "error"}}, + }, + }, + ErrorString: TmplString{ + Receiver: "err", + Template: "Error at {err.Key}: {err.Err.Error()}", + }, + }}, + Interfaces: []Interface{{ + Comment: Comment{` + Identifier requires ID() to return a uniquely identifiable + string for whatever this is embedded into. Typically, servers + and messages have IDs. It is worth mentioning that IDs should be + consistent throughout the lifespan of the program or maybe even + forever. + `}, + Name: "Identifier", + Methods: []Method{ + GetterMethod{ + RegularMethod: RegularMethod{Name: "ID"}, + Returns: []NamedType{{Type: "ID"}}, + }, + }, + }, { + Comment: Comment{` + Namer requires Name() to return the name of the object. + Typically, this implies usernames for sessions or service + names for services. + `}, + Name: "Namer", + Methods: []Method{ + GetterMethod{ + RegularMethod: RegularMethod{Name: "Name"}, + Returns: []NamedType{{Type: "text.Rich"}}, + }, + AsserterMethod{ + ChildType: "Iconer", + }, + }, + }, { + Comment: Comment{` + Iconer adds icon support into Namer, which in turn is returned + by other interfaces. Typically, Service would return the service + logo, Session would return the user's avatar, and Server would + return the server icon. + + For session, the avatar should be the same as the one returned + by messages sent by the current user. + `}, + Name: "Iconer", + Methods: []Method{ + ContainerMethod{ + RegularMethod: RegularMethod{Name: "Icon"}, + HasContext: true, + ContainerType: "IconContainer", + HasStopFn: true, + }, + }, + }, { + Comment: Comment{` + Noncer adds nonce support. A nonce is defined in this context as + a unique identifier from the frontend. This interface defines + the common nonce getter. + + Nonces are useful for frontends to know if an incoming event is + a reply from the server backend. As such, nonces should be + roundtripped through the server. For example, IRC would use + labeled responses. + + The Nonce method can return an empty string. This indicates that + either the frontend or backend (or neither) supports nonces. + + Contrary to other interfaces that extend with an "Is" method, + the Nonce method could return an empty string here. + `}, + Name: "Noncer", + Methods: []Method{ + GetterMethod{ + RegularMethod: RegularMethod{Name: "Nonce"}, + Returns: []NamedType{{Type: "string"}}, + }, + }, + }, { + Comment: Comment{` + Author is the interface for an identifiable author. The + interface defines that an author always have an ID and a name. + + An example of where this interface is used would be in + MessageCreate's Author method or embedded in Typer. The returned + ID may or may not be used by the frontend, but backends must + guarantee that the Author's ID is in fact a user ID. + + The frontend may use the ID to squash messages with the same + author together. + `}, + Name: "Author", + Embeds: []EmbeddedInterface{ + {InterfaceName: "Identifier"}, + {InterfaceName: "Namer"}, + }, + }, { + Comment: Comment{` + A service is a complete service that's capable of multiple + sessions. It has to implement the Authenticate() method, which + returns an implementation of Authenticator. + + A service can implement SessionRestorer, which would indicate + the frontend that it can restore past sessions. Sessions are + saved using the SessionSaver interface that Session can + implement. + + A service can also implement Configurator if it has additional + configurations. The current API is a flat key-value map, which + can be parsed by the backend itself into more meaningful data + structures. All configurations must be optional, as frontends + may not implement a configurator UI. + `}, + Name: "Service", + Embeds: []EmbeddedInterface{{ + Comment: Comment{` + Namer returns the name of the service. + `}, + InterfaceName: "Namer", + }}, + Methods: []Method{ + GetterMethod{ + RegularMethod: RegularMethod{Name: "Authenticate"}, + Returns: []NamedType{{Type: "Authenticator"}}, + }, + AsserterMethod{ + ChildType: "Configurator", + }, + AsserterMethod{ + ChildType: "SessionRestorer", + }, + }, + }, { + Comment: Comment{` + The authenticator interface allows for a multistage initial + authentication API that the backend could use. Multistage is + done by calling AuthenticateForm then Authenticate again forever + until no errors are returned. + + var s *cchat.Session + var err error + + for { + // Pseudo-function to render the form and return the results of those + // forms when the user confirms it. + outputs := renderAuthForm(svc.AuthenticateForm()) + + s, err = svc.Authenticate(outputs) + if err != nil { + renderError(errors.Wrap(err, "Error while authenticating")) + continue // retry + } + + break // success + } + `}, + Name: "Authenticator", + Methods: []Method{ + GetterMethod{ + RegularMethod: RegularMethod{ + Comment: Comment{` + AuthenticateForm should return a list of + authentication entries for the frontend to render. + `}, + Name: "AuthenticateForm", + }, + Returns: []NamedType{{Type: "[]AuthenticateEntry"}}, + }, + IOMethod{ + RegularMethod: RegularMethod{ + Comment: Comment{` + Authenticate will be called with a list of values + with indices correspond to the returned slice of + AuthenticateEntry. + `}, + Name: "Authenticate", + }, + Parameters: []NamedType{{Type: "[]string"}}, + ReturnValue: NamedType{Type: "Session"}, + ReturnError: true, + }, + }, + }, { + Comment: Comment{` + SessionRestorer extends Service and is called by the frontend to + restore a saved session. The frontend may call this at any time, + but it's usually on startup. + + To save a session, refer to SessionSaver. + `}, + Name: "SessionRestorer", + Methods: []Method{ + IOMethod{ + RegularMethod: RegularMethod{Name: "RestoreSession"}, + Parameters: []NamedType{{Type: "map[string]string"}}, + ReturnValue: NamedType{Type: "Session"}, + ReturnError: true, + }, + }, + }, { + Comment: Comment{` + Configurator is an interface which the backend can implement for a + primitive configuration API. Since these methods do return an error, + they are allowed to do IO. The frontend should handle this + appropriately, including running them asynchronously. + `}, + Name: "Configurator", + Methods: []Method{ + IOMethod{ + RegularMethod: RegularMethod{Name: "Configuration"}, + ReturnValue: NamedType{Type: "map[string]string"}, + ReturnError: true, + }, + IOMethod{ + RegularMethod: RegularMethod{Name: "SetConfiguration"}, + Parameters: []NamedType{{Type: "map[string]string"}}, + ReturnError: true, + }, + }, + }, { + Comment: Comment{` + A session is returned after authentication on the service. + Session implements Name(), which should return the username + most of the time. It also implements ID(), which might be + used by frontends to check against MessageAuthor.ID() and + other things. + + A session can implement SessionSaver, which would allow the + frontend to save the session into its keyring at any time. + Whether the keyring is completely secure or not is up to the + frontend. For a Gtk client, that would be using the GNOME + Keyring daemon. + `}, + Name: "Session", + Embeds: []EmbeddedInterface{{ + Comment: Comment{` + Identifier should typically return the user ID. + `}, + InterfaceName: "Identifier", + }, { + Comment: Comment{` + Namer gives the name of the session, which is typically the + username. + `}, + InterfaceName: "Namer", + }, { + InterfaceName: "Lister", + }}, + Methods: []Method{ + IOMethod{ + RegularMethod: RegularMethod{ + Comment: Comment{` + Disconnect asks the service to disconnect. It does + not necessarily mean removing the service. + + The frontend must cancel the active ServerMessage + before disconnecting. The backend can rely on this + behavior. + + The frontend will reuse the stored session data from + SessionSaver to reconnect. + + When this function fails, the frontend may display + the error upfront. However, it will treat the + session as actually disconnected. If needed, the + backend must implement reconnection by itself. + `}, + Name: "Disconnect", + }, + ReturnError: true, + }, + AsserterMethod{ChildType: "Commander"}, + AsserterMethod{ChildType: "SessionSaver"}, + }, + }, { + Comment: Comment{` + SessionSaver extends Session and is called by the frontend to + save the current session. This is typically called right after + authentication, but a frontend may call this any time, including + when it's closing. + + The frontend can ask to restore a session using SessionRestorer, + which extends Service. + + The SaveSession method must not do IO; if there are any reasons + that cause SaveSession to fail, then a nil map should be + returned. + `}, + Name: "SessionSaver", + Methods: []Method{ + GetterMethod{ + RegularMethod: RegularMethod{Name: "SaveSession"}, + Returns: []NamedType{{Type: "map[string]string"}}, + }, + }, + }, { + Comment: Comment{` + Commander is an optional interface that a session could + implement for command support. This is different from just + intercepting the SendMessage() API, as this extends globally to + the entire session. + + A very primitive use of this API would be to provide additional + features that are not in cchat through a very basic terminal + interface. + `}, + Name: "Commander", + Methods: []Method{ + IOMethod{ + RegularMethod: RegularMethod{ + Comment: Comment{` + RunCommand executes the given command, with the + slice being already split arguments, similar to + os.Args. The function could return an output stream, + in which the frontend must display it live and close + it on EOF. + + The function can do IO, and outputs should be + written to the given io.Writer. + + The client should make guarantees that an empty + string (and thus a zero-length string slice) should + be ignored. The backend should be able to assume + that the argument slice is always length 1 or more. + `}, + Name: "RunCommand", + }, + Parameters: []NamedType{ + {Type: "[]string"}, + {Type: "io.Writer"}, + }, + ReturnError: true, + }, + AsserterMethod{ChildType: "Completer"}, + }, + }, { + Comment: Comment{` + Server is a single server-like entity that could translate to a + guild, a channel, a chat-room, and such. A server must implement + at least ServerList or ServerMessage, else the frontend must + treat it as a no-op. + `}, + Name: "Server", + Embeds: []EmbeddedInterface{ + {InterfaceName: "Identifier"}, + {InterfaceName: "Namer"}, + }, + Methods: []Method{ + AsserterMethod{ChildType: "Lister"}, + AsserterMethod{ChildType: "Messenger"}, + AsserterMethod{ChildType: "Commander"}, + AsserterMethod{ChildType: "Configurator"}, + }, + }, { + Comment: Comment{` + Lister is for servers that contain children servers. This is + similar to guilds containing channels in Discord, or IRC servers + containing channels. + + There isn't a similar stop callback API unlike other interfaces + because all servers are expected to be listed. However, they + could be hidden, such as collapsing a tree. + + The backend should call both the container and other icon and + label containers, if any. + `}, + Name: "Lister", + Methods: []Method{ + ContainerMethod{ + RegularMethod: RegularMethod{ + Comment: Comment{` + Servers should call SetServers() on the given + ServersContainer to render all servers. This + function can do IO, and the frontend should run this + in a goroutine. + `}, + Name: "Servers", + }, + ContainerType: "ServersContainer", + }, + }, + }, { + Comment: Comment{` + Messenger is for servers that contain messages. This is similar + to Discord or IRC channels. + `}, + Name: "Messenger", + Methods: []Method{ + ContainerMethod{ + RegularMethod: RegularMethod{ + Comment: Comment{` + JoinServer joins a server that's capable of + receiving messages. The server may not necessarily + support sending messages. + `}, + Name: "JoinServer", + }, + HasContext: true, + ContainerType: "MessagesContainer", + HasStopFn: true, + }, + AsserterMethod{ChildType: "Sender"}, + AsserterMethod{ChildType: "Editor"}, + AsserterMethod{ChildType: "Actioner"}, + AsserterMethod{ChildType: "Nicknamer"}, + AsserterMethod{ChildType: "Backlogger"}, + AsserterMethod{ChildType: "MemberLister"}, + AsserterMethod{ChildType: "UnreadIndicator"}, + AsserterMethod{ChildType: "TypingIndicator"}, + }, + }, { + Comment: Comment{` + Sender adds message sending to a messenger. Messengers that + don't implement MessageSender will be considered read-only. + `}, + Name: "Sender", + Methods: []Method{ + IOMethod{ + RegularMethod: RegularMethod{ + Comment: Comment{` + Send is called by the frontend to send a message to + this channel. + `}, + Name: "Send", + }, + Parameters: []NamedType{ + {Type: "SendableMessage"}, + }, + ReturnError: true, + }, + GetterMethod{ + RegularMethod: RegularMethod{ + Comment: Comment{` + CanAttach returns whether or not the client is + allowed to upload files. + `}, + Name: "CanAttach", + }, + Returns: []NamedType{{Type: "bool"}}, + }, + AsserterMethod{ChildType: "Completer"}, + }, + }, { + Comment: Comment{` + Editor adds message editing to the messenger. Only EditMessage + can do IO. + `}, + Name: "Editor", + Methods: []Method{ + GetterMethod{ + RegularMethod: RegularMethod{ + Comment: Comment{` + MessageEditable returns whether or not a message can + be edited by the client. This method must not do IO. + `}, + Name: "MessageEditable", + }, + Parameters: []NamedType{{Name: "id", Type: "ID"}}, + Returns: []NamedType{{Type: "bool"}}, + }, + GetterMethod{ + RegularMethod: RegularMethod{ + Comment: Comment{` + RawMessageContent gets the original message text for + editing. This method must not do IO. + `}, + Name: "RawMessageContent", + }, + Parameters: []NamedType{{Name: "id", Type: "ID"}}, + Returns: []NamedType{{Type: "string"}}, + ReturnError: true, + }, + IOMethod{ + RegularMethod: RegularMethod{ + Comment: Comment{` + EditMessage edits the message with the given ID to + the given content, which is the edited string from + RawMessageContent. This method can do IO. + `}, + Name: "EditMessage", + }, + Parameters: []NamedType{ + {Name: "id", Type: "ID"}, + {Name: "content", Type: "string"}, + }, + ReturnError: true, + }, + }, + }, { + Comment: Comment{` + Actioner adds custom message actions into each message. + Similarly to ServerMessageEditor, some of these methods may + do IO. + `}, + Name: "Actioner", + Methods: []Method{ + GetterMethod{ + RegularMethod: RegularMethod{ + Comment: Comment{` + MessageActions returns a list of possible actions in + pretty strings that the frontend will use to + directly display. This method must not do IO. + + The string slice returned can be nil or empty. + `}, + Name: "Actions", + }, + Returns: []NamedType{{Type: "[]string"}}, + }, + IOMethod{ + RegularMethod: RegularMethod{ + Comment: Comment{` + DoAction executes a message action on the given + messageID, which would be taken from + MessageHeader.ID(). This method is allowed to do + IO; the frontend should take care of running it + asynchronously. + `}, + Name: "DoAction", + }, + Parameters: []NamedType{ + {Name: "action", Type: "string"}, + {Name: "id", Type: "ID"}, + }, + ReturnError: true, + }, + }, + }, { + Comment: Comment{` + Nicknamer adds the current user's nickname. + + The frontend will not traverse up the server tree, meaning the + backend must handle nickname inheritance. This also means that + servers that don't implement ServerMessage also don't need to + implement ServerNickname. By default, the session name should be + used. + `}, + Name: "Nicknamer", + Methods: []Method{ + ContainerMethod{ + RegularMethod: RegularMethod{Name: "Nickname"}, + HasContext: true, + ContainerType: "LabelContainer", + HasStopFn: true, + }, + }, + }, { + Comment: Comment{` + Backlogger adds message history capabilities into a message + container. The frontend should typically call this method when + the user scrolls to the top. + + As there is no stop callback, if the backend needs to fetch + messages asynchronously, it is expected to use the context to + know when to cancel. + + The frontend should usually call this method when the user + scrolls to the top. It is expected to guarantee not to call + MessagesBefore more than once on the same ID. This can usually + be done by deactivating the UI. + + Note: Although backends might rely on this context, the frontend + is still expected to invalidate the given container when the + channel is changed. + `}, + Name: "Backlogger", + Methods: []Method{ + IOMethod{ + RegularMethod: RegularMethod{ + Comment: Comment{` + MessagesBefore fetches messages before the given + message ID into the MessagesContainer. + `}, + Name: "MessagesBefore", + }, + Parameters: []NamedType{ + {Name: "ctx", Type: "context.Context"}, + {Name: "before", Type: "ID"}, + {Name: "c", Type: "MessagePrepender"}, + }, + ReturnError: true, + }, + }, + }, { + Comment: Comment{` + MemberLister adds a member list into a message server. + `}, + Name: "MemberLister", + Methods: []Method{ + ContainerMethod{ + RegularMethod: RegularMethod{ + Comment: Comment{` + ListMembers assigns the given container to the + channel's member list. The given context may be + used to provide HTTP request cancellations, but + frontends must not rely solely on this, as the + general context rules applies. + + Further behavioral documentations may be in + Messenger's JoinServer method. + `}, + Name: "ListMembers", + }, + HasContext: true, + ContainerType: "MemberListContainer", + HasStopFn: true, + }, + }, + }, { + Comment: Comment{` + UnreadIndicator adds an unread state API for frontends to use. + `}, + Name: "UnreadIndicator", + Methods: []Method{ + ContainerMethod{ + RegularMethod: RegularMethod{ + Comment: Comment{` + UnreadIndicate subscribes the given unread indicator + for unread and mention events. Examples include when + a new message is arrived and the backend needs to + indicate that it's unread. + + This function must provide a way to remove + callbacks, as clients must call this when the old + server is destroyed, such as when Servers is called. + `}, + Name: "UnreadIndicate", + }, + ContainerType: "UnreadContainer", + HasStopFn: true, + }, + }, + }, { + Comment: Comment{` + TypingIndicator optionally extends ServerMessage to provide + bidirectional typing indicating capabilities. This is similar to + typing events on Discord and typing client tags on IRCv3. + + The client should remove a typer when a message is received with + the same user ID, when RemoveTyper() is called by the backend or + when the timeout returned from TypingTimeout() has been reached. + `}, + Name: "TypingIndicator", + Methods: []Method{ + IOMethod{ + RegularMethod: RegularMethod{ + Comment: Comment{` + Typing is called by the client to indicate that the + user is typing. This function can do IO calls, and + the client must take care of calling it in a + goroutine (or an asynchronous queue) as well as + throttling it to TypingTimeout. + `}, + Name: "Typing", + }, + ReturnError: true, + }, + GetterMethod{ + RegularMethod: RegularMethod{ + Comment: Comment{` + TypingTimeout returns the interval between typing + events sent by the client as well as the timeout + before the client should remove the typer. + Typically, a constant should be returned. + `}, + Name: "TypingTimeout", + }, + Returns: []NamedType{{Type: "time.Duration"}}, + }, + ContainerMethod{ + RegularMethod: RegularMethod{ + Comment: Comment{` + TypingSubscribe subscribes the given indicator to + typing events sent by the backend. The added event + handlers have to be removed by the backend when the + stop() callback is called. + + This method does not take in a context, as it's + supposed to only use event handlers and not do any + IO calls. Nonetheless, the client must treat it + like it does and call it asynchronously. + `}, + Name: "TypingSubscribe", + }, + ContainerType: "TypingContainer", + HasStopFn: true, + }, + }, + }, { + Comment: Comment{` + Completer adds autocompletion into the message composer. IO is + not allowed, and the backend should do that only in goroutines + and update its state for future calls. + + Frontends could utilize the split package inside utils for + splitting words and index. This is the de-facto standard + implementation for splitting words, thus backends can rely on + their behaviors. + `}, + Name: "Completer", + Methods: []Method{ + GetterMethod{ + RegularMethod: RegularMethod{ + Comment: Comment{` + Complete returns the list of possible completion + entries for the given word list and the current word + index. It takes in a list of whitespace-split slice + of string as well as the position of the cursor + relative to the given string slice. + `}, + Name: "Complete", + }, + Parameters: []NamedType{ + {Name: "words", Type: "[]string"}, + {Name: "current", Type: "int64"}, + }, + Returns: []NamedType{ + {Type: "[]CompletionEntry"}, + }, + }, + }, + }, { + Comment: Comment{` + ServersContainer is any type of view that displays the list of + servers. It should implement a SetServers([]Server) that the + backend could use to call anytime the server list changes (at + all). + + Typically, most frontends should implement this interface onto a + tree node, as servers can be infinitely nested. Frontends should + also reset the entire node and its children when SetServers is + called again. + `}, + Name: "ServersContainer", + Methods: []Method{ + SetterMethod{ + RegularMethod: RegularMethod{ + Comment: Comment{` + SetServer is called by the backend service to + request a reset of the server list. The frontend can + choose to call Servers() on each of the given + servers, or it can call that later. The backend + should handle both cases. + `}, + Name: "SetServers", + }, + Parameters: []NamedType{{Type: "[]Server"}}, + }, + SetterMethod{ + RegularMethod: RegularMethod{Name: "UpdateServer"}, + Parameters: []NamedType{{Type: "ServerUpdate"}}, + }, + }, + }, { + Name: "ServerUpdate", + Embeds: []EmbeddedInterface{{ + Comment: Comment{` + Server embeds a complete server. Unlike MessageUpdate, which + only returns data on methods that are changed, + ServerUpdate's methods must return the complete data even if + they stay the same. As such, zero-value returns are treated + as not updated, including the name. + `}, + InterfaceName: "Server", + }}, + Methods: []Method{ + GetterMethod{ + RegularMethod: RegularMethod{ + Comment: Comment{` + PreviousID returns the ID of the item before this + server. + `}, + Name: "PreviousID", + }, + Returns: []NamedType{{Type: "ID"}}, + }, + }, + }, { + Comment: Comment{` + MessagesContainer is a view implementation that displays a list + of messages live. This implements the 3 most common message + events: CreateMessage, UpdateMessage and DeleteMessage. The + frontend must handle all 3. + + Since this container interface extends a single Server, the + frontend is allowed to have multiple views. This is usually done + with tabs or splits, but the backend should update them all + nonetheless. + `}, + Name: "MessagesContainer", + Methods: []Method{ + SetterMethod{}, + }, + }}, + }, +} diff --git a/repository/main_test.go b/repository/main_test.go new file mode 100644 index 0000000..073988a --- /dev/null +++ b/repository/main_test.go @@ -0,0 +1,29 @@ +package repository + +import ( + "bytes" + "encoding/gob" + "testing" + + "github.com/go-test/deep" +) + +func TestGob(t *testing.T) { + var buf bytes.Buffer + + if err := gob.NewEncoder(&buf).Encode(Main); err != nil { + t.Fatal("Failed to gob encode:", err) + } + + t.Log("Marshaled; total bytes:", buf.Len()) + + var unmarshaled Repositories + + if err := gob.NewDecoder(&buf).Decode(&unmarshaled); err != nil { + t.Fatal("Failed to gob decode:", err) + } + + if eq := deep.Equal(Main, unmarshaled); eq != nil { + t.Fatal("Inequalities after unmarshaling:", eq) + } +} diff --git a/repository/method.go b/repository/method.go new file mode 100644 index 0000000..bf8bf5e --- /dev/null +++ b/repository/method.go @@ -0,0 +1,91 @@ +package repository + +import "encoding/gob" + +func init() { + gob.Register(ContainerMethod{}) + gob.Register(AsserterMethod{}) + gob.Register(GetterMethod{}) + gob.Register(SetterMethod{}) + gob.Register(IOMethod{}) +} + +type Method interface { + UnderlyingName() string + internalMethod() +} + +type RegularMethod struct { + Comment + Name string +} + +func (m RegularMethod) UnderlyingName() string { return m.Name } +func (m RegularMethod) 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. +type GetterMethod struct { + RegularMethod + + // Parameters is the list of parameters in the function. + Parameters []NamedType + // Returns is the list of named types returned from the function. + Returns []NamedType + // ReturnError is true if the function returns an error at the end of + // returns. + ReturnError bool +} + +// SetterMethod is a method that sets values. These methods must not do IO, and +// they have to be non-blocking. +type SetterMethod struct { + RegularMethod + + // Parameters is the list of parameters in the function. These parameters + // should be the parameters to set. + Parameters []NamedType +} + +// IOMethod is a regular method that can do IO and thus is blocking. These +// methods usually always return errors. +type IOMethod struct { + RegularMethod + + // Parameters is the list of parameters in the function. + Parameters []NamedType + // ReturnValue is the return value in the function. + ReturnValue NamedType + // ReturnError is true if the function returns an error at the end of + // returns. + ReturnError bool +} + +// ContainerMethod is a method that uses a Container. These methods can do IO. +type ContainerMethod struct { + RegularMethod + + // HasContext is true if the method accepts a context as its first argument. + HasContext bool + // ContainerType is the name of the container interface. The name will + // almost always have "Container" as its suffix. + ContainerType string + // HasStopFn is true if the function returns a callback of type func() as + // its first return. The function will return an error in addition. If this + // is false, then only the error is returned. + HasStopFn bool +} + +// AsserterMethod is a method that allows the parent interface to extend itself +// into children interfaces. These methods must not do IO. +type AsserterMethod struct { + // ChildType is the children type that is returned. + ChildType string +} + +func (m AsserterMethod) internalMethod() {} + +// UnderlyingName returns the name of the method. +func (m AsserterMethod) UnderlyingName() string { + return "As" + m.ChildType +} diff --git a/repository/repository.go b/repository/repository.go new file mode 100644 index 0000000..7a1c193 --- /dev/null +++ b/repository/repository.go @@ -0,0 +1,34 @@ +package repository + +// MainNamespace is the name of the namespace that should be top level. +const MainNamespace = "cchat" + +type Repositories map[string]Repository + +type Repository struct { + Enums []Enumeration + TypeAliases []TypeAlias + Structs []Struct + ErrorTypes []ErrorType + Interfaces []Interface +} + +// Interface finds an interface. Nil is returned if none is found. +func (r Repository) Interface(name string) *Interface { + for _, iface := range r.Interfaces { + if iface.Name == name { + return &iface + } + } + return nil +} + +type NamedType struct { + Name string // optional + Type string +} + +// IsZero is true if t.Type is empty. +func (t NamedType) IsZero() bool { + return t.Type == "" +} diff --git a/repository/struct.go b/repository/struct.go new file mode 100644 index 0000000..4fade48 --- /dev/null +++ b/repository/struct.go @@ -0,0 +1,12 @@ +package repository + +type Struct struct { + Comment + Name string + Fields []StructField +} + +type StructField struct { + Comment + NamedType +} diff --git a/repository/tmplstring.go b/repository/tmplstring.go new file mode 100644 index 0000000..f005272 --- /dev/null +++ b/repository/tmplstring.go @@ -0,0 +1,21 @@ +package repository + +// TmplString is a generation-time templated string. It is used for string +// concatenation. +// +// Given the following TmplString: +// +// TmplString{Receiver: "v", Template: "Hello, {v.Foo()}"} +// +// The output of String() should be the same as the output of +// +// "Hello, " + v.Foo() +// +type TmplString struct { + Receiver string + Template string +} + +func (s TmplString) String() string { + panic("TODO") +} diff --git a/repository/type.go b/repository/type.go new file mode 100644 index 0000000..0422135 --- /dev/null +++ b/repository/type.go @@ -0,0 +1,8 @@ +package repository + +// TypeAlias represents a Go type alias. +type TypeAlias struct { + Comment + Name string + Type string +}