-
Notifications
You must be signed in to change notification settings - Fork 21
feat: auto generate configuration docs from code to keep docs in sync #406
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
|
The latest updates on your projects. Learn more about Vercel for Git ↗︎
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull Request Overview
This PR enhances the auto-generation of configuration documentation by augmenting configuration struct fields with additional struct tags (desc and required) to keep docs in sync with code. Key changes include:
- Adding descriptive tags and required-status indicators across multiple configuration files.
- Updating the go:generate directive and associated documentation plan to support automated documentation generation.
- Expanding configuration details for various providers (AWS SQS, GCP Pub/Sub, RabbitMQ, Portal, OpenTelemetry, etc.).
Reviewed Changes
Copilot reviewed 9 out of 9 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
| internal/config/publishmq.go | Added desc and required tags for PublishAWSSQSConfig, PublishGCPPubSubConfig, and PublishRabbitMQConfig. |
| internal/config/portal.go | Added descriptive metadata to PortalConfig fields. |
| internal/config/otel.go | Updated OpenTelemetry configuration structs with description and required-status tags. |
| internal/config/mq.go | Augmented MQ provider configuration structs (AWSSQSConfig, GCPPubSubConfig, RabbitMQConfig, MQsConfig) with tags. |
| internal/config/destinations.go | Enhanced destination configuration structs with descriptive tags. |
| internal/config/config.go | Updated the main Config struct and related configurations with additional tags and a go:generate directive. |
| contributing/.plans/2025-05-26-config-documentation-synchronization.md | Added a detailed plan outlining the approach for synchronizing configuration code and docs. |
- Updated the required status of several configuration fields in the Config struct to improve clarity and flexibility. - Adjusted descriptions for various fields to provide clearer guidance on their usage and requirements. - Removed default values from descriptions where applicable to avoid confusion. - Ensured consistency in required fields across different configuration types, including Redis, MQ, and Portal configurations. - Enhanced webhook and AWS Kinesis configurations by refining descriptions and removing unnecessary defaults.
| SignatureContentTemplate: "{{.Timestamp.Unix}}.{{.Body}}", | ||
| SignatureHeaderTemplate: "t={{.Timestamp.Unix}},v0={{.Signatures | join \",\"}}", | ||
| SignatureEncoding: "hex", | ||
| SignatureAlgorithm: "hmac-sha256", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note that this is a logic change that has crept in. Need to check whether we should remove this. cc: @alexluong
alexluong
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hey @leggetter, thanks for taking the initiative to set this up. The codegen command was interesting and seems to work quite well. Overall, the PR looks great and doesn't seem to affect the functionality. All test passing right now. We can merge this as is. I do have 2 notes:
- The docgen command logic is quite complicated (AST, ahhh). I didn't do a full audit but I did run it locally and seems like it works well. My only feedback is I think we could consider a different approach that is a bit simpler.
My understanding is we're parsing files within the internal/config package and using AST to detect config structs and parse through it.
I wonder if it's easier to use the reflect package to traverse the main Config struct. I'll share a snippet below, maybe it can be helpful if you want to explore this approach. Happy to just merge this for now and reiterate later.
- One semi-unrelated issue is the webhook header. I'll take a look later but if you have time, I'd appreciate it if you can help verify too. I think I changed the behavior of the signature where the default header "X-Outpost-Timestamp" is in epoch timestamp in millisecond, but in the "X-Outpost-Signature", the timestamp there is in second. Because of that, if you implement verification, it won't match correctly unless you convert ms into second first.
func parseWithReflection() error {
cfg := config.Config{} // Create an instance of the actual Config struct
cfg.InitDefaults() // Initialize it with default values
// Start the recursive traversal
fmt.Println("\n--- Starting Reflection-Based Parsing ---")
parseTypeRecursive(reflect.TypeOf(cfg), reflect.ValueOf(cfg), "", 0)
fmt.Println("--- Finished Reflection-Based Parsing ---")
return nil
}
func parseTypeRecursive(typ reflect.Type, val reflect.Value, pathPrefix string, indentLevel int) {
// If 'val' is a pointer, dereference it to get the actual struct value.
// Also, update 'typ' to be the type of the dereferenced value.
if val.Kind() == reflect.Ptr {
if val.IsNil() { // Important: can't proceed if the pointer is nil
indent := strings.Repeat(" ", indentLevel)
fmt.Printf("%sStruct: %s (Path: %s) is a <nil pointer>\n", indent, typ.String(), pathPrefix)
return
}
val = val.Elem()
typ = val.Type() // Update typ to the element's type
}
// Ensure we are dealing with a struct.
if val.Kind() != reflect.Struct {
// This can happen if a field is a basic type, slice, map, etc.,
// and a recursive call was made on it.
// Or if the initial call wasn't a struct or pointer to struct.
return
}
// typ should now definitely be a struct type because val is a struct value.
indent := strings.Repeat(" ", indentLevel)
fmt.Printf("%sStruct: %s (Path: %s)\n", indent, typ.Name(), pathPrefix) // typ.Name() is fine here
configPkgPath := reflect.TypeOf(config.Config{}).PkgPath() // For checking recursion scope
for i := 0; i < typ.NumField(); i++ {
fieldDesc := typ.Field(i) // reflect.StructField - describes the field
fieldVal := val.Field(i) // reflect.Value - holds the runtime value of this field
currentPath := fieldDesc.Name
if pathPrefix != "" {
currentPath = pathPrefix + "." + fieldDesc.Name
}
fmt.Printf("%s Field: %s (Type: %s, Path: %s)\n", indent, fieldDesc.Name, fieldDesc.Type.String(), currentPath)
fmt.Printf("%s YAML: '%s', Env: '%s', Desc: '%s', Required: '%s'\n",
indent,
fieldDesc.Tag.Get("yaml"),
fieldDesc.Tag.Get("env"),
fieldDesc.Tag.Get("desc"),
fieldDesc.Tag.Get("required"))
// Correctly use fieldVal to get the current value
var currentValueInterface interface{}
if fieldVal.CanInterface() { // Check if the field is exported
currentValueInterface = fieldVal.Interface()
if fieldVal.Kind() == reflect.Ptr && fieldVal.IsNil() {
currentValueInterface = "<nil pointer>"
}
} else {
currentValueInterface = "<unexported field>"
}
fmt.Printf("%s Current Value: %v\n", indent, currentValueInterface)
// --- Recursive Call Logic ---
// We need the type of the field for recursion checks, and the value for the next call.
fieldTypeForRecursion := fieldDesc.Type
fieldValueForRecursion := fieldVal
// Determine the actual element type if fieldTypeForRecursion is a pointer
actualFieldTypeForCheck := fieldTypeForRecursion
if actualFieldTypeForCheck.Kind() == reflect.Ptr {
actualFieldTypeForCheck = actualFieldTypeForCheck.Elem()
}
if actualFieldTypeForCheck.Kind() == reflect.Struct {
if actualFieldTypeForCheck.PkgPath() == configPkgPath {
// Pass the field's type and the field's value to the recursive call.
parseTypeRecursive(fieldTypeForRecursion, fieldValueForRecursion, currentPath, indentLevel+1)
} else {
fmt.Printf("%s (Skipping recursion into struct from different package: %s from %s)\n", indent, actualFieldTypeForCheck.Name(), actualFieldTypeForCheck.PkgPath())
}
}
}
} |
Captured in #412
Let's merge and I'll create an issue to investigate improving the docs generator #415 |
See contributing/.plans/2025-05-26-config-documentation-synchronization.md