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.
This commit is contained in:
diamondburned 2020-09-26 18:24:56 -07:00
parent 8b8c46a714
commit 8e9321928b
17 changed files with 1610 additions and 1 deletions

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=

68
repository/comment.go Normal file
View File

@ -0,0 +1,68 @@
package repository
import (
"bytes"
"go/doc"
"regexp"
"strings"
)
var (
// commentTrimSurrounding is a regex to trim surrounding new line and tabs.
// This is needed to find the correct level of indentation.
commentTrimSurrounding = regexp.MustCompile(`(^\n)|(\n\t+$)`)
)
// Comment represents a raw comment string. Most use cases should use GoString()
// to get the comment's content.
type Comment struct {
RawText string
}
// GoString formats the documentation string in 80 columns wide paragraphs.
func (c Comment) GoString() string {
return c.WrapText(80)
}
// WrapText wraps the raw text in n columns wide paragraphs.
func (c Comment) WrapText(column int) string {
var buf bytes.Buffer
doc.ToText(&buf, c.Unindent(), "", "\t", column)
return buf.String()
}
// Unindent removes the indentations that were there for the sake of syntax in
// RawText. It gets the lowest indentation level from each line and trim it.
func (c Comment) Unindent() string {
// Trim new lines.
txt := commentTrimSurrounding.ReplaceAllString(c.RawText, "")
// Split the lines and rejoin them later to trim the indentation.
var lines = strings.Split(txt, "\n")
var indent = 0
// Get the minimum indentation count.
for _, line := range lines {
linedent := strings.Count(line, "\t")
if linedent < 0 {
continue
}
if linedent < indent || indent == 0 {
indent = linedent
}
}
// Trim the indentation.
if indent > 0 {
for i, line := range lines {
if len(line) > 0 {
lines[i] = line[indent-1:]
}
}
}
// Rejoin.
txt = strings.Join(lines, "\n")
return txt
}

View File

@ -0,0 +1,41 @@
package repository
import (
"testing"
"github.com/go-test/deep"
)
const _comment = `
The authenticator interface allows for a multistage initial authentication API
that the backend could use. Multistage is done by calling AuthenticateForm then
Authenticate again forever until no errors are returned.
var s *cchat.Session
var err error
for {
// Pseudo-function to render the form and return the results of those
// forms when the user confirms it.
outputs := renderAuthForm(svc.AuthenticateForm())
s, err = svc.Authenticate(outputs)
if err != nil {
renderError(errors.Wrap(err, "Error while authenticating"))
continue // retry
}
break // success
}`
// Trim away the prefix new line.
var comment = _comment[1:]
func TestComment(t *testing.T) {
var authenticator = Main["cchat"].Interface("Authenticator")
var authDoc = authenticator.Comment.GoString()
if eq := deep.Equal(comment, authDoc); eq != nil {
t.Fatal("Comment inequality:", eq)
}
}

25
repository/enum.go Normal file
View File

@ -0,0 +1,25 @@
package repository
type Enumeration struct {
Comment
Name string
Values []EnumValue
Bitwise bool
}
type EnumValue struct {
Comment
Name string // also return value from String()
}
// IsPlaceholder returns true if the enumeration value is meant to be a
// placeholder. In Go, it would look like this:
//
// const (
// _ EnumType = iota // IsPlaceholder() == true
// V1
// )
//
func (v EnumValue) IsPlaceholder() bool {
return v.Name == ""
}

17
repository/error.go Normal file
View File

@ -0,0 +1,17 @@
package repository
type ErrorType struct {
Struct
ErrorString TmplString // used for Error()
}
// Wrapped returns true if the error struct contains a field with the error
// type.
func (t ErrorType) Wrapped() bool {
for _, ret := range t.Struct.Fields {
if ret.Type == "error" {
return true
}
}
return false
}

View File

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

View File

@ -0,0 +1,24 @@
// +build ignore
package main
import (
"encoding/gob"
"log"
"os"
"github.com/diamondburned/cchat/repository"
)
func main() {
f, err := os.Create("repository.gob")
if err != nil {
log.Fatalln("Failed to create file:", err)
}
defer f.Close()
if err := gob.NewEncoder(f).Encode(repository.Main); err != nil {
os.Remove("repository.gob")
log.Fatal("Failed to gob encode:", err)
}
}

Binary file not shown.

21
repository/interface.go Normal file
View File

@ -0,0 +1,21 @@
package repository
import "strings"
type Interface struct {
Comment
Name string
Embeds []EmbeddedInterface
Methods []Method // actual methods
}
type EmbeddedInterface struct {
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")
}

1207
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 Repositories
if err := gob.NewDecoder(&buf).Decode(&unmarshaled); err != nil {
t.Fatal("Failed to gob decode:", err)
}
if eq := deep.Equal(Main, unmarshaled); eq != nil {
t.Fatal("Inequalities after unmarshaling:", eq)
}
}

91
repository/method.go Normal file
View File

@ -0,0 +1,91 @@
package repository
import "encoding/gob"
func init() {
gob.Register(ContainerMethod{})
gob.Register(AsserterMethod{})
gob.Register(GetterMethod{})
gob.Register(SetterMethod{})
gob.Register(IOMethod{})
}
type Method interface {
UnderlyingName() string
internalMethod()
}
type RegularMethod struct {
Comment
Name string
}
func (m RegularMethod) UnderlyingName() string { return m.Name }
func (m RegularMethod) internalMethod() {}
// GetterMethod is a method that returns a regular value. These methods must not
// do any IO. An example of one would be ID() returning ID.
type GetterMethod struct {
RegularMethod
// Parameters is the list of parameters in the function.
Parameters []NamedType
// Returns is the list of named types returned from the function.
Returns []NamedType
// ReturnError is true if the function returns an error at the end of
// returns.
ReturnError bool
}
// SetterMethod is a method that sets values. These methods must not do IO, and
// they have to be non-blocking.
type SetterMethod struct {
RegularMethod
// Parameters is the list of parameters in the function. These parameters
// should be the parameters to set.
Parameters []NamedType
}
// IOMethod is a regular method that can do IO and thus is blocking. These
// methods usually always return errors.
type IOMethod struct {
RegularMethod
// Parameters is the list of parameters in the function.
Parameters []NamedType
// ReturnValue is the return value in the function.
ReturnValue NamedType
// ReturnError is true if the function returns an error at the end of
// returns.
ReturnError bool
}
// ContainerMethod is a method that uses a Container. These methods can do IO.
type ContainerMethod struct {
RegularMethod
// HasContext is true if the method accepts a context as its first argument.
HasContext bool
// ContainerType is the name of the container interface. The name will
// almost always have "Container" as its suffix.
ContainerType string
// HasStopFn is true if the function returns a callback of type func() as
// its first return. The function will return an error in addition. If this
// is false, then only the error is returned.
HasStopFn bool
}
// AsserterMethod is a method that allows the parent interface to extend itself
// into children interfaces. These methods must not do IO.
type AsserterMethod struct {
// ChildType is the children type that is returned.
ChildType string
}
func (m AsserterMethod) internalMethod() {}
// UnderlyingName returns the name of the method.
func (m AsserterMethod) UnderlyingName() string {
return "As" + m.ChildType
}

34
repository/repository.go Normal file
View File

@ -0,0 +1,34 @@
package repository
// MainNamespace is the name of the namespace that should be top level.
const MainNamespace = "cchat"
type Repositories map[string]Repository
type Repository struct {
Enums []Enumeration
TypeAliases []TypeAlias
Structs []Struct
ErrorTypes []ErrorType
Interfaces []Interface
}
// Interface finds an interface. Nil is returned if none is found.
func (r Repository) Interface(name string) *Interface {
for _, iface := range r.Interfaces {
if iface.Name == name {
return &iface
}
}
return nil
}
type NamedType struct {
Name string // optional
Type string
}
// IsZero is true if t.Type is empty.
func (t NamedType) IsZero() bool {
return t.Type == ""
}

12
repository/struct.go Normal file
View File

@ -0,0 +1,12 @@
package repository
type Struct struct {
Comment
Name string
Fields []StructField
}
type StructField struct {
Comment
NamedType
}

21
repository/tmplstring.go Normal file
View File

@ -0,0 +1,21 @@
package repository
// TmplString is a generation-time templated string. It is used for string
// concatenation.
//
// Given the following TmplString:
//
// TmplString{Receiver: "v", Template: "Hello, {v.Foo()}"}
//
// The output of String() should be the same as the output of
//
// "Hello, " + v.Foo()
//
type TmplString struct {
Receiver string
Template string
}
func (s TmplString) String() string {
panic("TODO")
}

8
repository/type.go Normal file
View File

@ -0,0 +1,8 @@
package repository
// TypeAlias represents a Go type alias.
type TypeAlias struct {
Comment
Name string
Type string
}