diff --git a/.gitignore b/.gitignore index 057dc5f..da74d5b 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,7 @@ root4/ root5/ root6/ root7/ +root8/ root_a/ root_b/ root_c/ @@ -20,4 +21,5 @@ root_f/ root_g/ root_h/ root_i/ +root_j/ Primate/ diff --git a/Makefile b/Makefile index f5fc5bd..4a1c1f8 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ sweep: - rm -rf ./root/ ./root1/ ./root2/ ./root3/ ./root4/ ./root5/ ./root6/ ./root7/ Primate/ gtree/ - rm -rf ./root_a/ ./root_b/ ./root_c/ ./root_d/ ./root_e/ ./root_f/ ./root_g/ ./root_h/ ./root_i/ + rm -rf ./root/ ./root1/ ./root2/ ./root3/ ./root4/ ./root5/ ./root6/ ./root7/ ./root8/ Primate/ gtree/ + rm -rf ./root_a/ ./root_b/ ./root_c/ ./root_d/ ./root_e/ ./root_f/ ./root_g/ ./root_h/ ./root_i/ ./root_j/ fmt: sweep go fmt ./... diff --git a/README.md b/README.md index e379c3c..62b50ff 100644 --- a/README.md +++ b/README.md @@ -158,6 +158,7 @@ OPTIONS: --file value, -f value specify the path to markdown file. (default: stdin) --two-spaces, --ts set this option when the markdown indent is 2 spaces. (default: tab spaces) --four-spaces, --fs set this option when the markdown indent is 4 spaces. (default: tab spaces) + --massive, -m set this option when there are very many blocks of markdown. (default: false) --json, -j set this option when outputting JSON. (default: tree) --yaml, -y set this option when outputting YAML. (default: tree) --toml, -t set this option when outputting TOML. (default: tree) @@ -400,6 +401,7 @@ OPTIONS: --file value, -f value specify the path to markdown file. (default: stdin) --two-spaces, --ts set this option when the markdown indent is 2 spaces. (default: tab spaces) --four-spaces, --fs set this option when the markdown indent is 4 spaces. (default: tab spaces) + --massive, -m set this option when there are very many blocks of markdown. (default: false) --dry-run, -d, --dr dry run. detects node that is invalid for directory generation. the order of the output and made directories does not always match. (default: false) --extension value, -e value, --ext value [ --extension value, -e value, --ext value ] set this option if you want to create file instead of directory. @@ -951,27 +953,31 @@ func main() { # Process +> **Note**
+> This process is for the Massive Roots mode. -## e.g. [*gtree/tree_handler.go*](https://github.com/ddddddO/gtree/blob/master/tree_handler.go) +## e.g. [*gtree/pipeline_tree.go*](https://github.com/ddddddO/gtree/blob/master/pipeline_tree.go) # Performance - > **Warning**
> Depends on the environment. -- Comparison before and after software architecture was changed. -- In the case of few Roots, previous architecture is faster in execution😅 -- However, for multiple Roots, execution speed tends to be faster💪✨ +- Comparison simple implementation and pipeline implementation. +- In the case of few Roots, simple implementation is faster in execution! + - Use this one by default. +- However, for multiple Roots, pipeline implementation execution speed tends to be faster💪✨ + - In the CLI, it is available by specifying `--massive`. + - In the Go program, it is available by specifying `WithMassive` func.
Benchmark log -## Before pipelining +## Simple implementation ```console $ go test -benchmem -bench Benchmark -benchtime 100x tree_handler_benchmark_test.go goos: linux @@ -991,7 +997,7 @@ PASS ok command-line-arguments 68.124s ``` -## After pipelining +## Pipeline implementation ```console $ go test -benchmem -bench Benchmark -benchtime 100x tree_handler_benchmark_test.go goos: linux diff --git a/cmd/gtree/main.go b/cmd/gtree/main.go index a651037..b44b0aa 100644 --- a/cmd/gtree/main.go +++ b/cmd/gtree/main.go @@ -37,6 +37,11 @@ func main() { Usage: "set this option when the markdown indent is 4 spaces.", DefaultText: "tab spaces", }, + &cli.BoolFlag{ + Name: "massive", + Aliases: []string{"m"}, + Usage: "set this option when there are very many blocks of markdown.", + }, } outputFlags := []cli.Flag{ diff --git a/config.go b/config.go index 4ab6b82..b53d628 100644 --- a/config.go +++ b/config.go @@ -5,6 +5,7 @@ type config struct { intermedialNodeFormat branchFormat space spaceType + massive bool encode encode dryrun bool fileExtensions []string @@ -20,8 +21,9 @@ func newConfig(options []Option) (*config, error) { directly: "├──", indirectly: "│ ", }, - space: spacesTab, - encode: encodeDefault, + space: spacesTab, + massive: false, + encode: encodeDefault, } for _, opt := range options { if opt == nil { @@ -72,6 +74,14 @@ func WithBranchFormatLastNode(directly, indirectly string) Option { } } +// WithMassive returns function for large amount roots. +func WithMassive() Option { + return func(c *config) error { + c.massive = true + return nil + } +} + // WithEncodeJSON returns function for output json format. func WithEncodeJSON() Option { return func(c *config) error { diff --git a/pipeline_tree.go b/pipeline_tree.go new file mode 100644 index 0000000..b888d23 --- /dev/null +++ b/pipeline_tree.go @@ -0,0 +1,158 @@ +//go:build !wasm + +package gtree + +import ( + "context" + "io" + + "github.com/fatih/color" + "golang.org/x/sync/errgroup" +) + +type treePipeline struct { + grower growerPipeline + spreader spreaderPipeline + mkdirer mkdirerPipeline +} + +func newTreePipeline(conf *config) *treePipeline { + growerFactory := func(lastNodeFormat, intermedialNodeFormat branchFormat, dryrun bool, encode encode) growerPipeline { + if encode != encodeDefault { + return newNopGrowerPipeline() + } + return newGrowerPipeline(lastNodeFormat, intermedialNodeFormat, dryrun) + } + + spreaderFactory := func(encode encode, dryrun bool, fileExtensions []string) spreaderPipeline { + if dryrun { + return newColorizeSpreaderPipeline(fileExtensions) + } + return newSpreaderPipeline(encode) + } + + mkdirerFactory := func(fileExtensions []string) mkdirerPipeline { + return newMkdirerPipeline(fileExtensions) + } + + return &treePipeline{ + grower: growerFactory( + conf.lastNodeFormat, + conf.intermedialNodeFormat, + conf.dryrun, + conf.encode, + ), + spreader: spreaderFactory( + conf.encode, + conf.dryrun, + conf.fileExtensions, + ), + mkdirer: mkdirerFactory( + conf.fileExtensions, + ), + } +} + +func (t *treePipeline) output(w io.Writer, r io.Reader, conf *config) error { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + splitStream, errcsl := split(ctx, r) + rootStream, errcr := newRootGeneratorPipeline(conf.space).generate(ctx, splitStream) + growStream, errcg := t.grow(ctx, rootStream) + errcs := t.spread(ctx, w, growStream) + return t.handlePipelineErr(errcsl, errcr, errcg, errcs) +} + +func (t *treePipeline) outputProgrammably(w io.Writer, root *Node, conf *config) error { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + rootStream := make(chan *Node) + go func() { + defer close(rootStream) + rootStream <- root + }() + growStream, errcg := t.grow(ctx, rootStream) + errcs := t.spread(ctx, w, growStream) + return t.handlePipelineErr(errcg, errcs) +} + +func (t *treePipeline) makedir(r io.Reader, conf *config) error { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + splitStream, errcsl := split(ctx, r) + rootStream, errcr := newRootGeneratorPipeline(conf.space).generate(ctx, splitStream) + growStream, errcg := t.grow(ctx, rootStream) + errcm := t.mkdir(ctx, growStream) + return t.handlePipelineErr(errcsl, errcr, errcg, errcm) +} + +func (t *treePipeline) makedirProgrammably(root *Node, conf *config) error { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + rootStream := make(chan *Node) + go func() { + defer close(rootStream) + rootStream <- root + }() + t.enableValidation() + // when detect invalid node name, return error. process end. + growStream, errcg := t.grow(ctx, rootStream) + if conf.dryrun { + // when detected no invalid node name, output tree. + errcs := t.spread(ctx, color.Output, growStream) + return t.handlePipelineErr(errcg, errcs) + } + // when detected no invalid node name, no output tree. + errcm := t.mkdir(ctx, growStream) + return t.handlePipelineErr(errcg, errcm) +} + +// 関心事は各ノードの枝の形成 +type growerPipeline interface { + grow(context.Context, <-chan *Node) (<-chan *Node, <-chan error) + enableValidation() +} + +// 関心事はtreeの出力 +type spreaderPipeline interface { + spread(context.Context, io.Writer, <-chan *Node) <-chan error +} + +// 関心事はファイルの生成 +// interfaceを使う必要はないが、growerPipeline/spreaderPipelineと合わせたいため +type mkdirerPipeline interface { + mkdir(context.Context, <-chan *Node) <-chan error +} + +func (t *treePipeline) grow(ctx context.Context, roots <-chan *Node) (<-chan *Node, <-chan error) { + return t.grower.grow(ctx, roots) +} + +func (t *treePipeline) spread(ctx context.Context, w io.Writer, roots <-chan *Node) <-chan error { + return t.spreader.spread(ctx, w, roots) +} + +func (t *treePipeline) mkdir(ctx context.Context, roots <-chan *Node) <-chan error { + return t.mkdirer.mkdir(ctx, roots) +} + +// パイプラインの全ステージで最初のエラーを返却 +func (*treePipeline) handlePipelineErr(echs ...<-chan error) error { + eg, _ := errgroup.WithContext(context.TODO()) + for i := range echs { + i := i + eg.Go(func() error { + for e := range echs[i] { + if e != nil { + return e + } + } + return nil + }) + } + return eg.Wait() +} diff --git a/tree_grower.go b/pipeline_tree_grower.go similarity index 70% rename from tree_grower.go rename to pipeline_tree_grower.go index 7f7efac..930308f 100644 --- a/tree_grower.go +++ b/pipeline_tree_grower.go @@ -7,26 +7,22 @@ import ( "sync" ) -func newGrower( +func newGrowerPipeline( lastNodeFormat, intermedialNodeFormat branchFormat, enabledValidation bool, -) grower { - return &defaultGrower{ +) growerPipeline { + return &defaultGrowerPipeline{ lastNodeFormat: lastNodeFormat, intermedialNodeFormat: intermedialNodeFormat, enabledValidation: enabledValidation, } } -func newNopGrower() grower { - return &nopGrower{} +func newNopGrowerPipeline() growerPipeline { + return &nopGrowerPipeline{} } -type branchFormat struct { - directly, indirectly string -} - -type defaultGrower struct { +type defaultGrowerPipeline struct { lastNodeFormat branchFormat intermedialNodeFormat branchFormat enabledValidation bool @@ -34,7 +30,7 @@ type defaultGrower struct { const workerGrowNum = 10 -func (dg *defaultGrower) grow(ctx context.Context, roots <-chan *Node) (<-chan *Node, <-chan error) { +func (dg *defaultGrowerPipeline) grow(ctx context.Context, roots <-chan *Node) (<-chan *Node, <-chan error) { nodes := make(chan *Node) errc := make(chan error, 1) @@ -55,7 +51,7 @@ func (dg *defaultGrower) grow(ctx context.Context, roots <-chan *Node) (<-chan * return nodes, errc } -func (dg *defaultGrower) worker(ctx context.Context, wg *sync.WaitGroup, roots <-chan *Node, nodes chan<- *Node, errc chan<- error) { +func (dg *defaultGrowerPipeline) worker(ctx context.Context, wg *sync.WaitGroup, roots <-chan *Node, nodes chan<- *Node, errc chan<- error) { defer wg.Done() for { select { @@ -74,7 +70,7 @@ func (dg *defaultGrower) worker(ctx context.Context, wg *sync.WaitGroup, roots < } } -func (dg *defaultGrower) assemble(current *Node) error { +func (dg *defaultGrowerPipeline) assemble(current *Node) error { if err := dg.assembleBranch(current); err != nil { return err } @@ -87,7 +83,7 @@ func (dg *defaultGrower) assemble(current *Node) error { return nil } -func (dg *defaultGrower) assembleBranch(current *Node) error { +func (dg *defaultGrowerPipeline) assembleBranch(current *Node) error { current.clean() // 例えば、MkdirProgrammably funcでrootノードを使いまわすと、前回func実行時に形成されたノードの枝が残ったまま追記されてしまうため。 dg.assembleBranchDirectly(current) @@ -108,7 +104,7 @@ func (dg *defaultGrower) assembleBranch(current *Node) error { return nil } -func (dg *defaultGrower) assembleBranchDirectly(current *Node) { +func (dg *defaultGrowerPipeline) assembleBranchDirectly(current *Node) { if current == nil || current.isRoot() { return } @@ -122,7 +118,7 @@ func (dg *defaultGrower) assembleBranchDirectly(current *Node) { } } -func (dg *defaultGrower) assembleBranchIndirectly(current, parent *Node) { +func (dg *defaultGrowerPipeline) assembleBranchIndirectly(current, parent *Node) { if current == nil || parent == nil || current.isRoot() { return } @@ -136,7 +132,7 @@ func (dg *defaultGrower) assembleBranchIndirectly(current, parent *Node) { } } -func (*defaultGrower) assembleBranchFinally(current, root *Node) { +func (*defaultGrowerPipeline) assembleBranchFinally(current, root *Node) { if current == nil { return } @@ -146,13 +142,13 @@ func (*defaultGrower) assembleBranchFinally(current, root *Node) { } } -func (dg *defaultGrower) enableValidation() { +func (dg *defaultGrowerPipeline) enableValidation() { dg.enabledValidation = true } -type nopGrower struct{} +type nopGrowerPipeline struct{} -func (*nopGrower) grow(ctx context.Context, roots <-chan *Node) (<-chan *Node, <-chan error) { +func (*nopGrowerPipeline) grow(ctx context.Context, roots <-chan *Node) (<-chan *Node, <-chan error) { nodes := make(chan *Node) errc := make(chan error, 1) @@ -179,9 +175,9 @@ func (*nopGrower) grow(ctx context.Context, roots <-chan *Node) (<-chan *Node, < return nodes, errc } -func (*nopGrower) enableValidation() {} +func (*nopGrowerPipeline) enableValidation() {} var ( - _ grower = (*defaultGrower)(nil) - _ grower = (*nopGrower)(nil) + _ growerPipeline = (*defaultGrowerPipeline)(nil) + _ growerPipeline = (*nopGrowerPipeline)(nil) ) diff --git a/tree_mkdirer.go b/pipeline_tree_mkdirer.go similarity index 66% rename from tree_mkdirer.go rename to pipeline_tree_mkdirer.go index 6108a8d..fc26c16 100644 --- a/tree_mkdirer.go +++ b/pipeline_tree_mkdirer.go @@ -9,19 +9,19 @@ import ( "sync" ) -func newMkdirer(fileExtensions []string) mkdirer { - return &defaultMkdirer{ +func newMkdirerPipeline(fileExtensions []string) mkdirerPipeline { + return &defaultMkdirerPipeline{ fileConsiderer: newFileConsiderer(fileExtensions), } } -type defaultMkdirer struct { +type defaultMkdirerPipeline struct { fileConsiderer *fileConsiderer } const workerMkdirNum = 10 -func (dm *defaultMkdirer) mkdir(ctx context.Context, roots <-chan *Node) <-chan error { +func (dm *defaultMkdirerPipeline) mkdir(ctx context.Context, roots <-chan *Node) <-chan error { errc := make(chan error, 1) go func() { @@ -38,7 +38,7 @@ func (dm *defaultMkdirer) mkdir(ctx context.Context, roots <-chan *Node) <-chan return errc } -func (dm *defaultMkdirer) worker(ctx context.Context, wg *sync.WaitGroup, roots <-chan *Node, errc chan<- error) { +func (dm *defaultMkdirerPipeline) worker(ctx context.Context, wg *sync.WaitGroup, roots <-chan *Node, errc chan<- error) { defer wg.Done() for { select { @@ -60,14 +60,14 @@ func (dm *defaultMkdirer) worker(ctx context.Context, wg *sync.WaitGroup, roots } } -func (*defaultMkdirer) isExistRoot(root *Node) bool { +func (*defaultMkdirerPipeline) isExistRoot(root *Node) bool { if _, err := os.Stat(root.path()); !os.IsNotExist(err) { return true } return false } -func (dm *defaultMkdirer) makeDirectoriesAndFiles(current *Node) error { +func (dm *defaultMkdirerPipeline) makeDirectoriesAndFiles(current *Node) error { if dm.fileConsiderer.isFile(current) { dir := strings.TrimSuffix(current.path(), current.name) if err := dm.mkdirAll(dir); err != nil { @@ -90,11 +90,11 @@ func (dm *defaultMkdirer) makeDirectoriesAndFiles(current *Node) error { const permission = 0o755 -func (*defaultMkdirer) mkdirAll(dir string) error { +func (*defaultMkdirerPipeline) mkdirAll(dir string) error { return os.MkdirAll(dir, permission) } -func (*defaultMkdirer) mkfile(path string) error { +func (*defaultMkdirerPipeline) mkfile(path string) error { f, err := os.Create(path) if err != nil { return err @@ -102,4 +102,4 @@ func (*defaultMkdirer) mkfile(path string) error { return f.Close() } -var _ mkdirer = (*defaultMkdirer)(nil) +var _ mkdirerPipeline = (*defaultMkdirerPipeline)(nil) diff --git a/tree_spreader.go b/pipeline_tree_spreader.go similarity index 52% rename from tree_spreader.go rename to pipeline_tree_spreader.go index b549639..df7db94 100644 --- a/tree_spreader.go +++ b/pipeline_tree_spreader.go @@ -15,21 +15,21 @@ import ( "gopkg.in/yaml.v3" ) -func newSpreader(encode encode) spreader { +func newSpreaderPipeline(encode encode) spreaderPipeline { switch encode { case encodeJSON: - return &jsonSpreader{} + return &jsonSpreaderPipeline{} case encodeYAML: - return &yamlSpreader{} + return &yamlSpreaderPipeline{} case encodeTOML: - return &tomlSpreader{} + return &tomlSpreaderPipeline{} default: - return &defaultSpreader{} + return &defaultSpreaderPipeline{} } } -func newColorizeSpreader(fileExtensions []string) spreader { - return &colorizeSpreader{ +func newColorizeSpreaderPipeline(fileExtensions []string) spreaderPipeline { + return &colorizeSpreaderPipeline{ fileConsiderer: newFileConsiderer(fileExtensions), fileColor: color.New(color.Bold, color.FgHiCyan), fileCounter: newCounter(), @@ -39,22 +39,13 @@ func newColorizeSpreader(fileExtensions []string) spreader { } } -type encode int - -const ( - encodeDefault encode = iota - encodeJSON - encodeYAML - encodeTOML -) - -type defaultSpreader struct { +type defaultSpreaderPipeline struct { sync.Mutex } const workerSpreadNum = 10 -func (ds *defaultSpreader) spread(ctx context.Context, w io.Writer, roots <-chan *Node) <-chan error { +func (ds *defaultSpreaderPipeline) spread(ctx context.Context, w io.Writer, roots <-chan *Node) <-chan error { errc := make(chan error, 1) go func() { @@ -76,7 +67,7 @@ func (ds *defaultSpreader) spread(ctx context.Context, w io.Writer, roots <-chan return errc } -func (ds *defaultSpreader) worker(ctx context.Context, wg *sync.WaitGroup, bw *bufio.Writer, roots <-chan *Node, errc chan<- error) { +func (ds *defaultSpreaderPipeline) worker(ctx context.Context, wg *sync.WaitGroup, bw *bufio.Writer, roots <-chan *Node, errc chan<- error) { defer wg.Done() for { select { @@ -97,19 +88,19 @@ func (ds *defaultSpreader) worker(ctx context.Context, wg *sync.WaitGroup, bw *b } } -func (*defaultSpreader) spreadBranch(current *Node) string { +func (*defaultSpreaderPipeline) spreadBranch(current *Node) string { ret := current.name + "\n" if !current.isRoot() { ret = current.branch() + " " + current.name + "\n" } for _, child := range current.children { - ret += (*defaultSpreader)(nil).spreadBranch(child) + ret += (*defaultSpreaderPipeline)(nil).spreadBranch(child) } return ret } -type colorizeSpreader struct { +type colorizeSpreaderPipeline struct { fileConsiderer *fileConsiderer fileColor *color.Color fileCounter *counter @@ -118,7 +109,7 @@ type colorizeSpreader struct { dirCounter *counter } -func (cs *colorizeSpreader) spread(ctx context.Context, w io.Writer, roots <-chan *Node) <-chan error { +func (cs *colorizeSpreaderPipeline) spread(ctx context.Context, w io.Writer, roots <-chan *Node) <-chan error { errc := make(chan error, 1) go func() { @@ -157,7 +148,7 @@ func (cs *colorizeSpreader) spread(ctx context.Context, w io.Writer, roots <-cha return errc } -func (cs *colorizeSpreader) spreadBranch(current *Node) string { +func (cs *colorizeSpreaderPipeline) spreadBranch(current *Node) string { ret := "" if current.isRoot() { ret = cs.colorize(current) + "\n" @@ -171,7 +162,7 @@ func (cs *colorizeSpreader) spreadBranch(current *Node) string { return ret } -func (cs *colorizeSpreader) colorize(current *Node) string { +func (cs *colorizeSpreaderPipeline) colorize(current *Node) string { if cs.fileConsiderer.isFile(current) { _ = cs.fileCounter.next() return cs.fileColor.Sprint(current.name) @@ -181,7 +172,7 @@ func (cs *colorizeSpreader) colorize(current *Node) string { } } -func (cs *colorizeSpreader) summary() string { +func (cs *colorizeSpreaderPipeline) summary() string { return fmt.Sprintf( "%d directories, %d files", cs.dirCounter.current(), @@ -189,9 +180,9 @@ func (cs *colorizeSpreader) summary() string { ) } -type jsonSpreader struct{} +type jsonSpreaderPipeline struct{} -func (*jsonSpreader) spread(ctx context.Context, w io.Writer, roots <-chan *Node) <-chan error { +func (*jsonSpreaderPipeline) spread(ctx context.Context, w io.Writer, roots <-chan *Node) <-chan error { errc := make(chan error, 1) go func() { @@ -218,31 +209,9 @@ func (*jsonSpreader) spread(ctx context.Context, w io.Writer, roots <-chan *Node return errc } -type jsonNode struct { - Name string `json:"value"` - Children []*jsonNode `json:"children"` -} - -func (parent *Node) toJSONNode(jParent *jsonNode) *jsonNode { - if jParent == nil { - jParent = &jsonNode{Name: parent.name} - } - if !parent.hasChild() { - return jParent - } +type tomlSpreaderPipeline struct{} - jParent.Children = make([]*jsonNode, len(parent.children)) - for i := range parent.children { - jParent.Children[i] = &jsonNode{Name: parent.children[i].name} - _ = parent.children[i].toJSONNode(jParent.Children[i]) - } - - return jParent -} - -type tomlSpreader struct{} - -func (*tomlSpreader) spread(ctx context.Context, w io.Writer, roots <-chan *Node) <-chan error { +func (*tomlSpreaderPipeline) spread(ctx context.Context, w io.Writer, roots <-chan *Node) <-chan error { errc := make(chan error, 1) go func() { @@ -269,31 +238,9 @@ func (*tomlSpreader) spread(ctx context.Context, w io.Writer, roots <-chan *Node return errc } -type tomlNode struct { - Name string `toml:"value"` - Children []*tomlNode `toml:"children"` -} - -func (parent *Node) toTOMLNode(tParent *tomlNode) *tomlNode { - if tParent == nil { - tParent = &tomlNode{Name: parent.name} - } - if !parent.hasChild() { - return tParent - } - - tParent.Children = make([]*tomlNode, len(parent.children)) - for i := range parent.children { - tParent.Children[i] = &tomlNode{Name: parent.children[i].name} - _ = parent.children[i].toTOMLNode(tParent.Children[i]) - } - - return tParent -} - -type yamlSpreader struct{} +type yamlSpreaderPipeline struct{} -func (*yamlSpreader) spread(ctx context.Context, w io.Writer, roots <-chan *Node) <-chan error { +func (*yamlSpreaderPipeline) spread(ctx context.Context, w io.Writer, roots <-chan *Node) <-chan error { errc := make(chan error, 1) go func() { @@ -320,32 +267,10 @@ func (*yamlSpreader) spread(ctx context.Context, w io.Writer, roots <-chan *Node return errc } -type yamlNode struct { - Name string `yaml:"value"` - Children []*yamlNode `yaml:"children"` -} - -func (parent *Node) toYAMLNode(yParent *yamlNode) *yamlNode { - if yParent == nil { - yParent = &yamlNode{Name: parent.name} - } - if !parent.hasChild() { - return yParent - } - - yParent.Children = make([]*yamlNode, len(parent.children)) - for i := range parent.children { - yParent.Children[i] = &yamlNode{Name: parent.children[i].name} - _ = parent.children[i].toYAMLNode(yParent.Children[i]) - } - - return yParent -} - var ( - _ spreader = (*defaultSpreader)(nil) - _ spreader = (*colorizeSpreader)(nil) - _ spreader = (*jsonSpreader)(nil) - _ spreader = (*yamlSpreader)(nil) - _ spreader = (*tomlSpreader)(nil) + _ spreaderPipeline = (*defaultSpreaderPipeline)(nil) + _ spreaderPipeline = (*colorizeSpreaderPipeline)(nil) + _ spreaderPipeline = (*jsonSpreaderPipeline)(nil) + _ spreaderPipeline = (*yamlSpreaderPipeline)(nil) + _ spreaderPipeline = (*tomlSpreaderPipeline)(nil) ) diff --git a/root_generator.go b/root_generator.go index 3541e3a..5ea2e25 100644 --- a/root_generator.go +++ b/root_generator.go @@ -5,23 +5,71 @@ package gtree import ( "bufio" "context" + "io" "strings" "sync" ) -type rootGenerator struct { +type rootGeneratorSimple struct { + counter *counter + scanner *bufio.Scanner nodeGenerator *nodeGenerator } -func newRootGenerator(st spaceType) *rootGenerator { - return &rootGenerator{ +func newRootGeneratorSimple(r io.Reader, st spaceType) *rootGeneratorSimple { + return &rootGeneratorSimple{ + counter: newCounter(), + scanner: bufio.NewScanner(r), + nodeGenerator: newNodeGenerator(st), + } +} + +func (rg *rootGeneratorSimple) generate() ([]*Node, error) { + var ( + stack *stack + roots []*Node + ) + + for rg.scanner.Scan() { + currentNode, err := rg.nodeGenerator.generate(rg.scanner.Text(), rg.counter.next()) + if err != nil { + return nil, err + } + if currentNode == nil { + continue + } + + if currentNode.isRoot() { + rg.counter.reset() + roots = append(roots, currentNode) + stack = newStack() + stack.push(currentNode) + continue + } + + if stack == nil { + return nil, errNilStack + } + + stack.dfs(currentNode) + } + + return roots, rg.scanner.Err() +} + +type rootGeneratorPipeline struct { + nodeGenerator *nodeGenerator +} + +func newRootGeneratorPipeline(st spaceType) *rootGeneratorPipeline { + return &rootGeneratorPipeline{ nodeGenerator: newNodeGenerator(st), } } const workerGenerateNum = 10 -func (rg *rootGenerator) generate(ctx context.Context, blocks <-chan string) (<-chan *Node, <-chan error) { +func (rg *rootGeneratorPipeline) generate(ctx context.Context, blocks <-chan string) (<-chan *Node, <-chan error) { rootc := make(chan *Node) errc := make(chan error, 1) @@ -42,7 +90,7 @@ func (rg *rootGenerator) generate(ctx context.Context, blocks <-chan string) (<- return rootc, errc } -func (rg *rootGenerator) worker(ctx context.Context, wg *sync.WaitGroup, blocks <-chan string, rootc chan<- *Node, errc chan<- error) { +func (rg *rootGeneratorPipeline) worker(ctx context.Context, wg *sync.WaitGroup, blocks <-chan string, rootc chan<- *Node, errc chan<- error) { defer wg.Done() for { select { diff --git a/simple_tree.go b/simple_tree.go new file mode 100644 index 0000000..e8b5bf6 --- /dev/null +++ b/simple_tree.go @@ -0,0 +1,128 @@ +//go:build !wasm + +package gtree + +import ( + "io" + + "github.com/fatih/color" +) + +type treeSimple struct { + grower growerSimple + spreader spreaderSimple + mkdirer mkdirerSimple +} + +func newTreeSimple(conf *config) *treeSimple { + growerFactory := func(lastNodeFormat, intermedialNodeFormat branchFormat, dryrun bool, encode encode) growerSimple { + if encode != encodeDefault { + return newNopGrowerSimple() + } + return newGrowerSimple(lastNodeFormat, intermedialNodeFormat, dryrun) + } + + spreaderFactory := func(encode encode, dryrun bool, fileExtensions []string) spreaderSimple { + if dryrun { + return newColorizeSpreaderSimple(fileExtensions) + } + return newSpreaderSimple(encode) + } + + mkdirerFactory := func(fileExtensions []string) mkdirerSimple { + return newMkdirerSimple(fileExtensions) + } + + return &treeSimple{ + grower: growerFactory( + conf.lastNodeFormat, + conf.intermedialNodeFormat, + conf.dryrun, + conf.encode, + ), + spreader: spreaderFactory( + conf.encode, + conf.dryrun, + conf.fileExtensions, + ), + mkdirer: mkdirerFactory( + conf.fileExtensions, + ), + } +} + +func (t *treeSimple) output(w io.Writer, r io.Reader, conf *config) error { + rg := newRootGeneratorSimple(r, conf.space) + roots, err := rg.generate() + if err != nil { + return err + } + + if err := t.grow(roots); err != nil { + return err + } + return t.spread(w, roots) +} + +func (t *treeSimple) outputProgrammably(w io.Writer, root *Node, conf *config) error { + if err := t.grow([]*Node{root}); err != nil { + return err + } + return t.spread(w, []*Node{root}) +} + +func (t *treeSimple) makedir(r io.Reader, conf *config) error { + rg := newRootGeneratorSimple(r, conf.space) + roots, err := rg.generate() + if err != nil { + return err + } + + if err := t.grow(roots); err != nil { + return err + } + return t.mkdir(roots) +} + +func (t *treeSimple) makedirProgrammably(root *Node, conf *config) error { + t.enableValidation() + // when detect invalid node name, return error. process end. + if err := t.grow([]*Node{root}); err != nil { + return err + } + if conf.dryrun { + // when detected no invalid node name, output tree. + return t.spread(color.Output, []*Node{root}) + } + // when detected no invalid node name, no output tree. + return t.mkdir([]*Node{root}) +} + +// 関心事は各ノードの枝の形成 +type growerSimple interface { + grow([]*Node) error + enableValidation() +} + +// 関心事はtreeの出力 +type spreaderSimple interface { + spread(io.Writer, []*Node) error +} + +// 関心事はファイルの生成 +// interfaceを使う必要はないが、growerSimple/spreaderSimpleと合わせたいため +type mkdirerSimple interface { + mkdir([]*Node) error +} + +func (t *treeSimple) grow(roots []*Node) error { + return t.grower.grow(roots) +} + +func (t *treeSimple) spread(w io.Writer, roots []*Node) error { + return t.spreader.spread(w, roots) +} + +func (t *treeSimple) mkdir(roots []*Node) error { + return t.mkdirer.mkdir(roots) +} diff --git a/simple_tree_grower.go b/simple_tree_grower.go new file mode 100644 index 0000000..e9d7532 --- /dev/null +++ b/simple_tree_grower.go @@ -0,0 +1,130 @@ +//go:build !wasm + +package gtree + +func newGrowerSimple( + lastNodeFormat, intermedialNodeFormat branchFormat, + enabledValidation bool, +) growerSimple { + return &defaultGrowerSimple{ + lastNodeFormat: lastNodeFormat, + intermedialNodeFormat: intermedialNodeFormat, + enabledValidation: enabledValidation, + } +} + +func newNopGrowerSimple() growerSimple { + return &nopGrowerSimple{} +} + +type branchFormat struct { + directly, indirectly string +} + +type defaultGrowerSimple struct { + lastNodeFormat branchFormat + intermedialNodeFormat branchFormat + enabledValidation bool +} + +func (dg *defaultGrowerSimple) grow(roots []*Node) error { + for _, root := range roots { + if err := dg.assemble(root); err != nil { + return err + } + } + return nil +} + +func (dg *defaultGrowerSimple) assemble(current *Node) error { + if err := dg.assembleBranch(current); err != nil { + return err + } + + for _, child := range current.children { + if err := dg.assemble(child); err != nil { + return err + } + } + return nil +} + +func (dg *defaultGrowerSimple) assembleBranch(current *Node) error { + current.clean() // 例えば、MkdirProgrammably funcでrootノードを使いまわすと、前回func実行時に形成されたノードの枝が残ったまま追記されてしまうため。 + + dg.assembleBranchDirectly(current) + + // go back to the root to form a branch. + tmpParent := current.parent + if tmpParent != nil { + for ; !tmpParent.isRoot(); tmpParent = tmpParent.parent { + dg.assembleBranchIndirectly(current, tmpParent) + } + } + + dg.assembleBranchFinally(current, tmpParent) + + if dg.enabledValidation { + return current.validatePath() + } + return nil +} + +func (dg *defaultGrowerSimple) assembleBranchDirectly(current *Node) { + if current == nil || current.isRoot() { + return + } + + current.setPath(current.name) + + if current.isLastOfHierarchy() { + current.setBranch(current.branch(), dg.lastNodeFormat.directly) + } else { + current.setBranch(current.branch(), dg.intermedialNodeFormat.directly) + } +} + +func (dg *defaultGrowerSimple) assembleBranchIndirectly(current, parent *Node) { + if current == nil || parent == nil || current.isRoot() { + return + } + + current.setPath(parent.name, current.path()) + + if parent.isLastOfHierarchy() { + current.setBranch(dg.lastNodeFormat.indirectly, current.branch()) + } else { + current.setBranch(dg.intermedialNodeFormat.indirectly, current.branch()) + } +} + +func (*defaultGrowerSimple) assembleBranchFinally(current, root *Node) { + if current == nil { + return + } + + if root != nil { + current.setPath(root.path(), current.path()) + } + + if current.isRoot() { + current.setBranch(current.name, "\n") + } else { + current.setBranch(current.branch(), " ", current.name, "\n") + } +} + +func (dg *defaultGrowerSimple) enableValidation() { + dg.enabledValidation = true +} + +type nopGrowerSimple struct{} + +func (*nopGrowerSimple) grow(_ []*Node) error { return nil } + +func (*nopGrowerSimple) enableValidation() {} + +var ( + _ growerSimple = (*defaultGrowerSimple)(nil) + _ growerSimple = (*nopGrowerSimple)(nil) +) diff --git a/simple_tree_mkdirer.go b/simple_tree_mkdirer.go new file mode 100644 index 0000000..7a7dd3e --- /dev/null +++ b/simple_tree_mkdirer.go @@ -0,0 +1,75 @@ +//go:build !wasm + +package gtree + +import ( + "os" + "strings" +) + +func newMkdirerSimple(fileExtensions []string) mkdirerSimple { + return &defaultMkdirerSimple{ + fileConsiderer: newFileConsiderer(fileExtensions), + } +} + +type defaultMkdirerSimple struct { + fileConsiderer *fileConsiderer +} + +func (dm *defaultMkdirerSimple) mkdir(roots []*Node) error { + if dm.isExistRoot(roots) { + return ErrExistPath + } + + for _, root := range roots { + if err := dm.makeDirectoriesAndFiles(root); err != nil { + return err + } + } + return nil +} + +func (*defaultMkdirerSimple) isExistRoot(roots []*Node) bool { + for _, root := range roots { + if _, err := os.Stat(root.path()); !os.IsNotExist(err) { + return true + } + } + return false +} + +func (dm *defaultMkdirerSimple) makeDirectoriesAndFiles(current *Node) error { + if dm.fileConsiderer.isFile(current) { + dir := strings.TrimSuffix(current.path(), current.name) + if err := dm.mkdirAll(dir); err != nil { + return err + } + return dm.mkfile(current.path()) + } + + if !current.hasChild() { + return dm.mkdirAll(current.path()) + } + + for _, child := range current.children { + if err := dm.makeDirectoriesAndFiles(child); err != nil { + return err + } + } + return nil +} + +func (*defaultMkdirerSimple) mkdirAll(dir string) error { + return os.MkdirAll(dir, permission) +} + +func (*defaultMkdirerSimple) mkfile(path string) error { + f, err := os.Create(path) + if err != nil { + return err + } + return f.Close() +} + +var _ mkdirerSimple = (*defaultMkdirerSimple)(nil) diff --git a/simple_tree_spreader.go b/simple_tree_spreader.go new file mode 100644 index 0000000..064a1ab --- /dev/null +++ b/simple_tree_spreader.go @@ -0,0 +1,236 @@ +//go:build !wasm + +package gtree + +import ( + "bufio" + "encoding/json" + "fmt" + "io" + + "github.com/fatih/color" + toml "github.com/pelletier/go-toml/v2" + "gopkg.in/yaml.v3" +) + +func newSpreaderSimple(encode encode) spreaderSimple { + switch encode { + case encodeJSON: + return &jsonSpreaderSimple{} + case encodeYAML: + return &yamlSpreaderSimple{} + case encodeTOML: + return &tomlSpreaderSimple{} + default: + return &defaultSpreaderSimple{} + } +} + +func newColorizeSpreaderSimple(fileExtensions []string) spreaderSimple { + return &colorizeSpreaderSimple{ + defaultSpreaderSimple: &defaultSpreaderSimple{}, + + fileConsiderer: newFileConsiderer(fileExtensions), + fileColor: color.New(color.Bold, color.FgHiCyan), + fileCounter: newCounter(), + + dirColor: color.New(color.FgGreen), + dirCounter: newCounter(), + } +} + +type encode int + +const ( + encodeDefault encode = iota + encodeJSON + encodeYAML + encodeTOML +) + +type defaultSpreaderSimple struct{} + +func (ds *defaultSpreaderSimple) spread(w io.Writer, roots []*Node) error { + branches := "" + for _, root := range roots { + branches += ds.spreadBranch(root) + } + return ds.write(w, branches) +} + +func (*defaultSpreaderSimple) spreadBranch(current *Node) string { + ret := current.branch() + for _, child := range current.children { + ret += (*defaultSpreaderSimple)(nil).spreadBranch(child) + } + return ret +} + +func (*defaultSpreaderSimple) write(w io.Writer, in string) error { + buf := bufio.NewWriter(w) + if _, err := buf.WriteString(in); err != nil { + return err + } + return buf.Flush() +} + +type colorizeSpreaderSimple struct { + *defaultSpreaderSimple // NOTE: xxx + + fileConsiderer *fileConsiderer + fileColor *color.Color + fileCounter *counter + + dirColor *color.Color + dirCounter *counter +} + +func (cs *colorizeSpreaderSimple) spread(w io.Writer, roots []*Node) error { + ret := "" + for _, root := range roots { + cs.fileCounter.reset() + cs.dirCounter.reset() + ret += fmt.Sprintf("%s\n%s", cs.spreadBranch(root), cs.summary()) + } + return cs.write(w, ret) +} + +func (cs *colorizeSpreaderSimple) spreadBranch(current *Node) string { + cs.colorize(current) + ret := current.branch() + for _, child := range current.children { + ret += cs.spreadBranch(child) + } + return ret +} + +func (cs *colorizeSpreaderSimple) colorize(current *Node) { + if cs.fileConsiderer.isFile(current) { + _ = cs.fileCounter.next() + current.name = cs.fileColor.Sprint(current.name) + } else { + _ = cs.dirCounter.next() + current.name = cs.dirColor.Sprint(current.name) + } +} + +func (cs *colorizeSpreaderSimple) summary() string { + return fmt.Sprintf( + "%d directories, %d files\n", + cs.dirCounter.current(), + cs.fileCounter.current(), + ) +} + +type jsonSpreaderSimple struct{} + +func (*jsonSpreaderSimple) spread(w io.Writer, roots []*Node) error { + enc := json.NewEncoder(w) + for _, root := range roots { + jRoot := root.toJSONNode(nil) + if err := enc.Encode(jRoot); err != nil { + return err + } + } + return nil +} + +type jsonNode struct { + Name string `json:"value"` + Children []*jsonNode `json:"children"` +} + +func (parent *Node) toJSONNode(jParent *jsonNode) *jsonNode { + if jParent == nil { + jParent = &jsonNode{Name: parent.name} + } + if !parent.hasChild() { + return jParent + } + + jParent.Children = make([]*jsonNode, len(parent.children)) + for i := range parent.children { + jParent.Children[i] = &jsonNode{Name: parent.children[i].name} + _ = parent.children[i].toJSONNode(jParent.Children[i]) + } + + return jParent +} + +type tomlSpreaderSimple struct{} + +func (*tomlSpreaderSimple) spread(w io.Writer, roots []*Node) error { + enc := toml.NewEncoder(w) + for _, root := range roots { + tRoot := root.toTOMLNode(nil) + if err := enc.Encode(tRoot); err != nil { + return err + } + } + return nil +} + +type tomlNode struct { + Name string `toml:"value"` + Children []*tomlNode `toml:"children"` +} + +func (parent *Node) toTOMLNode(tParent *tomlNode) *tomlNode { + if tParent == nil { + tParent = &tomlNode{Name: parent.name} + } + if !parent.hasChild() { + return tParent + } + + tParent.Children = make([]*tomlNode, len(parent.children)) + for i := range parent.children { + tParent.Children[i] = &tomlNode{Name: parent.children[i].name} + _ = parent.children[i].toTOMLNode(tParent.Children[i]) + } + + return tParent +} + +type yamlSpreaderSimple struct{} + +func (*yamlSpreaderSimple) spread(w io.Writer, roots []*Node) error { + enc := yaml.NewEncoder(w) + for _, root := range roots { + yRoot := root.toYAMLNode(nil) + if err := enc.Encode(yRoot); err != nil { + return err + } + } + return nil +} + +type yamlNode struct { + Name string `yaml:"value"` + Children []*yamlNode `yaml:"children"` +} + +func (parent *Node) toYAMLNode(yParent *yamlNode) *yamlNode { + if yParent == nil { + yParent = &yamlNode{Name: parent.name} + } + if !parent.hasChild() { + return yParent + } + + yParent.Children = make([]*yamlNode, len(parent.children)) + for i := range parent.children { + yParent.Children[i] = &yamlNode{Name: parent.children[i].name} + _ = parent.children[i].toYAMLNode(yParent.Children[i]) + } + + return yParent +} + +var ( + _ spreaderSimple = (*defaultSpreaderSimple)(nil) + _ spreaderSimple = (*colorizeSpreaderSimple)(nil) + _ spreaderSimple = (*jsonSpreaderSimple)(nil) + _ spreaderSimple = (*yamlSpreaderSimple)(nil) + _ spreaderSimple = (*tomlSpreaderSimple)(nil) +) diff --git a/tree.go b/tree.go index 984485d..023b08c 100644 --- a/tree.go +++ b/tree.go @@ -2,98 +2,11 @@ package gtree -import ( - "context" - "io" +import "io" - "golang.org/x/sync/errgroup" -) - -type tree struct { - grower grower - spreader spreader - mkdirer mkdirer -} - -// 関心事は各ノードの枝の形成 -type grower interface { - grow(context.Context, <-chan *Node) (<-chan *Node, <-chan error) - enableValidation() -} - -// 関心事はtreeの出力 -type spreader interface { - spread(context.Context, io.Writer, <-chan *Node) <-chan error -} - -// 関心事はファイルの生成 -// interfaceを使う必要はないが、grower/spreaderと合わせたいため -type mkdirer interface { - mkdir(context.Context, <-chan *Node) <-chan error -} - -func newTree(conf *config) *tree { - growerFactory := func(lastNodeFormat, intermedialNodeFormat branchFormat, dryrun bool, encode encode) grower { - if encode != encodeDefault { - return newNopGrower() - } - return newGrower(lastNodeFormat, intermedialNodeFormat, dryrun) - } - - spreaderFactory := func(encode encode, dryrun bool, fileExtensions []string) spreader { - if dryrun { - return newColorizeSpreader(fileExtensions) - } - return newSpreader(encode) - } - - mkdirerFactory := func(fileExtensions []string) mkdirer { - return newMkdirer(fileExtensions) - } - - return &tree{ - grower: growerFactory( - conf.lastNodeFormat, - conf.intermedialNodeFormat, - conf.dryrun, - conf.encode, - ), - spreader: spreaderFactory( - conf.encode, - conf.dryrun, - conf.fileExtensions, - ), - mkdirer: mkdirerFactory( - conf.fileExtensions, - ), - } -} - -func (t *tree) grow(ctx context.Context, roots <-chan *Node) (<-chan *Node, <-chan error) { - return t.grower.grow(ctx, roots) -} - -func (t *tree) spread(ctx context.Context, w io.Writer, roots <-chan *Node) <-chan error { - return t.spreader.spread(ctx, w, roots) -} - -func (t *tree) mkdir(ctx context.Context, roots <-chan *Node) <-chan error { - return t.mkdirer.mkdir(ctx, roots) -} - -// パイプラインの全ステージで最初のエラーを返却 -func handlePipelineErr(echs ...<-chan error) error { - eg, _ := errgroup.WithContext(context.TODO()) - for i := range echs { - i := i - eg.Go(func() error { - for e := range echs[i] { - if e != nil { - return e - } - } - return nil - }) - } - return eg.Wait() +type tree interface { + output(io.Writer, io.Reader, *config) error + outputProgrammably(io.Writer, *Node, *config) error + makedir(io.Reader, *config) error + makedirProgrammably(*Node, *config) error } diff --git a/tree_handler.go b/tree_handler.go index 5c1057e..6ac2ee2 100644 --- a/tree_handler.go +++ b/tree_handler.go @@ -3,7 +3,6 @@ package gtree import ( - "context" "io" ) @@ -14,15 +13,13 @@ func Output(w io.Writer, r io.Reader, options ...Option) error { return err } - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() + if conf.massive { + tree := newTreePipeline(conf) + return tree.output(w, r, conf) + } - tree := newTree(conf) - splitStream, errcsl := split(ctx, r) - rootStream, errcr := newRootGenerator(conf.space).generate(ctx, splitStream) - growStream, errcg := tree.grow(ctx, rootStream) - errcs := tree.spread(ctx, w, growStream) - return handlePipelineErr(errcsl, errcr, errcg, errcs) + tree := newTreeSimple(conf) + return tree.output(w, r, conf) } // Mkdir makes directories. @@ -32,13 +29,11 @@ func Mkdir(r io.Reader, options ...Option) error { return err } - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() + if conf.massive { + tree := newTreePipeline(conf) + return tree.makedir(r, conf) + } - tree := newTree(conf) - splitStream, errcsl := split(ctx, r) - rootStream, errcr := newRootGenerator(conf.space).generate(ctx, splitStream) - growStream, errcg := tree.grow(ctx, rootStream) - errcm := tree.mkdir(ctx, growStream) - return handlePipelineErr(errcsl, errcr, errcg, errcm) + tree := newTreeSimple(conf) + return tree.makedir(r, conf) } diff --git a/tree_handler_mkdir_test.go b/tree_handler_mkdir_test.go index 19ceffe..59330af 100644 --- a/tree_handler_mkdir_test.go +++ b/tree_handler_mkdir_test.go @@ -108,6 +108,21 @@ func TestMkdir(t *testing.T) { }, wantErr: nil, }, + { + name: "case(succeeded/make directories and files/massive root)", + in: in{ + input: strings.NewReader(strings.TrimSpace(` +- root_j + - b.go + - bb.go + - lll`)), + options: []gtree.Option{ + gtree.WithFileExtensions([]string{".go"}), + gtree.WithMassive(), + }, + }, + wantErr: nil, + }, } for _, tt := range tests { diff --git a/tree_handler_output_test.go b/tree_handler_output_test.go index 4d74c4c..0c83d5e 100644 --- a/tree_handler_output_test.go +++ b/tree_handler_output_test.go @@ -469,6 +469,126 @@ a prev tab err: nil, }, }, + { + // 複数Rootブロックを指定すべきだが、実装上、出力の順番が保証されないため1Rootで実施 + name: "case(succeeded/when massive root)", + in: in{ + input: strings.NewReader(strings.TrimSpace(` +- a + - b + - c`)), + options: []gtree.Option{ + gtree.WithMassive(), + }, + }, + out: out{ + output: strings.TrimPrefix(` +a +└── b + └── c +`, "\n"), + err: nil, + }, + }, + { + // 複数Rootブロックを指定すべきだが、実装上、出力の順番が保証されないため1Rootで実施 + name: "case(succeeded/when massive root and dryrun)", + in: in{ + input: strings.NewReader(strings.TrimSpace(` +- a + - b + - z + - c + - y`)), + options: []gtree.Option{ + gtree.WithMassive(), + gtree.WithDryRun(), + gtree.WithFileExtensions([]string{"c"}), + }, + }, + out: out{ + output: strings.TrimPrefix(` +a +├── b +│ ├── z +│ └── c +└── y + +4 directories, 1 files +`, "\n"), + err: nil, + }, + }, + { + // 複数Rootブロックを指定すべきだが、実装上、出力の順番が保証されないため1Rootで実施 + name: "case(succeeded/when massive root and json)", + in: in{ + input: strings.NewReader(strings.TrimSpace(` +- a + - b + - c`)), + options: []gtree.Option{ + gtree.WithMassive(), + gtree.WithEncodeJSON(), + }, + }, + out: out{ + output: `{"value":"a","children":[{"value":"b","children":[{"value":"c","children":null}]}]}` + "\n", + err: nil, + }, + }, + { + // 複数Rootブロックを指定すべきだが、実装上、出力の順番が保証されないため1Rootで実施 + name: "case(succeeded/when massive root and yaml)", + in: in{ + input: strings.NewReader(strings.TrimSpace(` +- a + - b + - c`)), + options: []gtree.Option{ + gtree.WithMassive(), + gtree.WithEncodeYAML(), + }, + }, + out: out{ + output: strings.TrimSpace(` +value: a +children: + - value: b + children: + - value: c + children: [] +`) + "\n", + err: nil, + }, + }, + { + // 複数Rootブロックを指定すべきだが、実装上、出力の順番が保証されないため1Rootで実施 + name: "case(succeeded/when massive root and toml)", + in: in{ + input: strings.NewReader(strings.TrimSpace(` +- a + - b + - c`)), + options: []gtree.Option{ + gtree.WithMassive(), + gtree.WithEncodeTOML(), + }, + }, + out: out{ + output: strings.TrimSpace(` +value = 'a' + +[[children]] +value = 'b' + +[[children.children]] +value = 'c' +children = [] +`) + "\n", + err: nil, + }, + }, } for _, tt := range tests { diff --git a/tree_handler_programmably.go b/tree_handler_programmably.go index 0ebece9..ad10698 100644 --- a/tree_handler_programmably.go +++ b/tree_handler_programmably.go @@ -3,11 +3,8 @@ package gtree import ( - "context" "errors" "io" - - "github.com/fatih/color" ) var ( @@ -38,18 +35,13 @@ func OutputProgrammably(w io.Writer, root *Node, options ...Option) error { idxCounter.reset() - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - - rootStream := make(chan *Node) - go func() { - defer close(rootStream) - rootStream <- root - }() - tree := newTree(conf) - growStream, errcg := tree.grow(ctx, rootStream) - errcs := tree.spread(ctx, w, growStream) - return handlePipelineErr(errcg, errcs) + if conf.massive { + tree := newTreePipeline(conf) + return tree.outputProgrammably(w, root, conf) + } + + tree := newTreeSimple(conf) + return tree.outputProgrammably(w, root, conf) } var ( @@ -74,29 +66,20 @@ func MkdirProgrammably(root *Node, options ...Option) error { idxCounter.reset() - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - - rootStream := make(chan *Node) - go func() { - defer close(rootStream) - rootStream <- root - }() - tree := newTree(conf) - tree.enableValidation() - // when detect invalid node name, return error. process end. - growStream, errcg := tree.grow(ctx, rootStream) - if conf.dryrun { - // when detected no invalid node name, output tree. - errcs := tree.spread(ctx, color.Output, growStream) - return handlePipelineErr(errcg, errcs) + if conf.massive { + tree := newTreePipeline(conf) + return tree.makedirProgrammably(root, conf) } - // when detected no invalid node name, no output tree. - errcm := tree.mkdir(ctx, growStream) - return handlePipelineErr(errcg, errcm) + + tree := newTreeSimple(conf) + return tree.makedirProgrammably(root, conf) +} + +func (t *treeSimple) enableValidation() { + t.grower.enableValidation() } -func (t *tree) enableValidation() { +func (t *treePipeline) enableValidation() { t.grower.enableValidation() } diff --git a/tree_handler_programmably_mkdir_test.go b/tree_handler_programmably_mkdir_test.go index 60b2f3c..1d899a8 100644 --- a/tree_handler_programmably_mkdir_test.go +++ b/tree_handler_programmably_mkdir_test.go @@ -19,6 +19,11 @@ func TestMkdirProgrammably(t *testing.T) { name: "case(succeeded)", root: prepare(), }, + { + name: "case(succeeded/massive)", + root: prepare_a(), + options: []gtree.Option{gtree.WithMassive()}, + }, { name: "case(not root)", root: prepareNotRoot(), @@ -96,3 +101,9 @@ func prepareExistRoot(t *testing.T) *gtree.Node { root.Add("temp") return root } + +func prepare_a() *gtree.Node { + root := gtree.NewRoot("root8") + root.Add("child 1").Add("child 2") + return root +} diff --git a/tree_handler_programmably_output_test.go b/tree_handler_programmably_output_test.go index 9cf9b36..c523d47 100644 --- a/tree_handler_programmably_output_test.go +++ b/tree_handler_programmably_output_test.go @@ -50,6 +50,17 @@ func TestOutputProgrammably(t *testing.T) { root: prepare(), want: strings.TrimPrefix(` root +└── child 1 + └── child 2 +`, "\n"), + wantErr: nil, + }, + { + name: "case(succeeded/massive)", + root: prepare(), + options: []gtree.Option{gtree.WithMassive()}, + want: strings.TrimPrefix(` +root └── child 1 └── child 2 `, "\n"),