Skip to content

Commit 6e5d960

Browse files
committed
refactor: split README logic into separate file
1 parent 3fa316d commit 6e5d960

File tree

2 files changed

+129
-47
lines changed

2 files changed

+129
-47
lines changed

cmd/readmevalidation/contributors.go

Lines changed: 2 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package main
22

33
import (
4-
"bufio"
54
"errors"
65
"fmt"
76
"net/url"
@@ -13,17 +12,7 @@ import (
1312
"gopkg.in/yaml.v3"
1413
)
1514

16-
const rootRegistryPath = "./registry"
17-
18-
var (
19-
validContributorStatuses = []string{"official", "partner", "community"}
20-
supportedAvatarFileFormats = []string{".png", ".jpeg", ".jpg", ".gif", ".svg"}
21-
)
22-
23-
type readme struct {
24-
filePath string
25-
rawText string
26-
}
15+
var validContributorStatuses = []string{"official", "partner", "community"}
2716

2817
type contributorProfileFrontmatter struct {
2918
DisplayName string `yaml:"display_name"`
@@ -65,40 +54,6 @@ func (vpe validationPhaseError) Error() string {
6554
return msg
6655
}
6756

68-
func extractFrontmatter(readmeText string) (string, error) {
69-
if readmeText == "" {
70-
return "", errors.New("README is empty")
71-
}
72-
73-
const fence = "---"
74-
fm := ""
75-
fenceCount := 0
76-
lineScanner := bufio.NewScanner(
77-
strings.NewReader(strings.TrimSpace(readmeText)),
78-
)
79-
for lineScanner.Scan() {
80-
nextLine := lineScanner.Text()
81-
if fenceCount == 0 && nextLine != fence {
82-
return "", errors.New("README does not start with frontmatter fence")
83-
}
84-
85-
if nextLine != fence {
86-
fm += nextLine + "\n"
87-
continue
88-
}
89-
90-
fenceCount++
91-
if fenceCount >= 2 {
92-
break
93-
}
94-
}
95-
96-
if fenceCount == 1 {
97-
return "", errors.New("README does not have two sets of frontmatter fences")
98-
}
99-
return fm, nil
100-
}
101-
10257
func validateContributorGithubUsername(githubUsername string) error {
10358
if githubUsername == "" {
10459
return errors.New("missing GitHub username")
@@ -297,7 +252,7 @@ func validateContributorYaml(yml contributorProfile) []error {
297252
}
298253

299254
func parseContributorProfile(rm readme) (contributorProfile, error) {
300-
fm, err := extractFrontmatter(rm.rawText)
255+
fm, _, err := separateFrontmatter(rm.rawText)
301256
if err != nil {
302257
return contributorProfile{}, fmt.Errorf("%q: failed to parse frontmatter: %v", rm.filePath, err)
303258
}

cmd/readmevalidation/readmes.go

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
package main
2+
3+
import (
4+
"bufio"
5+
"errors"
6+
"fmt"
7+
"strings"
8+
)
9+
10+
const rootRegistryPath = "./registry"
11+
12+
var supportedAvatarFileFormats = []string{".png", ".jpeg", ".jpg", ".gif", ".svg"}
13+
14+
// Readme represents a single README file within the repo (usually within the
15+
// "/registry" directory).
16+
type readme struct {
17+
filePath string
18+
rawText string
19+
}
20+
21+
// separateFrontmatter attempts to separate a README file's frontmatter content
22+
// from the main README body, returning both values in that order. It does not
23+
// validate whether the structure of the frontmatter is valid (i.e., that it's
24+
// structured as YAML).
25+
func separateFrontmatter(readmeText string) (string, string, error) {
26+
if readmeText == "" {
27+
return "", "", errors.New("README is empty")
28+
}
29+
30+
const fence = "---"
31+
fm := ""
32+
body := ""
33+
fenceCount := 0
34+
lineScanner := bufio.NewScanner(
35+
strings.NewReader(strings.TrimSpace(readmeText)),
36+
)
37+
for lineScanner.Scan() {
38+
nextLine := lineScanner.Text()
39+
if fenceCount < 2 && nextLine == fence {
40+
fenceCount++
41+
continue
42+
}
43+
// Break early if the very first line wasn't a fence, because then we
44+
// know for certain that the README has problems
45+
if fenceCount == 0 {
46+
break
47+
}
48+
49+
// It should be safe to trim each line of the frontmatter on a per-line
50+
// basis, because there shouldn't be any extra meaning attached to the
51+
// indentation. The same does NOT apply to the README; best we can do is
52+
// gather all the lines, and then trim around it
53+
if inReadmeBody := fenceCount >= 2; inReadmeBody {
54+
body += nextLine + "\n"
55+
} else {
56+
fm += strings.TrimSpace(nextLine) + "\n"
57+
}
58+
}
59+
if fenceCount < 2 {
60+
return "", "", errors.New("README does not have two sets of frontmatter fences")
61+
}
62+
if fm == "" {
63+
return "", "", errors.New("readme has frontmatter fences but no frontmatter content")
64+
}
65+
66+
return fm, strings.TrimSpace(body), nil
67+
}
68+
69+
// validationPhase represents a specific phase during README validation. It is
70+
// expected that each phase is discrete, and errors during one will prevent a
71+
// future phase from starting.
72+
type validationPhase int
73+
74+
const (
75+
// validationPhaseFilesystemRead indicates when a README file is being read
76+
// from the file system
77+
validationPhaseFilesystemRead validationPhase = iota
78+
79+
// validationPhaseReadmeParsing indicates when a README's frontmatter is being
80+
// parsed as YAML. This phase does not include YAML validation.
81+
validationPhaseReadmeParsing
82+
83+
// validationPhaseReadmeValidation indicates when a README's frontmatter is
84+
// being validated as proper YAML with expected keys.
85+
validationPhaseReadmeValidation
86+
87+
// validationPhaseAssetCrossReference indicates when a README's frontmatter
88+
// is having all its relative URLs be validated for whether they point to
89+
// valid resources.
90+
validationPhaseAssetCrossReference
91+
)
92+
93+
func (p validationPhase) String() string {
94+
switch p {
95+
case validationPhaseFilesystemRead:
96+
return "Filesystem reading"
97+
case validationPhaseReadmeParsing:
98+
return "README parsing"
99+
case validationPhaseReadmeValidation:
100+
return "README validation"
101+
case validationPhaseAssetCrossReference:
102+
return "Cross-referencing asset references"
103+
default:
104+
return "Unknown validation phase"
105+
}
106+
}
107+
108+
var _ error = ValidationPhaseError{}
109+
110+
// ValidationPhaseError represents an error that occurred during a specific
111+
// phase of README validation. It should be used to collect ALL validation
112+
// errors that happened during a specific phase, rather than the first one
113+
// encountered.
114+
type ValidationPhaseError struct {
115+
phase validationPhase
116+
errors []error
117+
}
118+
119+
func (vpe ValidationPhaseError) Error() string {
120+
msg := fmt.Sprintf("Error during %q phase of README validation:", vpe.phase.String())
121+
for _, e := range vpe.errors {
122+
msg += fmt.Sprintf("\n- %v", e)
123+
}
124+
msg += "\n"
125+
126+
return msg
127+
}

0 commit comments

Comments
 (0)