JSON dicts for back-end API

This commit is contained in:
Neale Pickett 2020-10-16 14:18:44 -06:00
parent 682a6a7f86
commit bb2565b06d
11 changed files with 75 additions and 34 deletions

View File

@ -8,6 +8,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed
- Clear Debug.summary field when making mothballs
### Changed
- Regulated category/puzzle provider API: now everything returns a JSON dictionary (or octet stream for files)
### Added
- More log events
- [Log channels document](docs/logs.md)

View File

@ -4,6 +4,7 @@ package main
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"log"
@ -13,6 +14,8 @@ import (
"strconv"
"strings"
"time"
"github.com/dirtbags/moth/pkg/transpile"
)
// ProviderCommand specifies a command to run for the puzzle API
@ -111,14 +114,13 @@ func (pc ProviderCommand) CheckAnswer(cat string, points int, answer string) (bo
} else if err != nil {
return false, err
}
result := strings.TrimSpace(string(stdout))
if result != "correct" {
log.Printf("WARNING: %s: Nothing written to stdout", pc.Path)
return false, nil
ans := transpile.AnswerResponse{}
if err := json.Unmarshal(stdout, &ans); err != nil {
return false, err
}
return true, nil
return ans.Correct, nil
}
// Mothball just returns an error

View File

@ -185,7 +185,7 @@ func (mh *MothRequestHandler) exportStateIfRegistered(override bool) *StateExpor
export.Config = mh.Config
teamName, err := mh.State.TeamName(mh.teamID)
registered := override || (err == nil)
registered := override || mh.Config.Devel || (err == nil)
export.Messages = mh.State.Messages()
export.TeamNames = make(map[string]string)

View File

@ -27,7 +27,23 @@ func NewTestServer() *MothServer {
return NewMothServer(Configuration{}, theme, state, puzzles)
}
func TestServer(t *testing.T) {
func TestDevelServer(t *testing.T) {
server := NewTestServer()
server.Config.Devel = true
anonHandler := server.NewHandler("badParticipantId", "badTeamId")
{
es := anonHandler.ExportState()
if !es.Config.Devel {
t.Error("Not marked as development server")
}
if len(es.Puzzles) != 1 {
t.Error("Wrong puzzles for anonymous state on devel server:", es.Puzzles)
}
}
}
func TestProdServer(t *testing.T) {
teamName := "OurTeam"
participantID := "participantID"
teamID := TestTeamID

View File

@ -26,9 +26,9 @@ EOT
;;
answer:pategory:1)
if [ "$ANSWER" = "answer" ]; then
echo "correct"
echo '{"Correct":true}'
else
echo "Sorry, wrong answer."
echo '{"Correct":false}'
fi
;;
answer:pategory:2)

View File

@ -45,12 +45,14 @@ def open_file(filename):
shutil.copyfileobj(f, sys.stdout.buffer)
def check_answer(check):
if answer == check:
print("correct")
else:
print("incorrect")
obj = {
"Correct": (answer == check)
}
json.dump(obj, sys.stdout)
if len(sys.argv) == 1:
raise RuntimeError("Command not provided")
elif sys.argv[1] == "puzzle":
puzzle()
elif sys.argv[1] == "file":
open_file(sys.argv[2])

View File

@ -9,12 +9,16 @@ import (
"os/exec"
"path"
"strconv"
"strings"
"time"
"github.com/spf13/afero"
)
// InventoryResponse is what's handed back when we ask for an inventory.
type InventoryResponse struct {
Puzzles []int
}
// Category defines the functionality required to be a puzzle category.
type Category interface {
// Inventory lists every puzzle in the category.
@ -141,12 +145,12 @@ func (c FsCommandCategory) Inventory() ([]int, error) {
return nil, err
}
ret := make([]int, 0)
if err := json.Unmarshal(stdout, &ret); err != nil {
inv := InventoryResponse{}
if err := json.Unmarshal(stdout, &inv); err != nil {
return nil, err
}
return ret, nil
return inv.Puzzles, nil
}
// Puzzle returns a Puzzle structure for the given point value.
@ -181,10 +185,11 @@ func (c FsCommandCategory) Answer(points int, answer string) bool {
return false
}
switch strings.TrimSpace(string(stdout)) {
case "correct":
return true
ans := AnswerResponse{}
if err := json.Unmarshal(stdout, &ans); err != nil {
log.Printf("ERROR: Answering %d points: %s", points, err)
return false
}
return false
return ans.Correct
}

View File

@ -22,6 +22,11 @@ import (
"gopkg.in/yaml.v2"
)
// AnswerResponse is handed back when we ask for an answer to be checked.
type AnswerResponse struct {
Correct bool
}
// Puzzle contains everything about a puzzle that a client would see.
type Puzzle struct {
Pre struct {
@ -425,9 +430,11 @@ func (fp FsCommandPuzzle) Answer(answer string) bool {
return false
}
switch strings.TrimSpace(string(stdout)) {
case "correct":
return true
}
ans := AnswerResponse{}
if err := json.Unmarshal(stdout, &ans); err != nil {
log.Printf("ERROR: checking answer: %s", err)
return false
}
return ans.Correct
}

View File

@ -7,8 +7,12 @@ fail () {
case $1:$2:$3 in
inventory::)
echo "[1,2,3,"
echo "4,5]"
cat <<EOT
{
"Puzzles": [1, 2, 3,
4, 5]
}
EOT
;;
puzzle:1:)
cat <<EOT
@ -31,10 +35,10 @@ EOT
fail "No such file: $2"
;;
answer:1:answer1.0)
echo "correct"
echo -n '{"Correct":true}'
;;
answer:1:*)
echo "incorrect"
echo '{"Correct":false}'
;;
answer:*:*)
fail "Fail answer"

View File

@ -24,16 +24,15 @@ EOT
fail "no such file: $1"
;;
answer:moo)
echo "correct"
echo '{"Correct":true}'
;;
answer:error)
fail "you requested an error"
;;
answer:*)
echo "incorrect"
echo '{"Correct":false}'
;;
*)
fail "What is $1"
;;
esac

View File

@ -30,6 +30,9 @@ function renderPuzzles(obj) {
// Create a sorted list of category names
let cats = Object.keys(obj)
cats.sort()
if (cats.length == 0) {
toast("No categories to serve!")
}
for (let cat of cats) {
if (cat.startsWith("__")) {
// Skip metadata
@ -103,8 +106,8 @@ function renderState(obj) {
let params = new URLSearchParams(window.location.search)
sessionStorage.id = "1"
sessionStorage.pid = "rodney"
}
if (Object.keys(obj.Puzzles).length > 0) {
renderPuzzles(obj.Puzzles)
} else if (Object.keys(obj.Puzzles).length > 0) {
renderPuzzles(obj.Puzzles)
if (obj.Config.Detachable) {
fetchAll(obj.Puzzles)