Compare commits
56 Commits
Author | SHA1 | Date |
---|---|---|
diamondburned | b5bb0c9bb9 | |
diamondburned | 8bfabf58ec | |
diamondburned | 410ac73469 | |
diamondburned | 4e11444f6c | |
diamondburned | 86956a65ec | |
diamondburned | f2de1cb84d | |
diamondburned | 0cb14b9819 | |
diamondburned | f24feb2002 | |
diamondburned | f8c644fa7e | |
diamondburned | c7d4473c23 | |
diamondburned | 174496bdf9 | |
diamondburned | da5c38eb2f | |
diamondburned | c2fb784dbf | |
diamondburned | d40f221221 | |
diamondburned | ee9c2cc37c | |
diamondburned | 0569261f72 | |
diamondburned | 4ea6773527 | |
diamondburned | 1ece6ea076 | |
diamondburned | 1251001e8c | |
diamondburned | 41a7dac033 | |
diamondburned | 02c686f994 | |
diamondburned | 1460ee6b4b | |
diamondburned | 06a26af5ba | |
diamondburned | 903fe9fbfd | |
diamondburned | 7cb512f8b1 | |
diamondburned | 24fc2c9bbb | |
diamondburned | f1db8e0601 | |
diamondburned | 7fe9b3ed4c | |
diamondburned | fd8106eaf1 | |
diamondburned | 9fd965d45a | |
diamondburned | 955b99c9b6 | |
diamondburned | c32c50c0e8 | |
diamondburned | 318c85ab65 | |
diamondburned | e59ab2dbf1 | |
diamondburned | ea2c12d119 | |
diamondburned | 4c835a467b | |
diamondburned | 10549e49e1 | |
diamondburned | 289eda1c25 | |
diamondburned | 05f8ec0cbf | |
diamondburned | 1dd36e0034 | |
diamondburned | 76f5201a6f | |
diamondburned | 4864d61476 | |
diamondburned | 0ebf0c3302 | |
diamondburned | d62231a4ef | |
diamondburned | cfc0e00c8a | |
diamondburned | 1b1e10a8a6 | |
diamondburned | 6140b5a131 | |
diamondburned | 285ac6403f | |
diamondburned | 819bcd3504 | |
diamondburned | 32fa6266db | |
diamondburned | 89b5ede1d8 | |
diamondburned | 5f7316cf9d | |
diamondburned | 086f987b3c | |
diamondburned | e08064021e | |
diamondburned | dd4e230e0f | |
diamondburned | 99f7224d32 |
464
cchat.go
464
cchat.go
|
@ -1,4 +1,4 @@
|
|||
// DO NOT EDIT: THIS FILE IS GENERATED!
|
||||
// Code generated by ./cmd/internal. DO NOT EDIT.
|
||||
|
||||
// Package cchat is a set of stabilized interfaces for cchat implementations,
|
||||
// joining the backend and frontend together.
|
||||
|
@ -6,6 +6,16 @@
|
|||
//
|
||||
// Backend
|
||||
//
|
||||
// Almost anything in the backend comes with an ID. For example, a Server must
|
||||
// have an ID, or a Session must have a user ID. The backend is required to
|
||||
// guarantee that IDs are somehow unique. This should already be the case for
|
||||
// most chat services; for example, Discord provides IDs for guilds, channels,
|
||||
// members, and more. The only time that the backend should not guarantee ID
|
||||
// uniqueness is across Sessions, because it doesn't make sense to do so. In
|
||||
// this case, the frontend should guarantee uniqueness instead, either by
|
||||
// discarding duplicated items, overriding them, or anything reasonable and
|
||||
// explicit.
|
||||
//
|
||||
// Methods implemented by the backend that have frontend containers as arguments
|
||||
// can do IO. Frontends must NOT rely on individual backend states and should
|
||||
// always assume that they will block.
|
||||
|
@ -100,8 +110,8 @@ const (
|
|||
StatusInvisible
|
||||
)
|
||||
|
||||
func (s Status) Has(has Status) bool {
|
||||
return s&has == has
|
||||
func (s Status) Is(is Status) bool {
|
||||
return s == is
|
||||
}
|
||||
|
||||
// AuthenticateEntry represents a single authentication entry, usually an email
|
||||
|
@ -133,6 +143,17 @@ type MessageAttachment struct {
|
|||
Name string
|
||||
}
|
||||
|
||||
// ReadIndication represents a read indication of a user/author in a messager
|
||||
// server. It relates to a message ID within the server and is meant to imply
|
||||
// that the user/author has read up to the given message ID.
|
||||
//
|
||||
// The frontend should override an existing author with the received ones. This
|
||||
// could be treated as upsert operations.
|
||||
type ReadIndication struct {
|
||||
User User
|
||||
MessageID ID
|
||||
}
|
||||
|
||||
// ErrInvalidConfigAtField is the structure for an error at a specific
|
||||
// configuration field. Frontends can use this and highlight fields if the
|
||||
// backends support it.
|
||||
|
@ -152,82 +173,72 @@ func (e ErrInvalidConfigAtField) Unwrap() error {
|
|||
// Actioner adds custom message actions into each message. Similarly to
|
||||
// ServerMessageEditor, some of these methods may do IO.
|
||||
type Actioner interface {
|
||||
// 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) error // Blocking
|
||||
// MessageActions returns a list of possible actions in pretty strings that the
|
||||
// frontend will use to directly display. This method must not do IO.
|
||||
// Do 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.
|
||||
Do(ctx context.Context, action string, id ID) error // Blocking
|
||||
// MessageActions returns a list of possible actions to a message 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() []string
|
||||
Actions(id ID) []string
|
||||
}
|
||||
|
||||
// Attachments extends SendableMessage which adds attachments into the message.
|
||||
// Backends that can use this interface should implement AttachmentSender.
|
||||
type Attachments interface {
|
||||
// Attacher adds attachments into the message being sent.
|
||||
type Attacher interface {
|
||||
Attachments() []MessageAttachment
|
||||
}
|
||||
|
||||
// AuthenticateError is the error returned when authenticating. This error
|
||||
// interface extends the normal error to allow backends to implement multi-stage
|
||||
// authentication if needed in a clean way without needing any loops.
|
||||
//
|
||||
// This interface satisfies the error interface.
|
||||
type AuthenticateError interface {
|
||||
// NextStage optionally returns a slice of Authenticator interfaces if the
|
||||
// authentication process requires another stage. It works similarly to
|
||||
// Service's Authenticate method, both of which returns a slice of
|
||||
// Authenticators.
|
||||
//
|
||||
// If the error returned is an actual error, and that the user should retry any
|
||||
// of the authentication fields, then NextStage could return nil to signify the
|
||||
// error. The frontend could reliably check nil on this field to determine
|
||||
// whether or not it should recreate the authentication fields.
|
||||
NextStage() []Authenticator
|
||||
// Error returns the error as a string. This method makes AuthenticateError
|
||||
// satisfy the built-in error interface.
|
||||
Error() string
|
||||
}
|
||||
|
||||
// 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
|
||||
// }
|
||||
// API that the backend could use. Multistage is done by calling Authenticate
|
||||
// and check for AuthenticateError's NextStage method.
|
||||
type Authenticator interface {
|
||||
// Authenticate will be called with a list of values with indices correspond to
|
||||
// the returned slice of AuthenticateEntry.
|
||||
Authenticate([]string) (Session, error) // Blocking
|
||||
Authenticate(context.Context, []string) (Session, AuthenticateError) // Blocking
|
||||
// AuthenticateForm should return a list of authentication entries for the
|
||||
// frontend to render.
|
||||
AuthenticateForm() []AuthenticateEntry
|
||||
}
|
||||
|
||||
// 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.
|
||||
type Author interface {
|
||||
Identifier
|
||||
|
||||
// Avatar returns the URL to the user's avatar or an empty string if they have
|
||||
// no avatar or the service does not have any avatars.
|
||||
Avatar() (url string)
|
||||
// Description returns the description of this authenticator method.
|
||||
Description() text.Rich
|
||||
// Name returns a short and concise name of this Authenticator method. The name
|
||||
// should not include the name of the Service.
|
||||
Name() text.Rich
|
||||
}
|
||||
|
||||
// Backlogger adds message history capabilities into a message container. The
|
||||
// backend should send old messages using the MessageCreate method of the
|
||||
// MessageContainer, and the frontend should automatically sort messages based
|
||||
// MessagesContainer, and the frontend should automatically sort messages based
|
||||
// on the timestamp.
|
||||
//
|
||||
// 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.
|
||||
// top. It is expected to guarantee not to call Backlogger more than once on the
|
||||
// same ID. This can usually be done by deactivating the UI.
|
||||
//
|
||||
// Note that the optional usage of contexts also apply here. The frontend should
|
||||
// deactivate the UI when the backend is working. However, the frontend can
|
||||
|
@ -235,12 +246,12 @@ type Author interface {
|
|||
// freeze the UI until the method is cancelled. This works even when the backend
|
||||
// does not use the context.
|
||||
type Backlogger interface {
|
||||
// MessagesBefore fetches messages before the given message ID into the
|
||||
// Backlog fetches messages before the given message ID into the
|
||||
// MessagesContainer.
|
||||
//
|
||||
// This method is technically a ContainerMethod, but is listed as an IOMethod
|
||||
// because of the additional message ID parameter.
|
||||
MessagesBefore(ctx context.Context, before ID, msgc MessagesContainer) error // Blocking
|
||||
Backlog(ctx context.Context, before ID, msgc MessagesContainer) error // Blocking
|
||||
}
|
||||
|
||||
// Commander is an optional interface that a session could implement for command
|
||||
|
@ -250,16 +261,41 @@ type Backlogger 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.
|
||||
// Run executes the given command, with the slice being already split arguments,
|
||||
// similar to os.Args. The function can return both a []byte and an error value.
|
||||
// The frontend should render the byte slice's value first, then display the
|
||||
// error.
|
||||
//
|
||||
// The function can do IO, and outputs should be written to the given io.Writer.
|
||||
// This function can do IO.
|
||||
//
|
||||
// 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.Writer) error // Blocking
|
||||
//
|
||||
//
|
||||
// Words
|
||||
//
|
||||
// This interface and everything else inside this interface must abide by shell
|
||||
// rules when splitting words. This is in contrary to the default behavior
|
||||
// elsewhere, such as in Sender's Completer, where words are split by whitespace
|
||||
// without care for quotes.
|
||||
//
|
||||
// For example, provided this string:
|
||||
//
|
||||
// echo "This is a string"
|
||||
//
|
||||
// This is the correct output:
|
||||
//
|
||||
// []string{"echo", "This is a string"}
|
||||
//
|
||||
// This is the incorrect output:
|
||||
//
|
||||
// []string{"echo", "\"This", "is", "a", "string\""}
|
||||
//
|
||||
// A helper function for this kind of behavior is available in package split,
|
||||
// under the ArgsIndexed function. This implementation also provides the rough
|
||||
// specifications.
|
||||
Run(ctx context.Context, words []string) ([]byte, error) // Blocking
|
||||
|
||||
// Asserters.
|
||||
|
||||
|
@ -282,46 +318,23 @@ type Completer interface {
|
|||
}
|
||||
|
||||
// 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.
|
||||
// configuration API.
|
||||
type Configurator interface {
|
||||
SetConfiguration(map[string]string) error // Blocking
|
||||
Configuration() (map[string]string, error) // Blocking
|
||||
SetConfiguration(map[string]string) error
|
||||
Configuration() map[string]string
|
||||
}
|
||||
|
||||
// Editor adds message editing to the messenger. Only EditMessage can do IO.
|
||||
type Editor interface {
|
||||
// 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 // Blocking
|
||||
// RawMessageContent gets the original message text for editing. This method
|
||||
// must not do IO.
|
||||
RawMessageContent(id ID) (string, error)
|
||||
// MessageEditable returns whether or not a message can be edited by the client.
|
||||
// This method must not do IO.
|
||||
MessageEditable(id ID) bool
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
// 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.
|
||||
type Iconer interface {
|
||||
Icon(context.Context, IconContainer) (stop func(), err error)
|
||||
// Edit edits the message with the given ID to the given content, which is the
|
||||
// edited string from RawMessageContent. This method can do IO.
|
||||
Edit(ctx context.Context, id ID, content string) error // Blocking
|
||||
// RawContent gets the original message text for editing. This method must not
|
||||
// do IO.
|
||||
RawContent(id ID) (string, error)
|
||||
// IsEditable returns whether or not a message can be edited by the client. This
|
||||
// method must not do IO.
|
||||
IsEditable(id ID) bool
|
||||
}
|
||||
|
||||
// Identifier requires ID() to return a uniquely identifiable string for
|
||||
|
@ -332,36 +345,27 @@ type Identifier interface {
|
|||
ID() ID
|
||||
}
|
||||
|
||||
// ImageContainer is a generic interface for any container that can hold an
|
||||
// image. It's typically used for icons that can update itself. Frontends should
|
||||
// not round these icons. For images that should be rounded, use IconContainer.
|
||||
//
|
||||
// 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 ImageContainer interface {
|
||||
SetImage(url string)
|
||||
}
|
||||
|
||||
// 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.
|
||||
//
|
||||
// Labels given to the frontend may contain images or avatars, and the frontend
|
||||
// has the choice to display them or not.
|
||||
type LabelContainer interface {
|
||||
SetLabel(text.Rich)
|
||||
SetLabel(context.Context, text.Rich)
|
||||
}
|
||||
|
||||
// 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.
|
||||
// ListMember represents a single member in the member list. Note that this
|
||||
// interface should be treated as a static container: updating a member will
|
||||
// involve a completely new ListMember instance with the same ID.
|
||||
//
|
||||
// Note that the frontend may give everyone an avatar regardless, or it may not
|
||||
// show any avatars at all.
|
||||
type ListMember interface {
|
||||
Identifier
|
||||
Namer
|
||||
|
||||
// Secondary returns the subtext of this member. This could be anything, such as
|
||||
// a user's custom status or away reason.
|
||||
|
@ -370,6 +374,9 @@ type ListMember interface {
|
|||
// offline members with the offline status if it doesn't want to show offline
|
||||
// menbers at all.
|
||||
Status() Status
|
||||
// Name returns the username or the nickname of the member, whichever the
|
||||
// backend should prefer.
|
||||
Name() text.Rich
|
||||
}
|
||||
|
||||
// Lister is for servers that contain children servers. This is similar to
|
||||
|
@ -385,7 +392,18 @@ 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) (err error)
|
||||
Servers(ServersContainer) (stop func(), err error)
|
||||
// Columnate is optionally used by servers to tell the frontend whether or not
|
||||
// its children should be put onto a new column instead of underneath it within
|
||||
// the same tree. If the method returns false, then the frontend can treat its
|
||||
// children as normal and show it as children within the same tree.
|
||||
//
|
||||
// For example, in Discord, guilds can be placed in guild folders, but guilds
|
||||
// and guild folders are put in the same column while guilds are actually
|
||||
// children of the folders. To replicate this behavior, guild folders should
|
||||
// return false, and guilds should return true. Both channels and categories can
|
||||
// return false.
|
||||
Columnate() bool
|
||||
}
|
||||
|
||||
// MemberDynamicSection represents a dynamically loaded member list section. The
|
||||
|
@ -401,14 +419,14 @@ type MemberDynamicSection interface {
|
|||
// 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 // Blocking
|
||||
LoadLess(context.Context) bool // 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() bool // Blocking
|
||||
LoadMore(context.Context) bool // Blocking
|
||||
}
|
||||
|
||||
// MemberListContainer is a generic interface for any container that can display
|
||||
|
@ -434,18 +452,22 @@ type MemberDynamicSection interface {
|
|||
type MemberListContainer interface {
|
||||
// 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)
|
||||
RemoveMember(ctx context.Context, sectionID ID, memberID ID)
|
||||
// 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)
|
||||
// should ignore this member, so, backends must call SetSections first before
|
||||
// SetMember on a new section.
|
||||
//
|
||||
// Typically, the backend should try and avoid calling this method and instead
|
||||
// update the labeler in the name. This method should only be used for adding
|
||||
// members.
|
||||
SetMember(ctx context.Context, sectionID ID, member ListMember)
|
||||
// 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)
|
||||
SetSections(ctx context.Context, sections []MemberSection)
|
||||
}
|
||||
|
||||
// MemberLister adds a member list into a message server.
|
||||
|
@ -476,13 +498,14 @@ type MemberSection interface {
|
|||
// MessageCreate is the interface for an incoming message.
|
||||
type MessageCreate interface {
|
||||
MessageHeader
|
||||
// Noncer is optional.
|
||||
Noncer
|
||||
|
||||
// Mentioned returns whether or not the message mentions the current user. If a
|
||||
// backend does not implement mentioning, then false can be returned.
|
||||
Mentioned()
|
||||
Mentioned() bool
|
||||
Content() text.Rich
|
||||
Author() Author
|
||||
Author() User
|
||||
}
|
||||
|
||||
// MessageDelete is the interface for a message delete event.
|
||||
|
@ -497,14 +520,13 @@ type MessageHeader interface {
|
|||
Time() time.Time
|
||||
}
|
||||
|
||||
// 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.
|
||||
// MessageUpdate is the interface for a message update (or edit) event. It is
|
||||
// only responsible for updating a message's content. The author's name should
|
||||
// be updated using MessageCreate's Author.
|
||||
type MessageUpdate interface {
|
||||
MessageHeader
|
||||
|
||||
Content() text.Rich
|
||||
Author() Author
|
||||
}
|
||||
|
||||
// MessagesContainer is a view implementation that displays a list of messages
|
||||
|
@ -515,12 +537,12 @@ type MessageUpdate interface {
|
|||
// allowed to have multiple views. This is usually done with tabs or splits, but
|
||||
// the backend should update them all nonetheless.
|
||||
type MessagesContainer interface {
|
||||
DeleteMessage(MessageDelete)
|
||||
UpdateMessage(MessageUpdate)
|
||||
DeleteMessage(context.Context, MessageDelete)
|
||||
UpdateMessage(context.Context, MessageUpdate)
|
||||
// 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)
|
||||
CreateMessage(context.Context, MessageCreate)
|
||||
}
|
||||
|
||||
// Messenger is for servers that contain messages. This is similar to Discord or
|
||||
|
@ -528,6 +550,13 @@ type MessagesContainer interface {
|
|||
type Messenger interface {
|
||||
// JoinServer joins a server that's capable of receiving messages. The server
|
||||
// may not necessarily support sending messages.
|
||||
//
|
||||
// Frontends must never call JoinServer on the same server more than twice
|
||||
// without calling the stop function first. This is the best of both worlds, as
|
||||
// it greatly reduces complexity on both sides in most cases, therefore the
|
||||
// backend can safely assume that there will only ever be one active JoinServer.
|
||||
// If the frontend wishes to do this, it must keep its own shared message
|
||||
// buffer.
|
||||
JoinServer(context.Context, MessagesContainer) (stop func(), err error)
|
||||
|
||||
// Asserters.
|
||||
|
@ -544,12 +573,15 @@ type Messenger interface {
|
|||
|
||||
// Namer requires Name() to return the name of the object. Typically, this
|
||||
// implies usernames for sessions or service names for services.
|
||||
//
|
||||
// Frontends can show the ID of the object when a name hasn't yet been set. The
|
||||
// backend may immediately update the name afterwards, but assumptions should
|
||||
// not be made.
|
||||
type Namer interface {
|
||||
Name() text.Rich
|
||||
|
||||
// Asserters.
|
||||
|
||||
AsIconer() Iconer // Optional
|
||||
// Name sets the given container to contain the name of the parent context. The
|
||||
// method has no stop method; stopping is implied to be dependent on the parent
|
||||
// context. As such, it's only used for updating.
|
||||
Name(context.Context, LabelContainer) (stop func(), err error)
|
||||
}
|
||||
|
||||
// Nicknamer adds the current user's nickname.
|
||||
|
@ -559,7 +591,7 @@ type Namer interface {
|
|||
// implement ServerMessage also don't need to implement ServerNickname. By
|
||||
// default, the session name should be used.
|
||||
type Nicknamer interface {
|
||||
Nickname(context.Context, LabelContainer) (stop func(), err error)
|
||||
Namer
|
||||
}
|
||||
|
||||
// Noncer adds nonce support. A nonce is defined in this context as a unique
|
||||
|
@ -578,6 +610,38 @@ type Noncer interface {
|
|||
Nonce() string
|
||||
}
|
||||
|
||||
// ReadContainer is an interface that a frontend container can implement to show
|
||||
// the read bubbles on messages. This container typically implies the message
|
||||
// container, but that is up to the frontend's implementation.
|
||||
type ReadContainer interface {
|
||||
// DeleteIndications deletes a list of unused users/authors associated with
|
||||
// their read indicators. The backend can use this to free up users/authors that
|
||||
// are no longer in the server, for example when they are offline or have left
|
||||
// the server.
|
||||
DeleteIndications(ctx context.Context, authorIDs []ID)
|
||||
// AddIndications adds a map of users/authors to the respective message ID of
|
||||
// the server that implements ReadIndicator.
|
||||
AddIndications(context.Context, []ReadIndication)
|
||||
}
|
||||
|
||||
// ReadIndicator adds a read indicator API for frontends to show. An example of
|
||||
// the read indicator is in Matrix, where each message can have a small avatar
|
||||
// indicating that the user in the room has read the message.
|
||||
type ReadIndicator interface {
|
||||
// ReadIndicate subscribes the given container for read activities. The backend
|
||||
// must keep track of which read states to send over to not overwhelm the
|
||||
// frontend, and the frontend must either keep track of them, or it should not
|
||||
// display it at all.
|
||||
ReadIndicate(context.Context, ReadContainer) (stop func(), err error)
|
||||
}
|
||||
|
||||
// Replier indicates that the message being sent is a reply to something.
|
||||
// Frontends that support replies can assume that all messages in a Sender can
|
||||
// be replied to, and the backend can choose to do nothing to the replied ID.
|
||||
type Replier interface {
|
||||
ReplyingTo() ID
|
||||
}
|
||||
|
||||
// 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.
|
||||
|
@ -591,8 +655,9 @@ type SendableMessage interface {
|
|||
|
||||
// Asserters.
|
||||
|
||||
AsNoncer() Noncer // Optional
|
||||
AsAttachments() Attachments // Optional
|
||||
AsNoncer() Noncer // Optional
|
||||
AsReplier() Replier // Optional
|
||||
AsAttacher() Attacher // Optional
|
||||
}
|
||||
|
||||
// Sender adds message sending to a messenger. Messengers that don't implement
|
||||
|
@ -601,7 +666,7 @@ type Sender interface {
|
|||
// CanAttach returns whether or not the client is allowed to upload files.
|
||||
CanAttach() bool
|
||||
// Send is called by the frontend to send a message to this channel.
|
||||
Send(SendableMessage) error // Blocking
|
||||
Send(context.Context, SendableMessage) error // Blocking
|
||||
|
||||
// Asserters.
|
||||
|
||||
|
@ -611,6 +676,9 @@ type Sender interface {
|
|||
// 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.
|
||||
//
|
||||
// Note that the Server is allowed to implement both Lister and Messenger. This
|
||||
// is useful when the messenger contains sub-servers, such as threads.
|
||||
type Server interface {
|
||||
Identifier
|
||||
Namer
|
||||
|
@ -623,11 +691,26 @@ type Server interface {
|
|||
AsConfigurator() Configurator // Optional
|
||||
}
|
||||
|
||||
// ServerUpdate represents a server update event.
|
||||
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
|
||||
// PreviousID returns the ID of the item, either to be replaced or to be
|
||||
// inserted in front of.
|
||||
//
|
||||
// If replace is true, then the returned ID is the ID of the item to be
|
||||
// replaced, and the frontend should only try to use the ID as-is to find the
|
||||
// old server and replace.
|
||||
//
|
||||
// If replace is false, then the returned ID will be the ID of the item in front
|
||||
// of the embedded server. If the ID is empty or the frontend cannot find the
|
||||
// server from this ID, then it should assume and prepend the server to the
|
||||
// start.
|
||||
PreviousID() (serverID ID, replace bool)
|
||||
}
|
||||
|
||||
// ServersContainer is any type of view that displays the list of servers. It
|
||||
|
@ -638,16 +721,22 @@ type ServerUpdate interface {
|
|||
// as servers can be infinitely nested. Frontends should also reset the entire
|
||||
// node and its children when SetServers is called again.
|
||||
type ServersContainer interface {
|
||||
UpdateServer(ServerUpdate)
|
||||
UpdateServer(context.Context, ServerUpdate)
|
||||
// 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)
|
||||
//
|
||||
// If the backend sets a nil server slice, then the frontend should take that as
|
||||
// an unavailable server list rather than an empty server list. The server list
|
||||
// should only be considered empty if it's an empty non-nil slice. An
|
||||
// unavailable list, on the other hand, can be treated as backend issues, e.g. a
|
||||
// connection issue.
|
||||
SetServers(context.Context, []Server)
|
||||
}
|
||||
|
||||
// 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.
|
||||
// Service is a complete service that's capable of multiple sessions. It has to
|
||||
// implement the Authenticate() method, which returns multiple implementations
|
||||
// of Authenticator.
|
||||
//
|
||||
// A service can implement SessionRestorer, which would indicate the frontend
|
||||
// that it can restore past sessions. Sessions are saved using the SessionSaver
|
||||
|
@ -659,9 +748,17 @@ type ServersContainer interface {
|
|||
// configurations must be optional, as frontends may not implement a
|
||||
// configurator UI.
|
||||
type Service interface {
|
||||
// Identifier returns the unique identifier for the service. There is no
|
||||
// enforced representation, but services are recommended to follow the Reverse
|
||||
// Domain Name Notation for consistency. An example of that would be:
|
||||
//
|
||||
// com.github.diamondburned.cchat-discord
|
||||
// com.github.username.service
|
||||
Identifier
|
||||
// Namer returns the name of the service.
|
||||
Namer
|
||||
|
||||
Authenticate() Authenticator
|
||||
Authenticate() []Authenticator
|
||||
|
||||
// Asserters.
|
||||
|
||||
|
@ -669,17 +766,19 @@ type Service interface {
|
|||
AsSessionRestorer() SessionRestorer // Optional
|
||||
}
|
||||
|
||||
// A session is returned after authentication on the service. Session implements
|
||||
// Session is returned after authentication on the service. It 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.
|
||||
// ID(), which might be used by frontends to check against User.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
|
||||
// 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
|
||||
|
||||
|
@ -695,7 +794,7 @@ type Session interface {
|
|||
// 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 // Blocking
|
||||
Disconnect(context.Context) error // Blocking, Disposer
|
||||
|
||||
// Asserters.
|
||||
|
||||
|
@ -709,7 +808,7 @@ type Session interface {
|
|||
//
|
||||
// To save a session, refer to SessionSaver.
|
||||
type SessionRestorer interface {
|
||||
RestoreSession(map[string]string) (Session, error) // Blocking
|
||||
RestoreSession(context.Context, map[string]string) (Session, error) // Blocking
|
||||
}
|
||||
|
||||
// SessionSaver extends Session and is called by the frontend to save the
|
||||
|
@ -725,15 +824,6 @@ type SessionSaver interface {
|
|||
SaveSession() map[string]string
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// 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
|
||||
|
@ -745,10 +835,11 @@ type TypingContainer interface {
|
|||
// 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)
|
||||
// AddTyper appends the typer into the frontend's list of typers, or it pushes
|
||||
// this typer on top of others.
|
||||
AddTyper(typer Typer)
|
||||
RemoveTyper(ctx context.Context, authorID ID)
|
||||
// AddTyper appends the typer (author) into the frontend's list of typers, or it
|
||||
// pushes this typer on top of others. The frontend should assume current time
|
||||
// every time AddTyper is called.
|
||||
AddTyper(context.Context, User)
|
||||
}
|
||||
|
||||
// TypingIndicator optionally extends ServerMessage to provide bidirectional
|
||||
|
@ -766,7 +857,7 @@ type TypingIndicator interface {
|
|||
// 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)
|
||||
TypingSubscribe(context.Context, TypingContainer) (stop func(), err 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.
|
||||
|
@ -775,17 +866,16 @@ type TypingIndicator interface {
|
|||
// 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 // Blocking
|
||||
Typing(context.Context) error // Blocking
|
||||
}
|
||||
|
||||
// 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 that implement this has to represent unread and mentioned
|
||||
// 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
|
||||
|
@ -795,10 +885,13 @@ type TypingIndicator interface {
|
|||
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)
|
||||
SetUnread(ctx context.Context, unread bool, mentioned bool)
|
||||
}
|
||||
|
||||
// UnreadIndicator adds an unread state API for frontends to use.
|
||||
// UnreadIndicator adds an unread state API for frontends to use. The unread
|
||||
// state describes whether a channel has been read or not by the current user.
|
||||
// It is not to be confused with ReadIndicator, which indicates the unread state
|
||||
// of others.
|
||||
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
|
||||
|
@ -806,5 +899,28 @@ type UnreadIndicator interface {
|
|||
//
|
||||
// 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)
|
||||
UnreadIndicate(context.Context, UnreadContainer) (stop func(), err error)
|
||||
// MarkRead marks a message in the server messenger as read. Backends that
|
||||
// implement the UnreadIndicator interface must give control of marking messages
|
||||
// as read to the frontend if possible.
|
||||
//
|
||||
// This method is assumed to be a setter method that does not error out, because
|
||||
// the frontend has no use in knowing the error. As such, marking messages as
|
||||
// read is best-effort. The backend is in charge of synchronizing the read state
|
||||
// with the server and coordinating it with reasonable rate limits, if needed.
|
||||
MarkRead(ctx context.Context, messageID ID)
|
||||
}
|
||||
|
||||
// User 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 User
|
||||
// method or embedded in Typer. The returned ID may or may not be used by the
|
||||
// frontend, but backends must guarantee that the User's ID is in fact a user
|
||||
// ID.
|
||||
//
|
||||
// The frontend may use the ID to squash messages with the same author together.
|
||||
type User interface {
|
||||
Identifier
|
||||
Namer
|
||||
}
|
||||
|
|
|
@ -5,6 +5,8 @@ import (
|
|||
"log"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/dave/jennifer/jen"
|
||||
|
@ -23,39 +25,57 @@ var comment = repository.Comment{Raw: `
|
|||
in cchat's root and text packages.
|
||||
`}
|
||||
|
||||
type Package struct {
|
||||
Path string
|
||||
repository.Package
|
||||
}
|
||||
|
||||
func main() {
|
||||
gen := jen.NewFile("empty")
|
||||
gen.HeaderComment("DO NOT EDIT: THIS FILE IS GENERATED!")
|
||||
gen := genutils.NewFile("empty")
|
||||
gen.PackageComment(comment.GoString(1))
|
||||
|
||||
for pkgpath, pk := range repository.Main {
|
||||
gen.ImportName(pkgpath, path.Base(pkgpath))
|
||||
// Sort.
|
||||
var packages = make([]Package, 0, len(repository.Main))
|
||||
|
||||
for _, iface := range pk.Interfaces {
|
||||
for pkgpath, pk := range repository.Main {
|
||||
packages = append(packages, Package{
|
||||
Path: pkgpath,
|
||||
Package: pk,
|
||||
})
|
||||
}
|
||||
|
||||
sort.Slice(packages, func(i, j int) bool {
|
||||
return packages[i].Path < packages[j].Path
|
||||
})
|
||||
|
||||
for _, pkg := range packages {
|
||||
gen.ImportName(pkg.Path, path.Base(pkg.Path))
|
||||
|
||||
for _, iface := range pkg.Interfaces {
|
||||
// Skip structs without asserter methods.
|
||||
if !hasAsserter(iface) {
|
||||
continue
|
||||
}
|
||||
|
||||
var ifaceName = newIfaceName(pkgpath, iface)
|
||||
var ifaceName = newIfaceName(pkg.Path, iface)
|
||||
|
||||
gen.Commentf("%[1]s provides no-op asserters for cchat.%[1]s.", ifaceName)
|
||||
gen.Type().Id(ifaceName).Struct()
|
||||
gen.Line()
|
||||
|
||||
for _, embed := range iface.Embeds {
|
||||
if iface := pk.Interface(embed.InterfaceName); iface != nil {
|
||||
genIfaceMethods(gen, *iface, ifaceName, pkgpath)
|
||||
if iface := pkg.Interface(embed.InterfaceName); iface != nil {
|
||||
genIfaceMethods(gen, *iface, ifaceName, pkg.Path)
|
||||
}
|
||||
}
|
||||
|
||||
genIfaceMethods(gen, iface, ifaceName, pkgpath)
|
||||
genIfaceMethods(gen, iface, ifaceName, pkg.Path)
|
||||
|
||||
gen.Line()
|
||||
}
|
||||
}
|
||||
|
||||
f, err := os.Create("empty.go")
|
||||
f, err := os.Create(filepath.Join(os.Args[1], "empty.go"))
|
||||
if err != nil {
|
||||
log.Fatalln("Failed to create output file:", err)
|
||||
}
|
||||
|
|
|
@ -35,8 +35,16 @@ func generateEnums(enums []repository.Enumeration) jen.Code {
|
|||
|
||||
c.Id(enum.Name + value.Name)
|
||||
|
||||
if i == 0 {
|
||||
switch {
|
||||
// Regular.
|
||||
case i == 0 && !enum.Bitwise:
|
||||
c.Id(enum.Name).Op("=").Iota()
|
||||
|
||||
// Bitwise.
|
||||
case i == 0 && enum.Bitwise:
|
||||
c.Id(enum.Name).Op("=").Lit(0)
|
||||
case i == 1 && enum.Bitwise:
|
||||
c.Id(enum.Name).Op("=").Lit(1).Op("<<").Iota()
|
||||
}
|
||||
|
||||
group.Add(&c)
|
||||
|
@ -49,15 +57,6 @@ func generateEnums(enums []repository.Enumeration) jen.Code {
|
|||
var recv = genutils.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")
|
||||
|
@ -66,6 +65,15 @@ func generateEnums(enums []repository.Enumeration) jen.Code {
|
|||
fn.BlockFunc(func(g *jen.Group) {
|
||||
g.Return(jen.Id(recv).Op("&").Id("has").Op("==").Id("has"))
|
||||
})
|
||||
} else {
|
||||
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"))
|
||||
})
|
||||
}
|
||||
|
||||
stmt.Line()
|
||||
|
|
|
@ -24,6 +24,9 @@ func generateInterfaces(ifaces []repository.Interface) jen.Code {
|
|||
stmt.Type().Id(iface.Name).InterfaceFunc(func(group *jen.Group) {
|
||||
if len(iface.Embeds) > 0 {
|
||||
for _, embed := range iface.Embeds {
|
||||
if !embed.Comment.IsEmpty() {
|
||||
group.Comment(embed.Comment.GoString(1))
|
||||
}
|
||||
group.Id(embed.InterfaceName)
|
||||
}
|
||||
|
||||
|
@ -59,14 +62,22 @@ func generateInterfaces(ifaces []repository.Interface) jen.Code {
|
|||
|
||||
switch method := method.(type) {
|
||||
case repository.GetterMethod:
|
||||
stmt.Params(generateFuncParams(method.Parameters, false)...)
|
||||
stmt.Params(generateFuncParams(method.Returns, method.ReturnError)...)
|
||||
stmt.Params(generateFuncParams(method.Parameters, "")...)
|
||||
stmt.Params(generateFuncParams(method.Returns, method.ErrorType)...)
|
||||
case repository.SetterMethod:
|
||||
stmt.Params(generateFuncParams(method.Parameters, false)...)
|
||||
stmt.Params(generateFuncParams(method.Parameters, "")...)
|
||||
stmt.Params(generateFuncParamsErr(repository.NamedType{}, method.ErrorType)...)
|
||||
case repository.ContainerUpdaterMethod:
|
||||
stmt.Params(generateFuncParamsCtx(method.Parameters, "")...)
|
||||
stmt.Params(generateFuncParamsErr(repository.NamedType{}, method.ErrorType)...)
|
||||
case repository.IOMethod:
|
||||
stmt.Params(generateFuncParams(method.Parameters, false)...)
|
||||
stmt.Params(generateFuncParamErr(method.ReturnValue, method.ReturnError)...)
|
||||
stmt.Comment("// Blocking")
|
||||
stmt.Params(generateFuncParamsCtx(method.Parameters, "")...)
|
||||
stmt.Params(generateFuncParamsErr(method.ReturnValue, method.ErrorType)...)
|
||||
var comment = "Blocking"
|
||||
if method.Disposer {
|
||||
comment += ", Disposer"
|
||||
}
|
||||
stmt.Comment("// " + comment)
|
||||
case repository.ContainerMethod:
|
||||
stmt.Params(generateContainerFuncParams(method)...)
|
||||
stmt.Params(generateContainerFuncReturns(method)...)
|
||||
|
@ -89,18 +100,18 @@ func generateInterfaces(ifaces []repository.Interface) jen.Code {
|
|||
return stmt
|
||||
}
|
||||
|
||||
func generateFuncParamErr(param repository.NamedType, genErr bool) []jen.Code {
|
||||
func generateFuncParamsErr(param repository.NamedType, errorType string) []jen.Code {
|
||||
stmt := make([]jen.Code, 0, 2)
|
||||
|
||||
if !param.IsZero() {
|
||||
stmt = append(stmt, generateFuncParam(param))
|
||||
}
|
||||
|
||||
if genErr {
|
||||
if errorType != "" {
|
||||
if param.Name == "" {
|
||||
stmt = append(stmt, jen.Error())
|
||||
stmt = append(stmt, jen.Id(errorType))
|
||||
} else {
|
||||
stmt = append(stmt, jen.Err().Error())
|
||||
stmt = append(stmt, jen.Err().Id(errorType))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -114,7 +125,19 @@ func generateFuncParam(param repository.NamedType) jen.Code {
|
|||
return jen.Id(param.Name).Add(genutils.GenerateType(param))
|
||||
}
|
||||
|
||||
func generateFuncParams(params []repository.NamedType, withError bool) []jen.Code {
|
||||
func generateFuncParamsCtx(params []repository.NamedType, errorType string) []jen.Code {
|
||||
var name string
|
||||
if len(params) > 0 && params[0].Name != "" {
|
||||
name = "ctx"
|
||||
}
|
||||
|
||||
p := []repository.NamedType{{Name: name, Type: "context.Context"}}
|
||||
p = append(p, params...)
|
||||
|
||||
return generateFuncParams(p, errorType)
|
||||
}
|
||||
|
||||
func generateFuncParams(params []repository.NamedType, errorType string) []jen.Code {
|
||||
if len(params) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
@ -124,11 +147,11 @@ func generateFuncParams(params []repository.NamedType, withError bool) []jen.Cod
|
|||
stmt.Add(generateFuncParam(param))
|
||||
}
|
||||
|
||||
if withError {
|
||||
if errorType != "" {
|
||||
if params[0].Name != "" {
|
||||
stmt.Add(jen.Err().Error())
|
||||
stmt.Add(jen.Err().Id(errorType))
|
||||
} else {
|
||||
stmt.Add(jen.Error())
|
||||
stmt.Add(jen.Id(errorType))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -138,9 +161,7 @@ func generateFuncParams(params []repository.NamedType, withError bool) []jen.Cod
|
|||
func generateContainerFuncReturns(method repository.ContainerMethod) []jen.Code {
|
||||
var stmt jen.Statement
|
||||
|
||||
if method.HasStopFn {
|
||||
stmt.Add(jen.Id("stop").Func().Params())
|
||||
}
|
||||
stmt.Add(jen.Id("stop").Func().Params())
|
||||
stmt.Add(jen.Err().Error())
|
||||
|
||||
return stmt
|
||||
|
|
|
@ -41,15 +41,7 @@ func generateErrorStructs(errStructs []repository.ErrorStruct) jen.Code {
|
|||
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.Block(jen.Return(generateTmplString(errStruct.ErrorString, recv)))
|
||||
|
||||
stmt.Line()
|
||||
stmt.Line()
|
||||
|
@ -58,9 +50,7 @@ func generateErrorStructs(errStructs []repository.ErrorStruct) jen.Code {
|
|||
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.Block(jen.Return(jen.Id(recv).Dot(wrap)))
|
||||
stmt.Line()
|
||||
stmt.Line()
|
||||
}
|
||||
|
@ -87,5 +77,39 @@ func generateStruct(s repository.Struct) jen.Code {
|
|||
}
|
||||
})
|
||||
|
||||
if !s.Stringer.IsEmpty() {
|
||||
stmt.Line()
|
||||
stmt.Line()
|
||||
|
||||
var recv = genutils.RecvName(s.Name)
|
||||
|
||||
if !s.Stringer.Comment.IsEmpty() {
|
||||
stmt.Comment(s.Stringer.Comment.GoString(1))
|
||||
stmt.Line()
|
||||
}
|
||||
|
||||
stmt.Func()
|
||||
stmt.Params(jen.Id(recv).Id(s.Name))
|
||||
stmt.Id("String").Params().String()
|
||||
stmt.BlockFunc(func(g *jen.Group) {
|
||||
if s.Stringer.Format == "%s" {
|
||||
g.Return(jen.Id(recv).Dot(s.Stringer.Fields[0]))
|
||||
return
|
||||
}
|
||||
|
||||
g.Return(generateTmplString(s.Stringer.TmplString, recv))
|
||||
})
|
||||
}
|
||||
|
||||
return stmt
|
||||
}
|
||||
|
||||
func generateTmplString(tmpl repository.TmplString, recv string) jen.Code {
|
||||
return jen.Qual("fmt", "Sprintf").CallFunc(func(g *jen.Group) {
|
||||
g.Lit(tmpl.Format)
|
||||
|
||||
for _, field := range tmpl.Fields {
|
||||
g.Add(jen.Id(recv).Dot(field))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
@ -6,6 +6,20 @@ import (
|
|||
"github.com/dave/jennifer/jen"
|
||||
)
|
||||
|
||||
// NewFile creates a new file with the appropriate header comments.
|
||||
func NewFile(pkgname string) *jen.File {
|
||||
gen := jen.NewFile(pkgname)
|
||||
gen.HeaderComment("Code generated by ./cmd/internal. DO NOT EDIT.")
|
||||
return gen
|
||||
}
|
||||
|
||||
// NewFilePath creates a new file with the appropriate header comments.
|
||||
func NewFilePath(pkgpath string) *jen.File {
|
||||
gen := jen.NewFilePath(pkgpath)
|
||||
gen.HeaderComment("Code generated by ./cmd/internal. DO NOT EDIT.")
|
||||
return gen
|
||||
}
|
||||
|
||||
type Qualer interface {
|
||||
Qual() (path, name string)
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@ import (
|
|||
"strings"
|
||||
|
||||
"github.com/dave/jennifer/jen"
|
||||
"github.com/diamondburned/cchat/cmd/internal/cchat-generator/genutils"
|
||||
"github.com/diamondburned/cchat/repository"
|
||||
)
|
||||
|
||||
|
@ -20,8 +21,11 @@ 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)
|
||||
destDir := filepath.Join(
|
||||
os.Args[1],
|
||||
filepath.FromSlash(trimPrefix(repository.RootPath, pkgPath)),
|
||||
)
|
||||
destFile := filepath.Base(pkgPath) + ".go"
|
||||
|
||||
// Guarantee that the directory exists.
|
||||
if destDir != "" {
|
||||
|
@ -30,7 +34,7 @@ func main() {
|
|||
}
|
||||
}
|
||||
|
||||
f, err := os.Create(filepath.Join(destDir, destFle+".go"))
|
||||
f, err := os.Create(filepath.Join(destDir, destFile))
|
||||
if err != nil {
|
||||
log.Fatalln("Failed to create output file:", err)
|
||||
}
|
||||
|
@ -48,8 +52,7 @@ func trimPrefix(rootPrefix, path string) string {
|
|||
}
|
||||
|
||||
func generate(pkgPath string, repo repository.Package) *jen.File {
|
||||
gen := jen.NewFilePath(pkgPath)
|
||||
gen.HeaderComment("DO NOT EDIT: THIS FILE IS GENERATED!")
|
||||
gen := genutils.NewFilePath(pkgPath)
|
||||
gen.PackageComment(repo.Comment.GoString(1))
|
||||
gen.Add(generateTypeAlises(repo.TypeAliases))
|
||||
gen.Line()
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"encoding/gob"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"github.com/diamondburned/cchat/repository"
|
||||
)
|
||||
|
||||
const output = "repository.gob"
|
||||
|
||||
func main() {
|
||||
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(output)
|
||||
log.Fatalln("Failed to gob encode:", err)
|
||||
}
|
||||
}
|
17
generator.go
17
generator.go
|
@ -1,3 +1,18 @@
|
|||
package cchat
|
||||
|
||||
//go:generate go run ./cmd/internal/cchat-generator
|
||||
//go:generate go run ./cmd/internal/cchat-generator ./
|
||||
//go:generate go run ./cmd/internal/cchat-empty-gen ./utils/empty/
|
||||
|
||||
type authenticateError struct{ error }
|
||||
|
||||
func (authenticateError) NextStage() []Authenticator { return nil }
|
||||
|
||||
// WrapAuthenticateError wraps the given error to become an AuthenticateError.
|
||||
// Its NextStage method returns nil. If the given err is nil, then nil is
|
||||
// returned.
|
||||
func WrapAuthenticateError(err error) AuthenticateError {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
return authenticateError{err}
|
||||
}
|
||||
|
|
|
@ -31,6 +31,10 @@ func (c Comment) IsEmpty() bool {
|
|||
// 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 c.Raw == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
if ident < 1 {
|
||||
ident = 1
|
||||
}
|
||||
|
@ -40,7 +44,12 @@ func (c Comment) GoString(ident int) string {
|
|||
|
||||
var lines = strings.Split(c.WrapText(80-len("// ")-ident), "\n")
|
||||
for i, line := range lines {
|
||||
lines[i] = "// " + line
|
||||
if line != "" {
|
||||
line = "// " + line
|
||||
} else {
|
||||
line = "//"
|
||||
}
|
||||
lines[i] = line
|
||||
}
|
||||
|
||||
return strings.Join(lines, "\n")
|
||||
|
@ -56,7 +65,10 @@ func (c Comment) WrapText(column int) string {
|
|||
buf := bytes.Buffer{}
|
||||
doc.ToText(&buf, txt, "", strings.Repeat(" ", TabWidth-1), column)
|
||||
|
||||
return strings.TrimRight(buf.String(), "\n")
|
||||
text := strings.TrimRight(buf.String(), "\n")
|
||||
text = strings.Replace(text, "\t", strings.Repeat(" ", TabWidth), -1)
|
||||
|
||||
return text
|
||||
}
|
||||
|
||||
// Unindent removes the indentations that were there for the sake of syntax in
|
||||
|
|
|
@ -6,36 +6,22 @@ import (
|
|||
"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
|
||||
}`
|
||||
const _goComment = `
|
||||
// The authenticator interface allows for a multistage initial authentication
|
||||
// API that the backend could use. Multistage is done by calling Authenticate
|
||||
// and check for AuthenticateError's NextStage method.`
|
||||
|
||||
// Trim away the prefix new line.
|
||||
var comment = _comment[1:]
|
||||
var goComment = _goComment[1:]
|
||||
|
||||
func TestComment(t *testing.T) {
|
||||
var authenticator = Main["cchat"].Interface("Authenticator")
|
||||
var authDoc = authenticator.Comment.GoString()
|
||||
var authenticator = Main[RootPath].Interface("Authenticator")
|
||||
|
||||
if eq := deep.Equal(comment, authDoc); eq != nil {
|
||||
t.Fatal("Comment inequality:", eq)
|
||||
}
|
||||
t.Run("godoc", func(t *testing.T) {
|
||||
godoc := authenticator.Comment.GoString(0)
|
||||
|
||||
if eq := deep.Equal(goComment, godoc); eq != nil {
|
||||
t.Fatal("go comment inequality:", eq)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
@ -1,10 +1,15 @@
|
|||
package repository
|
||||
|
||||
// Enumeration returns a Go enumeration.
|
||||
type Enumeration struct {
|
||||
Comment Comment
|
||||
Name string
|
||||
Values []EnumValue
|
||||
Bitwise bool // uint32 if true, uint8 otherwise
|
||||
|
||||
// Bitwise is true if the enumeration is a bitwise one. The type would then
|
||||
// be uint32 instead of uint8, allowing for 32 constants. As usual, the
|
||||
// first value of enum must be 0.
|
||||
Bitwise bool
|
||||
}
|
||||
|
||||
// GoType returns uint8 for a normal enum and uint32 for a bitwise enum. It
|
||||
|
|
|
@ -1,28 +1,3 @@
|
|||
// +build ignore
|
||||
package gob
|
||||
|
||||
package main
|
||||
|
||||
//go:generate go run ./generator.go
|
||||
|
||||
import (
|
||||
"encoding/gob"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"github.com/diamondburned/cchat/repository"
|
||||
)
|
||||
|
||||
const output = "repository.gob"
|
||||
|
||||
func main() {
|
||||
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(output)
|
||||
log.Fatalln("Failed to gob encode:", err)
|
||||
}
|
||||
}
|
||||
//go:generate go run ../../cmd/internal/cchat-gob-gen
|
||||
|
|
Binary file not shown.
|
@ -10,6 +10,7 @@ func init() {
|
|||
gob.Register(AsserterMethod{})
|
||||
gob.Register(GetterMethod{})
|
||||
gob.Register(SetterMethod{})
|
||||
gob.Register(ContainerUpdaterMethod{})
|
||||
gob.Register(IOMethod{})
|
||||
}
|
||||
|
||||
|
@ -55,24 +56,51 @@ type GetterMethod struct {
|
|||
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
|
||||
// ErrorType is non-empty if the function returns an error at the end of
|
||||
// returns. For the most part, this field should be "error" if that is the
|
||||
// case, but some methods may choose to extend the error base type.
|
||||
ErrorType string
|
||||
}
|
||||
|
||||
// ReturnError returns true if the method can error out.
|
||||
func (m GetterMethod) ReturnError() bool {
|
||||
return m.ErrorType != ""
|
||||
}
|
||||
|
||||
// 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.
|
||||
// they have to be non-blocking.
|
||||
type SetterMethod struct {
|
||||
method
|
||||
|
||||
// Parameters is the list of parameters in the function. These parameters
|
||||
// should be the parameters to set.
|
||||
Parameters []NamedType
|
||||
// ErrorType is non-empty if the function returns an error at the end of
|
||||
// returns. An error may be returned from the backend if the input is
|
||||
// invalid, but it must not do IO. Frontend setters must never error.
|
||||
ErrorType string
|
||||
}
|
||||
|
||||
// ContainerUpdaterMethod is a SetterMethod that passes to the container the
|
||||
// current context to prevent race conditions when synchronizing.
|
||||
// The rule of thumb is that any setter method done inside a method with a
|
||||
// context is usually this type of method.
|
||||
type ContainerUpdaterMethod struct {
|
||||
method
|
||||
|
||||
// Parameters is the list of parameters in the function. These parameters
|
||||
// should be the parameters to set.
|
||||
Parameters []NamedType
|
||||
// ErrorType is non-empty if the function returns an error at the end of
|
||||
// returns. An error may be returned from the backend if the input is
|
||||
// invalid, but it must not do IO. Frontend setters must never error.
|
||||
ErrorType string
|
||||
}
|
||||
|
||||
// IOMethod is a regular method that can do IO and thus is blocking. These
|
||||
// methods usually always return errors.
|
||||
// methods usually always return errors. IOMethods must always have means of
|
||||
// cancelling them in the API, but implementations don't have to use it; as
|
||||
// such, the user should always have a timeout to gracefully wait.
|
||||
type IOMethod struct {
|
||||
method
|
||||
|
||||
|
@ -80,12 +108,30 @@ type IOMethod struct {
|
|||
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
|
||||
// ErrorType is non-empty if the function returns an error at the end of
|
||||
// returns. For the most part, this field should be "error" if that is the
|
||||
// case, but some methods may choose to extend the error base type.
|
||||
ErrorType string
|
||||
// Disposer indicates that this method signals the disposal of the interface
|
||||
// that implements it. This is used similarly to stop functions, except all
|
||||
// disposer functions can be synchronous, and the frontend should handle
|
||||
// indicating such. The frontend can also ignore the result and run the
|
||||
// method in a dangling goroutine, but it must gracefully wait for it to be
|
||||
// done on exit.
|
||||
//
|
||||
// Similarly to the stop function, the instance that the disposer method belongs
|
||||
// to will also be considered invalid and should be freed once the function
|
||||
// returns regardless of the error.
|
||||
Disposer bool
|
||||
}
|
||||
|
||||
// ContainerMethod is a method that uses a Container. These methods can do IO.
|
||||
// ReturnError returns true if the method can error out.
|
||||
func (m IOMethod) ReturnError() bool {
|
||||
return m.ErrorType != ""
|
||||
}
|
||||
|
||||
// ContainerMethod is a method that uses a Container. These methods can do IO
|
||||
// and always return a stop callback and an error.
|
||||
type ContainerMethod struct {
|
||||
method
|
||||
|
||||
|
@ -94,10 +140,6 @@ type ContainerMethod struct {
|
|||
// 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.
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -17,7 +17,7 @@ func TestGob(t *testing.T) {
|
|||
|
||||
t.Log("Marshaled; total bytes:", buf.Len())
|
||||
|
||||
var unmarshaled Repositories
|
||||
var unmarshaled Packages
|
||||
|
||||
if err := gob.NewDecoder(&buf).Decode(&unmarshaled); err != nil {
|
||||
t.Fatal("Failed to gob decode:", err)
|
||||
|
|
|
@ -37,14 +37,68 @@ type Package struct {
|
|||
|
||||
// Interface finds an interface. Nil is returned if none is found.
|
||||
func (p Package) Interface(name string) *Interface {
|
||||
for _, iface := range p.Interfaces {
|
||||
for i, iface := range p.Interfaces {
|
||||
if iface.Name == name {
|
||||
return &iface
|
||||
return &p.Interfaces[i]
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Struct finds a struct or an error struct. Nil is returned if none is found.
|
||||
func (p Package) Struct(name string) *Struct {
|
||||
for i, sstruct := range p.Structs {
|
||||
if sstruct.Name == name {
|
||||
return &p.Structs[i]
|
||||
}
|
||||
}
|
||||
for i, estruct := range p.ErrorStructs {
|
||||
if estruct.Name == name {
|
||||
return &p.ErrorStructs[i].Struct
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Enum finds an enumeration. Nil is returned if none is found.
|
||||
func (p Package) Enum(name string) *Enumeration {
|
||||
for i, enum := range p.Enums {
|
||||
if enum.Name == name {
|
||||
return &p.Enums[i]
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// TypeAlias finds a type alias. Nil is returned if none is found.
|
||||
func (p Package) TypeAlias(name string) *TypeAlias {
|
||||
for i, alias := range p.TypeAliases {
|
||||
if alias.Name == name {
|
||||
return &p.TypeAliases[i]
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// FindType finds any type. Nil is returned if nothing is found; a pointer to
|
||||
// any of the type is returned if name is found.
|
||||
func (p Package) FindType(name string) interface{} {
|
||||
if iface := p.Interface(name); iface != nil {
|
||||
return iface
|
||||
}
|
||||
if sstr := p.Struct(name); sstr != nil {
|
||||
return sstr
|
||||
}
|
||||
if enum := p.Enum(name); enum != nil {
|
||||
return enum
|
||||
}
|
||||
if alias := p.TypeAlias(name); alias != nil {
|
||||
return alias
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// NamedType is an optionally named value with a type.
|
||||
type NamedType struct {
|
||||
Name string // optional
|
||||
Type string // import/path.Type OR (import/path).Type
|
||||
|
@ -97,3 +151,8 @@ type TmplString struct {
|
|||
Format string // printf format syntax
|
||||
Fields []string // list of struct fields
|
||||
}
|
||||
|
||||
// IsEmpty returns true if TmplString is zero.
|
||||
func (s TmplString) IsEmpty() bool {
|
||||
return s.Format == ""
|
||||
}
|
||||
|
|
|
@ -0,0 +1,34 @@
|
|||
package repository
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestTypeQual(t *testing.T) {
|
||||
type test struct {
|
||||
typePath string
|
||||
path string
|
||||
typ string
|
||||
}
|
||||
|
||||
var tests = []test{
|
||||
{"string", "", "string"},
|
||||
{"context.Context", "context", "Context"},
|
||||
{
|
||||
"github.com/diamondburned/cchat/text.Rich",
|
||||
"github.com/diamondburned/cchat/text", "Rich",
|
||||
},
|
||||
{
|
||||
"(github.com/diamondburned/cchat/text).Rich",
|
||||
"github.com/diamondburned/cchat/text", "Rich",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
path, typ := TypeQual(test.typePath)
|
||||
if path != test.path {
|
||||
t.Errorf("Unexpected path %q != %q", path, test.path)
|
||||
}
|
||||
if typ != test.typ {
|
||||
t.Errorf("Unexpected type %q != %q", typ, test.typ)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,9 +1,10 @@
|
|||
package repository
|
||||
|
||||
type Struct struct {
|
||||
Comment Comment
|
||||
Name string
|
||||
Fields []StructField
|
||||
Comment Comment
|
||||
Name string
|
||||
Fields []StructField
|
||||
Stringer Stringer // used for String()
|
||||
}
|
||||
|
||||
type StructField struct {
|
||||
|
@ -11,6 +12,13 @@ type StructField struct {
|
|||
NamedType
|
||||
}
|
||||
|
||||
type Stringer struct {
|
||||
Comment
|
||||
TmplString
|
||||
}
|
||||
|
||||
func (s Stringer) IsEmpty() bool { return s.TmplString.IsEmpty() }
|
||||
|
||||
// ErrorStruct are structs that implement the "error" interface and starts with
|
||||
// "Err".
|
||||
type ErrorStruct struct {
|
||||
|
|
55
text/text.go
55
text/text.go
|
@ -1,4 +1,4 @@
|
|||
// DO NOT EDIT: THIS FILE IS GENERATED!
|
||||
// Code generated by ./cmd/internal. DO NOT EDIT.
|
||||
|
||||
// Package text provides a rich text API for cchat interfaces to use.
|
||||
//
|
||||
|
@ -16,9 +16,9 @@ type Attribute uint32
|
|||
|
||||
const (
|
||||
// Normal is a zero-value attribute.
|
||||
AttributeNormal Attribute = iota
|
||||
AttributeNormal Attribute = 0
|
||||
// Bold represents bold text.
|
||||
AttributeBold
|
||||
AttributeBold Attribute = 1 << iota
|
||||
// Italics represents italicized text.
|
||||
AttributeItalics
|
||||
// Underline represents underlined text.
|
||||
|
@ -35,8 +35,8 @@ const (
|
|||
AttributeDimmed
|
||||
)
|
||||
|
||||
func (a Attribute) Is(is Attribute) bool {
|
||||
return a == is
|
||||
func (a Attribute) Has(has Attribute) bool {
|
||||
return a&has == has
|
||||
}
|
||||
|
||||
// Rich is a normal text wrapped with optional format segments.
|
||||
|
@ -45,6 +45,11 @@ type Rich struct {
|
|||
Segments []Segment
|
||||
}
|
||||
|
||||
// String returns the Content in plain text.
|
||||
func (r Rich) String() string {
|
||||
return r.Content
|
||||
}
|
||||
|
||||
// Attributor is a rich text markup format that a segment could implement. This
|
||||
// is to be applied directly onto the text.
|
||||
type Attributor interface {
|
||||
|
@ -88,7 +93,18 @@ type Colorer interface {
|
|||
}
|
||||
|
||||
// Imager implies the segment should be replaced with a (possibly inlined)
|
||||
// image. Only the starting bound matters, as images cannot substitute texts.
|
||||
// image.
|
||||
//
|
||||
// The Imager segment must return a bound of length zero, that is, the start and
|
||||
// end bounds must be the same, unless the Imager segment covers something
|
||||
// meaningful, as images must not substitute texts and only complement them.
|
||||
//
|
||||
// An example of the start and end bounds being the same would be any inline
|
||||
// image, and an Imager that belongs to a Mentioner segment should have its
|
||||
// bounds overlap. Normally, implementations with separated Mentioner and Imager
|
||||
// implementations don't have to bother about this, since with Mentioner, the
|
||||
// same Bounds will be shared, and with Imager, the Bounds method can easily
|
||||
// return the same variable for start and end.
|
||||
//
|
||||
// For segments that also implement mentioner, the image should be treated as a
|
||||
// square avatar.
|
||||
|
@ -122,6 +138,16 @@ type Mentioner interface {
|
|||
MentionInfo() Rich
|
||||
}
|
||||
|
||||
// MessageReferencer is similar to Linker, except it references a message
|
||||
// instead of an arbitrary URL. As such, its appearance may be formatted
|
||||
// similarly to a link, but this is up to the frontend to decide. When clicked,
|
||||
// the frontend should scroll to the message with the ID returned by MessageID()
|
||||
// and highlight it, though this is also for appearance, so the frontend may
|
||||
// decide in detail how to display it.
|
||||
type MessageReferencer interface {
|
||||
MessageID() string
|
||||
}
|
||||
|
||||
// Quoteblocker represents a quoteblock that behaves similarly to the blockquote
|
||||
// HTML tag. The quoteblock may be represented typically by an actaul quoteblock
|
||||
// or with green arrows prepended to each line.
|
||||
|
@ -144,12 +170,13 @@ type Segment interface {
|
|||
|
||||
// Asserters.
|
||||
|
||||
AsColorer() Colorer // Optional
|
||||
AsLinker() Linker // Optional
|
||||
AsImager() Imager // Optional
|
||||
AsAvatarer() Avatarer // Optional
|
||||
AsMentioner() Mentioner // Optional
|
||||
AsAttributor() Attributor // Optional
|
||||
AsCodeblocker() Codeblocker // Optional
|
||||
AsQuoteblocker() Quoteblocker // Optional
|
||||
AsColorer() Colorer // Optional
|
||||
AsLinker() Linker // Optional
|
||||
AsImager() Imager // Optional
|
||||
AsAvatarer() Avatarer // Optional
|
||||
AsMentioner() Mentioner // Optional
|
||||
AsAttributor() Attributor // Optional
|
||||
AsCodeblocker() Codeblocker // Optional
|
||||
AsQuoteblocker() Quoteblocker // Optional
|
||||
AsMessageReferencer() MessageReferencer // Optional
|
||||
}
|
||||
|
|
|
@ -8,5 +8,12 @@ func Plain(text string) Rich {
|
|||
// SolidColor takes in a 24-bit RGB color and overrides the alpha bits with
|
||||
// 0xFF, making the color solid.
|
||||
func SolidColor(rgb uint32) uint32 {
|
||||
return rgb | (0xFF << 24)
|
||||
return (rgb << 8) | 0xFF
|
||||
}
|
||||
|
||||
// IsEmpty returns true if the given rich segment's content is empty. Note that
|
||||
// a rich text is not necessarily empty if the content is empty, because there
|
||||
// may be images within the segments.
|
||||
func (r Rich) IsEmpty() bool {
|
||||
return r.Content == "" && len(r.Segments) == 0
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
// DO NOT EDIT: THIS FILE IS GENERATED!
|
||||
// Code generated by ./cmd/internal. DO NOT EDIT.
|
||||
|
||||
// Package empty provides no-op asserter method implementations of interfaces in
|
||||
// cchat's root and text packages.
|
||||
|
@ -9,45 +9,9 @@ import (
|
|||
"github.com/diamondburned/cchat/text"
|
||||
)
|
||||
|
||||
// TextSegment provides no-op asserters for cchat.TextSegment.
|
||||
type TextSegment struct{}
|
||||
|
||||
// AsColorer returns nil.
|
||||
func (TextSegment) AsColorer() text.Colorer { return nil }
|
||||
|
||||
// AsLinker returns nil.
|
||||
func (TextSegment) AsLinker() text.Linker { return nil }
|
||||
|
||||
// AsImager returns nil.
|
||||
func (TextSegment) AsImager() text.Imager { return nil }
|
||||
|
||||
// AsAvatarer returns nil.
|
||||
func (TextSegment) AsAvatarer() text.Avatarer { return nil }
|
||||
|
||||
// AsMentioner returns nil.
|
||||
func (TextSegment) AsMentioner() text.Mentioner { return nil }
|
||||
|
||||
// AsAttributor returns nil.
|
||||
func (TextSegment) AsAttributor() text.Attributor { return nil }
|
||||
|
||||
// AsCodeblocker returns nil.
|
||||
func (TextSegment) AsCodeblocker() text.Codeblocker { return nil }
|
||||
|
||||
// AsQuoteblocker returns nil.
|
||||
func (TextSegment) AsQuoteblocker() text.Quoteblocker { return nil }
|
||||
|
||||
// Namer provides no-op asserters for cchat.Namer.
|
||||
type Namer struct{}
|
||||
|
||||
// AsIconer returns nil.
|
||||
func (Namer) AsIconer() cchat.Iconer { return nil }
|
||||
|
||||
// Service provides no-op asserters for cchat.Service.
|
||||
type Service struct{}
|
||||
|
||||
// AsIconer returns nil.
|
||||
func (Service) AsIconer() cchat.Iconer { return nil }
|
||||
|
||||
// AsConfigurator returns nil.
|
||||
func (Service) AsConfigurator() cchat.Configurator { return nil }
|
||||
|
||||
|
@ -57,9 +21,6 @@ func (Service) AsSessionRestorer() cchat.SessionRestorer { return nil }
|
|||
// Session provides no-op asserters for cchat.Session.
|
||||
type Session struct{}
|
||||
|
||||
// AsIconer returns nil.
|
||||
func (Session) AsIconer() cchat.Iconer { return nil }
|
||||
|
||||
// AsCommander returns nil.
|
||||
func (Session) AsCommander() cchat.Commander { return nil }
|
||||
|
||||
|
@ -75,9 +36,6 @@ func (Commander) AsCompleter() cchat.Completer { return nil }
|
|||
// Server provides no-op asserters for cchat.Server.
|
||||
type Server struct{}
|
||||
|
||||
// AsIconer returns nil.
|
||||
func (Server) AsIconer() cchat.Iconer { return nil }
|
||||
|
||||
// AsLister returns nil.
|
||||
func (Server) AsLister() cchat.Lister { return nil }
|
||||
|
||||
|
@ -126,9 +84,6 @@ func (Sender) AsCompleter() cchat.Completer { return nil }
|
|||
// MemberSection provides no-op asserters for cchat.MemberSection.
|
||||
type MemberSection struct{}
|
||||
|
||||
// AsIconer returns nil.
|
||||
func (MemberSection) AsIconer() cchat.Iconer { return nil }
|
||||
|
||||
// AsMemberDynamicSection returns nil.
|
||||
func (MemberSection) AsMemberDynamicSection() cchat.MemberDynamicSection { return nil }
|
||||
|
||||
|
@ -138,5 +93,38 @@ type SendableMessage struct{}
|
|||
// AsNoncer returns nil.
|
||||
func (SendableMessage) AsNoncer() cchat.Noncer { return nil }
|
||||
|
||||
// AsAttachments returns nil.
|
||||
func (SendableMessage) AsAttachments() cchat.Attachments { return nil }
|
||||
// AsReplier returns nil.
|
||||
func (SendableMessage) AsReplier() cchat.Replier { return nil }
|
||||
|
||||
// AsAttacher returns nil.
|
||||
func (SendableMessage) AsAttacher() cchat.Attacher { return nil }
|
||||
|
||||
// TextSegment provides no-op asserters for cchat.TextSegment.
|
||||
type TextSegment struct{}
|
||||
|
||||
// AsColorer returns nil.
|
||||
func (TextSegment) AsColorer() text.Colorer { return nil }
|
||||
|
||||
// AsLinker returns nil.
|
||||
func (TextSegment) AsLinker() text.Linker { return nil }
|
||||
|
||||
// AsImager returns nil.
|
||||
func (TextSegment) AsImager() text.Imager { return nil }
|
||||
|
||||
// AsAvatarer returns nil.
|
||||
func (TextSegment) AsAvatarer() text.Avatarer { return nil }
|
||||
|
||||
// AsMentioner returns nil.
|
||||
func (TextSegment) AsMentioner() text.Mentioner { return nil }
|
||||
|
||||
// AsAttributor returns nil.
|
||||
func (TextSegment) AsAttributor() text.Attributor { return nil }
|
||||
|
||||
// AsCodeblocker returns nil.
|
||||
func (TextSegment) AsCodeblocker() text.Codeblocker { return nil }
|
||||
|
||||
// AsQuoteblocker returns nil.
|
||||
func (TextSegment) AsQuoteblocker() text.Quoteblocker { return nil }
|
||||
|
||||
// AsMessageReferencer returns nil.
|
||||
func (TextSegment) AsMessageReferencer() text.MessageReferencer { return nil }
|
||||
|
|
|
@ -1,3 +0,0 @@
|
|||
package empty
|
||||
|
||||
//go:generate go run ../../cmd/internal/cchat-empty-gen
|
|
@ -0,0 +1,136 @@
|
|||
package split
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
)
|
||||
|
||||
// The original shellwords implementation belongs to mattn. This version alters
|
||||
// some code along with some trivial optimizations.
|
||||
|
||||
// ArgsIndexed converts text into a shellwords-split list of strings. This
|
||||
// function roughly follows shell syntax, in that it works with a single space
|
||||
// character in bytes. This means that implementations could use bytes, as long
|
||||
// as it only works with characters within the ASCII range, assuming the source
|
||||
// text is in UTF-8 (which it should, per Go specifications).
|
||||
func ArgsIndexed(text string, offset int64) (args []string, argIndex int64) {
|
||||
// Quickly loop over everything to roughly count spaces. It doesn't have to
|
||||
// be accurate. This isn't very useful, to be honest.
|
||||
args = make([]string, 0, approxArgLen(text))
|
||||
|
||||
var escaped, doubleQuoted, singleQuoted bool
|
||||
argIndex = -1 // in case
|
||||
|
||||
buf := bytes.Buffer{}
|
||||
buf.Grow(len(text))
|
||||
|
||||
got := false
|
||||
cursor := 0
|
||||
|
||||
for i, length := int64(0), int64(len(text)); i < length; i++ {
|
||||
r := text[i]
|
||||
if offset == i {
|
||||
argIndex = int64(len(args))
|
||||
}
|
||||
|
||||
switch {
|
||||
case escaped:
|
||||
got = true
|
||||
escaped = false
|
||||
|
||||
if doubleQuoted {
|
||||
switch r {
|
||||
case 'n':
|
||||
buf.WriteByte('\n')
|
||||
continue
|
||||
case 't':
|
||||
buf.WriteByte('\t')
|
||||
continue
|
||||
}
|
||||
}
|
||||
buf.WriteByte(r)
|
||||
continue
|
||||
|
||||
case isSpace(r):
|
||||
switch {
|
||||
case singleQuoted, doubleQuoted:
|
||||
buf.WriteByte(r)
|
||||
case got:
|
||||
cursor += buf.Len()
|
||||
args = append(args, buf.String())
|
||||
buf.Reset()
|
||||
got = false
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
switch r {
|
||||
case '\\':
|
||||
if singleQuoted {
|
||||
buf.WriteByte(r)
|
||||
} else {
|
||||
escaped = true
|
||||
}
|
||||
continue
|
||||
|
||||
case '"':
|
||||
if !singleQuoted {
|
||||
if doubleQuoted {
|
||||
got = true
|
||||
}
|
||||
doubleQuoted = !doubleQuoted
|
||||
continue
|
||||
}
|
||||
|
||||
case '\'':
|
||||
if !doubleQuoted {
|
||||
if singleQuoted {
|
||||
got = true
|
||||
}
|
||||
singleQuoted = !singleQuoted
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
got = true
|
||||
buf.WriteByte(r)
|
||||
}
|
||||
|
||||
if got || escaped || singleQuoted || doubleQuoted {
|
||||
if argIndex < 0 {
|
||||
argIndex = int64(len(args))
|
||||
}
|
||||
|
||||
args = append(args, buf.String())
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// this is completely optional.
|
||||
func approxArgLen(text string) int {
|
||||
var arglen int
|
||||
var inside bool
|
||||
var escape bool
|
||||
for i := 0; i < len(text); i++ {
|
||||
switch b := text[i]; b {
|
||||
case '\\':
|
||||
escape = true
|
||||
continue
|
||||
case '\'', '"':
|
||||
if !escape {
|
||||
inside = !inside
|
||||
}
|
||||
default:
|
||||
if isSpace(b) && !inside {
|
||||
arglen++
|
||||
}
|
||||
}
|
||||
|
||||
if escape {
|
||||
escape = false
|
||||
}
|
||||
}
|
||||
|
||||
// Allocate 1 more just in case.
|
||||
return arglen + 1
|
||||
}
|
|
@ -0,0 +1,110 @@
|
|||
package split
|
||||
|
||||
import "testing"
|
||||
|
||||
var argsSplitTests = []testEntry{
|
||||
{
|
||||
input: "bruhemus 'momentus lorem' \"ipsum\"",
|
||||
offset: 13, // ^
|
||||
output: []string{"bruhemus", "momentus lorem", "ipsum"},
|
||||
index: 1,
|
||||
},
|
||||
{
|
||||
input: "Yoohoo! My name\\'s Astolfo! I belong to the Rider-class! And, and... uhm, nice " +
|
||||
"to meet you!",
|
||||
offset: 37, // ^
|
||||
output: []string{
|
||||
"Yoohoo!", "My", "name's", "Astolfo!", "I", "belong", "to", "the", "Rider-class!",
|
||||
"And,", "and...", "uhm,", "nice", "to", "meet", "you!"},
|
||||
index: 6,
|
||||
},
|
||||
{
|
||||
input: "sorry, what were you typing?",
|
||||
offset: int64(len("sorry, what were you typing?")) - 1,
|
||||
output: []string{"sorry,", "what", "were", "you", "typing?"},
|
||||
index: 4,
|
||||
},
|
||||
{
|
||||
input: "zeroed out input",
|
||||
offset: 0,
|
||||
output: []string{"zeroed", "out", "input"},
|
||||
index: 0,
|
||||
},
|
||||
{
|
||||
input: "に ほ ん ご",
|
||||
offset: 3,
|
||||
output: []string{"に ほ ん ご"},
|
||||
index: 0,
|
||||
},
|
||||
{
|
||||
input: `echo "this \"quote\" is a regular test"`,
|
||||
offset: 5,
|
||||
output: []string{"echo", `this "quote" is a regular test`},
|
||||
index: 1,
|
||||
},
|
||||
{
|
||||
input: `echo "this \"quote\" is a regular test"`,
|
||||
offset: 4,
|
||||
output: []string{"echo", `this "quote" is a regular test`},
|
||||
index: 0,
|
||||
},
|
||||
{
|
||||
input: `echo "`,
|
||||
offset: 6,
|
||||
output: []string{"echo", ""},
|
||||
index: 1,
|
||||
},
|
||||
{
|
||||
input: `info \n`,
|
||||
offset: 7,
|
||||
output: []string{"info", "n"},
|
||||
index: 1,
|
||||
},
|
||||
{
|
||||
input: `info "\n"`,
|
||||
offset: 7,
|
||||
output: []string{"info", "\n"},
|
||||
index: 1,
|
||||
},
|
||||
{
|
||||
input: `info '\nnot a new line!'`,
|
||||
offset: 15,
|
||||
output: []string{"info", `\nnot a new line!`},
|
||||
index: 1,
|
||||
},
|
||||
{
|
||||
input: `info \\n`,
|
||||
offset: 7,
|
||||
output: []string{"info", "\\n"},
|
||||
index: 1,
|
||||
},
|
||||
}
|
||||
|
||||
func TestArgsIndexed(t *testing.T) {
|
||||
for _, test := range argsSplitTests {
|
||||
a, j := ArgsIndexed(test.input, test.offset)
|
||||
test.compare(t, a, j)
|
||||
|
||||
if expect, got := approxArgLen(test.input), len(test.output); expect != got {
|
||||
t.Error("Approximated arg len is off, (expected/got)", expect, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const argsbenchstr = "Alright, Master! I\\'m your blade, your edge and your arrow! You\\'ve " +
|
||||
"placed so much trust in me, despite how weak I am - I\\'ll do everything in my power to not " +
|
||||
"disappoint you!"
|
||||
|
||||
func BenchmarkArgsIndexed(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
ArgsIndexed(benchstr, benchcursor)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkArgsIndexedLong(b *testing.B) {
|
||||
const benchstr = benchstr + benchstr + benchstr + benchstr + benchstr + benchstr
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
ArgsIndexed(benchstr, benchcursor)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,103 @@
|
|||
package split
|
||||
|
||||
import (
|
||||
"unicode"
|
||||
)
|
||||
|
||||
// These code to mostly be copied from package strings.
|
||||
|
||||
// SpaceIndexed returns a splitted string with the current index that
|
||||
// CompleteMessage wants. The text is the entire input string and the offset is
|
||||
// where the cursor currently is.
|
||||
func SpaceIndexed(text string, offset int64) ([]string, int64) {
|
||||
// First count the fields.
|
||||
n, hasRunes := countSpace(text)
|
||||
if hasRunes {
|
||||
// Some runes in the input string are not ASCII.
|
||||
return spaceIndexedRunes([]rune(text), offset)
|
||||
}
|
||||
|
||||
// ASCII fast path
|
||||
a := make([]string, n)
|
||||
na := int64(0)
|
||||
fieldStart := int64(0)
|
||||
i := int64(0)
|
||||
j := n - 1 // last by default
|
||||
|
||||
// Skip spaces in the front of the input.
|
||||
for i < int64(len(text)) && asciiSpace[text[i]] != 0 {
|
||||
i++
|
||||
}
|
||||
|
||||
fieldStart = i
|
||||
|
||||
for i < int64(len(text)) {
|
||||
if asciiSpace[text[i]] == 0 {
|
||||
i++
|
||||
continue
|
||||
}
|
||||
|
||||
a[na] = text[fieldStart:i]
|
||||
if fieldStart <= offset && offset <= i {
|
||||
j = na
|
||||
}
|
||||
|
||||
na++
|
||||
i++
|
||||
|
||||
// Skip spaces in between fields.
|
||||
for i < int64(len(text)) && asciiSpace[text[i]] != 0 {
|
||||
i++
|
||||
}
|
||||
fieldStart = i
|
||||
}
|
||||
if fieldStart < int64(len(text)) { // Last field might end at EOF.
|
||||
a[na] = text[fieldStart:]
|
||||
}
|
||||
|
||||
return a, j
|
||||
}
|
||||
|
||||
func spaceIndexedRunes(runes []rune, offset int64) ([]string, int64) {
|
||||
// A span is used to record a slice of s of the form s[start:end].
|
||||
// The start index is inclusive and the end index is exclusive.
|
||||
type span struct{ start, end int64 }
|
||||
|
||||
spans := make([]span, 0, 16)
|
||||
|
||||
// Find the field start and end indices.
|
||||
wasField := false
|
||||
fromIndex := int64(0)
|
||||
for i, rune := range runes {
|
||||
if unicode.IsSpace(rune) {
|
||||
if wasField {
|
||||
spans = append(spans, span{start: fromIndex, end: int64(i)})
|
||||
wasField = false
|
||||
}
|
||||
} else {
|
||||
if !wasField {
|
||||
fromIndex = int64(i)
|
||||
wasField = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Last field might end at EOF.
|
||||
if wasField {
|
||||
spans = append(spans, span{fromIndex, int64(len(runes))})
|
||||
}
|
||||
|
||||
// Create strings from recorded field indices.
|
||||
a := make([]string, 0, len(spans))
|
||||
j := int64(len(spans)) - 1 // assume last
|
||||
|
||||
for i, span := range spans {
|
||||
a = append(a, string(runes[span.start:span.end]))
|
||||
|
||||
if span.start <= offset && offset <= span.end {
|
||||
j = int64(i)
|
||||
}
|
||||
}
|
||||
|
||||
return a, j
|
||||
}
|
|
@ -0,0 +1,76 @@
|
|||
package split
|
||||
|
||||
import "testing"
|
||||
|
||||
var spaceSplitTests = []testEntry{
|
||||
{
|
||||
input: "bruhemus momentus lorem ipsum",
|
||||
offset: 13, // ^
|
||||
output: []string{"bruhemus", "momentus", "lorem", "ipsum"},
|
||||
index: 1,
|
||||
},
|
||||
{
|
||||
input: "Yoohoo! My name's Astolfo! I belong to the Rider-class! And, and... uhm, nice " +
|
||||
"to meet you!",
|
||||
offset: 37, // ^
|
||||
output: []string{
|
||||
"Yoohoo!", "My", "name's", "Astolfo!", "I", "belong", "to", "the", "Rider-class!",
|
||||
"And,", "and...", "uhm,", "nice", "to", "meet", "you!"},
|
||||
index: 6,
|
||||
},
|
||||
{
|
||||
input: "sorry, what were you typing?",
|
||||
offset: int64(len("sorry, what were you typing?")) - 1,
|
||||
output: []string{"sorry,", "what", "were", "you", "typing?"},
|
||||
index: 4,
|
||||
},
|
||||
{
|
||||
input: "zeroed out input",
|
||||
offset: 0,
|
||||
output: []string{"zeroed", "out", "input"},
|
||||
index: 0,
|
||||
},
|
||||
{
|
||||
input: "に ほ ん ご",
|
||||
offset: 3,
|
||||
output: []string{"に", "ほ", "ん", "ご"},
|
||||
index: 1,
|
||||
},
|
||||
}
|
||||
|
||||
func TestSpaceIndexed(t *testing.T) {
|
||||
for _, test := range spaceSplitTests {
|
||||
a, j := SpaceIndexed(test.input, test.offset)
|
||||
test.compare(t, a, j)
|
||||
}
|
||||
}
|
||||
|
||||
const benchstr = "Alright, Master! I\\'m your blade, your edge and your arrow! You\\'ve placed " +
|
||||
"so much trust in me, despite how weak I am - I\\'ll do everything in my power to not " +
|
||||
"disappoint you!"
|
||||
const benchcursor = 32 // arbitrary
|
||||
|
||||
func BenchmarkSpaceIndexed(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
SpaceIndexed(benchstr, benchcursor)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkSpaceIndexedLong(b *testing.B) {
|
||||
const benchstr = benchstr + benchstr + benchstr + benchstr + benchstr + benchstr
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
SpaceIndexed(benchstr, benchcursor)
|
||||
}
|
||||
}
|
||||
|
||||
// same as benchstr but w/ a horizontal line (outside ascii)
|
||||
const benchstr8 = "Alright, Master! I'm your blade, your edge and your arrow! You've placed " +
|
||||
"so much trust in me, despite how weak I am ― I'll do everything in my power to not " +
|
||||
"disappoint you!"
|
||||
|
||||
func BenchmarkSpaceIndexedUTF8(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
SpaceIndexed(benchstr8, benchcursor)
|
||||
}
|
||||
}
|
|
@ -2,117 +2,39 @@
|
|||
// CompleteMessage.
|
||||
package split
|
||||
|
||||
import (
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
import "unicode/utf8"
|
||||
|
||||
// SplitFunc is the type that describes the two splitter functions, SpaceIndexed
|
||||
// and ArgsIndexed.
|
||||
type SplitFunc = func(text string, offset int64) ([]string, int64)
|
||||
|
||||
var (
|
||||
_ SplitFunc = SpaceIndexed
|
||||
_ SplitFunc = ArgsIndexed
|
||||
)
|
||||
|
||||
// just helper functions here
|
||||
|
||||
var asciiSpace = [256]uint8{'\t': 1, '\n': 1, '\v': 1, '\f': 1, '\r': 1, ' ': 1}
|
||||
|
||||
// SpaceIndexed returns a splitted string with the current index that
|
||||
// CompleteMessage wants. The text is the entire input string and the offset is
|
||||
// where the cursor currently is.
|
||||
func SpaceIndexed(text string, offset int) ([]string, int) {
|
||||
// First count the fields.
|
||||
func isSpace(b byte) bool { return asciiSpace[b] == 1 }
|
||||
|
||||
func countSpace(text string) (n int64, hasRunes bool) {
|
||||
// This implementation to also be mostly copy-pasted from package strings.
|
||||
|
||||
// This is an exact count if s is ASCII, otherwise it is an approximation.
|
||||
n := 0
|
||||
wasSpace := 1
|
||||
// var n int64
|
||||
|
||||
wasSpace := int64(1)
|
||||
// setBits is used to track which bits are set in the bytes of s.
|
||||
setBits := uint8(0)
|
||||
for i := 0; i < len(text); i++ {
|
||||
r := text[i]
|
||||
setBits |= r
|
||||
isSpace := int(asciiSpace[r])
|
||||
isSpace := int64(asciiSpace[r])
|
||||
n += wasSpace & ^isSpace
|
||||
wasSpace = isSpace
|
||||
}
|
||||
|
||||
if setBits >= utf8.RuneSelf {
|
||||
// Some runes in the input string are not ASCII.
|
||||
return spaceIndexedRunes([]rune(text), offset)
|
||||
}
|
||||
|
||||
// ASCII fast path
|
||||
a := make([]string, n)
|
||||
na := 0
|
||||
fieldStart := 0
|
||||
i := 0
|
||||
j := n - 1 // last by default
|
||||
|
||||
// Skip spaces in the front of the input.
|
||||
for i < len(text) && asciiSpace[text[i]] != 0 {
|
||||
i++
|
||||
}
|
||||
|
||||
fieldStart = i
|
||||
|
||||
for i < len(text) {
|
||||
if asciiSpace[text[i]] == 0 {
|
||||
i++
|
||||
continue
|
||||
}
|
||||
|
||||
a[na] = text[fieldStart:i]
|
||||
if fieldStart <= offset && offset <= i {
|
||||
j = na
|
||||
}
|
||||
|
||||
na++
|
||||
i++
|
||||
|
||||
// Skip spaces in between fields.
|
||||
for i < len(text) && asciiSpace[text[i]] != 0 {
|
||||
i++
|
||||
}
|
||||
fieldStart = i
|
||||
}
|
||||
if fieldStart < len(text) { // Last field might end at EOF.
|
||||
a[na] = text[fieldStart:]
|
||||
}
|
||||
|
||||
return a, j
|
||||
}
|
||||
|
||||
func spaceIndexedRunes(runes []rune, offset int) ([]string, int) {
|
||||
// A span is used to record a slice of s of the form s[start:end].
|
||||
// The start index is inclusive and the end index is exclusive.
|
||||
type span struct{ start, end int }
|
||||
|
||||
spans := make([]span, 0, 16)
|
||||
|
||||
// Find the field start and end indices.
|
||||
wasField := false
|
||||
fromIndex := 0
|
||||
for i, rune := range runes {
|
||||
if unicode.IsSpace(rune) {
|
||||
if wasField {
|
||||
spans = append(spans, span{start: fromIndex, end: i})
|
||||
wasField = false
|
||||
}
|
||||
} else {
|
||||
if !wasField {
|
||||
fromIndex = i
|
||||
wasField = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Last field might end at EOF.
|
||||
if wasField {
|
||||
spans = append(spans, span{fromIndex, len(runes)})
|
||||
}
|
||||
|
||||
// Create strings from recorded field indices.
|
||||
a := make([]string, 0, len(spans))
|
||||
j := len(spans) - 1 // assume last
|
||||
|
||||
for i, span := range spans {
|
||||
a = append(a, string(runes[span.start:span.end]))
|
||||
|
||||
if span.start <= offset && offset <= span.end {
|
||||
j = i
|
||||
}
|
||||
}
|
||||
|
||||
return a, j
|
||||
return n, setBits >= utf8.RuneSelf
|
||||
}
|
||||
|
|
|
@ -2,83 +2,23 @@ package split
|
|||
|
||||
import "testing"
|
||||
|
||||
func TestSpaceIndexed(t *testing.T) {
|
||||
var tests = []struct {
|
||||
input string
|
||||
offset int
|
||||
output []string
|
||||
index int
|
||||
}{{
|
||||
input: "bruhemus momentus lorem ipsum",
|
||||
offset: 13, // ^
|
||||
output: []string{"bruhemus", "momentus", "lorem", "ipsum"},
|
||||
index: 1,
|
||||
}, {
|
||||
input: "Yoohoo! My name's Astolfo! I belong to the Rider-class! And, and... uhm, nice " +
|
||||
"to meet you!",
|
||||
offset: 37, // ^
|
||||
output: []string{
|
||||
"Yoohoo!", "My", "name's", "Astolfo!", "I", "belong", "to", "the", "Rider-class!",
|
||||
"And,", "and...", "uhm,", "nice", "to", "meet", "you!"},
|
||||
index: 6,
|
||||
}, {
|
||||
input: "sorry, what were you typing?",
|
||||
offset: len("sorry, what were you typing?") - 1,
|
||||
output: []string{"sorry,", "what", "were", "you", "typing?"},
|
||||
index: 4,
|
||||
}, {
|
||||
input: "zeroed out input",
|
||||
offset: 0,
|
||||
output: []string{"zeroed", "out", "input"},
|
||||
index: 0,
|
||||
}, {
|
||||
input: "に ほ ん ご",
|
||||
offset: 3,
|
||||
output: []string{"に", "ほ", "ん", "ご"},
|
||||
index: 1,
|
||||
}}
|
||||
type testEntry struct {
|
||||
input string
|
||||
offset int64
|
||||
output []string
|
||||
index int64
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
a, j := SpaceIndexed(test.input, test.offset)
|
||||
if !strsleq(a, test.output) {
|
||||
t.Error("Mismatch output (input/got/expected)", test.input, a, test.output)
|
||||
}
|
||||
if j != test.index {
|
||||
t.Error("Mismatch index (input/got/expected)", test.input, j, test.index)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const benchstr = "Alright, Master! I'm your blade, your edge and your arrow! You've placed " +
|
||||
"so much trust in me, despite how weak I am - I'll do everything in my power to not " +
|
||||
"disappoint you!"
|
||||
const benchcursor = 32 // arbitrary
|
||||
|
||||
func BenchmarkSpaceIndexed(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
SpaceIndexed(benchstr, benchcursor)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkSpaceIndexedLong(b *testing.B) {
|
||||
const benchstr = benchstr + benchstr + benchstr + benchstr + benchstr + benchstr
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
SpaceIndexed(benchstr, benchcursor)
|
||||
}
|
||||
}
|
||||
|
||||
// same as benchstr but w/ a horizontal line (outside ascii)
|
||||
const benchstr8 = "Alright, Master! I'm your blade, your edge and your arrow! You've placed " +
|
||||
"so much trust in me, despite how weak I am ― I'll do everything in my power to not " +
|
||||
"disappoint you!"
|
||||
|
||||
func BenchmarkSpaceIndexedUTF8(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
SpaceIndexed(benchstr8, benchcursor)
|
||||
func (test testEntry) compare(t *testing.T, words []string, index int64) {
|
||||
if !strsleq(words, test.output) {
|
||||
t.Error("Mismatch output (input/got/expected)", test.input, words, test.output)
|
||||
}
|
||||
if index != test.index {
|
||||
t.Error("Mismatch index (input/got/expected)", test.input, index, test.index)
|
||||
}
|
||||
}
|
||||
|
||||
// string slice equal
|
||||
func strsleq(s1, s2 []string) bool {
|
||||
if len(s1) != len(s2) {
|
||||
return false
|
||||
|
|
Loading…
Reference in New Issue