Add custom-api options and template requests

This commit is contained in:
Svilen Markov
2025-05-11 15:49:21 +01:00
parent 49c07f397e
commit 7bbf103e01
3 changed files with 248 additions and 13 deletions

View File

@@ -41,6 +41,7 @@ type customAPIWidget struct {
widgetBase `yaml:",inline"`
*CustomAPIRequest `yaml:",inline"` // the primary request
Subrequests map[string]*CustomAPIRequest `yaml:"subrequests"`
Options customAPIOptions `yaml:"options"`
Template string `yaml:"template"`
Frameless bool `yaml:"frameless"`
compiledTemplate *template.Template `yaml:"-"`
@@ -75,7 +76,9 @@ func (widget *customAPIWidget) initialize() error {
}
func (widget *customAPIWidget) update(ctx context.Context) {
compiledHTML, err := fetchAndParseCustomAPI(widget.CustomAPIRequest, widget.Subrequests, widget.compiledTemplate)
compiledHTML, err := fetchAndRenderCustomAPIRequest(
widget.CustomAPIRequest, widget.Subrequests, widget.Options, widget.compiledTemplate,
)
if !widget.canContinueUpdateAfterHandlingErr(err) {
return
}
@@ -87,9 +90,36 @@ func (widget *customAPIWidget) Render() template.HTML {
return widget.renderTemplate(widget, customAPIWidgetTemplate)
}
type customAPIOptions map[string]any
func (o *customAPIOptions) StringOr(key, defaultValue string) string {
return customAPIGetOptionOrDefault(*o, key, defaultValue)
}
func (o *customAPIOptions) IntOr(key string, defaultValue int) int {
return customAPIGetOptionOrDefault(*o, key, defaultValue)
}
func (o *customAPIOptions) FloatOr(key string, defaultValue float64) float64 {
return customAPIGetOptionOrDefault(*o, key, defaultValue)
}
func (o *customAPIOptions) BoolOr(key string, defaultValue bool) bool {
return customAPIGetOptionOrDefault(*o, key, defaultValue)
}
func customAPIGetOptionOrDefault[T any](o customAPIOptions, key string, defaultValue T) T {
if value, exists := o[key]; exists {
if typedValue, ok := value.(T); ok {
return typedValue
}
}
return defaultValue
}
func (req *CustomAPIRequest) initialize() error {
if req.URL == "" {
return errors.New("URL is required")
if req == nil || req.URL == "" {
return nil
}
if req.Body != nil {
@@ -156,6 +186,7 @@ type customAPIResponseData struct {
type customAPITemplateData struct {
*customAPIResponseData
subrequests map[string]*customAPIResponseData
Options customAPIOptions
}
func (data *customAPITemplateData) JSONLines() []decoratedGJSONResult {
@@ -183,7 +214,14 @@ func (data *customAPITemplateData) Subrequest(key string) *customAPIResponseData
return req
}
func fetchCustomAPIRequest(ctx context.Context, req *CustomAPIRequest) (*customAPIResponseData, error) {
func fetchCustomAPIResponse(ctx context.Context, req *CustomAPIRequest) (*customAPIResponseData, error) {
if req == nil || req.URL == "" {
return &customAPIResponseData{
JSON: decoratedGJSONResult{gjson.Result{}},
Response: &http.Response{},
}, nil
}
if req.bodyReader != nil {
req.bodyReader.Seek(0, io.SeekStart)
}
@@ -217,17 +255,16 @@ func fetchCustomAPIRequest(ctx context.Context, req *CustomAPIRequest) (*customA
}
data := &customAPIResponseData{
return &customAPIResponseData{
JSON: decoratedGJSONResult{gjson.Parse(body)},
Response: resp,
}
return data, nil
}, nil
}
func fetchAndParseCustomAPI(
func fetchAndRenderCustomAPIRequest(
primaryReq *CustomAPIRequest,
subReqs map[string]*CustomAPIRequest,
options customAPIOptions,
tmpl *template.Template,
) (template.HTML, error) {
var primaryData *customAPIResponseData
@@ -236,7 +273,7 @@ func fetchAndParseCustomAPI(
if len(subReqs) == 0 {
// If there are no subrequests, we can fetch the primary request in a much simpler way
primaryData, err = fetchCustomAPIRequest(context.Background(), primaryReq)
primaryData, err = fetchCustomAPIResponse(context.Background(), primaryReq)
} else {
// If there are subrequests, we need to fetch them concurrently
// and cancel all requests if any of them fail. There's probably
@@ -251,7 +288,7 @@ func fetchAndParseCustomAPI(
go func() {
defer wg.Done()
var localErr error
primaryData, localErr = fetchCustomAPIRequest(ctx, primaryReq)
primaryData, localErr = fetchCustomAPIResponse(ctx, primaryReq)
mu.Lock()
if localErr != nil && err == nil {
err = localErr
@@ -266,7 +303,7 @@ func fetchAndParseCustomAPI(
defer wg.Done()
var localErr error
var data *customAPIResponseData
data, localErr = fetchCustomAPIRequest(ctx, req)
data, localErr = fetchCustomAPIResponse(ctx, req)
mu.Lock()
if localErr == nil {
subData[key] = data
@@ -290,6 +327,7 @@ func fetchAndParseCustomAPI(
data := customAPITemplateData{
customAPIResponseData: primaryData,
subrequests: subData,
Options: options,
}
var templateBuffer bytes.Buffer
@@ -462,6 +500,7 @@ var customAPITemplateFuncs = func() template.FuncMap {
"parseTime": func(layout, value string) time.Time {
return customAPIFuncParseTimeInLocation(layout, value, time.UTC)
},
"formatTime": customAPIFuncFormatTime,
"parseLocalTime": func(layout, value string) time.Time {
return customAPIFuncParseTimeInLocation(layout, value, time.Local)
},
@@ -569,6 +608,49 @@ var customAPITemplateFuncs = func() template.FuncMap {
}
return out
},
"newRequest": func(url string) *CustomAPIRequest {
return &CustomAPIRequest{
URL: url,
}
},
"withHeader": func(key, value string, req *CustomAPIRequest) *CustomAPIRequest {
if req.Headers == nil {
req.Headers = make(map[string]string)
}
req.Headers[key] = value
return req
},
"withParameter": func(key, value string, req *CustomAPIRequest) *CustomAPIRequest {
if req.Parameters == nil {
req.Parameters = make(queryParametersField)
}
req.Parameters[key] = append(req.Parameters[key], value)
return req
},
"withStringBody": func(body string, req *CustomAPIRequest) *CustomAPIRequest {
req.Body = body
req.BodyType = "string"
return req
},
"getResponse": func(req *CustomAPIRequest) *customAPIResponseData {
err := req.initialize()
if err != nil {
panic(fmt.Sprintf("initializing request: %v", err))
}
data, err := fetchCustomAPIResponse(context.Background(), req)
if err != nil {
slog.Error("Could not fetch response within custom API template", "error", err)
return &customAPIResponseData{
JSON: decoratedGJSONResult{gjson.Result{}},
Response: &http.Response{
Status: err.Error(),
},
}
}
return data
},
}
for key, value := range globalTemplateFunctions {
@@ -580,6 +662,23 @@ var customAPITemplateFuncs = func() template.FuncMap {
return funcs
}()
func customAPIFuncFormatTime(layout string, t time.Time) string {
switch strings.ToLower(layout) {
case "unix":
return strconv.FormatInt(t.Unix(), 10)
case "rfc3339":
layout = time.RFC3339
case "rfc3339nano":
layout = time.RFC3339Nano
case "datetime":
layout = time.DateTime
case "dateonly":
layout = time.DateOnly
}
return t.Format(layout)
}
func customAPIFuncParseTimeInLocation(layout, value string, loc *time.Location) time.Time {
switch strings.ToLower(layout) {
case "unix":