adding basic data storage
This commit is contained in:
@@ -4,6 +4,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"crowsnest-frontend/internal/model"
|
"crowsnest-frontend/internal/model"
|
||||||
|
"crowsnest-frontend/internal/data"
|
||||||
)
|
)
|
||||||
|
|
||||||
func test(w http.ResponseWriter, req *http.Request) {
|
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() {
|
func main() {
|
||||||
http.HandleFunc("/test", test)
|
http.HandleFunc("/test", test)
|
||||||
|
|
||||||
@@ -24,7 +30,10 @@ func main() {
|
|||||||
|
|
||||||
a := &model.Article{}
|
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)
|
//http.ListenAndServe(":8090", nil)
|
||||||
}
|
}
|
||||||
|
|||||||
1
data.json
Normal file
1
data.json
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"article:0":"{\"Identifier\":0,\"SourceUrl\":\"\",\"PublishDate\":\"0001-01-01T00:00:00Z\",\"FetchDate\":\"0001-01-01T00:00:00Z\",\"Title\":\"\",\"Content\":\"\"}"}
|
||||||
126
internal/data/FileDatastore.go
Normal file
126
internal/data/FileDatastore.go
Normal file
@@ -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
|
||||||
|
}
|
||||||
9
internal/data/IDatastore.go
Normal file
9
internal/data/IDatastore.go
Normal file
@@ -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)
|
||||||
|
}
|
||||||
5
internal/data/IIdentifiable.go
Normal file
5
internal/data/IIdentifiable.go
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
package data
|
||||||
|
|
||||||
|
type IIdentifiable interface {
|
||||||
|
Id() uint
|
||||||
|
}
|
||||||
89
internal/data/IRepository.go
Normal file
89
internal/data/IRepository.go
Normal file
@@ -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
|
||||||
|
}
|
||||||
5
internal/data/ISearchCriteria.go
Normal file
5
internal/data/ISearchCriteria.go
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
package data
|
||||||
|
|
||||||
|
type ISearchCriteria[T any] interface {
|
||||||
|
Matches(t T) bool
|
||||||
|
}
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -6,15 +6,16 @@ import (
|
|||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
type Article struct {
|
type Article struct {
|
||||||
SourceUrl string
|
Identifier uint
|
||||||
PublishDate time.Time
|
SourceUrl string
|
||||||
FetchDate time.Time
|
PublishDate time.Time
|
||||||
Title string
|
FetchDate time.Time
|
||||||
Content string
|
Title string
|
||||||
|
Content string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
func (article *Article) Hash() string {
|
func (article *Article) Hash() string {
|
||||||
/* Generates a hash based on the source url of the article. Can be used to
|
/* Generates a hash based on the source url of the article. Can be used to
|
||||||
* identify the article. */
|
* identify the article. */
|
||||||
@@ -22,3 +23,10 @@ func (article *Article) Hash() string {
|
|||||||
hash := sha256.Sum256([]byte(article.SourceUrl))
|
hash := sha256.Sum256([]byte(article.SourceUrl))
|
||||||
return hex.EncodeToString(hash[:])
|
return hex.EncodeToString(hash[:])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// --- implementation of the IIdentifiable interface ---
|
||||||
|
|
||||||
|
func (article *Article) Id() uint {
|
||||||
|
return article.Identifier
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user