Start to support generated categories

This commit is contained in:
Neale Pickett 2020-09-03 20:04:43 -06:00
parent 7b06171839
commit f9cabc5255
5 changed files with 194 additions and 137 deletions

View File

@ -1,27 +1,42 @@
package main
import (
"fmt"
"io"
"log"
"strconv"
"github.com/spf13/afero"
)
// NewCategory returns a new category for the given path in the given fs.
func NewCategory(fs afero.Fs, cat string) Category {
return Category{
Fs: NewBasePathFs(fs, cat),
type NopReadCloser struct {
}
func (n NopReadCloser) Read(b []byte) (int, error) {
return 0, nil
}
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.
func NewFsCategory(fs afero.Fs) Category {
if info, err := fs.Stat("mkcategory"); (err == nil) && (info.Mode()&0100 != 0) {
return FsCommandCategory{fs: fs}
} else {
return FsCategory{fs: fs}
}
}
// Category represents an on-disk category.
type Category struct {
afero.Fs
type FsCategory struct {
fs afero.Fs
}
// Puzzles returns a list of puzzle values.
func (c Category) Puzzles() ([]int, error) {
puzzleEntries, err := afero.ReadDir(c, ".")
// Category returns a list of puzzle values.
func (c FsCategory) Inventory() ([]int, error) {
puzzleEntries, err := afero.ReadDir(c.fs, ".")
if err != nil {
return nil, err
}
@ -41,7 +56,44 @@ func (c Category) Puzzles() ([]int, error) {
return puzzles, nil
}
// PuzzleDir returns the PuzzleDir associated with points.
func (c Category) PuzzleDir(points int) *PuzzleDir {
return NewPuzzleDir(c.Fs, points)
func (c FsCategory) Puzzle(points int) (Puzzle, error) {
return NewFsPuzzle(c.fs, points).Puzzle()
}
func (c FsCategory) Open(points int, filename string) (io.ReadCloser, error) {
return NewFsPuzzle(c.fs, points).Open(filename)
}
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
}
type FsCommandCategory struct {
fs afero.Fs
}
func (c FsCommandCategory) Inventory() ([]int, error) {
return nil, fmt.Errorf("Not implemented")
}
func (c FsCommandCategory) Puzzle(points int) (Puzzle, error) {
return Puzzle{}, fmt.Errorf("Not implemented")
}
func (c FsCommandCategory) Open(points int, filename string) (io.ReadCloser, error) {
return NopReadCloser{}, fmt.Errorf("Not implemented")
}
func (c FsCommandCategory) Answer(points int, answer string) bool {
return false
}

View File

@ -13,6 +13,54 @@ import (
"github.com/spf13/afero"
)
// 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) (io.ReadCloser, error)
// Answer returns whether the given answer is correct.
Answer(points int, answer string) bool
}
// PuzzleDef contains everything about a puzzle.
type Puzzle struct {
Pre struct {
Authors []string
Attachments []Attachment
Scripts []Attachment
AnswerPattern string
Body string
}
Post struct {
Objective string
Success struct {
Acceptable string
Mastery string
}
KSAs []string
}
Debug struct {
Log []string
Errors []string
Hints []string
Summary string
}
Answers []string
}
// Attachment carries information about an attached file.
type Attachment struct {
Filename string // Filename presented as part of puzzle
FilesystemPath string // Filename in backing FS (URL, mothball, or local FS)
Listed bool // Whether this file is listed as an attachment
}
// T contains everything required for a transpilation invocation (across the nation).
type T struct {
// What action to take
@ -24,11 +72,6 @@ type T struct {
Fs afero.Fs
}
// NewCategory returns a new Category as specified by cat.
func (t *T) NewCategory(cat string) Category {
return NewCategory(t.Fs, cat)
}
// ParseArgs parses command-line arguments into T, returning the action to take
func (t *T) ParseArgs() string {
action := flag.String("action", "inventory", "Action to take: must be 'inventory', 'open', 'answer', or 'mothball'")
@ -66,7 +109,7 @@ func (t *T) PrintInventory() error {
for _, ent := range dirEnts {
if ent.IsDir() {
c := t.NewCategory(ent.Name())
if puzzles, err := c.Puzzles(); err != nil {
if puzzles, err := c.Inventory(); err != nil {
log.Print(err)
continue
} else {
@ -86,11 +129,10 @@ func (t *T) PrintInventory() error {
// Open writes a file to the writer.
func (t *T) Open() error {
c := t.NewCategory(t.Cat)
pd := c.PuzzleDir(t.Points)
switch t.Filename {
case "puzzle.json", "":
p, err := pd.Export()
p, err := c.Puzzle(t.Points)
if err != nil {
return err
}
@ -100,7 +142,7 @@ func (t *T) Open() error {
}
t.w.Write(jp)
default:
f, err := pd.Open(t.Filename)
f, err := c.Open(t.Points, t.Filename)
if err != nil {
return err
}
@ -113,6 +155,10 @@ func (t *T) Open() error {
return nil
}
func (t *T) NewCategory(name string) Category {
return NewFsCategory(NewBasePathFs(t.Fs, name))
}
func main() {
// XXX: Convert puzzle.py to standalone thingies

View File

@ -9,7 +9,6 @@ import (
"io"
"log"
"net/mail"
"os"
"os/exec"
"strconv"
"strings"
@ -20,56 +19,27 @@ import (
"gopkg.in/yaml.v2"
)
// NewPuzzleDir returns a new PuzzleDir for points.
func NewPuzzleDir(fs afero.Fs, points int) *PuzzleDir {
pd := &PuzzleDir{
// NewFsPuzzle returns a new FsPuzzle for points.
func NewFsPuzzle(fs afero.Fs, points int) *FsPuzzle {
fp := &FsPuzzle{
fs: NewBasePathFs(fs, strconv.Itoa(points)),
}
// BUG(neale): Doesn't yet handle "puzzle.py" or "mkpuzzle"
return pd
return fp
}
// PuzzleDir is a single puzzle's directory.
type PuzzleDir struct {
fs afero.Fs
// FsPuzzle is a single puzzle's directory.
type FsPuzzle struct {
fs afero.Fs
mkpuzzle bool
}
// Open returns a newly-opened file.
func (pd *PuzzleDir) Open(name string) (io.ReadCloser, error) {
// BUG(neale): You cannot open generated files in puzzles, only files actually on the disk
if _, err := pd.fs.Stat(""
return pd.fs.Open(name)
}
// Export returns a Puzzle struct for the current puzzle.
func (pd *PuzzleDir) Export() (Puzzle, error) {
p, staticErr := pd.exportStatic()
if staticErr == nil {
return p, nil
}
// Only fall through if the static files don't exist. Otherwise, report the error.
if !os.IsNotExist(staticErr) {
return p, staticErr
}
if p, cmdErr := pd.exportCommand(); cmdErr == nil {
return p, nil
} else if os.IsNotExist(cmdErr) {
// If the command doesn't exist either, report the non-existence of the static file instead.
return p, staticErr
} else {
return p, cmdErr
}
}
func (pd *PuzzleDir) exportStatic() (Puzzle, error) {
r, err := pd.fs.Open("puzzle.md")
// Puzzle returns a Puzzle struct for the current puzzle.
func (fp FsPuzzle) Puzzle() (Puzzle, error) {
r, err := fp.fs.Open("puzzle.md")
if err != nil {
var err2 error
if r, err2 = pd.fs.Open("puzzle.moth"); err2 != nil {
if r, err2 = fp.fs.Open("puzzle.moth"); err2 != nil {
return Puzzle{}, err
}
}
@ -124,34 +94,9 @@ func (pd *PuzzleDir) exportStatic() (Puzzle, error) {
return puzzle, nil
}
func (pd *PuzzleDir) exportCommand() (Puzzle, error) {
bfs, ok := pd.fs.(*BasePathFs)
if !ok {
return Puzzle{}, fmt.Errorf("Fs won't resolve real paths for %v", pd)
}
mkpuzzlePath, err := bfs.RealPath("mkpuzzle")
if err != nil {
return Puzzle{}, err
}
log.Print(mkpuzzlePath)
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
cmd := exec.CommandContext(ctx, mkpuzzlePath)
stdout, err := cmd.Output()
if err != nil {
return Puzzle{}, err
}
jsdec := json.NewDecoder(bytes.NewReader(stdout))
jsdec.DisallowUnknownFields()
puzzle := Puzzle{}
if err := jsdec.Decode(&puzzle); err != nil {
return Puzzle{}, err
}
return puzzle, nil
// Open returns a newly-opened file.
func (fp FsPuzzle) Open(name string) (io.ReadCloser, error) {
return fp.fs.Open(name)
}
func legacyAttachmentParser(val []string) []Attachment {
@ -175,39 +120,6 @@ func legacyAttachmentParser(val []string) []Attachment {
return ret
}
// Puzzle contains everything about a puzzle.
type Puzzle struct {
Pre struct {
Authors []string
Attachments []Attachment
Scripts []Attachment
AnswerPattern string
Body string
}
Post struct {
Objective string
Success struct {
Acceptable string
Mastery string
}
KSAs []string
}
Debug struct {
Log []string
Errors []string
Hints []string
Summary string
}
Answers []string
}
// Attachment carries information about an attached file.
type Attachment struct {
Filename string // Filename presented as part of puzzle
FilesystemPath string // Filename in backing FS (URL, mothball, or local FS)
Listed bool // Whether this file is listed as an attachment
}
func yamlHeaderParser(r io.Reader) (Puzzle, error) {
p := Puzzle{}
decoder := yaml.NewDecoder(r)
@ -249,3 +161,49 @@ func rfc822HeaderParser(r io.Reader) (Puzzle, error) {
return p, nil
}
func (fp FsPuzzle) Answer(answer string) bool {
return false
}
type FsCommandPuzzle struct {
fs afero.Fs
}
func (fp FsCommandPuzzle) Puzzle() (Puzzle, error) {
bfs, ok := fp.fs.(*BasePathFs)
if !ok {
return Puzzle{}, fmt.Errorf("Fs won't resolve real paths for %v", fp)
}
mkpuzzlePath, err := bfs.RealPath("mkpuzzle")
if err != nil {
return Puzzle{}, err
}
log.Print(mkpuzzlePath)
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
cmd := exec.CommandContext(ctx, mkpuzzlePath)
stdout, err := cmd.Output()
if err != nil {
return Puzzle{}, err
}
jsdec := json.NewDecoder(bytes.NewReader(stdout))
jsdec.DisallowUnknownFields()
puzzle := Puzzle{}
if err := jsdec.Decode(&puzzle); err != nil {
return Puzzle{}, err
}
return puzzle, nil
}
func (fp FsCommandPuzzle) Open(filename string) (io.ReadCloser, error) {
return NopReadCloser{}, fmt.Errorf("Not implemented")
}
func (fp FsCommandPuzzle) Answer(answer string) bool {
return false
}

View File

@ -14,8 +14,8 @@ func TestPuzzle(t *testing.T) {
catFs := afero.NewBasePathFs(puzzleFs, "cat0")
{
pd := NewPuzzleDir(catFs, 1)
p, err := pd.Export()
pd := NewFsPuzzle(catFs, 1)
p, err := pd.Puzzle()
if err != nil {
t.Error(err)
}
@ -32,7 +32,7 @@ func TestPuzzle(t *testing.T) {
}
{
p, err := NewPuzzleDir(catFs, 2).Export()
p, err := NewFsPuzzle(catFs, 2).Puzzle()
if err != nil {
t.Error(err)
}
@ -47,24 +47,24 @@ func TestPuzzle(t *testing.T) {
}
}
if _, err := NewPuzzleDir(catFs, 3).Export(); err != nil {
if _, err := NewFsPuzzle(catFs, 3).Puzzle(); err != nil {
t.Error("Legacy `puzzle.moth` file:", err)
}
if _, err := NewPuzzleDir(catFs, 99).Export(); err == nil {
if _, err := NewFsPuzzle(catFs, 99).Puzzle(); err == nil {
t.Error("Non-existent puzzle", err)
}
if _, err := NewPuzzleDir(catFs, 10).Export(); err == nil {
if _, err := NewFsPuzzle(catFs, 10).Puzzle(); err == nil {
t.Error("Broken YAML")
}
if _, err := NewPuzzleDir(catFs, 20).Export(); err == nil {
if _, err := NewFsPuzzle(catFs, 20).Puzzle(); err == nil {
t.Error("Bad RFC822 header")
}
if _, err := NewPuzzleDir(catFs, 21).Export(); err == nil {
if _, err := NewFsPuzzle(catFs, 21).Puzzle(); err == nil {
t.Error("Boken RFC822 header")
}
if p, err := NewPuzzleDir(catFs, 22).Export(); err == nil {
if p, err := NewFsPuzzle(catFs, 22).Puzzle(); err == nil {
t.Error("Duplicate bodies")
} else if !strings.HasPrefix(err.Error(), "Puzzle body present") {
t.Log(p)
@ -75,16 +75,16 @@ func TestPuzzle(t *testing.T) {
func TestFsPuzzle(t *testing.T) {
catFs := afero.NewBasePathFs(afero.NewOsFs(), "testdata")
if _, err := NewPuzzleDir(catFs, 1).Export(); err != nil {
if _, err := NewFsPuzzle(catFs, 1).Puzzle(); err != nil {
t.Error(err)
}
if _, err := NewPuzzleDir(catFs, 2).Export(); err != nil {
if _, err := NewFsPuzzle(catFs, 2).Puzzle(); err != nil {
t.Error(err)
}
mkpuzzleDir := NewPuzzleDir(catFs, 3)
if _, err := mkpuzzleDir.Export(); err != nil {
mkpuzzleDir := NewFsPuzzle(catFs, 3)
if _, err := mkpuzzleDir.Puzzle(); err != nil {
t.Error(err)
}

View File

@ -0,0 +1 @@
package main