github.com/ipld/go-ipld-prime@v0.21.0/printer/printer.go (about)

     1  package printer
     2  
     3  import (
     4  	"encoding/hex"
     5  	"io"
     6  	"os"
     7  	"strconv"
     8  	"strings"
     9  
    10  	"github.com/ipld/go-ipld-prime/datamodel"
    11  	"github.com/ipld/go-ipld-prime/schema"
    12  )
    13  
    14  // Print emits a textual description of the node tree straight to stdout.
    15  // All printer configuration will be the default;
    16  // links will be printed, and will not be traversed.
    17  func Print(n datamodel.Node) {
    18  	Config{}.Print(n)
    19  }
    20  
    21  // Sprint returns a textual description of the node tree.
    22  // All printer configuration will be the default;
    23  // links will be printed, and will not be traversed.
    24  func Sprint(n datamodel.Node) string {
    25  	return Config{}.Sprint(n)
    26  }
    27  
    28  // Fprint accepts an io.Writer to which a textual description of the node tree will be written.
    29  // All printer configuration will be the default;
    30  // links will be printed, and will not be traversed.
    31  func Fprint(w io.Writer, n datamodel.Node) {
    32  	Config{}.Fprint(w, n)
    33  }
    34  
    35  // Print emits a textual description of the node tree straight to stdout.
    36  // The configuration structure this method is attached to can be used to specified details for how the printout will be formatted.
    37  func (cfg Config) Print(n datamodel.Node) {
    38  	cfg.Fprint(os.Stdout, n)
    39  }
    40  
    41  // Sprint returns a textual description of the node tree.
    42  // The configuration structure this method is attached to can be used to specified details for how the printout will be formatted.
    43  func (cfg Config) Sprint(n datamodel.Node) string {
    44  	var buf strings.Builder
    45  	cfg.Fprint(&buf, n)
    46  	return buf.String()
    47  }
    48  
    49  // Fprint accepts an io.Writer to which a textual description of the node tree will be written.
    50  // The configuration structure this method is attached to can be used to specified details for how the printout will be formatted.
    51  func (cfg Config) Fprint(w io.Writer, n datamodel.Node) {
    52  	pr := printBuf{w, cfg}
    53  	pr.Config.init()
    54  	pr.doString(0, printState_normal, n)
    55  }
    56  
    57  type Config struct {
    58  	// If true, long strings and long byte sequences will truncated, and will include ellipses instead.
    59  	//
    60  	// Not yet supported.
    61  	Abbreviate bool
    62  
    63  	// If set, the indentation to use.
    64  	// If nil, it will be treated as a default "\t".
    65  	Indentation []byte
    66  
    67  	// Probably does exactly what you think it does.
    68  	StartingIndent []byte
    69  
    70  	// Set to true if you like verbosity, I guess.
    71  	// If false, strings will only have kind+type markings if they're typed.
    72  	//
    73  	// Not yet supported.
    74  	AlwaysMarkStrings bool
    75  
    76  	// Set to true if you want type info to be skipped for any type that's in the Prelude
    77  	// (e.g. instead of `string<String>{` seeing only `string{` is preferred, etc).
    78  	//
    79  	// Not yet supported.
    80  	ElidePreludeTypeInfo bool
    81  
    82  	// Set to true if you want maps to use "complex"-style printouts:
    83  	// meaning they will print their keys on separate lines than their values,
    84  	// and keys may spread across mutiple lines if appropriate.
    85  	//
    86  	// If not set, a heuristic will be used based on if the map is known to
    87  	// have keys that are complex enough that rendering them as oneline seems likely to overload.
    88  	// See Config.useCmplxKeys for exactly how that's deteremined.
    89  	UseMapComplexStyleAlways bool
    90  
    91  	// For maps to use "complex"-style printouts (or not) per type.
    92  	// See docs on UseMapComplexStyleAlways for the overview of what "complex"-style means.
    93  	UseMapComplexStyleOnType map[schema.TypeName]bool
    94  }
    95  
    96  func (cfg *Config) init() {
    97  	if cfg.Indentation == nil {
    98  		cfg.Indentation = []byte{'\t'}
    99  	}
   100  }
   101  
   102  // oneline decides if a value should be flatted into printing on a single,
   103  // or if it's allowed to spread out over multiple lines.
   104  // Note that this will not be asked if something outside of a value has already declared it's
   105  // doing a oneline rendering; that railroads everything within it into that mode too.
   106  func (cfg Config) oneline(typ schema.Type, isInKey bool) bool {
   107  	return isInKey // Future: this could become customizable, with some kind of Always|OnlyInKeys|Never option enum per type.
   108  }
   109  
   110  /* TODO: not implemented or used
   111  // useRepr decides if a value should be printed using its representation.
   112  // Sometimes configuring this to be true for structs or unions with stringy representations
   113  // will cause map printouts using them as keys to become drastically more readable
   114  // (if with some loss of informativeness, or at least loss of explicitness).
   115  func (cfg Config) useRepr(typ schema.Type, isInKey bool) bool {
   116  	return false
   117  }
   118  */
   119  
   120  // useCmplxKeys decides if a map should print itself using a multi-line and extra-indented style for keys.
   121  func (cfg Config) useCmplxKeys(mapn datamodel.Node) bool {
   122  	if cfg.UseMapComplexStyleAlways {
   123  		return true
   124  	}
   125  	tn, ok := mapn.(schema.TypedNode)
   126  	if !ok {
   127  		return false
   128  	}
   129  	tnt := tn.Type()
   130  	if tnt == nil {
   131  		return false
   132  	}
   133  	force, ok := cfg.UseMapComplexStyleOnType[tnt.Name()]
   134  	if ok {
   135  		return force
   136  	}
   137  	ti, ok := tnt.(*schema.TypeMap)
   138  	if !ok { // Probably should never even have been asked, then?
   139  		panic("how did you get here?")
   140  	}
   141  	return !cfg.oneline(ti.KeyType(), true)
   142  }
   143  
   144  // FUTURE: one could imagine putting an optional LinkSystem param into the Config, too, and some recursion control.
   145  // It's definitely going to be the default to do zero recursion across links, though,
   146  // as doing that requires creating graph visualizations, and that is both possible, yet to do well becomes rather nontrivial.
   147  // Also, often a single node's tree visualization has been enough to get started debugging whatever I need to debug so far.
   148  
   149  type printBuf struct {
   150  	wr io.Writer
   151  
   152  	Config
   153  }
   154  
   155  func (z *printBuf) writeString(s string) {
   156  	z.wr.Write([]byte(s))
   157  }
   158  
   159  func (z *printBuf) doIndent(indentLevel int) {
   160  	z.wr.Write(z.Config.StartingIndent)
   161  	for i := 0; i < indentLevel; i++ {
   162  		z.wr.Write(z.Config.Indentation)
   163  	}
   164  }
   165  
   166  const (
   167  	printState_normal       uint8 = iota
   168  	printState_isKey              // may sometimes entersen or stringify things harder.
   169  	printState_isValue            // signals that we're continuing a line that started with a key (so, don't emit indent).
   170  	printState_isCmplxKey         // used to ask something to use multiline form, and an extra indent -- the opposite of what isKey does.
   171  	printState_isCmplxValue       // we're continuing a line (so don't emit indent), and we're stuck in complex mode (so keep telling your children to stay in this state too).
   172  )
   173  
   174  func (z *printBuf) doString(indentLevel int, printState uint8, n datamodel.Node) {
   175  	// First: indent.
   176  	switch printState {
   177  	case printState_normal, printState_isKey, printState_isCmplxKey:
   178  		z.doIndent(indentLevel)
   179  	}
   180  	// Second: the typekind and type name; or, just the kind, if there's no type.
   181  	//  Note: this can be somewhat overbearing -- for example, typed strings are going to get called out as `string<String>{"value"}`.
   182  	//   This is rather agonizingly verbose, but also accurate; I'm not sure if we'd want to elide information about typed-vs-untyped entirely.
   183  	if tn, ok := n.(schema.TypedNode); ok {
   184  		var tnk schema.TypeKind
   185  		var tntName string
   186  		// Defensively check for nil node type
   187  		if tnt := tn.Type(); tnt == nil {
   188  			tntName = "?!nil"
   189  			tnk = schema.TypeKind_Invalid
   190  		} else {
   191  			tntName = tnt.Name()
   192  			tnk = tnt.TypeKind()
   193  		}
   194  		z.writeString(tnk.String())
   195  		z.writeString("<")
   196  		z.writeString(tntName)
   197  		z.writeString(">")
   198  		switch tnk {
   199  		case schema.TypeKind_Invalid:
   200  			z.writeString("{?!}")
   201  		case schema.TypeKind_Map:
   202  			// continue -- the data-model driven behavior is sufficient to handle the content.
   203  		case schema.TypeKind_List:
   204  			// continue -- the data-model driven behavior is sufficient to handle the content.
   205  		case schema.TypeKind_Unit:
   206  			return // that's it!  there's no content data for a unit type.
   207  		case schema.TypeKind_Bool:
   208  			// continue -- the data-model driven behavior is sufficient to handle the content.
   209  		case schema.TypeKind_Int:
   210  			// continue -- the data-model driven behavior is sufficient to handle the content.
   211  		case schema.TypeKind_Float:
   212  			// continue -- the data-model driven behavior is sufficient to handle the content.
   213  		case schema.TypeKind_String:
   214  			// continue -- the data-model driven behavior is sufficient to handle the content.
   215  		case schema.TypeKind_Bytes:
   216  			// continue -- the data-model driven behavior is sufficient to handle the content.
   217  		case schema.TypeKind_Link:
   218  			// continue -- the data-model driven behavior is sufficient to handle the content.
   219  		case schema.TypeKind_Struct:
   220  			// Very similar to a map, but keys aren't quoted.
   221  			// Also, because it's possible for structs to be keys in a map themselves, they potentially need oneline emission.
   222  			// Or, to customize emission in another direction if being a key in a map that's printing in "complex" mode.
   223  			// FUTURE: there should also probably be some way to configure instructions to use their representation form instead.
   224  			oneline :=
   225  				printState == printState_isCmplxValue ||
   226  					printState != printState_isCmplxKey && z.Config.oneline(tn.Type(), printState == printState_isKey)
   227  			deepen := 1
   228  			if printState == printState_isCmplxKey {
   229  				deepen = 2
   230  			}
   231  			childState := printState_isValue
   232  			if oneline {
   233  				childState = printState_isCmplxValue
   234  			}
   235  			z.writeString("{")
   236  			if !oneline && n.Length() > 0 {
   237  				z.writeString("\n")
   238  			}
   239  			for itr := n.MapIterator(); !itr.Done(); {
   240  				k, v, _ := itr.Next()
   241  				if !oneline {
   242  					z.doIndent(indentLevel + deepen)
   243  				}
   244  				fn, _ := k.AsString()
   245  				z.writeString(fn)
   246  				z.writeString(": ")
   247  				z.doString(indentLevel+deepen, childState, v)
   248  				if oneline {
   249  					if !itr.Done() {
   250  						z.writeString(", ")
   251  					}
   252  				} else {
   253  					z.writeString("\n")
   254  				}
   255  			}
   256  			if !oneline {
   257  				z.doIndent(indentLevel)
   258  			}
   259  			z.writeString("}")
   260  			return
   261  		case schema.TypeKind_Union:
   262  			// There will only be one thing in it, but we still have to use an iterator
   263  			//  to figure out what that is if we're doing this generically.
   264  			//  We can ignore the key and just look at the value type again though (even though those are the same in practice).
   265  			_, v, _ := n.MapIterator().Next()
   266  			z.writeString("{")
   267  			z.doString(indentLevel, printState_isValue, v)
   268  			z.writeString("}")
   269  			return
   270  		case schema.TypeKind_Enum:
   271  			panic("TODO")
   272  		default:
   273  			panic("unreachable")
   274  		}
   275  	} else {
   276  		if n.IsAbsent() {
   277  			z.writeString("absent")
   278  			return
   279  		}
   280  		z.writeString(n.Kind().String())
   281  	}
   282  	// Third: all the actual content.
   283  	// FUTURE: this is probably gonna become... somewhat more conditional, and may end up being a sub-function to be reasonably wieldy.
   284  	switch n.Kind() {
   285  	case datamodel.Kind_Map:
   286  		// Maps have to decide if they have complex keys and want to use an additionally-intended pattern to make that readable.
   287  		// "Complex" here means roughly: if you try to cram them into one line, it doesn't look good.
   288  		// This choice starts at the map but is mostly executed during the printing of the key:
   289  		//  the key will start itself at normal indentation,
   290  		//  but should then doubly indent all its nested values (assuming it has any).
   291  		cmplxKeys := z.Config.useCmplxKeys(n)
   292  		childKeyState := printState_isKey
   293  		if cmplxKeys {
   294  			childKeyState = printState_isCmplxKey
   295  		}
   296  		z.writeString("{")
   297  		if n.Length() > 0 {
   298  			z.writeString("\n")
   299  		} else {
   300  			z.writeString("}")
   301  			return
   302  		}
   303  		for itr := n.MapIterator(); !itr.Done(); {
   304  			k, v, err := itr.Next()
   305  			if err != nil {
   306  				z.doIndent(indentLevel + 1)
   307  				z.writeString("!! map iteration step yielded error: ")
   308  				z.writeString(err.Error())
   309  				z.writeString("\n")
   310  				break
   311  			}
   312  			z.doString(indentLevel+1, childKeyState, k)
   313  			z.writeString(": ")
   314  			z.doString(indentLevel+1, printState_isValue, v)
   315  			z.writeString("\n")
   316  		}
   317  		z.doIndent(indentLevel)
   318  		z.writeString("}")
   319  	case datamodel.Kind_List:
   320  		z.writeString("{")
   321  		if n.Length() > 0 {
   322  			z.writeString("\n")
   323  		} else {
   324  			z.writeString("}")
   325  			return
   326  		}
   327  		for itr := n.ListIterator(); !itr.Done(); {
   328  			idx, v, err := itr.Next()
   329  			if err != nil {
   330  				z.doIndent(indentLevel + 1)
   331  				z.writeString("!! list iteration step yielded error: ")
   332  				z.writeString(err.Error())
   333  				z.writeString("\n")
   334  				break
   335  			}
   336  			z.doIndent(indentLevel + 1)
   337  			z.writeString(strconv.FormatInt(idx, 10))
   338  			z.writeString(": ")
   339  			z.doString(indentLevel+1, printState_isValue, v)
   340  			z.writeString("\n")
   341  		}
   342  		z.doIndent(indentLevel)
   343  		z.writeString("}")
   344  	case datamodel.Kind_Null:
   345  		// nothing: we already wrote the word "null" when we wrote the kind info prefix.
   346  	case datamodel.Kind_Bool:
   347  		z.writeString("{")
   348  		if b, _ := n.AsBool(); b {
   349  			z.writeString("true")
   350  		} else {
   351  			z.writeString("false")
   352  		}
   353  		z.writeString("}")
   354  	case datamodel.Kind_Int:
   355  		x, _ := n.AsInt()
   356  		z.writeString("{")
   357  		z.writeString(strconv.FormatInt(x, 10))
   358  		z.writeString("}")
   359  	case datamodel.Kind_Float:
   360  		x, _ := n.AsFloat()
   361  		z.writeString("{")
   362  		z.writeString(strconv.FormatFloat(x, 'f', -1, 64))
   363  		z.writeString("}")
   364  	case datamodel.Kind_String:
   365  		x, _ := n.AsString()
   366  		z.writeString("{")
   367  		z.writeString(strconv.QuoteToGraphic(x))
   368  		z.writeString("}")
   369  	case datamodel.Kind_Bytes:
   370  		x, _ := n.AsBytes()
   371  		z.writeString("{")
   372  		dst := make([]byte, hex.EncodedLen(len(x)))
   373  		hex.Encode(dst, x)
   374  		z.writeString(string(dst))
   375  		z.writeString("}")
   376  	case datamodel.Kind_Link:
   377  		x, _ := n.AsLink()
   378  		z.writeString("{")
   379  		z.writeString(x.String())
   380  		z.writeString("}")
   381  	}
   382  }