diff --git a/.amock.json b/.amock.json index 5d5cf82..543cf75 100644 --- a/.amock.json +++ b/.amock.json @@ -2,6 +2,8 @@ "host": "localhost", "port": 8000, "entities": [ - "user.json" - ] + "user.json", + "post.json" + ], + "initCount": 1 } diff --git a/database.go b/database.go index 7071b1c..ab8a0a8 100644 --- a/database.go +++ b/database.go @@ -7,6 +7,7 @@ import ( "log" "os" "path" + "strings" "time" "github.com/jwalton/gchalk" @@ -16,13 +17,26 @@ func GenerateEntity(entity EntityJSON, table *Table) (Entity, *Table) { fields := make(Entity, len(entity)) for key, value := range entity { - fields[key], table = GenerateField(value, table) + options := FieldOptions{false, false, false} + fieldName := key + + if strings.HasSuffix(key, "!") { + options.Required = true + fieldName = strings.TrimSuffix(key, "!") + } else if strings.HasSuffix(key, "?") { + options.Nullable = true + fieldName = strings.TrimSuffix(key, "?") + } else if strings.HasSuffix(key, "[]") { + options.Children = true + fieldName = strings.TrimSuffix(key, "[]") + } + fields[fieldName], table = GenerateField(fieldName, value, table, options) } return fields, table } -func HydrateDatabase(db Database) Database { +func HydrateDatabase(db *Database) *Database { now := time.Now() Debug("Building database...") @@ -30,7 +44,7 @@ func HydrateDatabase(db Database) Database { for key, table := range db.Tables { updated := CreateTable(&table, entityJSON) - db.Tables[key] = updated + db.Tables[key] = *updated } elapsed := time.Since(now).String() @@ -39,18 +53,33 @@ func HydrateDatabase(db Database) Database { return db } -func CreateTable(table *Table, entityJSON EntityJSON) Table { +func CreateTable(table *Table, entityJSON EntityJSON) *Table { filename := table.Name + ".amock.json" dir := path.Join(DataDir, filename) + schemaDir := path.Join(SchemaDir, table.Name+".amock.schema.json") if _, err := os.Stat(dir); !errors.Is(err, os.ErrNotExist) { - table.File = dir - Debug("Table "+gchalk.Bold(table.Name)+" found at "+gchalk.Italic(dir)+" - skipping...", "table", table.Name, "file", dir) - - return *table + if _, err = os.Stat(schemaDir); !errors.Is(err, os.ErrNotExist) { + Debug("Table "+gchalk.Bold(table.Name)+" found at "+gchalk.Italic(dir)+" - skipping...", "table", table.Name, "file", dir, "schema", schemaDir) + table.File = dir + table.SchemaFile = schemaDir + + var schema []byte + schema, err = os.ReadFile(table.SchemaFile) + if err != nil { + log.Fatal(err) + } + + err = json.Unmarshal(schema, &table.Definition) + if err != nil { + log.Fatal(err) + } + + return table + } } - raw, err := os.ReadFile(table.Definition) + raw, err := os.ReadFile(table.DefinitionFile) if err != nil { log.Fatal(err) @@ -67,15 +96,18 @@ func CreateTable(table *Table, entityJSON EntityJSON) Table { entities[i], table = GenerateEntity(entityJSON, table) } + schema, _ := json.Marshal(table.Definition) + _ = os.WriteFile(schemaDir, schema, os.ModePerm) + b, _ := json.Marshal(entities) _ = os.WriteFile(dir, b, os.ModePerm) table.File = dir - Debug("Table "+gchalk.Bold(table.Name)+" created at "+gchalk.Italic(dir)+" from file "+gchalk.Bold(table.Definition), "table", table.Name, "file", dir, "schema", table.Definition) + Debug("Table "+gchalk.Bold(table.Name)+" created at "+gchalk.Italic(dir)+" from file "+gchalk.Bold(table.DefinitionFile), "table", table.Name, "file", dir, "schema", table.DefinitionFile) - return *table + return table } func GetTable(table *Table) ([]byte, error) { @@ -116,3 +148,17 @@ func WriteTable(table *Table, collection EntityCollection) error { return err } + +func AppendTable(table *Table, entity Entity) error { + collection, err := ReadTable(table) + + if err != nil { + return err + } + + collection = append(collection, entity) + + err = WriteTable(table, collection) + + return err +} diff --git a/generator.go b/generator.go index 6a9cf08..d414864 100644 --- a/generator.go +++ b/generator.go @@ -13,35 +13,37 @@ var FieldPattern = regroup.MustCompile(`(?P[a-z]+)(?P\.[a-z]+)?(? var NumberRangePattern = regroup.MustCompile(`(?P(-?[0-9]+(\.[0-9]+)?)|x)?-(?P(-?[0-9]+(\.[0-9]+)?)|x)?`) type Field struct { - Type string `regroup:"type"` - Subtype string `regroup:"subtype"` - Params string `regroup:"params"` + Type string `regroup:"type" json:"type"` + Subtype string `regroup:"subtype" json:"subtype"` + Params string `regroup:"params" json:"params"` + Required bool `json:"required"` + Nullable bool `json:"nullable"` } -func GenerateField(field string, table *Table) (any, *Table) { - f := &Field{} +type FieldOptions struct { + Required bool + Nullable bool + Children bool +} - err := FieldPattern.MatchToTarget(field, f) +func GenerateField(fieldName string, field string, table *Table, options FieldOptions) (any, *Table) { + f := *GetFieldType(field) + f.Required = options.Required + f.Nullable = options.Nullable + table.Definition[fieldName] = &f - if err != nil { - panic(err) - } + return GenerateEntityField(f, table) +} - var subtype, paramStr string +func GenerateEntityField(field Field, table *Table) (any, *Table) { + gen := GetGenerator(field.Type, field.Subtype) + var paramStr string var params []string - var gen any - t := f.Type - if len(f.Subtype) > 1 { - subtype = strings.TrimLeft(f.Subtype, ".") - } - - gen = GetGenerator(t, subtype) + if len(field.Params) > 1 { + paramStr = strings.TrimLeft(field.Params, ":") - if len(f.Params) > 1 { - paramStr = strings.TrimLeft(f.Params, ":") - - if f.Type == "number" { + if field.Type == "number" { params = strings.Split(paramStr, ",") for i, p := range params { if strings.Contains(p, "-") { @@ -57,7 +59,7 @@ func GenerateField(field string, table *Table) (any, *Table) { } else { params = strings.Split(paramStr, ",") } - } else if f.Type == "id" && subtype != "uuid" { + } else if field.Type == "id" && field.Subtype != "uuid" { params = []string{strconv.Itoa(int(table.LastAutoID))} table.LastAutoID = table.LastAutoID + 1 } @@ -73,6 +75,26 @@ func GenerateField(field string, table *Table) (any, *Table) { return reflect.ValueOf(gen).Call([]reflect.Value{})[0].Interface(), table } +func GetFieldType(field string) *Field { + f := &Field{} + + err := FieldPattern.MatchToTarget(field, f) + + if err != nil { + panic(err) + } + + var subtype string + + if len(f.Subtype) > 1 { + subtype = strings.TrimLeft(f.Subtype, ".") + } + + f.Subtype = subtype + + return f +} + type GeneratorFunc any type GeneratorMap map[string]GeneratorFunc diff --git a/logger.go b/logger.go index b8236b7..0b29e0b 100644 --- a/logger.go +++ b/logger.go @@ -57,7 +57,7 @@ func LogRequest(next http.Handler) http.Handler { log.SetPrefix("[amock]: ") remoteAddr := gchalk.Bold(r.RemoteAddr) - method := requestMethodColor(r.Method) + method := RequestMethodColor(r.Method, true) recorder := &StatusRecorder{w, http.StatusOK} next.ServeHTTP(recorder, r) @@ -71,26 +71,37 @@ func LogRequest(next http.Handler) http.Handler { }) } -func requestMethodColor(m string) string { - var method string +func RequestMethodColor(m string, inverse bool) string { + method := m + + if inverse { + method = " " + m + " " + } + switch m { case http.MethodGet: - method = gchalk.WithBrightWhite().WithBold().BgBrightBlue(" " + m + " ") + method = gchalk.WithBold().BrightBlue(method) case http.MethodPost: - method = gchalk.WithBrightWhite().WithBold().BgBrightGreen(" " + m + " ") + method = gchalk.WithBold().BrightGreen(method) case http.MethodPut: - method = gchalk.WithBrightWhite().WithBold().BgBrightYellow(" " + m + " ") + method = gchalk.WithBold().BrightYellow(method) case http.MethodPatch: - method = gchalk.WithBrightWhite().WithBold().BgBrightCyan(" " + m + " ") + method = gchalk.WithBold().BrightCyan(method) case http.MethodDelete: - method = gchalk.WithBrightWhite().WithBold().BgRed(" " + m + " ") + method = gchalk.WithBold().Red(method) case http.MethodOptions: - method = gchalk.WithBrightWhite().WithBold().BgBlue(" " + m + " ") + method = gchalk.WithBold().Blue(method) case http.MethodHead: - method = gchalk.WithBrightWhite().WithBold().BgMagenta(" " + m + " ") + method = gchalk.WithBold().Magenta(method) default: method = m } + + if inverse { + method = gchalk.BgBrightWhite(method) + method = gchalk.Inverse(method) + } + return method } diff --git a/main.go b/main.go index 7a6ed04..77fb9b5 100644 --- a/main.go +++ b/main.go @@ -29,6 +29,7 @@ var ConfigPaths = []string{ } var DataDir = path.Join(".amock", "data") +var SchemaDir = path.Join(".amock", "schema") type Database struct { Tables map[string]Table @@ -42,10 +43,12 @@ type Route struct { var Routes []Route type Table struct { - Name string - File string - Definition string - LastAutoID uint + Name string + File string + DefinitionFile string + Definition map[string]*Field + SchemaFile string + LastAutoID uint } type Config struct { @@ -53,7 +56,7 @@ type Config struct { Port int `yaml:"port" env:"PORT" env-default:"8080"` Dir string `yaml:"dir" env:"DIR"` Entities []string `yaml:"entities" env:"ENTITIES"` - InitCount int `yaml:"init_count" env:"INIT_COUNT" env-default:"20"` + InitCount int `yaml:"initCount" env:"INIT_COUNT" env-default:"20"` } type Entity map[string]any @@ -76,46 +79,17 @@ func init() { log.Fatal("No configuration file found") } - db.Tables = make(map[string]Table) - - if config.Dir != "" { - dir, err := os.ReadDir(config.Dir) + buildTablesFromConfig() + if _, err := os.Stat(DataDir); errors.Is(err, os.ErrNotExist) { + err = os.MkdirAll(DataDir, os.ModePerm) if err != nil { log.Fatal(err) } - - 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, - Definition: path.Join(config.Dir, filename), - File: filename, - LastAutoID: 1, - } - } - } } - 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, - Definition: entity, - File: path.Base(entity), - LastAutoID: 1, - } - } - } - } - - if _, err := os.Stat(DataDir); errors.Is(err, os.ErrNotExist) { - err = os.MkdirAll(DataDir, os.ModePerm) + if _, err := os.Stat(SchemaDir); errors.Is(err, os.ErrNotExist) { + err = os.MkdirAll(SchemaDir, os.ModePerm) if err != nil { log.Fatal(err) } @@ -123,7 +97,7 @@ func init() { Debug("Database created") - db = HydrateDatabase(db) + db = *HydrateDatabase(&db) } func main() { @@ -137,9 +111,9 @@ func main() { fmt.Println(gchalk.Bold("Starting server at " + url)) fmt.Println("\nAvailable routes:") - router := InitHandlers(config, db) + router := InitHandlers(config, &db) for _, route := range Routes { - fmt.Println(" " + gchalk.Italic(route.Method) + " " + url + route.Path) + fmt.Println(" " + gchalk.Bold(RequestMethodColor(route.Method, false)) + "\t" + url + route.Path + "\t" + gchalk.Dim("[entity: "+gchalk.WithItalic().Bold(strings.TrimPrefix(route.Path, "/"))+"]")) } fmt.Println("") @@ -163,3 +137,45 @@ func ParseConfigFiles(files ...string) (*Config, error) { return &cfg, nil } + +func buildTablesFromConfig() { + db.Tables = make(map[string]Table) + + if config.Dir != "" { + dir, err := os.ReadDir(config.Dir) + + if err != nil { + log.Fatal(err) + } + + 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, + } + } + } + } + + 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, + } + } + } + } +} diff --git a/post-user.http b/post-user.http index 4316feb..f81dcb9 100644 --- a/post-user.http +++ b/post-user.http @@ -1,3 +1,15 @@ +POST localhost:8000/user +Content-Type: application/json + +{ + "name": "John", + "surname": "Doe", + "city": null, + "age": 25 +} + +### + ### GET request to example server PUT localhost:8000/user 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/schema.go b/schema.go new file mode 100644 index 0000000..10d26c3 --- /dev/null +++ b/schema.go @@ -0,0 +1,83 @@ +package main + +import "strings" + +type ValidationResult struct { + Valid bool + Errors []string +} + +func ValidateField(field *Field, value any, key string, table *Table) *ValidationResult { + if value == nil { + if !field.Nullable { + return &ValidationResult{false, []string{"Field is not nullable: " + key}} + } + return &ValidationResult{true, nil} + } + + if field.Type == "enum" { + params := strings.Split(strings.TrimPrefix(field.Params, ":"), ",") + for _, param := range params { + if value == param { + return &ValidationResult{true, nil} + } + } + return &ValidationResult{false, []string{"Value doesn't match any of the enum values for field: " + key}} + } + + if field.Type == "id" && field.Subtype == "uuid" { + if len(value.(string)) == 36 { + return &ValidationResult{true, nil} + } + return &ValidationResult{false, []string{"Invalid UUID format for field: " + key}} + } else if field.Type == "id" && field.Subtype != "uuid" { + entities, err := ReadTable(table) + if err != nil { + return &ValidationResult{false, []string{err.Error()}} + } + idExists := false + for _, entity := range entities { + if entity[key] == value { + idExists = true + } + } + if !idExists { + return &ValidationResult{true, nil} + } else { + return &ValidationResult{false, []string{"Duplicate ID for field: " + key}} + } + } + + switch value.(type) { + case bool: + if field.Type == "bool" { + return &ValidationResult{true, nil} + } + return &ValidationResult{false, []string{"Invalid value for field: " + key}} + case string: + if field.Type == "string" || (field.Type == "date" && field.Subtype != "timestamp") { + return &ValidationResult{true, nil} + } + return &ValidationResult{false, []string{"Invalid value for field: " + key}} + case float32: + case float64: + case int: + case int8: + case int16: + case int32: + case int64: + if field.Type == "number" { + return &ValidationResult{true, nil} + } + + if field.Type == "date" && field.Subtype == "timestamp" { + return &ValidationResult{true, nil} + } + + return &ValidationResult{false, []string{"Invalid value for field: " + key}} + default: + return &ValidationResult{false, []string{"Invalid value for field: " + key}} + } + + return &ValidationResult{true, nil} +} diff --git a/server.go b/server.go index 8b75fab..d2abd67 100644 --- a/server.go +++ b/server.go @@ -1,12 +1,20 @@ package main import ( + "encoding/json" "net/http" + "strings" "github.com/julienschmidt/httprouter" ) -func InitHandlers(config *Config, db Database) *httprouter.Router { +type HTTPResponse struct { + Success bool + Code int + Message string +} + +func InitHandlers(config *Config, db *Database) *httprouter.Router { Debug("Initializing handlers...") router := httprouter.New() @@ -27,7 +35,14 @@ func InitHandlers(config *Config, db Database) *httprouter.Router { Routes = append(Routes, Route{"POST", "/" + table.Name}) router.POST("/"+table.Name, func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { - // var body []byte + Debug("POST request received", "table", table.Name) + handlePost(w, r, table) + }) + + Routes = append(Routes, Route{"PUT", "/" + table.Name}) + router.PUT("/"+table.Name, func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { + Debug("POST request received", "table", table.Name) + handlePost(w, r, table) }) } @@ -35,3 +50,90 @@ func InitHandlers(config *Config, db Database) *httprouter.Router { return 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": + var jsonData interface{} + err := json.NewDecoder(r.Body).Decode(&jsonData) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + switch data := jsonData.(type) { + case map[string]interface{}: + Debug("JSON object received") + // handle JSON object + response, newEntity := handleJsonObject(data, &table) + if !response.Success { + http.Error(w, response.Message, response.Code) + return + } + err = json.NewEncoder(w).Encode(newEntity) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + return + case []interface{}: + // handle JSON array + // you can iterate over the array with a for loop + default: + http.Error(w, "Invalid JSON", http.StatusBadRequest) + return + } + + _, _ = w.Write([]byte("POST request received")) + default: + http.Error(w, "Invalid content type", http.StatusBadRequest) + } +} + +func handleJsonObject(data Entity, table *Table) (HTTPResponse, *Entity) { + entity := Entity{} + + // iterate over the JSON object and validate fields + for key, value := range data { + Debug("Validating field", "field", key, "value", value) + if field, ok := table.Definition[key]; ok { + validation := ValidateField(field, value, key, table) + Debug("Validation result", "valid", validation.Valid, "errors", validation.Errors) + if validation.Valid { + entity[key] = value + if field.Type == "id" && field.Subtype != "uuid" { + if uint(value.(float64)) > table.LastAutoID { + table.LastAutoID = uint(value.(float64)) + 1 + } + } + } else { + return HTTPResponse{false, http.StatusBadRequest, validation.Errors[0]}, nil + } + } else { + return HTTPResponse{false, http.StatusUnprocessableEntity, "Unknown field: " + key}, nil + } + } + + // check if all required fields are present and generate missing optional fields + for key, field := range table.Definition { + if _, ok := entity[key]; !ok { + if field.Required { + return HTTPResponse{false, http.StatusBadRequest, "Missing required field: " + key}, nil + } + + entity[key], table = GenerateEntityField(*field, table) + } + } + + err := AppendTable(table, entity) + + if err != nil { + return HTTPResponse{false, http.StatusInternalServerError, "Error creating entity"}, nil + } + + return HTTPResponse{true, http.StatusOK, "Entity created!"}, &entity +} diff --git a/user.amock.json b/user.amock.json deleted file mode 100755 index 7676b81..0000000 --- a/user.amock.json +++ /dev/null @@ -1 +0,0 @@ -[{"age":18,"birthday":"2001-10-11","city":"Atlanta","country":"CX","created_at":1622076281,"email":"marlonmohr@schimmel.net","id":1,"is_active":false,"money":"5605.76","name":"Tressie","password":"0I3_83W32B3d3IEW","role":"user","street":"8417 Expresswayshire","surname":"Schiller","token":"iDXbbt","updated_at":1418871444,"username":"Schaden7875"},{"age":38,"birthday":"1919-02-18","city":"Portland","country":"TW","created_at":778470024,"email":"murphykuhn@rippin.com","id":2,"is_active":true,"money":"4031.05","name":"Janick","password":"itY-hQ$-rO38NT2s","role":"moderator","street":"488 Unionsside","surname":"Hintz","token":"t[`_?oO","updated_at":926040532,"username":"Hayes7513"},{"age":31,"birthday":"2001-01-04","city":"Baltimore","country":"MX","created_at":698457283,"email":"melynareichel@kreiger.com","id":3,"is_active":false,"money":"124.77","name":"Thaddeus","password":"AKcY0SlBA!Tx27C2","role":"user","street":"49347 East Roadbury","surname":"Labadie","token":"My^\\p=yEqx","updated_at":969512726,"username":"Schowalter1824"},{"age":31,"birthday":"1945-11-14","city":"Washington","country":"UZ","created_at":1235242631,"email":"emmanuelvolkman@dickens.biz","id":4,"is_active":true,"money":"4003.22","name":"Raegan","password":"NMbx7c2LOi2q9$?Q","role":"moderator","street":"55210 East Avenuemouth","surname":"Dicki","token":"XZ?Ov","updated_at":141717901,"username":"Stamm8325"},{"age":26,"birthday":"2005-02-20","city":"El Paso","country":"VA","created_at":1481277657,"email":"anikawalsh@johnson.org","id":5,"is_active":false,"money":"1434.01","name":"Aileen","password":"a9yp-Ah_21a0Ky.3","role":"admin","street":"378 North Viatown","surname":"Bergstrom","token":"z[AqKl","updated_at":141409958,"username":"Muller5448"},{"age":36,"birthday":"1989-08-25","city":"Greensboro","country":"MN","created_at":1093933644,"email":"stephandubuque@altenwerth.biz","id":6,"is_active":false,"money":"9162.01","name":"Dessie","password":"iCTd2IB1UtYyX025","role":"moderator","street":"379 Ridgestown","surname":"Jacobson","token":"fb@=ImRMT","updated_at":815141221,"username":"Cummings2585"},{"age":47,"birthday":"1958-12-21","city":"Boston","country":"SD","created_at":1568006339,"email":"oletaankunding@pouros.com","id":7,"is_active":false,"money":"9047.23","name":"Libbie","password":"5JA*pd43vi?KbC8Z","role":"moderator","street":"5780 Burgschester","surname":"Quitzon","token":"\u0026p`B#","updated_at":671676411,"username":"Schroeder6619"},{"age":53,"birthday":"1914-05-14","city":"Reno","country":"IL","created_at":106975878,"email":"mariemante@dibbert.org","id":8,"is_active":true,"money":"8669.49","name":"Camden","password":"m-CvubK#V$A$2T*D","role":"admin","street":"6154 Isleshire","surname":"Hudson","token":"Qh$[","updated_at":1092380065,"username":"Bradtke7323"},{"age":40,"birthday":"1948-11-07","city":"Milwaukee","country":"HM","created_at":636173462,"email":"santosdach@reichel.com","id":9,"is_active":true,"money":"1407.16","name":"Halie","password":"Xi4c-.m0mPOnfkwy","role":"moderator","street":"4485 Prairieland","surname":"Haley","token":"QooXh*![Ko","updated_at":1290336671,"username":"Langosh5468"},{"age":56,"birthday":"1962-11-10","city":"Columbus","country":"MQ","created_at":274084423,"email":"johathanerdman@orn.com","id":10,"is_active":true,"money":"1943.24","name":"Abbigail","password":"N36_M\u0026MOLN4p4w9T","role":"user","street":"3983 Roadsside","surname":"Crist","token":"v!KHLR_","updated_at":1323939525,"username":"Hane6195"},{"age":25,"birthday":"2004-05-21","city":"Albuquerque","country":"AS","created_at":1705213824,"email":"kalebzemlak@mann.net","id":11,"is_active":true,"money":"2693.76","name":"Jerome","password":"6OCIKQ1tCVtOzn$W","role":"user","street":"6834 East Prairiefurt","surname":"Leuschke","token":"jfAA$","updated_at":1030404821,"username":"Yundt2043"},{"age":61,"birthday":"1931-09-15","city":"Reno","country":"EG","created_at":997486566,"email":"ansellueilwitz@kessler.com","id":12,"is_active":true,"money":"5326.91","name":"Chance","password":"wk7Oq@\u00262obTM@va#","role":"admin","street":"79978 New Canyonchester","surname":"Abshire","token":"sokozBp","updated_at":446232063,"username":"Fay5074"},{"age":18,"birthday":"1948-04-23","city":"Washington","country":"VC","created_at":688322847,"email":"londonjohns@fadel.io","id":13,"is_active":true,"money":"427.81","name":"Jess","password":"!L_P3Yh3M2Iad7kP","role":"moderator","street":"781 Lodgeburgh","surname":"Osinski","token":"=eqrfm[#","updated_at":1086747525,"username":"Lebsack4632"},{"age":58,"birthday":"1966-12-14","city":"Irving","country":"UG","created_at":627223874,"email":"dejahdibbert@murphy.org","id":14,"is_active":false,"money":"3340.59","name":"Lilian","password":"RTmXCs7wZriJ_!\u0026e","role":"user","street":"8867 Centersbury","surname":"Aufderhar","token":"B`kqj","updated_at":275082923,"username":"Rolfson6952"},{"age":45,"birthday":"1936-09-15","city":"Birmingham","country":"CL","created_at":202862590,"email":"bennygleichner@collins.net","id":15,"is_active":false,"money":"9217.93","name":"Anabelle","password":"GT*F!tWT07M6\u0026XDe","role":"user","street":"33861 Port Landingville","surname":"Watsica","token":"n#+d","updated_at":1001342723,"username":"Champlin8491"},{"age":63,"birthday":"1963-05-08","city":"Louisville/Jefferson","country":"BZ","created_at":1653904318,"email":"lynngoodwin@hermiston.biz","id":16,"is_active":false,"money":"8984.66","name":"Dante","password":"cC.1L@iITuad4!qF","role":"moderator","street":"83780 Islandport","surname":"Crist","token":"=+rge","updated_at":1187093884,"username":"Daniel2511"},{"age":54,"birthday":"1917-01-16","city":"Charlotte","country":"GT","created_at":815840415,"email":"ariannawunsch@bechtelar.info","id":17,"is_active":true,"money":"6259.53","name":"Eliezer","password":"6.D54Sw6!\u0026ePfl2\u0026","role":"admin","street":"523 Grovesside","surname":"Oberbrunner","token":"SCMcnJSL!K","updated_at":774848123,"username":"Fritsch7121"},{"age":43,"birthday":"1913-10-12","city":"Jacksonville","country":"KI","created_at":676213262,"email":"bridiequitzon@wehner.io","id":18,"is_active":true,"money":"3311.71","name":"Alycia","password":"oGp7jFc!d9O3q3Rx","role":"admin","street":"311 Port Squaresbury","surname":"Collier","token":"maR+D!","updated_at":1187840383,"username":"Feeney1340"},{"age":53,"birthday":"1997-11-23","city":"Lincoln","country":"TM","created_at":1038295806,"email":"josephinehamill@willms.org","id":19,"is_active":true,"money":"3119.51","name":"Rose","password":"mhX9G4y2DxTORnIu","role":"moderator","street":"398 South Dalemouth","surname":"Lind","token":"Ztn`x?xa","updated_at":506076609,"username":"Mann3970"},{"age":47,"birthday":"1939-11-05","city":"Washington","country":"VG","created_at":12205558,"email":"bonitalang@bashirian.org","id":20,"is_active":true,"money":"5045.39","name":"Osvaldo","password":"#cdh-hE-!B1A\u0026M5K","role":"user","street":"7505 Inlettown","surname":"Stehr","token":"L!pzSFaz","updated_at":114064303,"username":"Waters4416"}] \ No newline at end of file diff --git a/user.json b/user.json index 329ad73..fd3ec63 100644 --- a/user.json +++ b/user.json @@ -1,19 +1,20 @@ { "id": "id.sequence", - "name": "string.firstname", - "surname": "string.lastname", + "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", + "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" + "updated_at": "date.timestamp", + "posts[]": "post.json" }