package cepis import ( "encoding/json" "errors" "fmt" "time" ) // Err is the root of every error this package returns. Callers who only care // that something went wrong can match one value: // // if errors.Is(err, cepis.Err) { ... } // // Below it, the split is by the decision the caller has to make, not by the // HTTP status that caused it. [ErrInvalidCEP] and [ErrCEPNotFound] are // problems with the request, and retrying them is pointless — the same CEP // fails again. [ErrNetwork], [ErrServer] and [ErrRateLimited] are problems // with transport, so retrying may help; the client already retried on its own // before returning any of them. var Err = errors.New("cepis") // The kinds of failure, each wrapping [Err]. Match them with errors.Is: // // if errors.Is(err, cepis.ErrCEPNotFound) { ... } var ( // ErrInvalidCEP means the CEP is not 8 digits. Usually returned without // any request being made: the client validates the format before going // to the network. It is also what a 400 from the API becomes — the // format passed local validation, so the server knows something this // client does not. ErrInvalidCEP = fmt.Errorf("%w: invalid CEP", Err) // ErrCEPNotFound means the CEP is well-formed but absent from the DNE // database. Looking a CEP up is conceptually a key lookup, and this is // the missing key. ErrCEPNotFound = fmt.Errorf("%w: CEP not found", Err) // ErrRateLimited means the API (or the edge in front of it) answered 429 // on every attempt. Error.RetryAfter carries what the server asked for. ErrRateLimited = fmt.Errorf("%w: rate limited", Err) // ErrServer means the API answered 5xx on every attempt. Error.Status // holds the status seen on the last one. ErrServer = fmt.Errorf("%w: server error", Err) // ErrNetwork means the request never completed: DNS, refused connection, // timeout, TLS. Returned only after every configured attempt failed. ErrNetwork = fmt.Errorf("%w: network error", Err) // ErrInvalidConfig means an [Option] was given a value the client cannot // work with. Only [New] returns it. ErrInvalidConfig = fmt.Errorf("%w: invalid configuration", Err) // ErrInvalidResponse means a success status came back with a body that // cannot be parsed. In practice something between you and the API is // answering instead of it — a captive Wi-Fi portal, a corporate proxy, // an HTML error page. Useful as a diagnosis: the problem is not in your // code. ErrInvalidResponse = fmt.Errorf("%w: invalid response", Err) ) // Error is the concrete type behind every failure in this package. It carries // the context a caller may need to act on — which CEP, which status, how long // the server asked to be left alone — and it matches its kind through // errors.Is: // // var cerr *cepis.Error // if errors.As(err, &cerr) && errors.Is(cerr, cepis.ErrRateLimited) { // reschedule(cerr.RetryAfter) // } type Error struct { // Kind is one of the package sentinels, and what errors.Is matches. Kind error // CEP is the normalised CEP the call was about, when there was one. CEP string // Status is the last HTTP status seen, or 0 when no response arrived. Status int // RetryAfter is what the server asked for through the Retry-After // header, or 0 when it did not say. It is reported as received, before // the client's own cap is applied to it. RetryAfter time.Duration // Message describes the failure without the "cepis: " prefix. Message string // Cause is the underlying error, when one exists — typically the // net/http error behind [ErrNetwork]. Cause error } // Error implements the error interface. func (e *Error) Error() string { msg := e.Message if msg == "" { if e.Kind != nil { msg = e.Kind.Error() } else { msg = Err.Error() } } else { msg = "cepis: " + msg } if e.Cause != nil { return msg + ": " + e.Cause.Error() } return msg } // Unwrap exposes both the kind and the underlying cause, so errors.Is matches // the package sentinels and errors.As reaches a net.Error underneath. func (e *Error) Unwrap() []error { switch { case e.Kind != nil && e.Cause != nil: return []error{e.Kind, e.Cause} case e.Kind != nil: return []error{e.Kind} case e.Cause != nil: return []error{e.Cause} } return nil } func newError(kind error, format string, args ...any) *Error { return &Error{Kind: kind, Message: fmt.Sprintf(format, args...)} } func apiMessage(body []byte, fallback string) string { var payload struct { Mensagem string `json:"mensagem"` } if err := json.Unmarshal(body, &payload); err == nil && payload.Mensagem != "" { return payload.Mensagem } return fallback }