614 lines
15 KiB
Go
614 lines
15 KiB
Go
package protocol
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"math"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
// Manifest expressions are JSON values with a deliberately small set of
|
|
// $-prefixed operators. They can construct provider payloads without running
|
|
// plugin code or exposing host objects.
|
|
func evaluateManifestValue(template any, env map[string]any) (any, error) {
|
|
switch value := template.(type) {
|
|
case nil, bool, float64, float32, int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64:
|
|
return value, nil
|
|
case string:
|
|
trimmed := strings.TrimSpace(value)
|
|
if strings.HasPrefix(trimmed, "${") && strings.HasSuffix(trimmed, "}") {
|
|
return manifestPathValue(env, strings.TrimSpace(trimmed[2:len(trimmed)-1])), nil
|
|
}
|
|
return value, nil
|
|
case []any:
|
|
result := make([]any, 0, len(value))
|
|
for _, item := range value {
|
|
evaluated, err := evaluateManifestValue(item, env)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if evaluated != nil {
|
|
result = append(result, evaluated)
|
|
}
|
|
}
|
|
return result, nil
|
|
case map[string]any:
|
|
if len(value) == 1 {
|
|
for operator, operand := range value {
|
|
if strings.HasPrefix(operator, "$") {
|
|
return evaluateManifestOperator(operator, operand, env)
|
|
}
|
|
}
|
|
}
|
|
result := make(map[string]any, len(value))
|
|
for key, item := range value {
|
|
evaluated, err := evaluateManifestValue(item, env)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("evaluate %s: %w", key, err)
|
|
}
|
|
if evaluated != nil {
|
|
result[key] = evaluated
|
|
}
|
|
}
|
|
return result, nil
|
|
default:
|
|
encoded, err := json.Marshal(value)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("unsupported manifest template value %T", template)
|
|
}
|
|
var normalized any
|
|
if err := json.Unmarshal(encoded, &normalized); err != nil {
|
|
return nil, err
|
|
}
|
|
return evaluateManifestValue(normalized, env)
|
|
}
|
|
}
|
|
|
|
func evaluateManifestOperator(operator string, operand any, env map[string]any) (any, error) {
|
|
switch operator {
|
|
case "$ref":
|
|
path, ok := operand.(string)
|
|
if !ok {
|
|
return nil, fmt.Errorf("$ref requires a string path")
|
|
}
|
|
return manifestPathValue(env, path), nil
|
|
case "$literal":
|
|
return operand, nil
|
|
case "$coalesce", "$default":
|
|
values, ok := operand.([]any)
|
|
if !ok {
|
|
return nil, fmt.Errorf("%s requires an array", operator)
|
|
}
|
|
for _, item := range values {
|
|
value, err := evaluateManifestValue(item, env)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if !manifestEmpty(value) {
|
|
return value, nil
|
|
}
|
|
}
|
|
return nil, nil
|
|
case "$concat":
|
|
values, ok := operand.([]any)
|
|
if !ok {
|
|
return nil, fmt.Errorf("$concat requires an array")
|
|
}
|
|
var result strings.Builder
|
|
for _, item := range values {
|
|
value, err := evaluateManifestValue(item, env)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if value != nil {
|
|
result.WriteString(manifestString(value))
|
|
}
|
|
}
|
|
return result.String(), nil
|
|
case "$concatArrays":
|
|
values, ok := operand.([]any)
|
|
if !ok {
|
|
return nil, fmt.Errorf("$concatArrays requires an array")
|
|
}
|
|
result := make([]any, 0)
|
|
for _, item := range values {
|
|
value, err := evaluateManifestValue(item, env)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
result = append(result, manifestArray(value)...)
|
|
}
|
|
return result, nil
|
|
case "$map", "$filter":
|
|
spec, ok := operand.(map[string]any)
|
|
if !ok {
|
|
return nil, fmt.Errorf("%s requires an object", operator)
|
|
}
|
|
from, err := evaluateManifestValue(spec["from"], env)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
alias := strings.TrimSpace(manifestString(spec["as"]))
|
|
if alias == "" {
|
|
alias = "item"
|
|
}
|
|
items := manifestArray(from)
|
|
result := make([]any, 0, len(items))
|
|
for index, item := range items {
|
|
child := cloneManifestEnv(env)
|
|
child[alias] = item
|
|
child[alias+"Index"] = index
|
|
if operator == "$filter" {
|
|
condition, err := evaluateManifestValue(spec["where"], child)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if manifestTruthy(condition) {
|
|
result = append(result, item)
|
|
}
|
|
continue
|
|
}
|
|
mapped, err := evaluateManifestValue(spec["in"], child)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if mapped != nil {
|
|
result = append(result, mapped)
|
|
}
|
|
}
|
|
return result, nil
|
|
case "$indexObject":
|
|
spec, ok := operand.(map[string]any)
|
|
if !ok {
|
|
return nil, fmt.Errorf("$indexObject requires an object")
|
|
}
|
|
from, err := evaluateManifestValue(spec["from"], env)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
alias := strings.TrimSpace(manifestString(spec["as"]))
|
|
if alias == "" {
|
|
alias = "item"
|
|
}
|
|
prefixValue, err := evaluateManifestValue(spec["prefix"], env)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
prefix := manifestString(prefixValue)
|
|
maxItems := len(manifestArray(from))
|
|
if spec["max"] != nil {
|
|
value, err := evaluateManifestValue(spec["max"], env)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if configured := manifestInt(value); configured >= 0 && configured < maxItems {
|
|
maxItems = configured
|
|
}
|
|
}
|
|
result := make(map[string]any, maxItems)
|
|
for index, item := range manifestArray(from)[:maxItems] {
|
|
child := cloneManifestEnv(env)
|
|
child[alias] = item
|
|
child[alias+"Index"] = index
|
|
value, err := evaluateManifestValue(spec["value"], child)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if value != nil && !manifestEmpty(value) {
|
|
result[prefix+strconv.Itoa(index)] = value
|
|
}
|
|
}
|
|
return result, nil
|
|
case "$first", "$last":
|
|
value, err := evaluateManifestValue(operand, env)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
items := manifestArray(value)
|
|
if len(items) == 0 {
|
|
return nil, nil
|
|
}
|
|
if operator == "$last" {
|
|
return items[len(items)-1], nil
|
|
}
|
|
return items[0], nil
|
|
case "$at":
|
|
values, ok := operand.([]any)
|
|
if !ok || len(values) != 2 {
|
|
return nil, fmt.Errorf("$at requires [array, index]")
|
|
}
|
|
value, err := evaluateManifestValue(values[0], env)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
indexValue, err := evaluateManifestValue(values[1], env)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
items := manifestArray(value)
|
|
index := manifestInt(indexValue)
|
|
if index < 0 || index >= len(items) {
|
|
return nil, nil
|
|
}
|
|
return items[index], nil
|
|
case "$len":
|
|
value, err := evaluateManifestValue(operand, env)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
switch typed := value.(type) {
|
|
case []any:
|
|
return len(typed), nil
|
|
case map[string]any:
|
|
return len(typed), nil
|
|
case string:
|
|
return len([]rune(typed)), nil
|
|
default:
|
|
return 0, nil
|
|
}
|
|
case "$if":
|
|
spec, ok := operand.(map[string]any)
|
|
if !ok {
|
|
return nil, fmt.Errorf("$if requires an object")
|
|
}
|
|
condition, err := evaluateManifestValue(spec["condition"], env)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if manifestTruthy(condition) {
|
|
return evaluateManifestValue(spec["then"], env)
|
|
}
|
|
return evaluateManifestValue(spec["else"], env)
|
|
case "$switch":
|
|
spec, ok := operand.(map[string]any)
|
|
if !ok {
|
|
return nil, fmt.Errorf("$switch requires an object")
|
|
}
|
|
for _, rawCase := range manifestArray(spec["cases"]) {
|
|
item, _ := rawCase.(map[string]any)
|
|
when, err := evaluateManifestValue(item["when"], env)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if manifestTruthy(when) {
|
|
return evaluateManifestValue(item["then"], env)
|
|
}
|
|
}
|
|
return evaluateManifestValue(spec["default"], env)
|
|
case "$omitEmpty":
|
|
value, err := evaluateManifestValue(operand, env)
|
|
if err != nil || manifestEmpty(value) {
|
|
return nil, err
|
|
}
|
|
return value, nil
|
|
case "$lower", "$upper", "$trim", "$toString", "$toInt", "$toBool", "$dataMime", "$dataPayload", "$json":
|
|
value, err := evaluateManifestValue(operand, env)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
text := manifestString(value)
|
|
switch operator {
|
|
case "$lower":
|
|
return strings.ToLower(text), nil
|
|
case "$upper":
|
|
return strings.ToUpper(text), nil
|
|
case "$trim":
|
|
return strings.TrimSpace(text), nil
|
|
case "$toString":
|
|
return text, nil
|
|
case "$toInt":
|
|
return manifestInt(value), nil
|
|
case "$toBool":
|
|
return manifestTruthy(value), nil
|
|
case "$dataMime":
|
|
return dataMIME(text), nil
|
|
case "$dataPayload":
|
|
return dataPayload(text), nil
|
|
default:
|
|
encoded, err := json.Marshal(value)
|
|
return string(encoded), err
|
|
}
|
|
case "$merge":
|
|
values, ok := operand.([]any)
|
|
if !ok {
|
|
return nil, fmt.Errorf("$merge requires an array")
|
|
}
|
|
result := make(map[string]any)
|
|
for _, item := range values {
|
|
value, err := evaluateManifestValue(item, env)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for key, entry := range manifestObject(value) {
|
|
result[key] = entry
|
|
}
|
|
}
|
|
return result, nil
|
|
case "$sortByOrder":
|
|
value, err := evaluateManifestValue(operand, env)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
items := append([]any(nil), manifestArray(value)...)
|
|
sort.SliceStable(items, func(i, j int) bool {
|
|
return manifestInt(manifestPathValue(items[i], "order")) < manifestInt(manifestPathValue(items[j], "order"))
|
|
})
|
|
return items, nil
|
|
case "$add", "$multiply", "$min", "$max":
|
|
values, ok := operand.([]any)
|
|
if !ok || len(values) == 0 {
|
|
return nil, fmt.Errorf("%s requires an array", operator)
|
|
}
|
|
result := 0.0
|
|
if operator == "$multiply" {
|
|
result = 1
|
|
} else if operator == "$min" {
|
|
result = math.Inf(1)
|
|
} else if operator == "$max" {
|
|
result = math.Inf(-1)
|
|
}
|
|
for _, item := range values {
|
|
value, err := evaluateManifestValue(item, env)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
number := manifestFloat(value)
|
|
switch operator {
|
|
case "$multiply":
|
|
result *= manifestFloat(value)
|
|
case "$min":
|
|
result = math.Min(result, number)
|
|
case "$max":
|
|
result = math.Max(result, number)
|
|
default:
|
|
result += manifestFloat(value)
|
|
}
|
|
}
|
|
return normalizeManifestNumber(result), nil
|
|
case "$ceilStep":
|
|
values, ok := operand.([]any)
|
|
if !ok || len(values) != 2 {
|
|
return nil, fmt.Errorf("$ceilStep requires [value, step]")
|
|
}
|
|
value, err := evaluateManifestValue(values[0], env)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
step, err := evaluateManifestValue(values[1], env)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
stepValue := manifestFloat(step)
|
|
if stepValue <= 0 {
|
|
return nil, fmt.Errorf("$ceilStep step must be positive")
|
|
}
|
|
return normalizeManifestNumber(math.Ceil(manifestFloat(value)/stepValue) * stepValue), nil
|
|
case "$eq", "$ne", "$gt", "$gte", "$lt", "$lte", "$in", "$and", "$or":
|
|
return evaluateManifestComparison(operator, operand, env)
|
|
case "$not":
|
|
value, err := evaluateManifestValue(operand, env)
|
|
return !manifestTruthy(value), err
|
|
default:
|
|
return nil, fmt.Errorf("unsupported manifest operator %q", operator)
|
|
}
|
|
}
|
|
|
|
func evaluateManifestComparison(operator string, operand any, env map[string]any) (any, error) {
|
|
values, ok := operand.([]any)
|
|
if !ok {
|
|
return nil, fmt.Errorf("%s requires an array", operator)
|
|
}
|
|
evaluated := make([]any, 0, len(values))
|
|
for _, item := range values {
|
|
value, err := evaluateManifestValue(item, env)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
evaluated = append(evaluated, value)
|
|
}
|
|
if operator == "$and" || operator == "$or" {
|
|
result := operator == "$and"
|
|
for _, value := range evaluated {
|
|
if operator == "$and" {
|
|
result = result && manifestTruthy(value)
|
|
} else {
|
|
result = result || manifestTruthy(value)
|
|
}
|
|
}
|
|
return result, nil
|
|
}
|
|
if len(evaluated) != 2 {
|
|
return nil, fmt.Errorf("%s requires two operands", operator)
|
|
}
|
|
left, right := evaluated[0], evaluated[1]
|
|
switch operator {
|
|
case "$eq", "$ne":
|
|
leftJSON, _ := json.Marshal(left)
|
|
rightJSON, _ := json.Marshal(right)
|
|
equal := string(leftJSON) == string(rightJSON)
|
|
if operator == "$ne" {
|
|
return !equal, nil
|
|
}
|
|
return equal, nil
|
|
case "$gt":
|
|
return manifestFloat(left) > manifestFloat(right), nil
|
|
case "$gte":
|
|
return manifestFloat(left) >= manifestFloat(right), nil
|
|
case "$lt":
|
|
return manifestFloat(left) < manifestFloat(right), nil
|
|
case "$lte":
|
|
return manifestFloat(left) <= manifestFloat(right), nil
|
|
case "$in":
|
|
for _, item := range manifestArray(right) {
|
|
leftJSON, _ := json.Marshal(left)
|
|
itemJSON, _ := json.Marshal(item)
|
|
if string(leftJSON) == string(itemJSON) {
|
|
return true, nil
|
|
}
|
|
}
|
|
return false, nil
|
|
}
|
|
return false, nil
|
|
}
|
|
|
|
func interpolateManifestString(value string, env map[string]any) string {
|
|
result := value
|
|
for {
|
|
start := strings.Index(result, "{{")
|
|
if start < 0 {
|
|
return result
|
|
}
|
|
endOffset := strings.Index(result[start+2:], "}}")
|
|
if endOffset < 0 {
|
|
return result
|
|
}
|
|
end := start + 2 + endOffset
|
|
path := strings.TrimSpace(result[start+2 : end])
|
|
result = result[:start] + manifestString(manifestPathValue(env, path)) + result[end+2:]
|
|
}
|
|
}
|
|
|
|
func manifestPathValue(root any, path string) any {
|
|
if strings.TrimSpace(path) == "" {
|
|
return root
|
|
}
|
|
value := root
|
|
for _, part := range strings.Split(strings.Trim(path, "."), ".") {
|
|
switch current := value.(type) {
|
|
case map[string]any:
|
|
value = current[part]
|
|
case []any:
|
|
index, err := strconv.Atoi(part)
|
|
if err != nil || index < 0 || index >= len(current) {
|
|
return nil
|
|
}
|
|
value = current[index]
|
|
default:
|
|
return nil
|
|
}
|
|
}
|
|
return value
|
|
}
|
|
|
|
func cloneManifestEnv(env map[string]any) map[string]any {
|
|
result := make(map[string]any, len(env)+2)
|
|
for key, value := range env {
|
|
result[key] = value
|
|
}
|
|
return result
|
|
}
|
|
|
|
func manifestArray(value any) []any {
|
|
if value == nil {
|
|
return nil
|
|
}
|
|
if items, ok := value.([]any); ok {
|
|
return items
|
|
}
|
|
return []any{value}
|
|
}
|
|
|
|
func manifestObject(value any) map[string]any {
|
|
result, _ := value.(map[string]any)
|
|
return result
|
|
}
|
|
|
|
func manifestEmpty(value any) bool {
|
|
switch typed := value.(type) {
|
|
case nil:
|
|
return true
|
|
case string:
|
|
return strings.TrimSpace(typed) == ""
|
|
case []any:
|
|
return len(typed) == 0
|
|
case map[string]any:
|
|
return len(typed) == 0
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
func manifestTruthy(value any) bool {
|
|
switch typed := value.(type) {
|
|
case nil:
|
|
return false
|
|
case bool:
|
|
return typed
|
|
case string:
|
|
normalized := strings.ToLower(strings.TrimSpace(typed))
|
|
return normalized != "" && normalized != "false" && normalized != "0" && normalized != "no" && normalized != "null"
|
|
case float64:
|
|
return typed != 0
|
|
case float32:
|
|
return typed != 0
|
|
case int:
|
|
return typed != 0
|
|
case int64:
|
|
return typed != 0
|
|
case []any:
|
|
return len(typed) > 0
|
|
case map[string]any:
|
|
return len(typed) > 0
|
|
default:
|
|
return true
|
|
}
|
|
}
|
|
|
|
func manifestString(value any) string {
|
|
switch typed := value.(type) {
|
|
case nil:
|
|
return ""
|
|
case string:
|
|
return typed
|
|
case json.Number:
|
|
return typed.String()
|
|
case float64:
|
|
return strconv.FormatFloat(typed, 'f', -1, 64)
|
|
case float32:
|
|
return strconv.FormatFloat(float64(typed), 'f', -1, 32)
|
|
case int:
|
|
return strconv.Itoa(typed)
|
|
case int64:
|
|
return strconv.FormatInt(typed, 10)
|
|
case bool:
|
|
return strconv.FormatBool(typed)
|
|
default:
|
|
encoded, _ := json.Marshal(value)
|
|
return string(encoded)
|
|
}
|
|
}
|
|
|
|
func manifestFloat(value any) float64 {
|
|
switch typed := value.(type) {
|
|
case float64:
|
|
return typed
|
|
case float32:
|
|
return float64(typed)
|
|
case int:
|
|
return float64(typed)
|
|
case int64:
|
|
return float64(typed)
|
|
case json.Number:
|
|
result, _ := typed.Float64()
|
|
return result
|
|
case string:
|
|
result, _ := strconv.ParseFloat(strings.TrimSpace(typed), 64)
|
|
return result
|
|
default:
|
|
return 0
|
|
}
|
|
}
|
|
|
|
func manifestInt(value any) int {
|
|
return int(manifestFloat(value))
|
|
}
|
|
|
|
func normalizeManifestNumber(value float64) any {
|
|
if math.Trunc(value) == value {
|
|
return int(value)
|
|
}
|
|
return value
|
|
}
|