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:
parent
c32c50c0e8
commit
955b99c9b6
45
cchat.go
45
cchat.go
|
@ -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
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
14
generator.go
14
generator.go
|
@ -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}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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.
|
@ -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
|
||||
|
||||
|
|
|
@ -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,
|
||||
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{
|
||||
|
@ -1069,7 +1095,7 @@ var Main = Packages{
|
|||
},
|
||||
Parameters: []NamedType{{Name: "id", Type: "ID"}},
|
||||
Returns: []NamedType{{Type: "string"}},
|
||||
ReturnError: true,
|
||||
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{
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Reference in New Issue