Skip to content

Commit 16c8f84

Browse files
committed
use cobra for cli
1 parent c4def95 commit 16c8f84

6 files changed

Lines changed: 217 additions & 121 deletions

File tree

README.md

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,21 +4,35 @@
44
[![Go Report Card](https://goreportcard.com/badge/github.com/denchenko/messageflow)](https://goreportcard.com/report/github.com/denchenko/messageflow)
55
[![GoDoc](https://godoc.org/github.com/denchenko/messageflow?status.svg)](https://godoc.org/github.com/denchenko/messageflow)
66

7-
MessageFlow is a Go library for visualizing AsyncAPI specifications. It provides tools to parse AsyncAPI documents and transform them into visual formats, making it easier to understand message flows and service interactions in asynchronous systems.
7+
MessageFlow is a Go library and CLI tool for visualizing AsyncAPI specifications. It provides tools to parse AsyncAPI documents and transform them into visual formats, making it easier to understand message flows and service interactions in asynchronous systems.
88

99
Example of visualizing a Notification service using [this](source/asyncapi/testdata/notification.yaml) AsyncAPI specification. Message payloads are displayed as thumbnails when hovering over specific queues. This approach was chosen to keep the schema clean and uncluttered.
1010

1111
![schema](target/d2/testdata/schema.svg)
1212

1313
## Usage
1414

15-
### As a CLI Tool
15+
### CLI
1616

17-
MessageFlow can be used directly from the command line:
17+
MessageFlow provides a command-line interface.
1818

1919
```bash
20-
go install github.com/denchenko/messageflow/cmd/messageflow
21-
messageflow --render-to-file schema.svg --asyncapi-file asyncapi.yaml
20+
go install github.com/denchenko/messageflow/cmd/messageflow@latest
21+
```
22+
23+
#### Generate Schema
24+
25+
The `gen-schema` command processes AsyncAPI files and generates formatted schemas or rendered diagrams:
26+
27+
```bash
28+
# Generate and render a diagram
29+
messageflow gen-schema --target d2 --render-to-file schema.svg --asyncapi-files asyncapi.yaml
30+
31+
# Generate formatted schema only
32+
messageflow gen-schema --format-to-file schema.d2 --asyncapi-files asyncapi.yaml
33+
34+
# Process multiple AsyncAPI files
35+
messageflow gen-schema --render-to-file combined.svg --asyncapi-files "file1.yaml,file2.yaml,file3.yaml"
2236
```
2337

2438
### As a Library
@@ -84,8 +98,3 @@ func main() {
8498
fmt.Println("Diagram generated successfully!")
8599
}
86100
```
87-
88-
## Dependencies
89-
90-
- [asyncapi-codegen](https://github.com/lerenn/asyncapi-codegen) - AsyncAPI code generation
91-
- [d2](https://github.com/terrastruct/d2) - Diagram generation
Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
package schema
2+
3+
import (
4+
"context"
5+
"errors"
6+
"fmt"
7+
"log"
8+
"os"
9+
"strings"
10+
11+
"github.com/denchenko/messageflow"
12+
"github.com/denchenko/messageflow/source/asyncapi"
13+
"github.com/denchenko/messageflow/target/d2"
14+
"github.com/spf13/cobra"
15+
)
16+
17+
type Command struct {
18+
cmd *cobra.Command
19+
}
20+
21+
// NewCommand creates a new gen-schema command
22+
func NewCommand() *Command {
23+
c := &Command{}
24+
25+
c.cmd = &cobra.Command{
26+
Use: "gen-schema",
27+
Short: "Generate schema from AsyncAPI files",
28+
Long: `Generate schema from AsyncAPI files and optionally format or render to output files.
29+
30+
Example:
31+
messageflow gen-schema --target d2 --render-to-file schema.svg --asyncapi-files asyncapi.yaml`,
32+
RunE: c.run,
33+
}
34+
35+
c.cmd.Flags().String("target", "d2", "Target type (d2)")
36+
c.cmd.Flags().String("format-to-file", "", "Output file for the formatted schema")
37+
c.cmd.Flags().String("render-to-file", "", "Output file for the rendered diagram")
38+
c.cmd.Flags().String("asyncapi-files", "", "Paths to asyncapi files separated by comma")
39+
c.cmd.Flags().String("channel", "", "Channel")
40+
c.cmd.Flags().String("service", "", "Service")
41+
c.cmd.Flags().String("format-mode", "service_channels", "Format mode")
42+
43+
// Mark required flags
44+
err := c.cmd.MarkFlagRequired("asyncapi-files")
45+
if err != nil {
46+
log.Fatalf("error marking asyncapi-files flag as required: %v", err)
47+
}
48+
49+
return c
50+
}
51+
52+
// GetCommand returns the cobra command
53+
func (c *Command) GetCommand() *cobra.Command {
54+
return c.cmd
55+
}
56+
57+
// run executes the gen-schema command
58+
func (c *Command) run(cmd *cobra.Command, _ []string) error {
59+
targetType, err := cmd.Flags().GetString("target")
60+
if err != nil {
61+
return fmt.Errorf("error getting target flag: %w", err)
62+
}
63+
64+
formatToFile, err := cmd.Flags().GetString("format-to-file")
65+
if err != nil {
66+
return fmt.Errorf("error getting format-to-file flag: %w", err)
67+
}
68+
69+
renderToFile, err := cmd.Flags().GetString("render-to-file")
70+
if err != nil {
71+
return fmt.Errorf("error getting render-to-file flag: %w", err)
72+
}
73+
74+
asyncAPIFilesPath, err := cmd.Flags().GetString("asyncapi-files")
75+
if err != nil {
76+
return fmt.Errorf("error getting asyncapi-files flag: %w", err)
77+
}
78+
79+
channel, err := cmd.Flags().GetString("channel")
80+
if err != nil {
81+
return fmt.Errorf("error getting channel flag: %w", err)
82+
}
83+
84+
service, err := cmd.Flags().GetString("service")
85+
if err != nil {
86+
return fmt.Errorf("error getting service flag: %w", err)
87+
}
88+
89+
formatMode, err := cmd.Flags().GetString("format-mode")
90+
if err != nil {
91+
return fmt.Errorf("error getting format-mode flag: %w", err)
92+
}
93+
94+
// Validate that at least one output is specified
95+
if formatToFile == "" && renderToFile == "" {
96+
return errors.New("either --format-to-file or --render-to-file must be specified")
97+
}
98+
99+
target, err := pickTarget(targetType)
100+
if err != nil {
101+
return fmt.Errorf("error picking target: %w", err)
102+
}
103+
104+
targetCaps := target.Capabilities()
105+
106+
if !targetCaps.Format {
107+
return errors.New("target doesn't support formatting")
108+
}
109+
110+
if renderToFile != "" && !targetCaps.Render {
111+
return errors.New("target doesn't support render")
112+
}
113+
114+
ctx := context.Background()
115+
116+
filePaths := strings.Split(asyncAPIFilesPath, ",")
117+
schemas := make([]messageflow.Schema, 0, len(filePaths))
118+
119+
for _, filePath := range filePaths {
120+
trimmedPath := strings.TrimSpace(filePath)
121+
s, err := asyncapi.NewSource(trimmedPath)
122+
if err != nil {
123+
return fmt.Errorf("error creating schema source from %s: %w", trimmedPath, err)
124+
}
125+
126+
schema, err := s.ExtractSchema(ctx)
127+
if err != nil {
128+
return fmt.Errorf("error extracting schema from %s: %w", trimmedPath, err)
129+
}
130+
131+
schemas = append(schemas, schema)
132+
}
133+
134+
schema := messageflow.MergeSchemas(schemas...)
135+
136+
formatOpts := messageflow.FormatOptions{
137+
Mode: messageflow.FormatMode(formatMode),
138+
Service: service,
139+
Channel: channel,
140+
}
141+
142+
fs, err := target.FormatSchema(ctx, schema, formatOpts)
143+
if err != nil {
144+
return fmt.Errorf("error formatting schema: %w", err)
145+
}
146+
147+
if formatToFile != "" {
148+
err = os.WriteFile(formatToFile, fs.Data, 0600)
149+
if err != nil {
150+
return fmt.Errorf("error writing to file %s: %w", formatToFile, err)
151+
}
152+
fmt.Printf("Formatted schema written to: %s\n", formatToFile)
153+
}
154+
155+
if renderToFile != "" {
156+
diagram, err := target.RenderSchema(ctx, fs)
157+
if err != nil {
158+
return fmt.Errorf("error rendering schema: %w", err)
159+
}
160+
161+
err = os.WriteFile(renderToFile, diagram, 0600)
162+
if err != nil {
163+
return fmt.Errorf("error writing to file %s: %w", renderToFile, err)
164+
}
165+
fmt.Printf("Rendered diagram written to: %s\n", renderToFile)
166+
}
167+
168+
return nil
169+
}
170+
171+
// pickTarget selects the appropriate target based on the target type
172+
func pickTarget(targetType string) (messageflow.Target, error) {
173+
switch targetType {
174+
case "d2":
175+
return d2.NewTarget()
176+
default:
177+
return nil, fmt.Errorf("unknown target: %s", targetType)
178+
}
179+
}

cmd/messageflow/main.go

Lines changed: 8 additions & 111 deletions
Original file line numberDiff line numberDiff line change
@@ -1,126 +1,23 @@
11
package main
22

33
import (
4-
"context"
5-
"errors"
6-
"flag"
74
"fmt"
85
"os"
9-
"strings"
106

11-
"github.com/denchenko/messageflow"
12-
"github.com/denchenko/messageflow/source/asyncapi"
13-
"github.com/denchenko/messageflow/target/d2"
7+
"github.com/denchenko/messageflow/cmd/messageflow/commands/schema"
8+
"github.com/spf13/cobra"
149
)
1510

1611
func main() {
17-
targetType := flag.String("target", "d2", "Target type (d2)")
18-
formatToFile := flag.String("format-to-file", "", "Output file for the formatted schema")
19-
renderToFile := flag.String("render-to-file", "", "Output file for the rendered diagram")
20-
asyncAPIFilesPath := flag.String("asyncapi-files", "", "Paths to asyncapi files separated by comma")
21-
channel := flag.String("channel", "", "Channel")
22-
service := flag.String("service", "", "Service")
23-
formatMode := flag.String("format-mode", "service_channels", "Format mode")
12+
rootCmd := &cobra.Command{
13+
Use: "messageflow",
14+
Short: "MessageFlow - AsyncAPI schema processing tool",
15+
Long: `MessageFlow is a tool for generating schemas/docs from AsyncAPI schemas.`}
2416

25-
help := flag.Bool("help", false, "Show help")
17+
rootCmd.AddCommand(schema.NewCommand().GetCommand())
2618

27-
flag.Parse()
28-
29-
if *help ||
30-
*targetType == "" ||
31-
(*formatToFile == "" && *renderToFile == "") {
32-
printUsage()
33-
os.Exit(1)
34-
}
35-
36-
target, err := pickTarget(*targetType)
37-
if err != nil {
19+
if err := rootCmd.Execute(); err != nil {
3820
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
3921
os.Exit(1)
4022
}
41-
42-
targetCaps := target.Capabilities()
43-
44-
if !targetCaps.Format {
45-
fmt.Fprintf(os.Stderr, "Error: Target doesn't support formatting\n")
46-
os.Exit(1)
47-
}
48-
49-
if *renderToFile != "" && !targetCaps.Render {
50-
fmt.Fprintf(os.Stderr, "Error: Target doesn't support render\n")
51-
os.Exit(1)
52-
}
53-
54-
ctx := context.Background()
55-
56-
filePaths := strings.Split(*asyncAPIFilesPath, ",")
57-
schemas := make([]messageflow.Schema, 0, len(filePaths))
58-
59-
for _, filePath := range filePaths {
60-
s, err := asyncapi.NewSource(strings.TrimSpace(filePath))
61-
if err != nil {
62-
fmt.Fprintf(os.Stderr, "Error: Creating schema source from %s %v\n", filePath, err)
63-
os.Exit(1)
64-
}
65-
66-
schema, err := s.ExtractSchema(ctx)
67-
if err != nil {
68-
fmt.Fprintf(os.Stderr, "Error: Extracting schema from %s %v\n", filePath, err)
69-
os.Exit(1)
70-
}
71-
72-
schemas = append(schemas, schema)
73-
}
74-
75-
schema := messageflow.MergeSchemas(schemas...)
76-
77-
formatOpts := messageflow.FormatOptions{
78-
Mode: messageflow.FormatMode(*formatMode),
79-
Service: *service,
80-
Channel: *channel,
81-
}
82-
83-
fs, err := target.FormatSchema(ctx, schema, formatOpts)
84-
if err != nil {
85-
fmt.Fprintf(os.Stderr, "Error: Formatting schema %v\n", err)
86-
os.Exit(1)
87-
}
88-
89-
if *formatToFile != "" {
90-
err = os.WriteFile(*formatToFile, fs.Data, 0600)
91-
if err != nil {
92-
fmt.Fprintf(os.Stderr, "Error : Writing to file %v\n", err)
93-
os.Exit(1)
94-
}
95-
}
96-
97-
if *renderToFile != "" {
98-
diagram, err := target.RenderSchema(ctx, fs)
99-
if err != nil {
100-
fmt.Fprintf(os.Stderr, "Error: Rendering schema %v\n", err)
101-
os.Exit(1)
102-
}
103-
104-
err = os.WriteFile(*renderToFile, diagram, 0600)
105-
if err != nil {
106-
fmt.Fprintf(os.Stderr, "Error: Writing to file %v\n", err)
107-
os.Exit(1)
108-
}
109-
}
110-
}
111-
112-
func pickTarget(targetType string) (messageflow.Target, error) {
113-
switch targetType {
114-
case "d2":
115-
return d2.NewTarget()
116-
}
117-
return nil, errors.New("unknown target")
118-
}
119-
120-
func printUsage() {
121-
fmt.Fprintf(os.Stderr, "Usage: messageflow [options]\n\n")
122-
fmt.Fprintf(os.Stderr, "Options:\n")
123-
flag.PrintDefaults()
124-
fmt.Fprintf(os.Stderr, "\nExample:\n")
125-
fmt.Fprintf(os.Stderr, " messageflow --target d2 --render-to-file schema.svg --asyncapi-file asyncapi.yaml\n")
12623
}

go.mod

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,14 @@ require (
2121
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
2222
github.com/google/pprof v0.0.0-20240927180334-d43a67379298 // indirect
2323
github.com/iancoleman/strcase v0.3.0 // indirect
24+
github.com/inconshreveable/mousetrap v1.1.0 // indirect
2425
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
2526
github.com/mazznoer/csscolorparser v0.1.5 // indirect
2627
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect
2728
github.com/pmezard/go-difflib v1.0.0 // indirect
2829
github.com/rivo/uniseg v0.4.7 // indirect
30+
github.com/spf13/cobra v1.9.1 // indirect
31+
github.com/spf13/pflag v1.0.6 // indirect
2932
github.com/yuin/goldmark v1.7.4 // indirect
3033
golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 // indirect
3134
golang.org/x/image v0.20.0 // indirect

0 commit comments

Comments
 (0)