1 package d2format_test
2
3 import (
4 "fmt"
5 "strings"
6 "testing"
7
8 "oss.terrastruct.com/util-go/assert"
9
10 "oss.terrastruct.com/d2/d2format"
11 "oss.terrastruct.com/d2/d2parser"
12 )
13
14 func TestPrint(t *testing.T) {
15 t.Parallel()
16
17 testCases := []struct {
18 name string
19 in string
20 exp string
21 }{
22 {
23 name: "basic",
24 in: `
25 x -> y
26 `,
27 exp: `x -> y
28 `,
29 },
30
31 {
32 name: "complex",
33 in: `
34 sql_example : sql_example {
35 board : {
36 shape: sql_table
37 id: int {constraint: primary_key}
38 frame: int {constraint: foreign_key}
39 diagram: int {constraint: foreign_key}
40 board_objects: jsonb
41 last_updated: timestamp with time zone
42 last_thumbgen: timestamp with time zone
43 dsl : text
44 }
45
46 # Normal.
47 board.diagram -> diagrams.id
48
49 # Self referential.
50 diagrams.id -> diagrams.representation
51
52 # SrcArrow test.
53 diagrams.id <- views . diagram
54 diagrams.id <-> steps . diagram
55
56 diagrams: {
57 shape: sql_table
58 id: {type: int ; constraint: primary_key}
59 representation: {type: jsonb}
60 }
61
62 views: {
63 shape: sql_table
64 id: {type: int; constraint: primary_key}
65 representation: {type: jsonb}
66 diagram: int {constraint: foreign_key}
67 }
68
69 steps: {
70 shape: sql_table
71 id: { type: int; constraint: primary_key }
72 representation: { type: jsonb }
73 diagram: int {constraint: foreign_key}
74 }
75 meow <- diagrams.id
76 }
77
78 D2 AST Parser {
79 shape: class
80
81 +prevRune : rune
82 prevColumn : int
83
84 +eatSpace(eatNewlines bool): (rune, error)
85 unreadRune()
86
87 \#scanKey(r rune): (k Key, _ error)
88 }
89
90 """dmaskkldsamkld """
91
92
93 """
94
95 dmaskdmasl
96 mdlkasdaskml
97 daklsmdakms
98
99 """
100
101 bs: |
102 dmasmdkals
103 dkmsamdklsa
104 |
105 bs2: | mdsalldkams|
106
107 y-->q: meow
108 x->y->z
109
110 meow: {
111 x: |` + "`" + `
112 meow
113 meow
114 ` + "`" + `| {
115 }
116 }
117
118
119 "meow\t": ok
120 `,
121 exp: `sql_example: sql_example {
122 board: {
123 shape: sql_table
124 id: int {constraint: primary_key}
125 frame: int {constraint: foreign_key}
126 diagram: int {constraint: foreign_key}
127 board_objects: jsonb
128 last_updated: timestamp with time zone
129 last_thumbgen: timestamp with time zone
130 dsl: text
131 }
132
133 # Normal.
134 board.diagram -> diagrams.id
135
136 # Self referential.
137 diagrams.id -> diagrams.representation
138
139 # SrcArrow test.
140 diagrams.id <- views.diagram
141 diagrams.id <-> steps.diagram
142
143 diagrams: {
144 shape: sql_table
145 id: {type: int; constraint: primary_key}
146 representation: {type: jsonb}
147 }
148
149 views: {
150 shape: sql_table
151 id: {type: int; constraint: primary_key}
152 representation: {type: jsonb}
153 diagram: int {constraint: foreign_key}
154 }
155 meow <- diagrams.id
156
157 steps: {
158 shape: sql_table
159 id: {type: int; constraint: primary_key}
160 representation: {type: jsonb}
161 diagram: int {constraint: foreign_key}
162 }
163 }
164
165 D2 AST Parser: {
166 shape: class
167
168 +prevRune: rune
169 prevColumn: int
170
171 +eatSpace(eatNewlines bool): (rune, error)
172 unreadRune()
173
174 \#scanKey(r rune): (k Key, _ error)
175 }
176
177 """ dmaskkldsamkld """
178
179 """
180
181 dmaskdmasl
182 mdlkasdaskml
183 daklsmdakms
184
185 """
186
187 bs: |md
188 dmasmdkals
189 dkmsamdklsa
190 |
191 bs2: |md mdsalldkams |
192
193 y -> q: meow
194 x -> y -> z
195
196 meow: {
197 x: |` + "`" + `md
198 meow
199 meow
200 ` + "`" + `|
201 }
202
203 "meow\t": ok
204 `,
205 },
206
207 {
208 name: "block_comment",
209 in: `
210 """
211 D2 AST Parser2: {
212 shape: class
213
214 reader: io.RuneReader
215 readerPos: d2ast.Position
216
217 lookahead: "[]rune"
218 lookaheadPos: d2ast.Position
219
220 peek() (r rune, eof bool)
221 -rewind(): ()
222 +commit()
223 \#peekn(n int) (s string, eof bool)
224 }
225 """
226 `,
227 exp: `"""
228 D2 AST Parser2: {
229 shape: class
230
231 reader: io.RuneReader
232 readerPos: d2ast.Position
233
234 lookahead: "[]rune"
235 lookaheadPos: d2ast.Position
236
237 peek() (r rune, eof bool)
238 -rewind(): ()
239 +commit()
240 \#peekn(n int) (s string, eof bool)
241 }
242 """
243 `,
244 },
245
246 {
247 name: "block_string_indent",
248 in: `
249 parent: {
250 example_code: |` + "`" + `go
251 package fs
252
253 type FS interface {
254 Open(name string) (File, error)
255 }
256
257 type File interface {
258 Stat() (FileInfo, error)
259 Read([]byte) (int, error)
260 Close() error
261 }
262
263 var (
264 ErrInvalid = errInvalid() // "invalid argument"
265 ErrPermission = errPermission() // "permission denied"
266 ErrExist = errExist() // "file already exists"
267 ErrNotExist = errNotExist() // "file does not exist"
268 ErrClosed = errClosed() // "file already closed"
269 )
270 ` + "`" + `|}`,
271 exp: `parent: {
272 example_code: |` + "`" + `go
273 package fs
274
275 type FS interface {
276 Open(name string) (File, error)
277 }
278
279 type File interface {
280 Stat() (FileInfo, error)
281 Read([]byte) (int, error)
282 Close() error
283 }
284
285 var (
286 ErrInvalid = errInvalid() // "invalid argument"
287 ErrPermission = errPermission() // "permission denied"
288 ErrExist = errExist() // "file already exists"
289 ErrNotExist = errNotExist() // "file does not exist"
290 ErrClosed = errClosed() // "file already closed"
291 )
292 ` + "`" + `|
293 }
294 `,
295 },
296
297 {
298
299
300 name: "block_string_indent_2",
301 in: `
302 parent: {
303 example_code: |` + "`" + `go
304 package fs
305
306 type FS interface {
307 Open(name string) (File, error)
308 }
309
310 type File interface {
311 Stat() (FileInfo, error)
312 Read([]byte) (int, error)
313 Close() error
314 }
315
316 var (
317 ErrInvalid = errInvalid() // "invalid argument"
318 ErrPermission = errPermission() // "permission denied"
319 ErrExist = errExist() // "file already exists"
320 ErrNotExist = errNotExist() // "file does not exist"
321 ErrClosed = errClosed() // "file already closed"
322 )
323 ` + "`" + `|}`,
324 exp: `parent: {
325 example_code: |` + "`" + `go
326 package fs
327
328 type FS interface {
329 Open(name string) (File, error)
330 }
331
332 type File interface {
333 Stat() (FileInfo, error)
334 Read([]byte) (int, error)
335 Close() error
336 }
337
338 var (
339 ErrInvalid = errInvalid() // "invalid argument"
340 ErrPermission = errPermission() // "permission denied"
341 ErrExist = errExist() // "file already exists"
342 ErrNotExist = errNotExist() // "file does not exist"
343 ErrClosed = errClosed() // "file already closed"
344 )
345 ` + "`" + `|
346 }
347 `,
348 },
349
350 {
351
352
353 name: "block_string_indent_3",
354 in: `
355 parent: {
356 example_code: |` + "`" + `go
357 package fs
358
359 type FS interface {
360 Open(name string) (File, error)
361 }
362
363 type File interface {
364 Stat() (FileInfo, error)
365 Read([]byte) (int, error)
366 Close() error
367 }
368
369 var (
370 ErrInvalid = errInvalid() // "invalid argument"
371 ErrPermission = errPermission() // "permission denied"
372 ErrExist = errExist() // "file already exists"
373 ErrNotExist = errNotExist() // "file does not exist"
374 ErrClosed = errClosed() // "file already closed"
375 )
376 ` + "`" + `|}`,
377 exp: `parent: {
378 example_code: |` + "`" + `go
379 package fs
380
381 type FS interface {
382 Open(name string) (File, error)
383 }
384
385 type File interface {
386 Stat() (FileInfo, error)
387 Read([]byte) (int, error)
388 Close() error
389 }
390
391 var (
392 ErrInvalid = errInvalid() // "invalid argument"
393 ErrPermission = errPermission() // "permission denied"
394 ErrExist = errExist() // "file already exists"
395 ErrNotExist = errNotExist() // "file does not exist"
396 ErrClosed = errClosed() // "file already closed"
397 )
398 ` + "`" + `|
399 }
400 `,
401 },
402
403 {
404
405 name: "block_string_uneven_indent",
406 in: `
407 parent: {
408 example_code: |` + "`" + `go
409 package fs
410
411 type FS interface {
412 Open(name string) (File, error)
413 }
414
415 type File interface {
416 Stat() (FileInfo, error)
417 Read([]byte) (int, error)
418 Close() error
419 }
420
421 var (
422 ErrInvalid = errInvalid() // "invalid argument"
423 ErrPermission = errPermission() // "permission denied"
424 ErrExist = errExist() // "file already exists"
425 ErrNotExist = errNotExist() // "file does not exist"
426 ErrClosed = errClosed() // "file already closed"
427 )
428 ` + "`" + `|}`,
429 exp: `parent: {
430 example_code: |` + "`" + `go
431 package fs
432
433 type FS interface {
434 Open(name string) (File, error)
435 }
436
437 type File interface {
438 Stat() (FileInfo, error)
439 Read([]byte) (int, error)
440 Close() error
441 }
442
443 var (
444 ErrInvalid = errInvalid() // "invalid argument"
445 ErrPermission = errPermission() // "permission denied"
446 ErrExist = errExist() // "file already exists"
447 ErrNotExist = errNotExist() // "file does not exist"
448 ErrClosed = errClosed() // "file already closed"
449 )
450 ` + "`" + `|
451 }
452 `,
453 },
454
455 {
456
457 name: "block_string_uneven_indent_2",
458 in: `
459 parent: {
460 example_code: |` + "`" + `go
461 package fs
462
463 type FS interface {
464 Open(name string) (File, error)
465 }
466
467 ` + "`" + `|}`,
468 exp: `parent: {
469 example_code: |` + "`" + `go
470 package fs
471
472 type FS interface {
473 Open(name string) (File, error)
474 }
475
476 ` + "`" + `|
477 }
478 `,
479 },
480
481 {
482 name: "block_comment_indent",
483 in: `
484 parent: {
485 """
486 hello
487 """ }`,
488 exp: `parent: {
489 """
490 hello
491 """
492 }
493 `,
494 },
495
496 {
497 name: "scalars",
498 in: `x: null
499 y: true
500 z: 343`,
501 exp: `x: null
502 y: true
503 z: 343
504 `,
505 },
506
507 {
508 name: "substitution",
509 in: `x: ${ok}; y: [...${yes}]`,
510 exp: `x: ${ok}; y: [...${yes}]
511 `,
512 },
513
514 {
515 name: "line_comment_block",
516 in: `# wsup
517 # hello
518 # The Least Successful Collector`,
519 exp: `# wsup
520 # hello
521 # The Least Successful Collector
522 `,
523 },
524
525 {
526 name: "inline_comment",
527 in: `hello: x # soldier
528 more`,
529 exp: `hello: x # soldier
530 more
531 `,
532 },
533
534 {
535 name: "array_one_line",
536 in: `a: [1;2;3;4]`,
537 exp: `a: [1; 2; 3; 4]
538 `,
539 },
540 {
541 name: "array",
542 in: `a: [
543 hi # Fraud is the homage that force pays to reason.
544 1
545 2
546
547 3
548 4
549 5; 6; 7
550 ]`,
551 exp: `a: [
552 hi # Fraud is the homage that force pays to reason.
553 1
554 2
555
556 3
557 4
558 5
559 6
560 7
561 ]
562 `,
563 },
564
565 {
566 name: "ampersand",
567 in: `&scenario: red`,
568 exp: `&scenario: red
569 `,
570 },
571
572 {
573 name: "complex_edge",
574 in: `pre.(src -> dst -> more)[3].post`,
575 exp: `pre.(src -> dst -> more)[3].post
576 `,
577 },
578 {
579 name: "edge_index_glob",
580 in: `(x -> y)[*]`,
581 exp: `(x -> y)[*]
582 `,
583 },
584 {
585 name: "bidirectional",
586 in: `x<>y`,
587 exp: `x <-> y
588 `,
589 },
590 {
591 name: "empty_map",
592 in: `x: {}
593 `,
594 exp: `x
595 `,
596 },
597 {
598 name: "leading_space_comments",
599 in: `# foo
600 # foobar
601 # baz
602 x -> y
603 #foo
604 y
605 `,
606 exp: `# foo
607 # foobar
608 # baz
609 x -> y
610 # foo
611 y
612 `,
613 },
614 {
615 name: "less_than_edge#955",
616 in: `
617 x <= y
618 `,
619 exp: `x <- = y
620 `,
621 },
622 {
623 name: "import/1",
624 in: `
625 x: @file.d2
626 `,
627 exp: `x: @file
628 `,
629 },
630 {
631 name: "import/2",
632 in: `
633 x: @file."d2"
634 `,
635 exp: `x: @file."d2"
636 `,
637 },
638 {
639 name: "import/3",
640 in: `
641 x: @./file
642 `,
643 exp: `x: @file
644 `,
645 },
646 {
647 name: "import/4",
648 in: `
649 x: @../file
650 `,
651 exp: `x: @../file
652 `,
653 },
654 {
655 name: "import/4",
656 in: `
657 x: @"x/../file"
658 `,
659 exp: `x: @file
660 `,
661 },
662 {
663 name: "layers_scenarios_steps_bottom_simple",
664 in: `layers: {
665 b: {
666 e
667 scenarios: {
668 p: {
669 x
670 }
671 }
672 }
673 steps: {
674 a
675 }
676 }
677 `,
678 exp: `layers: {
679 b: {
680 e
681
682 scenarios: {
683 p: {
684 x
685 }
686 }
687 }
688
689 steps: {
690 a
691 }
692 }
693 `,
694 },
695 {
696 name: "layers_scenarios_steps_bottom_complex",
697 in: `a
698
699 scenarios: {
700
701
702
703 scenario-1: {
704 steps: {
705 step-1: {
706 Test
707 }
708 step-2
709 }
710 non-step
711 }
712 }
713
714 layers: {
715 Test super nested: {
716 base-layer
717 layers: {
718 layers: {
719 grand-child-layer: {
720 grand-child-board
721 }
722 }
723 layer-board
724 }
725 last-layer
726 }
727 }
728 b
729 steps: {
730 1: {
731 step-1-content
732 }
733 }
734
735
736 scenarios: {
737 scenario-2: {
738 scenario-2-content
739 }
740 }
741
742 c
743 d
744
745 only-layers: {
746 layers: {
747 X
748 Y
749 }
750 layers: {
751 Z
752 }
753 }
754 `,
755 exp: `a
756 b
757
758 c
759 d
760
761 only-layers: {
762 layers: {
763 X
764 Y
765 }
766
767 layers: {
768 Z
769 }
770 }
771
772 layers: {
773 Test super nested: {
774 base-layer
775 last-layer
776
777 layers: {
778 layer-board
779
780 layers: {
781 grand-child-layer: {
782 grand-child-board
783 }
784 }
785 }
786 }
787 }
788
789 scenarios: {
790 scenario-1: {
791 non-step
792
793 steps: {
794 step-1: {
795 Test
796 }
797 step-2
798 }
799 }
800 }
801
802 scenarios: {
803 scenario-2: {
804 scenario-2-content
805 }
806 }
807
808 steps: {
809 1: {
810 step-1-content
811 }
812 }
813 `,
814 },
815 {
816 name: "substitution_mid_string",
817 in: `vars: {
818 test: hello
819 }
820
821 mybox: {
822 label: prefix${test}suffix
823 }
824 `,
825 exp: `vars: {
826 test: hello
827 }
828
829 mybox: {
830 label: prefix${test}suffix
831 }
832 `,
833 },
834 }
835
836 for _, tc := range testCases {
837 tc := tc
838 t.Run(tc.name, func(t *testing.T) {
839 t.Parallel()
840
841 ast, err := d2parser.Parse(fmt.Sprintf("%s.d2", t.Name()), strings.NewReader(tc.in), nil)
842 if err != nil {
843 t.Fatal(err)
844 }
845 assert.String(t, tc.exp, d2format.Format(ast))
846 })
847 }
848 }
849
850 func TestEdge(t *testing.T) {
851 t.Parallel()
852
853 mk, err := d2parser.ParseMapKey(`(x -> y)[0]`)
854 if err != nil {
855 t.Fatal(err)
856 }
857 if len(mk.Edges) != 1 {
858 t.Fatalf("expected one edge: %#v", mk.Edges)
859 }
860
861 assert.String(t, `x -> y`, d2format.Format(mk.Edges[0]))
862 assert.String(t, `[0]`, d2format.Format(mk.EdgeIndex))
863 }
864
View as plain text