feat: added routing tools to amtool (#1511)
Signed-off-by: Martin Chodur <m.chodur@seznam.cz>
This commit is contained in:
parent
e220c00a01
commit
5d222bce55
|
@ -1,6 +1,7 @@
|
||||||
## Next release
|
## Next release
|
||||||
|
|
||||||
* [CHANGE] Revert Alertmanager working directory changes in Docker image back to `/alertmanager` (#1435)
|
* [CHANGE] Revert Alertmanager working directory changes in Docker image back to `/alertmanager` (#1435)
|
||||||
|
* [FEATURE] [amtool] Added `config routes` tools for vizualization and testing routes (#1511)
|
||||||
|
|
||||||
## 0.15.2 / 2018-08-14
|
## 0.15.2 / 2018-08-14
|
||||||
|
|
||||||
|
|
16
README.md
16
README.md
|
@ -286,6 +286,22 @@ output: extended
|
||||||
receiver: team-X-pager
|
receiver: team-X-pager
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Routes
|
||||||
|
|
||||||
|
Amtool allows you to vizualize the routes of your configuration in form of text tree view.
|
||||||
|
Also you can use it to test the routing by passing it label set of an alert
|
||||||
|
and it prints out all receivers the alert would match ordered and separated by `,`.
|
||||||
|
(If you use `--verify.receivers` amtool returns error code 1 on mismatch)
|
||||||
|
|
||||||
|
Example of usage:
|
||||||
|
```
|
||||||
|
# View routing tree of remote Alertmanager
|
||||||
|
amtool config routes --alertmanager.url=http://localhost:9090
|
||||||
|
|
||||||
|
# Test if alert matches expected receiver
|
||||||
|
./amtool config routes test --config.file=doc/examples/simple.yml --tree --verify.receivers=team-X-pager service=database owner=team-X
|
||||||
|
```
|
||||||
|
|
||||||
## High Availability
|
## High Availability
|
||||||
|
|
||||||
> Warning: High Availability is under active development
|
> Warning: High Availability is under active development
|
||||||
|
|
|
@ -17,11 +17,9 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
|
|
||||||
"github.com/prometheus/client_golang/api"
|
|
||||||
"gopkg.in/alecthomas/kingpin.v2"
|
"gopkg.in/alecthomas/kingpin.v2"
|
||||||
|
|
||||||
"github.com/prometheus/alertmanager/cli/format"
|
"github.com/prometheus/alertmanager/cli/format"
|
||||||
"github.com/prometheus/alertmanager/client"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const configHelp = `View current config.
|
const configHelp = `View current config.
|
||||||
|
@ -34,17 +32,13 @@ The amount of output is controlled by the output selection flag:
|
||||||
|
|
||||||
// configCmd represents the config command
|
// configCmd represents the config command
|
||||||
func configureConfigCmd(app *kingpin.Application) {
|
func configureConfigCmd(app *kingpin.Application) {
|
||||||
app.Command("config", configHelp).Action(execWithTimeout(queryConfig)).PreAction(requireAlertManagerURL)
|
configCmd := app.Command("config", configHelp)
|
||||||
|
configCmd.Command("show", configHelp).Default().Action(execWithTimeout(queryConfig)).PreAction(requireAlertManagerURL)
|
||||||
|
configureRoutingCmd(configCmd)
|
||||||
}
|
}
|
||||||
|
|
||||||
func queryConfig(ctx context.Context, _ *kingpin.ParseContext) error {
|
func queryConfig(ctx context.Context, _ *kingpin.ParseContext) error {
|
||||||
c, err := api.NewClient(api.Config{Address: alertmanagerURL.String()})
|
status, err := getRemoteAlertmanagerConfigStatus(ctx, alertmanagerURL)
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
statusAPI := client.NewStatusAPI(c)
|
|
||||||
status, err := statusAPI.Get(ctx)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,120 @@
|
||||||
|
// Copyright 2018 Prometheus Team
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/xlab/treeprint"
|
||||||
|
|
||||||
|
"github.com/prometheus/alertmanager/client"
|
||||||
|
"github.com/prometheus/alertmanager/dispatch"
|
||||||
|
"gopkg.in/alecthomas/kingpin.v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
type routingShow struct {
|
||||||
|
configFile string
|
||||||
|
labels []string
|
||||||
|
expectedReceivers string
|
||||||
|
tree treeprint.Tree
|
||||||
|
debugTree bool
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
routingHelp = `Prints alert routing tree
|
||||||
|
|
||||||
|
Will print whole routing tree in form of ASCII tree view.
|
||||||
|
|
||||||
|
Routing is loaded from a local configuration file or a running Alertmanager configuration.
|
||||||
|
Specifying --config.file takes precedence over --alertmanager.url.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
./amtool config routes [show] --config.file=doc/examples/simple.yml
|
||||||
|
|
||||||
|
`
|
||||||
|
branchSlugSeparator = " "
|
||||||
|
)
|
||||||
|
|
||||||
|
func configureRoutingCmd(app *kingpin.CmdClause) {
|
||||||
|
var (
|
||||||
|
c = &routingShow{}
|
||||||
|
routingCmd = app.Command("routes", routingHelp)
|
||||||
|
routingShowCmd = routingCmd.Command("show", routingHelp).Default()
|
||||||
|
configFlag = routingCmd.Flag("config.file", "Config file to be tested.")
|
||||||
|
)
|
||||||
|
configFlag.ExistingFileVar(&c.configFile)
|
||||||
|
routingShowCmd.Action(execWithTimeout(c.routingShowAction))
|
||||||
|
configureRoutingTestCmd(routingCmd, c)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *routingShow) routingShowAction(ctx context.Context, _ *kingpin.ParseContext) error {
|
||||||
|
// Load configuration form file or URL.
|
||||||
|
cfg, err := loadAlertmanagerConfig(ctx, alertmanagerURL, c.configFile)
|
||||||
|
if err != nil {
|
||||||
|
kingpin.Fatalf("%s", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
route := dispatch.NewRoute(cfg.Route, nil)
|
||||||
|
tree := treeprint.New()
|
||||||
|
convertRouteToTree(route, tree)
|
||||||
|
fmt.Println("Routing tree:")
|
||||||
|
fmt.Println(tree.String())
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getRouteTreeSlug(route *dispatch.Route, showContinue bool, showReceiver bool) string {
|
||||||
|
var branchSlug bytes.Buffer
|
||||||
|
if route.Matchers.Len() == 0 {
|
||||||
|
branchSlug.WriteString("default-route")
|
||||||
|
} else {
|
||||||
|
branchSlug.WriteString(route.Matchers.String())
|
||||||
|
}
|
||||||
|
if route.Continue && showContinue {
|
||||||
|
branchSlug.WriteString(branchSlugSeparator)
|
||||||
|
branchSlug.WriteString("continue: true")
|
||||||
|
}
|
||||||
|
if showReceiver {
|
||||||
|
branchSlug.WriteString(branchSlugSeparator)
|
||||||
|
branchSlug.WriteString("receiver: ")
|
||||||
|
branchSlug.WriteString(route.RouteOpts.Receiver)
|
||||||
|
}
|
||||||
|
return branchSlug.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func convertRouteToTree(route *dispatch.Route, tree treeprint.Tree) {
|
||||||
|
branch := tree.AddBranch(getRouteTreeSlug(route, true, true))
|
||||||
|
for _, r := range route.Routes {
|
||||||
|
convertRouteToTree(r, branch)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getMatchingTree(route *dispatch.Route, tree treeprint.Tree, lset client.LabelSet) {
|
||||||
|
final := true
|
||||||
|
branch := tree.AddBranch(getRouteTreeSlug(route, false, false))
|
||||||
|
for _, r := range route.Routes {
|
||||||
|
if r.Matchers.Match(convertClientToCommonLabelSet(lset)) == true {
|
||||||
|
getMatchingTree(r, branch, lset)
|
||||||
|
final = false
|
||||||
|
if r.Continue != true {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if final == true {
|
||||||
|
branch.SetValue(getRouteTreeSlug(route, false, true))
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,101 @@
|
||||||
|
// Copyright 2018 Prometheus Team
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/prometheus/alertmanager/client"
|
||||||
|
"github.com/prometheus/alertmanager/dispatch"
|
||||||
|
"github.com/xlab/treeprint"
|
||||||
|
"gopkg.in/alecthomas/kingpin.v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
const routingTestHelp = `Test alert routing
|
||||||
|
|
||||||
|
Will return receiver names which the alert with given labels resolves to.
|
||||||
|
If the labelset resolves to multiple receivers, they are printed out in order as defined in the routing tree.
|
||||||
|
|
||||||
|
Routing is loaded from a local configuration file or a running Alertmanager configuration.
|
||||||
|
Specifying --config.file takes precedence over --alertmanager.url.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
./amtool config routes test --config.file=doc/examples/simple.yml --verify.receivers=team-DB-pager service=database
|
||||||
|
|
||||||
|
`
|
||||||
|
|
||||||
|
func configureRoutingTestCmd(cc *kingpin.CmdClause, c *routingShow) {
|
||||||
|
var routingTestCmd = cc.Command("test", routingTestHelp)
|
||||||
|
|
||||||
|
routingTestCmd.Flag("verify.receivers", "Checks if specified receivers matches resolved receivers. The command fails if the labelset does not route to the specified receivers.").StringVar(&c.expectedReceivers)
|
||||||
|
routingTestCmd.Flag("tree", "Prints out matching routes tree.").BoolVar(&c.debugTree)
|
||||||
|
routingTestCmd.Arg("labels", "List of labels to be tested against the configured routes.").StringsVar(&c.labels)
|
||||||
|
routingTestCmd.Action(execWithTimeout(c.routingTestAction))
|
||||||
|
}
|
||||||
|
|
||||||
|
// resolveAlertReceivers returns list of receiver names which given LabelSet resolves to.
|
||||||
|
func resolveAlertReceivers(mainRoute *dispatch.Route, labels *client.LabelSet) ([]string, error) {
|
||||||
|
var (
|
||||||
|
finalRoutes []*dispatch.Route
|
||||||
|
receivers []string
|
||||||
|
)
|
||||||
|
finalRoutes = mainRoute.Match(convertClientToCommonLabelSet(*labels))
|
||||||
|
for _, r := range finalRoutes {
|
||||||
|
receivers = append(receivers, r.RouteOpts.Receiver)
|
||||||
|
}
|
||||||
|
return receivers, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func printMatchingTree(mainRoute *dispatch.Route, ls client.LabelSet) {
|
||||||
|
tree := treeprint.New()
|
||||||
|
getMatchingTree(mainRoute, tree, ls)
|
||||||
|
fmt.Println("Matching routes:")
|
||||||
|
fmt.Println(tree.String())
|
||||||
|
fmt.Print("\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *routingShow) routingTestAction(ctx context.Context, _ *kingpin.ParseContext) error {
|
||||||
|
cfg, err := loadAlertmanagerConfig(ctx, alertmanagerURL, c.configFile)
|
||||||
|
if err != nil {
|
||||||
|
kingpin.Fatalf("%v\n", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
mainRoute := dispatch.NewRoute(cfg.Route, nil)
|
||||||
|
|
||||||
|
// Parse lables to LabelSet.
|
||||||
|
ls, err := parseLabels(c.labels)
|
||||||
|
if err != nil {
|
||||||
|
kingpin.Fatalf("Failed to parse labels: %v\n", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.debugTree == true {
|
||||||
|
printMatchingTree(mainRoute, ls)
|
||||||
|
}
|
||||||
|
|
||||||
|
receivers, err := resolveAlertReceivers(mainRoute, &ls)
|
||||||
|
receiversSlug := strings.Join(receivers, ",")
|
||||||
|
fmt.Printf("%s\n", receiversSlug)
|
||||||
|
|
||||||
|
if c.expectedReceivers != "" && c.expectedReceivers != receiversSlug {
|
||||||
|
fmt.Printf("WARNING: Expected receivers did not match resolved receivers.\n")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
|
@ -0,0 +1,64 @@
|
||||||
|
// Copyright 2018 Prometheus Team
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/prometheus/alertmanager/client"
|
||||||
|
"github.com/prometheus/alertmanager/config"
|
||||||
|
"github.com/prometheus/alertmanager/dispatch"
|
||||||
|
)
|
||||||
|
|
||||||
|
type routingTestDefinition struct {
|
||||||
|
alert client.LabelSet
|
||||||
|
expectedReceivers []string
|
||||||
|
configFile string
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkResolvedReceivers(mainRoute *dispatch.Route, ls client.LabelSet, expectedReceivers []string) error {
|
||||||
|
resolvedReceivers, err := resolveAlertReceivers(mainRoute, &ls)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(expectedReceivers, resolvedReceivers) {
|
||||||
|
return fmt.Errorf("Unexpected routing result want: `%s`, got: `%s`", strings.Join(expectedReceivers, ","), strings.Join(resolvedReceivers, ","))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRoutingTest(t *testing.T) {
|
||||||
|
tests := []*routingTestDefinition{
|
||||||
|
&routingTestDefinition{configFile: "testdata/conf.routing.yml", alert: client.LabelSet{"test": "1"}, expectedReceivers: []string{"test1"}},
|
||||||
|
&routingTestDefinition{configFile: "testdata/conf.routing.yml", alert: client.LabelSet{"test": "2"}, expectedReceivers: []string{"test1", "test2"}},
|
||||||
|
&routingTestDefinition{configFile: "testdata/conf.routing-reverted.yml", alert: client.LabelSet{"test": "2"}, expectedReceivers: []string{"test2", "test1"}},
|
||||||
|
&routingTestDefinition{configFile: "testdata/conf.routing.yml", alert: client.LabelSet{"test": "volovina"}, expectedReceivers: []string{"default"}},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
cfg, _, err := config.LoadFile(test.configFile)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to load test configuration: %v", err)
|
||||||
|
}
|
||||||
|
mainRoute := dispatch.NewRoute(cfg.Route, nil)
|
||||||
|
err = checkResolvedReceivers(mainRoute, test.alert, test.expectedReceivers)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("%v", err)
|
||||||
|
}
|
||||||
|
fmt.Println(" OK")
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,22 @@
|
||||||
|
global:
|
||||||
|
smtp_smarthost: 'localhost:25'
|
||||||
|
|
||||||
|
templates:
|
||||||
|
- '/etc/alertmanager/template/*.tmpl'
|
||||||
|
|
||||||
|
route:
|
||||||
|
receiver: default
|
||||||
|
routes:
|
||||||
|
- match:
|
||||||
|
test: 2
|
||||||
|
receiver: test2
|
||||||
|
continue: true
|
||||||
|
- match_re:
|
||||||
|
test: ^[12]$
|
||||||
|
receiver: test1
|
||||||
|
continue: true
|
||||||
|
|
||||||
|
receivers:
|
||||||
|
- name: default
|
||||||
|
- name: test1
|
||||||
|
- name: test2
|
|
@ -0,0 +1,21 @@
|
||||||
|
global:
|
||||||
|
smtp_smarthost: 'localhost:25'
|
||||||
|
|
||||||
|
templates:
|
||||||
|
- '/etc/alertmanager/template/*.tmpl'
|
||||||
|
|
||||||
|
route:
|
||||||
|
receiver: default
|
||||||
|
routes:
|
||||||
|
- match_re:
|
||||||
|
test: ^[12]$
|
||||||
|
receiver: test1
|
||||||
|
continue: true
|
||||||
|
- match:
|
||||||
|
test: 2
|
||||||
|
receiver: test2
|
||||||
|
|
||||||
|
receivers:
|
||||||
|
- name: default
|
||||||
|
- name: test1
|
||||||
|
- name: test2
|
54
cli/utils.go
54
cli/utils.go
|
@ -18,9 +18,12 @@ import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"os"
|
||||||
"path"
|
"path"
|
||||||
|
|
||||||
"github.com/prometheus/alertmanager/client"
|
"github.com/prometheus/alertmanager/client"
|
||||||
|
amconfig "github.com/prometheus/alertmanager/config"
|
||||||
|
"github.com/prometheus/client_golang/api"
|
||||||
kingpin "gopkg.in/alecthomas/kingpin.v2"
|
kingpin "gopkg.in/alecthomas/kingpin.v2"
|
||||||
|
|
||||||
"github.com/prometheus/alertmanager/pkg/parse"
|
"github.com/prometheus/alertmanager/pkg/parse"
|
||||||
|
@ -70,6 +73,57 @@ func parseMatchers(inputMatchers []string) ([]labels.Matcher, error) {
|
||||||
return matchers, nil
|
return matchers, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// getRemoteAlertmanagerConfigStatus returns status responsecontaining configuration from remote Alertmanager
|
||||||
|
func getRemoteAlertmanagerConfigStatus(ctx context.Context, alertmanagerURL *url.URL) (*client.ServerStatus, error) {
|
||||||
|
c, err := api.NewClient(api.Config{Address: alertmanagerURL.String()})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
statusAPI := client.NewStatusAPI(c)
|
||||||
|
status, err := statusAPI.Get(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return status, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkRoutingConfigInputFlags(alertmanagerURL *url.URL, configFile string) {
|
||||||
|
if alertmanagerURL != nil && configFile != "" {
|
||||||
|
fmt.Fprintln(os.Stderr, "Warning: --config.file flag overrides the --alertmanager.url.")
|
||||||
|
}
|
||||||
|
if alertmanagerURL == nil && configFile == "" {
|
||||||
|
kingpin.Fatalf("You have to specify one of --config.file or --alertmanager.url flags.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadAlertmanagerConfig(ctx context.Context, alertmanagerURL *url.URL, configFile string) (*amconfig.Config, error) {
|
||||||
|
checkRoutingConfigInputFlags(alertmanagerURL, configFile)
|
||||||
|
if configFile != "" {
|
||||||
|
cfg, _, err := amconfig.LoadFile(configFile)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return cfg, nil
|
||||||
|
}
|
||||||
|
if alertmanagerURL != nil {
|
||||||
|
status, err := getRemoteAlertmanagerConfigStatus(ctx, alertmanagerURL)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return status.ConfigJSON, nil
|
||||||
|
}
|
||||||
|
return nil, errors.New("Failed to get Alertmanager configuration.")
|
||||||
|
}
|
||||||
|
|
||||||
|
// convertClientToCommonLabelSet converts client.LabelSet to model.Labelset
|
||||||
|
func convertClientToCommonLabelSet(cls client.LabelSet) model.LabelSet {
|
||||||
|
mls := make(model.LabelSet, len(cls))
|
||||||
|
for ln, lv := range cls {
|
||||||
|
mls[model.LabelName(ln)] = model.LabelValue(lv)
|
||||||
|
}
|
||||||
|
return mls
|
||||||
|
}
|
||||||
|
|
||||||
// Parse a list of labels (cli arguments)
|
// Parse a list of labels (cli arguments)
|
||||||
func parseLabels(inputLabels []string) (client.LabelSet, error) {
|
func parseLabels(inputLabels []string) (client.LabelSet, error) {
|
||||||
labelSet := make(client.LabelSet, len(inputLabels))
|
labelSet := make(client.LabelSet, len(inputLabels))
|
||||||
|
|
|
@ -75,6 +75,7 @@ route:
|
||||||
- match:
|
- match:
|
||||||
owner: team-X
|
owner: team-X
|
||||||
receiver: team-X-pager
|
receiver: team-X-pager
|
||||||
|
continue: true
|
||||||
- match:
|
- match:
|
||||||
owner: team-Y
|
owner: team-Y
|
||||||
receiver: team-Y-pager
|
receiver: team-Y-pager
|
||||||
|
|
|
@ -0,0 +1,20 @@
|
||||||
|
The MIT License (MIT)
|
||||||
|
Copyright © 2016 Maxim Kupriianov <max@kc.vc>
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the “Software”), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in
|
||||||
|
all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
THE SOFTWARE.
|
|
@ -0,0 +1,128 @@
|
||||||
|
treeprint [![GoDoc](https://godoc.org/github.com/xlab/treeprint?status.svg)](https://godoc.org/github.com/xlab/treeprint) ![test coverage](https://img.shields.io/badge/coverage-68.6%25-green.svg)
|
||||||
|
=========
|
||||||
|
|
||||||
|
Package `treeprint` provides a simple ASCII tree composing tool.
|
||||||
|
|
||||||
|
<a href="https://upload.wikimedia.org/wikipedia/commons/5/58/ENC_SYSTEME_FIGURE.jpeg"><img alt="SYSTEME FIGURE" src="https://upload.wikimedia.org/wikipedia/commons/thumb/5/58/ENC_SYSTEME_FIGURE.jpeg/896px-ENC_SYSTEME_FIGURE.jpeg" align="left" width="300"></a>
|
||||||
|
|
||||||
|
If you are familiar with the [tree](http://mama.indstate.edu/users/ice/tree/) utility that is a recursive directory listing command that produces a depth indented listing of files, then you have the idea of what it would look like.
|
||||||
|
|
||||||
|
On my system the command yields the following
|
||||||
|
|
||||||
|
```
|
||||||
|
$ tree
|
||||||
|
.
|
||||||
|
├── LICENSE
|
||||||
|
├── README.md
|
||||||
|
├── treeprint.go
|
||||||
|
└── treeprint_test.go
|
||||||
|
|
||||||
|
0 directories, 4 files
|
||||||
|
```
|
||||||
|
|
||||||
|
and I'd like to have the same format for my Go data structures when I print them.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```
|
||||||
|
$ go get github.com/xlab/treeprint
|
||||||
|
```
|
||||||
|
|
||||||
|
## Concept of work
|
||||||
|
|
||||||
|
The general idea is that you initialise a new tree with `treeprint.New()` and then add nodes and
|
||||||
|
branches into it. Use `AddNode()` when you want add a node on the same level as the target or
|
||||||
|
use `AddBranch()` when you want to go a level deeper. So `tree.AddBranch().AddNode().AddNode()` would
|
||||||
|
create a new level with two distinct nodes on it. So `tree.AddNode().AddNode()` is a flat thing and
|
||||||
|
`tree.AddBranch().AddBranch().AddBranch()` is a high thing. Use `String()` or `Bytes()` on a branch
|
||||||
|
to render a subtree, or use it on the root to print the whole tree.
|
||||||
|
|
||||||
|
The utility will yield Unicode-friendly trees. The output is predictable and there is no platform-dependent exceptions, so if you have issues with displaying the tree in the console, all platform-related transformations can be done after the tree has been rendered: [an example](https://github.com/xlab/treeprint/issues/2#issuecomment-324944141) for Asian locales.
|
||||||
|
|
||||||
|
## Use cases
|
||||||
|
|
||||||
|
When you want to render a complex data structure:
|
||||||
|
|
||||||
|
```go
|
||||||
|
func main() {
|
||||||
|
tree := treeprint.New()
|
||||||
|
|
||||||
|
// create a new branch in the root
|
||||||
|
one := tree.AddBranch("one")
|
||||||
|
|
||||||
|
// add some nodes
|
||||||
|
one.AddNode("subnode1").AddNode("subnode2")
|
||||||
|
|
||||||
|
// create a new sub-branch
|
||||||
|
one.AddBranch("two").
|
||||||
|
AddNode("subnode1").AddNode("subnode2"). // add some nodes
|
||||||
|
AddBranch("three"). // add a new sub-branch
|
||||||
|
AddNode("subnode1").AddNode("subnode2") // add some nodes too
|
||||||
|
|
||||||
|
// add one more node that should surround the inner branch
|
||||||
|
one.AddNode("subnode3")
|
||||||
|
|
||||||
|
// add a new node to the root
|
||||||
|
tree.AddNode("outernode")
|
||||||
|
|
||||||
|
fmt.Println(tree.String())
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Will give you:
|
||||||
|
|
||||||
|
```
|
||||||
|
.
|
||||||
|
├── one
|
||||||
|
│ ├── subnode1
|
||||||
|
│ ├── subnode2
|
||||||
|
│ ├── two
|
||||||
|
│ │ ├── subnode1
|
||||||
|
│ │ ├── subnode2
|
||||||
|
│ │ └── three
|
||||||
|
│ │ ├── subnode1
|
||||||
|
│ │ └── subnode2
|
||||||
|
│ └── subnode3
|
||||||
|
└── outernode
|
||||||
|
```
|
||||||
|
|
||||||
|
Another case, when you have to make a tree where any leaf may have some meta-data (as `tree` is capable of it):
|
||||||
|
|
||||||
|
```go
|
||||||
|
func main {
|
||||||
|
tree := treeprint.New()
|
||||||
|
|
||||||
|
tree.AddNode("Dockerfile")
|
||||||
|
tree.AddNode("Makefile")
|
||||||
|
tree.AddNode("aws.sh")
|
||||||
|
tree.AddMetaBranch(" 204", "bin").
|
||||||
|
AddNode("dbmaker").AddNode("someserver").AddNode("testtool")
|
||||||
|
tree.AddMetaBranch(" 374", "deploy").
|
||||||
|
AddNode("Makefile").AddNode("bootstrap.sh")
|
||||||
|
tree.AddMetaNode("122K", "testtool.a")
|
||||||
|
|
||||||
|
fmt.Println(tree.String())
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Output:
|
||||||
|
|
||||||
|
```
|
||||||
|
.
|
||||||
|
├── Dockerfile
|
||||||
|
├── Makefile
|
||||||
|
├── aws.sh
|
||||||
|
├── [ 204] bin
|
||||||
|
│ ├── dbmaker
|
||||||
|
│ ├── someserver
|
||||||
|
│ └── testtool
|
||||||
|
├── [ 374] deploy
|
||||||
|
│ ├── Makefile
|
||||||
|
│ └── bootstrap.sh
|
||||||
|
└── [122K] testtool.a
|
||||||
|
```
|
||||||
|
|
||||||
|
Yay! So it works.
|
||||||
|
|
||||||
|
## License
|
||||||
|
MIT
|
|
@ -0,0 +1,47 @@
|
||||||
|
package treeprint
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func isEmpty(v *reflect.Value) bool {
|
||||||
|
switch v.Kind() {
|
||||||
|
case reflect.Array, reflect.Map, reflect.Slice, reflect.String:
|
||||||
|
return v.Len() == 0
|
||||||
|
case reflect.Bool:
|
||||||
|
return !v.Bool()
|
||||||
|
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||||
|
return v.Int() == 0
|
||||||
|
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
|
||||||
|
return v.Uint() == 0
|
||||||
|
case reflect.Float32, reflect.Float64:
|
||||||
|
return v.Float() == 0
|
||||||
|
case reflect.Interface, reflect.Ptr:
|
||||||
|
return v.IsNil()
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func tagSpec(tag string) (name string, omit bool) {
|
||||||
|
parts := strings.Split(tag, ",")
|
||||||
|
if len(parts) < 2 {
|
||||||
|
return tag, false
|
||||||
|
}
|
||||||
|
if parts[1] == "omitempty" {
|
||||||
|
return parts[0], true
|
||||||
|
}
|
||||||
|
return parts[0], false
|
||||||
|
}
|
||||||
|
|
||||||
|
func filterTags(tag reflect.StructTag) string {
|
||||||
|
tags := strings.Split(string(tag), " ")
|
||||||
|
filtered := make([]string, 0, len(tags))
|
||||||
|
for i := range tags {
|
||||||
|
if strings.HasPrefix(tags[i], "tree:") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
filtered = append(filtered, tags[i])
|
||||||
|
}
|
||||||
|
return strings.Join(filtered, " ")
|
||||||
|
}
|
|
@ -0,0 +1,322 @@
|
||||||
|
package treeprint
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type StructTreeOption int
|
||||||
|
|
||||||
|
const (
|
||||||
|
StructNameTree StructTreeOption = iota
|
||||||
|
StructValueTree
|
||||||
|
StructTagTree
|
||||||
|
StructTypeTree
|
||||||
|
StructTypeSizeTree
|
||||||
|
)
|
||||||
|
|
||||||
|
func FromStruct(v interface{}, opt ...StructTreeOption) (Tree, error) {
|
||||||
|
var treeOpt StructTreeOption
|
||||||
|
if len(opt) > 0 {
|
||||||
|
treeOpt = opt[0]
|
||||||
|
}
|
||||||
|
switch treeOpt {
|
||||||
|
case StructNameTree:
|
||||||
|
tree := New()
|
||||||
|
err := nameTree(tree, v)
|
||||||
|
return tree, err
|
||||||
|
case StructValueTree:
|
||||||
|
tree := New()
|
||||||
|
err := valueTree(tree, v)
|
||||||
|
return tree, err
|
||||||
|
case StructTagTree:
|
||||||
|
tree := New()
|
||||||
|
err := tagTree(tree, v)
|
||||||
|
return tree, err
|
||||||
|
case StructTypeTree:
|
||||||
|
tree := New()
|
||||||
|
err := typeTree(tree, v)
|
||||||
|
return tree, err
|
||||||
|
case StructTypeSizeTree:
|
||||||
|
tree := New()
|
||||||
|
err := typeSizeTree(tree, v)
|
||||||
|
return tree, err
|
||||||
|
default:
|
||||||
|
err := fmt.Errorf("treeprint: invalid StructTreeOption %v", treeOpt)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type FmtFunc func(name string, v interface{}) (string, bool)
|
||||||
|
|
||||||
|
func FromStructWithMeta(v interface{}, fmtFunc FmtFunc) (Tree, error) {
|
||||||
|
if fmtFunc == nil {
|
||||||
|
tree := New()
|
||||||
|
err := nameTree(tree, v)
|
||||||
|
return tree, err
|
||||||
|
}
|
||||||
|
tree := New()
|
||||||
|
err := metaTree(tree, v, fmtFunc)
|
||||||
|
return tree, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func Repr(v interface{}) string {
|
||||||
|
tree := New()
|
||||||
|
vType := reflect.TypeOf(v)
|
||||||
|
vValue := reflect.ValueOf(v)
|
||||||
|
_, val, isStruct := getValue(vType, &vValue)
|
||||||
|
if !isStruct {
|
||||||
|
return fmt.Sprintf("%+v", val.Interface())
|
||||||
|
}
|
||||||
|
err := valueTree(tree, val.Interface())
|
||||||
|
if err != nil {
|
||||||
|
return err.Error()
|
||||||
|
}
|
||||||
|
return tree.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func nameTree(tree Tree, v interface{}) error {
|
||||||
|
typ, val, err := checkType(v)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
fields := typ.NumField()
|
||||||
|
for i := 0; i < fields; i++ {
|
||||||
|
field := typ.Field(i)
|
||||||
|
fieldValue := val.Field(i)
|
||||||
|
name, skip, omit := getMeta(field.Name, field.Tag)
|
||||||
|
if skip || omit && isEmpty(&fieldValue) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
typ, val, isStruct := getValue(field.Type, &fieldValue)
|
||||||
|
if !isStruct {
|
||||||
|
tree.AddNode(name)
|
||||||
|
continue
|
||||||
|
} else if subNum := typ.NumField(); subNum == 0 {
|
||||||
|
tree.AddNode(name)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
branch := tree.AddBranch(name)
|
||||||
|
if err := nameTree(branch, val.Interface()); err != nil {
|
||||||
|
err := fmt.Errorf("%v on struct branch %s", err, name)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getMeta(fieldName string, tag reflect.StructTag) (name string, skip, omit bool) {
|
||||||
|
if tagStr := tag.Get("tree"); len(tagStr) > 0 {
|
||||||
|
name, omit = tagSpec(tagStr)
|
||||||
|
}
|
||||||
|
if name == "-" {
|
||||||
|
return fieldName, true, omit
|
||||||
|
}
|
||||||
|
if len(name) == 0 {
|
||||||
|
name = fieldName
|
||||||
|
} else if trimmed := strings.TrimSpace(name); len(trimmed) == 0 {
|
||||||
|
name = fieldName
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func valueTree(tree Tree, v interface{}) error {
|
||||||
|
typ, val, err := checkType(v)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
fields := typ.NumField()
|
||||||
|
for i := 0; i < fields; i++ {
|
||||||
|
field := typ.Field(i)
|
||||||
|
fieldValue := val.Field(i)
|
||||||
|
name, skip, omit := getMeta(field.Name, field.Tag)
|
||||||
|
if skip || omit && isEmpty(&fieldValue) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
typ, val, isStruct := getValue(field.Type, &fieldValue)
|
||||||
|
if !isStruct {
|
||||||
|
tree.AddMetaNode(val.Interface(), name)
|
||||||
|
continue
|
||||||
|
} else if subNum := typ.NumField(); subNum == 0 {
|
||||||
|
tree.AddMetaNode(val.Interface(), name)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
branch := tree.AddBranch(name)
|
||||||
|
if err := valueTree(branch, val.Interface()); err != nil {
|
||||||
|
err := fmt.Errorf("%v on struct branch %s", err, name)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func tagTree(tree Tree, v interface{}) error {
|
||||||
|
typ, val, err := checkType(v)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
fields := typ.NumField()
|
||||||
|
for i := 0; i < fields; i++ {
|
||||||
|
field := typ.Field(i)
|
||||||
|
fieldValue := val.Field(i)
|
||||||
|
name, skip, omit := getMeta(field.Name, field.Tag)
|
||||||
|
if skip || omit && isEmpty(&fieldValue) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
filteredTag := filterTags(field.Tag)
|
||||||
|
typ, val, isStruct := getValue(field.Type, &fieldValue)
|
||||||
|
if !isStruct {
|
||||||
|
tree.AddMetaNode(filteredTag, name)
|
||||||
|
continue
|
||||||
|
} else if subNum := typ.NumField(); subNum == 0 {
|
||||||
|
tree.AddMetaNode(filteredTag, name)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
branch := tree.AddMetaBranch(filteredTag, name)
|
||||||
|
if err := tagTree(branch, val.Interface()); err != nil {
|
||||||
|
err := fmt.Errorf("%v on struct branch %s", err, name)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func typeTree(tree Tree, v interface{}) error {
|
||||||
|
typ, val, err := checkType(v)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
fields := typ.NumField()
|
||||||
|
for i := 0; i < fields; i++ {
|
||||||
|
field := typ.Field(i)
|
||||||
|
fieldValue := val.Field(i)
|
||||||
|
name, skip, omit := getMeta(field.Name, field.Tag)
|
||||||
|
if skip || omit && isEmpty(&fieldValue) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
typ, val, isStruct := getValue(field.Type, &fieldValue)
|
||||||
|
typename := fmt.Sprintf("%T", val.Interface())
|
||||||
|
if !isStruct {
|
||||||
|
tree.AddMetaNode(typename, name)
|
||||||
|
continue
|
||||||
|
} else if subNum := typ.NumField(); subNum == 0 {
|
||||||
|
tree.AddMetaNode(typename, name)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
branch := tree.AddMetaBranch(typename, name)
|
||||||
|
if err := typeTree(branch, val.Interface()); err != nil {
|
||||||
|
err := fmt.Errorf("%v on struct branch %s", err, name)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func typeSizeTree(tree Tree, v interface{}) error {
|
||||||
|
typ, val, err := checkType(v)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
fields := typ.NumField()
|
||||||
|
for i := 0; i < fields; i++ {
|
||||||
|
field := typ.Field(i)
|
||||||
|
fieldValue := val.Field(i)
|
||||||
|
name, skip, omit := getMeta(field.Name, field.Tag)
|
||||||
|
if skip || omit && isEmpty(&fieldValue) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
typ, val, isStruct := getValue(field.Type, &fieldValue)
|
||||||
|
typesize := typ.Size()
|
||||||
|
if !isStruct {
|
||||||
|
tree.AddMetaNode(typesize, name)
|
||||||
|
continue
|
||||||
|
} else if subNum := typ.NumField(); subNum == 0 {
|
||||||
|
tree.AddMetaNode(typesize, name)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
branch := tree.AddMetaBranch(typesize, name)
|
||||||
|
if err := typeSizeTree(branch, val.Interface()); err != nil {
|
||||||
|
err := fmt.Errorf("%v on struct branch %s", err, name)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func metaTree(tree Tree, v interface{}, fmtFunc FmtFunc) error {
|
||||||
|
typ, val, err := checkType(v)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
fields := typ.NumField()
|
||||||
|
for i := 0; i < fields; i++ {
|
||||||
|
field := typ.Field(i)
|
||||||
|
fieldValue := val.Field(i)
|
||||||
|
name, skip, omit := getMeta(field.Name, field.Tag)
|
||||||
|
if skip || omit && isEmpty(&fieldValue) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
typ, val, isStruct := getValue(field.Type, &fieldValue)
|
||||||
|
formatted, show := fmtFunc(name, val.Interface())
|
||||||
|
if !isStruct {
|
||||||
|
if show {
|
||||||
|
tree.AddMetaNode(formatted, name)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
tree.AddNode(name)
|
||||||
|
continue
|
||||||
|
} else if subNum := typ.NumField(); subNum == 0 {
|
||||||
|
if show {
|
||||||
|
tree.AddMetaNode(formatted, name)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
tree.AddNode(name)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
var branch Tree
|
||||||
|
if show {
|
||||||
|
branch = tree.AddMetaBranch(formatted, name)
|
||||||
|
} else {
|
||||||
|
branch = tree.AddBranch(name)
|
||||||
|
}
|
||||||
|
if err := metaTree(branch, val.Interface(), fmtFunc); err != nil {
|
||||||
|
err := fmt.Errorf("%v on struct branch %s", err, name)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getValue(typ reflect.Type, val *reflect.Value) (reflect.Type, *reflect.Value, bool) {
|
||||||
|
switch typ.Kind() {
|
||||||
|
case reflect.Ptr:
|
||||||
|
typ = typ.Elem()
|
||||||
|
if typ.Kind() == reflect.Struct {
|
||||||
|
elem := val.Elem()
|
||||||
|
return typ, &elem, true
|
||||||
|
}
|
||||||
|
case reflect.Struct:
|
||||||
|
return typ, val, true
|
||||||
|
}
|
||||||
|
return typ, val, false
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkType(v interface{}) (reflect.Type, *reflect.Value, error) {
|
||||||
|
typ := reflect.TypeOf(v)
|
||||||
|
val := reflect.ValueOf(v)
|
||||||
|
switch typ.Kind() {
|
||||||
|
case reflect.Ptr:
|
||||||
|
typ = typ.Elem()
|
||||||
|
if typ.Kind() != reflect.Struct {
|
||||||
|
err := fmt.Errorf("treeprint: %T is not a struct we could work with", v)
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
val = val.Elem()
|
||||||
|
case reflect.Struct:
|
||||||
|
default:
|
||||||
|
err := fmt.Errorf("treeprint: %T is not a struct we could work with", v)
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
return typ, &val, nil
|
||||||
|
}
|
|
@ -0,0 +1,206 @@
|
||||||
|
// Package treeprint provides a simple ASCII tree composing tool.
|
||||||
|
package treeprint
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"reflect"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Value interface{}
|
||||||
|
type MetaValue interface{}
|
||||||
|
|
||||||
|
// Tree represents a tree structure with leaf-nodes and branch-nodes.
|
||||||
|
type Tree interface {
|
||||||
|
// AddNode adds a new node to a branch.
|
||||||
|
AddNode(v Value) Tree
|
||||||
|
// AddMetaNode adds a new node with meta value provided to a branch.
|
||||||
|
AddMetaNode(meta MetaValue, v Value) Tree
|
||||||
|
// AddBranch adds a new branch node (a level deeper).
|
||||||
|
AddBranch(v Value) Tree
|
||||||
|
// AddMetaBranch adds a new branch node (a level deeper) with meta value provided.
|
||||||
|
AddMetaBranch(meta MetaValue, v Value) Tree
|
||||||
|
// Branch converts a leaf-node to a branch-node,
|
||||||
|
// applying this on a branch-node does no effect.
|
||||||
|
Branch() Tree
|
||||||
|
// FindByMeta finds a node whose meta value matches the provided one by reflect.DeepEqual,
|
||||||
|
// returns nil if not found.
|
||||||
|
FindByMeta(meta MetaValue) Tree
|
||||||
|
// FindByValue finds a node whose value matches the provided one by reflect.DeepEqual,
|
||||||
|
// returns nil if not found.
|
||||||
|
FindByValue(value Value) Tree
|
||||||
|
// returns the last node of a tree
|
||||||
|
FindLastNode() Tree
|
||||||
|
// String renders the tree or subtree as a string.
|
||||||
|
String() string
|
||||||
|
// Bytes renders the tree or subtree as byteslice.
|
||||||
|
Bytes() []byte
|
||||||
|
|
||||||
|
SetValue(value Value)
|
||||||
|
}
|
||||||
|
|
||||||
|
type node struct {
|
||||||
|
Root *node
|
||||||
|
Meta MetaValue
|
||||||
|
Value Value
|
||||||
|
Nodes []*node
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *node) FindLastNode() Tree {
|
||||||
|
ns := n.Nodes
|
||||||
|
n = ns[len(ns)-1]
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *node) AddNode(v Value) Tree {
|
||||||
|
n.Nodes = append(n.Nodes, &node{
|
||||||
|
Root: n,
|
||||||
|
Value: v,
|
||||||
|
})
|
||||||
|
if n.Root != nil {
|
||||||
|
return n.Root
|
||||||
|
}
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *node) AddMetaNode(meta MetaValue, v Value) Tree {
|
||||||
|
n.Nodes = append(n.Nodes, &node{
|
||||||
|
Root: n,
|
||||||
|
Meta: meta,
|
||||||
|
Value: v,
|
||||||
|
})
|
||||||
|
if n.Root != nil {
|
||||||
|
return n.Root
|
||||||
|
}
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *node) AddBranch(v Value) Tree {
|
||||||
|
branch := &node{
|
||||||
|
Value: v,
|
||||||
|
}
|
||||||
|
n.Nodes = append(n.Nodes, branch)
|
||||||
|
return branch
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *node) AddMetaBranch(meta MetaValue, v Value) Tree {
|
||||||
|
branch := &node{
|
||||||
|
Meta: meta,
|
||||||
|
Value: v,
|
||||||
|
}
|
||||||
|
n.Nodes = append(n.Nodes, branch)
|
||||||
|
return branch
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *node) Branch() Tree {
|
||||||
|
n.Root = nil
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *node) FindByMeta(meta MetaValue) Tree {
|
||||||
|
for _, node := range n.Nodes {
|
||||||
|
if reflect.DeepEqual(node.Meta, meta) {
|
||||||
|
return node
|
||||||
|
}
|
||||||
|
if v := node.FindByMeta(meta); v != nil {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *node) FindByValue(value Value) Tree {
|
||||||
|
for _, node := range n.Nodes {
|
||||||
|
if reflect.DeepEqual(node.Value, value) {
|
||||||
|
return node
|
||||||
|
}
|
||||||
|
if v := node.FindByMeta(value); v != nil {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *node) Bytes() []byte {
|
||||||
|
buf := new(bytes.Buffer)
|
||||||
|
level := 0
|
||||||
|
var levelsEnded []int
|
||||||
|
if n.Root == nil {
|
||||||
|
buf.WriteString(fmt.Sprintf("%v",n.Value))
|
||||||
|
buf.WriteByte('\n')
|
||||||
|
} else {
|
||||||
|
edge := EdgeTypeMid
|
||||||
|
if len(n.Nodes) == 0 {
|
||||||
|
edge = EdgeTypeEnd
|
||||||
|
levelsEnded = append(levelsEnded, level)
|
||||||
|
}
|
||||||
|
printValues(buf, 0, levelsEnded, edge, n.Meta, n.Value)
|
||||||
|
}
|
||||||
|
if len(n.Nodes) > 0 {
|
||||||
|
printNodes(buf, level, levelsEnded, n.Nodes)
|
||||||
|
}
|
||||||
|
return buf.Bytes()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *node) String() string {
|
||||||
|
return string(n.Bytes())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *node) SetValue(value Value){
|
||||||
|
n.Value = value
|
||||||
|
}
|
||||||
|
|
||||||
|
func printNodes(wr io.Writer,
|
||||||
|
level int, levelsEnded []int, nodes []*node) {
|
||||||
|
|
||||||
|
for i, node := range nodes {
|
||||||
|
edge := EdgeTypeMid
|
||||||
|
if i == len(nodes)-1 {
|
||||||
|
levelsEnded = append(levelsEnded, level)
|
||||||
|
edge = EdgeTypeEnd
|
||||||
|
}
|
||||||
|
printValues(wr, level, levelsEnded, edge, node.Meta, node.Value)
|
||||||
|
if len(node.Nodes) > 0 {
|
||||||
|
printNodes(wr, level+1, levelsEnded, node.Nodes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func printValues(wr io.Writer,
|
||||||
|
level int, levelsEnded []int, edge EdgeType, meta MetaValue, val Value) {
|
||||||
|
|
||||||
|
for i := 0; i < level; i++ {
|
||||||
|
if isEnded(levelsEnded, i) {
|
||||||
|
fmt.Fprint(wr, " ")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
fmt.Fprintf(wr, "%s ", EdgeTypeLink)
|
||||||
|
}
|
||||||
|
if meta != nil {
|
||||||
|
fmt.Fprintf(wr, "%s [%v] %v\n", edge, meta, val)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fmt.Fprintf(wr, "%s %v\n", edge, val)
|
||||||
|
}
|
||||||
|
|
||||||
|
func isEnded(levelsEnded []int, level int) bool {
|
||||||
|
for _, l := range levelsEnded {
|
||||||
|
if l == level {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
type EdgeType string
|
||||||
|
|
||||||
|
var (
|
||||||
|
EdgeTypeLink EdgeType = "│"
|
||||||
|
EdgeTypeMid EdgeType = "├──"
|
||||||
|
EdgeTypeEnd EdgeType = "└──"
|
||||||
|
)
|
||||||
|
|
||||||
|
func New() Tree {
|
||||||
|
return &node{Value: "."}
|
||||||
|
}
|
|
@ -323,6 +323,12 @@
|
||||||
"revision": "1e01b2bdbae8edb393fcf555732304f34d192fc9",
|
"revision": "1e01b2bdbae8edb393fcf555732304f34d192fc9",
|
||||||
"revisionTime": "2016-11-21T14:22:35Z"
|
"revisionTime": "2016-11-21T14:22:35Z"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"checksumSHA1": "Oen67XBkt5voJj1wya8kIakvdT4=",
|
||||||
|
"path": "github.com/xlab/treeprint",
|
||||||
|
"revision": "d6fb6747feb6e7cfdc44682a024bddf87ef07ec2",
|
||||||
|
"revisionTime": "2018-06-15T14:19:04Z"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"checksumSHA1": "hKZGpbFuCrkE4s1vxubKK1aFlfI=",
|
"checksumSHA1": "hKZGpbFuCrkE4s1vxubKK1aFlfI=",
|
||||||
"path": "golang.org/x/net/context",
|
"path": "golang.org/x/net/context",
|
||||||
|
|
Loading…
Reference in New Issue