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:
parent
90638f5569
commit
c53978cd91
|
@ -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": {
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
39
internal/pdf/pdf.go
Normal 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
|
||||
}
|
|
@ -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
57
internal/pdf/tex.go
Normal 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
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
package templates
|
||||
package templateFuncs
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
package templates
|
||||
package templateFuncs
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
|
11
internal/templateFuncs/location.go
Normal file
11
internal/templateFuncs/location.go
Normal 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
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
package templates
|
||||
package templateFuncs
|
||||
|
||||
// NotLastElement returns true if an element of an array
|
||||
// or a slice is not the last.
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue