github.com/choria-io/go-choria@v0.28.1-0.20240416190746-b3bf9c7d5a45/providers/agent/mcorpc/client/reply.go (about) 1 // Copyright (c) 2021-2022, R.I. Pienaar and the Choria Project contributors 2 // 3 // SPDX-License-Identifier: Apache-2.0 4 5 package client 6 7 import ( 8 "encoding/json" 9 "fmt" 10 "time" 11 12 "github.com/Masterminds/semver" 13 "github.com/choria-io/go-choria/providers/agent/mcorpc" 14 "github.com/expr-lang/expr" 15 "github.com/expr-lang/expr/vm" 16 "github.com/google/go-cmp/cmp" 17 "github.com/tidwall/gjson" 18 ) 19 20 // RPCReply is a basic RPC reply 21 type RPCReply struct { 22 Action string `json:"action"` 23 Statuscode mcorpc.StatusCode `json:"statuscode"` 24 Statusmsg string `json:"statusmsg"` 25 Data json.RawMessage `json:"data"` 26 Sender string `json:"sender"` 27 Time time.Time `json:"time_utc"` 28 } 29 30 // MatchExpr determines if the Reply matches expression q using the expr format. 31 // The query q is expected to return a boolean type else an error will be raised 32 func (r *RPCReply) MatchExpr(q string, prog *vm.Program) (bool, *vm.Program, error) { 33 env := map[string]any{ 34 "msg": r.Statusmsg, 35 "code": int(r.Statuscode), 36 "data": r.lookup, 37 "ok": r.isOK, 38 "aborted": r.isAborted, 39 "invalid_data": r.isInvalidData, 40 "missing_data": r.isMissingData, 41 "unknown_action": r.isUnknownAction, 42 "unknown_error": r.isUnknownError, 43 "include": r.include, 44 "semver": r.semverCompare, 45 "sender": func() string { return r.Sender }, 46 "time": func() time.Time { return r.Time }, 47 } 48 49 var err error 50 if prog == nil { 51 prog, err = expr.Compile(q, expr.AllowUndefinedVariables(), expr.Env(env)) 52 if err != nil { 53 return false, nil, err 54 } 55 } 56 57 out, err := expr.Run(prog, env) 58 if err != nil { 59 return false, prog, err 60 } 61 62 matched, ok := out.(bool) 63 if !ok { 64 return false, prog, fmt.Errorf("match expressions should return boolean") 65 } 66 67 return matched, prog, nil 68 } 69 70 func (r *RPCReply) isOK() bool { 71 return r.Statuscode == mcorpc.OK 72 } 73 74 func (r *RPCReply) isAborted() bool { 75 return r.Statuscode == mcorpc.Aborted 76 } 77 78 func (r *RPCReply) isUnknownAction() bool { 79 return r.Statuscode == mcorpc.UnknownAction 80 } 81 82 func (r *RPCReply) isMissingData() bool { 83 return r.Statuscode == mcorpc.MissingData 84 } 85 86 func (r *RPCReply) isInvalidData() bool { 87 return r.Statuscode == mcorpc.InvalidData 88 } 89 90 func (r *RPCReply) isUnknownError() bool { 91 return r.Statuscode == mcorpc.UnknownError 92 } 93 94 // https://github.com/tidwall/gjson/blob/master/SYNTAX.md 95 func (r *RPCReply) lookup(query string) any { 96 return gjson.GetBytes(r.Data, query).Value() 97 } 98 99 func (r *RPCReply) semverCompare(value string, cmp string) (bool, error) { 100 cons, err := semver.NewConstraint(cmp) 101 if err != nil { 102 return false, err 103 } 104 105 v, err := semver.NewVersion(value) 106 if err != nil { 107 return false, err 108 } 109 110 return cons.Check(v), nil 111 } 112 113 func (r *RPCReply) include(hay []any, needle any) bool { 114 // gjson always turns numbers into float64 115 i, ok := needle.(int) 116 if ok { 117 needle = float64(i) 118 } 119 120 for _, i := range hay { 121 if cmp.Equal(i, needle) { 122 return true 123 } 124 } 125 126 return false 127 }