From 5cdd8c1b395f76de1e59c3782220e2b63599e397 Mon Sep 17 00:00:00 2001 From: schmidtw Date: Tue, 14 Jan 2025 20:37:12 -0800 Subject: [PATCH] feat:Add an environ variable form for WRPs. --- env.go | 225 +++++++++++++++++++++++++ env_test.go | 209 ++++++++++++++++++++++++ messages.go | 244 +++++++++++++++++++++------- messages_test.go | 271 +++++++++++++++++++++++++++++++ wrphttp/headers.go | 3 +- wrpvalidator/messageValidator.go | 2 +- 6 files changed, 895 insertions(+), 59 deletions(-) create mode 100644 env.go create mode 100644 env_test.go diff --git a/env.go b/env.go new file mode 100644 index 0000000..c1ae133 --- /dev/null +++ b/env.go @@ -0,0 +1,225 @@ +// SPDX-FileCopyrightText: 2025 Comcast Cable Communications Management, LLC +// SPDX-License-Identifier: Apache-2.0 + +package wrp + +import ( + "encoding/base64" + "fmt" + "reflect" + "regexp" + "strconv" + "strings" +) + +func toEnvMap(msg any) map[string]string { + v := reflect.ValueOf(msg) + if v.Kind() != reflect.Ptr || v.Elem().Kind() != reflect.Struct { + return nil + } + + v = v.Elem() + t := v.Type() + envVars := make(map[string]string) + + for i := 0; i < t.NumField(); i++ { + field := t.Field(i) + value := v.Field(i) + tag := field.Tag.Get("env") + if tag == "" || tag == "-" { + continue + } + + tagParts := strings.Split(tag, ",") + key := tagParts[0] + options := tagParts[1:] + + if key == "" { + continue + } + + if isEmptyValue(value) { + if contains(options, "omitempty") { + continue + } + envVars[key] = "" + } + + switch value.Kind() { + case reflect.String: + envVars[key] = value.String() + case reflect.Ptr: + if value.Elem().Kind() == reflect.Int64 { + if !value.IsNil() { + envVars[key] = fmt.Sprintf("%d", value.Elem().Int()) + } + } + case reflect.Slice: + if value.Type().Elem().Kind() == reflect.String { + if contains(options, "multiline") { + for i, s := range value.Interface().([]string) { + envVars[fmt.Sprintf("%s_%03d", key, i)] = s + } + } else { + envVars[key] = strings.Join(value.Interface().([]string), ",") + } + } else if value.Type().Elem().Kind() == reflect.Uint8 { + envVars[key] = base64.StdEncoding.EncodeToString(value.Interface().([]byte)) + } + case reflect.Map: + if value.Type().Key().Kind() == reflect.String && value.Type().Elem().Kind() == reflect.String { + for k, v := range value.Interface().(map[string]string) { + safe := sanitizeEnvVarName(k) + envVars[fmt.Sprintf("%s_%s", key, safe)] = k + "=" + v + } + } + case reflect.Int, reflect.Int64: + envVars[key] = fmt.Sprintf("%d", value.Int()) + } + } + + return envVars +} + +func fromEnvMap(envVars []string, msg any) error { + v := reflect.ValueOf(msg) + if v.Kind() != reflect.Ptr || v.Elem().Kind() != reflect.Struct { + return fmt.Errorf("msg must be a pointer to a struct") + } + + v = v.Elem() + t := v.Type() + envMap := make(map[string]string) + for _, envVar := range envVars { + parts := strings.SplitN(envVar, "=", 2) + if len(parts) == 2 { + envMap[parts[0]] = parts[1] + } + } + + for i := 0; i < t.NumField(); i++ { + field := t.Field(i) + value := v.Field(i) + tag := field.Tag.Get("env") + if tag == "" || tag == "-" { + continue + } + + tagParts := strings.Split(tag, ",") + key := tagParts[0] + options := tagParts[1:] + + if key == "" { + key = field.Name + } + + switch value.Kind() { + case reflect.String: + value.SetString(envMap[key]) + case reflect.Ptr: + if value.Type().Elem().Kind() == reflect.Int64 { + if v, exists := envMap[key]; !exists || v == "" { + continue + } + intVal, err := strconv.ParseInt(envMap[key], 10, 64) + if err != nil { + return err + } + value.Set(reflect.ValueOf(&intVal)) + } + case reflect.Slice: + if value.Type().Elem().Kind() == reflect.String { + if contains(options, "multiline") { + var slice []string + for i := 0; ; i++ { + multiKey := fmt.Sprintf("%s_%03d", key, i) + if multiValue, ok := envMap[multiKey]; ok { + slice = append(slice, multiValue) + } else { + break + } + } + if len(slice) > 0 { + value.Set(reflect.ValueOf(slice)) + } + } else { + if _, exists := envMap[key]; !exists { + continue + } + list := strings.Split(envMap[key], ",") + if len(list) > 0 && list[0] != "" { + value.Set(reflect.ValueOf(list)) + } + } + } else if value.Type().Elem().Kind() == reflect.Uint8 { + if _, exists := envMap[key]; !exists { + continue + } + decoded, err := base64.StdEncoding.DecodeString(envMap[key]) + if err != nil { + return err + } + if len(decoded) > 0 { + value.Set(reflect.ValueOf(decoded)) + } + } + case reflect.Map: + if value.Type().Key().Kind() == reflect.String && value.Type().Elem().Kind() == reflect.String { + mapValue := make(map[string]string) + for k, v := range envMap { + if strings.HasPrefix(k, key) { + list := strings.SplitN(v, "=", 2) + for i := range list { + list[i] = strings.TrimSpace(list[i]) + } + // If there is no value, append an empty string + list = append(list, "") + mapValue[list[0]] = list[1] + } + } + if len(mapValue) > 0 { + value.Set(reflect.ValueOf(mapValue)) + } + } + case reflect.Int, reflect.Int64: + if v, exists := envMap[key]; !exists || v == "" { + continue + } + intVal, err := strconv.ParseInt(envMap[key], 10, 64) + if err != nil { + return err + } + value.SetInt(intVal) + } + } + + return nil +} + +func contains(slice []string, item string) bool { + for _, s := range slice { + if s == item { + return true + } + } + return false +} + +func isEmptyValue(v reflect.Value) bool { + switch v.Kind() { + case reflect.String, reflect.Slice, reflect.Map: + return v.Len() == 0 + case reflect.Ptr, reflect.Interface: + return v.IsNil() + } + return false +} + +// Allowed environment variable characters. +var envVarNameRegexp = regexp.MustCompile(`[^a-zA-Z0-9_]`) + +func sanitizeEnvVarName(name string) string { + name = envVarNameRegexp.ReplaceAllString(name, "_") + // Chomp off leading underscores. + return strings.TrimLeft(name, "_") +} diff --git a/env_test.go b/env_test.go new file mode 100644 index 0000000..385b8a6 --- /dev/null +++ b/env_test.go @@ -0,0 +1,209 @@ +// SPDX-FileCopyrightText: 2025 Comcast Cable Communications Management, LLC +// SPDX-License-Identifier: Apache-2.0 + +package wrp + +import ( + "reflect" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestEnvMap(t *testing.T) { + tests := []struct { + name string + input any + output map[string]string + onlyTo bool + onlyFrom bool + }{ + { + name: "Basic struct", + input: &struct { + Name string `env:"NAME"` + Age *int64 `env:"AGE"` + }{ + Name: "John", + Age: int64Ptr(30), + }, + output: map[string]string{ + "NAME": "John", + "AGE": "30", + }, + }, { + name: "Handle ignored things", + input: &struct { + Name string `env:"NAME"` + Age *int64 `env:"AGE"` + Ignored string + Ignored2 string `env:"-"` + Ignored3 string `env:""` + Ignored4 string `env:",omitempty"` + }{ + Name: "John", + Age: int64Ptr(30), + Ignored: "ignored", + Ignored2: "ignored", + Ignored3: "ignored", + Ignored4: "ignored", + }, + output: map[string]string{ + "NAME": "John", + "AGE": "30", + }, + onlyTo: true, + }, { + name: "Omit empty fields", + input: &struct { + Name string `env:"NAME,omitempty"` + Age *int64 `env:"AGE,omitempty"` + }{ + Name: "", + Age: nil, + }, + output: map[string]string{}, + }, + { + name: "Include empty fields", + input: &struct { + Name string `env:"NAME"` + Age *int64 `env:"AGE"` + }{ + Name: "", + Age: nil, + }, + output: map[string]string{ + "NAME": "", + "AGE": "", + }, + }, { + name: "Multiline slice", + input: &struct { + Lines []string `env:"LINE,multiline"` + }{ + Lines: []string{"line1", "line2", "line3"}, + }, + output: map[string]string{ + "LINE_000": "line1", + "LINE_001": "line2", + "LINE_002": "line3", + }, + }, { + name: "Base64 encode []byte", + input: &struct { + Data []byte `env:"DATA"` + }{ + Data: []byte("hello"), + }, + output: map[string]string{ + "DATA": "aGVsbG8=", + }, + }, { + name: "Map of strings", + input: &struct { + Labels map[string]string `env:"LABEL"` + }{ + Labels: map[string]string{"key1": "value1", "key2": "value2"}, + }, + output: map[string]string{ + "LABEL_key1": "key1=value1", + "LABEL_key2": "key2=value2", + }, + }, { + name: "array of of strings", + input: &struct { + Labels []string `env:"LABEL"` + }{ + Labels: []string{"hello world", "goodbye world"}, + }, + output: map[string]string{ + "LABEL": "hello world,goodbye world", + }, + }, { + name: "Not a struct", + input: "not a struct", + onlyTo: true, + }, + { + name: "wrp.Message", + input: &Message{ + Type: SimpleEventMessageType, + Source: "mac:112233445566", + Destination: "event:device-status/foo", + TransactionUUID: "1234", + ContentType: "application/json", + Accept: "application/json", + Status: int64Ptr(200), + RequestDeliveryResponse: int64Ptr(1), + Headers: []string{ + "key1:value1", + "key2:value2", + }, + Metadata: map[string]string{ + "/key/1": "value1", + "/key/2": "value2", + }, + Path: "/api/v1/device-status/foo", + Payload: []byte("hello world"), + ServiceName: "device-status", + URL: "http://localhost:8080/api/v1/device-status/foo", + PartnerIDs: []string{"partner1", "partner2"}, + SessionID: "1234", + QualityOfService: 12, + }, + output: map[string]string{ + "WRP_MSG_TYPE": "4", + "WRP_SOURCE": "mac:112233445566", + "WRP_DEST": "event:device-status/foo", + "WRP_TRANSACTION_UUID": "1234", + "WRP_CONTENT_TYPE": "application/json", + "WRP_ACCEPT": "application/json", + "WRP_STATUS": "200", + "WRP_RDR": "1", + "WRP_HEADERS_000": "key1:value1", + "WRP_HEADERS_001": "key2:value2", + "WRP_METADATA_key_1": "/key/1=value1", + "WRP_METADATA_key_2": "/key/2=value2", + "WRP_PATH": "/api/v1/device-status/foo", + "WRP_PAYLOAD": "aGVsbG8gd29ybGQ=", + "WRP_SERVICE_NAME": "device-status", + "WRP_URL": "http://localhost:8080/api/v1/device-status/foo", + "WRP_PARTNER_IDS": "partner1,partner2", + "WRP_SESSION_ID": "1234", + "WRP_QOS": "12", + }, + }, + } + + for _, tt := range tests { + if !tt.onlyFrom { + t.Run("toEnvMap: "+tt.name, func(t *testing.T) { + got := toEnvMap(tt.input) + assert.Equal(t, tt.output, got) + }) + } + + if !tt.onlyTo { + t.Run("fromEnvMap: "+tt.name, func(t *testing.T) { + newInstance := reflect.New(reflect.TypeOf(tt.input).Elem()).Interface() + + var list []string + for k, v := range tt.output { + list = append(list, k+"="+v) + } + // Populate the new instance from the environment variables + err := fromEnvMap(list, newInstance) + assert.NoError(t, err) + + // Ensure the new instance matches the original input + assert.Equal(t, tt.input, newInstance) + + }) + } + } +} + +func int64Ptr(i int64) *int64 { + return &i +} diff --git a/messages.go b/messages.go index c57a8fa..14569f5 100644 --- a/messages.go +++ b/messages.go @@ -82,48 +82,50 @@ type Message struct { // Type is the message type for the message. // // example: SimpleRequestResponseMessageType - Type MessageType `json:"msg_type"` + Type MessageType `json:"msg_type" env:"WRP_MSG_TYPE"` // Source is the device_id name of the device originating the request or response. // // example: dns:talaria.xmidt.example.com - Source string `json:"source,omitempty"` + Source string `json:"source,omitempty" env:"WRP_SOURCE,omitempty"` // Destination is the device_id name of the target device of the request or response. // // example: event:device-status/mac:ffffffffdae4/online - Destination string `json:"dest,omitempty"` + Destination string `json:"dest,omitempty" env:"WRP_DEST,omitempty"` // TransactionUUID The transaction key for the message // // example: 546514d4-9cb6-41c9-88ca-ccd4c130c525 - TransactionUUID string `json:"transaction_uuid,omitempty"` + TransactionUUID string `json:"transaction_uuid,omitempty" env:"WRP_TRANSACTION_UUID,omitempty"` // ContentType The media type of the payload. // // example: json - ContentType string `json:"content_type,omitempty"` + ContentType string `json:"content_type,omitempty" env:"WRP_CONTENT_TYPE,omitempty"` // Accept is the media type accepted in the response. - Accept string `json:"accept,omitempty"` + Accept string `json:"accept,omitempty" env:"WRP_ACCEPT,omitempty"` // Status is the response status from the originating service. - Status *int64 `json:"status,omitempty"` + Status *int64 `json:"status,omitempty" env:"WRP_STATUS,omitempty"` // RequestDeliveryResponse is the request delivery response is the delivery result // of the previous (implied request) message with a matching transaction_uuid - RequestDeliveryResponse *int64 `json:"rdr,omitempty"` + RequestDeliveryResponse *int64 `json:"rdr,omitempty" env:"WRP_RDR,omitempty"` // Headers is the headers associated with the payload. - Headers []string `json:"headers,omitempty"` + Headers []string `json:"headers,omitempty" env:"WRP_HEADERS,omitempty,multiline"` // Metadata is the map of name/value pairs used by consumers of WRP messages for filtering & other purposes. // // example: {"/boot-time":"1542834188","/last-reconnect-reason":"spanish inquisition"} - Metadata map[string]string `json:"metadata,omitempty"` + Metadata map[string]string `json:"metadata,omitempty" env:"WRP_METADATA,omitempty"` // Spans is an array of arrays of timing values as a list in the format: "parent" (string), "name" (string), // "start time" (int), "duration" (int), "status" (int) + // + // Deprecated: A future version of wrp will remove this field. Spans [][]string `json:"spans,omitempty"` // IncludeSpans indicates whether timing values should be included in the response. @@ -132,7 +134,7 @@ type Message struct { IncludeSpans *bool `json:"include_spans,omitempty"` // Path is the path to which to apply the payload. - Path string `json:"path,omitempty"` + Path string `json:"path,omitempty" env:"WRP_PATH,omitempty"` // Payload is the payload for this message. It's format is expected to match ContentType. // @@ -141,26 +143,26 @@ type Message struct { // For msgpack, this field may be raw binary or a UTF-8 string. // // example: eyJpZCI6IjUiLCJ0cyI6IjIwMTktMDItMTJUMTE6MTA6MDIuNjE0MTkxNzM1WiIsImJ5dGVzLXNlbnQiOjAsIm1lc3NhZ2VzLXNlbnQiOjEsImJ5dGVzLXJlY2VpdmVkIjowLCJtZXNzYWdlcy1yZWNlaXZlZCI6MH0= - Payload []byte `json:"payload,omitempty"` + Payload []byte `json:"payload,omitempty" env:"WRP_PAYLOAD,omitempty"` // ServiceName is the originating point of the request or response. - ServiceName string `json:"service_name,omitempty"` + ServiceName string `json:"service_name,omitempty" env:"WRP_SERVICE_NAME,omitempty"` // URL is the url to use when connecting to the nanomsg pipeline. - URL string `json:"url,omitempty"` + URL string `json:"url,omitempty" env:"WRP_URL,omitempty"` // PartnerIDs is the list of partner ids the message is meant to target. // // example: ["hello","world"] - PartnerIDs []string `json:"partner_ids,omitempty"` + PartnerIDs []string `json:"partner_ids,omitempty" env:"WRP_PARTNER_IDS,omitempty"` // SessionID is the ID for the current session. - SessionID string `json:"session_id,omitempty"` + SessionID string `json:"session_id,omitempty" env:"WRP_SESSION_ID,omitempty"` // QualityOfService is the qos value associated with this message. Values between 0 and 99, inclusive, // are defined by the wrp spec. Negative values are assumed to be zero, and values larger than 99 // are assumed to be 99. - QualityOfService QOSValue `json:"qos"` + QualityOfService QOSValue `json:"qos" env:"WRP_QOS"` } func (msg *Message) FindEventStringSubMatch() string { @@ -241,6 +243,24 @@ func (msg *Message) TrimmedPartnerIDs() []string { return trimmed } +// ToEnvironForm converts the message to a map of strings suitable for +// use with os.Setenv(). +func (msg *Message) ToEnvironForm() map[string]string { + return toEnvMap(msg) +} + +// MessageFromEnviron creates a new Message from an array of strings, such as +// that returned by os.Environ(). +func MessageFromEnviron(env []string) (*Message, error) { + var msg Message + err := fromEnvMap(env, &msg) + if err != nil { + return nil, err + } + + return &msg, nil +} + // SimpleRequestResponse represents a WRP message of type SimpleRequestResponseMessageType. // // https://github.com/xmidt-org/wrp-c/wiki/Web-Routing-Protocol#simple-request-response-definition @@ -249,20 +269,21 @@ func (msg *Message) TrimmedPartnerIDs() []string { type SimpleRequestResponse struct { // Type is exposed principally for encoding. This field *must* be set to SimpleRequestResponseMessageType, // and is automatically set by the BeforeEncode method. - Type MessageType `json:"msg_type"` - Source string `json:"source"` - Destination string `json:"dest"` - ContentType string `json:"content_type,omitempty"` - Accept string `json:"accept,omitempty"` - TransactionUUID string `json:"transaction_uuid,omitempty"` - Status *int64 `json:"status,omitempty"` - RequestDeliveryResponse *int64 `json:"rdr,omitempty"` - Headers []string `json:"headers,omitempty"` - Metadata map[string]string `json:"metadata,omitempty"` + Type MessageType `json:"msg_type" env:"WRP_MSG_TYPE"` + Source string `json:"source" env:"WRP_SOURCE"` + Destination string `json:"dest" env:"WRP_DEST"` + ContentType string `json:"content_type,omitempty" env:"WRP_CONTENT_TYPE,omitempty"` + Accept string `json:"accept,omitempty" env:"WRP_ACCEPT,omitempty"` + TransactionUUID string `json:"transaction_uuid,omitempty" env:"WRP_TRANSACTION_UUID"` + Status *int64 `json:"status,omitempty" env:"WRP_STATUS,omitempty"` + RequestDeliveryResponse *int64 `json:"rdr,omitempty" env:"WRP_RDR,omitempty"` + Headers []string `json:"headers,omitempty" env:"WRP_HEADERS,omitempty,multiline"` + Metadata map[string]string `json:"metadata,omitempty" env:"WRP_METADATA,omitempty"` Spans [][]string `json:"spans,omitempty"` IncludeSpans *bool `json:"include_spans,omitempty"` - Payload []byte `json:"payload,omitempty"` - PartnerIDs []string `json:"partner_ids,omitempty"` + Payload []byte `json:"payload,omitempty" env:"WRP_PAYLOAD,omitempty"` + PartnerIDs []string `json:"partner_ids,omitempty" env:"WRP_PARTNER_IDS,omitempty"` + SessionID string `json:"session_id,omitempty" env:"WRP_SESSION,omitempty"` } func (msg *SimpleRequestResponse) FindEventStringSubMatch() string { @@ -322,6 +343,24 @@ func (msg *SimpleRequestResponse) Response(newSource string, requestDeliveryResp return &response } +// ToEnvironForm converts the message to a map of strings suitable for +// use with os.Setenv(). +func (msg *SimpleRequestResponse) ToEnvironForm() map[string]string { + return toEnvMap(msg) +} + +// SimpleRequestResponseFromEnviron creates a new Message from an array of +// strings, such as that returned by os.Environ(). +func SimpleRequestResponseFromEnviron(env []string) (*SimpleRequestResponse, error) { + var msg SimpleRequestResponse + err := fromEnvMap(env, &msg) + if err != nil { + return nil, err + } + + return &msg, nil +} + // SimpleEvent represents a WRP message of type SimpleEventMessageType. // // This type implements Routable, and as such has a Response method. However, in actual practice @@ -334,15 +373,15 @@ func (msg *SimpleRequestResponse) Response(newSource string, requestDeliveryResp type SimpleEvent struct { // Type is exposed principally for encoding. This field *must* be set to SimpleEventMessageType, // and is automatically set by the BeforeEncode method. - Type MessageType `json:"msg_type"` - Source string `json:"source"` - Destination string `json:"dest"` - ContentType string `json:"content_type,omitempty"` - Headers []string `json:"headers,omitempty"` - Metadata map[string]string `json:"metadata,omitempty"` - Payload []byte `json:"payload,omitempty"` - PartnerIDs []string `json:"partner_ids,omitempty"` - SessionID string `json:"session_id,omitempty"` + Type MessageType `json:"msg_type" env:"WRP_MSG_TYPE"` + Source string `json:"source" env:"WRP_SOURCE"` + Destination string `json:"dest" env:"WRP_DEST"` + ContentType string `json:"content_type,omitempty" env:"WRP_CONTENT_TYPE,omitempty"` + Headers []string `json:"headers,omitempty" env:"WRP_HEADERS,omitempty,multiline"` + Metadata map[string]string `json:"metadata,omitempty" env:"WRP_METADATA,omitempty"` + Payload []byte `json:"payload,omitempty" env:"WRP_PAYLOAD,omitempty"` + PartnerIDs []string `json:"partner_ids,omitempty" env:"WRP_PARTNER_IDS"` + SessionID string `json:"session_id,omitempty" env:"WRP_SESSION,omitempty"` } func (msg *SimpleEvent) BeforeEncode() error { @@ -380,6 +419,24 @@ func (msg *SimpleEvent) Response(newSource string, requestDeliveryResponse int64 return &response } +// ToEnvironForm converts the message to a map of strings suitable for +// use with os.Setenv(). +func (msg *SimpleEvent) ToEnvironForm() map[string]string { + return toEnvMap(msg) +} + +// SimpleRequestResponseFromEnviron creates a new Message from an array of +// strings, such as that returned by os.Environ(). +func SimpleEventFromEnviron(env []string) (*SimpleEvent, error) { + var msg SimpleEvent + err := fromEnvMap(env, &msg) + if err != nil { + return nil, err + } + + return &msg, nil +} + // CRUD represents a WRP message of one of the CRUD message types. This type does not implement BeforeEncode, // and so does not automatically set the Type field. Client code must set the Type code appropriately. // @@ -387,20 +444,21 @@ func (msg *SimpleEvent) Response(newSource string, requestDeliveryResponse int64 // // Deprecated: A future version of wrp will remove this type. type CRUD struct { - Type MessageType `json:"msg_type"` - Source string `json:"source"` - Destination string `json:"dest"` - TransactionUUID string `json:"transaction_uuid,omitempty"` - ContentType string `json:"content_type,omitempty"` - Headers []string `json:"headers,omitempty"` - Metadata map[string]string `json:"metadata,omitempty"` - Spans [][]string `json:"spans,omitempty"` - IncludeSpans *bool `json:"include_spans,omitempty"` - Status *int64 `json:"status,omitempty"` - RequestDeliveryResponse *int64 `json:"rdr,omitempty"` - Path string `json:"path"` - Payload []byte `json:"payload,omitempty"` - PartnerIDs []string `json:"partner_ids,omitempty"` + Type MessageType `json:"msg_type" env:"WRP_MSG_TYPE"` + Source string `json:"source" env:"WRP_SOURCE"` + Destination string `json:"dest" env:"WRP_DEST"` + TransactionUUID string `json:"transaction_uuid,omitempty" env:"WRP_TRANSACTION_UUID"` + ContentType string `json:"content_type,omitempty" env:"WRP_CONTENT_TYPE,omitempty"` + Headers []string `json:"headers,omitempty" env:"WRP_HEADERS,omitempty,multiline"` + Metadata map[string]string `json:"metadata,omitempty" env:"WRP_METADATA,omitempty"` + Spans [][]string `json:"spans,omitempty" env:"WRP_SPANS,omitempty"` + IncludeSpans *bool `json:"include_spans,omitempty" env:"WRP_INCLUDE_SPANS,omitempty"` + Status *int64 `json:"status,omitempty" env:"WRP_STATUS,omitempty"` + RequestDeliveryResponse *int64 `json:"rdr,omitempty" env:"WRP_RDR,omitempty"` + Path string `json:"path" env:"WRP_PATH,omitempty"` + Payload []byte `json:"payload,omitempty" env:"WRP_PAYLOAD,omitempty"` + PartnerIDs []string `json:"partner_ids,omitempty" env:"WRP_PARTNER_IDS,omitempty"` + SessionID string `json:"session_id,omitempty" env:"WRP_SESSION,omitempty"` } // SetStatus simplifies setting the optional Status field, which is a pointer type tagged with omitempty. @@ -450,6 +508,24 @@ func (msg *CRUD) Response(newSource string, requestDeliveryResponse int64) Routa return &response } +// ToEnvironForm converts the message to a map of strings suitable for +// use with os.Setenv(). +func (msg *CRUD) ToEnvironForm() map[string]string { + return toEnvMap(msg) +} + +// CRUDFromEnviron creates a new Message from an array of strings, such as +// that returned by os.Environ(). +func CRUDFromEnviron(env []string) (*CRUD, error) { + var msg CRUD + err := fromEnvMap(env, &msg) + if err != nil { + return nil, err + } + + return &msg, nil +} + // ServiceRegistration represents a WRP message of type ServiceRegistrationMessageType. // // https://github.com/xmidt-org/wrp-c/wiki/Web-Routing-Protocol#on-device-service-registration-message-definition @@ -458,9 +534,9 @@ func (msg *CRUD) Response(newSource string, requestDeliveryResponse int64) Routa type ServiceRegistration struct { // Type is exposed principally for encoding. This field *must* be set to ServiceRegistrationMessageType, // and is automatically set by the BeforeEncode method. - Type MessageType `json:"msg_type"` - ServiceName string `json:"service_name"` - URL string `json:"url"` + Type MessageType `json:"msg_type" env:"WRP_MSG_TYPE"` + ServiceName string `json:"service_name" env:"WRP_SERVICE_NAME"` + URL string `json:"url" env:"WRP_URL"` } func (msg *ServiceRegistration) BeforeEncode() error { @@ -468,6 +544,24 @@ func (msg *ServiceRegistration) BeforeEncode() error { return nil } +// ToEnvironForm converts the message to a map of strings suitable for +// use with os.Setenv(). +func (msg *ServiceRegistration) ToEnvironForm() map[string]string { + return toEnvMap(msg) +} + +// ServiceRegistrationFromEnviron creates a new Message from an array of strings, +// such as that returned by os.Environ(). +func ServiceRegistrationFromEnviron(env []string) (*ServiceRegistration, error) { + var msg ServiceRegistration + err := fromEnvMap(env, &msg) + if err != nil { + return nil, err + } + + return &msg, nil +} + // ServiceAlive represents a WRP message of type ServiceAliveMessageType. // // https://github.com/xmidt-org/wrp-c/wiki/Web-Routing-Protocol#on-device-service-alive-message-definition @@ -476,7 +570,7 @@ func (msg *ServiceRegistration) BeforeEncode() error { type ServiceAlive struct { // Type is exposed principally for encoding. This field *must* be set to ServiceAliveMessageType, // and is automatically set by the BeforeEncode method. - Type MessageType `json:"msg_type"` + Type MessageType `json:"msg_type" env:"WRP_MSG_TYPE"` } func (msg *ServiceAlive) BeforeEncode() error { @@ -484,6 +578,24 @@ func (msg *ServiceAlive) BeforeEncode() error { return nil } +// ToEnvironForm converts the message to a map of strings suitable for +// use with os.Setenv(). +func (msg *ServiceAlive) ToEnvironForm() map[string]string { + return toEnvMap(msg) +} + +// ServiceAliveFromEnviron creates a new Message from an array of strings, +// such as that returned by os.Environ(). +func ServiceAliveFromEnviron(env []string) (*ServiceAlive, error) { + var msg ServiceAlive + err := fromEnvMap(env, &msg) + if err != nil { + return nil, err + } + + return &msg, nil +} + // Unknown represents a WRP message of type UnknownMessageType. // // https://github.com/xmidt-org/wrp-c/wiki/Web-Routing-Protocol#unknown-message-definition @@ -492,7 +604,7 @@ func (msg *ServiceAlive) BeforeEncode() error { type Unknown struct { // Type is exposed principally for encoding. This field *must* be set to UnknownMessageType, // and is automatically set by the BeforeEncode method. - Type MessageType `json:"msg_type"` + Type MessageType `json:"msg_type" env:"WRP_MSG_TYPE"` } func (msg *Unknown) BeforeEncode() error { @@ -500,6 +612,24 @@ func (msg *Unknown) BeforeEncode() error { return nil } +// ToEnvironForm converts the message to a map of strings suitable for +// use with os.Setenv(). +func (msg *Unknown) ToEnvironForm() map[string]string { + return toEnvMap(msg) +} + +// UnknownFromEnviron creates a new Message from an array of strings, +// such as that returned by os.Environ(). +func UnknownFromEnviron(env []string) (*Unknown, error) { + var msg Unknown + err := fromEnvMap(env, &msg) + if err != nil { + return nil, err + } + + return &msg, nil +} + func findEventStringSubMatch(s string) string { var match = eventPattern.FindStringSubmatch(s) diff --git a/messages_test.go b/messages_test.go index 4b96644..0c346d4 100644 --- a/messages_test.go +++ b/messages_test.go @@ -796,3 +796,274 @@ func TestMessage_TrimmedPartnerIDs(t *testing.T) { }) } } + +func mapToEnviron(m map[string]string) []string { + result := make([]string, 0, len(m)) + for k, v := range m { + result = append(result, fmt.Sprintf("%s=%s", k, v)) + } + return result +} + +func TestEnviron_Message(t *testing.T) { + tests := []struct { + description string + want Message + err error + }{ + { + description: "empty", + }, { + description: "simple", + want: Message{ + Source: "source", + Destination: "destination", + TransactionUUID: "transaction_uuid", + QualityOfService: 24, + PartnerIDs: []string{"foo", "bar", "baz"}, + }, + }, + } + for _, tc := range tests { + t.Run(tc.description, func(t *testing.T) { + msg := tc.want + m := msg.ToEnvironForm() + assert.NotNil(t, m) + + got, err := MessageFromEnviron(mapToEnviron(m)) + + if tc.err != nil { + require.Error(t, err) + assert.Nil(t, got) + return + } + + require.NoError(t, err) + require.NotNil(t, got) + assert.Equal(t, &msg, got) + }) + } +} + +func TestEnviron_SimpleRequestResponse(t *testing.T) { + tests := []struct { + description string + want SimpleRequestResponse + err error + }{ + { + description: "empty", + }, { + description: "simple", + want: SimpleRequestResponse{ + Source: "source", + Destination: "destination", + TransactionUUID: "transaction_uuid", + PartnerIDs: []string{"foo", "bar", "baz"}, + }, + }, + } + for _, tc := range tests { + t.Run(tc.description, func(t *testing.T) { + msg := tc.want + m := msg.ToEnvironForm() + assert.NotNil(t, m) + + got, err := SimpleRequestResponseFromEnviron(mapToEnviron(m)) + + if tc.err != nil { + require.Error(t, err) + assert.Nil(t, got) + return + } + + require.NoError(t, err) + require.NotNil(t, got) + assert.Equal(t, &msg, got) + }) + } +} + +func TestEnviron_SimpleEvent(t *testing.T) { + tests := []struct { + description string + want SimpleEvent + err error + }{ + { + description: "empty", + }, { + description: "simple", + want: SimpleEvent{ + Source: "source", + Destination: "destination", + PartnerIDs: []string{"foo", "bar", "baz"}, + }, + }, + } + for _, tc := range tests { + t.Run(tc.description, func(t *testing.T) { + msg := tc.want + m := msg.ToEnvironForm() + assert.NotNil(t, m) + + got, err := SimpleEventFromEnviron(mapToEnviron(m)) + + if tc.err != nil { + require.Error(t, err) + assert.Nil(t, got) + return + } + + require.NoError(t, err) + require.NotNil(t, got) + assert.Equal(t, &msg, got) + }) + } +} + +func TestEnviron_CRUD(t *testing.T) { + tests := []struct { + description string + want CRUD + err error + }{ + { + description: "empty", + }, { + description: "simple", + want: CRUD{ + Source: "source", + Destination: "destination", + PartnerIDs: []string{"foo", "bar", "baz"}, + }, + }, + } + for _, tc := range tests { + t.Run(tc.description, func(t *testing.T) { + msg := tc.want + m := msg.ToEnvironForm() + assert.NotNil(t, m) + + got, err := CRUDFromEnviron(mapToEnviron(m)) + + if tc.err != nil { + require.Error(t, err) + assert.Nil(t, got) + return + } + + require.NoError(t, err) + require.NotNil(t, got) + assert.Equal(t, &msg, got) + }) + } +} + +func TestEnviron_ServiceRegistration(t *testing.T) { + tests := []struct { + description string + want ServiceRegistration + err error + }{ + { + description: "empty", + }, { + description: "simple", + want: ServiceRegistration{ + ServiceName: "service_name", + }, + }, + } + for _, tc := range tests { + t.Run(tc.description, func(t *testing.T) { + msg := tc.want + m := msg.ToEnvironForm() + assert.NotNil(t, m) + + got, err := ServiceRegistrationFromEnviron(mapToEnviron(m)) + + if tc.err != nil { + require.Error(t, err) + assert.Nil(t, got) + return + } + + require.NoError(t, err) + require.NotNil(t, got) + assert.Equal(t, &msg, got) + }) + } +} + +func TestEnviron_ServiceAlive(t *testing.T) { + tests := []struct { + description string + want ServiceAlive + err error + }{ + { + description: "empty", + }, { + description: "simple", + want: ServiceAlive{ + Type: ServiceAliveMessageType, + }, + }, + } + for _, tc := range tests { + t.Run(tc.description, func(t *testing.T) { + msg := tc.want + m := msg.ToEnvironForm() + assert.NotNil(t, m) + + got, err := ServiceAliveFromEnviron(mapToEnviron(m)) + + if tc.err != nil { + require.Error(t, err) + assert.Nil(t, got) + return + } + + require.NoError(t, err) + require.NotNil(t, got) + assert.Equal(t, &msg, got) + }) + } +} + +func TestEnviron_Unknown(t *testing.T) { + tests := []struct { + description string + want Unknown + err error + }{ + { + description: "empty", + }, { + description: "simple", + want: Unknown{ + Type: UnknownMessageType, + }, + }, + } + for _, tc := range tests { + t.Run(tc.description, func(t *testing.T) { + msg := tc.want + m := msg.ToEnvironForm() + assert.NotNil(t, m) + + got, err := UnknownFromEnviron(mapToEnviron(m)) + + if tc.err != nil { + require.Error(t, err) + assert.Nil(t, got) + return + } + + require.NoError(t, err) + require.NotNil(t, got) + assert.Equal(t, &msg, got) + }) + } +} diff --git a/wrphttp/headers.go b/wrphttp/headers.go index ad07f27..d4e5dea 100644 --- a/wrphttp/headers.go +++ b/wrphttp/headers.go @@ -308,7 +308,7 @@ func SetMessageFromHeaders(h http.Header, m *wrp.Message) (err error) { // nolint:staticcheck m.IncludeSpans = getBoolHeader(h, includeSpansHeader) } - m.Spans = getSpans(h) + m.Spans = getSpans(h) // nolint:staticcheck m.ContentType = h.Get("Content-Type") m.Accept = h.Get(AcceptHeader) if m.Accept == "" { @@ -368,6 +368,7 @@ func AddMessageHeaders(h http.Header, m *wrp.Message) { h.Set(IncludeSpansHeader, strconv.FormatBool(*m.IncludeSpans)) } + // nolint:staticcheck for _, s := range m.Spans { h.Add(SpanHeader, strings.Join(s, ",")) } diff --git a/wrpvalidator/messageValidator.go b/wrpvalidator/messageValidator.go index c553c84..7ebb4a5 100644 --- a/wrpvalidator/messageValidator.go +++ b/wrpvalidator/messageValidator.go @@ -143,7 +143,7 @@ func SimpleEventType(m wrp.Message) error { func Spans(m wrp.Message) error { var err error // Spans consist of individual Span(s), arrays of timing values. - for _, s := range m.Spans { + for _, s := range m.Spans { // nolint:staticcheck if len(s) != len(spanFormat) { err = multierr.Append(err, ErrorInvalidSpanLength) continue