diff --git a/cchat.go b/cchat.go index f5ad229..efc44ee 100644 --- a/cchat.go +++ b/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 diff --git a/cmd/internal/cchat-generator/generate_interface.go b/cmd/internal/cchat-generator/generate_interface.go index 22ade03..8e1d9f9 100644 --- a/cmd/internal/cchat-generator/generate_interface.go +++ b/cmd/internal/cchat-generator/generate_interface.go @@ -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)) } } diff --git a/cmd/internal/cchat-gob-gen/main.go b/cmd/internal/cchat-gob-gen/main.go new file mode 100644 index 0000000..2f94a89 --- /dev/null +++ b/cmd/internal/cchat-gob-gen/main.go @@ -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) + } +} diff --git a/generator.go b/generator.go index 2bb0e04..d8e7899 100644 --- a/generator.go +++ b/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} +} diff --git a/repository/comment.go b/repository/comment.go index b151d96..6052bb4 100644 --- a/repository/comment.go +++ b/repository/comment.go @@ -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 diff --git a/repository/comment_test.go b/repository/comment_test.go index 2a3c871..5a8b867 100644 --- a/repository/comment_test.go +++ b/repository/comment_test.go @@ -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) + } + }) } diff --git a/repository/gob/generator.go b/repository/gob/generator.go index dd90a24..fd1cef9 100644 --- a/repository/gob/generator.go +++ b/repository/gob/generator.go @@ -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 diff --git a/repository/gob/repository.gob b/repository/gob/repository.gob index 4da324f..b23a751 100644 Binary files a/repository/gob/repository.gob and b/repository/gob/repository.gob differ diff --git a/repository/interface.go b/repository/interface.go index 83876c9..d70a12c 100644 --- a/repository/interface.go +++ b/repository/interface.go @@ -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 diff --git a/repository/main.go b/repository/main.go index 2ca4fda..80a7d86 100644 --- a/repository/main.go +++ b/repository/main.go @@ -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{ diff --git a/repository/main_test.go b/repository/main_test.go index 073988a..4a76fd8 100644 --- a/repository/main_test.go +++ b/repository/main_test.go @@ -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)