From eceec4e634ce4747f48ad136d3bb23f76716eb3d Mon Sep 17 00:00:00 2001 From: Krzysztof Reczek Date: Wed, 2 Dec 2020 22:27:29 +0100 Subject: [PATCH] Add goDoc documentation (#16) --- README.md | 18 +++--- pkg/model/info.go | 18 ++++++ pkg/model/structure.go | 17 +++++ pkg/scraper/config.go | 13 +++- pkg/scraper/rule.go | 83 +++++++++++++++++++++--- pkg/scraper/scraper.go | 43 ++++++++++--- pkg/scraper/scraper_test.go | 8 +-- pkg/scraper/strategy.go | 26 ++++---- pkg/scraper/yaml.go | 3 +- pkg/scraper/yaml_test.go | 2 +- pkg/view/render.go | 19 +++--- pkg/view/view.go | 122 +++++++++++++++++++++++++++++------- pkg/view/view_test.go | 25 ++++---- pkg/view/yaml.go | 8 +-- pkg/view/yaml_test.go | 4 +- pkg/yaml/yaml.go | 11 ++++ 16 files changed, 324 insertions(+), 96 deletions(-) diff --git a/README.md b/README.md index c17b2be..69d81b8 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,7 @@ Scraper may be instantiated in one of two ways: 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/krzysztofreczek/pkg", + "github.com/org/pkg", ) s := scraper.NewScraper(config) ``` @@ -47,7 +47,7 @@ Each rule consists of: ```go r, err := scraper.NewRule(). - WithPkgRegexps("github.com/krzysztofreczek/pkg/foo/.*"). + WithPkgRegexps("github.com/org/pkg/foo/.*"). WithNameRegexp("^(.*)Client$"). WithApplyFunc( func(name string, _ ...string) model.Info { @@ -62,7 +62,7 @@ The apply function has two arguments: name and groups matched from the name regu See the example: ```go r, err := scraper.NewRule(). - WithPkgRegexps("github.com/krzysztofreczek/pkg/foo/.*"). + WithPkgRegexps("github.com/org/pkg/foo/.*"). WithNameRegexp(`^(\w*)\.(\w*)Client$`). WithApplyFunc( func(_ string, groups ...string) model.Info { @@ -79,12 +79,12 @@ Alternatively, you can instantiate the scraper form YAML configuration file: // go-structurizr.yml configuration: pkgs: - - "github.com/krzysztofreczek/pkg" + - "github.com/org/pkg" rules: - name_regexp: "^(.*)Client$" pkg_regexps: - - "github.com/krzysztofreczek/pkg/foo/.*" + - "github.com/org/pkg/foo/.*" component: description: "foo client" technology: "gRPC" @@ -97,7 +97,7 @@ Regex groups may also be used within yaml rule definition. Here you can find an rules: - name_regexp: "(\\w*)\\.(\\w*)Client$" pkg_regexps: - - "github.com/krzysztofreczek/pkg/foo/.*" + - "github.com/org/pkg/foo/.*" component: name: "Client of external {1} service" description: "foo client" @@ -112,12 +112,12 @@ 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`. ```go -structure := s.Scrap(app) +structure := s.Scrape(app) ``` ### View -Similarly to the scraper, view may be instantiated in one of two ways: +Similarly, to the scraper, view may be instantiated in one of two ways: * from the code * from the YAML file @@ -173,7 +173,7 @@ defer func() { _ = outFile.Close() }() -err = v.RenderTo(structure, outFile) +err = v.RenderStructureTo(structure, outFile) ``` ## Good practices diff --git a/pkg/model/info.go b/pkg/model/info.go index 0888113..fcb3d29 100644 --- a/pkg/model/info.go +++ b/pkg/model/info.go @@ -1,5 +1,11 @@ package model +// HasInfo wraps simple getter method returning component information. +// +// HasInfo interface informs that the type is able to provide component +// information on its own. +// All the types that implement the interface are automatically detected +// by default implementation of the scraper. type HasInfo interface { Info() Info } @@ -8,6 +14,14 @@ const ( infoKindComponent = "component" ) +// Info struct contains all component information details. +// +// Name is a component name. +// Kind is a type that reflects component level in terms of C4 diagrams. +// Description explains the responsibility of the component. +// Technology describes technology that the component is based on. +// Tags is a set of generic string tags that may be used as reference +// to a group of components. type Info struct { Name string Kind string @@ -16,6 +30,8 @@ type Info struct { Tags []string } +// ComponentInfo instantiates a new component of predefined kind "component". +// Variadic arguments are assigned to the rest of Info properties one-by-one. func ComponentInfo(s ...string) Info { return info(infoKindComponent, s...) } @@ -47,6 +63,8 @@ func info(kind string, s ...string) Info { return info } +// IsZero informs if the component is empty. +// If component has no kind specified it is considered as empty. func (i Info) IsZero() bool { return i.Kind == "" } diff --git a/pkg/model/structure.go b/pkg/model/structure.go index 439e32b..31d77de 100644 --- a/pkg/model/structure.go +++ b/pkg/model/structure.go @@ -1,5 +1,14 @@ package model +// Component is an open structure that represents details of scraped component. +// +// ID is a unique identifier of the component. +// Kind is a type that reflects component level in terms of C4 diagrams. +// Name is a component name. +// Description explains the responsibility of the component. +// Technology describes technology that the component is based on. +// Tags is a set of generic string tags that may be used as reference +// to a group of components. type Component struct { ID string Kind string @@ -9,11 +18,16 @@ type Component struct { Tags []string } +// Structure is an open stricture that represents whole scraped structure. +// +// Components contains all the scraped components by its IDs. +// Relations contains all the connections between components by its IDs. type Structure struct { Components map[string]Component Relations map[string]map[string]struct{} } +// NewStructure instantiates an empty structure. func NewStructure() Structure { return Structure{ Components: make(map[string]Component), @@ -21,6 +35,9 @@ func NewStructure() Structure { } } +// AddComponent adds component and corresponding relation to its parent. +// +// In case a parent of given ID does not exist relation will not be created. func (s Structure) AddComponent(c Component, parentID string) { s.Components[c.ID] = c if parentID != "" { diff --git a/pkg/scraper/config.go b/pkg/scraper/config.go index dafe32f..bb14d38 100644 --- a/pkg/scraper/config.go +++ b/pkg/scraper/config.go @@ -1,11 +1,20 @@ package scraper +// Configuration is an open structure that contains scraper configuration. +// +// Packages contain prefixes of packages for scraper to scrape. +// Each object of package that does not match any of predefined +// package prefixes is omitted and its internal structure is not scraped. +// When no package prefix is provided, the scraper will stop +// scraping given structure on a root level. type Configuration struct { - packages []string + Packages []string } +// NewConfiguration instantiates Configuration with a set of package +// prefixes provided with variadic argument. func NewConfiguration(packages ...string) Configuration { return Configuration{ - packages: packages, + Packages: packages, } } diff --git a/pkg/scraper/rule.go b/pkg/scraper/rule.go index 13aa68d..24425cc 100644 --- a/pkg/scraper/rule.go +++ b/pkg/scraper/rule.go @@ -11,11 +11,24 @@ var ( matchAllRegexp = regexp.MustCompile("^.*$") ) +// RuleApplyFunc defines a signature of method returning +// a component information of type model.Info. +// +// Arguments: +// - name is a scraped name of the type in format `package.TypeName` +// - groups is a slice of sub-groups resolved from the rule name +// regular expression type RuleApplyFunc func( name string, groups ...string, ) model.Info +// Rule defines an interface of any rule that maybe registered within scraper. +// +// Applies informs if rule should be applied to the given component +// considering its full package name and type name in format `package.TypeName`. +// Apply returns a component information of type model.Info based +// on the type name in format `package.TypeName`. type Rule interface { Applies( pkg string, @@ -70,6 +83,13 @@ func newRule( }, nil } +// Applies informs if rule should be applied to the given component +// considering its full package name and type name in format `package.TypeName`. +// +// Component will be recognised as applicable when all of the following +// conditions are met: +// - package matches at least one of the rule package regular expressions +// - name matches the rule name regular expression func (r rule) Applies( pkg string, name string, @@ -77,6 +97,13 @@ func (r rule) Applies( return r.nameApplies(name) && r.pkgApplies(pkg) } +// Apply returns a component information of type model.Info based +// on the type name in format `package.TypeName`. +// Apply will return result of registered RuleApplyFunc application function +// passing the following arguments: +// - name is a scraped name of the type in format `package.TypeName` +// - groups is a slice of sub-groups resolved from the rule name +// regular expression func (r rule) Apply( name string, ) model.Info { @@ -106,40 +133,73 @@ func (r rule) nameApplies(name string) bool { return r.nameRegex.MatchString(name) } -type RuleBuilder struct { +// Builder simplifies instantiation of default Rule implementation. +// +// WithPkgRegexps sets a list of package regular expressions. +// WithNameRegexp sets name regular expression. +// WithApplyFunc sets rule application function RuleApplyFunc. +// +// Build returns Rule implementation constructed from the provided expressions +// and application function. +// Build will return an error if at least one of the provided expressions +// is invalid and cannot be compiled. +// Build will return an error if application function RuleApplyFunc is missing. +type Builder interface { + WithPkgRegexps(rgx ...string) Builder + WithNameRegexp(rgx string) Builder + WithApplyFunc(f RuleApplyFunc) Builder + + Build() (Rule, error) +} + +type builder struct { pkgRegexes []string nameRegex string applyFunc RuleApplyFunc } -func NewRule() *RuleBuilder { - return &RuleBuilder{} +// NewRule returns an empty Builder. +func NewRule() Builder { + return &builder{} } -func (b *RuleBuilder) WithPkgRegexps(rgx ...string) *RuleBuilder { +// WithPkgRegexps sets a list of package regular expressions. +func (b *builder) WithPkgRegexps(rgx ...string) Builder { for _, r := range rgx { b.pkgRegexes = append(b.pkgRegexes, r) } return b } -func (b *RuleBuilder) WithNameRegexp(rgx string) *RuleBuilder { +// WithNameRegexp sets name regular expression. +func (b *builder) WithNameRegexp(rgx string) Builder { b.nameRegex = rgx return b } -func (b *RuleBuilder) WithApplyFunc(f RuleApplyFunc) *RuleBuilder { +// WithApplyFunc sets rule application function RuleApplyFunc. +func (b *builder) WithApplyFunc(f RuleApplyFunc) Builder { b.applyFunc = f return b } -func (b RuleBuilder) Build() (Rule, error) { +// Build returns Rule implementation constructed from the provided expressions +// and application function. +// +// In case no regular expression is provided either for name or package, +// those will be filled with regular expression matching all string "^.*$". +// +// Build will return an error if at least one of the provided expressions +// is invalid and cannot be compiled. +// Build will return an error if application function RuleApplyFunc is missing. +func (b builder) Build() (Rule, error) { pkgRegexes := make([]*regexp.Regexp, 0) for _, rgx := range b.pkgRegexes { r, err := regexp.Compile(rgx) if err != nil { return nil, errors.Wrapf(err, - "could not compile package expression `%s` as correct regular expression", rgx) + "could not compile package expression `%s` "+ + "as correct regular expression", rgx) } pkgRegexes = append(pkgRegexes, r) } @@ -153,11 +213,16 @@ func (b RuleBuilder) Build() (Rule, error) { r, err := regexp.Compile(b.nameRegex) if err != nil { return nil, errors.Wrapf(err, - "could not compile name expression `%s` as correct regular expression", b.nameRegex) + "could not compile name expression `%s` "+ + "as correct regular expression", b.nameRegex) } nameRegex = r } + if b.applyFunc == nil { + return nil, errors.New("apply function must be provided") + } + return newRule( pkgRegexes, nameRegex, diff --git a/pkg/scraper/scraper.go b/pkg/scraper/scraper.go index ff9a7fc..8102e06 100644 --- a/pkg/scraper/scraper.go +++ b/pkg/scraper/scraper.go @@ -1,27 +1,48 @@ package scraper import ( + "reflect" + "github.com/krzysztofreczek/go-structurizr/pkg/model" "github.com/krzysztofreczek/go-structurizr/pkg/yaml" "github.com/pkg/errors" - "reflect" ) -type Scraper struct { +// Scraper represents default scraper responsibilities. +// +// Scrape reflects given structure in accordance with internal configuration +// and registered rules. +// Scrape returns an open model.Structure with recognised components +// and relations between those. +// +// RegisterRule registers given Rule in the scraper. +// RegisterRule will return an error in case the given rule is nil. +type Scraper interface { + Scrape(i interface{}) model.Structure + RegisterRule(r Rule) error +} + +type scraper struct { config Configuration rules []Rule structure model.Structure } -func NewScraper(config Configuration) *Scraper { - return &Scraper{ +// NewScraper instantiates a default Scraper implementation +// with provided Configuration. +func NewScraper(config Configuration) Scraper { + return &scraper{ config: config, rules: make([]Rule, 0), structure: model.NewStructure(), } } -func NewScraperFromConfigFile(fileName string) (*Scraper, error) { +// NewScraperFromConfigFile instantiates a default Scraper implementation +// with Configuration loaded from provided YAML configuration file. +// NewScraperFromConfigFile will return an error in case the YAML configuration +// file does not exist or contains invalid content. +func NewScraperFromConfigFile(fileName string) (Scraper, error) { configuration, err := yaml.LoadFromFile(fileName) if err != nil { return nil, errors.Wrapf(err, @@ -35,14 +56,16 @@ func NewScraperFromConfigFile(fileName string) (*Scraper, error) { "could not load scraper rules from file `%s`", fileName) } - return &Scraper{ + return &scraper{ config: config, rules: rules, structure: model.NewStructure(), }, nil } -func (s *Scraper) RegisterRule(r Rule) error { +// RegisterRule registers given Rule in the scraper. +// RegisterRule will return an error in case the given rule is nil. +func (s *scraper) RegisterRule(r Rule) error { if r == nil { return errors.New("rule must not be nil") } @@ -50,7 +73,11 @@ func (s *Scraper) RegisterRule(r Rule) error { return nil } -func (s *Scraper) Scrap(i interface{}) model.Structure { +// Scrape reflects given structure in accordance with internal configuration +// and registered rules. +// Scrape returns an open model.Structure with recognised components +// and relations between those. +func (s *scraper) Scrape(i interface{}) model.Structure { v := reflect.ValueOf(i) s.scrap(v, "", 0) return s.structure diff --git a/pkg/scraper/scraper_test.go b/pkg/scraper/scraper_test.go index dfa4ec9..5bba760 100644 --- a/pkg/scraper/scraper_test.go +++ b/pkg/scraper/scraper_test.go @@ -60,7 +60,7 @@ func TestScraper_Scrap_package_matching(t *testing.T) { t.Run(tt.name, func(t *testing.T) { c := scraper.NewConfiguration(tt.packages...) s := scraper.NewScraper(c) - result := s.Scrap(tt.structure) + result := s.Scrape(tt.structure) require.Len(t, result.Components, tt.expectedNumberOfComponents) }) } @@ -558,7 +558,7 @@ func TestScraper_Scrap_has_info_interface(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { s := scraper.NewScraper(c) - result := s.Scrap(tt.structure) + result := s.Scrape(tt.structure) requireEqualComponentIDs(t, tt.expectedComponentIDs, result.Components) requireEqualRelations(t, tt.expectedRelations, result.Relations) }) @@ -606,7 +606,7 @@ func TestScraper_Scrap_has_info_interface_component_info(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { s := scraper.NewScraper(c) - result := s.Scrap(tt.structure) + result := s.Scrape(tt.structure) requireEqualComponents(t, tt.expectedComponents, result.Components) }) } @@ -762,7 +762,7 @@ func TestScraper_Scrap_rules(t *testing.T) { require.NoError(t, err) } - result := s.Scrap(tt.structure) + result := s.Scrape(tt.structure) requireEqualComponents(t, tt.expectedComponents, result.Components) }) } diff --git a/pkg/scraper/strategy.go b/pkg/scraper/strategy.go index 6c2ebd7..afe8338 100644 --- a/pkg/scraper/strategy.go +++ b/pkg/scraper/strategy.go @@ -10,7 +10,7 @@ import ( "github.com/krzysztofreczek/go-structurizr/pkg/model" ) -func (s *Scraper) scrap( +func (s *scraper) scrap( v reflect.Value, parentID string, level int, @@ -25,7 +25,7 @@ type scrapingStrategy func( level int, ) -func (s *Scraper) resolveScrapingStrategy(v reflect.Value) scrapingStrategy { +func (s *scraper) resolveScrapingStrategy(v reflect.Value) scrapingStrategy { switch v.Kind() { case reflect.Interface: return s.scrapeInterfaceStrategy @@ -40,7 +40,7 @@ func (s *Scraper) resolveScrapingStrategy(v reflect.Value) scrapingStrategy { return s.scrapeValue } -func (s *Scraper) scrapeInterfaceStrategy( +func (s *scraper) scrapeInterfaceStrategy( v reflect.Value, parentID string, level int, @@ -49,7 +49,7 @@ func (s *Scraper) scrapeInterfaceStrategy( s.scrap(v, parentID, level) } -func (s *Scraper) scrapePointerStrategy( +func (s *scraper) scrapePointerStrategy( v reflect.Value, parentID string, level int, @@ -58,7 +58,7 @@ func (s *Scraper) scrapePointerStrategy( s.scrap(v, parentID, level) } -func (s *Scraper) scrapeMapStrategy( +func (s *scraper) scrapeMapStrategy( v reflect.Value, parentID string, level int, @@ -72,7 +72,7 @@ func (s *Scraper) scrapeMapStrategy( } } -func (s *Scraper) scrapeIterableStrategy( +func (s *scraper) scrapeIterableStrategy( v reflect.Value, parentID string, level int, @@ -82,7 +82,7 @@ func (s *Scraper) scrapeIterableStrategy( } } -func (s *Scraper) scrapeValue( +func (s *scraper) scrapeValue( v reflect.Value, parentID string, level int, @@ -111,7 +111,7 @@ func (s *Scraper) scrapeValue( return } -func (s *Scraper) scrapeValueFields( +func (s *scraper) scrapeValueFields( v reflect.Value, parentID string, level int, @@ -121,7 +121,7 @@ func (s *Scraper) scrapeValueFields( } } -func (s *Scraper) addComponent( +func (s *scraper) addComponent( v reflect.Value, info model.Info, parentID string, @@ -138,9 +138,9 @@ func (s *Scraper) addComponent( return c } -func (s *Scraper) isScrappable(v reflect.Value) bool { +func (s *scraper) isScrappable(v reflect.Value) bool { vPkg := valuePackage(v) - for _, pkg := range s.config.packages { + for _, pkg := range s.config.Packages { if strings.HasPrefix(vPkg, pkg) { return true } @@ -148,7 +148,7 @@ func (s *Scraper) isScrappable(v reflect.Value) bool { return false } -func (s *Scraper) getInfoFromInterface(v reflect.Value) (model.Info, bool) { +func (s *scraper) getInfoFromInterface(v reflect.Value) (model.Info, bool) { var info model.HasInfo var ok bool @@ -172,7 +172,7 @@ func (s *Scraper) getInfoFromInterface(v reflect.Value) (model.Info, bool) { return info.Info(), true } -func (s *Scraper) getInfoFromRules(v reflect.Value) (model.Info, bool) { +func (s *scraper) getInfoFromRules(v reflect.Value) (model.Info, bool) { vPkg := valuePackage(v) name := componentName(v) for _, r := range s.rules { diff --git a/pkg/scraper/yaml.go b/pkg/scraper/yaml.go index 7a6ccd2..bee55d3 100644 --- a/pkg/scraper/yaml.go +++ b/pkg/scraper/yaml.go @@ -2,9 +2,10 @@ package scraper import ( "fmt" + "strings" + "github.com/krzysztofreczek/go-structurizr/pkg/model" "github.com/krzysztofreczek/go-structurizr/pkg/yaml" - "strings" ) func toScraperConfig(c yaml.Config) Configuration { diff --git a/pkg/scraper/yaml_test.go b/pkg/scraper/yaml_test.go index c839e6b..239a67e 100644 --- a/pkg/scraper/yaml_test.go +++ b/pkg/scraper/yaml_test.go @@ -17,7 +17,7 @@ func Test_toScraperConfig(t *testing.T) { } c := toScraperConfig(yamlConfiguration) - require.Equal(t, yamlConfiguration.Configuration.Packages, c.packages) + require.Equal(t, yamlConfiguration.Configuration.Packages, c.Packages) } func Test_toScraperRules(t *testing.T) { diff --git a/pkg/view/render.go b/pkg/view/render.go index 93055ac..ea61c53 100644 --- a/pkg/view/render.go +++ b/pkg/view/render.go @@ -7,7 +7,16 @@ import ( "github.com/krzysztofreczek/go-structurizr/pkg/model" ) -func (v View) render(s model.Structure) string { +// RenderStructureTo renders provided model.Structure into any io.Writer. +// RenderStructureTo will return an error in case the writer +// cannot be used. +func (v view) RenderStructureTo(s model.Structure, w io.Writer) error { + out := v.render(s) + _, err := w.Write([]byte(out)) + return err +} + +func (v view) render(s model.Structure) string { sb := strings.Builder{} sb.WriteString(buildUMLHead()) @@ -52,13 +61,7 @@ func (v View) render(s model.Structure) string { return sb.String() } -func (v View) RenderTo(s model.Structure, w io.Writer) error { - out := v.render(s) - _, err := w.Write([]byte(out)) - return err -} - -func (v View) hasTag(tags ...string) bool { +func (v view) hasTag(tags ...string) bool { if len(v.tags) == 0 { return true } diff --git a/pkg/view/view.go b/pkg/view/view.go index 98688b7..fd8494b 100644 --- a/pkg/view/view.go +++ b/pkg/view/view.go @@ -2,12 +2,23 @@ package view import ( "image/color" + "io" + "github.com/krzysztofreczek/go-structurizr/pkg/model" "github.com/krzysztofreczek/go-structurizr/pkg/yaml" "github.com/pkg/errors" ) -type View struct { +// View defines generic view. +// +// RenderStructureTo renders provided model.Structure into any io.Writer. +// RenderStructureTo will return an error in case the writer +// cannot be used. +type View interface { + RenderStructureTo(s model.Structure, w io.Writer) error +} + +type view struct { title string tags []string componentStyles map[string]ComponentStyle @@ -20,7 +31,7 @@ func newView( componentStyles map[string]ComponentStyle, lineColor color.Color, ) View { - return View{ + return view{ title: title, tags: tags, componentStyles: componentStyles, @@ -28,13 +39,10 @@ func newView( } } -type Builder struct { - View -} - -func NewView() *Builder { - return &Builder{ - View: View{ +// NewView returns an empty Builder. +func NewView() Builder { + return &builder{ + view: view{ title: "", tags: make([]string, 0), componentStyles: make(map[string]ComponentStyle), @@ -43,45 +51,87 @@ func NewView() *Builder { } } +// NewViewFromConfigFile instantiates a default View implementation +// with configuration loaded from provided YAML configuration file. +// NewViewFromConfigFile will return an error in case the YAML configuration +// file does not exist or contains invalid content. func NewViewFromConfigFile(fileName string) (View, error) { configuration, err := yaml.LoadFromFile(fileName) if err != nil { - return View{}, errors.Wrapf(err, + 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, + return view{}, errors.Wrapf(err, "could not load view from file `%s`", fileName) } return v, nil } -func (b *Builder) WithTitle(t string) *Builder { +// Builder simplifies instantiation of default View implementation. +// +// WithTitle sets view title. +// WithTag adds tag to the view. +// If at least one tag is defines, view will contain only those components +// which are tagged with at least one of those tags. +// WithComponentStyle adds custom component style to the view. +// ComponentStyle will be applied to the components tagged +// with component style ID. +// WithLineColor sets custom line color. +// +// Build returns default View implementation constructed from +// the provided configuration. +// If not specified all colors are defaulted to either black or white. +type Builder interface { + WithTitle(t string) Builder + WithTag(t string) Builder + WithComponentStyle(s ComponentStyle) Builder + WithLineColor(c color.Color) Builder + + Build() View +} + +type builder struct { + view +} + +// WithTitle sets view title. +func (b *builder) WithTitle(t string) Builder { b.title = t return b } -func (b *Builder) WithTag(t string) *Builder { +// WithTag adds tag to the view. +// If at least one tag is defines, view will contain only those components +// which are tagged with at least one of those tags. +func (b *builder) WithTag(t string) Builder { b.tags = append(b.tags, t) return b } -func (b *Builder) WithComponentStyle(s ComponentStyle) *Builder { +// WithComponentStyle adds custom component style to the view. +// ComponentStyle will be applied to the components tagged +// with component style ID. +func (b *builder) WithComponentStyle(s ComponentStyle) Builder { b.componentStyles[s.id] = s return b } -func (b *Builder) WithLineColor(c color.Color) *Builder { +// WithLineColor sets custom line color. +func (b *builder) WithLineColor(c color.Color) Builder { if c != nil { b.lineColor = c } return b } -func (b Builder) Build() View { +// Build returns default View implementation constructed from +// the provided configuration. +// If not specified all colors are defaulted to either black or white. +func (b builder) Build() View { return newView( b.title, b.tags, @@ -90,6 +140,9 @@ func (b Builder) Build() View { ) } +// ComponentStyle is a structure that represents custom view style +// that can be applied to scraped components. +// ComponentStyle is applied to the components tagged with component style ID. type ComponentStyle struct { id string backgroundColor color.Color @@ -111,12 +164,30 @@ func newComponentStyle( } } -type ComponentStyleBuilder struct { +// ComponentStyleBuilder simplifies instantiation of default ComponentStyle +// implementation. +// +// WithBackgroundColor sets background color. +// WithFontColor sets font color. +// WithBorderColor sets border color +// +// Build returns default ComponentStyle implementation constructed from +// the provided configuration. +type ComponentStyleBuilder interface { + WithBackgroundColor(c color.Color) ComponentStyleBuilder + WithFontColor(c color.Color) ComponentStyleBuilder + WithBorderColor(c color.Color) ComponentStyleBuilder + + Build() ComponentStyle +} + +type componentStyleBuilder struct { ComponentStyle } -func NewComponentStyle(id string) *ComponentStyleBuilder { - return &ComponentStyleBuilder{ +// NewView returns ComponentStyleBuilder with provided id. +func NewComponentStyle(id string) ComponentStyleBuilder { + return &componentStyleBuilder{ ComponentStyle: ComponentStyle{ id: id, backgroundColor: color.White, @@ -126,28 +197,33 @@ func NewComponentStyle(id string) *ComponentStyleBuilder { } } -func (b *ComponentStyleBuilder) WithBackgroundColor(c color.Color) *ComponentStyleBuilder { +// WithBackgroundColor sets background color. +func (b *componentStyleBuilder) WithBackgroundColor(c color.Color) ComponentStyleBuilder { if c != nil { b.backgroundColor = c } return b } -func (b *ComponentStyleBuilder) WithFontColor(c color.Color) *ComponentStyleBuilder { +// WithFontColor sets font color. +func (b *componentStyleBuilder) WithFontColor(c color.Color) ComponentStyleBuilder { if c != nil { b.fontColor = c } return b } -func (b *ComponentStyleBuilder) WithBorderColor(c color.Color) *ComponentStyleBuilder { +// WithBorderColor sets border color +func (b *componentStyleBuilder) WithBorderColor(c color.Color) ComponentStyleBuilder { if c != nil { b.borderColor = c } return b } -func (b ComponentStyleBuilder) Build() ComponentStyle { +// Build returns default ComponentStyle implementation constructed from +// the provided configuration. +func (b componentStyleBuilder) Build() ComponentStyle { return newComponentStyle( b.id, b.backgroundColor, diff --git a/pkg/view/view_test.go b/pkg/view/view_test.go index 8c56642..58dad1d 100644 --- a/pkg/view/view_test.go +++ b/pkg/view/view_test.go @@ -2,11 +2,12 @@ package view_test import ( "bytes" + "image/color" + "testing" + "github.com/krzysztofreczek/go-structurizr/pkg/model" "github.com/krzysztofreczek/go-structurizr/pkg/view" "github.com/stretchr/testify/require" - "image/color" - "testing" ) func TestNewView_empty(t *testing.T) { @@ -15,7 +16,7 @@ func TestNewView_empty(t *testing.T) { out := bytes.Buffer{} v := view.NewView().Build() - err := v.RenderTo(s, &out) + err := v.RenderStructureTo(s, &out) require.NoError(t, err) outString := string(out.Bytes()) @@ -47,7 +48,7 @@ func TestNewView_with_title(t *testing.T) { v := view.NewView(). WithTitle("TITLE"). Build() - err := v.RenderTo(s, &out) + err := v.RenderStructureTo(s, &out) require.NoError(t, err) outString := string(out.Bytes()) @@ -70,7 +71,7 @@ func TestNewView_with_custom_style(t *testing.T) { v := view.NewView(). WithComponentStyle(style). Build() - err := v.RenderTo(s, &out) + err := v.RenderStructureTo(s, &out) require.NoError(t, err) outString := string(out.Bytes()) @@ -100,7 +101,7 @@ func TestNewView_with_component(t *testing.T) { out := bytes.Buffer{} v := view.NewView().Build() - err := v.RenderTo(s, &out) + err := v.RenderStructureTo(s, &out) require.NoError(t, err) outString := string(out.Bytes()) @@ -123,7 +124,7 @@ func TestNewView_with_relation(t *testing.T) { out := bytes.Buffer{} v := view.NewView().Build() - err := v.RenderTo(s, &out) + err := v.RenderStructureTo(s, &out) require.NoError(t, err) outString := string(out.Bytes()) @@ -152,7 +153,7 @@ func TestNewView_with_custom_line_color(t *testing.T) { v := view.NewView(). WithLineColor(color.White). Build() - err := v.RenderTo(s, &out) + err := v.RenderStructureTo(s, &out) require.NoError(t, err) outString := string(out.Bytes()) @@ -181,7 +182,7 @@ func TestNewView_with_component_of_view_tag(t *testing.T) { v := view.NewView(). WithTag("tag 1"). Build() - err := v.RenderTo(s, &out) + err := v.RenderStructureTo(s, &out) require.NoError(t, err) outString := string(out.Bytes()) @@ -210,7 +211,7 @@ func TestNewView_with_component_with_no_view_tag(t *testing.T) { v := view.NewView(). WithTag("tag 1"). Build() - err := v.RenderTo(s, &out) + err := v.RenderStructureTo(s, &out) require.NoError(t, err) outString := string(out.Bytes()) @@ -253,7 +254,7 @@ func TestNewView_with_two_joined_components_of_view_tag(t *testing.T) { WithTag("tag 1"). WithTag("tag 2"). Build() - err := v.RenderTo(s, &out) + err := v.RenderStructureTo(s, &out) require.NoError(t, err) outString := string(out.Bytes()) @@ -306,7 +307,7 @@ func TestNewView_with_two_joined_components_where_one_with_no_view_tag(t *testin WithTag("tag 1"). WithTag("tag 2"). Build() - err := v.RenderTo(s, &out) + err := v.RenderStructureTo(s, &out) require.NoError(t, err) outString := string(out.Bytes()) diff --git a/pkg/view/yaml.go b/pkg/view/yaml.go index e4227b6..6c7a19c 100644 --- a/pkg/view/yaml.go +++ b/pkg/view/yaml.go @@ -14,7 +14,7 @@ func toView(c yaml.Config) (View, error) { if c.View.LineColor != "" { col, err := decodeHexColor(c.View.LineColor) if err != nil { - return View{}, err + return view{}, err } v.WithLineColor(col) } @@ -25,7 +25,7 @@ func toView(c yaml.Config) (View, error) { if s.BackgroundColor != "" { col, err := decodeHexColor(s.BackgroundColor) if err != nil { - return View{}, err + return view{}, err } style.WithBackgroundColor(col) } @@ -33,7 +33,7 @@ func toView(c yaml.Config) (View, error) { if s.FontColor != "" { col, err := decodeHexColor(s.FontColor) if err != nil { - return View{}, err + return view{}, err } style.WithFontColor(col) } @@ -41,7 +41,7 @@ func toView(c yaml.Config) (View, error) { if s.BorderColor != "" { col, err := decodeHexColor(s.BorderColor) if err != nil { - return View{}, err + return view{}, err } style.WithBorderColor(col) } diff --git a/pkg/view/yaml_test.go b/pkg/view/yaml_test.go index d6749d0..f3d4d4b 100644 --- a/pkg/view/yaml_test.go +++ b/pkg/view/yaml_test.go @@ -69,12 +69,12 @@ func Test_toView(t *testing.T) { } actualOutput := bytes.Buffer{} - err = actualView.RenderTo(s, &actualOutput) + err = actualView.RenderStructureTo(s, &actualOutput) require.NoError(t, err) require.NotEmpty(t, actualOutput) expectedOutput := bytes.Buffer{} - err = expectedView.RenderTo(s, &expectedOutput) + err = expectedView.RenderStructureTo(s, &expectedOutput) require.NoError(t, err) require.NotEmpty(t, expectedOutput) diff --git a/pkg/yaml/yaml.go b/pkg/yaml/yaml.go index 6f3daa2..36bf592 100644 --- a/pkg/yaml/yaml.go +++ b/pkg/yaml/yaml.go @@ -7,22 +7,26 @@ import ( "gopkg.in/yaml.v3" ) +// Config is an open YAML configuration structure. type Config struct { Configuration ConfigConfiguration `yaml:"configuration"` Rules []ConfigRule `yaml:"rules"` View ConfigView `yaml:"view"` } +// ConfigConfiguration is an open YAML configuration structure. type ConfigConfiguration struct { Packages []string `yaml:"pkgs"` } +// ConfigRule is an open YAML configuration structure. type ConfigRule struct { PackageRegexps []string `yaml:"pkg_regexps"` NameRegexp string `yaml:"name_regexp"` Component ConfigRuleComponent `yaml:"component"` } +// ConfigRuleComponent is an open YAML configuration structure. type ConfigRuleComponent struct { Name string `yaml:"name"` Description string `yaml:"description"` @@ -30,6 +34,7 @@ type ConfigRuleComponent struct { Tags []string `yaml:"tags"` } +// ConfigView is an open YAML configuration structure. type ConfigView struct { Title string `yaml:"title"` LineColor string `yaml:"line_color"` @@ -37,6 +42,7 @@ type ConfigView struct { Tags []string `yaml:"tags"` } +// ConfigViewStyle is an open YAML configuration structure. type ConfigViewStyle struct { ID string `yaml:"id"` BackgroundColor string `yaml:"background_color"` @@ -44,6 +50,9 @@ type ConfigViewStyle struct { BorderColor string `yaml:"border_color"` } +// LoadFromFile loads Config from YAML file. +// LoadFromFile will return an error in case file does not exists +// or cannot be decoded. func LoadFromFile(fileName string) (Config, error) { f, err := os.Open(fileName) if err != nil { @@ -56,6 +65,8 @@ func LoadFromFile(fileName string) (Config, error) { return LoadFrom(f) } +// LoadFrom loads Config from YAML content read from io.Reader. +// LoadFrom will return an error in case io.Reader content cannot be decoded. func LoadFrom(source io.Reader) (Config, error) { var cfg Config decoder := yaml.NewDecoder(source)