121 lines
3.5 KiB
Go
121 lines
3.5 KiB
Go
package apigen
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"sort"
|
|
"strconv"
|
|
|
|
apierrors "git.yuntuo.hk/k2safe/go-omni/errors"
|
|
"git.yuntuo.hk/k2safe/go-omni/response"
|
|
)
|
|
|
|
// A named catalog groups project definitions under one default HTTP status.
|
|
// Each JSON object key supplies the generated Go function's name, while key is
|
|
// the independent logical identifier and code is the public string/integer.
|
|
func (p *parser) parseErrorCatalog() error {
|
|
pos := p.next().position
|
|
name, err := p.ident()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !exportedName.MatchString(name) {
|
|
return p.err("errorCatalog requires an UpperCamelCase name")
|
|
}
|
|
for _, prior := range p.spec.ErrorCatalogs {
|
|
if prior == name {
|
|
return p.err("duplicate errorCatalog " + name)
|
|
}
|
|
}
|
|
p.spec.ErrorCatalogs = append(p.spec.ErrorCatalogs, name)
|
|
p.skip()
|
|
props, err := p.metadata()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defaultStatus := 400
|
|
if value, ok := props["defaultStatus"]; ok {
|
|
defaultStatus, err = strconv.Atoi(value)
|
|
if err != nil {
|
|
return p.err("defaultStatus must be an integer")
|
|
}
|
|
}
|
|
if !apierrors.ValidErrorStatus(defaultStatus) {
|
|
return p.err("invalid errorCatalog defaultStatus")
|
|
}
|
|
for key := range props {
|
|
if key != "defaultStatus" && key != "errors" {
|
|
return p.err("unsupported errorCatalog property " + key)
|
|
}
|
|
}
|
|
type entry struct {
|
|
Key string `json:"key"`
|
|
Code any `json:"code"`
|
|
Message string `json:"message"`
|
|
Status int `json:"status"`
|
|
Params []apierrors.Parameter `json:"params"`
|
|
Translation *apierrors.Translation `json:"translation"`
|
|
}
|
|
var entries map[string]entry
|
|
if err := apierrors.DecodeCatalogJSON([]byte(props["errors"]), &entries); err != nil {
|
|
return fmt.Errorf("%s: catalog %s: %w", pos, name, err)
|
|
}
|
|
if len(entries) == 0 {
|
|
return p.err("errorCatalog requires errors")
|
|
}
|
|
names := make([]string, 0, len(entries))
|
|
for key := range entries {
|
|
names = append(names, key)
|
|
}
|
|
sort.Strings(names)
|
|
for _, fn := range names {
|
|
e := entries[fn]
|
|
if e.Code == nil {
|
|
return p.err("errorCatalog error requires code")
|
|
}
|
|
if e.Status == 0 {
|
|
e.Status = defaultStatus
|
|
}
|
|
p.spec.Errors = append(p.spec.Errors, ErrorDefinition{Name: fn, Status: e.Status, Code: e.Key, Message: e.Message, BusinessCode: e.Code, Params: e.Params, Translation: e.Translation, Catalog: name, Position: pos})
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func errorCatalog(spec *Spec, names []string) apierrors.Catalog {
|
|
c := apierrors.Catalog{Definitions: map[string]apierrors.Definition{}}
|
|
selected := map[string]bool{}
|
|
for _, n := range names {
|
|
selected[n] = true
|
|
}
|
|
for _, e := range spec.Errors {
|
|
if names != nil && !selected[e.Name] {
|
|
continue
|
|
}
|
|
c.Definitions[e.Code] = apierrors.Definition{Status: e.Status, Code: e.BusinessCode, Message: e.Message, Params: e.Params, Translation: e.Translation}
|
|
}
|
|
return c
|
|
}
|
|
|
|
// Validate the exact catalog/template pair used by each route. Different routes
|
|
// can use different code types without forcing a global response representation.
|
|
func validateErrorPolicies(spec *Spec) error {
|
|
for _, r := range spec.Routes {
|
|
f, err := response.New(responseConfig(spec, r))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if _, err = response.WithErrors(errorCatalog(spec, append([]string{}, r.Errors...)), f); err != nil {
|
|
return fmt.Errorf("%s: %w", r.Position, err)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func errorCatalogJSON(spec *Spec, names []string) string {
|
|
data, err := json.Marshal(errorCatalog(spec, names))
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
return string(data)
|
|
}
|