Basic concept
The reflect
package does not provide access to exported identifiers, as there is no guarantee they will be linked to the executable binary (and thus available at runtime); more on this: Splitting client/server code; and How to remove unused code at compile time?
This is a source-code level checking. What I would do is write a test that checks if the number of error code constants matches the map length. The solution below will only check the map length. An improved version (see below) may also check if the keys in the map match the values of the constant declarations too.
You may use the go/parser
to parse the Go file containing the error code constants, which gives you an ast.File
describing the file, containing the constant declarations. You just need to walk through it, and count the error code constant declarations.
Let's say your original file is named "errcodes.go"
, write a test file named "errcodes_test.go"
.
This is how the test function could look like:
func TestMap(t *testing.T) {
fset := token.NewFileSet()
f, err := parser.ParseFile(fset, "errcodes.go", nil, 0)
if err != nil {
t.Errorf("Failed to parse file: %v", err)
return
}
errCodeCount := 0
// Range through declarations:
for _, dd := range f.Decls {
if gd, ok := dd.(*ast.GenDecl); ok {
// Find constant declrations:
if gd.Tok == token.CONST {
for _, sp := range gd.Specs {
if valSp, ok := sp.(*ast.ValueSpec); ok {
for _, name := range valSp.Names {
// Count those that start with "APIErrorCode"
if strings.HasPrefix(name.Name, "APIErrorCode") {
errCodeCount++
}
}
}
}
}
}
}
if exp, got := errCodeCount, len(APIErrorCodeMessages); exp != got {
t.Errorf("Expected %d err codes, got: %d", exp, got)
}
}
Running go test
will result in:
--- FAIL: TestMap (0.00s)
errcodes_test.go:39: Expected 2 err codes, got: 1
The test properly reveals that there are 2 constant error code declarations, but the APIErrorCodeMessages
map contains only 1 entry.
If we now "complete" the map:
var APIErrorCodeMessages = map[APIErrorCode]string{
APIErrorCodeInternalError: "Internal Error",
APIErrorCodeAuthentication: "asdf",
}
And run go test
again:
PASS
Note: it's a matter of style, but the big loop may be written this way to decrease nesting level:
// Range through declarations:
for _, dd := range f.Decls {
gd, ok := dd.(*ast.GenDecl)
if !ok {
continue
}
// Find constant declrations:
if gd.Tok != token.CONST {
continue
}
for _, sp := range gd.Specs {
valSp, ok := sp.(*ast.ValueSpec)
if !ok {
continue
}
for _, name := range valSp.Names {
// Count those that start with "APIErrorCode"
if strings.HasPrefix(name.Name, "APIErrorCode") {
errCodeCount++
}
}
}
}
Full, improved detection
This time we will check the exact type of the constants, not their names. We will also gather all the constant values, and in the end we will check each if that exact constant value is in the map. If something is missing, we will print the exact values of the missing codes.
So here it is:
func TestMap(t *testing.T) {
fset := token.NewFileSet()
f, err := parser.ParseFile(fset, "errcodes.go", nil, 0)
if err != nil {
t.Errorf("Failed to parse file: %v", err)
return
}
var keys []APIErrorCode
// Range through declarations:
for _, dd := range f.Decls {
gd, ok := dd.(*ast.GenDecl)
if !ok {
continue
}
// Find constant declrations:
if gd.Tok != token.CONST {
continue
}
for _, sp := range gd.Specs {
// Filter by APIErrorCode type:
valSp, ok := sp.(*ast.ValueSpec)
if !ok {
continue
}
if id, ok2 := valSp.Type.(*ast.Ident); !ok2 ||
id.Name != "APIErrorCode" {
continue
}
// And gather the constant values in keys:
for _, value := range valSp.Values {
bslit, ok := value.(*ast.BasicLit)
if !ok {
continue
}
keyValue, err := strconv.Atoi(bslit.Value)
if err != nil {
t.Errorf("Could not parse value from %v: %v",
bslit.Value, err)
}
keys = append(keys, APIErrorCode(keyValue))
}
}
}
for _, key := range keys {
if _, found := APIErrorCodeMessages[key]; !found {
t.Errorf("Could not found key in map: %v", key)
}
}
}
Running go test
with an "incomplete" APIErrorCodeMessages
map, we get the following output:
--- FAIL: TestMap (0.00s)
errcodes_test.go:58: Could not found key in map: 1000