datastar/site/routes_errors.go

320 lines
11 KiB
Go

package site
import (
"context"
"fmt"
"html"
"io"
"net/http"
"slices"
"strings"
"github.com/a-h/templ"
"github.com/delaneyj/toolbelt"
"github.com/go-chi/chi/v5"
"github.com/goccy/go-json"
"github.com/samber/lo"
"github.com/valyala/bytebufferpool"
)
type ErrorInfo interface {
isErrorInfo()
}
type RuntimeErrorInfo struct {
Plugin struct {
Name string `json:"name"`
Type string `json:"type"`
} `json:"plugin"`
Element struct {
ID string `json:"id"`
Tag string `json:"tag"`
} `json:"element"`
Expression struct {
RawKey string `json:"rawKey"`
Key string `json:"key"`
Value string `json:"value"`
ValidSignals []string `json:"validSignals"`
FnContent string `json:"fnContent"`
} `json:"expression"`
Action string `json:"action"`
ContentType string `json:"contentType"`
Error string `json:"error"`
Method string `json:"method"`
ResultType string `json:"resultType"`
Selector string `json:"selector"`
URL string `json:"url"`
}
func (RuntimeErrorInfo) isErrorInfo() {}
type InitErrorInfo struct {
Plugin struct {
Name string `json:"name"`
Type string `json:"type"`
} `json:"plugin"`
MergeMode string `json:"mergeMode"`
PluginType string `json:"pluginType"`
SelectorOrID string `json:"selectorOrID"`
}
func (InitErrorInfo) isErrorInfo() {}
type InternalErrorInfo map[string]any
func (InternalErrorInfo) isErrorInfo() {}
func setupErrors(router chi.Router) error {
type initComponentFn func(name string, params *InitErrorInfo) templ.Component
type internalComponentFn func(name string, params *InternalErrorInfo) templ.Component
type runtimeComponentFn func(name string, params *RuntimeErrorInfo) templ.Component
type anyComponentFn func(name string, params ErrorInfo) templ.Component
type componentGenerator struct {
ComponentFn anyComponentFn
Type string
}
initFn := func(fn initComponentFn) componentGenerator {
return componentGenerator{
Type: "init",
ComponentFn: func(name string, params ErrorInfo) templ.Component {
return fn(name, params.(*InitErrorInfo))
},
}
}
internalFn := func(fn internalComponentFn) componentGenerator {
return componentGenerator{
Type: "internal",
ComponentFn: func(name string, params ErrorInfo) templ.Component {
return fn(name, params.(*InternalErrorInfo))
},
}
}
runtimeFn := func(fn runtimeComponentFn) componentGenerator {
return componentGenerator{
Type: "runtime",
ComponentFn: func(name string, params ErrorInfo) templ.Component {
return fn(name, params.(*RuntimeErrorInfo))
},
}
}
sharedInternalErrFn := internalFn(InternalErrorView)
reasonComponents := map[string]componentGenerator{
"batch_error": sharedInternalErrFn,
"cleanup_effect_error": sharedInternalErrFn,
"end_effect_error": sharedInternalErrFn,
"get_computed_error": sharedInternalErrFn,
"invalid_morph_style": sharedInternalErrFn,
"invalid_signal_key": sharedInternalErrFn,
"new_element_could_not_be_created": sharedInternalErrFn,
"no_best_match_found": sharedInternalErrFn,
"no_content_found": sharedInternalErrFn,
"no_parent_element_found": sharedInternalErrFn,
"no_temporary_element_found": sharedInternalErrFn,
"signal_cycle_detected": sharedInternalErrFn,
"signal_not_found": sharedInternalErrFn,
"invalid_merge_mode": initFn(InvalidMergeMode),
"invalid_plugin_type": initFn(InvalidPluginType),
"morph_failed": initFn(MorphFailed),
"no_fragments_found": initFn(NoFragmentsFound),
"no_paths_provided": initFn(NoPathsProvided),
"no_selector_provided": initFn(NoSelectorProvided),
"no_script_provided": initFn(NoScriptProvided),
"no_targets_found": initFn(NoTargetsFound),
"attr_value_required": runtimeFn(AttrValueRequired),
"bind_key_and_value_provided": runtimeFn(BindKeyAndValueProvided),
"bind_key_or_value_required": runtimeFn(BindKeyOrValueRequired),
"bind_select_multiple": runtimeFn(BindSelectMultiple),
"bind_unsupported_signal_type": runtimeFn(BindUnsupportedSignalType),
"class_value_required": runtimeFn(ClassValueRequired),
"clipboard_not_available": runtimeFn(ClipboardNotAvailable),
"computed_key_required": runtimeFn(ComputedKeyRequired),
"computed_value_required": runtimeFn(ComputedValueRequired),
"custom_validity_invalid_element": runtimeFn(CustomValidityInvalidElement),
"custom_validity_invalid_expression": runtimeFn(CustomValidityInvalidExpression),
"custom_validity_key_not_allowed": runtimeFn(CustomValidityKeyNotAllowed),
"custom_validity_value_required": runtimeFn(CustomValidityValueRequired),
"execute_expression": runtimeFn(ExecuteExpression),
"generate_expression": runtimeFn(GenerateExpression),
"indicator_key_and_value_provided": runtimeFn(IndicatorKeyAndValueProvided),
"indicator_key_or_value_required": runtimeFn(IndicatorKeyOrValueRequired),
"invalid_content_type": runtimeFn(InvalidContentType),
"invalid_data_uri": runtimeFn(InvalidDataUri),
"invalid_file_result_type": runtimeFn(InvalidFileResultType),
"on_key_required": runtimeFn(OnKeyRequired),
"on_value_required": runtimeFn(OnValueRequired),
"on_intersect_key_not_allowed": runtimeFn(OnIntersectKeyNotAllowed),
"on_intersect_value_required": runtimeFn(OnIntersectValueRequired),
"on_interval_key_not_allowed": runtimeFn(OnIntervalKeyNotAllowed),
"on_interval_value_required": runtimeFn(OnIntervalValueRequired),
"on_load_key_not_allowed": runtimeFn(OnLoadKeyNotAllowed),
"on_load_value_required": runtimeFn(OnLoadValueRequired),
"on_raf_key_not_allowed": runtimeFn(OnRafKeyNotAllowed),
"on_raf_value_required": runtimeFn(OnRafValueRequired),
"on_signal_change_key_not_allowed": runtimeFn(OnSignalChangeKeyNotAllowed),
"on_signal_change_value_required": runtimeFn(OnSignalChangeValueRequired),
"ref_key_and_value_provided": runtimeFn(RefKeyAndValueProvided),
"ref_key_or_value_required": runtimeFn(RefKeyOrValueRequired),
"replace_url_key_not_allowed": runtimeFn(ReplaceUrlKeyNotAllowed),
"replace_url_value_required": runtimeFn(ReplaceUrlValueRequired),
"scroll_into_view_invalid_element": runtimeFn(ScrollIntoViewInvalidElement),
"scroll_into_view_key_not_allowed": runtimeFn(ScrollIntoViewKeyNotAllowed),
"scroll_into_view_value_not_allowed": runtimeFn(ScrollIntoViewValueNotAllowed),
"show_key_not_allowed": runtimeFn(ShowKeyNotAllowed),
"show_value_required": runtimeFn(ShowValueRequired),
"signals_value_required": runtimeFn(SignalsValueRequired),
"sse_closest_form_not_found": runtimeFn(SseClosestFormNotFound),
"sse_fetch_failed": runtimeFn(SseFetchFailed),
"sse_form_not_found": runtimeFn(SseFormNotFound),
"sse_invalid_content_type": runtimeFn(SseInvalidContentType),
"sse_no_url_provided": runtimeFn(SseNoUrlProvided),
"text_invalid_element": runtimeFn(TextInvalidElement),
"text_key_not_allowed": runtimeFn(TextKeyNotAllowed),
"text_value_required": runtimeFn(TextValueRequired),
"view_transition_key_not_allowed": runtimeFn(ViewTransitionKeyNotAllowed),
"view_transition_value_required": runtimeFn(ViewTransitionValueRequired),
}
sidebarLinks := make([]*SidebarLink, 0, len(reasonComponents))
for id := range reasonComponents {
sidebarLinks = append(sidebarLinks, &SidebarLink{ID: id})
}
slices.SortFunc(sidebarLinks, func(a, b *SidebarLink) int {
return strings.Compare(a.ID, b.ID)
})
sidebarGroups := []*SidebarGroup{
{
Label: "Errors",
Links: sidebarLinks,
},
}
lo.ForEach(sidebarGroups, func(group *SidebarGroup, grpIdx int) {
lo.ForEach(group.Links, func(link *SidebarLink, linkIdx int) {
typ := reasonComponents[link.ID].Type
link.URL = templ.SafeURL(fmt.Sprintf("/errors/%s/%s", typ, link.ID))
link.Label = toolbelt.Pascal(link.ID)
if linkIdx > 0 {
link.Prev = group.Links[linkIdx-1]
} else if grpIdx > 0 {
prvGrp := sidebarGroups[grpIdx-1]
link.Prev = prvGrp.Links[len(prvGrp.Links)-1]
}
if linkIdx < len(group.Links)-1 {
link.Next = group.Links[linkIdx+1]
} else if grpIdx < len(sidebarGroups)-1 {
nxtGrp := sidebarGroups[grpIdx+1]
link.Next = nxtGrp.Links[0]
}
})
})
router.Route("/errors", func(errorsRouter chi.Router) {
errorsRouter.Get("/", func(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, string(sidebarGroups[0].Links[0].URL), http.StatusFound)
})
errorsRouter.Get("/{type}/{reason}", func(w http.ResponseWriter, r *http.Request) {
typ := chi.URLParam(r, "type")
reason := chi.URLParam(r, "reason")
var currentLink *SidebarLink
for _, group := range sidebarGroups {
for _, link := range group.Links {
if link.ID == reason {
currentLink = link
break
}
}
}
metadataJSON := r.URL.Query().Get("metadata")
if metadataJSON == "" {
metadataJSON = "{}"
}
var params ErrorInfo
switch typ {
case "runtime":
params = &RuntimeErrorInfo{}
case "init":
params = &InitErrorInfo{}
case "internal":
params = &InternalErrorInfo{}
default:
http.Error(w, fmt.Sprintf("unknown error type %q", typ), http.StatusBadRequest)
return
}
if err := json.Unmarshal([]byte(metadataJSON), params); err != nil {
http.Error(w, fmt.Sprintf("failed to unmarshal metadata: %v", err), http.StatusBadRequest)
return
}
buf := bytebufferpool.Get()
defer bytebufferpool.Put(buf)
var (
c templ.Component
)
compGen, ok := reasonComponents[reason]
if ok {
c = compGen.ComponentFn(currentLink.Label, params)
} else {
if typ == "internal" {
p := params.(*InternalErrorInfo)
c = InternalErrorView(reason, p)
} else {
http.Error(w, "not found", http.StatusNotFound)
return
}
}
ctx := r.Context()
if err := c.Render(ctx, buf); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
contents := buf.String()
SidebarPage(r, sidebarGroups, currentLink, "Errors", "Error details", contents).Render(ctx, w)
})
})
return nil
}
func code(lang, source string) templ.Component {
buf := bytebufferpool.Get()
defer bytebufferpool.Put(buf)
unescapedSource := html.UnescapeString(source)
err := htmlHighlight(buf, unescapedSource, lang, "")
contents := buf.String()
return templ.Raw(contents, err)
}
func htmlSource() templ.Component {
return templ.ComponentFunc(func(ctx context.Context, w io.Writer) error {
children := templ.GetChildren(ctx)
buf := bytebufferpool.Get()
defer bytebufferpool.Put(buf)
if err := children.Render(ctx, buf); err != nil {
return err
}
return code("html", buf.String()).Render(ctx, w)
})
}
func getValue(value string, defaultValue string) string {
if value == "" {
return defaultValue
}
return value
}