moth/pkg/transpile/category.go

193 lines
4.8 KiB
Go
Raw Normal View History

2020-09-08 17:49:02 -06:00
package transpile
2019-08-17 16:00:15 -06:00
import (
2020-09-04 15:29:06 -06:00
"bytes"
"context"
"encoding/json"
2019-08-18 21:59:06 -06:00
"log"
2020-09-04 15:29:06 -06:00
"os/exec"
2019-08-18 21:59:06 -06:00
"strconv"
2020-09-04 15:29:06 -06:00
"strings"
"time"
2019-08-18 21:59:06 -06:00
2020-08-28 17:41:17 -06:00
"github.com/spf13/afero"
)
2019-08-18 21:59:06 -06:00
2020-09-08 17:49:02 -06:00
// Category defines the functionality required to be a puzzle category.
type Category interface {
// Inventory lists every puzzle in the category.
Inventory() ([]int, error)
// Puzzle provides a Puzzle structure for the given point value.
Puzzle(points int) (Puzzle, error)
// Open returns an io.ReadCloser for the given filename.
Open(points int, filename string) (ReadSeekCloser, error)
// Answer returns whether the given answer is correct.
Answer(points int, answer string) bool
}
2020-09-04 13:00:23 -06:00
// NopReadCloser provides an io.ReadCloser which does nothing.
2020-09-03 20:04:43 -06:00
type NopReadCloser struct {
}
2020-09-04 13:00:23 -06:00
// Read satisfies io.Reader.
2020-09-03 20:04:43 -06:00
func (n NopReadCloser) Read(b []byte) (int, error) {
return 0, nil
}
2020-09-04 13:00:23 -06:00
// Close satisfies io.Closer.
2020-09-03 20:04:43 -06:00
func (n NopReadCloser) Close() error {
return nil
}
// NewFsCategory returns a Category based on which files are present.
// If 'mkcategory' is present and executable, an FsCommandCategory is returned.
// Otherwise, FsCategory is returned.
2020-09-04 15:29:06 -06:00
func NewFsCategory(fs afero.Fs, cat string) Category {
bfs := NewRecursiveBasePathFs(fs, cat)
if info, err := bfs.Stat("mkcategory"); (err == nil) && (info.Mode()&0100 != 0) {
if command, err := bfs.RealPath(info.Name()); err != nil {
log.Println("Unable to resolve full path to", info.Name(), bfs)
} else {
return FsCommandCategory{
fs: bfs,
command: command,
timeout: 2 * time.Second,
}
}
2019-08-18 21:59:06 -06:00
}
2020-09-04 15:29:06 -06:00
return FsCategory{fs: bfs}
2019-08-18 21:59:06 -06:00
}
2020-09-04 13:00:23 -06:00
// FsCategory provides a category backed by a .md file.
2020-09-03 20:04:43 -06:00
type FsCategory struct {
fs afero.Fs
2020-08-28 17:41:17 -06:00
}
2019-08-18 21:59:06 -06:00
2020-09-04 13:00:23 -06:00
// Inventory returns a list of point values for this category.
2020-09-03 20:04:43 -06:00
func (c FsCategory) Inventory() ([]int, error) {
puzzleEntries, err := afero.ReadDir(c.fs, ".")
2019-08-18 21:59:06 -06:00
if err != nil {
return nil, err
}
2020-08-28 17:41:17 -06:00
puzzles := make([]int, 0, len(puzzleEntries))
for _, ent := range puzzleEntries {
if !ent.IsDir() {
continue
}
2020-08-28 17:41:17 -06:00
if points, err := strconv.Atoi(ent.Name()); err != nil {
log.Println("Skipping non-numeric directory", ent.Name())
continue
} else {
puzzles = append(puzzles, points)
}
}
2020-08-28 17:41:17 -06:00
return puzzles, nil
}
2020-09-04 13:00:23 -06:00
// Puzzle returns a Puzzle structure for the given point value.
2020-09-03 20:04:43 -06:00
func (c FsCategory) Puzzle(points int) (Puzzle, error) {
2020-09-11 13:03:19 -06:00
return NewFsPuzzlePoints(c.fs, points).Puzzle()
2020-09-03 20:04:43 -06:00
}
2020-09-04 13:00:23 -06:00
// Open returns an io.ReadCloser for the given filename.
2020-09-08 17:49:02 -06:00
func (c FsCategory) Open(points int, filename string) (ReadSeekCloser, error) {
2020-09-11 13:03:19 -06:00
return NewFsPuzzlePoints(c.fs, points).Open(filename)
2020-09-03 20:04:43 -06:00
}
2020-09-04 15:29:06 -06:00
// Answer checks whether an answer is correct.
2020-09-03 20:04:43 -06:00
func (c FsCategory) Answer(points int, answer string) bool {
// BUG(neale): FsCategory.Answer should probably always return false, to prevent you from running uncompiled puzzles with participants.
p, err := c.Puzzle(points)
if err != nil {
return false
}
for _, a := range p.Answers {
if a == answer {
return true
}
}
return false
}
2020-09-04 13:00:23 -06:00
// FsCommandCategory provides a category backed by running an external command.
2020-09-03 20:04:43 -06:00
type FsCommandCategory struct {
2020-09-04 15:29:06 -06:00
fs afero.Fs
command string
timeout time.Duration
2020-09-03 20:04:43 -06:00
}
2020-09-04 13:00:23 -06:00
// Inventory returns a list of point values for this category.
2020-09-03 20:04:43 -06:00
func (c FsCommandCategory) Inventory() ([]int, error) {
2020-09-04 15:29:06 -06:00
ctx, cancel := context.WithTimeout(context.Background(), c.timeout)
defer cancel()
cmd := exec.CommandContext(ctx, c.command, "inventory")
stdout, err := cmd.Output()
if err != nil {
return nil, err
}
ret := make([]int, 0)
if err := json.Unmarshal(stdout, &ret); err != nil {
return nil, err
}
return ret, nil
2020-09-03 20:04:43 -06:00
}
2020-09-04 13:00:23 -06:00
// Puzzle returns a Puzzle structure for the given point value.
2020-09-03 20:04:43 -06:00
func (c FsCommandCategory) Puzzle(points int) (Puzzle, error) {
2020-09-04 15:29:06 -06:00
var p Puzzle
ctx, cancel := context.WithTimeout(context.Background(), c.timeout)
defer cancel()
cmd := exec.CommandContext(ctx, c.command, "puzzle", strconv.Itoa(points))
stdout, err := cmd.Output()
if err != nil {
return p, err
}
if err := json.Unmarshal(stdout, &p); err != nil {
return p, err
}
2020-09-04 18:28:23 -06:00
p.computeAnswerHashes()
2020-09-04 15:29:06 -06:00
return p, nil
2020-09-03 20:04:43 -06:00
}
2020-09-04 13:00:23 -06:00
// Open returns an io.ReadCloser for the given filename.
2020-09-08 17:49:02 -06:00
func (c FsCommandCategory) Open(points int, filename string) (ReadSeekCloser, error) {
2020-09-04 15:29:06 -06:00
ctx, cancel := context.WithTimeout(context.Background(), c.timeout)
defer cancel()
cmd := exec.CommandContext(ctx, c.command, "file", strconv.Itoa(points), filename)
stdout, err := cmd.Output()
2020-09-08 17:49:02 -06:00
return nopCloser{bytes.NewReader(stdout)}, err
2020-09-03 20:04:43 -06:00
}
2020-09-04 15:29:06 -06:00
// Answer checks whether an answer is correct.
2020-09-03 20:04:43 -06:00
func (c FsCommandCategory) Answer(points int, answer string) bool {
2020-09-04 15:29:06 -06:00
ctx, cancel := context.WithTimeout(context.Background(), c.timeout)
defer cancel()
cmd := exec.CommandContext(ctx, c.command, "answer", strconv.Itoa(points), answer)
stdout, err := cmd.Output()
if err != nil {
log.Printf("ERROR: Answering %d points: %s", points, err)
return false
}
switch strings.TrimSpace(string(stdout)) {
case "correct":
return true
}
2020-09-03 20:04:43 -06:00
return false
2019-08-18 21:59:06 -06:00
}