From ed59a7b0ea10d0bd2d6501230b325cb023406d96 Mon Sep 17 00:00:00 2001 From: "Kirill Che." Date: Thu, 9 May 2024 13:53:51 +0400 Subject: [PATCH 1/6] chore: refactor ast parser and inspector Keep current context for `ast.File` in AST parser. Iterate over all files in file-set to traverse with AST parser. For #23 --- ast.go | 79 ++++++++++++++++++++++++++++++++++++---------------- inspector.go | 26 +++++++++++------ 2 files changed, 73 insertions(+), 32 deletions(-) diff --git a/ast.go b/ast.go index 9e911b1..f7b94bf 100644 --- a/ast.go +++ b/ast.go @@ -22,15 +22,17 @@ const ( ) type visitorNode struct { - kind nodeKind - typeName string // type name if node is a type or field type name if node is a field - names []string // it's possible that a field has multiple names - doc string // field or type documentation or comment if doc is empty - children []*visitorNode // optional children nodes for structs - parent *visitorNode // parent node - typeRef *visitorNode // type reference if field is a struct - tag string // field tag - isArray bool // true if field is an array + kind nodeKind + typeName string // type name if node is a type or field type name if node is a field + packageName string // package name if node is a type + currentFile string // current file name + names []string // it's possible that a field has multiple names + doc string // field or type documentation or comment if doc is empty + children []*visitorNode // optional children nodes for structs + parent *visitorNode // parent node + typeRef *visitorNode // type reference if field is a struct + tag string // field tag + isArray bool // true if field is an array } type ( @@ -39,22 +41,25 @@ type ( ) type astVisitor struct { - commentHandler astCommentsHandler + commentHandler astCommentsHandler + logger *log.Logger + fileSet *token.FileSet + typeDocResolver astTypeDocResolver - logger *log.Logger + currentNode *visitorNode + pendingType bool // true if the next type is a target type + targetName string // name of the type we are looking for + depth int // current depth in the AST (used for debugging, 1 based) - currentNode *visitorNode - pendingType bool // true if the next type is a target type - targetName string // name of the type we are looking for - depth int // current depth in the AST (used for debugging, 1 based) + err error // error that occurred during AST traversal } -func newAstVisitor(commentsHandler astCommentsHandler, typeDocsResolver astTypeDocResolver) *astVisitor { +func newAstVisitor(commentsHandler astCommentsHandler, fileSet *token.FileSet) *astVisitor { return &astVisitor{ - commentHandler: commentsHandler, - typeDocResolver: typeDocsResolver, - logger: logger(), - depth: 1, + commentHandler: commentsHandler, + fileSet: fileSet, + logger: logger(), + depth: 1, } } @@ -78,11 +83,24 @@ func (v *astVisitor) Walk(n ast.Node) { v.resolveFieldTypes() } +func (v *astVisitor) Error() error { + return v.err +} + +func (v *astVisitor) setErr(err error) { + v.logger.Printf("ast(%d): error: %v", v.depth, err) + v.err = err +} + func (v *astVisitor) Visit(n ast.Node) ast.Visitor { if n == nil { return nil } + if v.err != nil { + return nil + } + v.logger.Printf("ast(%d): visit node (%T)", v.depth, n) if v.currentNode == nil { @@ -90,6 +108,18 @@ func (v *astVisitor) Visit(n ast.Node) ast.Visitor { } switch t := n.(type) { + case *ast.File: + v.logger.Printf("ast(%d): visit file %q", v.depth, t.Name) + v.currentNode.packageName = t.Name.Name + f := v.fileSet.File(t.Pos()) + v.currentNode.currentFile = f.Name() + typeDocResolver, err := newASTTypeDocResolver(v.fileSet, t) + if err != nil { + v.setErr(fmt.Errorf("new ast type doc resolver: %w", err)) + return nil + } + v.typeDocResolver = typeDocResolver + return v case *ast.Comment: v.logger.Printf("ast(%d): visit comment", v.depth) if !v.pendingType { @@ -106,10 +136,11 @@ func (v *astVisitor) Visit(n ast.Node) ast.Visitor { v.logger.Printf("ast(%d): detect target type: %q", v.depth, name) } typeNode := &visitorNode{ - names: []string{name}, - typeName: name, - kind: nodeType, - doc: doc, + names: []string{name}, + typeName: name, + packageName: v.currentNode.packageName, + kind: nodeType, + doc: doc, } return v.push(typeNode, true) case *ast.StructType: diff --git a/inspector.go b/inspector.go index f9148ce..8f69233 100644 --- a/inspector.go +++ b/inspector.go @@ -2,6 +2,7 @@ package main import ( "fmt" + "go/ast" "go/parser" "go/token" "log" @@ -28,22 +29,31 @@ func newInspector(typeName string, all bool, execLine int, useFieldNames bool) * func (i *inspector) inspectFile(fileName string) ([]*EnvScope, error) { fileSet := token.NewFileSet() - file, err := parser.ParseFile(fileSet, fileName, nil, parser.ParseComments) + var astFiles []*ast.File + astFile, err := parser.ParseFile(fileSet, fileName, nil, parser.ParseComments) if err != nil { return nil, fmt.Errorf("parse file: %w", err) } - docResolver, err := newASTTypeDocResolver(fileSet, file) - if err != nil { - return nil, fmt.Errorf("new ast type doc resolver: %w", err) - } + astFiles = append(astFiles, astFile) var commentsHandler astCommentsHandler if i.all { commentsHandler = astCommentDummyHandler } else { - commentsHandler = newASTCommentTargetLineHandler(i.execLine, fileSet.File(file.Pos()).Lines()) + commentsHandler = newASTCommentTargetLineHandler(i.execLine, fileSet.File(astFile.Pos()).Lines()) + } + visitor := newAstVisitor(commentsHandler, fileSet) + var verr error + for _, astFile := range astFiles { + visitor.Walk(astFile) + if err := visitor.Error(); err != nil { + verr = err + break + } } - visitor := newAstVisitor(commentsHandler, docResolver) - visitor.Walk(file) + if verr != nil { + return nil, fmt.Errorf("walk file: %w", verr) + } + targetName := i.typeName if targetName == "" { targetName = visitor.targetName From 5df0879befab33ea3f897d862da36dfb44629b71 Mon Sep 17 00:00:00 2001 From: "Kirill Che." Date: Sun, 30 Jun 2024 19:49:29 +0400 Subject: [PATCH 2/6] feat: multi-files structure parsers Reimplemented the AST logic to parse multiple files from single file-set and parse types documentation from the fileset. Ref: #23 --- _examples/all.md | 104 +++++++++ _examples/all_files.go | 2 + _examples/clean.sh | 2 +- _examples/envprefix.env | 6 +- _examples/envprefix.go | 2 +- _examples/envprefix.html | 6 +- _examples/envprefix.md | 6 +- _examples/envprefix.txt | 6 +- ast.go | 297 ------------------------- ast/collectors.go | 151 +++++++++++++ ast/errors.go | 11 + ast/field.go | 35 +++ ast/file.go | 54 +++++ ast/file_test.go | 41 ++++ ast/model.go | 97 ++++++++ ast/pkg.go | 41 ++++ ast/pkg_test.go | 44 ++++ ast/testdata/empty.go | 3 + ast/testdata/fields.go | 11 + ast/testdata/onetype.go | 4 + ast/testdata/twotypes.go | 8 + ast/testhelper.go | 41 ++++ ast/type.go | 40 ++++ ast/utils.go | 127 +++++++++++ ast/walker.go | 11 + ast_test.go | 19 -- codecov.yml | 11 - config.go | 176 +++++++++++++++ converter.go | 145 ++++++++++++ converter_test.go | 102 +++++++++ debug.go | 63 ++++++ generator.go | 109 --------- generator_test.go | 147 ------------ go.mod | 4 +- go.sum | 2 + inspector.go | 221 ------------------ inspector_test.go | 470 --------------------------------------- log.go | 26 --- log_test.go | 21 -- main.go | 149 +++---------- main_test.go | 196 ---------------- types.go => model.go | 0 nodekind_string.go | 27 --- parser.go | 94 ++++++++ render.go | 58 ++++- render_test.go | 279 ----------------------- resolver.go | 50 +++++ tags.go | 60 +++++ tags_test.go | 42 ++++ testfile.go | 9 + utils.go | 121 ++-------- utils_test.go | 62 ------ 52 files changed, 1683 insertions(+), 2130 deletions(-) create mode 100644 _examples/all.md create mode 100644 _examples/all_files.go delete mode 100644 ast.go create mode 100644 ast/collectors.go create mode 100644 ast/errors.go create mode 100644 ast/field.go create mode 100644 ast/file.go create mode 100644 ast/file_test.go create mode 100644 ast/model.go create mode 100644 ast/pkg.go create mode 100644 ast/pkg_test.go create mode 100644 ast/testdata/empty.go create mode 100644 ast/testdata/fields.go create mode 100644 ast/testdata/onetype.go create mode 100644 ast/testdata/twotypes.go create mode 100644 ast/testhelper.go create mode 100644 ast/type.go create mode 100644 ast/utils.go create mode 100644 ast/walker.go delete mode 100644 ast_test.go delete mode 100644 codecov.yml create mode 100644 config.go create mode 100644 converter.go create mode 100644 converter_test.go create mode 100644 debug.go delete mode 100644 generator.go delete mode 100644 generator_test.go create mode 100644 go.sum delete mode 100644 inspector.go delete mode 100644 inspector_test.go delete mode 100644 log.go delete mode 100644 log_test.go delete mode 100644 main_test.go rename types.go => model.go (100%) delete mode 100644 nodekind_string.go create mode 100644 parser.go delete mode 100644 render_test.go create mode 100644 resolver.go create mode 100644 tags.go create mode 100644 tags_test.go create mode 100644 testfile.go delete mode 100644 utils_test.go diff --git a/_examples/all.md b/_examples/all.md new file mode 100644 index 0000000..44a18ec --- /dev/null +++ b/_examples/all.md @@ -0,0 +1,104 @@ +# Environment Variables + +## ComplexConfig + +ComplexConfig is an example configuration structure. +It contains a few fields with different types of tags. +It is trying to cover all the possible cases. + + - `SECRET` (from-file) - Secret is a secret value that is read from a file. + - `PASSWORD` (from-file, default: `/tmp/password`) - Password is a password that is read from a file. + - `CERTIFICATE` (expand, from-file, default: `${CERTIFICATE_FILE}`) - Certificate is a certificate that is read from a file. + - `SECRET_KEY` (**required**) - Key is a secret key. + - `SECRET_VAL` (**required**, non-empty) - SecretVal is a secret value. + - `HOSTS` (separated by `:`, **required**) - Hosts is a list of hosts. + - `WORDS` (comma-separated, from-file, default: `one,two,three`) - Words is just a list of words. + - `COMMENT` (**required**, default: `This is a comment.`) - Just a comment. + - Anon is an anonymous structure. + - `ANON_USER` (**required**) - User is a user name. + - `ANON_PASS` (**required**) - Pass is a password. + +## NextConfig + + - `MOUNT` (**required**) - Mount is a mount point. + +## FieldNames + +FieldNames uses field names as env names. + + - `QUUX` - Quux is a field with a tag. + +## Config + + - `HOST` (separated by `;`, **required**) - Hosts name of hosts to listen on. + - `PORT` (**required**, non-empty) - Port to listen on. + - `DEBUG` (default: `false`) - Debug mode enabled. + - `PREFIX` - Prefix for something. + +## Config + + - `START` (**required**, non-empty) - Start date. + +## Date + +Date is a time.Time wrapper that uses the time.DateOnly layout. + + +## Settings + +Settings is the application settings. + + - Database is the database settings + - `DB_PORT` (**required**) - Port is the port to connect to + - `DB_HOST` (**required**, non-empty, default: `localhost`) - Host is the host to connect to + - `DB_USER` - User is the user to connect as + - `DB_PASSWORD` - Password is the password to use + - `DB_DISABLE_TLS` - DisableTLS is the flag to disable TLS + - Server is the server settings + - `SERVER_PORT` (**required**) - Port is the port to listen on + - `SERVER_HOST` (**required**, non-empty, default: `localhost`) - Host is the host to listen on + - Timeout is the timeout settings + - `SERVER_TIMEOUT_READ` (default: `30`) - Read is the read timeout + - `SERVER_TIMEOUT_WRITE` (default: `30`) - Write is the write timeout + - `DEBUG` - Debug is the debug flag + +## Database + +Database is the database settings. + + - `PORT` (**required**) - Port is the port to connect to + - `HOST` (**required**, non-empty, default: `localhost`) - Host is the host to connect to + - `USER` - User is the user to connect as + - `PASSWORD` - Password is the password to use + - `DISABLE_TLS` - DisableTLS is the flag to disable TLS + +## ServerConfig + +ServerConfig is the server settings. + + - `PORT` (**required**) - Port is the port to listen on + - `HOST` (**required**, non-empty, default: `localhost`) - Host is the host to listen on + - Timeout is the timeout settings + - `TIMEOUT_READ` (default: `30`) - Read is the read timeout + - `TIMEOUT_WRITE` (default: `30`) - Write is the write timeout + +## TimeoutConfig + +TimeoutConfig is the timeout settings. + + - `READ` (default: `30`) - Read is the read timeout + - `WRITE` (default: `30`) - Write is the write timeout + +## Config + + - `START` - Start date. + +## Date + +Date is a time.Time wrapper that uses the time.DateOnly layout. + + +## appconfig + + - `PORT` (default: `8080`) - Port the application will listen on inside the container + diff --git a/_examples/all_files.go b/_examples/all_files.go new file mode 100644 index 0000000..9f12052 --- /dev/null +++ b/_examples/all_files.go @@ -0,0 +1,2 @@ +//go:generate go run ../ -output all.md -dir . -files * -types * +package main diff --git a/_examples/clean.sh b/_examples/clean.sh index 3dd60ab..ce1fd9e 100755 --- a/_examples/clean.sh +++ b/_examples/clean.sh @@ -3,4 +3,4 @@ set -euo pipefail cd ${0%/*} -find . -type f \( -name "*.md" -or -name '*.txt' -or -name '*.html' \) ! -name "README.md" -exec rm -v {} \; +find . -type f \( -name "*.md" -or -name '*.txt' -or -name '*.html' -or -name '*.env' \) ! -name "README.md" -exec rm -v {} \; diff --git a/_examples/envprefix.env b/_examples/envprefix.env index 52550c1..acdc7ad 100644 --- a/_examples/envprefix.env +++ b/_examples/envprefix.env @@ -5,7 +5,7 @@ ## Settings is the application settings. # # -## Database is the database settings. +## Database is the database settings # ## Port is the port to connect to ## (required) @@ -20,7 +20,7 @@ ## DisableTLS is the flag to disable TLS # DB_DISABLE_TLS="" # -## ServerConfig is the server settings. +## Server is the server settings # ## Port is the port to listen on ## (required) @@ -29,7 +29,7 @@ ## (required, non-empty, default: 'localhost') # SERVER_HOST="localhost" # -## TimeoutConfig is the timeout settings. +## Timeout is the timeout settings # ## Read is the read timeout ## (default: '30') diff --git a/_examples/envprefix.go b/_examples/envprefix.go index 553784a..2838586 100644 --- a/_examples/envprefix.go +++ b/_examples/envprefix.go @@ -2,10 +2,10 @@ package main // Settings is the application settings. // -//go:generate go run ../ -output envprefix.txt -format plaintext -type Settings //go:generate go run ../ -output envprefix.md -type Settings //go:generate go run ../ -output envprefix.html -format html -type Settings //go:generate go run ../ -output envprefix.env -format dotenv -type Settings +//go:generate go run ../ -output envprefix.txt -format plaintext -types Settings -debug type Settings struct { // Database is the database settings Database Database `envPrefix:"DB_"` diff --git a/_examples/envprefix.html b/_examples/envprefix.html index 9565723..bfc176d 100644 --- a/_examples/envprefix.html +++ b/_examples/envprefix.html @@ -79,7 +79,7 @@

Environment Variables

Settings

Settings is the application settings.

NextConfig

@@ -107,6 +103,7 @@

FieldNames

FieldNames uses field names as env names.

diff --git a/_examples/complex.md b/_examples/complex.md index 4c41e7b..4fa46fa 100644 --- a/_examples/complex.md +++ b/_examples/complex.md @@ -12,11 +12,10 @@ It is trying to cover all the possible cases. - `SECRET_KEY` (**required**) - Key is a secret key. - `SECRET_VAL` (**required**, non-empty) - SecretVal is a secret value. - `HOSTS` (separated by `:`, **required**) - Hosts is a list of hosts. - - `WORDS` (comma-separated, from-file, default: `one,two,three`) - Words is just a list of words. + - `WORDS` (comma-separated, from-file, default: `one`) - Words is just a list of words. - `COMMENT` (**required**, default: `This is a comment.`) - Just a comment. - - Anon is an anonymous structure. - - `ANON_USER` (**required**) - User is a user name. - - `ANON_PASS` (**required**) - Pass is a password. + - `ANON_USER` (**required**) - User is a user name. + - `ANON_PASS` (**required**) - Pass is a password. ## NextConfig @@ -27,4 +26,5 @@ It is trying to cover all the possible cases. FieldNames uses field names as env names. - `QUUX` - Quux is a field with a tag. + - Required is a required field. diff --git a/_examples/complex.txt b/_examples/complex.txt index 1f2a398..38ed385 100644 --- a/_examples/complex.txt +++ b/_examples/complex.txt @@ -12,11 +12,10 @@ It is trying to cover all the possible cases. * `SECRET_KEY` (required) - Key is a secret key. * `SECRET_VAL` (required, non-empty) - SecretVal is a secret value. * `HOSTS` (separated by `:`, required) - Hosts is a list of hosts. - * `WORDS` (comma-separated, from-file, default: `one,two,three`) - Words is just a list of words. + * `WORDS` (comma-separated, from-file, default: `one`) - Words is just a list of words. * `COMMENT` (required, default: `This is a comment.`) - Just a comment. - * Anon is an anonymous structure. - * `ANON_USER` (required) - User is a user name. - * `ANON_PASS` (required) - Pass is a password. + * `ANON_USER` (required) - User is a user name. + * `ANON_PASS` (required) - Pass is a password. ## NextConfig @@ -27,4 +26,5 @@ It is trying to cover all the possible cases. FieldNames uses field names as env names. * `QUUX` - Quux is a field with a tag. + * Required is a required field. diff --git a/_examples/config.env b/_examples/config.env index 4d32876..d22e3fb 100644 --- a/_examples/config.env +++ b/_examples/config.env @@ -1,20 +1,3 @@ # Environment Variables -## Config -## Config is an example configuration structure. -## It is used to generate documentation for the configuration -## using the commands below. -# -## Hosts name of hosts to listen on. -## (separated by ';', required) -# HOST="" -## Port to listen on. -## (required, non-empty) -# PORT="" -## Debug mode enabled. -## (default: 'false') -# DEBUG="false" -## Prefix for something. -# PREFIX="" - diff --git a/_examples/config.html b/_examples/config.html index b16feb0..d87b314 100644 --- a/_examples/config.html +++ b/_examples/config.html @@ -76,17 +76,6 @@

Environment Variables

-

Config

-

Config is an example configuration structure. -It is used to generate documentation for the configuration -using the commands below.

-
    -
  • HOST (separated by ";", required) - Hosts name of hosts to listen on.
  • -
  • PORT (required, non-empty) - Port to listen on.
  • -
  • DEBUG (default: false) - Debug mode enabled.
  • -
  • PREFIX - Prefix for something.
  • -
-
diff --git a/_examples/config.md b/_examples/config.md index 7a8f82f..e3db20b 100644 --- a/_examples/config.md +++ b/_examples/config.md @@ -1,13 +1,2 @@ # Environment Variables -## Config - -Config is an example configuration structure. -It is used to generate documentation for the configuration -using the commands below. - - - `HOST` (separated by `;`, **required**) - Hosts name of hosts to listen on. - - `PORT` (**required**, non-empty) - Port to listen on. - - `DEBUG` (default: `false`) - Debug mode enabled. - - `PREFIX` - Prefix for something. - diff --git a/_examples/config.txt b/_examples/config.txt index 5eb7ebe..5d2d30d 100644 --- a/_examples/config.txt +++ b/_examples/config.txt @@ -1,13 +1,2 @@ Environment Variables -## Config - -Config is an example configuration structure. -It is used to generate documentation for the configuration -using the commands below. - - * `HOST` (separated by `;`, required) - Hosts name of hosts to listen on. - * `PORT` (required, non-empty) - Port to listen on. - * `DEBUG` (default: `false`) - Debug mode enabled. - * `PREFIX` - Prefix for something. - diff --git a/_examples/embedded.md b/_examples/embedded.md index 13a0c08..e3db20b 100644 --- a/_examples/embedded.md +++ b/_examples/embedded.md @@ -1,6 +1,2 @@ # Environment Variables -## Config - - - `START` (**required**, non-empty) - Start date. - diff --git a/_examples/envprefix.env b/_examples/envprefix.env index acdc7ad..572bd98 100644 --- a/_examples/envprefix.env +++ b/_examples/envprefix.env @@ -4,9 +4,6 @@ ## Settings ## Settings is the application settings. # -# -## Database is the database settings -# ## Port is the port to connect to ## (required) # DB_PORT="" @@ -19,18 +16,12 @@ # DB_PASSWORD="" ## DisableTLS is the flag to disable TLS # DB_DISABLE_TLS="" -# -## Server is the server settings -# ## Port is the port to listen on ## (required) # SERVER_PORT="" ## Host is the host to listen on ## (required, non-empty, default: 'localhost') # SERVER_HOST="localhost" -# -## Timeout is the timeout settings -# ## Read is the read timeout ## (default: '30') # SERVER_TIMEOUT_READ="30" diff --git a/_examples/envprefix.html b/_examples/envprefix.html index bfc176d..d9f8df3 100644 --- a/_examples/envprefix.html +++ b/_examples/envprefix.html @@ -79,27 +79,15 @@

Environment Variables

Settings

Settings is the application settings.

diff --git a/_examples/envprefix.md b/_examples/envprefix.md index a7bf087..a71dad3 100644 --- a/_examples/envprefix.md +++ b/_examples/envprefix.md @@ -4,17 +4,14 @@ Settings is the application settings. - - Database is the database settings - - `DB_PORT` (**required**) - Port is the port to connect to - - `DB_HOST` (**required**, non-empty, default: `localhost`) - Host is the host to connect to - - `DB_USER` - User is the user to connect as - - `DB_PASSWORD` - Password is the password to use - - `DB_DISABLE_TLS` - DisableTLS is the flag to disable TLS - - Server is the server settings - - `SERVER_PORT` (**required**) - Port is the port to listen on - - `SERVER_HOST` (**required**, non-empty, default: `localhost`) - Host is the host to listen on - - Timeout is the timeout settings - - `SERVER_TIMEOUT_READ` (default: `30`) - Read is the read timeout - - `SERVER_TIMEOUT_WRITE` (default: `30`) - Write is the write timeout + - `DB_PORT` (**required**) - Port is the port to connect to + - `DB_HOST` (**required**, non-empty, default: `localhost`) - Host is the host to connect to + - `DB_USER` - User is the user to connect as + - `DB_PASSWORD` - Password is the password to use + - `DB_DISABLE_TLS` - DisableTLS is the flag to disable TLS + - `SERVER_PORT` (**required**) - Port is the port to listen on + - `SERVER_HOST` (**required**, non-empty, default: `localhost`) - Host is the host to listen on + - `SERVER_TIMEOUT_READ` (default: `30`) - Read is the read timeout + - `SERVER_TIMEOUT_WRITE` (default: `30`) - Write is the write timeout - `DEBUG` - Debug is the debug flag diff --git a/_examples/envprefix.txt b/_examples/envprefix.txt index 9aafba5..2130a6b 100644 --- a/_examples/envprefix.txt +++ b/_examples/envprefix.txt @@ -4,17 +4,14 @@ Environment Variables Settings is the application settings. - * Database is the database settings - * `DB_PORT` (required) - Port is the port to connect to - * `DB_HOST` (required, non-empty, default: `localhost`) - Host is the host to connect to - * `DB_USER` - User is the user to connect as - * `DB_PASSWORD` - Password is the password to use - * `DB_DISABLE_TLS` - DisableTLS is the flag to disable TLS - * Server is the server settings - * `SERVER_PORT` (required) - Port is the port to listen on - * `SERVER_HOST` (required, non-empty, default: `localhost`) - Host is the host to listen on - * Timeout is the timeout settings - * `SERVER_TIMEOUT_READ` (default: `30`) - Read is the read timeout - * `SERVER_TIMEOUT_WRITE` (default: `30`) - Write is the write timeout + * `DB_PORT` (required) - Port is the port to connect to + * `DB_HOST` (required, non-empty, default: `localhost`) - Host is the host to connect to + * `DB_USER` - User is the user to connect as + * `DB_PASSWORD` - Password is the password to use + * `DB_DISABLE_TLS` - DisableTLS is the flag to disable TLS + * `SERVER_PORT` (required) - Port is the port to listen on + * `SERVER_HOST` (required, non-empty, default: `localhost`) - Host is the host to listen on + * `SERVER_TIMEOUT_READ` (default: `30`) - Read is the read timeout + * `SERVER_TIMEOUT_WRITE` (default: `30`) - Write is the write timeout * `DEBUG` - Debug is the debug flag diff --git a/_examples/project/config.md b/_examples/project/config.md new file mode 100644 index 0000000..5ca04fa --- /dev/null +++ b/_examples/project/config.md @@ -0,0 +1,16 @@ +# Environment Variables + +## Config + + - `APP_NAME` (default: `myapp`) - AppName is the name of the application. + - `SERVER_HOST` (**required**) - Host of the server. + - `SERVER_PORT` (**required**) - Port of the server. + - `SERVER_TIMEOUT_READ` (**required**) - ReadTimeout of the server. + - `SERVER_TIMEOUT_WRITE` (**required**) - WriteTimeout of the server. + - `DB_HOST` (**required**) - Host of the database. + - `DB_PORT` (**required**) - Port of the database. + - `DB_USER` (default: `user`) - User of the database. + - `DB_PASSWORD` - Password of the database. + - `LOG_LEVEL` (default: `info`) - Level of the logging. + - `LOG_FORMAT` (default: `json`) - Format of the logging. + diff --git a/_examples/project/config/cfg.go b/_examples/project/config/cfg.go new file mode 100644 index 0000000..40094b6 --- /dev/null +++ b/_examples/project/config/cfg.go @@ -0,0 +1,21 @@ +package config + +import ( + "example.com/db" + "example.com/server" +) + +//go:generate envdoc -dir ../ -files ./config/cfg.go -types * -output ../config.md -format markdown +type Config struct { + // AppName is the name of the application. + AppName string `env:"APP_NAME" envDefault:"myapp"` + + // Server config. + Server server.Config `envPrefix:"SERVER_"` + + // Database config. + Database db.Config `envPrefix:"DB_"` + + // Logging config. + Logging Logging `envPrefix:"LOG_"` +} diff --git a/_examples/project/config/logging.go b/_examples/project/config/logging.go new file mode 100644 index 0000000..156693e --- /dev/null +++ b/_examples/project/config/logging.go @@ -0,0 +1,8 @@ +package config + +type Logging struct { + // Level of the logging. + Level string `env:"LEVEL" envDefault:"info"` + // Format of the logging. + Format string `env:"FORMAT" envDefault:"json"` +} diff --git a/_examples/project/db/cfg.go b/_examples/project/db/cfg.go new file mode 100644 index 0000000..546c85f --- /dev/null +++ b/_examples/project/db/cfg.go @@ -0,0 +1,25 @@ +package db + +// Config holds the configuration for the database. +type Config struct { + // Host of the database. + Host string `env:"HOST,required"` + // Port of the database. + Port string `env:"PORT,required"` + // User of the database. + User string `env:"USER" envDefault:"user"` + // Password of the database. + Password string `env:"PASSWORD,nonempty"` + + SslConfig `envPrefix:"SSL_"` +} + +// SslConfig holds the configuration for the SSL of the database. +type SslConfig struct { + // SslMode of the database. + SslMode string `env:"MODE" envDefault:"disable"` + // SslCert of the database. + SslCert string `env:"CERT"` + // SslKey of the database. + SslKey string `env:"KEY"` +} diff --git a/_examples/project/go.mod b/_examples/project/go.mod new file mode 100644 index 0000000..29186e6 --- /dev/null +++ b/_examples/project/go.mod @@ -0,0 +1,3 @@ +module example.com + +go 1.22.5 diff --git a/_examples/project/server/cfg.go b/_examples/project/server/cfg.go new file mode 100644 index 0000000..de1303c --- /dev/null +++ b/_examples/project/server/cfg.go @@ -0,0 +1,10 @@ +package server + +type Config struct { + // Host of the server. + Host string `env:"HOST,required"` + // Port of the server. + Port string `env:"PORT,required"` + // Timeout of the server. + Timeout TimeoutConfig `envPrefix:"TIMEOUT_"` +} diff --git a/_examples/project/server/timeout_cfg.go b/_examples/project/server/timeout_cfg.go new file mode 100644 index 0000000..864cce0 --- /dev/null +++ b/_examples/project/server/timeout_cfg.go @@ -0,0 +1,9 @@ +package server + +// TimeoutConfig holds the configuration for the timeouts of the server. +type TimeoutConfig struct { + // ReadTimeout of the server. + ReadTimeout string `env:"READ,required"` + // WriteTimeout of the server. + WriteTimeout string `env:"WRITE,required"` +} diff --git a/_examples/typedef.md b/_examples/typedef.md index 52c41c0..e3db20b 100644 --- a/_examples/typedef.md +++ b/_examples/typedef.md @@ -1,6 +1,2 @@ # Environment Variables -## Config - - - `START` - Start date. - diff --git a/_examples/unexported.md b/_examples/unexported.md index 7799201..e3db20b 100644 --- a/_examples/unexported.md +++ b/_examples/unexported.md @@ -1,6 +1,2 @@ # Environment Variables -## appconfig - - - `PORT` (default: `8080`) - Port the application will listen on inside the container - diff --git a/_examples/x_complex.md b/_examples/x_complex.md index 9f62d97..0baa2ce 100644 --- a/_examples/x_complex.md +++ b/_examples/x_complex.md @@ -12,11 +12,10 @@ It is trying to cover all the possible cases. - `X_SECRET_KEY` (**required**) - Key is a secret key. - `X_SECRET_VAL` (**required**, non-empty) - SecretVal is a secret value. - `X_HOSTS` (separated by `:`, **required**) - Hosts is a list of hosts. - - `X_WORDS` (comma-separated, from-file, default: `one,two,three`) - Words is just a list of words. + - `X_WORDS` (comma-separated, from-file, default: `one`) - Words is just a list of words. - `X_COMMENT` (**required**, default: `This is a comment.`) - Just a comment. - - `X_` - Anon is an anonymous structure. - - `X_ANON_USER` (**required**) - User is a user name. - - `X_ANON_PASS` (**required**) - Pass is a password. + - `X_ANON_USER` (**required**) - User is a user name. + - `X_ANON_PASS` (**required**) - Pass is a password. ## NextConfig @@ -27,4 +26,5 @@ It is trying to cover all the possible cases. FieldNames uses field names as env names. - `X_QUUX` - Quux is a field with a tag. + - `X_` (**required**) - Required is a required field. diff --git a/ast/collectors.go b/ast/collectors.go index 7c030f5..4e7be93 100644 --- a/ast/collectors.go +++ b/ast/collectors.go @@ -1,5 +1,12 @@ package ast +import ( + "sort" + "strings" + + "github.com/g4s8/envdoc/debug" +) + type RootCollectorOption func(*RootCollector) func WithFileGlob(glob func(string) bool) RootCollectorOption { @@ -41,6 +48,7 @@ var ( ) type RootCollector struct { + baseDir string fileGlob func(string) bool typeGlob func(string) bool gogenDecl *struct { @@ -57,8 +65,9 @@ type RootCollector struct { var globAcceptAll = func(string) bool { return true } -func NewRootCollector(opts ...RootCollectorOption) *RootCollector { +func NewRootCollector(baseDir string, opts ...RootCollectorOption) *RootCollector { c := &RootCollector{ + baseDir: baseDir, fileGlob: globAcceptAll, typeGlob: globAcceptAll, } @@ -69,16 +78,33 @@ func NewRootCollector(opts ...RootCollectorOption) *RootCollector { } func (c *RootCollector) Files() []*FileSpec { - return c.files + // order by file name + res := make([]*FileSpec, len(c.files)) + copy(res, c.files) + sort.Slice(res, func(i, j int) bool { + return res[i].Name < res[j].Name + }) + return res } func (c *RootCollector) onFile(f *FileSpec) interface { TypeHandler CommentHandler } { + // convert file name to relative path using baseDir + // if baseDir is empty or `.` then the file name is used as is. + name := f.Name + if c.baseDir != "" && c.baseDir != "." { + name, _ = strings.CutPrefix(name, c.baseDir) + name, _ = strings.CutPrefix(name, "/") + name = "./" + name + } + f.Name = name + if c.fileGlob(f.Name) { f.Export = true } + debug.Logf("# COL: file %q, export=%t\n", f.Name, f.Export) c.files = append(c.files, f) return c } diff --git a/ast/fieldtyperefkind_scan.go b/ast/fieldtyperefkind_scan.go new file mode 100644 index 0000000..c78cafe --- /dev/null +++ b/ast/fieldtyperefkind_scan.go @@ -0,0 +1,12 @@ +package ast + +func (r *FieldTypeRefKind) ScanStr(s string) bool { + for i := 0; i < len(_FieldTypeRefKind_index)-1; i++ { + from, to := _FieldTypeRefKind_index[i], _FieldTypeRefKind_index[i+1] + if s == _FieldTypeRefKind_name[from:to] { + *r = FieldTypeRefKind(i) + return true + } + } + return false +} diff --git a/ast/fieldtyperefkind_string.go b/ast/fieldtyperefkind_string.go new file mode 100644 index 0000000..4f98974 --- /dev/null +++ b/ast/fieldtyperefkind_string.go @@ -0,0 +1,28 @@ +// Code generated by "stringer -type=FieldTypeRefKind -trimprefix=FieldType"; DO NOT EDIT. + +package ast + +import "strconv" + +func _() { + // An "invalid array index" compiler error signifies that the constant values have changed. + // Re-run the stringer command to generate them again. + var x [1]struct{} + _ = x[FieldTypeIdent-0] + _ = x[FieldTypeSelector-1] + _ = x[FieldTypePtr-2] + _ = x[FieldTypeArray-3] + _ = x[FieldTypeMap-4] + _ = x[FieldTypeStruct-5] +} + +const _FieldTypeRefKind_name = "IdentSelectorPtrArrayMapStruct" + +var _FieldTypeRefKind_index = [...]uint8{0, 5, 13, 16, 21, 24, 30} + +func (i FieldTypeRefKind) String() string { + if i < 0 || i >= FieldTypeRefKind(len(_FieldTypeRefKind_index)-1) { + return "FieldTypeRefKind(" + strconv.FormatInt(int64(i), 10) + ")" + } + return _FieldTypeRefKind_name[_FieldTypeRefKind_index[i]:_FieldTypeRefKind_index[i+1]] +} diff --git a/ast/file_test.go b/ast/file_test.go index 1544787..ac21d34 100644 --- a/ast/file_test.go +++ b/ast/file_test.go @@ -5,37 +5,25 @@ import ( "testing" ) -type testFileVisitorHandler struct { - comments []*CommentSpec - types []*TypeSpec -} - -func (h *testFileVisitorHandler) setComment(c *CommentSpec) { - h.comments = append(h.comments, c) -} - -func (h *testFileVisitorHandler) onType(t *TypeSpec) typeVisitorHandler { - h.types = append(h.types, t) - return nil -} - func TestFileVisitor(t *testing.T) { fset, pkg, docs := loadTestFileSet(t) - h := &testFileVisitorHandler{} - file := pkg.Files["testdata/onetype.go"] - v := newFileVisitor(fset, file, docs, h) - ast.Walk(v, file) + fh, fv, file := testFileVisitor(fset, pkg, "testdata/onetype.go", docs) + ast.Walk(fv, file) - if expect, actual := 1, len(h.comments); expect != actual { + if expect, actual := 1, len(fh.comments); expect != actual { t.Fatalf("expected %d comments, got %d", expect, actual) } checkCommentsEq(t, &CommentSpec{ Text: "onetype", - }, h.comments[0]) - if expect, actual := 1, len(h.types); expect != actual { + }, fh.comments[0]) + types := make([]*TypeSpec, 0) + for _, f := range fh.files { + types = append(types, f.Types...) + } + if expect, actual := 1, len(types); expect != actual { t.Fatalf("expected %d types, got %d", expect, actual) } - if expect, actual := "One", h.types[0].Name; expect != actual { + if expect, actual := "One", types[0].Name; expect != actual { t.Fatalf("expected type name %q, got %q", expect, actual) } } diff --git a/ast/model.go b/ast/model.go index 1fbe14f..ae2340a 100644 --- a/ast/model.go +++ b/ast/model.go @@ -1,5 +1,8 @@ package ast +import "strings" + +//go:generate stringer -type=FieldTypeRefKind -trimprefix=FieldType type FieldTypeRefKind int const ( @@ -95,3 +98,15 @@ func (tr FieldTypeRef) String() string { } return "" } + +func (fs *FileSpec) String() string { + return fs.Name +} + +func (ts *TypeSpec) String() string { + return ts.Name +} + +func (fs *FieldSpec) String() string { + return strings.Join(fs.Names, ", ") +} diff --git a/ast/pkg_test.go b/ast/pkg_test.go index f1723c3..0964b82 100644 --- a/ast/pkg_test.go +++ b/ast/pkg_test.go @@ -6,15 +6,6 @@ import ( "testing" ) -type testFileHandler struct { - files []*FileSpec -} - -func (h *testFileHandler) onFile(f *FileSpec) fileVisitorHandler { - h.files = append(h.files, f) - return nil -} - func TestPkgVisitor(t *testing.T) { fset, pkg, _ := loadTestFileSet(t) h := &testFileHandler{} diff --git a/ast/testhelper.go b/ast/testhelper.go index f4fd778..39ae738 100644 --- a/ast/testhelper.go +++ b/ast/testhelper.go @@ -39,3 +39,86 @@ func checkCommentsEq(t T, expect, actual *CommentSpec) { t.Fatalf("expected comment line %d, got %d", expect.Line, actual.Line) } } + +var ( + _ TypeHandler = (*testTypeHandler)(nil) + _ CommentHandler = (*testTypeHandler)(nil) + _ FileHandler = (*testFileHandler)(nil) +) + +type TestTypeHandler interface { + TypeHandler + CommentHandler + + Types() []*TypeSpec +} + +type testCommentHandler struct { + comments []*CommentSpec +} + +func (h *testCommentHandler) setComment(c *CommentSpec) { + h.comments = append(h.comments, c) +} + +type testSubfieldHandler struct { + f *FieldSpec +} + +func (h *testSubfieldHandler) onField(f *FieldSpec) FieldHandler { + h.f.Fields = append(h.f.Fields, f) + return &testSubfieldHandler{f: f} +} + +type testFieldHandler struct { + testCommentHandler + t *TypeSpec +} + +func (h *testFieldHandler) onField(f *FieldSpec) FieldHandler { + h.t.Fields = append(h.t.Fields, f) + return &testSubfieldHandler{f: f} +} + +type testTypeHandler struct { + testCommentHandler + f *FileSpec +} + +func (h *testTypeHandler) onType(t *TypeSpec) typeVisitorHandler { + h.f.Types = append(h.f.Types, t) + return &testFieldHandler{t: t} +} + +func (h *testTypeHandler) Types() []*TypeSpec { + return h.f.Types +} + +type testFileHandler struct { + testCommentHandler + files []*FileSpec +} + +func (h *testFileHandler) onFile(f *FileSpec) interface { + TypeHandler + CommentHandler +} { + h.files = append(h.files, f) + return &testTypeHandler{f: f} +} + +// func loadTestFileSet(t T) (*token.FileSet, *ast.Package, *doc.Package) { +func testFileVisitor(fset *token.FileSet, pkg *ast.Package, fileName string, + docs *doc.Package, +) (*testFileHandler, *fileVisitor, *ast.File) { + fileAst := pkg.Files[fileName] + fileTkn := fset.File(fileAst.Pos()) + fileSpec := &FileSpec{ + Name: fileTkn.Name(), + Pkg: pkg.Name, + } + fh := &testFileHandler{} + th := fh.onFile(fileSpec) + fv := newFileVisitor(fset, fileAst, docs, th) + return fh, fv, fileAst +} diff --git a/ast/types_test.go b/ast/types_test.go new file mode 100644 index 0000000..d087d1a --- /dev/null +++ b/ast/types_test.go @@ -0,0 +1,57 @@ +package ast + +import ( + "go/ast" + "testing" +) + +type fileHandler struct { + types []*TypeSpec + typeH *typeHandler +} + +func (h *fileHandler) setComment(c *CommentSpec) { +} + +func (h *fileHandler) onType(t *TypeSpec) typeVisitorHandler { + h.types = append(h.types, t) + h.typeH = &typeHandler{} + return h.typeH +} + +type typeHandler struct { + fields []*FieldSpec + comments []*CommentSpec +} + +func (h *typeHandler) setComment(c *CommentSpec) { + h.comments = append(h.comments, c) +} + +func (h *typeHandler) onField(f *FieldSpec) FieldHandler { + h.fields = append(h.fields, f) + return nil +} + +func TestTypesVisitor(t *testing.T) { + fset, pkg, docs := loadTestFileSet(t) + file := pkg.Files["testdata/fields.go"] + h := &fileHandler{} + v := newFileVisitor(fset, file, docs, h) + + ast.Walk(v, file) + + fh := h.typeH + if expect, actual := 3, len(fh.fields); expect != actual { + t.Fatalf("expected %d fields, got %d", expect, actual) + } + if expect, actual := "A", fh.fields[0].Names[0]; expect != actual { + t.Fatalf("expected field name %q, got %q", expect, actual) + } + if expect, actual := "B", fh.fields[1].Names[0]; expect != actual { + t.Fatalf("expected field name %q, got %q", expect, actual) + } + if expect, actual := "C", fh.fields[2].Names[0]; expect != actual { + t.Fatalf("expected field name %q, got %q", expect, actual) + } +} diff --git a/ast/utils.go b/ast/utils.go index 4b1af4e..80a8258 100644 --- a/ast/utils.go +++ b/ast/utils.go @@ -1,11 +1,12 @@ package ast import ( - "fmt" "go/ast" "go/doc" "go/token" "strings" + + "github.com/g4s8/envdoc/debug" ) func getFieldTypeRef(f ast.Expr, ref *FieldTypeRef) bool { @@ -76,39 +77,38 @@ func getFieldSpec(n *ast.Field, pkg string) *FieldSpec { fs.Doc = doc } if tag := n.Tag; tag != nil { - fs.Tag = tag.Value + fs.Tag = strings.Trim(tag.Value, "`") } return &fs } -const debugLogs = false - func debugNode(src string, n ast.Node) { - if !debugLogs { + if !debug.Config.Enabled { return } if n == nil { return } + switch t := n.(type) { case *ast.File: - fmt.Printf("# AST(%s): File pkg=%q\n", src, t.Name.Name) + debug.Logf("# AST(%s): File pkg=%q\n", src, t.Name.Name) case *ast.Package: - fmt.Printf("# AST(%s): Package %s\n", src, t.Name) + debug.Logf("# AST(%s): Package %s\n", src, t.Name) case *ast.TypeSpec: - fmt.Printf("# AST(%s): Type %s\n", src, t.Name.Name) + debug.Logf("# AST(%s): Type %s\n", src, t.Name.Name) case *ast.Field: names := extractFieldNames(t) - fmt.Printf("# AST(%s): Field %s\n", src, strings.Join(names, ", ")) + debug.Logf("# AST(%s): Field %s\n", src, strings.Join(names, ", ")) case *ast.Comment: - fmt.Printf("# AST(%s): Comment %s\n", src, t.Text) + debug.Logf("# AST(%s): Comment %s\n", src, t.Text) case *ast.StructType: - fmt.Printf("# AST(%s): Struct\n", src) + debug.Logf("# AST(%s): Struct\n", src) case *ast.GenDecl, *ast.Ident, *ast.FuncDecl: // ignore default: - fmt.Printf("# AST(%s): %T\n", src, t) + debug.Logf("# AST(%s): %T\n", src, t) } } diff --git a/converter.go b/converter.go index c086c1a..83e1eb2 100644 --- a/converter.go +++ b/converter.go @@ -3,8 +3,10 @@ package main import ( "fmt" "os" + "strings" "github.com/g4s8/envdoc/ast" + "github.com/g4s8/envdoc/debug" ) type Converter struct { @@ -23,10 +25,12 @@ func (c *Converter) ScopesFromFiles(res *TypeResolver, files []*ast.FileSpec) [] var scopes []*EnvScope for _, f := range files { if !f.Export { + debug.Logf("# CONV: skip file %q\n", f.Name) continue } for _, t := range f.Types { if !t.Export { + debug.Logf("# CONV: skip type %q\n", t.Name) continue } scopes = append(scopes, c.ScopeFromType(res, t)) @@ -41,12 +45,14 @@ func (c *Converter) ScopeFromType(res *TypeResolver, t *ast.TypeSpec) *EnvScope Doc: t.Doc, } scope.Vars = c.DocItemsFromFields(res, c.envPrefix, t.Fields) + debug.Logf("# CONV: found scope %q\n", scope.Name) return scope } func (c *Converter) DocItemsFromFields(res *TypeResolver, prefix string, fields []*ast.FieldSpec) []*EnvDocItem { var items []*EnvDocItem for _, f := range fields { + debug.Logf("\t# CONV: field [%s]\n", strings.Join(f.Names, ",")) if len(f.Names) == 0 { // embedded field items = append(items, c.DocItemsFromFields(res, prefix, f.Fields)...) @@ -69,9 +75,9 @@ func (c *Converter) DocItemsFromField(resolver *TypeResolver, prefix string, f * } } for i, name := range names { - if name == "" { - continue - } + // if name == "" { + // continue + // } names[i] = prefix + name } @@ -112,6 +118,7 @@ func (c *Converter) DocItemsFromField(resolver *TypeResolver, prefix string, f * switch f.TypeRef.Kind { case ast.FieldTypeStruct: children = c.DocItemsFromFields(resolver, prefix, f.Fields) + debug.Logf("\t# CONV: struct %q (%d childrens)\n", f.TypeRef.String(), len(children)) case ast.FieldTypeSelector, ast.FieldTypeIdent, ast.FieldTypeArray, ast.FieldTypePtr: if !envPrefixed { break @@ -122,6 +129,7 @@ func (c *Converter) DocItemsFromField(resolver *TypeResolver, prefix string, f * break } children = c.DocItemsFromFields(resolver, prefix, tpe.Fields) + debug.Logf("\t# CONV: selector %q (%d childrens)\n", f.TypeRef.String(), len(children)) } res := make([]*EnvDocItem, len(names), len(names)+1) @@ -132,14 +140,11 @@ func (c *Converter) DocItemsFromField(resolver *TypeResolver, prefix string, f * Opts: opts, Children: children, } + debug.Logf("\t# CONV: docItem %q (%d childrens)\n", name, len(children)) } - if len(res) == 0 && len(children) > 0 { - res = append(res, &EnvDocItem{ - Name: "", - Doc: f.Doc, - Opts: opts, - Children: children, - }) + + if len(names) == 0 && len(children) > 0 { + return children } return res } diff --git a/converter_test.go b/converter_test.go index 3803215..0c2be7c 100644 --- a/converter_test.go +++ b/converter_test.go @@ -16,7 +16,7 @@ func TestConvertDocItems(t *testing.T) { Name: "string", Kind: ast.FieldTypeIdent, }, - Tag: `env:"FIELD1,required"`, + Tag: `env:"FIELD1,required,file"`, Doc: "Field1 doc", }, { @@ -27,6 +27,68 @@ func TestConvertDocItems(t *testing.T) { }, Doc: "Field2 and Field3 doc", }, + { + Names: []string{"FieldDef"}, + TypeRef: ast.FieldTypeRef{ + Name: "string", + Kind: ast.FieldTypeIdent, + }, + Doc: "Field with default", + Tag: `env:"FIELD_DEF" envDefault:"envdef"`, + }, + { + Names: []string{"FieldArr"}, + TypeRef: ast.FieldTypeRef{ + Name: "[]string", + Kind: ast.FieldTypeArray, + }, + Doc: "Field array", + Tag: `env:"FIELD_ARR"`, + }, + { + Names: []string{"FieldArrSep"}, + TypeRef: ast.FieldTypeRef{ + Name: "[]string", + Kind: ast.FieldTypeArray, + }, + Doc: "Field array with separator", + Tag: `env:"FIELD_ARR_SEP" envSeparator:":"`, + }, + { + Names: []string{"FooField"}, + TypeRef: ast.FieldTypeRef{ + Name: "Foo", + Kind: ast.FieldTypePtr, + }, + Tag: `envPrefix:"FOO_"`, + }, + { + Names: []string{"BarField"}, + TypeRef: ast.FieldTypeRef{ + Pkg: "config", + Name: "Bar", + Kind: ast.FieldTypeIdent, + }, + Tag: `envPrefix:"BAR_"`, + }, + { + Names: []string{"StructField"}, + TypeRef: ast.FieldTypeRef{ + Kind: ast.FieldTypeStruct, + }, + Fields: []*ast.FieldSpec{ + { + Names: []string{"Field1"}, + TypeRef: ast.FieldTypeRef{ + Name: "string", + Kind: ast.FieldTypeIdent, + }, + Doc: "Field1 doc", + Tag: `env:"FIELD1"`, + }, + }, + Tag: `envPrefix:"STRUCT_"`, + }, { Names: []string{}, Doc: "Embedded field", @@ -38,7 +100,7 @@ func TestConvertDocItems(t *testing.T) { Kind: ast.FieldTypeIdent, }, Doc: "Field4 doc", - Tag: `env:"FIELD4,notEmpty"`, + Tag: `env:"FIELD4,notEmpty,expand"`, }, }, Tag: `envPrefix:"PREFIX_"`, @@ -48,16 +110,41 @@ func TestConvertDocItems(t *testing.T) { }, } resolver := NewTypeResolver() + resolver.AddTypes("", []*ast.TypeSpec{ + { + Name: "Foo", + Doc: "Foo doc", + Fields: []*ast.FieldSpec{ + { + Names: []string{"FOne"}, + Doc: "Foo one field", + Tag: `env:"F1"`, + }, + }, + }, + }) + resolver.AddTypes("config", []*ast.TypeSpec{ + { + Name: "Bar", + Doc: "Bar doc", + Fields: []*ast.FieldSpec{ + { + Names: []string{"BOne"}, + Doc: "Bar one field", + Tag: `env:"B1"`, + }, + }, + }, + }) + res := c.DocItemsFromFields(resolver, "", fieldValues) - if len(res) != 4 { - t.Errorf("Expected 4 items, got %d", len(res)) - } expect := []*EnvDocItem{ { Name: "FIELD1", Doc: "Field1 doc", Opts: EnvVarOptions{ Required: true, + FromFile: true, }, }, { @@ -68,20 +155,188 @@ func TestConvertDocItems(t *testing.T) { Name: "FIELD3", Doc: "Field2 and Field3 doc", }, + { + Name: "FIELD_DEF", + Doc: "Field with default", + Opts: EnvVarOptions{ + Default: "envdef", + }, + }, + { + Name: "FIELD_ARR", + Doc: "Field array", + Opts: EnvVarOptions{ + Separator: ",", + }, + }, + { + Name: "FIELD_ARR_SEP", + Doc: "Field array with separator", + Opts: EnvVarOptions{ + Separator: ":", + }, + }, + { + Name: "FOO_FIELD", + Children: []*EnvDocItem{ + { + Name: "FOO_F1", + Doc: "Foo one field", + }, + }, + }, + { + Name: "BAR_FIELD", + Children: []*EnvDocItem{ + { + Name: "BAR_B1", + Doc: "Bar one field", + }, + }, + }, + { + Name: "STRUCT_FIELD", + Children: []*EnvDocItem{ + { + Name: "STRUCT_FIELD1", + Doc: "Field1 doc", + }, + }, + }, { Name: "FIELD4", Doc: "Field4 doc", Opts: EnvVarOptions{ Required: true, NonEmpty: true, + Expand: true, }, }, } + if len(expect) != len(res) { + t.Errorf("Expected %d items, got %d", len(expect), len(res)) + } for i, item := range expect { checkDocItem(t, fmt.Sprintf("%d", i), item, res[i]) } } +func TestConverterScopes(t *testing.T) { + files := []*ast.FileSpec{ + { + Name: "main.go", + Pkg: "main", + Export: true, + Types: []*ast.TypeSpec{ + { + Name: "Config", + Doc: "Config doc", + Export: true, + Fields: []*ast.FieldSpec{ + { + Names: []string{"Field1"}, + TypeRef: ast.FieldTypeRef{ + Name: "string", + Kind: ast.FieldTypeIdent, + }, + Doc: "Field1 doc", + Tag: `env:"FIELD1,required,file"`, + }, + }, + }, + { + Name: "Foo", + Doc: "Foo doc", + Export: false, + Fields: []*ast.FieldSpec{ + { + Names: []string{"FOne"}, + Doc: "Foo one field", + Tag: `env:"F1"`, + }, + }, + }, + }, + }, + { + Name: "config.go", + Pkg: "config", + Export: false, + Types: []*ast.TypeSpec{ + { + Name: "Bar", + Doc: "Bar doc", + Export: true, + Fields: []*ast.FieldSpec{ + { + Names: []string{"BOne"}, + Doc: "Bar one field", + Tag: `env:"B1"`, + }, + }, + }, + }, + }, + } + c := NewConverter("", false) + resolver := NewTypeResolver() + scopes := c.ScopesFromFiles(resolver, files) + expect := []*EnvScope{ + { + Name: "Config", + Doc: "Config doc", + Vars: []*EnvDocItem{ + { + Name: "FIELD1", + Doc: "Field1 doc", + Opts: EnvVarOptions{ + Required: true, + FromFile: true, + }, + }, + }, + }, + } + if len(expect) != len(scopes) { + t.Fatalf("Expected %d scopes, got %d", len(expect), len(scopes)) + } + for i, scope := range expect { + checkScope(t, fmt.Sprintf("%d", i), scope, scopes[i]) + } +} + +func TestConverterFailedToResolve(t *testing.T) { + field := &ast.FieldSpec{ + Names: []string{"BarField"}, + TypeRef: ast.FieldTypeRef{ + Pkg: "config", + Name: "Bar", + Kind: ast.FieldTypeIdent, + }, + Tag: `envPrefix:"BAR_"`, + } + c := NewConverter("", false) + resolver := NewTypeResolver() + _ = c.DocItemsFromField(resolver, "", field) +} + +func checkScope(t *testing.T, scope string, expect, actual *EnvScope) { + t.Helper() + + if expect.Name != actual.Name { + t.Errorf("Expected name %s, got %s", expect.Name, actual.Name) + } + if expect.Doc != actual.Doc { + t.Errorf("Expected doc %s, got %s", expect.Doc, actual.Doc) + } + if len(expect.Vars) != len(actual.Vars) { + t.Fatalf("Expected %d vars, got %d", len(expect.Vars), len(actual.Vars)) + } + for i, item := range expect.Vars { + checkDocItem(t, fmt.Sprintf("%s/%d", scope, i), item, actual.Vars[i]) + } +} + func checkDocItem(t *testing.T, scope string, expect, actual *EnvDocItem) { t.Helper() if expect.Name != actual.Name { diff --git a/debug.go b/debug.go index 2344417..1475af2 100644 --- a/debug.go +++ b/debug.go @@ -1,13 +1,21 @@ +//go:build !coverage + package main import ( "fmt" goast "go/ast" + "io" "strings" "github.com/g4s8/envdoc/ast" + "github.com/g4s8/envdoc/debug" ) +var DebugConfig struct { + Enabled bool +} + type debugVisitor int func (v debugVisitor) Visit(n goast.Node) goast.Visitor { @@ -61,3 +69,31 @@ func printTraverseFields(fields []*ast.FieldSpec, level int) { printTraverseFields(f.Fields, level+1) } } + +func (r *TypeResolver) fprint(out io.Writer) { + fmt.Fprintln(out, "Resolved types:") + for k, v := range r.types { + fmt.Fprintf(out, " %s.%s: %q (export=%t)\n", + k.pkg, k.name, v.Name, v.Export) + } +} + +func printScopesTree(s []*EnvScope) { + if !debug.Config.Enabled { + return + } + debug.Log("Scopes tree:\n") + for _, scope := range s { + debug.Logf(" - %q\n", scope.Name) + for _, item := range scope.Vars { + printDocItem(" ", item) + } + } +} + +func printDocItem(prefix string, item *EnvDocItem) { + debug.Logf("%s- %q\n", prefix, item.Name) + for _, child := range item.Children { + printDocItem(prefix+" ", child) + } +} diff --git a/debug/config.go b/debug/config.go new file mode 100644 index 0000000..088d260 --- /dev/null +++ b/debug/config.go @@ -0,0 +1,6 @@ +package debug + +// Config is a global debug configuration. +var Config struct { + Enabled bool +} diff --git a/debug/logger.go b/debug/logger.go new file mode 100644 index 0000000..5516c13 --- /dev/null +++ b/debug/logger.go @@ -0,0 +1,90 @@ +package debug + +import ( + "fmt" + "io" + "os" + "testing" +) + +type Logger interface { + Logf(format string, args ...interface{}) + Log(args ...interface{}) +} + +type ioLogger struct { + out io.Writer +} + +func NewLogger(out io.Writer) Logger { + return &ioLogger{out: out} +} + +func (l *ioLogger) Logf(format string, args ...interface{}) { + fmt.Fprintf(l.out, format, args...) +} + +func (l *ioLogger) Log(args ...interface{}) { + fmt.Fprint(l.out, args...) +} + +type testLogger struct { + t *testing.T +} + +func NewTestLogger(t *testing.T) Logger { + return &testLogger{t: t} +} + +func (l *testLogger) Logf(format string, args ...interface{}) { + l.t.Helper() + l.t.Logf(format, args...) +} + +func (l *testLogger) Log(args ...interface{}) { + l.t.Helper() + l.t.Log(args...) +} + +type nopLogger struct{} + +func (l *nopLogger) Logf(format string, args ...interface{}) {} + +func (l *nopLogger) Log(args ...interface{}) {} + +var NopLogger = &nopLogger{} + +var logger Logger + +func SetLogger() { + if !Config.Enabled { + logger = NopLogger + return + } + logger = NewLogger(os.Stdout) +} + +func SetTestLogger(t *testing.T) { + if logger == nil { + SetLogger() + } + currentLogger := logger + t.Cleanup(func() { + logger = currentLogger + }) + logger = NewTestLogger(t) +} + +func Logf(format string, args ...interface{}) { + if logger == nil { + SetLogger() + } + logger.Logf(format, args...) +} + +func Log(args ...interface{}) { + if logger == nil { + SetLogger() + } + logger.Log(args...) +} diff --git a/debug_test.go b/debug_test.go new file mode 100644 index 0000000..1e9a8fe --- /dev/null +++ b/debug_test.go @@ -0,0 +1,8 @@ +//go:build coverage + +package main + +import "github.com/g4s8/envdoc/ast" + +func printTraverse(files []*ast.FileSpec, level int) { +} diff --git a/go.mod b/go.mod index 9a6be53..7d314c3 100644 --- a/go.mod +++ b/go.mod @@ -2,4 +2,7 @@ module github.com/g4s8/envdoc go 1.22 -require github.com/gobwas/glob v0.2.3 // indirect +require ( + github.com/gobwas/glob v0.2.3 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect +) diff --git a/go.sum b/go.sum index 39fa9fa..87ddcfc 100644 --- a/go.sum +++ b/go.sum @@ -1,2 +1,5 @@ github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= diff --git a/main.go b/main.go index 8b1581c..9ee0f8f 100644 --- a/main.go +++ b/main.go @@ -1,9 +1,13 @@ +//go:build !coverage + package main import ( "bufio" "fmt" "os" + + "github.com/g4s8/envdoc/debug" ) func main() { @@ -12,6 +16,7 @@ func main() { fatal("Failed to load config: %v", err) } if cfg.Debug { + debug.Config.Enabled = true cfg.fprint(os.Stdout) } @@ -30,6 +35,7 @@ func main() { conv := NewConverter(cfg.EnvPrefix, cfg.FieldNames) scopes := conv.ScopesFromFiles(res, files) + printScopesTree(scopes) r := NewRenderer(cfg.OutFormat, cfg.EnvPrefix, cfg.NoStyles) out, err := os.Create(cfg.OutFile) diff --git a/main_test.go b/main_test.go new file mode 100644 index 0000000..816effe --- /dev/null +++ b/main_test.go @@ -0,0 +1,23 @@ +package main + +import ( + "flag" + "os" + "testing" + + "github.com/g4s8/envdoc/debug" +) + +type testConfig struct { + Debug bool +} + +func TestMain(m *testing.M) { + var cfg testConfig + flag.BoolVar(&cfg.Debug, "debug", false, "Enable debug mode") + flag.Parse() + + debug.Config.Enabled = cfg.Debug + + os.Exit(m.Run()) +} diff --git a/parser.go b/parser.go index 55204cb..b75a09e 100644 --- a/parser.go +++ b/parser.go @@ -5,6 +5,7 @@ import ( "go/parser" "go/token" "io/fs" + "path/filepath" "github.com/g4s8/envdoc/ast" ) @@ -58,11 +59,6 @@ func (p *Parser) Parse() ([]*ast.FileSpec, error) { matcher = m } - pkgs, err := parser.ParseDir(fset, p.dir, matcher, parser.ParseComments|parser.SkipObjectResolution) - if err != nil { - return nil, fmt.Errorf("failed to parse dir: %w", err) - } - var colOpts []ast.RootCollectorOption if p.typeGlob == "" { colOpts = append(colOpts, ast.WithGoGenDecl(p.gogenLine, p.gogenFile)) @@ -81,14 +77,43 @@ func (p *Parser) Parse() ([]*ast.FileSpec, error) { colOpts = append(colOpts, ast.WithFileGlob(m)) } - col := ast.NewRootCollector(colOpts...) - for _, pkg := range pkgs { - ast.Walk(pkg, fset, col) + col := ast.NewRootCollector(p.dir, colOpts...) + + if p.debug { + fmt.Printf("Parsing dir %q (f=%q t=%q)\n", p.dir, p.fileGlob, p.typeGlob) + } + // walk through the directory and each subdirectory and call parseDir for each of them + if err := filepath.Walk(p.dir, func(path string, info fs.FileInfo, err error) error { + if err != nil { + return fmt.Errorf("failed to walk through dir: %w", err) + } + if !info.IsDir() { + return nil + } + if err := parseDir(path, fset, matcher, col); err != nil { + return fmt.Errorf("failed to parse dir %q: %w", path, err) + } + return nil + }); err != nil { + return nil, fmt.Errorf("failed to walk through dir: %w", err) } if p.debug { + fmt.Printf("Resolved types:\n") printTraverse(col.Files(), 0) } return col.Files(), nil } + +func parseDir(dir string, fset *token.FileSet, matcher func(fs.FileInfo) bool, col *ast.RootCollector) error { + pkgs, err := parser.ParseDir(fset, dir, nil, parser.ParseComments|parser.SkipObjectResolution) + if err != nil { + return fmt.Errorf("failed to parse dir: %w", err) + } + + for _, pkg := range pkgs { + ast.Walk(pkg, fset, col) + } + return nil +} diff --git a/parser_test.go b/parser_test.go new file mode 100644 index 0000000..a588e05 --- /dev/null +++ b/parser_test.go @@ -0,0 +1,399 @@ +package main + +import ( + "bufio" + "fmt" + "io" + "os" + "path" + "strings" + "testing" + + "github.com/g4s8/envdoc/ast" + "github.com/g4s8/envdoc/debug" + "gopkg.in/yaml.v2" +) + +func copyFile(src, dst string) error { + in, err := os.Open(src) + if err != nil { + return fmt.Errorf("open source file: %s", err) + } + defer in.Close() + + out, err := os.Create(dst) + if err != nil { + return fmt.Errorf("create destination file: %s", err) + } + defer out.Close() + + bufIn := bufio.NewReader(in) + bufOut := bufio.NewWriter(out) + + if _, err = io.Copy(bufOut, bufIn); err != nil { + return fmt.Errorf("copy file: %s", err) + } + + if err = bufOut.Flush(); err != nil { + return fmt.Errorf("flush destination file: %s", err) + } + return nil +} + +func copyDir(src, dst string) error { + srcInfo, err := os.Stat(src) + if err != nil { + return fmt.Errorf("stat source dir %q: %w", src, err) + } + + if err = os.MkdirAll(dst, srcInfo.Mode()); err != nil { + return fmt.Errorf("create destination dir %q: %w", dst, err) + } + + entries, err := os.ReadDir(src) + if err != nil { + return fmt.Errorf("read source dir %q: %w", src, err) + } + + for _, entry := range entries { + srcPath := path.Join(src, entry.Name()) + dstPath := path.Join(dst, entry.Name()) + if entry.IsDir() { + if err := copyDir(srcPath, dstPath); err != nil { + return fmt.Errorf("copy dir %q: %w", srcPath, err) + } + continue + } + if err := copyFile(srcPath, dstPath); err != nil { + return fmt.Errorf("copy file %q: %w", srcPath, err) + } + } + return nil +} + +func setupParsserDir(t *testing.T, dir string) string { + // dir path contains the path to the directory where the test files are located + // just create a temp dir and copy all files from `dir` to the temp dir recursively + t.Helper() + + // tmpDir := path.Join(t.TempDir(), "parser") + tmpDir := path.Join("/tmp/q", "parser") + if err := os.MkdirAll(tmpDir, 0o755); err != nil { + t.Fatalf("failed to create temp dir: %s", err) + } + + if err := copyDir(path.Join("testdata", dir), tmpDir); err != nil { + t.Fatalf("failed to copy directory: %s", err) + } + return tmpDir +} + +func setupParserFiles(t *testing.T, file string) (dir string) { + t.Helper() + + dir = path.Join(t.TempDir(), "parser") + if err := os.MkdirAll(dir, 0o755); err != nil { + t.Fatalf("failed to create directory: %s", err) + } + if err := copyFile(path.Join("testdata", file), + path.Join(dir, file)); err != nil { + t.Fatalf("failed to copy file: %s", err) + } + return +} + +type parserExpectedTypeRef struct { + Name string `yaml:"name"` + Kind string `yaml:"kind"` + Package string `yaml:"pkg"` +} + +func (ref *parserExpectedTypeRef) toAST(t *testing.T) ast.FieldTypeRef { + t.Helper() + + if ref == nil { + return ast.FieldTypeRef{} + } + + var kind ast.FieldTypeRefKind + if !kind.ScanStr(ref.Kind) { + t.Fatalf("invalid type kind: %s", ref.Kind) + } + return ast.FieldTypeRef{ + Name: ref.Name, + Kind: kind, + Pkg: ref.Package, + } +} + +type parserExpectedField struct { + Names []string `yaml:"names"` + Doc string `yaml:"doc"` + Tag string `yaml:"tag"` + TypeRef *parserExpectedTypeRef `yaml:"type_ref"` + Fields []*parserExpectedField `yaml:"fields"` +} + +func (field *parserExpectedField) toAST(t *testing.T) *ast.FieldSpec { + t.Helper() + + names := make([]string, len(field.Names)) + copy(names, field.Names) + fields := make([]*ast.FieldSpec, len(field.Fields)) + for i, f := range field.Fields { + fields[i] = f.toAST(t) + } + return &ast.FieldSpec{ + Names: names, + Doc: field.Doc, + Tag: field.Tag, + Fields: fields, + TypeRef: field.TypeRef.toAST(t), + } +} + +type parserExpectedType struct { + Name string `yaml:"name"` + Exported bool `yaml:"export"` + Doc string `yaml:"doc"` + Fields []*parserExpectedField `yaml:"fields"` +} + +func (typ *parserExpectedType) toAST(t *testing.T) *ast.TypeSpec { + t.Helper() + + fields := make([]*ast.FieldSpec, len(typ.Fields)) + for i, f := range typ.Fields { + fields[i] = f.toAST(t) + } + return &ast.TypeSpec{ + Name: typ.Name, + Export: typ.Exported, + Doc: typ.Doc, + Fields: fields, + } +} + +type parserExpectedFile struct { + Name string `yaml:"name"` + Package string `yaml:"pkg"` + Exported bool `yaml:"export"` + Types []*parserExpectedType `yaml:"types"` +} + +func (file *parserExpectedFile) toAST(t *testing.T) *ast.FileSpec { + t.Helper() + + types := make([]*ast.TypeSpec, len(file.Types)) + for i, typ := range file.Types { + types[i] = typ.toAST(t) + } + return &ast.FileSpec{ + Name: file.Name, + Pkg: file.Package, + Export: file.Exported, + Types: types, + } +} + +func parserFilesToAST(t *testing.T, files []*parserExpectedFile) []*ast.FileSpec { + t.Helper() + + res := make([]*ast.FileSpec, len(files)) + for i, f := range files { + res[i] = f.toAST(t) + } + return res +} + +type parserTestCase struct { + SrcFile string `yaml:"src_file"` + SrcDir string `yaml:"src_dir"` + FileGlob string `yaml:"file_glob"` + TypeGlob string `yaml:"type_glob"` + Debug bool `yaml:"debug"` + + Expect []*parserExpectedFile `yaml:"files"` +} + +func loadParserTestCases(t *testing.T) []parserTestCase { + t.Helper() + + var parserTestCases struct { + Cases []parserTestCase `yaml:"test_cases"` + } + f, err := os.Open("testdata/_cases.yaml") + if err != nil { + t.Fatalf("failed to open test cases file: %s", err) + } + defer f.Close() + + buf := bufio.NewReader(f) + dec := yaml.NewDecoder(buf) + if err := dec.Decode(&parserTestCases); err != nil { + t.Fatalf("failed to decode test cases: %s", err) + } + return parserTestCases.Cases +} + +func TestFileParser(t *testing.T) { + cases := loadParserTestCases(t) + for i, tc := range cases { + t.Run(fmt.Sprintf("case(%d)", i), parserTestRunner(tc)) + } +} + +func parserTestRunner(tc parserTestCase) func(*testing.T) { + return func(t *testing.T) { + var dir string + if tc.SrcFile != "" { + dir = setupParserFiles(t, tc.SrcFile) + } else if tc.SrcDir != "" { + dir = setupParsserDir(t, tc.SrcDir) + } else { + t.Fatal("either src_file or src_dir must be set") + } + var opts []parserConfigOption + if tc.Debug || debug.Config.Enabled { + debug.SetTestLogger(t) + t.Log("Debug mode") + t.Logf("using dir: %s", dir) + opts = append(opts, withDebug(true)) + } + p := NewParser(dir, tc.FileGlob, tc.TypeGlob, opts...) + files, err := p.Parse() + if err != nil { + t.Fatalf("failed to parse files: %s", err) + } + astFiles := parserFilesToAST(t, tc.Expect) + checkFiles(t, "/files", astFiles, files) + } +} + +func checkFiles(t *testing.T, prefix string, expect, res []*ast.FileSpec) { + t.Helper() + + if len(expect) != len(res) { + t.Errorf("%s: Expected %d files, got %d", prefix, len(expect), len(res)) + for i, file := range expect { + t.Logf("Expected[%d]: %v", i, file) + } + for i, file := range res { + t.Logf("Got[%d]: %v", i, file) + } + return + } + for i, file := range expect { + checkFile(t, fmt.Sprintf("%s/%s", prefix, file.Name), file, res[i]) + } +} + +func checkFile(t *testing.T, prefix string, expect, res *ast.FileSpec) { + t.Helper() + + if !strings.HasSuffix(res.Name, expect.Name) { + t.Errorf("%s: Expected name %q, got %q", prefix, expect.Name, res.Name) + } + if expect.Pkg != res.Pkg { + t.Errorf("%s: Expected package %q, got %q", prefix, expect.Pkg, res.Pkg) + } + if expect.Export != res.Export { + t.Errorf("%s: Expected export %t, got %t", prefix, expect.Export, res.Export) + } + checkTypes(t, prefix+"/types", expect.Types, res.Types) +} + +func checkTypes(t *testing.T, prefix string, expect, res []*ast.TypeSpec) { + t.Helper() + + if len(expect) != len(res) { + t.Errorf("%s: Expected %d types, got %d", prefix, len(expect), len(res)) + for i, typ := range expect { + t.Logf("Expected[%d]: %v", i, typ) + } + for i, typ := range res { + t.Logf("Got[%d]: %v", i, typ) + } + return + } + for i, typ := range expect { + checkType(t, fmt.Sprintf("%s/%s", prefix, typ.Name), typ, res[i]) + } +} + +func checkType(t *testing.T, prefix string, expect, res *ast.TypeSpec) { + t.Helper() + + if expect.Name != res.Name { + t.Errorf("%s: Expected name %s, got %s", prefix, expect.Name, res.Name) + } + if expect.Doc != res.Doc { + t.Errorf("%s: Expected doc %s, got %s", prefix, expect.Doc, res.Doc) + } + if expect.Export != res.Export { + t.Errorf("%s: Expected export %t, got %t", prefix, expect.Export, res.Export) + } + checkFields(t, prefix+"/fields", expect.Fields, res.Fields) +} + +func checkFields(t *testing.T, prefix string, expect, res []*ast.FieldSpec) { + t.Helper() + + if len(expect) != len(res) { + t.Errorf("%s: Expected %d fields, got %d", prefix, len(expect), len(res)) + for i, field := range expect { + t.Logf("Expected[%d]: %s", i, field) + } + for i, field := range res { + t.Logf("Got[%d]: %s", i, field) + } + return + } + for i, field := range expect { + str := field.String() + if str == "" { + str = fmt.Sprintf("%d", i) + } + checkField(t, fmt.Sprintf("%s/%s", prefix, str), field, res[i]) + } +} + +func checkField(t *testing.T, prefix string, expect, res *ast.FieldSpec) { + t.Helper() + + if len(expect.Names) != len(res.Names) { + t.Errorf("%s: Expected %d names, got %d", prefix, len(expect.Names), len(res.Names)) + for i, name := range expect.Names { + t.Logf("Expected[%d]: %s", i, name) + } + for i, name := range res.Names { + t.Logf("Got[%d]: %s", i, name) + } + return + } + for i, name := range expect.Names { + if name != res.Names[i] { + t.Errorf("%s: Expected name at %s, got %s", prefix, name, res.Names[i]) + } + } + if expect.Doc != res.Doc { + t.Errorf("%s: Expected doc %s, got %s", prefix, expect.Doc, res.Doc) + } + if expect.Tag != res.Tag { + t.Errorf("%s: Expected tag %q, got %q", prefix, expect.Tag, res.Tag) + } + + checkTypeRef(t, prefix+"/typeref", &expect.TypeRef, &res.TypeRef) + checkFields(t, prefix+"/fields", expect.Fields, res.Fields) +} + +func checkTypeRef(t *testing.T, prefix string, expect, res *ast.FieldTypeRef) { + t.Helper() + + if expect.Name != res.Name { + t.Errorf("%s: Expected type name %s, got %s", prefix, expect.Name, res.Name) + } + if expect.Kind != res.Kind { + t.Errorf("%s: Expected type kind %s, got %s", prefix, expect.Kind, res.Kind) + } +} diff --git a/resolver.go b/resolver.go index 20d03cd..c016a53 100644 --- a/resolver.go +++ b/resolver.go @@ -1,9 +1,6 @@ package main import ( - "fmt" - "io" - "github.com/g4s8/envdoc/ast" ) @@ -32,14 +29,6 @@ func (r *TypeResolver) Resolve(ref *ast.FieldTypeRef) *ast.TypeSpec { return r.types[typeQualifier{pkg: ref.Pkg, name: ref.Name}] } -func (r *TypeResolver) fprint(out io.Writer) { - fmt.Fprintln(out, "Resolved types:") - for k, v := range r.types { - fmt.Fprintf(out, " %s.%s: %q (export=%t)\n", - k.pkg, k.name, v.Name, v.Export) - } -} - func ResolveAllTypes(files []*ast.FileSpec) *TypeResolver { r := NewTypeResolver() for _, f := range files { diff --git a/resolver_test.go b/resolver_test.go new file mode 100644 index 0000000..b0ad6ac --- /dev/null +++ b/resolver_test.go @@ -0,0 +1,67 @@ +package main + +import ( + "testing" + + "github.com/g4s8/envdoc/ast" +) + +func TestResolver(t *testing.T) { + res := ResolveAllTypes([]*ast.FileSpec{ + { + Pkg: "main", + Types: []*ast.TypeSpec{ + { + Name: "Foo", + Export: true, + }, + { + Name: "Bar", + Export: false, + }, + }, + }, + { + Pkg: "test", + Types: []*ast.TypeSpec{ + { + Name: "Baz", + Export: true, + }, + }, + }, + }) + foo := res.Resolve(&ast.FieldTypeRef{Pkg: "main", Name: "Foo"}) + if foo == nil { + t.Errorf("Foo type not resolved") + } + if foo.Name != "Foo" { + t.Errorf("Invalid Foo type: %s", foo.Name) + } + + bar := res.Resolve(&ast.FieldTypeRef{Pkg: "main", Name: "Bar"}) + if bar == nil { + t.Errorf("Bar type not resolved") + } + if bar != nil && bar.Name != "Bar" { + t.Errorf("Invalid Bar type: %s", bar.Name) + } + + baz := res.Resolve(&ast.FieldTypeRef{Pkg: "test", Name: "Baz"}) + if baz == nil { + t.Errorf("Baz type not resolved") + } + if baz.Name != "Baz" { + t.Errorf("Invalid Baz type: %s", baz.Name) + } + + nope := res.Resolve(&ast.FieldTypeRef{Pkg: "test", Name: "Nope"}) + if nope != nil { + t.Errorf("Nope type resolved, but it should not") + } + + wrongPgk := res.Resolve(&ast.FieldTypeRef{Pkg: "main", Name: "Baz"}) + if wrongPgk != nil { + t.Errorf("Baz type resolved, but it should not") + } +} diff --git a/tags.go b/tags.go index cd6c970..4b9031d 100644 --- a/tags.go +++ b/tags.go @@ -2,36 +2,6 @@ package main import "strings" -func fieldTagValues(tag, tagName string) []string { - tagPrefix := tagName + ":" - if !strings.Contains(tag, tagPrefix) { - return nil - } - tagValue := strings.Split(tag, tagPrefix)[1] - leftQ := strings.Index(tagValue, `"`) - if leftQ == -1 || leftQ == len(tagValue)-1 { - return nil - } - rightQ := strings.Index(tagValue[leftQ+1:], `"`) - if rightQ == -1 { - return nil - } - tagValue = tagValue[leftQ+1 : leftQ+rightQ+1] - return strings.Split(tagValue, ",") -} - -func getAllTagValues(tag string) map[string][]string { - tags := make(map[string][]string) - for _, t := range strings.Fields(tag) { - if !strings.Contains(t, ":") { - continue - } - parts := strings.Split(t, ":") - tags[parts[0]] = append(tags[parts[0]], parts[1]) - } - return tags -} - type FieldTag map[string][]string func ParseFieldTag(tag string) FieldTag { @@ -42,8 +12,9 @@ func ParseFieldTag(tag string) FieldTag { } parts := strings.Split(fields, ":") key := parts[0] - vals := fieldTagValues(tag, key) - t[key] = vals + if vals := fieldTagValues(tag, key); vals != nil { + t[key] = vals + } } return t } @@ -58,3 +29,21 @@ func (t FieldTag) GetFirst(key string) (string, bool) { } return t[key][0], true } + +func fieldTagValues(tag, tagName string) []string { + tagPrefix := tagName + ":" + if !strings.Contains(tag, tagPrefix) { + return nil + } + tagValue := strings.Split(tag, tagPrefix)[1] + leftQ := strings.Index(tagValue, `"`) + if leftQ == -1 || leftQ == len(tagValue)-1 { + return nil + } + rightQ := strings.Index(tagValue[leftQ+1:], `"`) + if rightQ == -1 { + return nil + } + tagValue = tagValue[leftQ+1 : leftQ+rightQ+1] + return strings.Split(tagValue, ",") +} diff --git a/tags_test.go b/tags_test.go index bd7b883..ae45c2f 100644 --- a/tags_test.go +++ b/tags_test.go @@ -1,10 +1,18 @@ package main import ( + "fmt" "slices" "testing" ) +func tagShouldErr(t *testing.T, tag string) { + t.Helper() + if len(ParseFieldTag(tag)) != 0 { + t.Errorf("expected empty, got %v", tag) + } +} + func TestFieldTags(t *testing.T) { const src = `env:"PASSWORD,required,file" envDefault:"/tmp/password" json:"password"` tag := ParseFieldTag(src) @@ -39,4 +47,69 @@ func TestFieldTags(t *testing.T) { t.Errorf("%q: expected empty, got %v", k, got) } } + + t.Run("error", func(t *testing.T) { + tagShouldErr(t, `envPASSWORD`) + tagShouldErr(t, `env:"PASSWORD`) + tagShouldErr(t, `env:PASSWORD"`) + }) +} + +func TestFieldTagValues(t *testing.T) { + tests := []struct { + tag, key string + expect []string + err bool + }{ + { + tag: `env:"PASSWORD,required,file"`, + key: "env", + expect: []string{"PASSWORD", "required", "file"}, + }, + { + tag: `envDefault:"/tmp/password"`, + key: "envDefault", + expect: []string{"/tmp/password"}, + }, + { + tag: `json:"password"`, + key: "json", + expect: []string{"password"}, + }, + { + tag: `jsonpassword`, + key: "json", + err: true, + }, + { + tag: `json:"password`, + key: "env", + err: true, + }, + { + tag: `env:PASSWORD"`, + key: "env", + err: true, + }, + { + tag: `env:"PASSWORD`, + key: "env", + err: true, + }, + } + for i, test := range tests { + t.Run(fmt.Sprintf("case%d", i), func(t *testing.T) { + vals := fieldTagValues(test.tag, test.key) + if test.err { + if vals != nil { + t.Errorf("expected nil, got %v", vals) + } + return + } + + if !slices.Equal(vals, test.expect) { + t.Errorf("expected %v, got %v", test.expect, vals) + } + }) + } } diff --git a/templates_test.go b/templates_test.go new file mode 100644 index 0000000..4ee313c --- /dev/null +++ b/templates_test.go @@ -0,0 +1,73 @@ +package main + +import ( + "slices" + "testing" +) + +func TestTplFuncs(t *testing.T) { + // "repeat": strings.Repeat, + // "split": strings.Split, + // "strAppend": func(arr []string, item string) []string { + // return append(arr, item) + // }, + // "join": strings.Join, + // "strSlice": func() []string { + // return make([]string, 0) + // }, + // "list": func(args ...any) []any { + // return args + // }, + // "sum": func(args ...int) int { + // var sum int + // for _, v := range args { + // sum += v + // } + // return sum + // }, + t.Run("repeat", func(t *testing.T) { + f := tplFuncs["repeat"].(func(string, int) string) + if f("a", 3) != "aaa" { + t.Error("repeat failed") + } + }) + t.Run("split", func(t *testing.T) { + f := tplFuncs["split"].(func(string, string) []string) + if f("a,b,c", ",") == nil { + t.Error("split failed") + } + }) + t.Run("strAppend", func(t *testing.T) { + f := tplFuncs["strAppend"].(func([]string, string) []string) + if !slices.Equal(f([]string{"a"}, "b"), []string{"a", "b"}) { + t.Error("strAppend failed") + } + }) + t.Run("join", func(t *testing.T) { + f := tplFuncs["join"].(func([]string, string) string) + if f([]string{"a", "b"}, ",") != "a,b" { + t.Error("join failed") + } + }) + t.Run("strSlice", func(t *testing.T) { + f := tplFuncs["strSlice"].(func() []string) + if f() == nil { + t.Error("strSlice failed") + } + }) + t.Run("list", func(t *testing.T) { + f := tplFuncs["list"].(func(...any) []any) + lst := f(1, 2, 3) + for i, v := range lst { + if v != i+1 { + t.Error("list failed") + } + } + }) + t.Run("sum", func(t *testing.T) { + f := tplFuncs["sum"].(func(...int) int) + if f(1, 2, 3) != 6 { + t.Error("sum failed") + } + }) +} diff --git a/test.cover b/test.cover new file mode 100644 index 0000000..092384b --- /dev/null +++ b/test.cover @@ -0,0 +1,418 @@ +mode: set +github.com/g4s8/envdoc/ast/collectors.go:12.63,13.32 1 0 +github.com/g4s8/envdoc/ast/collectors.go:13.32,15.3 1 0 +github.com/g4s8/envdoc/ast/collectors.go:18.63,19.32 1 0 +github.com/g4s8/envdoc/ast/collectors.go:19.32,21.3 1 0 +github.com/g4s8/envdoc/ast/collectors.go:24.63,25.32 1 0 +github.com/g4s8/envdoc/ast/collectors.go:25.32,33.3 1 0 +github.com/g4s8/envdoc/ast/collectors.go:66.39,66.54 1 0 +github.com/g4s8/envdoc/ast/collectors.go:68.83,74.27 2 0 +github.com/g4s8/envdoc/ast/collectors.go:74.27,76.3 1 0 +github.com/g4s8/envdoc/ast/collectors.go:77.2,77.10 1 0 +github.com/g4s8/envdoc/ast/collectors.go:80.45,84.38 3 0 +github.com/g4s8/envdoc/ast/collectors.go:84.38,86.3 1 0 +github.com/g4s8/envdoc/ast/collectors.go:87.2,87.12 1 0 +github.com/g4s8/envdoc/ast/collectors.go:93.3,97.41 2 0 +github.com/g4s8/envdoc/ast/collectors.go:97.41,101.3 3 0 +github.com/g4s8/envdoc/ast/collectors.go:102.2,105.24 3 0 +github.com/g4s8/envdoc/ast/collectors.go:105.24,107.3 1 0 +github.com/g4s8/envdoc/ast/collectors.go:108.2,109.10 2 0 +github.com/g4s8/envdoc/ast/collectors.go:112.49,113.23 1 0 +github.com/g4s8/envdoc/ast/collectors.go:113.23,114.37 1 0 +github.com/g4s8/envdoc/ast/collectors.go:116.2,116.32 1 0 +github.com/g4s8/envdoc/ast/collectors.go:122.3,126.24 3 0 +github.com/g4s8/envdoc/ast/collectors.go:126.24,127.20 1 0 +github.com/g4s8/envdoc/ast/collectors.go:127.20,130.4 2 0 +github.com/g4s8/envdoc/ast/collectors.go:131.8,131.33 1 0 +github.com/g4s8/envdoc/ast/collectors.go:131.33,133.3 1 0 +github.com/g4s8/envdoc/ast/collectors.go:135.2,137.34 3 0 +github.com/g4s8/envdoc/ast/collectors.go:140.55,143.24 2 0 +github.com/g4s8/envdoc/ast/collectors.go:143.24,145.3 1 0 +github.com/g4s8/envdoc/ast/collectors.go:146.2,146.75 1 0 +github.com/g4s8/envdoc/ast/collectors.go:146.75,148.3 1 0 +github.com/g4s8/envdoc/ast/collectors.go:155.47,157.2 1 0 +github.com/g4s8/envdoc/ast/collectors.go:159.55,160.22 1 0 +github.com/g4s8/envdoc/ast/collectors.go:160.22,162.3 1 0 +github.com/g4s8/envdoc/ast/collectors.go:165.60,168.2 2 0 +github.com/g4s8/envdoc/ast/collectors.go:174.61,177.2 2 0 +github.com/g4s8/envdoc/ast/field.go:12.64,14.2 1 1 +github.com/g4s8/envdoc/ast/field.go:16.54,18.23 2 1 +github.com/g4s8/envdoc/ast/field.go:19.23,21.11 2 0 +github.com/g4s8/envdoc/ast/field.go:22.18,23.16 1 0 +github.com/g4s8/envdoc/ast/field.go:23.16,25.4 1 0 +github.com/g4s8/envdoc/ast/field.go:26.3,27.16 2 0 +github.com/g4s8/envdoc/ast/field.go:27.16,29.4 1 0 +github.com/g4s8/envdoc/ast/field.go:30.3,30.39 1 0 +github.com/g4s8/envdoc/ast/field.go:30.39,32.4 1 0 +github.com/g4s8/envdoc/ast/field.go:34.2,34.10 1 1 +github.com/g4s8/envdoc/ast/fieldtyperefkind_scan.go:3.51,4.54 1 0 +github.com/g4s8/envdoc/ast/fieldtyperefkind_scan.go:4.54,6.43 2 0 +github.com/g4s8/envdoc/ast/fieldtyperefkind_scan.go:6.43,9.4 2 0 +github.com/g4s8/envdoc/ast/fieldtyperefkind_scan.go:11.2,11.14 1 0 +github.com/g4s8/envdoc/ast/fieldtyperefkind_string.go:23.43,24.68 1 0 +github.com/g4s8/envdoc/ast/fieldtyperefkind_string.go:24.68,26.3 1 0 +github.com/g4s8/envdoc/ast/fieldtyperefkind_string.go:27.2,27.88 1 0 +github.com/g4s8/envdoc/ast/file.go:22.112,29.2 1 1 +github.com/g4s8/envdoc/ast/file.go:31.53,33.23 2 1 +github.com/g4s8/envdoc/ast/file.go:34.20,42.13 5 1 +github.com/g4s8/envdoc/ast/file.go:43.21,48.17 2 1 +github.com/g4s8/envdoc/ast/file.go:48.17,50.4 1 1 +github.com/g4s8/envdoc/ast/file.go:51.3,51.13 1 0 +github.com/g4s8/envdoc/ast/file.go:53.2,53.10 1 1 +github.com/g4s8/envdoc/ast/model.go:84.40,85.17 1 0 +github.com/g4s8/envdoc/ast/model.go:86.22,87.17 1 0 +github.com/g4s8/envdoc/ast/model.go:88.25,89.32 1 0 +github.com/g4s8/envdoc/ast/model.go:90.20,91.23 1 0 +github.com/g4s8/envdoc/ast/model.go:92.22,93.24 1 0 +github.com/g4s8/envdoc/ast/model.go:94.20,95.33 1 0 +github.com/g4s8/envdoc/ast/model.go:96.23,97.18 1 0 +github.com/g4s8/envdoc/ast/model.go:99.2,99.11 1 0 +github.com/g4s8/envdoc/ast/model.go:102.37,104.2 1 0 +github.com/g4s8/envdoc/ast/model.go:106.37,108.2 1 0 +github.com/g4s8/envdoc/ast/model.go:110.38,112.2 1 0 +github.com/g4s8/envdoc/ast/pkg.go:17.68,22.2 1 1 +github.com/g4s8/envdoc/ast/pkg.go:24.52,26.23 2 1 +github.com/g4s8/envdoc/ast/pkg.go:27.20,30.11 3 1 +github.com/g4s8/envdoc/ast/pkg.go:31.17,36.17 2 1 +github.com/g4s8/envdoc/ast/pkg.go:36.17,38.4 1 1 +github.com/g4s8/envdoc/ast/pkg.go:40.2,40.10 1 1 +github.com/g4s8/envdoc/ast/testhelper.go:16.72,21.16 4 1 +github.com/g4s8/envdoc/ast/testhelper.go:21.16,23.3 1 0 +github.com/g4s8/envdoc/ast/testhelper.go:24.2,25.9 2 1 +github.com/g4s8/envdoc/ast/testhelper.go:25.9,27.3 1 0 +github.com/g4s8/envdoc/ast/testhelper.go:28.2,29.24 2 1 +github.com/g4s8/envdoc/ast/testhelper.go:32.56,35.32 2 0 +github.com/g4s8/envdoc/ast/testhelper.go:35.32,37.3 1 0 +github.com/g4s8/envdoc/ast/testhelper.go:38.2,38.32 1 0 +github.com/g4s8/envdoc/ast/testhelper.go:38.32,40.3 1 0 +github.com/g4s8/envdoc/ast/testhelper.go:60.57,62.2 1 1 +github.com/g4s8/envdoc/ast/testhelper.go:68.66,71.2 2 0 +github.com/g4s8/envdoc/ast/testhelper.go:78.63,81.2 2 1 +github.com/g4s8/envdoc/ast/testhelper.go:88.66,91.2 2 1 +github.com/g4s8/envdoc/ast/testhelper.go:93.47,95.2 1 0 +github.com/g4s8/envdoc/ast/testhelper.go:105.3,108.2 2 1 +github.com/g4s8/envdoc/ast/testhelper.go:113.47,124.2 7 1 +github.com/g4s8/envdoc/ast/type.go:17.68,19.2 1 1 +github.com/g4s8/envdoc/ast/type.go:21.53,23.23 2 1 +github.com/g4s8/envdoc/ast/type.go:24.20,28.13 2 0 +github.com/g4s8/envdoc/ast/type.go:29.18,31.16 2 1 +github.com/g4s8/envdoc/ast/type.go:31.16,33.4 1 0 +github.com/g4s8/envdoc/ast/type.go:34.3,34.39 1 1 +github.com/g4s8/envdoc/ast/type.go:34.39,36.4 1 1 +github.com/g4s8/envdoc/ast/type.go:37.3,37.13 1 1 +github.com/g4s8/envdoc/ast/type.go:39.2,39.10 1 1 +github.com/g4s8/envdoc/ast/utils.go:12.58,13.23 1 1 +github.com/g4s8/envdoc/ast/utils.go:14.18,16.28 2 1 +github.com/g4s8/envdoc/ast/utils.go:17.25,21.31 4 0 +github.com/g4s8/envdoc/ast/utils.go:22.21,24.26 2 0 +github.com/g4s8/envdoc/ast/utils.go:25.22,27.28 2 0 +github.com/g4s8/envdoc/ast/utils.go:28.20,30.26 2 0 +github.com/g4s8/envdoc/ast/utils.go:31.23,32.29 1 0 +github.com/g4s8/envdoc/ast/utils.go:33.10,34.15 1 0 +github.com/g4s8/envdoc/ast/utils.go:36.2,36.13 1 1 +github.com/g4s8/envdoc/ast/utils.go:39.47,41.28 2 1 +github.com/g4s8/envdoc/ast/utils.go:41.28,43.3 1 1 +github.com/g4s8/envdoc/ast/utils.go:44.2,44.14 1 1 +github.com/g4s8/envdoc/ast/utils.go:47.58,49.15 2 1 +github.com/g4s8/envdoc/ast/utils.go:49.15,51.3 1 1 +github.com/g4s8/envdoc/ast/utils.go:52.2,53.23 2 1 +github.com/g4s8/envdoc/ast/utils.go:56.79,58.28 2 1 +github.com/g4s8/envdoc/ast/utils.go:58.28,59.31 1 1 +github.com/g4s8/envdoc/ast/utils.go:59.31,61.4 1 1 +github.com/g4s8/envdoc/ast/utils.go:63.2,63.10 1 1 +github.com/g4s8/envdoc/ast/utils.go:66.56,69.43 3 1 +github.com/g4s8/envdoc/ast/utils.go:69.43,72.3 1 0 +github.com/g4s8/envdoc/ast/utils.go:73.2,73.26 1 1 +github.com/g4s8/envdoc/ast/utils.go:73.26,75.3 1 1 +github.com/g4s8/envdoc/ast/utils.go:76.2,76.39 1 1 +github.com/g4s8/envdoc/ast/utils.go:76.39,78.3 1 1 +github.com/g4s8/envdoc/ast/utils.go:79.2,79.30 1 1 +github.com/g4s8/envdoc/ast/utils.go:79.30,81.3 1 0 +github.com/g4s8/envdoc/ast/utils.go:83.2,83.12 1 1 +github.com/g4s8/envdoc/ast/utils.go:86.40,87.27 1 1 +github.com/g4s8/envdoc/ast/utils.go:87.27,89.3 1 1 +github.com/g4s8/envdoc/ast/utils.go:90.2,90.14 1 0 +github.com/g4s8/envdoc/ast/utils.go:90.14,92.3 1 0 +github.com/g4s8/envdoc/ast/utils.go:94.2,94.23 1 0 +github.com/g4s8/envdoc/ast/utils.go:95.17,96.59 1 0 +github.com/g4s8/envdoc/ast/utils.go:97.20,98.53 1 0 +github.com/g4s8/envdoc/ast/utils.go:99.21,100.55 1 0 +github.com/g4s8/envdoc/ast/utils.go:101.18,103.70 2 0 +github.com/g4s8/envdoc/ast/utils.go:104.20,105.53 1 0 +github.com/g4s8/envdoc/ast/utils.go:106.23,107.41 1 0 +github.com/g4s8/envdoc/ast/utils.go:108.47,108.47 0 0 +github.com/g4s8/envdoc/ast/utils.go:110.10,111.40 1 0 +github.com/g4s8/envdoc/ast/utils.go:115.65,118.18 3 1 +github.com/g4s8/envdoc/ast/utils.go:118.18,119.32 1 1 +github.com/g4s8/envdoc/ast/utils.go:119.32,120.26 1 1 +github.com/g4s8/envdoc/ast/utils.go:120.26,122.10 2 1 +github.com/g4s8/envdoc/ast/utils.go:126.2,126.15 1 1 +github.com/g4s8/envdoc/ast/walker.go:8.59,11.2 2 0 +github.com/g4s8/envdoc/debug/logger.go:19.38,21.2 1 0 +github.com/g4s8/envdoc/debug/logger.go:23.61,25.2 1 0 +github.com/g4s8/envdoc/debug/logger.go:27.45,29.2 1 0 +github.com/g4s8/envdoc/debug/logger.go:35.41,37.2 1 0 +github.com/g4s8/envdoc/debug/logger.go:39.63,42.2 2 0 +github.com/g4s8/envdoc/debug/logger.go:44.47,47.2 2 0 +github.com/g4s8/envdoc/debug/logger.go:51.63,51.64 0 0 +github.com/g4s8/envdoc/debug/logger.go:53.47,53.48 0 0 +github.com/g4s8/envdoc/debug/logger.go:59.18,60.21 1 0 +github.com/g4s8/envdoc/debug/logger.go:60.21,63.3 2 0 +github.com/g4s8/envdoc/debug/logger.go:64.2,64.31 1 0 +github.com/g4s8/envdoc/debug/logger.go:67.34,68.19 1 0 +github.com/g4s8/envdoc/debug/logger.go:68.19,70.3 1 0 +github.com/g4s8/envdoc/debug/logger.go:71.2,72.19 2 0 +github.com/g4s8/envdoc/debug/logger.go:72.19,74.3 1 0 +github.com/g4s8/envdoc/debug/logger.go:75.2,75.27 1 0 +github.com/g4s8/envdoc/debug/logger.go:78.47,79.19 1 0 +github.com/g4s8/envdoc/debug/logger.go:79.19,81.3 1 0 +github.com/g4s8/envdoc/debug/logger.go:82.2,82.30 1 0 +github.com/g4s8/envdoc/debug/logger.go:85.31,86.19 1 0 +github.com/g4s8/envdoc/debug/logger.go:86.19,88.3 1 0 +github.com/g4s8/envdoc/debug/logger.go:89.2,89.21 1 0 +github.com/g4s8/envdoc/debug/logger.go:19.38,21.2 1 0 +github.com/g4s8/envdoc/debug/logger.go:23.61,25.2 1 0 +github.com/g4s8/envdoc/debug/logger.go:27.45,29.2 1 0 +github.com/g4s8/envdoc/debug/logger.go:35.41,37.2 1 0 +github.com/g4s8/envdoc/debug/logger.go:39.63,42.2 2 0 +github.com/g4s8/envdoc/debug/logger.go:44.47,47.2 2 0 +github.com/g4s8/envdoc/debug/logger.go:51.63,51.64 0 0 +github.com/g4s8/envdoc/debug/logger.go:53.47,53.48 0 0 +github.com/g4s8/envdoc/debug/logger.go:59.18,60.21 1 0 +github.com/g4s8/envdoc/debug/logger.go:60.21,63.3 2 0 +github.com/g4s8/envdoc/debug/logger.go:64.2,64.31 1 0 +github.com/g4s8/envdoc/debug/logger.go:67.34,68.19 1 0 +github.com/g4s8/envdoc/debug/logger.go:68.19,70.3 1 0 +github.com/g4s8/envdoc/debug/logger.go:71.2,72.19 2 0 +github.com/g4s8/envdoc/debug/logger.go:72.19,74.3 1 0 +github.com/g4s8/envdoc/debug/logger.go:75.2,75.27 1 0 +github.com/g4s8/envdoc/debug/logger.go:78.47,79.19 1 0 +github.com/g4s8/envdoc/debug/logger.go:79.19,81.3 1 0 +github.com/g4s8/envdoc/debug/logger.go:82.2,82.30 1 0 +github.com/g4s8/envdoc/debug/logger.go:85.31,86.19 1 0 +github.com/g4s8/envdoc/debug/logger.go:86.19,88.3 1 0 +github.com/g4s8/envdoc/debug/logger.go:89.2,89.21 1 0 +github.com/g4s8/envdoc/config.go:49.52,71.45 13 0 +github.com/g4s8/envdoc/config.go:71.45,73.3 1 0 +github.com/g4s8/envdoc/config.go:76.2,76.27 1 0 +github.com/g4s8/envdoc/config.go:76.27,78.3 1 0 +github.com/g4s8/envdoc/config.go:79.2,79.29 1 0 +github.com/g4s8/envdoc/config.go:79.29,81.3 1 0 +github.com/g4s8/envdoc/config.go:82.2,82.40 1 0 +github.com/g4s8/envdoc/config.go:82.40,84.3 1 0 +github.com/g4s8/envdoc/config.go:87.2,88.20 2 0 +github.com/g4s8/envdoc/config.go:88.20,91.3 2 0 +github.com/g4s8/envdoc/config.go:92.2,92.9 1 0 +github.com/g4s8/envdoc/config.go:92.9,95.3 2 0 +github.com/g4s8/envdoc/config.go:96.2,96.33 1 0 +github.com/g4s8/envdoc/config.go:96.33,99.3 2 0 +github.com/g4s8/envdoc/config.go:101.2,101.12 1 0 +github.com/g4s8/envdoc/config.go:106.35,108.25 2 0 +github.com/g4s8/envdoc/config.go:108.25,110.3 1 0 +github.com/g4s8/envdoc/config.go:111.2,113.39 2 0 +github.com/g4s8/envdoc/config.go:113.39,115.17 2 0 +github.com/g4s8/envdoc/config.go:115.17,117.4 1 0 +github.com/g4s8/envdoc/config.go:118.3,118.17 1 0 +github.com/g4s8/envdoc/config.go:119.8,121.3 1 0 +github.com/g4s8/envdoc/config.go:123.2,123.38 1 0 +github.com/g4s8/envdoc/config.go:123.38,125.3 1 0 +github.com/g4s8/envdoc/config.go:127.2,127.12 1 0 +github.com/g4s8/envdoc/config.go:130.32,131.22 1 0 +github.com/g4s8/envdoc/config.go:131.22,133.3 1 0 +github.com/g4s8/envdoc/config.go:134.2,134.17 1 0 +github.com/g4s8/envdoc/config.go:134.17,136.3 1 0 +github.com/g4s8/envdoc/config.go:139.40,142.22 3 0 +github.com/g4s8/envdoc/config.go:142.22,144.3 1 0 +github.com/g4s8/envdoc/config.go:145.2,145.22 1 0 +github.com/g4s8/envdoc/config.go:145.22,147.3 1 0 +github.com/g4s8/envdoc/config.go:148.2,150.23 3 0 +github.com/g4s8/envdoc/config.go:150.23,152.3 1 0 +github.com/g4s8/envdoc/config.go:153.2,153.16 1 0 +github.com/g4s8/envdoc/config.go:153.16,155.3 1 0 +github.com/g4s8/envdoc/config.go:156.2,158.18 3 0 +github.com/g4s8/envdoc/config.go:158.18,160.3 1 0 +github.com/g4s8/envdoc/config.go:161.2,161.13 1 0 +github.com/g4s8/envdoc/config.go:161.13,163.3 1 0 +github.com/g4s8/envdoc/config.go:166.31,168.41 2 0 +github.com/g4s8/envdoc/config.go:168.41,170.3 1 0 +github.com/g4s8/envdoc/config.go:171.2,171.37 1 0 +github.com/g4s8/envdoc/config.go:171.37,173.3 1 0 +github.com/g4s8/envdoc/config.go:174.2,175.12 2 0 +github.com/g4s8/envdoc/converter.go:15.68,20.2 1 1 +github.com/g4s8/envdoc/converter.go:22.91,24.26 2 1 +github.com/g4s8/envdoc/converter.go:24.26,25.16 1 1 +github.com/g4s8/envdoc/converter.go:25.16,26.12 1 1 +github.com/g4s8/envdoc/converter.go:28.3,28.29 1 1 +github.com/g4s8/envdoc/converter.go:28.29,29.17 1 1 +github.com/g4s8/envdoc/converter.go:29.17,30.13 1 1 +github.com/g4s8/envdoc/converter.go:32.4,32.52 1 1 +github.com/g4s8/envdoc/converter.go:35.2,35.15 1 1 +github.com/g4s8/envdoc/converter.go:38.81,45.2 3 1 +github.com/g4s8/envdoc/converter.go:47.113,49.27 2 1 +github.com/g4s8/envdoc/converter.go:49.27,50.24 1 1 +github.com/g4s8/envdoc/converter.go:50.24,53.12 2 1 +github.com/g4s8/envdoc/converter.go:55.3,55.64 1 1 +github.com/g4s8/envdoc/converter.go:57.2,57.14 1 1 +github.com/g4s8/envdoc/converter.go:60.110,63.44 3 1 +github.com/g4s8/envdoc/converter.go:63.44,65.3 1 1 +github.com/g4s8/envdoc/converter.go:65.8,65.48 1 1 +github.com/g4s8/envdoc/converter.go:65.48,67.32 2 1 +github.com/g4s8/envdoc/converter.go:67.32,69.4 1 1 +github.com/g4s8/envdoc/converter.go:71.2,71.29 1 1 +github.com/g4s8/envdoc/converter.go:71.29,76.3 1 1 +github.com/g4s8/envdoc/converter.go:78.2,79.56 2 1 +github.com/g4s8/envdoc/converter.go:79.56,80.42 1 1 +github.com/g4s8/envdoc/converter.go:80.42,81.20 1 1 +github.com/g4s8/envdoc/converter.go:82.20,83.25 1 1 +github.com/g4s8/envdoc/converter.go:84.18,85.23 1 1 +github.com/g4s8/envdoc/converter.go:86.20,88.25 2 1 +github.com/g4s8/envdoc/converter.go:89.16,90.25 1 1 +github.com/g4s8/envdoc/converter.go:95.2,95.54 1 1 +github.com/g4s8/envdoc/converter.go:95.54,97.3 1 1 +github.com/g4s8/envdoc/converter.go:99.2,99.58 1 1 +github.com/g4s8/envdoc/converter.go:99.58,101.3 1 1 +github.com/g4s8/envdoc/converter.go:101.8,101.49 1 1 +github.com/g4s8/envdoc/converter.go:101.49,103.3 1 1 +github.com/g4s8/envdoc/converter.go:105.2,106.52 2 1 +github.com/g4s8/envdoc/converter.go:106.52,109.3 2 1 +github.com/g4s8/envdoc/converter.go:111.2,112.24 2 1 +github.com/g4s8/envdoc/converter.go:113.27,114.62 1 1 +github.com/g4s8/envdoc/converter.go:115.87,116.19 1 1 +github.com/g4s8/envdoc/converter.go:116.19,117.9 1 1 +github.com/g4s8/envdoc/converter.go:119.3,120.17 2 1 +github.com/g4s8/envdoc/converter.go:120.17,122.9 2 1 +github.com/g4s8/envdoc/converter.go:124.3,124.64 1 1 +github.com/g4s8/envdoc/converter.go:127.2,128.29 2 1 +github.com/g4s8/envdoc/converter.go:128.29,135.3 1 1 +github.com/g4s8/envdoc/converter.go:136.2,136.12 1 1 +github.com/g4s8/envdoc/parser.go:15.47,16.25 1 0 +github.com/g4s8/envdoc/parser.go:16.25,18.3 1 0 +github.com/g4s8/envdoc/parser.go:21.71,22.25 1 0 +github.com/g4s8/envdoc/parser.go:22.25,25.3 2 0 +github.com/g4s8/envdoc/parser.go:37.84,44.27 2 1 +github.com/g4s8/envdoc/parser.go:44.27,46.3 1 0 +github.com/g4s8/envdoc/parser.go:48.2,48.10 1 1 +github.com/g4s8/envdoc/parser.go:51.51,54.22 3 1 +github.com/g4s8/envdoc/parser.go:54.22,56.17 2 1 +github.com/g4s8/envdoc/parser.go:56.17,58.4 1 0 +github.com/g4s8/envdoc/parser.go:59.3,59.14 1 1 +github.com/g4s8/envdoc/parser.go:62.2,63.22 2 1 +github.com/g4s8/envdoc/parser.go:63.22,65.3 1 0 +github.com/g4s8/envdoc/parser.go:65.8,67.17 2 1 +github.com/g4s8/envdoc/parser.go:67.17,69.4 1 0 +github.com/g4s8/envdoc/parser.go:70.3,70.49 1 1 +github.com/g4s8/envdoc/parser.go:72.2,72.22 1 1 +github.com/g4s8/envdoc/parser.go:72.22,74.17 2 1 +github.com/g4s8/envdoc/parser.go:74.17,76.4 1 0 +github.com/g4s8/envdoc/parser.go:77.3,77.49 1 1 +github.com/g4s8/envdoc/parser.go:80.2,82.13 2 1 +github.com/g4s8/envdoc/parser.go:82.13,84.3 1 0 +github.com/g4s8/envdoc/parser.go:86.2,86.86 1 1 +github.com/g4s8/envdoc/parser.go:86.86,87.17 1 1 +github.com/g4s8/envdoc/parser.go:87.17,89.4 1 0 +github.com/g4s8/envdoc/parser.go:90.3,90.20 1 1 +github.com/g4s8/envdoc/parser.go:90.20,92.4 1 1 +github.com/g4s8/envdoc/parser.go:93.3,93.60 1 1 +github.com/g4s8/envdoc/parser.go:93.60,95.4 1 0 +github.com/g4s8/envdoc/parser.go:96.3,96.13 1 1 +github.com/g4s8/envdoc/parser.go:97.17,99.3 1 0 +github.com/g4s8/envdoc/parser.go:101.2,101.13 1 1 +github.com/g4s8/envdoc/parser.go:101.13,104.3 2 0 +github.com/g4s8/envdoc/parser.go:106.2,106.25 1 1 +github.com/g4s8/envdoc/parser.go:109.110,111.16 2 1 +github.com/g4s8/envdoc/parser.go:111.16,113.3 1 0 +github.com/g4s8/envdoc/parser.go:115.2,115.27 1 1 +github.com/g4s8/envdoc/parser.go:115.27,117.3 1 1 +github.com/g4s8/envdoc/parser.go:118.2,118.12 1 1 +github.com/g4s8/envdoc/render.go:14.79,20.2 1 0 +github.com/g4s8/envdoc/render.go:22.68,25.18 3 0 +github.com/g4s8/envdoc/render.go:26.25,28.23 2 0 +github.com/g4s8/envdoc/render.go:29.21,31.19 2 0 +github.com/g4s8/envdoc/render.go:32.20,34.24 2 0 +github.com/g4s8/envdoc/render.go:35.20,37.21 2 0 +github.com/g4s8/envdoc/render.go:38.10,39.52 1 0 +github.com/g4s8/envdoc/render.go:42.2,45.34 3 0 +github.com/g4s8/envdoc/render.go:45.34,47.3 1 0 +github.com/g4s8/envdoc/render.go:48.2,48.12 1 0 +github.com/g4s8/envdoc/render.go:72.58,75.35 3 0 +github.com/g4s8/envdoc/render.go:75.35,78.3 2 0 +github.com/g4s8/envdoc/render.go:79.2,79.12 1 0 +github.com/g4s8/envdoc/render.go:102.108,109.31 3 0 +github.com/g4s8/envdoc/render.go:109.31,115.35 2 0 +github.com/g4s8/envdoc/render.go:115.35,119.4 3 0 +github.com/g4s8/envdoc/render.go:120.3,120.28 1 0 +github.com/g4s8/envdoc/render.go:122.2,122.12 1 0 +github.com/g4s8/envdoc/render.go:125.67,127.38 2 0 +github.com/g4s8/envdoc/render.go:127.38,129.3 1 0 +github.com/g4s8/envdoc/render.go:130.2,140.3 1 0 +github.com/g4s8/envdoc/render.go:199.72,200.52 1 0 +github.com/g4s8/envdoc/render.go:200.52,201.43 1 0 +github.com/g4s8/envdoc/render.go:201.43,203.4 1 0 +github.com/g4s8/envdoc/render.go:204.3,204.13 1 0 +github.com/g4s8/envdoc/resolver.go:16.38,20.2 1 1 +github.com/g4s8/envdoc/resolver.go:22.68,23.26 1 1 +github.com/g4s8/envdoc/resolver.go:23.26,25.3 1 1 +github.com/g4s8/envdoc/resolver.go:28.69,30.2 1 1 +github.com/g4s8/envdoc/resolver.go:32.59,34.26 2 1 +github.com/g4s8/envdoc/resolver.go:34.26,37.3 2 1 +github.com/g4s8/envdoc/resolver.go:38.2,38.10 1 1 +github.com/g4s8/envdoc/tags.go:7.41,9.45 2 1 +github.com/g4s8/envdoc/tags.go:9.45,10.37 1 1 +github.com/g4s8/envdoc/tags.go:10.37,11.12 1 1 +github.com/g4s8/envdoc/tags.go:13.3,15.52 3 1 +github.com/g4s8/envdoc/tags.go:15.52,17.4 1 1 +github.com/g4s8/envdoc/tags.go:19.2,19.10 1 1 +github.com/g4s8/envdoc/tags.go:22.47,24.2 1 1 +github.com/g4s8/envdoc/tags.go:26.55,27.22 1 1 +github.com/g4s8/envdoc/tags.go:27.22,29.3 1 1 +github.com/g4s8/envdoc/tags.go:30.2,30.24 1 1 +github.com/g4s8/envdoc/tags.go:33.51,35.39 2 1 +github.com/g4s8/envdoc/tags.go:35.39,37.3 1 1 +github.com/g4s8/envdoc/tags.go:38.2,40.45 3 1 +github.com/g4s8/envdoc/tags.go:40.45,42.3 1 1 +github.com/g4s8/envdoc/tags.go:43.2,44.18 2 1 +github.com/g4s8/envdoc/tags.go:44.18,46.3 1 1 +github.com/g4s8/envdoc/tags.go:47.2,48.37 2 1 +github.com/g4s8/envdoc/templates.go:17.56,19.3 1 1 +github.com/g4s8/envdoc/templates.go:21.30,23.3 1 1 +github.com/g4s8/envdoc/templates.go:24.34,26.3 1 1 +github.com/g4s8/envdoc/templates.go:27.31,29.26 2 1 +github.com/g4s8/envdoc/templates.go:29.26,31.4 1 1 +github.com/g4s8/envdoc/templates.go:32.3,32.13 1 1 +github.com/g4s8/envdoc/templates.go:41.50,47.2 1 1 +github.com/g4s8/envdoc/utils.go:13.60,15.16 2 1 +github.com/g4s8/envdoc/utils.go:15.16,17.3 1 1 +github.com/g4s8/envdoc/utils.go:18.2,18.21 1 1 +github.com/g4s8/envdoc/utils.go:21.69,23.16 2 1 +github.com/g4s8/envdoc/utils.go:23.16,25.3 1 1 +github.com/g4s8/envdoc/utils.go:26.2,26.35 1 1 +github.com/g4s8/envdoc/utils.go:26.35,28.3 1 1 +github.com/g4s8/envdoc/utils.go:31.36,39.22 7 1 +github.com/g4s8/envdoc/utils.go:39.22,43.19 3 1 +github.com/g4s8/envdoc/utils.go:43.19,45.4 1 1 +github.com/g4s8/envdoc/utils.go:46.3,46.102 1 1 +github.com/g4s8/envdoc/utils.go:46.102,48.4 1 1 +github.com/g4s8/envdoc/utils.go:49.3,50.11 2 1 +github.com/g4s8/envdoc/utils.go:53.2,53.24 1 1 +github.com/g4s8/envdoc/debug/logger.go:19.38,21.2 1 0 +github.com/g4s8/envdoc/debug/logger.go:23.61,25.2 1 0 +github.com/g4s8/envdoc/debug/logger.go:27.45,29.2 1 0 +github.com/g4s8/envdoc/debug/logger.go:35.41,37.2 1 0 +github.com/g4s8/envdoc/debug/logger.go:39.63,42.2 2 0 +github.com/g4s8/envdoc/debug/logger.go:44.47,47.2 2 0 +github.com/g4s8/envdoc/debug/logger.go:51.63,51.64 0 1 +github.com/g4s8/envdoc/debug/logger.go:53.47,53.48 0 0 +github.com/g4s8/envdoc/debug/logger.go:59.18,60.21 1 1 +github.com/g4s8/envdoc/debug/logger.go:60.21,63.3 2 1 +github.com/g4s8/envdoc/debug/logger.go:64.2,64.31 1 0 +github.com/g4s8/envdoc/debug/logger.go:67.34,68.19 1 0 +github.com/g4s8/envdoc/debug/logger.go:68.19,70.3 1 0 +github.com/g4s8/envdoc/debug/logger.go:71.2,72.19 2 0 +github.com/g4s8/envdoc/debug/logger.go:72.19,74.3 1 0 +github.com/g4s8/envdoc/debug/logger.go:75.2,75.27 1 0 +github.com/g4s8/envdoc/debug/logger.go:78.47,79.19 1 1 +github.com/g4s8/envdoc/debug/logger.go:79.19,81.3 1 1 +github.com/g4s8/envdoc/debug/logger.go:82.2,82.30 1 1 +github.com/g4s8/envdoc/debug/logger.go:85.31,86.19 1 0 +github.com/g4s8/envdoc/debug/logger.go:86.19,88.3 1 0 +github.com/g4s8/envdoc/debug/logger.go:89.2,89.21 1 0 diff --git a/testdata/_cases.yaml b/testdata/_cases.yaml new file mode 100644 index 0000000..086d545 --- /dev/null +++ b/testdata/_cases.yaml @@ -0,0 +1,451 @@ +test_cases: + +- src_file: all.go + file_glob: "*.go" + type_glob: "*" + files: + - name: all.go + pkg: testdata + export: true + types: + - name: Foo + export: true + fields: + - names: [One] + doc: One is a one. + tag: env:"ONE" + type_ref: {name: string, kind: Ident} + - names: [Two] + doc: Two is a two. + tag: env:"TWO" + type_ref: {name: string, kind: Ident} + - name: Bar + export: true + doc: Bar is a bar. + fields: + - names: [Three] + doc: Three is a three. + tag: env:"THREE" + type_ref: {name: string, kind: Ident} + - names: [Four] + doc: Four is a four. + tag: env:"FOUR" + type_ref: {name: string, kind: Ident} + +- src_file: anonymous.go + file_glob: "*.go" + type_glob: "*" + files: + - name: anonymous.go + pkg: main + export: true + types: + - name: Config + export: true + doc: Config is the configuration for the application. + fields: + - names: [Repo] + doc: Repo is the configuration for the repository. + tag: envPrefix:"REPO_" + type_ref: {kind: Struct} + fields: + - names: [Conn] + doc: Conn is the connection string for the repository. + tag: env:"CONN,notEmpty" + type_ref: {name: string, kind: Ident} + +- src_file: arrays.go + file_glob: "*.go" + type_glob: "*" + files: + - name: arrays.go + pkg: testdata + export: true + types: + - name: Arrays + export: true + doc: Arrays stub + fields: + - names: [DotSeparated] + doc: DotSeparated stub + tag: env:"DOT_SEPARATED" envSeparator:"." + type_ref: {name: string, kind: Array} + - names: [CommaSeparated] + doc: CommaSeparated stub + tag: env:"COMMA_SEPARATED" + type_ref: {name: string, kind: Array} + +- src_file: comments.go + file_glob: "*.go" + type_glob: "*" + files: + - name: comments.go + pkg: testdata + export: true + types: + - name: Comments + export: true + fields: + - names: [Foo] + doc: Foo stub + tag: env:"FOO" + type_ref: {name: int, kind: Ident} + - names: [Bar] + doc: Bar stub + tag: env:"BAR" + type_ref: {name: int, kind: Ident} + +- src_file: embedded.go + file_glob: "*.go" + type_glob: Config + files: + - name: embedded.go + pkg: testdata + export: true + types: + - name: Config + export: true + fields: + - names: [Start] + doc: Start date. + tag: env:"START,notEmpty" + type_ref: {name: Date, kind: Ident} + - name: Date + export: false + doc: Date is a time.Time wrapper that uses the time.DateOnly layout. + fields: + - type_ref: {name: Time, package: time, kind: Selector} + +- src_file: envprefix.go + file_glob: "*.go" + type_glob: Settings + files: + - name: envprefix.go + pkg: main + export: true + types: + - name: Settings + export: true + doc: Settings is the application settings. + fields: + - names: [Database] + doc: Database is the database settings + tag: envPrefix:"DB_" + type_ref: {name: Database, kind: Ident} + - names: [Server] + doc: Server is the server settings + tag: envPrefix:"SERVER_" + type_ref: {name: ServerConfig, kind: Ident} + - names: [Debug] + doc: Debug is the debug flag + tag: env:"DEBUG" + type_ref: {name: bool, kind: Ident} + - name: Database + export: false + doc: Database is the database settings. + fields: + - names: [Port] + doc: Port is the port to connect to + tag: env:"PORT,required" + type_ref: {name: Int, kind: Ident} + - names: [Host] + doc: Host is the host to connect to + tag: env:"HOST,notEmpty" envDefault:"localhost" + type_ref: {name: string, kind: Ident} + - names: [User] + doc: User is the user to connect as + tag: env:"USER" + type_ref: {name: string, kind: Ident} + - names: [Password] + doc: Password is the password to use + tag: env:"PASSWORD" + type_ref: {name: string, kind: Ident} + - names: [DisableTLS] + doc: DisableTLS is the flag to disable TLS + tag: env:"DISABLE_TLS" + type_ref: {name: bool, kind: Ident} + - name: ServerConfig + export: false + doc: ServerConfig is the server settings. + fields: + - names: [Port] + doc: Port is the port to listen on + tag: env:"PORT,required" + type_ref: {name: Int, kind: Ident} + - names: [Host] + doc: Host is the host to listen on + tag: env:"HOST,notEmpty" envDefault:"localhost" + type_ref: {name: string, kind: Ident} + - names: [Timeout] + doc: Timeout is the timeout settings + tag: envPrefix:"TIMEOUT_" + type_ref: {name: TimeoutConfig, kind: Ident} + - name: TimeoutConfig + export: false + doc: TimeoutConfig is the timeout settings. + fields: + - names: [Read] + doc: Read is the read timeout + tag: env:"READ" envDefault:"30" + type_ref: {name: Int, kind: Ident} + - names: [Write] + doc: Write is the write timeout + tag: env:"WRITE" envDefault:"30" + type_ref: {name: Int, kind: Ident} + +- src_file: field_names.go + file_glob: "*.go" + type_glob: FieldNames + files: + - name: field_names.go + pkg: testdata + export: true + types: + - name: FieldNames + export: true + doc: FieldNames uses field names as env names. + fields: + - names: [Foo] + doc: Foo is a single field. + type_ref: {name: string, kind: Ident} + - names: [Bar, Baz] + doc: Bar and Baz are two fields. + type_ref: {name: string, kind: Ident} + - names: [Quux] + doc: Quux is a field with a tag. + tag: env:"QUUX" + type_ref: {name: string, kind: Ident} + - names: [FooBar] + doc: FooBar is a field with a default value. + tag: envDefault:"quuux" + type_ref: {name: string, kind: Ident} + - names: [Required] + doc: Required is a required field. + tag: env:",required" + type_ref: {name: string, kind: Ident} + +- src_file: funcs.go + file_glob: "*.go" + type_glob: aconfig + files: + - name: funcs.go + pkg: testdata + export: true + types: + - name: aconfig + export: true + fields: + - names: [somevalue] + doc: this is some value + tag: env:"SOME_VALUE" envDefault:"somevalue" + type_ref: {name: string, kind: Ident} + +- src_file: go_generate.go + file_glob: "*.go" + type_glob: "*" + files: + - name: go_generate.go + pkg: testdata + export: true + types: + - name: Type1 + export: true + fields: + - names: [Foo] + doc: Foo stub + tag: env:"FOO" + type_ref: {name: int, kind: Ident} + - name: Type2 + export: true + fields: + - names: [Baz] + doc: Baz stub + tag: env:"BAZ" + type_ref: {name: int, kind: Ident} + +- src_file: nodocs.go + file_glob: "*.go" + type_glob: Config + files: + - name: nodocs.go + pkg: main + export: true + types: + - name: Config + export: true + fields: + - names: [Repo] + type_ref: {kind: Struct} + tag: envPrefix:"REPO_" + fields: + - names: [Conn] + tag: env:"CONN,notEmpty" + type_ref: {name: string, kind: Ident} + +- src_file: tags.go + file_glob: "*.go" + type_glob: Type1 + files: + - name: tags.go + pkg: testdata + export: true + types: + - name: Type1 + export: true + fields: + - names: [Secret] + doc: Secret is a secret value that is read from a file. + tag: env:"SECRET,file" + type_ref: {name: string, kind: Ident} + - names: [Password] + doc: Password is a password that is read from a file. + tag: env:"PASSWORD,file" envDefault:"/tmp/password" json:"password" + type_ref: {name: string, kind: Ident} + - names: [Certificate] + doc: Certificate is a certificate that is read from a file. + tag: env:"CERTIFICATE,file,expand" envDefault:"${CERTIFICATE_FILE}" + type_ref: {name: string, kind: Ident} + - names: [SecretKey] + doc: Key is a secret key. + tag: env:"SECRET_KEY,required" json:"secret_key" + type_ref: {name: string, kind: Ident} + - names: [SecretVal] + doc: SecretVal is a secret value. + tag: json:"secret_val" env:"SECRET_VAL,notEmpty" + type_ref: {name: string, kind: Ident} + - names: [NotEnv] + doc: NotEnv is not an environment variable. + tag: json:"not_env" + type_ref: {name: string, kind: Ident} + - names: [NoTag] + doc: NoTag is not tagged. + type_ref: {name: string, kind: Ident} + - names: [BrokenTag] + doc: BrokenTag is a tag that is broken. + tag: 'env:"BROKEN_TAG,required' + type_ref: {name: string, kind: Ident} + +- src_file: type.go + file_glob: "*.go" + type_glob: Type* + files: + - name: type.go + pkg: testdata + export: true + types: + - name: Type1 + export: true + fields: + - names: [Foo] + doc: Foo stub + tag: env:"FOO" + type_ref: {name: int, kind: Ident} + - name: Type2 + export: true + fields: + - names: [Baz] + doc: Baz stub + tag: env:"BAZ" + type_ref: {name: int, kind: Ident} + +- src_file: typedef.go + file_glob: "*.go" + type_glob: Config + files: + - name: typedef.go + pkg: testdata + export: true + types: + - name: Config + export: true + fields: + - names: [Start] + doc: Start date. + tag: env:"START" + type_ref: {name: Date, kind: Ident} + - name: Date + export: false + doc: Date is a time.Time wrapper that uses the time.DateOnly layout. + +- src_file: unexported.go + file_glob: "*.go" + type_glob: appconfig + files: + - name: unexported.go + pkg: testdata + export: true + types: + - name: appconfig + export: true + fields: + - names: [Port] + doc: Port the application will listen on inside the container + tag: env:"PORT" envDefault:"8080" + type_ref: {name: int, kind: Ident} + +- src_dir: project + file_glob: "*/cfg/config.go" + type_glob: 'Config' + # debug: true + files: + - name: ./cfg/config.go + pkg: cfg + export: true + types: + - name: 'Config' + export: true + fields: + - names: [Environment] + doc: Environment of the application. + tag: env:"ENVIRONMENT,notEmpty" envDefault:"development" + type_ref: {name: string, kind: Ident} + - + type_ref: {name: ServerConfig, kind: Ident} + tag: envPrefix:"SERVER_" + - names: [Database] + doc: Database config. + tag: envPrefix:"DB_" + type_ref: {name: Config, kind: Selector} + - name: ./cfg/server.go + pkg: cfg + export: false + types: + - name: Config + export: true + fields: + - names: [Host] + doc: Host of the server. + tag: env:"HOST,notEmpty" + type_ref: {name: string, kind: Ident} + - names: [Port] + doc: Port of the server. + tag: env:"PORT" envDefault:"8080" + type_ref: {name: int, kind: Ident} + - name: ./db/config.go + pkg: db + export: false + types: + - name: Config + doc: Config for the database. + export: true + fields: + - names: [Host] + doc: Host of the database. + tag: env:"HOST,notEmpty" + type_ref: {name: string, kind: Ident} + - names: [Port] + doc: Port of the database. + tag: env:"PORT" envDefault:"5432" + type_ref: {name: int, kind: Ident} + - names: [Username] + doc: Username of the database. + tag: env:"USERNAME,notEmpty" + type_ref: {name: string, kind: Ident} + - names: [Password] + doc: Password of the database. + tag: env:"PASSWORD,notEmpty" + type_ref: {name: string, kind: Ident} + - names: [Name] + doc: Name of the database. + tag: env:"NAME,notEmpty" + type_ref: {name: string, kind: Ident} diff --git a/testdata/project/cfg/config.go b/testdata/project/cfg/config.go new file mode 100644 index 0000000..d467c66 --- /dev/null +++ b/testdata/project/cfg/config.go @@ -0,0 +1,14 @@ +package cfg + +import "github.com/smallstep/certificates/db" + +// Config for the application. +type Config struct { + // Environment of the application. + Environment string `env:"ENVIRONMENT,notEmpty" envDefault:"development"` + + ServerConfig `envPrefix:"SERVER_"` + + // Database config. + Database db.Config `envPrefix:"DB_"` +} diff --git a/testdata/project/cfg/server.go b/testdata/project/cfg/server.go new file mode 100644 index 0000000..383d5a9 --- /dev/null +++ b/testdata/project/cfg/server.go @@ -0,0 +1,8 @@ +package cfg + +type Config struct { + // Host of the server. + Host string `env:"HOST,notEmpty"` + // Port of the server. + Port int `env:"PORT" envDefault:"8080"` +} diff --git a/testdata/project/db/config.go b/testdata/project/db/config.go new file mode 100644 index 0000000..1218838 --- /dev/null +++ b/testdata/project/db/config.go @@ -0,0 +1,15 @@ +package db + +// Config for the database. +type Config struct { + // Host of the database. + Host string `env:"HOST,notEmpty"` + // Port of the database. + Port int `env:"PORT" envDefault:"5432"` + // Username of the database. + Username string `env:"USERNAME,notEmpty"` + // Password of the database. + Password string `env:"PASSWORD,notEmpty"` + // Name of the database. + Name string `env:"NAME,notEmpty"` +} diff --git a/utils.go b/utils.go index d5106b7..91c26c5 100644 --- a/utils.go +++ b/utils.go @@ -5,6 +5,7 @@ import ( "io/fs" "strings" "unicode" + "unicode/utf8" "github.com/gobwas/glob" ) @@ -32,9 +33,17 @@ func camelToSnake(s string) string { var result strings.Builder result.Grow(len(s) + 5) + var buf [utf8.UTFMax]byte var prev rune + var pos int for i, r := range s { - if i > 0 && prev != underscore && r != underscore && unicode.IsUpper(r) { + pos += utf8.EncodeRune(buf[:], r) + // read next rune + var next rune + if pos < len(s) { + next, _ = utf8.DecodeRuneInString(s[pos:]) + } + if i > 0 && prev != underscore && r != underscore && unicode.IsUpper(r) && (unicode.IsLower(next)) { result.WriteRune(underscore) } result.WriteRune(unicode.ToUpper(r)) diff --git a/utils_test.go b/utils_test.go new file mode 100644 index 0000000..ebd9f3d --- /dev/null +++ b/utils_test.go @@ -0,0 +1,120 @@ +package main + +import ( + "io/fs" + "testing" + "time" +) + +type fakeFileInfo struct { + name string +} + +func (fi fakeFileInfo) Name() string { + return fi.name +} + +func (fi fakeFileInfo) Size() int64 { + panic("Size() not implemented") +} + +func (fi fakeFileInfo) Mode() fs.FileMode { + panic("Mode() not implemented") +} + +func (fi fakeFileInfo) ModTime() time.Time { + panic("ModTime() not implemented") +} + +func (fi fakeFileInfo) IsDir() bool { + panic("IsDir() not implemented") +} + +func (fi fakeFileInfo) Sys() interface{} { + panic("Sys() not implemented") +} + +func globTesteer(matcher func(string) bool, targets map[string]bool) func(*testing.T) { + return func(t *testing.T) { + for target, expected := range targets { + if matcher(target) != expected { + t.Errorf("unexpected result for %q: got %v, want %v", target, !expected, expected) + } + } + } +} + +var globTestTargets = map[string]bool{ + "main.go": true, + "main_test.go": true, + "utils.go": true, + "utils_test.java": false, + "file.txt": false, + "test.go.txt": false, + "cfg/Config.go": true, +} + +func TestGlobMatcher(t *testing.T) { + m, err := newGlobMatcher("*.go") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + t.Run("match", globTesteer(m, globTestTargets)) + + t.Run("error", func(t *testing.T) { + _, err := newGlobMatcher("[") + if err == nil { + t.Fatalf("expected error but got nil") + } + }) +} + +func TestGlobFileMatcher(t *testing.T) { + m, err := newGlobFileMatcher("*.go") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + fileWrapper := func(name string) bool { + fi := fakeFileInfo{name} + return m(fi) + } + + t.Run("match", globTesteer(fileWrapper, globTestTargets)) + t.Run("error", func(t *testing.T) { + _, err := newGlobFileMatcher("[") + if err == nil { + t.Fatalf("expected error but got nil") + } + }) +} + +func TestCamelToSnake(t *testing.T) { + tests := map[string]string{ + "CamelCase": "CAMEL_CASE", + "camelCase": "CAMEL_CASE", + "camel": "CAMEL", + "Camel": "CAMEL", + "camel_case": "CAMEL_CASE", + "camel_case_": "CAMEL_CASE_", + "camel_case__": "CAMEL_CASE__", + "camelCase_": "CAMEL_CASE_", + "camelCase__": "CAMEL_CASE__", + "camel_case__snake": "CAMEL_CASE__SNAKE", + "": "", + " ": " ", + "_": "_", + "_A_": "_A_", + "ABBRFoo": "ABBR_FOO", + "FOO_BAR": "FOO_BAR", + "ЮниКод": "ЮНИ_КОД", + "ՅունիԿոդ": "ՅՈՒՆԻ_ԿՈԴ", + } + + for input, expected := range tests { + if got := camelToSnake(input); got != expected { + t.Errorf("unexpected result for %q: got %q, want %q", input, got, expected) + } + } +} From 18039132ab15f24dff70d1c66529b78ea6892a84 Mon Sep 17 00:00:00 2001 From: "Kirill Che." Date: Tue, 30 Jul 2024 12:35:35 +0400 Subject: [PATCH 4/6] doc: fix examples --- _examples/project/config.md | 16 ---------------- 1 file changed, 16 deletions(-) delete mode 100644 _examples/project/config.md diff --git a/_examples/project/config.md b/_examples/project/config.md deleted file mode 100644 index 5ca04fa..0000000 --- a/_examples/project/config.md +++ /dev/null @@ -1,16 +0,0 @@ -# Environment Variables - -## Config - - - `APP_NAME` (default: `myapp`) - AppName is the name of the application. - - `SERVER_HOST` (**required**) - Host of the server. - - `SERVER_PORT` (**required**) - Port of the server. - - `SERVER_TIMEOUT_READ` (**required**) - ReadTimeout of the server. - - `SERVER_TIMEOUT_WRITE` (**required**) - WriteTimeout of the server. - - `DB_HOST` (**required**) - Host of the database. - - `DB_PORT` (**required**) - Port of the database. - - `DB_USER` (default: `user`) - User of the database. - - `DB_PASSWORD` - Password of the database. - - `LOG_LEVEL` (default: `info`) - Level of the logging. - - `LOG_FORMAT` (default: `json`) - Format of the logging. - From 74424f658a811de47a24e2ebd42a3e5e675c0e68 Mon Sep 17 00:00:00 2001 From: "Kirill Che." Date: Tue, 30 Jul 2024 13:01:34 +0400 Subject: [PATCH 5/6] lint: fix linter warnings --- debug.go | 30 ------------------------------ model.go | 2 -- resolver_test.go | 6 +++--- 3 files changed, 3 insertions(+), 35 deletions(-) diff --git a/debug.go b/debug.go index 1475af2..519fc8e 100644 --- a/debug.go +++ b/debug.go @@ -4,7 +4,6 @@ package main import ( "fmt" - goast "go/ast" "io" "strings" @@ -16,35 +15,6 @@ var DebugConfig struct { Enabled bool } -type debugVisitor int - -func (v debugVisitor) Visit(n goast.Node) goast.Visitor { - indent := strings.Repeat(" ", int(v)) - // print only files, packages, types, struccts and fields - switch n := n.(type) { - case *goast.File: - fmt.Printf("%sFILE: %s\n", indent, n.Name.Name) - case *goast.Package: - fmt.Printf("%sPACKAGE: %s\n", indent, n.Name) - case *goast.TypeSpec: - fmt.Printf("%sTYPE: %s\n", indent, n.Name.Name) - case *goast.StructType: - fmt.Printf("%sSTRUCT\n", indent) - case *goast.Field: - var name string - if len(n.Names) > 0 { - name = n.Names[0].Name - } else { - name = "" - } - fmt.Printf("%sFIELD: %s\n", indent, name) - default: - fmt.Printf("%sNODE: %T\n", indent, n) - } - - return v + 1 -} - func printTraverse(files []*ast.FileSpec, level int) { indent := strings.Repeat(" ", level) for _, file := range files { diff --git a/model.go b/model.go index 018677d..5d8b909 100644 --- a/model.go +++ b/model.go @@ -10,8 +10,6 @@ type EnvDocItem struct { Opts EnvVarOptions // Children is a list of child environment variables. Children []*EnvDocItem - - debugName string // item name for debug logs. } type EnvScope struct { diff --git a/resolver_test.go b/resolver_test.go index b0ad6ac..094e939 100644 --- a/resolver_test.go +++ b/resolver_test.go @@ -33,7 +33,7 @@ func TestResolver(t *testing.T) { }) foo := res.Resolve(&ast.FieldTypeRef{Pkg: "main", Name: "Foo"}) if foo == nil { - t.Errorf("Foo type not resolved") + t.Fatalf("Foo type not resolved") } if foo.Name != "Foo" { t.Errorf("Invalid Foo type: %s", foo.Name) @@ -41,7 +41,7 @@ func TestResolver(t *testing.T) { bar := res.Resolve(&ast.FieldTypeRef{Pkg: "main", Name: "Bar"}) if bar == nil { - t.Errorf("Bar type not resolved") + t.Fatalf("Bar type not resolved") } if bar != nil && bar.Name != "Bar" { t.Errorf("Invalid Bar type: %s", bar.Name) @@ -49,7 +49,7 @@ func TestResolver(t *testing.T) { baz := res.Resolve(&ast.FieldTypeRef{Pkg: "test", Name: "Baz"}) if baz == nil { - t.Errorf("Baz type not resolved") + t.Fatalf("Baz type not resolved") } if baz.Name != "Baz" { t.Errorf("Invalid Baz type: %s", baz.Name) From 591db749fdd092eb9c6cdd21962e113cb1c28270 Mon Sep 17 00:00:00 2001 From: "Kirill Che." Date: Tue, 30 Jul 2024 13:07:03 +0400 Subject: [PATCH 6/6] ci: bump golangci version --- .github/workflows/go.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 6344410..9670f47 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -46,4 +46,4 @@ jobs: - name: golangci-lint uses: golangci/golangci-lint-action@v3 with: - version: v1.53.3 + version: v1.59.1