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!"} }