feat(runtime): enhance payload rule resolution with dynamic path support
- Introduced `resolvePayloadRulePaths` function to dynamically resolve rule paths supporting array queries and complex logic. - Updated payload processing logic (`apply defaults`, `overrides`, `filters`) to handle resolved paths for better flexibility. - Added helper functions for path parsing, query matching, and logical resolution to improve modularity and reusability.
This commit is contained in:
@@ -2,6 +2,7 @@ package helps
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/router-for-me/CLIProxyAPI/v7/internal/config"
|
"github.com/router-for-me/CLIProxyAPI/v7/internal/config"
|
||||||
@@ -55,18 +56,20 @@ func ApplyPayloadConfigWithRoot(cfg *config.Config, model, protocol, root string
|
|||||||
if fullPath == "" {
|
if fullPath == "" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if gjson.GetBytes(source, fullPath).Exists() {
|
for _, resolvedPath := range resolvePayloadRulePaths(out, fullPath) {
|
||||||
continue
|
if gjson.GetBytes(source, resolvedPath).Exists() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, ok := appliedDefaults[resolvedPath]; ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
updated, errSet := sjson.SetBytes(out, resolvedPath, value)
|
||||||
|
if errSet != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
out = updated
|
||||||
|
appliedDefaults[resolvedPath] = struct{}{}
|
||||||
}
|
}
|
||||||
if _, ok := appliedDefaults[fullPath]; ok {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
updated, errSet := sjson.SetBytes(out, fullPath, value)
|
|
||||||
if errSet != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
out = updated
|
|
||||||
appliedDefaults[fullPath] = struct{}{}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Apply default raw rules: first write wins per field across all matching rules.
|
// Apply default raw rules: first write wins per field across all matching rules.
|
||||||
@@ -80,22 +83,24 @@ func ApplyPayloadConfigWithRoot(cfg *config.Config, model, protocol, root string
|
|||||||
if fullPath == "" {
|
if fullPath == "" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if gjson.GetBytes(source, fullPath).Exists() {
|
for _, resolvedPath := range resolvePayloadRulePaths(out, fullPath) {
|
||||||
continue
|
if gjson.GetBytes(source, resolvedPath).Exists() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, ok := appliedDefaults[resolvedPath]; ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
rawValue, ok := payloadRawValue(value)
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
updated, errSet := sjson.SetRawBytes(out, resolvedPath, rawValue)
|
||||||
|
if errSet != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
out = updated
|
||||||
|
appliedDefaults[resolvedPath] = struct{}{}
|
||||||
}
|
}
|
||||||
if _, ok := appliedDefaults[fullPath]; ok {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
rawValue, ok := payloadRawValue(value)
|
|
||||||
if !ok {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
updated, errSet := sjson.SetRawBytes(out, fullPath, rawValue)
|
|
||||||
if errSet != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
out = updated
|
|
||||||
appliedDefaults[fullPath] = struct{}{}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Apply override rules: last write wins per field across all matching rules.
|
// Apply override rules: last write wins per field across all matching rules.
|
||||||
@@ -109,11 +114,13 @@ func ApplyPayloadConfigWithRoot(cfg *config.Config, model, protocol, root string
|
|||||||
if fullPath == "" {
|
if fullPath == "" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
updated, errSet := sjson.SetBytes(out, fullPath, value)
|
for _, resolvedPath := range resolvePayloadRulePaths(out, fullPath) {
|
||||||
if errSet != nil {
|
updated, errSet := sjson.SetBytes(out, resolvedPath, value)
|
||||||
continue
|
if errSet != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
out = updated
|
||||||
}
|
}
|
||||||
out = updated
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Apply override raw rules: last write wins per field across all matching rules.
|
// Apply override raw rules: last write wins per field across all matching rules.
|
||||||
@@ -131,11 +138,13 @@ func ApplyPayloadConfigWithRoot(cfg *config.Config, model, protocol, root string
|
|||||||
if !ok {
|
if !ok {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
updated, errSet := sjson.SetRawBytes(out, fullPath, rawValue)
|
for _, resolvedPath := range resolvePayloadRulePaths(out, fullPath) {
|
||||||
if errSet != nil {
|
updated, errSet := sjson.SetRawBytes(out, resolvedPath, rawValue)
|
||||||
continue
|
if errSet != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
out = updated
|
||||||
}
|
}
|
||||||
out = updated
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Apply filter rules: remove matching paths from payload.
|
// Apply filter rules: remove matching paths from payload.
|
||||||
@@ -149,11 +158,15 @@ func ApplyPayloadConfigWithRoot(cfg *config.Config, model, protocol, root string
|
|||||||
if fullPath == "" {
|
if fullPath == "" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
updated, errDel := sjson.DeleteBytes(out, fullPath)
|
resolvedPaths := resolvePayloadRulePaths(out, fullPath)
|
||||||
if errDel != nil {
|
for i := len(resolvedPaths) - 1; i >= 0; i-- {
|
||||||
continue
|
resolvedPath := resolvedPaths[i]
|
||||||
|
updated, errDel := sjson.DeleteBytes(out, resolvedPath)
|
||||||
|
if errDel != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
out = updated
|
||||||
}
|
}
|
||||||
out = updated
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -254,6 +267,235 @@ func buildPayloadPath(root, path string) string {
|
|||||||
return r + "." + p
|
return r + "." + p
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func resolvePayloadRulePaths(payload []byte, path string) []string {
|
||||||
|
path = strings.TrimSpace(path)
|
||||||
|
if path == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if !strings.Contains(path, "#(") {
|
||||||
|
return []string{path}
|
||||||
|
}
|
||||||
|
parts := splitPayloadRulePath(path)
|
||||||
|
if len(parts) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
paths := []string{""}
|
||||||
|
for _, part := range parts {
|
||||||
|
query, allMatches, ok := parsePayloadQueryPathPart(part)
|
||||||
|
if !ok {
|
||||||
|
for i := range paths {
|
||||||
|
paths[i] = appendPayloadPathPart(paths[i], part)
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
nextPaths := make([]string, 0, len(paths))
|
||||||
|
for _, basePath := range paths {
|
||||||
|
array := payloadValueAtPath(payload, basePath)
|
||||||
|
if !array.Exists() || !array.IsArray() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for index, item := range array.Array() {
|
||||||
|
if !payloadQueryMatches(item, query) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
nextPaths = append(nextPaths, appendPayloadPathPart(basePath, strconv.Itoa(index)))
|
||||||
|
if !allMatches {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
paths = nextPaths
|
||||||
|
if len(paths) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return paths
|
||||||
|
}
|
||||||
|
|
||||||
|
func splitPayloadRulePath(path string) []string {
|
||||||
|
var parts []string
|
||||||
|
start := 0
|
||||||
|
depth := 0
|
||||||
|
var quote byte
|
||||||
|
escaped := false
|
||||||
|
for i := 0; i < len(path); i++ {
|
||||||
|
ch := path[i]
|
||||||
|
if escaped {
|
||||||
|
escaped = false
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if ch == '\\' {
|
||||||
|
escaped = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if quote != 0 {
|
||||||
|
if ch == quote {
|
||||||
|
quote = 0
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if ch == '"' || ch == '\'' {
|
||||||
|
quote = ch
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if ch == '(' {
|
||||||
|
depth++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if ch == ')' {
|
||||||
|
if depth > 0 {
|
||||||
|
depth--
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if ch == '.' && depth == 0 {
|
||||||
|
parts = append(parts, path[start:i])
|
||||||
|
start = i + 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
parts = append(parts, path[start:])
|
||||||
|
return parts
|
||||||
|
}
|
||||||
|
|
||||||
|
func parsePayloadQueryPathPart(part string) (string, bool, bool) {
|
||||||
|
if !strings.HasPrefix(part, "#(") {
|
||||||
|
return "", false, false
|
||||||
|
}
|
||||||
|
closeIndex := findPayloadQueryClose(part)
|
||||||
|
if closeIndex < 0 {
|
||||||
|
return "", false, false
|
||||||
|
}
|
||||||
|
suffix := part[closeIndex+1:]
|
||||||
|
if suffix != "" && suffix != "#" {
|
||||||
|
return "", false, false
|
||||||
|
}
|
||||||
|
return strings.TrimSpace(part[2:closeIndex]), suffix == "#", true
|
||||||
|
}
|
||||||
|
|
||||||
|
func findPayloadQueryClose(part string) int {
|
||||||
|
var quote byte
|
||||||
|
escaped := false
|
||||||
|
depth := 1
|
||||||
|
for i := 2; i < len(part); i++ {
|
||||||
|
ch := part[i]
|
||||||
|
if escaped {
|
||||||
|
escaped = false
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if ch == '\\' {
|
||||||
|
escaped = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if quote != 0 {
|
||||||
|
if ch == quote {
|
||||||
|
quote = 0
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if ch == '"' || ch == '\'' {
|
||||||
|
quote = ch
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if ch == '(' {
|
||||||
|
depth++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if ch == ')' {
|
||||||
|
depth--
|
||||||
|
if depth == 0 {
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
|
||||||
|
func appendPayloadPathPart(path, part string) string {
|
||||||
|
if path == "" {
|
||||||
|
return part
|
||||||
|
}
|
||||||
|
if part == "" {
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
return path + "." + part
|
||||||
|
}
|
||||||
|
|
||||||
|
func payloadValueAtPath(payload []byte, path string) gjson.Result {
|
||||||
|
if path == "" {
|
||||||
|
return gjson.ParseBytes(payload)
|
||||||
|
}
|
||||||
|
return gjson.GetBytes(payload, path)
|
||||||
|
}
|
||||||
|
|
||||||
|
func payloadQueryMatches(item gjson.Result, query string) bool {
|
||||||
|
for _, orPart := range splitPayloadLogical(query, "||") {
|
||||||
|
if payloadQueryAndMatches(item, orPart) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func payloadQueryAndMatches(item gjson.Result, query string) bool {
|
||||||
|
parts := splitPayloadLogical(query, "&&")
|
||||||
|
if len(parts) == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for _, part := range parts {
|
||||||
|
if !payloadQueryTermMatches(item, part) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func splitPayloadLogical(query, operator string) []string {
|
||||||
|
var parts []string
|
||||||
|
start := 0
|
||||||
|
var quote byte
|
||||||
|
escaped := false
|
||||||
|
for i := 0; i < len(query); i++ {
|
||||||
|
ch := query[i]
|
||||||
|
if escaped {
|
||||||
|
escaped = false
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if ch == '\\' {
|
||||||
|
escaped = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if quote != 0 {
|
||||||
|
if ch == quote {
|
||||||
|
quote = 0
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if ch == '"' || ch == '\'' {
|
||||||
|
quote = ch
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(query[i:], operator) {
|
||||||
|
parts = append(parts, strings.TrimSpace(query[start:i]))
|
||||||
|
i += len(operator) - 1
|
||||||
|
start = i + 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
parts = append(parts, strings.TrimSpace(query[start:]))
|
||||||
|
return parts
|
||||||
|
}
|
||||||
|
|
||||||
|
func payloadQueryTermMatches(item gjson.Result, term string) bool {
|
||||||
|
term = strings.TrimSpace(term)
|
||||||
|
if term == "" || item.Raw == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
wrapped := make([]byte, 0, len(item.Raw)+2)
|
||||||
|
wrapped = append(wrapped, '[')
|
||||||
|
wrapped = append(wrapped, item.Raw...)
|
||||||
|
wrapped = append(wrapped, ']')
|
||||||
|
return gjson.GetBytes(wrapped, "#("+term+")").Exists()
|
||||||
|
}
|
||||||
|
|
||||||
func removeToolTypeFromPayloadWithRoot(payload []byte, root string, toolType string) []byte {
|
func removeToolTypeFromPayloadWithRoot(payload []byte, root string, toolType string) []byte {
|
||||||
if len(payload) == 0 {
|
if len(payload) == 0 {
|
||||||
return payload
|
return payload
|
||||||
|
|||||||
Reference in New Issue
Block a user