package pages
import (
"embed"
"net/http"
)
// sharedFS holds this package's own templates: the error partial parsed into
// every set, and the error page used by a service that ships none.
//
//go:embed templates
var sharedFS embed.FS
const (
sharedDir = "templates"
// errorPartialFile defines ErrorPartial and nothing else, so that it can be
// parsed into every page set without bringing a content block with it.
errorPartialFile = "error.tmpl"
// errorPageFile is the default error page: a content block that is one
// invocation of ErrorPartial.
errorPageFile = "error.html"
)
// ErrorPage is the name Load registers the error page under, and the name to
// pass Render for it. It is a constant because a service's `fail` names it on
// every arm of its switch.
const ErrorPage = "error"
// ErrorPartial is the shared error body: the status line, the message and the
// way back. Its dot is an ErrorData — not a view struct — so a service that
// wants its own error.html around it invokes it as
// {{template "srht-error" .Data}}.
const ErrorPartial = "srht-error"
// The messages of the error pages a surface produces on its own. They are
// constants, and shared ones, for the reason the donors gave: the visibility
// rules of these services require "somebody else's private thing" and "no such
// thing" to be indistinguishable, and two 404s that differed in their prose
// would rebuild the distinction the status code was chosen to erase.
//
// A 400 has no constant deliberately. It is the one class that describes
// something the viewer just typed — "1y is not a duration", "grants are not
// lower case" — and an error page that replaced that with a house phrase would
// send them back to the form with nothing to change.
const (
NotFoundMessage = "There is nothing here."
UnauthorizedMessage = "You need to be logged in to do that."
ForbiddenMessage = "You may not do that."
MethodMessage = "That is not something you can do to this page."
InternalMessage = "Something went wrong on our side. It has been logged."
UnavailableMessage = "Something we depend on is not answering. Try again in a moment."
)
// Message is the standard message for a status, or "" for a status that has
// none — a 400 above all, whose message is the caller's own text.
// The machine-facing halves of the same table. A REST surface answers a caller
// that parses, not a person that reads, and every service on the instance keeps
// its 404 body byte-identical on purpose: two spellings of "not found" are two
// facts a client can accidentally distinguish, which is exactly what the shared
// status was chosen to prevent.
//
// They live beside the page sentences rather than in a second package because
// they are one table read two ways — and because the service that tried to
// reuse RenderRefusals on its REST surface could not, for want of these.
const (
APINotFoundMessage = "not found"
APIUnauthorizedMessage = "unauthorized"
APIForbiddenMessage = "forbidden"
APIMethodMessage = "method not allowed"
APIInternalMessage = "internal server error"
APIUnavailableMessage = "service unavailable"
)
// APIMessage is Message for a machine-facing surface: the same statuses, in the
// register a JSON client expects. An unmapped status returns "", which the
// caller renders as it likes — usually http.StatusText.
func APIMessage(status int) string {
switch status {
case http.StatusUnauthorized:
return APIUnauthorizedMessage
case http.StatusForbidden:
return APIForbiddenMessage
case http.StatusNotFound:
return APINotFoundMessage
case http.StatusMethodNotAllowed:
return APIMethodMessage
case http.StatusInternalServerError:
return APIInternalMessage
case http.StatusBadGateway, http.StatusServiceUnavailable, http.StatusGatewayTimeout:
return APIUnavailableMessage
default:
return ""
}
}
func Message(status int) string {
switch status {
case http.StatusUnauthorized:
return UnauthorizedMessage
case http.StatusForbidden:
return ForbiddenMessage
case http.StatusNotFound:
return NotFoundMessage
case http.StatusMethodNotAllowed:
return MethodMessage
case http.StatusInternalServerError:
return InternalMessage
case http.StatusBadGateway, http.StatusServiceUnavailable, http.StatusGatewayTimeout:
return UnavailableMessage
default:
return ""
}
}
// A Link is a href and the text that carries it.
type Link struct {
Href string
Text string
}
// DefaultBack is the way back off an error page for a service that names none:
// its own root, which every one of these services answers with a landing page.
var DefaultBack = Link{Href: "/", Text: "Back to the start"}
// ErrorData is the payload of the error page. A service puts it where its view
// struct keeps page payload — the field the layout's content sees as .Data in
// all of the donors:
//
// vd := s.view(r, http.StatusText(status))
// vd.Data = pages.Error(status, message).BackTo("/tokens", "Back to your tokens")
// err := s.pages.Render(w, status, pages.ErrorPage, vd)
type ErrorData struct {
Status int
StatusText string
// Message is what the viewer can act on. It is never the raw error from
// below: those name tables, queries and paths. It goes through
// html/template, which escapes it — which matters, because the messages
// that are not constants quote what the caller typed.
Message string
// Back is the way off this page. Empty renders no link at all rather than a
// link to nowhere, which is what a hand-built ErrorData would otherwise get.
Back Link
}
// Error builds the payload for a status. An empty message takes the standard
// one for that status (Message), so a caller that has nothing of its own to add
// says nothing rather than inventing a phrase.
func Error(status int, message string) ErrorData {
if message == "" {
message = Message(status)
}
return ErrorData{
Status: status,
StatusText: http.StatusText(status),
Message: message,
Back: DefaultBack,
}
}
// BackTo replaces the way back, for the services whose landing page is not "/"
// or whose word for it is not "back to the start". It returns a copy, so it
// chains off Error.
func (d ErrorData) BackTo(href, text string) ErrorData {
d.Back = Link{Href: href, Text: text}
return d
}