Add custom-api options and template requests
This commit is contained in:
@@ -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":
|
||||
|
||||
Reference in New Issue
Block a user