diff --git a/.amockrc.json b/.amockrc.json new file mode 100644 index 0000000..70073ea --- /dev/null +++ b/.amockrc.json @@ -0,0 +1,6 @@ +{ + "host": "localhost", + "port": 8000, + "dir": "examples", + "initCount": 2 +} diff --git a/.github/workflows/releaser.yml b/.github/workflows/releaser.yml index d6fe7bb..41f3b06 100644 --- a/.github/workflows/releaser.yml +++ b/.github/workflows/releaser.yml @@ -51,6 +51,7 @@ jobs: run: | git config --global user.email "${{ secrets.EMAIL }}" git config --global user.name "Matronator" + cp README.md ./npm/README.md cd ./npm && npm version from-git --no-git-tag-version && npm publish git fetch && git checkout main && git pull && git add . && git commit -am "Bump npm version" && git push origin --all env: diff --git a/.gitignore b/.gitignore index 1f7185a..f40336d 100644 --- a/.gitignore +++ b/.gitignore @@ -24,7 +24,7 @@ node_modules go.work # amock files -+.amock.json +.amockrc .amock/ dist/ diff --git a/.idea/amock.iml b/.idea/amock.iml index 999be74..f8b1f32 100644 --- a/.idea/amock.iml +++ b/.idea/amock.iml @@ -5,6 +5,7 @@ + diff --git a/README.md b/README.md index 0b9ebd7..27f97f7 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,6 @@ Amock is a simple API mock server that uses JSON files to define entities from w * [API Mock Server](#api-mock-server) * [Instalation](#instalation) * [npm (macOS, Linux, Windows)](#npm-macos-linux-windows) - * [GoBinaries (macOS, Linux)](#gobinaries-macos-linux) * [Homebrew (macOS)](#homebrew-macos) * [Manually download from releases (macOS, Linux, Windows)](#manually-download-from-releases-macos-linux-windows) * [1. Move the binary to `/usr/local/bin` or some other folder in your PATH:](#1-move-the-binary-to-usrlocalbin-or-some-other-folder-in-your-path) @@ -46,12 +45,6 @@ Amock is a simple API mock server that uses JSON files to define entities from w npm install -g amock-cli ``` -### GoBinaries (macOS, Linux) - -```bash -curl -sf https://gobinaries.com/matronator/amock | sh -``` - ### Homebrew (macOS) ```bash @@ -158,9 +151,23 @@ if( $PATH -notlike "*"+$amock_path+"*" ){ ## Usage +After installing you can simply start the server by running: + +```bash +amock +``` + +You can also optionally specify the host you want to use for the server by supplying it as the first argument like this: + +```bash +amock localhost:1234 +``` + +This will overwrite the host and port set in your config file and start the server on `localhost:1234`. + ### Configuration -First you need to create a config file. The config file is a JSON/YAML/TOML file that defines the entities that the server will mock. Valid config file names are these in order of priority (the first one found will be used): +You need to create a config file for the server to be of any use. The config file is a JSON/YAML/TOML file that defines the entities that the server will mock and some other settings. Valid config file names are these in order of priority (the first one found will be used): ```json [".amock.json", ".amockrc", ".amock.json.json", ".amock.json.yml", @@ -179,7 +186,7 @@ Here is an example of a config file: "user.json", // relative path to the entity file "post.json" ], - "dir": "path/to/entities/folder", // default is empty + "dir": "relative/path/to/entities/dir", // default is empty "initCount": 20 // default is 20 - number of entities to generate on server start } ``` @@ -194,7 +201,7 @@ AMOCK_ENTITIES='[user.json, post.json]' # default is empty AMOCK_INIT_COUNT=20 ``` -You must set either `entities` where you list individual files or `dir` where you specify a directory containing the entity files and all files in that directory will be used. +You must set either `entities` where you list individual files or `dir` where you specify a directory containing the entity files and all valid files in that directory will be used. You can set both but files from `entities` will override files with the same name found in the `dir` directory. Both `entities` and `dir` are optional but at least one must be set and paths in both are relative to the config file. @@ -255,13 +262,18 @@ This would generate an entity object `user` looking like this: When defining your entity you can use the following types and the server will generate the data for you. Data is generated once on server start if not already in store and whenever a new entity is created without specifying the property (if it's not required). -The syntax for defining a property is `"": ".:"` where `name` is the name of the property, `type` is the type of the property followed by a dot and an optional `subtype` and optionally a colon followed by `options` for the type. +The syntax for defining a property is + +`"": ".:"` + +where `name` is the name of the property, `type` is the type of the property followed by a dot and an optional `subtype` and optionally a colon followed by `options` for the type. ##### Required and nullable properties If the property name ends with `!` it is required and must be present in the request. If it ends with `?` it is nullable meaning that you can send a `null` value for it in your request. -You can check some examples of how to define entities in the [examples](/examples) folder. +> [!TIP] +> You can check some examples of how to define entities in the [examples](/examples) folder. ##### Types @@ -309,7 +321,8 @@ You can use the following types and subtypes to define your properties: "range": Random float in Range, }, "date": { - "": Date, // no subtype + "": Date, // no subtype, but you can specify the format + [string: Date format (e.g. yyyy-MM-dd)], "timestamp": Timestamp, "day": Day, "month": Month, @@ -318,8 +331,8 @@ You can use the following types and subtypes to define your properties: "future": Future, "past": Past, }, -"bool": True or false, -"enum": Pick a random item from the list provided in options, +"bool": true or false, +"enum": Pick a random item from the list provided in options, separated by commas, "id": { "": Sequential ID, // no subtype "sequence": Sequential ID, diff --git a/database.go b/database.go index 4fba4e2..6ffc09a 100644 --- a/database.go +++ b/database.go @@ -239,16 +239,41 @@ func WriteTable(table *Table, collection EntityCollection) error { return err } -func AppendTable(table *Table, entity Entity) error { +func AppendTable(table *Table, entity *Entity) error { collection, err := ReadTable(table) if err != nil { return err } - collection = append(collection, entity) + collection = append(collection, *entity) err = WriteTable(table, collection) return err } + +func RemoveById(table *Table, id string) error { + collection, err := ReadTable(table) + + Debug("Removing entity", "id", id, "table", table.Name) + + if err != nil { + return err + } + + for i, entity := range collection { + if entity["id"] == id { + collection = append(collection[:i], collection[i+1:]...) + break + } + } + + err = WriteTable(table, collection) + + if err != nil { + return err + } + + return nil +} diff --git a/examples/.amock.json b/examples/.amock.json deleted file mode 100644 index 543cf75..0000000 --- a/examples/.amock.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "host": "localhost", - "port": 8000, - "entities": [ - "user.json", - "post.json" - ], - "initCount": 1 -} diff --git a/examples/user.json b/examples/user.json index fd3ec63..193ab90 100644 --- a/examples/user.json +++ b/examples/user.json @@ -15,6 +15,5 @@ "is_active": "bool", "token": "string", "created_at": "date.timestamp", - "updated_at": "date.timestamp", - "posts[]": "post.json" + "updated_at": "date.timestamp" } diff --git a/main.go b/main.go index 3e83d07..bf6d264 100644 --- a/main.go +++ b/main.go @@ -11,6 +11,7 @@ import ( "path" "strconv" "strings" + "text/tabwriter" "github.com/ilyakaznacheev/cleanenv" "github.com/jwalton/gchalk" @@ -18,7 +19,7 @@ import ( var ConfigPaths = []string{ ".amock.json", - ".amockrc", + ".amockrc.json", ".amock.json.json", ".amock.json.yml", ".amock.json.yaml", @@ -52,6 +53,8 @@ func init() { config, _ = parseConfigFiles(ConfigPaths...) + Debug("Configuration loaded", "config", config) + if config == nil { log.Fatal("No configuration file found") } @@ -118,9 +121,20 @@ func StartServer() { fmt.Println(gchalk.Bold("Starting server at " + url)) fmt.Println("\nAvailable routes:") + writer := tabwriter.NewWriter(os.Stdout, 0, 8, 2, '\t', tabwriter.AlignRight) router := InitHandlers(config, &db) + + Debug("Initializing routes...") + Debug("Routes", "routes", Routes) + + fmt.Println("-----------------------------------------------") + for _, route := range Routes { - fmt.Println(" " + gchalk.Bold(RequestMethodColor(route.Method, false)) + "\t" + url + route.Path + "\t" + gchalk.Dim("[entity: "+gchalk.WithItalic().Bold(strings.Split(route.Path, "/")[1])+"]")) + _, err := fmt.Fprintln(writer, gchalk.Bold(RequestMethodColor(route.Method, false))+"\t"+url+route.Path+"\t"+gchalk.Dim("[entity: "+gchalk.WithItalic().Bold(strings.Split(route.Path, "/")[1])+"]")) + writer.Flush() + if err != nil { + Error("Error writing to tabwriter", "error", err) + } } fmt.Println("") @@ -173,6 +187,7 @@ func buildTablesFromConfig() { for _, entry := range dir { filename := entry.Name() table, name := getOrCreateTable(filename, path.Join(config.Dir, filename)) + Debug("Table "+gchalk.Bold(name)+" created from file "+gchalk.Bold(filename), "table", name, "file", filename) db.Tables[name] = *table } } diff --git a/post.json b/post.json new file mode 100644 index 0000000..de04812 --- /dev/null +++ b/post.json @@ -0,0 +1,6 @@ +{ + "id": "id.uuid", + "user_id": "id.sequence", + "title!": "string.word", + "content": "string.paragraph" +} diff --git a/server.go b/server.go index 4a635a8..faae28f 100644 --- a/server.go +++ b/server.go @@ -70,6 +70,22 @@ func InitHandlers(config *Config, db *Database) *httprouter.Router { Debug("PUT request received", "table", table.Name) handlePost(w, r, &table) }) + + Routes = append(Routes, Route{"DELETE", "/" + table.Name + "/:id"}) + router.DELETE("/"+table.Name+"/:id", func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { + Debug("DELETE request received", "table", table.Name) + + err := RemoveById(&table, ps.ByName("id")) + + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + + _, _ = w.Write([]byte(`{"message": "Entity removed"}`)) + }) } Debug("Handlers initialized") @@ -100,7 +116,7 @@ func handlePost(w http.ResponseWriter, r *http.Request, table *Table) { return } - err = AppendTable(newTable, *newEntity) + err = AppendTable(newTable, newEntity) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) @@ -133,7 +149,7 @@ func handlePost(w http.ResponseWriter, r *http.Request, table *Table) { backup, _ := ReadTable(newTable) for _, entity := range collection { - err = AppendTable(newTable, entity) + err = AppendTable(newTable, &entity) if err != nil { _ = WriteTable(newTable, backup) diff --git a/user.json b/user.json new file mode 100644 index 0000000..fd3ec63 --- /dev/null +++ b/user.json @@ -0,0 +1,20 @@ +{ + "id": "id.sequence", + "name!": "string.firstname", + "surname!": "string.lastname", + "age": "number.int:18-64", + "birthday": "date:yyyy-MM-dd", + "money": "number.decimal:2,0-10000", + "email": "string.email", + "password": "string.password", + "username": "string.username", + "city?": "string.city", + "street?": "string.street", + "country?": "string.country:short", + "role": "enum:admin,user,moderator", + "is_active": "bool", + "token": "string", + "created_at": "date.timestamp", + "updated_at": "date.timestamp", + "posts[]": "post.json" +}