mirror of
https://gitea.com/gitea/tea.git
synced 2026-10-26 11:01:25 +01:00
refactor: drop the URL-based error classifier and hint lookups
Errors are named by the service call that failed: Classify, NotFoundError and ConflictError at the call site. Remove the endpoint parser that guessed the resource from the request path, and the follow-up reads the explainer made at error time (repository state, issue-vs-pull-request, the existing pull request's URL) together with the credentialed transport each API error carried for them. Calls that the endpoint parser covered and the services did not now classify themselves: issues, pull requests, reviews, comments, releases, branches, wiki pages, repositories, organizations and SSH keys. Unclassified failures fall back to the generic "not found (HTTP 404 GET ...)" text. Signed-off-by: bircni <bircni@icloud.com>
This commit is contained in:
20 files changed
+129
-495
No files matched your search
@@ -212,170 +212,3 @@ func refusal(apiErr *transport.APIError) string {
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// ClassifyRequest is Classify for a failure no service classified: it infers the resource from
|
||||
// the failed request's endpoint, so a 404 for GET /api/v1/repos/alice/x/issues/9 reports
|
||||
// "issue #9 not found in alice/x".
|
||||
func ClassifyRequest(err error) error {
|
||||
apiErr, ok := errors.AsType[*transport.APIError](err)
|
||||
if !ok {
|
||||
return err
|
||||
}
|
||||
resource := RequestResource(apiErr.Endpoint)
|
||||
kind, key := resource.Kind, resource.Key
|
||||
// A refused POST to a collection concerns the resource it would have created.
|
||||
if apiErr.Status != http.StatusNotFound && resource.Collection != "" {
|
||||
kind, key = resource.Collection, ""
|
||||
}
|
||||
return Classify(err, kind, key, resource.Scope)
|
||||
}
|
||||
|
||||
// Resource is what a Gitea API endpoint addresses.
|
||||
type Resource struct {
|
||||
// Kind and Key name one resource, such as "issue" and "#9"; both are empty for collections.
|
||||
Kind, Key string
|
||||
// Collection names the kind of resource a collection endpoint holds, such as "branch" for
|
||||
// POST /repos/{owner}/{repo}/branches.
|
||||
Collection string
|
||||
// Scope is the repository or organization the resource belongs to.
|
||||
Scope string
|
||||
// Repository is the owner/name of a repository endpoint, else empty.
|
||||
Repository string
|
||||
// Section is the first path segment below the repository, such as "issues" or "pulls".
|
||||
Section string
|
||||
}
|
||||
|
||||
// RequestResource reports what endpoint, an API path such as /api/v1/repos/alice/x/issues/9,
|
||||
// addresses.
|
||||
func RequestResource(endpoint string) Resource {
|
||||
_, rest, ok := strings.Cut(endpoint, "/api/v1/")
|
||||
if !ok {
|
||||
return Resource{}
|
||||
}
|
||||
parts := strings.Split(strings.Trim(rest, "/"), "/")
|
||||
if len(parts) < 3 || parts[0] != "repos" {
|
||||
return instanceResource(parts)
|
||||
}
|
||||
repo := parts[1] + "/" + parts[2]
|
||||
if len(parts) == 3 {
|
||||
return Resource{Kind: "repository", Key: repo, Repository: repo}
|
||||
}
|
||||
resource := repositoryResource(parts[3:])
|
||||
resource.Repository, resource.Scope, resource.Section = repo, repo, parts[3]
|
||||
return resource
|
||||
}
|
||||
|
||||
// collections names the resources that POST to a repository collection creates.
|
||||
var collections = map[string]string{
|
||||
"branches": "branch", "tags": "tag", "releases": "release", "labels": "label", "milestones": "milestone",
|
||||
"pulls": "pull request", "issues": "issue", "hooks": "webhook", "keys": "deploy key", "wiki": "wiki page",
|
||||
}
|
||||
|
||||
// repositoryResource matches the path below /repos/{owner}/{repo}.
|
||||
func repositoryResource(parts []string) Resource {
|
||||
at := func(i int) string {
|
||||
if i < len(parts) {
|
||||
return parts[i]
|
||||
}
|
||||
return ""
|
||||
}
|
||||
rest := func(i int) string { return strings.Join(parts[min(i, len(parts)):], "/") }
|
||||
switch {
|
||||
case at(0) == "issues" && at(1) == "comments" && at(2) != "":
|
||||
return Resource{Kind: "comment", Key: at(2)}
|
||||
case (at(0) == "issues" || at(0) == "pulls") && isNumber(at(1)):
|
||||
kind := "issue"
|
||||
if at(0) == "pulls" {
|
||||
kind = "pull request"
|
||||
}
|
||||
return Resource{Kind: kind, Key: "#" + at(1)}
|
||||
case at(0) == "releases" && at(1) == "tags" && at(2) != "":
|
||||
return Resource{Kind: "release", Key: rest(2)}
|
||||
case at(0) == "releases" && at(2) == "assets" && at(3) != "":
|
||||
return Resource{Kind: "release asset", Key: at(3)}
|
||||
case at(0) == "releases" && isNumber(at(1)):
|
||||
return Resource{Kind: "release", Key: at(1)}
|
||||
case at(0) == "labels" && at(1) != "":
|
||||
return Resource{Kind: "label", Key: at(1)}
|
||||
case at(0) == "milestones" && at(1) != "":
|
||||
return Resource{Kind: "milestone", Key: nameKey(at(1))}
|
||||
case at(0) == "branches" && at(1) != "":
|
||||
return Resource{Kind: "branch", Key: rest(1)}
|
||||
case at(0) == "branch_protections" && at(1) != "":
|
||||
return Resource{Kind: "branch protection", Key: rest(1)}
|
||||
case at(0) == "tags" && at(1) != "":
|
||||
return Resource{Kind: "tag", Key: rest(1)}
|
||||
case at(0) == "wiki" && (at(1) == "page" || at(1) == "revisions") && at(2) != "":
|
||||
return Resource{Kind: "wiki page", Key: strconv.Quote(at(2))}
|
||||
case at(0) == "hooks" && isNumber(at(1)):
|
||||
return Resource{Kind: "webhook", Key: at(1)}
|
||||
case at(0) == "collaborators" && at(1) != "":
|
||||
return Resource{Kind: "collaborator", Key: strconv.Quote(at(1))}
|
||||
case at(0) == "contents" && at(1) != "":
|
||||
return Resource{Kind: "file", Key: rest(1)}
|
||||
case at(0) == "actions":
|
||||
return actionsResource(at(1), at(2))
|
||||
}
|
||||
return Resource{Collection: collections[at(0)]}
|
||||
}
|
||||
|
||||
func actionsResource(collection, key string) Resource {
|
||||
kinds := map[string]string{"runs": "run", "jobs": "job", "artifacts": "artifact", "workflows": "workflow", "secrets": "secret", "variables": "variable"}
|
||||
kind := kinds[collection]
|
||||
if key == "" {
|
||||
return Resource{Collection: kind}
|
||||
}
|
||||
return Resource{Kind: kind, Key: key}
|
||||
}
|
||||
|
||||
// instanceResource matches endpoints outside a repository, such as /users/{name}.
|
||||
func instanceResource(parts []string) Resource {
|
||||
at := func(i int) string {
|
||||
if i < len(parts) {
|
||||
return parts[i]
|
||||
}
|
||||
return ""
|
||||
}
|
||||
switch {
|
||||
case at(0) == "orgs" && at(1) != "" && (at(2) == "members" || at(2) == "public_members") && at(3) != "":
|
||||
return Resource{Kind: "member", Key: strconv.Quote(at(3)), Scope: at(1)}
|
||||
case at(0) == "orgs" && at(1) != "" && at(2) == "repos":
|
||||
return Resource{Kind: "organization", Key: strconv.Quote(at(1)), Collection: "repository"}
|
||||
case at(0) == "orgs" && at(1) != "" && at(2) == "teams":
|
||||
return Resource{Kind: "organization", Key: strconv.Quote(at(1)), Collection: "team"}
|
||||
case at(0) == "orgs" && at(1) != "":
|
||||
return Resource{Kind: "organization", Key: strconv.Quote(at(1))}
|
||||
case at(0) == "orgs":
|
||||
return Resource{Collection: "organization"}
|
||||
case at(0) == "users" && at(1) != "":
|
||||
return Resource{Kind: "user", Key: strconv.Quote(at(1))}
|
||||
case at(0) == "admin" && at(1) == "users" && at(2) != "":
|
||||
return Resource{Kind: "user", Key: strconv.Quote(at(2))}
|
||||
case at(0) == "admin" && at(1) == "users":
|
||||
return Resource{Collection: "user"}
|
||||
case at(0) == "teams" && isNumber(at(1)):
|
||||
return Resource{Kind: "team", Key: at(1)}
|
||||
case at(0) == "user" && at(1) == "keys" && at(2) != "":
|
||||
return Resource{Kind: "SSH key", Key: at(2)}
|
||||
case at(0) == "user" && at(1) == "keys":
|
||||
return Resource{Collection: "SSH key"}
|
||||
case at(0) == "user" && at(1) == "repos":
|
||||
return Resource{Collection: "repository"}
|
||||
case at(0) == "notifications" && at(1) == "threads" && at(2) != "":
|
||||
return Resource{Kind: "notification", Key: at(2)}
|
||||
}
|
||||
return Resource{}
|
||||
}
|
||||
|
||||
// nameKey quotes a name, but not a numeric ID.
|
||||
func nameKey(value string) string {
|
||||
if isNumber(value) {
|
||||
return value
|
||||
}
|
||||
return strconv.Quote(value)
|
||||
}
|
||||
|
||||
func isNumber(value string) bool {
|
||||
n, err := strconv.ParseInt(value, 10, 64)
|
||||
return err == nil && n > 0
|
||||
}
|
||||
Reference in new issue
Block a user