feat: add a field for location type, code refactor

- feat: add a field for the type of work location (e.g. hybrid)
- refactor: move the Tex and PDF generating code to a new internal
  package which also moves the templates there as well.
- fix: add a default value for the --output field for the generate
  command.
- fix: add an error for when the user does not specify an input file
  when generating the PDF.
- fix: the package name for each of the files in the templateFuncs
  package.
This commit is contained in:
Dan Anglin 2023-08-15 17:25:00 +01:00
parent 90638f5569
commit c53978cd91
Signed by: dananglin
GPG key ID: 0C1D44CFBEE68638
13 changed files with 150 additions and 100 deletions

View file

@ -41,7 +41,8 @@
"employment": [
{
"company": "Company C",
"location": "Manchester (Remote)",
"location": "Manchester",
"locationType": "Remote",
"jobTitle": "Software Engineer (Cloud Platform)",
"duration": {
"start": {
@ -59,7 +60,8 @@
},
{
"company": "Company B",
"location": "London (Hybrid)",
"location": "London",
"locationType": "Hybrid",
"jobTitle": "Software Engineer (Backend)",
"duration": {
"start": {
@ -81,7 +83,8 @@
},
{
"company": "Company A",
"location": "London (Onsite)",
"location": "London",
"locationType": "On-site",
"jobTitle": "Software Engineer (Backend)",
"duration": {
"start": {
@ -103,7 +106,8 @@
},
{
"company": "Engineering Department, Example University",
"location": "London (Onsite)",
"location": "London",
"locationType": "On-site",
"jobTitle": "Research and Development Assistant",
"duration": {
"start": {

View file

@ -64,9 +64,10 @@ func (c *CreateCommand) Run() error {
data.Employment = []cv.Experience{
{
Company: "",
Location: "",
JobTitle: "",
Company: "",
Location: "",
LocationType: "",
JobTitle: "",
Duration: cv.Duration{
Start: cv.Date{
Year: time.Now().Year(),

View file

@ -1,24 +1,17 @@
package cmd
import (
"embed"
"flag"
"fmt"
"io"
"log/slog"
"os"
"os/exec"
"path/filepath"
"text/template"
"time"
"codeflow.dananglin.me.uk/apollo/spruce/internal/cv"
tf "codeflow.dananglin.me.uk/apollo/spruce/internal/templateFuncs"
"codeflow.dananglin.me.uk/apollo/spruce/internal/pdf"
)
//go:embed templates/tex/*
var templates embed.FS
type GenerateCommand struct {
*flag.FlagSet
summary string
@ -28,6 +21,12 @@ type GenerateCommand struct {
verbose bool
}
type noInputSpecifiedError struct{}
func (e noInputSpecifiedError) Error() string {
return "no input file specified, please set the --input field"
}
func NewGenerateCommand() *GenerateCommand {
gc := GenerateCommand{
FlagSet: flag.NewFlagSet("generate", flag.ExitOnError),
@ -35,7 +34,7 @@ func NewGenerateCommand() *GenerateCommand {
}
gc.StringVar(&gc.input, "input", "", "specify the CV JSON file that you want to input to the builder.")
gc.StringVar(&gc.output, "output", "", "specify the name of the output CV file.")
gc.StringVar(&gc.output, "output", "cv.pdf", "specify the name of the output CV file.")
gc.IntVar(&gc.employmentHistory, "employment-history", 10, "show employment history within these number of years.")
gc.BoolVar(&gc.verbose, "verbose", false, "set to true to enable verbose logging.")
@ -45,6 +44,10 @@ func NewGenerateCommand() *GenerateCommand {
}
func (g *GenerateCommand) Run() error {
if g.input == "" {
return noInputSpecifiedError{}
}
historyLimit := time.Now().AddDate(-1*g.employmentHistory, 0, 0)
tempDir, err := os.MkdirTemp("/tmp", "cv-builder-")
@ -59,104 +62,37 @@ func (g *GenerateCommand) Run() error {
}
}()
texFile, err := tex(g.input, tempDir, historyLimit)
pdfFileName, err := pdf.Generate(tempDir, g.input, historyLimit, g.verbose)
if err != nil {
return fmt.Errorf("unable to create the tex file; %w", err)
}
if err := pdf(tempDir, texFile, g.output, g.verbose); err != nil {
return fmt.Errorf("unable to create the PDF file; %w", err)
}
return nil
}
// tex generates the CV document as a Tex file.
func tex(input, tempDir string, historyLimit time.Time) (string, error) {
slog.Info("Creating the Tex file.")
c, err := cv.NewCVFromFile(input)
if err != nil {
return "", fmt.Errorf("unable to create a new CV value from %s; %w", input, err)
if err := copyfile(filepath.Join(tempDir, "cv.pdf"), g.output); err != nil {
return fmt.Errorf("unable to copy %s to %s; %w", pdfFileName, g.output, err)
}
output := filepath.Join(tempDir, "cv.tex")
file, err := os.Create(output)
if err != nil {
return "", fmt.Errorf("unable to create output file %s; %w", output, err)
}
defer file.Close()
fmap := template.FuncMap{
"notLastElement": tf.NotLastElement,
"join": tf.JoinSentences,
"durationToString": tf.FormatDuration,
"withinTimePeriod": tf.WithinTimePeriod(historyLimit),
}
t := template.Must(template.New("cv.tmpl.tex").
Funcs(fmap).
Delims("<<", ">>").
ParseFS(templates, "templates/tex/*.tmpl.tex"),
)
if err = t.Execute(file, c); err != nil {
return "", fmt.Errorf("unable to execute the CV template. %w", err)
}
slog.Info("Tex file successfully created.", "filename", output)
return output, nil
}
// pdf generates the CV document as a PDF file from the tex file.
func pdf(tempDir, texFile, output string, verbose bool) error {
slog.Info("Creating the PDF document.")
pathArg := "--path=" + tempDir
command := exec.Command("mtxrun", pathArg, "--script", "context", texFile)
if verbose {
command.Stderr = os.Stderr
command.Stdout = os.Stdout
}
if err := command.Run(); err != nil {
return err
}
if output == "" {
output = "./cv.pdf"
}
if err := copyfile(filepath.Join(tempDir, "cv.pdf"), output); err != nil {
return err
}
slog.Info("PDF document successfully created.", "filename", output)
return nil
}
func copyfile(input, output string) error {
inputFile, err := os.Open(input)
func copyfile(source, destination string) error {
inputFile, err := os.Open(source)
if err != nil {
return fmt.Errorf("unable to open %s; %w", input, err)
return fmt.Errorf("unable to open %s; %w", source, err)
}
defer inputFile.Close()
outputFile, err := os.Create(output)
outputFile, err := os.Create(destination)
if err != nil {
return fmt.Errorf("unable to create %s; %w", output, err)
return fmt.Errorf("unable to create %s; %w", destination, err)
}
defer outputFile.Close()
_, err = io.Copy(outputFile, inputFile)
if err != nil {
return fmt.Errorf("unable to copy %s to %s; %w", input, output, err)
return fmt.Errorf("unable to copy %s to %s; %w", source, destination, err)
}
slog.Info("File successfully copied.", "source", source, "destination", destination)
return nil
}

View file

@ -27,6 +27,7 @@ type Experience struct {
Company string `json:"company"`
School string `json:"school"`
Location string `json:"location"`
LocationType string `json:"locationType"`
JobTitle string `json:"jobTitle"`
Qualification string `json:"qualification"`
Duration Duration `json:"duration"`

39
internal/pdf/pdf.go Normal file
View file

@ -0,0 +1,39 @@
package pdf
import (
"fmt"
"log/slog"
"os"
"os/exec"
"path/filepath"
"time"
)
// Generate generates the CV PDF document from the JSON file.
func Generate(tempDir, input string, historyLimit time.Time, verbose bool) (string, error) {
texFileName, err := tex(input, tempDir, historyLimit)
if err != nil {
return "", fmt.Errorf("unable to create the tex file; %w", err)
}
slog.Info("Creating the PDF document.")
pathArg := "--path=" + tempDir
command := exec.Command("mtxrun", pathArg, "--script", "context", texFileName)
if verbose {
command.Stderr = os.Stderr
command.Stdout = os.Stdout
}
if err := command.Run(); err != nil {
return "", err
}
output := filepath.Join(tempDir, "cv.pdf")
slog.Info("PDF document successfully created.", "filename", output)
return output, nil
}

View file

@ -30,7 +30,7 @@
\section{EXPERIENCE}
<<- range .Employment>>
<<- if withinTimePeriod .Duration>>
\jobsection{<<.Company>>}{<<.Location>>}{<<.JobTitle>>}{<<durationToString .Duration>>}
\jobsection{<<.Company>>}{<<location .Location .LocationType>>}{<<.JobTitle>>}{<<durationToString .Duration>>}
\startitemize
<<range .Details>>
\item <<.>>

57
internal/pdf/tex.go Normal file
View file

@ -0,0 +1,57 @@
package pdf
import (
"embed"
"fmt"
"log/slog"
"os"
"path/filepath"
"text/template"
"time"
"codeflow.dananglin.me.uk/apollo/spruce/internal/cv"
tf "codeflow.dananglin.me.uk/apollo/spruce/internal/templateFuncs"
)
//go:embed templates/tex/*
var templates embed.FS
// tex generates the CV document as a Tex file.
func tex(input, tempDir string, historyLimit time.Time) (string, error) {
slog.Info("Creating the Tex file.")
c, err := cv.NewCVFromFile(input)
if err != nil {
return "", fmt.Errorf("unable to create a new CV value from %s; %w", input, err)
}
output := filepath.Join(tempDir, "cv.tex")
file, err := os.Create(output)
if err != nil {
return "", fmt.Errorf("unable to create output file %s; %w", output, err)
}
defer file.Close()
fmap := template.FuncMap{
"notLastElement": tf.NotLastElement,
"join": tf.JoinSentences,
"durationToString": tf.FormatDuration,
"withinTimePeriod": tf.WithinTimePeriod(historyLimit),
"location": tf.Location,
}
t := template.Must(template.New("cv.tmpl.tex").
Funcs(fmap).
Delims("<<", ">>").
ParseFS(templates, "templates/tex/*.tmpl.tex"),
)
if err = t.Execute(file, c); err != nil {
return "", fmt.Errorf("unable to execute the CV template. %w", err)
}
slog.Info("Tex file successfully created.", "filename", output)
return output, nil
}

View file

@ -1,4 +1,4 @@
package templates
package templateFuncs
import (
"fmt"

View file

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

View file

@ -0,0 +1,11 @@
package templateFuncs
// Location returns a string with both the location and the
// location type if the location type is not empty.
func Location(location, locationType string) string {
if locationType != "" {
return location + " (" + locationType + ")"
}
return location
}

View file

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

View file

@ -1,8 +1,9 @@
package templates
package templateFuncs
import (
"codeflow.dananglin.me.uk/apollo/spruce/internal/cv"
"time"
"codeflow.dananglin.me.uk/apollo/spruce/internal/cv"
)
// WithinTimePeriod returns true if the employment's end date is within