feat: add project-owned error catalogs and HTTP 200 failure contracts
This commit is contained in:
@@ -8,6 +8,7 @@ import (
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
apierrors "git.yuntuo.hk/k2safe/go-omni/errors"
|
||||
"git.yuntuo.hk/k2safe/go-omni/openapi"
|
||||
"git.yuntuo.hk/k2safe/go-omni/response"
|
||||
)
|
||||
@@ -20,14 +21,18 @@ var credentialName = regexp.MustCompile("^[!#$%&'*+.^_`|~0-9A-Za-z-]+$")
|
||||
func prepareMetadata(original *Spec) (*Spec, error) {
|
||||
spec := *original
|
||||
spec.Routes = append([]Route(nil), original.Routes...)
|
||||
spec.Errors = append([]ErrorDefinition(nil), original.Errors...)
|
||||
if err := validateResponses(&spec); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
definitions := map[string]ErrorDefinition{}
|
||||
codes, symbols := map[string]bool{}, map[string]bool{}
|
||||
codes, symbols := map[string]bool{}, map[string]bool{"Catalog": true}
|
||||
for _, e := range spec.Errors {
|
||||
if !exportedName.MatchString(e.Name) || e.Status < 400 || e.Status > 599 || !errorCode.MatchString(e.Code) || strings.TrimSpace(e.Message) == "" {
|
||||
return nil, fmt.Errorf("%s: error requires UpperCamelCase name, status 400..599, UPPER_SNAKE_CASE code and a nonempty public message", e.Position)
|
||||
if !exportedName.MatchString(e.Name) || !apierrors.ValidErrorStatus(e.Status) || strings.TrimSpace(e.Message) == "" {
|
||||
return nil, fmt.Errorf("%s: error requires UpperCamelCase name, a final body-bearing status (200..599 except 204/205/304) and a nonempty public message", e.Position)
|
||||
}
|
||||
if e.Catalog == "" && !errorCode.MatchString(e.Code) {
|
||||
return nil, fmt.Errorf("%s: legacy error declaration requires UPPER_SNAKE_CASE code", e.Position)
|
||||
}
|
||||
if codes[e.Code] {
|
||||
return nil, fmt.Errorf("%s: duplicate error code %s", e.Position, e.Code)
|
||||
@@ -41,6 +46,17 @@ func prepareMetadata(original *Spec) (*Spec, error) {
|
||||
codes[e.Code] = true
|
||||
definitions[e.Name] = e
|
||||
}
|
||||
registry, err := apierrors.NewRegistry(errorCatalog(&spec, nil))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
normalized := registry.Catalog()
|
||||
for i := range spec.Errors {
|
||||
spec.Errors[i].BusinessCode = normalized.Definitions[spec.Errors[i].Code].Code
|
||||
}
|
||||
if err := validateErrorPolicies(&spec); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
auth := map[string]AuthDefinition{}
|
||||
for _, a := range spec.Auth {
|
||||
if !exportedName.MatchString(a.Name) || !goName.MatchString(a.Middleware) || strings.EqualFold(a.Middleware, "defaults") || strings.EqualFold(a.Middleware, "registryGen") {
|
||||
@@ -84,7 +100,7 @@ func prepareMetadata(original *Spec) (*Spec, error) {
|
||||
seen := map[string]bool{}
|
||||
for _, name := range r.Errors {
|
||||
if _, ok := definitions[name]; !ok {
|
||||
return nil, fmt.Errorf("%s: unknown error %s; declare it in error (...)", r.Position, name)
|
||||
return nil, fmt.Errorf("%s: unknown error %s; declare it in errorCatalog or error (...)", r.Position, name)
|
||||
}
|
||||
if seen[name] {
|
||||
return nil, fmt.Errorf("%s: duplicate error %s", r.Position, name)
|
||||
@@ -182,11 +198,31 @@ func decorateDocument(doc map[string]any, spec *Spec) {
|
||||
response = genericErrorResponse(http.StatusText(status), cfg)
|
||||
responses[key] = response
|
||||
}
|
||||
media := response["content"].(map[string]any)["application/json"].(map[string]any)
|
||||
content, ok := response["content"].(map[string]any)
|
||||
if !ok {
|
||||
content = map[string]any{}
|
||||
response["content"] = content
|
||||
}
|
||||
media, hasJSON := content["application/json"].(map[string]any)
|
||||
if !hasJSON {
|
||||
media = map[string]any{}
|
||||
content["application/json"] = media
|
||||
}
|
||||
errorMedia := genericErrorResponse("", cfg)["content"].(map[string]any)["application/json"].(map[string]any)
|
||||
if exists && status < 400 && media["schema"] != nil {
|
||||
media["schema"] = map[string]any{"anyOf": []any{media["schema"], errorMedia["schema"]}}
|
||||
} else if media["schema"] == nil {
|
||||
media["schema"] = errorMedia["schema"]
|
||||
}
|
||||
examples := map[string]any{}
|
||||
codes := []any{}
|
||||
seenCodes := map[string]bool{}
|
||||
description := []string{}
|
||||
if exists {
|
||||
if text, ok := response["description"].(string); ok && text != "" {
|
||||
description = append(description, text)
|
||||
}
|
||||
}
|
||||
sort.Slice(errs, func(i, j int) bool { return errs[i].Name < errs[j].Name })
|
||||
for _, e := range errs {
|
||||
value := policy.Value(responseResult(e))
|
||||
@@ -207,7 +243,7 @@ func decorateDocument(doc map[string]any, spec *Spec) {
|
||||
}
|
||||
}
|
||||
func responseResult(e ErrorDefinition) response.Result {
|
||||
return response.Result{Status: e.Status, Code: e.Code, Message: e.Message, RequestID: "exampleRequest"}
|
||||
return response.Result{Status: e.Status, Code: e.Code, Message: e.Message, BusinessCode: e.BusinessCode, Failed: true, RequestID: "exampleRequest"}
|
||||
}
|
||||
func genericErrorResponse(description string, cfg response.Config) map[string]any {
|
||||
if description == "" {
|
||||
|
||||
Reference in New Issue
Block a user