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 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 // The authenticator interface allows for a multistage initial authentication
// API that the backend could use. Multistage is done by calling // API that the backend could use. Multistage is done by calling Authenticate
// AuthenticateForm then Authenticate again forever until no errors are // and check for AuthenticateError's NextStage method.
// 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
// }
type Authenticator interface { type Authenticator interface {
// Authenticate will be called with a list of values with indices correspond to // Authenticate will be called with a list of values with indices correspond to
// the returned slice of AuthenticateEntry. // 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 // AuthenticateForm should return a list of authentication entries for the
// frontend to render. // frontend to render.
AuthenticateForm() []AuthenticateEntry AuthenticateForm() []AuthenticateEntry

View File

@ -62,13 +62,13 @@ func generateInterfaces(ifaces []repository.Interface) jen.Code {
switch method := method.(type) { switch method := method.(type) {
case repository.GetterMethod: case repository.GetterMethod:
stmt.Params(generateFuncParams(method.Parameters, false)...) stmt.Params(generateFuncParams(method.Parameters, "")...)
stmt.Params(generateFuncParams(method.Returns, method.ReturnError)...) stmt.Params(generateFuncParams(method.Returns, method.ErrorType)...)
case repository.SetterMethod: case repository.SetterMethod:
stmt.Params(generateFuncParams(method.Parameters, false)...) stmt.Params(generateFuncParams(method.Parameters, "")...)
case repository.IOMethod: case repository.IOMethod:
stmt.Params(generateFuncParams(method.Parameters, false)...) stmt.Params(generateFuncParams(method.Parameters, "")...)
stmt.Params(generateFuncParamErr(method.ReturnValue, method.ReturnError)...) stmt.Params(generateFuncParamErr(method.ReturnValue, method.ErrorType)...)
stmt.Comment("// Blocking") stmt.Comment("// Blocking")
case repository.ContainerMethod: case repository.ContainerMethod:
stmt.Params(generateContainerFuncParams(method)...) stmt.Params(generateContainerFuncParams(method)...)
@ -92,18 +92,18 @@ func generateInterfaces(ifaces []repository.Interface) jen.Code {
return stmt 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) stmt := make([]jen.Code, 0, 2)
if !param.IsZero() { if !param.IsZero() {
stmt = append(stmt, generateFuncParam(param)) stmt = append(stmt, generateFuncParam(param))
} }
if genErr { if errorType != "" {
if param.Name == "" { if param.Name == "" {
stmt = append(stmt, jen.Error()) stmt = append(stmt, jen.Id(errorType))
} else { } 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)) 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 { if len(params) == 0 {
return nil return nil
} }
@ -127,11 +127,11 @@ func generateFuncParams(params []repository.NamedType, withError bool) []jen.Cod
stmt.Add(generateFuncParam(param)) stmt.Add(generateFuncParam(param))
} }
if withError { if errorType != "" {
if params[0].Name != "" { if params[0].Name != "" {
stmt.Add(jen.Err().Error()) stmt.Add(jen.Err().Id(errorType))
} else { } 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 package cchat
//go:generate go run ./cmd/internal/cchat-generator //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{} buf := bytes.Buffer{}
doc.ToText(&buf, txt, "", strings.Repeat(" ", TabWidth-1), column) 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 // 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" "github.com/go-test/deep"
) )
const _comment = ` const _goComment = `
The authenticator interface allows for a multistage initial authentication API // The authenticator interface allows for a multistage initial authentication
that the backend could use. Multistage is done by calling AuthenticateForm then // API that the backend could use. Multistage is done by calling
Authenticate again forever until no errors are returned. // AuthenticateForm then Authenticate again forever until no errors are
// returned.
var s *cchat.Session //
var err error // var s *cchat.Session
// var err error
for { //
// Pseudo-function to render the form and return the results of those // for {
// forms when the user confirms it. // // Pseudo-function to render the form and return the results of those
outputs := renderAuthForm(svc.AuthenticateForm()) // // forms when the user confirms it.
// outputs := renderAuthForm(svc.AuthenticateForm())
s, err = svc.Authenticate(outputs) //
if err != nil { // s, err = svc.Authenticate(outputs)
renderError(errors.Wrap(err, "Error while authenticating")) // if err != nil {
continue // retry // renderError(errors.Wrap(err, "Error while authenticating"))
} // continue // retry
// }
break // success //
}` // break // success
// }`
// Trim away the prefix new line. // Trim away the prefix new line.
var comment = _comment[1:] var goComment = _goComment[1:]
func TestComment(t *testing.T) { func TestComment(t *testing.T) {
var authenticator = Main["cchat"].Interface("Authenticator") var authenticator = Main[RootPath].Interface("Authenticator")
var authDoc = authenticator.Comment.GoString()
if eq := deep.Equal(comment, authDoc); eq != nil { t.Run("godoc", func(t *testing.T) {
t.Fatal("Comment inequality:", eq) 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 ../../cmd/internal/cchat-gob-gen
//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)
}
}

Binary file not shown.

View File

@ -55,9 +55,15 @@ type GetterMethod struct {
Parameters []NamedType Parameters []NamedType
// Returns is the list of named types returned from the function. // Returns is the list of named types returned from the function.
Returns []NamedType Returns []NamedType
// ReturnError is true if the function returns an error at the end of // ErrorType is non-empty if the function returns an error at the end of
// returns. // returns. For the most part, this field should be "error" if that is the
ReturnError bool // 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 // SetterMethod is a method that sets values. These methods must not do IO, and
@ -80,12 +86,19 @@ type IOMethod struct {
Parameters []NamedType Parameters []NamedType
// ReturnValue is the return value in the function. // ReturnValue is the return value in the function.
ReturnValue NamedType ReturnValue NamedType
// ReturnError is true if the function returns an error at the end of // ErrorType is non-empty if the function returns an error at the end of
// returns. // returns. For the most part, this field should be "error" if that is the
ReturnError bool // 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 { type ContainerMethod struct {
method method

View File

@ -332,7 +332,7 @@ var Main = Packages{
}, },
}}, }},
}, },
"github.com/diamondburned/cchat": { RootPath: {
Comment: Comment{` Comment: Comment{`
Package cchat is a set of stabilized interfaces for cchat Package cchat is a set of stabilized interfaces for cchat
implementations, joining the backend and frontend together. implementations, joining the backend and frontend together.
@ -681,29 +681,55 @@ var Main = Packages{
ChildType: "SessionRestorer", 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{` Comment: Comment{`
The authenticator interface allows for a multistage initial The authenticator interface allows for a multistage initial
authentication API that the backend could use. Multistage is authentication API that the backend could use. Multistage is
done by calling AuthenticateForm then Authenticate again forever done by calling Authenticate and check for AuthenticateError's
until no errors are returned. NextStage method.
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
}
`}, `},
Name: "Authenticator", Name: "Authenticator",
Methods: []Method{ Methods: []Method{
@ -753,7 +779,7 @@ var Main = Packages{
}, },
Parameters: []NamedType{{Type: "[]string"}}, Parameters: []NamedType{{Type: "[]string"}},
ReturnValue: NamedType{Type: "Session"}, ReturnValue: NamedType{Type: "Session"},
ReturnError: true, ErrorType: "AuthenticateError",
}, },
}, },
}, { }, {
@ -770,7 +796,7 @@ var Main = Packages{
method: method{Name: "RestoreSession"}, method: method{Name: "RestoreSession"},
Parameters: []NamedType{{Type: "map[string]string"}}, Parameters: []NamedType{{Type: "map[string]string"}},
ReturnValue: NamedType{Type: "Session"}, ReturnValue: NamedType{Type: "Session"},
ReturnError: true, ErrorType: "error",
}, },
}, },
}, { }, {
@ -785,12 +811,12 @@ var Main = Packages{
IOMethod{ IOMethod{
method: method{Name: "Configuration"}, method: method{Name: "Configuration"},
ReturnValue: NamedType{Type: "map[string]string"}, ReturnValue: NamedType{Type: "map[string]string"},
ReturnError: true, ErrorType: "error",
}, },
IOMethod{ IOMethod{
method: method{Name: "SetConfiguration"}, method: method{Name: "SetConfiguration"},
Parameters: []NamedType{{Type: "map[string]string"}}, Parameters: []NamedType{{Type: "map[string]string"}},
ReturnError: true, ErrorType: "error",
}, },
}, },
}, { }, {
@ -843,7 +869,7 @@ var Main = Packages{
`}, `},
Name: "Disconnect", Name: "Disconnect",
}, },
ReturnError: true, ErrorType: "error",
}, },
AsserterMethod{ChildType: "Commander"}, AsserterMethod{ChildType: "Commander"},
AsserterMethod{ChildType: "SessionSaver"}, AsserterMethod{ChildType: "SessionSaver"},
@ -930,7 +956,7 @@ var Main = Packages{
{Name: "words", Type: "[]string"}, {Name: "words", Type: "[]string"},
}, },
ReturnValue: NamedType{Type: "[]byte"}, ReturnValue: NamedType{Type: "[]byte"},
ReturnError: true, ErrorType: "error",
}, },
AsserterMethod{ChildType: "Completer"}, AsserterMethod{ChildType: "Completer"},
}, },
@ -1027,7 +1053,7 @@ var Main = Packages{
Parameters: []NamedType{ Parameters: []NamedType{
{Type: "SendableMessage"}, {Type: "SendableMessage"},
}, },
ReturnError: true, ErrorType: "error",
}, },
GetterMethod{ GetterMethod{
method: method{ method: method{
@ -1067,9 +1093,9 @@ var Main = Packages{
`}, `},
Name: "RawContent", Name: "RawContent",
}, },
Parameters: []NamedType{{Name: "id", Type: "ID"}}, Parameters: []NamedType{{Name: "id", Type: "ID"}},
Returns: []NamedType{{Type: "string"}}, Returns: []NamedType{{Type: "string"}},
ReturnError: true, ErrorType: "error",
}, },
IOMethod{ IOMethod{
method: method{ method: method{
@ -1084,7 +1110,7 @@ var Main = Packages{
{Name: "id", Type: "ID"}, {Name: "id", Type: "ID"},
{Name: "content", Type: "string"}, {Name: "content", Type: "string"},
}, },
ReturnError: true, ErrorType: "error",
}, },
}, },
}, { }, {
@ -1123,7 +1149,7 @@ var Main = Packages{
{Name: "action", Type: "string"}, {Name: "action", Type: "string"},
{Name: "id", Type: "ID"}, {Name: "id", Type: "ID"},
}, },
ReturnError: true, ErrorType: "error",
}, },
}, },
}, { }, {
@ -1187,7 +1213,7 @@ var Main = Packages{
{"before", "ID"}, {"before", "ID"},
{"msgc", "MessagesContainer"}, {"msgc", "MessagesContainer"},
}, },
ReturnError: true, ErrorType: "error",
}, },
}, },
}, { }, {
@ -1262,7 +1288,7 @@ var Main = Packages{
`}, `},
Name: "Typing", Name: "Typing",
}, },
ReturnError: true, ErrorType: "error",
}, },
GetterMethod{ GetterMethod{
method: method{ method: method{

View File

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