Add config loading and dry-run validation

This commit is contained in:
2026-05-31 01:53:13 +00:00
parent 22d0424232
commit 29dbad2967
16 changed files with 928 additions and 17 deletions

153
internal/config/validate.go Normal file
View File

@@ -0,0 +1,153 @@
package config
import (
"fmt"
"regexp"
"strings"
)
var idPattern = regexp.MustCompile(`^[a-zA-Z0-9][a-zA-Z0-9._-]*$`)
type ValidationErrors []string
func (e ValidationErrors) Error() string {
if len(e) == 1 {
return e[0]
}
return strings.Join(e, "; ")
}
func Validate(cfg Config) error {
var errs ValidationErrors
if len(cfg.Pipelines) == 0 {
errs = append(errs, "pipelines is required")
}
pipelineIDs := make(map[string]struct{}, len(cfg.Pipelines))
for pipelineIndex, pipeline := range cfg.Pipelines {
pipelineContext := fmt.Sprintf("pipelines[%d]", pipelineIndex)
if pipeline.ID == "" {
errs = append(errs, pipelineContext+".id is required")
} else if !idPattern.MatchString(pipeline.ID) {
errs = append(errs, pipelineContext+".id must be a slug-like identifier")
} else if _, exists := pipelineIDs[pipeline.ID]; exists {
errs = append(errs, "pipeline id "+pipeline.ID+" is duplicated")
} else {
pipelineIDs[pipeline.ID] = struct{}{}
}
errs = validateBackend(errs, pipelineContext+".source", pipeline.Source.Backend, pipeline.Source.Path, pipeline.Source.URI, pipeline.Source.Endpoint, pipeline.Source.Bucket)
errs = validateValidationPolicy(errs, pipelineContext+".validation", pipeline.Validation)
if len(pipeline.Destinations) == 0 {
errs = append(errs, pipelineContext+".destinations is required")
}
destinationIDs := make(map[string]struct{}, len(pipeline.Destinations))
for destinationIndex, destination := range pipeline.Destinations {
destinationContext := fmt.Sprintf("%s.destinations[%d]", pipelineContext, destinationIndex)
if destination.ID == "" {
errs = append(errs, destinationContext+".id is required")
} else if !idPattern.MatchString(destination.ID) {
errs = append(errs, destinationContext+".id must be a slug-like identifier")
} else if _, exists := destinationIDs[destination.ID]; exists {
errs = append(errs, "destination id "+destination.ID+" is duplicated in pipeline "+pipeline.ID)
} else {
destinationIDs[destination.ID] = struct{}{}
}
errs = validateBackend(errs, destinationContext, destination.Backend, destination.Path, destination.URI, destination.Endpoint, destination.Bucket)
errs = validatePublishPolicy(errs, destinationContext+".publish", destination.Publish)
errs = validateTransform(errs, destinationContext+".transform", destination.Publish, destination.Transform)
errs = validateTransferPolicy(errs, destinationContext+".transfer", destination.Transfer)
}
}
if len(errs) > 0 {
return errs
}
return nil
}
func validateBackend(errs ValidationErrors, context, backend, path, uri, endpoint, bucket string) ValidationErrors {
switch backend {
case "":
errs = append(errs, context+".backend is required")
case BackendLocal:
if path == "" {
errs = append(errs, context+".path is required for local backend")
}
case BackendSSH:
if uri == "" {
errs = append(errs, context+".uri is required for ssh backend")
}
if path == "" {
errs = append(errs, context+".path is required for ssh backend")
}
case BackendS3:
if endpoint == "" {
errs = append(errs, context+".endpoint is required for s3 backend")
}
if bucket == "" {
errs = append(errs, context+".bucket is required for s3 backend")
}
default:
errs = append(errs, context+".backend "+backend+" is unsupported")
}
return errs
}
func validateValidationPolicy(errs ValidationErrors, context string, policy ValidationPolicy) ValidationErrors {
if policy.OnDigestMismatch != ValidationActionFail {
errs = append(errs, context+".on_digest_mismatch must be "+ValidationActionFail)
}
return errs
}
func validatePublishPolicy(errs ValidationErrors, context string, policy *PublishPolicy) ValidationErrors {
if policy == nil {
errs = append(errs, context+" is required")
return errs
}
if !policy.Source && !policy.HTML {
errs = append(errs, context+" must enable source or html")
}
return errs
}
func validateTransform(errs ValidationErrors, context string, publish *PublishPolicy, transform Transform) ValidationErrors {
publishesHTML := publish != nil && publish.HTML
if transform.MarkdownToHTML == nil {
if publishesHTML {
errs = append(errs, context+".markdown_to_html is required when publish.html is true")
}
return errs
}
if publishesHTML && !transform.MarkdownToHTML.Enabled {
errs = append(errs, context+".markdown_to_html.enabled must be true when publish.html is true")
}
if transform.MarkdownToHTML.Enabled && transform.MarkdownToHTML.Mode != TransformModeSidecar {
errs = append(errs, context+".markdown_to_html.mode must be "+TransformModeSidecar)
}
if !transform.MarkdownToHTML.Enabled && transform.MarkdownToHTML.Mode != "" && transform.MarkdownToHTML.Mode != TransformModeSidecar {
errs = append(errs, context+".markdown_to_html.mode must be "+TransformModeSidecar)
}
return errs
}
func validateTransferPolicy(errs ValidationErrors, context string, policy TransferPolicy) ValidationErrors {
if policy.OnDestinationSame != TransferActionSkip && policy.OnDestinationSame != TransferActionFail {
errs = append(errs, context+".on_destination_same must be skip or fail")
}
if policy.OnDestinationOlder != TransferActionReplace && policy.OnDestinationOlder != TransferActionFail {
errs = append(errs, context+".on_destination_older must be replace or fail")
}
if policy.OnDestinationNewer != TransferActionSkip && policy.OnDestinationNewer != TransferActionFail {
errs = append(errs, context+".on_destination_newer must be skip or fail")
}
if policy.OnConflict != TransferActionFail {
errs = append(errs, context+".on_conflict must be fail")
}
return errs
}