github.com/choria-io/go-choria@v0.28.1-0.20240416190746-b3bf9c7d5a45/protocol/v1/reply.go (about)

     1  // Copyright (c) 2017-2022, R.I. Pienaar and the Choria Project contributors
     2  //
     3  // SPDX-License-Identifier: Apache-2.0
     4  
     5  package v1
     6  
     7  import (
     8  	"encoding/json"
     9  	"fmt"
    10  	"strings"
    11  	"sync"
    12  	"time"
    13  
    14  	"github.com/choria-io/go-choria/protocol"
    15  )
    16  
    17  type Reply struct {
    18  	Protocol    protocol.ProtocolVersion `json:"protocol"`
    19  	MessageBody string                   `json:"message"`
    20  	Envelope    *ReplyEnvelope           `json:"envelope"`
    21  
    22  	mu sync.Mutex
    23  }
    24  
    25  type ReplyEnvelope struct {
    26  	RequestID string `json:"requestid"`
    27  	SenderID  string `json:"senderid"`
    28  	Agent     string `json:"agent"`
    29  	Time      int64  `json:"time"`
    30  
    31  	seenBy     [][3]string
    32  	federation *FederationTransportHeader
    33  }
    34  
    35  // RecordNetworkHop appends a hop onto the list of those who processed this message
    36  func (r *Reply) RecordNetworkHop(in string, processor string, out string) {
    37  	r.mu.Lock()
    38  	defer r.mu.Unlock()
    39  
    40  	r.Envelope.seenBy = append(r.Envelope.seenBy, [3]string{in, processor, out})
    41  }
    42  
    43  // NetworkHops returns a list of tuples this messaged traveled through
    44  func (r *Reply) NetworkHops() [][3]string {
    45  	r.mu.Lock()
    46  	defer r.mu.Unlock()
    47  
    48  	return r.Envelope.seenBy
    49  }
    50  
    51  // SetMessage sets the data to be stored in the Reply.  It should be JSON encoded already.
    52  func (r *Reply) SetMessage(message []byte) {
    53  	r.mu.Lock()
    54  	defer r.mu.Unlock()
    55  
    56  	r.MessageBody = string(message)
    57  }
    58  
    59  // Message retrieves the JSON encoded message set using SetMessage
    60  func (r *Reply) Message() (msg []byte) {
    61  	r.mu.Lock()
    62  	defer r.mu.Unlock()
    63  
    64  	return []byte(r.MessageBody)
    65  }
    66  
    67  // RequestID retrieves the unique request id
    68  func (r *Reply) RequestID() string {
    69  	r.mu.Lock()
    70  	defer r.mu.Unlock()
    71  
    72  	return r.Envelope.RequestID
    73  }
    74  
    75  // SenderID retrieves the identity of the sending node
    76  func (r *Reply) SenderID() string {
    77  	r.mu.Lock()
    78  	defer r.mu.Unlock()
    79  
    80  	return r.Envelope.SenderID
    81  }
    82  
    83  // Agent retrieves the agent name that sent this reply
    84  func (r *Reply) Agent() string {
    85  	r.mu.Lock()
    86  	defer r.mu.Unlock()
    87  
    88  	return r.Envelope.Agent
    89  }
    90  
    91  // Time retrieves the time stamp that this message was made
    92  func (r *Reply) Time() time.Time {
    93  	r.mu.Lock()
    94  	defer r.mu.Unlock()
    95  
    96  	return time.Unix(r.Envelope.Time, 0)
    97  }
    98  
    99  // JSON creates a JSON encoded reply
   100  func (r *Reply) JSON() ([]byte, error) {
   101  	r.mu.Lock()
   102  	j, err := json.Marshal(r)
   103  	r.mu.Unlock()
   104  	if err != nil {
   105  		protocolErrorCtr.Inc()
   106  		return nil, fmt.Errorf("could not JSON Marshal: %s", err)
   107  	}
   108  
   109  	err = r.IsValidJSON(j)
   110  	if err != nil {
   111  		return nil, fmt.Errorf("serialized JSON produced from the Reply does not pass validation: %s", err)
   112  	}
   113  
   114  	return j, nil
   115  }
   116  
   117  // Version retrieves the protocol version for this message
   118  func (r *Reply) Version() protocol.ProtocolVersion {
   119  	r.mu.Lock()
   120  	defer r.mu.Unlock()
   121  
   122  	return r.Protocol
   123  }
   124  
   125  // IsValidJSON validates the given JSON data against the schema
   126  func (r *Reply) IsValidJSON(data []byte) (err error) {
   127  	if !protocol.ClientStrictValidation {
   128  		return nil
   129  	}
   130  
   131  	_, errors, err := schemaValidate(protocol.ReplyV1, data)
   132  	if err != nil {
   133  		return fmt.Errorf("could not validate Reply JSON data: %s", err)
   134  	}
   135  
   136  	if len(errors) != 0 {
   137  		return fmt.Errorf("supplied JSON document is not a valid Reply message: %s", strings.Join(errors, ", "))
   138  	}
   139  
   140  	return nil
   141  }
   142  
   143  // FederationTargets retrieves the list of targets this message is destined for
   144  func (r *Reply) FederationTargets() (targets []string, federated bool) {
   145  	r.mu.Lock()
   146  	defer r.mu.Unlock()
   147  
   148  	if r.Envelope.federation == nil {
   149  		return nil, false
   150  	}
   151  
   152  	return r.Envelope.federation.Targets, true
   153  }
   154  
   155  // FederationReplyTo retrieves the reply to string set by the federation broker
   156  func (r *Reply) FederationReplyTo() (replyto string, federated bool) {
   157  	r.mu.Lock()
   158  	defer r.mu.Unlock()
   159  
   160  	if r.Envelope.federation == nil {
   161  		federated = false
   162  		return
   163  	}
   164  
   165  	federated = true
   166  	replyto = r.Envelope.federation.ReplyTo
   167  
   168  	return
   169  }
   170  
   171  // FederationRequestID retrieves the federation specific requestid
   172  func (r *Reply) FederationRequestID() (id string, federated bool) {
   173  	r.mu.Lock()
   174  	defer r.mu.Unlock()
   175  
   176  	if r.Envelope.federation == nil {
   177  		federated = false
   178  		return
   179  	}
   180  
   181  	federated = true
   182  	id = r.Envelope.federation.RequestID
   183  
   184  	return
   185  }
   186  
   187  // SetFederationTargets sets the list of hosts this message should go to.
   188  //
   189  // Federation brokers will duplicate the message and send one for each target
   190  func (r *Reply) SetFederationTargets(targets []string) {
   191  	r.mu.Lock()
   192  	defer r.mu.Unlock()
   193  
   194  	if r.Envelope.federation == nil {
   195  		r.Envelope.federation = &FederationTransportHeader{}
   196  	}
   197  
   198  	r.Envelope.federation.Targets = targets
   199  }
   200  
   201  // SetFederationReplyTo stores the original reply-to destination in the federation headers
   202  func (r *Reply) SetFederationReplyTo(reply string) {
   203  	r.mu.Lock()
   204  	defer r.mu.Unlock()
   205  
   206  	if r.Envelope.federation == nil {
   207  		r.Envelope.federation = &FederationTransportHeader{}
   208  	}
   209  
   210  	r.Envelope.federation.ReplyTo = reply
   211  }
   212  
   213  // SetFederationRequestID sets the request ID for federation purposes
   214  func (r *Reply) SetFederationRequestID(id string) {
   215  	r.mu.Lock()
   216  	defer r.mu.Unlock()
   217  
   218  	if r.Envelope.federation == nil {
   219  		r.Envelope.federation = &FederationTransportHeader{}
   220  	}
   221  
   222  	r.Envelope.federation.RequestID = id
   223  }
   224  
   225  // IsFederated determines if this message is federated
   226  func (r *Reply) IsFederated() bool {
   227  	r.mu.Lock()
   228  	defer r.mu.Unlock()
   229  
   230  	return r.Envelope.federation != nil
   231  }
   232  
   233  // SetUnfederated removes any federation information from the message
   234  func (r *Reply) SetUnfederated() {
   235  	r.mu.Lock()
   236  	defer r.mu.Unlock()
   237  
   238  	r.Envelope.federation = nil
   239  }