diff --git a/cmd/crowsnest-frontend/main.go b/cmd/crowsnest-frontend/main.go index c252090..f1dbef6 100644 --- a/cmd/crowsnest-frontend/main.go +++ b/cmd/crowsnest-frontend/main.go @@ -4,6 +4,7 @@ import ( "fmt" "net/http" "crowsnest-frontend/internal/model" + "crowsnest-frontend/internal/data" ) func test(w http.ResponseWriter, req *http.Request) { @@ -14,6 +15,11 @@ func test(w http.ResponseWriter, req *http.Request) { } } +func PrintAllKeys(ds data.IDatastore) { + m, err := ds.GetAllKeys() + fmt.Println(m, err) +} + func main() { http.HandleFunc("/test", test) @@ -24,7 +30,10 @@ func main() { a := &model.Article{} - fmt.Println(a.Hash()) + fds, _ := data.NewFileDatastore("data.json") + fmt.Println(fds.jetAll()) + repo := data.NewDefaultRepository[*model.Article](fds, "article") + fmt.Println(repo.Create(a)) //http.ListenAndServe(":8090", nil) } diff --git a/data.json b/data.json new file mode 100644 index 0000000..1aef73f --- /dev/null +++ b/data.json @@ -0,0 +1 @@ +{"article:0":"{\"Identifier\":0,\"SourceUrl\":\"\",\"PublishDate\":\"0001-01-01T00:00:00Z\",\"FetchDate\":\"0001-01-01T00:00:00Z\",\"Title\":\"\",\"Content\":\"\"}"} diff --git a/internal/data/FileDatastore.go b/internal/data/FileDatastore.go new file mode 100644 index 0000000..b461ad5 --- /dev/null +++ b/internal/data/FileDatastore.go @@ -0,0 +1,126 @@ +package data + +import ( + "os" + "maps" + "errors" + "encoding/json" +) + +type FileDatastore struct { + /* A very simple datastructure, implementing the IDatastore interface. It + * uses a simple text file to the data as json. */ + path string +} + + +func NewFileDatastore(path string) (*FileDatastore, error) { + /* Creates a new FileDatastore object, creating the storage file in the + * process. */ + + fds := &FileDatastore{ path: path } + + if _, err := fds.readMapObj(); err != nil { + if err := fds.writeMapObj(make(map[string]string)); err != nil { return nil, err } + } + + return fds, nil +} + +func (fds *FileDatastore) readMapObj() (map[string]string, error) { + /* Read the contents of the storage file and convert to a map object. May + * throw an error, if the file does not exit or the file content can not be + * converted. */ + + dat, err := os.ReadFile(fds.path) + if err != nil { return nil, err } + + var mapobj map[string]string + err = json.Unmarshal(dat, &mapobj) + if err != nil { return nil, err } + + return mapobj, nil +} + +func (fds *FileDatastore) writeMapObj(m map[string]string) error { + /* Write the map object to the storage file. Will overwrite the content of + * the file. May throw an error, if the file cannot be created or written + * to. */ + + file, err := os.Create(fds.path) + if err != nil { return err } + defer file.Close() + + encoder := json.NewEncoder(file) + if err := encoder.Encode(m); err != nil { return err } + + return nil +} + +// --- implement IDatastore interface --- + +func (fds *FileDatastore) Set(key string, val string) error { + /* Sets the key value pair given, overwriting if the key already exists. + * May through an error if the file cannot be opened or the contents cannot + * be decoded correctly. */ + + m, err := fds.readMapObj() + if err != nil { return err } + + m[key] = val + + err = fds.writeMapObj(m) + if err != nil { return err } + + return nil +} + +func (fds *FileDatastore) KeyExists(key string) (bool, error) { + /* Check if for the given key a entry does exit. May through an error if + * the file cannot be opened or the contents cannot be decoded correctly. */ + + m, err := fds.readMapObj() + if err != nil { return false, err } + + _, ok := m[key] + + return ok, nil +} + +func (fds *FileDatastore) Get(key string) (string, error) { + /* Gets the value for the given key. May through an error if the key does + * not exit, the file cannot be opened or the contents cannot be decoded + * correctly. */ + + m, err := fds.readMapObj() + if err != nil { return "", err } + + val, ok := m[key] + if !ok { return "", errors.New("key not found") } + + return val, nil +} + +func (fds *FileDatastore) GetAll() (map[string]string, error) { + /* Gets all the key value pairs from the file and returns them as a map + * object. May through an error if the file cannot be opened or the contents + * cannot be decoded correctly. */ + + return fds.readMapObj() +} + +func (fds *FileDatastore) GetAllKeys() (map[string]bool, error) { + /* Gets all the key the file and returns them as a map object. May through + * an error if the file cannot be opened or the contents cannot be decoded + * correctly. */ + + m, err := fds.readMapObj() + if err != nil { return nil, err } + + out := make(map[string]bool) + for key := range maps.Keys(m) { + out[key] = true + } + + return out, nil +} diff --git a/internal/data/IDatastore.go b/internal/data/IDatastore.go new file mode 100644 index 0000000..26b047f --- /dev/null +++ b/internal/data/IDatastore.go @@ -0,0 +1,9 @@ +package data + +type IDatastore interface { + Set(key string, val string) error + KeyExists(key string) (bool, error) + Get(key string) (string, error) + GetAll() (map[string]string, error) + GetAllKeys() (map[string]bool, error) +} diff --git a/internal/data/IIdentifiable.go b/internal/data/IIdentifiable.go new file mode 100644 index 0000000..c54759d --- /dev/null +++ b/internal/data/IIdentifiable.go @@ -0,0 +1,5 @@ +package data + +type IIdentifiable interface { + Id() uint +} diff --git a/internal/data/IRepository.go b/internal/data/IRepository.go new file mode 100644 index 0000000..06857b9 --- /dev/null +++ b/internal/data/IRepository.go @@ -0,0 +1,89 @@ +package data + +import ( + "encoding/json" + "errors" + "strconv" +) + + +type IRepository[T IIdentifiable] interface { + /* An interface to manage generic structure objects persistently. Should + * use an IDatastore as the interface that actually stores and retrieves the + * data from an external source. */ + Create(t T) error + Update(t T) error + Delete(t T) error + GetAll() ([]T, error) + GetById(id uint) (T, error) + GetByCriteria(c ISearchCriteria[T]) ([]T, error) +} + +// --- default implementation --- + +type DefaultRepository[T IIdentifiable] struct { + ds IDatastore + prefix string +} + +func NewDefaultRepository[T IIdentifiable](ds IDatastore, prefix string) *DefaultRepository[T] { + return &DefaultRepository[T]{ ds: ds, prefix: prefix } +} + +func (repo *DefaultRepository[T]) Create(t T) error { + /* Creates a new entry in the repository with the given object t. Throws an + * error if there already exists an entry with the same id, the json + * encoding fails or the connection to the IDatastore fails. */ + + key := repo.prefix + ":" + strconv.FormatUint(uint64(t.Id()), 10) + exists, err := repo.ds.KeyExists(key) + if err != nil { return err } + if exists { return errors.New("key already exits") } + + d, err := json.Marshal(t) + if err != nil { return err } + + err = repo.ds.Set(key, string(d)) + if err != nil { return err } + + return nil +} + +func (repo *DefaultRepository[T]) Update(t T) error { + /* Updates the entry with the same id as t in the repository with the + * values of t. Trows an error if the json encoding fails or the connection + * to the IDatastore fails. */ + + key := repo.prefix + ":" + strconv.FormatUint(uint64(t.Id()), 10) + exists, err := repo.ds.KeyExists(key) + if err != nil { return err } + if !exists { return errors.New("key does not exits") } + + d, err := json.Marshal(t) + if err != nil { return err } + + err = repo.ds.Set(key, string(d)) + if err != nil { return err } + + return nil +} + +func (repo *DefaultRepository[T]) Delete(t T) error { + // TODO + return nil +} + +func (repo *DefaultRepository[T]) GetAll() ([]T, error) { + // TODO + return nil, nil +} + +func (repo *DefaultRepository[T]) GetById(id uint) (T, error) { + // TODO + return *new(T), nil +} + +func (repo *DefaultRepository[T]) GetByCriteria(c ISearchCriteria[T]) ([]T, error) { + // TODO + return nil, nil +} diff --git a/internal/data/ISearchCriteria.go b/internal/data/ISearchCriteria.go new file mode 100644 index 0000000..194a88c --- /dev/null +++ b/internal/data/ISearchCriteria.go @@ -0,0 +1,5 @@ +package data + +type ISearchCriteria[T any] interface { + Matches(t T) bool +} diff --git a/internal/data/datastore.go b/internal/data/datastore.go deleted file mode 100644 index 1563067..0000000 --- a/internal/data/datastore.go +++ /dev/null @@ -1,8 +0,0 @@ -package data - -type Datastore interface { - Set(key string) error - Get(key string) string, error - GetAll(rowkey string) map[string]string, error - GetAllKeys() map[string]bool, error -} diff --git a/internal/model/article.go b/internal/model/article.go index d60e726..d2fb790 100644 --- a/internal/model/article.go +++ b/internal/model/article.go @@ -6,15 +6,16 @@ import ( "encoding/hex" ) - type Article struct { - SourceUrl string - PublishDate time.Time - FetchDate time.Time - Title string - Content string + Identifier uint + SourceUrl string + PublishDate time.Time + FetchDate time.Time + Title string + Content string } + func (article *Article) Hash() string { /* Generates a hash based on the source url of the article. Can be used to * identify the article. */ @@ -22,3 +23,10 @@ func (article *Article) Hash() string { hash := sha256.Sum256([]byte(article.SourceUrl)) return hex.EncodeToString(hash[:]) } + + +// --- implementation of the IIdentifiable interface --- + +func (article *Article) Id() uint { + return article.Identifier +}