Skip to content

Commit

Permalink
Merge branch 'master' into 219-implement-solution-for-puzzle-2022day02
Browse files Browse the repository at this point in the history
  • Loading branch information
obalunenko committed Dec 5, 2023
2 parents ccd107a + 639a395 commit 08c249b
Show file tree
Hide file tree
Showing 9 changed files with 504 additions and 0 deletions.
4 changes: 4 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -157,5 +157,9 @@ new-version: vet test-regression build
open-advent-homepage:
./scripts/browser-opener.sh -u 'https://adventofcode.com/'

gen-boilerplate:
./scripts/codegen/puzzle-boilerplate.sh
.PHONY: gen-boilerplate

.DEFAULT_GOAL := help

2 changes: 2 additions & 0 deletions deployments/docker-compose/go-tools-docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ services:
extends:
service: tools
entrypoint: /bin/sh -c 'git config --global --add safe.directory /app && ./scripts/tests/run.sh'
environment:
AOC_PUZZLE_URL: ${AOC_PUZZLE_URL}

run-tests-regression:
extends:
Expand Down
170 changes: 170 additions & 0 deletions internal/puzzles/solutions/new.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
package solutions

import (
"fmt"
"os"
"path/filepath"
"strconv"
"text/template"

"github.com/obalunenko/advent-of-code/internal/puzzles/solutions/templates"
)

func createNewFromTemplate(purl string) error {
const (
perms = 0o655
yearLen = 4
dayLen = 2
)

pd, err := parsePuzzleURL(purl)
if err != nil {
return fmt.Errorf("parse puzzle url %q: %w", purl, err)
}

day := strconv.Itoa(pd.day)
if len(day) < dayLen {
day = "0" + day
}

if len(day) != dayLen {
return fmt.Errorf("invalid day: %s", day)
}

year := strconv.Itoa(pd.year)

if len(year) != yearLen {
return fmt.Errorf("invalid year: %s", year)
}

params := templates.Params{
Year: year,
Day: pd.day,
DayStr: day,
URL: purl,
}

path := filepath.Clean(filepath.Join(year, "day"+day))

if err = createPuzzleDir(path, perms); err != nil {
return fmt.Errorf("failed to create puzzle dir: %w", err)
}

testdata := filepath.Clean(filepath.Join(path, "testdata"))

if err = createTestdata(testdata, perms); err != nil {
return fmt.Errorf("failed to create testdata: %w", err)
}

tmplsFns := []func() (*template.Template, error){
templates.SolutionTmpl, templates.SolutionTestTmpl, templates.SpecTmpl,
}

for _, tmplFn := range tmplsFns {
var tmpl *template.Template

tmpl, err = tmplFn()
if err != nil {
return fmt.Errorf("failed to get template: %w", err)
}

if err = createFromTemplate(tmpl, path, perms, params); err != nil {
return fmt.Errorf("failed to create from template: %w", err)
}
}

return nil
}

func createFromTemplate(tmpl *template.Template, path string, perms os.FileMode, params templates.Params) error {
fpath := filepath.Clean(filepath.Join(path, tmpl.Name()))

if isExist(fpath) {
return nil
}

var content []byte

content, err := templates.SubstituteTemplate(tmpl, params)
if err != nil {
return fmt.Errorf("failed to substitute template: %w", err)
}

if err = os.WriteFile(fpath, content, perms); err != nil {
return fmt.Errorf("failed to write file: %w", err)
}

return nil
}

func createPuzzleDir(path string, perms os.FileMode) error {
if !isExist(path) {
if err := os.MkdirAll(path, perms); err != nil {
return fmt.Errorf("failed to create dir: %w", err)
}
}

return nil
}

func createTestdata(path string, perms os.FileMode) error {
if !isExist(path) {
if err := os.MkdirAll(path, perms); err != nil {
return fmt.Errorf("failed to create dir: %w", err)
}
}

input := filepath.Clean(filepath.Join(path, "input.txt"))

if !isExist(input) {
var f *os.File

f, err := os.Create(input)
if err != nil {
return fmt.Errorf("failed to create file: %w", err)
}

if err = f.Close(); err != nil {
return fmt.Errorf("failed to close file: %w", err)
}
}

return nil
}

func isExist(path string) bool {
stat, err := os.Stat(path)
if err != nil && !os.IsNotExist(err) {
return false
}

return stat != nil && stat.Name() != ""
}

type puzzleDate struct {
year int
day int
}

func parsePuzzleURL(url string) (puzzleDate, error) {
const (
urlFmt = "https://adventofcode.com/%d/day/%d"
paramsNum = 2
)

var year, day int

n, err := fmt.Sscanf(url, urlFmt, &year, &day)
if err != nil {
return puzzleDate{}, fmt.Errorf("parse puzzle url: %w", err)
}

if n != paramsNum {
return puzzleDate{}, fmt.Errorf("invalid puzzle url: %s", url)
}

return puzzleDate{
year: year,
day: day,
}, nil
}
94 changes: 94 additions & 0 deletions internal/puzzles/solutions/new_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package solutions

import (
"errors"
"testing"

"github.com/obalunenko/getenv"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func Test_createNewFromTemplate(t *testing.T) {
const envName = "AOC_PUZZLE_URL"

purl, err := getenv.Env[string](envName)
if err != nil {
if errors.Is(err, getenv.ErrNotSet) {
t.Skipf("%s is not set", envName)
}

t.Fatalf("failed to get environment variable[%s]: %v", envName, err)
}

require.NoError(t, createNewFromTemplate(purl))
}

func Test_parsePuzzleURL(t *testing.T) {
type args struct {
url string
}

tests := []struct {
name string
args args
wandDate puzzleDate
wantErr assert.ErrorAssertionFunc
}{
{
name: "valid url",
args: args{
url: "https://adventofcode.com/2022/day/1",
},
wandDate: puzzleDate{
year: 2022,
day: 1,
},
wantErr: assert.NoError,
},
{
name: "invalid url",
args: args{
url: "https://adventofcode.com/2022",
},
wandDate: puzzleDate{
year: 0,
day: 0,
},
wantErr: assert.Error,
},
{
name: "empty url",
args: args{
url: "",
},
wandDate: puzzleDate{
year: 0,
day: 0,
},
wantErr: assert.Error,
},
{
name: "whitespace url",
args: args{
url: " ",
},
wandDate: puzzleDate{
year: 0,
day: 0,
},
wantErr: assert.Error,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
gotDate, err := parsePuzzleURL(tt.args.url)
if !tt.wantErr(t, err) {
return
}

assert.Equal(t, tt.wandDate, gotDate)
})
}
}
68 changes: 68 additions & 0 deletions internal/puzzles/solutions/templates/embed.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
// Package templates contains templates for solution.go, solution_test.go and spec.md files.
package templates

import (
"bytes"
_ "embed"
"fmt"
"text/template"
)

var (
//go:embed solution.go.tmpl
solutionTmpl string
//go:embed solution_test.go.tmpl
solutionTestTmpl string
//go:embed spec.md.tmpl
specTmpl string
)

// Params contains parameters for templates.
type Params struct {
Year string // e.g. "2023"
Day int // e.g. 2
DayStr string // e.g. "02"
URL string // e.g. "https://adventofcode.com/2023/day/2"
}

// SolutionTmpl returns template for solution.go file.
func SolutionTmpl() (*template.Template, error) {
tmpl, err := template.New("solution.go").Parse(solutionTmpl)
if err != nil {
return nil, fmt.Errorf("failed to parse solution template: %w", err)
}

return tmpl, nil
}

// SolutionTestTmpl returns template for solution_test.go file.
func SolutionTestTmpl() (*template.Template, error) {
tmpl, err := template.New("solution_test.go").Parse(solutionTestTmpl)
if err != nil {
return nil, fmt.Errorf("failed to parse solution test template: %w", err)
}

return tmpl, nil
}

// SpecTmpl returns template for spec.md file.
func SpecTmpl() (*template.Template, error) {
tmpl, err := template.New("spec.md").Parse(specTmpl)
if err != nil {
return nil, fmt.Errorf("failed to parse spec template: %w", err)
}

return tmpl, nil
}

// SubstituteTemplate substitutes template with given parameters.
func SubstituteTemplate(tmpl *template.Template, p Params) ([]byte, error) {
var buf bytes.Buffer

err := tmpl.Execute(&buf, p)
if err != nil {
return nil, fmt.Errorf("failed to execute template: %w", err)
}

return buf.Bytes(), nil
}
30 changes: 30 additions & 0 deletions internal/puzzles/solutions/templates/solution.go.tmpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// Package day{{ .DayStr }} contains solution for {{ .URL }} puzzle.
package day{{ .DayStr }}

import (
"io"

"github.com/obalunenko/advent-of-code/internal/puzzles"
)

func init() {
puzzles.Register(solution{})
}

type solution struct{}

func (s solution) Year() string {
return puzzles.Year{{ .Year }}.String()
}

func (s solution) Day() string {
return puzzles.Day{{ .DayStr }}.String()
}

func (s solution) Part1(input io.Reader) (string, error) {
return "", puzzles.ErrNotImplemented
}

func (s solution) Part2(input io.Reader) (string, error) {
return "", puzzles.ErrNotImplemented
}
Loading

0 comments on commit 08c249b

Please sign in to comment.