├── .deepsource.toml
├── .github
├── dependabot.yml
└── workflows
│ ├── dependency-review.yml
│ └── test.yml
├── .gitignore
├── .golangci.yml
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── DSL.md
├── LICENSE
├── Makefile
├── README.md
├── assets
└── model-banner.png
├── cmd
├── mdl
│ ├── main.go
│ ├── serve.go
│ ├── watch.go
│ └── webapp
│ │ ├── .babelrc.js
│ │ ├── README.md
│ │ ├── data
│ │ ├── layout.json
│ │ └── model.json
│ │ ├── dist
│ │ ├── 1815e00441357e01619e.ttf
│ │ ├── 2463b90d9a316e4e5294.woff2
│ │ ├── 2582b0e4bcf85eceead0.ttf
│ │ ├── 89999bdf5d835c012025.woff2
│ │ ├── 914997e1bdfc990d0897.ttf
│ │ ├── c210719e60948b211a12.woff2
│ │ ├── da94ef451f4969af06e6.ttf
│ │ ├── ea8f94e1d22e0d35ccd4.woff2
│ │ ├── index.html
│ │ ├── main.js
│ │ ├── main.js.map
│ │ ├── runtime.js
│ │ └── runtime.js.map
│ │ ├── package.json
│ │ ├── src
│ │ ├── Root.tsx
│ │ ├── components
│ │ │ └── Toolbar.tsx
│ │ ├── graph-view
│ │ │ ├── defs.ts
│ │ │ ├── graph-react.tsx
│ │ │ ├── graph.ts
│ │ │ ├── intersect.ts
│ │ │ ├── layout.ts
│ │ │ ├── shapes.ts
│ │ │ ├── svg-create.ts
│ │ │ ├── svg-text.ts
│ │ │ └── undo.ts
│ │ ├── hooks.ts
│ │ ├── index.html
│ │ ├── index.tsx
│ │ ├── parseModel.ts
│ │ ├── shortcuts.tsx
│ │ ├── static
│ │ │ ├── index.html
│ │ │ └── index.tsx
│ │ ├── style.css
│ │ ├── utils.ts
│ │ ├── utils
│ │ │ └── platform.ts
│ │ └── websocket.ts
│ │ ├── tsconfig.json
│ │ ├── webpack.config.base.js
│ │ ├── webpack.config.js
│ │ ├── webpack.config.static.js
│ │ └── yarn.lock
└── stz
│ └── main.go
├── codegen
└── json.go
├── dsl
├── deployment.go
├── design.go
├── doc.go
├── elements.go
├── person.go
├── relationship.go
├── styles.go
└── views.go
├── editor.png
├── examples
├── basic
│ ├── README.md
│ ├── gen
│ │ └── SystemContext.svg
│ ├── main.go
│ └── model
│ │ ├── model.go
│ │ └── model.json
├── big_bank_plc
│ ├── README.md
│ ├── model.json
│ ├── model.layout.json
│ └── model
│ │ └── model.go
├── json
│ ├── README.md
│ ├── main.go
│ └── model
│ │ └── model.go
├── nested
│ ├── README.md
│ ├── model
│ │ ├── model.go
│ │ ├── subsystem1
│ │ │ └── model.go
│ │ └── subsystem2
│ │ │ └── model.go
│ └── styles
│ │ └── styles.go
├── relationship_style
│ ├── gen
│ │ └── SystemContext.svg
│ └── model
│ │ └── model.go
├── shapes
│ ├── gen
│ │ ├── SystemContext.svg
│ │ └── layout.json
│ └── model
│ │ └── model.go
├── staticcheck.conf
└── usage
│ ├── README.md
│ ├── gen
│ └── view.svg
│ └── model
│ └── model.go
├── expr
├── component.go
├── component_test.go
├── container.go
├── container_test.go
├── deployment.go
├── design.go
├── doc.go
├── element.go
├── filtered_views.go
├── idify_test.go
├── model.go
├── model_test.go
├── person.go
├── person_test.go
├── registry.go
├── relationship.go
├── relationship_test.go
├── render.go
├── render_test.go
├── system.go
├── system_test.go
├── testing.go
├── view_props.go
├── views.go
└── views_test.go
├── go.mod
├── go.sum
├── mdl
├── deployment.go
├── doc.go
├── elements.go
├── eval.go
├── model.go
├── relationship.go
└── views.go
├── pkg
└── version.go
├── plugin
└── generate.go
└── stz
├── client.go
├── client_test.go
├── configuration.go
├── doc.go
├── eval.go
├── layout.go
├── views.go
└── workspace.go
/.deepsource.toml:
--------------------------------------------------------------------------------
1 | version = 1
2 | [[analyzers]]
3 | name = "test-coverage"
4 | enabled = true
5 | [[analyzers]]
6 | name = "go"
7 | [analyzers.meta]
8 | import_root = "goa.design/model"
9 | [[analyzers]]
10 | name = "secrets"
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: "npm"
4 | directory: "/cmd/mdl/webapp"
5 | schedule:
6 | interval: "weekly"
7 | - package-ecosystem: "gomod"
8 | directory: "/"
9 | schedule:
10 | interval: "weekly"
11 | - package-ecosystem: "github-actions"
12 | directory: "/"
13 | schedule:
14 | interval: "weekly"
15 |
--------------------------------------------------------------------------------
/.github/workflows/dependency-review.yml:
--------------------------------------------------------------------------------
1 | # Dependency Review Action
2 | #
3 | # This Action will scan dependency manifest files that change as part of a Pull Request, surfacing known-vulnerable versions of the packages declared or updated in the PR. Once installed, if the workflow run is marked as required, PRs introducing known-vulnerable packages will be blocked from merging.
4 | #
5 | # Source repository: https://github.com/actions/dependency-review-action
6 | # Public documentation: https://docs.github.com/en/code-security/supply-chain-security/understanding-your-software-supply-chain/about-dependency-review#dependency-review-enforcement
7 | name: 'Dependency Review'
8 | on: [pull_request]
9 |
10 | permissions:
11 | contents: read
12 |
13 | jobs:
14 | dependency-review:
15 | runs-on: ubuntu-latest
16 | steps:
17 | - name: 'Checkout Repository'
18 | uses: actions/checkout@v4
19 | - name: 'Dependency Review'
20 | uses: actions/dependency-review-action@v4
21 |
--------------------------------------------------------------------------------
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
1 | name: Run Static Checks and Tests
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 | pull_request:
8 | branches:
9 | - main
10 |
11 | jobs:
12 | lint:
13 | name: Lint
14 | runs-on: ubuntu-latest
15 | steps:
16 | - name: Check out code
17 | uses: actions/checkout@v4
18 |
19 | - name: Set up Go
20 | uses: actions/setup-go@v5
21 | with:
22 | go-version: '1.23'
23 |
24 | - name: Build UI
25 | run: |
26 | cd cmd/mdl/webapp
27 | npm install
28 | npm run build
29 |
30 | - name: golangci-lint
31 | uses: golangci/golangci-lint-action@v8
32 | with:
33 | version: latest
34 | args: --timeout=5m
35 |
36 | test:
37 | name: Test
38 | strategy:
39 | fail-fast: true
40 | matrix:
41 | go: ['1.24']
42 | os: ['ubuntu-latest']
43 | runs-on: ${{ matrix.os }}
44 |
45 | steps:
46 | - name: Check out code
47 | uses: actions/checkout@v4
48 |
49 | - name: Set up Go ${{ matrix.go }}
50 | uses: actions/setup-go@v5
51 | with:
52 | go-version: ${{ matrix.go }}
53 |
54 | - name: Install dependencies
55 | run: go mod download
56 |
57 | - name: Build UI
58 | run: |
59 | cd cmd/mdl/webapp
60 | npm install
61 | npm run build
62 |
63 | - name: Build
64 | run: |
65 | cd cmd/mdl && go build
66 | cd ../stz && go build
67 |
68 | - name: Run tests
69 | run: go test ./... -coverprofile=cover.out
70 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | cmd/mdl/mdl
2 | cmd/stz/stz
3 | cmd/mdl/webapp/node_modules
4 | cmd/mdl/webapp/dist-static
5 | cmd/mdl/webapp/package-lock.json
6 |
7 | /gen
8 | /gen-*
9 | .idea
10 | .vscode
11 |
12 |
13 | cover.out
14 |
--------------------------------------------------------------------------------
/.golangci.yml:
--------------------------------------------------------------------------------
1 | version: "2"
2 |
3 | run:
4 | timeout: 5m
5 |
6 | linters:
7 | enable:
8 | - errcheck
9 | - govet
10 | - ineffassign
11 | - staticcheck
12 | - unused
13 | - misspell
14 | exclusions:
15 | rules:
16 | - linters:
17 | - staticcheck
18 | text: "ST1001"
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.
6 |
7 | ## Our Standards
8 |
9 | Examples of behavior that contributes to creating a positive environment include:
10 |
11 | * Using welcoming and inclusive language
12 | * Being respectful of differing viewpoints and experiences
13 | * Gracefully accepting constructive criticism
14 | * Focusing on what is best for the community
15 | * Showing empathy towards other community members
16 |
17 | Examples of unacceptable behavior by participants include:
18 |
19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances
20 | * Trolling, insulting/derogatory comments, and personal or political attacks
21 | * Public or private harassment
22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission
23 | * Other conduct which could reasonably be considered inappropriate in a professional setting
24 |
25 | ## Our Responsibilities
26 |
27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
28 |
29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
30 |
31 | ## Scope
32 |
33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
34 |
35 | ## Enforcement
36 |
37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at info@goa.design. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
38 |
39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
40 |
41 | ## Attribution
42 |
43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version]
44 |
45 | [homepage]: http://contributor-covenant.org
46 | [version]: http://contributor-covenant.org/version/1/4/
47 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing to Model
2 |
3 | Thank you for your interest in contributing to the Model project! We
4 | appreciate contributions via submitting Github issues and/or pull requests.
5 |
6 | Below are some guidelines to follow when contributing to this project:
7 |
8 | * Before opening an issue in Github, check
9 | [open issues](https://github.com/goadesign/model/issues) and
10 | [pull requests](https://github.com/goadesign/model/pulls) for
11 | existing issues and fixes.
12 | * If your issue has not been addressed,
13 | [open a Github issue](https://github.com/goadesign/model/issues/new)
14 | and follow the checklist presented in the issue description section. A simple
15 | model DSL that reproduces your issue helps immensely.
16 | * If you know how to fix your bug, we highly encourage PR contributions. See
17 | [How Can I Get Started section](#how-can-i-get-started?) on how to submit a PR.
18 | * For feature requests and submitting major changes,
19 | [open an issue](https://github.com/goadesign/model/issues/new)
20 | or hop onto the Goa slack channel (see https://goa.design to join) to discuss
21 | the feature first.
22 | * Keep conversations friendly! Constructive criticism goes a long way.
23 | * Have fun contributing!
24 |
25 | ## How Can I Get Started?
26 |
27 | 1) Read the README carefully as well as details on the [C4 model](https://c4model.com)
28 | for more information on the C4 model.
29 | 2) To get your hands dirty, fork the model repo and issue PRs from the fork.
30 | **PRO Tip:** Add a [git remote](https://git-scm.com/docs/git-remote.html) to
31 | your forked repo in the source code (in $GOPATH/src/goa.design/model when
32 | installed using `go get`) to avoid messing with import paths while testing
33 | your fix.
34 | 3) [Open issues](https://github.com/goadesign/model/issues) labeled as `good first
35 | issue` are ideal to understand the source code and make minor contributions.
36 | Issues labeled `help wanted` are bugs/features that are not currently being
37 | worked on and contributing to them are most welcome.
38 | 4) Link the issue that the PR intends to solve in the PR description. If an issue
39 | does not exist, adding a description in the PR that describes the issue and the
40 | fix is recommended.
41 | 5) Ensure the CI build passes when you issue a PR to Goa.
42 | 7) Join our slack channel(the Goa slack channgel, see https://goa.design to join) and
43 | participate in the conversations.
44 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2020 Raphael Simon, Victor Dramba and Model Contributors
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in
13 | all copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | THE SOFTWARE.
22 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | #! /usr/bin/make
2 | #
3 | # Makefile for Model
4 | #
5 | # Targets:
6 | # - "depend" retrieves the Go packages needed to run the linter and tests
7 | # - "lint" runs the linter and checks the code format using goimports
8 | # - "test" runs the tests
9 | # - "release" creates a new release commit, tags the commit and pushes the tag to GitHub.
10 | #
11 | # Meta targets:
12 | # - "all" is the default target, it runs "lint" and "test"
13 | # - "ci" runs "depend" and "all"
14 | #
15 | MAJOR=1
16 | MINOR=11
17 | BUILD=2
18 |
19 | GO_FILES=$(shell find . -type f -name '*.go')
20 |
21 | # React app source files and dependencies
22 | WEBAPP_DIR=cmd/mdl/webapp
23 | WEBAPP_SRC_FILES=$(shell find $(WEBAPP_DIR)/src -type f \( -name '*.tsx' -o -name '*.ts' -o -name '*.css' -o -name '*.html' \) 2>/dev/null || true)
24 | WEBAPP_CONFIG_FILES=$(WEBAPP_DIR)/package.json $(WEBAPP_DIR)/tsconfig.json $(WEBAPP_DIR)/webpack.config.js $(WEBAPP_DIR)/webpack.config.base.js $(WEBAPP_DIR)/.babelrc.js
25 | WEBAPP_BUILD_OUTPUT=$(WEBAPP_DIR)/dist/main.js
26 |
27 | # Only list test and build dependencies
28 | # Standard dependencies are installed via go get
29 | DEPEND=\
30 | golang.org/x/tools/cmd/goimports@latest \
31 | github.com/golangci/golangci-lint/cmd/golangci-lint@latest \
32 | github.com/mjibson/esc@latest
33 |
34 | all: lint test build
35 |
36 | ci: depend all
37 |
38 | depend:
39 | @go mod download
40 | @for package in $(DEPEND); do go install $$package; done
41 |
42 | lint:
43 | ifneq ($(GOOS),windows)
44 | @if [ "`goimports -l $(GO_FILES) | tee /dev/stderr`" ]; then \
45 | echo "^ - Repo contains improperly formatted go files" && echo && exit 1; \
46 | fi
47 | @output=$$(golangci-lint run ./... | grep -v "^0 issues\\.$$"); \
48 | if [ -n "$$output" ]; then \
49 | echo "$$output" && echo "^ - golangci-lint errors!" && echo && exit 1; \
50 | fi
51 | endif
52 |
53 | test:
54 | go test ./... --coverprofile=cover.out
55 |
56 | # Ensure package-lock.json exists or is updated if package.json changes
57 | $(WEBAPP_DIR)/package-lock.json: $(WEBAPP_DIR)/package.json
58 | @echo "Generating/updating package-lock.json..."
59 | @cd $(WEBAPP_DIR) && npm install --package-lock-only
60 |
61 | # Install npm dependencies if package.json or package-lock.json changed
62 | $(WEBAPP_DIR)/node_modules/.install-timestamp: $(WEBAPP_DIR)/package.json $(WEBAPP_DIR)/package-lock.json
63 | @echo "Installing npm dependencies..."
64 | @cd $(WEBAPP_DIR) && npm install
65 | @touch $(WEBAPP_DIR)/node_modules/.install-timestamp
66 |
67 | # Build React app only if source files or config changed
68 | # package-lock.json is not a direct dependency here because its generation is handled by the rule above,
69 | # and npm install (triggered by .install-timestamp) will use it.
70 | $(WEBAPP_BUILD_OUTPUT): $(WEBAPP_SRC_FILES) $(WEBAPP_CONFIG_FILES) $(WEBAPP_DIR)/node_modules/.install-timestamp
71 | @echo "Building React app..."
72 | @cd $(WEBAPP_DIR) && npm run build
73 |
74 | # Phony target that depends on the actual build output
75 | build-ui: $(WEBAPP_BUILD_OUTPUT)
76 |
77 | # Force rebuild of UI (useful for development)
78 | build-ui-force:
79 | @echo "Force building React app..."
80 | @cd $(WEBAPP_DIR) && npm install
81 | @cd $(WEBAPP_DIR) && npm run build
82 |
83 | build: build-ui
84 | @cd cmd/mdl && go install
85 | @cd cmd/stz && go install
86 |
87 | serve: build
88 | @cmd/mdl/mdl serve
89 |
90 | # Clean build artifacts
91 | clean-ui:
92 | @echo "Cleaning React build artifacts..."
93 | @rm -rf $(WEBAPP_DIR)/dist/*
94 | @rm -f $(WEBAPP_DIR)/node_modules/.install-timestamp
95 |
96 | clean: clean-ui
97 |
98 | release: build
99 | # First make sure all is clean
100 | @git diff-index --quiet HEAD
101 | @go mod tidy
102 |
103 | # Bump version number
104 | @sed 's/Major = .*/Major = $(MAJOR)/' pkg/version.go > _tmp && mv _tmp pkg/version.go
105 | @sed 's/Minor = .*/Minor = $(MINOR)/' pkg/version.go > _tmp && mv _tmp pkg/version.go
106 | @sed 's/Build = .*/Build = $(BUILD)/' pkg/version.go > _tmp && mv _tmp pkg/version.go
107 | @sed 's/badge\/Version-.*/badge\/Version-v$(MAJOR).$(MINOR).$(BUILD)-blue.svg)/' README.md > _tmp && mv _tmp README.md
108 | @sed 's/model[ @]v.*\/\(.*\)tab=doc/model@v$(MAJOR).$(MINOR).$(BUILD)\/\1tab=doc/' README.md > _tmp && mv _tmp README.md
109 | @sed 's/mdl v[0-9]*\.[0-9]*\.[0-9]*, editor started\./mdl v$(MAJOR).$(MINOR).$(BUILD), editor started./' README.md > _tmp && mv _tmp README.md
110 | @sed 's/model@v.*\/\(.*\)tab=doc/model@v$(MAJOR).$(MINOR).$(BUILD)\/\1tab=doc/' DSL.md > _tmp && mv _tmp DSL.md
111 |
112 | # Commit and push
113 | @git add .
114 | @git commit -m "Release v$(MAJOR).$(MINOR).$(BUILD)"
115 | @git tag v$(MAJOR).$(MINOR).$(BUILD)
116 | @git push origin main
117 | @git push origin v$(MAJOR).$(MINOR).$(BUILD)
118 |
119 | .PHONY: all ci depend lint test build-ui build-ui-force build serve release clean-ui clean
120 |
--------------------------------------------------------------------------------
/assets/model-banner.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/goadesign/model/bd8a3a9db6a60d208e63be12487f2d4cdd1f8890/assets/model-banner.png
--------------------------------------------------------------------------------
/cmd/mdl/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "encoding/json"
5 | "flag"
6 | "fmt"
7 | "os"
8 | "path/filepath"
9 | "strings"
10 |
11 | goacodegen "goa.design/goa/v3/codegen"
12 |
13 | "goa.design/model/codegen"
14 | "goa.design/model/mdl"
15 | model "goa.design/model/pkg"
16 | )
17 |
18 | type config struct {
19 | debug bool
20 | help bool
21 | out string
22 | dir string
23 | port int
24 | devmode bool
25 | devdist string
26 | }
27 |
28 | func main() {
29 | cfg := parseArgs()
30 |
31 | if cfg.help {
32 | printUsage()
33 | os.Exit(0)
34 | }
35 |
36 | cmd, pkg := parseCommand()
37 |
38 | var err error
39 | switch cmd {
40 | case "gen":
41 | err = generateJSON(pkg, cfg)
42 | case "serve":
43 | err = startServer(pkg, cfg)
44 | case "version":
45 | fmt.Printf("%s %s\n", os.Args[0], model.Version())
46 | case "", "help":
47 | printUsage()
48 | default:
49 | fail(`unknown command %q, use "--help" for usage`, cmd)
50 | }
51 |
52 | if err != nil {
53 | fail(err.Error())
54 | }
55 | }
56 |
57 | func parseArgs() config {
58 | cfg := config{
59 | out: "design.json",
60 | dir: goacodegen.Gendir,
61 | port: 8080,
62 | devmode: os.Getenv("DEVMODE") == "1",
63 | devdist: os.Getenv("DEVDIST"),
64 | }
65 |
66 | flag.BoolVar(&cfg.debug, "debug", false, "print debug output")
67 | flag.BoolVar(&cfg.help, "help", false, "print this information")
68 | flag.BoolVar(&cfg.help, "h", false, "print this information")
69 | flag.StringVar(&cfg.out, "out", cfg.out, "set path to generated JSON representation")
70 | flag.StringVar(&cfg.dir, "dir", cfg.dir, "set output directory used by editor to save SVG files")
71 | flag.IntVar(&cfg.port, "port", cfg.port, "set local HTTP port used to serve diagram editor")
72 |
73 | // Parse only the flags, not the command and package
74 | args := os.Args[1:]
75 | flagStart := findFlagStart(args)
76 | if flagStart > 0 {
77 | if err := flag.CommandLine.Parse(args[flagStart:]); err != nil {
78 | fail("failed to parse flags: %s", err.Error())
79 | }
80 | }
81 |
82 | return cfg
83 | }
84 |
85 | func parseCommand() (string, string) {
86 | args := os.Args[1:]
87 | var cmd, pkg string
88 |
89 | for i, arg := range args {
90 | if strings.HasPrefix(arg, "-") {
91 | break
92 | }
93 | switch i {
94 | case 0:
95 | cmd = arg
96 | case 1:
97 | pkg = arg
98 | default:
99 | printUsage()
100 | os.Exit(1)
101 | }
102 | }
103 |
104 | return cmd, pkg
105 | }
106 |
107 | func findFlagStart(args []string) int {
108 | for i, arg := range args {
109 | if strings.HasPrefix(arg, "-") {
110 | return i
111 | }
112 | }
113 | return len(args)
114 | }
115 |
116 | func generateJSON(pkg string, cfg config) error {
117 | if pkg == "" {
118 | return fmt.Errorf(`missing PACKAGE argument, use "--help" for usage`)
119 | }
120 |
121 | b, err := codegen.JSON(pkg, cfg.debug)
122 | if err != nil {
123 | return err
124 | }
125 |
126 | return os.WriteFile(cfg.out, b, 0600)
127 | }
128 |
129 | func startServer(pkg string, cfg config) error {
130 | if pkg == "" {
131 | return fmt.Errorf(`missing PACKAGE argument, use "--help" for usage`)
132 | }
133 |
134 | absDir, err := filepath.Abs(cfg.dir)
135 | if err != nil {
136 | return err
137 | }
138 |
139 | if err := os.MkdirAll(absDir, 0700); err != nil {
140 | return err
141 | }
142 |
143 | if cfg.devmode && cfg.devdist == "" {
144 | cfg.devdist = "./cmd/mdl/webapp/dist"
145 | }
146 |
147 | return serve(absDir, pkg, cfg.port, cfg.devdist, cfg.debug)
148 | }
149 |
150 | func serve(out, pkg string, port int, devdist string, debug bool) error {
151 | // Load initial design
152 | design, err := loadDesign(pkg, debug)
153 | if err != nil {
154 | return err
155 | }
156 |
157 | server := NewServer(design)
158 |
159 | // Watch for changes and update server
160 | if err := watch(pkg, func() {
161 | if newDesign, err := loadDesign(pkg, debug); err != nil {
162 | fmt.Println("error parsing DSL:\n" + err.Error())
163 | } else {
164 | server.SetDesign(newDesign)
165 | }
166 | }); err != nil {
167 | return err
168 | }
169 |
170 | return server.Serve(out, devdist, port)
171 | }
172 |
173 | func loadDesign(pkg string, debug bool) (*mdl.Design, error) {
174 | b, err := codegen.JSON(pkg, debug)
175 | if err != nil {
176 | return nil, err
177 | }
178 |
179 | var design mdl.Design
180 | if err := json.Unmarshal(b, &design); err != nil {
181 | return nil, fmt.Errorf("failed to load design: %s", err.Error())
182 | }
183 |
184 | return &design, nil
185 | }
186 |
187 | func printUsage() {
188 | fmt.Fprintln(os.Stderr, "Usage:")
189 | fmt.Fprintf(os.Stderr, " %s serve PACKAGE [FLAGS]\n", os.Args[0])
190 | fmt.Fprintf(os.Stderr, " Start a HTTP server that serves a graphical editor for the design described in PACKAGE.\n")
191 | fmt.Fprintf(os.Stderr, " %s gen PACKAGE [FLAGS]\n", os.Args[0])
192 | fmt.Fprintf(os.Stderr, " Generate a JSON representation of the design described in PACKAGE.\n")
193 | fmt.Fprintf(os.Stderr, "\nPACKAGE must be the import path to a Go package containing Model DSL.\n\n")
194 | fmt.Fprintf(os.Stderr, "FLAGS:\n")
195 | flag.PrintDefaults()
196 | }
197 |
198 | func fail(format string, args ...any) {
199 | fmt.Fprintf(os.Stderr, format+"\n", args...)
200 | os.Exit(1)
201 | }
202 |
--------------------------------------------------------------------------------
/cmd/mdl/watch.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "os"
6 | "path/filepath"
7 | "strings"
8 | "time"
9 |
10 | "github.com/fsnotify/fsnotify"
11 | "github.com/jaschaephraim/lrserver"
12 | "golang.org/x/tools/go/packages"
13 |
14 | "goa.design/model/codegen"
15 | )
16 |
17 | // watch implements functionality to listen to changes in the model files
18 | // when notifications are received from the filesystem, the model is rebuild
19 | // and the editor page is refreshed via live reload server `lrserver`
20 | func watch(pkg string, reload func()) error {
21 | // Watch model design and regenerate on change
22 | watcher, err := fsnotify.NewWatcher()
23 | if err != nil {
24 | return err
25 | }
26 |
27 | pkgs, err := packages.Load(&packages.Config{Mode: packages.NeedFiles}, pkg+"//...")
28 | if err != nil {
29 | return err
30 | }
31 | if len(pkgs) == 0 {
32 | fmt.Println("Nothing to watch")
33 | return nil
34 | }
35 | fmt.Println("Watching:", filepath.Dir(pkgs[0].GoFiles[0]))
36 | for _, p := range pkgs { // we need to watch the subpackages too
37 | if err = watcher.Add(filepath.Dir(p.GoFiles[0])); err != nil {
38 | return err
39 | }
40 | }
41 |
42 | // Create live reload server and hookup to watcher
43 | lr := lrserver.New(lrserver.DefaultName, lrserver.DefaultPort)
44 | lr.SetStatusLog(nil)
45 | lr.SetErrorLog(nil)
46 | go func() {
47 | if err := lr.ListenAndServe(); err != nil {
48 | fail(err.Error())
49 | }
50 | }()
51 | go func() {
52 | for {
53 | select {
54 | case ev := <-watcher.Events:
55 | if strings.HasPrefix(filepath.Base(ev.Name), codegen.TmpDirPrefix) {
56 | continue
57 | }
58 |
59 | // debounce, because some editors do several file operations when you save
60 | // we wait for the stream of events to become silent for `interval`
61 | interval := 100 * time.Millisecond
62 | timer := time.NewTimer(interval)
63 | outer:
64 | for {
65 | select {
66 | case ev = <-watcher.Events:
67 | timer.Reset(interval)
68 | case <-timer.C:
69 | break outer
70 | }
71 | }
72 |
73 | fmt.Println(ev.String())
74 | reload()
75 | lr.Reload(ev.Name)
76 |
77 | case err := <-watcher.Errors:
78 | fmt.Fprintln(os.Stderr, "Error watching files:", err)
79 | }
80 | }
81 | }()
82 |
83 | return nil
84 | }
85 |
--------------------------------------------------------------------------------
/cmd/mdl/webapp/.babelrc.js:
--------------------------------------------------------------------------------
1 | module.exports = api => {
2 | // This caches the Babel config by environment.
3 | api.cache.using(() => process.env.NODE_ENV);
4 |
5 | return {
6 | presets: [
7 | '@babel/preset-env',
8 | '@babel/preset-typescript',
9 | '@babel/preset-react'
10 | ],
11 | plugins: [
12 | // Applies the react-refresh Babel plugin on development modes only
13 | api.env('development') && 'react-refresh/babel'
14 | ].filter(Boolean)
15 | };
16 | };
17 |
--------------------------------------------------------------------------------
/cmd/mdl/webapp/README.md:
--------------------------------------------------------------------------------
1 | # Model Diagram Layout (MDL) Webapp
2 |
3 | This webapp provides an interactive interface for viewing and editing model diagrams with automatic layout capabilities.
4 |
5 | ## Features
6 |
7 | ### Interactive Diagram Editing
8 |
9 | The webapp provides intuitive mouse interactions for navigating and editing diagrams:
10 |
11 | #### Mouse Interactions
12 |
13 | - **Pan View**: Drag on empty space to pan around the diagram
14 | - **Zoom**: Use scroll wheel to zoom in/out at cursor position
15 | - **Select Elements**: Click on nodes or edges to select them
16 | - **Multi-Selection**: Shift+click to add/remove elements from selection
17 | - **Box Selection**: Shift+drag on empty space to select multiple elements
18 | - **Move Elements**: Drag selected elements to reposition them
19 | - **Undo/Redo**: All changes are tracked for easy reversal
20 |
21 | #### Toolbar Features
22 |
23 | - **Alignment Tools**: Align selected elements horizontally or vertically
24 | - **Distribution Tools**: Evenly distribute selected elements with equal spacing
25 | - **Auto Layout**: Apply automatic layout algorithms to organize the entire diagram
26 | - **Connection Routing**: Change how connections are drawn (orthogonal, straight, curved)
27 | - **Zoom Controls**: Zoom in/out, fit to screen, or reset to 100%
28 | - **Save**: Save the current layout and positions
29 |
30 | ### Auto Layout Engine
31 |
32 | The webapp now uses **ELK.js** (Eclipse Layout Kernel) for automatic diagram layout, providing significant improvements over the previous Dagre.js implementation:
33 |
34 | #### Available Layout Algorithms
35 |
36 | 1. **Layered (Hierarchical)** - Default algorithm, ideal for directed graphs with clear hierarchy
37 | - Optimized for node-link diagrams with inherent direction
38 | - Excellent edge routing with minimal crossings
39 | - Supports grouping and nested structures
40 |
41 | 2. **Stress (Force-directed)** - Physics-based layout for general graphs
42 | - Good for understanding overall graph structure
43 | - Minimizes edge lengths and overlaps
44 |
45 | 3. **Tree** - Specialized for tree structures
46 | - Optimized spacing for hierarchical data
47 | - Clean, readable layouts for tree-like diagrams
48 |
49 | 4. **Force** - Alternative force-directed algorithm
50 | - Different physics simulation approach
51 | - Good for dense graphs
52 |
53 | 5. **Radial** - Circular layout with central focus
54 | - Places important nodes at the center
55 | - Good for showing relationships radiating from key elements
56 |
57 | 6. **Disco** - Disconnected components layout
58 | - Handles graphs with multiple disconnected parts
59 | - Optimizes space usage for complex diagrams
60 |
61 | #### Layout Features
62 |
63 | - **Improved Space Optimization**: Better node spacing and edge routing
64 | - **Orthogonal Edge Routing**: Cleaner, more readable edge paths
65 | - **Group Support**: Proper handling of nested diagram elements
66 | - **Configurable Options**: Adjustable spacing, direction, and algorithm-specific parameters
67 | - **Fallback Handling**: Graceful degradation if layout fails
68 |
69 | #### Usage
70 |
71 | 1. Select your preferred layout algorithm from the dropdown menu
72 | 2. Click "Auto Layout" to apply the selected algorithm
73 | 3. The system will automatically optimize node positions and edge routing
74 | 4. Use "Fit" to zoom and center the resulting layout
75 |
76 | ### Keyboard Shortcuts
77 |
78 | #### Navigation & Zoom
79 | - **Scroll Wheel**: Zoom in/out
80 | - **Ctrl + =**: Zoom in
81 | - **Ctrl + -**: Zoom out
82 | - **Ctrl + 9**: Fit diagram to screen
83 | - **Ctrl + 0**: Reset zoom to 100%
84 |
85 | #### Selection & Editing
86 | - **Ctrl + A**: Select all elements
87 | - **Esc**: Deselect all elements
88 | - **Delete/Backspace**: Remove selected edge vertices
89 | - **Arrow Keys**: Move selected elements (1px)
90 | - **Shift + Arrow Keys**: Move selected elements (10px)
91 |
92 | #### File Operations
93 | - **Ctrl + S**: Save current layout
94 | - **Ctrl + Z**: Undo last change
95 | - **Ctrl + Shift + Z** or **Ctrl + Y**: Redo last undone change
96 |
97 | #### Help
98 | - **Shift + ?** or **Shift + F1**: Show/hide keyboard shortcuts help
99 |
100 | #### Advanced Editing
101 | - **Alt + Click**: Add vertex to relationship edge (Option + Click on Mac)
102 | - **Alt + Shift + Click**: Add label anchor to relationship edge (Option + Shift + Click on Mac)
103 |
104 | ## Technical Implementation
105 |
106 | - **ELK.js v0.10.0**: Modern layout algorithms with TypeScript support
107 | - **Async Layout**: Non-blocking layout calculation with progress indication
108 | - **Error Handling**: Robust fallback mechanisms for layout failures
109 | - **Performance**: Optimized for both small and large diagrams
110 |
111 | ## Development
112 |
113 | ```bash
114 | npm install
115 | npm start # Development server
116 | npm run build # Production build
117 | ```
118 |
119 | ## Migration from Dagre.js
120 |
121 | The migration from Dagre.js to ELK.js provides:
122 | - **Better algorithms**: More sophisticated layout techniques
123 | - **Improved edge routing**: Cleaner, more readable connections
124 | - **Enhanced grouping**: Better support for nested elements
125 | - **Modern codebase**: Active development and TypeScript support
126 | - **Multiple algorithms**: Choose the best layout for your diagram type
127 |
128 | ## Editor webapp
129 |
130 | The mdl editor is a web application that runs in the browser.
131 | It is embedded in the go executable, so it can be served
132 | as static files. `mdl` runs an HTTP server that serves the
133 | editor as static files. `mdl` also serves the model and layout data
134 | dynamically, for the editor to load. The editor then renders the
135 | selected view as an SVG, allowing the user to edit positions of elements
136 | and shapes of relationships.
137 |
138 | ### Development setup
139 |
140 | To develop the mdl and the editor, start the TypeScript compiler in watch mode and run mdl go program in devmode.
141 |
142 | `mdl` can be instructed to serve the editor files from disk instead
143 | of the embedded copies, to allow for easy development.
144 | ```
145 | DEVMODE=1 go run ./cmd/mdl ... mdl params
146 | ```
147 |
148 | Compile and run the TypeScript application in watch mode
149 | ```
150 | yarn install
151 | yarn watch
152 | ```
153 |
154 | `yarn watch` will watch for changes in the webapp files and recompile.
155 | Simply refresh the browser to see the changes.
156 |
--------------------------------------------------------------------------------
/cmd/mdl/webapp/data/layout.json:
--------------------------------------------------------------------------------
1 | {
2 | "Components": {
3 | "11kj53l": {
4 | "x": 1596.5491079817027,
5 | "y": 950.9669353128886
6 | },
7 | "15o55d9": {
8 | "x": 265.7023341743573,
9 | "y": 954.2572092999188
10 | },
11 | "1hfi68p": {
12 | "x": 261.04362489149975,
13 | "y": 1283.9524457340424
14 | },
15 | "1njagt4": {
16 | "x": 1594.8461550212464,
17 | "y": 1288.1969674005459
18 | },
19 | "1olxg1c": {
20 | "x": 1590.784198208602,
21 | "y": 612.4401417515865
22 | },
23 | "1qm4oey": {
24 | "x": 1118.1451334807587,
25 | "y": 192.5904352570644
26 | },
27 | "ay5wb5": {
28 | "x": 945.3671456159877,
29 | "y": 1270.2460724859745
30 | },
31 | "do2bvv": {
32 | "x": 945.2224675774833,
33 | "y": 945.2955331015629
34 | },
35 | "hd9fd5": {
36 | "x": 807.5159305644356,
37 | "y": 191.0893871761279
38 | },
39 | "jdi4s4": {
40 | "x": 948.2691929908412,
41 | "y": 622.5406891574396
42 | },
43 | "sks1g4": {
44 | "x": 267.2210049931528,
45 | "y": 624.1553832371161
46 | }
47 | },
48 | "Containers": {
49 | "1a44kjx": {
50 | "x": 949.1109620730406,
51 | "y": 154.49170594273608
52 | },
53 | "1hfi68p": {
54 | "x": 229.42723802613912,
55 | "y": 1254.0520673207977
56 | },
57 | "1njagt4": {
58 | "x": 2074.0651098667267,
59 | "y": 1271.5822134595332
60 | },
61 | "1qm4oey": {
62 | "x": 233.46725973794676,
63 | "y": 737.6778529958701
64 | },
65 | "1rhkirw": {
66 | "x": 905.8717684401352,
67 | "y": 733.2362818997086
68 | },
69 | "ay5wb5": {
70 | "x": 2056.8158776060086,
71 | "y": 740.3815976828853
72 | },
73 | "hd9fd5": {
74 | "x": 1476.392982873835,
75 | "y": 731.464973950552
76 | },
77 | "uu8ny2": {
78 | "x": 1480.2172434568556,
79 | "y": 1275.0158471255827
80 | }
81 | },
82 | "DevelopmentDeployment": {
83 | "144pryr": {
84 | "x": 1184.5998314965739,
85 | "y": 583.9016731967849
86 | },
87 | "16l1tmw": {
88 | "x": 1186.073293706343,
89 | "y": 268.0378803848122
90 | },
91 | "1echf4l": {
92 | "x": 196.2275520324854,
93 | "y": 177.64737476313292
94 | },
95 | "1io29d4": {
96 | "x": 1803.9328924813683,
97 | "y": 293.34428998482065
98 | }
99 | },
100 | "LiveDeployment": {
101 | "1drz7sz": {
102 | "x": 1726.4641182835253,
103 | "y": 702.413319347758
104 | },
105 | "1iy36nc": {
106 | "x": 1038.4544306466573,
107 | "y": 291.07280602889637
108 | },
109 | "1kb17li": {
110 | "x": 355.13150861644084,
111 | "y": 245.52383803088605
112 | },
113 | "5fensq": {
114 | "x": 1731.3511023750038,
115 | "y": 255.46814681727824
116 | },
117 | "sf7s62": {
118 | "x": 359.0947314188105,
119 | "y": 782.9693251929668
120 | },
121 | "xg5rs9": {
122 | "x": 1039.54496256825,
123 | "y": 803.2569324150502
124 | }
125 | },
126 | "SignIn": {
127 | "15o55d9": {
128 | "x": 1271,
129 | "y": 308
130 | },
131 | "1hfi68p": {
132 | "x": 1829.428184393335,
133 | "y": 304.1773665935631
134 | },
135 | "1qm4oey": {
136 | "x": 284.97551626459705,
137 | "y": 312.08868329678154
138 | },
139 | "sks1g4": {
140 | "x": 788.6207830774708,
141 | "y": 313.1528828581602
142 | }
143 | },
144 | "SystemLandscape": {
145 | "1a44kjx": {
146 | "x": 313.8988809038172,
147 | "y": 181.74720225954295
148 | },
149 | "1mmpe00": {
150 | "x": 1673.2322127139207,
151 | "y": 170.74720345163578
152 | },
153 | "1njagt4": {
154 | "x": 1675.5655427358852,
155 | "y": 1041.413841640529
156 | },
157 | "23ylt4": {
158 | "x": 2314.898841366071,
159 | "y": 1046.080523738175
160 | },
161 | "ay5wb5": {
162 | "x": 317.8988852748243,
163 | "y": 1050.0805179763931
164 | },
165 | "gsdtx7": {
166 | "x": 1062.5655453187528,
167 | "y": 1046.7471806031895
168 | },
169 | "o14fz3": {
170 | "x": 1051.1485244315527,
171 | "y": 429.6853416129542
172 | }
173 | }
174 | }
--------------------------------------------------------------------------------
/cmd/mdl/webapp/dist/1815e00441357e01619e.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/goadesign/model/bd8a3a9db6a60d208e63be12487f2d4cdd1f8890/cmd/mdl/webapp/dist/1815e00441357e01619e.ttf
--------------------------------------------------------------------------------
/cmd/mdl/webapp/dist/2463b90d9a316e4e5294.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/goadesign/model/bd8a3a9db6a60d208e63be12487f2d4cdd1f8890/cmd/mdl/webapp/dist/2463b90d9a316e4e5294.woff2
--------------------------------------------------------------------------------
/cmd/mdl/webapp/dist/2582b0e4bcf85eceead0.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/goadesign/model/bd8a3a9db6a60d208e63be12487f2d4cdd1f8890/cmd/mdl/webapp/dist/2582b0e4bcf85eceead0.ttf
--------------------------------------------------------------------------------
/cmd/mdl/webapp/dist/89999bdf5d835c012025.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/goadesign/model/bd8a3a9db6a60d208e63be12487f2d4cdd1f8890/cmd/mdl/webapp/dist/89999bdf5d835c012025.woff2
--------------------------------------------------------------------------------
/cmd/mdl/webapp/dist/914997e1bdfc990d0897.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/goadesign/model/bd8a3a9db6a60d208e63be12487f2d4cdd1f8890/cmd/mdl/webapp/dist/914997e1bdfc990d0897.ttf
--------------------------------------------------------------------------------
/cmd/mdl/webapp/dist/c210719e60948b211a12.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/goadesign/model/bd8a3a9db6a60d208e63be12487f2d4cdd1f8890/cmd/mdl/webapp/dist/c210719e60948b211a12.woff2
--------------------------------------------------------------------------------
/cmd/mdl/webapp/dist/da94ef451f4969af06e6.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/goadesign/model/bd8a3a9db6a60d208e63be12487f2d4cdd1f8890/cmd/mdl/webapp/dist/da94ef451f4969af06e6.ttf
--------------------------------------------------------------------------------
/cmd/mdl/webapp/dist/ea8f94e1d22e0d35ccd4.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/goadesign/model/bd8a3a9db6a60d208e63be12487f2d4cdd1f8890/cmd/mdl/webapp/dist/ea8f94e1d22e0d35ccd4.woff2
--------------------------------------------------------------------------------
/cmd/mdl/webapp/dist/index.html:
--------------------------------------------------------------------------------
1 |
Model - Architecture Diagrams as Code
--------------------------------------------------------------------------------
/cmd/mdl/webapp/dist/runtime.js:
--------------------------------------------------------------------------------
1 | (()=>{"use strict";var e,r={},t={};function o(e){var n=t[e];if(void 0!==n)return n.exports;var i=t[e]={id:e,exports:{}};return r[e](i,i.exports,o),i.exports}o.m=r,e=[],o.O=(r,t,n,i)=>{if(!t){var a=1/0;for(p=0;p=i)&&Object.keys(o.O).every((e=>o.O[e](t[f])))?t.splice(f--,1):(l=!1,i0&&e[p-1][2]>i;p--)e[p]=e[p-1];e[p]=[t,n,i]},o.n=e=>{var r=e&&e.__esModule?()=>e.default:()=>e;return o.d(r,{a:r}),r},o.d=(e,r)=>{for(var t in r)o.o(r,t)&&!o.o(e,t)&&Object.defineProperty(e,t,{enumerable:!0,get:r[t]})},o.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(e){if("object"==typeof window)return window}}(),o.o=(e,r)=>Object.prototype.hasOwnProperty.call(e,r),o.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},o.p="/",(()=>{o.b=document.baseURI||self.location.href;var e={121:0};o.O.j=r=>0===e[r];var r=(r,t)=>{var n,i,[a,l,f]=t,u=0;if(a.some((r=>0!==e[r]))){for(n in l)o.o(l,n)&&(o.m[n]=l[n]);if(f)var p=f(o)}for(r&&r(t);u = ({ model, layout }) => (
18 |
19 |
20 | } />
21 |
22 |
23 | );
24 |
25 | export const refreshGraph = () => {
26 | const currentID = getCurrentViewID();
27 | clearGraphCache(currentID);
28 | };
29 |
30 | const ModelPane: FC<{ model: any; layouts: any }> = ({ model, layouts }) => {
31 | const [searchParams, setSearchParams] = useSearchParams();
32 | const currentID = decodeURI(searchParams.get('id') || '');
33 |
34 | // UI State
35 | const [helpVisible, setHelpVisible] = useState(false);
36 | const [dragMode, setDragMode] = useState<'pan' | 'select'>('pan');
37 |
38 | // Get or create graph for current view
39 | const graph = useGraph(model, layouts, currentID);
40 |
41 | // Custom hooks for functionality
42 | const { layouting, handleAutoLayout } = useAutoLayout(graph || ({} as GraphData));
43 | const { saving, handleSave } = useSave(graph || ({} as GraphData), currentID);
44 |
45 | if (!graph) {
46 | return ;
47 | }
48 |
49 | const handleToggleHelp = useCallback(() => {
50 | setHelpVisible(!helpVisible);
51 | }, [helpVisible]);
52 |
53 | // Update document title when view changes
54 | useEffect(() => {
55 | if (graph && graph.name) {
56 | document.title = `${graph.name} - Model`;
57 | }
58 | }, [graph]);
59 |
60 | // Setup keyboard shortcuts
61 | useKeyboardShortcuts(handleToggleHelp, handleSave, graph, dragMode, setDragMode, handleAutoLayout);
62 |
63 | const handleViewChange = useCallback((id: string) => {
64 | setSearchParams({ id: encodeURIComponent(id) });
65 | }, [setSearchParams]);
66 |
67 | const handleSelect = useCallback((id: string | null) => {
68 | if (id) {
69 | const element = graph.metadata.elements.find((m: any) => m.id === id);
70 | console.log(removeEmptyProps(element));
71 | }
72 | }, [graph]);
73 |
74 | return (
75 | <>
76 |
89 |
95 | {helpVisible && }
96 | >
97 | );
98 | };
99 |
100 | const ViewRedirect: FC<{ model: any }> = ({ model }) => {
101 | const views = listViews(model);
102 |
103 | React.useEffect(() => {
104 | // Set default title when no view is selected
105 | document.title = 'Model - Architecture Diagrams as Code';
106 |
107 | if (views.length > 0) {
108 | document.location.href = '?id=' + views[0].key;
109 | }
110 | }, [views]);
111 |
112 | if (views.length > 0) {
113 | return <>Redirecting to {views[0].title}>;
114 | }
115 | return <>No views available>;
116 | };
117 |
118 |
--------------------------------------------------------------------------------
/cmd/mdl/webapp/src/graph-view/defs.ts:
--------------------------------------------------------------------------------
1 | export const defs = `
2 |
3 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | `
--------------------------------------------------------------------------------
/cmd/mdl/webapp/src/graph-view/graph-react.tsx:
--------------------------------------------------------------------------------
1 | import React, {FC, useEffect, useRef, useState} from "react";
2 | import {buildGraph, getZoomAuto, GraphData, Node, setZoom, addCursorInteraction, restoreViewState, saveViewState} from "./graph";
3 |
4 | interface Props {
5 | data: GraphData;
6 | onSelect: (nodeName: string | null) => void;
7 | dragMode: 'pan' | 'select';
8 | }
9 |
10 | export const Graph: FC = ({data, onSelect, dragMode}) => {
11 | const [graphState, setGraphState] = useState(null);
12 | const ref = useRef(null);
13 |
14 | // Single effect for building the graph and handling all setup/cleanup
15 | useEffect(() => {
16 | if (!ref.current) return;
17 |
18 | // Clear previous content
19 | ref.current.innerHTML = '';
20 |
21 | // Build graph with current props
22 | const g = buildGraph(data, (n: Node | null) => onSelect(n ? n.id : null), dragMode);
23 | ref.current.append(g.svg);
24 | setGraphState(g);
25 |
26 | // Try to restore previous view state, otherwise use auto zoom
27 | if (!restoreViewState(data.id)) {
28 | setZoom(getZoomAuto());
29 | }
30 |
31 | // Save view state before page unload
32 | const handleBeforeUnload = () => {
33 | if (data?.id) {
34 | saveViewState(data.id);
35 | }
36 | };
37 |
38 | window.addEventListener('beforeunload', handleBeforeUnload);
39 |
40 | return () => {
41 | // Save view state before cleanup
42 | if (data?.id) {
43 | saveViewState(data.id);
44 | }
45 |
46 | window.removeEventListener('beforeunload', handleBeforeUnload);
47 |
48 | if (ref.current) {
49 | ref.current.innerHTML = '';
50 | }
51 | };
52 | }, [data, onSelect, dragMode]);
53 |
54 | // Effect for updating drag mode on existing graph
55 | useEffect(() => {
56 | if (graphState?.svg) {
57 | // Clean up existing cursor interaction
58 | const svg = graphState.svg;
59 | const existingCleanup = (svg as any).__cursorInteractionCleanup;
60 | if (existingCleanup) {
61 | existingCleanup();
62 | }
63 |
64 | // Set up cursor interaction with current drag mode
65 | addCursorInteraction(svg, dragMode);
66 | }
67 | }, [dragMode, graphState]);
68 |
69 | return ;
70 | }
--------------------------------------------------------------------------------
/cmd/mdl/webapp/src/graph-view/intersect.ts:
--------------------------------------------------------------------------------
1 | interface Point {
2 | x: number;
3 | y: number;
4 | }
5 |
6 | interface BBox extends Point {
7 | width: number;
8 | height: number;
9 | }
10 |
11 |
12 | export function insideBox(p: Point, b: BBox, centeredBox = true): boolean {
13 | return centeredBox ?
14 | (p.x > b.x - b.width / 2 && p.x < b.x + b.width / 2 && p.y > b.y - b.height / 2 && p.y < b.y + b.height / 2) :
15 | (p.x > b.x && p.x < b.x + b.width && p.y > b.y && p.y < b.y + b.height)
16 | }
17 |
18 | export function boxesOverlap(b1: BBox, b2: BBox): boolean {
19 | return b1.x < b2.x + b2.width && b1.y < b2.y + b2.height && b1.x + b1.width > b2.x && b1.y + b1.height > b2.y
20 | }
21 |
22 | export function uncenterBox(b: BBox): BBox {
23 | return {x: b.x - b.width / 2, y: b.y - b.height / 2, width: b.width, height: b.height}
24 | }
25 |
26 | export function scaleBox(b: BBox, sc: number): BBox {
27 | return {x: b.x * sc, y: b.y * sc, width: b.width * sc, height: b.height * sc}
28 | }
29 |
30 | // intersect 2 segments (p1->q1) with (p2, q2)
31 | // if the lines intersect, the result contains the x and y of the intersection (treating the lines as infinite)
32 | // and booleans for whether line segment 1 or line segment 2 contain the point
33 | function segmentIntersection(p1: Point, q1: Point, p2: Point, q2: Point) {
34 | let denominator, a, b, numerator1, numerator2,
35 | result: { x: number, y: number, onLine1: boolean, onLine2: boolean } = {
36 | x: null,
37 | y: null,
38 | onLine1: false,
39 | onLine2: false
40 | };
41 | denominator = (q2.y - p2.y) * (q1.x - p1.x) - (q2.x - p2.x) * (q1.y - p1.y);
42 | if (denominator == 0) {
43 | return result;
44 | }
45 | a = p1.y - p2.y;
46 | b = p1.x - p2.x;
47 | numerator1 = ((q2.x - p2.x) * a) - ((q2.y - p2.y) * b);
48 | numerator2 = ((q1.x - p1.x) * a) - ((q1.y - p1.y) * b);
49 | a = numerator1 / denominator;
50 | b = numerator2 / denominator;
51 |
52 | // if we cast these lines infinitely in both directions, they intersect here:
53 | result.x = p1.x + (a * (q1.x - p1.x));
54 | result.y = p1.y + (a * (q1.y - p1.y));
55 |
56 | // if line1 is a segment and line2 is infinite, they intersect if:
57 | if (a > 0 && a < 1) {
58 | result.onLine1 = true;
59 | }
60 | // if line2 is a segment and line1 is infinite, they intersect if:
61 | if (b >= 0 && b <= 1) {
62 | result.onLine2 = true;
63 | }
64 | // if line1 and line2 are segments, they intersect if both of the above are true
65 | return result;
66 | }
67 |
68 | // intersects a segment (p1->p2) with a box
69 | export function intersectRectFull(p1: Point, p2: Point, box: BBox): Point[] {
70 | const w = box.width / 2
71 | const h = box.height / 2
72 | const segs: { p: Point; q: Point }[] = [
73 | {p: {x: box.x - w, y: box.y - h}, q: {x: box.x - w, y: box.y + h}},
74 | {p: {x: box.x - w, y: box.y - h}, q: {x: box.x + w, y: box.y - h}},
75 | {p: {x: box.x + w, y: box.y - h}, q: {x: box.x + w, y: box.y + h}},
76 | {p: {x: box.x - w, y: box.y + h}, q: {x: box.x + w, y: box.y + h}},
77 | ]
78 | return segs.map(s => segmentIntersection(p1, p2, s.p, s.q)).filter(ret => ret.onLine1 && ret.onLine2)
79 | }
80 |
81 | // intersects a line that goes from p to the center of the box
82 | export function intersectRect(box: BBox, p: Point): Point {
83 | if (insideBox(p, box)) return {x: box.x, y: box.y}
84 | return intersectRectFull(box, p, box)[0] || {x: box.x, y: box.y}
85 | }
86 |
87 | export function intersectEllipse(ellCenter: Point, rx: number, ry: number, nodeCenter: Point, point: Point) {
88 |
89 | //translate all to center ellipse
90 | const p1 = {x: point.x - ellCenter.x, y: point.y - ellCenter.y}
91 | const p2 = {x: nodeCenter.x - ellCenter.x, y: nodeCenter.y - ellCenter.y}
92 |
93 | if (p2.x == p1.x) { //hack to avoid singularity
94 | p1.x += .0000001
95 | }
96 |
97 | const s = (p2.y - p1.y) / (p2.x - p1.x);
98 | const si = p2.y - (s * p2.x);
99 | const a = (ry * ry) + (rx * rx * s * s);
100 | const b = 2 * rx * rx * si * s;
101 | const c = rx * rx * si * si - rx * rx * ry * ry;
102 |
103 | const radicand_sqrt = Math.sqrt((b * b) - (4 * a * c));
104 | const x = p1.x > p2.x ?
105 | (-b + radicand_sqrt) / (2 * a) :
106 | (-b - radicand_sqrt) / (2 * a)
107 | const pos = {
108 | x: x,
109 | y: s * x + si
110 | }
111 | //translate back
112 | pos.x += ellCenter.x;
113 | pos.y += ellCenter.y
114 |
115 | return pos;
116 | }
117 |
118 | export interface Segment {
119 | p: Point;
120 | q: Point;
121 | }
122 |
123 | // given a polyline as a list of segments, interrupt it over the box so no line is inside the box
124 | export function intersectPolylineBox(segments: Segment[], box: BBox) {
125 | for (let i = 0; i < segments.length; i++) {
126 | const s = segments[i]
127 | if (insideBox(s.p, box)) {
128 | if (insideBox(s.q, box)) { // segment both ends inside box
129 | segments.splice(i, 1)
130 | i -= 1
131 | } else { // segment start inside box
132 | s.p = intersectRectFull(s.p, s.q, box)[0]
133 | }
134 | } else {
135 | if (insideBox(s.q, box)) { // segment end inside box
136 | s.q = intersectRectFull(s.p, s.q, box)[0]
137 | } else { // both ends outside
138 | const ret = intersectRectFull(s.p, s.q, box)
139 | if (ret.length == 2) { // intersects the box, splice segment
140 | // order the intersection points, closest first
141 | const dst1 = Math.abs(ret[0].x - s.p.x) + Math.abs(ret[0].y - s.p.y)
142 | const dst2 = Math.abs(ret[1].x - s.p.x) + Math.abs(ret[1].y - s.p.y)
143 | if (dst1 > dst2) ret.reverse()
144 | // split the segment in 2
145 | const s2 = {p: ret[1], q: s.q}
146 | s.q = ret[0]
147 | segments.splice(i + 1, 0, s2)
148 | i += 1
149 | }
150 | }
151 | }
152 | }
153 | }
154 |
155 | export function project(p: Point, a: Point, b: Point): Point {
156 | let atob = {x: b.x - a.x, y: b.y - a.y};
157 | let atop = {x: p.x - a.x, y: p.y - a.y};
158 | let len = atob.x * atob.x + atob.y * atob.y;
159 | let dot = atop.x * atob.x + atop.y * atob.y;
160 | let t = Math.min(1, Math.max(0, dot / len));
161 | return {
162 | x: a.x + atob.x * t,
163 | y: a.y + atob.y * t
164 | };
165 | }
166 |
167 | export function cabDistance(p1: Point, p2: Point): number {
168 | return Math.abs(p2.x - p1.x) + Math.abs(p2.y - p1.y)
169 | }
--------------------------------------------------------------------------------
/cmd/mdl/webapp/src/graph-view/svg-create.ts:
--------------------------------------------------------------------------------
1 | import {svgTextWrap} from "./svg-text";
2 |
3 | export const create = {
4 | element(type: string, attrs: Record
= {}, className?: string) {
5 | const el = document.createElementNS('http://www.w3.org/2000/svg', type);
6 | Object.entries(attrs).forEach(([k, v]) => el.setAttribute(k, String(v)));
7 | if (className) el.classList.add(className);
8 | return el;
9 | },
10 |
11 | use(id: string, attrs: Record = {}) {
12 | const el = this.element('use', attrs);
13 | el.setAttributeNS('http://www.w3.org/1999/xlink', 'xlink:href', '#' + id);
14 | return el;
15 | },
16 |
17 | path(path: string, attrs: Record = {}, className?: string) {
18 | const p = this.element("path", {...attrs, d: path}, className);
19 | return p;
20 | },
21 |
22 | text(text: string, attrs: Record = {}) {
23 | const t = this.element('text', attrs) as SVGTextElement;
24 | if (text) t.textContent = text;
25 | return t;
26 | },
27 |
28 | textArea(text: string, width: number, fontSize: number, bold: boolean, x = 0, y = 0, anchor = '') {
29 | const attrs: Record = {
30 | 'font-size': `${fontSize}px`,
31 | 'font-weight': bold ? 'bold' : 'normal'
32 | };
33 | const {lines, maxW} = svgTextWrap(text, width, attrs);
34 | const txt = this.text('', {x: 0, y, 'text-anchor': anchor || undefined});
35 |
36 | lines.forEach((line, i) => {
37 | const span = this.element('tspan', {x, dy: `${fontSize + 2}px`, ...attrs});
38 | span.textContent = line;
39 | txt.append(span);
40 | });
41 |
42 | return {txt, dy: (lines.length + 1) * (fontSize + 2), maxW};
43 | },
44 |
45 | rect(width: number, height: number, x = 0, y = 0, r = 0, className?: string) {
46 | return this.element('rect', {x, y, rx: r, ry: r, width, height}, className) as SVGRectElement;
47 | },
48 |
49 | icon(icon: string, x = 0, y = 0) {
50 | return this.use(icon, {x, y});
51 | },
52 |
53 | expand(x: number, y: number, expanded: boolean) {
54 | const g = this.element('g', {transform: `translate(${x},${y})`}, 'expand') as SVGGElement;
55 | g.append(
56 | this.rect(19, 19, 0, 0, 1),
57 | this.text(expanded ? '-' : '+', {x: 10, y: 14, 'text-anchor': 'middle'})
58 | );
59 | return g;
60 | }
61 | };
62 |
63 | export function setPosition(g: SVGGElement, x: number, y: number) {
64 | g.setAttribute('transform', `translate(${x},${y})`);
65 | }
--------------------------------------------------------------------------------
/cmd/mdl/webapp/src/graph-view/svg-text.ts:
--------------------------------------------------------------------------------
1 | const textMeasure = () => {
2 | const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
3 | document.body.appendChild(svg);
4 |
5 | return {
6 | measure: (text: string, attrs: { [key: string]: string }) => {
7 | const node = document.createElementNS('http://www.w3.org/2000/svg', 'text')
8 | node.setAttribute('x', '0');
9 | node.setAttribute('y', '0');
10 | for (let attr in attrs) {
11 | node.setAttribute(attr, attrs[attr]);
12 | }
13 | node.appendChild(document.createTextNode(text));
14 |
15 | svg.appendChild(node);
16 | const {width, height} = node.getBBox();
17 | svg.removeChild(node);
18 | return {width, height};
19 | },
20 | clean: () => {
21 | document.body.removeChild(svg);
22 | }
23 | }
24 | }
25 |
26 | // Helper function to break long words that exceed width
27 | const breakLongWord = (word: string, maxWidth: number, attrs: { [key: string]: string }, mt: any): string[] => {
28 | const parts: string[] = [];
29 | let currentPart = '';
30 |
31 | for (let i = 0; i < word.length; i++) {
32 | const testPart = currentPart + word[i];
33 | const size = mt.measure(testPart, attrs);
34 |
35 | if (size.width > maxWidth && currentPart.length > 0) {
36 | parts.push(currentPart);
37 | currentPart = word[i];
38 | } else {
39 | currentPart = testPart;
40 | }
41 | }
42 |
43 | if (currentPart.length > 0) {
44 | parts.push(currentPart);
45 | }
46 |
47 | return parts;
48 | }
49 |
50 | // split a text in lines wrapped at a certain width
51 | export const svgTextWrap = (text: string, width: number, attrs: { [key: string]: string }) => {
52 | const mt = textMeasure()
53 | let maxW = 0;
54 |
55 | const ret = text.trim().split('\n').map(text => { //split paragraphs
56 | //do one paragraph
57 | const words = text.trim().split(/\s+/);
58 | let lines: string[] = [];
59 | let currentLine: string[] = [];
60 |
61 | words.forEach(word => {
62 | // First check if the single word exceeds the width
63 | const wordSize = mt.measure(word, attrs);
64 | if (wordSize.width > width) {
65 | // If we have content in current line, finish it first
66 | if (currentLine.length > 0) {
67 | lines.push(currentLine.join(' '));
68 | currentLine = [];
69 | }
70 | // Break the long word into smaller parts
71 | const brokenParts = breakLongWord(word, width, attrs, mt);
72 | // Add all but the last part as complete lines
73 | for (let i = 0; i < brokenParts.length - 1; i++) {
74 | lines.push(brokenParts[i]);
75 | maxW = Math.max(maxW, mt.measure(brokenParts[i], attrs).width);
76 | }
77 | // Start new line with the last part
78 | if (brokenParts.length > 0) {
79 | currentLine = [brokenParts[brokenParts.length - 1]];
80 | }
81 | } else {
82 | // Normal word processing
83 | const newLine = [...currentLine, word];
84 | const size = mt.measure(newLine.join(' '), attrs);
85 | if (size.width > width && currentLine.length > 0) {
86 | lines.push(currentLine.join(' '));
87 | currentLine = [word];
88 | } else {
89 | maxW = Math.max(maxW, size.width)
90 | currentLine = newLine;
91 | }
92 | }
93 | });
94 |
95 | if (currentLine.length > 0) {
96 | lines.push(currentLine.join(' '));
97 | }
98 | return lines;
99 | }).reduce((a, v) => a.concat(v), []) //flatten
100 |
101 | mt.clean()
102 | return {lines: ret, maxW};
103 | };
104 |
105 |
106 |
--------------------------------------------------------------------------------
/cmd/mdl/webapp/src/graph-view/undo.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Undo functionality
3 | * at every change in the document, Undo can save a new version
4 | * so the user can "undo" and "redo" changes by reverting to an
5 | * older version of the document
6 | */
7 |
8 | export class Undo {
9 | private readonly versions: Doc[] = [];
10 | private pos: number = 0;
11 | private lastSavedPos: number = 0;
12 | private readonly exportDoc: () => Doc;
13 | private readonly importDoc: (d: Doc) => void;
14 | change: () => void;
15 | private tmpPreviousState: Doc | null = null;
16 |
17 | constructor(id: string, exportDoc: () => Doc, importDoc: (d: Doc) => void) {
18 | this.exportDoc = exportDoc;
19 | this.importDoc = importDoc;
20 | this.change = debounce(() => this.saveNow(), 300);
21 | }
22 |
23 | // Store the state previous to the changes collected in the debounce period
24 | beforeChange() {
25 | if (!this.tmpPreviousState) {
26 | this.tmpPreviousState = this.deepClone(this.exportDoc());
27 | }
28 | }
29 |
30 | length() {
31 | return this.versions.length;
32 | }
33 |
34 | currentState() {
35 | return this.deepClone(this.versions[this.pos - 1]);
36 | }
37 |
38 | private saveNow() {
39 | if (!this.tmpPreviousState) {
40 | throw Error("undo.change() was called without previously calling undo.beforeChange()!");
41 | }
42 |
43 | this.versions[this.pos] = this.deepClone(this.exportDoc());
44 | this.versions[this.pos - 1] = this.tmpPreviousState;
45 | this.tmpPreviousState = null;
46 | this.pos += 1;
47 |
48 | // Remove anything that might be on top of this version
49 | this.versions.splice(this.pos);
50 | }
51 |
52 | private deepClone(doc: Doc): Doc {
53 | // Use modern structuredClone if available, fallback to JSON
54 | if (typeof structuredClone !== 'undefined') {
55 | return structuredClone(doc);
56 | }
57 | return JSON.parse(JSON.stringify(doc));
58 | }
59 |
60 | undo() {
61 | if (this.pos < 2) return;
62 | this.pos -= 1;
63 | const doc = this.versions[this.pos - 1];
64 | this.importDoc(this.deepClone(doc));
65 | }
66 |
67 | redo() {
68 | if (this.pos > this.versions.length - 1) return;
69 | const doc = this.versions[this.pos];
70 | this.importDoc(this.deepClone(doc));
71 | this.pos += 1;
72 | }
73 |
74 | changed() {
75 | return this.pos !== this.lastSavedPos;
76 | }
77 |
78 | setSaved() {
79 | this.lastSavedPos = this.pos;
80 | }
81 | }
82 |
83 | function debounce(func: () => void, wait: number) {
84 | let timeout: ReturnType;
85 | return function () {
86 | const context = this;
87 | const later = function () {
88 | timeout = null;
89 | func.apply(context);
90 | };
91 | clearTimeout(timeout);
92 | timeout = setTimeout(later, wait);
93 | };
94 | }
--------------------------------------------------------------------------------
/cmd/mdl/webapp/src/hooks.ts:
--------------------------------------------------------------------------------
1 | import { useState, useCallback, useEffect } from 'react';
2 | import { GraphData } from './graph-view/graph';
3 | import { parseView } from './parseModel';
4 | import { LayoutOptions } from './graph-view/layout';
5 | import {
6 | findShortcut,
7 | HELP,
8 | SAVE,
9 | TOGGLE_DRAG_MODE,
10 | ALIGN_HORIZONTAL,
11 | ALIGN_VERTICAL,
12 | DISTRIBUTE_HORIZONTAL,
13 | DISTRIBUTE_VERTICAL,
14 | AUTO_LAYOUT,
15 | RESET_POSITION,
16 | TOGGLE_GRID,
17 | TOGGLE_SNAP_TO_GRID,
18 | SNAP_ALL_TO_GRID,
19 | MOVE_LEFT,
20 | MOVE_RIGHT,
21 | MOVE_UP,
22 | MOVE_DOWN,
23 | MOVE_LEFT_FINE,
24 | MOVE_RIGHT_FINE,
25 | MOVE_UP_FINE,
26 | MOVE_DOWN_FINE
27 | } from './shortcuts';
28 |
29 | // Global state for graphs to preserve edits
30 | const graphs: { [key: string]: GraphData } = {};
31 |
32 | // Custom hook for graph management
33 | export const useGraph = (model: any, layouts: any, currentID: string): GraphData | null => {
34 | if (graphs[currentID]) {
35 | return graphs[currentID];
36 | }
37 |
38 | const graph = parseView(model, layouts, currentID);
39 | if (graph) {
40 | graphs[currentID] = graph;
41 | }
42 |
43 | return graph;
44 | };
45 |
46 | // Custom hook for auto layout functionality
47 | export const useAutoLayout = (graph: GraphData) => {
48 | const [layouting, setLayouting] = useState(false);
49 |
50 | const handleAutoLayout = useCallback(async () => {
51 | setLayouting(true);
52 | try {
53 | const options: LayoutOptions = {
54 | direction: 'DOWN',
55 | compactLayout: true
56 | };
57 | await graph.autoLayout(options);
58 | } catch (error) {
59 | console.error('Layout failed:', error);
60 | alert('Layout failed. See console for details.');
61 | } finally {
62 | setLayouting(false);
63 | }
64 | }, [graph]);
65 |
66 | return { layouting, handleAutoLayout };
67 | };
68 |
69 | // Custom hook for save functionality
70 | export const useSave = (graph: GraphData, currentID: string) => {
71 | const [saving, setSaving] = useState(false);
72 |
73 | const handleSave = useCallback(async () => {
74 | setSaving(true);
75 |
76 | try {
77 | const response = await fetch('data/save?id=' + encodeURIComponent(currentID), {
78 | method: 'post',
79 | body: graph.exportSVG()
80 | });
81 |
82 | if (response.status !== 202) {
83 | alert('Error saving\nSee terminal output.');
84 | } else {
85 | graph.setSaved();
86 | }
87 | } catch (error) {
88 | console.error('Save failed:', error);
89 | alert('Save failed. See console for details.');
90 | } finally {
91 | setSaving(false);
92 | }
93 | }, [graph, currentID]);
94 |
95 | return { saving, handleSave };
96 | };
97 |
98 | // Custom hook for keyboard shortcuts
99 | export const useKeyboardShortcuts = (
100 | toggleHelp: () => void,
101 | saveLayout: () => void,
102 | graph?: GraphData,
103 | dragMode?: 'pan' | 'select',
104 | setDragMode?: (mode: 'pan' | 'select') => void,
105 | onAutoLayout?: () => void
106 | ) => {
107 | useEffect(() => {
108 | const handleKeyDown = (e: KeyboardEvent) => {
109 | const shortcut = findShortcut(e);
110 |
111 | // Prevent browser default for all recognized shortcuts
112 | if (shortcut) {
113 | e.preventDefault();
114 | }
115 |
116 | if (shortcut === HELP) {
117 | toggleHelp();
118 | } else if (shortcut === SAVE) {
119 | saveLayout();
120 | } else if (shortcut === TOGGLE_DRAG_MODE && setDragMode && dragMode) {
121 | setDragMode(dragMode === 'pan' ? 'select' : 'pan');
122 | } else if (graph) {
123 | // Graph-dependent shortcuts
124 | if (shortcut === ALIGN_HORIZONTAL) {
125 | graph.alignSelectionH();
126 | } else if (shortcut === ALIGN_VERTICAL) {
127 | graph.alignSelectionV();
128 | } else if (shortcut === DISTRIBUTE_HORIZONTAL) {
129 | graph.distributeSelectionH();
130 | } else if (shortcut === DISTRIBUTE_VERTICAL) {
131 | graph.distributeSelectionV();
132 | } else if (shortcut === AUTO_LAYOUT && onAutoLayout) {
133 | onAutoLayout();
134 | } else if (shortcut === RESET_POSITION) {
135 | graph.alignTopLeft();
136 | graph.resetPanTransform();
137 | } else if (shortcut === TOGGLE_GRID) {
138 | graph.toggleGrid();
139 | } else if (shortcut === TOGGLE_SNAP_TO_GRID) {
140 | graph.toggleSnapToGrid();
141 | } else if (shortcut === SNAP_ALL_TO_GRID) {
142 | graph.snapAllToGrid();
143 | } else if (shortcut === MOVE_LEFT) {
144 | graph.moveSelected(-graph.getGridSize(), 0);
145 | } else if (shortcut === MOVE_LEFT_FINE) {
146 | graph.moveSelected(-1, 0, true); // Disable snap for fine movement
147 | } else if (shortcut === MOVE_RIGHT) {
148 | graph.moveSelected(graph.getGridSize(), 0);
149 | } else if (shortcut === MOVE_RIGHT_FINE) {
150 | graph.moveSelected(1, 0, true); // Disable snap for fine movement
151 | } else if (shortcut === MOVE_UP) {
152 | graph.moveSelected(0, -graph.getGridSize());
153 | } else if (shortcut === MOVE_UP_FINE) {
154 | graph.moveSelected(0, -1, true); // Disable snap for fine movement
155 | } else if (shortcut === MOVE_DOWN) {
156 | graph.moveSelected(0, graph.getGridSize());
157 | } else if (shortcut === MOVE_DOWN_FINE) {
158 | graph.moveSelected(0, 1, true); // Disable snap for fine movement
159 | }
160 | }
161 | };
162 |
163 | window.addEventListener('keydown', handleKeyDown);
164 | return () => window.removeEventListener('keydown', handleKeyDown);
165 | }, [toggleHelp, saveLayout, graph, dragMode, setDragMode, onAutoLayout]);
166 | };
167 |
168 | // Utility function to clear graph cache
169 | export const clearGraphCache = (currentID?: string) => {
170 | if (currentID) {
171 | delete graphs[currentID];
172 | } else {
173 | Object.keys(graphs).forEach(key => delete graphs[key]);
174 | }
175 | };
--------------------------------------------------------------------------------
/cmd/mdl/webapp/src/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Model - Architecture Diagrams as Code
6 |
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/cmd/mdl/webapp/src/index.tsx:
--------------------------------------------------------------------------------
1 | import { createRoot } from 'react-dom/client';
2 | import React, { Suspense, lazy, useEffect, useState } from 'react';
3 | import { refreshGraph } from "./Root";
4 | import './style.css';
5 | import '@fortawesome/fontawesome-free/css/all.css';
6 | import { RefreshConnector } from "./websocket";
7 |
8 | const Root = lazy(() => import('./Root').then(module => ({ default: module.Root })));
9 |
10 | interface ModelData {
11 | model: any;
12 | layout: any;
13 | }
14 |
15 | interface AppState {
16 | data: ModelData | null;
17 | error: string | null;
18 | loading: boolean;
19 | }
20 |
21 | const App: React.FC = () => {
22 | const [state, setState] = useState({
23 | data: null,
24 | error: null,
25 | loading: true
26 | });
27 |
28 | const loadData = async () => {
29 | setState(prev => ({ ...prev, loading: true, error: null }));
30 |
31 | try {
32 | const [modelResponse, layoutResponse] = await Promise.all([
33 | fetch('data/model.json'),
34 | fetch('data/layout.json')
35 | ]);
36 |
37 | if (!modelResponse.ok) {
38 | throw new Error(`Failed to fetch model: ${modelResponse.statusText}`);
39 | }
40 |
41 | if (!layoutResponse.ok) {
42 | throw new Error(`Failed to fetch layout: ${layoutResponse.statusText}`);
43 | }
44 |
45 | const [model, layout] = await Promise.all([
46 | modelResponse.json(),
47 | layoutResponse.json()
48 | ]);
49 |
50 | setState({
51 | data: { model, layout },
52 | error: null,
53 | loading: false
54 | });
55 | } catch (error) {
56 | console.error('Failed to load data:', error);
57 | setState({
58 | data: null,
59 | error: error instanceof Error ? error.message : 'Unknown error occurred',
60 | loading: false
61 | });
62 | }
63 | };
64 |
65 | const handleFileChange = (path: string) => {
66 | if (path.endsWith('.svg')) {
67 | return; // Ignore SVG changes to avoid infinite loops
68 | }
69 |
70 | console.log('File changed:', path);
71 | refreshGraph();
72 | loadData();
73 | };
74 |
75 | useEffect(() => {
76 | // Setup refresh connector
77 | const refreshConnector = new RefreshConnector(handleFileChange);
78 | refreshConnector.connect();
79 |
80 | // Initial data load
81 | loadData();
82 |
83 | // Cleanup function
84 | return () => {
85 | // RefreshConnector cleanup would go here if it had a disconnect method
86 | };
87 | }, []);
88 |
89 | if (state.loading) {
90 | return ;
91 | }
92 |
93 | if (state.error) {
94 | return ;
95 | }
96 |
97 | if (!state.data) {
98 | return ;
99 | }
100 |
101 | return (
102 | }>
103 |
104 |
105 | );
106 | };
107 |
108 | const LoadingScreen: React.FC = () => (
109 |
118 | );
119 |
120 | const ErrorScreen: React.FC<{ error: string; onRetry: () => void }> = ({ error, onRetry }) => (
121 |
132 |
Error loading application
133 |
{error}
134 |
148 |
149 | );
150 |
151 | // Initialize the application
152 | const container = document.getElementById('root');
153 | if (!container) {
154 | throw new Error('Root container not found');
155 | }
156 |
157 | const root = createRoot(container);
158 | root.render();
--------------------------------------------------------------------------------
/cmd/mdl/webapp/src/static/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Model - Architecture Diagrams as Code
6 |
7 |
17 |
18 |
19 |
20 |
21 |
108 |
109 |
114 |
115 |
116 |
117 |
118 |
--------------------------------------------------------------------------------
/cmd/mdl/webapp/src/static/index.tsx:
--------------------------------------------------------------------------------
1 | import {parseView} from "../parseModel";
2 | import {buildGraphView} from "../graph-view/graph";
3 |
4 | document.querySelectorAll('.c4-diagram').forEach(async el => {
5 | const dataEl = document.getElementById(el.getAttribute('data-model'))
6 | const model = JSON.parse(dataEl.textContent)
7 |
8 | const graph = parseView(model as any, {}, el.getAttribute('data-view-key'))
9 | const svg = buildGraphView(graph)
10 | el.append(svg)
11 |
12 | try {
13 | await graph.autoLayout()
14 | } catch (error) {
15 | console.error('Auto layout failed for static diagram:', error)
16 | }
17 | })
18 |
--------------------------------------------------------------------------------
/cmd/mdl/webapp/src/utils.ts:
--------------------------------------------------------------------------------
1 | // Helper functions for the application
2 |
3 | export function removeEmptyProps(obj: any) {
4 | return JSON.parse(JSON.stringify(obj));
5 | }
6 |
7 | export function camelToWords(camel: string) {
8 | const split = camel.replace(/([A-Z])/g, " $1");
9 | return split.charAt(0).toUpperCase() + split.slice(1);
10 | }
11 |
12 | export function getCurrentViewID() {
13 | const params = new URLSearchParams(document.location.search);
14 | return params.get('id') || '';
15 | }
--------------------------------------------------------------------------------
/cmd/mdl/webapp/src/utils/platform.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Robust platform detection utilities
3 | */
4 |
5 | /**
6 | * Detects if the user is on a Mac platform using multiple detection methods
7 | * for maximum compatibility across browsers and future-proofing.
8 | */
9 | export const isMac = (): boolean => {
10 | if (typeof navigator === 'undefined') return false;
11 |
12 | // Method 1: Check userAgentData (modern browsers, most reliable)
13 | if ('userAgentData' in navigator && (navigator as any).userAgentData) {
14 | const platform = (navigator as any).userAgentData.platform;
15 | if (platform && platform.toLowerCase().includes('mac')) {
16 | return true;
17 | }
18 | }
19 |
20 | // Method 2: Check userAgent string (widely supported)
21 | const userAgent = navigator.userAgent.toLowerCase();
22 | if (userAgent.includes('mac os') || userAgent.includes('macintosh')) {
23 | return true;
24 | }
25 |
26 | // Method 3: Check platform (fallback, deprecated but still widely supported)
27 | if (navigator.platform) {
28 | const platform = navigator.platform.toLowerCase();
29 | if (platform.includes('mac') || platform.includes('darwin')) {
30 | return true;
31 | }
32 | }
33 |
34 | // Method 4: Check for Mac-specific features as additional validation
35 | try {
36 | const testEvent = new KeyboardEvent('keydown', { metaKey: true });
37 | if (testEvent.metaKey !== undefined) {
38 | // Additional heuristic: Mac typically has different key layouts
39 | return /mac|darwin|os x/i.test(navigator.userAgent);
40 | }
41 | } catch (e) {
42 | // Ignore errors in older browsers
43 | }
44 |
45 | return false;
46 | };
47 |
48 | /**
49 | * Gets the appropriate modifier key name for the current platform
50 | */
51 | export const getModifierKeyName = (): string => {
52 | return isMac() ? 'Cmd' : 'Ctrl';
53 | };
54 |
55 | /**
56 | * Gets the appropriate modifier key property for keyboard events
57 | */
58 | export const getModifierKeyProperty = (event: KeyboardEvent): boolean => {
59 | return isMac() ? event.metaKey : event.ctrlKey;
60 | };
61 |
62 | /**
63 | * Gets the appropriate Alt key name for the current platform
64 | */
65 | export const getAltKeyName = (): string => {
66 | return isMac() ? 'Option' : 'Alt';
67 | };
--------------------------------------------------------------------------------
/cmd/mdl/webapp/src/websocket.ts:
--------------------------------------------------------------------------------
1 | interface LiveReloadOptions {
2 | minDelay: number;
3 | maxDelay: number;
4 | handshakeTimeout: number;
5 | }
6 |
7 | interface LiveReloadMessage {
8 | command: string;
9 | protocols?: string[];
10 | ver?: string;
11 | path?: string;
12 | }
13 |
14 | class Timer {
15 | private readonly callback: () => void;
16 | private readonly handler: () => void;
17 | private running: boolean = false;
18 | private timeoutId: ReturnType | null = null;
19 |
20 | constructor(callback: () => void) {
21 | this.callback = callback;
22 | this.handler = () => {
23 | this.running = false;
24 | this.timeoutId = null;
25 | this.callback();
26 | };
27 | }
28 |
29 | start(timeout: number): void {
30 | if (this.running) {
31 | this.stop();
32 | }
33 | this.timeoutId = setTimeout(this.handler, timeout);
34 | this.running = true;
35 | }
36 |
37 | stop(): void {
38 | if (this.running && this.timeoutId !== null) {
39 | clearTimeout(this.timeoutId);
40 | this.running = false;
41 | this.timeoutId = null;
42 | }
43 | }
44 |
45 | isRunning(): boolean {
46 | return this.running;
47 | }
48 | }
49 |
50 | /**
51 | * RefreshConnector implements the livereload protocol to listen for file changes via WebSocket
52 | */
53 | export class RefreshConnector {
54 | private static readonly DEFAULT_OPTIONS: LiveReloadOptions = {
55 | minDelay: 1000,
56 | maxDelay: 60000,
57 | handshakeTimeout: 5000
58 | };
59 |
60 | private static readonly LIVERELOAD_PROTOCOLS = [
61 | 'http://livereload.com/protocols/official-9',
62 | 'http://livereload.com/protocols/2.x-remote-control'
63 | ];
64 |
65 | private readonly uri: string;
66 | private readonly options: LiveReloadOptions;
67 | private readonly fileChangeHandler: (path: string) => void;
68 |
69 | private socket: WebSocket | null = null;
70 | private nextDelay: number;
71 | private connectionDesired: boolean = false;
72 | private disconnectionReason: string = '';
73 |
74 | private readonly handshakeTimeout: Timer;
75 | private readonly reconnectTimer: Timer;
76 |
77 | constructor(fileChangeHandler: (file: string) => void, options?: Partial) {
78 | this.fileChangeHandler = fileChangeHandler;
79 | this.options = { ...RefreshConnector.DEFAULT_OPTIONS, ...options };
80 | this.uri = 'ws://localhost:35729/livereload';
81 | this.nextDelay = this.options.minDelay;
82 |
83 | this.handshakeTimeout = new Timer(() => this.handleHandshakeTimeout());
84 | this.reconnectTimer = new Timer(() => this.attemptReconnection());
85 | }
86 |
87 | connect(): void {
88 | this.connectionDesired = true;
89 |
90 | if (this.isSocketConnected()) {
91 | return;
92 | }
93 |
94 | this.prepareForConnection();
95 | this.createWebSocket();
96 | }
97 |
98 | disconnect(): void {
99 | this.connectionDesired = false;
100 | this.reconnectTimer.stop();
101 |
102 | if (this.isSocketConnected()) {
103 | this.disconnectionReason = 'manual';
104 | this.socket!.close();
105 | }
106 | }
107 |
108 | private isSocketConnected(): boolean {
109 | return this.socket !== null && this.socket.readyState === WebSocket.OPEN;
110 | }
111 |
112 | private prepareForConnection(): void {
113 | this.reconnectTimer.stop();
114 | this.disconnectionReason = 'cannot-connect';
115 | }
116 |
117 | private createWebSocket(): void {
118 | this.socket = new WebSocket(this.uri);
119 | this.socket.onopen = () => this.handleOpen();
120 | this.socket.onclose = () => this.handleClose();
121 | this.socket.onmessage = (event) => this.handleMessage(event);
122 | this.socket.onerror = () => this.handleError();
123 | }
124 |
125 | private handleOpen(): void {
126 | this.disconnectionReason = 'handshake-failed';
127 | this.startHandshake();
128 | }
129 |
130 | private handleClose(): void {
131 | console.log(`WebSocket disconnected: ${this.disconnectionReason}. Retry in ${this.nextDelay}ms`);
132 | this.scheduleReconnection();
133 | }
134 |
135 | private handleMessage(event: MessageEvent): void {
136 | try {
137 | const message: LiveReloadMessage = JSON.parse(event.data);
138 | this.processMessage(message);
139 | } catch (error) {
140 | console.error('Failed to parse WebSocket message:', error);
141 | }
142 | }
143 |
144 | private handleError(): void {
145 | // Error handling is done in onclose
146 | }
147 |
148 | private processMessage(message: LiveReloadMessage): void {
149 | switch (message.command) {
150 | case 'hello':
151 | this.handleHelloMessage();
152 | break;
153 | case 'reload':
154 | this.handleReloadMessage(message);
155 | break;
156 | default:
157 | console.log('Unknown WebSocket message received:', message);
158 | }
159 | }
160 |
161 | private handleHelloMessage(): void {
162 | this.handshakeTimeout.stop();
163 | this.nextDelay = this.options.minDelay;
164 | }
165 |
166 | private handleReloadMessage(message: LiveReloadMessage): void {
167 | // The livereload server closes connection after sending reload
168 | // We must reconnect
169 | this.reconnectTimer.stop();
170 | this.connect();
171 |
172 | if (message.path) {
173 | this.fileChangeHandler(message.path);
174 | }
175 | }
176 |
177 | private startHandshake(): void {
178 | const helloMessage: LiveReloadMessage = {
179 | command: 'hello',
180 | protocols: RefreshConnector.LIVERELOAD_PROTOCOLS,
181 | ver: '3.3.1'
182 | };
183 |
184 | this.sendCommand(helloMessage);
185 | this.handshakeTimeout.start(this.options.handshakeTimeout);
186 | }
187 |
188 | private handleHandshakeTimeout(): void {
189 | if (this.isSocketConnected()) {
190 | this.disconnectionReason = 'handshake-timeout';
191 | this.socket!.close();
192 | }
193 | }
194 |
195 | private attemptReconnection(): void {
196 | if (this.connectionDesired) {
197 | this.connect();
198 | }
199 | }
200 |
201 | private scheduleReconnection(): void {
202 | if (!this.connectionDesired) {
203 | return; // Don't reconnect after manual disconnection
204 | }
205 |
206 | if (!this.reconnectTimer.isRunning()) {
207 | this.reconnectTimer.start(this.nextDelay);
208 | this.nextDelay = Math.min(this.options.maxDelay, this.nextDelay * 2);
209 | }
210 | }
211 |
212 | private sendCommand(command: LiveReloadMessage): void {
213 | if (this.isSocketConnected()) {
214 | this.socket!.send(JSON.stringify(command));
215 | }
216 | }
217 | }
--------------------------------------------------------------------------------
/cmd/mdl/webapp/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "noImplicitAny": true,
4 | "esModuleInterop": true,
5 | "jsx": "react",
6 | "resolveJsonModule": true
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/cmd/mdl/webapp/webpack.config.base.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/no-var-requires */
2 | /* eslint-env node */
3 | const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin');
4 | const CopyWebpackPlugin = require('copy-webpack-plugin');
5 |
6 | const isDevelopment = process.env.NODE_ENV !== 'production';
7 |
8 | module.exports = {
9 | mode: isDevelopment ? 'development' : 'production',
10 |
11 | devtool: isDevelopment ? 'eval-source-map' : 'source-map',
12 | resolve: {
13 | extensions: ['.ts', '.tsx', '.js', '.jsx']
14 | },
15 | module: {
16 | rules: [
17 | {
18 | test: /\.(j|t)sx?$/,
19 | exclude: /node_modules/,
20 | use: {
21 | loader: 'babel-loader'
22 | }
23 | },
24 | {
25 | test: /\.css$/i,
26 | use: ['style-loader', 'css-loader']
27 | },
28 | {
29 | test: /\.html$/,
30 | use: [
31 | {
32 | loader: 'html-loader'
33 | }
34 | ]
35 | },
36 | { test: /\.svg$/, loader: 'svg-react-loader' },
37 | {
38 | test: /\.(png|jp(e*)g)$/,
39 | use: [{
40 | loader: 'url-loader',
41 | options: {
42 | limit: 8000, // Convert images < 8kb to base64 strings
43 | name: 'images/[hash]-[name].[ext]'
44 | }
45 | }]
46 | }
47 | ]
48 | },
49 | plugins: [
50 | isDevelopment && new ReactRefreshWebpackPlugin()
51 | ].filter(Boolean),
52 | optimization: {
53 | runtimeChunk: 'single',
54 | },
55 | devServer: {
56 | historyApiFallback: true,
57 | hot: true,
58 | }
59 | };
60 |
--------------------------------------------------------------------------------
/cmd/mdl/webapp/webpack.config.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/no-var-requires */
2 | /* eslint-env node */
3 | const HtmlWebPackPlugin = require('html-webpack-plugin');
4 | const CopyWebpackPlugin = require('copy-webpack-plugin');
5 | const { CleanWebpackPlugin } = require('clean-webpack-plugin');
6 | const path = require('path');
7 |
8 | const base = require('./webpack.config.base')
9 |
10 | base.entry = './src/index.tsx'
11 | base.plugins.push(
12 | new HtmlWebPackPlugin({
13 | template: './src/index.html',
14 | filename: './index.html',
15 | }),
16 | new CleanWebpackPlugin({
17 | protectWebpackAssets: false,
18 | cleanAfterEveryBuildPatterns: ['*.LICENSE.txt'],
19 | })
20 | )
21 | base.output = {
22 | path: path.resolve(__dirname, 'dist/'),
23 | publicPath: '/',
24 | }
25 | base.devtool = 'source-map'
26 |
27 | module.exports = base
28 |
--------------------------------------------------------------------------------
/cmd/mdl/webapp/webpack.config.static.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/no-var-requires */
2 | /* eslint-env node */
3 | const HtmlWebPackPlugin = require('html-webpack-plugin');
4 | const { CleanWebpackPlugin } = require('clean-webpack-plugin');
5 | const path = require('path');
6 |
7 | const base = require('./webpack.config.base')
8 |
9 | base.entry = './src/static/index.tsx'
10 | base.plugins.push(
11 | new HtmlWebPackPlugin({
12 | template: './src/static/index.html',
13 | filename: './index.html',
14 | }),
15 | new CleanWebpackPlugin({
16 | protectWebpackAssets: false,
17 | cleanAfterEveryBuildPatterns: ['*.LICENSE.txt'],
18 | })
19 | )
20 | base.output = {
21 | path: path.resolve(__dirname, 'dist-static/'),
22 | publicPath: '.',
23 | }
24 |
25 | module.exports = base
26 |
--------------------------------------------------------------------------------
/codegen/json.go:
--------------------------------------------------------------------------------
1 | package codegen
2 |
3 | import (
4 | "fmt"
5 | "os"
6 | "os/exec"
7 | "path"
8 |
9 | "goa.design/goa/v3/codegen"
10 | "golang.org/x/tools/go/packages"
11 | )
12 |
13 | // TmpDirPrefix is the prefix used to create temporary directories.
14 | const TmpDirPrefix = "mdl--"
15 |
16 | // JSON generates a JSON representation of the model described in pkg.
17 | // pkg must be a valid Go package import path.
18 | func JSON(pkg string, debug bool) ([]byte, error) {
19 | // Validate package import path
20 | if _, err := packages.Load(&packages.Config{Mode: packages.NeedName}, pkg); err != nil {
21 | return nil, err
22 | }
23 |
24 | // Write program that generates JSON
25 | cwd, err := os.Getwd()
26 | if err != nil {
27 | cwd = "."
28 | }
29 | tmpDir, err := os.MkdirTemp(cwd, TmpDirPrefix)
30 | if err != nil {
31 | return nil, err
32 | }
33 | defer func() {
34 | if err := os.RemoveAll(tmpDir); err != nil {
35 | fmt.Fprintf(os.Stderr, "failed to remove temp dir: %v\n", err)
36 | }
37 | }()
38 | var sections []*codegen.SectionTemplate
39 | {
40 | imports := []*codegen.ImportSpec{
41 | codegen.SimpleImport("fmt"),
42 | codegen.SimpleImport("encoding/json"),
43 | codegen.SimpleImport("os"),
44 | codegen.SimpleImport("goa.design/model/mdl"),
45 | codegen.NewImport("_", pkg),
46 | }
47 | sections = []*codegen.SectionTemplate{
48 | codegen.Header("Code Generator", "main", imports),
49 | {Name: "main", Source: mainT},
50 | }
51 | }
52 | cf := &codegen.File{Path: "main.go", SectionTemplates: sections}
53 | if _, err := cf.Render(tmpDir); err != nil {
54 | return nil, err
55 | }
56 |
57 | // Compile program
58 | gobin, err := exec.LookPath("go")
59 | if err != nil {
60 | return nil, fmt.Errorf(`failed to find a go compiler, looked in "%s"`, os.Getenv("PATH"))
61 | }
62 | if _, err := runCmd(gobin, tmpDir, "build", "-o", "mdl"); err != nil {
63 | return nil, err
64 | }
65 |
66 | // Run program
67 | o, err := runCmd(path.Join(tmpDir, "mdl"), tmpDir, "model.json")
68 | if debug {
69 | fmt.Fprintln(os.Stderr, o)
70 | }
71 | if err != nil {
72 | return nil, err
73 | }
74 | return os.ReadFile(path.Join(tmpDir, "model.json"))
75 | }
76 |
77 | func runCmd(path, dir string, args ...string) (string, error) {
78 | args = append([]string{path}, args...) // args[0] becomes exec path
79 | c := exec.Cmd{Path: path, Args: args, Dir: dir}
80 | b, err := c.CombinedOutput()
81 | if err != nil {
82 | if len(b) > 0 {
83 | return "", fmt.Errorf("%s", string(b))
84 | }
85 | return "", fmt.Errorf("failed to run command %q in directory %q: %s", path, dir, err)
86 | }
87 | return string(b), nil
88 | }
89 |
90 | // mainT is the template for the generator main.
91 | const mainT = `func main() {
92 | // Retrieve output path
93 | out := os.Args[1]
94 |
95 | // Run the model DSL
96 | w, err := mdl.RunDSL()
97 | if err != nil {
98 | fmt.Fprint(os.Stderr, err.Error())
99 | os.Exit(1)
100 | }
101 | b, err := json.MarshalIndent(w, "", " ")
102 | if err != nil {
103 | fmt.Fprintf(os.Stderr, "failed to encode into JSON: %s", err.Error())
104 | os.Exit(1)
105 | }
106 | if err := os.WriteFile(out, b, 0644); err != nil {
107 | fmt.Fprintf(os.Stderr, "failed to write file: %s", err.Error())
108 | os.Exit(1)
109 | }
110 | }
111 | `
112 |
--------------------------------------------------------------------------------
/dsl/doc.go:
--------------------------------------------------------------------------------
1 | /*
2 | Package dsl implements a Go based DSL that makes it possible to describe
3 | softare architecture models following the C4 model (https://c4model.com).
4 |
5 | It is recommended to use "dot import" when using this package to write DSLs,
6 | for example:
7 |
8 | package design
9 |
10 | import . "goa.design/model/dsl"
11 |
12 | var _ = Design("", "[description]", func() {
13 | // ...
14 | })
15 |
16 | Some DSL functions accept a anonymous function as last argument (such as
17 | Design above) which makes it possible to define a nesting structure. The
18 | general shape of the DSL is:
19 |
20 | Design Design
21 | ├── Version └── Views
22 | ├── Enterprise ├── SystemLandscapeView
23 | ├── Person │ ├── Title
24 | │ ├── Tag │ ├── AddDefault
25 | │ ├── URL │ ├── Add
26 | │ ├── External │ ├── AddAll
27 | │ ├── Prop │ ├── AddNeighbors
28 | │ ├── Uses │ ├── Link
29 | │ └── InteractsWith │ ├── Remove
30 | ├── SoftwareSystem │ ├── RemoveTagged
31 | │ ├── Tag │ ├── RemoveUnreachable
32 | │ ├── URL │ ├── RemoveUnrelated
33 | │ ├── External │ ├── Unlink
34 | │ ├── Prop │ ├── AutoLayout
35 | │ ├── Uses │ ├── AnimationStep
36 | │ ├── Delivers │ ├── PaperSize
37 | │ └── Container │ └── EnterpriseBoundaryVisible
38 | │ ├── Tag ├── SystemContextView
39 | │ ├── URL │ └── ... (same as SystemLandsapeView)
40 | │ ├── Prop ├── ContainerView
41 | │ ├── Uses │ ├── AddContainers
42 | │ ├── Delivers │ ├── AddInfluencers
43 | │ └── Component │ ├── SystemBoundariesVisible
44 | │ ├── Tag │ └── ... (same as SystemLandscapeView*)
45 | │ ├── URL ├── ComponentView
46 | │ ├── Prop │ ├── AddContainers
47 | │ ├── Uses │ ├── AddComponents
48 | │ └── Delivers │ ├── ContainerBoundariesVisible
49 | └── DeploymentEnvironment │ └── ... (same as SystemLandscapeView*)
50 | ├── DeploymentNode ├── FilteredView
51 | │ ├── Tag │ ├── FilterTag
52 | │ ├── Instances │ └── Exclude
53 | │ ├── URL ├── DynamicView
54 | │ ├── Prop │ ├── Title
55 | │ └── DeploymentNode │ ├── AutoLayout
56 | │ └── ... │ ├── PaperSize
57 | ├── InfrastructureNode │ ├── Add
58 | │ ├── Tag ├── DeploymentView
59 | │ ├── URL │ └── ... (same as SystemLandscapeView*)
60 | │ └── Prop └── Style
61 | └── ContainerInstance ├── ElementStyle
62 | ├── Tag └── RelationshipStyle
63 | ├── HealthCheck
64 | └── Prop (* minus EnterpriseBoundaryVisible)
65 | */
66 | package dsl
67 |
--------------------------------------------------------------------------------
/dsl/person.go:
--------------------------------------------------------------------------------
1 | package dsl
2 |
3 | import (
4 | "strings"
5 |
6 | "goa.design/goa/v3/eval"
7 | "goa.design/model/expr"
8 | )
9 |
10 | // Person defines a person (user, actor, role or persona).
11 | //
12 | // Person must appear in a Model expression.
13 | //
14 | // Person takes one to three arguments. The first argument is the name of the
15 | // person. An optional description may be passed as second argument. The last
16 | // argument may be a function that defines tags associated with the Person.
17 | //
18 | // The valid syntax for Person is thus:
19 | //
20 | // Person("name")
21 | //
22 | // Person("name", "description")
23 | //
24 | // Person("name", func())
25 | //
26 | // Person("name", "description", func())
27 | //
28 | // Example:
29 | //
30 | // var _ = Design(func() {
31 | // Person("Employee")
32 | // Person("Customer", "A customer", func() {
33 | // Tag("system")
34 | // External()
35 | // URL("https://acme.com/docs/customer.html")
36 | // Uses(System)
37 | // InteractsWith(Employee)
38 | // })
39 | // })
40 | func Person(name string, args ...any) *expr.Person {
41 | w, ok := eval.Current().(*expr.Design)
42 | if !ok {
43 | eval.IncompatibleDSL()
44 | return nil
45 | }
46 | if strings.Contains(name, "/") {
47 | eval.ReportError("Person: name cannot include slashes")
48 | }
49 | var (
50 | desc string
51 | dsl func()
52 | )
53 | if len(args) > 0 {
54 | switch a := args[0].(type) {
55 | case string:
56 | desc = a
57 | case func():
58 | dsl = a
59 | default:
60 | eval.InvalidArgError("description or DSL function", args[0])
61 | }
62 | if len(args) > 1 {
63 | if dsl != nil {
64 | eval.ReportError("Person: DSL function must be last argument")
65 | }
66 | dsl, ok = args[1].(func())
67 | if !ok {
68 | eval.InvalidArgError("DSL function", args[1])
69 | }
70 | if len(args) > 2 {
71 | eval.ReportError("Person: too many arguments")
72 | }
73 | }
74 | }
75 | p := &expr.Person{
76 | Element: &expr.Element{
77 | Name: name,
78 | Description: desc,
79 | DSLFunc: dsl,
80 | },
81 | }
82 | return w.Model.AddPerson(p)
83 | }
84 |
--------------------------------------------------------------------------------
/editor.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/goadesign/model/bd8a3a9db6a60d208e63be12487f2d4cdd1f8890/editor.png
--------------------------------------------------------------------------------
/examples/basic/README.md:
--------------------------------------------------------------------------------
1 | # Basic Model Example
2 |
3 | This example `model` package contains a valid DSL describing the example used
4 | in the README of the repo.
5 |
6 | ## Usage
7 |
8 | ### Using the example command
9 |
10 | The example contains a command that uploads the diagram to the Structurizr
11 | service. The `main` function loads the Structurizr workspace ID, API key and
12 | API secret from the environment:
13 |
14 | * `$STRUCTURIZR_WORKSPACE_ID`: Workspace ID
15 | * `$STRUCTURIZR_KEY`: API key
16 | * `$STRUCTURIZR_SECRET`: API secret
17 |
18 | Follow the steps below to run the command in `bash` (substitute the values
19 | between brackets):
20 |
21 | ```bash
22 | cd $GOPATH/src/goa.design/model/examples/basic
23 | export STRUCTURIZR_WORKSPACE_ID=""
24 | export STRUCTURIZR_KEY=""
25 | export STRUCTURIZR_SECRET=""
26 | go run main.go
27 | ```
28 |
29 | Open the diagram in a browser:
30 |
31 | ```bash
32 | open https://structurizr.com/workspace/$STRUCTURUZR_WORKSPACE_ID/diagrams#SystemContext
33 | ```
34 |
35 | ## Using the `mdl` tool
36 |
37 | Alternatively the `mdl` tool can be used to render the diagram locally. Make sure the
38 | tool is installed:
39 |
40 | ```bash
41 | mdl version
42 | ```
43 |
44 | If the command above returns an error then try reinstalling the tool:
45 |
46 | ```bash
47 | go install goa.design/model/cmd/mdl
48 | ```
49 |
50 | Serve the static page using the tool:
51 |
52 | ```bash
53 | mdl serve goa.design/model/examples/basic/model
54 | ```
55 |
56 | Open the diagram in a browser:
57 |
58 | ```bash
59 | open http://localhost:8080
60 | ```
61 |
--------------------------------------------------------------------------------
/examples/basic/gen/SystemContext.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/examples/basic/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "os"
6 |
7 | _ "goa.design/model/examples/basic/model" // DSL
8 | "goa.design/model/stz"
9 | )
10 |
11 | // Executes the DSL and uploads the corresponding workspace to Structurizr.
12 | func main() {
13 | // Run the model DSL
14 | w, err := stz.RunDSL()
15 | if err != nil {
16 | fmt.Fprintf(os.Stderr, "invalid design: %s", err.Error())
17 | os.Exit(1)
18 | }
19 |
20 | // Upload the design to the Structurizr service.
21 | // The API key and secret must be set in the STRUCTURIZR_KEY and
22 | // STRUCTURIZR_SECRET environment variables respectively. The
23 | // workspace ID must be set in STRUCTURIZR_WORKSPACE_ID.
24 | var (
25 | key = os.Getenv("STRUCTURIZR_KEY")
26 | secret = os.Getenv("STRUCTURIZR_SECRET")
27 | wid = os.Getenv("STRUCTURIZR_WORKSPACE_ID")
28 | )
29 | if key == "" || secret == "" || wid == "" {
30 | fmt.Fprintln(os.Stderr, "missing STRUCTURIZR_KEY, STRUCTURIZR_SECRET or STRUCTURIZR_WORKSPACE_ID environment variable.")
31 | os.Exit(1)
32 | }
33 | c := stz.NewClient(key, secret)
34 | if err := c.Put(wid, w); err != nil {
35 | fmt.Fprintf(os.Stderr, "failed to store workspace: %s\n", err.Error())
36 | os.Exit(1)
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/examples/basic/model/model.go:
--------------------------------------------------------------------------------
1 | package design
2 |
3 | import . "goa.design/model/dsl"
4 |
5 | var _ = Design("Getting Started", "This is a model of my software system.", func() {
6 | var System = SoftwareSystem("Software System", "My software system.", func() {
7 | Tag("system")
8 | })
9 |
10 | Person("User", "A user of my software system.", func() {
11 | Uses(System, "Uses")
12 | Tag("person")
13 | })
14 |
15 | Views(func() {
16 | SystemContextView(System, "SystemContext", "An example of a System Context diagram.", func() {
17 | AddAll()
18 | AutoLayout(RankLeftRight)
19 | })
20 | Styles(func() {
21 | ElementStyle("system", func() {
22 | Background("#1168bd")
23 | Color("#ffffff")
24 | })
25 | ElementStyle("person", func() {
26 | Background("#08427b")
27 | Color("#ffffff")
28 | Shape(ShapePerson)
29 | })
30 | })
31 | })
32 | })
33 |
--------------------------------------------------------------------------------
/examples/basic/model/model.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Getting Started",
3 | "description": "This is a model of my software system.",
4 | "model": {
5 | "people": [
6 | {
7 | "id": "1qbyk9e",
8 | "name": "User",
9 | "description": "A user of my software system.",
10 | "tags": "person,Element,Person",
11 | "relationships": [
12 | {
13 | "id": "1t0pjo3",
14 | "description": "Uses",
15 | "tags": "Relationship",
16 | "sourceId": "1qbyk9e",
17 | "destinationId": "122st75",
18 | "interactionStyle": "Undefined"
19 | }
20 | ],
21 | "location": "Internal"
22 | }
23 | ],
24 | "softwareSystems": [
25 | {
26 | "id": "122st75",
27 | "name": "Software System",
28 | "description": "My software system.",
29 | "tags": "system,Element,Software System",
30 | "location": "Internal"
31 | }
32 | ]
33 | },
34 | "views": {
35 | "systemContextViews": [
36 | {
37 | "description": "An example of a System Context diagram.",
38 | "key": "SystemContext",
39 | "automaticLayout": {
40 | "rankDirection": "LeftRight",
41 | "rankSeparation": 300,
42 | "nodeSeparation": 600,
43 | "edgeSeparation": 200
44 | },
45 | "elements": [
46 | {
47 | "id": "1qbyk9e"
48 | },
49 | {
50 | "id": "122st75"
51 | }
52 | ],
53 | "relationships": [
54 | {
55 | "id": "1t0pjo3"
56 | }
57 | ],
58 | "softwareSystemId": "122st75"
59 | }
60 | ],
61 | "configuration": {
62 | "styles": {
63 | "elements": [
64 | {
65 | "tag": "system",
66 | "background": "#1168bd",
67 | "color": "#ffffff"
68 | },
69 | {
70 | "tag": "person",
71 | "background": "#08427b",
72 | "color": "#ffffff",
73 | "shape": "Person"
74 | }
75 | ]
76 | }
77 | }
78 | }
79 | }
--------------------------------------------------------------------------------
/examples/big_bank_plc/README.md:
--------------------------------------------------------------------------------
1 | # Big Bank PLC Example
2 |
3 | This examples reproduces the Structurizr [Big Bank PLC](https://structurizr.com/share/36141) example with Model.
4 | See
5 | [model.go](https://github.com/goadesign/model/blob/master/examples/big_bank_plc/model/model.go)
6 | for the complete DSL.
7 |
8 | ## Running
9 |
10 | The example can be uploaded to the Structurizr service using the `stz`
11 | command line tool (see the
12 | [README](https://github.com/goadesign/model/tree/master/README.md) for
13 | details on installing and using the tool).
14 |
15 | ```bash
16 | stz gen goa.design/model/examples/big_bank_plc/model
17 | ```
18 |
19 | This generates the file `model.json` that contains the JSON representation of
20 | the design. This file can be uploaded to a Structurizr workspace:
21 |
22 | ```bash
23 | stz put model.json -id XXX -key YYY -secret ZZZ
24 | ```
25 |
--------------------------------------------------------------------------------
/examples/big_bank_plc/model.layout.json:
--------------------------------------------------------------------------------
1 | {
2 | "Components": {
3 | "elements": [
4 | {
5 | "id": "11kj53l",
6 | "x": 1925,
7 | "y": 817
8 | },
9 | {
10 | "id": "15o55d9",
11 | "x": 105,
12 | "y": 817
13 | },
14 | {
15 | "id": "1hfi68p",
16 | "x": 105,
17 | "y": 1307
18 | },
19 | {
20 | "id": "1njagt4",
21 | "x": 1925,
22 | "y": 1307
23 | },
24 | {
25 | "id": "1olxg1c",
26 | "x": 1925,
27 | "y": 436
28 | },
29 | {
30 | "id": "1qm4oey",
31 | "x": 560,
32 | "y": 10
33 | },
34 | {
35 | "id": "ay5wb5",
36 | "x": 1015,
37 | "y": 1307
38 | },
39 | {
40 | "id": "do2bvv",
41 | "x": 1015,
42 | "y": 817
43 | },
44 | {
45 | "id": "hd9fd5",
46 | "x": 1470,
47 | "y": 11
48 | },
49 | {
50 | "id": "jdi4s4",
51 | "x": 1015,
52 | "y": 436
53 | },
54 | {
55 | "id": "sks1g4",
56 | "x": 105,
57 | "y": 436
58 | }
59 | ]
60 | },
61 | "Containers": {
62 | "elements": [
63 | {
64 | "id": "1a44kjx",
65 | "x": 1056,
66 | "y": 24
67 | },
68 | {
69 | "id": "1hfi68p",
70 | "x": 37,
71 | "y": 1214
72 | },
73 | {
74 | "id": "1njagt4",
75 | "x": 2012,
76 | "y": 1214
77 | },
78 | {
79 | "id": "1qm4oey",
80 | "x": 780,
81 | "y": 664
82 | },
83 | {
84 | "id": "1rhkirw",
85 | "x": 37,
86 | "y": 664
87 | },
88 | {
89 | "id": "ay5wb5",
90 | "x": 2012,
91 | "y": 664
92 | },
93 | {
94 | "id": "hd9fd5",
95 | "x": 1283,
96 | "y": 664
97 | },
98 | {
99 | "id": "uu8ny2",
100 | "x": 1031,
101 | "y": 1214
102 | }
103 | ]
104 | },
105 | "SystemLandscape": {
106 | "elements": [
107 | {
108 | "id": "1a44kjx",
109 | "x": 87,
110 | "y": 643
111 | },
112 | {
113 | "id": "1mmpe00",
114 | "x": 1947,
115 | "y": 36
116 | },
117 | {
118 | "id": "1njagt4",
119 | "x": 1922,
120 | "y": 693
121 | },
122 | {
123 | "id": "23ylt4",
124 | "x": 1947,
125 | "y": 1241
126 | },
127 | {
128 | "id": "ay5wb5",
129 | "x": 1012,
130 | "y": 1326
131 | },
132 | {
133 | "id": "gsdtx7",
134 | "x": 1012,
135 | "y": 813
136 | },
137 | {
138 | "id": "o14fz3",
139 | "x": 1012,
140 | "y": 301
141 | }
142 | ],
143 | "relationships": [
144 | {
145 | "id": "avismw",
146 | "description": "Asks questions to",
147 | "vertices": [
148 | {
149 | "x": 285,
150 | "y": 240
151 | }
152 | ]
153 | }
154 | ]
155 | }
156 | }
--------------------------------------------------------------------------------
/examples/json/README.md:
--------------------------------------------------------------------------------
1 | # JSON Model Example
2 |
3 | This example `model` package contains a valid DSL definition of a simple
4 | software system that makes use of the `Endpoint` and `Calls` DSLs. The
5 | main function in the `main` package serializes the underlying model as
6 | JSON and prints it to standard output.
7 |
8 | ## Usage
9 |
10 | To run this example, execute the following from the examples/json
11 | directory:
12 |
13 | ```
14 | go run main.go
15 | ```
16 |
17 | This will print the model as JSON to standard output.
--------------------------------------------------------------------------------
/examples/json/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 |
6 | "goa.design/model/codegen"
7 | )
8 |
9 | // Executes the DSL and serializes the resulting model to JSON.
10 | func main() {
11 | // Run the model DSL
12 | js, err := codegen.JSON("goa.design/model/examples/json/model", true)
13 | if err != nil {
14 | panic(err)
15 | }
16 | // Print the JSON
17 | fmt.Println(string(js))
18 | }
19 |
--------------------------------------------------------------------------------
/examples/json/model/model.go:
--------------------------------------------------------------------------------
1 | package design
2 |
3 | import . "goa.design/model/dsl"
4 |
5 | var _ = Design("Getting Started", "This is a model of my software system.", func() {
6 | var System = SoftwareSystem("Software System", "My software system.", func() {
7 | Container("Application Database", "Stores application data.", func() {
8 | Tag("database")
9 | })
10 | Container("Web Application", "Delivers content to users.", func() {
11 | Component("Dashboard Endpoint", "Serve dashboard content.", func() {
12 | Tag("endpoint")
13 | })
14 | Uses("Application Database", "Reads from and writes to", "MySQL", Synchronous)
15 | })
16 | Container("Load Balancer", "Distributes requests across the Web Application instances.", func() {
17 | Uses("Web Application/Dashboard Endpoint", "Routes requests to", "HTTPS", Synchronous)
18 | })
19 | Tag("system")
20 | })
21 |
22 | Person("User", "A user of my software system.", func() {
23 | Uses(System, "Uses", Synchronous)
24 | Tag("person")
25 | })
26 |
27 | Views(func() {
28 | SystemContextView(System, "SystemContext", "An example of a System Context diagram.", func() {
29 | AddAll()
30 | AutoLayout(RankLeftRight)
31 | })
32 | Styles(func() {
33 | ElementStyle("system", func() {
34 | Background("#1168bd")
35 | Color("#ffffff")
36 | })
37 | ElementStyle("person", func() {
38 | Background("#08427b")
39 | Color("#ffffff")
40 | Shape(ShapePerson)
41 | })
42 | ElementStyle("database", func() {
43 | Shape(ShapeCylinder)
44 | })
45 | })
46 | })
47 | })
48 |
--------------------------------------------------------------------------------
/examples/nested/README.md:
--------------------------------------------------------------------------------
1 | # Nested Example
2 |
3 | This examples illustrates how multiple packages can be contribute to the same
4 | design. This makes it possible for multiple teams to collaborate on the same
5 | overall software architecture while maintaining only the section relevant to
6 | them.
7 |
8 | The example also illustrates how styles may be shared between multiple
9 | designs by defining them in a shared Go package.
10 |
11 | The nesting is done simply by leveraging Go `import`: the design being
12 | generated imports the packages that defines the nested designs. The parent
13 | design can refer to elements defined in the child designs by using the
14 | functions exposed on the corresponding element structs:
15 |
16 | * The `Person` and `SoftwareSystem` workspace methods return a person or
17 | software system element given their name.
18 | * The `Container` software system method returns a container given its name.
19 | * The `Component` container method returns a component given its name.
20 | * The `DeploymentNode` workspace method returns a deployment node given its
21 | name, the `Child` deployment node method returns a child deployment node
22 | given its name.
23 | * The `InfrastructureNode` deployment node method returns an infrastructure
24 | node given its name.
25 | * The `ContainerInstance` deployment node method returns a container instance
26 | given a reference to the container being instantiated (or its name) and the
27 | corresponding instance ID (1 if there is only one instance of the container
28 | in the deployment node).
29 |
30 | When used that way the `Design` expression defined in imported models gets
31 | overridden by the one defined in the package being generated.
32 |
33 | ## Running
34 |
35 | ### Rendering the diagram locally
36 |
37 | Use the `mdl` tool to serve the static page:
38 |
39 | ```bash
40 | mdl serve goa.design/model/examples/nested/model
41 | ```
42 |
43 | Open the pages for each view:
44 |
45 | Subsystem 1 context view:
46 |
47 | ```bash
48 | open http://http://localhost:6070/Subsystem%201%20context
49 | ```
50 |
51 | Subsystem 2 context view:
52 |
53 | ```bash
54 | open http://http://localhost:6070/Subsystem%202%20context
55 | ```
56 |
57 | ### Using the Structurizr service
58 |
59 | Generate the Structurizr workspace from the design:
60 |
61 | ```bash
62 | stz gen goa.design/model/examples/nested/model
63 | ```
64 |
65 | This generates the file `model.json` that contains the JSON representation
66 | of the design. This file can be uploaded to a Structurizr workspace, again
67 | using `stz`:
68 |
69 | ```bash
70 | stz put model.json -id XXX -key YYY -secret ZZZ
71 | ```
72 |
73 | Where `XXX` is the Structurizr workspace ID, `YYY` the API key and `ZZZ` the
74 | API secret.
75 |
--------------------------------------------------------------------------------
/examples/nested/model/model.go:
--------------------------------------------------------------------------------
1 | package design
2 |
3 | import (
4 | _ "goa.design/model/examples/nested/model/subsystem1"
5 | s2 "goa.design/model/examples/nested/model/subsystem2"
6 |
7 | . "goa.design/model/dsl"
8 | )
9 |
10 | var _ = Design("Global workspace", "The model for all systems", func() {
11 | // Add a new dependency for the person "User" defined in subsystem 1 to the
12 | // software system defined in subsystem 2.
13 | Person("User", "A user of both Subsystems.", func() {
14 | Uses(s2.Subsystem2.SoftwareSystem("Subsystem 2"), "Uses")
15 | Tag("person")
16 | })
17 | })
18 |
--------------------------------------------------------------------------------
/examples/nested/model/subsystem1/model.go:
--------------------------------------------------------------------------------
1 | package design
2 |
3 | import (
4 | . "goa.design/model/dsl"
5 | "goa.design/model/examples/nested/styles"
6 | )
7 |
8 | // Subsystem1 defines the design for subsystem 1.
9 | var Subsystem1 = Design("Subsystem 1", "This is a model of subsystem 1.", func() {
10 | var System = SoftwareSystem("Subsystem 1", "A software system that belongs to subsystem 1.", func() {
11 | Tag("system")
12 | })
13 |
14 | Person("User", "A user of Subsystem 1.", func() {
15 | Uses(System, "Uses")
16 | Tag("person")
17 | })
18 |
19 | Views(func() {
20 | styles.DefineAll() // Use shared styles
21 |
22 | SystemContextView(System, "Subsystem 1 context", "System context diagram for Subsystem 1.", func() {
23 | AddAll()
24 | AutoLayout(RankTopBottom)
25 | })
26 | })
27 | })
28 |
--------------------------------------------------------------------------------
/examples/nested/model/subsystem2/model.go:
--------------------------------------------------------------------------------
1 | package design
2 |
3 | import (
4 | . "goa.design/model/dsl"
5 | "goa.design/model/examples/nested/styles"
6 | )
7 |
8 | // Subsystem2 defines the design for subsystem 2.
9 | var Subsystem2 = Design("Subsystem 2", "This is a model of subsystem 2.", func() {
10 | var System = SoftwareSystem("Subsystem 2", "A software system that belongs to subsystem 2.", func() {
11 | Container("Microservice A", "A microservice of subsystem 2", "Go and Goa")
12 | Tag("system")
13 | })
14 |
15 | Views(func() {
16 | styles.DefineAll() // Use shared styles
17 |
18 | SystemContextView(System, "Subsystem 2 context", "System context diagram for Subsystem 2.", func() {
19 | AddAll()
20 | AutoLayout(RankTopBottom)
21 | })
22 | })
23 | })
24 |
--------------------------------------------------------------------------------
/examples/nested/styles/styles.go:
--------------------------------------------------------------------------------
1 | /*
2 | Package styles provide shared styles used by multiple models.
3 | */
4 | package styles
5 |
6 | import . "goa.design/model/dsl"
7 |
8 | // DefineAll defines all the styles described in this package.
9 | func DefineAll() {
10 | SystemStyle()
11 | PersonStyle()
12 | }
13 |
14 | // SystemStyle defines the style used to render software systems. All elements tagged
15 | // with "system" inherit the style.
16 | func SystemStyle() {
17 | Styles(func() {
18 | ElementStyle("person", func() {
19 | Background("#08427b")
20 | Color("#ffffff")
21 | Shape(ShapePerson)
22 | })
23 | })
24 | }
25 |
26 | // PersonStyle defines the style used to render people. All elements tagged with
27 | // "person" inherit the style.
28 | func PersonStyle() {
29 | Styles(func() {
30 | ElementStyle("person", func() {
31 | Background("#08427b")
32 | Color("#ffffff")
33 | Shape(ShapePerson)
34 | })
35 | })
36 | }
37 |
--------------------------------------------------------------------------------
/examples/relationship_style/model/model.go:
--------------------------------------------------------------------------------
1 | package design
2 |
3 | import (
4 | . "goa.design/model/dsl"
5 | )
6 |
7 | var _ = Design("Getting Started", "This is a model of my software system.", func() {
8 | SoftwareSystem("The System", "My Software System")
9 | Person("Person1", "A person using the system.", func() {
10 | Uses("The System", "Thick, red edge\nwith vertices", func() {
11 | Tag("labelPos")
12 | })
13 | })
14 | Person("Person2", "Two relationships\nautomatically spread", func() {
15 | Uses("The System", "Read")
16 | Uses("The System", "Write")
17 | Tag("Customer")
18 | })
19 | Person("Person3", "Another person", func() {
20 | Uses("The System", "Dashed\nOrthogonal", func() {
21 | Tag("knows")
22 | })
23 | })
24 |
25 | Views(func() {
26 | SystemContextView("The System", "SystemContext", "System Context diagram.", func() {
27 | AddAll()
28 | Link("Person1", "The System", "Thick, red edge\nwith vertices", func() {
29 | Vertices(300, 300, 300, 800)
30 | })
31 | AutoLayout(RankLeftRight)
32 | })
33 | Styles(func() {
34 | ElementStyle("Person", func() {
35 | Shape(ShapePerson)
36 | Background("#fffff0")
37 | })
38 | ElementStyle("Customer", func() {
39 | Background("#ffffa0")
40 | })
41 | // Defaults all relationships to solid line
42 | RelationshipStyle("Relationship", func() {
43 | Solid()
44 | })
45 | RelationshipStyle("labelPos", func() {
46 | Position(40)
47 | Color("#FF0000")
48 | Thickness(7)
49 | })
50 | RelationshipStyle("knows", func() {
51 | Routing(RoutingOrthogonal)
52 | // overwrite line style
53 | Dashed()
54 | })
55 | })
56 | })
57 | })
58 |
--------------------------------------------------------------------------------
/examples/shapes/gen/layout.json:
--------------------------------------------------------------------------------
1 | {"SystemContext":{"14fm4f4":{"x":2003.3348518984822,"y":241.86133680745627},"14plq43":{"x":2476.3399322293926,"y":244.59821259269154},"14zlbt2":{"x":2736.7024792771113,"y":793.852040072419},"159kxi1":{"x":2737.631324863108,"y":1323.2806415208174},"15tk4vz":{"x":574.8291622131244,"y":237.70803643837752},"163jqky":{"x":1046.9837491508706,"y":241.5582973837889},"16djc9x":{"x":1504.7881174204226,"y":245.19838527663768},"17rhcqs":{"x":2453.135227666788,"y":1821.3414763959354},"181gyfr":{"x":1836.4088503401708,"y":1816.057778022873},"1mf4fhg":{"x":329.1562988505294,"y":765.0630598432717},"1mp416f":{"x":330.1961748540052,"y":1276.851803676052},"1mz3mve":{"x":547.7653661528783,"y":1818.0208915646112},"1n938kd":{"x":1209.053099909411,"y":1817.1735275595934},"1qbyk9e":{"x":1514.1359246632694,"y":943.5224778579358}}}
--------------------------------------------------------------------------------
/examples/shapes/model/model.go:
--------------------------------------------------------------------------------
1 | package design
2 |
3 | import (
4 | "fmt"
5 |
6 | . "goa.design/model/dsl"
7 | "goa.design/model/mdl"
8 | )
9 |
10 | var shapes = []ShapeKind{
11 | ShapeBox,
12 | ShapeCircle,
13 | ShapeCylinder,
14 | ShapeEllipse,
15 | ShapeHexagon,
16 | ShapeRoundedBox,
17 | ShapeComponent,
18 | ShapeFolder,
19 | ShapeMobileDeviceLandscape,
20 | ShapeMobileDevicePortrait,
21 | //ShapePerson,
22 | ShapePipe,
23 | ShapeRobot,
24 | ShapeWebBrowser,
25 | }
26 |
27 | var _ = Design("Getting Started", "This is a model of my software system.", func() {
28 | for i, sh := range shapes {
29 | func(i int) {
30 | SoftwareSystem(fmt.Sprintf("System %d", i+1), fmt.Sprintf("Shape: %s.", shapeName(sh)), func() {
31 | Tag(fmt.Sprintf("system%d", i+1))
32 | })
33 | }(i)
34 | }
35 |
36 | Person("User", "A person using shapes.", func() {
37 | for i := range shapes {
38 | Uses(fmt.Sprintf("System %d", i+1), "Uses")
39 | }
40 | Tag("person")
41 | })
42 |
43 | Views(func() {
44 | SystemContextView("System 1", "SystemContext", "An example of a System Context diagram.", func() {
45 | AddAll()
46 | AutoLayout(RankLeftRight)
47 | })
48 | Styles(func() {
49 | ElementStyle("person", func() {
50 | Shape(ShapePerson)
51 | Background("#f0f7ff")
52 | })
53 | for i, shape := range shapes {
54 | ElementStyle(fmt.Sprintf("system%d", i+1), func() {
55 | Shape(shape)
56 | Background("#f0f7ff")
57 | })
58 | }
59 | })
60 | })
61 | })
62 |
63 | func shapeName(sh ShapeKind) string {
64 | b, _ := mdl.ShapeKind(sh).MarshalJSON()
65 | return string(b)
66 | }
67 |
--------------------------------------------------------------------------------
/examples/staticcheck.conf:
--------------------------------------------------------------------------------
1 | checks = ["all", "-ST1000", "-ST1001"]
--------------------------------------------------------------------------------
/examples/usage/README.md:
--------------------------------------------------------------------------------
1 | # Model Usage Diagram
2 |
3 | This example is used to produce the diagram that illustrates the multiple
4 | Model usage flows in this repo README.
5 |
--------------------------------------------------------------------------------
/examples/usage/model/model.go:
--------------------------------------------------------------------------------
1 | package design
2 |
3 | import . "goa.design/model/dsl"
4 |
5 | var _ = Design("Model Usage", "Not a software architecture but a diagram illustrating how to use Model.", func() {
6 | SoftwareSystem("Model Usage", func() {
7 | Container("Model Usage", func() {
8 | Component("Design", "Go package containing Model DSL that describes the system architecture", func() {
9 | Uses("Visual Editor", "mdl serve")
10 | Uses("Design JSON", "mdl gen")
11 | // Uses("Static HTML", "mdl gen")
12 | Uses("Structurizr workspace JSON", "stz gen")
13 | Tag("Design")
14 | })
15 | Component("Design JSON", "JSON representation of the design")
16 | Component("Structurizr workspace JSON", "JSON representation of a Structurizr workspace corresponding to the software architecture model described in the design", func() {
17 | Uses("Structurizr Service", "stz put")
18 | Tag("Structurizr")
19 | })
20 | Component("Visual Editor", "Edit diagram element positions and save to SVG", func() {
21 | Uses("View Renderings", "Save")
22 | Tag("Editor")
23 | })
24 | // Component("Static HTML", "Static HTML rendering of each view")
25 | Component("View Renderings", "SVG files corresponding to design views.", func() {
26 | Tag("SVG")
27 | })
28 | Component("Structurizr Service", "Structurizr service hosted at https://structurizr.com", func() {
29 | Tag("Editor", "Structurizr")
30 | })
31 | })
32 | })
33 |
34 | Person("User", "Model user.", func() {
35 | Uses("Model Usage/Model Usage/Design", "Writes")
36 | Uses("Model Usage/Model Usage/Visual Editor", "Uses")
37 | Tag("Person")
38 | })
39 |
40 | Views(func() {
41 | ComponentView("Model Usage/Model Usage", "view", func() {
42 | AddDefault()
43 | })
44 | Styles(func() {
45 | ElementStyle("Person", func() {
46 | Shape(ShapePerson)
47 | Stroke("#55AAEE")
48 | })
49 | ElementStyle("Design", func() {
50 | Shape(ShapeFolder)
51 | })
52 | ElementStyle("Editor", func() {
53 | Shape(ShapeWebBrowser)
54 | Stroke("#EEAA55")
55 | })
56 | ElementStyle("SVG", func() {
57 | Shape(ShapeRoundedBox)
58 | Stroke("#EEAA55")
59 | })
60 | ElementStyle("Structurizr", func() {
61 | Stroke("#55AAEE")
62 | })
63 | })
64 | })
65 | })
66 |
--------------------------------------------------------------------------------
/expr/component.go:
--------------------------------------------------------------------------------
1 | package expr
2 |
3 | import (
4 | "fmt"
5 | )
6 |
7 | type (
8 | // Component represents a component.
9 | Component struct {
10 | *Element
11 | Container *Container
12 | }
13 |
14 | // Components is a slice of components that can be easily converted into
15 | // a slice of ElementHolder.
16 | Components []*Component
17 | )
18 |
19 | // ComponentTags lists the tags that are added to all components.
20 | var ComponentTags = []string{"Element", "Component"}
21 |
22 | // EvalName returns the generic expression name used in error messages.
23 | func (c *Component) EvalName() string {
24 | if c.Name == "" {
25 | return "unnamed component"
26 | }
27 | return fmt.Sprintf("component %q", c.Name)
28 | }
29 |
30 | // Finalize adds the 'Component' tag ands finalizes relationships.
31 | func (c *Component) Finalize() {
32 | c.PrefixTags(ComponentTags...)
33 | c.Element.Finalize()
34 | }
35 |
36 | // Elements returns a slice of ElementHolder that contains the elements of c.
37 | func (cs Components) Elements() []ElementHolder {
38 | res := make([]ElementHolder, len(cs))
39 | for i, cc := range cs {
40 | res[i] = cc
41 | }
42 | return res
43 | }
44 |
--------------------------------------------------------------------------------
/expr/component_test.go:
--------------------------------------------------------------------------------
1 | package expr
2 |
3 | import (
4 | "fmt"
5 | "testing"
6 | )
7 |
8 | func TestComponentEvalName(t *testing.T) {
9 | tests := []struct {
10 | name, want string
11 | }{
12 | {name: "", want: "unnamed component"},
13 | {name: "foo", want: `component "foo"`},
14 | }
15 | for _, tt := range tests {
16 | tt := tt
17 | t.Run(tt.name, func(t *testing.T) {
18 | t.Parallel()
19 | component := Component{
20 | Element: &Element{
21 | Name: tt.name,
22 | },
23 | }
24 | if got := component.EvalName(); got != tt.want {
25 | t.Errorf("got %s, want %s", got, tt.want)
26 | }
27 | })
28 | }
29 | }
30 |
31 | func TestComponentFinalize(t *testing.T) {
32 | t.Parallel()
33 | component := Component{
34 | Element: &Element{
35 | Name: "foo",
36 | },
37 | }
38 | seq := []struct {
39 | pre func()
40 | want string
41 | }{
42 | {want: ""},
43 | {pre: func() { component.Tags = "foo" }, want: "foo"},
44 | {pre: func() { component.Finalize() }, want: "Element,Component,foo"},
45 | }
46 | for i, tt := range seq {
47 | tt := tt
48 | t.Run(fmt.Sprint(i), func(t *testing.T) {
49 | if tt.pre != nil {
50 | tt.pre()
51 | }
52 | if got := component.Tags; got != tt.want {
53 | t.Errorf("got %s, want %s", got, tt.want)
54 | }
55 | })
56 | }
57 | }
58 |
59 | func TestComponentsElements(t *testing.T) {
60 | t.Parallel()
61 | components := Components{
62 | {Element: &Element{Name: "foo"}},
63 | {Element: &Element{Name: "bar"}},
64 | {Element: &Element{Name: "baz"}},
65 | }
66 | if got := components.Elements(); len(got) != len(components) {
67 | t.Errorf("got %d, want %d", len(got), len(components))
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/expr/container.go:
--------------------------------------------------------------------------------
1 | package expr
2 |
3 | import (
4 | "fmt"
5 | "strings"
6 | )
7 |
8 | type (
9 | // Container represents a container.
10 | Container struct {
11 | *Element
12 | Components Components
13 | System *SoftwareSystem
14 | }
15 |
16 | // Containers is a slice of containers that can be easily
17 | // converted into a slice of ElementHolder.
18 | Containers []*Container
19 | )
20 |
21 | // ContainerTags lists the tags that are added to all containers.
22 | var ContainerTags = []string{"Element", "Container"}
23 |
24 | // EvalName returns the generic expression name used in error messages.
25 | func (c *Container) EvalName() string {
26 | if c.Name == "" {
27 | return "unnamed container"
28 | }
29 | return fmt.Sprintf("container %q", c.Name)
30 | }
31 |
32 | // Finalize adds the 'Container' tag ands finalizes relationships.
33 | func (c *Container) Finalize() {
34 | c.PrefixTags(ContainerTags...)
35 | c.Element.Finalize()
36 | }
37 |
38 | // Elements returns a slice of ElementHolder that contains the elements of c.
39 | func (c Containers) Elements() []ElementHolder {
40 | res := make([]ElementHolder, len(c))
41 | for i, cc := range c {
42 | res[i] = cc
43 | }
44 | return res
45 | }
46 |
47 | // Component returns the component with the given name if any, nil otherwise.
48 | func (c *Container) Component(name string) *Component {
49 | for _, cc := range c.Components {
50 | if cc.Name == name {
51 | return cc
52 | }
53 | }
54 | return nil
55 | }
56 |
57 | // AddComponent adds the given component to the container. If there is already a
58 | // component with the given name then AddComponent merges both definitions. The
59 | // merge algorithm:
60 | //
61 | // - overrides the description, technology and URL if provided,
62 | // - merges any new tag or propery into the existing tags and properties,
63 | //
64 | // AddComponent returns the new or merged component.
65 | func (c *Container) AddComponent(cmp *Component) *Component {
66 | existing := c.Component(cmp.Name)
67 | if existing == nil {
68 | Identify(cmp)
69 | c.Components = append(c.Components, cmp)
70 | return cmp
71 | }
72 | if cmp.Description != "" {
73 | existing.Description = cmp.Description
74 | }
75 | if cmp.Technology != "" {
76 | existing.Technology = cmp.Technology
77 | }
78 | if cmp.URL != "" {
79 | existing.URL = cmp.URL
80 | }
81 | existing.MergeTags(strings.Split(cmp.Tags, ",")...)
82 | if olddsl := existing.DSLFunc; olddsl != nil {
83 | existing.DSLFunc = func() { olddsl(); cmp.DSLFunc() }
84 | }
85 | return existing
86 | }
87 |
--------------------------------------------------------------------------------
/expr/container_test.go:
--------------------------------------------------------------------------------
1 | package expr
2 |
3 | import (
4 | "fmt"
5 | "testing"
6 | )
7 |
8 | func TestContainerEvalName(t *testing.T) {
9 | tests := []struct {
10 | name, want string
11 | }{
12 | {name: "", want: "unnamed container"},
13 | {name: "foo", want: `container "foo"`},
14 | }
15 | for _, tt := range tests {
16 | tt := tt
17 | t.Run(tt.name, func(t *testing.T) {
18 | t.Parallel()
19 | container := Container{
20 | Element: &Element{
21 | Name: tt.name,
22 | },
23 | }
24 | if got := container.EvalName(); got != tt.want {
25 | t.Errorf("got %s, want %s", got, tt.want)
26 | }
27 | })
28 | }
29 | }
30 |
31 | func TestContainerFinalize(t *testing.T) {
32 | t.Parallel()
33 | container := Container{
34 | Element: &Element{
35 | Name: "foo",
36 | },
37 | }
38 | seq := []struct {
39 | pre func()
40 | want string
41 | }{
42 | {want: ""},
43 | {pre: func() { container.Tags = "foo" }, want: "foo"},
44 | {pre: func() { container.Finalize() }, want: "Element,Container,foo"},
45 | }
46 | for i, tt := range seq {
47 | tt := tt
48 | t.Run(fmt.Sprint(i), func(t *testing.T) {
49 | if tt.pre != nil {
50 | tt.pre()
51 | }
52 | if got := container.Tags; got != tt.want {
53 | t.Errorf("got %s, want %s", got, tt.want)
54 | }
55 | })
56 | }
57 | }
58 |
59 | func TestContainersElements(t *testing.T) {
60 | t.Parallel()
61 | containers := Containers{
62 | {Element: &Element{Name: "foo"}},
63 | {Element: &Element{Name: "bar"}},
64 | {Element: &Element{Name: "baz"}},
65 | }
66 | if got := containers.Elements(); len(got) != len(containers) {
67 | t.Errorf("got %d, want %d", len(got), len(containers))
68 | }
69 | }
70 |
71 | func TestContainerComponent(t *testing.T) {
72 | container := Container{
73 | Components: Components{
74 | {Element: &Element{Name: "foo"}},
75 | {Element: &Element{Name: "bar"}},
76 | {Element: &Element{Name: "baz"}},
77 | },
78 | }
79 | tests := []struct {
80 | name string
81 | want *Component
82 | }{
83 | {name: "thud", want: nil},
84 | {name: "bar", want: container.Components[1]},
85 | }
86 | for i, tt := range tests {
87 | tt := tt
88 | t.Run(fmt.Sprint(i), func(t *testing.T) {
89 | t.Parallel()
90 | if got := container.Component(tt.name); got != tt.want {
91 | t.Errorf("got %#v, want %#v", got.Element, tt.want.Element)
92 | }
93 | })
94 | }
95 | }
96 |
97 | func TestAddComponent(t *testing.T) {
98 | t.Parallel()
99 | mElementFoo := Element{ID: "1", Name: "foo", Description: ""}
100 | mElementBar := Element{ID: "2", Name: "bar", Description: ""}
101 | componentFoo := Component{
102 | Element: &Element{ID: "1", Name: "foo", Description: "", DSLFunc: func() {
103 | fmt.Println("1")
104 | }},
105 | }
106 | components := make([]*Component, 1)
107 | components[0] = &componentFoo
108 | OuterContainer := Container{
109 | Element: &mElementFoo,
110 | Components: components,
111 | System: nil,
112 | }
113 | InnerContainer := Container{
114 | Element: &mElementBar,
115 | Components: components,
116 | System: nil,
117 | }
118 | componentBar := Component{
119 | Element: &Element{ID: "2", Name: "bar", Description: ""},
120 | Container: &InnerContainer,
121 | }
122 | componentFooPlus := Component{
123 | Element: &Element{ID: "3", Name: "foo", Description: "Description", Technology: "Golang", URL: "https://github.com/goadesign/model/", DSLFunc: func() {
124 | fmt.Printf("hello")
125 | }},
126 | }
127 |
128 | seq := []struct {
129 | name string
130 | component2Add *Component
131 | want *Component
132 | }{
133 | {name: "foo", component2Add: &componentFoo, want: &componentFoo}, // already in container
134 | {name: "bar", component2Add: &componentBar, want: &componentBar}, // now appended
135 | {name: "foo", component2Add: &componentFooPlus, want: &componentFoo},
136 | }
137 | for i, tt := range seq {
138 | tt := tt
139 | t.Run(fmt.Sprint(i), func(t *testing.T) {
140 | got := OuterContainer.AddComponent(tt.component2Add)
141 | if got != tt.want {
142 | t.Errorf("got %#v, want %#v", got.Element, tt.want.Element)
143 | }
144 | })
145 | }
146 | }
147 |
--------------------------------------------------------------------------------
/expr/design.go:
--------------------------------------------------------------------------------
1 | package expr
2 |
3 | import (
4 | "fmt"
5 |
6 | "goa.design/goa/v3/eval"
7 | "goa.design/goa/v3/expr"
8 | model "goa.design/model/pkg"
9 | )
10 |
11 | type (
12 | // Design contains the AST generated from the DSL.
13 | Design struct {
14 | Name string
15 | Description string
16 | Version string
17 | Model *Model
18 | Views *Views
19 | }
20 | )
21 |
22 | // Root is the design root expression.
23 | var Root = &Design{Model: &Model{}, Views: &Views{}}
24 |
25 | // Register design root with eval engine.
26 | func init() {
27 | eval.Register(Root) // nolint: errcheck
28 | }
29 |
30 | // WalkSets iterates over the elements and views.
31 | // Elements DSL cannot be executed on init because all elements must first be
32 | // loaded and their IDs captured in the registry before relationships can be
33 | // built with DSL.
34 | func (d *Design) WalkSets(walk eval.SetWalker) {
35 | // 1. Model
36 | walk([]eval.Expression{d.Model})
37 | // 2. People
38 | walk(eval.ToExpressionSet(d.Model.People))
39 | // 3. Systems
40 | walk(eval.ToExpressionSet(d.Model.Systems))
41 | // 4. Containers
42 | for _, s := range d.Model.Systems {
43 | walk(eval.ToExpressionSet(s.Containers))
44 | }
45 | // 5. Components
46 | for _, s := range d.Model.Systems {
47 | for _, c := range s.Containers {
48 | walk(eval.ToExpressionSet(c.Components))
49 | }
50 | }
51 | // 6. Deployment environments
52 | walkDeploymentNodes(d.Model.DeploymentNodes, walk)
53 | // 7. Views
54 | walk([]eval.Expression{d.Views})
55 | }
56 |
57 | // Packages returns the import path to the Go packages that make
58 | // up the DSL. This is used to skip frames that point to files
59 | // in these packages when computing the location of errors.
60 | func (*Design) Packages() []string {
61 | return []string{
62 | "goa.design/model/expr",
63 | "goa.design/model/dsl",
64 | fmt.Sprintf("goa.design/model@%s/expr", model.Version()),
65 | fmt.Sprintf("goa.design/model@%s/dsl", model.Version()),
66 | }
67 | }
68 |
69 | // DependsOn tells the eval engine to run the goa DSL first.
70 | func (*Design) DependsOn() []eval.Root { return []eval.Root{expr.Root} }
71 |
72 | // EvalName returns the generic expression name used in error messages.
73 | func (*Design) EvalName() string { return "root" }
74 |
75 | func walkDeploymentNodes(n []*DeploymentNode, walk eval.SetWalker) {
76 | if n == nil {
77 | return
78 | }
79 | walk(eval.ToExpressionSet(n))
80 | for _, d := range n {
81 | walk(eval.ToExpressionSet(d.InfrastructureNodes))
82 | walk(eval.ToExpressionSet(d.ContainerInstances))
83 | walkDeploymentNodes(d.Children, walk)
84 | }
85 | }
86 |
87 | // Person returns the person with the given name if any, nil otherwise.
88 | func (d *Design) Person(name string) *Person {
89 | return d.Model.Person(name)
90 | }
91 |
92 | // SoftwareSystem returns the software system with the given name if any, nil
93 | // otherwise.
94 | func (d *Design) SoftwareSystem(name string) *SoftwareSystem {
95 | return d.Model.SoftwareSystem(name)
96 | }
97 |
98 | // DeploymentNode returns the deployment node with the given name in the given
99 | // environment if any, nil otherwise.
100 | func (d *Design) DeploymentNode(env, name string) *DeploymentNode {
101 | return d.Model.DeploymentNode(env, name)
102 | }
103 |
--------------------------------------------------------------------------------
/expr/doc.go:
--------------------------------------------------------------------------------
1 | /*
2 | Package expr contains the data structure and associated logic that are built
3 | by the DSL. These data structures are leveraged by the code generation
4 | package to produce the software architecture model and views.
5 |
6 | The expression data structures implement interfaces defined by the Goa eval
7 | package. The corresponding methods (EvalName, Prepare, Validate, Finalize
8 | etc.) are invoked by the eval package when evaluating the DSL.
9 | */
10 | package expr
11 |
--------------------------------------------------------------------------------
/expr/element.go:
--------------------------------------------------------------------------------
1 | package expr
2 |
3 | import (
4 | "strings"
5 | )
6 |
7 | type (
8 | // Element describes an element.
9 | Element struct {
10 | ID string
11 | Name string
12 | Description string
13 | Technology string
14 | Tags string
15 | URL string
16 | Properties map[string]string
17 | Relationships []*Relationship
18 | DSLFunc func()
19 | }
20 |
21 | // ElementHolder provides access to the underlying element.
22 | ElementHolder interface {
23 | GetElement() *Element
24 | }
25 |
26 | // LocationKind is the enum for possible locations.
27 | LocationKind int
28 | )
29 |
30 | const (
31 | // LocationUndefined means no location specified in design.
32 | LocationUndefined LocationKind = iota
33 | // LocationInternal defines an element internal to the enterprise.
34 | LocationInternal
35 | // LocationExternal defines an element external to the enterprise.
36 | LocationExternal
37 | )
38 |
39 | // DSL returns the attached DSL.
40 | func (e *Element) DSL() func() { return e.DSLFunc }
41 |
42 | // Finalize finalizes the relationships.
43 | func (e *Element) Finalize() {
44 | for _, rel := range e.Relationships {
45 | rel.Finalize()
46 | }
47 | }
48 |
49 | // GetElement returns the underlying element.
50 | func (e *Element) GetElement() *Element { return e }
51 |
52 | // MergeTags adds the given tags. It skips tags already present in e.Tags.
53 | func (e *Element) MergeTags(tags ...string) {
54 | e.Tags = mergeTags(e.Tags, tags)
55 | }
56 |
57 | // PrefixTags adds the given tags to the beginning of the comma separated list.
58 | func (e *Element) PrefixTags(tags ...string) {
59 | prefix := strings.Join(tags, ",")
60 | if e.Tags == "" {
61 | e.Tags = prefix
62 | return
63 | }
64 | e.Tags = mergeTags(prefix, strings.Split(e.Tags, ","))
65 | }
66 |
67 | // mergeTags merges the comma separated tags in old with the ones in tags and
68 | // returns a comma separated string with the results.
69 | func mergeTags(existing string, tags []string) string {
70 | if existing == "" {
71 | return strings.Join(tags, ",")
72 | }
73 | old := strings.Split(existing, ",")
74 | var merged []string
75 | for _, o := range old {
76 | found := false
77 | for _, tag := range tags {
78 | if tag == o {
79 | found = true
80 | break
81 | }
82 | }
83 | if !found {
84 | merged = append(merged, o)
85 | }
86 | }
87 | for _, tag := range tags {
88 | if tag == "" {
89 | continue
90 | }
91 | found := false
92 | for _, o := range merged {
93 | if tag == o {
94 | found = true
95 | break
96 | }
97 | }
98 | if !found {
99 | merged = append(merged, tag)
100 | }
101 | }
102 | return strings.Join(merged, ",")
103 | }
104 |
--------------------------------------------------------------------------------
/expr/filtered_views.go:
--------------------------------------------------------------------------------
1 | package expr
2 |
3 | import (
4 | "fmt"
5 | )
6 |
7 | type (
8 | // FilteredView describes a filtered view on top of a specified view.
9 | FilteredView struct {
10 | Title string
11 | Description string
12 | Key string `json:"key"`
13 | BaseKey string
14 | Exclude bool
15 | FilterTags []string
16 | }
17 | )
18 |
19 | // EvalName returns the generic expression name used in error messages.
20 | func (fv *FilteredView) EvalName() string {
21 | var suffix string
22 | if fv.Key != "" {
23 | suffix = fmt.Sprintf(" key %q and", fv.Key)
24 | }
25 | return fmt.Sprintf("filtered view with%s base key %q", suffix, fv.BaseKey)
26 | }
27 |
--------------------------------------------------------------------------------
/expr/idify_test.go:
--------------------------------------------------------------------------------
1 | package expr
2 |
3 | import (
4 | "encoding/base32"
5 | "hash/fnv"
6 | "testing"
7 | )
8 |
9 | func BenchmarkIdify(b *testing.B) {
10 | sample := []string{"0", "medium", "a longer string", "a super long duper long very very long string"}
11 | for n := 0; n < b.N; n++ {
12 | for _, s := range sample {
13 | idify(s)
14 | }
15 | }
16 | }
17 |
18 | func BenchmarkBase32(b *testing.B) {
19 | var h = fnv.New32a()
20 | sample := []string{"0", "medium", "a longer string", "a super long duper long very very long string"}
21 | for n := 0; n < b.N; n++ {
22 | for _, s := range sample {
23 | h.Reset()
24 | h.Write([]byte(s))
25 | b := h.Sum(nil)
26 | base32.StdEncoding.EncodeToString(b)
27 | }
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/expr/person.go:
--------------------------------------------------------------------------------
1 | package expr
2 |
3 | import (
4 | "fmt"
5 | )
6 |
7 | type (
8 | // Person represents a person.
9 | Person struct {
10 | *Element
11 | Location LocationKind
12 | }
13 |
14 | // People is a slide of Person that can easily be converted into a slice of ElementHolder.
15 | People []*Person
16 | )
17 |
18 | // PersonTags list the tags that are automatically added to all people.
19 | var PersonTags = []string{"Element", "Person"}
20 |
21 | // EvalName returns the generic expression name used in error messages.
22 | func (p *Person) EvalName() string {
23 | if p.Name == "" {
24 | return "unnamed person"
25 | }
26 | return fmt.Sprintf("person %q", p.Name)
27 | }
28 |
29 | // Finalize adds the 'Person' tag ands finalizes relationships.
30 | func (p *Person) Finalize() {
31 | p.PrefixTags(PersonTags...)
32 | p.Element.Finalize()
33 | }
34 |
35 | // Elements returns a slice of ElementHolder that contains the people.
36 | func (p People) Elements() []ElementHolder {
37 | res := make([]ElementHolder, len(p))
38 | for i, pp := range p {
39 | res[i] = pp
40 | }
41 | return res
42 | }
43 |
--------------------------------------------------------------------------------
/expr/person_test.go:
--------------------------------------------------------------------------------
1 | package expr
2 |
3 | import (
4 | "fmt"
5 | "testing"
6 | )
7 |
8 | func TestPersonEvalName(t *testing.T) {
9 | tests := []struct {
10 | name, want string
11 | }{
12 | {name: "", want: "unnamed person"},
13 | {name: "foo", want: `person "foo"`},
14 | }
15 | for _, tt := range tests {
16 | tt := tt
17 | t.Run(tt.name, func(t *testing.T) {
18 | t.Parallel()
19 | person := Person{
20 | Element: &Element{
21 | Name: tt.name,
22 | },
23 | }
24 | if got := person.EvalName(); got != tt.want {
25 | t.Errorf("got %s, want %s", got, tt.want)
26 | }
27 | })
28 | }
29 | }
30 |
31 | func TestPersonFinalize(t *testing.T) {
32 | t.Parallel()
33 | person := Person{
34 | Element: &Element{
35 | Name: "foo",
36 | },
37 | }
38 | seq := []struct {
39 | pre func()
40 | want string
41 | }{
42 | {want: ""},
43 | {pre: func() { person.Tags = "foo" }, want: "foo"},
44 | {pre: func() { person.Finalize() }, want: "Element,Person,foo"},
45 | }
46 | for i, tt := range seq {
47 | tt := tt
48 | t.Run(fmt.Sprint(i), func(t *testing.T) {
49 | if tt.pre != nil {
50 | tt.pre()
51 | }
52 | if got := person.Tags; got != tt.want {
53 | t.Errorf("got %s, want %s", got, tt.want)
54 | }
55 | })
56 | }
57 | }
58 |
59 | func TestPeopleElements(t *testing.T) {
60 | t.Parallel()
61 | people := People{
62 | {Element: &Element{Name: "foo"}},
63 | {Element: &Element{Name: "bar"}},
64 | {Element: &Element{Name: "baz"}},
65 | }
66 | if got := people.Elements(); len(got) != len(people) {
67 | t.Errorf("got %d, want %d", len(got), len(people))
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/expr/registry.go:
--------------------------------------------------------------------------------
1 | package expr
2 |
3 | import (
4 | "fmt"
5 | "hash/fnv"
6 | "math/big"
7 | "sort"
8 | )
9 |
10 | // Registry captures all the elements, people and relationships.
11 | var Registry = make(map[string]any)
12 |
13 | // Iterate iterates through all elements, people and relationships in the
14 | // registry in a consistent order.
15 | func Iterate(visitor func(elem any)) {
16 | keys := make([]string, len(Registry))
17 | i := 0
18 | for k := range Registry {
19 | keys[i] = k
20 | i++
21 | }
22 | sort.Strings(keys)
23 | for _, k := range keys {
24 | visitor(Registry[k])
25 | }
26 | }
27 |
28 | // IterateRelationships iterates through all relationships in the registry in a
29 | // consistent order.
30 | func IterateRelationships(visitor func(r *Relationship)) {
31 | Iterate(func(e any) {
32 | if r, ok := e.(*Relationship); ok {
33 | visitor(r)
34 | }
35 | })
36 | }
37 |
38 | // Identify sets the ID field of the given element or relationship and registers
39 | // it with the global registry. The algorithm first compute a unique moniker
40 | // for the element or relatioship (based on names and parent scope ID) then
41 | // hashes and base36 encodes the result.
42 | func Identify(element any) {
43 | var id string
44 | switch e := element.(type) {
45 | case *Person:
46 | id = idify(e.Name)
47 | e.ID = id
48 | case *SoftwareSystem:
49 | id = idify(e.Name)
50 | e.ID = id
51 | case *Container:
52 | id = idify(e.System.ID + ":" + e.Name)
53 | e.ID = id
54 | case *Component:
55 | id = idify(e.Container.ID + ":" + e.Name)
56 | e.ID = id
57 | case *DeploymentNode:
58 | prefix := "dn:" + e.Environment + ":"
59 | for f := e.Parent; f != nil; f = f.Parent {
60 | prefix += f.ID + ":"
61 | }
62 | id = idify(prefix + e.Name)
63 | e.ID = id
64 | case *InfrastructureNode:
65 | id = idify(e.Environment + ":" + e.Parent.ID + ":" + e.Name)
66 | e.ID = id
67 | case *ContainerInstance:
68 | id = idify(e.Environment + ":" + e.Parent.ID + ":" + e.ContainerID)
69 | e.ID = id
70 | case *Relationship:
71 | var dest string
72 | if e.Destination != nil {
73 | dest = e.Destination.ID
74 | } else {
75 | dest = e.DestinationPath
76 | }
77 | id = idify(e.Source.ID + ":" + dest + ":" + e.Description)
78 | e.ID = id
79 | default:
80 | panic(fmt.Sprintf("element of type %T does not have an ID", element)) // bug
81 | }
82 | if _, ok := Registry[id]; ok {
83 | // Could have been imported from another model package
84 | return
85 | }
86 | Registry[id] = element
87 | }
88 |
89 | var h = fnv.New32a()
90 |
91 | func idify(s string) string {
92 | h.Reset()
93 | h.Write([]byte(s))
94 | return encodeToBase36(h.Sum(nil))
95 | }
96 |
97 | var encodeStd = [36]byte{
98 | '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e',
99 | 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't',
100 | 'u', 'v', 'w', 'x', 'y', 'z',
101 | }
102 |
103 | var bigRadix = big.NewInt(36)
104 | var bigZero = big.NewInt(0)
105 |
106 | func encodeToBase36(b []byte) string {
107 | x := new(big.Int)
108 | x.SetBytes(b)
109 | res := make([]byte, 0, len(b)*136/100)
110 | for x.Cmp(bigZero) > 0 {
111 | mod := new(big.Int)
112 | x.DivMod(x, bigRadix, mod)
113 | res = append(res, encodeStd[mod.Int64()])
114 | }
115 | for _, i := range b {
116 | if i != 0 {
117 | break
118 | }
119 | res = append(res, byte(48))
120 | }
121 | alen := len(res)
122 | for i := 0; i < alen/2; i++ {
123 | res[i], res[alen-1-i] = res[alen-1-i], res[i]
124 | }
125 | return string(res)
126 | }
127 |
--------------------------------------------------------------------------------
/expr/relationship.go:
--------------------------------------------------------------------------------
1 | package expr
2 |
3 | import (
4 | "fmt"
5 | "strings"
6 | )
7 |
8 | type (
9 | // Relationship describes a uni-directional relationship between two elements.
10 | Relationship struct {
11 | ID string
12 | Source *Element
13 | Description string
14 | Technology string
15 | InteractionStyle InteractionStyleKind
16 | Tags string
17 | URL string
18 |
19 | // DestinationPath is used to compute the destination after all DSL has
20 | // completed execution.
21 | DestinationPath string
22 |
23 | // Destination is only guaranteed to be initialized after the DSL has
24 | // been executed. It can be used in validations and finalizers.
25 | Destination *Element
26 |
27 | // LinkedRelationshipID is the ID of the relationship pointing to the
28 | // container corresponding to the container instance with this
29 | // relationship.
30 | LinkedRelationshipID string
31 | }
32 |
33 | // InteractionStyleKind is the enum for possible interaction styles.
34 | InteractionStyleKind int
35 | )
36 |
37 | const (
38 | // InteractionUndefined means no interaction style specified in design.
39 | InteractionUndefined InteractionStyleKind = iota
40 | // InteractionSynchronous describes a synchronous interaction.
41 | InteractionSynchronous
42 | // InteractionAsynchronous describes an asynchronous interaction.
43 | InteractionAsynchronous
44 | )
45 |
46 | // RelationshipTags lists the tags that are added to all relationships.
47 | var RelationshipTags = []string{"Relationship", "Asynchronous", "Synchronous"}
48 |
49 | // EvalName is the qualified name of the expression.
50 | func (r *Relationship) EvalName() string {
51 | var src, dest = "", ""
52 | if r.Source != nil {
53 | src = r.Source.Name
54 | }
55 | if r.Destination != nil {
56 | dest = r.Destination.Name
57 | }
58 | return fmt.Sprintf("relationship %q [%s -> %s]", r.Description, src, dest)
59 | }
60 |
61 | // Finalize computes the destination and adds the "Relationship" tag.
62 | func (r *Relationship) Finalize() {
63 | // prefix tags
64 | if r.InteractionStyle == InteractionAsynchronous {
65 | r.Tags = mergeTags("Asynchronous", strings.Split(r.Tags, ","))
66 | }
67 | r.Tags = mergeTags(RelationshipTags[0], strings.Split(r.Tags, ","))
68 | }
69 |
70 | // PrefixTags adds the given tags to the beginning of the comm
71 | // Dup creates a new relationship with identical description, tags, URL,
72 | // technology and interaction style as r. Dup also creates a new ID for the
73 | // result.
74 | func (r *Relationship) Dup(newSrc, newDest *Element) *Relationship {
75 | dup := &Relationship{
76 | Source: newSrc,
77 | InteractionStyle: r.InteractionStyle,
78 | Tags: r.Tags,
79 | URL: r.URL,
80 | Destination: newDest,
81 | Description: r.Description,
82 | Technology: r.Technology,
83 | }
84 | Identify(dup)
85 | return dup
86 | }
87 |
88 | // MergeTags adds the given tags. It skips tags already present in e.Tags.
89 | func (r *Relationship) MergeTags(tags ...string) {
90 | r.Tags = mergeTags(r.Tags, tags)
91 | }
92 |
--------------------------------------------------------------------------------
/expr/relationship_test.go:
--------------------------------------------------------------------------------
1 | package expr
2 |
3 | import (
4 | "fmt"
5 | "testing"
6 | )
7 |
8 | // Don't use t.parallel. Crashes when file tests are run as concurrent map read & write.
9 |
10 | func Test_Relationship_EvalName(t *testing.T) {
11 | mRelationship := Relationship{
12 | Source: &Element{Name: "Source"},
13 | Destination: &Element{Name: "Destination"},
14 | Description: "Sample",
15 | }
16 |
17 | tests := []struct {
18 | want string
19 | }{
20 | {want: "relationship \"Sample\" [Source -> Destination]"},
21 | }
22 | for i, tt := range tests {
23 | tt := tt
24 | t.Run(fmt.Sprint(i), func(t *testing.T) {
25 | if got := mRelationship.EvalName(); got != tt.want {
26 | t.Errorf("got %s, want %s", got, tt.want)
27 | }
28 | })
29 | }
30 |
31 | }
32 |
33 | func Test_Relationship_Finalize(t *testing.T) {
34 | mRelationship := Relationship{
35 | Description: "Sample",
36 | Tags: "Tag0",
37 | }
38 |
39 | tests := []struct {
40 | InteractionStyle InteractionStyleKind
41 | want string
42 | }{
43 | {InteractionStyle: InteractionAsynchronous, want: "Relationship,Asynchronous"},
44 | {InteractionStyle: InteractionSynchronous, want: "Relationship"},
45 | }
46 | for i, tt := range tests {
47 | mRelationship.InteractionStyle = tt.InteractionStyle
48 | mRelationship.Tags = ""
49 | t.Run(fmt.Sprint(i), func(t *testing.T) {
50 | mRelationship.Finalize()
51 | if mRelationship.Tags != tt.want {
52 | t.Errorf("received %s, wanted %s", mRelationship.Tags, tt.want)
53 | }
54 | })
55 | }
56 | }
57 |
58 | func Test_Relationship_MergeTags(t *testing.T) {
59 | mRelationship := Relationship{
60 | Source: &Element{Name: "Source"},
61 | Destination: &Element{Name: "Destination"},
62 | Description: "Sample",
63 | Tags: "Tag0",
64 | }
65 |
66 | tests := []struct {
67 | tag1 string
68 | tag2 string
69 | }{
70 | {tag1: "Tag1", tag2: "Tag2"},
71 | }
72 | for i, tt := range tests {
73 | tt := tt
74 | t.Run(fmt.Sprint(i), func(t *testing.T) {
75 | mRelationship.MergeTags(tt.tag1, tt.tag2)
76 | if mRelationship.Tags != "Tag0,Tag1,Tag2" {
77 | t.Errorf("had %s, wanted %s", mRelationship.Tags, "Tag0,Tag1,Tag2")
78 | }
79 | })
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/expr/system.go:
--------------------------------------------------------------------------------
1 | package expr
2 |
3 | import (
4 | "fmt"
5 | "strings"
6 | )
7 |
8 | type (
9 | // SoftwareSystem represents a software system.
10 | SoftwareSystem struct {
11 | *Element
12 | Location LocationKind
13 | Containers Containers
14 | }
15 |
16 | // SoftwareSystems is a slice of software system that can be easily
17 | // converted into a slice of ElementHolder.
18 | SoftwareSystems []*SoftwareSystem
19 | )
20 |
21 | // SoftwareSystemTags lists the tags that are added to all software systems.
22 | var SoftwareSystemTags = []string{"Element", "Software System"}
23 |
24 | // EvalName returns the generic expression name used in error messages.
25 | func (s *SoftwareSystem) EvalName() string {
26 | if s.Name == "" {
27 | return "unnamed software system"
28 | }
29 | return fmt.Sprintf("software system %q", s.Name)
30 | }
31 |
32 | // Finalize adds the 'SoftwareSystem' tag ands finalizes relationships.
33 | func (s *SoftwareSystem) Finalize() {
34 | s.PrefixTags(SoftwareSystemTags...)
35 | s.Element.Finalize()
36 | }
37 |
38 | // Elements returns a slice of ElementHolder that contains the elements of s.
39 | func (s SoftwareSystems) Elements() []ElementHolder {
40 | res := make([]ElementHolder, len(s))
41 | for i, ss := range s {
42 | res[i] = ss
43 | }
44 | return res
45 | }
46 |
47 | // Container returns the container with the given name if any, nil otherwise.
48 | func (s *SoftwareSystem) Container(name string) *Container {
49 | for _, c := range s.Containers {
50 | if c.Name == name {
51 | return c
52 | }
53 | }
54 | return nil
55 | }
56 |
57 | // AddContainer adds the given container to the software system. If there is
58 | // already a container with the given name then AddContainer merges both
59 | // definitions. The merge algorithm:
60 | //
61 | // - overrides the description, technology and URL if provided,
62 | // - merges any new tag or propery into the existing tags and properties,
63 | // - merges any new component into the existing components.
64 | //
65 | // AddContainer returns the new or merged person.
66 | func (s *SoftwareSystem) AddContainer(c *Container) *Container {
67 | existing := s.Container(c.Name)
68 | if existing == nil {
69 | Identify(c)
70 | s.Containers = append(s.Containers, c)
71 | return c
72 | }
73 | if c.Description != "" {
74 | existing.Description = c.Description
75 | }
76 | if c.Technology != "" {
77 | existing.Technology = c.Technology
78 | }
79 | if c.URL != "" {
80 | existing.URL = c.URL
81 | }
82 | existing.MergeTags(strings.Split(c.Tags, ",")...)
83 | for _, cmp := range c.Components {
84 | existing.AddComponent(cmp) // will merge if needed
85 | }
86 | if olddsl := existing.DSLFunc; olddsl != nil && c.DSLFunc != nil {
87 | existing.DSLFunc = func() { olddsl(); c.DSLFunc() }
88 | }
89 | return existing
90 | }
91 |
--------------------------------------------------------------------------------
/expr/system_test.go:
--------------------------------------------------------------------------------
1 | package expr
2 |
3 | import (
4 | "fmt"
5 | "testing"
6 | )
7 |
8 | func Test_AddContainer(t *testing.T) {
9 |
10 | mSoftwareSystem := SoftwareSystem{
11 | Element: &Element{ID: "1", Name: "Software system"},
12 | }
13 | Identify(&mSoftwareSystem)
14 | mContainer := Container{
15 | Element: &Element{ID: "1", Name: "Container", Tags: "One,Two"},
16 | System: &mSoftwareSystem,
17 | }
18 |
19 | mNewContainer := Container{
20 | Element: &Element{ID: "1",
21 | Name: "Container",
22 | Description: "Uncertain",
23 | Technology: "CottonOnString",
24 | URL: "microsoft.com",
25 | Tags: "Three,Four",
26 | },
27 | System: &mSoftwareSystem,
28 | }
29 |
30 | tests := []struct {
31 | in *Container
32 | want *Container
33 | }{
34 | {in: &mContainer, want: &mNewContainer},
35 | {in: &mNewContainer, want: &mNewContainer},
36 | }
37 | for i, tt := range tests {
38 | t.Run(fmt.Sprint(i), func(t *testing.T) {
39 | got := mSoftwareSystem.AddContainer(tt.in)
40 | if i == 0 {
41 | if got.Name != mContainer.Name {
42 | t.Errorf("Got %s, wanted %s", got.Name, tt.want.Name)
43 | }
44 | } else {
45 | if got.Description != mContainer.Description {
46 | t.Errorf("Got %s, wanted %s", got.Description, tt.want.Description)
47 | }
48 | if got.Technology != mContainer.Technology {
49 | t.Errorf("Got %s, wanted %s", got.Technology, tt.want.Technology)
50 | }
51 | }
52 | })
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/expr/view_props.go:
--------------------------------------------------------------------------------
1 | package expr
2 |
3 | import (
4 | "fmt"
5 |
6 | "goa.design/goa/v3/eval"
7 | )
8 |
9 | type (
10 | // ViewProps contains common properties of a view as well as helper
11 | // methods to fetch them.
12 | ViewProps struct {
13 | Key string
14 | Description string
15 | Title string
16 | AutoLayout *AutoLayout
17 | PaperSize PaperSizeKind
18 | ElementViews []*ElementView
19 | RelationshipViews []*RelationshipView
20 | AnimationSteps []*AnimationStep
21 |
22 | // The following fields are used to compute the elements and
23 | // relationships that should be added to the view.
24 | AddAll bool
25 | AddDefault bool
26 | AddNeighbors []*Element
27 | RemoveElements []*Element
28 | RemoveTags []string
29 | RemoveRelationships []*Relationship
30 | RemoveUnreachable []*Element
31 | RemoveUnrelated bool
32 | CoalescedRelationships []*CoalescedRelationship
33 | CoalesceAllRelationships bool
34 | }
35 |
36 | // ElementView describes an instance of a model element (Person,
37 | // Software System, Container or Component) in a View.
38 | ElementView struct {
39 | Element *Element
40 | NoRelationship bool
41 | X *int
42 | Y *int
43 | }
44 |
45 | // RelationshipView describes an instance of a model relationship in a
46 | // view.
47 | RelationshipView struct {
48 | Source *Element
49 | Destination *Element
50 | Description string
51 | Technology string
52 | Order string
53 | Vertices []*Vertex
54 | Routing RoutingKind
55 | Position *int
56 |
57 | // RelationshipID is computed in finalize.
58 | RelationshipID string
59 | }
60 |
61 | // AutoLayout describes an automatic layout.
62 | AutoLayout struct {
63 | Implementation ImplementationKind
64 | RankDirection RankDirectionKind
65 | RankSep *int
66 | NodeSep *int
67 | EdgeSep *int
68 | Vertices *bool
69 | }
70 |
71 | // Vertex describes the x and y coordinate of a bend in a line.
72 | Vertex struct {
73 | X int
74 | Y int
75 | }
76 |
77 | // AnimationStep represents an animation step.
78 | AnimationStep struct {
79 | Elements []ElementHolder
80 | RelationshipIDs []string
81 | Order int
82 | View View
83 | }
84 |
85 | // CoalescedRelationship describes relationships that should be coalesced.
86 | CoalescedRelationship struct {
87 | Source *Element
88 | Destination *Element
89 | Description string
90 | Technology string
91 | }
92 |
93 | // PaperSizeKind is the enum for possible paper kinds.
94 | PaperSizeKind int
95 |
96 | // RoutingKind is the enum for possible routing algorithms.
97 | RoutingKind int
98 |
99 | // ImplementationKind is the enum for possible automatic layout implementations
100 | ImplementationKind int
101 |
102 | // RankDirectionKind is the enum for possible automatic layout rank
103 | // directions.
104 | RankDirectionKind int
105 | )
106 |
107 | const (
108 | SizeUndefined PaperSizeKind = iota
109 | SizeA0Landscape
110 | SizeA0Portrait
111 | SizeA1Landscape
112 | SizeA1Portrait
113 | SizeA2Landscape
114 | SizeA2Portrait
115 | SizeA3Landscape
116 | SizeA3Portrait
117 | SizeA4Landscape
118 | SizeA4Portrait
119 | SizeA5Landscape
120 | SizeA5Portrait
121 | SizeA6Landscape
122 | SizeA6Portrait
123 | SizeLegalLandscape
124 | SizeLegalPortrait
125 | SizeLetterLandscape
126 | SizeLetterPortrait
127 | SizeSlide16X10
128 | SizeSlide16X9
129 | SizeSlide4X3
130 | )
131 |
132 | const (
133 | RoutingUndefined RoutingKind = iota
134 | RoutingDirect
135 | RoutingOrthogonal
136 | RoutingCurved
137 | )
138 |
139 | const (
140 | ImplementationUndefined ImplementationKind = iota
141 | ImplementationGraphviz
142 | ImplementationDagre
143 | )
144 |
145 | const (
146 | RankUndefined RankDirectionKind = iota
147 | RankTopBottom
148 | RankBottomTop
149 | RankLeftRight
150 | RankRightLeft
151 | )
152 |
153 | // ElementView returns the element view for the element with the given ID if
154 | // any.
155 | func (v *ViewProps) ElementView(id string) *ElementView {
156 | for _, ev := range v.ElementViews {
157 | if ev.Element.ID == id {
158 | return ev
159 | }
160 | }
161 | return nil
162 | }
163 |
164 | // Props returns the underlying properties object.
165 | func (v *ViewProps) Props() *ViewProps { return v }
166 |
167 | // EvalName returns the generic expression name used in error messages.
168 | func (v *ViewProps) EvalName() string {
169 | var suf string
170 | switch {
171 | case v.Title != "":
172 | suf = fmt.Sprintf(" with title %q and key %q", v.Title, v.Key)
173 | default:
174 | suf = fmt.Sprintf(" %q", v.Key)
175 | }
176 | return fmt.Sprintf("view%s", suf)
177 | }
178 |
179 | // EvalName returns the generic expression name used in error messages.
180 | func (*AnimationStep) EvalName() string { return "animation step" }
181 |
182 | // Add adds the given elements to the animation step.
183 | func (l *AnimationStep) Add(eh ElementHolder) {
184 | l.Elements = append(l.Elements, eh)
185 | }
186 |
187 | // EvalName returns the generic expression name used in error messages.
188 | func (*AutoLayout) EvalName() string { return "automatic layout" }
189 |
190 | // EvalName returns the generic expression name used in error messages.
191 | func (*ElementView) EvalName() string { return "element view" }
192 |
193 | // EvalName returns the generic expression name used in error messages.
194 | func (*RelationshipView) EvalName() string { return "relationship view" }
195 |
196 | // EvalName returns the generic expression name used in error messages.
197 | func (*CoalescedRelationship) EvalName() string { return "coalesced relationship" }
198 |
199 | // Validate makes sure there is a corresponding relationship (and exactly one).
200 | func (v *RelationshipView) Validate() error {
201 | verr := new(eval.ValidationErrors)
202 | var rel *Relationship
203 | found := false
204 | IterateRelationships(func(r *Relationship) {
205 | if r.Source.ID == v.Source.ID && r.Destination.ID == v.Destination.ID {
206 | if v.Description != "" {
207 | if r.Description == v.Description {
208 | rel = r
209 | }
210 | } else {
211 | rel = r
212 | if found {
213 | verr.Add(v, "Link: there exists multiple relationships between %q and %q, specify the relationship description.", v.Source.Name, v.Destination.Name)
214 | }
215 | found = true
216 | }
217 | }
218 | })
219 | if rel == nil {
220 | var suffix string
221 | if v.Description != "" {
222 | suffix = fmt.Sprintf(" with description %q", v.Description)
223 | }
224 | verr.Add(v, "Link: no relationship between %q and %q%s", v.Source.Name, v.Destination.Name, suffix)
225 | }
226 | return verr
227 | }
228 |
229 | // reachable returns the IDs of all elements that can be reached by traversing
230 | // the relationships from the given root.
231 | func reachable(e *Element) (res []string) {
232 | seen := make(map[string]struct{})
233 | traverse(e, seen)
234 | res = make([]string, len(seen))
235 | for k := range seen {
236 | res = append(res, k)
237 | }
238 | return
239 | }
240 |
241 | func traverse(e *Element, seen map[string]struct{}) {
242 | add := func(nid string) bool {
243 | for id := range seen {
244 | if id == nid {
245 | return false
246 | }
247 | }
248 | seen[nid] = struct{}{}
249 | return true
250 | }
251 | IterateRelationships(func(r *Relationship) {
252 | if r.Source.ID == e.ID {
253 | if add(r.Destination.ID) {
254 | traverse(r.Destination, seen)
255 | }
256 | }
257 | if r.Destination.ID == e.ID {
258 | if add(r.Source.ID) {
259 | traverse(r.Source, seen)
260 | }
261 | }
262 | })
263 | }
264 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module goa.design/model
2 |
3 | go 1.23.0
4 |
5 | toolchain go1.24.1
6 |
7 | require (
8 | github.com/fsnotify/fsnotify v1.9.0
9 | github.com/jaschaephraim/lrserver v0.0.0-20240306232639-afed386b3640
10 | github.com/kylelemons/godebug v1.1.0
11 | github.com/stretchr/testify v1.10.0
12 | goa.design/goa/v3 v3.21.1
13 | golang.org/x/tools v0.33.0
14 | )
15 |
16 | require (
17 | github.com/davecgh/go-spew v1.1.1 // indirect
18 | github.com/dimfeld/httppath v0.0.0-20170720192232-ee938bf73598 // indirect
19 | github.com/go-chi/chi/v5 v5.2.1 // indirect
20 | github.com/google/uuid v1.6.0 // indirect
21 | github.com/gorilla/websocket v1.5.3 // indirect
22 | github.com/kr/pretty v0.1.0 // indirect
23 | github.com/manveru/faker v0.0.0-20171103152722-9fbc68a78c4d // indirect
24 | github.com/pmezard/go-difflib v1.0.0 // indirect
25 | golang.org/x/mod v0.24.0 // indirect
26 | golang.org/x/sync v0.14.0 // indirect
27 | golang.org/x/sys v0.33.0 // indirect
28 | golang.org/x/text v0.25.0 // indirect
29 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
30 | gopkg.in/yaml.v3 v3.0.1 // indirect
31 | )
32 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
2 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
3 | github.com/dimfeld/httppath v0.0.0-20170720192232-ee938bf73598 h1:MGKhKyiYrvMDZsmLR/+RGffQSXwEkXgfLSA08qDn9AI=
4 | github.com/dimfeld/httppath v0.0.0-20170720192232-ee938bf73598/go.mod h1:0FpDmbrt36utu8jEmeU05dPC9AB5tsLYVVi+ZHfyuwI=
5 | github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
6 | github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
7 | github.com/go-chi/chi/v5 v5.2.1 h1:KOIHODQj58PmL80G2Eak4WdvUzjSJSm0vG72crDCqb8=
8 | github.com/go-chi/chi/v5 v5.2.1/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
9 | github.com/gohugoio/hashstructure v0.5.0 h1:G2fjSBU36RdwEJBWJ+919ERvOVqAg9tfcYp47K9swqg=
10 | github.com/gohugoio/hashstructure v0.5.0/go.mod h1:Ser0TniXuu/eauYmrwM4o64EBvySxNzITEOLlm4igec=
11 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
12 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
13 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
14 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
15 | github.com/gopherjs/gopherjs v1.17.2 h1:fQnZVsXk8uxXIStYb0N4bGk7jeyTalG/wsZjQ25dO0g=
16 | github.com/gopherjs/gopherjs v1.17.2/go.mod h1:pRRIvn/QzFLrKfvEz3qUuEhtE/zLCWfreZ6J5gM2i+k=
17 | github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
18 | github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
19 | github.com/jaschaephraim/lrserver v0.0.0-20240306232639-afed386b3640 h1:qxoA9wh1IZAbMhfFSE81tn8RsB48LNd7ecH6lFpxucc=
20 | github.com/jaschaephraim/lrserver v0.0.0-20240306232639-afed386b3640/go.mod h1:1Dkfm1/kgjeZc+2TBUAyZ3TJeQ/HaKbj8ig+7nAHkws=
21 | github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
22 | github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
23 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
24 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
25 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
26 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
27 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
28 | github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
29 | github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
30 | github.com/manveru/faker v0.0.0-20171103152722-9fbc68a78c4d h1:Zj+PHjnhRYWBK6RqCDBcAhLXoi3TzC27Zad/Vn+gnVQ=
31 | github.com/manveru/faker v0.0.0-20171103152722-9fbc68a78c4d/go.mod h1:WZy8Q5coAB1zhY9AOBJP0O6J4BuDfbupUDavKY+I3+s=
32 | github.com/manveru/gobdd v0.0.0-20131210092515-f1a17fdd710b h1:3E44bLeN8uKYdfQqVQycPnaVviZdBLbizFhU49mtbe4=
33 | github.com/manveru/gobdd v0.0.0-20131210092515-f1a17fdd710b/go.mod h1:Bj8LjjP0ReT1eKt5QlKjwgi5AFm5mI6O1A2G4ChI0Ag=
34 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
35 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
36 | github.com/smarty/assertions v1.15.0 h1:cR//PqUBUiQRakZWqBiFFQ9wb8emQGDb0HeGdqGByCY=
37 | github.com/smarty/assertions v1.15.0/go.mod h1:yABtdzeQs6l1brC900WlRNwj6ZR55d7B+E8C6HtKdec=
38 | github.com/smartystreets/goconvey v1.8.1 h1:qGjIddxOk4grTu9JPOU31tVfq3cNdBlNa5sSznIX1xY=
39 | github.com/smartystreets/goconvey v1.8.1/go.mod h1:+/u4qLyY6x1jReYOp7GOM2FSt8aP9CzCZL03bI28W60=
40 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
41 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
42 | goa.design/goa/v3 v3.21.1 h1:tLwhbcNoEBJm1CcJc3ks6oZ8BHYl6vFuxEBnl2kC428=
43 | goa.design/goa/v3 v3.21.1/go.mod h1:E+97AYffVIvDi6LkuNdfdvMZb8UFb/+ie3V0/WBBdgc=
44 | golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU=
45 | golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
46 | golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ=
47 | golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
48 | golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
49 | golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
50 | golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
51 | golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
52 | golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc=
53 | golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI=
54 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
55 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
56 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
57 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
58 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
59 |
--------------------------------------------------------------------------------
/mdl/deployment.go:
--------------------------------------------------------------------------------
1 | package mdl
2 |
3 | type (
4 | // DeploymentNode describes a single deployment node.
5 | DeploymentNode struct {
6 | // ID of element.
7 | ID string `json:"id"`
8 | // Name of element - not applicable to ContainerInstance.
9 | Name string `json:"name,omitempty"`
10 | // Description of element if any.
11 | Description string `json:"description,omitempty"`
12 | // Technology used by element if any - not applicable to Person.
13 | Technology string `json:"technology,omitempty"`
14 | // Environment is the deployment environment in which this deployment
15 | // node resides (e.g. "Development", "Live", etc).
16 | Environment string `json:"environment"`
17 | // Instances is the number of instances.
18 | Instances *string `json:"instances,omitempty"`
19 | // Tags attached to element as comma separated list if any.
20 | Tags string `json:"tags,omitempty"`
21 | // URL where more information about this element can be found.
22 | URL string `json:"url,omitempty"`
23 | // Children describe the child deployment nodes if any.
24 | Children []*DeploymentNode `json:"children,omitempty"`
25 | // InfrastructureNodes describe the infrastructure nodes (load
26 | // balancers, firewall etc.)
27 | InfrastructureNodes []*InfrastructureNode `json:"infrastructureNodes,omitempty"`
28 | // ContainerInstances describe instances of containers deployed in
29 | // deployment node.
30 | ContainerInstances []*ContainerInstance `json:"containerInstances,omitempty"`
31 | // Set of arbitrary name-value properties (shown in diagram tooltips).
32 | Properties map[string]string `json:"properties,omitempty"`
33 | // Relationships is the set of relationships from this element to other
34 | // elements.
35 | Relationships []*Relationship `json:"relationships,omitempty"`
36 | }
37 |
38 | // InfrastructureNode describes an infrastructure node.
39 | InfrastructureNode struct {
40 | // ID of element.
41 | ID string `json:"id"`
42 | // Name of element - not applicable to ContainerInstance.
43 | Name string `json:"name,omitempty"`
44 | // Description of element if any.
45 | Description string `json:"description,omitempty"`
46 | // Technology used by element if any - not applicable to Person.
47 | Technology string `json:"technology,omitempty"`
48 | // Tags attached to element as comma separated list if any.
49 | Tags string `json:"tags,omitempty"`
50 | // URL where more information about this element can be found.
51 | URL string `json:"url,omitempty"`
52 | // Set of arbitrary name-value properties (shown in diagram tooltips).
53 | Properties map[string]string `json:"properties,omitempty"`
54 | // Relationships is the set of relationships from this element to other
55 | // elements.
56 | Relationships []*Relationship `json:"relationships,omitempty"`
57 | // Environment is the deployment environment in which this
58 | // infrastructure node resides (e.g. "Development", "Live",
59 | // etc).
60 | Environment string `json:"environment"`
61 | }
62 |
63 | // ContainerInstance describes an instance of a container.
64 | ContainerInstance struct {
65 | // ID of element.
66 | ID string `json:"id"`
67 | // Tags attached to element as comma separated list if any.
68 | Tags string `json:"tags,omitempty"`
69 | // URL where more information about this element can be found.
70 | URL string `json:"url,omitempty"`
71 | // Set of arbitrary name-value properties (shown in diagram tooltips).
72 | Properties map[string]string `json:"properties,omitempty"`
73 | // Relationships is the set of relationships from this element to other
74 | // elements.
75 | Relationships []*Relationship `json:"relationships,omitempty"`
76 | // ID of container that is instantiated.
77 | ContainerID string `json:"containerId"`
78 | // InstanceID is the number/index of this instance.
79 | InstanceID int `json:"instanceId"`
80 | // Environment is the deployment environment of this instance.
81 | Environment string `json:"environment"`
82 | // HealthChecks is the set of HTTP-based health checks for this
83 | // container instance.
84 | HealthChecks []*HealthCheck `json:"healthChecks,omitempty"`
85 | }
86 |
87 | // HealthCheck is a HTTP-based health check.
88 | HealthCheck struct {
89 | // Name of health check.
90 | Name string `json:"name"`
91 | // Health check URL/endpoint.
92 | URL string `json:"url"`
93 | // Polling interval, in seconds.
94 | Interval int `json:"interval"`
95 | // Timeout after which health check is deemed as failed, in milliseconds.
96 | Timeout int `json:"timeout"`
97 | // Set of name-value pairs corresponding to HTTP headers to be sent with request.
98 | Headers map[string]string `json:"headers"`
99 | }
100 | )
101 |
--------------------------------------------------------------------------------
/mdl/doc.go:
--------------------------------------------------------------------------------
1 | /*
2 | Package mdl defines data structures that represent a software architecture
3 | model and accompanying views that follow the C4 model (https://c4model.com).
4 |
5 | The data structures can be serialized into JSON. The top level data structure
6 | is Design which defines the model and views as well as a name, description
7 | and version for the design. Model describes the people, software systems,
8 | containers and components that make up the architecture as well as the
9 | deployment nodes that represent runtime deployments. Views describes diagrams
10 | that represent different level of details - from contextual views that
11 | represent the overall system in context with other systems to component level
12 | views that render software components and their relationships.
13 | */
14 | package mdl
15 |
--------------------------------------------------------------------------------
/mdl/elements.go:
--------------------------------------------------------------------------------
1 | package mdl
2 |
3 | import (
4 | "bytes"
5 | "encoding/json"
6 | )
7 |
8 | type (
9 | // Person represents a person.
10 | Person struct {
11 | // ID of element.
12 | ID string `json:"id"`
13 | // Name of element - not applicable to ContainerInstance.
14 | Name string `json:"name,omitempty"`
15 | // Description of element if any.
16 | Description string `json:"description,omitempty"`
17 | // Tags attached to element as comma separated list if any.
18 | Tags string `json:"tags,omitempty"`
19 | // URL where more information about this element can be found.
20 | URL string `json:"url,omitempty"`
21 | // Set of arbitrary name-value properties (shown in diagram tooltips).
22 | Properties map[string]string `json:"properties,omitempty"`
23 | // Relationships is the set of relationships from this element to other
24 | // elements.
25 | Relationships []*Relationship `json:"relationships,omitempty"`
26 | // Location of person.
27 | Location LocationKind `json:"location,omitempty"`
28 | }
29 |
30 | // SoftwareSystem represents a software system.
31 | SoftwareSystem struct {
32 | // ID of element.
33 | ID string `json:"id"`
34 | // Name of element - not applicable to ContainerInstance.
35 | Name string `json:"name,omitempty"`
36 | // Description of element if any.
37 | Description string `json:"description,omitempty"`
38 | // Tags attached to element as comma separated list if any.
39 | Tags string `json:"tags,omitempty"`
40 | // URL where more information about this element can be found.
41 | URL string `json:"url,omitempty"`
42 | // Set of arbitrary name-value properties (shown in diagram tooltips).
43 | Properties map[string]string `json:"properties,omitempty"`
44 | // Relationships is the set of relationships from this element to other
45 | // elements.
46 | Relationships []*Relationship `json:"relationships,omitempty"`
47 | // Location of element.
48 | Location LocationKind `json:"location,omitempty"`
49 | // Containers list the containers within the software system.
50 | Containers []*Container `json:"containers,omitempty"`
51 | }
52 |
53 | // Container represents a container.
54 | Container struct {
55 | // ID of element.
56 | ID string `json:"id"`
57 | // Name of element - not applicable to ContainerInstance.
58 | Name string `json:"name,omitempty"`
59 | // Description of element if any.
60 | Description string `json:"description,omitempty"`
61 | // Technology used by element if any - not applicable to Person.
62 | Technology string `json:"technology,omitempty"`
63 | // Tags attached to element as comma separated list if any.
64 | Tags string `json:"tags,omitempty"`
65 | // URL where more information about this element can be found.
66 | URL string `json:"url,omitempty"`
67 | // Set of arbitrary name-value properties (shown in diagram tooltips).
68 | Properties map[string]string `json:"properties,omitempty"`
69 | // Relationships is the set of relationships from this element to other
70 | // elements.
71 | Relationships []*Relationship `json:"relationships,omitempty"`
72 | // Components list the components within the container.
73 | Components []*Component `json:"components,omitempty"`
74 | }
75 |
76 | // Component represents a component.
77 | Component struct {
78 | // ID of element.
79 | ID string `json:"id"`
80 | // Name of element - not applicable to ContainerInstance.
81 | Name string `json:"name,omitempty"`
82 | // Description of element if any.
83 | Description string `json:"description,omitempty"`
84 | // Technology used by element if any - not applicable to Person.
85 | Technology string `json:"technology,omitempty"`
86 | // Tags attached to element as comma separated list if any.
87 | Tags string `json:"tags,omitempty"`
88 | // URL where more information about this element can be found.
89 | URL string `json:"url,omitempty"`
90 | // Set of arbitrary name-value properties (shown in diagram tooltips).
91 | Properties map[string]string `json:"properties,omitempty"`
92 | // Relationships is the set of relationships from this element to other
93 | // elements.
94 | Relationships []*Relationship `json:"relationships,omitempty"`
95 | }
96 |
97 | // LocationKind is the enum for possible locations.
98 | LocationKind int
99 | )
100 |
101 | const (
102 | // LocationUndefined means no location specified in design.
103 | LocationUndefined LocationKind = iota
104 | // LocationInternal defines an element internal to the enterprise.
105 | LocationInternal
106 | // LocationExternal defines an element external to the enterprise.
107 | LocationExternal
108 | )
109 |
110 | // MarshalJSON replaces the constant value with the proper string value.
111 | func (l LocationKind) MarshalJSON() ([]byte, error) {
112 | buf := bytes.NewBufferString(`"`)
113 | switch l {
114 | case LocationInternal:
115 | buf.WriteString("Internal")
116 | case LocationExternal:
117 | buf.WriteString("External")
118 | case LocationUndefined:
119 | buf.WriteString("Undefined")
120 | }
121 | buf.WriteString(`"`)
122 | return buf.Bytes(), nil
123 | }
124 |
125 | // UnmarshalJSON sets the constant from its JSON representation.
126 | func (l *LocationKind) UnmarshalJSON(data []byte) error {
127 | var val string
128 | if err := json.Unmarshal(data, &val); err != nil {
129 | return err
130 | }
131 | switch val {
132 | case "Internal":
133 | *l = LocationInternal
134 | case "External":
135 | *l = LocationExternal
136 | case "Undefined":
137 | *l = LocationUndefined
138 | }
139 | return nil
140 | }
141 |
--------------------------------------------------------------------------------
/mdl/model.go:
--------------------------------------------------------------------------------
1 | package mdl
2 |
3 | import (
4 | "encoding/json"
5 | "sort"
6 | )
7 |
8 | type (
9 | // Model describes a software architecture model.
10 | Model struct {
11 | // Enterprise associated with model if any.
12 | Enterprise *Enterprise `json:"enterprise,omitempty"`
13 | // People lists Person elements.
14 | People []*Person `json:"people,omitempty"`
15 | // Systems lists Software System elements.
16 | Systems []*SoftwareSystem `json:"softwareSystems,omitempty"`
17 | // DeploymentNodes list the deployment nodes.
18 | DeploymentNodes []*DeploymentNode `json:"deploymentNodes,omitempty"`
19 | }
20 |
21 | // Enterprise describes a named enterprise / organization.
22 | Enterprise struct {
23 | // Name of enterprise.
24 | Name string `json:"name"`
25 | }
26 |
27 | // alias to call original json.Unmarshal
28 | _model Model
29 | )
30 |
31 | // MarshalJSON guarantees the order of elements in generated JSON arrays that
32 | // correspond to sets.
33 | func (m *Model) MarshalJSON() ([]byte, error) {
34 | sort.Slice(m.People, func(i, j int) bool { return m.People[i].Name < m.People[j].Name })
35 | for _, p := range m.People {
36 | sort.Slice(p.Relationships, func(i, j int) bool { return p.Relationships[i].ID < p.Relationships[j].ID })
37 | }
38 | sort.Slice(m.Systems, func(i, j int) bool { return m.Systems[i].Name < m.Systems[j].Name })
39 | for _, sys := range m.Systems {
40 | sort.Slice(sys.Relationships, func(i, j int) bool { return sys.Relationships[i].ID < sys.Relationships[j].ID })
41 | sort.Slice(sys.Containers, func(i, j int) bool { return sys.Containers[i].Name < sys.Containers[j].Name })
42 | for _, c := range sys.Containers {
43 | sort.Slice(c.Relationships, func(i, j int) bool { return c.Relationships[i].ID < c.Relationships[j].ID })
44 | sort.Slice(c.Components, func(i, j int) bool { return c.Components[i].Name < c.Components[j].Name })
45 | for _, cmp := range c.Components {
46 | sort.Slice(cmp.Relationships, func(i, j int) bool { return cmp.Relationships[i].ID < cmp.Relationships[j].ID })
47 | }
48 | }
49 | }
50 | sortDeploymentNodes(m.DeploymentNodes)
51 | mm := _model(*m)
52 | return json.Marshal(&mm)
53 | }
54 |
55 | func sortDeploymentNodes(nodes []*DeploymentNode) {
56 | sort.Slice(nodes, func(i, j int) bool { return nodes[i].Name < nodes[j].Name })
57 | for _, node := range nodes {
58 | sortDeploymentNodes(node.Children)
59 | sort.Slice(node.InfrastructureNodes, func(i, j int) bool { return node.InfrastructureNodes[i].Name < node.InfrastructureNodes[j].Name })
60 | sort.Slice(node.ContainerInstances, func(i, j int) bool { return node.ContainerInstances[i].ID < node.ContainerInstances[j].ID })
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/mdl/relationship.go:
--------------------------------------------------------------------------------
1 | package mdl
2 |
3 | import (
4 | "bytes"
5 | "encoding/json"
6 | )
7 |
8 | type (
9 | // Relationship describes a uni-directional relationship between two elements.
10 | Relationship struct {
11 | // ID of relationship.
12 | ID string `json:"id"`
13 | // Description of relationship if any.
14 | Description string `json:"description"`
15 | // Tags attached to relationship as comma separated list if any.
16 | Tags string `json:"tags,omitempty"`
17 | // URL where more information can be found.
18 | URL string `json:"url,omitempty"`
19 | // SourceID is the ID of the source element.
20 | SourceID string `json:"sourceId"`
21 | // DestinationID is ID the destination element.
22 | DestinationID string `json:"destinationId"`
23 | // Technology associated with relationship.
24 | Technology string `json:"technology,omitempty"`
25 | // InteractionStyle describes whether the interaction is synchronous or
26 | // asynchronous
27 | InteractionStyle InteractionStyleKind `json:"interactionStyle"`
28 | // ID of container-container relationship upon which this container
29 | // instance-container instance relationship is based.
30 | LinkedRelationshipID string `json:"linkedRelationshipId,omitempty"`
31 | }
32 |
33 | // InteractionStyleKind is the enum for possible interaction styles.
34 | InteractionStyleKind int
35 | )
36 |
37 | const (
38 | // InteractionUndefined means no interaction style specified in design.
39 | InteractionUndefined InteractionStyleKind = iota
40 | // InteractionSynchronous describes a synchronous interaction.
41 | InteractionSynchronous
42 | // InteractionAsynchronous describes an asynchronous interaction.
43 | InteractionAsynchronous
44 | )
45 |
46 | // MarshalJSON replaces the constant value with the proper string value.
47 | func (i InteractionStyleKind) MarshalJSON() ([]byte, error) {
48 | buf := bytes.NewBufferString(`"`)
49 | switch i {
50 | case InteractionSynchronous:
51 | buf.WriteString("Synchronous")
52 | case InteractionAsynchronous:
53 | buf.WriteString("Asynchronous")
54 | case InteractionUndefined:
55 | buf.WriteString("Undefined")
56 | }
57 | buf.WriteString(`"`)
58 | return buf.Bytes(), nil
59 | }
60 |
61 | // UnmarshalJSON sets the constant from its JSON representation.
62 | func (i *InteractionStyleKind) UnmarshalJSON(data []byte) error {
63 | var val string
64 | if err := json.Unmarshal(data, &val); err != nil {
65 | return err
66 | }
67 | switch val {
68 | case "Synchronous":
69 | *i = InteractionSynchronous
70 | case "Asynchronous":
71 | *i = InteractionAsynchronous
72 | case "Undefined":
73 | *i = InteractionUndefined
74 | }
75 | return nil
76 | }
77 |
--------------------------------------------------------------------------------
/pkg/version.go:
--------------------------------------------------------------------------------
1 | package model
2 |
3 | import (
4 | "fmt"
5 | )
6 |
7 | const (
8 | // Major version number
9 | Major = 1
10 | // Minor version number
11 | Minor = 11
12 | // Build number
13 | Build = 2
14 | // Suffix - set to empty string in release tag commits.
15 | Suffix = ""
16 | )
17 |
18 | // Version returns the complete version number.
19 | func Version() string {
20 | if Suffix != "" {
21 | return fmt.Sprintf("v%d.%d.%d-%s", Major, Minor, Build, Suffix)
22 | }
23 | return fmt.Sprintf("v%d.%d.%d", Major, Minor, Build)
24 | }
25 |
--------------------------------------------------------------------------------
/plugin/generate.go:
--------------------------------------------------------------------------------
1 | package plugin
2 |
3 | import (
4 | "encoding/json"
5 | "os"
6 | "path/filepath"
7 | "text/template"
8 |
9 | "goa.design/goa/v3/codegen"
10 | "goa.design/goa/v3/eval"
11 | "goa.design/model/expr"
12 | "goa.design/model/stz"
13 | )
14 |
15 | // init registers the plugin generator function.
16 | func init() {
17 | codegen.RegisterPlugin("model", "gen", nil, Generate)
18 | }
19 |
20 | // Generate produces the design JSON representation inside the top level gen
21 | // directory.
22 | func Generate(_ string, _ []eval.Root, files []*codegen.File) ([]*codegen.File, error) {
23 | err := eval.RunDSL()
24 | if err != nil {
25 | return nil, err
26 | }
27 |
28 | // Generate structurizr JSON
29 | path := filepath.Join(codegen.Gendir, "structurizr", "model.json")
30 | if _, err := os.Stat(path); !os.IsNotExist(err) {
31 | if err := os.Remove(path); err != nil {
32 | return nil, err
33 | }
34 | }
35 | section := &codegen.SectionTemplate{
36 | Name: "model",
37 | FuncMap: template.FuncMap{"toJSON": toJSON},
38 | Source: "{{ toJSON . }}",
39 | Data: stz.WorkspaceFromDesign(expr.Root),
40 | }
41 | files = append(files, &codegen.File{
42 | Path: path,
43 | SectionTemplates: []*codegen.SectionTemplate{section},
44 | })
45 |
46 | return files, nil
47 | }
48 |
49 | func toJSON(d any) string {
50 | b, err := json.MarshalIndent(d, "", " ")
51 | if err != nil {
52 | panic("design: " + err.Error()) // bug
53 | }
54 | return string(b)
55 | }
56 |
--------------------------------------------------------------------------------
/stz/client.go:
--------------------------------------------------------------------------------
1 | /*
2 | Package stz implements a client for the
3 | [Structurizr service HTTP APIs](https://structurizr.com/).
4 | */
5 | package stz
6 |
7 | import (
8 | "bytes"
9 | "crypto/hmac"
10 | "crypto/md5"
11 | "crypto/sha256"
12 | "encoding/base64"
13 | "encoding/hex"
14 | "encoding/json"
15 | "errors"
16 | "fmt"
17 | "io"
18 | "net/http"
19 | "net/url"
20 | "os"
21 | "os/user"
22 | "strconv"
23 | "time"
24 |
25 | goahttp "goa.design/goa/v3/http"
26 | )
27 |
28 | var (
29 | // Host is the Structurizr API host (var for testing).
30 | Host = "api.structurizr.com"
31 |
32 | // Scheme is the HTTP scheme used to make requests to the Structurizr service.
33 | Scheme = "https"
34 | )
35 |
36 | // UserAgent is the user agent used by this package.
37 | const UserAgent = "structurizr-go/1.0"
38 |
39 | // Response describes the API response returned by some endpoints.
40 | type Response struct {
41 | // Success is true if the API call was successful, false otherwise.
42 | Success bool `json:"success"`
43 | // Message is a human readable response message.
44 | Message string `json:"message"`
45 | // Revision is hte internal revision number.
46 | Revision int `json:"revision"`
47 | }
48 |
49 | // Doer is an interface that encapsulate a HTTP client Do method.
50 | type Doer interface {
51 | Do(*http.Request) (*http.Response, error)
52 | }
53 |
54 | // Client holds the credentials needed to make the requests.
55 | type Client struct {
56 | // Key is the API key.
57 | Key string
58 | // Secret is the API secret.
59 | Secret string
60 | // HTTP is the low level HTTP client.
61 | HTTP Doer
62 | }
63 |
64 | // NewClient instantiates a client using the default HTTP client.
65 | func NewClient(key, secret string) *Client {
66 | return &Client{Key: key, Secret: secret, HTTP: http.DefaultClient}
67 | }
68 |
69 | // Get retrieves the given workspace.
70 | func (c *Client) Get(id string) (*Workspace, error) {
71 | u := &url.URL{Scheme: Scheme, Host: Host, Path: fmt.Sprintf("/workspace/%s", id)}
72 | req, _ := http.NewRequest("GET", u.String(), http.NoBody)
73 | c.sign(req, "", "")
74 | resp, err := c.HTTP.Do(req)
75 | if err != nil {
76 | return nil, err
77 | }
78 | defer func() {
79 | if err := resp.Body.Close(); err != nil {
80 | fmt.Fprintf(os.Stderr, "failed to close response body: %v\n", err)
81 | }
82 | }()
83 | if resp.StatusCode != http.StatusOK {
84 | body, _ := io.ReadAll(resp.Body)
85 | return nil, fmt.Errorf("service error: %s", string(body))
86 | }
87 | var workspace Workspace
88 | if err := json.NewDecoder(resp.Body).Decode(&workspace); err != nil {
89 | return nil, err
90 | }
91 | return &workspace, nil
92 | }
93 |
94 | // Put stores the given workspace.
95 | func (c *Client) Put(id string, w *Workspace) error {
96 | u := &url.URL{Scheme: Scheme, Host: Host, Path: fmt.Sprintf("/workspace/%s", id)}
97 | body, _ := json.Marshal(w)
98 | req, _ := http.NewRequest("PUT", u.String(), bytes.NewReader(body))
99 | ct := "application/json; charset=UTF-8"
100 | c.sign(req, string(body), ct)
101 | resp, err := c.HTTP.Do(req)
102 | if err != nil {
103 | return err
104 | }
105 | defer func() {
106 | if err := resp.Body.Close(); err != nil {
107 | fmt.Fprintf(os.Stderr, "failed to close response body: %v\n", err)
108 | }
109 | }()
110 | if resp.StatusCode != http.StatusOK {
111 | body, _ := io.ReadAll(resp.Body)
112 | return fmt.Errorf("service error: %s", string(body))
113 | }
114 | return nil
115 | }
116 |
117 | // Lock locks the given workspace.
118 | func (c *Client) Lock(id string) error { return c.lockUnlock(id, true) }
119 |
120 | // Unlock unlocks a previously locked workspace.
121 | func (c *Client) Unlock(id string) error { return c.lockUnlock(id, false) }
122 |
123 | // EnableDebug causes the client to print debug information to Stderr.
124 | func (c *Client) EnableDebug() {
125 | c.HTTP = goahttp.NewDebugDoer(c.HTTP)
126 | }
127 |
128 | // lockUnlock implements the Lock and Unlock calls.
129 | func (c *Client) lockUnlock(id string, lock bool) error {
130 | u := &url.URL{Scheme: Scheme, Host: Host, Path: fmt.Sprintf("/workspace/%s/lock", id)}
131 | name := "anon"
132 | if usr, err := user.Current(); err == nil {
133 | name = usr.Name
134 | if name == "" {
135 | name = usr.Username
136 | }
137 | }
138 | // the order matters :(
139 | u.RawQuery = "user=" + url.QueryEscape(name) + "&agent=" + url.QueryEscape(UserAgent)
140 |
141 | verb := "PUT"
142 | if !lock {
143 | verb = "DELETE"
144 | }
145 | req, _ := http.NewRequest(verb, u.String(), http.NoBody)
146 | c.sign(req, "", "")
147 | resp, err := c.HTTP.Do(req)
148 | if err != nil {
149 | return err
150 | }
151 | defer func() {
152 | if err := resp.Body.Close(); err != nil {
153 | fmt.Fprintf(os.Stderr, "failed to close response body: %v\n", err)
154 | }
155 | }()
156 |
157 | if resp.StatusCode != http.StatusOK {
158 | var res Response
159 | json.NewDecoder(resp.Body).Decode(&res) // nolint: errcheck
160 | err = fmt.Errorf("service error: %s", resp.Status)
161 | if res.Message != "" {
162 | err = errors.New(res.Message)
163 | }
164 | return err
165 | }
166 |
167 | return nil
168 | }
169 |
170 | // sign signs the requests as per https://structurizr.com/help/web-api
171 | func (c *Client) sign(req *http.Request, content, ct string) {
172 | // 1. Compute digest
173 | var digest, nonce string
174 | var md5s string
175 | {
176 | h := md5.New()
177 | h.Write([]byte(content))
178 | md5b := h.Sum(nil)
179 | md5s = hex.EncodeToString(md5b)
180 | nonce = strconv.FormatInt(time.Now().UnixNano()/int64(time.Millisecond), 10)
181 | digest = fmt.Sprintf("%s\n%s\n%s\n%s\n%s\n", req.Method, req.URL.RequestURI(), md5s, ct, nonce)
182 | }
183 |
184 | // 2. Compute HMAC
185 | var hm []byte
186 | {
187 | h := hmac.New(sha256.New, []byte(c.Secret))
188 | h.Write([]byte(digest))
189 | hm = h.Sum(nil)
190 | }
191 |
192 | // 3. Write headers
193 | auth := base64.StdEncoding.EncodeToString([]byte(hex.EncodeToString(hm)))
194 | req.Header.Set("X-Authorization", fmt.Sprintf("%s:%s", c.Key, auth))
195 | req.Header.Set("Nonce", nonce)
196 | if req.Method == http.MethodPut {
197 | req.Header.Set("Content-Md5", base64.StdEncoding.EncodeToString([]byte(md5s)))
198 | req.Header.Set("Content-Type", ct)
199 | }
200 | req.Header.Set("User-Agent", UserAgent)
201 | }
202 |
--------------------------------------------------------------------------------
/stz/configuration.go:
--------------------------------------------------------------------------------
1 | package stz
2 |
3 | import (
4 | "bytes"
5 | "encoding/json"
6 |
7 | "goa.design/model/mdl"
8 | )
9 |
10 | type (
11 | // Configuration encapsulate Structurizr service specific view configuration information.
12 | Configuration struct {
13 | // Styles associated with views.
14 | Styles *mdl.Styles `json:"styles,omitempty"`
15 | // Key of view that was saved most recently.
16 | LastSavedView string `json:"lastSavedView,omitempty"`
17 | // Key of view shown by default.
18 | DefaultView string `json:"defaultView,omitempty"`
19 | // URL(s) of theme(s) used when rendering diagram.
20 | Themes []string `json:"themes,omitempty"`
21 | // Branding used in views.
22 | Branding *Branding `json:"branding,omitempty"`
23 | // Terminology used in workspace.
24 | Terminology *Terminology `json:"terminology,omitempty"`
25 | // Type of symbols used when rendering metadata.
26 | MetadataSymbols SymbolKind `json:"metadataSymbols,omitempty"`
27 | }
28 |
29 | // Branding is a wrapper for font and logo for diagram/documentation
30 | // branding purposes.
31 | Branding struct {
32 | // URL of PNG/JPG/GIF file, or Base64 data URI representation.
33 | Logo string `json:"logo,omitempty"`
34 | // Font details.
35 | Font *Font `json:"font,omitempty"`
36 | }
37 |
38 | // Terminology used on diagrams.
39 | Terminology struct {
40 | // Terminology used when rendering enterprise boundaries.
41 | Enterprise string `json:"enterprise,omitempty"`
42 | // Terminology used when rendering people.
43 | Person string `json:"person,omitempty"`
44 | // Terminology used when rendering software systems.
45 | SoftwareSystem string `json:"softwareSystem,omitempty"`
46 | // Terminology used when rendering containers.
47 | Container string `json:"container,omitempty"`
48 | // Terminology used when rendering components.
49 | Component string `json:"component,omitempty"`
50 | // Terminology used when rendering code elements.
51 | Code string `json:"code,omitempty"`
52 | // Terminology used when rendering deployment nodes.
53 | DeploymentNode string `json:"deploymentNode,omitempty"`
54 | // Terminology used when rendering relationships.
55 | Relationship string `json:"relationship,omitempty"`
56 | }
57 |
58 | // Font details including name and optional URL for web fonts.
59 | Font struct {
60 | // Name of font.
61 | Name string `json:"name,omitempty"`
62 | // Web font URL.
63 | URL string `json:"url,omitempty"`
64 | }
65 |
66 | // SymbolKind is the enum used to represent symbols used to render metadata.
67 | SymbolKind int
68 | )
69 |
70 | const (
71 | SymbolUndefined SymbolKind = iota
72 | SymbolSquareBrackets
73 | SymbolRoundBrackets
74 | SymbolCurlyBrackets
75 | SymbolAngleBrackets
76 | SymbolDoubleAngleBrackets
77 | SymbolNone
78 | )
79 |
80 | // MarshalJSON replaces the constant value with the proper string value.
81 | func (s SymbolKind) MarshalJSON() ([]byte, error) {
82 | buf := bytes.NewBufferString(`"`)
83 | switch s {
84 | case SymbolSquareBrackets:
85 | buf.WriteString("SquareBrackets")
86 | case SymbolRoundBrackets:
87 | buf.WriteString("RoundBrackets")
88 | case SymbolCurlyBrackets:
89 | buf.WriteString("CurlyBrackets")
90 | case SymbolAngleBrackets:
91 | buf.WriteString("AngleBrackets")
92 | case SymbolDoubleAngleBrackets:
93 | buf.WriteString("DoubleAngleBrackets")
94 | case SymbolNone:
95 | buf.WriteString("None")
96 | }
97 | buf.WriteString(`"`)
98 | return buf.Bytes(), nil
99 | }
100 |
101 | // UnmarshalJSON sets the constant from its JSON representation.
102 | func (s *SymbolKind) UnmarshalJSON(data []byte) error {
103 | var val string
104 | if err := json.Unmarshal(data, &val); err != nil {
105 | return err
106 | }
107 | switch val {
108 | case "SquareBrackets":
109 | *s = SymbolSquareBrackets
110 | case "RoundBrackets":
111 | *s = SymbolRoundBrackets
112 | case "CurlyBrackets":
113 | *s = SymbolCurlyBrackets
114 | case "AngleBrackets":
115 | *s = SymbolAngleBrackets
116 | case "DoubleAngleBrackets":
117 | *s = SymbolDoubleAngleBrackets
118 | case "None":
119 | *s = SymbolNone
120 | }
121 | return nil
122 | }
123 |
--------------------------------------------------------------------------------
/stz/doc.go:
--------------------------------------------------------------------------------
1 | /*
2 | Package stz complements the serializable model defined in mdl package
3 | with data specific to Structurizr service.
4 |
5 | The top level data structure is Workspace which replaces mdl.Design.
6 |
7 | The JSON representation of the workspace data structure is compatible with
8 | the Structurizr service API (https://structurizr.com). The package also
9 | includes a Structurize API client that can be used to upload and download
10 | workspaces to and from the service.
11 | */
12 | package stz
13 |
--------------------------------------------------------------------------------
/stz/eval.go:
--------------------------------------------------------------------------------
1 | package stz
2 |
3 | import (
4 | "goa.design/goa/v3/eval"
5 | "goa.design/model/expr"
6 | "goa.design/model/mdl"
7 | )
8 |
9 | // RunDSL runs the DSL defined in a global variable and returns the corresponding
10 | // Structurize workspace.
11 | func RunDSL() (*Workspace, error) {
12 | if err := eval.RunDSL(); err != nil {
13 | return nil, err
14 | }
15 | return WorkspaceFromDesign(expr.Root), nil
16 | }
17 |
18 | // WorkspaceFromDesign returns a Structurizr workspace initialized from the
19 | // given design.
20 | func WorkspaceFromDesign(d *expr.Design) *Workspace {
21 | design := mdl.ModelizeDesign(d)
22 | v := design.Views
23 |
24 | return &Workspace{
25 | Name: d.Name,
26 | Description: d.Description,
27 | Version: d.Version,
28 | Model: design.Model,
29 | Views: &Views{
30 | LandscapeViews: v.LandscapeViews,
31 | ContextViews: v.ContextViews,
32 | ContainerViews: v.ContainerViews,
33 | ComponentViews: v.ComponentViews,
34 | DynamicViews: v.DynamicViews,
35 | DeploymentViews: v.DeploymentViews,
36 | FilteredViews: v.FilteredViews,
37 | Configuration: &Configuration{Styles: v.Styles},
38 | },
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/stz/views.go:
--------------------------------------------------------------------------------
1 | package stz
2 |
3 | import (
4 | "goa.design/model/mdl"
5 | )
6 |
7 | type (
8 | // Views is the container for all views.
9 | Views struct {
10 | // LandscapeViewss describe the system landscape views.
11 | LandscapeViews []*mdl.LandscapeView `json:"systemLandscapeViews,omitempty"`
12 | // ContextViews lists the system context views.
13 | ContextViews []*mdl.ContextView `json:"systemContextViews,omitempty"`
14 | // ContainerViews lists the container views.
15 | ContainerViews []*mdl.ContainerView `json:"containerViews,omitempty"`
16 | // ComponentViews lists the component views.
17 | ComponentViews []*mdl.ComponentView `json:"componentViews,omitempty"`
18 | // DynamicViews lists the dynamic views.
19 | DynamicViews []*mdl.DynamicView `json:"dynamicViews,omitempty"`
20 | // DeploymentViews lists the deployment views.
21 | DeploymentViews []*mdl.DeploymentView `json:"deploymentViews,omitempty"`
22 | // FilteredViews lists the filtered views.
23 | FilteredViews []*mdl.FilteredView `json:"filteredViews,omitempty"`
24 | // Configuration contains view specific configuration information.
25 | Configuration *Configuration `json:"configuration,omitempty"`
26 | }
27 | )
28 |
--------------------------------------------------------------------------------
/stz/workspace.go:
--------------------------------------------------------------------------------
1 | package stz
2 |
3 | import (
4 | "bytes"
5 | "encoding/json"
6 |
7 | "goa.design/model/mdl"
8 | )
9 |
10 | type (
11 | // Workspace describes a Structurizr service workspace.
12 | Workspace struct {
13 | // ID of workspace.
14 | ID int `json:"id,omitempty"`
15 | // Name of workspace.
16 | Name string `json:"name"`
17 | // Description of workspace if any.
18 | Description string `json:"description,omitempty"`
19 | // Version number for the workspace.
20 | Version string `json:"version,omitempty"`
21 | // Revision number, automatically generated.
22 | Revision int `json:"revision,omitempty"`
23 | // Thumbnail associated with the workspace; a Base64 encoded PNG file as a
24 | // data URI (data:image/png;base64).
25 | Thumbnail string `json:"thumbnail,omitempty"`
26 | // The last modified date, in ISO 8601 format (e.g. "2018-09-08T12:40:03Z").
27 | LastModifiedDate string `json:"lastModifiedDate,omitempty"`
28 | // A string identifying the user who last modified the workspace (e.g. an
29 | // e-mail address or username).
30 | LastModifiedUser string `json:"lastModifiedUser,omitempty"`
31 | // A string identifying the agent that was last used to modify the workspace
32 | // (e.g. "model-go/1.2.0").
33 | LastModifiedAgent string `json:"lastModifiedAgent,omitempty"`
34 | // Model is the software architecture model.
35 | Model *mdl.Model `json:"model,omitempty"`
36 | // Views contains the views if any.
37 | Views *Views `json:"views,omitempty"`
38 | // Documentation associated with software architecture model.
39 | Documentation *Documentation `json:"documentation,omitempty"`
40 | // Configuration of workspace.
41 | Configuration *WorkspaceConfiguration `json:"configuration,omitempty"`
42 | }
43 |
44 | // WorkspaceConfiguration describes the workspace configuration.
45 | WorkspaceConfiguration struct {
46 | // Users that have access to the workspace.
47 | Users []*User `json:"users"`
48 | }
49 |
50 | // User of Structurizr service.
51 | User struct {
52 | Username string `json:"username"`
53 | // Role of user, one of "ReadWrite" or "ReadOnly".
54 | Role string `json:"role"`
55 | }
56 |
57 | // Documentation associated with software architecture model.
58 | Documentation struct {
59 | // Documentation sections.
60 | Sections []*DocumentationSection `json:"sections,omitempty"`
61 | // ADR decisions.
62 | Decisions []*Decision `json:"decisions,omitempty"`
63 | // Images used in documentation.
64 | Images []*Image `json:"images,omitempty"`
65 | // Information about template used to render documentation.
66 | Template *DocumentationTemplateMetadata `json:"template,omitempty"`
67 | }
68 |
69 | // DocumentationSection corresponds to a documentation section.
70 | DocumentationSection struct {
71 | // Title (name/section heading) of section.
72 | Title string `json:"title"`
73 | // Markdown or AsciiDoc content of section.
74 | Content string `json:"string"`
75 | // Content format.
76 | Format DocFormatKind `json:"format"`
77 | // Order (index) of section in document.
78 | Order int `json:"order"`
79 | // ID of element (in model) that section applies to (optional).
80 | ElementID string `json:"elementId,omitempty"`
81 | }
82 |
83 | // Decision record (e.g. architecture decision record).
84 | Decision struct {
85 | // ID of decision.
86 | ID string `json:"id"`
87 | // Date of decision in ISO 8601 format.
88 | Date string `json:"date"`
89 | // Status of decision.
90 | Decision DecisionStatusKind `json:"decision"`
91 | // Title of decision
92 | Title string `json:"title"`
93 | // Markdown or AsciiDoc content of decision.
94 | Content string `json:"content"`
95 | // Content format.
96 | Format DocFormatKind `json:"format"`
97 | // ID of element (in model) that decision applies to (optional).
98 | ElementID string `json:"elementId,omitempty"`
99 | }
100 |
101 | // Image represents a Base64 encoded image (PNG/JPG/GIF).
102 | Image struct {
103 | // Name of image.
104 | Name string `json:"image"`
105 | // Base64 encoded content.
106 | Content string `json:"content"`
107 | // Image MIME type (e.g. "image/png")
108 | Type string `json:"type"`
109 | }
110 |
111 | // DocumentationTemplateMetadata provides information about a documentation
112 | // template used to create documentation.
113 | DocumentationTemplateMetadata struct {
114 | // Name of documentation template.
115 | Name string `json:"name"`
116 | // Name of author of documentation template.
117 | Author string `json:"author,omitempty"`
118 | // URL that points to more information about template.
119 | URL string `json:"url,omitempty"`
120 | }
121 |
122 | // DocFormatKind is the enum used to represent documentation format.
123 | DocFormatKind int
124 |
125 | // DecisionStatusKind is the enum used to represent status of decision.
126 | DecisionStatusKind int
127 | )
128 |
129 | const (
130 | FormatUndefined DocFormatKind = iota
131 | FormatMarkdown
132 | FormatASCIIDoc
133 | )
134 |
135 | const (
136 | DecisionUndefined DecisionStatusKind = iota
137 | DecisionProposed
138 | DecisionAccepted
139 | DecisionSuperseded
140 | DecisionDeprecated
141 | DecisionRejected
142 | )
143 |
144 | // MarshalJSON replaces the constant value with the proper string value.
145 | func (d DocFormatKind) MarshalJSON() ([]byte, error) {
146 | buf := bytes.NewBufferString(`"`)
147 | switch d {
148 | case FormatMarkdown:
149 | buf.WriteString("Markdown")
150 | case FormatASCIIDoc:
151 | buf.WriteString("AsciiDoc")
152 | }
153 | buf.WriteString(`"`)
154 | return buf.Bytes(), nil
155 | }
156 |
157 | // UnmarshalJSON sets the constant from its JSON representation.
158 | func (d *DocFormatKind) UnmarshalJSON(data []byte) error {
159 | var val string
160 | if err := json.Unmarshal(data, &val); err != nil {
161 | return err
162 | }
163 | switch val {
164 | case "Markdown":
165 | *d = FormatMarkdown
166 | case "AsciiDoc":
167 | *d = FormatASCIIDoc
168 | }
169 | return nil
170 | }
171 |
172 | // MarshalJSON replaces the constant value with the proper string value.
173 | func (d DecisionStatusKind) MarshalJSON() ([]byte, error) {
174 | buf := bytes.NewBufferString(`"`)
175 | switch d {
176 | case DecisionProposed:
177 | buf.WriteString("Proposed")
178 | case DecisionAccepted:
179 | buf.WriteString("Accepted")
180 | case DecisionSuperseded:
181 | buf.WriteString("Superseded")
182 | case DecisionDeprecated:
183 | buf.WriteString("Deprecated")
184 | case DecisionRejected:
185 | buf.WriteString("Rejected")
186 | }
187 | buf.WriteString(`"`)
188 | return buf.Bytes(), nil
189 | }
190 |
191 | // UnmarshalJSON sets the constant from its JSON representation.
192 | func (d *DecisionStatusKind) UnmarshalJSON(data []byte) error {
193 | var val string
194 | if err := json.Unmarshal(data, &val); err != nil {
195 | return err
196 | }
197 | switch val {
198 | case "Proposed":
199 | *d = DecisionProposed
200 | case "Accepted":
201 | *d = DecisionAccepted
202 | case "Superseded":
203 | *d = DecisionSuperseded
204 | case "Deprecated":
205 | *d = DecisionDeprecated
206 | case "Rejected":
207 | *d = DecisionRejected
208 | }
209 | return nil
210 | }
211 |
--------------------------------------------------------------------------------