Skip to content

Commit

Permalink
Merge pull request #1 from Jagerente/docgen
Browse files Browse the repository at this point in the history
documentation builder;
  • Loading branch information
Jagerente committed Feb 8, 2024
2 parents 3c4b4c9 + e58612c commit 327dd9a
Show file tree
Hide file tree
Showing 6 changed files with 668 additions and 15 deletions.
81 changes: 66 additions & 15 deletions config.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,11 @@ import (

// Default tags for struct field annotations
const (
structKeyTag = "env"
structDefaultTag = "default"
structAllowEmptyTag = "omitempty"
structKeyTag = "env"
structDefaultTag = "default"
structAllowEmptyTag = "omitempty"
structDescriptionTag = "description"
structTitleTag = "title"
)

// ValueProvider defines the interface for retrieving values based on keys
Expand All @@ -26,25 +28,34 @@ type ParserProvider interface {
Get(reflect.Value) (func(v string) (interface{}, error), bool)
}

// DocGenerator defines the interface for generating documentation for struct fields
type DocGenerator interface {
GenerateDoc(*DocTree) error
}

// ConfigManager represents the configuration manager
type ConfigManager struct {
structKeyTag string
structDefaultTag string
structAllowEmptyTag string
parserProviders []ParserProvider
valueProviders []ValueProvider
useDefaults bool
forceDefaults bool
structKeyTag string
structDefaultTag string
structAllowEmptyTag string
parserProviders []ParserProvider
valueProviders []ValueProvider
useDefaults bool
forceDefaults bool
structDescriptionTag string
structTitleTag string
}

// NewEmpty creates a new ConfigManager instance with default tags and empty providers
func NewEmpty() *ConfigManager {
return &ConfigManager{
structKeyTag: structKeyTag,
structDefaultTag: structDefaultTag,
structAllowEmptyTag: structAllowEmptyTag,
parserProviders: make([]ParserProvider, 0),
valueProviders: make([]ValueProvider, 0),
structKeyTag: structKeyTag,
structDefaultTag: structDefaultTag,
structAllowEmptyTag: structAllowEmptyTag,
structDescriptionTag: structDescriptionTag,
structTitleTag: structTitleTag,
parserProviders: make([]ParserProvider, 0),
valueProviders: make([]ValueProvider, 0),
}
}

Expand Down Expand Up @@ -176,6 +187,46 @@ func (c *ConfigManager) Unmarshal(cfg interface{}) error {
return nil
}

func (c *ConfigManager) GenerateDocumentation(cfg interface{}, docGen DocGenerator) error {
doc := NewDoc()

c.parseDocGroup(doc, cfg)

if err := docGen.GenerateDoc(doc); err != nil {
return err
}

return nil
}

func (c *ConfigManager) parseDocGroup(docGroup *DocTree, cfg interface{}) {
val := reflect.ValueOf(cfg).Elem()

for i := 0; i < val.NumField(); i++ {
var (
field = val.Field(i)
tag = val.Type().Field(i).Tag.Get(c.structKeyTag)
key = strings.Split(tag, ",")[0]
allowEmpty = strings.Contains(tag, c.structAllowEmptyTag)
defaultValue = val.Type().Field(i).Tag.Get(c.structDefaultTag)
description = val.Type().Field(i).Tag.Get(c.structDescriptionTag)
title = val.Type().Field(i).Tag.Get(c.structTitleTag)
)

if field.Kind() == reflect.Struct {
c.parseDocGroup(docGroup.AddGroup(title), field.Addr().Interface())
continue
}

docGroup.AddField(&DocField{
Key: key,
OmitEmpty: allowEmpty,
Description: description,
DefaultValue: defaultValue,
})
}
}

// getValue retrieves the value for a key from registered value providers
func (c *ConfigManager) getValue(key string) string {
for _, p := range c.valueProviders {
Expand Down
96 changes: 96 additions & 0 deletions config_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package gocfg

import (
"errors"
"github.com/Jagerente/gocfg/pkg/values"
"github.com/stretchr/testify/assert"
"os"
Expand Down Expand Up @@ -327,3 +328,98 @@ func Test_GetParserUnsupportedField(t *testing.T) {
assert.Error(t, err)
assert.Contains(t, err.Error(), "failed to get parser for UNSUPPORTED_FIELD: unsupported")
}

type MockDocGenerator struct {
GeneratedDoc *DocTree
WithErr bool
}

func (m *MockDocGenerator) GenerateDoc(doc *DocTree) error {
if m.WithErr {
return errors.New("failed to generate doc")
}

m.GeneratedDoc = doc
return nil
}

func Test_GenerateDocumentation(t *testing.T) {
type Nested struct {
BoolField bool `env:"NESTED_BOOL_FIELD" description:"Description for Nested BoolField"`
}

type TestConfig struct {
StringField string `env:"STRING_FIELD" description:"Description for StringField"`
IntField int `env:"INT_FIELD" description:"Description for IntField"`
NestedStruct Nested `title:"Nested Struct Config"`
WithoutEnvTag string `description:"Description for WithoutEnvTag"`
}

cfg := new(TestConfig)
mockDocGenerator := &MockDocGenerator{}

cfgManager := NewEmpty()
err := cfgManager.GenerateDocumentation(cfg, mockDocGenerator)

assert.NoError(t, err)
assert.NotNil(t, mockDocGenerator.GeneratedDoc)
assert.Equal(t, "", mockDocGenerator.GeneratedDoc.Title)
assert.Len(t, mockDocGenerator.GeneratedDoc.Fields, 3)
assert.Len(t, mockDocGenerator.GeneratedDoc.Groups, 1)
assert.Equal(t, "STRING_FIELD", mockDocGenerator.GeneratedDoc.Fields[0].Key)
assert.Equal(t, "Description for StringField", mockDocGenerator.GeneratedDoc.Fields[0].Description)
assert.Equal(t, "INT_FIELD", mockDocGenerator.GeneratedDoc.Fields[1].Key)
assert.Equal(t, "Description for IntField", mockDocGenerator.GeneratedDoc.Fields[1].Description)
assert.Equal(t, "Nested Struct Config", mockDocGenerator.GeneratedDoc.Groups[0].Title)
assert.Len(t, mockDocGenerator.GeneratedDoc.Groups[0].Fields, 1)
assert.Equal(t, "NESTED_BOOL_FIELD", mockDocGenerator.GeneratedDoc.Groups[0].Fields[0].Key)
assert.Equal(t, "Description for Nested BoolField", mockDocGenerator.GeneratedDoc.Groups[0].Fields[0].Description)
}

func Test_GenerateDocumentation_WithError(t *testing.T) {
type TestConfig struct {
}

cfg := new(TestConfig)
mockDocGenerator := &MockDocGenerator{
WithErr: true,
}

cfgManager := NewEmpty()
err := cfgManager.GenerateDocumentation(cfg, mockDocGenerator)

assert.NotNil(t, err)
}

func Test_parseDocGroup(t *testing.T) {
type Nested struct {
BoolField bool `env:"NESTED_BOOL_FIELD" description:"Description for Nested BoolField"`
}

type TestConfig struct {
StringField string `env:"STRING_FIELD" description:"Description for StringField"`
IntField int `env:"INT_FIELD" description:"Description for IntField"`
NestedStruct Nested `title:"Nested Struct Config"`
WithoutEnvTag string `description:"Description for WithoutEnvTag"`
}

cfg := new(TestConfig)

docGroup := NewDoc()

cfgManager := NewEmpty()
cfgManager.parseDocGroup(docGroup, cfg)

assert.NotNil(t, docGroup)
assert.Equal(t, "", docGroup.Title)
assert.Len(t, docGroup.Fields, 3)
assert.Len(t, docGroup.Groups, 1)
assert.Equal(t, "STRING_FIELD", docGroup.Fields[0].Key)
assert.Equal(t, "Description for StringField", docGroup.Fields[0].Description)
assert.Equal(t, "INT_FIELD", docGroup.Fields[1].Key)
assert.Equal(t, "Description for IntField", docGroup.Fields[1].Description)
assert.Equal(t, "Nested Struct Config", docGroup.Groups[0].Title)
assert.Len(t, docGroup.Groups[0].Fields, 1)
assert.Equal(t, "NESTED_BOOL_FIELD", docGroup.Groups[0].Fields[0].Key)
assert.Equal(t, "Description for Nested BoolField", docGroup.Groups[0].Fields[0].Description)
}
39 changes: 39 additions & 0 deletions doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package gocfg

type DocField struct {
Key string
Description string
DefaultValue string
OmitEmpty bool
}

type DocTree struct {
Title string
Fields []*DocField
Groups []*DocTree
}

func NewDoc() *DocTree {
return &DocTree{
Fields: []*DocField{},
Groups: []*DocTree{},
}
}

func (d *DocTree) AddGroup(name string) *DocTree {
g := &DocTree{
Title: name,
Fields: []*DocField{},
Groups: []*DocTree{},
}

d.Groups = append(d.Groups, g)

return g
}

func (d *DocTree) AddField(field *DocField) *DocField {
d.Fields = append(d.Fields, field)

return field
}
77 changes: 77 additions & 0 deletions doc_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package gocfg

import (
"testing"

"github.com/stretchr/testify/assert"
)

func Test_NewDoc(t *testing.T) {
doc := NewDoc()

assert.NotNil(t, doc)
assert.NotNil(t, doc.Fields)
assert.NotNil(t, doc.Groups)
assert.Empty(t, doc.Fields)
assert.Empty(t, doc.Groups)
}

func Test_AddGroup(t *testing.T) {
doc := NewDoc()

group1 := doc.AddGroup("Group 1")
assert.NotNil(t, group1)
assert.Equal(t, "Group 1", group1.Title)
assert.Empty(t, group1.Fields)
assert.Empty(t, group1.Groups)

group2 := doc.AddGroup("Group 2")
assert.NotNil(t, group2)
assert.Equal(t, "Group 2", group2.Title)
assert.Empty(t, group2.Fields)
assert.Empty(t, group2.Groups)

assert.Len(t, doc.Groups, 2)
assert.Equal(t, group1, doc.Groups[0])
assert.Equal(t, group2, doc.Groups[1])
}

func Test_AddField(t *testing.T) {
doc := NewDoc()

field1 := &DocField{Key: "Field 1", Description: "Description 1", DefaultValue: "Default 1", OmitEmpty: true}
doc.AddField(field1)
assert.NotNil(t, doc.Fields)
assert.Len(t, doc.Fields, 1)
assert.Equal(t, field1, doc.Fields[0])

field2 := &DocField{Key: "Field 2", Description: "Description 2", DefaultValue: "Default 2", OmitEmpty: false}
doc.AddField(field2)
assert.Len(t, doc.Fields, 2)
assert.Equal(t, field2, doc.Fields[1])
}

func Test_DocTree(t *testing.T) {
doc := NewDoc()

group1 := doc.AddGroup("Group 1")
group1.AddField(&DocField{Key: "Field 1", Description: "Description 1", DefaultValue: "Default 1", OmitEmpty: true})

group2 := doc.AddGroup("Group 2")
group2.AddField(&DocField{Key: "Field 2", Description: "Description 2", DefaultValue: "Default 2", OmitEmpty: false})

assert.Len(t, doc.Groups, 2)
assert.Equal(t, "Group 1", doc.Groups[0].Title)
assert.Len(t, doc.Groups[0].Fields, 1)
assert.Equal(t, "Field 1", doc.Groups[0].Fields[0].Key)
assert.Equal(t, "Description 1", doc.Groups[0].Fields[0].Description)
assert.Equal(t, "Default 1", doc.Groups[0].Fields[0].DefaultValue)
assert.True(t, doc.Groups[0].Fields[0].OmitEmpty)

assert.Equal(t, "Group 2", doc.Groups[1].Title)
assert.Len(t, doc.Groups[1].Fields, 1)
assert.Equal(t, "Field 2", doc.Groups[1].Fields[0].Key)
assert.Equal(t, "Description 2", doc.Groups[1].Fields[0].Description)
assert.Equal(t, "Default 2", doc.Groups[1].Fields[0].DefaultValue)
assert.False(t, doc.Groups[1].Fields[0].OmitEmpty)
}
Loading

0 comments on commit 327dd9a

Please sign in to comment.