feat: add generate business architecture diagram command

This commit is contained in:
zhuyasen 2024-10-21 19:25:19 +08:00
parent 53de39630c
commit d1caaab932
6 changed files with 683 additions and 3 deletions

View File

@ -0,0 +1,631 @@
package commands
import (
"errors"
"fmt"
"sort"
"strings"
"github.com/fatih/color"
"github.com/goccy/go-graphviz"
"github.com/goccy/go-graphviz/cgraph"
"github.com/spf13/cobra"
"github.com/zhufuyi/sponge/pkg/conf"
"github.com/zhufuyi/sponge/pkg/gofile"
)
// GenGraphCommand generate graph command
func GenGraphCommand() *cobra.Command {
var (
isAll bool
projectDir string
serverDir []string
)
cmd := &cobra.Command{
Use: "graph",
Short: "Generate business architecture diagram for the project",
Long: "Generate business architecture diagram for the project.",
Example: color.HiBlackString(` # If there are multiple servers in a project, simply specify the project directory path to generate a diagram between servers
sponge graph --project-dir=/path/to/project
# You can also specify multiple services to generate a business framework diagram
sponge graph --server-dir=/path/to/server1 --server-dir=/path/to/server2
# Includes database related servers
sponge graph --project-dir=/path/to/project --all`),
SilenceErrors: true,
SilenceUsage: true,
RunE: func(cmd *cobra.Command, args []string) error {
if projectDir == "" && len(serverDir) == 0 {
return errors.New("no project directory or server directory specified\n\n" + cmd.Example)
}
// get yaml file from project and server directories
yamlFiles := getYamlFiles(projectDir, serverDir)
if len(yamlFiles) == 0 {
fmt.Println("No yaml file found in project directory or server directories")
return nil
}
outFile := "./business_architecture_diagram.svg"
err := generateSvg(yamlFiles, isAll, outFile)
if err != nil {
return err
}
fmt.Printf("generated servers relationship diagram successfully, out = %s\n", color.HiCyanString("%s", outFile))
return nil
},
}
cmd.Flags().BoolVarP(&isAll, "all", "a", false, "does it include services such as databases")
cmd.Flags().StringVarP(&projectDir, "project-dir", "p", "", "project directory")
cmd.Flags().StringSliceVarP(&serverDir, "server-dir", "s", []string{}, "server directory, multiple parameters can be set")
return cmd
}
func mergeYamlFile(files []string, yamlFiles []string) []string {
for _, file := range files {
if !strings.Contains(file, "_cc.") {
yamlFiles = append(yamlFiles, file)
}
}
return yamlFiles
}
func filterYamlFiles(configsDirs []string, yamlFiles []string) []string {
for _, dir := range configsDirs {
files, _ := gofile.ListFiles(dir, gofile.WithSuffix(".yaml"))
yamlFiles = mergeYamlFile(files, yamlFiles)
files, _ = gofile.ListFiles(dir, gofile.WithSuffix(".yml"))
yamlFiles = mergeYamlFile(files, yamlFiles)
}
return yamlFiles
}
func getYamlFiles(projectDir string, serverDirs []string) []string {
var yamlFiles []string
configsDirs, _ := gofile.ListSubDirs(projectDir, "configs")
yamlFiles = filterYamlFiles(configsDirs, yamlFiles)
for _, dir := range serverDirs {
configsDirs, _ = gofile.ListSubDirs(dir, "configs")
yamlFiles = filterYamlFiles(configsDirs, yamlFiles)
}
return yamlFiles
}
// -------------------------------------------------------------------------------------
// Service represents a service in the project.
type Service struct {
Name string `yaml:"name"` // service name
Type string `yaml:"type"` // http, grpc, db, mq
Dependencies map[string][]string `yaml:"dependencies"` // map[Type][]serviceName
}
// ProjectConfig represents the configuration of the project.
type ProjectConfig struct {
Services []Service `yaml:"services"`
}
// NewProjectConfig creates a new ProjectConfig.
func NewProjectConfig() *ProjectConfig {
return &ProjectConfig{}
}
// AddService adds a new service to the project configuration.
func (c *ProjectConfig) AddService(config *GenericConfig) {
name := config.GetServiceName()
dependencies, additionalServices := config.GetDependencies(name)
service := Service{
Name: name,
Type: config.GetServiceType(),
Dependencies: dependencies,
}
additionalServices = append(additionalServices, service)
c.merge(additionalServices...)
}
func (c *ProjectConfig) merge(services ...Service) {
for _, service := range services {
for i, s := range c.Services {
if s.Name == service.Name {
// merge dependencies
s.Name = service.Name
s.Type = service.Type
for k, v := range service.Dependencies {
s.Dependencies[k] = append(s.Dependencies[k], v...)
}
c.Services[i] = s
//continue
}
}
c.Services = append(c.Services, service)
}
}
func generateSvg(yamlFiles []string, isAll bool, outFile string) error {
pc := NewProjectConfig()
for _, file := range yamlFiles {
config, err := ParseYaml(file, isAll)
if err != nil {
return fmt.Errorf("Failed to parse YAML: %v, file: %s", err, file)
}
pc.AddService(config)
}
// create Graphviz graph
g := graphviz.New()
graph, err := g.Graph()
if err != nil {
return fmt.Errorf("Failed to create Graphviz graph: %v", err)
}
defer func() {
if err := graph.Close(); err != nil {
fmt.Printf("Failed to close Graphviz graph: %v", err)
}
_ = g.Close()
}()
setCustomGraphStyle(graph)
edgeMap := map[string]*cgraph.Edge{}
// add nodes and edges to graph
for _, service := range pc.Services {
node1, _ := graph.CreateNode(service.Name)
setCustomNodeStyle(node1, getNodeColor(service.Name))
for typeLabel, dependencies := range service.Dependencies {
for _, serviceName := range dependencies {
key := sortAndMergeFields(node1.Name(), serviceName, typeLabel)
if e, ok := edgeMap[key]; ok {
setCustomEdgeStyle(e, typeLabel, getEdgeColor(typeLabel), true)
continue
}
node2, _ := graph.CreateNode(serviceName)
setCustomNodeStyle(node2, getNodeColor(serviceName))
e, _ := graph.CreateEdge(typeLabel, node1, node2)
setCustomEdgeStyle(e, typeLabel, getEdgeColor(typeLabel), false)
edgeMap[key] = e
}
}
}
return g.RenderFilename(graph, graphviz.SVG, outFile)
}
// Attribute style https://graphviz.gitlab.io/docs/attr-types/style/
func setCustomNodeStyle(n *cgraph.Node, nodeColor string) {
//color := "#99BD25"
n.SetStyle("rounded,filled")
n.SetShape(cgraph.RectShape)
n.SetWidth(1.2)
n.SetHeight(0.66)
n.SetColor(nodeColor)
n.SetFillColor(nodeColor)
n.SetFontColor("#ffffff")
n.SetFontSize(22.0)
}
func setCustomEdgeStyle(e *cgraph.Edge, typeLabel string, edgeColor string, isBothArrow bool) {
//color := "#909090"
e.SetLabel(typeLabel)
e.SetStyle("solid") // solid, dashed, dotted, bold
e.SetColor(edgeColor) // edge color
e.SetArrowSize(0.6)
e.SetFontColor(edgeColor) // label font color
e.SetFontSize(10.0)
e.SetConstraint(true)
if isBothArrow {
e.SetDir(cgraph.BothDir)
}
// rewriting the db edge style
if typeLabel == "db" {
e.SetLabel("")
e.SetStyle("dashed")
e.SetArrowSize(0.4)
}
}
func setCustomGraphStyle(g *cgraph.Graph) {
//g.SetBackgroundColor("#eeeeee")
g.SetCenter(true)
}
func getEdgeColor(typeLabel string) string {
switch typeLabel {
case "mq":
return "#16B69E"
case "db":
return "#3999C6"
case "http":
return "#955E42"
default:
return "#909090"
}
}
func getNodeColor(name string) string {
name = strings.ToLower(name)
switch name {
case "kafka", "rabbitmq", "rocketmq", "nsq", "nats", "activemq", "pulsar", "zeromq":
return "#16B69E"
case "mysql", "redis", "mongodb", "postgresql", "sqlite", "oracle", "sqlserver", "cassandra", "influxdb", "elasticsearch", "clickhouse", "cockroachdb", "tidb":
return "#3BD0FB"
default:
return "#99BD25"
}
}
func sortAndMergeFields(field ...string) string {
sort.Strings(field)
return strings.Join(field, "-")
}
// --------------------------------- sponge yaml file --------------------------------------
// ParseYaml parses the YAML file and returns a GenericConfig, yaml file content example:
/*
app:
name: "eshop-gw"
# http server settings example
http:
port: 8080
timeout: 0
# http client settings example
httpClient:
- name: "flashSale"
baseURL: "http://127.0.0.1:8080"
- name: "product"
baseURL: "http://127.0.0.1:8081"
# grpc server settings example
grpc:
port: 8282
httpPort: 8283
# grpc client settings example
grpcClient:
- name: "user"
host: "127.0.0.1"
port: 18282
- name: "order"
host: "127.0.0.1"
port: 28282
# db settings example
database:
mysql:
dsn: "root:123456@(192.168.3.37:3306)/eshop_order?parseTime=true&loc=Local&charset=utf8,utf8mb4"
mongodb:
dsn: "root:123456@192.168.3.37:27017/account?connectTimeoutMS=15000"
# or
redis:
dsn: "default:123456@192.168.3.37:6379/0"
postgresql:
dsn: "root:123456@192.168.3.37:5432/account?sslmode=disable"
# mq settings example
kafka:
mode: "producer,consumer"
brokers: ["192.168.3.37:9092"]
rabbitmq:
mode: "consumer"
host: "192.168.3.37"
port: 5672
*/
func ParseYaml(configFile string, isAll bool) (*GenericConfig, error) {
config := &GenericConfig{}
err := conf.Parse(configFile, config)
config.isAll = isAll
return config, err
}
type GenericConfig struct {
App App `yaml:"app" json:"app"`
Grpc Grpc `yaml:"grpc" json:"grpc"`
GrpcClient []GrpcClient `yaml:"grpcClient" json:"grpcClient"`
HTTP HTTP `yaml:"http" json:"http"`
HTTPClient []HTTPClient `yaml:"httpClient" json:"httpClient"`
// database config, match 2 configuration modes, one is separate configuration, the other is unified configuration
Database Database `yaml:"database" json:"database"`
Mysql map[string]interface{} `yaml:"mysql" json:"mysql"`
Redis map[string]interface{} `yaml:"redis" json:"redis"`
Mongodb map[string]interface{} `yaml:"mongodb" json:"mongodb"`
Postgresql map[string]interface{} `yaml:"postgresql" json:"postgresql"`
Sqlite map[string]interface{} `yaml:"sqlite" json:"sqlite"`
Oracle map[string]interface{} `yaml:"oracle" json:"oracle"`
SQLServer map[string]interface{} `yaml:"sqlserver" json:"sqlserver"`
Cassandra map[string]interface{} `yaml:"cassandra" json:"cassandra"`
InfluxDB map[string]interface{} `yaml:"influxdb" json:"influxdb"`
Elasticsearch map[string]interface{} `yaml:"elasticsearch" json:"elasticsearch"`
Clickhouse map[string]interface{} `yaml:"clickhouse" json:"clickhouse"`
Cockroachdb map[string]interface{} `yaml:"cockroachdb" json:"cockroachdb"`
Tidb map[string]interface{} `yaml:"tidb" json:"tidb"`
// add more database
// mq config, match 2 configuration modes, one is separate configuration, the other is unified configuration
MqClient []MqClient `yaml:"mqClient" json:"mqClient"`
Rabbitmq map[string]interface{} `yaml:"rabbitmq" json:"rabbitmq"`
Kafka map[string]interface{} `yaml:"kafka" json:"kafka"`
Activemq map[string]interface{} `yaml:"activemq" json:"activemq"`
Rocketmq map[string]interface{} `yaml:"rocketmq" json:"rocketmq"`
Nats map[string]interface{} `yaml:"nats" json:"nats"`
Nsq map[string]interface{} `yaml:"nsq" json:"nsq"`
Asynq map[string]interface{} `yaml:"asynq" json:"asynq"`
Pulsar map[string]interface{} `yaml:"pulsar" json:"pulsar"`
Zeromq map[string]interface{} `yaml:"zeromq" json:"zeromq"`
// add more mq
isAll bool
}
type HTTPClient struct {
BaseURL string `yaml:"baseURL" json:"baseURL"`
Name string `yaml:"name" json:"name"`
}
type Grpc struct {
HTTPPort int `yaml:"httpPort" json:"httpPort"`
Port int `yaml:"port" json:"port"`
}
type GrpcClient struct {
Host string `yaml:"host" json:"host"`
Name string `yaml:"name" json:"name"`
Port int `yaml:"port" json:"port"`
}
type MqClient struct {
Name string `yaml:"name" json:"name"` // rabbitmq, kafka, activemq, rocketmq, nats, nsq, redis, asynq, pulsar, zeromq, etc.
Mode string `yaml:"mode" json:"mode"` // producer, consumer.
Rabbitmq map[string]interface{} `yaml:"rabbitmq" json:"rabbitmq"`
Kafka map[string]interface{} `yaml:"kafka" json:"kafka"`
Activemq map[string]interface{} `yaml:"activemq" json:"activemq"`
Rocketmq map[string]interface{} `yaml:"rocketmq" json:"rocketmq"`
Nats map[string]interface{} `yaml:"nats" json:"nats"`
Nsq map[string]interface{} `yaml:"nsq" json:"nsq"`
Redis map[string]interface{} `yaml:"redis" json:"redis"`
Asynq map[string]interface{} `yaml:"asynq" json:"asynq"`
Pulsar map[string]interface{} `yaml:"pulsar" json:"pulsar"`
Zeromq map[string]interface{} `yaml:"zeromq" json:"zeromq"`
// add more mq
}
type App struct {
Name string `yaml:"name" json:"name"`
EnableTrace bool `yaml:"enableTrace" json:"enableTrace"`
RegistryDiscoveryType string `yaml:"registryDiscoveryType" json:"registryDiscoveryType"`
CacheType string `yaml:"cacheType" json:"cacheType"`
}
type HTTP struct {
Port int `yaml:"port" json:"port"`
Timeout int `yaml:"timeout" json:"timeout"`
}
type Database struct {
Driver string `yaml:"driver" json:"driver"`
Mysql DbAddr `yaml:"mysql" json:"mysql"`
Redis DbAddr `yaml:"redis" json:"redis"`
Mongodb DbAddr `yaml:"mongodb" json:"mongodb"`
Postgresql DbAddr `yaml:"postgresql" json:"postgresql"`
Sqlite DbAddr `yaml:"sqlite" json:"sqlite"`
Oracle DbAddr `yaml:"oracle" json:"oracle"`
SQLServer DbAddr `yaml:"sqlserver" json:"sqlserver"`
Cassandra DbAddr `yaml:"cassandra" json:"cassandra"`
InfluxDB DbAddr `yaml:"influxdb" json:"influxdb"`
Elasticsearch DbAddr `yaml:"elasticsearch" json:"elasticsearch"`
Clickhouse DbAddr `yaml:"clickhouse" json:"clickhouse"`
Cockroachdb DbAddr `yaml:"cockroachdb" json:"cockroachdb"`
Tidb DbAddr `yaml:"tidb" json:"tidb"`
// add more database
}
type DbAddr struct {
Dsn string `yaml:"dsn" json:"dsn"`
DBFile string `yaml:"dbFile" json:"dbFile"` // sqlite
}
// GetServiceName returns the name of the service.
func (c *GenericConfig) GetServiceName() string {
return c.App.Name
}
// GetServiceType returns the type of the service.
func (c *GenericConfig) GetServiceType() string {
var serviceType string
if c.HTTP.Port > 0 {
serviceType = "http"
} else if c.Grpc.Port > 0 {
serviceType = "grpc"
}
return serviceType
}
// GetDependencies returns the dependencies of the service.
func (c *GenericConfig) GetDependencies(serviceName string) (map[string][]string, []Service) {
mapDependencies := make(map[string][]string)
var httpDependencies []string
for _, client := range c.HTTPClient {
if client.Name != "" {
httpDependencies = append(httpDependencies, client.Name)
}
}
if len(httpDependencies) > 0 {
mapDependencies["http"] = httpDependencies
}
var grpcDependencies []string
for _, client := range c.GrpcClient {
if client.Name != "" && client.Name != c.App.Name && client.Name != "your_grpc_service_name" && client.Name != "your-rpc-server-name" {
grpcDependencies = append(grpcDependencies, client.Name)
}
}
if len(grpcDependencies) > 0 {
mapDependencies["grpc"] = grpcDependencies
}
if c.isAll {
dbDependencies := c.getDBDependencies()
if len(dbDependencies) > 0 {
mapDependencies["db"] = dbDependencies
}
}
mqDependencies, services := c.getMqDependencies(serviceName)
if len(mqDependencies) > 0 {
mapDependencies["mq"] = mqDependencies
}
return mapDependencies, services
}
// nolint
func (c *GenericConfig) getDBDependencies() []string {
db := c.Database
var dbDependencies []string
if db.Mysql.Dsn != "" || len(c.Mysql) > 0 {
dbDependencies = append(dbDependencies, "mysql")
}
if db.Redis.Dsn != "" || len(c.Redis) > 0 {
dbDependencies = append(dbDependencies, "redis")
}
if db.Mongodb.Dsn != "" || len(c.Mongodb) > 0 {
dbDependencies = append(dbDependencies, "mongodb")
}
if db.Postgresql.Dsn != "" || len(c.Postgresql) > 0 {
dbDependencies = append(dbDependencies, "postgresql")
}
if db.Sqlite.DBFile != "" || db.Sqlite.Dsn != "" || len(c.Sqlite) > 0 {
dbDependencies = append(dbDependencies, "sqlite")
}
if db.Oracle.Dsn != "" || len(c.Oracle) > 0 {
dbDependencies = append(dbDependencies, "oracle")
}
if db.SQLServer.Dsn != "" || len(c.SQLServer) > 0 {
dbDependencies = append(dbDependencies, "sqlserver")
}
if db.Cassandra.Dsn != "" || len(c.Cassandra) > 0 {
dbDependencies = append(dbDependencies, "cassandra")
}
if db.InfluxDB.Dsn != "" || len(c.InfluxDB) > 0 {
dbDependencies = append(dbDependencies, "influxdb")
}
if db.Elasticsearch.Dsn != "" || len(c.Elasticsearch) > 0 {
dbDependencies = append(dbDependencies, "elasticsearch")
}
if db.Clickhouse.Dsn != "" || len(c.Clickhouse) > 0 {
dbDependencies = append(dbDependencies, "clickhouse")
}
if db.Cockroachdb.Dsn != "" || len(c.Cockroachdb) > 0 {
dbDependencies = append(dbDependencies, "cockroachdb")
}
if db.Tidb.Dsn != "" || len(c.Tidb) > 0 {
dbDependencies = append(dbDependencies, "tidb")
}
return dbDependencies
}
func (c *GenericConfig) getMqDependencies(serviceName string) ([]string, []Service) {
var mqDependencies []string
var services []Service
for _, client := range c.MqClient {
mqMode := strings.ReplaceAll(client.Mode, " ", "")
mqDependencies, services = addMqDependencies(mqMode, client.Name, serviceName, mqDependencies, services)
}
if len(c.Rabbitmq) > 0 {
mqDependencies, services = addMqDependencies(getMqMode(c.Rabbitmq), "rabbitmq", serviceName, mqDependencies, services)
}
if len(c.Kafka) > 0 {
mqDependencies, services = addMqDependencies(getMqMode(c.Kafka), "kafka", serviceName, mqDependencies, services)
}
if len(c.Activemq) > 0 {
mqDependencies, services = addMqDependencies(getMqMode(c.Activemq), "activemq", serviceName, mqDependencies, services)
}
if len(c.Rocketmq) > 0 {
mqDependencies, services = addMqDependencies(getMqMode(c.Rocketmq), "rocketmq", serviceName, mqDependencies, services)
}
if len(c.Nats) > 0 {
mqDependencies, services = addMqDependencies(getMqMode(c.Nats), "nats", serviceName, mqDependencies, services)
}
if len(c.Nsq) > 0 {
mqDependencies, services = addMqDependencies(getMqMode(c.Nsq), "nsq", serviceName, mqDependencies, services)
}
if len(c.Asynq) > 0 {
mqDependencies, services = addMqDependencies(getMqMode(c.Asynq), "asynq", serviceName, mqDependencies, services)
}
if len(c.Pulsar) > 0 {
mqDependencies, services = addMqDependencies(getMqMode(c.Pulsar), "pulsar", serviceName, mqDependencies, services)
}
if len(c.Zeromq) > 0 {
mqDependencies, services = addMqDependencies(getMqMode(c.Zeromq), "zeromq", serviceName, mqDependencies, services)
}
return mqDependencies, services
}
func getMqMode(mq map[string]interface{}) string {
mqMode := "producer"
if mode, ok := mq["mode"]; ok {
if v, ok2 := mode.(string); ok2 {
mqMode = v
}
}
return mqMode
}
func addMqDependencies(mqMode string, mqName string, serviceName string, mqDependencies []string, services []Service) ([]string, []Service) {
isOnlyConsumer, s := checkMqMode(mqMode, mqName, serviceName)
if !isOnlyConsumer {
mqDependencies = append(mqDependencies, mqName)
}
if s.Name != "" {
services = append(services, s)
}
return mqDependencies, services
}
func checkMqMode(mqMode string, mqName string, serviceName string) (bool, Service) {
isOnlyConsumer := false
switch mqMode {
case "consumer", "producer-consumer", "producer,consumer", "producer|consumer", "producerconsumer":
if mqMode == "consumer" {
isOnlyConsumer = true
}
s := Service{
Name: mqName,
Type: "mq",
Dependencies: map[string][]string{
"mq": {serviceName},
},
}
return isOnlyConsumer, s
}
return isOnlyConsumer, Service{}
}

View File

@ -5,6 +5,7 @@ import (
"fmt"
"os"
"github.com/fatih/color"
"github.com/spf13/cobra"
"github.com/zhufuyi/sponge/cmd/sponge/commands/generate"
@ -19,9 +20,9 @@ var (
func NewRootCMD() *cobra.Command {
cmd := &cobra.Command{
Use: "sponge",
Long: `Sponge is a powerful Go development framework, it's easy to develop web and microservice projects.
repo: https://github.com/zhufuyi/sponge
docs: https://go-sponge.com`,
Long: fmt.Sprintf(`Sponge is a powerful Go development framework, it's easy to develop web and microservice projects.
Repo: %s
Docs: %s`, color.HiCyanString("https://github.com/zhufuyi/sponge"), color.HiCyanString("https://go-sponge.com")),
SilenceErrors: true,
SilenceUsage: true,
Version: getVersion(),
@ -38,6 +39,7 @@ docs: https://go-sponge.com`,
OpenUICommand(),
MergeCommand(),
PatchCommand(),
GenGraphCommand(),
)
return cmd

4
go.mod
View File

@ -105,6 +105,7 @@ require (
github.com/eapache/queue v1.1.0 // indirect
github.com/envoyproxy/go-control-plane v0.11.1 // indirect
github.com/envoyproxy/protoc-gen-validate v1.0.2 // indirect
github.com/fogleman/gg v1.3.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/go-logr/logr v1.4.1 // indirect
@ -116,8 +117,10 @@ require (
github.com/go-openapi/swag v0.19.15 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/goccy/go-graphviz v0.1.3 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
github.com/golang/glog v1.1.2 // indirect
github.com/golang/mock v1.6.0 // indirect
github.com/golang/protobuf v1.5.4 // indirect
@ -202,6 +205,7 @@ require (
go.uber.org/atomic v1.7.0 // indirect
go.uber.org/multierr v1.9.0 // indirect
golang.org/x/arch v0.3.0 // indirect
golang.org/x/image v0.14.0 // indirect
golang.org/x/net v0.25.0 // indirect
golang.org/x/oauth2 v0.14.0 // indirect
golang.org/x/sys v0.23.0 // indirect

8
go.sum
View File

@ -177,6 +177,8 @@ github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w=
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
github.com/felixge/fgprof v0.9.3 h1:VvyZxILNuCiUCSXtPtYmmtGvb65nqXh2QFWc0Wpf2/g=
github.com/felixge/fgprof v0.9.3/go.mod h1:RdbpDgzqYVh/T9fPELJyV7EYJuHB55UTEULNun8eiPw=
github.com/fogleman/gg v1.3.0 h1:/7zJX8F6AaYQc57WQCyN9cAIz+4bCJGO9B+dyW29am8=
github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k=
github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw=
github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE=
github.com/fsnotify/fsnotify v1.5.4 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwVZI=
@ -244,6 +246,8 @@ github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LB
github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc=
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/goccy/go-graphviz v0.1.3 h1:Pkt8y4FBnBNI9tfSobpoN5qy1qMNqRXPQYvLhaSUasY=
github.com/goccy/go-graphviz v0.1.3/go.mod h1:pMYpbAqJT10V8dzV1JN/g/wUlG/0imKPzn3ZsrchGCI=
github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
@ -254,6 +258,8 @@ github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69
github.com/goji/httpauth v0.0.0-20160601135302-2da839ab0f4d/go.mod h1:nnjvkQ9ptGaCkuDUx6wNykzzlUixGxvkme+H/lnzb+A=
github.com/golang-jwt/jwt/v5 v5.0.0 h1:1n1XNM9hk7O9mnQoNBGolZvzebBQ7p93ULHRc28XJUE=
github.com/golang-jwt/jwt/v5 v5.0.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/glog v1.1.2 h1:DVjP2PbBOzHyzA+dn3WhHIq4NdVu3Q+pvivFICf/7fo=
github.com/golang/glog v1.1.2/go.mod h1:zR+okUeTbrL6EL3xHUDxZuEtGv04p5shwip1+mL/rLQ=
@ -787,6 +793,8 @@ golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EH
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.14.0 h1:tNgSxAFe3jC4uYqvZdTr84SZoM1KfwdC9SKIFrLjFn4=
golang.org/x/image v0.14.0/go.mod h1:HUYqC05R2ZcZ3ejNQsIHQDQiwWM4JBqmm6MKANTp4LE=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=

View File

@ -336,3 +336,28 @@ func walkDir(dirPath string, allFiles *[]string) error {
return nil
}
// ListSubDirs list all sub dirs that have the specified sub dir, if sub dir is empty, return all sub dirs
func ListSubDirs(root string, subDir string) ([]string, error) {
var dirs []string
err := filepath.Walk(root, func(dirPath string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if info.IsDir() && hasSubDir(dirPath, subDir) {
if subDir == "" {
dirs = append(dirs, dirPath)
} else {
dirs = append(dirs, dirPath+GetPathDelimiter()+subDir)
}
}
return nil
})
return dirs, err
}
func hasSubDir(dirPath string, subDir string) bool {
_, err := os.Stat(filepath.Join(dirPath, subDir))
return err == nil || os.IsExist(err)
}

View File

@ -170,3 +170,13 @@ func TestListDirs(t *testing.T) {
t.Log(FilterDirs(dirs, WithPrefix("query")))
t.Log(FilterDirs(dirs, WithContain("auth")))
}
func TestListSubDirs(t *testing.T) {
dir := ".."
dirs, err := ListSubDirs(dir, "gin")
if err != nil {
t.Error(err)
return
}
t.Log(dirs)
}