Compare commits

...

138 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
diamondburned aaa29f35b0 Author now has Name() over Namer
Prior to this commit, interface Author embedded interface Namer. This
doesn't work, as it is discouraged to keep a working state inside the
implementation of Author, but Namer's embedded Iconer requires a state.

The commit changed Author to use a Name method instead, which is only a
getter. It will no longer satisfy interface Name.
2020-10-03 22:52:21 -07:00
diamondburned 1588cfef9c Fixed IconContainer.SetIcon; added ImageContainer
This commit fixes IconContainer's SetIcon, which was SetImage prior to
this commit. Before the code generation commits, this container
originally had SetIcon.

This commit also adds back ImageContainer, which was a regression during
the code generation changes. It has the SetImage method.
2020-10-03 22:06:50 -07:00
diamondburned e2751cc260 Added helper functions in package text
This package adds back the Plain function from the old package text as
well as a new function called SolidColor that returns a new RGBA color
with the alpha bits maxed out.

These functions are there for convenience. They're also outside the
scope of the code generator and repository.
2020-10-03 21:29:15 -07:00
diamondburned 7b9b4864a5 Fixed package empty having wrong package name 2020-10-03 20:00:09 -07:00
diamondburned 2d00544d67 Added missing methods from embedded interfaces
This commit adds missing empty asserter methods from the interfaces
embedded in the parent interface.
2020-10-03 19:56:58 -07:00
diamondburned 59778af1dd Empty impls for package text
This commit adds empty structs that implement no-op asserter methods for
interfaces in package text. Those implementations have "Text" prefixed
to their names.

The added implementations stay in the same place as cchat's.
2020-10-03 19:31:44 -07:00
diamondburned 9a64b50703 Package text to use asserters; colors now RGBA
This commit regenerates package text to use asserters instead of
manually asserting structs. This is to both bring consistency to the
interfaces and prepare it for the incoming IPC additions.

This commit also changed the Colorer interface to require returning a
32-bit RGBA color. Before, backends could return 24-bit RGB OR 32-bit
RGBA, but there wasn't a good way to distinguish between the two. Now,
backends must set the alpha bits to 0xFF if it's 24-bit only.
2020-10-03 15:43:05 -07:00
diamondburned 25980eb794 Added package empty
This package adds the code generation for package empty, which provides
structs that has no-op asserter methods for ease of use.

The package demonstrates one of the many possible use cases of having a
repository ready for code generation.
2020-10-03 14:31:38 -07:00
diamondburned 2d93bf62ea Regenerated with Backlogger and fixes 2020-09-27 20:37:27 -07:00
diamondburned f515470458 Generated code now compiles; added DO NOT EDIT 2020-09-27 19:30:36 -07:00
diamondburned 516532ee01 Repository changes; regenerated code
This commit fixes some trivial errors in the repository package. The
changes are breaking.

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

    go generate ./...

The code is generated using the Jennifer library. In the future, all
generated code (including package empty and the RPC library) will use
Jennifer.
2020-09-27 18:41:17 -07:00
diamondburned 8e9321928b Added a repository for API source of truth
This commit adds a new package repository containing every single cchat
types that the package provides. Its goal is to be the source of truth
for the cchat files to be generated from.

A huge advantage of this is having types in an easily representable
format. This means that other languages can easily parse the repository
and generate its own types that are similar to the original ones.

Having a repository also allows easier code generation. For example,
this commit will allow generating the "empty" package in the future,
which would contain empty implementations of cchat databases that would
return nil for asserter methods.
2020-09-26 18:24:56 -07:00
diamondburned 8b8c46a714 Refactored to a completely new API
This commit refactors entirely the ways cchat interfaces extend others.
Prior to this commit, interfaces extend itself simply by implementing
methods. This change is crucial to allow structs to decide whether or
not an interface is extended during runtime.

The current change adds the "As" methods into interfaces. When said, for
example, "Messenger extends Server," we now have the Server interface
implementing the AsMessenger method instead of before where the struct
implementing Server also implemented Messenger's methods.

For future references, these method will be called asserter methods.

The biggest motivation for this change is that these asserter methods
can allow backends to decide whether or not certain features are
implemented during runtime. For example, not all servers may support
sending messages. The asserting method is also simpler than the actual
type assertions done before.

Another motivation is to prepare cchat for an API that can reasonably be
translated to something that can be transferred over the wire. Although
the API itself will likely not be transferred over actual networking,
there are lots of plans for IPC-ing the API. This could mean that
developers would be able to develop the backends and frontends in any
programming language.

A downside to this is that the API is more restricted in terms of
extending beyond interfaces defined in the package. The initial goal of
this was to allow certain frontends to check for additional interfaces
outside of cchat that certain services could implement. However, this
goal is mostly moot, as interfaces like these require prior extensive
knowledge from both the developers of the backend and frontend
libraries.
2020-09-25 19:31:01 -07:00
diamondburned d51668512b Fixed typo in MemberListDynamicSection 2020-09-07 20:36:32 -07:00
diamondburned 62711b89f2 Fixed superfluous Lister in Session 2020-09-07 18:30:12 -07:00
diamondburned 214233cf3d Fixed error: Messager -> Messenger
This commit breaks existing v0.1.0 code to fix a grammatical error.
2020-09-07 17:19:18 -07:00
diamondburned ab2b4d48fa Fixed assert example in package header
This commit fixed an error in the package header from the previous
commit.
2020-09-05 19:46:58 -07:00
diamondburned 40dbe21c82 Shorter interface names; "Is" method in interfaces
This commit introduces many breaking changes that will break all current
code, both in the frontend and backend.

This commit changes the previous interface names to shorter versions.
This is done because, with the addition of the parent interface being
embedded in every extension interfaces, it is pointless to have the
name indicate this relationship. Furthermore, shorter and concise names
are more idiomatic.

This commit also introduces the "Is" method that is in every extension
interfaces. The purpose of this method is to provide an alternative
mechanism to check if an interface is extended.

Prior to the "Is" method, the only way for a backend to indicate
channels that can either be sent a message or not is to use two
separate types. Now, backends could implement a single type and return a
true or false on the Is method.

This method has a major disadvantage: it makes type assertions longer
and more complex. Refer to the "assert extension interfaces" example for
an example.

Despite the above disadvantage, this change is needed by the RPC
implementation in the future. Thus, it is worth the trouble of checks
being more verbose.
2020-09-05 19:39:34 -07:00
diamondburned 4239dc47c4 Added rich.Empty
This commit adds rich.Empty for convenience. It describes an empty text
segment.
2020-09-05 19:39:34 -07:00
diamondburned cd018ef8f9 Added ID type; Added backlog interfaces
This commit adds the ID type, which is a type alias to a string. This
change does not break any APIs and is done purely for documentation
purposes.

This commit also adds backlog interfaces to add support for services
capable of storing and showing chat history.

A subtle behavior change with the above change would be that
MessageContainer implementations are now required to add a mechanism to
invalidate old containers when needed. For example, the MessagePrepender
passed into MessagesBefore must be invalidated by the frontend when the
channel in view is changed. This prevents stray messages from old
channels coming in.

There are many ways to invalidate a container, but the easiest way would
be to attach an optionally atomic boolean into the store and completely
separate the store from the view (aka widget).
2020-08-19 15:58:36 -07:00
diamondburned 1b70301711 Added MemberListDynamicSection
The interface MemberListDynamicSection was added to allow server
implementations to ignore the dynamic section methods if needed.

A new method LoadLess was also introduced into the new interfaces along
with the old LoadMore. This method basically unrolls the member list as
the user scroll back up.
2020-08-15 14:19:44 -07:00
diamondburned 681cc520d9 Added text.Plain helper function
This function was added for convenience.
2020-08-15 14:10:53 -07:00
diamondburned 2f5c86aa60 Breaking changes on member list interfaces
This commit breaks the old member list API in order to change several
things. These changes are done to improve the overall usability and
flexibility of their interfaces.

This commit introduces the method LoadMore into the MemberListSection
interface. The method was added to allow sections to be lazy loaded
conditionally. This allows the server to lazy load the member list over
the network.

In the future, there might be further modifications to allow clients to
invalidate or mark sections as stale, which would allow the server to
free up the list.

This commit also changed the old Name method of MemberListSection to use
the unified Namer interface. It also moved from using section's contents
as identifiers to having a dedicated ID method using the unified
Identifier interface.
2020-08-14 14:11:28 -07:00
diamondburned 3f4d50fa92 Added a Description and Placeholder field for auth entry
This commit adds 2 extra string fields into the authentication entry
struct. The objective is to allow backends to hint additional
information that the user might want to know while authenticating.

Frontends that cannot do placeholders can opt for another way to display
the information, such as adding it into the name, surrounded by
parentheses.
2020-07-29 16:55:28 -07:00
diamondburned 7ae629e1ca Added the ISC license 2020-07-19 10:44:34 -07:00
diamondburned c45d874a80 Added member list support; moved documentation off README and into GoDoc
This commit adds member list support for servers capable of showing
messages. This includes both backend and frontend interfaces.

A UserStatus type was added with the appropriate constants for this purpose,
but it could be used in the future for other purposes.

All cchat documentation has been moved off of the README and into
GoDoc's documentation sections. This is done to free up the README for
other useful information about the project that doesn't have to do with
the code itself.
2020-07-19 10:37:51 -07:00
diamondburned 8827df937d Added MentionerAvatar and MentionedImage; clarified new behavior for Mentioner
This commit adds the MentionerAvatar and MentionedImage interfaces. This
interface gives the mentioned object an avatar or image. An example of
this would be showing a user's avatar. This allows the frontend to do
simple layouting.

The new behavior for Mentioner states that the frontends should be able
to use the highlighted segment as the mentioned object's display name.
For example, if the mentioned segment highlights "@username", then the
frontend should display "@username" as the large text.
2020-07-19 00:55:57 -07:00
diamondburned 9a7fe13cef Removed SetMentioned from UnreadIndicator
This change was done because using a message as an argument for
SetMentioned was too much. The client can manually check which messages
have Mentioned being true and highlight them.

As we no longer need a message forr SetMentioned, it is now merged into
SetUnread. As such, SetUnread now takes both an unread and a mentioned
boolean.
2020-07-16 18:53:54 -07:00
diamondburned 9974fcc636 Changes ServerMessageUnreadIndicator to return a cancel callback
This commit is a breaking change. It changes the UnreadIndicate method
to require returning an additional stop callback similar to that in
Nickname.

The motivation for this change was that frontends need a way to announce
cancellation before it destroys its server containers. This may happen
when the backend wants to replace in the container a completely new
list. As such, old lists will be destroyed, and the frontend will call
UnreadIndicate again. Because of that, the old callbacks must be cleaned
up.
2020-07-16 18:40:18 -07:00
diamondburned 1fe254db60 Added documentation to clarify attachments 2020-07-09 16:13:17 -07:00
diamondburned 106b543f09 Adds message attachments
This commit adds message attachments. More specifically, the
MessageAttachment struct was added to represent a single attachment.
Interfaces are added as well, that is ServerMessageAttachmentSender and
SendableMessageAttachments.

For the most parts, the frontend will use SendableMessageAttachments,
which extends the usual SendableMessage.
2020-07-09 16:03:35 -07:00
diamondburned ecbd4515a2 Added Mentioner and clarifications
This commit adds the Mentioner interface. The objective is to have a
clickable mention string similar to that in Discord, that when pressed
will popup a small box containing the given user info.

In the future, Mentioner may be expanded to add Avatar capabilities, but
for now, it can be added using text segments.

This commit also clarified that segments can implement multiple
interfaces. This is done to allow segments such as Mentioner to also be
colored using Colorer.

Frontend implementations should, instead of a type switch, use multiple
if statements separately. This may introduce a lot more boilerplate code.
2020-07-07 13:26:39 -07:00
diamondburned (Forefront) 0abbf861bc Added Avatarer into package text
This commit adds Avatarer into the list of supported text segments. This
should work very similarly to Imager, except that avatars should be
rounded by the frontend.

This commit should make the text image APIs consistent with the main
cchat package.
2020-07-04 01:15:50 -07:00
diamondburned (Forefront) c8d6c89a08 Added Time into Typer 2020-07-01 13:32:54 -07:00
diamondburned (Forefront) 78767a3f2f Undo latest changes that added RoundIconContainers
This commit undos these latest changes and replaced them with the new
ImageContainer API as well as Image boolean in CompletionEntry.

These changes, unlike the earlier commits, are not breaking changes.
They are only additions.

ImageContainer is added for future usages, which translates to the
previous commits' IconContainer. The current IconContainer translates to
the previous commits' RoundIconContainer.
2020-07-01 10:49:44 -07:00
diamondburned (Forefront) d29ee70d56 Updated README to add RoundIconContainer 2020-06-30 19:46:50 -07:00
diamondburned (Forefront) e7aa6fb885 Added secondary text into completion entry; added RoundIconContainer
This change was done without breaking the existing API. Initially, the
idea was to use a URL fragment to indicate if an icon should be round.
That, however, was a bad idea, as URL fragments are part of the URL
string and would require additional effort to parse them. As such,
RoundIconContainer was added.

Frontends don't need to round icons from RoundIconContainer, and as
such, may call IconContainer in the implementation. The choice of using
round icons is up to the backend implementations.
2020-06-30 19:43:42 -07:00
diamondburned (Forefront) d754f011ba Clarified RunCommand's documentation
This clarification applies on the latest release; however, no releases
will be made for it.
2020-06-29 19:57:11 -07:00
diamondburned (Forefront) a8cfc54f6d Breaking: Added stop callbacks to functions that take in containers
This breaking change was done to provide a clean API for backends to
remove event handlers when they're not needed anymore. It also moves the
cancellation logic from the backend to the frontend, making it easier
for backends.
2020-06-29 13:27:00 -07:00
diamondburned (Forefront) 88879d45f2 Added typing capabilities and indicator interfaces
This commit adds typing capabilities and indicator interfaces into
cchat. The objective is to provide an API for typing event APIs similar
to Discord's and IRCv3's.
2020-06-29 11:39:59 -07:00
diamondburned (Forefront) 88834ab465 Added MessageEditable into ServerMessageEditor
This is a breaking change, as it modifies the ServerMessageEditor
interface.

This breaking change is done with the assumption that not all services
will support editing every single message. For example, Discord only
allows editing your own messages.

With the introduction of a MessageEditable method, services shouldn't
have to return an error to indicate that a message isn't editable
anymore.
2020-06-27 23:31:34 -07:00
diamondburned (Forefront) 5d2cd4a57b MessageActions now takes in a message ID.
This change was done to add support to messages that may have more or
less actions. For example, this lets the backend only display the
"Delete" option when the message can be deleted.

Documentation is also corrected and further done in this commit.
2020-06-20 15:52:04 -07:00
diamondburned (Forefront) 6c7cd5feb2 Added UnreadIndicator and ServerMessageIndicator for unread indicators 2020-06-20 15:02:05 -07:00
diamondburned (Forefront) dfb60ac0eb text.Rich to implement Stringer; extra docs 2020-06-20 00:10:23 -07:00
diamondburned (Forefront) e953dbbcb1 Added extra methods onto Imager.
This is done instead of text substitution, as images might have contexts
and other important attributes. One such example is an emoji, which may
have worked with text substitution, but would also work with a simple
string append.
2020-06-17 20:45:30 -07:00
diamondburned (Forefront) 05b7a6a10c Specified that Images cannot do text substitution
This commit specifies that images cannot substitute a piece of text with
itself. This means that end bounds of those segments are ignored, and
images will be inserted where the start bounds point them to.

Frontends that can't actually display images should represent this
information in another way by itself. One way is to treat this as a
hyperlink that says "Image."
2020-06-17 17:34:38 -07:00
diamondburned (Forefront) ba88528a7a Removed text substitution from hyperlinks
This was done because handling text substitution would make rich text
renderers much more complex. Instead of lazily preprocessing all
segments into a list of attributes and lazily inserting them into a
new text, they would now have to account for text substitutions,
which would be overkill for a single link.

As a tradeoff, frontends that don't render rich texts and only use the
Content string will not see any URLs. Instead, it will only see the
underlying text of the URL, except without the actual hyperlink.
2020-06-17 17:02:56 -07:00
diamondburned (Forefront) d4917d2e6d Added Has to text.Attribute 2020-06-17 16:10:53 -07:00
diamondburned (Forefront) 6bb1d742a2 Clarified IO rules for ServerMessageEditor 2020-06-17 13:39:52 -07:00
diamondburned (Forefront) 7698aa5fc2 Removed some arguments such as context to reduce complexity 2020-06-14 18:46:59 -07:00
diamondburned (Forefront) 8768baf196 Added usage of context.Context into the API for cancellation 2020-06-14 16:00:18 -07:00
diamondburned (Forefront) 391004677b Added error handling behavior documentation for Disconnect 2020-06-13 16:33:33 -07:00
diamondburned (Forefront) 831a6ea7e6 Added Disconnect into the server requirement 2020-06-13 16:29:30 -07:00
diamondburned (Forefront) 4a7f7a7994 Added Raw into CompletionEntry 2020-06-10 17:38:59 -07:00
diamondburned (Forefront) b38dc6c6b4 Added extra docs 2020-06-10 16:10:03 -07:00
diamondburned (Forefront) 8fdf82883a Moved package split to utils 2020-06-10 12:19:13 -07:00
diamondburned (Forefront) 66dd2b1b11 Added split package to help with completions 2020-06-10 01:12:41 -07:00
diamondburned (Forefront) b6eca8eafa Improved documentation for CompleteEntry 2020-06-09 21:05:59 -07:00
diamondburned (Forefront) 2e7c6b0098 Added blocks into richtext and AttrDim into inline attributes 2020-06-09 20:51:54 -07:00
diamondburned (Forefront) 557ac54a04 Extended CompleteMessage 2020-06-09 20:38:44 -07:00
diamondburned (Forefront) 2eed8da97f Minor documentation fix 2020-06-09 20:25:10 -07:00
diamondburned (Forefront) 242f2c6192 Added clarifications on containers (applies to all versions) 2020-06-08 20:52:24 -07:00
diamondburned (Forefront) 72966ad02a Allow Names to be in rich text 2020-06-08 20:37:40 -07:00
diamondburned (Forefront) ce009a8cba Name is now required for some interfaces, improved README 2020-06-08 20:35:04 -07:00
diamondburned (Forefront) b4abf67cca Revert cancel callback on most containers 2020-06-08 15:49:53 -07:00
diamondburned (Forefront) a2235171a1 DoAction should not need to return a stop callback. 2020-06-08 14:46:36 -07:00
diamondburned (Forefront) 01e6e1ce31 Deprecated LeaveServer API in favor of cancel callbacks 2020-06-08 14:20:21 -07:00
diamondburned (Forefront) 7194d04894 Added MessagesContainer into DoMessageAction 2020-06-08 00:00:39 -07:00
diamondburned (Forefront) 231088e94d Added ServerMessage{Editor,Actioner} 2020-06-07 23:42:01 -07:00
diamondburned (Forefront) fa7f2fdca4 Fixed color hexadecimal invalid size 2020-06-03 21:15:52 -07:00
diamondburned (Forefront) deadc66d46 Added label and icon containers to allow updates, minor changes 2020-06-03 19:40:36 -07:00
diamondburned (Forefront) 3eb64db96c Fixed MessageCreate not having updated Author return type 2020-06-03 11:42:12 -07:00
diamondburned (Forefront) 46debdf53b Clarify that Commander extends Sessions 2020-06-03 00:05:14 -07:00
diamondburned (Forefront) 9b3e03753c Added full interface implementations for reference 2020-06-02 23:56:34 -07:00
diamondburned (Forefront) 5d3c3568f8 Changed MessageAuthor.Text to .Name 2020-06-02 23:32:24 -07:00
diamondburned (Forefront) f846071657 Updated README 2020-06-02 23:28:01 -07:00
diamondburned (Forefront) c1846a7796 Added MessageMentionable 2020-06-02 23:27:43 -07:00
diamondburned (Forefront) 3d2eaae6de Added MessageAuthor and Session.UserID() 2020-06-02 23:23:08 -07:00
diamondburned (Forefront) 6df6fab132 Added SessionSaver and SessionRestorer 2020-05-29 00:00:00 -07:00
diamondburned (Forefront) 4669362443 Added Multiline in AuthenticateEntry 2020-05-28 23:41:35 -07:00
diamondburned (Forefront) c49bea418a Added Authenticate() to allow resetting the auth process 2020-05-25 15:01:17 -07:00
35 changed files with 5023 additions and 319 deletions

13
LICENSE Normal file
View File

@ -0,0 +1,13 @@
Copyright 2020 diamondburned
Permission to use, copy, modify, and/or distribute this software for any purpose
with or without fee is hereby granted, provided that the above copyright notice
and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER
TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF
THIS SOFTWARE.

134
README.md
View File

@ -1,131 +1,27 @@
# cchat
# [cchat][godoc]
A set of stabilized interfaces for cchat implementations, joining the backend
and frontend together.
## Backend
Refer to the [GoDoc][godoc] for interfaces and documentations.
### Service
[godoc]: https://godoc.org/github.com/diamondburned/cchat
A service is a complete service that's capable of multiple sessions.
## Known implementations
#### Interfaces
The following sections contain known cchat implementations. PRs are welcomed for
more implementations to be added here.
- Authenticator
- Configurator (optional)
- Icon (optional)
### Backend
### Authenticator
- [diamondburned/cchat-mock](https://github.com/diamondburned/cchat-mock)
- A small subset of the cchat backend implementation mocked with fake data
for testing.
- [diamondburned/cchat-discord](https://github.com/diamondburned/cchat-discord)
- A Discord backend implementing cchat interfaces.
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.
### Frontend
```go
var s *cchat.Session
var err error
- [diamondburned/cchat-gtk](https://github.com/diamondburned/cchat-gtk)
- A GTK+3 implementation of a cchat frontend.
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
}
```
### Session
A session is returned after authentication on the service. Session implements
`Name()`, which should return the username most of the time.
#### Interfaces
- ServerList
- Icon (optional)
### Commander
The commander interface allows the backend to implement custom commands to
easily extend the API.
#### Interfaces
- CommandCompleter (optional)
### Identifier
The identifier interface forces whatever interface that embeds this one to be
uniquely identifiable.
### Configurator
The configurator interface is a way for the frontend to display configuration
options that the backend has.
### Server
A server is any entity that is usually a channel or a guild.
#### Interfaces
- ServerList and/or ServerMessage
- Icon (optional)
### ServerMessage
A server message is an entity that contains messages to be displayed. An example
would be channels in Discord and IRC.
#### Interfaces
- ServerMessageSender (optional): adds message sending capability.
- ServerMessageSendCompleter (optional): adds message completion capability.
### Messages
#### Interfaces
- MessageHeader: the minimum for a proper message.
- MessageCreate or MessageUpdate or MessageDelete
- MessageNonce (optional)
- MessageAuthorAvatar (optional)
## Frontend
### ServersContainer
A servers container is any type of view that displays the list of servers. It
should implement a `SetServers([]Server)` that the backend could use to call
anytime the server list changes (at all).
Typically, most frontend should implement this interface onto a tree node
instead of a tree view, as servers can be infinitely nested.
This interface expects the frontend to handle its own errors.
### MessagesContainer
A messages container is a view implementation that displays a list of messages
live. This implements the 3 most common message events: `CreateMessage`,
`UpdateMessage` and `DeleteMessage`. The frontend must handle all 3.
Since this container interface extends a single Server, the frontend is allowed
to have multiple views. This is usually done with tabs or splits, but the
backend should update them all nonetheless.
### SendableMessage
The frontend can make its own send message data implementation to indicate extra
capabilities that the backend may want.
An example of this is `MessageNonce`, which is similar to IRCv3's [labeled
response extension](https://ircv3.net/specs/extensions/labeled-response).
The frontend could implement this interface and check if incoming
`MessageCreate` events implement the same interface.

1009
cchat.go

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,126 @@
package main
import (
"fmt"
"log"
"os"
"path"
"path/filepath"
"sort"
"strings"
"github.com/dave/jennifer/jen"
"github.com/diamondburned/cchat/cmd/internal/cchat-generator/genutils"
"github.com/diamondburned/cchat/repository"
)
const OutputDir = "."
func init() {
log.SetFlags(0)
}
var comment = repository.Comment{Raw: `
Package empty provides no-op asserter method implementations of interfaces
in cchat's root and text packages.
`}
type Package struct {
Path string
repository.Package
}
func main() {
gen := genutils.NewFile("empty")
gen.PackageComment(comment.GoString(1))
// Sort.
var packages = make([]Package, 0, len(repository.Main))
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(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 := pkg.Interface(embed.InterfaceName); iface != nil {
genIfaceMethods(gen, *iface, ifaceName, pkg.Path)
}
}
genIfaceMethods(gen, iface, ifaceName, pkg.Path)
gen.Line()
}
}
f, err := os.Create(filepath.Join(os.Args[1], "empty.go"))
if err != nil {
log.Fatalln("Failed to create output file:", err)
}
defer f.Close()
if err := gen.Render(f); err != nil {
log.Fatalln("Failed to render output:", err)
}
}
func newIfaceName(pkgpath string, iface repository.Interface) string {
if pkgpath == repository.RootPath {
return iface.Name
} else {
return strings.Title(repository.TrimRoot(pkgpath)) + iface.Name
}
}
func genIfaceMethods(gen *jen.File, iface repository.Interface, ifaceName, pkgpath string) {
for _, method := range iface.Methods {
am, ok := method.(repository.AsserterMethod)
if !ok {
continue
}
name := fmt.Sprintf("As%s", am.ChildType)
gen.Comment(fmt.Sprintf("%s returns nil.", name))
stmt := jen.Func()
stmt.Parens(jen.Id(ifaceName))
stmt.Id(name)
stmt.Params()
stmt.Add(genutils.GenerateExternType(pkgpath, am))
stmt.Values(jen.Return(jen.Nil()))
gen.Add(stmt)
}
}
func hasAsserter(iface repository.Interface) bool {
for _, method := range iface.Methods {
if _, isA := method.(repository.AsserterMethod); isA {
return true
}
}
return false
}

View File

@ -0,0 +1,84 @@
package main
import (
"sort"
"github.com/dave/jennifer/jen"
"github.com/diamondburned/cchat/cmd/internal/cchat-generator/genutils"
"github.com/diamondburned/cchat/repository"
)
func generateEnums(enums []repository.Enumeration) jen.Code {
sort.Slice(enums, func(i, j int) bool {
return enums[i].Name < enums[j].Name
})
var stmt = new(jen.Statement)
for _, enum := range enums {
if !enum.Comment.IsEmpty() {
stmt.Comment(enum.Comment.GoString(1))
stmt.Line()
}
stmt.Type().Id(enum.Name).Id(enum.GoType())
stmt.Line()
stmt.Line()
stmt.Const().DefsFunc(func(group *jen.Group) {
for i, value := range enum.Values {
var c jen.Statement
if !value.Comment.IsEmpty() {
c.Comment(value.Comment.GoString(2))
c.Line()
}
c.Id(enum.Name + value.Name)
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)
}
})
stmt.Line()
stmt.Line()
var recv = genutils.RecvName(enum.Name)
if enum.Bitwise {
fn := stmt.Func()
fn.Params(jen.Id(recv).Id(enum.Name))
fn.Id("Has")
fn.Params(jen.Id("has").Id(enum.Name))
fn.Bool()
fn.BlockFunc(func(g *jen.Group) {
g.Return(jen.Id(recv).Op("&").Id("has").Op("==").Id("has"))
})
} 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()
stmt.Line()
}
return stmt
}

View File

@ -0,0 +1,179 @@
package main
import (
"sort"
"github.com/dave/jennifer/jen"
"github.com/diamondburned/cchat/cmd/internal/cchat-generator/genutils"
"github.com/diamondburned/cchat/repository"
)
func generateInterfaces(ifaces []repository.Interface) jen.Code {
sort.Slice(ifaces, func(i, j int) bool {
return ifaces[i].Name < ifaces[j].Name
})
var stmt = new(jen.Statement)
for _, iface := range ifaces {
if !iface.Comment.IsEmpty() {
stmt.Comment(iface.Comment.GoString(1))
stmt.Line()
}
stmt.Type().Id(iface.Name).InterfaceFunc(func(group *jen.Group) {
if len(iface.Embeds) > 0 {
for _, embed := range iface.Embeds {
if !embed.Comment.IsEmpty() {
group.Comment(embed.Comment.GoString(1))
}
group.Id(embed.InterfaceName)
}
group.Line()
}
// Put Asserter methods last.
sort.SliceStable(iface.Methods, func(i, j int) bool {
_, assert := iface.Methods[i].(repository.AsserterMethod)
return !assert
})
// Boolean to only write the Asserter comment once.
var writtenComment bool
for _, method := range iface.Methods {
if !writtenComment {
if _, ok := method.(repository.AsserterMethod); ok {
group.Line()
group.Comment("// Asserters.")
group.Line()
writtenComment = true
}
}
var stmt = new(jen.Statement)
if comment := method.UnderlyingComment(); !comment.IsEmpty() {
stmt.Comment(comment.GoString(1))
stmt.Line()
}
stmt.Id(method.UnderlyingName())
switch method := method.(type) {
case repository.GetterMethod:
stmt.Params(generateFuncParams(method.Parameters, "")...)
stmt.Params(generateFuncParams(method.Returns, method.ErrorType)...)
case repository.SetterMethod:
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(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)...)
case repository.AsserterMethod:
stmt.Params()
stmt.Params(genutils.GenerateType(method))
stmt.Comment("// Optional")
default:
continue
}
group.Add(stmt)
}
})
stmt.Line()
stmt.Line()
}
return stmt
}
func generateFuncParamsErr(param repository.NamedType, errorType string) []jen.Code {
stmt := make([]jen.Code, 0, 2)
if !param.IsZero() {
stmt = append(stmt, generateFuncParam(param))
}
if errorType != "" {
if param.Name == "" {
stmt = append(stmt, jen.Id(errorType))
} else {
stmt = append(stmt, jen.Err().Id(errorType))
}
}
return stmt
}
func generateFuncParam(param repository.NamedType) jen.Code {
if param.Name == "" {
return genutils.GenerateType(param)
}
return jen.Id(param.Name).Add(genutils.GenerateType(param))
}
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
}
var stmt jen.Statement
for _, param := range params {
stmt.Add(generateFuncParam(param))
}
if errorType != "" {
if params[0].Name != "" {
stmt.Add(jen.Err().Id(errorType))
} else {
stmt.Add(jen.Id(errorType))
}
}
return stmt
}
func generateContainerFuncReturns(method repository.ContainerMethod) []jen.Code {
var stmt jen.Statement
stmt.Add(jen.Id("stop").Func().Params())
stmt.Add(jen.Err().Error())
return stmt
}
func generateContainerFuncParams(method repository.ContainerMethod) []jen.Code {
var stmt jen.Statement
if method.HasContext {
stmt.Qual("context", "Context")
}
stmt.Add(genutils.GenerateType(method))
return stmt
}

View File

@ -0,0 +1,115 @@
package main
import (
"sort"
"github.com/dave/jennifer/jen"
"github.com/diamondburned/cchat/cmd/internal/cchat-generator/genutils"
"github.com/diamondburned/cchat/repository"
)
func generateStructs(structs []repository.Struct) jen.Code {
sort.Slice(structs, func(i, j int) bool {
return structs[i].Name < structs[j].Name
})
var stmt = new(jen.Statement)
for _, s := range structs {
stmt.Add(generateStruct(s))
stmt.Line()
stmt.Line()
}
return stmt
}
func generateErrorStructs(errStructs []repository.ErrorStruct) jen.Code {
sort.Slice(errStructs, func(i, j int) bool {
return errStructs[i].Name < errStructs[j].Name
})
var stmt = new(jen.Statement)
for _, errStruct := range errStructs {
stmt.Add(generateStruct(errStruct.Struct))
stmt.Line()
stmt.Line()
var recv = genutils.RecvName(errStruct.Name)
stmt.Func()
stmt.Params(jen.Id(recv).Id(errStruct.Name))
stmt.Id("Error").Params().String()
stmt.Block(jen.Return(generateTmplString(errStruct.ErrorString, recv)))
stmt.Line()
stmt.Line()
if wrap := errStruct.Wrapped(); wrap != "" {
stmt.Func()
stmt.Params(jen.Id(recv).Id(errStruct.Name))
stmt.Id("Unwrap").Params().Error()
stmt.Block(jen.Return(jen.Id(recv).Dot(wrap)))
stmt.Line()
stmt.Line()
}
}
return stmt
}
func generateStruct(s repository.Struct) jen.Code {
var stmt = new(jen.Statement)
if !s.Comment.IsEmpty() {
stmt.Comment(s.Comment.GoString(1))
stmt.Line()
}
stmt.Type().Id(s.Name).StructFunc(func(group *jen.Group) {
for _, field := range s.Fields {
var stmt = new(jen.Statement)
if field.Name != "" {
stmt.Id(field.Name)
}
stmt.Add(genutils.GenerateType(field))
group.Add(stmt)
}
})
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

@ -0,0 +1,30 @@
package main
import (
"sort"
"github.com/dave/jennifer/jen"
"github.com/diamondburned/cchat/cmd/internal/cchat-generator/genutils"
"github.com/diamondburned/cchat/repository"
)
func generateTypeAlises(aliases []repository.TypeAlias) jen.Code {
sort.Slice(aliases, func(i, j int) bool {
return aliases[i].Name < aliases[j].Name
})
var stmt = new(jen.Statement)
for _, alias := range aliases {
if !alias.Comment.IsEmpty() {
stmt.Comment(alias.Comment.GoString(1))
stmt.Line()
}
stmt.Type().Id(alias.Name).Op("=").Add(genutils.GenerateType(alias))
stmt.Line()
stmt.Line()
}
return stmt
}

View File

@ -0,0 +1,50 @@
package genutils
import (
"unicode"
"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)
}
// GenerateType generates a jen.Qual from the given Qualer.
func GenerateType(t Qualer) jen.Code {
path, name := t.Qual()
if path == "" {
return jen.Id(name)
}
return jen.Qual(path, name)
}
// GenerateExternType generates a jen.Qual from the given Qualer, except if
// the path is empty, root is used instead.
func GenerateExternType(root string, t Qualer) jen.Code {
path, name := t.Qual()
if path == "" {
return jen.Qual(root, name)
}
return jen.Qual(path, name)
}
// RecvName is used to get the receiver variable name. It returns the first
// letter lower-cased. It does NOT do length checking. It only works with ASCII.
func RecvName(name string) string {
return string(unicode.ToLower(rune(name[0])))
}

View File

@ -0,0 +1,69 @@
package main
import (
"log"
"os"
"path/filepath"
"strings"
"github.com/dave/jennifer/jen"
"github.com/diamondburned/cchat/cmd/internal/cchat-generator/genutils"
"github.com/diamondburned/cchat/repository"
)
const OutputDir = "."
func init() {
log.SetFlags(0)
}
func main() {
for pkgPath, pkg := range repository.Main {
g := generate(pkgPath, pkg)
destDir := filepath.Join(
os.Args[1],
filepath.FromSlash(trimPrefix(repository.RootPath, pkgPath)),
)
destFile := filepath.Base(pkgPath) + ".go"
// Guarantee that the directory exists.
if destDir != "" {
if err := os.MkdirAll(destDir, os.ModePerm); err != nil {
log.Fatalln("Failed to mkdir -p:", err)
}
}
f, err := os.Create(filepath.Join(destDir, destFile))
if err != nil {
log.Fatalln("Failed to create output file:", err)
}
if err := g.Render(f); err != nil {
log.Fatalln("Failed to render output:", err)
}
f.Close()
}
}
func trimPrefix(rootPrefix, path string) string {
return strings.Trim(strings.TrimPrefix(path, rootPrefix), "/")
}
func generate(pkgPath string, repo repository.Package) *jen.File {
gen := genutils.NewFilePath(pkgPath)
gen.PackageComment(repo.Comment.GoString(1))
gen.Add(generateTypeAlises(repo.TypeAliases))
gen.Line()
gen.Add(generateEnums(repo.Enums))
gen.Line()
gen.Add(generateStructs(repo.Structs))
gen.Line()
gen.Add(generateErrorStructs(repo.ErrorStructs))
gen.Line()
gen.Add(generateInterfaces(repo.Interfaces))
gen.Line()
return gen
}

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

18
generator.go Normal file
View File

@ -0,0 +1,18 @@
package cchat
//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}
}

6
go.mod
View File

@ -2,4 +2,8 @@ module github.com/diamondburned/cchat
go 1.14
require github.com/pkg/errors v0.9.1
require (
github.com/dave/jennifer v1.4.1
github.com/go-test/deep v1.0.7
github.com/pkg/errors v0.9.1
)

4
go.sum
View File

@ -1,2 +1,6 @@
github.com/dave/jennifer v1.4.1 h1:XyqG6cn5RQsTj3qlWQTKlRGAyrTcsk1kUmWdZBzRjDw=
github.com/dave/jennifer v1.4.1/go.mod h1:7jEdnm+qBcxl8PC0zyp7vxcpSRnzXSt9r39tpTVGlwA=
github.com/go-test/deep v1.0.7 h1:/VSMRlnY/JSyqxQUzQLKVMAskpY/NZKFA5j2P+0pP2M=
github.com/go-test/deep v1.0.7/go.mod h1:QV8Hv/iy04NyLBxAdO9njL0iVPN1S4d/A3NVv1V36o8=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=

112
repository/comment.go Normal file
View File

@ -0,0 +1,112 @@
package repository
import (
"bytes"
"go/doc"
"regexp"
"strings"
)
var (
// commentTrimSurrounding is a regex to trim surrounding new line and tabs.
// This is needed to find the correct level of indentation.
commentTrimSurrounding = regexp.MustCompile(`(^\n)|(\n\t+$)`)
)
// TabWidth is used to format comments.
var TabWidth = 4
// Comment represents a raw comment string. Most use cases should use GoString()
// to get the comment's content.
type Comment struct {
Raw string
}
// IsEmpty returns true if the comment is empty.
func (c Comment) IsEmpty() bool {
return c.Raw == ""
}
// GoString formats the documentation string in 80 columns wide paragraphs and
// prefix each line with "// ". The ident argument controls the nested level. If
// less than or equal to zero, then it is changed to 1, which is the top level.
func (c Comment) GoString(ident int) string {
if c.Raw == "" {
return ""
}
if ident < 1 {
ident = 1
}
ident-- // 0th-indexed
ident *= TabWidth
var lines = strings.Split(c.WrapText(80-len("// ")-ident), "\n")
for i, line := range lines {
if line != "" {
line = "// " + line
} else {
line = "//"
}
lines[i] = line
}
return strings.Join(lines, "\n")
}
// WrapText wraps the raw text in n columns wide paragraphs.
func (c Comment) WrapText(column int) string {
var txt = c.Unindent()
if txt == "" {
return ""
}
buf := bytes.Buffer{}
doc.ToText(&buf, txt, "", strings.Repeat(" ", TabWidth-1), column)
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
// RawText. It gets the lowest indentation level from each line and trim it.
func (c Comment) Unindent() string {
if c.IsEmpty() {
return ""
}
// Trim new lines.
txt := commentTrimSurrounding.ReplaceAllString(c.Raw, "")
// Split the lines and rejoin them later to trim the indentation.
var lines = strings.Split(txt, "\n")
var indent = 0
// Get the minimum indentation count.
for _, line := range lines {
linedent := strings.Count(line, "\t")
if linedent < 0 {
continue
}
if linedent < indent || indent == 0 {
indent = linedent
}
}
// Trim the indentation.
if indent > 0 {
for i, line := range lines {
if len(line) > 0 {
lines[i] = line[indent-1:]
}
}
}
// Rejoin.
txt = strings.Join(lines, "\n")
return txt
}

View File

@ -0,0 +1,27 @@
package repository
import (
"testing"
"github.com/go-test/deep"
)
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 goComment = _goComment[1:]
func TestComment(t *testing.T) {
var authenticator = Main[RootPath].Interface("Authenticator")
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)
}
})
}

49
repository/enum.go Normal file
View File

@ -0,0 +1,49 @@
package repository
// Enumeration returns a Go enumeration.
type Enumeration struct {
Comment Comment
Name string
Values []EnumValue
// 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
// returns an empty string if the length of values is overbound.
//
// The maximum number of values in a normal enum is math.MaxUint8 or 255. The
// maximum number of values in a bitwise enum is 32 for 32 bits in a uint32.
func (e Enumeration) GoType() string {
if !e.Bitwise {
if len(e.Values) > 255 {
return ""
}
return "uint8"
}
if len(e.Values) > 32 {
return ""
}
return "uint32"
}
type EnumValue struct {
Comment Comment
Name string // also return value from String()
}
// IsPlaceholder returns true if the enumeration value is meant to be a
// placeholder. In Go, it would look like this:
//
// const (
// _ EnumType = iota // IsPlaceholder() == true
// V1
// )
//
func (v EnumValue) IsPlaceholder() bool {
return v.Name == ""
}

View File

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

Binary file not shown.

168
repository/interface.go Normal file
View File

@ -0,0 +1,168 @@
package repository
import (
"encoding/gob"
"strings"
)
func init() {
gob.Register(ContainerMethod{})
gob.Register(AsserterMethod{})
gob.Register(GetterMethod{})
gob.Register(SetterMethod{})
gob.Register(ContainerUpdaterMethod{})
gob.Register(IOMethod{})
}
type Interface struct {
Comment Comment
Name string
Embeds []EmbeddedInterface
Methods []Method // actual methods
}
type EmbeddedInterface struct {
Comment Comment
InterfaceName string
}
// IsContainer returns true if the interface is a frontend container interface,
// that is when its name has "Container" at the end.
func (i Interface) IsContainer() bool {
return strings.HasSuffix(i.Name, "Container")
}
type Method interface {
UnderlyingName() string
UnderlyingComment() Comment
internalMethod()
}
type method struct {
Comment Comment
Name string
}
func (m method) UnderlyingName() string { return m.Name }
func (m method) UnderlyingComment() Comment { return m.Comment }
func (m method) internalMethod() {}
// GetterMethod is a method that returns a regular value. These methods must not
// do any IO. An example of one would be ID() returning ID.
type GetterMethod struct {
method
// Parameters is the list of parameters in the function.
Parameters []NamedType
// Returns is the list of named types returned from the function.
Returns []NamedType
// 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.
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. 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
// Parameters is the list of parameters in the function.
Parameters []NamedType
// ReturnValue is the return value in the function.
ReturnValue NamedType
// 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
}
// 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
// HasContext is true if the method accepts a context as its first argument.
HasContext bool
// ContainerType is the name of the container interface. The name will
// almost always have "Container" as its suffix.
ContainerType string
}
// Qual returns what TypeQual returns with m.ContainerType.
func (m ContainerMethod) Qual() (path, name string) {
return TypeQual(m.ContainerType)
}
// AsserterMethod is a method that allows the parent interface to extend itself
// into children interfaces. These methods must not do IO.
type AsserterMethod struct {
// ChildType is the children type that is returned.
ChildType string
}
func (m AsserterMethod) internalMethod() {}
func (m AsserterMethod) UnderlyingComment() Comment { return Comment{} }
// UnderlyingName returns the name of the method.
func (m AsserterMethod) UnderlyingName() string {
return "As" + m.ChildType
}
// Qual returns what TypeQual returns with m.ChildType.
func (m AsserterMethod) Qual() (path, name string) {
return TypeQual(m.ChildType)
}

2002
repository/main.go Normal file

File diff suppressed because it is too large Load Diff

29
repository/main_test.go Normal file
View File

@ -0,0 +1,29 @@
package repository
import (
"bytes"
"encoding/gob"
"testing"
"github.com/go-test/deep"
)
func TestGob(t *testing.T) {
var buf bytes.Buffer
if err := gob.NewEncoder(&buf).Encode(Main); err != nil {
t.Fatal("Failed to gob encode:", err)
}
t.Log("Marshaled; total bytes:", buf.Len())
var unmarshaled Packages
if err := gob.NewDecoder(&buf).Decode(&unmarshaled); err != nil {
t.Fatal("Failed to gob decode:", err)
}
if eq := deep.Equal(Main, unmarshaled); eq != nil {
t.Fatal("Inequalities after unmarshaling:", eq)
}
}

158
repository/repository.go Normal file
View File

@ -0,0 +1,158 @@
package repository
import (
"fmt"
"path"
"strings"
)
// MakePath returns RootPath joined with relPath.
func MakePath(relPath string) string {
return path.Join(RootPath, relPath)
}
// MakeQual returns a qualified identifier that is the full path and name of
// something.
func MakeQual(relPath, name string) string {
return fmt.Sprintf("(%s).%s", MakePath(relPath), name)
}
// TrimRoot trims the root path and returns the path relative to root.
func TrimRoot(fullPath string) string {
return strings.TrimPrefix(strings.TrimPrefix(fullPath, RootPath), "/")
}
// Packages maps Go module paths to packages.
type Packages map[string]Package
type Package struct {
Comment Comment
Enums []Enumeration
TypeAliases []TypeAlias
Structs []Struct
ErrorStructs []ErrorStruct
Interfaces []Interface
}
// Interface finds an interface. Nil is returned if none is found.
func (p Package) Interface(name string) *Interface {
for i, iface := range p.Interfaces {
if iface.Name == name {
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
}
// Qual splits the type name into the path and type name. Refer to TypeQual.
func (t NamedType) Qual() (path, typeName string) {
return TypeQual(t.Type)
}
// IsZero is true if t.Type is empty.
func (t NamedType) IsZero() bool {
return t.Type == ""
}
// TypeQual splits the type name into path and type name. It accepts inputs that
// are similar to the example below:
//
// string
// context.Context
// github.com/diamondburned/cchat/text.Rich
// (github.com/diamondburned/cchat/text).Rich
//
func TypeQual(typePath string) (path, typeName string) {
parts := strings.Split(typePath, ".")
if len(parts) > 1 {
path = strings.Join(parts[:len(parts)-1], ".")
path = strings.TrimPrefix(path, "(")
path = strings.TrimSuffix(path, ")")
typeName = parts[len(parts)-1]
return
}
typeName = typePath
return
}
// TmplString is a generation-time templated string. It is used for string
// concatenation.
//
// Given the following TmplString:
//
// TmplString{Format: "Hello, %s", Fields: []string{"Foo()"}}
//
// The output should be the same as the output of
//
// fmt.Sprintf("Hello, %s", v.Foo())
//
type TmplString struct {
Format string // printf format syntax
Fields []string // list of struct fields
}
// 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)
}
}
}

38
repository/struct.go Normal file
View File

@ -0,0 +1,38 @@
package repository
type Struct struct {
Comment Comment
Name string
Fields []StructField
Stringer Stringer // used for String()
}
type StructField struct {
Comment
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 {
Struct
ErrorString TmplString // used for Error()
}
// Wrapped returns true if the error struct contains a field with the error
// type.
func (t ErrorStruct) Wrapped() (fieldName string) {
for _, ret := range t.Struct.Fields {
if ret.Type == "error" {
return ret.Name
}
}
return ""
}

7
repository/type.go Normal file
View File

@ -0,0 +1,7 @@
package repository
// TypeAlias represents a Go type alias.
type TypeAlias struct {
Comment Comment
NamedType
}

View File

@ -1,41 +1,53 @@
// Code generated by ./cmd/internal. DO NOT EDIT.
// Package text provides a rich text API for cchat interfaces to use.
//
//
// Asserting
//
// Although interfaces here contain asserter methods similarly to cchat, the
// backend should take care to not implement multiple interfaces that may seem
// conflicting. For example, if Avatarer is already implemented, then Imager
// shouldn't be.
package text
// Attribute is the type for basic rich text markup attributes.
type Attribute uint32
const (
// Normal is a zero-value attribute.
AttributeNormal Attribute = 0
// Bold represents bold text.
AttributeBold Attribute = 1 << iota
// Italics represents italicized text.
AttributeItalics
// Underline represents underlined text.
AttributeUnderline
// Strikethrough represents struckthrough text.
AttributeStrikethrough
// Spoiler represents spoiler text, which usually looks blacked out until
// hovered or clicked on.
AttributeSpoiler
// Monospace represents monospaced text, typically for inline code.
AttributeMonospace
// Dimmed represents dimmed text, typically slightly less visible than other
// text.
AttributeDimmed
)
func (a Attribute) Has(has Attribute) bool {
return a&has == has
}
// Rich is a normal text wrapped with optional format segments.
type Rich struct {
Content string
// Segments are optional rich-text segment markers.
Content string
Segments []Segment
}
func (r Rich) Empty() bool {
return r.Content == ""
}
// Segment is the minimum requirement for a format segment. Frontends will use
// this to determine when the format starts and ends. They will also assert this
// interface to any other formatting interface, including Linker, Colorer and
// Attributor.
type Segment interface {
Bounds() (start, end int)
}
// Linker is a hyperlink format that a segment could implement. This implies
// that the segment should be replaced with a hyperlink, similarly to the anchor
// tag with href being the URL and the inner text being the text string.
type Linker interface {
Link() (text, url string)
}
// Imager implies the segment should be replaced with a (possibly inlined)
// image.
type Imager interface {
Image() (url string)
}
// Colorer is a text color format that a segment could implement. This is to be
// applied directly onto the text.
type Colorer interface {
Color() uint16
// 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
@ -44,15 +56,127 @@ type Attributor interface {
Attribute() Attribute
}
// Attribute is the type for basic rich text markup attributes.
type Attribute uint16
// Avatarer implies the segment should be replaced with a rounded-corners image.
// This works similarly to Imager.
//
// For segments that also implement mentioner, the image should be treated as a
// round avatar.
type Avatarer interface {
// AvatarText returns the underlying text of the image. Frontends could use this
// for hovering or displaying the text instead of the image.
AvatarText() string
// AvatarSize returns the requested dimension for the image. This function could
// return (0, 0), which the frontend should use the avatar's dimensions.
AvatarSize() (size int)
// Avatar returns the URL for the image.
Avatar() (url string)
}
const (
AttrBold Attribute = 1 << iota
AttrItalics
AttrUnderline
AttrStrikethrough
AttrSpoiler
AttrMonospace
AttrQuoted
)
// Codeblocker is a codeblock that supports optional syntax highlighting using
// the language given. Note that as this is a block, it will appear separately
// from the rest of the paragraph.
//
// This interface is equivalent to Markdown's codeblock syntax.
type Codeblocker interface {
CodeblockLanguage() (language string)
}
// Colorer is a text color format that a segment could implement. This is to be
// applied directly onto the text.
//
// The Color method must return a valid 32-bit RGBA color. That is, if the text
// color is solid, then the alpha value must be 0xFF. Frontends that support
// 32-bit colors must render alpha accordingly without any edge cases.
type Colorer interface {
// Color returns a 32-bit RGBA color.
Color() uint32
}
// Imager implies the segment should be replaced with a (possibly inlined)
// 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.
type Imager interface {
// ImageText returns the underlying text of the image. Frontends could use this
// for hovering or displaying the text instead of the image.
ImageText() string
// ImageSize returns the requested dimension for the image. This function could
// return (0, 0), which the frontend should use the image's dimensions.
ImageSize() (w int, h int)
// Image returns the URL for the image.
Image() (url string)
}
// Linker is a hyperlink format that a segment could implement. This implies
// that the segment should be replaced with a hyperlink, similarly to the anchor
// tag with href being the URL and the inner text being the text string.
type Linker interface {
Link() (url string)
}
// Mentioner implies that the segment can be clickable, and when clicked it
// should open up a dialog containing information from MentionInfo().
//
// It is worth mentioning that frontends should assume whatever segment that
// Mentioner highlighted to be the display name of that user. This would allow
// frontends to flexibly layout the labels.
type Mentioner interface {
// MentionInfo returns the popup information of the mentioned segment. This is
// typically user information or something similar to that context.
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.
type Quoteblocker interface {
// QuotePrefix returns the prefix that every line the segment covers have. This
// is typically the greater-than sign ">" in Markdown. Frontends could use this
// information to format the quote properly.
QuotePrefix() (prefix string)
}
// Segment is the minimum requirement for a format segment. Frontends will use
// this to determine when the format starts and ends. They will also assert this
// interface to any other formatting interface, including Linker, Colorer and
// Attributor.
//
// Note that a segment may implement multiple interfaces. For example, a
// Mentioner may also implement Colorer.
type Segment interface {
Bounds() (start int, end int)
// 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
AsMessageReferencer() MessageReferencer // Optional
}

19
text/text_extras.go Normal file
View File

@ -0,0 +1,19 @@
package text
// Plain creates a new text.Rich without any formatting segments.
func Plain(text string) Rich {
return Rich{Content: text}
}
// 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 << 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
}

130
utils/empty/empty.go Normal file
View File

@ -0,0 +1,130 @@
// 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.
package empty
import (
"github.com/diamondburned/cchat"
"github.com/diamondburned/cchat/text"
)
// Service provides no-op asserters for cchat.Service.
type Service struct{}
// AsConfigurator returns nil.
func (Service) AsConfigurator() cchat.Configurator { return nil }
// AsSessionRestorer returns nil.
func (Service) AsSessionRestorer() cchat.SessionRestorer { return nil }
// Session provides no-op asserters for cchat.Session.
type Session struct{}
// AsCommander returns nil.
func (Session) AsCommander() cchat.Commander { return nil }
// AsSessionSaver returns nil.
func (Session) AsSessionSaver() cchat.SessionSaver { return nil }
// Commander provides no-op asserters for cchat.Commander.
type Commander struct{}
// AsCompleter returns nil.
func (Commander) AsCompleter() cchat.Completer { return nil }
// Server provides no-op asserters for cchat.Server.
type Server struct{}
// AsLister returns nil.
func (Server) AsLister() cchat.Lister { return nil }
// AsMessenger returns nil.
func (Server) AsMessenger() cchat.Messenger { return nil }
// AsCommander returns nil.
func (Server) AsCommander() cchat.Commander { return nil }
// AsConfigurator returns nil.
func (Server) AsConfigurator() cchat.Configurator { return nil }
// Messenger provides no-op asserters for cchat.Messenger.
type Messenger struct{}
// AsSender returns nil.
func (Messenger) AsSender() cchat.Sender { return nil }
// AsEditor returns nil.
func (Messenger) AsEditor() cchat.Editor { return nil }
// AsActioner returns nil.
func (Messenger) AsActioner() cchat.Actioner { return nil }
// AsNicknamer returns nil.
func (Messenger) AsNicknamer() cchat.Nicknamer { return nil }
// AsBacklogger returns nil.
func (Messenger) AsBacklogger() cchat.Backlogger { return nil }
// AsMemberLister returns nil.
func (Messenger) AsMemberLister() cchat.MemberLister { return nil }
// AsUnreadIndicator returns nil.
func (Messenger) AsUnreadIndicator() cchat.UnreadIndicator { return nil }
// AsTypingIndicator returns nil.
func (Messenger) AsTypingIndicator() cchat.TypingIndicator { return nil }
// Sender provides no-op asserters for cchat.Sender.
type Sender struct{}
// AsCompleter returns nil.
func (Sender) AsCompleter() cchat.Completer { return nil }
// MemberSection provides no-op asserters for cchat.MemberSection.
type MemberSection struct{}
// AsMemberDynamicSection returns nil.
func (MemberSection) AsMemberDynamicSection() cchat.MemberDynamicSection { return nil }
// SendableMessage provides no-op asserters for cchat.SendableMessage.
type SendableMessage struct{}
// AsNoncer returns nil.
func (SendableMessage) AsNoncer() cchat.Noncer { 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 }

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

40
utils/split/split.go Normal file
View File

@ -0,0 +1,40 @@
// Package split provides a simple string splitting utility for use with
// CompleteMessage.
package split
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}
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.
// 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 := int64(asciiSpace[r])
n += wasSpace & ^isSpace
wasSpace = isSpace
}
return n, setBits >= utf8.RuneSelf
}

32
utils/split/split_test.go Normal file
View File

@ -0,0 +1,32 @@
package split
import "testing"
type testEntry struct {
input string
offset int64
output []string
index int64
}
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
}
for i := 0; i < len(s1); i++ {
if s1[i] != s2[i] {
return false
}
}
return true
}