github.com/0xsequence/ethkit@v1.25.0/cmd/ethkit/print.go (about)

     1  package main
     2  
     3  import (
     4  	"bytes"
     5  	"encoding/base64"
     6  	"encoding/hex"
     7  	"encoding/json"
     8  	"fmt"
     9  	"reflect"
    10  	"strconv"
    11  	"strings"
    12  	"text/tabwriter"
    13  )
    14  
    15  type printableFormat struct {
    16  	minwidth int
    17  	tabwidth int
    18  	padding  int
    19  	padchar  byte
    20  }
    21  
    22  // NewPrintableFormat returns a customized configuration format
    23  func NewPrintableFormat(minwidth, tabwidth, padding int, padchar byte) *printableFormat {
    24  	return &printableFormat{minwidth, tabwidth, padding, padchar}
    25  }
    26  
    27  // Printable is a generic key-value (map) structure that could contain nested objects.
    28  type Printable map[string]any
    29  
    30  // PrettyJSON prints an object in "prettified" JSON format
    31  func PrettyJSON(toJSON any) (*string, error) {
    32  	b, err := json.MarshalIndent(toJSON, "", "  ")
    33  	if err != nil {
    34  		return nil, err
    35  	}
    36  	jsonString := string(b)
    37  
    38  	// remove the trailing newline character ("%")
    39  	if jsonString[len(jsonString)-1] == '\n' {
    40  		jsonString = jsonString[:len(jsonString)-1]
    41  	}
    42  
    43  	return &jsonString, nil
    44  }
    45  
    46  // FromStruct converts a struct into a Printable using, when available, JSON field names as keys
    47  func (p *Printable) FromStruct(input any) error {
    48  	bytes, err := json.Marshal(input)
    49  	if err != nil {
    50  		return err
    51  	}
    52  	if err := json.Unmarshal(bytes, &p); err != nil {
    53  		return err
    54  	}
    55  
    56  	return nil
    57  }
    58  
    59  // Columnize returns a formatted-in-columns (vertically aligned) string based on a provided configuration.
    60  func (p *Printable) Columnize(pf printableFormat) string {
    61  	var buf bytes.Buffer
    62  	w := tabwriter.NewWriter(&buf, pf.minwidth, pf.tabwidth, pf.padding, pf.padchar, tabwriter.Debug)
    63  	// NOTE: Order is not maintained whilst looping over map's . Results from different execution may differ.
    64  	for k, v := range *p {
    65  		printKeyValue(w, k, v)
    66  	}
    67  	w.Flush()
    68  
    69  	return buf.String()
    70  }
    71  
    72  func printKeyValue(w *tabwriter.Writer, key string, value any) {
    73  	switch t := value.(type) {
    74  	// NOTE: Printable is not directly inferred as map[string]any therefore explicit reference is necessary
    75  	case map[string]any:
    76  		fmt.Fprintln(w, key, "\t")
    77  		for tk, tv := range t {
    78  			printKeyValue(w, "\t "+tk, tv)
    79  		}
    80  	case []any:
    81  		fmt.Fprintln(w, key, "\t")
    82  		for _, elem := range t {
    83  			elemMap, ok := elem.(map[string]any)
    84  			if ok {
    85  				for tk, tv := range elemMap {
    86  					printKeyValue(w, "\t "+tk, tv)
    87  				}
    88  				fmt.Fprintln(w, "\t", "\t")
    89  			} else {
    90  				fmt.Fprintln(w, "\t", customFormat(elem))
    91  			}
    92  		}
    93  	default:
    94  		// custom format for numbers to avoid scientific notation
    95  		fmt.Fprintf(w, "%s\t %s\n", key, customFormat(value))
    96  	}
    97  }
    98  
    99  func customFormat(value any) string {
   100  	switch v := value.(type) {
   101  	case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64:
   102  		return fmt.Sprintf("%d", v)
   103  	case float32, float64:
   104  		return formatFloat(v)
   105  	default:
   106  		return fmt.Sprintf("%v", base64ToHex(value))
   107  	}
   108  }
   109  
   110  func formatFloat(f any) string {
   111  	str := fmt.Sprintf("%v", f)
   112  	if strings.ContainsAny(str, "eE.") {
   113  		if floatValue, err := strconv.ParseFloat(str, 64); err == nil {
   114  			return strconv.FormatFloat(floatValue, 'f', -1, 64)
   115  		}
   116  	}
   117  
   118  	return str
   119  }
   120  
   121  func base64ToHex(str any) any {
   122  	_, ok := str.(string); if !ok {
   123  		return str
   124  	}
   125  	decoded, err := base64.StdEncoding.DecodeString(str.(string))
   126  	if err != nil {
   127  		return str
   128  	}
   129  
   130  	return "0x" + hex.EncodeToString(decoded)
   131  }
   132  
   133  // GetValueByJSONTag returns the value of a struct field matching a JSON tag provided in input.
   134  func GetValueByJSONTag(input any, jsonTag string) any {
   135  	// TODO: Refactor to support both nil values and errors when key not found
   136  	return findField(reflect.ValueOf(input), jsonTag)
   137  }
   138  
   139  func findField(val reflect.Value, jsonTag string) any {
   140  	seen := make(map[uintptr]bool)
   141  
   142  	// take the value the pointer val points to
   143  	if val.Kind() == reflect.Ptr {
   144  		val = val.Elem()
   145  	}
   146  
   147  	// return if the element is not a struct
   148  	if val.Kind() != reflect.Struct {
   149  		return nil
   150  	}
   151  
   152  	// check if the struct has already been processed to avoid infinite recursion
   153  	if val.CanAddr() {
   154  		ptr := val.Addr().Pointer()
   155  		if seen[ptr] {
   156  			return nil
   157  		}
   158  		seen[ptr] = true
   159  	}
   160  
   161  	t := val.Type()
   162  
   163  	for i := 0; i < val.NumField(); i++ {
   164  		field := t.Field(i)
   165  		tag := field.Tag.Get("json")
   166  
   167  		fieldValue := val.Field(i)
   168  		if fieldValue.Kind() == reflect.Struct {
   169  			// recursively process fields including embedded ones
   170  			return findField(fieldValue, jsonTag)
   171  		} else {
   172  			if strings.EqualFold(strings.ToLower(tag), strings.ToLower(jsonTag)) {
   173  				return val.Field(i).Interface()
   174  			}
   175  		}
   176  	}
   177  
   178  	return nil
   179  }