diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ab6648a --- /dev/null +++ b/.gitignore @@ -0,0 +1,36 @@ +## Intellij +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/encodings.xml +.idea/**/compiler.xml +.idea/**/misc.xml +.idea/**/modules.xml +.idea/**/vcs.xml + +## VSCode +.vscode/ + +## File-based project format: +*.iws +*.iml +.idea/ + +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib +*.dat +*.DS_Store +go.sum + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Goreleaser builds +dist/** + diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..c1d8e80 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 Luca Sepe + +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. diff --git a/README.md b/README.md new file mode 100644 index 0000000..2c3cf8e --- /dev/null +++ b/README.md @@ -0,0 +1,261 @@ +# Draft + +A commandline tool that generate **H**igh **L**evel microservice & serverless **A**rchitecture diagrams using a declarative syntax defined in a YAML file. + +- Works on Linux, Mac OSX, Windows +- Just a single portable binary file +- It Does One Thing Well +- Input data in flat YAML text files +- Usable with shell scripts +- Can take input from pipes `cat` + +## How `draft` works? + +`draft` takes in input a declarative YAML file and generates a [`dot`](https://en.wikipedia.org/wiki/DOT_(graph_description_language)) script for [Graphviz](https://www.graphviz.org/) + +```bash +draft backend-for-frontend.yml | dot -Tpng -Gdpi=200 > backend-for-frontend.png +``` + +Piping the `draft` output to [GraphViz](http://www.graphviz.org/doc/info/output.html/) `dot` you can generate the following output formats: + +| format | command | +|:-------------|:---------------------------------------------------------------| +| GIF | draft input.yml | dot -Tgif > output.gif | +| JPEG | draft input.yml | dot -Tjpg > output.jpg | +| PostScript | draft input.yml | dot -Tps > output.ps | +| PSD | draft input.yml | dot -Tpsd > output.psd | +| SVG | draft input.yml | dot -Tsvg > output.svg | +| WebP | draft input.yml | dot -Twebp > output.webp | + +To install GraphViz to your favorite OS, please, follow this link [https://graphviz.gitlab.io/download/](https://graphviz.gitlab.io/download/). + +## Components + +### A picture is worth a thousand words + +... and this is particularly true in regard to complex IT architectures. + +The basic unit of each _draft_ design is the `component`: + +```go +type Component struct { + ID string `yaml:"id,omitempty"` // optional - autogenerated if omitted (read more for details...) + Kind string `yaml:"kind"` // required (one of: service, gateway, queue, broker, function, storage, database) + Label string `yaml:"label,omitempty"` // optional - the component description (or scope) + Provider string `yaml:"provider,omitempty"` // optional - you can use this to specify the implementation + FillColor string `yaml:"fillColor,omitempty"` // optional - the hex code for the background color + FontColor string `yaml:"fontColor,omitempty"` // optional - the hex code for the foreground color + Rounded bool `yaml:"rounded,omitempty"` // optional - set to true if you wants rounded shapes +} +``` + +Draft uses a set of symbols independent from the different providers (AWS, Microsoft Azure, GCP). + +- you can eventually describe the implementation using the `provider` attribute. + +Below is a list of all the components currently implemented. + +| Component | Kind | YAML | Output | +|:-------------------|:------------|:--------------------------|:--------------------------------:| +| **Client** | `client` | ![](./examples/cl.jpg) | ![](./examples/client.png) | +| **Microservice** | `service` | ![](./examples/ms.jpg) | ![](./examples/service.png) | +| **Gateway** | `gateway` | ![](./examples/gt.jpg) | ![](./examples/gateway.png) | +| **Message Broker** | `broker` | ![](./examples/br.jpg) | ![](./examples/broker.png) | +| **Queue Service** | `queue` | ![](./examples/qs.jpg) | ![](./examples/queue.png) | +| **Object Storage** | `storage` | ![](./examples/st.jpg) | ![](./examples/storage.png) | +| **Function** | `function` | ![](./examples/fn.jpg) | ![](./examples/function.png) | +| **Database** | `database` | ![](./examples/db.jpg) | ![](./examples/database.png) | + +## Connections + +You can connect each component by arrows. + +To be able to connect an _origin component_ with one or more _target component_ you need to specify each `componentId`. + +- you can define your component `id` explicitly +- you can omit the component `id` attribute and it will be autogenerated + +### Autogenerated `id` + +An autogenerated `id` has a prefix and a sequential number + +- the prefix is related to the component `kind` + +Aautogenerated `id` prefix mapping. + +| a kind of... | will generate an `id` prefix with... | examples | +|:-------------|:-------------------------------------|:---------------| +| `client` | `cl` | `cl1, cl2,...` | +| `service` | `ms` | `ms1, ms2,...` | +| `gateway` | `gt` | `gt1, gt2,...` | +| `broker` | `br` | `br1, br2,...` | +| `queue` | `qs` | `qs1, qs2,...` | +| `storage` | `st` | `st1, st2,...` | +| `function` | `fn` | `fn1, fn2,...` | +| `database` | `db` | `db1, db2,...` | + +A `connection` has the following properties: + +```go +type Connection struct { + Origin struct { + ComponentID string `yaml:"componentId"` + } `yaml:"origin"` + Targets []struct { + ComponentID string `yaml:"componentId"` + Label string `yaml:"label,omitempty"` + Color string `yaml:"color,omitempty"` + Dashed bool `yaml:"dashed,omitempty"` + Dir string `yaml:"dir,omitempty"` + Highlight bool `yaml:"highlight,omitempty"` + } `yaml:"targets"` +} +``` + +## Example 1 - Message Bus Pattern + +Create the `draft` architecture descriptor YAML with your favorite editor: + +```yaml +title: message bus pattern +backgroundColor: '#ffffff' +components: + - + kind: service + label: Producer + provider: AWS EC2 + - + kind: broker + label: "Notification\nService" + provider: AWS SNS + - + kind: queue + label: "event queue @ topic 1" + provider: AWS SQS + - + kind: queue + label: "event queue @ topic 2" + provider: AWS SQS + - + kind: service + label: "Consumer\n@ topic 1" + provider: AWS EC2 + - + kind: service + label: "Consumer\n@ topic 2" + provider: AWS EC2 +connections: + - + origin: + componentId: ms1 + targets: + - + componentId: br1 + - + origin: + componentId: br1 + targets: + - + componentId: qs1 + dashed: true + - + componentId: qs2 + dashed: true + - + origin: + componentId: qs1 + targets: + - + componentId: ms2 + dir: back + - + origin: + componentId: qs2 + targets: + - + componentId: ms3 + dir: back +``` + +Then run `draft`: + +```bash +draft message-bus-pattern.yml | dot -Tpng > message-bus-pattern.png +``` + +Here the generated output: + +![](./examples/message-bus-pattern.png) + + +## Example 2 - AWS Cognito Custom Authentication Flow + +Create the draft architecture descriptor YAML with your favorite editor: + +```yaml +title: Amazon Cognito Custom Authentication Flow with external database +backgroundColor: '#ffffff' +components: + - + kind: client + label: "Web App" + - + kind: client + label: "Mobile App" + - + kind: service + label: "Cognito" + provider: "AWS Cognito" + fillColor: '#991919' + fontColor: '#fafafa' + - + kind: function + label: "Define\nAuthChallange" + provider: "AWS Lambda" + - + kind: function + label: "Create\nAuthChallange" + provider: "AWS Lambda" + - + kind: function + label: "Verify\nAuthChallange" + provider: "AWS Lambda" + - + kind: database + label: "Users\nRepository" + provider: "AWS RDS" +connections: + - + origin: + componentId: cl1 + targets: + - + componentId: ms1 + - + origin: + componentId: cl2 + targets: + - + componentId: ms1 + - + origin: + componentId: ms1 + targets: + - + componentId: fn1 + - + componentId: fn2 + - + componentId: fn3 + - + origin: + componentId: fn2 + targets: + - + componentId: db1 +``` + +Here the generated output: + +![](./examples/aws-cognito-custom-auth-flow.png) \ No newline at end of file diff --git a/broker.go b/broker.go new file mode 100644 index 0000000..41a1656 --- /dev/null +++ b/broker.go @@ -0,0 +1,54 @@ +package draft + +import ( + "fmt" + "strings" + + "github.com/emicklei/dot" + "github.com/lucasepe/draft/pkg/cluster" + "github.com/lucasepe/draft/pkg/node" +) + +type broker struct { + seq int16 +} + +func (rcv *broker) nextID() string { + rcv.seq++ + return fmt.Sprintf("br%d", rcv.seq) +} + +func (rcv *broker) sketch(graph *dot.Graph, comp Component) { + id := comp.ID + if strings.TrimSpace(comp.ID) == "" { + id = rcv.nextID() + } + + label := comp.Label + if strings.TrimSpace(comp.Label) == "" { + label = "Message Broker" + } + + cl := cluster.New(graph, id, cluster.Label(comp.Provider)) + + el := node.New(cl, id, + node.Label(label), + node.Rounded(comp.Rounded), + node.FontColor(comp.FontColor), + node.FillColor(comp.FillColor, "#e0eeeeff"), + node.Shape("cds"), + ) + el.Attr("height", "0.8") +} + +/** Alternative + +label= + + + + +
 topic 1 
 topic 2 
 ... 
 topic N 
> +shape="plain" + +**/ diff --git a/client.go b/client.go new file mode 100644 index 0000000..3e68abb --- /dev/null +++ b/client.go @@ -0,0 +1,38 @@ +package draft + +import ( + "fmt" + "strings" + + "github.com/emicklei/dot" + "github.com/lucasepe/draft/pkg/cluster" + "github.com/lucasepe/draft/pkg/node" +) + +type client struct { + seq int16 +} + +func (rcv *client) nextID() string { + rcv.seq++ + return fmt.Sprintf("cl%d", rcv.seq) +} + +func (rcv *client) sketch(graph *dot.Graph, comp Component) { + id := comp.ID + if strings.TrimSpace(comp.ID) == "" { + id = rcv.nextID() + } + + cl := cluster.New(graph, id, cluster.Label(comp.Provider)) + + el := node.New(cl, id, + node.Label(comp.Label), + node.Rounded(comp.Rounded), + node.FontColor(comp.FontColor), + node.FillColor(comp.FillColor, "#90ee90ff"), + node.Shape("underline"), + ) + el.Attr("fontsize", "8") + el.Attr("height", "0.3") +} diff --git a/cmd/.gitignore b/cmd/.gitignore new file mode 100644 index 0000000..3c46005 --- /dev/null +++ b/cmd/.gitignore @@ -0,0 +1,38 @@ +## Intellij +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/encodings.xml +.idea/**/compiler.xml +.idea/**/misc.xml +.idea/**/modules.xml +.idea/**/vcs.xml + +## File-based project format: +*.iws +*.iml +.idea/ + +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib +*.dat +*.DS_Store +go.sum + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Goreleaser builds +dist/** + +# Goreleaser file +.goreleaser.yml + +# Custom Bash Scripts +draft-all.sh diff --git a/cmd/main.go b/cmd/main.go new file mode 100644 index 0000000..0ccee21 --- /dev/null +++ b/cmd/main.go @@ -0,0 +1,95 @@ +package main + +import ( + "flag" + "fmt" + "io/ioutil" + "os" + "path/filepath" + "strings" + + "github.com/lucasepe/draft" +) + +// go run main.go | dot -Tpng -Gdpi=300 > test.png + +const ( + maxFileSize = 500 * 1024 + banner = ` +______ __ _ +| _ \ / _|| | Crafted with passion by Luca Sepe +| | | | _ __ __ _ | |_ | |_ +| | | || '__| / _' || _|| __| https://github.com/lucasepe/draft +| |/ / | | | (_| || | | |_ +|___/ |_| \__,_||_| \__| {{VERSION}}` +) + +var ( + version = "dev" + commit = "none" + date = "unknown" +) + +func main() { + configureFlags() + + if flag.CommandLine.Arg(0) == "" { + flag.CommandLine.Usage() + os.Exit(2) + } + + fn, err := filepath.Abs(flag.Args()[0]) + handleErr(err) + + file, err := os.Open(fn) + handleErr(err) + + defer file.Close() + + ark, err := draft.NewDraft(file) + handleErr(err) + + str, err := ark.Sketch() + handleErr(err) + + fmt.Println(str) +} + +// handleErr check for an error and eventually exit +func handleErr(err error) { + if err != nil { + fmt.Fprintf(os.Stderr, "error: %s\n", err.Error()) + os.Exit(1) + } +} + +func configureFlags() { + flag.CommandLine.Usage = func() { + printBanner() + fmt.Printf("Generate High Level Microservice Architecture diagrams for GraphViz using simple YAML syntax.\n\n") + + name := filepath.Base(os.Args[0]) + + fmt.Print("USAGE:\n\n") + fmt.Printf(" %s [options] /path/to/yaml/file\n\n", name) + + fmt.Print("EXAMPLE:\n\n") + fmt.Printf(" %s input.yml | dot -Tpng -Gdpi=200 > output.png\n\n", name) + + fmt.Print("OPTIONS:\n\n") + flag.CommandLine.SetOutput(os.Stdout) + flag.CommandLine.PrintDefaults() + flag.CommandLine.SetOutput(ioutil.Discard) // hide flag errors + fmt.Print(" -help\n\tprints this message\n") + fmt.Println() + } + + flag.CommandLine.SetOutput(ioutil.Discard) // hide flag errors + flag.CommandLine.Init(os.Args[0], flag.ExitOnError) + + flag.CommandLine.Parse(os.Args[1:]) +} + +func printBanner() { + fmt.Print(strings.Trim(strings.Replace(banner, "{{VERSION}}", version, 1), "\n"), "\n\n") +} diff --git a/database.go b/database.go new file mode 100644 index 0000000..42990e8 --- /dev/null +++ b/database.go @@ -0,0 +1,38 @@ +package draft + +import ( + "fmt" + "strings" + + "github.com/emicklei/dot" + "github.com/lucasepe/draft/pkg/cluster" + "github.com/lucasepe/draft/pkg/node" +) + +type database struct { + seq int16 +} + +func (rcv *database) nextID() string { + rcv.seq++ + return fmt.Sprintf("db%d", rcv.seq) +} + +func (rcv *database) sketch(graph *dot.Graph, comp Component) { + id := comp.ID + if strings.TrimSpace(comp.ID) == "" { + id = rcv.nextID() + } + + cl := cluster.New(graph, id, cluster.Label(comp.Provider)) + + el := node.New(cl, id, + node.Label(comp.Label), + node.Rounded(comp.Rounded), + node.FontColor(comp.FontColor), + node.FillColor(comp.FillColor, "#f5f5dcff"), + node.Shape("cylinder"), + ) + el.Attr("height", "0.5") + el.Attr("fontsize", "6") +} diff --git a/draft.go b/draft.go new file mode 100644 index 0000000..887d87f --- /dev/null +++ b/draft.go @@ -0,0 +1,137 @@ +package draft + +import ( + "fmt" + "io" + + "github.com/emicklei/dot" + "github.com/lucasepe/draft/pkg/edge" + "github.com/lucasepe/draft/pkg/graph" + "gopkg.in/yaml.v2" +) + +const ( + kindClient = "client" + kindGateway = "gateway" + kindService = "service" + kindQueue = "queue" + kindBroker = "broker" + kindStorage = "storage" + kindDatabase = "database" + kindFunction = "function" +) + +// Connection is a link between two components. +type Connection struct { + Origin struct { + ComponentID string `yaml:"componentId"` + } `yaml:"origin"` + Targets []struct { + ComponentID string `yaml:"componentId"` + Label string `yaml:"label,omitempty"` + Color string `yaml:"color,omitempty"` + Dashed bool `yaml:"dashed,omitempty"` + Dir string `yaml:"dir,omitempty"` + Highlight bool `yaml:"highlight,omitempty"` + } `yaml:"targets"` +} + +// Component is a basic architecture unit. +type Component struct { + ID string `yaml:"id,omitempty"` + Kind string `yaml:"kind"` + Label string `yaml:"label,omitempty"` + Provider string `yaml:"provider,omitempty"` + FillColor string `yaml:"fillColor,omitempty"` + FontColor string `yaml:"fontColor,omitempty"` + Rounded bool `yaml:"rounded,omitempty"` +} + +// Draft represents a whole diagram. +type Draft struct { + Title string `yaml:"title,omitempty"` + BackgroundColor string `yaml:"backgroundColor,omitempty"` + Components []Component `yaml:"components"` + Connections []Connection `yaml:"connections,omitempty"` + + sketchers map[string]interface { + sketch(*dot.Graph, Component) + } +} + +// NewDraft returns a new decoded Draft struct +func NewDraft(r io.Reader) (*Draft, error) { + res := &Draft{ + sketchers: map[string]interface { + sketch(*dot.Graph, Component) + }{ + kindClient: &client{}, + kindGateway: &gateway{}, + kindService: &service{}, + kindBroker: &broker{}, + kindQueue: &queue{}, + kindFunction: &function{}, + kindStorage: &storage{}, + kindDatabase: &database{}, + }, + } + + // Init new YAML decode + d := yaml.NewDecoder(r) + + // Start YAML decoding from file + if err := d.Decode(&res); err != nil { + return nil, err + } + + return res, nil +} + +// Sketch generates the GraphViz definition for this architecture diagram. +func (ark *Draft) Sketch() (string, error) { + g := graph.New(graph.BackgroundColor(ark.BackgroundColor), graph.Label(ark.Title)) + + if err := sketchComponents(g, ark); err != nil { + return "", err + } + + if err := sketchConnections(g, ark); err != nil { + return "", err + } + + return g.String(), nil +} + +func sketchComponents(graph *dot.Graph, draft *Draft) error { + for _, el := range draft.Components { + sketcher, ok := draft.sketchers[el.Kind] + if !ok { + return fmt.Errorf("render not found for component of kind '%s'", el.Kind) + } + + sketcher.sketch(graph, el) + } + + return nil +} + +func sketchConnections(graph *dot.Graph, draft *Draft) error { + for _, el := range draft.Connections { + var from = el.Origin.ComponentID + + for _, x := range el.Targets { + err := edge.New(graph, from, x.ComponentID, + edge.Label(x.Label), + edge.Dir(x.Dir), + edge.Color(x.Color), + edge.Dashed(x.Dashed), + edge.Highlight(x.Highlight)) + + if err != nil { + return err + } + } + } + + return nil +} diff --git a/examples/aws-cognito-custom-auth-flow.png b/examples/aws-cognito-custom-auth-flow.png new file mode 100644 index 0000000..1354b1f Binary files /dev/null and b/examples/aws-cognito-custom-auth-flow.png differ diff --git a/examples/aws-cognito-custom-auth-flow.yml b/examples/aws-cognito-custom-auth-flow.yml new file mode 100644 index 0000000..6eff08e --- /dev/null +++ b/examples/aws-cognito-custom-auth-flow.yml @@ -0,0 +1,62 @@ +title: Amazon Cognito Custom Authentication Flow with external database +backgroundColor: '#ffffff' +components: + - + kind: client + label: "Web App" + provider: SPA + - + kind: client + label: "Mobile App" + provider: "Android & iOS" + - + kind: service + label: "Cognito" + provider: "AWS Cognito" + fillColor: '#991919' + fontColor: '#fafafa' + - + kind: function + label: "Define\nAuthChallange" + provider: "AWS Lambda" + - + kind: function + label: "Create\nAuthChallange" + provider: "AWS Lambda" + - + kind: function + label: "Verify\nAuthChallange" + provider: "AWS Lambda" + - + kind: database + label: "Users\nRepository" + provider: "AWS RDS" +connections: + - + origin: + componentId: cl1 + targets: + - + componentId: ms1 + - + origin: + componentId: cl2 + targets: + - + componentId: ms1 + - + origin: + componentId: ms1 + targets: + - + componentId: fn1 + - + componentId: fn2 + - + componentId: fn3 + - + origin: + componentId: fn2 + targets: + - + componentId: db1 \ No newline at end of file diff --git a/examples/backend-for-frontend.png b/examples/backend-for-frontend.png new file mode 100644 index 0000000..2174d9c Binary files /dev/null and b/examples/backend-for-frontend.png differ diff --git a/examples/backend-for-frontend.yml b/examples/backend-for-frontend.yml new file mode 100644 index 0000000..fc59986 --- /dev/null +++ b/examples/backend-for-frontend.yml @@ -0,0 +1,79 @@ +title: Backend For Frontend (BFF) +backgroundColor: '#ffffff' +components: + - + kind: client + label: Web App + fillColor: '#ee82ee' + - + kind: client + label: Mobile App + fillColor: '#708090' + - + kind: gateway + label: "Web BFF\nAPI Gateway" + - + kind: gateway + label: "Mobile BFF\nAPI Gateway" + - + kind: service + label: μService A + fillColor: '#b0e0e6' + - + kind: service + label: μService B + - + kind: service + label: μService C + fillColor: '#00ff7f' + - + kind: service + label: μService D + fillColor: '#00007f' + fontColor: '#fafafa' +connections: + - + origin: + componentId: cl1 + targets: + - + componentId: gt1 + color: '#ee82ee' + - + origin: + componentId: cl2 + targets: + - + componentId: gt2 + - + origin: + componentId: gt1 + targets: + - + componentId: ms1 + color: '#ee82ee' + - + componentId: ms2 + color: '#ee82ee' + - + componentId: ms3 + color: '#ee82ee' + - + componentId: ms4 + color: '#ee82ee' + - + origin: + componentId: gt2 + targets: + - + componentId: ms1 + highlight: true + - + componentId: ms2 + highlight: true + - + componentId: ms3 + highlight: true + - + componentId: ms4 + highlight: true \ No newline at end of file diff --git a/examples/br.jpg b/examples/br.jpg new file mode 100644 index 0000000..2d7c08a Binary files /dev/null and b/examples/br.jpg differ diff --git a/examples/broker.png b/examples/broker.png new file mode 100644 index 0000000..6adb5c7 Binary files /dev/null and b/examples/broker.png differ diff --git a/examples/broker.yml b/examples/broker.yml new file mode 100644 index 0000000..2ceb258 --- /dev/null +++ b/examples/broker.yml @@ -0,0 +1,5 @@ +--- +components: + - + kind: broker + label: "Message Broker" \ No newline at end of file diff --git a/examples/broker_impl.yml b/examples/broker_impl.yml new file mode 100644 index 0000000..bbfd5ae --- /dev/null +++ b/examples/broker_impl.yml @@ -0,0 +1,6 @@ +--- +components: + - + kind: broker + label: "Message Broker" + provider: AWS SNS \ No newline at end of file diff --git a/examples/cl.jpg b/examples/cl.jpg new file mode 100644 index 0000000..9859aeb Binary files /dev/null and b/examples/cl.jpg differ diff --git a/examples/cl.png b/examples/cl.png new file mode 100644 index 0000000..dfd2e6e Binary files /dev/null and b/examples/cl.png differ diff --git a/examples/client.png b/examples/client.png new file mode 100644 index 0000000..875542f Binary files /dev/null and b/examples/client.png differ diff --git a/examples/client.yml b/examples/client.yml new file mode 100644 index 0000000..a656f77 --- /dev/null +++ b/examples/client.yml @@ -0,0 +1,6 @@ +--- +components: + - + kind: client + label: "Web App" + fillColor: '#ee82ee' diff --git a/examples/client_impl.yml b/examples/client_impl.yml new file mode 100644 index 0000000..5989bfb --- /dev/null +++ b/examples/client_impl.yml @@ -0,0 +1,6 @@ +--- +components: + - + kind: client + label: "Web App" + provider: SPA diff --git a/examples/database.png b/examples/database.png new file mode 100644 index 0000000..3e92d28 Binary files /dev/null and b/examples/database.png differ diff --git a/examples/database.yml b/examples/database.yml new file mode 100644 index 0000000..55060fb --- /dev/null +++ b/examples/database.yml @@ -0,0 +1,5 @@ +--- +components: + - + kind: database + label: "Users\nRepository" diff --git a/examples/database_impl.yml b/examples/database_impl.yml new file mode 100644 index 0000000..6d464cc --- /dev/null +++ b/examples/database_impl.yml @@ -0,0 +1,6 @@ +--- +components: + - + kind: database + label: "Users\nRepository" + provider: AWS RDS diff --git a/examples/db.jpg b/examples/db.jpg new file mode 100644 index 0000000..dedfdea Binary files /dev/null and b/examples/db.jpg differ diff --git a/examples/fn.jpg b/examples/fn.jpg new file mode 100644 index 0000000..d5b33d4 Binary files /dev/null and b/examples/fn.jpg differ diff --git a/examples/function.png b/examples/function.png new file mode 100644 index 0000000..12c3847 Binary files /dev/null and b/examples/function.png differ diff --git a/examples/function.yml b/examples/function.yml new file mode 100644 index 0000000..06dbe4c --- /dev/null +++ b/examples/function.yml @@ -0,0 +1,5 @@ +--- +components: + - + kind: function + label: "Create\nAuth Challange" diff --git a/examples/function_impl.yml b/examples/function_impl.yml new file mode 100644 index 0000000..58f7ba1 --- /dev/null +++ b/examples/function_impl.yml @@ -0,0 +1,6 @@ +--- +components: + - + kind: function + label: "Create\nAuth Challange" + provider: AWS Lambda diff --git a/examples/gateway.png b/examples/gateway.png new file mode 100644 index 0000000..24b5695 Binary files /dev/null and b/examples/gateway.png differ diff --git a/examples/gateway.yml b/examples/gateway.yml new file mode 100644 index 0000000..c669810 --- /dev/null +++ b/examples/gateway.yml @@ -0,0 +1,6 @@ +--- +components: + - + kind: gateway + label: "API Gateway" + fontColor: '#fafafaff' \ No newline at end of file diff --git a/examples/gateway_impl.yml b/examples/gateway_impl.yml new file mode 100644 index 0000000..9f6ade4 --- /dev/null +++ b/examples/gateway_impl.yml @@ -0,0 +1,7 @@ +--- +components: + - + kind: gateway + label: "API Gateway" + fontColor: '#fafafaff' + provider: "AWS API\nGateway" \ No newline at end of file diff --git a/examples/gt.jpg b/examples/gt.jpg new file mode 100644 index 0000000..27631b5 Binary files /dev/null and b/examples/gt.jpg differ diff --git a/examples/message-bus-pattern.png b/examples/message-bus-pattern.png new file mode 100644 index 0000000..d19fabf Binary files /dev/null and b/examples/message-bus-pattern.png differ diff --git a/examples/message-bus-pattern.yml b/examples/message-bus-pattern.yml new file mode 100644 index 0000000..cd64f1a --- /dev/null +++ b/examples/message-bus-pattern.yml @@ -0,0 +1,58 @@ +title: message bus pattern +backgroundColor: '#ffffff' +components: + - + kind: service + label: Producer + provider: AWS EC2 + - + kind: broker + label: "Notification\nService" + provider: AWS SNS + - + kind: queue + label: "event queue @ topic 1" + provider: AWS SQS + - + kind: queue + label: "event queue @ topic 2" + provider: AWS SQS + - + kind: service + label: "Consumer\n@ topic 1" + provider: AWS EC2 + - + kind: service + label: "Consumer\n@ topic 2" + provider: AWS EC2 +connections: + - + origin: + componentId: ms1 + targets: + - + componentId: br1 + - + origin: + componentId: br1 + targets: + - + componentId: qs1 + dashed: true + - + componentId: qs2 + dashed: true + - + origin: + componentId: qs1 + targets: + - + componentId: ms2 + dir: back + - + origin: + componentId: qs2 + targets: + - + componentId: ms3 + dir: back diff --git a/examples/ms.jpg b/examples/ms.jpg new file mode 100644 index 0000000..d48fe24 Binary files /dev/null and b/examples/ms.jpg differ diff --git a/examples/qs.jpg b/examples/qs.jpg new file mode 100644 index 0000000..ffaf71b Binary files /dev/null and b/examples/qs.jpg differ diff --git a/examples/queue.png b/examples/queue.png new file mode 100644 index 0000000..313c627 Binary files /dev/null and b/examples/queue.png differ diff --git a/examples/queue.yml b/examples/queue.yml new file mode 100644 index 0000000..1b3baab --- /dev/null +++ b/examples/queue.yml @@ -0,0 +1,5 @@ +--- +components: + - + kind: queue + label: "event queue" \ No newline at end of file diff --git a/examples/queue_impl.yml b/examples/queue_impl.yml new file mode 100644 index 0000000..a1336b7 --- /dev/null +++ b/examples/queue_impl.yml @@ -0,0 +1,6 @@ +--- +components: + - + kind: queue + label: "event queue" + provider: "AWS SQS" \ No newline at end of file diff --git a/examples/service.png b/examples/service.png new file mode 100644 index 0000000..56fe2a7 Binary files /dev/null and b/examples/service.png differ diff --git a/examples/service.yml b/examples/service.yml new file mode 100644 index 0000000..705a759 --- /dev/null +++ b/examples/service.yml @@ -0,0 +1,5 @@ +--- +components: + - + kind: service + label: "μService" diff --git a/examples/service_impl.yml b/examples/service_impl.yml new file mode 100644 index 0000000..c7f6080 --- /dev/null +++ b/examples/service_impl.yml @@ -0,0 +1,6 @@ +--- +components: + - + kind: service + label: "μService" + provider: AWS EC2 diff --git a/examples/st.jpg b/examples/st.jpg new file mode 100644 index 0000000..3e103a1 Binary files /dev/null and b/examples/st.jpg differ diff --git a/examples/storage.png b/examples/storage.png new file mode 100644 index 0000000..c945571 Binary files /dev/null and b/examples/storage.png differ diff --git a/examples/storage.yml b/examples/storage.yml new file mode 100644 index 0000000..8a0870e --- /dev/null +++ b/examples/storage.yml @@ -0,0 +1,5 @@ +--- +components: + - + kind: storage + label: "*.jpg\n*.png" \ No newline at end of file diff --git a/examples/storage_impl.yml b/examples/storage_impl.yml new file mode 100644 index 0000000..dc539bc --- /dev/null +++ b/examples/storage_impl.yml @@ -0,0 +1,6 @@ +--- +components: + - + kind: storage + label: "*.jpg\n*.png" + provider: "AWS S3" \ No newline at end of file diff --git a/function.go b/function.go new file mode 100644 index 0000000..5642f20 --- /dev/null +++ b/function.go @@ -0,0 +1,38 @@ +package draft + +import ( + "fmt" + "strings" + + "github.com/emicklei/dot" + "github.com/lucasepe/draft/pkg/cluster" + "github.com/lucasepe/draft/pkg/node" +) + +type function struct { + seq int16 +} + +func (rcv *function) nextID() string { + rcv.seq++ + return fmt.Sprintf("fn%d", rcv.seq) +} + +func (rcv *function) sketch(graph *dot.Graph, comp Component) { + id := comp.ID + if strings.TrimSpace(comp.ID) == "" { + id = rcv.nextID() + } + + cl := cluster.New(graph, id, cluster.Label(comp.Provider)) + + el := node.New(cl, id, + node.Label(comp.Label), + node.Rounded(comp.Rounded), + node.FontColor(comp.FontColor), + node.FillColor(comp.FillColor, "#abd9e9ff"), + node.Shape("signature"), + ) + el.Attr("fontsize", "6") + el.Attr("height", "0.5") +} diff --git a/gateway.go b/gateway.go new file mode 100644 index 0000000..c61ce98 --- /dev/null +++ b/gateway.go @@ -0,0 +1,43 @@ +package draft + +import ( + "fmt" + "strings" + + "github.com/emicklei/dot" + "github.com/lucasepe/draft/pkg/cluster" + "github.com/lucasepe/draft/pkg/node" +) + +type gateway struct { + seq int16 +} + +func (rcv *gateway) nextID() string { + rcv.seq++ + return fmt.Sprintf("gt%d", rcv.seq) +} + +func (rcv *gateway) sketch(graph *dot.Graph, comp Component) { + id := comp.ID + if strings.TrimSpace(comp.ID) == "" { + id = rcv.nextID() + } + + label := comp.Label + if strings.TrimSpace(comp.Label) == "" { + label = "API Gateway" + } + + cl := cluster.New(graph, id, cluster.Label(comp.Provider)) + + el := node.New(cl, id, + node.Label(label), + node.Rounded(comp.Rounded), + node.FontColor(comp.FontColor), + node.FillColor(comp.FillColor, "#ff7f00ff"), + node.FontSize(7), + node.Shape("point"), + ) + el.Attr("width", "0.3") +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..65e2863 --- /dev/null +++ b/go.mod @@ -0,0 +1,8 @@ +module github.com/lucasepe/draft + +go 1.14 + +require ( + github.com/emicklei/dot v0.11.0 + gopkg.in/yaml.v2 v2.3.0 +) diff --git a/pkg/cluster/cluster.go b/pkg/cluster/cluster.go new file mode 100644 index 0000000..170565b --- /dev/null +++ b/pkg/cluster/cluster.go @@ -0,0 +1,65 @@ +package cluster + +import ( + "fmt" + "strings" + + "github.com/emicklei/dot" +) + +type Attribute func(*dot.Graph) + +func Label(label string) Attribute { + return func(el *dot.Graph) { + if strings.TrimSpace(label) != "" { + el.Attr("label", label) + el.Attr("pencolor", "#f5deb3") + el.Attr("style", "dashed") + } + } +} + +func PenColor(color string) Attribute { + return func(el *dot.Graph) { + if strings.TrimSpace(color) != "" { + el.Attr("pencolor", color) + } + } +} + +func FontColor(color string) Attribute { + return func(el *dot.Graph) { + if strings.TrimSpace(color) != "" { + el.Attr("fontcolor", color) + } else { + el.Attr("fontcolor", "#000000ff") + } + } +} + +func FontName(name string) Attribute { + return func(el *dot.Graph) { + el.Attr("fontname", name) + } +} + +func FontSize(size float32) Attribute { + return func(el *dot.Graph) { + fs := fmt.Sprintf("%.2f", size) + el.Attr("fontsize", fs) + } +} + +func New(parent *dot.Graph, id string, attrs ...Attribute) *dot.Graph { + cluster := parent.Subgraph(id, dot.ClusterOption{}) + + // default attributes + FontName("Fira Mono Bold")(cluster) + FontSize(9)(cluster) + PenColor("transparent")(cluster) + + for _, opt := range attrs { + opt(cluster) + } + return cluster +} diff --git a/pkg/edge/edge.go b/pkg/edge/edge.go new file mode 100644 index 0000000..afce977 --- /dev/null +++ b/pkg/edge/edge.go @@ -0,0 +1,105 @@ +package edge + +import ( + "fmt" + "strings" + + "github.com/emicklei/dot" +) + +type Attribute func(*dot.Edge) + +func Label(label string) Attribute { + return func(el *dot.Edge) { + el.Attr("label", label) + } +} + +func FontName(name string) Attribute { + return func(el *dot.Edge) { + el.Attr("fontname", name) + } +} + +func FontSize(size float32) Attribute { + return func(el *dot.Edge) { + fs := fmt.Sprintf("%.2f", size) + el.Attr("fontsize", fs) + } +} + +func Dir(dir string) Attribute { + return func(el *dot.Edge) { + if strings.TrimSpace(dir) != "" { + el.Attr("dir", dir) + } + } +} + +func Dashed(dashed bool) Attribute { + return func(el *dot.Edge) { + if dashed { + el.Attr("style", "dashed") + } + } +} + +func Color(color string) Attribute { + return func(el *dot.Edge) { + if strings.TrimSpace(color) != "" { + el.Attr("color", color) + } else { + el.Attr("color", "#708090ff") + } + } +} + +func PenWidth(size float32) Attribute { + return func(el *dot.Edge) { + pw := fmt.Sprintf("%.2f", size) + el.Attr("penwidth", pw) + } +} + +func ArrowSize(size float32) Attribute { + return func(el *dot.Edge) { + pw := fmt.Sprintf("%.2f", size) + el.Attr("arrowsize", pw) + } +} + +func Highlight(ok bool) Attribute { + return func(el *dot.Edge) { + if ok { + el.Attr("penwidth", "1.2") + el.Attr("arrowsize", "0.9") + } else { + el.Attr("penwidth", "0.6") + el.Attr("arrowsize", "0.6") + } + } +} + +func New(g *dot.Graph, fromNodeID, toNodeID string, attrs ...Attribute) error { + n1, ok := g.FindNodeById(fromNodeID) + if !ok { + return fmt.Errorf("node with id=%s not found", fromNodeID) + } + + n2, ok := g.FindNodeById(toNodeID) + if !ok { + return fmt.Errorf("node with id=%s not found", toNodeID) + } + + el := g.Edge(n1, n2) + + FontName("Fira Mono")(&el) + FontSize(8)(&el) + Highlight(false)(&el) + + for _, opt := range attrs { + opt(&el) + } + + return nil +} diff --git a/pkg/graph/graph.go b/pkg/graph/graph.go new file mode 100644 index 0000000..81fdcd6 --- /dev/null +++ b/pkg/graph/graph.go @@ -0,0 +1,75 @@ +package graph + +import ( + "fmt" + "strings" + + "github.com/emicklei/dot" +) + +type Attribute func(*dot.Graph) + +func Label(label string) Attribute { + return func(el *dot.Graph) { + if strings.TrimSpace(label) != "" { + el.Attr("label", label) + } + } +} + +func FontName(name string) Attribute { + return func(el *dot.Graph) { + el.Attr("fontname", name) + } +} + +func FontSize(size float32) Attribute { + return func(el *dot.Graph) { + fs := fmt.Sprintf("%.2f", size) + el.Attr("fontsize", fs) + } +} + +func LeftToRight() Attribute { + return func(el *dot.Graph) { + el.Attr("rankdir", "LR") + } +} + +func TopToBottom() Attribute { + return func(el *dot.Graph) { + el.Attr("rankdir", "TB") + } +} + +func RankSep(size float32) Attribute { + return func(el *dot.Graph) { + fs := fmt.Sprintf("%.2f", size) + el.Attr("ranksep", fs) + } +} + +func BackgroundColor(color string) Attribute { + return func(el *dot.Graph) { + if strings.TrimSpace(color) != "" { + el.Attr("bgcolor", color) + } else { + el.Attr("bgcolor", "transparent") + } + } +} + +func New(attrs ...Attribute) *dot.Graph { + el := dot.NewGraph(dot.Directed) + + FontName("Fira Mono Bold")(el) + FontSize(13)(el) + LeftToRight()(el) + RankSep(1.1)(el) + + for _, opt := range attrs { + opt(el) + } + + return el +} diff --git a/pkg/node/node.go b/pkg/node/node.go new file mode 100644 index 0000000..892d4d0 --- /dev/null +++ b/pkg/node/node.go @@ -0,0 +1,79 @@ +package node + +import ( + "fmt" + "strings" + + "github.com/emicklei/dot" +) + +type Attribute func(*dot.Node) + +func Label(label string) Attribute { + return func(el *dot.Node) { + el.Attr("label", label) + } +} + +func Shape(shape string) Attribute { + return func(el *dot.Node) { + el.Attr("shape", shape) + } +} + +func Rounded(rounded bool) Attribute { + return func(el *dot.Node) { + if rounded { + el.Attr("style", "rounded,filled") + } else { + el.Attr("style", "filled") + } + } +} + +func FillColor(color, fallback string) Attribute { + return func(el *dot.Node) { + if strings.TrimSpace(color) != "" { + el.Attr("fillcolor", color) + } else { + el.Attr("fillcolor", fallback) + } + } +} + +func FontColor(color string) Attribute { + return func(el *dot.Node) { + if strings.TrimSpace(color) != "" { + el.Attr("fontcolor", color) + } else { + el.Attr("fontcolor", "#000000ff") + } + } +} + +func FontName(name string) Attribute { + return func(el *dot.Node) { + el.Attr("fontname", name) + } +} + +func FontSize(size float32) Attribute { + return func(el *dot.Node) { + fs := fmt.Sprintf("%.2f", size) + el.Attr("fontsize", fs) + } +} + +func New(cluster *dot.Graph, id string, attrs ...Attribute) *dot.Node { + el := cluster.Node(id) + + // default attributes + FontName("Fira Mono")(&el) + FontSize(9)(&el) + + for _, opt := range attrs { + opt(&el) + } + + return &el +} diff --git a/queue.go b/queue.go new file mode 100644 index 0000000..ebd5f58 --- /dev/null +++ b/queue.go @@ -0,0 +1,58 @@ +package draft + +import ( + "fmt" + "strings" + + "github.com/emicklei/dot" + "github.com/lucasepe/draft/pkg/cluster" + "github.com/lucasepe/draft/pkg/node" +) + +type queue struct { + seq int16 +} + +func (rcv *queue) nextID() string { + rcv.seq++ + return fmt.Sprintf("qs%d", rcv.seq) +} + +func (rcv *queue) sketch(graph *dot.Graph, comp Component) { + id := comp.ID + if strings.TrimSpace(comp.ID) == "" { + id = rcv.nextID() + } + + cl := cluster.New(graph, id, cluster.Label(comp.Provider)) + + el := node.New(cl, id, + node.Label(comp.Label), + node.Rounded(comp.Rounded), + node.FontColor(comp.FontColor), + node.FillColor("", "transparent"), + // ^^^ hack to set a transparent background + // color since we will use the HTML table. + node.Shape("plaintext"), + ) + + caption := strings.TrimSpace(comp.Label) + if len(caption) == 0 { + caption = " " + } + + fillColor := comp.FillColor + if strings.TrimSpace(comp.FillColor) == "" { + fillColor = "#bdb76bff" + } + + label := fmt.Sprintf(` + + + + + +
 
msg N...msg 1
%s
`, fillColor, fillColor, fillColor, caption) + + el.Attr("label", dot.HTML(label)) +} diff --git a/service.go b/service.go new file mode 100644 index 0000000..ab53eaf --- /dev/null +++ b/service.go @@ -0,0 +1,37 @@ +package draft + +import ( + "fmt" + "strings" + + "github.com/emicklei/dot" + "github.com/lucasepe/draft/pkg/cluster" + "github.com/lucasepe/draft/pkg/node" +) + +type service struct { + seq int16 +} + +func (rcv *service) nextID() string { + rcv.seq++ + return fmt.Sprintf("ms%d", rcv.seq) +} + +func (rcv *service) sketch(graph *dot.Graph, comp Component) { + id := comp.ID + if strings.TrimSpace(comp.ID) == "" { + id = rcv.nextID() + } + + cl := cluster.New(graph, id, cluster.Label(comp.Provider)) + + el := node.New(cl, id, + node.Label(comp.Label), + node.Rounded(comp.Rounded), + node.FontColor(comp.FontColor), + node.FillColor(comp.FillColor, "#f5f5dcff"), + node.Shape("box"), + ) + el.Attr("height", "0.5") +} diff --git a/storage.go b/storage.go new file mode 100644 index 0000000..09135b1 --- /dev/null +++ b/storage.go @@ -0,0 +1,38 @@ +package draft + +import ( + "fmt" + "strings" + + "github.com/emicklei/dot" + "github.com/lucasepe/draft/pkg/cluster" + "github.com/lucasepe/draft/pkg/node" +) + +type storage struct { + seq int16 +} + +func (rcv *storage) nextID() string { + rcv.seq++ + return fmt.Sprintf("st%d", rcv.seq) +} + +func (rcv *storage) sketch(graph *dot.Graph, comp Component) { + id := comp.ID + if strings.TrimSpace(comp.ID) == "" { + id = rcv.nextID() + } + + cl := cluster.New(graph, id, cluster.Label(comp.Provider)) + + el := node.New(cl, id, + node.Label(comp.Label), + node.Rounded(comp.Rounded), + node.FontColor(comp.FontColor), + node.FillColor(comp.FillColor, "#f0e77fff"), + node.FontSize(8), + node.Shape("folder"), + ) + el.Attr("height", "0.4") +}