github.com/abemedia/appcast@v0.4.0/pkg/pipe/validate.go (about)

     1  package pipe
     2  
     3  import (
     4  	"fmt"
     5  	"io/fs"
     6  	"reflect"
     7  	"strings"
     8  	"unicode"
     9  
    10  	"github.com/abemedia/appcast/pkg/version"
    11  	locales "github.com/go-playground/locales/en"
    12  	ut "github.com/go-playground/universal-translator"
    13  	"github.com/go-playground/validator/v10"
    14  	translations "github.com/go-playground/validator/v10/translations/en"
    15  	"golang.org/x/mod/semver"
    16  )
    17  
    18  type Error struct{ Errors []string }
    19  
    20  func (e *Error) Error() string {
    21  	return fmt.Sprintf("invalid config:\n  %s", strings.Join(e.Errors, "\n  "))
    22  }
    23  
    24  func Validate(c *config) error {
    25  	v := validator.New(validator.WithRequiredStructEnabled(), validator.WithOmitAnonymousName())
    26  	v.RegisterTagNameFunc(func(fld reflect.StructField) string {
    27  		name, _, _ := strings.Cut(fld.Tag.Get("yaml"), ",")
    28  		if name == "-" {
    29  			return ""
    30  		}
    31  		return name
    32  	})
    33  	_ = v.RegisterValidation("dirname", isDirname)
    34  	_ = v.RegisterValidation("version", isVersion)
    35  	_ = v.RegisterValidation("version_constraint", isConstraint)
    36  
    37  	uni := ut.New(locales.New())
    38  	trans, _ := uni.GetTranslator("en")
    39  	_ = registerTranslations(v, trans)
    40  
    41  	err := v.Struct(c)
    42  	validationErrors, ok := err.(validator.ValidationErrors)
    43  	if !ok {
    44  		return err
    45  	}
    46  
    47  	errs := &Error{Errors: make([]string, len(validationErrors))}
    48  	for i, ve := range validationErrors {
    49  		_, ns, _ := strings.Cut(ve.Namespace(), ".")
    50  		errs.Errors[i] = ns + strings.TrimPrefix(ve.Translate(trans), ve.Field())
    51  	}
    52  
    53  	return errs
    54  }
    55  
    56  func registerTranslations(v *validator.Validate, trans ut.Translator) error {
    57  	err := translations.RegisterDefaultTranslations(v, trans)
    58  	if err != nil {
    59  		return err
    60  	}
    61  
    62  	translations := []struct {
    63  		name    string
    64  		message string
    65  	}{
    66  		{
    67  			name:    "dir",
    68  			message: "{0} must be a valid path to a directory",
    69  		},
    70  		{
    71  			name:    "dirname",
    72  			message: "{0} must be a valid folder name",
    73  		},
    74  		{
    75  			name:    "version_constraint",
    76  			message: "{0} must be a valid version constraint",
    77  		},
    78  		{
    79  			name:    "version",
    80  			message: "{0} must be a valid semver version",
    81  		},
    82  		{
    83  			name:    "http_url",
    84  			message: "{0} must be a valid URL",
    85  		},
    86  		{
    87  			name:    "fqdn|http_url",
    88  			message: "{0} must be a valid URL or FQDN",
    89  		},
    90  	}
    91  
    92  	for _, t := range translations {
    93  		err = v.RegisterTranslation(t.name, trans, func(ut ut.Translator) error {
    94  			return ut.Add(t.name, t.message, true)
    95  		}, func(ut ut.Translator, fe validator.FieldError) string {
    96  			t, _ := ut.T(fe.Tag(), fe.Field())
    97  			return t
    98  		})
    99  		if err != nil {
   100  			return err
   101  		}
   102  	}
   103  
   104  	return nil
   105  }
   106  
   107  // isDirname validates if a dirname is valid in an URL, as well as common targets.
   108  //
   109  //   - RFC3986 allows the following in a URL path (i.e. pchar): A-Z a-z 0-9 - . _ ~ ! $ & ' ( ) * + , ; = : @
   110  //   - GCS forbids # [ ] * ? : " < > | (See https://cloud.google.com/storage/docs/objects#naming)
   111  //   - S3 forbids \ { ^ } % ` ] " > [ ~ < # | (See https://docs.aws.amazon.com/AmazonS3/latest/userguide/object-keys.html#object-key-guidelines-avoid-characters)
   112  //   - This leaves us with letters, numbers and - . _ ! $ ' ( ) + , ; = @
   113  func isDirname(fl validator.FieldLevel) bool {
   114  	s := fl.Field().String()
   115  	return fs.ValidPath(s) && !strings.ContainsFunc(s, func(r rune) bool {
   116  		return !unicode.IsLetter(r) && !unicode.IsNumber(r) && !strings.ContainsRune("/-._!$&'()+,;=@ ", r)
   117  	})
   118  }
   119  
   120  func isVersion(fl validator.FieldLevel) bool {
   121  	v := fl.Field().String()
   122  	if !strings.HasPrefix(v, "v") {
   123  		v = "v" + v
   124  	}
   125  	return semver.IsValid(v)
   126  }
   127  
   128  func isConstraint(fl validator.FieldLevel) bool {
   129  	_, err := version.NewConstraint(fl.Field().String())
   130  	return err == nil
   131  }