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  }