aboutsummaryrefslogtreecommitdiffstats
path: root/pkg/aflow/schema.go
diff options
context:
space:
mode:
Diffstat (limited to 'pkg/aflow/schema.go')
-rw-r--r--pkg/aflow/schema.go102
1 files changed, 102 insertions, 0 deletions
diff --git a/pkg/aflow/schema.go b/pkg/aflow/schema.go
new file mode 100644
index 000000000..e34d465ea
--- /dev/null
+++ b/pkg/aflow/schema.go
@@ -0,0 +1,102 @@
+// Copyright 2025 syzkaller project authors. All rights reserved.
+// Use of this source code is governed by Apache 2 LICENSE that can be found in the LICENSE file.
+
+package aflow
+
+import (
+ "encoding/json"
+ "fmt"
+ "iter"
+ "maps"
+ "reflect"
+
+ "github.com/google/jsonschema-go/jsonschema"
+)
+
+func schemaFor[T any]() (*jsonschema.Schema, error) {
+ typ := reflect.TypeFor[T]()
+ if typ.Kind() != reflect.Struct {
+ return nil, fmt.Errorf("%v is not a struct", typ.Name())
+ }
+ for _, field := range reflect.VisibleFields(typ) {
+ if field.Tag.Get("jsonschema") == "" {
+ return nil, fmt.Errorf("%v.%v does not have a jsonschema tag with description",
+ typ.Name(), field.Name)
+ }
+ }
+ schema, err := jsonschema.For[T](nil)
+ if err != nil {
+ return nil, err
+ }
+ resolved, err := schema.Resolve(nil)
+ if err != nil {
+ return nil, err
+ }
+ return resolved.Schema(), nil
+}
+
+func mustSchemaFor[T any]() *jsonschema.Schema {
+ schema, err := schemaFor[T]()
+ if err != nil {
+ panic(err)
+ }
+ return schema
+}
+
+func convertToMap[T any](val T) map[string]any {
+ res := make(map[string]any)
+ for name, val := range foreachField(&val) {
+ res[name] = val.Interface()
+ }
+ return res
+}
+
+// convertFromMap converts an untyped map to a struct.
+// It always ensures that all struct fields are present in the map.
+// In the strict mode it also checks that the map does not contain any other unused elements.
+func convertFromMap[T any](m map[string]any, strict bool) (T, error) {
+ m = maps.Clone(m)
+ var val T
+ for name, field := range foreachField(&val) {
+ f, ok := m[name]
+ if !ok {
+ return val, fmt.Errorf("field %v is not present when converting map to %T", name, val)
+ }
+ delete(m, name)
+ if mm, ok := f.(map[string]any); ok && field.Type() == reflect.TypeFor[json.RawMessage]() {
+ raw, err := json.Marshal(mm)
+ if err != nil {
+ return val, err
+ }
+ field.Set(reflect.ValueOf(json.RawMessage(raw)))
+ } else {
+ field.Set(reflect.ValueOf(f))
+ }
+ }
+ if strict && len(m) != 0 {
+ return val, fmt.Errorf("unused fields when converting map to %T: %v", val, m)
+ }
+ return val, nil
+}
+
+// foreachField iterates over all public fields of the struct provided in data.
+func foreachField(data any) iter.Seq2[string, reflect.Value] {
+ return func(yield func(string, reflect.Value) bool) {
+ v := reflect.ValueOf(data).Elem()
+ for _, field := range reflect.VisibleFields(v.Type()) {
+ if !yield(field.Name, v.FieldByIndex(field.Index)) {
+ break
+ }
+ }
+ }
+}
+
+func foreachFieldOf[T any]() iter.Seq2[string, reflect.Type] {
+ return func(yield func(string, reflect.Type) bool) {
+ for name, val := range foreachField(new(T)) {
+ if !yield(name, val.Type()) {
+ break
+ }
+ }
+ }
+}