Skip to content

Commit

Permalink
Improve code documentation and README.md (#157)
Browse files Browse the repository at this point in the history
  • Loading branch information
krzysztofreczek authored Sep 8, 2024
1 parent ce30e5b commit dc5ba35
Show file tree
Hide file tree
Showing 9 changed files with 227 additions and 216 deletions.
123 changes: 70 additions & 53 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,62 +1,66 @@
[![CircleCI](https://circleci.com/gh/krzysztofreczek/go-structurizr.svg?style=shield)](https://circleci.com/gh/krzysztofreczek/go-structurizr)
[![CircleCI](https://circleci.com/gh/krzysztofreczek/go-structurizr.svg?style=shield)](https://circleci.com/gh/krzysztofreczek/go-structurizr)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)

# go-structurizr
This library allows you to auto-generate C4 component diagrams out from the Golang code.

This library allows you to auto-generate C4 component diagrams from Go code.

![Example](images/example.png)

## Usage and examples
If you want to get directly into usage of the library check my [blog post](https://threedots.tech/post/auto-generated-c4-architecture-diagrams-in-go/) with a step-by-step implementation guide.
## Usage and Examples

To quickly learn how to use the library, check out my [blog post](https://threedots.tech/post/auto-generated-c4-architecture-diagrams-in-go/) for a step-by-step implementation guide.

You may also find a couple of examples implemented in the `cmd` directory. In order to run any of those examples, please run the shell script attached.
You can also find several examples in the `cmd` directory. To run any of these examples, use the provided shell script.

## How it works?
The library provides a set of tools (Scraper and View) that allow you to scrape and render given Golang structures into a [C4 component](https://c4model.com/) diagram in [*.plantuml](https://plantuml.com/) format.
## How It Works

**Scraper** component reflects given structure in accordance with structure interfaces, predefined rules and configuration.
The library provides tools (Scraper and View) to scrape and render Go structures into a [C4 component](https://c4model.com/) diagram in [PlantUML](https://plantuml.com/) format.

You may pass the scraped structure into a **View** definition which you can then render into a plantUML diagram code.
- **Scraper** reflects a given structure according to interfaces, predefined rules, and configurations.
- You can pass the scraped structure into a **View** definition, which can then be rendered into PlantUML diagram code.

Scraper identifies components to scrape in one of the following cases:
* type that is being examined implements an interface `model.HasInfo`.
* type that is being examined applies to one of the rules registered in the scraper.
Scraper identifies components to scrape under the following conditions:
- The type being examined implements the `model.HasInfo` interface.
- The type being examined matches one of the rules registered with the scraper.

## Components

### Component Info

Structure `model.Info` is a basic structure that defines a component included in the scraped structure of your code.
The `model.Info` structure defines a component in the scraped structure of your code:

```go
type Info struct {
Kind string // kind of scraped component
Name string // component name
Description string // component description
Technology string // technology used within the component
Tags []string // tags are used to match view styles to component
Tags []string // tags used to match view styles to component
}
```

### Scraper

Scraper may be instantiated in one of two ways:
* from the code
* from the YAML file
You can instantiate the scraper in one of two ways:
- From the code
- From a YAML file

To instantiate the scraper, provide a configuration that includes the prefixes of packages you want to reflect. Types that do not match any of the specified prefixes will not be processed.

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/org/pkg",
)
s := scraper.NewScraper(config)
```

Having a scraper instantiated, you can register a set of rules that will allow the scraper to identify the components to include in the output structure.
After creating a scraper instance, you can register rules that will allow it to identify components to include in the output structure.

Each rule consists of:
* Set of package regexp's - only types in a package matching at least one of the package regexp's will be processed
* Name regexp - only type of name matching regexp will be processed
* Apply function - function that produces `model.Info` describing the component included in the scraped structure.
- A set of package regular expressions: Only types in a package matching at least one of the regular expressions will be processed.
- A name regular expression: Only types whose names match the regular expression will be processed.
- An apply function: A function that produces `model.Info` describing the component to include in the scraped structure.

```go
r, err := scraper.NewRule().
Expand All @@ -70,26 +74,27 @@ r, err := scraper.NewRule().
err = s.RegisterRule(r)
```

The apply function has two arguments: name and groups matched from the name regular expression.
The apply function has two arguments: the name and groups matched from the name regular expression.

See the example:
Example:
```go
r, err := scraper.NewRule().
WithPkgRegexps("github.com/org/pkg/foo/.*").
WithNameRegexp(`^(\w*)\.(\w*)Client$`).
WithApplyFunc(
func(_ string, groups ...string) model.Info {
// Do some groups sanity checks first, then:
// Perform checks on the groups, then:
n := fmt.Sprintf("Client of external %s service", groups[1])
return model.ComponentInfo(n, "foo client", "gRPC", "TAG")
}).
Build()
err = s.RegisterRule(r)
```

Alternatively, you can instantiate the scraper form YAML configuration file:
Alternatively, you can instantiate the scraper from a YAML configuration file:

```yaml
// go-structurizr.yml
# go-structurizr.yml
configuration:
pkgs:
- "github.com/org/pkg"
Expand All @@ -105,7 +110,8 @@ rules:
- TAG
```
Regex groups may also be used within yaml rule definition. Here you can find an example:
You can also use regular expression groups in YAML rule definitions:
```yaml
rules:
- name_regexp: "(\\w*)\\.(\\w*)Client$"
Expand All @@ -119,38 +125,42 @@ rules:
- TAG
```
To create a scraper from the configuration file:
```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`.
Once the scraper is instantiated and configured, you can use it to scrape any structure. The scraper returns a `model.Structure`.

```go
structure := s.Scrape(app)
```

### View

Similarly, to the scraper, view may be instantiated in one of two ways:
* from the code
* from the YAML file
Similarly to the scraper, a view can be instantiated in one of two ways:
- From the code
- From a YAML file

In order to render scraped structure, you will need to instantiate and configure a view.
View consists of:
* title
* component styles - styles are applied to the components by matching first of component tags with style ids
* additional styling (i.e. line color)
* component tags - if specified, view will contain only components tagged with one of the view tags. When no tag is defined, all components will be included in the rendered view.
* root component tags - if specified, view will contain only those components which have connection (direct or in-direct) to at least one of components with root tag.
To render a scraped structure, you need to instantiate and configure a view. A view consists of:
- Title
- Component styles: Styles are applied to components by matching the first component tag with style IDs.
- Additional styling (e.g., line color)
- Component tags: If specified, the view will only contain components tagged with one of the view tags. If no tags are defined, all components will be included.
- Root component tags: If specified, the view will only include components that have a direct or indirect connection to at least one component with a root tag.

To instantiate a default view, use the view builder:

In order to instantiate default view, use the view builder:
```go
v := view.NewView().Build()
```

In case you need to customize it, use available builder methods:
To customize it, use the available builder methods:

```go
v := view.NewView().
WithTitle("Title")
WithTitle("Title").
WithComponentStyle(
view.NewComponentStyle("TAG").
WithBackgroundColor(color.White).
Expand All @@ -164,9 +174,10 @@ v := view.NewView().
Build()
```

Alternatively, you can instantiate the view form YAML configuration file:
Alternatively, you can instantiate the view from a YAML configuration file:

```yaml
// go-structurizr.yml
# go-structurizr.yml
view:
title: "Title"
line_color: 000000ff
Expand All @@ -182,11 +193,14 @@ view:
- TAG
```
To create a view from the configuration file:
```go
v, err := view.NewViewFromConfigFile("./go-structurizr.yml")
```

As the view is initialized, you can now render the structure into planUML diagram.
Once the view is initialized, you can render the structure into a PlantUML diagram:

```go
outFile, _ := os.Create("c4.plantuml")
defer func() {
Expand All @@ -196,13 +210,16 @@ defer func() {
err = v.RenderStructureTo(structure, outFile)
```

## Debug mode
In order to see detailed scraping or view rendering logs, set `LOG_LEVEL` env variable with `debug` of `DEBUG`.
## Debug Mode

To enable detailed scraping or view rendering logs, set the `LOG_LEVEL` environment variable to `debug` or `DEBUG`.

## Best Practices

For the best results and experience with the library, follow these practices:
- Use a solid, well-organized application context following clean architecture principles. This makes your diagrams simpler and easier to read and allows for a shorter list of component types and styles.
- Maintain consistent naming conventions to simplify and clarify scraper rules.

## Good practices
The best results and experience in using the library will be ensured by enforcing the following practices:
- Having a solid and well-organized application context following clean-architecture principles will make your diagrams simple and easy to read. Also, this will allow you to create a short list of component types and styles.
- Following consistent naming conventions will help you in creating simple and straight-forward scraper rules.
## Full Code Documentation

## Full code documentation
https://pkg.go.dev/github.com/krzysztofreczek/go-structurizr
For full code documentation, visit [pkg.go.dev](https://pkg.go.dev/github.com/krzysztofreczek/go-structurizr).
30 changes: 14 additions & 16 deletions pkg/model/info.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
package model

// HasInfo wraps simple getter method returning component information.
// HasInfo represents a simple getter method that returns 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.
// The HasInfo interface indicates that a type can provide its own component
// information. All types that implement this interface are automatically
// detected by the default implementation of the scraper.
type HasInfo interface {
Info() Info
}
Expand All @@ -14,14 +13,13 @@ const (
infoKindComponent = "component"
)

// Info struct contains all component information details.
// Info struct contains details about a component.
//
// 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.
// Name is the name of the component.
// Kind represents the component's level or type in C4 diagrams.
// Description provides an explanation of the component's responsibility.
// Technology describes the technology the component is based on.
// Tags is a set of generic strings used to group and reference components.
type Info struct {
Name string
Kind string
Expand All @@ -30,8 +28,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.
// ComponentInfo creates a new component with a predefined kind "component".
// Variadic arguments are assigned sequentially to the remaining Info properties.
func ComponentInfo(s ...string) Info {
return info(infoKindComponent, s...)
}
Expand Down Expand Up @@ -63,8 +61,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.
// IsZero checks whether the component is empty.
// A component is considered empty if it does not have a specified kind.
func (i Info) IsZero() bool {
return i.Kind == ""
}
31 changes: 15 additions & 16 deletions pkg/model/structure.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,14 @@ import (

const version = 1

// Component is an open structure that represents details of scraped component.
// Component is an open structure representing the details of a 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.
// ID is a unique identifier for the component.
// Kind represents the component's level or type in C4 diagrams.
// Name is the name of the component.
// Description provides a brief explanation of the component's responsibility.
// Technology describes the technology the component is based on.
// Tags is a set of generic strings used to group and reference components.
type Component struct {
ID string
Kind string
Expand All @@ -27,26 +26,26 @@ type Component struct {
Tags []string
}

// Structure is an open stricture that represents whole scraped structure.
// Structure is an open structure representing the entire scraped system.
//
// Components contains all the scraped components by its IDs.
// Relations contains all the connections between components by its IDs.
// Components contains all the scraped components, indexed by their IDs.
// Relations contains all the connections between components, indexed by their IDs.
type Structure struct {
Components map[string]Component
Relations map[string]map[string]struct{}
}

// NewStructure instantiates an empty structure.
// NewStructure creates and returns an empty Structure.
func NewStructure() Structure {
return Structure{
Components: make(map[string]Component),
Relations: make(map[string]map[string]struct{}),
}
}

// AddComponent adds component and corresponding relation to its parent.
// AddComponent adds a component and creates a corresponding relation to its parent.
//
// In case a parent of given ID does not exist relation will not be created.
// If a parent with the given ID does not exist, the relation will not be created.
func (s Structure) AddComponent(c Component, parentID string) {
s.Components[c.ID] = c
if parentID != "" {
Expand All @@ -58,9 +57,9 @@ func (s Structure) AddComponent(c Component, parentID string) {
}
}

// Checksum returns a hash of the Structure
// Checksum returns a hash of the Structure.
//
// Checksum may be used for tracking changes between the structures.
// The checksum can be used to track changes between different structures.
func (s Structure) Checksum() (string, error) {
cIDs := make([]string, 0)
for id := range s.Components {
Expand Down
16 changes: 8 additions & 8 deletions pkg/scraper/config.go
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
package scraper

// Configuration is an open structure that contains scraper configuration.
// Configuration is an open structure that holds the 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.
// Packages contain prefixes of packages for the scraper to process.
// Any package object that does not match the provided prefixes will be omitted,
// and its internal structure will not be scraped.
// If no package prefixes are provided, the scraper will only process the root level of the structure.
type Configuration struct {
Packages []string
}

// NewConfiguration instantiates Configuration with a set of package
// prefixes provided with variadic argument.
// NewConfiguration creates a Configuration with the specified package prefixes.
//
// It takes a variadic argument to accept multiple package prefixes.
func NewConfiguration(packages ...string) Configuration {
return Configuration{
Packages: packages,
Expand Down
Loading

0 comments on commit dc5ba35

Please sign in to comment.