Skip to content

Commit 6cee8ed

Browse files
committed
Merge branch 'master' into 2023/day02
2 parents ab4fe5f + de7b343 commit 6cee8ed

File tree

7 files changed

+483
-0
lines changed

7 files changed

+483
-0
lines changed

deployments/docker-compose/go-tools-docker-compose.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ services:
2929
extends:
3030
service: tools
3131
entrypoint: /bin/sh -c 'git config --global --add safe.directory /app && ./scripts/tests/run.sh'
32+
environment:
33+
AOC_PUZZLE_URL: ${AOC_PUZZLE_URL}
3234

3335
run-tests-regression:
3436
extends:

internal/puzzles/solutions/new.go

Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
package solutions
2+
3+
import (
4+
"fmt"
5+
"os"
6+
"path/filepath"
7+
"strconv"
8+
"text/template"
9+
10+
"github.com/obalunenko/advent-of-code/internal/puzzles/solutions/templates"
11+
)
12+
13+
func createNewFromTemplate(purl string) error {
14+
const (
15+
perms = 0o766
16+
yearLen = 4
17+
dayLen = 2
18+
)
19+
20+
pd, err := parsePuzzleURL(purl)
21+
if err != nil {
22+
return fmt.Errorf("parse puzzle url %q: %w", purl, err)
23+
}
24+
25+
day := strconv.Itoa(pd.day)
26+
if len(day) < dayLen {
27+
day = "0" + day
28+
}
29+
30+
if len(day) != dayLen {
31+
return fmt.Errorf("invalid day: %s", day)
32+
}
33+
34+
year := strconv.Itoa(pd.year)
35+
36+
if len(year) != yearLen {
37+
return fmt.Errorf("invalid year: %s", year)
38+
}
39+
40+
params := templates.Params{
41+
Year: year,
42+
Day: pd.day,
43+
DayStr: day,
44+
URL: purl,
45+
}
46+
47+
path := filepath.Clean(filepath.Join(year, "day"+day))
48+
49+
if err = createPuzzleDir(path, perms); err != nil {
50+
return fmt.Errorf("failed to create puzzle dir: %w", err)
51+
}
52+
53+
testdata := filepath.Clean(filepath.Join(path, "testdata"))
54+
55+
if err = createTestdata(testdata, perms); err != nil {
56+
return fmt.Errorf("failed to create testdata: %w", err)
57+
}
58+
59+
tmplsFns := []func() (*template.Template, error){
60+
templates.SolutionTmpl, templates.SolutionTestTmpl, templates.SpecTmpl,
61+
}
62+
63+
for _, tmplFn := range tmplsFns {
64+
var tmpl *template.Template
65+
66+
tmpl, err = tmplFn()
67+
if err != nil {
68+
return fmt.Errorf("failed to get template: %w", err)
69+
}
70+
71+
if err = createFromTemplate(tmpl, path, perms, params); err != nil {
72+
return fmt.Errorf("failed to create from template: %w", err)
73+
}
74+
}
75+
76+
return nil
77+
}
78+
79+
func createFromTemplate(tmpl *template.Template, path string, perms os.FileMode, params templates.Params) error {
80+
fpath := filepath.Clean(filepath.Join(path, tmpl.Name()))
81+
82+
if isExist(fpath) {
83+
return nil
84+
}
85+
86+
var content []byte
87+
88+
content, err := templates.SubstituteTemplate(tmpl, params)
89+
if err != nil {
90+
return fmt.Errorf("failed to substitute template: %w", err)
91+
}
92+
93+
if err = os.WriteFile(fpath, content, perms); err != nil {
94+
return fmt.Errorf("failed to write file: %w", err)
95+
}
96+
97+
return nil
98+
}
99+
100+
func createPuzzleDir(path string, perms os.FileMode) error {
101+
if !isExist(path) {
102+
if err := os.MkdirAll(path, perms); err != nil {
103+
return fmt.Errorf("failed to create dir: %w", err)
104+
}
105+
}
106+
107+
return nil
108+
}
109+
110+
func createTestdata(path string, perms os.FileMode) error {
111+
if !isExist(path) {
112+
if err := os.MkdirAll(path, perms); err != nil {
113+
return fmt.Errorf("failed to create dir: %w", err)
114+
}
115+
}
116+
117+
input := filepath.Clean(filepath.Join(path, "input.txt"))
118+
119+
if !isExist(input) {
120+
var f *os.File
121+
122+
f, err := os.Create(input)
123+
if err != nil {
124+
return fmt.Errorf("failed to create file: %w", err)
125+
}
126+
127+
if err = f.Close(); err != nil {
128+
return fmt.Errorf("failed to close file: %w", err)
129+
}
130+
}
131+
132+
return nil
133+
}
134+
135+
func isExist(path string) bool {
136+
stat, err := os.Stat(path)
137+
if err != nil && !os.IsNotExist(err) {
138+
return false
139+
}
140+
141+
return stat != nil && stat.Name() != ""
142+
}
143+
144+
type puzzleDate struct {
145+
year int
146+
day int
147+
}
148+
149+
func parsePuzzleURL(url string) (puzzleDate, error) {
150+
const (
151+
urlFmt = "https://adventofcode.com/%d/day/%d"
152+
paramsNum = 2
153+
)
154+
155+
var year, day int
156+
157+
n, err := fmt.Sscanf(url, urlFmt, &year, &day)
158+
if err != nil {
159+
return puzzleDate{}, fmt.Errorf("parse puzzle url: %w", err)
160+
}
161+
162+
if n != paramsNum {
163+
return puzzleDate{}, fmt.Errorf("invalid puzzle url: %s", url)
164+
}
165+
166+
return puzzleDate{
167+
year: year,
168+
day: day,
169+
}, nil
170+
}
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
package solutions
2+
3+
import (
4+
"errors"
5+
"testing"
6+
7+
"github.com/obalunenko/getenv"
8+
"github.com/stretchr/testify/assert"
9+
"github.com/stretchr/testify/require"
10+
)
11+
12+
func Test_createNewFromTemplate(t *testing.T) {
13+
const envName = "AOC_PUZZLE_URL"
14+
15+
purl, err := getenv.Env[string](envName)
16+
if err != nil {
17+
if errors.Is(err, getenv.ErrNotSet) {
18+
t.Skipf("%s is not set", envName)
19+
}
20+
21+
t.Fatalf("failed to get environment variable[%s]: %v", envName, err)
22+
}
23+
24+
require.NoError(t, createNewFromTemplate(purl))
25+
}
26+
27+
func Test_parsePuzzleURL(t *testing.T) {
28+
type args struct {
29+
url string
30+
}
31+
32+
tests := []struct {
33+
name string
34+
args args
35+
wandDate puzzleDate
36+
wantErr assert.ErrorAssertionFunc
37+
}{
38+
{
39+
name: "valid url",
40+
args: args{
41+
url: "https://adventofcode.com/2022/day/1",
42+
},
43+
wandDate: puzzleDate{
44+
year: 2022,
45+
day: 1,
46+
},
47+
wantErr: assert.NoError,
48+
},
49+
{
50+
name: "invalid url",
51+
args: args{
52+
url: "https://adventofcode.com/2022",
53+
},
54+
wandDate: puzzleDate{
55+
year: 0,
56+
day: 0,
57+
},
58+
wantErr: assert.Error,
59+
},
60+
{
61+
name: "empty url",
62+
args: args{
63+
url: "",
64+
},
65+
wandDate: puzzleDate{
66+
year: 0,
67+
day: 0,
68+
},
69+
wantErr: assert.Error,
70+
},
71+
{
72+
name: "whitespace url",
73+
args: args{
74+
url: " ",
75+
},
76+
wandDate: puzzleDate{
77+
year: 0,
78+
day: 0,
79+
},
80+
wantErr: assert.Error,
81+
},
82+
}
83+
84+
for _, tt := range tests {
85+
t.Run(tt.name, func(t *testing.T) {
86+
gotDate, err := parsePuzzleURL(tt.args.url)
87+
if !tt.wantErr(t, err) {
88+
return
89+
}
90+
91+
assert.Equal(t, tt.wandDate, gotDate)
92+
})
93+
}
94+
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
// Package templates contains templates for solution.go, solution_test.go and spec.md files.
2+
package templates
3+
4+
import (
5+
"bytes"
6+
_ "embed"
7+
"fmt"
8+
"text/template"
9+
)
10+
11+
var (
12+
//go:embed solution.go.tmpl
13+
solutionTmpl string
14+
//go:embed solution_test.go.tmpl
15+
solutionTestTmpl string
16+
//go:embed spec.md.tmpl
17+
specTmpl string
18+
)
19+
20+
// Params contains parameters for templates.
21+
type Params struct {
22+
Year string // e.g. "2023"
23+
Day int // e.g. 2
24+
DayStr string // e.g. "02"
25+
URL string // e.g. "https://adventofcode.com/2023/day/2"
26+
}
27+
28+
// SolutionTmpl returns template for solution.go file.
29+
func SolutionTmpl() (*template.Template, error) {
30+
tmpl, err := template.New("solution.go").Parse(solutionTmpl)
31+
if err != nil {
32+
return nil, fmt.Errorf("failed to parse solution template: %w", err)
33+
}
34+
35+
return tmpl, nil
36+
}
37+
38+
// SolutionTestTmpl returns template for solution_test.go file.
39+
func SolutionTestTmpl() (*template.Template, error) {
40+
tmpl, err := template.New("solution_test.go").Parse(solutionTestTmpl)
41+
if err != nil {
42+
return nil, fmt.Errorf("failed to parse solution test template: %w", err)
43+
}
44+
45+
return tmpl, nil
46+
}
47+
48+
// SpecTmpl returns template for spec.md file.
49+
func SpecTmpl() (*template.Template, error) {
50+
tmpl, err := template.New("spec.md").Parse(specTmpl)
51+
if err != nil {
52+
return nil, fmt.Errorf("failed to parse spec template: %w", err)
53+
}
54+
55+
return tmpl, nil
56+
}
57+
58+
// SubstituteTemplate substitutes template with given parameters.
59+
func SubstituteTemplate(tmpl *template.Template, p Params) ([]byte, error) {
60+
var buf bytes.Buffer
61+
62+
err := tmpl.Execute(&buf, p)
63+
if err != nil {
64+
return nil, fmt.Errorf("failed to execute template: %w", err)
65+
}
66+
67+
return buf.Bytes(), nil
68+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
// Package day{{ .DayStr }} contains solution for {{ .URL }} puzzle.
2+
package day{{ .DayStr }}
3+
4+
import (
5+
"io"
6+
7+
"github.com/obalunenko/advent-of-code/internal/puzzles"
8+
)
9+
10+
func init() {
11+
puzzles.Register(solution{})
12+
}
13+
14+
type solution struct{}
15+
16+
func (s solution) Year() string {
17+
return puzzles.Year{{ .Year }}.String()
18+
}
19+
20+
func (s solution) Day() string {
21+
return puzzles.Day{{ .DayStr }}.String()
22+
}
23+
24+
func (s solution) Part1(input io.Reader) (string, error) {
25+
return "", puzzles.ErrNotImplemented
26+
}
27+
28+
func (s solution) Part2(input io.Reader) (string, error) {
29+
return "", puzzles.ErrNotImplemented
30+
}

0 commit comments

Comments
 (0)