Collections
This commit is contained in:
parent
4da6162dd4
commit
15830d888f
10 changed files with 474 additions and 115 deletions
|
@ -16,6 +16,11 @@ type Quiz struct {
|
||||||
Answers []*Answer `json:"answers"`
|
Answers []*Answer `json:"answers"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Collection struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Query string `json:"query"`
|
||||||
|
}
|
||||||
|
|
||||||
type BaseResponse struct {
|
type BaseResponse struct {
|
||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
Message string `json:"message"`
|
Message string `json:"message"`
|
||||||
|
@ -52,3 +57,11 @@ type CreateUpdateQuizRequest struct {
|
||||||
type DeleteQuizRequest struct {
|
type DeleteQuizRequest struct {
|
||||||
ID string
|
ID string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type CreateUpdateCollectionRequest struct {
|
||||||
|
*Collection
|
||||||
|
}
|
||||||
|
|
||||||
|
type DeleteCollectionRequest struct {
|
||||||
|
ID string
|
||||||
|
}
|
||||||
|
|
9
models/collection.go
Normal file
9
models/collection.go
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
package models
|
||||||
|
|
||||||
|
type Collection struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Query string `json:"query"`
|
||||||
|
|
||||||
|
IDs []string `json:"ids"`
|
||||||
|
}
|
|
@ -5,5 +5,5 @@ import "time"
|
||||||
type Meta struct {
|
type Meta struct {
|
||||||
ID string `json:"id" yaml:"id"`
|
ID string `json:"id" yaml:"id"`
|
||||||
CreatedAt time.Time `json:"created_at" yaml:"created_at"`
|
CreatedAt time.Time `json:"created_at" yaml:"created_at"`
|
||||||
Tags []*Tag `json:"tags" yaml:"tags"`
|
Tags []*Tag `json:"tags" yaml:"-"`
|
||||||
}
|
}
|
||||||
|
|
98
store/file/collection_test.go
Normal file
98
store/file/collection_test.go
Normal file
|
@ -0,0 +1,98 @@
|
||||||
|
package file
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"git.andreafazzi.eu/andrea/probo/client"
|
||||||
|
"github.com/remogatto/prettytest"
|
||||||
|
)
|
||||||
|
|
||||||
|
type collectionTestSuite struct {
|
||||||
|
prettytest.Suite
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *collectionTestSuite) TestCreateCollection() {
|
||||||
|
store, err := NewFileProboCollectorStore(testdataDir)
|
||||||
|
t.Nil(err, fmt.Sprintf("A file store should be initialized without problems but an error occurred: %v", err))
|
||||||
|
|
||||||
|
if !t.Failed() {
|
||||||
|
quiz_1, err := createQuizOnDisk(store, &client.CreateUpdateQuizRequest{
|
||||||
|
Quiz: &client.Quiz{
|
||||||
|
Question: &client.Question{Text: "Question text with #tag1."},
|
||||||
|
Answers: []*client.Answer{
|
||||||
|
{Text: "Answer 1", Correct: true},
|
||||||
|
{Text: "Answer 2", Correct: false},
|
||||||
|
{Text: "Answer 3", Correct: false},
|
||||||
|
{Text: "Answer 4", Correct: false},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Nil(err, "The quiz to be updated should be created without issue")
|
||||||
|
|
||||||
|
path_1, _ := store.GetQuizPath(quiz_1)
|
||||||
|
|
||||||
|
if !t.Failed() {
|
||||||
|
quiz_2, err := createQuizOnDisk(store, &client.CreateUpdateQuizRequest{
|
||||||
|
Quiz: &client.Quiz{
|
||||||
|
Question: &client.Question{Text: "Another question text with #tag1."},
|
||||||
|
Answers: []*client.Answer{
|
||||||
|
{Text: "Answer 1", Correct: true},
|
||||||
|
{Text: "Answer 2", Correct: false},
|
||||||
|
{Text: "Answer 3", Correct: false},
|
||||||
|
{Text: "Answer 4", Correct: false},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Nil(err, "The quiz to be updated should be created without issue")
|
||||||
|
|
||||||
|
path_2, _ := store.GetQuizPath(quiz_2)
|
||||||
|
|
||||||
|
if !t.Failed() {
|
||||||
|
|
||||||
|
quiz_3, err := createQuizOnDisk(store, &client.CreateUpdateQuizRequest{
|
||||||
|
Quiz: &client.Quiz{
|
||||||
|
Question: &client.Question{Text: "Question text without tags."},
|
||||||
|
Answers: []*client.Answer{
|
||||||
|
{Text: "Answer 1", Correct: true},
|
||||||
|
{Text: "Answer 2", Correct: false},
|
||||||
|
{Text: "Answer 3", Correct: false},
|
||||||
|
{Text: "Answer 4", Correct: false},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Nil(err, "The quiz to be updated should be created without issue")
|
||||||
|
|
||||||
|
path_3, _ := store.GetQuizPath(quiz_3)
|
||||||
|
|
||||||
|
if !t.Failed() {
|
||||||
|
|
||||||
|
collection, err := store.CreateCollection(
|
||||||
|
&client.CreateUpdateCollectionRequest{
|
||||||
|
Collection: &client.Collection{
|
||||||
|
Name: "MyCollection",
|
||||||
|
Query: "#tag1",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Nil(err, "Creating a collection should not return an error")
|
||||||
|
|
||||||
|
collectionPath, _ := store.GetCollectionPath(collection)
|
||||||
|
|
||||||
|
if !t.Failed() {
|
||||||
|
t.Equal(2, len(collection.IDs))
|
||||||
|
|
||||||
|
os.Remove(path_1)
|
||||||
|
os.Remove(path_2)
|
||||||
|
os.Remove(path_3)
|
||||||
|
os.Remove(collectionPath)
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,6 +3,7 @@ package file
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
@ -22,13 +23,23 @@ import (
|
||||||
"github.com/go-yaml/yaml"
|
"github.com/go-yaml/yaml"
|
||||||
)
|
)
|
||||||
|
|
||||||
var ErrorMetaHeaderIsNotPresent = errors.New("Meta header was not found in file.")
|
var (
|
||||||
|
ErrorMetaHeaderIsNotPresent = errors.New("Meta header was not found in file.")
|
||||||
|
|
||||||
|
DefaultQuizzesDir = "quizzes"
|
||||||
|
DefaultCollectionsDir = "collections"
|
||||||
|
)
|
||||||
|
|
||||||
type FileProboCollectorStore struct {
|
type FileProboCollectorStore struct {
|
||||||
Dir string
|
Dir string
|
||||||
|
|
||||||
memoryStore *memory.MemoryProboCollectorStore
|
memoryStore *memory.MemoryProboCollectorStore
|
||||||
paths map[string]string
|
|
||||||
|
quizzesPaths map[string]string
|
||||||
|
collectionsPaths map[string]string
|
||||||
|
|
||||||
|
quizzesDir string
|
||||||
|
collectionsDir string
|
||||||
|
|
||||||
// A mutex is used to synchronize read/write access to the map
|
// A mutex is used to synchronize read/write access to the map
|
||||||
lock sync.RWMutex
|
lock sync.RWMutex
|
||||||
|
@ -39,6 +50,9 @@ func NewFileProboCollectorStore(dirname string) (*FileProboCollectorStore, error
|
||||||
|
|
||||||
s.Dir = dirname
|
s.Dir = dirname
|
||||||
|
|
||||||
|
s.quizzesDir = filepath.Join(s.Dir, DefaultQuizzesDir)
|
||||||
|
s.collectionsDir = filepath.Join(s.Dir, DefaultCollectionsDir)
|
||||||
|
|
||||||
err := s.Reindex()
|
err := s.Reindex()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -47,14 +61,20 @@ func NewFileProboCollectorStore(dirname string) (*FileProboCollectorStore, error
|
||||||
return s, nil
|
return s, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *FileProboCollectorStore) Reindex() error {
|
func (s *FileProboCollectorStore) GetQuizzesDir() string {
|
||||||
files, err := ioutil.ReadDir(s.Dir)
|
return s.quizzesDir
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *FileProboCollectorStore) GetCollectionsDir() string {
|
||||||
|
return s.collectionsDir
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *FileProboCollectorStore) reindexQuizzes() error {
|
||||||
|
files, err := ioutil.ReadDir(s.quizzesDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
s.paths = make(map[string]string)
|
|
||||||
|
|
||||||
markdownFiles := make([]fs.FileInfo, 0)
|
markdownFiles := make([]fs.FileInfo, 0)
|
||||||
|
|
||||||
for _, file := range files {
|
for _, file := range files {
|
||||||
|
@ -68,13 +88,9 @@ func (s *FileProboCollectorStore) Reindex() error {
|
||||||
return fmt.Errorf("The directory is empty.")
|
return fmt.Errorf("The directory is empty.")
|
||||||
}
|
}
|
||||||
|
|
||||||
s.memoryStore = memory.NewMemoryProboCollectorStore(
|
|
||||||
sha256.NewDefault256Hasher(sha256.DefaultSHA256HashingFn),
|
|
||||||
)
|
|
||||||
|
|
||||||
for _, file := range markdownFiles {
|
for _, file := range markdownFiles {
|
||||||
filename := file.Name()
|
filename := file.Name()
|
||||||
fullPath := filepath.Join(s.Dir, filename)
|
fullPath := filepath.Join(s.quizzesDir, filename)
|
||||||
|
|
||||||
content, err := os.ReadFile(fullPath)
|
content, err := os.ReadFile(fullPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -98,7 +114,72 @@ func (s *FileProboCollectorStore) Reindex() error {
|
||||||
CreatedAt: time.Now(),
|
CreatedAt: time.Now(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
s.SetPath(q, fullPath)
|
s.SetQuizPath(q, fullPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *FileProboCollectorStore) reindexCollections() error {
|
||||||
|
files, err := ioutil.ReadDir(s.collectionsDir)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
jsonFiles := make([]fs.FileInfo, 0)
|
||||||
|
|
||||||
|
for _, file := range files {
|
||||||
|
filename := file.Name()
|
||||||
|
if !file.IsDir() && strings.HasSuffix(filename, ".json") {
|
||||||
|
jsonFiles = append(jsonFiles, file)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, file := range jsonFiles {
|
||||||
|
filename := file.Name()
|
||||||
|
fullPath := filepath.Join(s.collectionsDir, filename)
|
||||||
|
|
||||||
|
content, err := os.ReadFile(fullPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var clientCollection *client.Collection
|
||||||
|
|
||||||
|
err = json.Unmarshal(content, &clientCollection)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
collection, err := s.memoryStore.CreateCollection(&client.CreateUpdateCollectionRequest{
|
||||||
|
Collection: clientCollection,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
s.SetCollectionPath(collection, fullPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *FileProboCollectorStore) Reindex() error {
|
||||||
|
s.memoryStore = memory.NewMemoryProboCollectorStore(
|
||||||
|
sha256.NewDefault256Hasher(sha256.DefaultSHA256HashingFn),
|
||||||
|
)
|
||||||
|
|
||||||
|
s.quizzesPaths = make(map[string]string)
|
||||||
|
s.collectionsPaths = make(map[string]string)
|
||||||
|
|
||||||
|
err := s.reindexQuizzes()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = s.reindexCollections()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
@ -153,7 +234,7 @@ func (s *FileProboCollectorStore) DeleteQuiz(r *client.DeleteQuizRequest) (*mode
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
path, err := s.GetPath(quiz)
|
path, err := s.GetQuizPath(quiz)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -171,21 +252,47 @@ func (s *FileProboCollectorStore) DeleteQuiz(r *client.DeleteQuizRequest) (*mode
|
||||||
return quiz, nil
|
return quiz, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *FileProboCollectorStore) GetPath(quiz *models.Quiz) (string, error) {
|
func (s *FileProboCollectorStore) GetQuizPath(quiz *models.Quiz) (string, error) {
|
||||||
|
if quiz == nil {
|
||||||
|
return "", errors.New("Quiz object passed as argument is nil!")
|
||||||
|
}
|
||||||
|
|
||||||
s.lock.RLock()
|
s.lock.RLock()
|
||||||
defer s.lock.RUnlock()
|
defer s.lock.RUnlock()
|
||||||
|
|
||||||
path, ok := s.paths[quiz.ID]
|
path, ok := s.quizzesPaths[quiz.ID]
|
||||||
if !ok {
|
if !ok {
|
||||||
return "", errors.New(fmt.Sprintf("Path not found for quiz ID %v", quiz.ID))
|
return "", errors.New(fmt.Sprintf("Path not found for quiz ID %v", quiz.ID))
|
||||||
}
|
}
|
||||||
return path, nil
|
return path, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *FileProboCollectorStore) SetPath(quiz *models.Quiz, path string) string {
|
func (s *FileProboCollectorStore) GetCollectionPath(collection *models.Collection) (string, error) {
|
||||||
|
s.lock.RLock()
|
||||||
|
defer s.lock.RUnlock()
|
||||||
|
|
||||||
|
path, ok := s.collectionsPaths[collection.ID]
|
||||||
|
if !ok {
|
||||||
|
return "", errors.New(fmt.Sprintf("Path not found for collection ID %v", collection.ID))
|
||||||
|
}
|
||||||
|
return path, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *FileProboCollectorStore) SetQuizPath(quiz *models.Quiz, path string) string {
|
||||||
s.lock.Lock()
|
s.lock.Lock()
|
||||||
defer s.lock.Unlock()
|
defer s.lock.Unlock()
|
||||||
s.paths[quiz.ID] = path
|
|
||||||
|
s.quizzesPaths[quiz.ID] = path
|
||||||
|
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *FileProboCollectorStore) SetCollectionPath(collection *models.Collection, path string) string {
|
||||||
|
s.lock.Lock()
|
||||||
|
defer s.lock.Unlock()
|
||||||
|
|
||||||
|
s.collectionsPaths[collection.ID] = path
|
||||||
|
|
||||||
return path
|
return path
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -216,67 +323,6 @@ func MarkdownFromQuiz(quiz *models.Quiz) (string, error) {
|
||||||
return markdown, nil
|
return markdown, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// func QuizFromMarkdown(markdown string) (*client.Quiz, *models.Meta, error) {
|
|
||||||
// meta, remainingMarkdown, err := parseMetaHeaderFromMarkdown(markdown)
|
|
||||||
// if err != nil {
|
|
||||||
// return nil, nil, err
|
|
||||||
// }
|
|
||||||
|
|
||||||
// if meta == nil {
|
|
||||||
// meta = new(models.Meta)
|
|
||||||
// if meta.Tags == nil {
|
|
||||||
// meta.Tags = make([]*models.Tag, 0)
|
|
||||||
// }
|
|
||||||
// } else if meta.Tags == nil {
|
|
||||||
// meta.Tags = make([]*models.Tag, 0)
|
|
||||||
// }
|
|
||||||
|
|
||||||
// lines := strings.Split(remainingMarkdown, "\n")
|
|
||||||
|
|
||||||
// questionText := ""
|
|
||||||
// answers := []*client.Answer{}
|
|
||||||
|
|
||||||
// for _, line := range lines {
|
|
||||||
// // Check if the line contains a tag
|
|
||||||
// if strings.Contains(line, "#") {
|
|
||||||
// // Split the line into words
|
|
||||||
// words := strings.Split(line, " ")
|
|
||||||
// for _, word := range words {
|
|
||||||
// // If the word starts with '#', add it to the tags
|
|
||||||
// if strings.HasPrefix(word, "#") {
|
|
||||||
// meta.Tags = append(meta.Tags, &models.Tag{Name: word[1:]})
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// if strings.HasPrefix(line, "*") {
|
|
||||||
// answerText := strings.TrimPrefix(line, "* ")
|
|
||||||
// correct := len(answers) == 0
|
|
||||||
// answer := &client.Answer{Text: answerText, Correct: correct}
|
|
||||||
// answers = append(answers, answer)
|
|
||||||
// } else {
|
|
||||||
// if questionText != "" {
|
|
||||||
// questionText += "\n"
|
|
||||||
// }
|
|
||||||
// questionText += line
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// questionText = strings.TrimRight(questionText, "\n")
|
|
||||||
|
|
||||||
// if questionText == "" {
|
|
||||||
// return nil, nil, fmt.Errorf("Question text should not be empty.")
|
|
||||||
// }
|
|
||||||
|
|
||||||
// if len(answers) < 2 {
|
|
||||||
// return nil, nil, fmt.Errorf("Number of answers should be at least 2 but parsed answers are %d.", len(answers))
|
|
||||||
// }
|
|
||||||
// question := &client.Question{Text: questionText}
|
|
||||||
// quiz := &client.Quiz{Question: question, Answers: answers}
|
|
||||||
|
|
||||||
// return quiz, meta, nil
|
|
||||||
// }
|
|
||||||
|
|
||||||
func QuizFromMarkdown(markdown string) (*client.Quiz, *models.Meta, error) {
|
func QuizFromMarkdown(markdown string) (*client.Quiz, *models.Meta, error) {
|
||||||
meta, remainingMarkdown, err := parseMetaHeaderFromMarkdown(markdown)
|
meta, remainingMarkdown, err := parseMetaHeaderFromMarkdown(markdown)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -319,7 +365,7 @@ func QuizFromMarkdown(markdown string) (*client.Quiz, *models.Meta, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *FileProboCollectorStore) ReadMetaHeaderFromFile(filename string) (*models.Meta, error) {
|
func (s *FileProboCollectorStore) ReadMetaHeaderFromFile(filename string) (*models.Meta, error) {
|
||||||
data, err := ioutil.ReadFile(path.Join(s.Dir, filename))
|
data, err := ioutil.ReadFile(path.Join(s.quizzesDir, filename))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -336,7 +382,7 @@ func (s *FileProboCollectorStore) WriteMetaHeaderToFile(filename string, meta *m
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if readMeta == nil {
|
if readMeta == nil {
|
||||||
_, err := writeMetaHeader(path.Join(s.Dir, filename), meta)
|
_, err := writeMetaHeader(path.Join(s.quizzesDir, filename), meta)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -345,8 +391,22 @@ func (s *FileProboCollectorStore) WriteMetaHeaderToFile(filename string, meta *m
|
||||||
return meta, nil
|
return meta, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *FileProboCollectorStore) CreateCollection(r *client.CreateUpdateCollectionRequest) (*models.Collection, error) {
|
||||||
|
collection, err := s.memoryStore.CreateCollection(r)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = s.createOrUpdateCollectionFile(collection)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.memoryStore.ReadCollectionByID(collection.ID)
|
||||||
|
}
|
||||||
|
|
||||||
func (s *FileProboCollectorStore) removeMetaFromFile(filename string) (*models.Meta, error) {
|
func (s *FileProboCollectorStore) removeMetaFromFile(filename string) (*models.Meta, error) {
|
||||||
file, err := os.Open(path.Join(s.Dir, filename))
|
file, err := os.Open(path.Join(s.quizzesDir, filename))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -399,7 +459,7 @@ func (s *FileProboCollectorStore) removeMetaFromFile(filename string) (*models.M
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
file, err = os.Create(path.Join(s.Dir, filename))
|
file, err = os.Create(path.Join(s.quizzesDir, filename))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -419,15 +479,16 @@ func (s *FileProboCollectorStore) createOrUpdateMarkdownFile(quiz *models.Quiz)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
fn, _ := s.GetPath(quiz)
|
fn, _ := s.GetQuizPath(quiz)
|
||||||
if fn == "" {
|
if fn == "" {
|
||||||
fn = filepath.Join(s.Dir, fmt.Sprintf("quiz_%v.%s", time.Now().Unix(), "md"))
|
fn = filepath.Join(s.quizzesDir, fmt.Sprintf("quiz_%v.%s", quiz.ID, "md"))
|
||||||
}
|
}
|
||||||
|
|
||||||
file, err := os.Create(fn)
|
file, err := os.Create(fn)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
defer file.Close()
|
defer file.Close()
|
||||||
|
|
||||||
markdownWithMetaHeader, err := addMetaHeaderToMarkdown(markdown, &quiz.Meta)
|
markdownWithMetaHeader, err := addMetaHeaderToMarkdown(markdown, &quiz.Meta)
|
||||||
|
@ -440,11 +501,37 @@ func (s *FileProboCollectorStore) createOrUpdateMarkdownFile(quiz *models.Quiz)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
s.SetPath(quiz, fn)
|
s.SetQuizPath(quiz, fn)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *FileProboCollectorStore) createOrUpdateCollectionFile(collection *models.Collection) error {
|
||||||
|
json, err := json.Marshal(collection)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
fn, _ := s.GetCollectionPath(collection)
|
||||||
|
if fn == "" {
|
||||||
|
fn = filepath.Join(s.collectionsDir, fmt.Sprintf("collection_%v.%s", collection.ID, "json"))
|
||||||
|
}
|
||||||
|
|
||||||
|
file, err := os.Create(fn)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
_, err = file.Write([]byte(json))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
s.SetCollectionPath(collection, fn)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
func addMetaHeaderToMarkdown(content string, meta *models.Meta) (string, error) {
|
func addMetaHeaderToMarkdown(content string, meta *models.Meta) (string, error) {
|
||||||
var buffer bytes.Buffer
|
var buffer bytes.Buffer
|
||||||
|
|
||||||
|
|
|
@ -13,18 +13,21 @@ import (
|
||||||
"github.com/remogatto/prettytest"
|
"github.com/remogatto/prettytest"
|
||||||
)
|
)
|
||||||
|
|
||||||
type testSuite struct {
|
var testdataDir = "./testdata"
|
||||||
|
|
||||||
|
type quizTestSuite struct {
|
||||||
prettytest.Suite
|
prettytest.Suite
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRunner(t *testing.T) {
|
func TestRunner(t *testing.T) {
|
||||||
prettytest.Run(
|
prettytest.Run(
|
||||||
t,
|
t,
|
||||||
new(testSuite),
|
new(quizTestSuite),
|
||||||
|
new(collectionTestSuite),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *testSuite) TestQuizFromMarkdown() {
|
func (t *quizTestSuite) TestQuizFromMarkdown() {
|
||||||
markdown := `Question text (1).
|
markdown := `Question text (1).
|
||||||
|
|
||||||
Question text (2).
|
Question text (2).
|
||||||
|
@ -55,8 +58,8 @@ Question text with #tag1 #tag2 (3).
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *testSuite) TestReadAllQuizzes() {
|
func (t *quizTestSuite) TestReadAllQuizzes() {
|
||||||
store, err := NewFileProboCollectorStore("./testdata/quizzes")
|
store, err := NewFileProboCollectorStore("./testdata/")
|
||||||
t.True(err == nil, fmt.Sprintf("A file store should be initialized without problems but an error occurred: %v", err))
|
t.True(err == nil, fmt.Sprintf("A file store should be initialized without problems but an error occurred: %v", err))
|
||||||
|
|
||||||
if !t.Failed() {
|
if !t.Failed() {
|
||||||
|
@ -75,7 +78,7 @@ func (t *testSuite) TestReadAllQuizzes() {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *testSuite) TestMarkdownFromQuiz() {
|
func (t *quizTestSuite) TestMarkdownFromQuiz() {
|
||||||
store := memory.NewMemoryProboCollectorStore(sha256.NewDefault256Hasher(sha256.DefaultSHA256HashingFn))
|
store := memory.NewMemoryProboCollectorStore(sha256.NewDefault256Hasher(sha256.DefaultSHA256HashingFn))
|
||||||
quiz, err := store.CreateQuiz(
|
quiz, err := store.CreateQuiz(
|
||||||
&client.CreateUpdateQuizRequest{
|
&client.CreateUpdateQuizRequest{
|
||||||
|
@ -102,9 +105,8 @@ func (t *testSuite) TestMarkdownFromQuiz() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *testSuite) TestCreateQuiz() {
|
func (t *quizTestSuite) TestCreateQuiz() {
|
||||||
dirname := "./testdata/quizzes"
|
store, err := NewFileProboCollectorStore(testdataDir)
|
||||||
store, err := NewFileProboCollectorStore(dirname)
|
|
||||||
|
|
||||||
t.True(err == nil, fmt.Sprintf("A file store should be initialized without problems but an error occurred: %v", err))
|
t.True(err == nil, fmt.Sprintf("A file store should be initialized without problems but an error occurred: %v", err))
|
||||||
|
|
||||||
|
@ -127,7 +129,7 @@ func (t *testSuite) TestCreateQuiz() {
|
||||||
t.Nil(err, fmt.Sprintf("An error was raised when saving the quiz on disk: %v", err))
|
t.Nil(err, fmt.Sprintf("An error was raised when saving the quiz on disk: %v", err))
|
||||||
|
|
||||||
if !t.Failed() {
|
if !t.Failed() {
|
||||||
path, err := store.GetPath(quiz)
|
path, err := store.GetQuizPath(quiz)
|
||||||
t.Nil(err, "GetPath should not raise an error.")
|
t.Nil(err, "GetPath should not raise an error.")
|
||||||
|
|
||||||
if !t.Failed() {
|
if !t.Failed() {
|
||||||
|
@ -151,9 +153,8 @@ func (t *testSuite) TestCreateQuiz() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *testSuite) TestDeleteQuiz() {
|
func (t *quizTestSuite) TestDeleteQuiz() {
|
||||||
dirname := "./testdata/quizzes"
|
store, err := NewFileProboCollectorStore(testdataDir)
|
||||||
store, err := NewFileProboCollectorStore(dirname)
|
|
||||||
t.True(err == nil, fmt.Sprintf("A file store should be initialized without problems but an error occurred: %v", err))
|
t.True(err == nil, fmt.Sprintf("A file store should be initialized without problems but an error occurred: %v", err))
|
||||||
|
|
||||||
if !t.Failed() {
|
if !t.Failed() {
|
||||||
|
@ -171,7 +172,7 @@ func (t *testSuite) TestDeleteQuiz() {
|
||||||
|
|
||||||
t.Nil(err, "The quiz to be deleted should be created without issue")
|
t.Nil(err, "The quiz to be deleted should be created without issue")
|
||||||
|
|
||||||
path, err := store.GetPath(quiz)
|
path, err := store.GetQuizPath(quiz)
|
||||||
t.True(path != "", "Quiz path should be obtained without errors")
|
t.True(path != "", "Quiz path should be obtained without errors")
|
||||||
|
|
||||||
if !t.Failed() {
|
if !t.Failed() {
|
||||||
|
@ -184,9 +185,8 @@ func (t *testSuite) TestDeleteQuiz() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *testSuite) TestUpdateQuiz() {
|
func (t *quizTestSuite) TestUpdateQuiz() {
|
||||||
dirname := "./testdata/quizzes"
|
store, err := NewFileProboCollectorStore(testdataDir)
|
||||||
store, err := NewFileProboCollectorStore(dirname)
|
|
||||||
t.True(err == nil, fmt.Sprintf("A file store should be initialized without problems but an error occurred: %v", err))
|
t.True(err == nil, fmt.Sprintf("A file store should be initialized without problems but an error occurred: %v", err))
|
||||||
|
|
||||||
if !t.Failed() {
|
if !t.Failed() {
|
||||||
|
@ -225,7 +225,7 @@ func (t *testSuite) TestUpdateQuiz() {
|
||||||
t.True(len(updatedQuiz.Tags) == 1, "Length of tags array should be 1")
|
t.True(len(updatedQuiz.Tags) == 1, "Length of tags array should be 1")
|
||||||
|
|
||||||
if !t.Failed() {
|
if !t.Failed() {
|
||||||
path, err := store.GetPath(updatedQuiz)
|
path, err := store.GetQuizPath(updatedQuiz)
|
||||||
|
|
||||||
if !t.Failed() {
|
if !t.Failed() {
|
||||||
t.Nil(err, "GetPath should not raise an error.")
|
t.Nil(err, "GetPath should not raise an error.")
|
||||||
|
@ -246,11 +246,9 @@ func (t *testSuite) TestUpdateQuiz() {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *testSuite) TestReadMetaHeaderFromFile() {
|
func (t *quizTestSuite) TestReadMetaHeaderFromFile() {
|
||||||
dirname := "./testdata/quizzes"
|
store, err := NewFileProboCollectorStore(testdataDir)
|
||||||
store, err := NewFileProboCollectorStore(dirname)
|
|
||||||
t.True(err == nil, fmt.Sprintf("A file store should be initialized without problems but an error occurred: %v", err))
|
t.True(err == nil, fmt.Sprintf("A file store should be initialized without problems but an error occurred: %v", err))
|
||||||
|
|
||||||
meta, err := store.ReadMetaHeaderFromFile("quiz_4.md")
|
meta, err := store.ReadMetaHeaderFromFile("quiz_4.md")
|
||||||
t.True(err == nil, fmt.Sprintf("An error occurred: %v", err))
|
t.True(err == nil, fmt.Sprintf("An error occurred: %v", err))
|
||||||
if !t.Failed() {
|
if !t.Failed() {
|
||||||
|
@ -259,9 +257,8 @@ func (t *testSuite) TestReadMetaHeaderFromFile() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *testSuite) TestWriteMetaHeaderToFile() {
|
func (t *quizTestSuite) TestWriteMetaHeaderToFile() {
|
||||||
dirname := "./testdata/quizzes"
|
store, err := NewFileProboCollectorStore(testdataDir)
|
||||||
store, err := NewFileProboCollectorStore(dirname)
|
|
||||||
|
|
||||||
t.True(err == nil, fmt.Sprintf("A file store should be initialized without problems but an error occurred: %v", err))
|
t.True(err == nil, fmt.Sprintf("A file store should be initialized without problems but an error occurred: %v", err))
|
||||||
|
|
||||||
|
|
4
store/file/testdata/quizzes/quiz_5.md
vendored
4
store/file/testdata/quizzes/quiz_5.md
vendored
|
@ -1,3 +1,7 @@
|
||||||
|
---
|
||||||
|
id: f7034ebc-d62d-43eb-a6cf-57f4886c3a7c
|
||||||
|
created_at: !!timestamp 2023-10-07T11:38:32.049804383+02:00
|
||||||
|
---
|
||||||
This quiz is initially without metadata.
|
This quiz is initially without metadata.
|
||||||
|
|
||||||
* Answer 1
|
* Answer 1
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package memory
|
package memory
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
@ -13,7 +14,7 @@ import (
|
||||||
|
|
||||||
type MemoryProboCollectorStore struct {
|
type MemoryProboCollectorStore struct {
|
||||||
quizzes map[string]*models.Quiz
|
quizzes map[string]*models.Quiz
|
||||||
|
collections map[string]*models.Collection
|
||||||
questionsHashes map[string]*models.Question
|
questionsHashes map[string]*models.Question
|
||||||
answersHashes map[string]*models.Answer
|
answersHashes map[string]*models.Answer
|
||||||
quizzesHashes map[string]*models.Quiz
|
quizzesHashes map[string]*models.Quiz
|
||||||
|
@ -34,6 +35,7 @@ func NewMemoryProboCollectorStore(hasher hasher.Hasher) *MemoryProboCollectorSto
|
||||||
s.quizzesHashes = make(map[string]*models.Quiz)
|
s.quizzesHashes = make(map[string]*models.Quiz)
|
||||||
|
|
||||||
s.quizzes = make(map[string]*models.Quiz)
|
s.quizzes = make(map[string]*models.Quiz)
|
||||||
|
s.collections = make(map[string]*models.Collection)
|
||||||
|
|
||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
@ -62,6 +64,18 @@ func (s *MemoryProboCollectorStore) getQuizFromID(id string) *models.Quiz {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *MemoryProboCollectorStore) getCollectionFromID(id string) *models.Collection {
|
||||||
|
s.lock.RLock()
|
||||||
|
defer s.lock.RUnlock()
|
||||||
|
|
||||||
|
collection, ok := s.collections[id]
|
||||||
|
if ok {
|
||||||
|
return collection
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s *MemoryProboCollectorStore) getQuestionFromHash(hash string) *models.Question {
|
func (s *MemoryProboCollectorStore) getQuestionFromHash(hash string) *models.Question {
|
||||||
s.lock.RLock()
|
s.lock.RLock()
|
||||||
defer s.lock.RUnlock()
|
defer s.lock.RUnlock()
|
||||||
|
@ -185,6 +199,9 @@ func (s *MemoryProboCollectorStore) parseTextForTags(text string, tags *[]*model
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *MemoryProboCollectorStore) createOrUpdateQuiz(r *client.CreateUpdateQuizRequest, id string) (*models.Quiz, bool, error) {
|
func (s *MemoryProboCollectorStore) createOrUpdateQuiz(r *client.CreateUpdateQuizRequest, id string) (*models.Quiz, bool, error) {
|
||||||
|
if r.Quiz == nil {
|
||||||
|
return nil, false, errors.New("A request was made passing a nil quiz object")
|
||||||
|
}
|
||||||
hashes := s.hasher.QuizHashes(r.Quiz)
|
hashes := s.hasher.QuizHashes(r.Quiz)
|
||||||
quizHash := hashes[len(hashes)-1]
|
quizHash := hashes[len(hashes)-1]
|
||||||
|
|
||||||
|
@ -260,3 +277,74 @@ func (s *MemoryProboCollectorStore) UpdateQuiz(r *client.CreateUpdateQuizRequest
|
||||||
func (s *MemoryProboCollectorStore) DeleteQuiz(id string) (*models.Quiz, error) {
|
func (s *MemoryProboCollectorStore) DeleteQuiz(id string) (*models.Quiz, error) {
|
||||||
return s.deleteQuiz(id)
|
return s.deleteQuiz(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *MemoryProboCollectorStore) CreateCollection(r *client.CreateUpdateCollectionRequest) (*models.Collection, error) {
|
||||||
|
q, _, err := s.createOrUpdateCollection(r, "")
|
||||||
|
return q, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *MemoryProboCollectorStore) UpdateCollection(r *client.CreateUpdateCollectionRequest, id string) (*models.Collection, bool, error) {
|
||||||
|
return s.createOrUpdateCollection(r, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *MemoryProboCollectorStore) ReadCollectionByID(id string) (*models.Collection, error) {
|
||||||
|
collection := s.getCollectionFromID(id)
|
||||||
|
if collection == nil {
|
||||||
|
return nil, fmt.Errorf("Collection ID %v not found in the store", collection.ID)
|
||||||
|
}
|
||||||
|
return collection, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *MemoryProboCollectorStore) createOrUpdateCollection(r *client.CreateUpdateCollectionRequest, id string) (*models.Collection, bool, error) {
|
||||||
|
var collection *models.Collection
|
||||||
|
|
||||||
|
if r.Collection == nil {
|
||||||
|
return nil, false, errors.New("A request was made passing a nil collection object")
|
||||||
|
}
|
||||||
|
|
||||||
|
if id != "" { // we're updating a collection
|
||||||
|
collection = s.getCollectionFromID(id)
|
||||||
|
if collection == nil { // Quiz is not present in the store
|
||||||
|
return nil, false, fmt.Errorf("Collection ID %v doesn't exist in the store!", id)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
id = uuid.New().String()
|
||||||
|
collection = new(models.Collection)
|
||||||
|
}
|
||||||
|
|
||||||
|
collection.Name = r.Collection.Name
|
||||||
|
collection.Query = r.Collection.Query
|
||||||
|
|
||||||
|
collection.IDs = s.query(collection.Query)
|
||||||
|
|
||||||
|
return s.createCollectionFromID(id, collection), true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *MemoryProboCollectorStore) query(query string) []string {
|
||||||
|
s.lock.Lock()
|
||||||
|
defer s.lock.Unlock()
|
||||||
|
|
||||||
|
result := make([]string, 0)
|
||||||
|
|
||||||
|
for id, quiz := range s.quizzes {
|
||||||
|
for _, tag := range quiz.Tags {
|
||||||
|
if query == tag.Name {
|
||||||
|
result = append(result, id)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *MemoryProboCollectorStore) createCollectionFromID(id string, collection *models.Collection) *models.Collection {
|
||||||
|
s.lock.Lock()
|
||||||
|
defer s.lock.Unlock()
|
||||||
|
|
||||||
|
collection.ID = id
|
||||||
|
|
||||||
|
s.collections[id] = collection
|
||||||
|
|
||||||
|
return collection
|
||||||
|
}
|
||||||
|
|
|
@ -139,3 +139,62 @@ func (t *testSuite) TestDeleteQuiz() {
|
||||||
_, err = store.ReadQuizByHash(quiz.Hash)
|
_, err = store.ReadQuizByHash(quiz.Hash)
|
||||||
t.True(err != nil, "Reading a non existent quiz should return an error")
|
t.True(err != nil, "Reading a non existent quiz should return an error")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (t *testSuite) TestUpdateCollection() {
|
||||||
|
store := NewMemoryProboCollectorStore(
|
||||||
|
sha256.NewDefault256Hasher(sha256.DefaultSHA256HashingFn),
|
||||||
|
)
|
||||||
|
|
||||||
|
quiz_1, _ := store.CreateQuiz(
|
||||||
|
&client.CreateUpdateQuizRequest{
|
||||||
|
Quiz: &client.Quiz{
|
||||||
|
Question: &client.Question{Text: "Question text with #tag1."},
|
||||||
|
Answers: []*client.Answer{
|
||||||
|
{Text: "Answer 1", Correct: true},
|
||||||
|
{Text: "Answer 2", Correct: false},
|
||||||
|
{Text: "Answer 3", Correct: false},
|
||||||
|
{Text: "Answer 4", Correct: false},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
quiz_2, _ := store.CreateQuiz(
|
||||||
|
&client.CreateUpdateQuizRequest{
|
||||||
|
Quiz: &client.Quiz{
|
||||||
|
Question: &client.Question{Text: "Another question text with #tag1."},
|
||||||
|
Answers: []*client.Answer{
|
||||||
|
{Text: "Answer 1", Correct: true},
|
||||||
|
{Text: "Answer 2", Correct: false},
|
||||||
|
{Text: "Answer 3", Correct: false},
|
||||||
|
{Text: "Answer 4", Correct: false},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
collection, _ := store.CreateCollection(
|
||||||
|
&client.CreateUpdateCollectionRequest{
|
||||||
|
Collection: &client.Collection{
|
||||||
|
Name: "MyCollection",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
updatedCollection, updated, err := store.UpdateCollection(
|
||||||
|
&client.CreateUpdateCollectionRequest{
|
||||||
|
Collection: &client.Collection{
|
||||||
|
Name: "MyUpdatedCollection",
|
||||||
|
Query: "#tag1",
|
||||||
|
},
|
||||||
|
}, collection.ID)
|
||||||
|
|
||||||
|
t.Nil(err, fmt.Sprintf("The update returned an error: %v", err))
|
||||||
|
|
||||||
|
if !t.Failed() {
|
||||||
|
t.True(updated)
|
||||||
|
t.Equal("MyUpdatedCollection", updatedCollection.Name)
|
||||||
|
t.True(len(updatedCollection.IDs) == 2)
|
||||||
|
if !t.Failed() {
|
||||||
|
t.Equal(quiz_1.ID, updatedCollection.IDs[0])
|
||||||
|
t.Equal(quiz_2.ID, updatedCollection.IDs[1])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -7,10 +7,14 @@ import (
|
||||||
|
|
||||||
type ProboCollectorStore interface {
|
type ProboCollectorStore interface {
|
||||||
ReadAllQuizzes() ([]*models.Quiz, error)
|
ReadAllQuizzes() ([]*models.Quiz, error)
|
||||||
|
|
||||||
ReadQuizByHash(hash string) (*models.Quiz, error)
|
ReadQuizByHash(hash string) (*models.Quiz, error)
|
||||||
|
|
||||||
CreateQuiz(r *client.CreateUpdateQuizRequest) (*models.Quiz, error)
|
CreateQuiz(r *client.CreateUpdateQuizRequest) (*models.Quiz, error)
|
||||||
UpdateQuiz(r *client.CreateUpdateQuizRequest, id string) (*models.Quiz, error)
|
UpdateQuiz(r *client.CreateUpdateQuizRequest, id string) (*models.Quiz, error)
|
||||||
DeleteQuiz(r *client.DeleteQuizRequest) (*models.Quiz, error)
|
DeleteQuiz(r *client.DeleteQuizRequest) (*models.Quiz, error)
|
||||||
|
|
||||||
|
ReadAllCollections() ([]*models.Collection, error)
|
||||||
|
ReadCollectionByID(id string) (*models.Collection, error)
|
||||||
|
CreateCollection(r *client.CreateUpdateCollectionRequest) (*models.Collection, error)
|
||||||
|
UpdateCollection(r *client.CreateUpdateCollectionRequest, id string) (*models.Collection, error)
|
||||||
|
DeleteCollection(r *client.DeleteCollectionRequest) (*models.Collection, error)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue