Compare commits

...

57 Commits

Author SHA1 Message Date
diamondburned b5bb0c9bb9 Revert "Replace stop callbacks with contexts"
This reverts commit 410ac73469.

The rationale is that the frontend can wrap its own components with a
disposable thread-safe wrapper that doesn't work once it's invalidated
if it wants the guarantee that the component doesn't work anymore once
the context is stopped.
2021-05-04 16:19:13 -07:00
diamondburned 8bfabf58ec Make SetterMethods ContainerUpdaterMethods
This commit separated SetterMethods that are specifically for updating
containers to another method type, named ContainerUpdaterMethod. This
change is done to force a context parameter into container setters,
allowing the frontend to know if an incoming update is valid or not,
based on the state of the context given.

The validity check should be the same as any other context:

    select {
    case <-ctx.Done():
        return
    default:
        addEvent()
    }

It is crucial, however, to do the checking and updating in the same
thread or lock as the context is cancelled. This explicit
synchronization is required to prevent any race condition whatsoever
with cancellation of the context.

The backend must pass in the right context, that is, any context that
inherits the cancellation from the frontend. Passing in the invalid
context is undefined behavior and will eventually cause a data race.
2021-05-01 21:41:39 -07:00
diamondburned 410ac73469 Replace stop callbacks with contexts
This commit removes all stop callbacks in ContainerMethods. The
intention is to have backends disconnect callbacks when the context is
cancelled, rather than when the stop function is called.

This helps get rid of countless race condition flaws caused by the
duration between the context being cancelled on one thread and the stop
callback being set in another, causing the handlers to not disconnect.
2021-05-01 17:49:25 -07:00
diamondburned 4e11444f6c Configurator to be SetterMethods
This commit changes Configurator's methods to be SetterMethods instead
of IOMethods, as Configurator is specifically made for frontend-managed
settings just for the backend, so no storing/loading is needed on the
backend's side.

This commit also changes SetterMethod to allow methods done to the
backend to error out, in case the setting value is invalid somehow.
Setter methods that are called by the backend (as opposed to the
frontend) must never error.
2021-05-01 17:22:01 -07:00
diamondburned 86956a65ec Allow cancels for ContainerMethods and IOMethods
This commit changes several ContainerMethods to take in a context. It
also changes all IOMethods to take in a context.

This addition adds consistency to the API as well as allowing better
graceful cancellation and cleanups if needed when, for example, the user
wants to discard an ongoing process.
2021-05-01 17:02:02 -07:00
diamondburned f2de1cb84d Added missing text.Rich return in ListMember 2021-03-25 16:25:44 -07:00
diamondburned 0cb14b9819 ListMember to no longer use Namer
This commit broke ListMember to remove the Namer interface. This is
because the whole interface should act as a static container with
information to be updated.
2021-03-25 16:05:35 -07:00
diamondburned f24feb2002 MessageUpdate should only update the content
This commit changes MessageUpdate so that it only updates the message
content. Updating the username should be up to MessageCreate's Author.
2021-03-20 00:13:39 -07:00
diamondburned f8c644fa7e Allow empty texts with segments
This commit allows segments in an empty text segment to account for
segments with only an image.
2021-03-19 22:40:31 -07:00
diamondburned c7d4473c23 Nicknamer to embed Name instead
This commit breaks Nicknamer to embed Name instead of having its own
ContainerMethod with a similar function signature but different name.
This allows the frontend to reuse the same LabelContainer abstraction
for Nickname as well.
2021-03-19 22:15:42 -07:00
diamondburned 174496bdf9 Enforce Identifier on all Services
This commit breaks the Service interface to force all services to have a
global unique identifier. The commit does not enforce any particular
format, but the Reverse Domain Name Notation is recommended.

For reference:
https://en.wikipedia.org/wiki/Reverse_domain_name_notation
2021-03-19 16:52:41 -07:00
diamondburned da5c38eb2f Columnate to return bool
This commit breaks the previous Columnate API to return booleans instead
of constant integers. This makes handling Columnate API much simpler
with less false values (since all possible boolean values are valid).
2021-03-18 14:20:30 -07:00
diamondburned c2fb784dbf Move Columnate to Lister
This commit broke Lister to add Columnate, and the method is removed
from Server, because only Lister gets nested.
2021-03-18 12:27:50 -07:00
diamondburned d40f221221 Add missing return in Columnate 2021-03-18 10:06:52 -07:00
diamondburned ee9c2cc37c Remove redundant function 2021-03-18 09:58:13 -07:00
diamondburned 0569261f72 Enforce Columnate in Server
This commit breaks the API to enforce all servers to have a Columnate
method.

This commit also changes some of the documentation to be more obvious.
2021-03-18 09:52:14 -07:00
diamondburned 4ea6773527 Force ContainerMethod stop funcs
This commit breaks ContainerMethod to enforce explicit destructors. This
gives the frontend explicit control over when the container is
unsubscribed, but it also eases unsubscription implementations in the
backend.

With this new change, the backend can now add the container into a
global repository and unsubscribe from it explicitly from the callback.
2021-03-12 22:41:46 -08:00
diamondburned 1ece6ea076 Rename Author to Namer; Namer to use LabelContainer
This commit breaks more of the API to force all implementations of Namer
to use a LabelContainer instead of just returning a text. This is done
to allow updating of all labels instead of having to update the whole
parent context. This allows the backend to do book-keeping of labels and
images and trivially update them simultaneously without updating the
parent context.

The Author interface is also renamed to User. This allows the user
interface to be used everywhere else outside of Message.
2021-03-10 15:09:48 -08:00
diamondburned 1251001e8c Removed Icon interfaces, added ReadIndicator
This commit introduced a big breaking change of changing Author and
Namer to no longer have any reference to Icon or Image containers and
interfaces.

Instead, in the case of Author and Namer, it relies on the label being
updated by either an update setter or LabelContainer. The frontend
should get the first image/avatar to display that instead.

This commit also added ReadIndicator and related interfaces to support
the read receipts feature seen in Matrix, Telegram, Messenger and co.

The UnreadIndicator interface was broken to add the MarkRead method,
which hands explicit control of setting read messages for the current
user to the frontend instead.
2021-03-08 22:20:10 -08:00
diamondburned 41a7dac033 Added Columnator
This commit added the Columnator interface. This interface accommodates
the fact that some services (such as Discord) stylizes certain nested
servers in the same column.

This commit also fixed a minor test case.
2021-03-08 16:22:30 -08:00
diamondburned 02c686f994 Added helper methods to Package 2021-01-13 18:44:26 -08:00
diamondburned 1460ee6b4b Allow IOMethods to be explicit disposers
This commit changes IOMethods and clarifies stop functions that they
will act as destructors or disposers for whatever interface that
implements the methods, and that both the backend and frontend should
free that interface when they're called.

This commit is added as part of the IPC protocol.
2021-01-13 16:17:55 -08:00
diamondburned 06a26af5ba Slightly cleaner generation structure 2021-01-08 19:55:35 -08:00
diamondburned 903fe9fbfd Fixed ReplyingTo invalid type 2021-01-01 14:28:30 -08:00
diamondburned 7cb512f8b1 Added Replier and renamed Attachments
This commit renamed Attachments to Attacher, as the new name is more
idiomatic.

This commit also added the Replier interface, which is used to indicate
that a message being sent is a reply towards something.
2021-01-01 14:09:42 -08:00
diamondburned 24fc2c9bbb Fixed invalid types and bugs in MessageReferencer 2020-12-17 17:18:02 -08:00
diamondburned f1db8e0601 Added MessageReferencer for text.Rich
This commit added MessageReferencer for the text.Rich segments, which
allows a message to highlight a URL or text as a reference to other
messages. This could be used for replies as well as links that are
supposed to go to other messages.

The frontend gets to decide how exactly to represent the message when it
is clicked. However, as of right now, there is no API to fetch a single
message individually, so this API is limited to just within the message
buffer.
2020-12-17 17:14:21 -08:00
diamondburned 7fe9b3ed4c Clarified JoinServer opening n times
This commit clarifies that JoinServer must only be opened by one
container. In other words, before it is called again, the stop callback
of the last call must be called beforehand. This applies per messenger.
2020-12-17 16:47:38 -08:00
diamondburned fd8106eaf1 Revert Nonce deprecation
This commit reverts commit 9fd965d45a.

The reason for this reversion is that Send does not return an ID, and
therefore cannot know if any of its incoming messages are what it sent
or not.

Although a message return can be added into Send, that would be
extraneous, as the same message may now arrive by Send returning and/or
through the messenger container. Working around this would require
having an Upsert behavior instead of Insert or Update.
2020-12-17 12:44:52 -08:00
diamondburned 9fd965d45a Deprecated Nonce
This commit deprecates all Nonce methods as well as the concept of
Nonces in general. This is because in certain cases where coordination
between message sends and echoes would require far too much effort with
nonces as a method to detect message arrivals.

Starting from this commit, frontend implementations must assume that a
nil error returned from Sender's Send method means that the message has
successfully arrived.

Backend implementations must provide this guarantee if necessary by
blocking Send until it's sure that the message has arrived. For services
that send messages asynchronously, a handler and a blocking channel
(or pubsub) could be used to signal to the Send goroutine from the event
loop that the message has arrived. Backends may choose to transmit its
own nonces for this purpose.
2020-12-16 23:44:50 -08:00
diamondburned 955b99c9b6 Added AuthenticateError
This commit broke both the cchat API and its repository generation API
to accomodate for custom error types, as the new Authenticator API now
uses AuthenticateError over error to add in multi-stage authentication
instead of the old method with the for loop.

This commit also removed the multistage example documented in
Authenticator, as the API is now clearer.

This commit also added the WrapAuthenticateError helper function that
wraps a normal error into an AuthenticateError that does not have a
NextStage return. Backends should use this for
2020-10-27 13:33:52 -07:00
diamondburned c32c50c0e8 Changed Namer to a Name method in Authenticator
This commit removes the Namer interface and use a normal Name() method
instead. This is because we don't want to add icons into Authenticators.
2020-10-26 22:26:42 -07:00
diamondburned 318c85ab65 Added Namer and Description into Authenticator
This commit embeds the Namer interface into Authenticator as well as
adding the new Description method, both of which returns a text.Rich.
These methods are added to provide contextual clues to the user about
each Authenticator method.

Frontends can use the Name as the title, so the name should be short,
concise, and not include the name of the service.
2020-10-26 22:17:38 -07:00
diamondburned e59ab2dbf1 Clarified ID uniqueness
This commit clarifies the rules regarding ID uniqueness and guarantees
when it comes to the backend and frontend. This piece of documentation
was added into the top package section.
2020-10-26 21:50:01 -07:00
diamondburned ea2c12d119 Allow multiple implementations of Authenticator
This commit breaks the API to allow backends to return a slice of
Authenticators instead of a single Authenticator in the Service
interface. This is because some services might allow for more than one
method of authentication.

Note that the representation of multiple authenticators depends on the
frontend. One may choose to use tabs, notebooks, stacks, or anything
that is reasonable.
2020-10-26 21:49:57 -07:00
diamondburned 4c835a467b Fixed several ArgsIndexed bugs
This commit fixes several ArgsIndexed bugs that would cause the function
to return no words with -1. Additional test cases have been added.
2020-10-14 23:24:26 -07:00
diamondburned 10549e49e1 Added SplitFunc helper type
This commit adds the SplitFunc helper type which has a function
signature matching that of ArgsIndexer and SplitIndexer. Implementations
can use this type to provide pluggable splitters.
2020-10-14 18:26:49 -07:00
diamondburned 289eda1c25 Clarified Commander split rules; added ArgsIndexed
This commit clarified the word split rules when it comes to the
Commander interface. Specifically, this interface now has an edge case
of having split rules similarly to shell words (or shell syntax).

The implementation of these split rules is added into package split,
similarly to SplitIndexed. It is called ArgsIndexed. For the most parts,
it will behave similarly to shell syntax.
2020-10-14 18:08:35 -07:00
diamondburned 05f8ec0cbf Changed Commander to use []byte over io.Writer
This commit breaks the Commander interface. Prior to this, the Run
method would take in an io.Writer and do its tasks in the background.
Although this has lots of potential for usages, it is also very
overkill. Moreover, it makes IPC harder, since it now has to send over
fragments of data in synchronized order.

This commit gets rid of the io.Writer and only take a []byte for return
along with the error. This makes it easier for both the frontend and
backend to implement most commands, as well as making it easier for data
to be transferred over the wire.
2020-10-13 22:22:02 -07:00
diamondburned 1dd36e0034 Clarified text.Imager's bound behavior
This commit clarifies text.Imager's (and therefore text.Avatarer's as
well) bound behaviors. Prior to this commit, it is unclear which end
bound an implementation should return. This commit clarifies that in
order for the image to be inlined, the start must overlap the end
bounds.

This clarification was needed in order to differentiate images to be
inlined with images to be associated with other contexts, such as
Mentioned. The inline check would therefore be very simple:

    if start == end {
        if imager := segment.AsImager(); imager != nil {
            log.Println("Segment has an inline image.")
        }
    }

Note that since there's now a way to explicitly define whether an image
is inlined or not, for implementations that can't display images, the
ImageText() should only be used if the image is actually inlined.
Therefore, the same check also applies to ImageText(). This also applies
to AvatarText().
2020-10-13 18:30:21 -07:00
diamondburned 76f5201a6f Fix text.SolidColor returning invalid color
This commit fixes the implementation of the helper function SolidColor.
It now does correctly what it says: it sets the alpha bits to 0xFF.
Prior to this, the function would override part of the color due to an
incorrect shift.
2020-10-13 16:46:18 -07:00
diamondburned 4864d61476 Changed ServerUpdate's PreviousID API
Prior to this commit, the PreviousID method ambiguously confused two
different behaviors for the same result, that is when the returned ID is
empty.

This commit adds a return boolean to the method to differentiate those
two behaviors.
2020-10-09 12:11:23 -07:00
diamondburned 0ebf0c3302 Clarified ServerUpdate behavior
Prior to this commit, the PreviousID method of ServerUpdate seemed to be
a big unknown. This commit clarified that unknown by declaring two
conditions: when PreviousID returns an empty and non-empty ID.

The above change allows ServerUpdate events to both modify existing
servers as well as inserting new ones.
2020-10-09 11:51:16 -07:00
diamondburned d62231a4ef Fixed Backlogger method name
This commit fixes a mistake in the Backlogger interface, that is the
Backlog method was called Backlogger incorrectly.
2020-10-09 10:27:38 -07:00
diamondburned cfc0e00c8a Shorter, more idiomatic method names
This commit breaks several cchat interfaces to rename some method names
and make them shorter. These new method names are more idiomatic.
2020-10-09 10:09:38 -07:00
diamondburned 1b1e10a8a6 Updated reference split package to int64
This commit breaks package split's API to take in int64 types instead of
int. This is because CompletionEntry now uses int64 over int for
concreteness.
2020-10-09 09:34:02 -07:00
diamondburned 6140b5a131 Clarified text.{Imager,Avatarer}'s bound behavior
This commit clarifies text.Imager and text.Avatarer's bound behaviors.
Prior to this, the only behavior that those two interfaces have
regarding bounds is that only the starting bound matters, because images
must not substitute texts.

This commit clarifies that images are allowed complement other sections.
For example, a Mentioner can "have" an Imager by having the bounds
overlap.

These details are intentionally vaguely defined (it doesn't list any
interfaces beyond Mentioner), so implementations of either side can
implement these however they want, as long as the bounds overlap.

In the future, further clarification rules may be added if needed.
2020-10-09 00:17:58 -07:00
diamondburned 285ac6403f Added (text.Rich).IsEmpty
This commit restores the old IsEmpty API that was removed during code
generation.
2020-10-09 00:12:02 -07:00
diamondburned 819bcd3504 Clarified bitwise enum starting point; regenerate 2020-10-09 00:05:25 -07:00
diamondburned 32fa6266db Fixed Bitwise codegen being flipped 2020-10-08 23:58:58 -07:00
diamondburned 89b5ede1d8 Regenerated code to adhere to codegen header
This commit regenerates all files to adhere to the arguably-official
convention of having a standardized comment format to allow
distinguishing between written and generated files.

Refer to https://golang.org/s/generatedcode for more information.
2020-10-04 14:33:57 -07:00
diamondburned 5f7316cf9d Actioner.Actions to take a message ID
This commit restores the old API prior to the repository commits to make
the Actions method of the Actioner interface take in a message ID and
return a slice of strings.
2020-10-04 11:45:18 -07:00
diamondburned 086f987b3c Add Stringer into struct repositories
This commit adds the Stringer method representation into the repository.
The Rich struct of package text now implements Stringer and returns the
Content in plain text.

Prior to the repository commits, Rich used to have String().
2020-10-04 10:28:48 -07:00
diamondburned e08064021e Reproducible empty interface code generation
Prior to this commit, the code generator for package empty doesn't have
a defined order. This commit now sorts the packages before generation,
which gets rid of the main map's undefined order.
2020-10-03 23:24:15 -07:00
diamondburned dd4e230e0f Fixed UnreadContainer's comment
Prior to this commit, UnreadContainer's comments mentioned deprecated
SetUnread and such methods. It now reflects the new API.
2020-10-03 23:17:32 -07:00
diamondburned 99f7224d32 Mentioned now returns bool
Prior to this commit, the Mentioned method in MessageCreate didn't
return anything. This is a regression. It now returns a boolean that
indicates mentioned.
2020-10-03 23:14:54 -07:00
diamondburned 555931f974 Added Avatar() into Author
This commit adds the Avatar method into the Author interface. It returns
the URL if one, or it can return an empty string if either the service
does not support avatars or the user doesn't have avatars.
2020-10-03 23:08:24 -07:00
30 changed files with 1697 additions and 785 deletions

461
cchat.go
View File

@ -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,79 +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
// 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
@ -232,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
@ -247,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.
@ -279,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
@ -329,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.
@ -367,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
@ -382,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
@ -398,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
@ -431,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.
@ -473,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.
@ -494,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
@ -512,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
@ -525,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.
@ -541,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.
@ -556,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
@ -575,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.
@ -588,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
@ -598,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.
@ -608,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
@ -620,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
@ -635,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
@ -656,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.
@ -666,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
@ -692,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.
@ -706,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
@ -722,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
@ -742,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
@ -763,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.
@ -772,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
@ -792,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
@ -803,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
}

View File

@ -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)
}

View File

@ -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()

View File

@ -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

View File

@ -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))
}
})
}

View File

@ -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)
}

View File

@ -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()

View File

@ -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)
}
}

View File

@ -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}
}

View File

@ -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

View File

@ -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)
}
})
}

View File

@ -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

View File

@ -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.

View File

@ -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

View File

@ -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)

View File

@ -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 == ""
}

View File

@ -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)
}
}
}

View File

@ -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 {

View File

@ -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
}

View File

@ -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
}

View File

@ -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 }

View File

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

136
utils/split/argsplit.go Normal file
View File

@ -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
}

View File

@ -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)
}
}

103
utils/split/spacesplit.go Normal file
View File

@ -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
}

View File

@ -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)
}
}

View File

@ -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
}

View File

@ -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