From c3c51a09e3e3be750f05f4fbe8d1084b7805c6a0 Mon Sep 17 00:00:00 2001 From: Krzysztof Reczek Date: Thu, 19 Nov 2020 21:49:39 +0100 Subject: [PATCH] Add YAML configuration (#5) --- README.md | 54 ++++++++++++++++++++++++++++++++++-- go.mod | 5 +++- go.sum | 3 ++ pkg/scraper/config.go | 1 - pkg/scraper/scraper.go | 25 +++++++++++++++-- pkg/scraper/yaml.go | 40 +++++++++++++++++++++++++++ pkg/view/snippets.go | 10 +++---- pkg/view/view.go | 23 +++++++++++++++- pkg/view/yaml.go | 62 ++++++++++++++++++++++++++++++++++++++++++ pkg/yaml/yaml.go | 61 +++++++++++++++++++++++++++++++++++++++++ 10 files changed, 271 insertions(+), 13 deletions(-) create mode 100644 pkg/scraper/yaml.go create mode 100644 pkg/view/yaml.go create mode 100644 pkg/yaml/yaml.go diff --git a/README.md b/README.md index 3222a8f..db5e50c 100644 --- a/README.md +++ b/README.md @@ -25,10 +25,14 @@ type Info struct { ### Scraper +Scraper may be instantiated in one of two ways: +* from the code +* from the YAML file + In order to instantiate the scraper you need to provide scraper configuration which contains a slice of prefixes of packages that you want to reflect. Types that do not match any of the given prefixes will not be traversed. ```go config := scraper.NewConfiguration( - "github.com/karhoo/svc-billing", + "github.com/krzysztofreczek/pkg", ) s := scraper.NewScraper(config) ``` @@ -41,7 +45,7 @@ Each rule consists of: * Apply function - function that produces `model.Info` describing the component included in the scraped structure ```go -r, _ := scraper.NewRule(). +r, err := scraper.NewRule(). WithPkgRegexps("github.com/krzysztofreczek/pkg/foo/.*"). WithNameRegexp("^(.*)Client$"). WithApplyFunc( @@ -49,7 +53,29 @@ r, _ := scraper.NewRule(). return model.ComponentInfo("foo client", "client of a foo service", "TAG") }). Build() -_ = s.RegisterRule(r) +err = s.RegisterRule(r) +``` + +Alternatively, you can instantiate the scraper form YAML configuration file: +```yaml +// go-structurizr.yml +configuration: + pkgs: + - "github.com/krzysztofreczek/pkg" + +rules: + - name_regexp: "^(.*)Client$" + pkg_regexps: + - "github.com/krzysztofreczek/pkg/foo/.*" + component: + description: "foo client" + technology: "client of a foo service" + tags: + - TAG +``` + +```go +s, err := scraper.NewScraperFromConfigFile("./go-structurizr.yml") ``` Eventually, having the scraper instantiated and configured you can use it to scrape any structure you want. Scraper returns a struct `model.Structure`. @@ -59,6 +85,10 @@ structure := s.Scrap(app) ### View +Similarly to the scraper, view may be instantiated in one of two ways: +* from the code +* from the YAML file + In order to render scraped structure, you will need to instantiate and configure a view. View consists of: * title @@ -73,6 +103,7 @@ v := view.NewView().Build() In case you need to customize it, use available builder methods: ```go v := view.NewView(). + WithTitle("Title") WithComponentStyle( view.NewComponentStyle("TAG"). WithBackgroundColor(color.White). @@ -82,6 +113,23 @@ v := view.NewView(). Build() ``` +Alternatively, you can instantiate the view form YAML configuration file: +```yaml +// go-structurizr.yml +view: + title: "Title" + line_color: 000000ff + styles: + - id: TAG + background_color: ffffffff + font_color: 000000ff + border_color: 000000ff +``` + +```go +v, err := view.NewViewFromConfigFile("./go-structurizr.yml") +``` + As the view is initialized, you can now render the structure into planUML diagram. ```go outFile, _ := os.Create("c4.plantuml") diff --git a/go.mod b/go.mod index 7a9d701..c94da99 100644 --- a/go.mod +++ b/go.mod @@ -2,4 +2,7 @@ module github.com/krzysztofreczek/go-structurizr go 1.15 -require github.com/pkg/errors v0.9.1 +require ( + github.com/pkg/errors v0.9.1 + gopkg.in/yaml.v2 v2.3.0 +) diff --git a/go.sum b/go.sum index 7c401c3..81025fa 100644 --- a/go.sum +++ b/go.sum @@ -1,2 +1,5 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/pkg/scraper/config.go b/pkg/scraper/config.go index b3951e7..dafe32f 100644 --- a/pkg/scraper/config.go +++ b/pkg/scraper/config.go @@ -9,4 +9,3 @@ func NewConfiguration(packages ...string) Configuration { packages: packages, } } - diff --git a/pkg/scraper/scraper.go b/pkg/scraper/scraper.go index 106d42e..ba938a5 100644 --- a/pkg/scraper/scraper.go +++ b/pkg/scraper/scraper.go @@ -1,7 +1,6 @@ package scraper import ( - "errors" "fmt" "hash/fnv" "reflect" @@ -9,6 +8,8 @@ import ( "unsafe" "github.com/krzysztofreczek/go-structurizr/pkg/model" + "github.com/krzysztofreczek/go-structurizr/pkg/yaml" + "github.com/pkg/errors" ) type Scraper struct { @@ -25,6 +26,27 @@ func NewScraper(config Configuration) *Scraper { } } +func NewScraperFromConfigFile(fileName string) (*Scraper, error) { + configuration, err := yaml.LoadFromFile(fileName) + if err != nil { + return nil, errors.Wrapf(err, + "could not load configuration from file `%s`", fileName) + } + + config := toScraperConfig(configuration) + rules, err := toScraperRules(configuration) + if err != nil { + return nil, errors.Wrapf(err, + "could not load scraper rules from from configuration file `%s`", fileName) + } + + return &Scraper{ + config: config, + rules: rules, + structure: model.NewStructure(), + }, nil +} + func (s *Scraper) RegisterRule(r Rule) error { if r == nil { return errors.New("rule must not be nil") @@ -67,7 +89,6 @@ func (s *Scraper) scrap( for i := 0; i < v.Len(); i++ { s.scrap(v.Index(i), parentID, level) } - return } v = normalize(v) diff --git a/pkg/scraper/yaml.go b/pkg/scraper/yaml.go new file mode 100644 index 0000000..22d4814 --- /dev/null +++ b/pkg/scraper/yaml.go @@ -0,0 +1,40 @@ +package scraper + +import ( + "github.com/krzysztofreczek/go-structurizr/pkg/model" + "github.com/krzysztofreczek/go-structurizr/pkg/yaml" +) + +func toScraperConfig(c yaml.Config) Configuration { + return NewConfiguration(c.Configuration.Packages...) +} + +func toScraperRules(c yaml.Config) ([]Rule, error) { + rules := make([]Rule, len(c.Rules)) + for i, r := range c.Rules { + r := r + rule, err := NewRule(). + WithNameRegexp(r.NameRegexp). + WithPkgRegexps(r.PackageRegexps...). + WithApplyFunc( + func() model.Info { + info := make([]string, len(r.Component.Tags)+2) + info[0] = r.Component.Description + info[1] = r.Component.Technology + + idx := 2 + for _, t := range r.Component.Tags { + info[idx] = t + idx++ + } + + return model.ComponentInfo(info...) + }, + ).Build() + if err != nil { + return nil, err + } + rules[i] = rule + } + return rules, nil +} diff --git a/pkg/view/snippets.go b/pkg/view/snippets.go index db3c2d1..728d014 100644 --- a/pkg/view/snippets.go +++ b/pkg/view/snippets.go @@ -82,9 +82,9 @@ func buildSkinParamRectangle( ) string { s := snippetSkinParamRectangle s = strings.Replace(s, paramRectangleName, name, -1) - s = strings.Replace(s, paramBackgroundColor, hex(backgroundColor), -1) - s = strings.Replace(s, paramFontColor, hex(fontColor), -1) - s = strings.Replace(s, paramBorderColor, hex(borderColor), -1) + s = strings.Replace(s, paramBackgroundColor, toHex(backgroundColor), -1) + s = strings.Replace(s, paramFontColor, toHex(fontColor), -1) + s = strings.Replace(s, paramBorderColor, toHex(borderColor), -1) return s } @@ -118,11 +118,11 @@ func buildComponentConnection( s := snippetComponentConnection s = strings.Replace(s, paramComponentIDFrom, fromID, -1) s = strings.Replace(s, paramComponentIDTo, toID, -1) - s = strings.Replace(s, paramLineColor, hex(lineColor), -1) + s = strings.Replace(s, paramLineColor, toHex(lineColor), -1) return s } -func hex(c color.Color) string { +func toHex(c color.Color) string { rgba := color.RGBAModel.Convert(c).(color.RGBA) return fmt.Sprintf("#%.2x%.2x%.2x", rgba.R, rgba.G, rgba.B) } diff --git a/pkg/view/view.go b/pkg/view/view.go index 35ef5ce..2f002a2 100644 --- a/pkg/view/view.go +++ b/pkg/view/view.go @@ -1,6 +1,11 @@ package view -import "image/color" +import ( + "image/color" + + "github.com/krzysztofreczek/go-structurizr/pkg/yaml" + "github.com/pkg/errors" +) type View struct { title string @@ -34,6 +39,22 @@ func NewView() *Builder { } } +func NewViewFromConfigFile(fileName string) (View, error) { + configuration, err := yaml.LoadFromFile(fileName) + if err != nil { + return View{}, errors.Wrapf(err, + "could not load configuration from file `%s`", fileName) + } + + v, err := toView(configuration) + if err != nil { + return View{}, errors.Wrapf(err, + "could not load view from file `%s`", fileName) + } + + return v, nil +} + func (b *Builder) WithTitle(t string) *Builder { b.title = t return b diff --git a/pkg/view/yaml.go b/pkg/view/yaml.go new file mode 100644 index 0000000..0136ad2 --- /dev/null +++ b/pkg/view/yaml.go @@ -0,0 +1,62 @@ +package view + +import ( + "encoding/hex" + "image/color" + "log" + + "github.com/krzysztofreczek/go-structurizr/pkg/yaml" +) + +func toView(c yaml.Config) (View, error) { + v := NewView().WithTitle(c.View.Title) + + if c.View.LineColor != "" { + col, err := decodeHexColor(c.View.LineColor) + if err != nil { + return View{}, err + } + v.WithLineColor(col) + } + + for _, s := range c.View.Styles { + style := NewComponentStyle(s.ID) + + if s.BackgroundColor != "" { + col, err := decodeHexColor(s.BackgroundColor) + if err != nil { + return View{}, err + } + style.WithBackgroundColor(col) + } + + if s.FontColor != "" { + col, err := decodeHexColor(s.FontColor) + if err != nil { + return View{}, err + } + style.WithFontColor(col) + } + + if s.BorderColor != "" { + col, err := decodeHexColor(s.BorderColor) + if err != nil { + return View{}, err + } + style.WithBorderColor(col) + } + + v.WithComponentStyle(style.Build()) + } + + return v.Build(), nil +} + +func decodeHexColor(s string) (color.Color, error) { + b, err := hex.DecodeString(s) + if err != nil { + log.Fatal(err) + } + + return color.RGBA{R: b[0], G: b[1], B: b[2], A: 255}, nil +} diff --git a/pkg/yaml/yaml.go b/pkg/yaml/yaml.go new file mode 100644 index 0000000..a04d550 --- /dev/null +++ b/pkg/yaml/yaml.go @@ -0,0 +1,61 @@ +package yaml + +import ( + "os" + + "gopkg.in/yaml.v2" +) + +type Config struct { + Configuration ConfigConfiguration `yaml:"configuration"` + Rules []ConfigRule `yaml:"rules"` + View ConfigView `yaml:"view"` +} + +type ConfigConfiguration struct { + Packages []string `yaml:"pkgs"` +} + +type ConfigRule struct { + PackageRegexps []string `yaml:"pkg_regexps"` + NameRegexp string `yaml:"name_regexp"` + Component ConfigRuleComponent `yaml:"component"` +} + +type ConfigRuleComponent struct { + Description string `yaml:"description"` + Technology string `yaml:"technology"` + Tags []string `yaml:"tags"` +} + +type ConfigView struct { + Title string `yaml:"title"` + LineColor string `yaml:"line_color"` + Styles []ConfigViewStyle `yaml:"styles"` +} + +type ConfigViewStyle struct { + ID string `yaml:"id"` + BackgroundColor string `yaml:"background_color"` + FontColor string `yaml:"font_color"` + BorderColor string `yaml:"border_color"` +} + +func LoadFromFile(fileName string) (Config, error) { + f, err := os.Open(fileName) + if err != nil { + return Config{}, err + } + defer func() { + _ = f.Close() + }() + + var cfg Config + decoder := yaml.NewDecoder(f) + err = decoder.Decode(&cfg) + if err != nil { + return Config{}, err + } + + return cfg, nil +}