This commit is contained in:
Gabe Farrell 2023-12-09 07:04:17 +00:00
commit a9cc5a8aad
15 changed files with 1640 additions and 0 deletions

109
trivia/questions.go Normal file
View file

@ -0,0 +1,109 @@
package trivia
import (
"encoding/json"
"io"
"math/rand"
"strconv"
"strings"
"sync"
)
// represents the structure of trivia questions stored
// in the trivia.json file
type Question struct {
Question string `json:"question"`
Category string `json:"category"`
Format string `json:"format"`
Choices []string `json:"choices"`
Answer string `json:"answer"`
}
type Questions struct {
Categories []string
M map[string][]Question
lock *sync.RWMutex
}
func (q *Questions) Init() {
q.lock = &sync.RWMutex{}
}
func (q *Questions) Load(r io.Reader) error {
q.lock.Lock()
defer q.lock.Unlock()
if q.M == nil {
q.M = make(map[string][]Question, 0)
}
err := json.NewDecoder(r).Decode(&q.M)
if err != nil {
return err
}
if q.Categories == nil {
q.Categories = make([]string, 0)
}
for key := range q.M {
q.Categories = append(q.Categories, key)
}
return nil
}
func (q *Questions) categoryExists(cat string) bool {
q.lock.RLock()
defer q.lock.RUnlock()
cat = strings.ToLower(cat)
if q.M[cat] == nil || len(q.M[cat]) < 1 {
return false
}
return true
}
func (q *Questions) getRandomCategory() string {
q.lock.RLock()
defer q.lock.RUnlock()
return q.Categories[rand.Int()%len(q.Categories)]
}
// Gets a random question from the category, if specified.
func (q *Questions) GetRandomQuestion(category string) (*Question, int) {
q.lock.RLock()
defer q.lock.RUnlock()
// NOTE: it is okay to call another function that locks the RWMutex here,
// as it will not cause a deadlock since CategoryExists only locks the Read
if category == "" {
category = q.getRandomCategory()
} else if !q.categoryExists(category) {
return nil, 0
}
category = strings.ToLower(category)
qIndex := rand.Int() % len(q.M[category])
return &q.M[category][qIndex], qIndex
}
func (q *Questions) GetQuestionById(id string) *Question {
// get values from question_id
questionSlice := strings.Split(id, "|")
if len(questionSlice) != 2 {
return nil
}
category, indexS := questionSlice[0], questionSlice[1]
category = strings.ToLower(category)
index, err := strconv.Atoi(indexS)
if err != nil {
return nil
}
q.lock.RLock()
defer q.lock.RUnlock()
// ensure category exists
if !q.categoryExists(category) {
return nil
}
// ensure question index is valid
if len(q.M[category]) <= index {
return nil
}
// retrieve question
return &q.M[category][index]
}

163
trivia/questions_test.go Normal file
View file

@ -0,0 +1,163 @@
package trivia_test
import (
"bytes"
"reflect"
"slices"
"testing"
"github.com/gabehf/trivia-api/trivia"
)
func TestGetRandomQuestion(t *testing.T) {
// on OK path, GetTrivia must return the question in our test data
tq, _ := Q.GetRandomQuestion("world history")
if tq == nil {
t.Fatal("trivia question must not be nil")
}
if !reflect.DeepEqual(tq, expect) {
t.Errorf("returned question does not match expectation, got %v", tq)
}
// with no category specified, GetTrivia must pick a random category and fetch a question
// with only one question in our test data, it is the same question from before
tq, _ = Q.GetRandomQuestion("")
if tq == nil {
t.Fatal("trivia question must not be nil")
}
if !reflect.DeepEqual(tq, expect) {
t.Errorf("returned question does not match expectation, got %v", tq)
}
// on FAIL path, GetTrivia must return nil to indicate no questions are found
tq, _ = Q.GetRandomQuestion("Geography")
if tq != nil {
t.Errorf("expected nil, got %v", tq)
}
}
func TestGetQuestionById(t *testing.T) {
// on OK path, GetTrivia must return the question in our test data
tq := Q.GetQuestionById("world History|0")
if tq == nil {
t.Fatal("trivia question must not be nil")
}
if !reflect.DeepEqual(tq, expect) {
t.Errorf("returned question does not match expectation, got %v", tq)
}
// FAIL path: malformed id
tq = Q.GetQuestionById("hey")
if tq != nil {
t.Errorf("expected nil, got %v", tq)
}
// FAIL path: invalid category
tq = Q.GetQuestionById("hey|0")
if tq != nil {
t.Errorf("expected nil, got %v", tq)
}
// FAIL path: invalid index
tq = Q.GetQuestionById("world history|9")
if tq != nil {
t.Errorf("expected nil, got %v", tq)
}
}
func TestLoad(t *testing.T) {
json := []byte(`
{
"world history": [
{
"category": "World History",
"question": "How many years did the 100 years war last?",
"answer": "116",
"format": "MultipleChoice",
"choices": [
"116",
"87",
"12",
"205"
]
},
{
"category": "World History",
"question": "True or False: John Wilkes Booth assassinated Abraham Lincoln.",
"answer": "True",
"format": "TrueFalse"
}
],
"geography": [
{
"category": "Geography",
"question": "What is the capital city of Japan?",
"answer": "Tokyo",
"format": "MultipleChoice",
"choices": [
"Beijing",
"Seoul",
"Bangkok",
"Tokyo"
]
},
{
"category": "Geography",
"question": "True or False: The Amazon Rainforest is located in Africa.",
"answer": "False",
"format": "TrueFalse"
}
]
}
`)
expectCategories := []string{"world history", "geography"}
expectQuestions := map[string][]trivia.Question{
"world history": {
{
Question: "How many years did the 100 years war last?",
Format: "MultipleChoice",
Answer: "116",
Category: "World History",
Choices: []string{
"116",
"87",
"12",
"205",
},
},
{
Question: "True or False: John Wilkes Booth assassinated Abraham Lincoln.",
Format: "TrueFalse",
Answer: "True",
Category: "World History",
},
},
"geography": {
{
Question: "What is the capital city of Japan?",
Format: "MultipleChoice",
Answer: "Tokyo",
Category: "Geography",
Choices: []string{"Beijing", "Seoul", "Bangkok", "Tokyo"},
},
{
Question: "True or False: The Amazon Rainforest is located in Africa.",
Format: "TrueFalse",
Answer: "False",
Category: "Geography",
},
},
}
qq := new(trivia.Questions)
qq.Init()
err := qq.Load(bytes.NewReader(json))
if err != nil {
t.Errorf("expected error to be nil, got %v", err)
}
for _, cat := range expectCategories {
if !slices.Contains(qq.Categories, cat) {
t.Errorf("expected category %s not present", cat)
}
}
if !reflect.DeepEqual(qq.M, expectQuestions) {
t.Errorf("unexpected question map, got %v", qq.M)
}
}

View file

@ -0,0 +1,44 @@
package trivia_test
import (
"testing"
"github.com/gabehf/trivia-api/trivia"
)
var Q trivia.Questions
var expect *trivia.Question
func TestMain(m *testing.M) {
Q.Init()
Q.Categories = []string{"world history"}
Q.M = map[string][]trivia.Question{
"world history": {
{
Question: "The ancient city of Rome was built on how many hills?",
Format: "MultipleChoice",
Category: "World History",
Choices: []string{
"Eight",
"Four",
"Nine",
"Seven",
},
Answer: "Seven",
},
},
}
expect = &trivia.Question{
Question: "The ancient city of Rome was built on how many hills?",
Category: "World History",
Format: "MultipleChoice",
Choices: []string{
"Eight",
"Four",
"Nine",
"Seven",
},
Answer: "Seven",
}
m.Run()
}