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
This commit is contained in:
diamondburned 2020-10-27 13:33:52 -07:00
parent c32c50c0e8
commit 955b99c9b6
11 changed files with 195 additions and 133 deletions

View File

@ -180,31 +180,34 @@ type Attachments interface {
Attachments() []MessageAttachment
}
// AuthenticateError is the error returned when authenticating. This error
// interface extends the normal error to allow backends to implement multi-stage
// authentication if needed in a clean way without needing any loops.
//
// This interface satisfies the error interface.
type AuthenticateError interface {
// NextStage optionally returns a slice of Authenticator interfaces if the
// authentication process requires another stage. It works similarly to
// Service's Authenticate method, both of which returns a slice of
// Authenticators.
//
// If the error returned is an actual error, and that the user should retry any
// of the authentication fields, then NextStage could return nil to signify the
// error. The frontend could reliably check nil on this field to determine
// whether or not it should recreate the authentication fields.
NextStage() []Authenticator
// Error returns the error as a string. This method makes AuthenticateError
// satisfy the built-in error interface.
Error() string
}
// The authenticator interface allows for a multistage initial authentication
// API that the backend could use. Multistage is done by calling
// AuthenticateForm then Authenticate again forever until no errors are
// returned.
//
// var s *cchat.Session
// var err error
//
// for {
// // Pseudo-function to render the form and return the results of those
// // forms when the user confirms it.
// outputs := renderAuthForm(svc.AuthenticateForm())
//
// s, err = svc.Authenticate(outputs)
// if err != nil {
// renderError(errors.Wrap(err, "Error while authenticating"))
// continue // retry
// }
//
// break // success
// }
// API that the backend could use. Multistage is done by calling Authenticate
// and check for AuthenticateError's NextStage method.
type Authenticator interface {
// Authenticate will be called with a list of values with indices correspond to
// the returned slice of AuthenticateEntry.
Authenticate([]string) (Session, error) // Blocking
Authenticate([]string) (Session, AuthenticateError) // Blocking
// AuthenticateForm should return a list of authentication entries for the
// frontend to render.
AuthenticateForm() []AuthenticateEntry

View File

@ -62,13 +62,13 @@ func generateInterfaces(ifaces []repository.Interface) jen.Code {
switch method := method.(type) {
case repository.GetterMethod:
stmt.Params(generateFuncParams(method.Parameters, false)...)
stmt.Params(generateFuncParams(method.Returns, method.ReturnError)...)
stmt.Params(generateFuncParams(method.Parameters, "")...)
stmt.Params(generateFuncParams(method.Returns, method.ErrorType)...)
case repository.SetterMethod:
stmt.Params(generateFuncParams(method.Parameters, false)...)
stmt.Params(generateFuncParams(method.Parameters, "")...)
case repository.IOMethod:
stmt.Params(generateFuncParams(method.Parameters, false)...)
stmt.Params(generateFuncParamErr(method.ReturnValue, method.ReturnError)...)
stmt.Params(generateFuncParams(method.Parameters, "")...)
stmt.Params(generateFuncParamErr(method.ReturnValue, method.ErrorType)...)
stmt.Comment("// Blocking")
case repository.ContainerMethod:
stmt.Params(generateContainerFuncParams(method)...)
@ -92,18 +92,18 @@ func generateInterfaces(ifaces []repository.Interface) jen.Code {
return stmt
}
func generateFuncParamErr(param repository.NamedType, genErr bool) []jen.Code {
func generateFuncParamErr(param repository.NamedType, errorType string) []jen.Code {
stmt := make([]jen.Code, 0, 2)
if !param.IsZero() {
stmt = append(stmt, generateFuncParam(param))
}
if genErr {
if errorType != "" {
if param.Name == "" {
stmt = append(stmt, jen.Error())
stmt = append(stmt, jen.Id(errorType))
} else {
stmt = append(stmt, jen.Err().Error())
stmt = append(stmt, jen.Err().Id(errorType))
}
}
@ -117,7 +117,7 @@ func generateFuncParam(param repository.NamedType) jen.Code {
return jen.Id(param.Name).Add(genutils.GenerateType(param))
}
func generateFuncParams(params []repository.NamedType, withError bool) []jen.Code {
func generateFuncParams(params []repository.NamedType, errorType string) []jen.Code {
if len(params) == 0 {
return nil
}
@ -127,11 +127,11 @@ func generateFuncParams(params []repository.NamedType, withError bool) []jen.Cod
stmt.Add(generateFuncParam(param))
}
if withError {
if errorType != "" {
if params[0].Name != "" {
stmt.Add(jen.Err().Error())
stmt.Add(jen.Err().Id(errorType))
} else {
stmt.Add(jen.Error())
stmt.Add(jen.Id(errorType))
}
}

View File

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

View File

@ -1,3 +1,17 @@
package cchat
//go:generate go run ./cmd/internal/cchat-generator
type authenticateError struct{ error }
func (authenticateError) NextStage() []Authenticator { return nil }
// WrapAuthenticateError wraps the given error to become an AuthenticateError.
// Its NextStage method returns nil. If the given err is nil, then nil is
// returned.
func WrapAuthenticateError(err error) AuthenticateError {
if err == nil {
return nil
}
return authenticateError{err}
}

View File

@ -56,7 +56,10 @@ func (c Comment) WrapText(column int) string {
buf := bytes.Buffer{}
doc.ToText(&buf, txt, "", strings.Repeat(" ", TabWidth-1), column)
return strings.TrimRight(buf.String(), "\n")
text := strings.TrimRight(buf.String(), "\n")
text = strings.Replace(text, "\t", strings.Repeat(" ", TabWidth), -1)
return text
}
// Unindent removes the indentations that were there for the sake of syntax in

View File

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

View File

@ -1,28 +1,3 @@
// +build ignore
package gob
package main
//go:generate go run ./generator.go
import (
"encoding/gob"
"log"
"os"
"github.com/diamondburned/cchat/repository"
)
const output = "repository.gob"
func main() {
f, err := os.Create(output)
if err != nil {
log.Fatalln("Failed to create file:", err)
}
defer f.Close()
if err := gob.NewEncoder(f).Encode(repository.Main); err != nil {
os.Remove(output)
log.Fatalln("Failed to gob encode:", err)
}
}
//go:generate go run ../../cmd/internal/cchat-gob-gen

Binary file not shown.

View File

@ -55,9 +55,15 @@ type GetterMethod struct {
Parameters []NamedType
// Returns is the list of named types returned from the function.
Returns []NamedType
// ReturnError is true if the function returns an error at the end of
// returns.
ReturnError bool
// ErrorType is non-empty if the function returns an error at the end of
// returns. For the most part, this field should be "error" if that is the
// case, but some methods may choose to extend the error base type.
ErrorType string
}
// ReturnError returns true if the method can error out.
func (m GetterMethod) ReturnError() bool {
return m.ErrorType != ""
}
// SetterMethod is a method that sets values. These methods must not do IO, and
@ -80,12 +86,19 @@ type IOMethod struct {
Parameters []NamedType
// ReturnValue is the return value in the function.
ReturnValue NamedType
// ReturnError is true if the function returns an error at the end of
// returns.
ReturnError bool
// ErrorType is non-empty if the function returns an error at the end of
// returns. For the most part, this field should be "error" if that is the
// case, but some methods may choose to extend the error base type.
ErrorType string
}
// ContainerMethod is a method that uses a Container. These methods can do IO.
// ReturnError returns true if the method can error out.
func (m IOMethod) ReturnError() bool {
return m.ErrorType != ""
}
// ContainerMethod is a method that uses a Container. These methods can do IO
// and always return an error.
type ContainerMethod struct {
method

View File

@ -332,7 +332,7 @@ var Main = Packages{
},
}},
},
"github.com/diamondburned/cchat": {
RootPath: {
Comment: Comment{`
Package cchat is a set of stabilized interfaces for cchat
implementations, joining the backend and frontend together.
@ -681,29 +681,55 @@ var Main = Packages{
ChildType: "SessionRestorer",
},
},
}, {
Comment: Comment{`
AuthenticateError is the error returned when authenticating.
This error interface extends the normal error to allow backends
to implement multi-stage authentication if needed in a clean way
without needing any loops.
This interface satisfies the error interface.
`},
Name: "AuthenticateError",
Methods: []Method{
GetterMethod{
method: method{
Comment: Comment{`
Error returns the error as a string. This method
makes AuthenticateError satisfy the built-in error
interface.
`},
Name: "Error",
},
Returns: []NamedType{{Type: "string"}},
},
GetterMethod{
method: method{
Comment: Comment{`
NextStage optionally returns a slice of
Authenticator interfaces if the authentication
process requires another stage. It works similarly
to Service's Authenticate method, both of which
returns a slice of Authenticators.
If the error returned is an actual error, and that
the user should retry any of the authentication
fields, then NextStage could return nil to signify
the error. The frontend could reliably check nil on
this field to determine whether or not it should
recreate the authentication fields.
`},
Name: "NextStage",
},
Returns: []NamedType{{Type: "[]Authenticator"}},
},
},
}, {
Comment: 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
}
done by calling Authenticate and check for AuthenticateError's
NextStage method.
`},
Name: "Authenticator",
Methods: []Method{
@ -753,7 +779,7 @@ var Main = Packages{
},
Parameters: []NamedType{{Type: "[]string"}},
ReturnValue: NamedType{Type: "Session"},
ReturnError: true,
ErrorType: "AuthenticateError",
},
},
}, {
@ -770,7 +796,7 @@ var Main = Packages{
method: method{Name: "RestoreSession"},
Parameters: []NamedType{{Type: "map[string]string"}},
ReturnValue: NamedType{Type: "Session"},
ReturnError: true,
ErrorType: "error",
},
},
}, {
@ -785,12 +811,12 @@ var Main = Packages{
IOMethod{
method: method{Name: "Configuration"},
ReturnValue: NamedType{Type: "map[string]string"},
ReturnError: true,
ErrorType: "error",
},
IOMethod{
method: method{Name: "SetConfiguration"},
Parameters: []NamedType{{Type: "map[string]string"}},
ReturnError: true,
method: method{Name: "SetConfiguration"},
Parameters: []NamedType{{Type: "map[string]string"}},
ErrorType: "error",
},
},
}, {
@ -843,7 +869,7 @@ var Main = Packages{
`},
Name: "Disconnect",
},
ReturnError: true,
ErrorType: "error",
},
AsserterMethod{ChildType: "Commander"},
AsserterMethod{ChildType: "SessionSaver"},
@ -930,7 +956,7 @@ var Main = Packages{
{Name: "words", Type: "[]string"},
},
ReturnValue: NamedType{Type: "[]byte"},
ReturnError: true,
ErrorType: "error",
},
AsserterMethod{ChildType: "Completer"},
},
@ -1027,7 +1053,7 @@ var Main = Packages{
Parameters: []NamedType{
{Type: "SendableMessage"},
},
ReturnError: true,
ErrorType: "error",
},
GetterMethod{
method: method{
@ -1067,9 +1093,9 @@ var Main = Packages{
`},
Name: "RawContent",
},
Parameters: []NamedType{{Name: "id", Type: "ID"}},
Returns: []NamedType{{Type: "string"}},
ReturnError: true,
Parameters: []NamedType{{Name: "id", Type: "ID"}},
Returns: []NamedType{{Type: "string"}},
ErrorType: "error",
},
IOMethod{
method: method{
@ -1084,7 +1110,7 @@ var Main = Packages{
{Name: "id", Type: "ID"},
{Name: "content", Type: "string"},
},
ReturnError: true,
ErrorType: "error",
},
},
}, {
@ -1123,7 +1149,7 @@ var Main = Packages{
{Name: "action", Type: "string"},
{Name: "id", Type: "ID"},
},
ReturnError: true,
ErrorType: "error",
},
},
}, {
@ -1187,7 +1213,7 @@ var Main = Packages{
{"before", "ID"},
{"msgc", "MessagesContainer"},
},
ReturnError: true,
ErrorType: "error",
},
},
}, {
@ -1262,7 +1288,7 @@ var Main = Packages{
`},
Name: "Typing",
},
ReturnError: true,
ErrorType: "error",
},
GetterMethod{
method: method{

View File

@ -17,7 +17,7 @@ func TestGob(t *testing.T) {
t.Log("Marshaled; total bytes:", buf.Len())
var unmarshaled Repositories
var unmarshaled Packages
if err := gob.NewDecoder(&buf).Decode(&unmarshaled); err != nil {
t.Fatal("Failed to gob decode:", err)