Skip to content

Commit

Permalink
feature: ground work for rich message support (#121)
Browse files Browse the repository at this point in the history
* feat: add rich message API
* feat: add field base tag and default field helper
* format: fix naming and missing comments
* docs: add missing comments for public api
* test(util): add and update util tests
  • Loading branch information
piksel committed Jan 31, 2021
1 parent 568c1bc commit 9ef6db2
Show file tree
Hide file tree
Showing 15 changed files with 447 additions and 61 deletions.
35 changes: 26 additions & 9 deletions pkg/format/formatter.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ func (fmtr *formatter) formatStructMap(structType reflect.Type, structItem inter
fmtr.Errors = append(fmtr.Errors, err)
}
} else if nextDepth < fmtr.MaxDepth {
value, valueLen = fmtr.getFieldValueString(values.FieldByName(field.Name), nextDepth)
value, valueLen = fmtr.getFieldValueString(values.FieldByName(field.Name), field.Base, nextDepth)
}
} else {
// Since no values was supplied, let's substitute the value with the type
Expand Down Expand Up @@ -147,6 +147,7 @@ type FieldInfo struct {
Template string
Required bool
Title bool
Base int
Keys []string
}

Expand All @@ -171,6 +172,10 @@ func (fmtr *formatter) getStructFieldInfo(structType reflect.Type) []FieldInfo {
Title: false,
}

if util.IsNumeric(fieldDef.Type.Kind()) {
info.Base = getFieldBase(fieldDef)
}

if tag, ok := fieldDef.Tag.Lookup("desc"); ok {
info.Description = tag
}
Expand All @@ -193,6 +198,7 @@ func (fmtr *formatter) getStructFieldInfo(structType reflect.Type) []FieldInfo {
}

if tag, ok := fieldDef.Tag.Lookup("key"); ok {
tag := strings.ToLower(tag)
info.Keys = strings.Split(tag, ",")
}

Expand All @@ -210,17 +216,28 @@ func (fmtr *formatter) getStructFieldInfo(structType reflect.Type) []FieldInfo {
return fields
}

func (fmtr *formatter) getFieldValueString(field reflect.Value, depth uint8) (string, int) {
func getFieldBase(field reflect.StructField) int {
if tag, ok := field.Tag.Lookup("base"); ok {
if base, err := strconv.ParseUint(tag, 10, 8); err == nil {
return int(base)
}
}

// Default to base 10 if not tagged
return 10
}

func (fmtr *formatter) getFieldValueString(field reflect.Value, base int, depth uint8) (string, int) {

nextDepth := depth + 1
kind := field.Kind()

if util.IsUnsignedDecimal(kind) {
strVal := fmt.Sprintf("%d", field.Uint())
if util.IsUnsignedInt(kind) {
strVal := strconv.FormatUint(field.Uint(), base)
return ColorizeNumber(fmt.Sprintf("%s", strVal)), len(strVal)
}
if util.IsSignedDecimal(kind) {
strVal := fmt.Sprintf("%d", field.Int())
if util.IsSignedInt(kind) {
strVal := strconv.FormatInt(field.Int(), base)
return ColorizeNumber(fmt.Sprintf("%s", strVal)), len(strVal)
}
if kind == reflect.String {
Expand All @@ -242,7 +259,7 @@ func (fmtr *formatter) getFieldValueString(field reflect.Value, depth uint8) (st
totalLen := 4
var itemLen int
for i := 0; i < fieldLen; i++ {
items[i], itemLen = fmtr.getFieldValueString(field.Index(i), nextDepth)
items[i], itemLen = fmtr.getFieldValueString(field.Index(i), base, nextDepth)
totalLen += itemLen
}
if fieldLen > 1 {
Expand All @@ -259,8 +276,8 @@ func (fmtr *formatter) getFieldValueString(field reflect.Value, depth uint8) (st
// initial value for totalLen is surrounding curlies and spaces, and separating commas
totalLen := 4 + (field.Len() - 1)
for iter.Next() {
key, keyLen := fmtr.getFieldValueString(iter.Key(), nextDepth)
value, valueLen := fmtr.getFieldValueString(iter.Value(), nextDepth)
key, keyLen := fmtr.getFieldValueString(iter.Key(), base, nextDepth)
value, valueLen := fmtr.getFieldValueString(iter.Value(), base, nextDepth)
items[index] = fmt.Sprintf("%s: %s", key, value)
totalLen += keyLen + valueLen + 2
}
Expand Down
22 changes: 18 additions & 4 deletions pkg/format/prop_key_resolver.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ type PropKeyResolver struct {
keys []string
}

// BindKeys is called to map config fields to it's tagged query keys
// NewPropKeyResolver creates a new PropKeyResolver and initializes it using the provided config
func NewPropKeyResolver(config types.ServiceConfig) PropKeyResolver {

_, fields := GetConfigFormat(config)
Expand Down Expand Up @@ -67,16 +67,16 @@ func (pkr *PropKeyResolver) Set(key string, value string) error {
}

// set sets the value of a target struct tagged with the corresponding key
func (c *PropKeyResolver) set(target reflect.Value, key string, value string) error {
if field, found := c.keyFields[strings.ToLower(key)]; found {
func (pkr *PropKeyResolver) set(target reflect.Value, key string, value string) error {
if field, found := pkr.keyFields[strings.ToLower(key)]; found {
valid, err := SetConfigField(target, field, value)
if !valid && err == nil {
return errors.New("invalid value for type")
}
return err
}

return fmt.Errorf("%v is not a valid config key %v", key, c.keys)
return fmt.Errorf("%v is not a valid config key %v", key, pkr.keys)
}

// UpdateConfigFromParams mutates the provided config, updating the values from it's corresponding params
Expand All @@ -91,12 +91,25 @@ func (pkr *PropKeyResolver) UpdateConfigFromParams(config types.ServiceConfig, p
return nil
}

// SetDefaultProps mutates the provided config, setting the tagged fields with their default values
func (pkr *PropKeyResolver) SetDefaultProps(config types.ServiceConfig) error {
for key, info := range pkr.keyFields {
if err := pkr.set(reflect.Indirect(reflect.ValueOf(config)), key, info.DefaultValue); err != nil {
return err
}
}
return nil
}

// Bind is called to set the internal config reference for the PropKeyResolver
func (pkr *PropKeyResolver) Bind(config types.ServiceConfig) PropKeyResolver {
bound := *pkr
bound.confValue = reflect.ValueOf(config)
return bound
}

// GetConfigQueryResolver returns the config itself if it implements ConfigQueryResolver
// otherwise it creates and returns a PropKeyResolver that implements it
func GetConfigQueryResolver(config types.ServiceConfig) types.ConfigQueryResolver {
var resolver types.ConfigQueryResolver
var ok bool
Expand All @@ -107,6 +120,7 @@ func GetConfigQueryResolver(config types.ServiceConfig) types.ConfigQueryResolve
return resolver
}

// KeyIsPrimary returns whether the key is the primary (and not an alias)
func (pkr *PropKeyResolver) KeyIsPrimary(key string) bool {
return pkr.keyFields[key].Keys[0] == key
}
23 changes: 23 additions & 0 deletions pkg/router/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,29 @@ func (router *ServiceRouter) Send(message string, params *t.Params) []error {
return errors
}

// SendItems sends the specified message items using the routers underlying services
func (router *ServiceRouter) SendItems(items []t.MessageItem, params t.Params) []error {
if router == nil {
return []error{fmt.Errorf("error sending message: no senders")}
}

// Fallback using old API for now
message := strings.Builder{}
for _, item := range items {
message.WriteString(item.Text)
}

serviceCount := len(router.services)
errors := make([]error, serviceCount)
results := router.SendAsync(message.String(), &params)

for i := range router.services {
errors[i] = <-results
}

return errors
}

// SendAsync sends the specified message using the routers underlying services
func (router *ServiceRouter) SendAsync(message string, params *t.Params) chan error {
serviceCount := len(router.services)
Expand Down
30 changes: 30 additions & 0 deletions pkg/types/field.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package types

import "sort"

// Field is a Key/Value pair used for extra data in log messages
type Field struct {
Key string
Value string
}

// FieldsFromMap creates a Fields slice from a map, optionally sorting keys
func FieldsFromMap(fieldMap map[string]string, sorted bool) []Field {
keys := make([]string, 0, len(fieldMap))
fields := make([]Field, 0, len(fieldMap))

for key := range fieldMap {
keys = append(keys, key)
}

if sorted {
sort.Strings(keys)
}

for i, key := range keys {
fields[i].Key = key
fields[i].Value = fieldMap[key]
}

return fields
}
56 changes: 56 additions & 0 deletions pkg/types/message_item.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package types

import (
"time"
)

// MessageLevel is used to denote the urgency of a message item
type MessageLevel uint8

const (
// Unknown is the default message level
Unknown MessageLevel = iota
// Debug is the lowest kind of known message level
Debug
// Info is generally used as the "normal" message level
Info
// Warning is generally used to denote messages that might be OK, but can cause problems
Warning
// Error is generally used for messages about things that did not go as planned
Error
messageLevelCount
// MessageLevelCount is used to create arrays that maps levels to other values
MessageLevelCount = int(messageLevelCount)
)

var messageLevelStrings = [MessageLevelCount]string{
"Unknown",
"Debug",
"Info",
"Warning",
"Error",
}

func (level MessageLevel) String() string {
if level >= messageLevelCount {
return messageLevelStrings[0]
}
return messageLevelStrings[level]
}

// MessageItem is an entry in a notification being sent by a service
type MessageItem struct {
Text string
Timestamp time.Time
Level MessageLevel
Fields []Field
}

// WithField appends the key/value pair to the message items fields
func (mi *MessageItem) WithField(key, value string) *MessageItem {
mi.Fields = append(mi.Fields, Field{
Key: key,
Value: value,
})
return mi
}
10 changes: 10 additions & 0 deletions pkg/types/message_limit.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package types

// MessageLimit is used for declaring the payload limits for services upstream APIs
type MessageLimit struct {
ChunkSize int
TotalChunkSize int

// Maximum number of chunks (including the last chunk for meta data)
ChunkCount int
}
13 changes: 13 additions & 0 deletions pkg/types/params.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,16 @@ package types

// Params is the string map used to provide additional variables to the service templates
type Params map[string]string

const titleKey = "title"

// SetTitle sets the "title" param to the specified value
func (p Params) SetTitle(title string) {
p[titleKey] = title
}

// Title returns the "title" param
func (p Params) Title() (title string, found bool) {
title, found = p[titleKey]
return
}
6 changes: 6 additions & 0 deletions pkg/types/rich_sender.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package types

// RichSender is the interface needed to implement to send rich notifications
type RichSender interface {
SendItems(items []MessageItem, params Params) error
}
15 changes: 0 additions & 15 deletions pkg/util/contains.go

This file was deleted.

0 comments on commit 9ef6db2

Please sign in to comment.