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 }