diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml new file mode 100644 index 0000000..a885b5a --- /dev/null +++ b/.github/workflows/go.yml @@ -0,0 +1,42 @@ +name: Test and build Go +on: [push, pull_request] +jobs: + + lint: + name: Lint + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-go@v4 + with: + go-version: '1.20.x' + cache: false + - name: golangci-lint + uses: golangci/golangci-lint-action@v3 + + build: + name: Build + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest, windows-latest, macos-latest] + go-version: [1.20.x] + steps: + + - name: Install Go + uses: actions/setup-go@v4 + with: + go-version: ${{ matrix.go-version }} + + - name: Check out code into the Go module directory + uses: actions/checkout@v3 + + - name: Get dependencies + run: | + go get -v -t -d ./... + + - name: Test + run: go test -v . + + - name: Build + run: go build -v . diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml new file mode 100644 index 0000000..92a2402 --- /dev/null +++ b/.github/workflows/release.yaml @@ -0,0 +1,27 @@ +name: goreleaser +on: + create: + tags: + - 'v*.*.*' + +jobs: + releaser: + name: Release on GitHub + runs-on: ubuntu-latest + steps: + - + name: Checkout + uses: actions/checkout@v3 + - + name: Set up Go + uses: actions/setup-go@v4 + with: + go-version: 1.20.x + - + name: Run GoReleaser + uses: goreleaser/goreleaser-action@v4 + with: + version: latest + args: release --clean + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.goreleaser.yml b/.goreleaser.yml new file mode 100644 index 0000000..c6fa6a7 --- /dev/null +++ b/.goreleaser.yml @@ -0,0 +1,28 @@ +before: + hooks: + - go mod download + - go generate ./... +builds: +- env: + - CGO_ENABLED=0 + goos: + - linux + - windows + - darwin + goarch: + - 386 + - amd64 + - arm + - arm64 +archives: +- format: zip +checksum: + name_template: 'checksums.txt' +snapshot: + name_template: "{{ .Tag }}-next" +changelog: + sort: asc + filters: + exclude: + - '^docs:' + - '^test:' diff --git a/README.md b/README.md new file mode 100644 index 0000000..d8b09a8 --- /dev/null +++ b/README.md @@ -0,0 +1,82 @@ +# sysdoc + +`sysdoc` allows to model system dependencies in a text based manner and process these information in an automated way. + +## Installation + +Head to https://github.com/unprofession-al/sysdoc/releases/latest and grab the release fitting your need. Unpack the archive +and put the `sysdoc` binary somewhere in your `$PATH`. + +## Usage + +### Document the System + +`sysdoc` reads a directory structure and finds files in this structure which are expected to describe your system architecture. +It assumes a hirachical structure (as for example the [C4 Model](https://c4model.com/) suggests) where every folder represents +layer. Each layer (and therefore folder) can contain a Frontmatter file (Markdown with a YAML header, usually a `README.md` file) +do describe the entity of the given layer. + +_Details about the documentation format of system need to be documented here_ + +### Use the Documentation with `sysdoc` + +``` +# sysdoc -h +sysdoc allows to document dependencies between systems + +Usage: + sysdoc [command] + +Available Commands: + completion Generate the autocompletion script for the specified shell + help Help about any command + render renders system documentation in a given template to standard output + serve renders system documentation in a given d2lang template to a svg file and serves it over http + svg renders system documentation in a given d2lang template to a svg file + version Print version info + +Flags: + --base string base directory of the sysdoc definitions (default ".") + --config string configuration file path (default "./sysdoc.yaml") + --focus strings elements to be focussed + --glob string glob to find sysdoc definitions (default "README.md") + -h, --help help for sysdoc + +Use "sysdoc [command] --help" for more information about a command. +``` + +### Writing Renderers + + +_Details about the templating of renderers need to be documented here_ + +## Why `sysdoc`? + +Commonly there are a few ways to document a system architecture: + +__No documentation whatsoever:__ This is bad for apparent reasons. + +__Some wiki pages:__ This is a good and easy approach but requires a decent amount of sustained editorial work to keep things +up to date. Particularly dependencies between systems owned by differend parties seem to be hard to get right and even harder +to keep maintained. Also, no added benefit is generated with an up to date documetation (other than for the documtations sake) +as the data is unstructured and usually hard to access from third party applications. + +__Self-generated documentation:__ In an ideal setting, documentation is generated either by the software itself or by its run +time (kubernetes and some service mesh can do a pretty neat job with this). This should be the goal of a green field project +but seems to be an unrealistic effort for most project that are build ontop of some legacy or around some software that is not +build in-house/consumend as SaaS/of-the-shelve and therefore not exactly built to spec. + +__Specialist tool:__ There are a bunch of [specialist tools](https://content.ardoq.com/en/gartner-magic-quadrant-for-enterprise-architecture-tools) +available to create a proper enterprise architecture documentation. These tools often cost a good amount of money to use and are a bit +overkill in many situations. + +`sysdoc` attempts to provide an low profile, relatively easy and cost efficient way to document your system environment. It is +based on Markdown (with structured data provided as YAML or JSON), which allows you to keep everything neatly under control with +a SCM of your choice. As your system descriptions are provided in simple Markdown, the documentation is already usable as is, as +for example GitHub can render the information in a user consumable way. However, as the meta data of your systems cosist of stuctured +data (in particular the interfaces of a system and the dependencies from systems to interfaces of other systems), more information +can be generated. For example, the following questions can be answered quite easily: + +- A system experiences a downtime. Which other systems are affected? +- A system will be subject of change. What dependencies do we have to take care of? +- ... diff --git a/cli.go b/cli.go new file mode 100644 index 0000000..c83ed93 --- /dev/null +++ b/cli.go @@ -0,0 +1,209 @@ +package main + +import ( + "fmt" + "net/http" + "os" + "strings" + + "github.com/carlmjohnson/versioninfo" + "github.com/spf13/cobra" +) + +type App struct { + flags struct { + configfile string + base string + glob string + focus []string + render struct { + renderer string + } + svg struct { + renderer string + out string + } + serve struct { + renderer string + listener string + } + } + + // entry point + Execute func() error +} + +func NewApp() *App { + a := &App{} + appName := "sysdoc" + + // root + rootCmd := &cobra.Command{ + Use: appName, + Short: "sysdoc allows to document dependencies between systems", + } + rootCmd.PersistentFlags().StringVar(&a.flags.configfile, "config", "./sysdoc.yaml", "configuration file path") + rootCmd.PersistentFlags().StringVar(&a.flags.base, "base", ".", "base directory of the sysdoc definitions") + rootCmd.PersistentFlags().StringVar(&a.flags.glob, "glob", "README.md", "glob to find sysdoc definitions") + rootCmd.PersistentFlags().StringSliceVar(&a.flags.focus, "focus", []string{}, "elements to be focussed") + a.Execute = rootCmd.Execute + + // render + renderCmd := &cobra.Command{ + Use: "render", + Short: "renders system documentation in a given template to standard output", + Run: a.renderCmd, + } + renderCmd.PersistentFlags().StringVar(&a.flags.render.renderer, "renderer", "default", "name of the renederer (set of templates in the configuration file)") + rootCmd.AddCommand(renderCmd) + + // svg + svgCmd := &cobra.Command{ + Use: "svg", + Short: "renders system documentation in a given d2lang template to a svg file", + Run: a.svgCmd, + } + svgCmd.PersistentFlags().StringVar(&a.flags.svg.renderer, "renderer", "default", "name of the renederer (set of templates in the configuration file)") + svgCmd.PersistentFlags().StringVar(&a.flags.svg.out, "out", "sysdoc.svg", "name of the file to be written") + rootCmd.AddCommand(svgCmd) + + // serve + serveCmd := &cobra.Command{ + Use: "serve", + Short: "renders system documentation in a given d2lang template to a svg file and serves it over http", + Long: `With the subcommnad 'serve', a small web server is launched locally. Access the server via +browser and get a rendered SVG picture of your system architecture. The server provides +a simple API to change the focus as well as the renderer on the fly: + +- To change the focus provide a list of elements, where every element is separated with a '+'. +- To switch the renderer provide the 'renderer' query parameter. + +For example, focus on the elements 'A.AB' and 'C' and render the output with a renderer +called 'custom' using the following URL: http://localhost:8080/A.AB+C?renderer=custom`, + Run: a.serveCmd, + } + serveCmd.PersistentFlags().StringVar(&a.flags.serve.listener, "listener", "127.0.0.1:8080", "listener to be used by the http server") + serveCmd.PersistentFlags().StringVar(&a.flags.serve.renderer, "renderer", "serve", "name of the default renderer to be used") + rootCmd.AddCommand(serveCmd) + + // version + versionCmd := &cobra.Command{ + Use: "version", + Short: "Print version info", + Run: a.versionCmd, + } + rootCmd.AddCommand(versionCmd) + + return a +} + +func (a *App) renderCmd(cmd *cobra.Command, args []string) { + cfg, err := NewConfig(a.flags.configfile) + exitOnErr(err) + + // build system + sys, errs := build(a.flags.base, a.flags.glob, a.flags.focus) + exitOnErr(errs...) + + // render template + renderer, ok := cfg.Renderer[a.flags.render.renderer] + if !ok { + exitOnErr(fmt.Errorf("renderer %s not specified in %s", a.flags.render.renderer, a.flags.configfile)) + } + out, err := render(sys, renderer) + exitOnErr(err) + + fmt.Println(out) +} + +func (a *App) svgCmd(cmd *cobra.Command, args []string) { + cfg, err := NewConfig(a.flags.configfile) + exitOnErr(err) + + // build system + sys, errs := build(a.flags.base, a.flags.glob, a.flags.focus) + exitOnErr(errs...) + + // render template + renderer, ok := cfg.Renderer[a.flags.svg.renderer] + if !ok { + exitOnErr(fmt.Errorf("renderer %s not specified in %s", a.flags.svg.renderer, a.flags.configfile)) + } + out, err := render(sys, renderer) + exitOnErr(err) + + // create svg + img, err := svg(out) + exitOnErr(err) + + err = os.WriteFile(a.flags.svg.out, img, 0644) + exitOnErr(err) + + fmt.Printf("file '%s' written...\n", a.flags.svg.out) +} + +func (a *App) serveCmd(cmd *cobra.Command, args []string) { + http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + var focus []string + for _, e := range strings.Split(r.URL.Path, "+") { + focus = append(focus, strings.Trim(e, "/")) + } + + rendererName := r.URL.Query().Get("renderer") + if rendererName == "" { + rendererName = a.flags.serve.renderer + } + + cfg, err := NewConfig(a.flags.configfile) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + _, _ = w.Write([]byte(err.Error())) + return + } + + // build system + sys, errs := build(a.flags.base, a.flags.glob, focus) + if len(errs) > 0 { + w.WriteHeader(http.StatusInternalServerError) + out := "" + for _, err = range errs { + out += fmt.Sprintf("%s\n", err.Error()) + } + _, _ = w.Write([]byte(out)) + return + } + + // render template + renderer, ok := cfg.Renderer[rendererName] + if !ok { + exitOnErr(fmt.Errorf("renderer %s not specified in %s", rendererName, a.flags.configfile)) + } + out, err := render(sys, renderer) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + _, _ = w.Write([]byte(err.Error())) + return + } + + // create svg + img, err := svg(out) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + _, _ = w.Write([]byte(err.Error())) + return + } + + w.Header().Set("Content-Type", "image/svg+xml") + _, _ = w.Write(img) + }) + + fmt.Printf("server listening on http://%s/, hit CTRL-C to stop server...\n", a.flags.serve.listener) + _ = http.ListenAndServe(a.flags.serve.listener, nil) +} + +func (a *App) versionCmd(cmd *cobra.Command, args []string) { + fmt.Println("Version: ", versioninfo.Version) + fmt.Println("Revision: ", versioninfo.Revision) + fmt.Println("DirtyBuild:", versioninfo.DirtyBuild) + fmt.Println("LastCommit:", versioninfo.LastCommit) +} diff --git a/config.go b/config.go new file mode 100644 index 0000000..703052c --- /dev/null +++ b/config.go @@ -0,0 +1,37 @@ +package main + +import ( + "fmt" + "os" + + "github.com/mitchellh/go-homedir" + "gopkg.in/yaml.v3" +) + +type config struct { + Renderer map[string]renderConfig `yaml:"renderers"` +} + +func NewConfig(path string) (config, error) { + c := defaults() + path, err := homedir.Expand(path) + if err != nil { + return c, err + } + + data, err := os.ReadFile(path) + if err != nil { + return c, fmt.Errorf("could not read file %s: %s", path, err.Error()) + } + + err = yaml.Unmarshal(data, &c) + if err != nil { + return c, fmt.Errorf("could not read data from config file %s: %s", path, err.Error()) + } + + return c, nil +} + +func defaults() config { + return config{} +} diff --git a/dependency.go b/dependency.go new file mode 100644 index 0000000..7b6f113 --- /dev/null +++ b/dependency.go @@ -0,0 +1,43 @@ +package main + +import ( + "strings" +) + +type dependencyConfiguration struct { + DependsOn string `yaml:"depends_on" json:"depends_on"` + Description string `yaml:"description" json:"description"` + Tags map[string]string `yaml:"tags" json:"tags"` +} + +type dependency struct { + fragment string + description string + reference string + tags map[string]string + + dependsOn *interf + viaPropagation *interf + belongsTo *element + k bool +} + +func newDependency(fragment string, c dependencyConfiguration, e *element) *dependency { + return &dependency{ + fragment: fragment, + description: c.Description, + reference: c.DependsOn, + tags: c.Tags, + belongsTo: e, + } +} + +func (d *dependency) positionFromReference() []string { + return strings.Split(d.reference, ".") +} + +func (d *dependency) keep() { + d.k = true + d.dependsOn.keep() + d.belongsTo.keep() +} diff --git a/element.go b/element.go new file mode 100644 index 0000000..bb5f932 --- /dev/null +++ b/element.go @@ -0,0 +1,415 @@ +package main + +import ( + "bytes" + "fmt" + "io/fs" + "os" + "path/filepath" + "sort" + "strings" + + "github.com/adrg/frontmatter" +) + +type elementConfiguration struct { + Name string `yaml:"name" json:"name"` + Tags map[string]string `yaml:"tags" json:"tags"` + Dependencies map[string]dependencyConfiguration `yaml:"dependencies" json:"dependencies"` + Interfaces map[string]interfConfiguration `yaml:"interfaces" json:"interfaces"` + Doc []byte `yaml:"-" json:"-"` +} + +func newElementConfigurationFromFile(path string) (elementConfiguration, error) { + ec := elementConfiguration{} + info, err := os.Stat(path) + if err != nil || info.IsDir() { + return ec, nil + } + data, err := os.ReadFile(path) + if err != nil { + err = fmt.Errorf("Could not read file '%s', error occured: %w", path, err) + return ec, err + } + reader := bytes.NewReader(data) + doc, err := frontmatter.Parse(reader, &ec) + if err != nil { + err = fmt.Errorf("Could not parse data of '%s', error occured: %w", path, err) + return ec, err + } + ec.Doc = doc + return ec, err +} + +type element struct { + // configured values + fragment string + name string + tags map[string]string + doc []byte + + // calculated values + dependencies []*dependency + interfaces []*interf + propagations []*interf + children []*element + parent *element + k bool +} + +func newElement(fragment string, c elementConfiguration) *element { + e := &element{ + fragment: fragment, + name: c.Name, + tags: c.Tags, + doc: c.Doc, + } + for key, dep := range c.Dependencies { + e.dependencies = append(e.dependencies, newDependency(key, dep, e)) + } + for key, interf := range c.Interfaces { + e.interfaces = append(e.interfaces, newInterf(key, interf, e)) + } + return e +} + +func positionFromID(id, devider string) []string { + seg := strings.Split(id, devider) + out := []string{} + for _, s := range seg { + if s != "" { + out = append(out, s) + } + } + return out +} + +func getPosition(basepath, path string) []string { + basepath = filepath.Clean(basepath) + path = filepath.Clean(path) + //basepath = filepath.Dir(basepath) + path = strings.TrimPrefix(path, basepath) + path = strings.Trim(path, string(os.PathSeparator)) + pos := strings.Split(path, string(os.PathSeparator)) + if len(pos) == 1 && pos[0] == "" { + return []string{} + } + return pos +} + +func newElementFromFS(basepath, matcher string) (*element, error) { + basepath = filepath.Clean(basepath) + _, err := os.Stat(basepath) + if err != nil { + return nil, err + } + + // read all configuration files + configs := map[string]elementConfiguration{} + err = filepath.WalkDir(basepath, func(path string, dir fs.DirEntry, err error) error { + if err != nil { + return err + } + info, err := os.Stat(path) + if err != nil { + err = fmt.Errorf("Could not stat file '%s', error occured: %w", path, err) + return err + } + if !info.IsDir() { + return nil + } + c, err := newElementConfigurationFromFile(filepath.Join(path, matcher)) + if err != nil { + return err + } + configs[filepath.Clean(path)] = c + return nil + }) + if err != nil { + return nil, err + } + + // generate element tree from configurations + e := &element{} + inited := false + // sort by path + keys := make([]string, 0, len(configs)) + for k := range configs { + keys = append(keys, k) + } + sort.Strings(keys) + // create elements and add to tree + for _, k := range keys { + pos := getPosition(basepath, k) + fragment := "root" + if len(pos) > 0 { + fragment = pos[len(pos)-1] + } + elem := newElement(fragment, configs[k]) + if !inited { + e = elem + inited = true + } else { + err = e.appendAt(elem, pos) + if err != nil { + return e, err + } + } + } + + return e, nil +} + +// appendAt adds a child element on the root element, where the path indicates each fragment of +// the parent elements and the element itself +func (e *element) appendAt(a *element, pos []string) error { + if len(pos) == 1 { + e.children = append(e.children, a) + a.parent = e + return nil + } else if len(pos) > 1 { + for _, child := range e.children { + if child.fragment == pos[0] { + trimmed := pos[1:] + return child.appendAt(a, trimmed) + } + } + } + return fmt.Errorf("Could not append '%s' to '%s'", strings.Join(pos, "/"), strings.Join(e.position(), "/")) +} + +func (e *element) position() []string { + out := []string{e.fragment} + if e.parent != nil { + out = append(e.parent.position(), out...) + } + return out +} + +func (e *element) getID(join string) string { + return strings.Join(e.position()[1:], join) +} + +func (e *element) findElementByPosition(pos []string) (*element, error) { + if len(pos) == 0 { + return e, nil + } + for _, elem := range e.children { + // skip loop if no match + if elem.fragment != pos[0] { + continue + } + // return element if position is onsy one element + if len(pos) == 1 { + return elem, nil + } + // go deeper if more than one element + next, err := elem.findElementByPosition(pos[1:]) + if err != nil { + break + } + return next, nil + + } + return nil, fmt.Errorf("Element '%s' not found", strings.Join(pos, ".")) +} + +func (e *element) findInterfaceByPosition(pos []string) (*interf, error) { + if len(pos) < 1 { + return nil, fmt.Errorf("Position too short") + } + elemPos := pos[:len(pos)-1] + elem, err := e.findElementByPosition(elemPos) + if err != nil { + return nil, fmt.Errorf("Error while finding interface '%s': %w", strings.Join(pos, "."), err) + } + for _, i := range elem.interfaces { + if i.fragment == pos[len(pos)-1] { + return i, nil + } + } + return nil, fmt.Errorf("Element '%s' does not provide interface '%s'", strings.Join(elemPos, "."), strings.Join(pos, ".")) +} + +func (e *element) resolveDependencies(root *element) []error { + errs := []error{} + for _, dep := range e.dependencies { + pos := dep.positionFromReference() + i, err := root.findInterfaceByPosition(pos) + if err != nil { + err = fmt.Errorf("Could not resolve dependency '%s' of element '%s': %w", dep.reference, strings.Join(e.position(), "."), err) + errs = append(errs, err) + } + dep.dependsOn = i + } + for _, child := range e.children { + childErrs := child.resolveDependencies(root) + errs = append(errs, childErrs...) + } + return errs +} + +func (e *element) getDependencies() []*dependency { + out := e.dependencies + for _, elem := range e.children { + out = append(out, elem.getDependencies()...) + } + return out +} + +func (e *element) getPropagations() []*interf { + out := []*interf{} + out = append(out, e.propagations...) + for _, elem := range e.children { + out = append(out, elem.getPropagations()...) + } + return out +} + +func (e *element) propagateInterfaces() error { + for _, dep := range e.dependencies { + i := dep.dependsOn + if i == nil { + return fmt.Errorf("Dependencies of '%s' are not yet resolved", e.getID(".")) + } + sibling := e.closestSibling(i.belongsTo) + if sibling == nil { + return fmt.Errorf("Could not propagate interface '%s' of '%s', no sibling found", i.name, i.belongsTo.getID(".")) + } + // do not propagate to root + if sibling.parent != nil { + prop := i.propagateTo(sibling) + dep.viaPropagation = prop + } else { + dep.viaPropagation = i + } + } + for _, child := range e.children { + err := child.propagateInterfaces() + if err != nil { + return err + } + } + return nil +} + +func (a *element) hasParent(b *element) bool { + if len(b.position()) >= len(a.position()) { + return false + } + if a.parent == b { + return true + } + return a.parent.hasParent(b) +} + +func (a *element) sharedParent(b *element) *element { + // determine lenght of shortest position + shortest, longest := &element{}, &element{} + if len(a.position()) < len(b.position()) { + shortest = a + longest = b + } else { + shortest = b + longest = a + } + if len(shortest.position()) == 0 { + return nil + } + if longest.hasParent(shortest) { + return shortest + } + return longest.sharedParent(shortest.parent) +} + +func (a *element) closestSibling(b *element) *element { + if a.parent == b.parent { + return b + } + sp := a.sharedParent(b) + if sp == b.parent { + return sp + } + for _, e := range sp.children { + if b.hasParent(e) { + return e + } + } + return nil +} + +func (e *element) keep() { + e.k = true + if e.parent != nil { + e.parent.keep() + } +} + +func (root *element) focus(pos []string) error { + elems := []*element{} + for _, p := range pos { + e, err := root.findElementByPosition(positionFromID(p, ".")) + if err != nil { + return err + } + elems = append(elems, e) + } + for _, e := range elems { + root.setKeep(e) + } + root.tidy() + return nil +} + +func (base *element) setKeep(k *element) { + if base == k { + if base.tags == nil { + base.tags = map[string]string{} + } + base.tags["focussed"] = "true" + } + if base == k || base.hasParent(k) { + base.keep() + for _, d := range base.dependencies { + d.keep() + } + for _, i := range base.interfaces { + i.keep() + } + } + for _, d := range base.dependencies { + if d.dependsOn.belongsTo == k || d.dependsOn.belongsTo.hasParent(k) { + d.keep() + } + } + for _, c := range base.children { + c.setKeep(k) + } +} + +func (e *element) tidy() { + dependencies := []*dependency{} + for key := range e.dependencies { + if e.dependencies[key].k { + dependencies = append(dependencies, e.dependencies[key]) + } + } + e.dependencies = dependencies + + interfaces := []*interf{} + for key := range e.interfaces { + if e.interfaces[key].k { + interfaces = append(interfaces, e.interfaces[key]) + } + } + e.interfaces = interfaces + + children := []*element{} + for key := range e.children { + if e.children[key].k { + e.children[key].tidy() + children = append(children, e.children[key]) + } + } + e.children = children +} diff --git a/element_test.go b/element_test.go new file mode 100644 index 0000000..e9788c1 --- /dev/null +++ b/element_test.go @@ -0,0 +1,26 @@ +package main + +import ( + "testing" + "reflect" +) + +func TestPositionFromID(t *testing.T) { + tests := []struct { + input string + sep string + want []string + }{ + {input: "a.b.c", sep: ".", want: []string{"a", "b", "c"}}, + {input: "a.b.c.", sep: ".", want: []string{"a","b","c"}}, + {input: ".a.b.c", sep: ".", want: []string{"a","b","c"}}, + {input: "abc", sep: ".", want: []string{"abc"}}, + {input: "a/b/c", sep: "/", want: []string{"a", "b", "c" }}, + } + for _, tc := range tests { + got := positionFromID(tc.input, tc.sep) + if !reflect.DeepEqual(tc.want, got) { + t.Fatalf("expected: %v, got: %v", tc.want, got) + } + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..f2e8b9e --- /dev/null +++ b/go.mod @@ -0,0 +1,47 @@ +module sysdoc + +go 1.20 + +require ( + github.com/adrg/frontmatter v0.2.0 + github.com/carlmjohnson/versioninfo v0.22.4 + github.com/mitchellh/go-homedir v1.1.0 + github.com/spf13/cobra v1.7.0 + gopkg.in/yaml.v3 v3.0.1 + oss.terrastruct.com/d2 v0.5.1 +) + +require ( + cdr.dev/slog v1.4.2-0.20221206192828-e4803b10ae17 // indirect + github.com/BurntSushi/toml v0.3.1 // indirect + github.com/PuerkitoBio/goquery v1.8.0 // indirect + github.com/alecthomas/chroma v0.10.0 // indirect + github.com/alecthomas/chroma/v2 v2.5.0 // indirect + github.com/andybalholm/cascadia v1.3.1 // indirect + github.com/dlclark/regexp2 v1.8.1 // indirect + github.com/dop251/goja v0.0.0-20230122112309-96b1610dd4f7 // indirect + github.com/fatih/color v1.13.0 // indirect + github.com/go-sourcemap/sourcemap v2.1.3+incompatible // indirect + github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect + github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/lucasb-eyer/go-colorful v1.2.0 // indirect + github.com/mattn/go-colorable v0.1.9 // indirect + github.com/mattn/go-isatty v0.0.14 // indirect + github.com/mazznoer/csscolorparser v0.1.3 // indirect + github.com/rivo/uniseg v0.4.3 // indirect + github.com/spf13/pflag v1.0.5 // indirect + github.com/yuin/goldmark v1.5.3 // indirect + go.opencensus.io v0.24.0 // indirect + golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 // indirect + golang.org/x/exp v0.0.0-20221126150942-6ab00d035af9 // indirect + golang.org/x/image v0.3.0 // indirect + golang.org/x/net v0.8.0 // indirect + golang.org/x/sys v0.6.0 // indirect + golang.org/x/term v0.6.0 // indirect + golang.org/x/text v0.8.0 // indirect + golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect + gonum.org/v1/plot v0.12.0 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + oss.terrastruct.com/util-go v0.0.0-20230604222829-11c3c60fec14 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..33ec76c --- /dev/null +++ b/go.sum @@ -0,0 +1,233 @@ +cdr.dev/slog v1.4.2-0.20221206192828-e4803b10ae17 h1:Jf+VOk2lif79HeTlnLaZ70zYTsuVSUEu/47U9VaG2Rw= +cdr.dev/slog v1.4.2-0.20221206192828-e4803b10ae17/go.mod h1:YPVZsUbRMaLaPgme0RzlPWlC7fI7YmDj/j/kZLuvICs= +cloud.google.com/go v0.26.0 h1:e0WKqKTd5BnrG8aKH3J3h+QvEIQtSUcf2n5UZ5ZgLtQ= +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +git.sr.ht/~sbinet/gg v0.3.1 h1:LNhjNn8DerC8f9DHLz6lS0YYul/b602DUxDgGkd/Aik= +github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/PuerkitoBio/goquery v1.8.0 h1:PJTF7AmFCFKk1N6V6jmKfrNH9tV5pNE6lZMkG0gta/U= +github.com/PuerkitoBio/goquery v1.8.0/go.mod h1:ypIiRMtY7COPGk+I/YbZLbxsxn9g5ejnI2HSMtkjZvI= +github.com/adrg/frontmatter v0.2.0 h1:/DgnNe82o03riBd1S+ZDjd43wAmC6W35q67NHeLkPd4= +github.com/adrg/frontmatter v0.2.0/go.mod h1:93rQCj3z3ZlwyxxpQioRKC1wDLto4aXHrbqIsnH9wmE= +github.com/ajstarks/svgo v0.0.0-20211024235047-1546f124cd8b h1:slYM766cy2nI3BwyRiyQj/Ud48djTMtMebDqepE95rw= +github.com/alecthomas/assert/v2 v2.2.1 h1:XivOgYcduV98QCahG8T5XTezV5bylXe+lBxLG2K2ink= +github.com/alecthomas/chroma v0.10.0 h1:7XDcGkCQopCNKjZHfYrNLraA+M7e0fMiJ/Mfikbfjek= +github.com/alecthomas/chroma v0.10.0/go.mod h1:jtJATyUxlIORhUOFNA9NZDWGAQ8wpxQQqNSB4rjA/1s= +github.com/alecthomas/chroma/v2 v2.5.0 h1:CQCdj1BiBV17sD4Bd32b/Bzuiq/EqoNTrnIhyQAZ+Rk= +github.com/alecthomas/chroma/v2 v2.5.0/go.mod h1:yrkMI9807G1ROx13fhe1v6PN2DDeaR73L3d+1nmYQtw= +github.com/alecthomas/repr v0.2.0 h1:HAzS41CIzNW5syS8Mf9UwXhNH1J9aix/BvDRf1Ml2Yk= +github.com/andybalholm/cascadia v1.3.1 h1:nhxRkql1kdYCc8Snf7D5/D3spOX+dBgjA6u8x004T2c= +github.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEqc0Sk8XGwHqvA= +github.com/carlmjohnson/versioninfo v0.22.4 h1:AucUHDSKmk6j7Yx3dECGUxaowGHOAN0Zx5/EBtsXn4Y= +github.com/carlmjohnson/versioninfo v0.22.4/go.mod h1:QT9mph3wcVfISUKd0i9sZfVrPviHuSF+cUtLjm2WSf8= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= +github.com/dlclark/regexp2 v1.4.1-0.20201116162257-a2a8dda75c91/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= +github.com/dlclark/regexp2 v1.7.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= +github.com/dlclark/regexp2 v1.8.1 h1:6Lcdwya6GjPUNsBct8Lg/yRPwMhABj269AAzdGSiR+0= +github.com/dlclark/regexp2 v1.8.1/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= +github.com/dop251/goja v0.0.0-20211022113120-dc8c55024d06/go.mod h1:R9ET47fwRVRPZnOGvHxxhuZcbrMCuiqOz3Rlrh4KSnk= +github.com/dop251/goja v0.0.0-20230122112309-96b1610dd4f7 h1:kgvzE5wLsLa7XKfV85VZl40QXaMCaeFtHpPwJ8fhotY= +github.com/dop251/goja v0.0.0-20230122112309-96b1610dd4f7/go.mod h1:yRkwfj0CBpOGre+TwBsqPV0IH0Pk73e4PXJOeNDboGs= +github.com/dop251/goja_nodejs v0.0.0-20210225215109-d91c329300e7/go.mod h1:hn7BA7c8pLvoGndExHudxTDKZ84Pyvv+90pbBjbTz0Y= +github.com/dop251/goja_nodejs v0.0.0-20211022123610-8dd9abb0616d/go.mod h1:DngW8aVqWbuLRMHItjPUyqdj+HWPvnQe8V8y1nDpIbM= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= +github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= +github.com/go-fonts/liberation v0.2.0 h1:jAkAWJP4S+OsrPLZM4/eC9iW7CtHy+HBXrEwZXWo5VM= +github.com/go-latex/latex v0.0.0-20210823091927-c0d11ff05a81 h1:6zl3BbBhdnMkpSj2YY30qV3gDcVBGtFgVsV3+/i+mKQ= +github.com/go-pdf/fpdf v0.6.0 h1:MlgtGIfsdMEEQJr2le6b/HNr1ZlQwxyWr77r2aj2U/8= +github.com/go-sourcemap/sourcemap v2.1.3+incompatible h1:W1iEw64niKVGogNgBN3ePyLFfuisuzeidWPMPWmECqU= +github.com/go-sourcemap/sourcemap v2.1.3+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg= +github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g= +github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.3 h1:JjCZWpVbqXDqFVmTfYWEVTMIYrL/NPdPSCHPJ0T/raM= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= +github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= +github.com/mattn/go-colorable v0.1.9 h1:sqDoxXbdeALODt0DAeJCVp38ps9ZogZEAXjus69YV3U= +github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/mazznoer/csscolorparser v0.1.3 h1:vug4zh6loQxAUxfU1DZEu70gTPufDPspamZlHAkKcxE= +github.com/mazznoer/csscolorparser v0.1.3/go.mod h1:Aj22+L/rYN/Y6bj3bYqO3N6g1dtdHtGfQ32xZ5PJQic= +github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/rivo/uniseg v0.4.3 h1:utMvzDsuh3suAEnhH0RdHmoPbU648o6CvXxTx4SBMOw= +github.com/rivo/uniseg v0.4.3/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k= +github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I= +github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +github.com/yuin/goldmark v1.5.3 h1:3HUJmBFbQW9fhQOzMgseU134xfi6hU+mjWywx5Ty+/M= +github.com/yuin/goldmark v1.5.3/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= +go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= +go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= +go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 h1:7I4JAnoQBe7ZtJcBaYHi5UtiO8tQHbUSXxL+pnGRANg= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20221126150942-6ab00d035af9 h1:yZNXmy+j/JpX19vZkVktWqAo7Gny4PBWYYK3zskGpx4= +golang.org/x/exp v0.0.0-20221126150942-6ab00d035af9/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= +golang.org/x/image v0.3.0 h1:HTDXbdK9bjfSWkPzDJIw89W8CAtfFGduujWs33NLLsg= +golang.org/x/image v0.3.0/go.mod h1:fXd9211C/0VTlYuAcOhW8dY/RtEJqODXOWBDpmYBf+A= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210916014120-12bc252f5db8/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ= +golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.6.0 h1:clScbb1cHjoCkyRbWwBEUZ5H/tIFu5TAXIqaZD0Gcjw= +golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68= +golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 h1:H2TDz8ibqkAF6YGhCdN3jS9O0/s90v0rJh3X/OLHEUk= +golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= +gonum.org/v1/plot v0.12.0 h1:y1ZNmfz/xHuHvtgFe8USZVyykQo5ERXPnspQNVK15Og= +gonum.org/v1/plot v0.12.0/go.mod h1:PgiMf9+3A3PnZdJIciIXmyN1FwdAA6rXELSN761oQkw= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013 h1:+kGHl1aib/qcwaRi1CbqBZ1rk19r85MNUf8HaBghugY= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.33.2 h1:EQyQC3sa8M+p6Ulc8yy9SWSS2GVwyRc83gAbG8lrl4o= +google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.25.0 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +oss.terrastruct.com/d2 v0.5.1 h1:w4N2yfV0s3mSJOgSlFGG1wxfVz5x5NIkqWLWxjf524w= +oss.terrastruct.com/d2 v0.5.1/go.mod h1:ZyzsiefzsZ3w/BDnfF/hcDx9LKBlgieuolX8pXi7oJY= +oss.terrastruct.com/util-go v0.0.0-20230604222829-11c3c60fec14 h1:oy5vtt6O2qYxeSpqWhyevrdUenFfuhphixozUlpL6qY= +oss.terrastruct.com/util-go v0.0.0-20230604222829-11c3c60fec14/go.mod h1:eMWv0sOtD9T2RUl90DLWfuShZCYp4NrsqNpI8eqO6U4= +rsc.io/pdf v0.1.1 h1:k1MczvYDUvJBe93bYd7wrZLLUEcLZAuF824/I4e5Xr4= diff --git a/interface.go b/interface.go new file mode 100644 index 0000000..16cf1bc --- /dev/null +++ b/interface.go @@ -0,0 +1,73 @@ +package main + +import ( + "fmt" + "strings" +) + +type interfConfiguration struct { + Name string `yaml:"name" json:"name"` + Description string `yaml:"description" json:"description"` + Tags map[string]string `yaml:"tags" json:"tags"` +} + +type interf struct { + fragment string + name string + description string + tags map[string]string + + belongsTo *element + propagates *interf + k bool +} + +func newInterf(fragment string, c interfConfiguration, e *element) *interf { + return &interf{ + fragment: fragment, + name: c.Name, + description: c.Description, + tags: c.Tags, + belongsTo: e, + } +} + +func (i *interf) propagateTo(e *element) *interf { + if i.belongsTo == e { + return i + } + parent := i.belongsTo.parent + if parent == nil { + return nil + } + prop := &interf{ + fragment: fmt.Sprintf("%s-%s", i.fragment, i.belongsTo.fragment), + name: fmt.Sprintf("%s (propagated from %s)", i.name, i.belongsTo.name), + description: i.description, + belongsTo: parent, + propagates: i, + } + exists := false + for _, p := range parent.propagations { + if p.fragment == prop.fragment { + prop = p + exists = true + } + } + if !exists { + parent.propagations = append(parent.propagations, prop) + } + if e == parent { + return prop + } + return prop.propagateTo(e) +} + +func (i *interf) getID(join string) string { + return strings.Join(append([]string{i.belongsTo.getID(join)}, i.fragment), join) +} + +func (i *interf) keep() { + i.k = true + i.belongsTo.keep() +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..b086035 --- /dev/null +++ b/main.go @@ -0,0 +1,80 @@ +package main + +import ( + "context" + "fmt" + "os" + + "oss.terrastruct.com/d2/d2graph" + "oss.terrastruct.com/d2/d2layouts/d2elklayout" + "oss.terrastruct.com/d2/d2lib" + "oss.terrastruct.com/d2/d2renderers/d2svg" + "oss.terrastruct.com/d2/d2themes/d2themescatalog" + "oss.terrastruct.com/d2/lib/textmeasure" +) + +func main() { + err := NewApp().Execute() + exitOnErr(err) +} + +func build(basedir, glob string, focus []string) (*element, []error) { + sys, err := newElementFromFS(basedir, glob) + if err != nil { + return sys, []error{err} + } + + errs := sys.resolveDependencies(sys) + if err != nil { + return sys, errs + } + + if len(focus) > 0 { + err = sys.focus(focus) + if err != nil { + return sys, []error{err} + } + } + + err = sys.propagateInterfaces() + if err != nil { + return sys, []error{err} + } + return sys, nil +} + +func svg(code string) ([]byte, error) { + ruler, err := textmeasure.NewRuler() + if err != nil { + return []byte{}, err + } + defaultLayout := func(ctx context.Context, g *d2graph.Graph) error { + return d2elklayout.Layout(ctx, g, nil) + } + diagram, _, err := d2lib.Compile(context.Background(), code, &d2lib.CompileOptions{ + Layout: defaultLayout, + Ruler: ruler, + }) + if err != nil { + return []byte{}, err + } + return d2svg.Render(diagram, &d2svg.RenderOpts{ + Pad: d2svg.DEFAULT_PADDING, + ThemeID: d2themescatalog.GrapeSoda.ID, + }) +} + +func exitOnErr(errs ...error) { + errNotNil := false + for _, err := range errs { + if err == nil { + continue + } + errNotNil = true + fmt.Fprintf(os.Stderr, "ERROR: %s\n", err.Error()) + } + if errNotNil { + fmt.Print("\n") + os.Exit(-1) + } +} diff --git a/render.go b/render.go new file mode 100644 index 0000000..7f7aea9 --- /dev/null +++ b/render.go @@ -0,0 +1,157 @@ +package main + +import ( + "bytes" + "text/template" +) + +// ELEMENT + +type ElementTemplateData struct { + data *element + templ string +} + +func newElementTemplateData(e *element, t string) ElementTemplateData { + return ElementTemplateData{data: e, templ: t} +} +func (e ElementTemplateData) ID(sep string) string { return e.data.getID(sep) } +func (e ElementTemplateData) Fragment() string { return e.data.fragment } +func (e ElementTemplateData) Name() string { return e.data.name } +func (e ElementTemplateData) Tags() map[string]string { return e.data.tags } +func (e ElementTemplateData) Doc() string { return string(e.data.doc) } +func (e ElementTemplateData) Children() []string { + out := []string{} + for _, child := range e.data.children { + er, _ := newElementTemplateData(child, e.templ).render() + out = append(out, er) + } + return out +} +func (e ElementTemplateData) Interfaces() []InterfaceTemplateData { + out := []InterfaceTemplateData{} + for _, i := range e.data.interfaces { + out = append(out, newInterfaceTemplateData(i, "")) + } + return out +} +func (e ElementTemplateData) Propagations() []InterfaceTemplateData { + out := []InterfaceTemplateData{} + for _, i := range e.data.propagations { + out = append(out, newInterfaceTemplateData(i, "")) + } + return out +} +func (e ElementTemplateData) render() (string, error) { + var b bytes.Buffer + t, err := template.New("tmpl").Parse(e.templ) + if err != nil { + return "", err + } + err = t.Execute(&b, e) + return b.String(), err +} + +// INTERFACE + +type InterfaceTemplateData struct { + data *interf + templ string +} + +func newInterfaceTemplateData(i *interf, t string) InterfaceTemplateData { + return InterfaceTemplateData{data: i, templ: t} +} +func (i InterfaceTemplateData) Name() string { return i.data.name } +func (i InterfaceTemplateData) Fragment() string { return i.data.fragment } +func (i InterfaceTemplateData) ID(sep string) string { return i.data.getID(sep) } +func (i InterfaceTemplateData) PropagatesID(sep string) string { return i.data.propagates.getID(sep) } +func (i InterfaceTemplateData) Tags() map[string]string { return i.data.tags } +func (i InterfaceTemplateData) render() (string, error) { + var b bytes.Buffer + t, err := template.New("tmpl").Parse(i.templ) + if err != nil { + return "", err + } + err = t.Execute(&b, i) + return b.String(), err +} + +// DEPENDENCY + +type DependencyTemplateData struct { + data *dependency + templ string +} + +func newDependencyTemplateData(d *dependency, t string) DependencyTemplateData { + return DependencyTemplateData{data: d, templ: t} +} +func (d DependencyTemplateData) render() (string, error) { + var b bytes.Buffer + t, err := template.New("tmpl").Parse(d.templ) + if err != nil { + return "", err + } + err = t.Execute(&b, d) + return b.String(), err +} +func (d DependencyTemplateData) Fragment() string { return d.data.fragment } +func (d DependencyTemplateData) Description() string { return d.data.description } +func (d DependencyTemplateData) Tags() map[string]string { return d.data.tags } +func (d DependencyTemplateData) BelongsToID(sep string) string { return d.data.belongsTo.getID(sep) } +func (d DependencyTemplateData) DependsOnID(sep string) string { return d.data.dependsOn.getID(sep) } +func (d DependencyTemplateData) ViaPropagation(sep string) string { + return d.data.viaPropagation.getID(sep) +} + +// RENDER + +func render(e *element, rc renderConfig) (string, error) { + data := struct { + Elements string + Dependencies string + Propagations string + }{} + + er, err := newElementTemplateData(e, rc.Templates.Element).render() + if err != nil { + return "", err + } + data.Elements += er + + for _, dep := range e.getDependencies() { + dr, err := newDependencyTemplateData(dep, rc.Templates.Dependency).render() + if err != nil { + return "", err + } + data.Dependencies += dr + } + + for _, prop := range e.getPropagations() { + pr, err := newInterfaceTemplateData(prop, rc.Templates.Propagation).render() + if err != nil { + return "", err + } + data.Propagations += pr + } + + var b bytes.Buffer + t, err := template.New("tmpl").Parse(rc.Templates.Global) + if err != nil { + return "", err + } + + err = t.Execute(&b, data) + return b.String(), err +} + +type renderConfig struct { + ChildIndent string `yaml:"child_indent"` + Templates struct { + Element string `yaml:"element"` + Dependency string `yaml:"dependency"` + Propagation string `yaml:"propagation"` + Global string `yaml:"global"` + } `yaml:"templates"` +}