diff --git a/.vscode/launch.json b/.vscode/launch.json
new file mode 100644
index 0000000..166db3a
--- /dev/null
+++ b/.vscode/launch.json
@@ -0,0 +1,30 @@
+{
+ // Use IntelliSense to learn about possible attributes.
+ // Hover to view descriptions of existing attributes.
+ // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
+ "version": "0.2.0",
+ "configurations": [
+ {
+ "name": "Launch",
+ "type": "go",
+ "request": "launch",
+ "mode": "auto",
+ "program": "${fileDirname}",
+ "env": {},
+ "args": []
+ },
+ {
+ "name": "MOTHd",
+ "type": "go",
+ "request": "launch",
+ "mode": "auto",
+ "program": "${workspaceFolder}/cmd/mothd",
+ "env": {},
+ "args": [
+ "--state", "/tmp/state",
+ "--puzzles", "${workspaceFolder}/example-puzzles",
+ "--theme", "${workspaceFolder}/theme",
+ ]
+ }
+ ]
+}
\ No newline at end of file
diff --git a/TODO.md b/TODO.md
index a05e17a..665f4ed 100644
--- a/TODO.md
+++ b/TODO.md
@@ -1,3 +1,5 @@
* Figure out how to log JSend short text in addition to HTTP code
* We've got logic in state.go and httpd.go that is neither httpd nor state specific.
Pull this into some other file that means "here are the brains of the server".
+* Get Bo's answer pattern anchors working again
+* Are we logging every transaction now?
diff --git a/cmd/mothd/__debug_bin b/cmd/mothd/__debug_bin
new file mode 100755
index 0000000..10b302f
Binary files /dev/null and b/cmd/mothd/__debug_bin differ
diff --git a/cmd/mothd/main.go b/cmd/mothd/main.go
index f457edf..718348b 100644
--- a/cmd/mothd/main.go
+++ b/cmd/mothd/main.go
@@ -27,7 +27,7 @@ func main() {
puzzlePath := flag.String(
"puzzles",
"",
- "Path to puzzles tree; if specified, enables development mode",
+ "Path to puzzles tree (enables development mode)",
)
refreshInterval := flag.Duration(
"refresh",
diff --git a/cmd/transpile/main.go b/cmd/transpile/main.go
index 119a381..84f3293 100644
--- a/cmd/transpile/main.go
+++ b/cmd/transpile/main.go
@@ -14,84 +14,101 @@ import (
"github.com/spf13/afero"
)
-// T contains everything required for a transpilation invocation (across the nation).
+// T represents the state of things
type T struct {
- // What action to take
- w io.Writer
- Cat string
- Points int
- Answer string
- Filename string
- Fs afero.Fs
+ Stdout io.Writer
+ Stderr io.Writer
+ Args []string
+ BaseFs afero.Fs
+ fs afero.Fs
+ filename string
+ answer string
}
-// ParseArgs parses command-line arguments into T, returning the action to take.
-// BUG(neale): CLI arguments are not related to how the CLI will be used.
-func (t *T) ParseArgs() string {
- action := flag.String("action", "inventory", "Action to take: must be 'inventory', 'open', 'answer', or 'mothball'")
- flag.StringVar(&t.Cat, "cat", "", "Puzzle category")
- flag.IntVar(&t.Points, "points", 0, "Puzzle point value")
- flag.StringVar(&t.Answer, "answer", "", "Answer to check for correctness, for 'answer' action")
- flag.StringVar(&t.Filename, "filename", "", "Filename, for 'open' action")
- basedir := flag.String("basedir", ".", "Base directory containing all puzzles")
- flag.Parse()
+// Command is a function invoked by the user
+type Command func() error
- osfs := afero.NewOsFs()
- t.Fs = afero.NewBasePathFs(osfs, *basedir)
-
- return *action
+func nothing() error {
+ return nil
}
-// Handle performs the requested action
-func (t *T) Handle(action string) error {
- switch action {
- case "inventory":
- return t.PrintInventory()
- case "open":
- return t.Open()
- case "mothball":
- return t.Mothball()
- default:
- return fmt.Errorf("Unimplemented action: %s", action)
+// ParseArgs parses arguments and runs the appropriate action.
+func (t *T) ParseArgs() (Command, error) {
+ var cmd Command
+
+ if len(t.Args) == 1 {
+ fmt.Fprintln(t.Stderr, "Usage: transpile COMMAND [flags]")
+ fmt.Fprintln(t.Stderr, "")
+ fmt.Fprintln(t.Stderr, " mothball: Compile a mothball")
+ fmt.Fprintln(t.Stderr, " inventory: Show category inventory")
+ fmt.Fprintln(t.Stderr, " open: Open a file for a puzzle")
+ fmt.Fprintln(t.Stderr, " answer: Check correctness of an answer")
+ return nothing, nil
}
+
+ flags := flag.NewFlagSet(t.Args[1], flag.ContinueOnError)
+ directory := flags.String("dir", "", "Work directory")
+
+ switch t.Args[1] {
+ case "mothball":
+ cmd = t.DumpMothball
+ case "inventory":
+ cmd = t.PrintInventory
+ case "open":
+ flags.StringVar(&t.filename, "file", "puzzle.json", "Filename to open")
+ cmd = t.DumpFile
+ case "answer":
+ flags.StringVar(&t.answer, "answer", "", "Answer to check")
+ cmd = t.CheckAnswer
+ default:
+ return nothing, fmt.Errorf("%s is not a valid command", t.Args[1])
+ }
+
+ flags.SetOutput(t.Stderr)
+ if err := flags.Parse(t.Args[2:]); err != nil {
+ return nothing, err
+ }
+ if *directory != "" {
+ t.fs = afero.NewBasePathFs(t.BaseFs, *directory)
+ } else {
+ t.fs = t.BaseFs
+ }
+ log.Println(t.Args, t.fs)
+
+ return cmd, nil
}
// PrintInventory prints a puzzle inventory to stdout
func (t *T) PrintInventory() error {
- inv := make(map[string][]int)
-
- dirEnts, err := afero.ReadDir(t.Fs, ".")
+ inv, err := transpile.FsInventory(t.fs)
if err != nil {
return err
}
- for _, ent := range dirEnts {
- if ent.IsDir() {
- c := t.NewCategory(ent.Name())
- if puzzles, err := c.Inventory(); err != nil {
- log.Print(err)
- continue
- } else {
- sort.Ints(puzzles)
- inv[ent.Name()] = puzzles
- }
- }
- }
- m := json.NewEncoder(t.w)
- if err := m.Encode(inv); err != nil {
- return err
+ cats := make([]string, 0, len(inv))
+ for cat := range inv {
+ cats = append(cats, cat)
+ }
+ sort.Strings(cats)
+ for _, cat := range cats {
+ puzzles := inv[cat]
+ fmt.Fprint(t.Stdout, cat)
+ for _, p := range puzzles {
+ fmt.Fprint(t.Stdout, " ", p)
+ }
+ fmt.Fprintln(t.Stdout)
}
return nil
}
-// Open writes a file to the writer.
-func (t *T) Open() error {
- c := t.NewCategory(t.Cat)
+// DumpFile writes a file to the writer.
+func (t *T) DumpFile() error {
+ puzzle := transpile.NewFsPuzzle(t.fs)
- switch t.Filename {
+ switch t.filename {
case "puzzle.json", "":
// BUG(neale): we need a way to tell the transpiler to strip answers
- p, err := c.Puzzle(t.Points)
+ p, err := puzzle.Puzzle()
if err != nil {
return err
}
@@ -99,14 +116,14 @@ func (t *T) Open() error {
if err != nil {
return err
}
- t.w.Write(jp)
+ t.Stdout.Write(jp)
default:
- f, err := c.Open(t.Points, t.Filename)
+ f, err := puzzle.Open(t.filename)
if err != nil {
return err
}
defer f.Close()
- if _, err := io.Copy(t.w, f); err != nil {
+ if _, err := io.Copy(t.Stdout, f); err != nil {
return err
}
}
@@ -114,32 +131,43 @@ func (t *T) Open() error {
return nil
}
-// Mothball writes a mothball to the writer.
-func (t *T) Mothball() error {
- c := t.NewCategory(t.Cat)
+// DumpMothball writes a mothball to the writer.
+func (t *T) DumpMothball() error {
+ c := transpile.NewFsCategory(t.fs, "")
mb, err := transpile.Mothball(c)
if err != nil {
return err
}
- if _, err := io.Copy(t.w, mb); err != nil {
+ if _, err := io.Copy(t.Stdout, mb); err != nil {
return err
}
return nil
}
-// NewCategory returns a new Fs-backed category.
-func (t *T) NewCategory(name string) transpile.Category {
- return transpile.NewFsCategory(t.Fs, name)
+// CheckAnswer prints whether an answer is correct.
+func (t *T) CheckAnswer() error {
+ c := transpile.NewFsPuzzle(t.fs)
+ if c.Answer(t.answer) {
+ fmt.Fprintln(t.Stdout, "correct")
+ } else {
+ fmt.Fprintln(t.Stdout, "wrong")
+ }
+ return nil
}
func main() {
// XXX: Convert puzzle.py to standalone thingies
t := &T{
- w: os.Stdout,
+ Stdout: os.Stdout,
+ Stderr: os.Stderr,
+ Args: os.Args,
}
- action := t.ParseArgs()
- if err := t.Handle(action); err != nil {
+ cmd, err := t.ParseArgs()
+ if err != nil {
+ log.Fatal(err)
+ }
+ if err := cmd(); err != nil {
log.Fatal(err)
}
}
diff --git a/cmd/transpile/main_test.go b/cmd/transpile/main_test.go
index 77a6bdb..67c353a 100644
--- a/cmd/transpile/main_test.go
+++ b/cmd/transpile/main_test.go
@@ -3,7 +3,7 @@ package main
import (
"bytes"
"encoding/json"
- "strings"
+ "log"
"testing"
"github.com/dirtbags/moth/pkg/transpile"
@@ -23,62 +23,51 @@ pre:
---
YAML body
`)
-var testMothRfc822 = []byte(`author: test
-Author: Arthur
-author: Fred Flintstone
-answer: RFC822 answer
-
-RFC822 body
-`)
func newTestFs() afero.Fs {
fs := afero.NewMemMapFs()
afero.WriteFile(fs, "cat0/1/puzzle.md", testMothYaml, 0644)
- afero.WriteFile(fs, "cat0/1/moo.txt", []byte("Moo."), 0644)
- afero.WriteFile(fs, "cat0/2/puzzle.md", testMothRfc822, 0644)
+ afero.WriteFile(fs, "cat0/1/moo.txt", testMothYaml, 0644)
+ afero.WriteFile(fs, "cat0/2/puzzle.moth", testMothYaml, 0644)
afero.WriteFile(fs, "cat0/3/puzzle.moth", testMothYaml, 0644)
afero.WriteFile(fs, "cat0/4/puzzle.md", testMothYaml, 0644)
afero.WriteFile(fs, "cat0/5/puzzle.md", testMothYaml, 0644)
- afero.WriteFile(fs, "cat0/10/puzzle.md", []byte(`---
-Answers:
- - moo
-Authors:
- - bad field
----
-body
-`), 0644)
- afero.WriteFile(fs, "cat0/20/puzzle.md", []byte("Answer: no\nBadField: yes\n\nbody\n"), 0644)
- afero.WriteFile(fs, "cat0/21/puzzle.md", []byte("Answer: broken\nSpooon\n"), 0644)
- afero.WriteFile(fs, "cat0/22/puzzle.md", []byte("---\nanswers:\n - pencil\npre:\n unused-field: Spooon\n---\nSpoon?\n"), 0644)
- afero.WriteFile(fs, "cat1/93/puzzle.md", []byte("Answer: no\n\nbody"), 0644)
- afero.WriteFile(fs, "cat1/barney/puzzle.md", testMothYaml, 0644)
+ afero.WriteFile(fs, "cat0/10/puzzle.md", testMothYaml, 0644)
afero.WriteFile(fs, "unbroken/1/puzzle.md", testMothYaml, 0644)
- afero.WriteFile(fs, "unbroken/1/moo.txt", []byte("Moo."), 0644)
- afero.WriteFile(fs, "unbroken/2/puzzle.md", testMothRfc822, 0644)
+ afero.WriteFile(fs, "unbroken/2/puzzle.md", testMothYaml, 0644)
return fs
}
+func (tp T) Run(args ...string) error {
+ tp.Args = append([]string{"transpile"}, args...)
+ command, err := tp.ParseArgs()
+ log.Println(tp.fs)
+ if err != nil {
+ return err
+ }
+ return command()
+}
+
func TestEverything(t *testing.T) {
stdout := new(bytes.Buffer)
+ stderr := new(bytes.Buffer)
tp := T{
- w: stdout,
- Fs: newTestFs(),
+ Stdout: stdout,
+ Stderr: stderr,
+ BaseFs: newTestFs(),
}
- if err := tp.Handle("inventory"); err != nil {
+ if err := tp.Run("inventory"); err != nil {
t.Error(err)
}
- if strings.TrimSpace(stdout.String()) != `{"cat0":[1,2,3,4,5,10,20,21,22],"cat1":[93],"unbroken":[1,2]}` {
+ if stdout.String() != "cat0 1 2 3 4 5 10\nunbroken 1 2\n" {
t.Errorf("Bad inventory: %#v", stdout.String())
}
stdout.Reset()
- tp.Cat = "cat0"
- tp.Points = 1
- if err := tp.Handle("open"); err != nil {
+ if err := tp.Run("open", "-dir=cat0/1"); err != nil {
t.Error(err)
}
-
p := transpile.Puzzle{}
if err := json.Unmarshal(stdout.Bytes(), &p); err != nil {
t.Error(err)
@@ -88,8 +77,7 @@ func TestEverything(t *testing.T) {
}
stdout.Reset()
- tp.Filename = "moo.txt"
- if err := tp.Handle("open"); err != nil {
+ if err := tp.Run("open", "-dir=cat0/1", "-file=moo.txt"); err != nil {
t.Error(err)
}
if stdout.String() != "Moo." {
@@ -97,8 +85,9 @@ func TestEverything(t *testing.T) {
}
stdout.Reset()
- tp.Cat = "unbroken"
- if err := tp.Handle("mothball"); err != nil {
+ if err := tp.Run("mothball", "-dir=cat0"); err != nil {
+ t.Log(tp.BaseFs)
+ t.Log(tp.fs)
t.Error(err)
}
if stdout.Len() < 200 {
diff --git a/example-puzzles/example/3/mkpuzzle b/example-puzzles/example/3/mkpuzzle
new file mode 100755
index 0000000..b23b47f
--- /dev/null
+++ b/example-puzzles/example/3/mkpuzzle
@@ -0,0 +1,34 @@
+#! /bin/sh
+
+number=$(seq 20 500 | shuf -n 1)
+answer=$(echo $(grep -v "['A-Z]" /usr/share/dict/words | shuf -n 4))
+
+case "$1:$2" in
+ :)
+ cat <mkpuzzles
program in the puzzle directory.