master
go 145 lines 3.48 KB
Raw
1 // SPDX-License-Identifier: GPL-3.0-or-later
2
3 package main
4
5 import (
6 "encoding/json"
7 "flag"
8 "fmt"
9 "io"
10 "os"
11 "path/filepath"
12
13 topologyv1 "github.com/netdata/netdata/go/plugins/pkg/topology/v1"
14 "github.com/santhosh-tekuri/jsonschema/v6"
15 )
16
17 func main() {
18 var schemaPath string
19 var inputPath string
20 var minRows int
21 var requireRows bool
22
23 flag.StringVar(&schemaPath, "schema", "", "schema file path (optional)")
24 flag.StringVar(&inputPath, "input", "", "input JSON file (default: stdin)")
25 flag.IntVar(&minRows, "min-rows", 0, "minimum rows required in data responses (0 disables row check)")
26 flag.BoolVar(&requireRows, "require-rows", false, "require at least one row in data responses")
27 flag.Parse()
28
29 schemaBytes, err := loadSchema(schemaPath)
30 if err != nil {
31 exitErr("load schema: %v", err)
32 }
33
34 inputBytes, err := loadInput(inputPath)
35 if err != nil {
36 exitErr("load input: %v", err)
37 }
38
39 payload, err := validateJSON(schemaBytes, inputBytes)
40 if err != nil {
41 exitErr("%v", err)
42 }
43
44 if requireRows && minRows == 0 {
45 minRows = 1
46 }
47 if minRows > 0 {
48 rows, err := countRows(payload)
49 if err != nil {
50 exitErr("row check failed: %v", err)
51 }
52 if rows < minRows {
53 exitErr("row check failed: expected at least %d rows, got %d", minRows, rows)
54 }
55 }
56 }
57
58 func validateJSON(schemaBytes, inputBytes []byte) (any, error) {
59 var payload any
60 if err := json.Unmarshal(inputBytes, &payload); err != nil {
61 return nil, fmt.Errorf("parse input JSON: %w", err)
62 }
63
64 var schemaDoc any
65 if err := json.Unmarshal(schemaBytes, &schemaDoc); err != nil {
66 return nil, fmt.Errorf("parse schema JSON: %w", err)
67 }
68
69 compiler := jsonschema.NewCompiler()
70 if err := compiler.AddResource("schema.json", schemaDoc); err != nil {
71 return nil, fmt.Errorf("add schema resource: %w", err)
72 }
73 schema, err := compiler.Compile("schema.json")
74 if err != nil {
75 return nil, fmt.Errorf("compile schema: %w", err)
76 }
77
78 if err := schema.Validate(payload); err != nil {
79 return nil, fmt.Errorf("validation failed: %w", err)
80 }
81
82 if obj, ok := payload.(map[string]any); ok && isTopologyResponse(obj) {
83 if data, ok := obj["data"]; ok && topologyv1.IsDecodedData(data) {
84 if err := topologyv1.ValidateDecodedResponse(payload); err != nil {
85 return nil, fmt.Errorf("topology validation failed: %w", err)
86 }
87 }
88 }
89
90 return payload, nil
91 }
92
93 func countRows(payload any) (int, error) {
94 obj, ok := payload.(map[string]any)
95 if !ok {
96 return 0, fmt.Errorf("expected JSON object")
97 }
98
99 if errMsg, ok := obj["errorMessage"]; ok {
100 if s, ok := errMsg.(string); ok && s != "" {
101 return 0, fmt.Errorf("error response: %s", s)
102 }
103 return 0, fmt.Errorf("error response without message")
104 }
105
106 data, ok := obj["data"]
107 if !ok {
108 return 0, fmt.Errorf("missing data field")
109 }
110
111 if isTopologyResponse(obj) && topologyv1.IsDecodedData(data) {
112 return topologyv1.GraphRowsFromDecodedData(data)
113 }
114
115 rows, ok := data.([]any)
116 if !ok {
117 return 0, fmt.Errorf("data is not an array")
118 }
119
120 return len(rows), nil
121 }
122
123 func isTopologyResponse(obj map[string]any) bool {
124 responseType, ok := obj["type"].(string)
125 return ok && responseType == "topology"
126 }
127
128 func loadSchema(path string) ([]byte, error) {
129 if path == "" {
130 path = filepath.Clean(filepath.Join("..", "plugins.d", "FUNCTION_UI_SCHEMA.json"))
131 }
132 return os.ReadFile(path)
133 }
134
135 func loadInput(path string) ([]byte, error) {
136 if path == "" || path == "-" {
137 return io.ReadAll(os.Stdin)
138 }
139 return os.ReadFile(path)
140 }
141
142 func exitErr(format string, args ...any) {
143 fmt.Fprintf(os.Stderr, format+"\n", args...)
144 os.Exit(1)
145 }