Repository changes; regenerated code

This commit fixes some trivial errors in the repository package. The
changes are breaking.

This commit also replaced the old cchat and text Go source code files
with ones generated straight from the repository. To regenerate, run

    go generate ./...

The code is generated using the Jennifer library. In the future, all
generated code (including package empty and the RPC library) will use
Jennifer.
This commit is contained in:
diamondburned 2020-09-27 18:41:17 -07:00
parent 8e9321928b
commit 516532ee01
22 changed files with 1777 additions and 844 deletions

647
cchat.go
View File

@ -1,6 +1,7 @@
// Package cchat is a set of stabilized interfaces for cchat implementations,
// joining the backend and frontend together.
//
//
// Backend
//
// Methods implemented by the backend that have frontend containers as arguments
@ -13,17 +14,19 @@
//
// Backend implementations have certain conditions that should be adhered to:
//
// - Storing MessagesContainer and ServersContainer are advised against;
// however, they should be done if need be.
// - Other containers such as LabelContainer and IconContainer should also
// not be stored; however, the same rule as above applies.
// - For the server list, icon updates and such that happen after their calls
// should use SetServers().
// - For the nickname of the current server, the backend can store the state
// of the label container. It must, however, remove the container when the
// stop callback from JoinServer() is called.
// - Some methods that take in a container may take in a context as well.
// Although implementations don't have to use this context, it should try to.
// - Storing MessagesContainer and ServersContainer are advised
// against; however, they should be done if need be.
// - Other containers such as LabelContainer and IconContainer
// should also not be stored; however, the same rule as above
// applies.
// - For the server list, icon updates and such that happen after
// their calls should use SetServers().
// - For the nickname of the current server, the backend can store
// the state of the label container. It must, however, remove the
// container when the stop callback from JoinServer() is called.
// - Some methods that take in a container may take in a context as
// well. Although implementations don't have to use this context,
// it should try to.
//
// Note: IO in most cases usually refer to networking, but they should files and
// anything that is blocking, such as mutexes or semaphores.
@ -53,6 +56,7 @@
// println("Server implements Backlogger.")
// }
//
//
// Frontend
//
// Frontend contains all interfaces that a frontend can or must implement. The
@ -64,22 +68,85 @@
// error handling API, as frontends can do that themselves. Errors returned by
// backend methods will be errors from the backend itself and never the frontend
// errors.
//
package cchat
import (
"context"
"fmt"
text "github.com/diamondburned/cchat/text"
"io"
"time"
"github.com/diamondburned/cchat/text"
)
// 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
// and documentation purposes only. Implementations could either use this type
// or a string type.
type ID = string
// Status represents a user's status. This might be used by the frontend to
// visually display the status.
type Status uint8
const (
StatusUnknown Status = iota
StatusOnline
StatusIdle
StatusBusy
StatusAway
StatusOffline
// Invisible is reserved.
StatusInvisible
)
func (s Status) Has(has Status) bool {
return s&has == has
}
// 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.
type AuthenticateEntry struct {
Name string
Placeholder string
Description string
Secret bool
Multiline bool
}
// CompletionEntry is a single completion entry returned by CompleteMessage. The
// icon URL field is optional.
type CompletionEntry struct {
Raw string
Text text.Rich
Secondary text.Rich
IconURL string
Image bool
}
// MessageAttachment represents a single file attachment. If needed, the
// frontend will close the reader after the message is sent, that is when the
// SendMessage function returns. The backend must not use the reader after that.
type MessageAttachments struct {
io.Reader
Name string
}
// ErrInvalidConfigAtField is the structure for an error at a specific
// configuration field. Frontends can use this and highlight fields if the
// backends support it.
type ErrInvalidConfigAtField struct {
Key string
Err error
}
func (e ErrInvalidConfigAtField) Error() string {
return fmt.Sprintf("Error at %s: %s", e.Key, e.Err.Error())
}
func (e ErrInvalidConfigAtField) Unwrap() error {
return e.Err
}
// 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
@ -92,8 +159,7 @@ type Identifier interface {
// implies usernames for sessions or service names for services.
type Namer interface {
Name() text.Rich
AsIconer() Iconer
AsIconer() Iconer // Optional
}
// Iconer adds icon support into Namer, which in turn is returned by other
@ -150,14 +216,11 @@ type Author interface {
// configurations must be optional, as frontends may not implement a
// configurator UI.
type Service interface {
// Namer returns the name of the service.
Namer
// Authenticate begins the authentication process. It's put into a method so
// backends can easily restart the entire process.
Authenticate() Authenticator
AsConfigurator() Configurator
AsSessionRestorer() SessionRestorer
Authenticate() Authenticator
AsConfigurator() Configurator // Optional
AsSessionRestorer() SessionRestorer // Optional
}
// The authenticator interface allows for a multistage initial authentication
@ -169,37 +232,26 @@ type Service interface {
// 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())
// // 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
// }
// s, err = svc.Authenticate(outputs)
// if err != nil {
// renderError(errors.Wrap(err, "Error while authenticating"))
// continue // retry
// }
//
// break // success
// break // success
// }
//
type Authenticator interface {
// AuthenticateForm should return a list of authentication entries for
// the frontend to render.
AuthenticateForm() []AuthenticateEntry
// Authenticate will be called with a list of values with indices
// correspond to the returned slice of AuthenticateEntry.
Authenticate([]string) (Session, error)
}
// 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.
type AuthenticateEntry struct {
Name string
Placeholder string
Description string
Secret bool
Multiline bool
// AuthenticateForm should return a list of authentication entries for the
// frontend to render.
AuthenticateForm() []AuthenticateEntry
// Authenticate will be called with a list of values with indices correspond to
// the returned slice of AuthenticateEntry.
Authenticate([]string) []string // Blocking
}
// SessionRestorer extends Service and is called by the frontend to restore a
@ -208,7 +260,7 @@ type AuthenticateEntry struct {
//
// To save a session, refer to SessionSaver.
type SessionRestorer interface {
RestoreSession(map[string]string) (Session, error)
RestoreSession(map[string]string) map[string]string // Blocking
}
// Configurator is an interface which the backend can implement for a primitive
@ -216,28 +268,8 @@ type SessionRestorer interface {
// to do IO. The frontend should handle this appropriately, including running
// them asynchronously.
type Configurator interface {
Configuration() (map[string]string, error)
SetConfiguration(map[string]string) error
}
// ErrInvalidConfigAtField is the structure for an error at a specific
// configuration field. Frontends can use this and highlight fields if the
// backends support it.
type ErrInvalidConfigAtField struct {
Key string
Err error
}
var _ error = (*ErrInvalidConfigAtField)(nil)
// Error formats the error; it satisfies the error interface.
func (err *ErrInvalidConfigAtField) Error() string {
return "Error at " + err.Key + ": " + err.Err.Error()
}
// Unwrap returns the underlying error.
func (err *ErrInvalidConfigAtField) Unwrap() error {
return err.Err
Configuration() // Blocking
SetConfiguration(map[string]string) map[string]string // Blocking
}
// A session is returned after authentication on the service. Session implements
@ -250,29 +282,25 @@ func (err *ErrInvalidConfigAtField) Unwrap() error {
// secure or not is up to the frontend. For a Gtk client, that would be using
// the GNOME Keyring daemon.
type Session interface {
// Identifier should typically return the user ID.
Identifier
// Namer gives the name of the session, which is typically the username.
Namer
Lister
// 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 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.
Disconnect() error
Lister
AsCommander() Commander
AsSessionSaver() SessionSaver
// However, it will treat the session as actually disconnected. If needed, the
// backend must implement reconnection by itself.
Disconnect() // Blocking
AsCommander() Commander // Optional
AsSessionSaver() SessionSaver // Optional
}
// SessionSaver extends Session and is called by the frontend to save the
@ -295,19 +323,18 @@ type SessionSaver interface {
// A very primitive use of this API would be to provide additional features that
// are not in cchat through a very basic terminal interface.
type Commander interface {
// 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.
// 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 must not do any IO; if it does, then they have to be in a
// goroutine and stream their results to the ReadCloser.
// 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.
RunCommand([]string) (io.ReadCloser, error)
AsCompleter() Completer
// zero-length string slice) should be ignored. The backend should be able to
// assume that the argument slice is always length 1 or more.
RunCommand([]string, io.Writer) ([]string, io.Writer) // Blocking
AsCompleter() Completer // Optional
}
// Server is a single server-like entity that could translate to a guild, a
@ -317,15 +344,13 @@ type Server interface {
Identifier
Namer
// Implement either one of those only.
AsLister() Lister
AsMessenger() Messenger
AsCommander() Commander
AsConfigurator() Configurator
AsLister() Lister // Optional
AsMessenger() Messenger // Optional
AsCommander() Commander // Optional
AsConfigurator() Configurator // Optional
}
// ServerList is for servers that contain children servers. This is similar to
// 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
@ -335,71 +360,74 @@ type Server interface {
// The backend should call both the container and other icon and label
// containers, if any.
type Lister interface {
// 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.
Servers(ServersContainer) error
// 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.
Servers(ServersContainer) (err error)
}
// ServerMessage is for servers that contain messages. This is similar to
// Discord or IRC channels.
// Messenger is for servers that contain messages. This is similar to Discord or
// IRC channels.
type Messenger interface {
// JoinServer joins a server that's capable of receiving messages. The
// server may not necessarily support sending messages.
JoinServer(context.Context, MessagesContainer) (stop func(), err error)
AsSender() Sender
AsEditor() Editor
AsActioner() Actioner
AsNicknamer() Nicknamer
AsBacklogger() Backlogger
AsMemberLister() MemberLister
AsUnreadIndicator() UnreadIndicator
AsTypingIndicator() TypingIndicator
// JoinServer joins a server that's capable of receiving messages. The server
// may not necessarily support sending messages.
JoinServer(context.Context, MessagesContainer) (stop func(), err error)
AsSender() Sender // Optional
AsEditor() Editor // Optional
AsActioner() Actioner // Optional
AsNicknamer() Nicknamer // Optional
AsBacklogger() Backlogger // Optional
AsMemberLister() MemberLister // Optional
AsUnreadIndicator() UnreadIndicator // Optional
AsTypingIndicator() TypingIndicator // Optional
}
// MessageSender adds message sending to a messenger. Messengers that don't
// implement MessageSender will be considered read-only.
// Sender adds message sending to a messenger. Messengers that don't implement
// MessageSender will be considered read-only.
type Sender interface {
// Send is called by the frontend to send a message to this channel.
Send(SendableMessage) error
Send(SendableMessage) SendableMessage // Blocking
// CanAttach returns whether or not the client is allowed to upload files.
CanAttach() bool
AsCompleter() Completer
AsCompleter() Completer // Optional
}
// Editor adds message editing to the messenger. Only EditMessage can do IO.
type Editor interface {
// MessageEditable returns whether or not a message can be edited by the
// client. This method must not do IO.
// MessageEditable returns whether or not a message can be edited by the client.
// This method must not do IO.
MessageEditable(id ID) bool
// RawMessageContent gets the original message text for editing. This method
// must not do IO.
RawMessageContent(id ID) (string, error)
// EditMessage edits the message with the given ID to the given content,
// which is the edited string from RawMessageContent. This method can do IO.
EditMessage(id ID, content string) error
// EditMessage edits the message with the given ID to the given content, which
// is the edited string from RawMessageContent. This method can do IO.
EditMessage(id ID, content string) (id ID, content string) // Blocking
}
// Actioner adds custom message actions into each message. Similarly to
// ServerMessageEditor, some of these methods may do IO.
type Actioner interface {
// MessageActions returns a list of possible actions in pretty strings that
// the frontend will use to directly display. This method must not do IO.
// 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.
Actions(id string) []string
// DoMessageAction 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.
DoAction(action, id string) error
Actions() []string
// 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.
DoAction(action string, id ID) (action string, id ID) // Blocking
}
// 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
// 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.
type Nicknamer interface {
@ -419,92 +447,333 @@ type Nicknamer interface {
// Note: Although backends might rely on this context, the frontend is still
// expected to invalidate the given container when the channel is changed.
type Backlogger interface {
// MessagesBefore fetches messages before the given message ID into the
// MessagesContainer.
MessagesBefore(ctx context.Context, before ID, c MessagePrepender) error
MessagesBefore(ctx context.Context, before ID, c MessagePrepender) (ctx context.Context, before ID, c MessagePrepender) // Blocking
}
// MemberLister adds a member list into a message server.
type MemberLister interface {
// 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.
// 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.
// Further behavioral documentations may be in Messenger's JoinServer method.
ListMembers(context.Context, MemberListContainer) (stop func(), err error)
}
// UnreadIndicator adds an unread state API for frontends to use.
type UnreadIndicator interface {
// 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.
// 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.
// 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.
UnreadIndicate(UnreadContainer) (stop func(), err error)
}
// ServerMessageTypingIndicator optionally extends ServerMessage to provide
// bidirectional typing indicating capabilities. This is similar to typing
// events on Discord and typing client tags on IRCv3.
// 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.
type TypingIndicator interface {
// 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
// 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.
Typing() error
// 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.
Typing() // Blocking
// 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.
TypingTimeout() time.Duration
// 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.
// 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.
// 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.
TypingSubscribe(TypingContainer) (stop func(), err error)
}
// MessageCompleter 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.
// 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.
type Completer interface {
// 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.
Complete(words []string, current int) []CompletionEntry
// 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.
Complete(words []string, current int64) []CompletionEntry
}
// CompletionEntry is a single completion entry returned by CompleteMessage. The
// icon URL field is optional.
type CompletionEntry struct {
// Raw is the text to be replaced in the input box.
Raw string
// Text is the label to be displayed.
Text text.Rich
// 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.
Secondary text.Rich
// IconURL is the URL to the icon that will be displayed on the left of the
// text. This field is optional.
IconURL string
// Image returns whether or not the icon URL is actually an image, which
// indicates that the frontend should not do rounded corners.
Image bool
// 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.
type ServersContainer interface {
// 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.
SetServers([]Server)
UpdateServer(ServerUpdate)
}
type ServerUpdate interface {
Server
// PreviousID returns the ID of the item before this server.
PreviousID() ID
}
// 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.
type MessagesContainer interface {
// CreateMessage inserts a message into the container. The frontend must
// guarantee that the messages are in order based on what's returned from
// Time().
CreateMessage(MessageCreate)
UpdateMessage(MessageUpdate)
DeleteMessage(MessageDelete)
AsMessagePrepender() MessagePrepender // Optional
}
// MessageHeader implements the minimum interface for any message event.
type MessageHeader interface {
Identifier
Time() time.Time
}
// MessageCreate is the interface for an incoming message.
type MessageCreate interface {
MessageHeader
Noncer
Author() Author
Content() text.Rich
// Mentioned returns whether or not the message mentions the current user. If a
// backend does not implement mentioning, then false can be returned.
Mentioned()
}
// MessageUpdate is the interface for a message update (or edit) event. If the
// returned text.Rich returns true for Empty(), then the element shouldn't be
// changed.
type MessageUpdate interface {
MessageHeader
Author() Author
Content() text.Rich
}
// MessageDelete is the interface for a message delete event.
type MessageDelete interface {
MessageHeader
}
// LabelContainer is a generic interface for any container that can hold texts.
// It's typically used for rich text labelling for usernames and server names.
//
// Methods that takes in a LabelContainer typically holds it in the state and
// may call SetLabel any time it wants. Thus, the frontend should synchronize
// calls with the main thread if needed.
type LabelContainer interface {
SetLabel(text.Rich)
}
// IconContainer is a generic interface for any container that can hold an
// image. It's typically used for icons that can update itself. Frontends should
// round these icons. For images that shouldn't be rounded, use ImageContainer.
//
// Methods may call SetIcon at any time in its main thread, so the frontend must
// do any I/O (including downloading the image) in another goroutine to avoid
// blocking the backend.
type IconContainer interface {
SetImage(url string)
}
// UnreadContainer is an interface that a single server container (such as a
// button or a tree node) can implement if it's capable of indicating the read
// and mentioned status for that channel.
//
// Server containers that implement this has to implement both SetUnread and
// SetMentioned, and they should also represent those statuses differently. For
// example, a mentioned channel could have a red outline, while an unread
// channel could appear brighter.
//
// Server containers are expected to represent this information in their parent
// nodes as well. For example, if a server is unread, then its parent servers as
// well as the session node should indicate the same status. Highlighting the
// session and service nodes are, however, implementation details, meaning that
// this decision is up to the frontend to decide.
type UnreadContainer interface {
// SetUnread sets the container's unread state to the given boolean. The
// frontend may choose how to represent this.
SetUnread(unread bool, mentioned bool)
}
// TypingContainer is a generic interface for any container that can display
// users typing in the current chatbox. The typing indicator must adhere to the
// TypingTimeout returned from ServerMessageTypingIndicator. The backend should
// assume that to be the case and send events appropriately.
//
// For more documentation, refer to TypingIndicator.
type TypingContainer interface {
// AddTyper appends the typer into the frontend's list of typers, or it pushes
// this typer on top of others.
AddTyper(typer Typer)
// RemoveTyper explicitly removes the typer with the given user ID from the list
// of typers. This function is usually not needed, as the client will take care
// of removing them after TypingTimeout has been reached or other conditions
// listed in ServerMessageTypingIndicator are met.
RemoveTyper(typerID ID)
}
// Typer is an individual user that's typing. This interface is used
// interchangably in TypingIndicator and thus ServerMessageTypingIndicator as
// well.
type Typer interface {
Author
Time() time.Time
}
// MemberListContainer is a generic interface for any container that can display
// a member list. This is similar to Discord's right-side member list or IRC's
// users list. Below is a visual representation of a typical member list
// container:
//
// +-MemberList-----------\
// | +-Section------------|
// | | |
// | | Header - Total |
// | | |
// | | +-Member-----------|
// | | | Name |
// | | | Secondary |
// | | \__________________|
// | | |
// | | +-Member-----------|
// | | | Name |
// | | | Secondary |
// | | \__________________|
// \_\____________________/
type MemberListContainer interface {
// SetSections (re)sets the list of sections to be the given slice. Members from
// the old section list should be transferred over to the new section entry if
// the section name's content is the same. Old sections that don't appear in the
// new slice should be removed.
SetSections(sections []MemberSection)
// SetMember adds or updates (or upsert) a member into a section. This operation
// must not change the section's member count. As such, changes should be done
// separately in SetSection. If the section does not exist, then the client
// should ignore this member. As such, backends must call SetSections first
// before SetMember on a new section.
SetMember(sectionID ID, member ListMember)
// RemoveMember removes a member from a section. If neither the member nor the
// section exists, then the client should ignore it.
RemoveMember(sectionID ID, memberID ID)
}
// ListMember represents a single member in the member list. This is a base
// interface that may implement more interfaces, such as Iconer for the user's
// avatar.
//
// Note that the frontend may give everyone an avatar regardless, or it may not
// show any avatars at all.
type ListMember interface {
Identifier
Namer
// Status returns the status of the member. The backend does not have to show
// offline members with the offline status if it doesn't want to show offline
// menbers at all.
Status() Status
// Secondary returns the subtext of this member. This could be anything, such as
// a user's custom status or away reason.
Secondary() text.Rich
}
// MemberListSection represents a member list section. The section name's
// content must be unique among other sections from the same list regardless of
// the rich segments.
type MemberListSection interface {
Identifier
Namer
// Total returns the total member count.
Total() int
AsMemberDynamicSection() MemberDynamicSection // Optional
}
// MemberDynamicSection represents a dynamically loaded member list section. The
// section behaves similarly to MemberListSection, except the information
// displayed will be considered incomplete until LoadMore returns false.
//
// LoadLess can be called by the client to mark chunks as stale, which the
// server can then unsubscribe from.
type MemberDynamicSection interface {
// LoadMore is a method which the client can call to ask for more members. This
// method can do IO.
//
// Clients may call this method on the last section in the section slice;
// however, calling this method on any section is allowed. Clients may not call
// this method if the number of members in this section is equal to Total.
LoadMore() // Blocking
// LoadMore is a method which the client can call to ask for more members. This
// method can do IO.
//
// Clients may call this method on the last section in the section slice;
// however, calling this method on any section is allowed. Clients may not call
// this method if the number of members in this section is equal to Total.
LoadMore() // Blocking
}
// SendableMessage is the bare minimum interface of a sendable message, that is,
// a message that can be sent with SendMessage(). This allows the frontend to
// implement its own message data implementation.
//
// An example of extending this interface is MessageNonce, which is similar to
// IRCv3's labeled response extension or Discord's nonces. The frontend could
// implement this interface and check if incoming MessageCreate events implement
// the same interface.
type SendableMessage interface {
Content() string
AsNoncer() Noncer // Optional
AsAttachments() Attachments // Optional
}
// Attachments extends SendableMessage which adds attachments into the message.
// Backends that can use this interface should implement
// ServerMessageAttachmentSender.
type Attachments interface {
Attachments() []MessageAttachment
}

View File

@ -1,339 +0,0 @@
package cchat
import (
"fmt"
"io"
"time"
"github.com/diamondburned/cchat/text"
)
// 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.
type ServersContainer interface {
// 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.
SetServers([]Server)
// Update
UpdateServer(ServerUpdate)
}
type ServerUpdate interface {
// 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.
Server
// PreviousID returns the ID of the item before this server.
PreviousID() ID
}
// 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.
type MessagesContainer interface {
CreateMessage(MessageCreate)
UpdateMessage(MessageUpdate)
DeleteMessage(MessageDelete)
}
// MessagePrepender extends MessagesContainer for backlog implementations. The
// backend is expected to call this interface's method from latest to earliest.
type MessagePrepender interface {
// PrependMessage prepends the given MessageCreate message into the top of
// the chat buffer.
PrependMessage(MessageCreate)
}
// MessageHeader implements the minimum interface for any message event.
type MessageHeader interface {
Identifier
Time() time.Time
}
// MessageCreate is the interface for an incoming message.
type MessageCreate interface {
MessageHeader
Author() Author
Content() text.Rich
// Optional interfaces and methods that can return zero-values.
Noncer
// Mentioned returns whether or not the message mentions the current user.
Mentioned() bool
}
// MessageUpdate is the interface for a message update (or edit) event. If the
// returned text.Rich returns true for Empty(), then the element shouldn't be
// changed.
type MessageUpdate interface {
MessageHeader
Author() Author // optional (nilable)
Content() text.Rich // optional (rich.Content == "")
}
// MessageDelete is the interface for a message delete event.
type MessageDelete interface {
MessageHeader
}
// LabelContainer is a generic interface for any container that can hold texts.
// It's typically used for rich text labelling for usernames and server names.
//
// Methods that takes in a LabelContainer typically holds it in the state and
// may call SetLabel any time it wants. Thus, the frontend should synchronize
// calls with the main thread if needed.
type LabelContainer interface {
SetLabel(text.Rich)
}
// IconContainer is a generic interface for any container that can hold an
// image. It's typically used for icons that can update itself. Frontends should
// round these icons. For images that shouldn't be rounded, use ImageContainer.
//
// Methods may call SetIcon at any time in its main thread, so the frontend must
// do any I/O (including downloading the image) in another goroutine to avoid
// blocking the backend.
type IconContainer interface {
SetIcon(url string)
}
// ImageContainer does nothing; it's reserved for future API usages. Typically,
// images don't have round corners while icons do.
type ImageContainer interface {
SetImage(url string)
}
// UnreadIndicator is an interface that a single server container (such as a
// button or a tree node) can implement if it's capable of indicating the read
// and mentioned status for that channel.
//
// Server containers that implement this has to implement both SetUnread and
// SetMentioned, and they should also represent those statuses differently. For
// example, a mentioned channel could have a red outline, while an unread
// channel could appear brighter.
//
// Server containers are expected to represent this information in their parent
// nodes as well. For example, if a server is unread, then its parent servers as
// well as the session node should indicate the same status. Highlighting the
// session and service nodes are, however, implementation details, meaning that
// this decision is up to the frontend to decide.
type UnreadContainer interface {
// Unread sets the container's unread state to the given boolean. The
// frontend may choose how to represent this.
SetUnread(unread, mentioned bool)
}
// TypingIndicator is a generic interface for any container that can display
// users typing in the current chatbox. The typing indicator must adhere to the
// TypingTimeout returned from ServerMessageTypingIndicator. The backend should
// assume that to be the case and send events appropriately.
//
// For more documentation, refer to ServerMessageTypingIndicator.
type TypingContainer interface {
// AddTyper appends the typer into the frontend's list of typers, or it
// pushes this typer on top of others.
AddTyper(Typer)
// RemoveTyper explicitly removes the typer with the given user ID from the
// list of typers. This function is usually not needed, as the client will
// take care of removing them after TypingTimeout has been reached or other
// conditions listed in ServerMessageTypingIndicator are met.
RemoveTyper(ID)
}
// Typer is an individual user that's typing. This interface is used
// interchangably in TypingIndicator and thus ServerMessageTypingIndicator as
// well.
type Typer interface {
Author
Time() time.Time
}
// MemberListContainer is a generic interface for any container that can display
// a member list. This is similar to Discord's right-side member list or IRC's
// users list. Below is a visual representation of a typical member list
// container:
//
// +-MemberList-----------\
// | +-Section------------|
// | | |
// | | Header - Total |
// | | |
// | | +-Member-----------|
// | | | Name |
// | | | Secondary |
// | | \__________________|
// | | |
// | | +-Member-----------|
// | | | Name |
// | | | Secondary |
// | | \__________________|
// \_\____________________/
//
type MemberListContainer interface {
// SetSections (re)sets the list of sections to be the given slice. Members
// from the old section list should be transferred over to the new section
// entry if the section name's content is the same. Old sections that don't
// appear in the new slice should be removed.
SetSections(sections []MemberSection)
// SetMember adds or updates (or upsert) a member into a section. This
// operation must not change the section's member count. As such, changes
// should be done separately in SetSection. If the section does not exist,
// then the client should ignore this member. As such, backends must call
// SetSections first before SetMember on a new section.
SetMember(sectionID ID, member ListMember)
// RemoveMember removes a member from a section. If neither the member nor
// the section exists, then the client should ignore it.
RemoveMember(sectionID, memberID ID)
}
// ListMember represents a single member in the member list. This is a base
// interface that may implement more interfaces, such as Iconer for the user's
// avatar.
//
// Note that the frontend may give everyone an avatar regardless, or it may not
// show any avatars at all.
type ListMember interface {
// Identifier identifies the individual member. This works similarly to
// MessageAuthor.
Identifier
// Namer returns the name of the member. This works similarly to a
// MessageAuthor.
Namer
// Status returns the status of the member. The backend does not have to
// show offline members with the offline status if it doesn't want to show
// offline menbers at all.
Status() UserStatus
// Secondary returns the subtext of this member. This could be anything,
// such as a user's custom status or away reason.
Secondary() text.Rich
}
// UserStatus represents a user's status. This might be used by the frontend to
// visually display the status.
type UserStatus uint8
const (
UnknownStatus UserStatus = iota
OnlineStatus
IdleStatus
BusyStatus // a.k.a. Do Not Disturb
AwayStatus
OfflineStatus
InvisibleStatus // reserved; currently unused
)
// String formats a user status as a title string, such as "Online" or
// "Unknown". It treats unknown constants as UnknownStatus.
func (s UserStatus) String() string {
switch s {
case OnlineStatus:
return "Online"
case IdleStatus:
return "Idle"
case BusyStatus:
return "Busy"
case AwayStatus:
return "Away"
case OfflineStatus:
return "Offline"
case InvisibleStatus:
return "Invisible"
case UnknownStatus:
return "Unknown"
default:
return fmt.Sprintf("UserStatus(%d)", s)
}
}
// MemberListSection represents a member list section. The section name's
// content must be unique among other sections from the same list regardless of
// the rich segments.
type MemberSection interface {
// Identifier identifies the current section.
Identifier
// Namer returns the section name.
Namer
// Total returns the total member count.
Total() int
// Optionals.
AsMemberDynamicSection() MemberDynamicSection
}
// MemberListDynamicSection represents a dynamically loaded member list section.
// The section behaves similarly to MemberListSection, except the information
// displayed will be considered incomplete until LoadMore returns false.
//
// LoadLess can be called by the client to mark chunks as stale, which the
// server can then unsubscribe from.
type MemberDynamicSection interface {
// LoadMore is a method which the client can call to ask for more members.
// This method can do IO.
//
// Clients may call this method on the last section in the section slice;
// however, calling this method on any section is allowed. Clients may not
// call this method if the number of members in this section is equal to
// Total.
LoadMore() bool
// LoadLess is a method which the client must call after it is done
// displaying entries that were added from calling LoadMore.
//
// The client can call this method exactly as many times as it has called
// LoadMore. However, false should be returned if the client should stop,
// and future calls without LoadMore should still return false.
LoadLess() bool
}
// SendableMessage is the bare minimum interface of a sendable message, that is,
// a message that can be sent with SendMessage(). This allows the frontend to
// implement its own message data implementation.
//
// An example of extending this interface is MessageNonce, which is similar to
// IRCv3's labeled response extension or Discord's nonces. The frontend could
// implement this interface and check if incoming MessageCreate events implement
// the same interface.
//
// SendableMessage can implement the following interfaces:
//
// - MessageNonce (optional)
// - SendableMessageAttachments (optional): refer to ServerMessageAttachmentSender
type SendableMessage interface {
Content() string
// Optionals.
AsNoncer() Noncer
AsAttachments() Attachments
}
// SendableMessageAttachments extends SendableMessage which adds attachments
// into the message. Backends that can use this interface should implement
// ServerMessageAttachmentSender.
type Attachments interface {
Attachments() []MessageAttachment
}
// MessageAttachment represents a single file attachment.
//
// If needed, the frontend will close the reader after the message is sent, that
// is when the SendMessage function returns. The backend must not use the reader
// after that.
type MessageAttachment struct {
io.Reader
Name string
}

View File

@ -0,0 +1,69 @@
package main
import (
"github.com/dave/jennifer/jen"
"github.com/diamondburned/cchat/repository"
)
func generateEnums(enums []repository.Enumeration) jen.Code {
var stmt = new(jen.Statement)
for _, enum := range enums {
if !enum.Comment.IsEmpty() {
stmt.Comment(enum.Comment.GoString(1))
stmt.Line()
}
stmt.Type().Id(enum.Name).Id(enum.GoType())
stmt.Line()
stmt.Line()
stmt.Const().DefsFunc(func(group *jen.Group) {
for i, value := range enum.Values {
var c jen.Statement
if !value.Comment.IsEmpty() {
c.Comment(value.Comment.GoString(2))
c.Line()
}
c.Id(enum.Name + value.Name)
if i == 0 {
c.Id(enum.Name).Op("=").Iota()
}
group.Add(&c)
}
})
stmt.Line()
stmt.Line()
var recv = recvName(enum.Name)
if enum.Bitwise {
fn := stmt.Func()
fn.Params(jen.Id(recv).Id(enum.Name))
fn.Id("Is")
fn.Params(jen.Id("is").Id(enum.Name))
fn.Bool()
fn.BlockFunc(func(g *jen.Group) {
g.Return(jen.Id(recv).Id("==").Id("is"))
})
} else {
fn := stmt.Func()
fn.Params(jen.Id(recv).Id(enum.Name))
fn.Id("Has")
fn.Params(jen.Id("has").Id(enum.Name))
fn.Bool()
fn.BlockFunc(func(g *jen.Group) {
g.Return(jen.Id(recv).Op("&").Id("has").Op("==").Id("has"))
})
}
stmt.Line()
stmt.Line()
}
return stmt
}

View File

@ -0,0 +1,113 @@
package main
import (
"github.com/dave/jennifer/jen"
"github.com/diamondburned/cchat/repository"
)
func generateInterfaces(ifaces []repository.Interface) jen.Code {
var stmt = new(jen.Statement)
for _, iface := range ifaces {
if !iface.Comment.IsEmpty() {
stmt.Comment(iface.Comment.GoString(1))
stmt.Line()
}
stmt.Type().Id(iface.Name).InterfaceFunc(func(group *jen.Group) {
for _, embed := range iface.Embeds {
group.Id(embed.InterfaceName)
}
group.Line()
for _, method := range iface.Methods {
var stmt = new(jen.Statement)
if comment := method.UnderlyingComment(); !comment.IsEmpty() {
stmt.Comment(comment.GoString(1))
stmt.Line()
}
stmt.Id(method.UnderlyingName())
switch method := method.(type) {
case repository.GetterMethod:
stmt.Params(generateFuncParams(method.Parameters, false)...)
stmt.Params(generateFuncParams(method.Returns, method.ReturnError)...)
case repository.SetterMethod:
stmt.Params(generateFuncParams(method.Parameters, false)...)
case repository.IOMethod:
stmt.Params(generateFuncParams(method.Parameters, false)...)
stmt.Params(generateFuncParams(method.Parameters, false)...)
stmt.Comment("// Blocking")
case repository.ContainerMethod:
stmt.Params(generateContainerFuncParams(method)...)
stmt.Params(generateContainerFuncReturns(method)...)
case repository.AsserterMethod:
stmt.Params()
stmt.Params(generateType(method))
stmt.Comment("// Optional")
default:
continue
}
group.Add(stmt)
}
})
stmt.Line()
stmt.Line()
}
return stmt
}
func generateFuncParam(param repository.NamedType) jen.Code {
if param.Name == "" {
return generateType(param)
}
return jen.Id(param.Name).Add(generateType(param))
}
func generateFuncParams(params []repository.NamedType, withError bool) []jen.Code {
if len(params) == 0 {
return nil
}
var stmt jen.Statement
for _, param := range params {
stmt.Add(generateFuncParam(param))
}
if withError {
if params[0].Name != "" {
stmt.Add(jen.Err().Error())
} else {
stmt.Add(jen.Error())
}
}
return stmt
}
func generateContainerFuncReturns(method repository.ContainerMethod) []jen.Code {
var stmt jen.Statement
if method.HasStopFn {
stmt.Add(jen.Id("stop").Func().Params())
}
stmt.Add(jen.Err().Error())
return stmt
}
func generateContainerFuncParams(method repository.ContainerMethod) []jen.Code {
var stmt jen.Statement
if method.HasContext {
stmt.Qual("context", "Context")
}
stmt.Add(generateType(method))
return stmt
}

View File

@ -0,0 +1,80 @@
package main
import (
"github.com/dave/jennifer/jen"
"github.com/diamondburned/cchat/repository"
)
func generateStructs(structs []repository.Struct) jen.Code {
var stmt = new(jen.Statement)
for _, s := range structs {
stmt.Add(generateStruct(s))
stmt.Line()
stmt.Line()
}
return stmt
}
func generateErrorStructs(errStructs []repository.ErrorStruct) jen.Code {
var stmt = new(jen.Statement)
for _, errStruct := range errStructs {
stmt.Add(generateStruct(errStruct.Struct))
stmt.Line()
stmt.Line()
var recv = recvName(errStruct.Name)
stmt.Func()
stmt.Params(jen.Id(recv).Id(errStruct.Name))
stmt.Id("Error").Params().String()
stmt.BlockFunc(func(g *jen.Group) {
g.Return(jen.Qual("fmt", "Sprintf").CallFunc(func(g *jen.Group) {
g.Lit(errStruct.ErrorString.Format)
for _, field := range errStruct.ErrorString.Fields {
g.Add(jen.Id(recv).Dot(field))
}
}))
})
stmt.Line()
stmt.Line()
if wrap := errStruct.Wrapped(); wrap != "" {
stmt.Func()
stmt.Params(jen.Id(recv).Id(errStruct.Name))
stmt.Id("Unwrap").Params().Error()
stmt.BlockFunc(func(g *jen.Group) {
g.Return(jen.Id(recv).Dot(wrap))
})
stmt.Line()
stmt.Line()
}
}
return stmt
}
func generateStruct(s repository.Struct) jen.Code {
var stmt = new(jen.Statement)
if !s.Comment.IsEmpty() {
stmt.Comment(s.Comment.GoString(1))
stmt.Line()
}
stmt.Type().Id(s.Name).StructFunc(func(group *jen.Group) {
for _, field := range s.Fields {
var stmt = new(jen.Statement)
if field.Name != "" {
stmt.Id(field.Name)
}
stmt.Add(generateType(field))
group.Add(stmt)
}
})
return stmt
}

View File

@ -0,0 +1,35 @@
package main
import (
"github.com/dave/jennifer/jen"
"github.com/diamondburned/cchat/repository"
)
func generateTypeAlises(aliases []repository.TypeAlias) jen.Code {
var stmt = new(jen.Statement)
for _, alias := range aliases {
if !alias.Comment.IsEmpty() {
stmt.Comment(alias.Comment.GoString(1))
stmt.Line()
}
stmt.Type().Id(alias.Name).Op("=").Add(generateType(alias))
stmt.Line()
stmt.Line()
}
return stmt
}
type qualer interface {
Qual() (path, name string)
}
func generateType(t qualer) jen.Code {
path, name := t.Qual()
if path == "" {
return jen.Id(name)
}
return jen.Qual(path, name)
}

View File

@ -0,0 +1,72 @@
package main
import (
"log"
"os"
"path/filepath"
"strings"
"unicode"
"github.com/dave/jennifer/jen"
"github.com/diamondburned/cchat/repository"
)
const OutputDir = "."
func init() {
log.SetFlags(0)
}
func main() {
for pkgPath, pkg := range repository.Main {
g := generate(pkgPath, pkg)
var destDir = filepath.FromSlash(trimPrefix(repository.RootPath, pkgPath))
var destFle = filepath.Base(pkgPath)
// Guarantee that the directory exists.
if destDir != "" {
if err := os.MkdirAll(destDir, os.ModePerm); err != nil {
log.Fatalln("Failed to mkdir -p:", err)
}
}
f, err := os.Create(filepath.Join(destDir, destFle+".go"))
if err != nil {
log.Fatalln("Failed to create output file:", err)
}
if err := g.Render(f); err != nil {
log.Fatalln("Failed to render output:", err)
}
f.Close()
}
}
func trimPrefix(rootPrefix, path string) string {
return strings.Trim(strings.TrimPrefix(path, rootPrefix), "/")
}
// recvName is used to get the receiver variable name. It returns the first
// letter lower-cased. It does NOT do length checking. It only works with ASCII.
func recvName(name string) string {
return string(unicode.ToLower(rune(name[0])))
}
func generate(pkgPath string, repo repository.Package) *jen.File {
gen := jen.NewFilePath(pkgPath)
gen.PackageComment(repo.Comment.GoString(1))
gen.Add(generateTypeAlises(repo.TypeAliases))
gen.Line()
gen.Add(generateEnums(repo.Enums))
gen.Line()
gen.Add(generateStructs(repo.Structs))
gen.Line()
gen.Add(generateErrorStructs(repo.ErrorStructs))
gen.Line()
gen.Add(generateInterfaces(repo.Interfaces))
gen.Line()
return gen
}

3
generator.go Normal file
View File

@ -0,0 +1,3 @@
package cchat
//go:generate go run ./cmd/cchat-generator

View File

@ -13,29 +13,61 @@ var (
commentTrimSurrounding = regexp.MustCompile(`(^\n)|(\n\t+$)`)
)
// TabWidth is used to format comments.
var TabWidth = 4
// Comment represents a raw comment string. Most use cases should use GoString()
// to get the comment's content.
type Comment struct {
RawText string
Raw string
}
// GoString formats the documentation string in 80 columns wide paragraphs.
func (c Comment) GoString() string {
return c.WrapText(80)
// IsEmpty returns true if the comment is empty.
func (c Comment) IsEmpty() bool {
return c.Raw == ""
}
// GoString formats the documentation string in 80 columns wide paragraphs and
// prefix each line with "// ". The ident argument controls the nested level. If
// less than or equal to zero, then it is changed to 1, which is the top level.
func (c Comment) GoString(ident int) string {
if ident < 1 {
ident = 1
}
ident-- // 0th-indexed
ident *= TabWidth
var lines = strings.Split(c.WrapText(80-len("// ")-ident), "\n")
for i, line := range lines {
lines[i] = "// " + line
}
return strings.Join(lines, "\n")
}
// 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()
var txt = c.Unindent()
if txt == "" {
return ""
}
buf := bytes.Buffer{}
doc.ToText(&buf, txt, "", strings.Repeat(" ", TabWidth-1), column)
return strings.TrimRight(buf.String(), "\n")
}
// 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 {
if c.IsEmpty() {
return ""
}
// Trim new lines.
txt := commentTrimSurrounding.ReplaceAllString(c.RawText, "")
txt := commentTrimSurrounding.ReplaceAllString(c.Raw, "")
// Split the lines and rejoin them later to trim the indentation.
var lines = strings.Split(txt, "\n")

View File

@ -1,15 +1,34 @@
package repository
type Enumeration struct {
Comment
Comment Comment
Name string
Values []EnumValue
Bitwise bool
Bitwise bool // uint32 if true, uint8 otherwise
}
// GoType returns uint8 for a normal enum and uint32 for a bitwise enum. It
// returns an empty string if the length of values is overbound.
//
// The maximum number of values in a normal enum is math.MaxUint8 or 255. The
// maximum number of values in a bitwise enum is 32 for 32 bits in a uint32.
func (e Enumeration) GoType() string {
if !e.Bitwise {
if len(e.Values) > 255 {
return ""
}
return "uint8"
}
if len(e.Values) > 32 {
return ""
}
return "uint32"
}
type EnumValue struct {
Comment
Name string // also return value from String()
Comment Comment
Name string // also return value from String()
}
// IsPlaceholder returns true if the enumeration value is meant to be a

View File

@ -1,17 +0,0 @@
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
}

View File

@ -1,3 +0,0 @@
package xml
//go:generate go run ./generator.go

View File

@ -1,7 +1,7 @@
// +build ignore
package main
//go:generate go run ./generator.go
import (
"encoding/gob"
"log"
@ -10,15 +10,17 @@ import (
"github.com/diamondburned/cchat/repository"
)
const output = "repository.gob"
func main() {
f, err := os.Create("repository.gob")
f, err := os.Create(output)
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)
os.Remove(output)
log.Fatalln("Failed to gob encode:", err)
}
}

Binary file not shown.

View File

@ -1,16 +1,27 @@
package repository
import "strings"
import (
"encoding/gob"
"strings"
)
func init() {
gob.Register(ContainerMethod{})
gob.Register(AsserterMethod{})
gob.Register(GetterMethod{})
gob.Register(SetterMethod{})
gob.Register(IOMethod{})
}
type Interface struct {
Comment
Comment Comment
Name string
Embeds []EmbeddedInterface
Methods []Method // actual methods
}
type EmbeddedInterface struct {
Comment
Comment Comment
InterfaceName string
}
@ -19,3 +30,97 @@ type EmbeddedInterface struct {
func (i Interface) IsContainer() bool {
return strings.HasSuffix(i.Name, "Container")
}
type Method interface {
UnderlyingName() string
UnderlyingComment() Comment
internalMethod()
}
type method struct {
Comment Comment
Name string
}
func (m method) UnderlyingName() string { return m.Name }
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.
type GetterMethod struct {
method
// 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. They're used only for containers. Actual setter
// methods implemented by the backend belongs to IOMethods.
type SetterMethod struct {
method
// 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 {
method
// 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 {
method
// 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
}
// Qual returns what TypeQual returns with m.ContainerType.
func (m ContainerMethod) Qual() (path, name string) {
return TypeQual(m.ContainerType)
}
// 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() {}
func (m AsserterMethod) UnderlyingComment() Comment { return Comment{} }
// UnderlyingName returns the name of the method.
func (m AsserterMethod) UnderlyingName() string {
return "As" + m.ChildType
}
// Qual returns what TypeQual returns with m.ChildType.
func (m AsserterMethod) Qual() (path, name string) {
return TypeQual(m.ChildType)
}

File diff suppressed because it is too large Load Diff

View File

@ -1,91 +0,0 @@
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
}

View File

@ -1,21 +1,25 @@
package repository
// MainNamespace is the name of the namespace that should be top level.
const MainNamespace = "cchat"
import (
"strings"
)
type Repositories map[string]Repository
// Packages maps Go module paths to packages.
type Packages map[string]Package
type Repository struct {
Enums []Enumeration
TypeAliases []TypeAlias
Structs []Struct
ErrorTypes []ErrorType
Interfaces []Interface
type Package struct {
Comment Comment
Enums []Enumeration
TypeAliases []TypeAlias
Structs []Struct
ErrorStructs []ErrorStruct
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 {
func (p Package) Interface(name string) *Interface {
for _, iface := range p.Interfaces {
if iface.Name == name {
return &iface
}
@ -25,10 +29,53 @@ func (r Repository) Interface(name string) *Interface {
type NamedType struct {
Name string // optional
Type string
Type string // import/path.Type OR (import/path).Type
}
// Qual splits the type name into the path and type name. Refer to TypeQual.
func (t NamedType) Qual() (path, typeName string) {
return TypeQual(t.Type)
}
// IsZero is true if t.Type is empty.
func (t NamedType) IsZero() bool {
return t.Type == ""
}
// TypeQual splits the type name into path and type name. It accepts inputs that
// are similar to the example below:
//
// string
// context.Context
// github.com/diamondburned/cchat/text.Rich
// (github.com/diamondburned/cchat/text).Rich
//
func TypeQual(typePath string) (path, typeName string) {
parts := strings.Split(typePath, ".")
if len(parts) > 1 {
path = strings.Join(parts[:len(parts)-1], ".")
path = strings.TrimPrefix(path, "(")
path = strings.TrimSuffix(path, ")")
typeName = parts[len(parts)-1]
return
}
typeName = typePath
return
}
// TmplString is a generation-time templated string. It is used for string
// concatenation.
//
// Given the following TmplString:
//
// TmplString{Format: "Hello, %s", Fields: []string{"Foo()"}}
//
// The output should be the same as the output of
//
// fmt.Sprintf("Hello, %s", v.Foo())
//
type TmplString struct {
Format string // printf format syntax
Fields []string // list of struct fields
}

View File

@ -1,12 +1,30 @@
package repository
type Struct struct {
Comment
Name string
Fields []StructField
Comment Comment
Name string
Fields []StructField
}
type StructField struct {
Comment
NamedType
}
// ErrorStruct are structs that implement the "error" interface and starts with
// "Err".
type ErrorStruct struct {
Struct
ErrorString TmplString // used for Error()
}
// Wrapped returns true if the error struct contains a field with the error
// type.
func (t ErrorStruct) Wrapped() (fieldName string) {
for _, ret := range t.Struct.Fields {
if ret.Type == "error" {
return ret.Name
}
}
return ""
}

View File

@ -1,21 +0,0 @@
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")
}

View File

@ -2,7 +2,6 @@ package repository
// TypeAlias represents a Go type alias.
type TypeAlias struct {
Comment
Name string
Type string
Comment Comment
NamedType
}

View File

@ -1,30 +1,40 @@
// Package text provides a rich text API for cchat interfaces to use.
package text
// Attribute is the type for basic rich text markup attributes.
type Attribute uint32
const (
// Normal is a zero-value attribute.
AttributeNormal Attribute = iota
// Bold represents bold text.
AttributeBold
// Italics represents italicized text.
AttributeItalics
// Underline represents underlined text.
AttributeUnderline
// Strikethrough represents struckthrough text.
AttributeStrikethrough
// Spoiler represents spoiler text, which usually looks blacked out until
// hovered or clicked on.
AttributeSpoiler
// Monospace represents monospaced text, typically for inline code.
AttributeMonospace
// Dimmed represents dimmed text, typically slightly less visible than other
// text.
AttributeDimmed
)
func (a Attribute) Is(is Attribute) bool {
return a == is
}
// Rich is a normal text wrapped with optional format segments.
type Rich struct {
Content string
// Segments are optional rich-text segment markers.
Content string
Segments []Segment
}
// Empty describes an empty rich text segment with no formatting.
var Empty = Rich{}
// Plain creates a rich text with no formatting.
func Plain(text string) Rich {
return Rich{Content: text}
}
// Empty returns whether or not the rich text is considered empty.
func (r Rich) Empty() bool {
return r.Content == ""
}
// String returns the content. This is used mainly for printing.
func (r Rich) String() string {
return r.Content
}
// Segment is the minimum requirement for a format segment. Frontends will use
// this to determine when the format starts and ends. They will also assert this
// interface to any other formatting interface, including Linker, Colorer and
@ -33,7 +43,7 @@ func (r Rich) String() string {
// Note that a segment may implement multiple interfaces. For example, a
// Mentioner may also implement Colorer.
type Segment interface {
Bounds() (start, end int)
Bounds() (start int, end int)
}
// Linker is a hyperlink format that a segment could implement. This implies
@ -41,6 +51,7 @@ type Segment interface {
// tag with href being the URL and the inner text being the text string.
type Linker interface {
Segment
Link() (url string)
}
@ -48,29 +59,29 @@ type Linker interface {
// image. Only the starting bound matters, as images cannot substitute texts.
type Imager interface {
Segment
// Image returns the URL for the image.
Image() (url string)
// ImageSize returns the requested dimension for the image. This function
// could return (0, 0), which the frontend should use the image's
// dimensions.
ImageSize() (w, h int)
// ImageText returns the underlying text of the image. Frontends could use
// this for hovering or displaying the text instead of the image.
// ImageSize returns the requested dimension for the image. This function could
// return (0, 0), which the frontend should use the image's dimensions.
ImageSize() (w int, h int)
// ImageText returns the underlying text of the image. Frontends could use this
// for hovering or displaying the text instead of the image.
ImageText() string
}
// Avatarer implies the segment should be replaced with a rounded-corners
// image. This works similarly to Imager.
// Avatarer implies the segment should be replaced with a rounded-corners image.
// This works similarly to Imager.
type Avatarer interface {
Segment
// Avatar returns the URL for the image.
Avatar() (url string)
// AvatarSize returns the requested dimension for the image. This function
// could return (0, 0), which the frontend should use the avatar's
// dimensions.
// AvatarSize returns the requested dimension for the image. This function could
// return (0, 0), which the frontend should use the avatar's dimensions.
AvatarSize() (size int)
// AvatarText returns the underlying text of the image. Frontends could use
// this for hovering or displaying the text instead of the image.
// AvatarText returns the underlying text of the image. Frontends could use this
// for hovering or displaying the text instead of the image.
AvatarText() string
}
@ -82,16 +93,18 @@ type Avatarer interface {
// frontends to flexibly layout the labels.
type Mentioner interface {
Segment
// MentionInfo returns the popup information of the mentioned segment. This
// is typically user information or something similar to that context.
// MentionInfo returns the popup information of the mentioned segment. This is
// typically user information or something similar to that context.
MentionInfo() Rich
}
// MentionerImage extends Mentioner to give the mentioned object an image.
// This interface allows the frontend to be more flexible in layouting. A
// Mentioner can only implement EITHER MentionedImage or MentionedAvatar.
// MentionerImage extends Mentioner to give the mentioned object an image. This
// interface allows the frontend to be more flexible in layouting. A Mentioner
// can only implement EITHER MentionedImage or MentionedAvatar.
type MentionerImage interface {
Mentioner
// Image returns the mentioned object's image URL.
Image() (url string)
}
@ -101,6 +114,7 @@ type MentionerImage interface {
// Mentioner can only implement EITHER MentionedImage or MentionedAvatar.
type MentionerAvatar interface {
Mentioner
// Avatar returns the mentioned object's avatar URL.
Avatar() (url string)
}
@ -108,44 +122,20 @@ type MentionerAvatar interface {
// Colorer is a text color format that a segment could implement. This is to be
// applied directly onto the text.
type Colorer interface {
Segment
Mentioner
// Color returns a 24-bit RGB or 32-bit RGBA color.
Color() uint32
}
// Attributor is a rich text markup format that a segment could implement. This
// is to be applied directly onto the text.
type Attributor interface {
Segment
Mentioner
Attribute() Attribute
}
// Attribute is the type for basic rich text markup attributes.
type Attribute uint16
// HasAttr returns whether or not "attr" has "this" attribute.
func (attr Attribute) Has(this Attribute) bool {
return (attr & this) == this
}
const (
// AttrBold represents bold text.
AttrBold Attribute = 1 << iota
// AttrItalics represents italicized text.
AttrItalics
// AttrUnderline represents underlined text.
AttrUnderline
// AttrStrikethrough represents strikethrough text.
AttrStrikethrough
// AttrSpoiler represents spoiler text, which usually looks blacked out
// until hovered or clicked on.
AttrSpoiler
// AttrMonospace represents monospaced text, typically for inline code.
AttrMonospace
// AttrDimmed represents dimmed text, typically slightly less visible than
// other text.
AttrDimmed
)
// Codeblocker is a codeblock that supports optional syntax highlighting using
// the language given. Note that as this is a block, it will appear separately
// from the rest of the paragraph.
@ -153,7 +143,8 @@ const (
// This interface is equivalent to Markdown's codeblock syntax.
type Codeblocker interface {
Segment
CodeblockLanguage() string
CodeblockLanguage() (language string)
}
// Quoteblocker represents a quoteblock that behaves similarly to the blockquote
@ -161,6 +152,9 @@ type Codeblocker interface {
// or with green arrows prepended to each line.
type Quoteblocker interface {
Segment
// Quote does nothing; it's only here to distinguish the interface.
Quote()
// QuotePrefix returns the prefix that every line the segment covers have. This
// is typically the greater-than sign ">" in Markdown. Frontends could use this
// information to format the quote properly.
QuotePrefix() (prefix string)
}