From 00af4a29a7bd0200385e702e012529747791d91c Mon Sep 17 00:00:00 2001
From: Matronator <5470780+matronator@users.noreply.github.com>
Date: Thu, 11 Apr 2024 01:34:56 +0200
Subject: [PATCH] Refactor and add support for host via cli args
---
.idea/active-tab-highlighter-v2.xml | 1 +
.idea/dictionaries/matronator.xml | 3 +
README.md | 4 +-
database.go | 90 +++++++++++++++---
main.go | 140 +++++++++++++++++++---------
post-user.http | 5 +
server.go | 7 +-
7 files changed, 191 insertions(+), 59 deletions(-)
create mode 100644 .idea/dictionaries/matronator.xml
diff --git a/.idea/active-tab-highlighter-v2.xml b/.idea/active-tab-highlighter-v2.xml
index 258e6b2..bdaf016 100644
--- a/.idea/active-tab-highlighter-v2.xml
+++ b/.idea/active-tab-highlighter-v2.xml
@@ -3,5 +3,6 @@
+
\ No newline at end of file
diff --git a/.idea/dictionaries/matronator.xml b/.idea/dictionaries/matronator.xml
new file mode 100644
index 0000000..5d3d5a7
--- /dev/null
+++ b/.idea/dictionaries/matronator.xml
@@ -0,0 +1,3 @@
+
+
+
\ No newline at end of file
diff --git a/README.md b/README.md
index cd4abc0..ea65dac 100644
--- a/README.md
+++ b/README.md
@@ -87,8 +87,8 @@ Alternatively you can define environment variables or an `.env` file to configur
```dotenv
AMOCK_HOST=localhost
AMOCK_PORT=8080
-AMOCK_DIR=path/to/entities# default is empty
-AMOCK_ENTITIES='[user.json, post.json]'# default is empty
+AMOCK_DIR='path/to/entities' # default is empty
+AMOCK_ENTITIES='[user.json, post.json]' # default is empty
AMOCK_INIT_COUNT=20
```
diff --git a/database.go b/database.go
index 65b932d..4fba4e2 100644
--- a/database.go
+++ b/database.go
@@ -14,6 +14,49 @@ import (
"github.com/jwalton/gchalk"
)
+type Database struct {
+ Tables map[string]Table
+}
+
+type Table struct {
+ Name string
+ File string
+ DefinitionFile string
+ Definition map[string]*Field
+ SchemaFile string
+ LastAutoID uint
+}
+
+type Entity map[string]any
+
+type EntityCollection []Entity
+
+type EntityJSON map[string]string
+
+type Filter struct {
+ Field string
+ Operator string
+ Value any
+ Apply func(EntityCollection) EntityCollection
+}
+
+type Sort struct {
+ Field string
+ Order string
+}
+
+type PaginatedItems struct {
+ First int
+ Last int
+ Prev int
+ Next int
+ Pages int
+ Count int
+ Items EntityCollection
+}
+
+type EntityIds map[string]uint
+
func GenerateEntity(entity EntityJSON, table *Table) (Entity, *Table) {
fields := make(Entity, len(entity))
@@ -111,6 +154,10 @@ func CreateTable(table *Table, entityJSON EntityJSON) *Table {
return table
}
+// func SearchTable(table *Table, filters map[string]any) (EntityCollection, error) {
+//
+// }
+
func GetTable(table *Table) ([]byte, error) {
raw, err := os.ReadFile(table.File)
@@ -121,27 +168,46 @@ func GetTable(table *Table) ([]byte, error) {
return raw, err
}
-func GetEntity(table *Table, id string) ([]byte, error) {
+func GetEntityById(table *Table, id string) ([]byte, error) {
collection, err := ReadTable(table)
-
if err != nil {
return nil, err
}
- for _, entity := range collection {
- newId, _ := strconv.ParseFloat(id, 64)
- if entity["id"] == newId {
- b, err2 := json.Marshal(entity)
- if err2 != nil {
- return nil, fmt.Errorf("could not marshal entity: %w", err2)
- }
+ var (
+ entity *Entity
+ found bool
+ )
- return b, err
- }
+ newId, err := strconv.ParseFloat(id, 64)
+ if err != nil {
+ entity, found = FindBy[string](&collection, "id", id)
+ } else {
+ entity, found = FindBy[float64](&collection, "id", newId)
+ }
+
+ if !found {
+ return nil, errors.New("entity not found, id: " + id)
}
- return nil, errors.New("entity not found")
+ b, err := json.Marshal(entity)
+ if err != nil {
+ return nil, fmt.Errorf("could not marshal entity: %w", err)
+ }
+
+ return b, err
+}
+
+func FindBy[T comparable](collection *EntityCollection, key string, search T) (*Entity, bool) {
+ for _, entity := range *collection {
+ Debug("Comparing", "key", key, "search", search, "entity", entity[key])
+ if entity[key] == search {
+ Debug("Found entity", "entity", entity)
+ return &entity, true
+ }
+ }
+ return nil, false
}
func ReadTable(table *Table) (EntityCollection, error) {
diff --git a/main.go b/main.go
index 0dbc95f..3e83d07 100644
--- a/main.go
+++ b/main.go
@@ -1,7 +1,9 @@
package main
import (
+ "encoding/json"
"errors"
+ "flag"
"fmt"
"log"
"net/http"
@@ -30,19 +32,7 @@ var ConfigPaths = []string{
var DataDir = path.Join(".amock", "data")
var SchemaDir = path.Join(".amock", "schema")
-
-type Database struct {
- Tables map[string]Table
-}
-
-type Table struct {
- Name string
- File string
- DefinitionFile string
- Definition map[string]*Field
- SchemaFile string
- LastAutoID uint
-}
+var TablesDir = path.Join(".amock", "tables")
type Config struct {
Host string `yaml:"host" env:"AMOCK_HOST" env-default:"localhost"`
@@ -52,12 +42,6 @@ type Config struct {
InitCount int `yaml:"initCount" env:"AMOCK_INIT_COUNT" env-default:"20"`
}
-type Entity map[string]any
-
-type EntityCollection []Entity
-
-type EntityJSON map[string]string
-
var config *Config
var db Database
@@ -66,12 +50,14 @@ func init() {
InitLogger()
Debug("Creating database from config...")
- config, _ = ParseConfigFiles(ConfigPaths...)
+ config, _ = parseConfigFiles(ConfigPaths...)
if config == nil {
log.Fatal("No configuration file found")
}
+ getHostFromArgs()
+
buildTablesFromConfig()
if _, err := os.Stat(DataDir); errors.Is(err, os.ErrNotExist) {
@@ -93,6 +79,35 @@ func init() {
db = *HydrateDatabase(&db)
}
+func getHostFromArgs() {
+ host := flag.Arg(0)
+ if host != "" {
+ var noPrefix string
+ var prefix string
+ if strings.Contains(host, "http://") {
+ noPrefix = strings.TrimPrefix(host, "http://")
+ prefix = "http://"
+ } else if strings.Contains(host, "https://") {
+ noPrefix = strings.TrimPrefix(host, "https://")
+ prefix = "https://"
+ } else {
+ noPrefix = host
+ }
+ if strings.Contains(noPrefix, ":") {
+ parts := strings.Split(noPrefix, ":")
+ config.Host = prefix + parts[0]
+
+ port, err := strconv.Atoi(parts[1])
+ if err != nil {
+ log.Fatal(err)
+ }
+ config.Port = port
+ } else {
+ config.Host = host
+ }
+ }
+}
+
func main() {
StartServer()
}
@@ -112,8 +127,9 @@ func StartServer() {
log.Fatal(http.ListenAndServe(config.Host+":"+strconv.Itoa(config.Port), LogRequest(router)))
}
-func ParseConfigFiles(files ...string) (*Config, error) {
+func parseConfigFiles(files ...string) (*Config, error) {
var cfg Config
+ fileRead := false
for i := 0; i < len(files); i++ {
if _, err := os.Stat(files[i]); errors.Is(err, os.ErrNotExist) {
@@ -121,8 +137,15 @@ func ParseConfigFiles(files ...string) (*Config, error) {
}
err := cleanenv.ReadConfig(files[i], &cfg)
+ if err == nil {
+ fileRead = true
+ }
+ }
+
+ if !fileRead {
+ err := cleanenv.ReadEnv(&cfg)
if err != nil {
- log.Printf("Error reading configuration from file:%v", files[i])
+ log.Printf("Error reading configuration from file or environment: %v\n", err)
return nil, err
}
}
@@ -131,6 +154,13 @@ func ParseConfigFiles(files ...string) (*Config, error) {
}
func buildTablesFromConfig() {
+ if _, err := os.Stat(TablesDir); errors.Is(err, os.ErrNotExist) {
+ err = os.MkdirAll(TablesDir, os.ModePerm)
+ if err != nil {
+ log.Fatal(err)
+ }
+ }
+
db.Tables = make(map[string]Table)
if config.Dir != "" {
@@ -142,34 +172,60 @@ func buildTablesFromConfig() {
for _, entry := range dir {
filename := entry.Name()
-
- if path.Ext(filename) == ".json" {
- name := strings.ToLower(filename[:len(filename)-5])
- db.Tables[name] = Table{
- Name: name,
- DefinitionFile: path.Join(config.Dir, filename),
- Definition: make(map[string]*Field),
- File: filename,
- LastAutoID: 1,
- }
- }
+ table, name := getOrCreateTable(filename, path.Join(config.Dir, filename))
+ db.Tables[name] = *table
}
}
if len(config.Entities) > 0 {
for _, entity := range config.Entities {
- if path.Ext(entity) == ".json" {
- name := entity[:len(entity)-5]
- db.Tables[name] = Table{
- Name: name,
- DefinitionFile: entity,
- Definition: make(map[string]*Field),
- File: path.Base(entity),
- LastAutoID: 1,
- }
+ table, name := getOrCreateTable(entity, entity)
+ db.Tables[name] = *table
+ }
+ }
+}
+
+func getOrCreateTable(filename string, definitionFile string) (*Table, string) {
+ createNew := false
+ tempTable := Table{}
+ var name string
+
+ if path.Ext(filename) == ".json" {
+ tableFilePath := path.Join(TablesDir, filename+".table")
+ name = strings.ToLower(filename[:len(filename)-5])
+
+ if _, err := os.Stat(tableFilePath); errors.Is(err, os.ErrNotExist) {
+ createNew = true
+ } else {
+ tableFile, err := os.ReadFile(tableFilePath)
+ if err != nil {
+ createNew = true
+ }
+
+ Debug("Table "+gchalk.Bold(name)+" found at "+gchalk.Italic(tableFilePath)+" - skipping...", "table", name, "file", tableFilePath)
+
+ err = json.Unmarshal(tableFile, &tempTable)
+ if err != nil {
+ createNew = true
}
}
}
+
+ if createNew {
+ tempTable = createNewTable(name, filename, definitionFile)
+ }
+
+ return &tempTable, name
+}
+
+func createNewTable(name string, filename string, definitionFile string) Table {
+ return Table{
+ Name: name,
+ DefinitionFile: definitionFile,
+ Definition: make(map[string]*Field),
+ File: filename,
+ LastAutoID: 1,
+ }
}
func constructUrl() string {
diff --git a/post-user.http b/post-user.http
index f81dcb9..aa5a79d 100644
--- a/post-user.http
+++ b/post-user.http
@@ -14,3 +14,8 @@ Content-Type: application/json
PUT localhost:8000/user
###
+
+### GET request to example server
+GET localhost:8000/user/1
+
+###
diff --git a/server.go b/server.go
index e797347..4a635a8 100644
--- a/server.go
+++ b/server.go
@@ -42,7 +42,7 @@ func InitHandlers(config *Config, db *Database) *httprouter.Router {
Routes = append(Routes, Route{"GET", "/" + table.Name + "/:id"})
router.GET("/"+table.Name+"/:id", func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
- content, err := GetEntity(&table, ps.ByName("id"))
+ content, err := GetEntityById(&table, ps.ByName("id"))
if err != nil {
if strings.Contains(err.Error(), "entity not found") {
@@ -80,7 +80,6 @@ func InitHandlers(config *Config, db *Database) *httprouter.Router {
func handlePost(w http.ResponseWriter, r *http.Request, table *Table) {
contentType := r.Header.Get("Content-Type")
Debug("Content-Type is " + contentType)
- w.Header().Set("Content-Type", "application/json")
switch strings.ToLower(contentType) {
case "application/json":
@@ -157,6 +156,8 @@ func handlePost(w http.ResponseWriter, r *http.Request, table *Table) {
default:
http.Error(w, "Invalid content type", http.StatusBadRequest)
}
+
+ w.Header().Set("Content-Type", "application/json")
}
func handleJsonObject(data Entity, table *Table) (HTTPResponse, *Entity, *Table) {
@@ -203,5 +204,5 @@ func createEntityFromData(data Entity, table *Table) (*Entity, *Table, HTTPRespo
entity[key], table = GenerateEntityField(*field, table)
}
}
- return &entity, table, HTTPResponse{}
+ return &entity, table, HTTPResponse{true, http.StatusCreated, "Entity created!"}
}