Skip to content

Commit 134e02a

Browse files
authored
Merge pull request #918 from 1Password/jh/application-validation
Add application parsing and validation, test issues
2 parents 4b37c28 + cce4922 commit 134e02a

16 files changed

+1067
-8
lines changed

.github/workflows/test-processor.yml

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
name: Test processor
2+
3+
on:
4+
push:
5+
paths:
6+
- "script/**"
7+
8+
jobs:
9+
test-processor:
10+
runs-on: ubuntu-latest
11+
steps:
12+
- name: Checkout repository
13+
uses: actions/checkout@v4
14+
15+
- name: Setup Go
16+
uses: actions/setup-go@v5
17+
with:
18+
go-version-file: "script/go.mod"
19+
cache-dependency-path: "script/go.sum"
20+
21+
- name: Install dependencies
22+
run: make install_deps
23+
24+
- name: Test processor
25+
run: make test

Makefile

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@ build_processor:
88
$(info Building processor...)
99
@cd ./script && go build -v -o ../processor .
1010

11+
test:
12+
$(info Running tests...)
13+
@cd ./script && go test
14+
1115
bump_version:
1216
$(info Bumping version...)
1317
@$(eval LAST_TAG=$(shell git rev-list --tags='processor-*' --max-count=1 | xargs -r git describe --tags --match 'processor-*'))

script/application.go

Lines changed: 227 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,227 @@
1+
package main
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"log"
7+
"strings"
8+
"time"
9+
10+
"github.com/google/go-github/v60/github"
11+
)
12+
13+
type Project struct {
14+
Name string `json:"name"`
15+
Description string `json:"description"`
16+
Contributors int `json:"contributors"`
17+
HomeURL string `json:"home_url"`
18+
RepoURL string `json:"repo_url,omitempty"`
19+
LicenseType string `json:"license_type,omitempty"`
20+
LicenseURL string `json:"license_url,omitempty"`
21+
IsEvent bool `json:"is_event"`
22+
IsTeam bool `json:"is_team"`
23+
}
24+
25+
type Applicant struct {
26+
Name string `json:"name"`
27+
Email string `json:"email"`
28+
Role string `json:"role"`
29+
ID int64 `json:"id"`
30+
}
31+
32+
type Application struct {
33+
validator Validator `json:"-"`
34+
sections map[string]string `json:"-"`
35+
Problems []error `json:"-"`
36+
37+
Account string `json:"account"`
38+
Project Project `json:"project"`
39+
Applicant Applicant `json:"applicant"`
40+
CanContact bool `json:"can_contact"`
41+
ApproverId int `json:"approver_id,omitempty"`
42+
IssueNumber int `json:"issue_number"`
43+
CreatedAt time.Time `json:"created_at"`
44+
}
45+
46+
func (a *Application) Parse(issue *github.Issue) {
47+
a.validator = Validator{}
48+
49+
if strings.Contains(*issue.Title, "[project name]") {
50+
a.validator.AddError("Application title", *issue.Title, "is missing project name")
51+
}
52+
53+
a.sections = a.extractSections(*issue.Body)
54+
55+
if isTestingIssue() {
56+
data, err := json.MarshalIndent(a.sections, "", "\t")
57+
if err != nil {
58+
log.Fatalf("Could not marshal Sections input data: %s", err.Error())
59+
}
60+
61+
debugMessage("Parsed input data:", string(data))
62+
}
63+
64+
a.CreatedAt = issue.CreatedAt.Time
65+
a.IssueNumber = *issue.Number
66+
a.Account = a.stringSection("Account URL", true, ParseAccountURL)
67+
a.boolSection("Non-commercial confirmation", true, ParseCheckbox, IsChecked)
68+
69+
a.Project.IsTeam = a.boolSection("Team application", false, ParseCheckbox)
70+
a.Project.IsEvent = a.boolSection("Event application", false, ParseCheckbox)
71+
72+
isProject := !a.Project.IsTeam && !a.Project.IsEvent
73+
74+
a.Project.Name = a.stringSection("Project name", true, ParsePlainString)
75+
a.Project.Description = a.stringSection("Short description", true, ParsePlainString)
76+
a.Project.Contributors = a.intSection("Number of team members/core contributors", true, ParsePlainString)
77+
a.Project.HomeURL = a.stringSection("Homepage URL", true, IsURL)
78+
a.Project.RepoURL = a.stringSection("Repository URL", false, IsURL)
79+
a.Project.LicenseType = a.stringSection("License type", isProject, ParsePlainString)
80+
a.Project.LicenseURL = a.stringSection("License URL", isProject, IsURL)
81+
a.boolSection("Age confirmation", isProject, ParseCheckbox, When(isProject, IsChecked))
82+
83+
a.Applicant.Name = a.stringSection("Name", true, ParsePlainString)
84+
a.Applicant.Email = a.stringSection("Email", true, IsEmail)
85+
a.Applicant.Role = a.stringSection("Project role", true)
86+
a.Applicant.ID = *issue.User.ID
87+
88+
a.stringSection("Profile or website", false, IsURL)
89+
a.stringSection("Additional comments", false)
90+
91+
a.CanContact = a.boolSection("Can we contact you?", false, ParseCheckbox)
92+
93+
if isTestingIssue() {
94+
debugMessage("Application data:", a.GetData())
95+
}
96+
97+
for _, err := range a.validator.Errors {
98+
a.Problems = append(a.Problems, fmt.Errorf(err.Error()))
99+
}
100+
}
101+
102+
func (a *Application) IsValid() bool {
103+
return len(a.Problems) == 0
104+
}
105+
106+
func (a *Application) GetData() string {
107+
data, err := json.MarshalIndent(a, "", "\t")
108+
if err != nil {
109+
log.Fatalf("Could not marshal Application data: %s", err.Error())
110+
}
111+
112+
return string(data)
113+
}
114+
115+
// Take the Markdown-format body of an issue and break it down by section header
116+
// and the content directly below it. We can reasonably expect the correct format
117+
// here if someone files an issue using the application template, but it will also
118+
// gracefully handle when this format is not present. Note that this will only
119+
// create an entry when there is content to be added; in other words, a section
120+
// header without any content will not be added.
121+
func (a *Application) extractSections(body string) map[string]string {
122+
sections := make(map[string]string)
123+
124+
lines := strings.Split(body, "\n")
125+
var currentHeader string
126+
contentBuilder := strings.Builder{}
127+
128+
// For each line of the body content, it can either be a section's
129+
// header or the content associated with that section's header.
130+
for _, line := range lines {
131+
trimmedLine := strings.TrimSpace(line)
132+
133+
// If we're in a section and the content doesn't start with
134+
// a header marker, append it to our content builder
135+
if !strings.HasPrefix(trimmedLine, "### ") {
136+
if currentHeader == "" {
137+
continue
138+
}
139+
140+
contentBuilder.WriteString(line + "\n")
141+
continue
142+
}
143+
144+
// The content has a header marker, so create a new
145+
// section entry and prepare the content builder
146+
if currentHeader != "" && contentBuilder.Len() > 0 {
147+
sections[currentHeader] = strings.TrimSpace(contentBuilder.String())
148+
contentBuilder.Reset()
149+
}
150+
151+
currentHeader = strings.TrimSpace(trimmedLine[4:])
152+
}
153+
154+
// Once the loop has completed check if there's a
155+
// trailing section needing to be closed
156+
if currentHeader != "" && contentBuilder.Len() > 0 {
157+
sections[currentHeader] = strings.TrimSpace(contentBuilder.String())
158+
}
159+
160+
return sections
161+
}
162+
163+
func (a *Application) stringSection(sectionName string, required bool, callbacks ...ValidatorCallback) string {
164+
value, exists := a.sections[sectionName]
165+
_, value, _ = ParseInput(value)
166+
167+
// If the section is required, apply the presence validator if the entry
168+
// exists, early fail validation if it doesn't exist. If the section is
169+
// not required and there is no content to work with, don't try to run
170+
// additional validations.
171+
if required {
172+
if exists {
173+
callbacks = append([]ValidatorCallback{IsPresent}, callbacks...)
174+
} else {
175+
a.validator.AddError(sectionName, value, "was not completed for application")
176+
return value
177+
}
178+
} else if !exists || value == "" {
179+
return value
180+
}
181+
182+
for _, callback := range callbacks {
183+
pass, newValue, message := callback(value)
184+
value = newValue
185+
186+
if !pass {
187+
a.validator.AddError(sectionName, value, message)
188+
break
189+
}
190+
}
191+
192+
return value
193+
}
194+
195+
func (a *Application) intSection(sectionName string, required bool, callbacks ...ValidatorCallback) int {
196+
value := a.stringSection(sectionName, required, callbacks...)
197+
198+
// Don't bother proceeding if there's already an error parsing the string
199+
if a.validator.HasError(sectionName) || value == "" {
200+
return 0
201+
}
202+
203+
pass, number, message := ParseNumber(value)
204+
if !pass {
205+
a.validator.AddError(sectionName, fmt.Sprintf("%d", number), message)
206+
return 0
207+
}
208+
209+
return number
210+
}
211+
212+
func (a *Application) boolSection(sectionName string, required bool, callbacks ...ValidatorCallback) bool {
213+
value := a.stringSection(sectionName, required, callbacks...)
214+
215+
// Don't bother proceeding if there's already an error parsing the string
216+
if a.validator.HasError(sectionName) || value == "" {
217+
return false
218+
}
219+
220+
pass, boolean, message := ParseBool(value)
221+
if !pass {
222+
a.validator.AddError(sectionName, fmt.Sprintf("%t", boolean), message)
223+
return false
224+
}
225+
226+
return boolean
227+
}

0 commit comments

Comments
 (0)