NOISSUE - Refactor reports template validation (#260)

* fix validation

Signed-off-by: nyagamunene <stevenyaga2014@gmail.com>

* fix failing linter

Signed-off-by: nyagamunene <stevenyaga2014@gmail.com>

* remove unused code

Signed-off-by: nyagamunene <stevenyaga2014@gmail.com>

* NOISSUE - Bump github.com/nats-io/nats.go from 1.43.0 to 1.44.0 (#262)

Bumps [github.com/nats-io/nats.go](https://github.com/nats-io/nats.go) from 1.43.0 to 1.44.0.
- [Release notes](https://github.com/nats-io/nats.go/releases)
- [Commits](https://github.com/nats-io/nats.go/compare/v1.43.0...v1.44.0)

---
updated-dependencies:
- dependency-name: github.com/nats-io/nats.go
  dependency-version: 1.44.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* NOISSUE -  Update to using OpenBao in certs service (#259)

* chore: update smq dep

Signed-off-by: Felix Gateru <felix.gateru@gmail.com>

* chore: remove am-certs and replace with open-bao

Signed-off-by: Felix Gateru <felix.gateru@gmail.com>

* refactor: remove vault references

Signed-off-by: Felix Gateru <felix.gateru@gmail.com>

---------

Signed-off-by: Felix Gateru <felix.gateru@gmail.com>

* NOISSUE - Bump github.com/prometheus/client_golang from 1.22.0 to 1.23.0 (#263)

Bumps [github.com/prometheus/client_golang](https://github.com/prometheus/client_golang) from 1.22.0 to 1.23.0.
- [Release notes](https://github.com/prometheus/client_golang/releases)
- [Changelog](https://github.com/prometheus/client_golang/blob/v1.23.0/CHANGELOG.md)
- [Commits](https://github.com/prometheus/client_golang/compare/v1.22.0...v1.23.0)

---
updated-dependencies:
- dependency-name: github.com/prometheus/client_golang
  dependency-version: 1.23.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* address comments

Signed-off-by: nyagamunene <stevenyaga2014@gmail.com>

* failing linter

Signed-off-by: nyagamunene <stevenyaga2014@gmail.com>

* remove walk node method

Signed-off-by: nyagamunene <stevenyaga2014@gmail.com>

* address comments

Signed-off-by: nyagamunene <stevenyaga2014@gmail.com>

---------

Signed-off-by: nyagamunene <stevenyaga2014@gmail.com>
Signed-off-by: dependabot[bot] <support@github.com>
Signed-off-by: Felix Gateru <felix.gateru@gmail.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Felix Gateru <felix.gateru@gmail.com>
This commit is contained in:
Steve Munene
2025-08-04 15:56:46 +03:00
committed by GitHub
parent c07922e0be
commit 067bdc3631
+100 -93
View File
@@ -6,48 +6,8 @@ package reports
import (
"encoding/json"
"fmt"
"regexp"
"strings"
)
var (
requiredFields = []string{
"{{$.Title}}",
"{{$.GeneratedDate}}",
"{{$.GeneratedTime}}",
"{{.Metric.Name}}",
"{{.Metric.ClientID}}",
"{{.Metric.ChannelID}}",
"{{len .Messages}}",
"{{range .Messages}}",
"{{formatTime .Time}}",
"{{formatValue .}}",
"{{.Unit}}",
"{{.Protocol}}",
"{{.Subtopic}}",
"{{end}}",
}
requiredStructure = []string{
"<!DOCTYPE html>",
"<html",
"<head>",
"<body>",
"<style>",
"</style>",
"</head>",
"</body>",
"</html>",
}
requiredCSS = []string{
".page",
".header",
".content-area",
".metrics-section",
".data-table",
".footer",
}
"text/template"
"text/template/parse"
)
type ReportTemplate string
@@ -71,65 +31,112 @@ func (temp *ReportTemplate) UnmarshalJSON(data []byte) error {
}
func (temp ReportTemplate) Validate() error {
template := string(temp)
templateStr := string(temp)
for _, required := range requiredStructure {
if !strings.Contains(template, required) {
return fmt.Errorf("missing required HTML element: %s", required)
// Validate template syntax using Go's template parser
tmpl := template.New("validate").Funcs(template.FuncMap{
"add": func(a, b int) int { return a + b },
"formatTime": func(t interface{}) string { return "" },
"formatValue": func(v interface{}) string { return "" },
})
parsed, err := tmpl.Parse(templateStr)
if err != nil {
return fmt.Errorf("template syntax error: %w", err)
}
var hasTitle, hasRange, hasFormatTime, hasFormatValue, hasEnd bool
// Validate essential fields are present using template parsing
if err := validateEssentialFields(parsed.Tree.Root, &hasTitle, &hasRange, &hasFormatTime, &hasFormatValue, &hasEnd); err != nil {
return err
}
if !hasTitle {
return fmt.Errorf("missing essential template field: {{$.Title}}")
}
if !hasRange {
return fmt.Errorf("missing essential template field: {{range .Messages}}")
}
if !hasFormatTime {
return fmt.Errorf("missing essential template field: {{formatTime .Time}}")
}
if !hasFormatValue {
return fmt.Errorf("missing essential template field: {{formatValue .}}")
}
if !hasEnd {
return fmt.Errorf("missing essential template field: {{end}}")
}
return nil
}
func validateEssentialFields(node parse.Node, hasTitle, hasRange, hasFormatTime, hasFormatValue, hasEnd *bool) error {
if node == nil {
return nil
}
switch n := node.(type) {
case *parse.ListNode:
for _, sub := range n.Nodes {
if err := validateEssentialFields(sub, hasTitle, hasRange, hasFormatTime, hasFormatValue, hasEnd); err != nil {
return err
}
}
}
cleaned := strings.TrimSpace(template)
commentPattern := regexp.MustCompile(`^(?s:<!--.*?-->\s*)*`)
cleaned = commentPattern.ReplaceAllString(cleaned, "")
if !strings.HasPrefix(cleaned, "<!DOCTYPE html>") {
return fmt.Errorf("template must start with <!DOCTYPE html>")
}
for _, field := range requiredFields {
if !strings.Contains(template, field) {
return fmt.Errorf("missing required template field: %s", field)
case *parse.ActionNode:
if n.Pipe != nil {
for _, cmd := range n.Pipe.Cmds {
cmdStr := cmd.String()
if cmdStr == "$.Title" {
*hasTitle = true
}
if len(cmd.Args) > 0 {
firstArg := cmd.Args[0].String()
if firstArg == "formatTime" {
*hasFormatTime = true
}
if firstArg == "formatValue" {
*hasFormatValue = true
}
}
}
}
}
blockStartPattern := regexp.MustCompile(`\{\{\s*(range|if|with)\b[^{}]*\}\}`)
blockEndPattern := regexp.MustCompile(`\{\{\s*end\s*\}\}`)
blockStarts := blockStartPattern.FindAllString(template, -1)
blockEnds := blockEndPattern.FindAllString(template, -1)
if len(blockStarts) != len(blockEnds) {
return fmt.Errorf("unmatched template blocks: found %d block start(s) (range/if/with) and %d end(s)",
len(blockStarts), len(blockEnds))
}
for _, class := range requiredCSS {
pattern := fmt.Sprintf(`\.%s\s*\{`, strings.TrimPrefix(class, "."))
matched, _ := regexp.MatchString(pattern, template)
if !matched {
return fmt.Errorf("missing required CSS class: %s", class)
case *parse.RangeNode:
if n.Pipe != nil && len(n.Pipe.Cmds) > 0 {
cmdStr := n.Pipe.Cmds[0].String()
if cmdStr == ".Messages" {
*hasRange = true
}
}
}
requiredTableElements := []string{
"<table",
"<thead>",
"<tbody>",
"<th",
"<td",
}
for _, element := range requiredTableElements {
if !strings.Contains(template, element) {
return fmt.Errorf("missing required table element: %s", element)
if err := validateEssentialFields(n.List, hasTitle, hasRange, hasFormatTime, hasFormatValue, hasEnd); err != nil {
return err
}
}
if n.ElseList != nil {
if err := validateEssentialFields(n.ElseList, hasTitle, hasRange, hasFormatTime, hasFormatValue, hasEnd); err != nil {
return err
}
}
*hasEnd = true
expectedHeaders := []string{"Time", "Value", "Unit", "Protocol", "Subtopic"}
for _, header := range expectedHeaders {
if !strings.Contains(template, header) {
return fmt.Errorf("missing expected table header: %s", header)
case *parse.IfNode:
if err := validateEssentialFields(n.List, hasTitle, hasRange, hasFormatTime, hasFormatValue, hasEnd); err != nil {
return err
}
if n.ElseList != nil {
if err := validateEssentialFields(n.ElseList, hasTitle, hasRange, hasFormatTime, hasFormatValue, hasEnd); err != nil {
return err
}
}
case *parse.WithNode:
if err := validateEssentialFields(n.List, hasTitle, hasRange, hasFormatTime, hasFormatValue, hasEnd); err != nil {
return err
}
if n.ElseList != nil {
if err := validateEssentialFields(n.ElseList, hasTitle, hasRange, hasFormatTime, hasFormatValue, hasEnd); err != nil {
return err
}
}
}