refactor: start splitting up files
This commit is contained in:
parent
404b1e0a46
commit
36d33895b7
@ -1,7 +1,6 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/url"
|
"net/url"
|
||||||
@ -13,18 +12,10 @@ import (
|
|||||||
"gopkg.in/yaml.v3"
|
"gopkg.in/yaml.v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
const rootRegistryPath = "./registry"
|
|
||||||
|
|
||||||
var (
|
var (
|
||||||
validContributorStatuses = []string{"official", "partner", "community"}
|
validContributorStatuses = []string{"official", "partner", "community"}
|
||||||
supportedAvatarFileFormats = []string{".png", ".jpeg", ".jpg", ".gif", ".svg"}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type readme struct {
|
|
||||||
filePath string
|
|
||||||
rawText string
|
|
||||||
}
|
|
||||||
|
|
||||||
type contributorProfileFrontmatter struct {
|
type contributorProfileFrontmatter struct {
|
||||||
DisplayName string `yaml:"display_name"`
|
DisplayName string `yaml:"display_name"`
|
||||||
Bio string `yaml:"bio"`
|
Bio string `yaml:"bio"`
|
||||||
@ -44,61 +35,6 @@ type contributorProfile struct {
|
|||||||
filePath string
|
filePath string
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ error = validationPhaseError{}
|
|
||||||
|
|
||||||
type validationPhaseError struct {
|
|
||||||
phase string
|
|
||||||
errors []error
|
|
||||||
}
|
|
||||||
|
|
||||||
func (vpe validationPhaseError) Error() string {
|
|
||||||
validationStrs := []string{}
|
|
||||||
for _, e := range vpe.errors {
|
|
||||||
validationStrs = append(validationStrs, fmt.Sprintf("- %v", e))
|
|
||||||
}
|
|
||||||
slices.Sort(validationStrs)
|
|
||||||
|
|
||||||
msg := fmt.Sprintf("Error during %q phase of README validation:", vpe.phase)
|
|
||||||
msg += strings.Join(validationStrs, "\n")
|
|
||||||
msg += "\n"
|
|
||||||
|
|
||||||
return msg
|
|
||||||
}
|
|
||||||
|
|
||||||
func extractFrontmatter(readmeText string) (string, error) {
|
|
||||||
if readmeText == "" {
|
|
||||||
return "", errors.New("README is empty")
|
|
||||||
}
|
|
||||||
|
|
||||||
const fence = "---"
|
|
||||||
fm := ""
|
|
||||||
fenceCount := 0
|
|
||||||
lineScanner := bufio.NewScanner(
|
|
||||||
strings.NewReader(strings.TrimSpace(readmeText)),
|
|
||||||
)
|
|
||||||
for lineScanner.Scan() {
|
|
||||||
nextLine := lineScanner.Text()
|
|
||||||
if fenceCount == 0 && nextLine != fence {
|
|
||||||
return "", errors.New("README does not start with frontmatter fence")
|
|
||||||
}
|
|
||||||
|
|
||||||
if nextLine != fence {
|
|
||||||
fm += nextLine + "\n"
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
fenceCount++
|
|
||||||
if fenceCount >= 2 {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if fenceCount == 1 {
|
|
||||||
return "", errors.New("README does not have two sets of frontmatter fences")
|
|
||||||
}
|
|
||||||
return fm, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func validateContributorGithubUsername(githubUsername string) error {
|
func validateContributorGithubUsername(githubUsername string) error {
|
||||||
if githubUsername == "" {
|
if githubUsername == "" {
|
||||||
return errors.New("missing GitHub username")
|
return errors.New("missing GitHub username")
|
||||||
@ -260,10 +196,6 @@ func validateContributorAvatarURL(avatarURL *string) []error {
|
|||||||
return problems
|
return problems
|
||||||
}
|
}
|
||||||
|
|
||||||
func addFilePathToError(filePath string, err error) error {
|
|
||||||
return fmt.Errorf("%q: %v", filePath, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
func validateContributorYaml(yml contributorProfile) []error {
|
func validateContributorYaml(yml contributorProfile) []error {
|
||||||
allProblems := []error{}
|
allProblems := []error{}
|
||||||
|
|
||||||
@ -297,7 +229,7 @@ func validateContributorYaml(yml contributorProfile) []error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func parseContributorProfile(rm readme) (contributorProfile, error) {
|
func parseContributorProfile(rm readme) (contributorProfile, error) {
|
||||||
fm, err := extractFrontmatter(rm.rawText)
|
fm, _, err := separateFrontmatter(rm.rawText)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return contributorProfile{}, fmt.Errorf("%q: failed to parse frontmatter: %v", rm.filePath, err)
|
return contributorProfile{}, fmt.Errorf("%q: failed to parse frontmatter: %v", rm.filePath, err)
|
||||||
}
|
}
|
||||||
@ -331,7 +263,7 @@ func parseContributorFiles(readmeEntries []readme) (map[string]contributorProfil
|
|||||||
}
|
}
|
||||||
if len(yamlParsingErrors) != 0 {
|
if len(yamlParsingErrors) != 0 {
|
||||||
return nil, validationPhaseError{
|
return nil, validationPhaseError{
|
||||||
phase: "YAML parsing",
|
phase: validationPhaseReadmeParsing,
|
||||||
errors: yamlParsingErrors,
|
errors: yamlParsingErrors,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -356,11 +288,11 @@ func parseContributorFiles(readmeEntries []readme) (map[string]contributorProfil
|
|||||||
if _, found := profilesByUsername[companyName]; found {
|
if _, found := profilesByUsername[companyName]; found {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
yamlValidationErrors = append(yamlValidationErrors, fmt.Errorf("company %q does not exist in %q directory but is referenced by these profiles: [%s]", companyName, rootRegistryPath, strings.Join(group, ", ")))
|
yamlValidationErrors = append(yamlValidationErrors, fmt.Errorf("%q: company %q does not exist but is referenced by these profiles: [%s]", rootRegistryPath, companyName, strings.Join(group, ", ")))
|
||||||
}
|
}
|
||||||
if len(yamlValidationErrors) != 0 {
|
if len(yamlValidationErrors) != 0 {
|
||||||
return nil, validationPhaseError{
|
return nil, validationPhaseError{
|
||||||
phase: "Raw YAML Validation",
|
phase: validationPhaseReadmeParsing,
|
||||||
errors: yamlValidationErrors,
|
errors: yamlValidationErrors,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -397,7 +329,7 @@ func aggregateContributorReadmeFiles() ([]readme, error) {
|
|||||||
|
|
||||||
if len(problems) != 0 {
|
if len(problems) != 0 {
|
||||||
return nil, validationPhaseError{
|
return nil, validationPhaseError{
|
||||||
phase: "FileSystem reading",
|
phase: validationPhaseFileLoad,
|
||||||
errors: problems,
|
errors: problems,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -405,7 +337,7 @@ func aggregateContributorReadmeFiles() ([]readme, error) {
|
|||||||
return allReadmeFiles, nil
|
return allReadmeFiles, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func validateRelativeUrls(
|
func validateContributorRelativeUrls(
|
||||||
contributors map[string]contributorProfile,
|
contributors map[string]contributorProfile,
|
||||||
) error {
|
) error {
|
||||||
// This function only validates relative avatar URLs for now, but it can be
|
// This function only validates relative avatar URLs for now, but it can be
|
||||||
@ -440,7 +372,7 @@ func validateRelativeUrls(
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return validationPhaseError{
|
return validationPhaseError{
|
||||||
phase: "Relative URL validation",
|
phase: validationPhaseAssetCrossReference,
|
||||||
errors: problems,
|
errors: problems,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
28
cmd/readmevalidation/errors.go
Normal file
28
cmd/readmevalidation/errors.go
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
// validationPhaseError represents an error that occurred during a specific
|
||||||
|
// phase of README validation. It should be used to collect ALL validation
|
||||||
|
// errors that happened during a specific phase, rather than the first one
|
||||||
|
// encountered.
|
||||||
|
type validationPhaseError struct {
|
||||||
|
phase validationPhase
|
||||||
|
errors []error
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ error = validationPhaseError{}
|
||||||
|
|
||||||
|
func (vpe validationPhaseError) Error() string {
|
||||||
|
msg := fmt.Sprintf("Error during %q phase of README validation:", vpe.phase.String())
|
||||||
|
for _, e := range vpe.errors {
|
||||||
|
msg += fmt.Sprintf("\n- %v", e)
|
||||||
|
}
|
||||||
|
msg += "\n"
|
||||||
|
|
||||||
|
return msg
|
||||||
|
}
|
||||||
|
|
||||||
|
func addFilePathToError(filePath string, err error) error {
|
||||||
|
return fmt.Errorf("%q: %v", filePath, err)
|
||||||
|
}
|
||||||
@ -26,7 +26,7 @@ func main() {
|
|||||||
log.Panic(err)
|
log.Panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = validateRelativeUrls(contributors)
|
err = validateContributorRelativeUrls(contributors)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Panic(err)
|
log.Panic(err)
|
||||||
}
|
}
|
||||||
|
|||||||
113
cmd/readmevalidation/readmeFiles.go
Normal file
113
cmd/readmevalidation/readmeFiles.go
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
const rootRegistryPath = "./registry"
|
||||||
|
|
||||||
|
var supportedAvatarFileFormats = []string{".png", ".jpeg", ".jpg", ".gif", ".svg"}
|
||||||
|
|
||||||
|
// readme represents a single README file within the repo (usually within the
|
||||||
|
// top-level "/registry" directory).
|
||||||
|
type readme struct {
|
||||||
|
filePath string
|
||||||
|
rawText string
|
||||||
|
}
|
||||||
|
|
||||||
|
// separateFrontmatter attempts to separate a README file's frontmatter content
|
||||||
|
// from the main README body, returning both values in that order. It does not
|
||||||
|
// validate whether the structure of the frontmatter is valid (i.e., that it's
|
||||||
|
// structured as YAML).
|
||||||
|
func separateFrontmatter(readmeText string) (string, string, error) {
|
||||||
|
if readmeText == "" {
|
||||||
|
return "", "", errors.New("README is empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
const fence = "---"
|
||||||
|
fm := ""
|
||||||
|
body := ""
|
||||||
|
fenceCount := 0
|
||||||
|
lineScanner := bufio.NewScanner(
|
||||||
|
strings.NewReader(strings.TrimSpace(readmeText)),
|
||||||
|
)
|
||||||
|
for lineScanner.Scan() {
|
||||||
|
nextLine := lineScanner.Text()
|
||||||
|
if fenceCount < 2 && nextLine == fence {
|
||||||
|
fenceCount++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// Break early if the very first line wasn't a fence, because then we
|
||||||
|
// know for certain that the README has problems
|
||||||
|
if fenceCount == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
// It should be safe to trim each line of the frontmatter on a per-line
|
||||||
|
// basis, because there shouldn't be any extra meaning attached to the
|
||||||
|
// indentation. The same does NOT apply to the README; best we can do is
|
||||||
|
// gather all the lines, and then trim around it
|
||||||
|
if inReadmeBody := fenceCount >= 2; inReadmeBody {
|
||||||
|
body += nextLine + "\n"
|
||||||
|
} else {
|
||||||
|
fm += strings.TrimSpace(nextLine) + "\n"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if fenceCount < 2 {
|
||||||
|
return "", "", errors.New("README does not have two sets of frontmatter fences")
|
||||||
|
}
|
||||||
|
if fm == "" {
|
||||||
|
return "", "", errors.New("readme has frontmatter fences but no frontmatter content")
|
||||||
|
}
|
||||||
|
|
||||||
|
return fm, strings.TrimSpace(body), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// validationPhase represents a specific phase during README validation. It is
|
||||||
|
// expected that each phase is discrete, and errors during one will prevent a
|
||||||
|
// future phase from starting.
|
||||||
|
type validationPhase int
|
||||||
|
|
||||||
|
const (
|
||||||
|
// validationPhaseFileStructureValidation indicates when the entire Registry
|
||||||
|
// directory is being verified for having all files be placed in the file
|
||||||
|
// system as expected.
|
||||||
|
validationPhaseFileStructureValidation validationPhase = iota
|
||||||
|
|
||||||
|
// validationPhaseFileLoad indicates when README files are being read from
|
||||||
|
// the file system
|
||||||
|
validationPhaseFileLoad
|
||||||
|
|
||||||
|
// validationPhaseReadmeParsing indicates when a README's frontmatter is
|
||||||
|
// being parsed as YAML. This phase does not include YAML validation.
|
||||||
|
validationPhaseReadmeParsing
|
||||||
|
|
||||||
|
// validationPhaseReadmeValidation indicates when a README's frontmatter is
|
||||||
|
// being validated as proper YAML with expected keys.
|
||||||
|
validationPhaseReadmeValidation
|
||||||
|
|
||||||
|
// validationPhaseAssetCrossReference indicates when a README's frontmatter
|
||||||
|
// is having all its relative URLs be validated for whether they point to
|
||||||
|
// valid resources.
|
||||||
|
validationPhaseAssetCrossReference
|
||||||
|
)
|
||||||
|
|
||||||
|
func (p validationPhase) String() string {
|
||||||
|
switch p {
|
||||||
|
case validationPhaseFileStructureValidation:
|
||||||
|
return "File structure validation"
|
||||||
|
case validationPhaseFileLoad:
|
||||||
|
return "Filesystem reading"
|
||||||
|
case validationPhaseReadmeParsing:
|
||||||
|
return "README parsing"
|
||||||
|
case validationPhaseReadmeValidation:
|
||||||
|
return "README validation"
|
||||||
|
case validationPhaseAssetCrossReference:
|
||||||
|
return "Cross-referencing relative asset URLs"
|
||||||
|
default:
|
||||||
|
return fmt.Sprintf("Unknown validation phase: %d", p)
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user