| |
| package util |
|
|
| import ( |
| "fmt" |
| "sort" |
| "strings" |
|
|
| "github.com/tidwall/gjson" |
| "github.com/tidwall/sjson" |
| ) |
|
|
| var gjsonPathKeyReplacer = strings.NewReplacer(".", "\\.", "*", "\\*", "?", "\\?") |
|
|
| |
| |
| |
| func CleanJSONSchemaForAntigravity(jsonStr string) string { |
| |
| jsonStr = convertRefsToHints(jsonStr) |
| jsonStr = convertConstToEnum(jsonStr) |
| jsonStr = addEnumHints(jsonStr) |
| jsonStr = addAdditionalPropertiesHints(jsonStr) |
| jsonStr = moveConstraintsToDescription(jsonStr) |
|
|
| |
| jsonStr = mergeAllOf(jsonStr) |
| jsonStr = flattenAnyOfOneOf(jsonStr) |
| jsonStr = flattenTypeArrays(jsonStr) |
|
|
| |
| jsonStr = removeUnsupportedKeywords(jsonStr) |
| jsonStr = cleanupRequiredFields(jsonStr) |
|
|
| |
| jsonStr = addEmptySchemaPlaceholder(jsonStr) |
|
|
| return jsonStr |
| } |
|
|
| |
| func convertRefsToHints(jsonStr string) string { |
| paths := findPaths(jsonStr, "$ref") |
| sortByDepth(paths) |
|
|
| for _, p := range paths { |
| refVal := gjson.Get(jsonStr, p).String() |
| defName := refVal |
| if idx := strings.LastIndex(refVal, "/"); idx >= 0 { |
| defName = refVal[idx+1:] |
| } |
|
|
| parentPath := trimSuffix(p, ".$ref") |
| hint := fmt.Sprintf("See: %s", defName) |
| if existing := gjson.Get(jsonStr, descriptionPath(parentPath)).String(); existing != "" { |
| hint = fmt.Sprintf("%s (%s)", existing, hint) |
| } |
|
|
| replacement := `{"type":"object","description":""}` |
| replacement, _ = sjson.Set(replacement, "description", hint) |
| jsonStr = setRawAt(jsonStr, parentPath, replacement) |
| } |
| return jsonStr |
| } |
|
|
| func convertConstToEnum(jsonStr string) string { |
| for _, p := range findPaths(jsonStr, "const") { |
| val := gjson.Get(jsonStr, p) |
| if !val.Exists() { |
| continue |
| } |
| enumPath := trimSuffix(p, ".const") + ".enum" |
| if !gjson.Get(jsonStr, enumPath).Exists() { |
| jsonStr, _ = sjson.Set(jsonStr, enumPath, []interface{}{val.Value()}) |
| } |
| } |
| return jsonStr |
| } |
|
|
| func addEnumHints(jsonStr string) string { |
| for _, p := range findPaths(jsonStr, "enum") { |
| arr := gjson.Get(jsonStr, p) |
| if !arr.IsArray() { |
| continue |
| } |
| items := arr.Array() |
| if len(items) <= 1 || len(items) > 10 { |
| continue |
| } |
|
|
| var vals []string |
| for _, item := range items { |
| vals = append(vals, item.String()) |
| } |
| jsonStr = appendHint(jsonStr, trimSuffix(p, ".enum"), "Allowed: "+strings.Join(vals, ", ")) |
| } |
| return jsonStr |
| } |
|
|
| func addAdditionalPropertiesHints(jsonStr string) string { |
| for _, p := range findPaths(jsonStr, "additionalProperties") { |
| if gjson.Get(jsonStr, p).Type == gjson.False { |
| jsonStr = appendHint(jsonStr, trimSuffix(p, ".additionalProperties"), "No extra properties allowed") |
| } |
| } |
| return jsonStr |
| } |
|
|
| var unsupportedConstraints = []string{ |
| "minLength", "maxLength", "exclusiveMinimum", "exclusiveMaximum", |
| "pattern", "minItems", "maxItems", "format", |
| "default", "examples", |
| } |
|
|
| func moveConstraintsToDescription(jsonStr string) string { |
| for _, key := range unsupportedConstraints { |
| for _, p := range findPaths(jsonStr, key) { |
| val := gjson.Get(jsonStr, p) |
| if !val.Exists() || val.IsObject() || val.IsArray() { |
| continue |
| } |
| parentPath := trimSuffix(p, "."+key) |
| if isPropertyDefinition(parentPath) { |
| continue |
| } |
| jsonStr = appendHint(jsonStr, parentPath, fmt.Sprintf("%s: %s", key, val.String())) |
| } |
| } |
| return jsonStr |
| } |
|
|
| func mergeAllOf(jsonStr string) string { |
| paths := findPaths(jsonStr, "allOf") |
| sortByDepth(paths) |
|
|
| for _, p := range paths { |
| allOf := gjson.Get(jsonStr, p) |
| if !allOf.IsArray() { |
| continue |
| } |
| parentPath := trimSuffix(p, ".allOf") |
|
|
| for _, item := range allOf.Array() { |
| if props := item.Get("properties"); props.IsObject() { |
| props.ForEach(func(key, value gjson.Result) bool { |
| destPath := joinPath(parentPath, "properties."+escapeGJSONPathKey(key.String())) |
| jsonStr, _ = sjson.SetRaw(jsonStr, destPath, value.Raw) |
| return true |
| }) |
| } |
| if req := item.Get("required"); req.IsArray() { |
| reqPath := joinPath(parentPath, "required") |
| current := getStrings(jsonStr, reqPath) |
| for _, r := range req.Array() { |
| if s := r.String(); !contains(current, s) { |
| current = append(current, s) |
| } |
| } |
| jsonStr, _ = sjson.Set(jsonStr, reqPath, current) |
| } |
| } |
| jsonStr, _ = sjson.Delete(jsonStr, p) |
| } |
| return jsonStr |
| } |
|
|
| func flattenAnyOfOneOf(jsonStr string) string { |
| for _, key := range []string{"anyOf", "oneOf"} { |
| paths := findPaths(jsonStr, key) |
| sortByDepth(paths) |
|
|
| for _, p := range paths { |
| arr := gjson.Get(jsonStr, p) |
| if !arr.IsArray() || len(arr.Array()) == 0 { |
| continue |
| } |
|
|
| parentPath := trimSuffix(p, "."+key) |
| parentDesc := gjson.Get(jsonStr, descriptionPath(parentPath)).String() |
|
|
| items := arr.Array() |
| bestIdx, allTypes := selectBest(items) |
| selected := items[bestIdx].Raw |
|
|
| if parentDesc != "" { |
| selected = mergeDescriptionRaw(selected, parentDesc) |
| } |
|
|
| if len(allTypes) > 1 { |
| hint := "Accepts: " + strings.Join(allTypes, " | ") |
| selected = appendHintRaw(selected, hint) |
| } |
|
|
| jsonStr = setRawAt(jsonStr, parentPath, selected) |
| } |
| } |
| return jsonStr |
| } |
|
|
| func selectBest(items []gjson.Result) (bestIdx int, types []string) { |
| bestScore := -1 |
| for i, item := range items { |
| t := item.Get("type").String() |
| score := 0 |
|
|
| switch { |
| case t == "object" || item.Get("properties").Exists(): |
| score, t = 3, orDefault(t, "object") |
| case t == "array" || item.Get("items").Exists(): |
| score, t = 2, orDefault(t, "array") |
| case t != "" && t != "null": |
| score = 1 |
| default: |
| t = orDefault(t, "null") |
| } |
|
|
| if t != "" { |
| types = append(types, t) |
| } |
| if score > bestScore { |
| bestScore, bestIdx = score, i |
| } |
| } |
| return |
| } |
|
|
| func flattenTypeArrays(jsonStr string) string { |
| paths := findPaths(jsonStr, "type") |
| sortByDepth(paths) |
|
|
| nullableFields := make(map[string][]string) |
|
|
| for _, p := range paths { |
| res := gjson.Get(jsonStr, p) |
| if !res.IsArray() || len(res.Array()) == 0 { |
| continue |
| } |
|
|
| hasNull := false |
| var nonNullTypes []string |
| for _, item := range res.Array() { |
| s := item.String() |
| if s == "null" { |
| hasNull = true |
| } else if s != "" { |
| nonNullTypes = append(nonNullTypes, s) |
| } |
| } |
|
|
| firstType := "string" |
| if len(nonNullTypes) > 0 { |
| firstType = nonNullTypes[0] |
| } |
|
|
| jsonStr, _ = sjson.Set(jsonStr, p, firstType) |
|
|
| parentPath := trimSuffix(p, ".type") |
| if len(nonNullTypes) > 1 { |
| hint := "Accepts: " + strings.Join(nonNullTypes, " | ") |
| jsonStr = appendHint(jsonStr, parentPath, hint) |
| } |
|
|
| if hasNull { |
| parts := splitGJSONPath(p) |
| if len(parts) >= 3 && parts[len(parts)-3] == "properties" { |
| fieldNameEscaped := parts[len(parts)-2] |
| fieldName := unescapeGJSONPathKey(fieldNameEscaped) |
| objectPath := strings.Join(parts[:len(parts)-3], ".") |
| nullableFields[objectPath] = append(nullableFields[objectPath], fieldName) |
|
|
| propPath := joinPath(objectPath, "properties."+fieldNameEscaped) |
| jsonStr = appendHint(jsonStr, propPath, "(nullable)") |
| } |
| } |
| } |
|
|
| for objectPath, fields := range nullableFields { |
| reqPath := joinPath(objectPath, "required") |
| req := gjson.Get(jsonStr, reqPath) |
| if !req.IsArray() { |
| continue |
| } |
|
|
| var filtered []string |
| for _, r := range req.Array() { |
| if !contains(fields, r.String()) { |
| filtered = append(filtered, r.String()) |
| } |
| } |
|
|
| if len(filtered) == 0 { |
| jsonStr, _ = sjson.Delete(jsonStr, reqPath) |
| } else { |
| jsonStr, _ = sjson.Set(jsonStr, reqPath, filtered) |
| } |
| } |
| return jsonStr |
| } |
|
|
| func removeUnsupportedKeywords(jsonStr string) string { |
| keywords := append(unsupportedConstraints, |
| "$schema", "$defs", "definitions", "const", "$ref", "additionalProperties", |
| "propertyNames", |
| ) |
| for _, key := range keywords { |
| for _, p := range findPaths(jsonStr, key) { |
| if isPropertyDefinition(trimSuffix(p, "."+key)) { |
| continue |
| } |
| jsonStr, _ = sjson.Delete(jsonStr, p) |
| } |
| } |
| return jsonStr |
| } |
|
|
| func cleanupRequiredFields(jsonStr string) string { |
| for _, p := range findPaths(jsonStr, "required") { |
| parentPath := trimSuffix(p, ".required") |
| propsPath := joinPath(parentPath, "properties") |
|
|
| req := gjson.Get(jsonStr, p) |
| props := gjson.Get(jsonStr, propsPath) |
| if !req.IsArray() || !props.IsObject() { |
| continue |
| } |
|
|
| var valid []string |
| for _, r := range req.Array() { |
| key := r.String() |
| if props.Get(escapeGJSONPathKey(key)).Exists() { |
| valid = append(valid, key) |
| } |
| } |
|
|
| if len(valid) != len(req.Array()) { |
| if len(valid) == 0 { |
| jsonStr, _ = sjson.Delete(jsonStr, p) |
| } else { |
| jsonStr, _ = sjson.Set(jsonStr, p, valid) |
| } |
| } |
| } |
| return jsonStr |
| } |
|
|
| |
| |
| func addEmptySchemaPlaceholder(jsonStr string) string { |
| |
| paths := findPaths(jsonStr, "type") |
|
|
| |
| sortByDepth(paths) |
|
|
| for _, p := range paths { |
| typeVal := gjson.Get(jsonStr, p) |
| if typeVal.String() != "object" { |
| continue |
| } |
|
|
| |
| parentPath := trimSuffix(p, ".type") |
|
|
| |
| propsPath := joinPath(parentPath, "properties") |
| propsVal := gjson.Get(jsonStr, propsPath) |
| reqPath := joinPath(parentPath, "required") |
| reqVal := gjson.Get(jsonStr, reqPath) |
| hasRequiredProperties := reqVal.IsArray() && len(reqVal.Array()) > 0 |
|
|
| needsPlaceholder := false |
| if !propsVal.Exists() { |
| |
| needsPlaceholder = true |
| } else if propsVal.IsObject() && len(propsVal.Map()) == 0 { |
| |
| needsPlaceholder = true |
| } |
|
|
| if needsPlaceholder { |
| |
| reasonPath := joinPath(propsPath, "reason") |
| jsonStr, _ = sjson.Set(jsonStr, reasonPath+".type", "string") |
| jsonStr, _ = sjson.Set(jsonStr, reasonPath+".description", "Brief explanation of why you are calling this tool") |
|
|
| |
| jsonStr, _ = sjson.Set(jsonStr, reqPath, []string{"reason"}) |
| continue |
| } |
|
|
| |
| if propsVal.IsObject() && !hasRequiredProperties { |
| |
| |
| if parentPath == "" { |
| continue |
| } |
| placeholderPath := joinPath(propsPath, "_") |
| if !gjson.Get(jsonStr, placeholderPath).Exists() { |
| jsonStr, _ = sjson.Set(jsonStr, placeholderPath+".type", "boolean") |
| } |
| jsonStr, _ = sjson.Set(jsonStr, reqPath, []string{"_"}) |
| } |
| } |
|
|
| return jsonStr |
| } |
|
|
| |
|
|
| func findPaths(jsonStr, field string) []string { |
| var paths []string |
| Walk(gjson.Parse(jsonStr), "", field, &paths) |
| return paths |
| } |
|
|
| func sortByDepth(paths []string) { |
| sort.Slice(paths, func(i, j int) bool { return len(paths[i]) > len(paths[j]) }) |
| } |
|
|
| func trimSuffix(path, suffix string) string { |
| if path == strings.TrimPrefix(suffix, ".") { |
| return "" |
| } |
| return strings.TrimSuffix(path, suffix) |
| } |
|
|
| func joinPath(base, suffix string) string { |
| if base == "" { |
| return suffix |
| } |
| return base + "." + suffix |
| } |
|
|
| func setRawAt(jsonStr, path, value string) string { |
| if path == "" { |
| return value |
| } |
| result, _ := sjson.SetRaw(jsonStr, path, value) |
| return result |
| } |
|
|
| func isPropertyDefinition(path string) bool { |
| return path == "properties" || strings.HasSuffix(path, ".properties") |
| } |
|
|
| func descriptionPath(parentPath string) string { |
| if parentPath == "" || parentPath == "@this" { |
| return "description" |
| } |
| return parentPath + ".description" |
| } |
|
|
| func appendHint(jsonStr, parentPath, hint string) string { |
| descPath := parentPath + ".description" |
| if parentPath == "" || parentPath == "@this" { |
| descPath = "description" |
| } |
| existing := gjson.Get(jsonStr, descPath).String() |
| if existing != "" { |
| hint = fmt.Sprintf("%s (%s)", existing, hint) |
| } |
| jsonStr, _ = sjson.Set(jsonStr, descPath, hint) |
| return jsonStr |
| } |
|
|
| func appendHintRaw(jsonRaw, hint string) string { |
| existing := gjson.Get(jsonRaw, "description").String() |
| if existing != "" { |
| hint = fmt.Sprintf("%s (%s)", existing, hint) |
| } |
| jsonRaw, _ = sjson.Set(jsonRaw, "description", hint) |
| return jsonRaw |
| } |
|
|
| func getStrings(jsonStr, path string) []string { |
| var result []string |
| if arr := gjson.Get(jsonStr, path); arr.IsArray() { |
| for _, r := range arr.Array() { |
| result = append(result, r.String()) |
| } |
| } |
| return result |
| } |
|
|
| func contains(slice []string, item string) bool { |
| for _, s := range slice { |
| if s == item { |
| return true |
| } |
| } |
| return false |
| } |
|
|
| func orDefault(val, def string) string { |
| if val == "" { |
| return def |
| } |
| return val |
| } |
|
|
| func escapeGJSONPathKey(key string) string { |
| return gjsonPathKeyReplacer.Replace(key) |
| } |
|
|
| func unescapeGJSONPathKey(key string) string { |
| if !strings.Contains(key, "\\") { |
| return key |
| } |
| var b strings.Builder |
| b.Grow(len(key)) |
| for i := 0; i < len(key); i++ { |
| if key[i] == '\\' && i+1 < len(key) { |
| i++ |
| b.WriteByte(key[i]) |
| continue |
| } |
| b.WriteByte(key[i]) |
| } |
| return b.String() |
| } |
|
|
| func splitGJSONPath(path string) []string { |
| if path == "" { |
| return nil |
| } |
|
|
| parts := make([]string, 0, strings.Count(path, ".")+1) |
| var b strings.Builder |
| b.Grow(len(path)) |
|
|
| for i := 0; i < len(path); i++ { |
| c := path[i] |
| if c == '\\' && i+1 < len(path) { |
| b.WriteByte('\\') |
| i++ |
| b.WriteByte(path[i]) |
| continue |
| } |
| if c == '.' { |
| parts = append(parts, b.String()) |
| b.Reset() |
| continue |
| } |
| b.WriteByte(c) |
| } |
| parts = append(parts, b.String()) |
| return parts |
| } |
|
|
| func mergeDescriptionRaw(schemaRaw, parentDesc string) string { |
| childDesc := gjson.Get(schemaRaw, "description").String() |
| switch { |
| case childDesc == "": |
| schemaRaw, _ = sjson.Set(schemaRaw, "description", parentDesc) |
| return schemaRaw |
| case childDesc == parentDesc: |
| return schemaRaw |
| default: |
| combined := fmt.Sprintf("%s (%s)", parentDesc, childDesc) |
| schemaRaw, _ = sjson.Set(schemaRaw, "description", combined) |
| return schemaRaw |
| } |
| } |
|
|