refactor: add golangci-lint with code refactoring

Add golangci-lint for linting and refactor the code based on the
feedback from running it.

Changes:

- Add configuration for golangci-lint.
- Break the large function in create.go into smaller ones.
- Rename internal/templateFuncs to internal/templatefuncs to remove
  upper case letters in the package name.
- Add a mage target for lint tests.
This commit is contained in:
Dan Anglin 2023-08-21 03:07:06 +01:00
parent 4f90a4c8bb
commit 71d62ecaf6
Signed by: dananglin
GPG key ID: 0C1D44CFBEE68638
15 changed files with 230 additions and 138 deletions

55
.golangci.yml Normal file
View file

@ -0,0 +1,55 @@
---
run:
concurrency: 2
timeout: 1m
issues-exit-code: 1
tests: true
modules-download-mode: readonly
output:
format: colored-line-number
print-issues-lines: true
print-linter-name: true
uniq-by-line: true
sort-results: true
linters:
enable-all: true
disable:
- exhaustruct
- exhaustivestruct # (deprecated)
- golint # (deprecated)
- ifshort # (deprecated)
- scopelint # (deprecated)
- maligned # (deprecated)
- deadcode # (deprecated)
- nosnakecase # (deprecated)
- interfacer # (deprecated)
- varcheck # (deprecated)
- structcheck # (deprecated)
fast: false
linters-settings:
depguard:
rules:
main:
files:
- $all
allow:
- $gostd
- codeflow.dananglin.me.uk/apollo/spruce
lll:
line-length: 140
issues:
exclude-rules:
- path: cmd/spruce/main.go
linters:
- gochecknoglobals
- path: cmd/spruce-docgen/main.go
linters:
- gochecknoglobals
- cyclop
- path: internal/cmd/generate.go
linters:
- gomnd

View file

@ -3,6 +3,7 @@ package main
import (
"bytes"
"encoding/json"
"fmt"
"log"
"os"
"strings"
@ -10,8 +11,6 @@ import (
"unicode"
)
var schemaFile = "./schema/cv.schema.json"
var schemaReferenceTemplate = `= JSON schema reference
NOTE: This page was auto-generated with spruce-docgen.
@ -52,7 +51,7 @@ NOTE: This page was auto-generated with spruce-docgen.
{{- end }}
`
// schema minimally represents the JSON schema format
// schema minimally represents the JSON schema format.
type schema struct {
Title string `json:"title"`
Description string `json:"description"`
@ -65,14 +64,14 @@ type schema struct {
AdditionalProperties *schema `json:"additionalProperties"`
}
func (s *schema) UnmarshalJSON(b []byte) error {
if bytes.Equal(b, []byte("true")) || bytes.Equal(b, []byte("false")) {
func (s *schema) UnmarshalJSON(data []byte) error {
if bytes.Equal(data, []byte("true")) || bytes.Equal(data, []byte("false")) {
*s = schema{}
} else {
type rawSchema schema
var res rawSchema
if err := json.Unmarshal(b, &res); err != nil {
return err
if err := json.Unmarshal(data, &res); err != nil {
return fmt.Errorf("unable to unmarshal to rawSchema; %w", err)
}
*s = schema(res)
@ -82,6 +81,8 @@ func (s *schema) UnmarshalJSON(b []byte) error {
}
func main() {
schemaFile := "./schema/cv.schema.json"
file, err := os.Open(schemaFile)
if err != nil {
log.Fatal(err)
@ -93,7 +94,7 @@ func main() {
var data schema
if err := decoder.Decode(&data); err != nil {
log.Fatal(err)
log.Panic(err)
}
funcMap := template.FuncMap{
@ -104,7 +105,7 @@ func main() {
t := template.Must(template.New("asciidoc").Funcs(funcMap).Parse(schemaReferenceTemplate))
if err = t.Execute(os.Stdout, data); err != nil {
log.Fatal(err)
log.Panic(err)
}
}
@ -116,35 +117,39 @@ func title(str string) string {
return string(runes)
}
func getType(s schema) string {
if s.Type != "" && s.Type != "array" && s.Type != "object" {
return s.Type
const (
typeArray = "array"
typeObject = "object"
)
func getType(data schema) string {
if data.Type != "" && data.Type != typeArray && data.Type != typeObject {
return data.Type
}
if s.Type == "array" {
if s.Items == nil {
if data.Type == typeArray {
switch {
case data.Items == nil:
return "list(UNKNOWN)"
case data.Items.Type != "":
return "list(" + data.Items.Type + ")"
case data.Items.Ref != "":
return "list(<<" + title(refType(data.Items.Ref)) + ">>)"
default:
return "list(UNKNOWN)"
}
if s.Items.Type != "" {
return "list(" + s.Items.Type + ")"
}
if s.Items.Ref != "" {
return "list(<<" + title(refType(s.Items.Ref)) + ">>)"
}
}
if s.Type == "" && s.Ref != "" {
return "<<" + title(refType(s.Ref)) + ">>"
if data.Type == "" && data.Ref != "" {
return "<<" + title(refType(data.Ref)) + ">>"
}
if s.Type == "object" {
if s.AdditionalProperties != nil && s.AdditionalProperties.Type != "" {
return "map(" + s.AdditionalProperties.Type + ")"
if data.Type == typeObject {
if data.AdditionalProperties.Type != "" {
return "map(" + data.AdditionalProperties.Type + ")"
}
return "object"
return typeObject
}
return "UNKNOWN"

View file

@ -87,34 +87,37 @@ func main() {
func spruceUsageFunc(summaries map[string]string) func() {
cmds := make([]string, len(summaries))
ind := 0
for k := range summaries {
cmds[ind] = k
ind++
}
slices.Sort(cmds)
return func() {
var b strings.Builder
var builder strings.Builder
builder.WriteString("SUMMARY:\n spruce - A command-line tool for building CVs\n\n")
b.WriteString("SUMMARY:\n spruce - A command-line tool for building CVs\n\n")
if binaryVersion != "" {
b.WriteString("VERSION:\n " + binaryVersion + "\n\n")
builder.WriteString("VERSION:\n " + binaryVersion + "\n\n")
}
b.WriteString("USAGE:\n spruce [flags]\n spruce [command]\n\nCOMMANDS:")
builder.WriteString("USAGE:\n spruce [flags]\n spruce [command]\n\nCOMMANDS:")
for _, cmd := range cmds {
fmt.Fprintf(&b, "\n %s\t%s", cmd, summaries[cmd])
fmt.Fprintf(&builder, "\n %s\t%s", cmd, summaries[cmd])
}
b.WriteString("\n\nFLAGS:\n -help, --help\n print the help message\n")
builder.WriteString("\n\nFLAGS:\n -help, --help\n print the help message\n")
flag.VisitAll(func(f *flag.Flag) {
fmt.Fprintf(&b, "\n -%s, --%s\n %s\n", f.Name, f.Name, f.Usage)
fmt.Fprintf(&builder, "\n -%s, --%s\n %s\n", f.Name, f.Name, f.Usage)
})
b.WriteString("\nUse \"spruce [command] --help\" for more information about a command.\n")
builder.WriteString("\nUse \"spruce [command] --help\" for more information about a command.\n")
w := flag.CommandLine.Output()
fmt.Fprint(w, b.String())
fmt.Fprint(w, builder.String())
}
}

View file

@ -14,18 +14,18 @@ type Runner interface {
func usageFunc(name, summary string, flagset *flag.FlagSet) func() {
return func() {
var b strings.Builder
var builder strings.Builder
fmt.Fprintf(&b, "SUMMARY:\n %s - %s\n\nUSAGE:\n spruce %s [flags]\n\nFLAGS:", name, summary, name)
fmt.Fprintf(&builder, "SUMMARY:\n %s - %s\n\nUSAGE:\n spruce %s [flags]\n\nFLAGS:", name, summary, name)
flagset.VisitAll(func(f *flag.Flag) {
fmt.Fprintf(&b, "\n -%s, --%s\n %s", f.Name, f.Name, f.Usage)
fmt.Fprintf(&builder, "\n -%s, --%s\n %s", f.Name, f.Name, f.Usage)
})
b.WriteString("\n")
builder.WriteString("\n")
w := flag.CommandLine.Output()
fmt.Fprint(w, b.String())
fmt.Fprint(w, builder.String())
}
}

View file

@ -37,75 +37,16 @@ func NewCreateCommand(name, summary string) *CreateCommand {
}
func (c *CreateCommand) Run() error {
detailLen := 2
data := cv.NewCV(c.firstName, c.lastName, c.jobTitle)
data.Contact = map[string]string{
"Email": "",
"Phone": "",
}
data.Links = map[string]string{
"GitHub": "",
"Website": "",
}
data.Summary = make([]string, 2)
data.Skills = []cv.Skills{
{
Category: "",
Values: make([]string, 1),
},
{
Category: "",
Values: make([]string, 1),
},
}
data.Employment = []cv.Employment{
{
Company: "",
Location: "",
LocationType: "",
JobTitle: "",
Duration: cv.Duration{
Start: cv.Date{
Day: int64(time.Now().Day()),
Month: int64(time.Now().Month()),
Year: int64(time.Now().Year()),
},
End: &cv.Date{
Day: int64(time.Now().Day()),
Month: int64(time.Now().Month()),
Year: int64(time.Now().Year()),
},
Present: false,
},
Details: make([]string, 2),
},
}
data.Education = []cv.Education{
{
School: "",
Location: "",
Qualification: "",
Duration: cv.Duration{
Start: cv.Date{
Year: int64(time.Now().Year()),
Month: int64(time.Now().Month()),
Day: int64(time.Now().Day()),
},
End: &cv.Date{
Year: int64(time.Now().Year()),
Month: int64(time.Now().Month()),
Day: int64(time.Now().Day()),
},
},
},
}
data.Interests = make([]string, 2)
data.Contact = contact()
data.Links = links()
data.Summary = make([]string, detailLen)
data.Skills = skills()
data.Employment = employment(detailLen)
data.Education = education()
data.Interests = make([]string, detailLen)
file, err := os.Create(c.filename)
if err != nil {
@ -125,3 +66,77 @@ func (c *CreateCommand) Run() error {
return nil
}
func contact() map[string]string {
return map[string]string{
"Email": "",
"Phone": "",
}
}
func links() map[string]string {
return map[string]string{
"GitHub": "",
"Website": "",
}
}
func skills() []cv.Skills {
return []cv.Skills{
{
Category: "",
Values: make([]string, 1),
},
{
Category: "",
Values: make([]string, 1),
},
}
}
func employment(detailLen int) []cv.Employment {
return []cv.Employment{
{
Company: "",
Location: "",
LocationType: "",
JobTitle: "",
Duration: cv.Duration{
Start: cv.Date{
Day: int64(time.Now().Day()),
Month: int64(time.Now().Month()),
Year: int64(time.Now().Year()),
},
End: &cv.Date{
Day: int64(time.Now().Day()),
Month: int64(time.Now().Month()),
Year: int64(time.Now().Year()),
},
Present: false,
},
Details: make([]string, detailLen),
},
}
}
func education() []cv.Education {
return []cv.Education{
{
School: "",
Location: "",
Qualification: "",
Duration: cv.Duration{
Start: cv.Date{
Year: int64(time.Now().Year()),
Month: int64(time.Now().Month()),
Day: int64(time.Now().Day()),
},
End: &cv.Date{
Year: int64(time.Now().Year()),
Month: int64(time.Now().Month()),
Day: int64(time.Now().Day()),
},
},
},
}
}

View file

@ -3,6 +3,7 @@ package cmd
import (
"flag"
"fmt"
"os"
"strings"
)
@ -34,13 +35,22 @@ func NewVersionCommand(binaryVersion, buildTime, goVersion, gitCommit, name, sum
}
func (c *VersionCommand) Run() error {
var b strings.Builder
var builder strings.Builder
if c.fullVersion {
fmt.Fprintf(&b, "Spruce\n Version: %s\n Git commit: %s\n Go version: %s\n Build date: %s\n", c.binaryVersion, c.gitCommit, c.goVersion, c.buildTime)
fmt.Fprintf(
&builder,
"Spruce\n Version: %s\n Git commit: %s\n Go version: %s\n Build date: %s\n",
c.binaryVersion,
c.gitCommit,
c.goVersion,
c.buildTime,
)
} else {
fmt.Fprintln(&b, c.binaryVersion)
fmt.Fprintln(&builder, c.binaryVersion)
}
fmt.Print(b.String())
fmt.Fprint(os.Stdout, builder.String())
return nil
}

View file

@ -17,36 +17,35 @@ func NewCVFromFile(path string) (CV, error) {
decoder := json.NewDecoder(file)
var c CV
var output CV
if err = decoder.Decode(&c); err != nil {
if err = decoder.Decode(&output); err != nil {
return CV{}, fmt.Errorf("unable to decode JSON data; %w", err)
}
return c, nil
return output, nil
}
// NewCV returns a new value of type CV.
func NewCV(firstName, lastName, jobTitle string) CV {
cv := CV{
output := CV{
FirstName: firstName,
LastName: lastName,
JobTitle: jobTitle,
}
return cv
return output
}
// After returns true if the Duration's end date is set after time t.
// After returns true if the Duration's end date is set after the earliest experience date.
// An error is returned if the end date is not parsed successfully.
func (d Duration) After(t time.Time) (bool, error) {
func (d Duration) After(earliestExperienceDate time.Time) (bool, error) {
endDate, err := d.End.Parse()
if err != nil {
return false, err
}
return endDate.After(t), nil
return endDate.After(earliestExperienceDate), nil
}
// Parse parses Date and returns a value of type time.Time.

View file

@ -28,7 +28,7 @@ func Generate(tempDir, input string, historyLimit time.Time, verbose bool) (stri
}
if err := command.Run(); err != nil {
return "", err
return "", fmt.Errorf("an error occurred when creating the PDF file; %w", err)
}
output := filepath.Join(tempDir, "cv.pdf")

View file

@ -10,7 +10,7 @@ import (
"time"
"codeflow.dananglin.me.uk/apollo/spruce/internal/cv"
tf "codeflow.dananglin.me.uk/apollo/spruce/internal/templateFuncs"
tf "codeflow.dananglin.me.uk/apollo/spruce/internal/templatefuncs"
)
//go:embed templates/tex/*
@ -20,7 +20,7 @@ var templates embed.FS
func tex(input, tempDir string, historyLimit time.Time) (string, error) {
slog.Info("Creating the Tex file.")
c, err := cv.NewCVFromFile(input)
data, err := cv.NewCVFromFile(input)
if err != nil {
return "", fmt.Errorf("unable to create a new CV value from %s; %w", input, err)
}
@ -41,13 +41,13 @@ func tex(input, tempDir string, historyLimit time.Time) (string, error) {
"location": tf.Location,
}
t := template.Must(template.New("cv.tmpl.tex").
tmpl := template.Must(template.New("cv.tmpl.tex").
Funcs(fmap).
Delims("<<", ">>").
ParseFS(templates, "templates/tex/*.tmpl.tex"),
)
if err = t.Execute(file, c); err != nil {
if err = tmpl.Execute(file, data); err != nil {
return "", fmt.Errorf("unable to execute the CV template. %w", err)
}

View file

@ -1,4 +1,4 @@
package templateFuncs
package templatefuncs
import (
"fmt"
@ -8,10 +8,10 @@ import (
// FormatDuration outputs the employment/education
// duration as a formatted string.
func FormatDuration(d cv.Duration) string {
func FormatDuration(duration cv.Duration) string {
var start string
startDate, err := d.Start.Parse()
startDate, err := duration.Start.Parse()
if err != nil {
start = "Unknown"
@ -21,10 +21,10 @@ func FormatDuration(d cv.Duration) string {
var end string
if d.Present {
if duration.Present {
end = "Present"
} else {
endDate, err := d.End.Parse()
endDate, err := duration.End.Parse()
if err != nil {
end = "Unknown"
} else {

View file

@ -1,4 +1,4 @@
package templateFuncs
package templatefuncs
import (
"strings"

View file

@ -1,4 +1,4 @@
package templateFuncs
package templatefuncs
// Location returns a string with both the location and the
// location type if the location type is not empty.

View file

@ -1,4 +1,4 @@
package templateFuncs
package templatefuncs
// NotLastElement returns true if an element of an array
// or a slice is not the last.

View file

@ -1,4 +1,4 @@
package templateFuncs
package templatefuncs
import (
"time"
@ -8,13 +8,13 @@ import (
// WithinTimePeriod returns true if the employment's end date is within
// the specified time period.
func WithinTimePeriod(t time.Time) func(d cv.Duration) bool {
func WithinTimePeriod(date time.Time) func(d cv.Duration) bool {
return func(d cv.Duration) bool {
if d.Present {
return true
}
result, err := d.After(t)
result, err := d.After(date)
if err != nil {
return false
}

View file

@ -19,6 +19,11 @@ var (
defaultInstallPrefix = "/usr/local"
)
// Lint runs golangci-lint against the code.
func Lint() error {
return sh.RunV("golangci-lint", "run", "--color", "always")
}
// Build builds the binary.
func Build() error {
flags := ldflags()