package d2chaos import ( "fmt" mathrand "math/rand" "strings" "time" "oss.terrastruct.com/util-go/go2" "oss.terrastruct.com/d2/d2ast" "oss.terrastruct.com/d2/d2format" "oss.terrastruct.com/d2/d2graph" "oss.terrastruct.com/d2/d2oracle" "oss.terrastruct.com/d2/d2target" ) const complexIDs = false func GenDSL(maxi int) (_ string, err error) { gs := &dslGenState{ rand: mathrand.New(mathrand.NewSource(time.Now().UnixNano())), g: d2graph.NewGraph(), nodeShapes: make(map[string]string), nodeContainer: make(map[string]string), } gs.g.AST = &d2ast.Map{} err = gs.gen(maxi) if err != nil { return "", err } return d2format.Format(gs.g.AST), nil } type dslGenState struct { rand *mathrand.Rand g *d2graph.Graph nodesArr []string nodeShapes map[string]string nodeContainer map[string]string } func (gs *dslGenState) gen(maxi int) error { maxi = gs.rand.Intn(maxi) + 1 for i := 0; i < maxi; i++ { switch gs.roll(25, 75) { case 0: // 25% chance of creating a new node. err := gs.node() if err != nil { return err } case 1: // 75% chance of connecting two random nodes with a random label. err := gs.edge() if err != nil { return err } } } return nil } func (gs *dslGenState) genNode(containerID string) (string, error) { maxLen := 8 if complexIDs { maxLen = 32 } nodeID := gs.randStr(maxLen, true) if containerID != "" { nodeID = containerID + "." + nodeID } var err error gs.g, nodeID, err = d2oracle.Create(gs.g, nil, nodeID) if err != nil { return "", err } gs.nodesArr = append(gs.nodesArr, nodeID) gs.nodeShapes[nodeID] = "square" gs.nodeContainer[nodeID] = containerID return nodeID, nil } func (gs *dslGenState) node() error { containerID := "" var err error if gs.roll(25, 75) == 1 { // 75% chance of creating this as a child under a container. containerID, err = gs.randContainer() if err != nil { return err } } nodeID, err := gs.genNode(containerID) if err != nil { return err } if gs.roll(25, 75) == 0 { // 25% chance of adding a label. maxLen := 8 if complexIDs { maxLen = 256 } gs.g, err = d2oracle.Set(gs.g, nil, nodeID, nil, go2.Pointer(gs.randStr(maxLen, false))) if err != nil { return err } } if gs.roll(25, 75) == 1 { // 75% chance of adding a shape. randShape := gs.randShape() gs.g, err = d2oracle.Set(gs.g, nil, nodeID+".shape", nil, go2.Pointer(randShape)) if err != nil { return err } gs.nodeShapes[nodeID] = randShape } return nil } func (gs *dslGenState) edge() error { var src string var dst string var err error for { src, err = gs.randNode() if err != nil { return err } dst, err = gs.randNode() if err != nil { return err } if gs.findOuterSequenceDiagram(src) == gs.findOuterSequenceDiagram(dst) { break } err = gs.node() if err != nil { return err } } if src == dst && gs.nodeShapes[dst] == d2target.ShapeSequenceDiagram { return nil } srcArrow := "-" if gs.randBool() { srcArrow = "<" } dstArrow := "-" if gs.randBool() { dstArrow = ">" if srcArrow == "<" { dstArrow = "->" } } key := fmt.Sprintf("%s %s%s %s", src, srcArrow, dstArrow, dst) gs.g, key, err = d2oracle.Create(gs.g, nil, key) if err != nil { return err } if gs.randBool() { maxLen := 8 if complexIDs { maxLen = 128 } gs.g, err = d2oracle.Set(gs.g, nil, key, nil, go2.Pointer(gs.randStr(maxLen, false))) if err != nil { return err } } return nil } func (gs *dslGenState) randContainer() (string, error) { containers := go2.Filter(gs.nodesArr, func(x string) bool { shape := gs.nodeShapes[x] return shape != "image" && shape != "code" && shape != "sql_table" && shape != "text" && shape != "class" }) if len(containers) == 0 { return "", nil } return containers[gs.rand.Intn(len(containers))], nil } func (gs *dslGenState) randNode() (string, error) { if len(gs.nodesArr) == 0 { return gs.genNode("") } return gs.nodesArr[gs.rand.Intn(len(gs.nodesArr))], nil } func (gs *dslGenState) randBool() bool { return gs.rand.Intn(2) == 0 } // TODO go back to using xrand.String, currently some incompatibility with // stuffing these strings into a script for dagre func randRune() rune { if complexIDs { if mathrand.Int31n(100) == 0 { // Generate newline 1% of the time. return '\n' } return mathrand.Int31n(128) + 1 } else { return mathrand.Int31n(26) + 97 } } func (gs *dslGenState) findOuterSequenceDiagram(nodeID string) string { for { containerID := gs.nodeContainer[nodeID] if containerID == "" || gs.nodeShapes[containerID] == d2target.ShapeSequenceDiagram { return containerID } nodeID = containerID } } func String(n int, exclude []rune) string { var b strings.Builder for i := 0; i < n; i++ { r := randRune() excluded := false for _, xr := range exclude { if r == xr { excluded = true break } } if excluded { i-- continue } b.WriteRune(r) } return b.String() } func (gs *dslGenState) randStr(n int, inKey bool) string { // Underscores have semantic meaning (parent) // Backticks are for opening and closing these strings // Curly braces can trigger templating // \\ triggers octal sequences s := String(gs.rand.Intn(n), []rune{ rune('_'), rune('`'), rune('}'), rune('{'), rune('\\'), }) as := d2ast.RawString(s, inKey) return d2format.Format(as) } func (gs *dslGenState) randShape() string { for { s := shapes[gs.rand.Intn(len(shapes))] if s != d2target.ShapeImage && s != d2target.ShapeText { return s } } } func (gs *dslGenState) roll(probs ...int) int { max := 0 for _, p := range probs { max += p } n := gs.rand.Intn(max) var acc int for i, p := range probs { if n >= acc && n < acc+p { return i } acc += p } panic("d2chaos: unreachable") } var shapes = d2target.Shapes