From 1661c88b05fb12f751ee3ad69235a6aee16afc3e Mon Sep 17 00:00:00 2001 From: Helder Betiol <37706737+helderbetiol@users.noreply.github.com> Date: Wed, 3 Jul 2024 21:35:08 +0200 Subject: [PATCH] Add dry run and refactor controller (#486) * feat(api) update validate endpoint * feat(cli) add dry run * refactor(cli) adapt pattern * refactor(cli) packages * refactor(cli) simplify create obj * refactor(cli) pattern * fix(cli) tests * fix(cli,api) tests * fix(cli, api) minor fixes * fix(cli) improve Cognitive Complexity * refactor(cli) minor improvements * fix(cli) test * fix(api) test * fix(api) improve cc * fix(api) improve cc * fix(api) improve cc * feat(cli) add dry run flag to cmds --- API/controllers/entity.go | 22 +- API/controllers/entity_test.go | 9 +- API/models/attributes.go | 154 +++ API/models/create_object_test.go | 6 +- API/models/model.go | 4 +- .../{sanitiseEntity.go => sanitise_entity.go} | 0 API/models/schemas/group_schema.json | 3 +- API/models/validateEntity.go | 474 --------- API/models/validate_entity.go | 414 ++++++++ ...Entity_test.go => validate_entity_test.go} | 6 +- API/repository/filters.go | 10 + API/utils/util.go | 18 +- CLI/controllers/api.go | 208 +++- CLI/controllers/api_test.go | 143 +++ CLI/controllers/commandController.go | 874 ---------------- CLI/controllers/commandController_test.go | 947 ------------------ CLI/controllers/controllerUtils.go | 126 --- CLI/controllers/create.go | 378 ++----- CLI/controllers/create_test.go | 196 ++-- CLI/controllers/delete.go | 36 +- CLI/controllers/delete_test.go | 44 + CLI/controllers/draw.go | 57 ++ CLI/controllers/draw_test.go | 104 ++ CLI/controllers/get.go | 71 +- .../{initController.go => init.go} | 2 +- .../{layers_test.go => layer_test.go} | 0 CLI/controllers/link.go | 45 + CLI/controllers/link_test.go | 99 ++ CLI/controllers/ls.go | 10 + CLI/controllers/ogree3d.go | 56 ++ CLI/controllers/ogree3d_test.go | 126 ++- CLI/controllers/path.go | 137 +++ CLI/controllers/path_test.go | 38 + CLI/controllers/responseSchemaController.go | 91 -- CLI/controllers/select.go | 38 - CLI/controllers/sendSchemaController.go | 192 ---- CLI/controllers/sendSchemaController_test.go | 110 -- CLI/controllers/shell.go | 253 +++++ CLI/controllers/shell_test.go | 26 + CLI/controllers/sortController.go | 115 --- CLI/controllers/stateController.go | 76 -- CLI/controllers/template.go | 179 ++-- CLI/controllers/ui.go | 109 ++ CLI/controllers/ui_test.go | 178 ++++ CLI/controllers/user.go | 107 ++ CLI/controllers/user_test.go | 94 ++ CLI/controllers/utils.go | 64 ++ CLI/main.go | 28 +- CLI/models/attributes.go | 193 ++++ CLI/models/attributes_test.go | 45 + CLI/models/com.go | 22 - CLI/models/entity.go | 21 + CLI/models/path.go | 11 + CLI/other/man/cmds.txt | 10 +- CLI/{ => parser}/ast.go | 202 +++- CLI/{ => parser}/ast_test.go | 4 +- CLI/{ => parser}/astbool.go | 2 +- CLI/{ => parser}/astbool_test.go | 2 +- CLI/{ => parser}/astflow.go | 4 +- CLI/{ => parser}/astflow_test.go | 2 +- CLI/{ => parser}/astnum.go | 10 +- CLI/{ => parser}/astnum_test.go | 8 +- CLI/{ => parser}/aststr.go | 2 +- CLI/{ => parser}/aststr_test.go | 2 +- CLI/{ => parser}/astutil.go | 2 +- CLI/{ => parser}/astutil_test.go | 2 +- CLI/{ => parser}/lexer.go | 2 +- CLI/{ => parser}/lexer_test.go | 2 +- CLI/{ => parser}/ocli.go | 57 +- CLI/{ => parser}/ocli_test.go | 4 +- CLI/{ => parser}/parser.go | 4 +- CLI/{ => parser}/parser_test.go | 2 +- CLI/{ => parser}/repl.go | 25 +- CLI/utils/util.go | 72 +- CLI/utils/util_test.go | 48 +- CLI/views/dryrun.go | 20 + CLI/views/ls.go | 12 +- 77 files changed, 3577 insertions(+), 3692 deletions(-) create mode 100644 API/models/attributes.go rename API/models/{sanitiseEntity.go => sanitise_entity.go} (100%) delete mode 100644 API/models/validateEntity.go create mode 100644 API/models/validate_entity.go rename API/models/{validateEntity_test.go => validate_entity_test.go} (97%) create mode 100644 CLI/controllers/api_test.go delete mode 100755 CLI/controllers/commandController.go delete mode 100644 CLI/controllers/commandController_test.go delete mode 100644 CLI/controllers/controllerUtils.go create mode 100644 CLI/controllers/draw_test.go rename CLI/controllers/{initController.go => init.go} (99%) rename CLI/controllers/{layers_test.go => layer_test.go} (100%) create mode 100644 CLI/controllers/link.go create mode 100644 CLI/controllers/link_test.go create mode 100644 CLI/controllers/path.go create mode 100644 CLI/controllers/path_test.go delete mode 100644 CLI/controllers/responseSchemaController.go delete mode 100644 CLI/controllers/sendSchemaController.go delete mode 100644 CLI/controllers/sendSchemaController_test.go create mode 100644 CLI/controllers/shell.go create mode 100644 CLI/controllers/shell_test.go delete mode 100644 CLI/controllers/sortController.go delete mode 100755 CLI/controllers/stateController.go create mode 100644 CLI/controllers/ui.go create mode 100644 CLI/controllers/ui_test.go create mode 100644 CLI/controllers/user.go create mode 100644 CLI/controllers/user_test.go create mode 100644 CLI/controllers/utils.go create mode 100644 CLI/models/attributes.go create mode 100644 CLI/models/attributes_test.go delete mode 100755 CLI/models/com.go rename CLI/{ => parser}/ast.go (90%) rename CLI/{ => parser}/ast_test.go (99%) rename CLI/{ => parser}/astbool.go (99%) rename CLI/{ => parser}/astbool_test.go (99%) rename CLI/{ => parser}/astflow.go (94%) rename CLI/{ => parser}/astflow_test.go (99%) rename CLI/{ => parser}/astnum.go (89%) rename CLI/{ => parser}/astnum_test.go (93%) rename CLI/{ => parser}/aststr.go (98%) rename CLI/{ => parser}/aststr_test.go (98%) rename CLI/{ => parser}/astutil.go (99%) rename CLI/{ => parser}/astutil_test.go (99%) rename CLI/{ => parser}/lexer.go (99%) rename CLI/{ => parser}/lexer_test.go (99%) rename CLI/{ => parser}/ocli.go (66%) rename CLI/{ => parser}/ocli_test.go (97%) rename CLI/{ => parser}/parser.go (99%) rename CLI/{ => parser}/parser_test.go (99%) rename CLI/{ => parser}/repl.go (67%) create mode 100644 CLI/views/dryrun.go diff --git a/API/controllers/entity.go b/API/controllers/entity.go index e016f8b3a..942b72f9b 100644 --- a/API/controllers/entity.go +++ b/API/controllers/entity.go @@ -462,7 +462,7 @@ func HandleGenericObjects(w http.ResponseWriter, r *http.Request) { // Get objects filters := getFiltersFromQueryParams(r) req := u.FilteredReqFromQueryParams(r.URL) - entities := u.GetEntitiesByNamespace(filters.Namespace, filters.Id) + entities := u.GetEntitiesById(filters.Namespace, filters.Id) for _, entStr := range entities { // Get objects @@ -669,7 +669,7 @@ func HandleComplexFilters(w http.ResponseWriter, r *http.Request) { // Get objects filters := getFiltersFromQueryParams(r) req := u.FilteredReqFromQueryParams(r.URL) - entities := u.GetEntitiesByNamespace(filters.Namespace, filters.Id) + entities := u.GetEntitiesById(filters.Namespace, filters.Id) for _, entStr := range entities { // Get objects @@ -906,7 +906,7 @@ func GetLayerObjects(w http.ResponseWriter, r *http.Request) { // Get objects matchingObjects := []map[string]interface{}{} - entities := u.GetEntitiesByNamespace(u.Any, searchId) + entities := u.GetEntitiesById(u.Any, searchId) fmt.Println(req) fmt.Println(entities) for _, entStr := range entities { @@ -2047,7 +2047,13 @@ func ValidateEntity(w http.ResponseWriter, r *http.Request) { } if u.IsEntityHierarchical(entInt) { - if permission := models.CheckUserPermissions(user.Roles, entInt, obj["domain"].(string)); permission < models.WRITE { + domain := "" + if entInt == u.DOMAIN { + domain = obj["parentId"].(string) + obj["name"].(string) + } else if domainStr, ok := obj["domain"].(string); ok { + domain = domainStr + } + if permission := models.CheckUserPermissions(user.Roles, entInt, domain); permission < models.WRITE { w.WriteHeader(http.StatusUnauthorized) u.Respond(w, u.Message("This user"+ " does not have sufficient permissions to create"+ @@ -2058,12 +2064,10 @@ func ValidateEntity(w http.ResponseWriter, r *http.Request) { } } - uErr := models.ValidateEntity(entInt, obj) - if uErr == nil { - u.Respond(w, u.Message("This object can be created")) - return + if ok, err := models.ValidateJsonSchema(entInt, obj); !ok { + u.RespondWithError(w, err) } else { - u.RespondWithError(w, uErr) + u.Respond(w, u.Message("This object can be created")) } } diff --git a/API/controllers/entity_test.go b/API/controllers/entity_test.go index 8c68ec1dd..10699fb57 100644 --- a/API/controllers/entity_test.go +++ b/API/controllers/entity_test.go @@ -513,20 +513,21 @@ func TestValidateEntityWithoutAttributes(t *testing.T) { func TestValidateEntity(t *testing.T) { integration.CreateTestDomain(t, "temporaryDomain", "", "") integration.CreateTestPhysicalEntity(t, utils.BLDG, "tempBldg", "tempSite", true) - room := test_utils.GetEntityMap("room", "roomA", "tempSite.tempBldg", "") + room := test_utils.GetEntityMap("room", "roomA", "tempSite.tempBldg", integration.TestDBName) endpoint := test_utils.GetEndpoint("validateEntity", "rooms") tests := []struct { name string - domain string + domain any statusCode int message string }{ - {"NonExistentDomain", "invalid", http.StatusNotFound, "Domain not found: invalid"}, - {"InvalidDomain", "temporaryDomain", http.StatusBadRequest, "Object domain is not equal or child of parent's domain"}, + {"ValidRoomEntity", integration.TestDBName, http.StatusOK, "This object can be created"}, + {"NonExistentDomain", 222, http.StatusBadRequest, "JSON body doesn't validate with the expected JSON schema"}, } for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { room["domain"] = tt.domain requestBody, _ := json.Marshal(room) diff --git a/API/models/attributes.go b/API/models/attributes.go new file mode 100644 index 000000000..fb333ceb4 --- /dev/null +++ b/API/models/attributes.go @@ -0,0 +1,154 @@ +package models + +import ( + "p3/repository" + u "p3/utils" + "strings" + + "github.com/elliotchance/pie/v2" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +func validateAttributes(entity int, data, parent map[string]any) *u.Error { + attributes := data["attributes"].(map[string]any) + switch entity { + case u.CORRIDOR: + setCorridorColor(attributes) + case u.GROUP: + if err := validateGroupContent(attributes["content"].([]any), + data["parentId"].(string), parent["parent"].(string)); err != nil { + return err + } + case u.DEVICE: + var deviceSlots []string + var err *u.Error + if deviceSlots, err = slotToValidSlice(attributes); err != nil { + return err + } + // check if all requested slots are free + if err = validateDeviceSlots(deviceSlots, + data["name"].(string), data["parentId"].(string)); err != nil { + return err + } + case u.VIRTUALOBJ: + if attributes["vlinks"] != nil { + // check if all vlinks point to valid objects + if err := validateVlinks(attributes["vlinks"].([]any)); err != nil { + return err + } + } + } + return nil +} + +func validateDeviceSlots(deviceSlots []string, deviceName, deviceParentd string) *u.Error { + // check if all requested slots are free + var siblings []map[string]any + var err *u.Error + + // find siblings + idPattern := primitive.Regex{Pattern: "^" + deviceParentd + + "(." + u.NAME_REGEX + "){1}$", Options: ""} + if siblings, err = GetManyObjects(u.EntityToString(u.DEVICE), bson.M{"id": idPattern}, + u.RequestFilters{}, "", nil); err != nil { + return err + } + + for _, obj := range siblings { + if obj["name"] == deviceName { + // do not check itself + continue + } + if siblingSlots, err := slotToValidSlice(obj["attributes"].(map[string]any)); err == nil { + for _, requestedSlot := range deviceSlots { + if pie.Contains(siblingSlots, requestedSlot) { + return &u.Error{Type: u.ErrBadFormat, + Message: "Invalid slot: one or more requested slots are already in use"} + } + } + } else { + // fmt.Println(err) + } + } + return nil +} + +func validateVlinks(vlinks []any) *u.Error { + for _, vlinkId := range vlinks { + count, err := repository.CountObjectsManyEntities([]int{u.DEVICE, u.VIRTUALOBJ}, + bson.M{"id": strings.Split(vlinkId.(string), "#")[0]}) + if err != nil { + return err + } + + if count != 1 { + return &u.Error{ + Type: u.ErrBadFormat, + Message: "One or more vlink objects could not be found. Note that it must be device or virtual obj", + } + } + } + return nil +} + +func validateGroupContent(content []any, parentId, parentCategory string) *u.Error { + if len(content) <= 1 && content[0] == "" { + return &u.Error{ + Type: u.ErrBadFormat, + Message: "objects separated by a comma must be on the payload", + } + } + + // Ensure objects are all unique + if !pie.AreUnique(content) { + return &u.Error{ + Type: u.ErrBadFormat, + Message: "The group cannot have duplicate objects", + } + } + + // Ensure objects all exist + if err := checkGroupContentExists(content, parentId, parentCategory); err != nil { + return err + } + + return nil +} + +func checkGroupContentExists(content []any, parentId, parentCategory string) *u.Error { + // Get filter + filter := repository.GroupContentToOrFilter(content, parentId) + + // Get entities + var siblingsEnts []int + if parentCategory == "rack" { + // If parent is rack, retrieve devices + siblingsEnts = []int{u.DEVICE} + } else { + // If parent is room, retrieve room children + siblingsEnts = u.RoomChildren + } + + // Try to get the whole content + count, err := repository.CountObjectsManyEntities(siblingsEnts, filter) + if err != nil { + return err + } + if count != len(content) { + return &u.Error{ + Type: u.ErrBadFormat, + Message: "Some object(s) could not be found. Please check and try again", + } + } + return nil +} + +func setCorridorColor(attributes map[string]any) { + // Set the color manually based on temp. as specified by client + if attributes["temperature"] == "warm" { + attributes["color"] = "990000" + } else if attributes["temperature"] == "cold" { + attributes["color"] = "000099" + } +} diff --git a/API/models/create_object_test.go b/API/models/create_object_test.go index 90ecfc805..dcd7821db 100644 --- a/API/models/create_object_test.go +++ b/API/models/create_object_test.go @@ -154,13 +154,13 @@ func TestValidateEntityGroupParent(t *testing.T) { } err := models.ValidateEntity(u.GROUP, template) assert.NotNil(t, err) - assert.Equal(t, "Group parent should correspond to existing rack or room", err.Message) + assert.Equal(t, "JSON body doesn't validate with the expected JSON schema", err.Message) template["parentId"] = "temporarySite.temporaryBuilding.temporaryRoom" template["name"] = "groupA" err = models.ValidateEntity(u.GROUP, template) assert.NotNil(t, err) - assert.Equal(t, "All group objects must be directly under the parent (no . allowed)", err.Message) + assert.Equal(t, "JSON body doesn't validate with the expected JSON schema", err.Message) template["parentId"] = "temporarySite.temporaryBuilding.temporaryRoom" template["name"] = "groupA" @@ -225,7 +225,7 @@ func TestCreateCorridorOrGenericWithSameNameAsRackReturnsError(t *testing.T) { _, err := tt.createFunction(roomId, childName) assert.NotNil(t, err) assert.Equal(t, u.ErrBadFormat, err.Type) - assert.Equal(t, "Object name must be unique among corridors, racks and generic objects", err.Message) + assert.Equal(t, "This object ID is not unique", err.Message) }) } } diff --git a/API/models/model.go b/API/models/model.go index 53f35cf2e..2458f23c1 100644 --- a/API/models/model.go +++ b/API/models/model.go @@ -175,7 +175,7 @@ func prepareCreateEntity(entity int, t map[string]interface{}, userRoles map[str func GetHierarchyObjectById(hierarchyName string, filters u.RequestFilters, userRoles map[string]Role) (map[string]interface{}, *u.Error) { // Get possible collections for this name - rangeEntities := u.GetEntitiesByNamespace(u.PHierarchy, hierarchyName) + rangeEntities := u.GetEntitiesById(u.PHierarchy, hierarchyName) req := bson.M{"id": hierarchyName} // Search each collection @@ -460,7 +460,7 @@ func getHierarchyWithNamespace(namespace u.Namespace, userRoles map[string]Role, } // Search collections according to namespace - entities := u.GetEntitiesByNamespace(namespace, "") + entities := u.GetEntitiesById(namespace, "") for _, entityName := range entities { // Get data diff --git a/API/models/sanitiseEntity.go b/API/models/sanitise_entity.go similarity index 100% rename from API/models/sanitiseEntity.go rename to API/models/sanitise_entity.go diff --git a/API/models/schemas/group_schema.json b/API/models/schemas/group_schema.json index 298a24790..db8c31a73 100644 --- a/API/models/schemas/group_schema.json +++ b/API/models/schemas/group_schema.json @@ -10,7 +10,8 @@ "content": { "type": "array", "items": { - "type": "string" + "type": "string", + "$ref": "refs/types.json#/definitions/name" }, "minItems": 1 }, diff --git a/API/models/validateEntity.go b/API/models/validateEntity.go deleted file mode 100644 index ce089d7e0..000000000 --- a/API/models/validateEntity.go +++ /dev/null @@ -1,474 +0,0 @@ -package models - -import ( - "bytes" - "embed" - "encoding/json" - "fmt" - "io" - "p3/repository" - u "p3/utils" - "strings" - - "github.com/bmatcuk/doublestar/v4" - "github.com/elliotchance/pie/v2" - "github.com/santhosh-tekuri/jsonschema/v5" - "go.mongodb.org/mongo-driver/bson" - "go.mongodb.org/mongo-driver/bson/primitive" -) - -//go:embed schemas/*.json -//go:embed schemas/refs/*.json -var embeddfs embed.FS -var c *jsonschema.Compiler -var types map[string]any - -func init() { - // Load JSON schemas - c = jsonschema.NewCompiler() - println("Loaded json schemas for validation:") - loadJsonSchemas("") - loadJsonSchemas("refs/") - println() -} - -func loadJsonSchemas(schemaPrefix string) { - var schemaPath = "schemas/" - dir := strings.Trim(schemaPath+schemaPrefix, "/") // without trailing '/' - entries, err := embeddfs.ReadDir((dir)) - if err != nil { - println(err.Error()) - } - - for _, e := range entries { - if strings.HasSuffix(e.Name(), ".json") { - file, err := embeddfs.Open(schemaPath + schemaPrefix + e.Name()) - if err == nil { - if e.Name() == "types.json" { - // Make two copies of the reader stream - var buf bytes.Buffer - tee := io.TeeReader(file, &buf) - - print(schemaPrefix + e.Name() + " ") - c.AddResource(schemaPrefix+e.Name(), tee) - - // Read and unmarshall types.json file - typesBytes, _ := io.ReadAll(&buf) - json.Unmarshal(typesBytes, &types) - - // Remove types that do not have a "pattern" attribute - types = types["definitions"].(map[string]any) - for key, definition := range types { - if _, ok := definition.(map[string]any)["pattern"]; !ok { - delete(types, key) - } - } - } else { - print(schemaPrefix + e.Name() + " ") - c.AddResource(schemaPrefix+e.Name(), file) - } - } - } - } -} - -func validateParent(ent string, entNum int, t map[string]interface{}) (map[string]interface{}, *u.Error) { - if entNum == u.SITE { - return nil, nil - } - - //Check ParentID is valid - if t["parentId"] == nil || t["parentId"] == "" { - if entNum == u.DOMAIN || entNum == u.STRAYOBJ || entNum == u.VIRTUALOBJ { - return nil, nil - } - return nil, &u.Error{Type: u.ErrBadFormat, Message: "ParentID is not valid"} - } - - // Anyone can have a stray parent - if parent := getParent([]string{"stray_object"}, t); parent != nil { - return parent, nil - } - - // If not, search specific possibilities - switch entNum { - case u.DEVICE: - if parent := getParent([]string{"rack", "device"}, t); parent != nil { - if err := validateDeviceSlotExists(t, parent); err != nil { - return nil, err - } - delete(parent, "attributes") // only used to check slots - return parent, nil - } - - return nil, &u.Error{Type: u.ErrInvalidValue, - Message: "ParentID should correspond to existing rack or device ID"} - - case u.GROUP: - if parent := getParent([]string{"rack", "room"}, t); parent != nil { - return parent, nil - } - - return nil, &u.Error{Type: u.ErrInvalidValue, - Message: "Group parent should correspond to existing rack or room"} - - case u.VIRTUALOBJ: - if parent := getParent([]string{"device", "virtual_obj"}, t); parent != nil { - return parent, nil - } - - return nil, &u.Error{Type: u.ErrInvalidValue, - Message: "Group parent should correspond to existing device or virtual_obj"} - default: - parentStr := u.EntityToString(u.GetParentOfEntityByInt(entNum)) - if parent := getParent([]string{parentStr}, t); parent != nil { - return parent, nil - } - - return nil, &u.Error{Type: u.ErrInvalidValue, - Message: fmt.Sprintf("ParentID should correspond to existing %s ID", parentStr)} - - } -} - -func getParent(parentEntities []string, t map[string]any) map[string]any { - parent := map[string]any{"parent": ""} - req := bson.M{"id": t["parentId"].(string)} - for _, parentEnt := range parentEntities { - obj, _ := GetObject(req, parentEnt, u.RequestFilters{}, nil) - if obj != nil { - parent["parent"] = parentEnt - parent["domain"] = obj["domain"] - parent["id"] = obj["id"] - if t["category"] == "device" { - // need attributes to check slots - parent["attributes"] = obj["attributes"] - } - return parent - } - } - return nil -} - -func validateDeviceSlotExists(deviceData map[string]interface{}, parentData map[string]interface{}) *u.Error { - fmt.Println(deviceData["attributes"].(map[string]any)["slot"]) - if deviceSlots, err := slotToValidSlice(deviceData["attributes"].(map[string]any)); err == nil { - // check if requested slots exist in parent device - countFound := 0 - if templateSlug, ok := parentData["attributes"].(map[string]any)["template"].(string); ok { - template, _ := GetObject(bson.M{"slug": templateSlug}, "obj_template", u.RequestFilters{}, nil) - if ps, ok := template["slots"].(primitive.A); ok { - parentSlots := []interface{}(ps) - for _, parentSlot := range parentSlots { - if pie.Contains(deviceSlots, parentSlot.(map[string]any)["location"].(string)) { - countFound = countFound + 1 - } - } - } - } - if len(deviceSlots) != countFound { - return &u.Error{Type: u.ErrInvalidValue, - Message: "Invalid slot: parent does not have all the requested slots"} - } - } else if err != nil { - return err - } - return nil -} - -func validateJsonSchema(entity int, t map[string]interface{}) (bool, *u.Error) { - // Get JSON schema - var schemaName string - switch entity { - case u.AC, u.CABINET, u.PWRPNL: - schemaName = "base_schema.json" - case u.STRAYOBJ: - schemaName = "stray_schema.json" - default: - schemaName = u.EntityToString(entity) + "_schema.json" - } - - sch, err := c.Compile(schemaName) - if err != nil { - return false, &u.Error{Type: u.ErrInternal, Message: err.Error()} - } - - // Validate JSON Schema - if err := sch.Validate(t); err != nil { - switch v := err.(type) { - case *jsonschema.ValidationError: - fmt.Println(t) - println(v.GoString()) - // Format errors array - errSlice := []string{} - for _, schErr := range v.BasicOutput().Errors { - // Check all types - for _, definition := range types { - pattern := definition.(map[string]any)["pattern"].(string) - // If the pattern is in the error message - if strings.Contains(schErr.Error, "does not match pattern "+quote(pattern)) || strings.Contains(schErr.Error, "does not match pattern "+pattern) { - // Substitute it for the more user-friendly description - schErr.Error = "should be " + definition.(map[string]any)["descriptions"].(map[string]any)["en"].(string) - } - } - if len(schErr.Error) > 0 && !strings.Contains(schErr.Error, "doesn't validate with") { - if len(schErr.InstanceLocation) > 0 { - errSlice = append(errSlice, schErr.InstanceLocation+" "+schErr.Error) - } else { - errSlice = append(errSlice, schErr.Error) - } - } - } - return false, &u.Error{Type: u.ErrBadFormat, - Message: "JSON body doesn't validate with the expected JSON schema", - Details: errSlice} - } - return false, &u.Error{Type: u.ErrBadFormat, Message: err.Error()} - } else { - println("JSON Schema: all good, validated!") - return true, nil - } -} - -func ValidateEntity(entity int, t map[string]interface{}) *u.Error { - if shouldFillTags(entity, u.RequestFilters{}) { - t = fillTags(t) - } - - // Validate JSON Schema - if ok, err := validateJsonSchema(entity, t); !ok { - return err - } - - // Extra checks - // Check parent and domain for objects - var parent map[string]interface{} - if u.IsEntityHierarchical(entity) { - var err *u.Error - parent, err = validateParent(u.EntityToString(entity), entity, t) - if err != nil { - return err - } else if parent["id"] != nil { - t["id"] = parent["id"].(string) + - u.HN_DELIMETER + t["name"].(string) - } else { - t["id"] = t["name"].(string) - } - //Check domain - if entity != u.DOMAIN { - if !CheckDomainExists(t["domain"].(string)) { - return &u.Error{Type: u.ErrNotFound, - Message: "Domain not found: " + t["domain"].(string)} - } - if parentDomain, ok := parent["domain"].(string); ok { - if !DomainIsEqualOrChild(parentDomain, t["domain"].(string)) { - return &u.Error{Type: u.ErrBadFormat, - Message: "Object domain is not equal or child of parent's domain"} - } - } - } - } - - // Check attributes - if entity == u.RACK || entity == u.GROUP || entity == u.CORRIDOR || entity == u.GENERIC || - entity == u.DEVICE || entity == u.VIRTUALOBJ { - attributes := t["attributes"].(map[string]any) - - if pie.Contains(u.RoomChildren, entity) { - // if entity is room children, verify that the id (name) is not repeated in other children - idIsPresent, err := ObjectsHaveAttribute( - u.SliceRemove(u.RoomChildren, entity), - "id", - t["id"].(string), - ) - if err != nil { - return err - } - - if idIsPresent { - return &u.Error{ - Type: u.ErrBadFormat, - Message: "Object name must be unique among corridors, racks and generic objects", - } - } - } - - switch entity { - case u.CORRIDOR: - // Set the color manually based on temp. as specified by client - if attributes["temperature"] == "warm" { - attributes["color"] = "990000" - } else if attributes["temperature"] == "cold" { - attributes["color"] = "000099" - } - case u.GROUP: - objects := attributes["content"].([]interface{}) - if len(objects) <= 1 && objects[0] == "" { - return &u.Error{ - Type: u.ErrBadFormat, - Message: "objects separated by a comma must be on the payload", - } - } - - // Ensure objects are all unique - if !pie.AreUnique(objects) { - return &u.Error{ - Type: u.ErrBadFormat, - Message: "The group cannot have duplicate objects", - } - } - - // Ensure objects all exist - orReq := bson.A{} - for _, objectName := range objects { - if strings.Contains(objectName.(string), u.HN_DELIMETER) { - return &u.Error{ - Type: u.ErrBadFormat, - Message: "All group objects must be directly under the parent (no . allowed)", - } - } - orReq = append(orReq, bson.M{"id": t["parentId"].(string) + u.HN_DELIMETER + objectName.(string)}) - } - filter := bson.M{"$or": orReq} - - // If parent is rack, retrieve devices - if parent["parent"].(string) == "rack" { - count, err := repository.CountObjects(u.DEVICE, filter) - if err != nil { - return err - } - - if count != len(objects) { - return &u.Error{Type: u.ErrBadFormat, - Message: "Unable to verify objects in specified group" + - " please check and try again"} - } - } else if parent["parent"].(string) == "room" { - // If parent is room, retrieve room children - count, err := repository.CountObjectsManyEntities(u.RoomChildren, filter) - if err != nil { - return err - } - - if count != len(objects) { - return &u.Error{ - Type: u.ErrBadFormat, - Message: "Some object(s) could not be found. Please check and try again", - } - } - } - - // Check if Group ID is unique - entities := u.GetEntitiesByNamespace(u.Physical, t["id"].(string)) - for _, entStr := range entities { - if entStr != u.EntityToString(u.GROUP) { - // Get objects - entData, err := GetManyObjects(entStr, bson.M{"id": t["id"]}, u.RequestFilters{}, "", nil) - if err != nil { - err.Message = "Error while check id unicity at " + entStr + ":" + err.Message - return err - } - if len(entData) > 0 { - return &u.Error{Type: u.ErrBadFormat, - Message: "This group ID is not unique among " + entStr + "s"} - } - } - } - case u.DEVICE: - if deviceSlots, err := slotToValidSlice(attributes); err == nil { - // check if all requested slots are free - idPattern := primitive.Regex{Pattern: "^" + t["parentId"].(string) + - "(." + u.NAME_REGEX + "){1}$", Options: ""} // find siblings - if siblings, err := GetManyObjects(u.EntityToString(u.DEVICE), bson.M{"id": idPattern}, - u.RequestFilters{}, "", nil); err != nil { - return err - } else { - for _, obj := range siblings { - if obj["name"] != t["name"] { // do not check itself - if siblingSlots, err := slotToValidSlice(obj["attributes"].(map[string]any)); err == nil { - for _, requestedSlot := range deviceSlots { - if pie.Contains(siblingSlots, requestedSlot) { - return &u.Error{Type: u.ErrBadFormat, - Message: "Invalid slot: one or more requested slots are already in use"} - } - } - } - } - } - } - } else if err != nil { - return err - } - case u.VIRTUALOBJ: - if attributes["vlinks"] != nil { - // check if all vlinks point to valid objects - for _, vlinkId := range attributes["vlinks"].([]any) { - count, err := repository.CountObjectsManyEntities([]int{u.DEVICE, u.VIRTUALOBJ}, - bson.M{"id": strings.Split(vlinkId.(string), "#")[0]}) - if err != nil { - return err - } - - if count != 1 { - return &u.Error{ - Type: u.ErrBadFormat, - Message: "One or more vlink objects could not be found. Note that it must be device or virtual obj", - } - } - } - } - } - } else if entity == u.LAYER && !doublestar.ValidatePattern(t["applicability"].(string)) { - return &u.Error{ - Type: u.ErrBadFormat, - Message: "Layer applicability pattern is not valid", - } - } - - //Successfully validated the Object - return nil -} - -// Returns true if at least 1 objects of type "entities" have the "value" for the "attribute". -func ObjectsHaveAttribute(entities []int, attribute, value string) (bool, *u.Error) { - for _, entity := range entities { - count, err := repository.CountObjects(entity, bson.M{attribute: value}) - if err != nil { - return false, err - } - - if count > 0 { - return true, nil - } - } - - return false, nil -} - -func slotToValidSlice(attributes map[string]any) ([]string, *u.Error) { - slotAttr := attributes["slot"] - if pa, ok := slotAttr.(primitive.A); ok { - slotAttr = []interface{}(pa) - } - if arr, ok := slotAttr.([]interface{}); ok { - if len(arr) < 1 { - return []string{}, &u.Error{Type: u.ErrInvalidValue, - Message: "Invalid slot: must be a vector [] with at least one element"} - } - slotSlice := make([]string, len(arr)) - for i := range arr { - slotSlice[i] = arr[i].(string) - } - return slotSlice, nil - } else { // no slot provided (just posU is valid) - return []string{}, nil - } -} - -// Returns single-quoted string -func quote(s string) string { - s = fmt.Sprintf("%q", s) - s = strings.ReplaceAll(s, `\"`, `"`) - s = strings.ReplaceAll(s, `'`, `\'`) - return "'" + s[1:len(s)-1] + "'" -} diff --git a/API/models/validate_entity.go b/API/models/validate_entity.go new file mode 100644 index 000000000..4ec1ea5a0 --- /dev/null +++ b/API/models/validate_entity.go @@ -0,0 +1,414 @@ +package models + +import ( + "bytes" + "embed" + "encoding/json" + "fmt" + "io" + "io/fs" + "p3/repository" + u "p3/utils" + "strings" + + "github.com/bmatcuk/doublestar/v4" + "github.com/elliotchance/pie/v2" + "github.com/santhosh-tekuri/jsonschema/v5" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +//go:embed schemas/*.json +//go:embed schemas/refs/*.json +var embeddfs embed.FS +var c *jsonschema.Compiler +var schemaTypes map[string]any + +func init() { + // Load JSON schemas + c = jsonschema.NewCompiler() + loadJsonSchemas("") + loadJsonSchemas("refs/") +} + +func loadJsonSchemas(schemaPrefix string) { + var schemaPath = "schemas/" + dir := strings.Trim(schemaPath+schemaPrefix, "/") // without trailing '/' + entries, err := embeddfs.ReadDir((dir)) + if err != nil { + println(err.Error()) + } + + for _, e := range entries { + if strings.HasSuffix(e.Name(), ".json") { + file, err := embeddfs.Open(schemaPath + schemaPrefix + e.Name()) + if err != nil { + continue + } + loadJsonSchema(schemaPrefix, e.Name(), file) + } + } +} + +func loadJsonSchema(schemaPrefix, fileName string, file fs.File) { + fullFileName := schemaPrefix + fileName + if fileName == "types.json" { + // Make two copies of the reader stream + var buf bytes.Buffer + tee := io.TeeReader(file, &buf) + + c.AddResource(fullFileName, tee) + + // Read and unmarshall types.json file + typesBytes, _ := io.ReadAll(&buf) + json.Unmarshal(typesBytes, &schemaTypes) + + // Remove types that do not have a "pattern" attribute + schemaTypes = schemaTypes["definitions"].(map[string]any) + for key, definition := range schemaTypes { + if _, ok := definition.(map[string]any)["pattern"]; !ok { + delete(schemaTypes, key) + } + } + } else { + c.AddResource(fullFileName, file) + } +} + +func validateDomain(entity int, obj, parent map[string]any) *u.Error { + if entity == u.DOMAIN || !u.IsEntityHierarchical(entity) { + return nil + } + if !CheckDomainExists(obj["domain"].(string)) { + return &u.Error{Type: u.ErrNotFound, + Message: "Domain not found: " + obj["domain"].(string)} + } + if parentDomain, ok := parent["domain"].(string); ok { + if !DomainIsEqualOrChild(parentDomain, obj["domain"].(string)) { + return &u.Error{Type: u.ErrBadFormat, + Message: "Object domain is not equal or child of parent's domain"} + } + } + return nil +} + +func getParentSetId(entity int, obj map[string]any) (map[string]any, *u.Error) { + var parent map[string]interface{} + if u.IsEntityHierarchical(entity) { + var err *u.Error + parent, err = validateParent(u.EntityToString(entity), entity, obj) + if err != nil { + return parent, err + } else if parent["id"] != nil { + obj["id"] = parent["id"].(string) + + u.HN_DELIMETER + obj["name"].(string) + } else { + obj["id"] = obj["name"].(string) + } + } + return parent, nil +} + +func validateParentId(entNum int, parentId any) (bool, *u.Error) { + if entNum == u.SITE { + // never has a parent + return false, nil + } + // Check ParentID is valid + if parentId == nil || parentId == "" { + if entNum == u.DOMAIN || entNum == u.STRAYOBJ || entNum == u.VIRTUALOBJ { + // allowed to not have a parent + return false, nil + } + return false, &u.Error{Type: u.ErrBadFormat, Message: "ParentID is not valid"} + } + return true, nil +} + +func validateParent(ent string, entNum int, t map[string]interface{}) (map[string]interface{}, *u.Error) { + if hasParentId, err := validateParentId(entNum, t["parentId"]); !hasParentId { + return nil, err + } + + // Anyone can have a stray parent + if parent := getParent([]string{"stray_object"}, t); parent != nil { + return parent, nil + } + + // If not, search specific possibilities + switch entNum { + case u.DEVICE: + if parent := getParent([]string{"rack", "device"}, t); parent != nil { + if err := validateDeviceSlotExists(t, parent); err != nil { + return nil, err + } + delete(parent, "attributes") // only used to check slots + return parent, nil + } + + return nil, &u.Error{Type: u.ErrInvalidValue, + Message: "ParentID should correspond to existing rack or device ID"} + + case u.GROUP: + if parent := getParent([]string{"rack", "room"}, t); parent != nil { + return parent, nil + } + + return nil, &u.Error{Type: u.ErrInvalidValue, + Message: "Group parent should correspond to existing rack or room"} + + case u.VIRTUALOBJ: + if parent := getParent([]string{"device", "virtual_obj"}, t); parent != nil { + return parent, nil + } + + return nil, &u.Error{Type: u.ErrInvalidValue, + Message: "Group parent should correspond to existing device or virtual_obj"} + default: + parentStr := u.EntityToString(u.GetParentOfEntityByInt(entNum)) + if parent := getParent([]string{parentStr}, t); parent != nil { + return parent, nil + } + + return nil, &u.Error{Type: u.ErrInvalidValue, + Message: fmt.Sprintf("ParentID should correspond to existing %s ID", parentStr)} + } +} + +func getParent(parentEntities []string, t map[string]any) map[string]any { + parent := map[string]any{"parent": ""} + req := bson.M{"id": t["parentId"].(string)} + for _, parentEnt := range parentEntities { + obj, _ := GetObject(req, parentEnt, u.RequestFilters{}, nil) + if obj != nil { + parent["parent"] = parentEnt + parent["domain"] = obj["domain"] + parent["id"] = obj["id"] + if t["category"] == "device" { + // need attributes to check slots + parent["attributes"] = obj["attributes"] + } + return parent + } + } + return nil +} + +func validateDeviceSlotExists(deviceData map[string]interface{}, parentData map[string]interface{}) *u.Error { + // get requested slots + deviceSlots, err := slotToValidSlice(deviceData["attributes"].(map[string]any)) + if err != nil { + return err + } + + // check if requested slots exist in parent device + countFound := 0 + if templateSlug, ok := parentData["attributes"].(map[string]any)["template"].(string); ok { + // get parent slots from its template + template, _ := GetObject(bson.M{"slug": templateSlug}, "obj_template", u.RequestFilters{}, nil) + if ps, ok := template["slots"].(primitive.A); ok { + parentSlots := []interface{}(ps) + for _, parentSlot := range parentSlots { + if pie.Contains(deviceSlots, parentSlot.(map[string]any)["location"].(string)) { + countFound = countFound + 1 + } + } + } + } + + // check if all was found + if len(deviceSlots) != countFound { + return &u.Error{Type: u.ErrInvalidValue, + Message: "Invalid slot: parent does not have all the requested slots"} + } + + return nil +} + +func formatJsonSchemaErrors(errors []jsonschema.BasicError) []string { + errSlice := []string{} + for _, schErr := range errors { + // Check all json schema defined types + for _, definition := range schemaTypes { + pattern := definition.(map[string]any)["pattern"].(string) + // If the pattern is in the error message + patternErrPrefix := "does not match pattern " + if strings.Contains(schErr.Error, patternErrPrefix+quote(pattern)) || strings.Contains(schErr.Error, patternErrPrefix+pattern) { + // Substitute it for the more user-friendly description given by the schema + schErr.Error = "should be " + definition.(map[string]any)["descriptions"].(map[string]any)["en"].(string) + } + } + if len(schErr.Error) > 0 && !strings.Contains(schErr.Error, "doesn't validate with") { + if len(schErr.InstanceLocation) > 0 { + errSlice = append(errSlice, schErr.InstanceLocation+" "+schErr.Error) + } else { + errSlice = append(errSlice, schErr.Error) + } + } + } + return errSlice +} + +func ValidateJsonSchema(entity int, t map[string]interface{}) (bool, *u.Error) { + // Get JSON schema + var schemaName string + switch entity { + case u.AC, u.CABINET, u.PWRPNL: + schemaName = "base_schema.json" + case u.STRAYOBJ: + schemaName = "stray_schema.json" + default: + schemaName = u.EntityToString(entity) + "_schema.json" + } + + sch, err := c.Compile(schemaName) + if err != nil { + return false, &u.Error{Type: u.ErrInternal, Message: err.Error()} + } + + // Validate JSON Schema + if err := sch.Validate(t); err != nil { + switch v := err.(type) { + case *jsonschema.ValidationError: + fmt.Println(t) + println(v.GoString()) + // Format errors array + errSlice := formatJsonSchemaErrors(v.BasicOutput().Errors) + return false, &u.Error{Type: u.ErrBadFormat, + Message: "JSON body doesn't validate with the expected JSON schema", + Details: errSlice} + } + return false, &u.Error{Type: u.ErrBadFormat, Message: err.Error()} + } else { + println("JSON Schema: all good, validated!") + return true, nil + } +} + +func ValidateEntity(entity int, t map[string]interface{}) *u.Error { + if shouldFillTags(entity, u.RequestFilters{}) { + t = fillTags(t) + } + + // Validate JSON Schema + if ok, err := ValidateJsonSchema(entity, t); !ok { + return err + } + + // Check parent and domain for objects + var parent map[string]interface{} + parent, err := getParentSetId(entity, t) + if err != nil { + return err + } + if err := validateDomain(entity, t, parent); err != nil { + return err + } + + // Check ID unique for some entities + if err := checkIdUnique(entity, t["id"]); err != nil { + return err + } + + // Check attributes + if pie.Contains(u.EntitiesWithAttributeCheck, entity) { + if err := validateAttributes(entity, t, parent); err != nil { + return err + } + } + + // Layer extra check + if entity == u.LAYER && !doublestar.ValidatePattern(t["applicability"].(string)) { + return &u.Error{ + Type: u.ErrBadFormat, + Message: "Layer applicability pattern is not valid", + } + } + + //Successfully validated the Object + return nil +} + +// Returns true if at least 1 objects of type "entities" have the "value" for the "attribute". +func ObjectsHaveAttribute(entities []int, attribute, value string) (bool, *u.Error) { + for _, entity := range entities { + count, err := repository.CountObjects(entity, bson.M{attribute: value}) + if err != nil { + return false, err + } + + if count > 0 { + return true, nil + } + } + + return false, nil +} + +func slotToValidSlice(attributes map[string]any) ([]string, *u.Error) { + slotAttr := attributes["slot"] + if pa, ok := slotAttr.(primitive.A); ok { + slotAttr = []interface{}(pa) + } + if arr, ok := slotAttr.([]interface{}); ok { + if len(arr) < 1 { + return []string{}, &u.Error{Type: u.ErrInvalidValue, + Message: "Invalid slot: must be a vector [] with at least one element"} + } + slotSlice := make([]string, len(arr)) + for i := range arr { + slotSlice[i] = arr[i].(string) + } + return slotSlice, nil + } else { // no slot provided (just posU is valid) + return []string{}, nil + } +} + +// Returns single-quoted string +func quote(s string) string { + s = fmt.Sprintf("%q", s) + s = strings.ReplaceAll(s, `\"`, `"`) + s = strings.ReplaceAll(s, `'`, `\'`) + return "'" + s[1:len(s)-1] + "'" +} + +// ID is guaranteed to be unique for each entity by mongo +// but some entities need some extra checks +func checkIdUnique(entity int, id any) *u.Error { + // Check if Room Child ID is unique among all room children + if pie.Contains(u.RoomChildren, entity) { + if err := checkIdUniqueAmongEntities(u.SliceRemove(u.RoomChildren, entity), + id.(string)); err != nil { + return err + } + } + // Check if Group ID is unique + if entity == u.GROUP { + entities := u.GetEntitiesById(u.Physical, id.(string)) + if err := checkIdUniqueAmongEntities(u.EntitiesStrToInt(entities), + id.(string)); err != nil { + return err + } + } + return nil +} + +func checkIdUniqueAmongEntities(entities []int, id string) *u.Error { + idIsPresent, err := ObjectsHaveAttribute( + entities, + "id", + id, + ) + if err != nil { + return err + } + + if idIsPresent { + return &u.Error{ + Type: u.ErrBadFormat, + Message: "This object ID is not unique", + } + } + return nil +} diff --git a/API/models/validateEntity_test.go b/API/models/validate_entity_test.go similarity index 97% rename from API/models/validateEntity_test.go rename to API/models/validate_entity_test.go index 1198885a6..3866cf3c1 100644 --- a/API/models/validateEntity_test.go +++ b/API/models/validate_entity_test.go @@ -21,7 +21,7 @@ func TestValidateJsonSchemaExamples(t *testing.T) { t.Error(e.Error()) } json.Unmarshal(data, &obj) // only one example per schema - ok, err := validateJsonSchema(entInt, obj["examples"].([]interface{})[0].(map[string]interface{})) + ok, err := ValidateJsonSchema(entInt, obj["examples"].([]interface{})[0].(map[string]interface{})) if !ok { t.Errorf("Error validating json schema: %s", err.Message) } @@ -51,7 +51,7 @@ func TestValidateJsonSchema(t *testing.T) { } println("*** Testing " + testObjName) - ok, err := validateJsonSchema(entInt, testObj) + ok, err := ValidateJsonSchema(entInt, testObj) if !ok { t.Errorf("Error validating json schema: %s", err.Message) } @@ -123,7 +123,7 @@ func TestErrorValidateJsonSchema(t *testing.T) { t.Error("Unable to convert json test file") } - ok, err := validateJsonSchema(entInt, testObj) + ok, err := ValidateJsonSchema(entInt, testObj) if ok { t.Errorf("Validated json schema that should have these errors: %v", expectedErrors[testObjName]) } else { diff --git a/API/repository/filters.go b/API/repository/filters.go index 7a40d1606..65cf14665 100644 --- a/API/repository/filters.go +++ b/API/repository/filters.go @@ -1,6 +1,7 @@ package repository import ( + u "p3/utils" "time" "go.mongodb.org/mongo-driver/bson" @@ -30,3 +31,12 @@ func GetDateFilters(req bson.M, startDate string, endDate string) error { } return nil } + +func GroupContentToOrFilter(content []any, parentId string) primitive.M { + orReq := bson.A{} + for _, objectName := range content { + orReq = append(orReq, bson.M{"id": parentId + u.HN_DELIMETER + objectName.(string)}) + } + filter := bson.M{"$or": orReq} + return filter +} diff --git a/API/utils/util.go b/API/utils/util.go index 8180bd027..b0e5b1da5 100644 --- a/API/utils/util.go +++ b/API/utils/util.go @@ -253,6 +253,10 @@ var EntitiesWithTags = []int{ STRAYOBJ, SITE, BLDG, ROOM, RACK, DEVICE, AC, CABINET, CORRIDOR, GENERIC, PWRPNL, GROUP, VIRTUALOBJ, } +var EntitiesWithAttributeCheck = []int{ + CORRIDOR, GROUP, DEVICE, VIRTUALOBJ, +} + var RoomChildren = []int{RACK, CORRIDOR, GENERIC} func EntityHasTags(entity int) bool { @@ -312,6 +316,10 @@ func EntityToString(entity int) string { } } +func EntitiesStrToInt(entities []string) []int { + return pie.Map[string, int](entities, EntityStrToInt) +} + func EntityStrToInt(entity string) int { switch entity { case "site": @@ -362,7 +370,7 @@ func NamespaceToString(namespace Namespace) string { return ref.String() } -func GetEntitiesByNamespace(namespace Namespace, hierarchyName string) []string { +func GetEntitiesById(namespace Namespace, hierarchyId string) []string { var entNames []string switch namespace { case Organisational: @@ -386,7 +394,7 @@ func GetEntitiesByNamespace(namespace Namespace, hierarchyName string) []string case Physical, PHierarchy, Any: entities := []int{VIRTUALOBJ} - if hierarchyName == "" || hierarchyName == "**" { + if hierarchyId == "" || hierarchyId == "**" { // All entities of each namespace switch namespace { case Physical: @@ -406,11 +414,11 @@ func GetEntitiesByNamespace(namespace Namespace, hierarchyName string) []string } // Add entities according to hierarchyName possibilities - if strings.Contains(hierarchyName, ".**") { + if strings.Contains(hierarchyId, ".**") { var initialEntity int finalEntity := GROUP - switch strings.Count(hierarchyName, HN_DELIMETER) { + switch strings.Count(hierarchyId, HN_DELIMETER) { case 1, 2: initialEntity = BLDG case 3: @@ -429,7 +437,7 @@ func GetEntitiesByNamespace(namespace Namespace, hierarchyName string) []string entities = append(entities, entity) } } else { - switch strings.Count(hierarchyName, HN_DELIMETER) { + switch strings.Count(hierarchyId, HN_DELIMETER) { case 0: entities = append(entities, SITE) if namespace == Any { diff --git a/CLI/controllers/api.go b/CLI/controllers/api.go index 2c0b44a3d..2379cc5cc 100644 --- a/CLI/controllers/api.go +++ b/CLI/controllers/api.go @@ -1,8 +1,15 @@ package controllers import ( + "bytes" "cli/models" + "encoding/json" "fmt" + "io" + "net/http" + "net/url" + "strconv" + "strings" ) const ( @@ -18,13 +25,14 @@ type APIPort interface { type apiPortImpl struct{} +// Request func (api *apiPortImpl) Request(method string, endpoint string, body map[string]any, expectedStatus int) (*Response, error) { URL := State.APIURL + endpoint - httpResponse, err := models.Send(method, URL, GetKey(), body) + httpResponse, err := Send(method, URL, GetKey(), body) if err != nil { return nil, err } - response, err := ParseResponseClean(httpResponse) + response, err := ParseResponse(httpResponse) if err != nil { return nil, fmt.Errorf("on %s %s : %s", method, endpoint, err.Error()) } @@ -45,3 +53,199 @@ func (api *apiPortImpl) Request(method string, endpoint string, body map[string] } return response, nil } + +func Send(method, URL, key string, data map[string]any) (*http.Response, error) { + client := &http.Client{} + dataJSON, err := json.Marshal(data) + if err != nil { + return nil, err + } + req, err := http.NewRequest(method, URL, bytes.NewBuffer(dataJSON)) + if err != nil { + return nil, err + } + req.Header.Set("Authorization", "Bearer "+key) + return client.Do(req) +} + +// Response handling +type Response struct { + Status int + message string + Body map[string]any +} + +func ParseResponse(response *http.Response) (*Response, error) { + bodyBytes, err := io.ReadAll(response.Body) + if err != nil { + return nil, err + } + defer response.Body.Close() + responseBody := map[string]interface{}{} + message := "" + if len(bodyBytes) > 0 { + err = json.Unmarshal(bodyBytes, &responseBody) + if err != nil { + return nil, fmt.Errorf("cannot unmarshal json : \n%s", string(bodyBytes)) + } + message, _ = responseBody["message"].(string) + } + return &Response{response.StatusCode, message, responseBody}, nil +} + +// URL handling +func (controller Controller) ObjectUrl(pathStr string, depth int) (string, error) { + path, err := controller.SplitPath(pathStr) + if err != nil { + return "", err + } + useGeneric := false + + var baseUrl string + switch path.Prefix { + case models.StrayPath: + baseUrl = "/api/stray_objects" + case models.PhysicalPath: + baseUrl = "/api/hierarchy_objects" + case models.ObjectTemplatesPath: + baseUrl = "/api/obj_templates" + case models.RoomTemplatesPath: + baseUrl = "/api/room_templates" + case models.BuildingTemplatesPath: + baseUrl = "/api/bldg_templates" + case models.GroupsPath: + baseUrl = "/api/groups" + case models.TagsPath: + baseUrl = "/api/tags" + case models.LayersPath: + baseUrl = LayersURL + case models.DomainsPath: + baseUrl = "/api/domains" + case models.VirtualObjsPath: + if strings.Contains(path.ObjectID, ".Physical.") { + baseUrl = "/api/objects" + path.ObjectID = strings.Split(path.ObjectID, ".Physical.")[1] + useGeneric = true + } else { + baseUrl = "/api/virtual_objs" + } + default: + return "", fmt.Errorf("invalid object path") + } + + params := url.Values{} + if useGeneric { + params.Add("id", path.ObjectID) + if depth > 0 { + params.Add("limit", strconv.Itoa(depth)) + } + } else { + baseUrl += "/" + path.ObjectID + if depth > 0 { + baseUrl += "/all" + params.Add("limit", strconv.Itoa(depth)) + } + } + parsedUrl, _ := url.Parse(baseUrl) + parsedUrl.RawQuery = params.Encode() + return parsedUrl.String(), nil +} + +func (controller Controller) ObjectUrlGeneric(pathStr string, depth int, filters map[string]string, recursive *RecursiveParams) (string, error) { + path, err := controller.SplitPath(pathStr) + if err != nil { + return "", err + } + + if recursive != nil { + err = path.MakeRecursive(recursive.MinDepth, recursive.MaxDepth, recursive.PathEntered) + if err != nil { + return "", err + } + } + + if filters == nil { + filters = map[string]string{} + } + + isNodeLayerInVirtualPath := false + if path.Layer != nil { + path.Layer.ApplyFilters(filters) + if path.Prefix == models.VirtualObjsPath && path.Layer.Name() == "#nodes" { + isNodeLayerInVirtualPath = true + filters["filter"] = strings.Replace(filters["filter"], "category=virtual_obj", + "virtual_config.clusterId="+path.ObjectID[:len(path.ObjectID)-2], 1) + } + } + + params, err := getUrlParamsFromPath(path, isNodeLayerInVirtualPath) + if err != nil { + return "", err + } + + if depth > 0 { + params.Add("limit", strconv.Itoa(depth)) + } + + endpoint := "/api/objects" + for key, value := range filters { + if key != "filter" { + params.Set(key, value) + } else { + endpoint = "/api/objects/search" + } + } + + url, _ := url.Parse(endpoint) + url.RawQuery = params.Encode() + + return url.String(), nil +} + +func getUrlParamsFromPath(path models.Path, isNodeLayerInVirtualPath bool) (url.Values, error) { + params := url.Values{} + switch path.Prefix { + case models.StrayPath: + params.Add("namespace", "physical.stray") + params.Add("id", path.ObjectID) + case models.PhysicalPath: + params.Add("namespace", "physical.hierarchy") + params.Add("id", path.ObjectID) + case models.ObjectTemplatesPath: + params.Add("namespace", "logical.objtemplate") + params.Add("slug", path.ObjectID) + case models.RoomTemplatesPath: + params.Add("namespace", "logical.roomtemplate") + params.Add("slug", path.ObjectID) + case models.BuildingTemplatesPath: + params.Add("namespace", "logical.bldgtemplate") + params.Add("slug", path.ObjectID) + case models.TagsPath: + params.Add("namespace", "logical.tag") + params.Add("slug", path.ObjectID) + case models.LayersPath: + params.Add("namespace", "logical.layer") + params.Add("slug", path.ObjectID) + case models.GroupsPath: + params.Add("namespace", "logical") + params.Add("category", "group") + params.Add("id", path.ObjectID) + case models.DomainsPath: + params.Add("namespace", "organisational") + params.Add("id", path.ObjectID) + case models.VirtualObjsPath: + if !isNodeLayerInVirtualPath { + params.Add("category", "virtual_obj") + if path.ObjectID != "Logical."+models.VirtualObjsNode+".*" { + params.Add("id", path.ObjectID) + } + } + default: + return params, fmt.Errorf("invalid object path") + } + return params, nil +} + +func GetKey() string { + return State.APIKEY +} diff --git a/CLI/controllers/api_test.go b/CLI/controllers/api_test.go new file mode 100644 index 000000000..e82cb01b8 --- /dev/null +++ b/CLI/controllers/api_test.go @@ -0,0 +1,143 @@ +package controllers_test + +import ( + "cli/controllers" + "cli/models" + "net/url" + "strings" + "testing" + + "github.com/stretchr/testify/assert" +) + +// Tests ObjectUrlGeneric +func TestObjectUrlGenericInvalidPath(t *testing.T) { + _, err := controllers.C.ObjectUrlGeneric("/invalid/path", 0, nil, nil) + assert.NotNil(t, err) + assert.Equal(t, "invalid object path", err.Error()) +} + +func TestObjectUrlGenericWithNoFilters(t *testing.T) { + paths := []map[string]any{ + map[string]any{ + "basePath": models.StrayPath, + "objectId": "stray-object", + "endpoint": "/api/objects", + "idName": "id", + "namespace": "physical.stray", + }, + map[string]any{ + "basePath": models.PhysicalPath, + "objectId": "BASIC/A", + "endpoint": "/api/objects", + "idName": "id", + "namespace": "physical.hierarchy", + }, + map[string]any{ + "basePath": models.ObjectTemplatesPath, + "objectId": "my-template", + "endpoint": "/api/objects", + "idName": "slug", + "namespace": "logical.objtemplate", + }, + map[string]any{ + "basePath": models.RoomTemplatesPath, + "objectId": "my-room-template", + "endpoint": "/api/objects", + "idName": "slug", + "namespace": "logical.roomtemplate", + }, + map[string]any{ + "basePath": models.BuildingTemplatesPath, + "objectId": "my-building-template", + "endpoint": "/api/objects", + "idName": "slug", + "namespace": "logical.bldgtemplate", + }, + map[string]any{ + "basePath": models.GroupsPath, + "objectId": "group1", + "endpoint": "/api/objects", + "idName": "id", + "namespace": "logical", + "extraParams": map[string]any{ + "category": "group", + }, + }, + map[string]any{ + "basePath": models.TagsPath, + "objectId": "my-tag", + "endpoint": "/api/objects", + "idName": "slug", + "namespace": "logical.tag", + }, + map[string]any{ + "basePath": models.LayersPath, + "objectId": "my-layer", + "endpoint": "/api/objects", + "idName": "slug", + "namespace": "logical.layer", + }, + map[string]any{ + "basePath": models.DomainsPath, + "objectId": "domain1", + "endpoint": "/api/objects", + "idName": "id", + "namespace": "organisational", + }, + map[string]any{ + "basePath": models.DomainsPath, + "objectId": "domain1/subdomain", + "endpoint": "/api/objects", + "idName": "id", + "namespace": "organisational", + }, + } + for _, value := range paths { + resultUrl, err := controllers.C.ObjectUrlGeneric(value["basePath"].(string)+value["objectId"].(string), 0, nil, nil) + assert.Nil(t, err) + assert.NotNil(t, resultUrl) + + parsedUrl, _ := url.Parse(resultUrl) + assert.Equal(t, value["endpoint"], parsedUrl.Path) + assert.Equal(t, strings.Replace(value["objectId"].(string), "/", ".", -1), parsedUrl.Query().Get(value["idName"].(string))) + assert.Equal(t, value["namespace"], parsedUrl.Query().Get("namespace")) + + if extraParams, ok := value["extraParams"]; ok { + for k, v := range extraParams.(map[string]any) { + assert.Equal(t, v, parsedUrl.Query().Get(k)) + } + } + } +} + +func TestObjectUrlGenericWithNormalFilters(t *testing.T) { + filters := map[string]string{ + "color": "00ED00", + } + id := "BASIC/A" + resultUrl, err := controllers.C.ObjectUrlGeneric(models.PhysicalPath+id, 0, filters, nil) + assert.Nil(t, err) + assert.NotNil(t, resultUrl) + + parsedUrl, _ := url.Parse(resultUrl) + assert.Equal(t, "/api/objects", parsedUrl.Path) + assert.Equal(t, strings.Replace(id, "/", ".", -1), parsedUrl.Query().Get("id")) + assert.Equal(t, "physical.hierarchy", parsedUrl.Query().Get("namespace")) + assert.Equal(t, "00ED00", parsedUrl.Query().Get("color")) +} + +func TestObjectUrlGenericWithFilterField(t *testing.T) { + filters := map[string]string{ + "filter": "color=00ED00", + } + id := "BASIC/A" + resultUrl, err := controllers.C.ObjectUrlGeneric(models.PhysicalPath+id, 0, filters, nil) + assert.Nil(t, err) + assert.NotNil(t, resultUrl) + + parsedUrl, _ := url.Parse(resultUrl) + assert.Equal(t, "/api/objects/search", parsedUrl.Path) + assert.Equal(t, strings.Replace(id, "/", ".", -1), parsedUrl.Query().Get("id")) + assert.Equal(t, "physical.hierarchy", parsedUrl.Query().Get("namespace")) +} diff --git a/CLI/controllers/commandController.go b/CLI/controllers/commandController.go deleted file mode 100755 index ba7b07314..000000000 --- a/CLI/controllers/commandController.go +++ /dev/null @@ -1,874 +0,0 @@ -package controllers - -import ( - "cli/commands" - l "cli/logger" - "cli/models" - "cli/readline" - "cli/utils" - "cli/views" - "fmt" - "math/rand" - "net/http" - "net/url" - "os" - "os/exec" - "runtime" - "strconv" - "strings" - - "golang.org/x/exp/slices" -) - -func PWD() string { - println(State.CurrPath) - return State.CurrPath -} - -func (controller Controller) UnfoldPath(path string) ([]string, error) { - if strings.Contains(path, "*") || models.PathHasLayer(path) { - _, subpaths, err := controller.GetObjectsWildcard(path, nil, nil) - return subpaths, err - } - - if path == "_" { - return State.ClipBoard, nil - } - - return []string{path}, nil -} - -func (controller Controller) ObjectUrl(pathStr string, depth int) (string, error) { - path, err := controller.SplitPath(pathStr) - if err != nil { - return "", err - } - useGeneric := false - - var baseUrl string - switch path.Prefix { - case models.StrayPath: - baseUrl = "/api/stray_objects" - case models.PhysicalPath: - baseUrl = "/api/hierarchy_objects" - case models.ObjectTemplatesPath: - baseUrl = "/api/obj_templates" - case models.RoomTemplatesPath: - baseUrl = "/api/room_templates" - case models.BuildingTemplatesPath: - baseUrl = "/api/bldg_templates" - case models.GroupsPath: - baseUrl = "/api/groups" - case models.TagsPath: - baseUrl = "/api/tags" - case models.LayersPath: - baseUrl = LayersURL - case models.DomainsPath: - baseUrl = "/api/domains" - case models.VirtualObjsPath: - if strings.Contains(path.ObjectID, ".Physical.") { - baseUrl = "/api/objects" - path.ObjectID = strings.Split(path.ObjectID, ".Physical.")[1] - useGeneric = true - } else { - baseUrl = "/api/virtual_objs" - } - default: - return "", fmt.Errorf("invalid object path") - } - - params := url.Values{} - if useGeneric { - params.Add("id", path.ObjectID) - if depth > 0 { - params.Add("limit", strconv.Itoa(depth)) - } - } else { - baseUrl += "/" + path.ObjectID - if depth > 0 { - baseUrl += "/all" - params.Add("limit", strconv.Itoa(depth)) - } - } - parsedUrl, _ := url.Parse(baseUrl) - parsedUrl.RawQuery = params.Encode() - return parsedUrl.String(), nil -} - -func (controller Controller) ObjectUrlGeneric(pathStr string, depth int, filters map[string]string, recursive *RecursiveParams) (string, error) { - params := url.Values{} - path, err := controller.SplitPath(pathStr) - if err != nil { - return "", err - } - - if recursive != nil { - err = path.MakeRecursive(recursive.MinDepth, recursive.MaxDepth, recursive.PathEntered) - if err != nil { - return "", err - } - } - - if filters == nil { - filters = map[string]string{} - } - - isNodeLayerInVirtualPath := false - if path.Layer != nil { - path.Layer.ApplyFilters(filters) - if path.Prefix == models.VirtualObjsPath && path.Layer.Name() == "#nodes" { - isNodeLayerInVirtualPath = true - filters["filter"] = strings.Replace(filters["filter"], "category=virtual_obj", - "virtual_config.clusterId="+path.ObjectID[:len(path.ObjectID)-2], 1) - } - } - - switch path.Prefix { - case models.StrayPath: - params.Add("namespace", "physical.stray") - params.Add("id", path.ObjectID) - case models.PhysicalPath: - params.Add("namespace", "physical.hierarchy") - params.Add("id", path.ObjectID) - case models.ObjectTemplatesPath: - params.Add("namespace", "logical.objtemplate") - params.Add("slug", path.ObjectID) - case models.RoomTemplatesPath: - params.Add("namespace", "logical.roomtemplate") - params.Add("slug", path.ObjectID) - case models.BuildingTemplatesPath: - params.Add("namespace", "logical.bldgtemplate") - params.Add("slug", path.ObjectID) - case models.TagsPath: - params.Add("namespace", "logical.tag") - params.Add("slug", path.ObjectID) - case models.LayersPath: - params.Add("namespace", "logical.layer") - params.Add("slug", path.ObjectID) - case models.GroupsPath: - params.Add("namespace", "logical") - params.Add("category", "group") - params.Add("id", path.ObjectID) - case models.DomainsPath: - params.Add("namespace", "organisational") - params.Add("id", path.ObjectID) - case models.VirtualObjsPath: - if !isNodeLayerInVirtualPath { - params.Add("category", "virtual_obj") - if path.ObjectID != "Logical."+models.VirtualObjsNode+".*" { - params.Add("id", path.ObjectID) - } - } - default: - return "", fmt.Errorf("invalid object path") - } - if depth > 0 { - params.Add("limit", strconv.Itoa(depth)) - } - - endpoint := "/api/objects" - for key, value := range filters { - if key != "filter" { - params.Set(key, value) - } else { - endpoint = "/api/objects/search" - } - } - - url, _ := url.Parse(endpoint) - url.RawQuery = params.Encode() - - return url.String(), nil -} - -func (controller Controller) GetSlot(rack map[string]any, location string) (map[string]any, error) { - templateAny, ok := rack["attributes"].(map[string]any)["template"] - if !ok { - return nil, nil - } - template := templateAny.(string) - if template == "" { - return nil, nil - } - resp, err := controller.API.Request("GET", "/api/obj_templates/"+template, nil, http.StatusOK) - if err != nil { - return nil, err - } - slots, ok := resp.Body["data"].(map[string]any)["slots"] - if !ok { - return nil, nil - } - for _, slotAny := range slots.([]any) { - slot := slotAny.(map[string]any) - if slot["location"] == location { - return slot, nil - } - } - return nil, fmt.Errorf("the slot %s does not exist", location) -} - -func (controller Controller) UnsetAttribute(path string, attr string) error { - obj, err := controller.GetObject(path) - if err != nil { - return err - } - delete(obj, "id") - delete(obj, "lastUpdated") - delete(obj, "createdDate") - attributes, hasAttributes := obj["attributes"].(map[string]any) - if !hasAttributes { - return fmt.Errorf("object has no attributes") - } - if vconfigAttr, found := strings.CutPrefix(attr, VIRTUALCONFIG+"."); found { - if len(vconfigAttr) < 1 { - return fmt.Errorf("invalid attribute name") - } else if vAttrs, ok := attributes[VIRTUALCONFIG].(map[string]any); !ok { - return fmt.Errorf("object has no " + VIRTUALCONFIG) - } else { - delete(vAttrs, vconfigAttr) - } - } else { - delete(attributes, attr) - } - url, err := controller.ObjectUrl(path, 0) - if err != nil { - return err - } - _, err = controller.API.Request("PUT", url, obj, http.StatusOK) - return err -} - -// Specific update for deleting elements in an array of an obj -func (controller Controller) UnsetInObj(Path, attr string, idx int) (map[string]interface{}, error) { - var arr []interface{} - - //Check for valid idx - if idx < 0 { - return nil, - fmt.Errorf("Index out of bounds. Please provide an index greater than 0") - } - - //Get the object - obj, err := controller.GetObject(Path) - if err != nil { - return nil, err - } - - //Check if attribute exists in object - existing, nested := AttrIsInObj(obj, attr) - if !existing { - if State.DebugLvl > ERROR { - l.GetErrorLogger().Println("Attribute :" + attr + " was not found") - } - return nil, fmt.Errorf("Attribute :" + attr + " was not found") - } - - //Check if attribute is an array - if nested { - objAttributes := obj["attributes"].(map[string]interface{}) - if _, ok := objAttributes[attr].([]interface{}); !ok { - if State.DebugLvl > ERROR { - println("Attribute is not an array") - } - return nil, fmt.Errorf("Attribute is not an array") - - } - arr = objAttributes[attr].([]interface{}) - - } else { - if _, ok := obj[attr].([]interface{}); !ok { - if State.DebugLvl > ERROR { - l.GetErrorLogger().Println("Attribute :" + attr + " was not found") - } - return nil, fmt.Errorf("Attribute :" + attr + " was not found") - } - arr = obj[attr].([]interface{}) - } - - //Ensure that we can delete elt in array - if len(arr) == 0 { - if State.DebugLvl > ERROR { - println("Cannot delete anymore elements") - } - return nil, fmt.Errorf("Cannot delete anymore elements") - } - - //Perform delete - if idx >= len(arr) { - idx = len(arr) - 1 - } - arr = slices.Delete(arr, idx, idx+1) - - //Save back into obj - if nested { - obj["attributes"].(map[string]interface{})[attr] = arr - } else { - obj[attr] = arr - } - - URL, err := controller.ObjectUrl(Path, 0) - if err != nil { - return nil, err - } - - _, err = controller.API.Request("PUT", URL, obj, http.StatusOK) - if err != nil { - return nil, err - } - - return nil, nil -} - -func Clear() { - switch runtime.GOOS { - case "windows": - cmd := exec.Command("cmd", "/c", "cls") - cmd.Stdout = os.Stdout - cmd.Run() - default: - fmt.Printf("\033[2J\033[H") - } -} - -func LSOG() error { - fmt.Println("********************************************") - fmt.Println("OGREE Shell Information") - fmt.Println("********************************************") - - fmt.Println("USER EMAIL:", State.User.Email) - fmt.Println("API URL:", State.APIURL+"/api/") - fmt.Println("OGrEE-3D URL:", Ogree3D.URL()) - fmt.Println("OGrEE-3D connected: ", Ogree3D.IsConnected()) - fmt.Println("BUILD DATE:", BuildTime) - fmt.Println("BUILD TREE:", BuildTree) - fmt.Println("BUILD HASH:", BuildHash) - fmt.Println("COMMIT DATE: ", GitCommitDate) - fmt.Println("CONFIG FILE PATH: ", State.ConfigPath) - fmt.Println("LOG PATH:", "./log.txt") - fmt.Println("HISTORY FILE PATH:", State.HistoryFilePath) - fmt.Println("DEBUG LEVEL: ", State.DebugLvl) - - fmt.Printf("\n\n") - fmt.Println("********************************************") - fmt.Println("API Information") - fmt.Println("********************************************") - - //Get API Information here - resp, err := API.Request("GET", "/api/version", nil, http.StatusOK) - if err != nil { - return err - } - apiInfo, ok := resp.Body["data"].(map[string]any) - if !ok { - return fmt.Errorf("invalid response from API on GET /api/version") - } - fmt.Println("BUILD DATE:", apiInfo["BuildDate"]) - fmt.Println("BUILD TREE:", apiInfo["BuildTree"]) - fmt.Println("BUILD HASH:", apiInfo["BuildHash"]) - fmt.Println("COMMIT DATE: ", apiInfo["CommitDate"]) - fmt.Println("CUSTOMER: ", apiInfo["Customer"]) - return nil -} - -func LSEnterprise() error { - resp, err := API.Request("GET", "/api/stats", nil, http.StatusOK) - if err != nil { - return err - } - views.DisplayJson("", resp.Body) - return nil -} - -// Displays environment variable values -// and user defined variables and funcs -func Env(userVars, userFuncs map[string]interface{}) { - fmt.Println("Filter: ", State.FilterDisplay) - fmt.Println() - fmt.Println("Objects Unity shall be informed of upon update:") - for _, k := range State.ObjsForUnity { - fmt.Println(k) - } - fmt.Println() - fmt.Println("Objects Unity shall draw:") - for _, k := range State.DrawableObjs { - fmt.Println(models.EntityToString(k)) - } - - fmt.Println() - fmt.Println("Currently defined user variables:") - for name, k := range userVars { - if k != nil { - fmt.Println("Name:", name, " Value: ", k) - } - - } - - fmt.Println() - fmt.Println("Currently defined user functions:") - for name := range userFuncs { - fmt.Println("Name:", name) - } -} - -func (controller Controller) GetByAttr(path string, u interface{}) error { - obj, err := controller.GetObjectWithChildren(path, 1) - if err != nil { - return err - } - cat := obj["category"].(string) - if cat != "rack" { - return fmt.Errorf("command may only be performed on rack objects") - } - children := obj["children"].([]any) - devices := infArrToMapStrinfArr(children) - switch u.(type) { - case int: - for i := range devices { - if attr, ok := devices[i]["attributes"].(map[string]interface{}); ok { - uStr := strconv.Itoa(u.(int)) - if attr["height"] == uStr { - views.DisplayJson("", devices[i]) - return nil //What if the user placed multiple devices at same height? - } - } - } - if State.DebugLvl > NONE { - println("The 'U' you provided does not correspond to any device in this rack") - } - default: //String - for i := range devices { - if attr, ok := devices[i]["attributes"].(map[string]interface{}); ok { - if attr["slot"] == u.(string) { - views.DisplayJson("", devices[i]) - return nil //What if the user placed multiple devices at same slot? - } - } - } - if State.DebugLvl > NONE { - println("The slot you provided does not correspond to any device in this rack") - } - } - return nil -} - -func Help(entry string) { - var path string - entry = strings.TrimSpace(entry) - switch entry { - case "ls", "pwd", "print", "printf", "cd", "tree", "get", "clear", - "lsog", "grep", "for", "while", "if", "env", - "cmds", "var", "unset", "selection", commands.Connect3D, commands.Disconnect3D, "camera", "ui", "hc", "drawable", - "link", "unlink", "draw", "getu", "getslot", "undraw", - "lsenterprise", commands.Cp: - path = "./other/man/" + entry + ".txt" - - case ">": - path = "./other/man/focus.txt" - - case "+": - path = "./other/man/plus.txt" - - case "=": - path = "./other/man/equal.txt" - - case "-": - path = "./other/man/minus.txt" - - case ".template": - path = "./other/man/template.txt" - - case ".cmds": - path = "./other/man/cmds.txt" - - case ".var": - path = "./other/man/var.txt" - - case "lsobj", "lsten", "lssite", commands.LsBuilding, "lsroom", "lsrack", - "lsdev", "lsac", "lscorridor", "lspanel", "lscabinet": - path = "./other/man/lsobj.txt" - - default: - path = "./other/man/default.txt" - } - text, e := os.ReadFile(utils.ExeDir() + "/" + path) - if e != nil { - println("Manual Page not found!") - } else { - println(string(text)) - } - -} - -// Function is an abstraction of a normal exit -func Exit() { - //writeHistoryOnExit(&State.sessionBuffer) - //runtime.Goexit() - os.Exit(0) -} - -func Connect3D(url string) error { - return Ogree3D.Connect(url, *State.Terminal) -} - -func Disconnect3D() { - Ogree3D.InformOptional("Disconnect3d", -1, map[string]interface{}{"type": "logout", "data": ""}) - Ogree3D.Disconnect() -} - -func (controller Controller) UIDelay(time float64) error { - subdata := map[string]interface{}{"command": "delay", "data": time} - data := map[string]interface{}{"type": "ui", "data": subdata} - if State.DebugLvl > WARNING { - Disp(data) - } - - return controller.Ogree3D.Inform("HandleUI", -1, data) -} - -func (controller Controller) UIToggle(feature string, enable bool) error { - subdata := map[string]interface{}{"command": feature, "data": enable} - data := map[string]interface{}{"type": "ui", "data": subdata} - if State.DebugLvl > WARNING { - Disp(data) - } - - return controller.Ogree3D.Inform("HandleUI", -1, data) -} - -func (controller Controller) UIHighlight(path string) error { - obj, err := controller.GetObject(path) - if err != nil { - return err - } - - subdata := map[string]interface{}{"command": "highlight", "data": obj["id"]} - data := map[string]interface{}{"type": "ui", "data": subdata} - if State.DebugLvl > WARNING { - Disp(data) - } - - return controller.Ogree3D.Inform("HandleUI", -1, data) -} - -func (controller Controller) UIClearCache() error { - subdata := map[string]interface{}{"command": "clearcache", "data": ""} - data := map[string]interface{}{"type": "ui", "data": subdata} - if State.DebugLvl > WARNING { - Disp(data) - } - - return controller.Ogree3D.Inform("HandleUI", -1, data) -} - -func (controller Controller) CameraMove(command string, position []float64, rotation []float64) error { - subdata := map[string]interface{}{"command": command} - subdata["position"] = map[string]interface{}{"x": position[0], "y": position[1], "z": position[2]} - subdata["rotation"] = map[string]interface{}{"x": rotation[0], "y": rotation[1]} - data := map[string]interface{}{"type": "camera", "data": subdata} - if State.DebugLvl > WARNING { - Disp(data) - } - - return controller.Ogree3D.Inform("HandleUI", -1, data) -} - -func (controller Controller) CameraWait(time float64) error { - subdata := map[string]interface{}{"command": "wait"} - subdata["position"] = map[string]interface{}{"x": 0, "y": 0, "z": 0} - subdata["rotation"] = map[string]interface{}{"x": 999, "y": time} - data := map[string]interface{}{"type": "camera", "data": subdata} - if State.DebugLvl > WARNING { - Disp(data) - } - - return controller.Ogree3D.Inform("HandleUI", -1, data) -} - -func (controller Controller) FocusUI(path string) error { - var id string - if path != "" { - obj, err := controller.GetObject(path) - if err != nil { - return err - } - category := models.EntityStrToInt(obj["category"].(string)) - if !models.IsPhysical(path) || category == models.SITE || category == models.BLDG || category == models.ROOM { - msg := "You cannot focus on this object. Note you cannot" + - " focus on Sites, Buildings and Rooms. " + - "For more information please refer to the help doc (man >)" - return fmt.Errorf(msg) - } - id = obj["id"].(string) - } else { - id = "" - } - - data := map[string]interface{}{"type": "focus", "data": id} - err := controller.Ogree3D.Inform("FocusUI", -1, data) - if err != nil { - return err - } - - if path != "" { - return controller.CD(path) - } else { - fmt.Println("Focus is now empty") - } - - return nil -} - -func (controller Controller) LinkObject(source string, destination string, attrs []string, values []any, slots []string) error { - sourceUrl, err := controller.ObjectUrl(source, 0) - if err != nil { - return err - } - destPath, err := controller.SplitPath(destination) - if err != nil { - return err - } - if !strings.HasPrefix(sourceUrl, "/api/stray_objects/") { - return fmt.Errorf("only stray objects can be linked") - } - payload := map[string]any{"parentId": destPath.ObjectID} - - if slots != nil { - if slots, err = ExpandStrVector(slots); err != nil { - return err - } - payload["slot"] = slots - } - - _, err = controller.API.Request("PATCH", sourceUrl+"/link", payload, http.StatusOK) - if err != nil { - return err - } - return nil -} - -func (controller Controller) UnlinkObject(path string) error { - sourceUrl, err := controller.ObjectUrl(path, 0) - if err != nil { - return err - } - _, err = controller.API.Request("PATCH", sourceUrl+"/unlink", nil, http.StatusOK) - return err -} - -func (controller Controller) IsEntityDrawable(path string) (bool, error) { - obj, err := controller.GetObject(path) - if err != nil { - return false, err - } - if catInf, ok := obj["category"]; ok { - if category, ok := catInf.(string); ok { - return IsDrawableEntity(category), nil - } - } - return false, nil -} - -func IsCategoryAttrDrawable(category string, attr string) bool { - templateJson := State.DrawableJsons[category] - if templateJson == nil { - return true - } - - switch attr { - case "id", "name", "category", "parentID", - "description", "domain", "parentid", "parentId", "tags": - if val, ok := templateJson[attr]; ok { - if valBool, ok := val.(bool); ok { - return valBool - } - } - return false - default: - if tmp, ok := templateJson["attributes"]; ok { - if attributes, ok := tmp.(map[string]interface{}); ok { - if val, ok := attributes[attr]; ok { - if valBool, ok := val.(bool); ok { - return valBool - } - } - } - } - return false - } -} - -func (controller Controller) IsAttrDrawable(path string, attr string) (bool, error) { - obj, err := controller.GetObject(path) - if err != nil { - return false, err - } - category := obj["category"].(string) - return IsCategoryAttrDrawable(category, attr), nil -} - -func ShowClipBoard() []string { - if State.ClipBoard != nil { - for _, k := range State.ClipBoard { - println(k) - } - return State.ClipBoard - } - return nil -} - -func SetEnv(arg string, val interface{}) { - switch arg { - case "Filter": - if _, ok := val.(bool); !ok { - msg := "Can only assign bool values for " + arg + " Env Var" - l.GetWarningLogger().Println(msg) - if State.DebugLvl > 0 { - println(msg) - } - } else { - if arg == "Filter" { - State.FilterDisplay = val.(bool) - } - - println(arg + " Display Environment variable set") - } - - default: - println(arg + " is not an environment variable") - } -} - -// Utility functions -func determineStrKey(x map[string]interface{}, possible []string) string { - for idx := range possible { - if _, ok := x[possible[idx]]; ok { - return possible[idx] - } - } - return "" //The code should not reach this point! -} - -func randPassword(n int) string { - const passChars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" - b := make([]byte, n) - for i := range b { - b[i] = passChars[rand.Intn(len(passChars))] - } - return string(b) -} - -func (controller Controller) CreateUser(email string, role string, domain string) error { - password := randPassword(14) - response, err := controller.API.Request( - "POST", - "/api/users", - map[string]any{ - "email": email, - "password": password, - "roles": map[string]any{ - domain: role, - }, - }, - http.StatusCreated, - ) - if err != nil { - return err - } - println(response.message) - println("password:" + password) - return nil -} - -func (controller Controller) AddRole(email string, role string, domain string) error { - response, err := controller.API.Request("GET", "/api/users", nil, http.StatusOK) - if err != nil { - return err - } - userList, userListOk := response.Body["data"].([]any) - if !userListOk { - return fmt.Errorf("response contains no user list") - } - userID := "" - for _, user := range userList { - userMap, ok := user.(map[string]any) - if !ok { - continue - } - userEmail, emailOk := userMap["email"].(string) - id, idOk := userMap["_id"].(string) - if emailOk && idOk && userEmail == email { - userID = id - break - } - } - if userID == "" { - return fmt.Errorf("user not found") - } - response, err = controller.API.Request("PATCH", fmt.Sprintf("/api/users/%s", userID), - map[string]any{ - "roles": map[string]any{ - domain: role, - }, - }, - http.StatusOK, - ) - if err != nil { - return err - } - println(response.message) - return nil -} - -func ChangePassword() error { - currentPassword, err := readline.Password("Current password: ") - if err != nil { - return err - } - newPassword, err := readline.Password("New password: ") - if err != nil { - return err - } - response, err := API.Request("POST", "/api/users/password/change", - map[string]any{ - "currentPassword": string(currentPassword), - "newPassword": string(newPassword), - }, - http.StatusOK, - ) - if err != nil { - return err - } - println(response.message) - return nil -} - -func (controller Controller) SplitPath(pathStr string) (models.Path, error) { - for _, prefix := range models.PathPrefixes { - if strings.HasPrefix(pathStr, string(prefix)) { - var id string - if prefix == models.VirtualObjsPath && strings.HasPrefix(pathStr, prefix+"#") { - // virtual root layer, keep the virtual node - id = pathStr[1:] - } else { - id = pathStr[len(prefix):] - } - id = strings.ReplaceAll(id, "/", ".") - - var layer models.Layer - var err error - - id, layer, err = controller.GetLayer(id) - if err != nil { - return models.Path{}, err - } - - return models.Path{ - Prefix: prefix, - ObjectID: id, - Layer: layer, - }, nil - } - } - - return models.Path{}, fmt.Errorf("invalid object path") -} diff --git a/CLI/controllers/commandController_test.go b/CLI/controllers/commandController_test.go deleted file mode 100644 index 8633d097d..000000000 --- a/CLI/controllers/commandController_test.go +++ /dev/null @@ -1,947 +0,0 @@ -package controllers_test - -import ( - "cli/controllers" - "cli/models" - test_utils "cli/test" - "errors" - "net/url" - "strings" - "testing" - - "github.com/stretchr/testify/assert" - "golang.org/x/exp/slices" -) - -const testRackObjPath = "/api/hierarchy_objects/BASIC.A.R1.A01" - -// Test PWD -func TestPWD(t *testing.T) { - controller, mockAPI, _ := layersSetup(t) - controller.CD("/") - location := controllers.PWD() - assert.Equal(t, "/", location) - - test_utils.MockGetObject(mockAPI, rack1) - path := "/Physical/" + strings.Replace(rack1["id"].(string), ".", "/", -1) - err := controller.CD(path) - assert.Nil(t, err) - - location = controllers.PWD() - assert.Equal(t, path, location) -} - -// Test UnfoldPath -func TestUnfoldPath(t *testing.T) { - controller, mockAPI, _, _ := test_utils.SetMainEnvironmentMock(t) - wildcardPath := "/Physical/site/building/room/rack*" - firstRackPath := "/Physical/site/building/room/rack1" - secondRackPath := "/Physical/site/building/room/rack2" - rack1 := test_utils.GetEntity("rack", "rack1", "site.building.room", "") - rack2 := test_utils.GetEntity("rack", "rack2", "site.building.room", "") - test_utils.MockGetObjects(mockAPI, "id=site.building.room.rack*&namespace=physical.hierarchy", []any{rack1, rack2}) - controllers.State.ClipBoard = []string{firstRackPath} - tests := []struct { - name string - path string - expectedValue []string - }{ - {"StringWithStar", wildcardPath, []string{firstRackPath, secondRackPath}}, - {"Clipboard", "_", controllers.State.ClipBoard}, - {"SimplePath", secondRackPath, []string{secondRackPath}}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - results, err := controller.UnfoldPath(tt.path) - assert.Nil(t, err) - assert.Equal(t, tt.expectedValue, results) - }) - } -} - -// Tests ObjectUrl -func TestObjectUrlInvalidPath(t *testing.T) { - _, err := controllers.C.ObjectUrl("/invalid/path", 0) - assert.NotNil(t, err) - assert.Equal(t, "invalid object path", err.Error()) -} - -func TestObjectUrlPaths(t *testing.T) { - paths := map[string]any{ - models.StrayPath + "stray-object": "/api/stray_objects/stray-object", - models.PhysicalPath + "BASIC/A": "/api/hierarchy_objects/BASIC.A", - models.ObjectTemplatesPath + "my-template": "/api/obj_templates/my-template", - models.RoomTemplatesPath + "my-room-template": "/api/room_templates/my-room-template", - models.BuildingTemplatesPath + "my-building-template": "/api/bldg_templates/my-building-template", - models.GroupsPath + "group1": "/api/groups/group1", - models.TagsPath + "my-tag": "/api/tags/my-tag", - models.LayersPath + "my-layer": "/api/layers/my-layer", - models.DomainsPath + "domain1": "/api/domains/domain1", - models.DomainsPath + "domain1/subdomain": "/api/domains/domain1.subdomain", - } - - for key, value := range paths { - basePath, err := controllers.C.ObjectUrl(key, 0) - assert.Nil(t, err) - assert.Equal(t, value, basePath) - } -} - -// Tests ObjectUrlGeneric -func TestObjectUrlGenericInvalidPath(t *testing.T) { - _, err := controllers.C.ObjectUrlGeneric("/invalid/path", 0, nil, nil) - assert.NotNil(t, err) - assert.Equal(t, "invalid object path", err.Error()) -} - -func TestObjectUrlGenericWithNoFilters(t *testing.T) { - paths := []map[string]any{ - map[string]any{ - "basePath": models.StrayPath, - "objectId": "stray-object", - "endpoint": "/api/objects", - "idName": "id", - "namespace": "physical.stray", - }, - map[string]any{ - "basePath": models.PhysicalPath, - "objectId": "BASIC/A", - "endpoint": "/api/objects", - "idName": "id", - "namespace": "physical.hierarchy", - }, - map[string]any{ - "basePath": models.ObjectTemplatesPath, - "objectId": "my-template", - "endpoint": "/api/objects", - "idName": "slug", - "namespace": "logical.objtemplate", - }, - map[string]any{ - "basePath": models.RoomTemplatesPath, - "objectId": "my-room-template", - "endpoint": "/api/objects", - "idName": "slug", - "namespace": "logical.roomtemplate", - }, - map[string]any{ - "basePath": models.BuildingTemplatesPath, - "objectId": "my-building-template", - "endpoint": "/api/objects", - "idName": "slug", - "namespace": "logical.bldgtemplate", - }, - map[string]any{ - "basePath": models.GroupsPath, - "objectId": "group1", - "endpoint": "/api/objects", - "idName": "id", - "namespace": "logical", - "extraParams": map[string]any{ - "category": "group", - }, - }, - map[string]any{ - "basePath": models.TagsPath, - "objectId": "my-tag", - "endpoint": "/api/objects", - "idName": "slug", - "namespace": "logical.tag", - }, - map[string]any{ - "basePath": models.LayersPath, - "objectId": "my-layer", - "endpoint": "/api/objects", - "idName": "slug", - "namespace": "logical.layer", - }, - map[string]any{ - "basePath": models.DomainsPath, - "objectId": "domain1", - "endpoint": "/api/objects", - "idName": "id", - "namespace": "organisational", - }, - map[string]any{ - "basePath": models.DomainsPath, - "objectId": "domain1/subdomain", - "endpoint": "/api/objects", - "idName": "id", - "namespace": "organisational", - }, - } - for _, value := range paths { - resultUrl, err := controllers.C.ObjectUrlGeneric(value["basePath"].(string)+value["objectId"].(string), 0, nil, nil) - assert.Nil(t, err) - assert.NotNil(t, resultUrl) - - parsedUrl, _ := url.Parse(resultUrl) - assert.Equal(t, value["endpoint"], parsedUrl.Path) - assert.Equal(t, strings.Replace(value["objectId"].(string), "/", ".", -1), parsedUrl.Query().Get(value["idName"].(string))) - assert.Equal(t, value["namespace"], parsedUrl.Query().Get("namespace")) - - if extraParams, ok := value["extraParams"]; ok { - for k, v := range extraParams.(map[string]any) { - assert.Equal(t, v, parsedUrl.Query().Get(k)) - } - } - } -} - -func TestObjectUrlGenericWithNormalFilters(t *testing.T) { - filters := map[string]string{ - "color": "00ED00", - } - id := "BASIC/A" - resultUrl, err := controllers.C.ObjectUrlGeneric(models.PhysicalPath+id, 0, filters, nil) - assert.Nil(t, err) - assert.NotNil(t, resultUrl) - - parsedUrl, _ := url.Parse(resultUrl) - assert.Equal(t, "/api/objects", parsedUrl.Path) - assert.Equal(t, strings.Replace(id, "/", ".", -1), parsedUrl.Query().Get("id")) - assert.Equal(t, "physical.hierarchy", parsedUrl.Query().Get("namespace")) - assert.Equal(t, "00ED00", parsedUrl.Query().Get("color")) -} - -func TestObjectUrlGenericWithFilterField(t *testing.T) { - filters := map[string]string{ - "filter": "color=00ED00", - } - id := "BASIC/A" - resultUrl, err := controllers.C.ObjectUrlGeneric(models.PhysicalPath+id, 0, filters, nil) - assert.Nil(t, err) - assert.NotNil(t, resultUrl) - - parsedUrl, _ := url.Parse(resultUrl) - assert.Equal(t, "/api/objects/search", parsedUrl.Path) - assert.Equal(t, strings.Replace(id, "/", ".", -1), parsedUrl.Query().Get("id")) - assert.Equal(t, "physical.hierarchy", parsedUrl.Query().Get("namespace")) -} - -// Tests GetSlot -func TestGetSlotWithNoTemplate(t *testing.T) { - rack := map[string]any{ - "attributes": map[string]any{}, - } - result, err := controllers.C.GetSlot(rack, "") - assert.Nil(t, err) - assert.Nil(t, result) - - rack["attributes"].(map[string]any)["template"] = "" - result, err = controllers.C.GetSlot(rack, "") - assert.Nil(t, err) - assert.Nil(t, result) -} - -func TestGetSlotWithTemplateNonExistentSlot(t *testing.T) { - controller, mockAPI, _ := layersSetup(t) - - template := map[string]any{ - "slug": "rack-template", - "description": "", - "category": "rack", - "sizeWDHmm": []any{605, 1200, 2003}, - "fbxModel": "", - "attributes": map[string]any{ - "vendor": "IBM", - "model": "9360-4PX", - }, - "slots": []any{}, - } - - test_utils.MockGetObjTemplate(mockAPI, template) - rack := map[string]any{ - "attributes": map[string]any{ - "template": "rack-template", - }, - } - _, err := controller.GetSlot(rack, "u02") - assert.NotNil(t, err) - assert.Equal(t, "the slot u02 does not exist", err.Error()) -} - -func TestGetSlotWithTemplateWorks(t *testing.T) { - controller, mockAPI, _ := layersSetup(t) - slot := map[string]any{ - "location": "u01", - "type": "u", - "elemOrient": []any{33.3, -44.4, 107}, - "elemPos": []any{58, 51, 44.45}, - "elemSize": []any{482.6, 1138, 44.45}, - "mandatory": "no", - "labelPos": "frontrear", - } - - template := map[string]any{ - "slug": "rack-template", - "description": "", - "category": "rack", - "sizeWDHmm": []any{605, 1200, 2003}, - "fbxModel": "", - "attributes": map[string]any{ - "vendor": "IBM", - "model": "9360-4PX", - }, - "slots": []any{ - slot, - }, - } - - test_utils.MockGetObjTemplate(mockAPI, template) - rack := map[string]any{ - "attributes": map[string]any{ - "template": "rack-template", - }, - } - result, err := controller.GetSlot(rack, "u01") - assert.Nil(t, err) - assert.Equal(t, slot["location"], result["location"]) -} - -// Tests UnsetAttribute -func TestUnsetAttributeObjectNotFound(t *testing.T) { - controller, mockAPI, _ := layersSetup(t) - - test_utils.MockObjectNotFound(mockAPI, testRackObjPath) - - err := controller.UnsetAttribute("/Physical/BASIC/A/R1/A01", "color") - assert.NotNil(t, err) - assert.Equal(t, "object not found", err.Error()) -} - -func TestUnsetAttributeWorks(t *testing.T) { - controller, mockAPI, _ := layersSetup(t) - rack := map[string]any{ - "category": "rack", - "id": "BASIC.A.R1.A01", - "name": "A01", - "parentId": "BASIC.A.R1", - "domain": "test-domain", - "description": "", - "attributes": map[string]any{ - "height": "47", - "heightUnit": "U", - "rotation": `[45, 45, 45]`, - "posXYZ": `[4.6666666666667, -2, 0]`, - "posXYUnit": "m", - "size": `[1, 1]`, - "sizeUnit": "cm", - "color": "00ED00", - }, - } - updatedRack := test_utils.CopyMap(rack) - delete(updatedRack["attributes"].(map[string]any), "color") - delete(updatedRack, "id") - - test_utils.MockGetObject(mockAPI, rack) - test_utils.MockPutObject(mockAPI, updatedRack, updatedRack) - - err := controller.UnsetAttribute("/Physical/BASIC/A/R1/A01", "color") - assert.Nil(t, err) -} - -// Tests UnsetInObj -func TestUnsetInObjInvalidIndex(t *testing.T) { - controller, _, _ := layersSetup(t) - - result, err := controller.UnsetInObj("/Physical/BASIC/A/R1/A01", "color", -1) - assert.NotNil(t, err) - assert.Nil(t, result) - assert.Equal(t, "Index out of bounds. Please provide an index greater than 0", err.Error()) -} - -func TestUnsetInObjObjectNotFound(t *testing.T) { - controller, mockAPI, _ := layersSetup(t) - - test_utils.MockObjectNotFound(mockAPI, testRackObjPath) - - result, err := controller.UnsetInObj("/Physical/BASIC/A/R1/A01", "color", 0) - assert.NotNil(t, err) - assert.Nil(t, result) - assert.Equal(t, "object not found", err.Error()) -} - -func TestUnsetInObjAttributeNotFound(t *testing.T) { - controller, mockAPI, _ := layersSetup(t) - - rack := test_utils.CopyMap(rack1) - rack["attributes"] = map[string]any{} - - test_utils.MockGetObject(mockAPI, rack) - - result, err := controller.UnsetInObj("/Physical/BASIC/A/R1/A01", "color", 0) - assert.NotNil(t, err) - assert.Nil(t, result) - assert.Equal(t, "Attribute :color was not found", err.Error()) -} - -func TestUnsetInObjAttributeNotAnArray(t *testing.T) { - controller, mockAPI, _ := layersSetup(t) - - rack := test_utils.CopyMap(rack1) - rack["attributes"] = map[string]any{ - "color": "00ED00", - } - - test_utils.MockGetObject(mockAPI, rack) - - result, err := controller.UnsetInObj("/Physical/BASIC/A/R1/A01", "color", 0) - assert.NotNil(t, err) - assert.Nil(t, result) - assert.Equal(t, "Attribute is not an array", err.Error()) -} - -func TestUnsetInObjEmptyArray(t *testing.T) { - controller, mockAPI, _ := layersSetup(t) - - rack := test_utils.CopyMap(rack1) - rack["attributes"] = map[string]any{ - "posXYZ": []any{}, - } - - test_utils.MockGetObject(mockAPI, rack) - - result, err := controller.UnsetInObj("/Physical/BASIC/A/R1/A01", "posXYZ", 0) - assert.NotNil(t, err) - assert.Nil(t, result) - assert.Equal(t, "Cannot delete anymore elements", err.Error()) -} - -func TestUnsetInObjWorksWithNestedAttribute(t *testing.T) { - controller, mockAPI, _ := layersSetup(t) - - rack := test_utils.CopyMap(rack1) - rack["attributes"] = map[string]any{ - "posXYZ": []any{1, 2, 3}, - } - updatedRack := test_utils.CopyMap(rack1) - updatedRack["attributes"] = map[string]any{ - "posXYZ": []any{1.0, 3.0}, - } - delete(updatedRack, "children") - - test_utils.MockGetObject(mockAPI, rack) - test_utils.MockPutObject(mockAPI, updatedRack, updatedRack) - - result, err := controller.UnsetInObj("/Physical/BASIC/A/R1/A01", "posXYZ", 1) - assert.Nil(t, err) - assert.Nil(t, result) -} - -func TestUnsetInObjWorksWithAttribute(t *testing.T) { - controller, mockAPI, _ := layersSetup(t) - - template := map[string]any{ - "slug": "small-room", - "category": "room", - "axisOrientation": "+x+y", - "sizeWDHm": []any{9.6, 22.8, 3.0}, - "floorUnit": "t", - "technicalArea": []any{5.0, 0.0, 0.0, 0.0}, - "reservedArea": []any{3.0, 1.0, 1.0, 3.0}, - "colors": []any{ - map[string]any{ - "name": "my-color1", - "value": "00ED00", - }, - map[string]any{ - "name": "my-color2", - "value": "ffffff", - }, - }, - } - updatedTemplate := test_utils.CopyMap(template) - updatedTemplate["colors"] = slices.Delete(updatedTemplate["colors"].([]any), 1, 2) - test_utils.MockPutObject(mockAPI, updatedTemplate, updatedTemplate) - test_utils.MockGetRoomTemplate(mockAPI, template) - - result, err := controller.UnsetInObj(models.RoomTemplatesPath+"small-room", "colors", 1) - assert.Nil(t, err) - assert.Nil(t, result) -} - -// Tests GetByAttr -func TestGetByAttrErrorWhenObjIsNotRack(t *testing.T) { - controller, mockAPI, _ := layersSetup(t) - - test_utils.MockGetObjectHierarchy(mockAPI, chassis) - - err := controller.GetByAttr(models.PhysicalPath+"BASIC/A/R1/A01/chT", "colors") - assert.NotNil(t, err) - assert.Equal(t, "command may only be performed on rack objects", err.Error()) -} - -func TestGetByAttrErrorWhenObjIsRackWithSlotName(t *testing.T) { - controller, mockAPI, _ := layersSetup(t) - - rack := test_utils.CopyMap(rack1) - rack["attributes"] = map[string]any{ - "slot": []any{ - map[string]any{ - "location": "u01", - "type": "u", - "elemOrient": []any{33.3, -44.4, 107}, - "elemPos": []any{58, 51, 44.45}, - "elemSize": []any{482.6, 1138, 44.45}, - "mandatory": "no", - "labelPos": "frontrear", - "color": "@color1", - }, - }, - } - test_utils.MockGetObjectHierarchy(mockAPI, rack) - - err := controller.GetByAttr(models.PhysicalPath+"BASIC/A/R1/A01", "u01") - assert.Nil(t, err) -} - -func TestGetByAttrErrorWhenObjIsRackWithHeight(t *testing.T) { - controller, mockAPI, _ := layersSetup(t) - - rack := test_utils.CopyMap(rack1) - rack["height"] = "47" - test_utils.MockGetObjectHierarchy(mockAPI, rack) - - err := controller.GetByAttr(models.PhysicalPath+"BASIC/A/R1/A01", 47) - assert.Nil(t, err) -} - -// Test UI (UIDelay, UIToggle, UIHighlight) -func TestUIDelay(t *testing.T) { - controller, _, ogree3D := layersSetup(t) - // ogree3D. - time := 15.0 - data := map[string]interface{}{ - "type": "ui", - "data": map[string]interface{}{ - "command": "delay", - "data": time, - }, - } - ogree3D.On("Inform", "HandleUI", -1, data).Return(nil).Once() // The inform should be called once - err := controller.UIDelay(time) - assert.Nil(t, err) -} - -func TestUIToggle(t *testing.T) { - controller, _, ogree3D := layersSetup(t) - // ogree3D. - feature := "feature" - enable := true - data := map[string]interface{}{ - "type": "ui", - "data": map[string]interface{}{ - "command": feature, - "data": enable, - }, - } - - ogree3D.On("Inform", "HandleUI", -1, data).Return(nil).Once() // The inform should be called once - err := controller.UIToggle(feature, enable) - assert.Nil(t, err) -} - -func TestUIHighlightObjectNotFound(t *testing.T) { - controller, mockAPI, ogree3D := layersSetup(t) - path := testRackObjPath - - test_utils.MockObjectNotFound(mockAPI, path) - - data := map[string]interface{}{ - "type": "ui", - "data": map[string]interface{}{ - "command": "highlight", - "data": "BASIC.A.R1.A01", - }, - } - - ogree3D.AssertNotCalled(t, "HandleUI", -1, data) - err := controller.UIHighlight("/Physical/BASIC/A/R1/A01") - assert.NotNil(t, err) - assert.Equal(t, "object not found", err.Error()) -} - -func TestUIHighlightWorks(t *testing.T) { - controller, mockAPI, ogree3D := layersSetup(t) - data := map[string]interface{}{ - "type": "ui", - "data": map[string]interface{}{ - "command": "highlight", - "data": rack1["id"], - }, - } - - test_utils.MockGetObject(mockAPI, rack1) - ogree3D.On("Inform", "HandleUI", -1, data).Return(nil).Once() // The inform should be called once - err := controller.UIHighlight("/Physical/BASIC/A/R1/A01") - assert.Nil(t, err) -} - -func TestUIClearCache(t *testing.T) { - controller, _, ogree3D := layersSetup(t) - data := map[string]interface{}{ - "type": "ui", - "data": map[string]interface{}{ - "command": "clearcache", - "data": "", - }, - } - - ogree3D.On("Inform", "HandleUI", -1, data).Return(nil).Once() // The inform should be called once - err := controller.UIClearCache() - assert.Nil(t, err) -} - -func TestCameraMove(t *testing.T) { - controller, _, ogree3D := layersSetup(t) - data := map[string]interface{}{ - "type": "camera", - "data": map[string]interface{}{ - "command": "move", - "position": map[string]interface{}{"x": 0.0, "y": 1.0, "z": 2.0}, - "rotation": map[string]interface{}{"x": 0.0, "y": 0.0}, - }, - } - - ogree3D.On("Inform", "HandleUI", -1, data).Return(nil).Once() // The inform should be called once - err := controller.CameraMove("move", []float64{0, 1, 2}, []float64{0, 0}) - assert.Nil(t, err) -} - -func TestCameraWait(t *testing.T) { - controller, _, ogree3D := layersSetup(t) - time := 15.0 - data := map[string]interface{}{ - "type": "camera", - "data": map[string]interface{}{ - "command": "wait", - "position": map[string]interface{}{"x": 0, "y": 0, "z": 0}, - "rotation": map[string]interface{}{"x": 999, "y": time}, - }, - } - - ogree3D.On("Inform", "HandleUI", -1, data).Return(nil).Once() // The inform should be called once - err := controller.CameraWait(time) - assert.Nil(t, err) -} - -func TestFocusUIObjectNotFound(t *testing.T) { - controller, mockAPI, ogree3D := layersSetup(t) - - test_utils.MockObjectNotFound(mockAPI, "/api/hierarchy_objects/"+rack1["id"].(string)) - err := controller.FocusUI("/Physical/" + strings.Replace(rack1["id"].(string), ".", "/", -1)) - ogree3D.AssertNotCalled(t, "Inform", "mock.Anything", "mock.Anything", "mock.Anything") - assert.NotNil(t, err) - assert.Equal(t, "object not found", err.Error()) -} - -func TestFocusUIEmptyPath(t *testing.T) { - controller, mockAPI, ogree3D := layersSetup(t) - data := map[string]interface{}{ - "type": "focus", - "data": "", - } - - ogree3D.On("Inform", "FocusUI", -1, data).Return(nil).Once() // The inform should be called once - err := controller.FocusUI("") - mockAPI.AssertNotCalled(t, "Request", "GET", "mock.Anything", "mock.Anything", "mock.Anything") - assert.Nil(t, err) -} - -func TestFocusUIErrorWithRoom(t *testing.T) { - controller, mockAPI, ogree3D := layersSetup(t) - errorMessage := "You cannot focus on this object. Note you cannot focus on Sites, Buildings and Rooms. " - errorMessage += "For more information please refer to the help doc (man >)" - - test_utils.MockGetObject(mockAPI, roomWithoutChildren) - err := controller.FocusUI("/Physical/" + strings.Replace(roomWithoutChildren["id"].(string), ".", "/", -1)) - ogree3D.AssertNotCalled(t, "Inform", "mock.Anything", "mock.Anything", "mock.Anything") - assert.NotNil(t, err) - assert.Equal(t, errorMessage, err.Error()) -} - -func TestFocusUIWorks(t *testing.T) { - controller, mockAPI, ogree3D := layersSetup(t) - data := map[string]interface{}{ - "type": "focus", - "data": rack1["id"], - } - - ogree3D.On("Inform", "FocusUI", -1, data).Return(nil).Once() // The inform should be called once - // Get Object will be called two times: Once in FocusUI and a second time in FocusUI->CD->Tree - test_utils.MockGetObject(mockAPI, rack1) - test_utils.MockGetObject(mockAPI, rack1) - err := controller.FocusUI("/Physical/" + strings.Replace(rack1["id"].(string), ".", "/", -1)) - assert.Nil(t, err) -} - -// Tests LinkObject -func TestLinkObjectErrorNotStaryObject(t *testing.T) { - controller, _, _ := layersSetup(t) - - err := controller.LinkObject(models.PhysicalPath+"BASIC/A/R1/A01", models.PhysicalPath+"BASIC/A/R1/A01", []string{}, []any{}, []string{}) - assert.NotNil(t, err) - assert.Equal(t, "only stray objects can be linked", err.Error()) -} - -func TestLinkObjectWithoutSlots(t *testing.T) { - controller, mockAPI, _ := layersSetup(t) - - strayDevice := test_utils.CopyMap(chassis) - delete(strayDevice, "id") - delete(strayDevice, "parentId") - response := map[string]any{"message": "successfully linked"} - body := map[string]any{"parentId": "BASIC.A.R1.A01", "slot": []string{}} - - test_utils.MockUpdateObject(mockAPI, body, response) - - slots := []string{} - attributes := []string{} - values := []any{} - for key, value := range strayDevice["attributes"].(map[string]any) { - attributes = append(attributes, key) - values = append(values, value) - } - err := controller.LinkObject(models.StrayPath+"chT", models.PhysicalPath+"BASIC/A/R1/A01", attributes, values, slots) - assert.Nil(t, err) -} - -func TestLinkObjectWithInvalidSlots(t *testing.T) { - controller, _, _ := layersSetup(t) - - strayDevice := test_utils.CopyMap(chassis) - delete(strayDevice, "id") - delete(strayDevice, "parentId") - - slots := []string{"slot01..slot03", "slot4"} - attributes := []string{} - values := []any{} - for key, value := range strayDevice["attributes"].(map[string]any) { - attributes = append(attributes, key) - values = append(values, value) - } - err := controller.LinkObject(models.StrayPath+"chT", models.PhysicalPath+"BASIC/A/R1/A01", attributes, values, slots) - assert.NotNil(t, err) - assert.Equal(t, "Invalid device syntax: .. can only be used in a single element vector", err.Error()) -} - -func TestLinkObjectWithValidSlots(t *testing.T) { - controller, mockAPI, _ := layersSetup(t) - - strayDevice := test_utils.CopyMap(chassis) - delete(strayDevice, "id") - delete(strayDevice, "parentId") - response := map[string]any{"message": "successfully linked"} - body := map[string]any{"parentId": "BASIC.A.R1.A01", "slot": []string{"slot01"}} - - test_utils.MockUpdateObject(mockAPI, body, response) - - slots := []string{"slot01"} - attributes := []string{} - values := []any{} - for key, value := range strayDevice["attributes"].(map[string]any) { - attributes = append(attributes, key) - values = append(values, value) - } - err := controller.LinkObject(models.StrayPath+"chT", models.PhysicalPath+"BASIC/A/R1/A01", attributes, values, slots) - assert.Nil(t, err) -} - -// Tests UnlinkObject -func TestUnlinkObjectWithInvalidPath(t *testing.T) { - controller, _, _ := layersSetup(t) - - err := controller.UnlinkObject("/invalid/path") - assert.NotNil(t, err) - assert.Equal(t, "invalid object path", err.Error()) -} - -func TestUnlinkObjectWithValidPath(t *testing.T) { - controller, mockAPI, _ := layersSetup(t) - - test_utils.MockUpdateObject(mockAPI, nil, map[string]any{"message": "successfully unlinked"}) - - err := controller.UnlinkObject(models.PhysicalPath + "BASIC/A/R1/A01") - assert.Nil(t, err) -} - -// Tests IsEntityDrawable -func TestIsEntityDrawableObjectNotFound(t *testing.T) { - controller, mockAPI, _ := layersSetup(t) - - test_utils.MockObjectNotFound(mockAPI, testRackObjPath) - - isDrawable, err := controller.IsEntityDrawable(models.PhysicalPath + "BASIC/A/R1/A01") - assert.False(t, isDrawable) - assert.NotNil(t, err) - assert.Equal(t, "object not found", err.Error()) -} - -func TestIsEntityDrawable(t *testing.T) { - tests := []struct { - name string - drawableObjects []int - expectedIsDrawable bool - }{ - {"CategoryIsNotDrawable", []int{models.EntityStrToInt("device")}, false}, - {"CategoryIsDrawable", []int{models.EntityStrToInt("rack")}, true}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - controller, mockAPI, _ := layersSetup(t) - controllers.State.DrawableObjs = tt.drawableObjects - - test_utils.MockGetObject(mockAPI, rack1) - - isDrawable, err := controller.IsEntityDrawable(models.PhysicalPath + "BASIC/A/R1/A01") - assert.Equal(t, tt.expectedIsDrawable, isDrawable) - assert.Nil(t, err) - }) - } -} - -// Tests IsAttrDrawable (and IsCategoryAttrDrawable) -func TestIsAttrDrawableObjectNotFound(t *testing.T) { - controller, mockAPI, _ := layersSetup(t) - path := testRackObjPath - - test_utils.MockObjectNotFound(mockAPI, path) - - isAttrDrawable, err := controller.IsAttrDrawable(models.PhysicalPath+"BASIC/A/R1/A01", "color") - assert.False(t, isAttrDrawable) - assert.NotNil(t, err) - assert.Equal(t, "object not found", err.Error()) -} - -func TestIsAttrDrawableTemplateJsonIsNil(t *testing.T) { - controller, mockAPI, _ := layersSetup(t) - controllers.State.DrawableObjs = []int{models.EntityStrToInt("rack")} - - controllers.State.DrawableJsons = map[string]map[string]any{ - "rack": nil, - } - - test_utils.MockGetObject(mockAPI, rack1) - - isAttrDrawable, err := controller.IsAttrDrawable(models.PhysicalPath+"BASIC/A/R1/A01", "color") - assert.True(t, isAttrDrawable) - assert.Nil(t, err) -} - -func TestIsAttrDrawable(t *testing.T) { - tests := []struct { - name string - attributeDrawable string - attributeNonDrawable string - }{ - {"SpecialAttribute", "name", "description"}, - {"SpecialAttribute", "color", "height"}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - controller, mockAPI, _ := layersSetup(t) - controllers.State.DrawableObjs = []int{models.EntityStrToInt("rack")} - - controllers.State.DrawableJsons = test_utils.GetTestDrawableJson() - - test_utils.MockGetObject(mockAPI, rack1) - isAttrDrawable, err := controller.IsAttrDrawable(models.PhysicalPath+"BASIC/A/R1/A01", tt.attributeDrawable) - assert.True(t, isAttrDrawable) - assert.Nil(t, err) - - test_utils.MockGetObject(mockAPI, rack1) - isAttrDrawable, err = controller.IsAttrDrawable(models.PhysicalPath+"BASIC/A/R1/A01", tt.attributeNonDrawable) - assert.False(t, isAttrDrawable) - assert.Nil(t, err) - }) - } -} - -// Tests CreateUser -func TestCreateUserInvalidEmail(t *testing.T) { - controller, mockAPI, _ := layersSetup(t) - - mockAPI.On( - "Request", "POST", - "/api/users", - "mock.Anything", 201, - ).Return( - &controllers.Response{ - Body: map[string]any{ - "message": "A valid email address is required", - }, - Status: 400, - }, errors.New("[Response From API] A valid email address is required"), - ).Once() - - err := controller.CreateUser("email", "manager", "*") - assert.NotNil(t, err) - assert.Equal(t, "[Response From API] A valid email address is required", err.Error()) -} - -func TestCreateUserWorks(t *testing.T) { - controller, mockAPI, _ := layersSetup(t) - - mockAPI.On("Request", "POST", - "/api/users", - "mock.Anything", 201, - ).Return( - &controllers.Response{ - Body: map[string]any{ - "message": "Account has been created", - }, - }, nil, - ).Once() - - err := controller.CreateUser("email@email.com", "manager", "*") - assert.Nil(t, err) -} - -// Tests AddRole -func TestAddRoleUserNotFound(t *testing.T) { - controller, mockAPI, _ := layersSetup(t) - - mockAPI.On("Request", "GET", "/api/users", "mock.Anything", 200).Return( - &controllers.Response{ - Body: map[string]any{ - "data": []any{}, - }, - }, nil, - ).Once() - - err := controller.AddRole("email@email.com", "manager", "*") - assert.NotNil(t, err) - assert.Equal(t, "user not found", err.Error()) -} - -func TestAddRoleWorks(t *testing.T) { - controller, mockAPI, _ := layersSetup(t) - - mockAPI.On("Request", "GET", "/api/users", "mock.Anything", 200).Return( - &controllers.Response{ - Body: map[string]any{ - "data": []any{ - map[string]any{ - "_id": "507f1f77bcf86cd799439011", - "email": "email@email.com", - }, - }, - }, - }, nil, - ).Once() - - mockAPI.On("Request", "PATCH", "/api/users/507f1f77bcf86cd799439011", "mock.Anything", 200).Return( - &controllers.Response{ - Body: map[string]any{ - "message": "successfully updated user roles", - }, - }, nil, - ).Once() - - err := controller.AddRole("email@email.com", "manager", "*") - assert.Nil(t, err) -} diff --git a/CLI/controllers/controllerUtils.go b/CLI/controllers/controllerUtils.go deleted file mode 100644 index 8a6a8e08c..000000000 --- a/CLI/controllers/controllerUtils.go +++ /dev/null @@ -1,126 +0,0 @@ -package controllers - -//This file has a collection of utility functions used in the -//controller package -//And const definitions used throughout the controllers package -import ( - "encoding/json" - "fmt" - "path" - "strings" -) - -// Debug Level Declaration -const ( - NONE = iota - ERROR - WARNING - INFO - DEBUG -) - -const RACKUNIT = .04445 //meter -const VIRTUALCONFIG = "virtual_config" - -// displays contents of maps -func Disp(x map[string]interface{}) { - - jx, _ := json.Marshal(x) - - println("JSON: ", string(jx)) -} - -func DispWithAttrs(objs []interface{}, attrs []string) { - for _, objInf := range objs { - if obj, ok := objInf.(map[string]interface{}); ok { - for _, a := range attrs { - //Check if attr is in object - if ok, nested := AttrIsInObj(obj, a); ok { - if nested { - fmt.Print("\t"+a+":", - obj["attributes"].(map[string]interface{})[a]) - } else { - fmt.Print("\t"+a+":", obj[a]) - } - } else { - fmt.Print("\t" + a + ": NULL") - } - } - fmt.Printf("\tName:%s\n", obj["name"].(string)) - } - } -} - -// Returns true/false if exists and true/false if attr -// is in "attributes" maps -func AttrIsInObj(obj map[string]interface{}, attr string) (bool, bool) { - if _, ok := obj[attr]; ok { - return ok, false - } - - if hasAttr, _ := AttrIsInObj(obj, "attributes"); hasAttr == true { - if objAttributes, ok := obj["attributes"].(map[string]interface{}); ok { - _, ok := objAttributes[attr] - return ok, true - } - } - - return false, false -} - -func TranslatePath(p string, acceptSelection bool) string { - if p == "" { - p = "." - } - if p == "_" && acceptSelection { - return "_" - } - if p == "-" { - return State.PrevPath - } - var output_words []string - if p[0] != '/' { - outputBase := State.CurrPath - if p[0] == '-' { - outputBase = State.PrevPath - } - - output_words = strings.Split(outputBase, "/")[1:] - if len(output_words) == 1 && output_words[0] == "" { - output_words = output_words[0:0] - } - } else { - p = p[1:] - } - input_words := strings.Split(p, "/") - for i, word := range input_words { - if word == "." || (i == 0 && word == "-") { - continue - } else if word == ".." { - if len(output_words) > 0 { - output_words = output_words[:len(output_words)-1] - } - } else { - output_words = append(output_words, word) - } - } - if len(output_words) > 0 { - if output_words[0] == "P" { - output_words[0] = "Physical" - } else if output_words[0] == "L" { - output_words[0] = "Logical" - } else if output_words[0] == "O" { - output_words[0] = "Organisation" - } - } - return path.Clean("/" + strings.Join(output_words, "/")) -} - -type ErrorWithInternalError struct { - UserError error - InternalError error -} - -func (err ErrorWithInternalError) Error() string { - return err.UserError.Error() + " caused by " + err.InternalError.Error() -} diff --git a/CLI/controllers/create.go b/CLI/controllers/create.go index 419707c13..9989cda6c 100644 --- a/CLI/controllers/create.go +++ b/CLI/controllers/create.go @@ -1,12 +1,11 @@ package controllers import ( - l "cli/logger" "cli/models" + "cli/utils" "fmt" "net/http" pathutil "path" - "strconv" ) func (controller Controller) PostObj(ent int, entity string, data map[string]any, path string) error { @@ -32,310 +31,93 @@ func (controller Controller) PostObj(ent int, entity string, data map[string]any return nil } -func (controller Controller) CreateObject(path string, ent int, data map[string]any) error { - var parent map[string]any +func (controller Controller) ValidateObj(ent int, entity string, data map[string]any, path string) error { + resp, err := controller.API.Request(http.MethodPost, "/api/validate/"+entity+"s", data, http.StatusOK) + if err != nil { + fmt.Println(err) + fmt.Println("RESP:") + fmt.Println(resp) + return err + } + + return nil +} + +func (controller Controller) CreateObject(path string, ent int, data map[string]any, validate ...bool) error { + isValidate := false + if len(validate) > 0 { + // if true, dry run (no API requests) + isValidate = validate[0] + } - name := pathutil.Base(path) - path = pathutil.Dir(path) - if name == "." || name == "" { - l.GetWarningLogger().Println("Invalid path name provided for OCLI object creation") - return fmt.Errorf("Invalid path name provided for OCLI object creation") + // Object base data + if err := models.SetObjectBaseData(ent, path, data); err != nil { + return err } - data["name"] = name - data["category"] = models.EntityToString(ent) - data["description"] = "" - //Retrieve Parent + // Retrieve parent + parentId, parent, err := controller.GetParentFromPath(pathutil.Dir(path), ent, isValidate) + if err != nil { + return err + } if ent != models.SITE && ent != models.STRAY_DEV { - var err error - parent, err = controller.PollObject(path) - if err != nil { - return err - } - if parent == nil && (ent != models.DOMAIN || path != "/Organisation/Domain") && - ent != models.VIRTUALOBJ { - return fmt.Errorf("parent not found") - } + data["parentId"] = parentId } + // Set domain if ent != models.DOMAIN { - if parent != nil { - data["domain"] = parent["domain"] - } else { + if parent == nil || isValidate { data["domain"] = State.Customer + } else { + data["domain"] = parent["domain"] } } - attr, hasAttributes := data["attributes"].(map[string]any) - if !hasAttributes { - attr = map[string]any{} - } - - var err error + // Attributes + attr := data["attributes"].(map[string]any) switch ent { - case models.DOMAIN: - if parent != nil { - data["parentId"] = parent["id"] - } else { - data["parentId"] = "" - } - - case models.SITE: - break - - case models.BLDG: - //Check for template - if _, ok := attr["template"]; ok { - err := controller.ApplyTemplate(attr, data, models.BLDG) - if err != nil { - return err - } - } else { - //Serialise size and posXY manually instead - attr["size"] = serialiseVector(attr, "size") - } - - if _, ok := attr["size"].([]any); !ok { - if size, ok := attr["size"].([]float64); !ok || len(size) == 0 { - if State.DebugLvl > 0 { - l.GetErrorLogger().Println( - "User gave invalid size value for creating building") - return fmt.Errorf("Invalid size attribute provided." + - " \nIt must be an array/list/vector with 3 elements." + - " Please refer to the wiki or manual reference" + - " for more details on how to create objects " + - "using this syntax") - } - return nil - } - } - - attr["posXY"] = serialiseVector(attr, "posXY") - if posXY, ok := attr["posXY"].([]float64); !ok || len(posXY) != 2 { - if State.DebugLvl > 0 { - l.GetErrorLogger().Println( - "User gave invalid posXY value for creating building") - return fmt.Errorf("Invalid posXY attribute provided." + - " \nIt must be an array/list/vector with 2 elements." + - " Please refer to the wiki or manual reference" + - " for more details on how to create objects " + - "using this syntax") - } - return nil - } - - attr["posXYUnit"] = "m" - attr["sizeUnit"] = "m" - attr["heightUnit"] = "m" - //attr["height"] = 0 //Should be set from parser by default - data["parentId"] = parent["id"] - - case models.ROOM: - baseAttrs := map[string]any{ - "floorUnit": "t", - "posXYUnit": "m", - "sizeUnit": "m", - "heightUnit": "m", - } - - MergeMaps(attr, baseAttrs, false) - - //If user provided templates, get the JSON - //and parse into templates - //NOTE this function also assigns value for "size" attribute - err := controller.ApplyTemplate(attr, data, ent) - if err != nil { + case models.BLDG, models.ROOM, models.RACK, models.CORRIDOR, models.GENERIC: + utils.MergeMaps(attr, models.BaseAttrs[ent], false) + if hasTemplate, err := controller.ApplyTemplateOrSetSize(attr, data, ent, + isValidate); err != nil { return err - } - - attr["posXY"] = serialiseVector(attr, "posXY") - - if posXY, ok := attr["posXY"].([]float64); !ok || len(posXY) != 2 { - if State.DebugLvl > 0 { - l.GetErrorLogger().Println( - "User gave invalid posXY value for creating room") - return fmt.Errorf("Invalid posXY attribute provided." + - " \nIt must be an array/list/vector with 2 elements." + - " Please refer to the wiki or manual reference" + - " for more details on how to create objects " + - "using this syntax") - } + } else if hasTemplate && isValidate { return nil } - if _, ok := attr["size"].([]any); !ok { - if size, ok := attr["size"].([]float64); !ok || len(size) == 0 { - if State.DebugLvl > 0 { - l.GetErrorLogger().Println( - "User gave invalid size value for creating room") - return fmt.Errorf("Invalid size attribute provided." + - " \nIt must be an array/list/vector with 3 elements." + - " Please refer to the wiki or manual reference" + - " for more details on how to create objects " + - "using this syntax") - } - return nil - } - } - - data["parentId"] = parent["id"] - if State.DebugLvl >= 3 { - println("DEBUG VIEW THE JSON") - Disp(data) - } - - case models.RACK, models.CORRIDOR, models.GENERIC: - baseAttrs := map[string]any{ - "sizeUnit": "cm", - "heightUnit": "U", - } - if ent == models.CORRIDOR || ent == models.GENERIC { - baseAttrs["heightUnit"] = "cm" - } - - MergeMaps(attr, baseAttrs, false) - - //If user provided templates, get the JSON - //and parse into templates - err := controller.ApplyTemplate(attr, data, ent) - if err != nil { + if err := models.SetPosAttr(ent, attr); err != nil { return err } - - if _, ok := attr["size"].([]any); !ok { - if size, ok := attr["size"].([]float64); !ok || len(size) == 0 { - if State.DebugLvl > 0 { - l.GetErrorLogger().Println( - "User gave invalid size value for creating rack") - return fmt.Errorf("Invalid size attribute/template provided." + - " \nThe size must be an array/list/vector with " + - "3 elements." + "\n\nIf you have provided a" + - " template, please check that you are referring to " + - "an existing template" + - "\n\nFor more information " + - "please refer to the wiki or manual reference" + - " for more details on how to create objects " + - "using this syntax") - } - return nil - } - } - - //Serialise posXY if given - attr["posXYZ"] = serialiseVector(attr, "posXYZ") - - data["parentId"] = parent["id"] - case models.DEVICE: - //Special routine to perform on device - //based on if the parent has a "slot" attribute - - //First check if attr has only posU & sizeU - //reject if true while also converting sizeU to string if numeric - //if len(attr) == 2 { - if sizeU, ok := attr["sizeU"]; ok { - sizeUValid := checkNumeric(attr["sizeU"]) - - if _, ok := attr["template"]; !ok && !sizeUValid { - l.GetWarningLogger().Println("Invalid template / sizeU parameter provided for device ") - return fmt.Errorf("Please provide a valid device template or sizeU") - } - - //Convert block - //And Set height - if sizeUInt, ok := sizeU.(int); ok { - attr["sizeU"] = sizeUInt - attr["height"] = float64(sizeUInt) * 44.5 - } else if sizeUFloat, ok := sizeU.(float64); ok { - attr["sizeU"] = sizeUFloat - attr["height"] = sizeUFloat * 44.5 - } - //End of convert block - if _, ok := attr["slot"]; ok { - l.GetWarningLogger().Println("Invalid device syntax encountered") - return fmt.Errorf("Invalid device syntax: If you have provided a template, it was not found") - } - } - //} - - //Process the posU/slot attribute - if x, ok := attr["posU/slot"].([]string); ok && len(x) > 0 { - delete(attr, "posU/slot") - if posU, err := strconv.Atoi(x[0]); len(x) == 1 && err == nil { - attr["posU"] = posU - } else { - if slots, err := ExpandStrVector(x); err != nil { - return err - } else { - attr["slot"] = slots - } - } - } - - //If user provided templates, get the JSON - //and parse into templates - if _, ok := attr["template"]; ok { - err := controller.ApplyTemplate(attr, data, models.DEVICE) - if err != nil { - return err - } - } else { - var slot map[string]any - if attr["slot"] != nil { - slots := attr["slot"].([]string) - if len(slots) != 1 { - return fmt.Errorf("Invalid device syntax: only one slot can be provided if no template") - } - slot, err = C.GetSlot(parent, slots[0]) - if err != nil { - return err - } - } - if slot != nil { - size := slot["elemSize"].([]any) - attr["size"] = []float64{size[0].(float64) / 10., size[1].(float64) / 10.} - } else { - if parAttr, ok := parent["attributes"].(map[string]interface{}); ok { - if rackSize, ok := parAttr["size"]; ok { - attr["size"] = rackSize - } - } - } + models.SetDeviceSizeUIfExists(attr) + if err := models.SetDeviceSlotOrPosU(attr); err != nil { + return err } - //End of device special routine - baseAttrs := map[string]interface{}{ - "orientation": "front", - "sizeUnit": "mm", - "heightUnit": "mm", + if hasTemplate, err := controller.ApplyTemplateIfExists(attr, data, ent, + isValidate); !hasTemplate { + // apply user input + setDevNoTemplateSize(attr, parent, isValidate) + } else if err != nil { + return err + } else if isValidate { + return nil } - MergeMaps(attr, baseAttrs, false) - - data["parentId"] = parent["id"] - - case models.GROUP: - data["parentId"] = parent["id"] - + utils.MergeMaps(attr, models.DeviceBaseAttrs, false) case models.STRAY_DEV: - if _, ok := attr["template"]; ok { - err := controller.ApplyTemplate(attr, data, models.DEVICE) - if err != nil { - return err - } - } - - case models.VIRTUALOBJ: - if parent != nil { - data["parentId"] = parent["id"] + if _, err := controller.ApplyTemplateIfExists(attr, data, ent, + isValidate); err != nil { + return err } default: - //Execution should not reach here! - return fmt.Errorf("Invalid Object Specified!") + break } - data["attributes"] = attr + if isValidate { + return controller.ValidateObj(ent, data["category"].(string), data, path) + } return controller.PostObj(ent, data["category"].(string), data, path) } @@ -346,3 +128,45 @@ func (controller Controller) CreateTag(slug, color string) error { "color": color, }, models.TagsPath+slug) } + +func setDevNoTemplateSize(attr map[string]any, parent map[string]any, isValidate bool) error { + var slot map[string]any + var err error + // get slot, if possible + if slot, err = getSlotDevNoTemplate(attr, parent, isValidate); err != nil { + return err + } + if slot != nil { + // apply size from slot + size := slot["elemSize"].([]any) + attr["size"] = []float64{size[0].(float64) / 10., size[1].(float64) / 10.} + } else { + if isValidate { + // apply random size to validate + attr["size"] = []float64{10, 10, 10} + } else if parAttr, ok := parent["attributes"].(map[string]interface{}); ok { + if rackSize, ok := parAttr["size"]; ok { + // apply size from rack + attr["size"] = rackSize + } + } + } + return nil +} + +func getSlotDevNoTemplate(attr map[string]any, parent map[string]any, isValidate bool) (map[string]any, error) { + // get slot (no template -> only one slot accepted) + if attr["slot"] != nil { + slots := attr["slot"].([]string) + if len(slots) != 1 { + return nil, fmt.Errorf("invalid device syntax: only one slot can be provided if no template") + } + if !isValidate { + slot, err := C.GetSlot(parent, slots[0]) + if err != nil { + return slot, err + } + } + } + return nil, nil +} diff --git a/CLI/controllers/create_test.go b/CLI/controllers/create_test.go index ec7d992c9..f8c124496 100644 --- a/CLI/controllers/create_test.go +++ b/CLI/controllers/create_test.go @@ -2,6 +2,7 @@ package controllers_test import ( "cli/controllers" + l "cli/logger" "cli/models" test_utils "cli/test" "maps" @@ -43,13 +44,17 @@ var createRoom = map[string]any{ "domain": "test-domain", } +func init() { + l.InitLogs() +} + func TestCreateObjectPathErrors(t *testing.T) { tests := []struct { name string path string errorMessage string }{ - {"InvalidPath", "/.", "Invalid path name provided for OCLI object creation"}, + {"InvalidPath", "/.", "invalid path name provided for OCLI object creation"}, {"ParentNotFound", "/", "parent not found"}, } @@ -150,7 +155,7 @@ func TestCreateGenericWithTemplateWorks(t *testing.T) { "posXYUnit": "m", "template": "generic-template", }, - }) + }, false) assert.Nil(t, err) } @@ -194,18 +199,9 @@ func TestCreateBuildingInvalidSize(t *testing.T) { buildingInvalidSize["attributes"].(map[string]any)["size"] = "[1,2,3]" test_utils.MockGetObject(mockAPI, baseSite) - - // with state.DebugLvl = 0 err := controller.CreateObject("/Physical/BASIC/A", models.BLDG, buildingInvalidSize) - // returns nil but the object is not created - assert.Nil(t, err) - - // with state.DebugLvl > 0 - controllers.State.DebugLvl = 1 - test_utils.MockGetObject(mockAPI, baseSite) - err = controller.CreateObject("/Physical/BASIC/A", models.BLDG, buildingInvalidSize) assert.NotNil(t, err) - assert.ErrorContains(t, err, "Invalid size attribute provided."+ + assert.ErrorContains(t, err, "invalid size attribute provided."+ " \nIt must be an array/list/vector with 3 elements."+ " Please refer to the wiki or manual reference"+ " for more details on how to create objects "+ @@ -219,18 +215,10 @@ func TestCreateBuildingInvalidPosXY(t *testing.T) { buildingInvalidPosXY := maps.Clone(baseBuilding) buildingInvalidPosXY["attributes"].(map[string]any)["posXY"] = []float64{} - // with state.DebugLvl = 0 test_utils.MockGetObject(mockAPI, baseSite) - err := controller.CreateObject("/Physical/BASIC/A", models.BLDG, maps.Clone(buildingInvalidPosXY)) - // returns nil but the object is not created - assert.Nil(t, err) - - // with state.DebugLvl > 0 - controllers.State.DebugLvl = 1 - test_utils.MockGetObject(mockAPI, baseSite) - err = controller.CreateObject("/Physical/BASIC/A", models.BLDG, buildingInvalidPosXY) + err := controller.CreateObject("/Physical/BASIC/A", models.BLDG, buildingInvalidPosXY) assert.NotNil(t, err) - assert.ErrorContains(t, err, "Invalid posXY attribute provided."+ + assert.ErrorContains(t, err, "invalid posXY attribute provided."+ " \nIt must be an array/list/vector with 2 elements."+ " Please refer to the wiki or manual reference"+ " for more details on how to create objects "+ @@ -286,17 +274,10 @@ func TestCreateRoomInvalidSize(t *testing.T) { }, } - // with state.DebugLvl = 0 test_utils.MockGetObject(mockAPI, roomsBuilding) err := controller.CreateObject("/Physical/BASIC/A/R1", models.ROOM, room) - assert.Nil(t, err) - - // with state.DebugLvl > 0 - controllers.State.DebugLvl = 1 - test_utils.MockGetObject(mockAPI, roomsBuilding) - err = controller.CreateObject("/Physical/BASIC/A/R1", models.ROOM, room) assert.NotNil(t, err) - assert.ErrorContains(t, err, "Invalid size attribute provided."+ + assert.ErrorContains(t, err, "invalid size attribute provided."+ " \nIt must be an array/list/vector with 3 elements."+ " Please refer to the wiki or manual reference"+ " for more details on how to create objects "+ @@ -325,18 +306,10 @@ func TestCreateRoomInvalidPosXY(t *testing.T) { }, } - // with state.DebugLvl = 0 - test_utils.MockGetObject(mockAPI, roomsBuilding) - - err := controller.CreateObject("/Physical/BASIC/A/R1", models.ROOM, test_utils.CopyMap(room)) - assert.Nil(t, err) - - // with state.DebugLvl > 0 - controllers.State.DebugLvl = 1 test_utils.MockGetObject(mockAPI, roomsBuilding) - err = controller.CreateObject("/Physical/BASIC/A/R1", models.ROOM, room) + err := controller.CreateObject("/Physical/BASIC/A/R1", models.ROOM, room) assert.NotNil(t, err) - assert.ErrorContains(t, err, "Invalid posXY attribute provided."+ + assert.ErrorContains(t, err, "invalid posXY attribute provided."+ " \nIt must be an array/list/vector with 2 elements."+ " Please refer to the wiki or manual reference"+ " for more details on how to create objects "+ @@ -415,25 +388,10 @@ func TestCreateRackInvalidSize(t *testing.T) { }, } - // with state.DebugLvl = 0 test_utils.MockGetObject(mockAPI, room) err := controller.CreateObject("/Physical/BASIC/A/R1/A01", models.RACK, rack) - assert.Nil(t, err) - - // with state.DebugLvl > 0 - controllers.State.DebugLvl = 1 - test_utils.MockGetObject(mockAPI, room) - err = controller.CreateObject("/Physical/BASIC/A/R1/A01", models.RACK, rack) assert.NotNil(t, err) - assert.ErrorContains(t, err, "Invalid size attribute/template provided."+ - " \nThe size must be an array/list/vector with "+ - "3 elements."+"\n\nIf you have provided a"+ - " template, please check that you are referring to "+ - "an existing template"+ - "\n\nFor more information "+ - "please refer to the wiki or manual reference"+ - " for more details on how to create objects "+ - "using this syntax") + assert.ErrorContains(t, err, "invalid size attribute provided.") controllers.State.DebugLvl = 0 } @@ -576,3 +534,129 @@ func TestCreateTag(t *testing.T) { err := controller.CreateTag(slug, color) assert.Nil(t, err) } + +// Tests GetSlot +func TestGetSlotWithNoTemplate(t *testing.T) { + rack := map[string]any{ + "attributes": map[string]any{}, + } + result, err := controllers.C.GetSlot(rack, "") + assert.Nil(t, err) + assert.Nil(t, result) + + rack["attributes"].(map[string]any)["template"] = "" + result, err = controllers.C.GetSlot(rack, "") + assert.Nil(t, err) + assert.Nil(t, result) +} + +func TestGetSlotWithTemplateNonExistentSlot(t *testing.T) { + controller, mockAPI, _ := layersSetup(t) + + template := map[string]any{ + "slug": "rack-template", + "description": "", + "category": "rack", + "sizeWDHmm": []any{605, 1200, 2003}, + "fbxModel": "", + "attributes": map[string]any{ + "vendor": "IBM", + "model": "9360-4PX", + }, + "slots": []any{}, + } + + test_utils.MockGetObjTemplate(mockAPI, template) + rack := map[string]any{ + "attributes": map[string]any{ + "template": "rack-template", + }, + } + _, err := controller.GetSlot(rack, "u02") + assert.NotNil(t, err) + assert.Equal(t, "the slot u02 does not exist", err.Error()) +} + +func TestGetSlotWithTemplateWorks(t *testing.T) { + controller, mockAPI, _ := layersSetup(t) + slot := map[string]any{ + "location": "u01", + "type": "u", + "elemOrient": []any{33.3, -44.4, 107}, + "elemPos": []any{58, 51, 44.45}, + "elemSize": []any{482.6, 1138, 44.45}, + "mandatory": "no", + "labelPos": "frontrear", + } + + template := map[string]any{ + "slug": "rack-template", + "description": "", + "category": "rack", + "sizeWDHmm": []any{605, 1200, 2003}, + "fbxModel": "", + "attributes": map[string]any{ + "vendor": "IBM", + "model": "9360-4PX", + }, + "slots": []any{ + slot, + }, + } + + test_utils.MockGetObjTemplate(mockAPI, template) + rack := map[string]any{ + "attributes": map[string]any{ + "template": "rack-template", + }, + } + result, err := controller.GetSlot(rack, "u01") + assert.Nil(t, err) + assert.Equal(t, slot["location"], result["location"]) +} + +// Tests GetByAttr +func TestGetByAttrErrorWhenObjIsNotRack(t *testing.T) { + controller, mockAPI, _ := layersSetup(t) + + test_utils.MockGetObjectHierarchy(mockAPI, chassis) + + err := controller.GetByAttr(models.PhysicalPath+"BASIC/A/R1/A01/chT", "colors") + assert.NotNil(t, err) + assert.Equal(t, "command may only be performed on rack objects", err.Error()) +} + +func TestGetByAttrErrorWhenObjIsRackWithSlotName(t *testing.T) { + controller, mockAPI, _ := layersSetup(t) + + rack := test_utils.CopyMap(rack1) + rack["attributes"] = map[string]any{ + "slot": []any{ + map[string]any{ + "location": "u01", + "type": "u", + "elemOrient": []any{33.3, -44.4, 107}, + "elemPos": []any{58, 51, 44.45}, + "elemSize": []any{482.6, 1138, 44.45}, + "mandatory": "no", + "labelPos": "frontrear", + "color": "@color1", + }, + }, + } + test_utils.MockGetObjectHierarchy(mockAPI, rack) + + err := controller.GetByAttr(models.PhysicalPath+"BASIC/A/R1/A01", "u01") + assert.Nil(t, err) +} + +func TestGetByAttrErrorWhenObjIsRackWithHeight(t *testing.T) { + controller, mockAPI, _ := layersSetup(t) + + rack := test_utils.CopyMap(rack1) + rack["height"] = "47" + test_utils.MockGetObjectHierarchy(mockAPI, rack) + + err := controller.GetByAttr(models.PhysicalPath+"BASIC/A/R1/A01", 47) + assert.Nil(t, err) +} diff --git a/CLI/controllers/delete.go b/CLI/controllers/delete.go index b8336a32e..e8e584c2f 100644 --- a/CLI/controllers/delete.go +++ b/CLI/controllers/delete.go @@ -2,7 +2,9 @@ package controllers import ( "cli/models" + "fmt" "net/http" + "strings" ) func (controller Controller) DeleteObj(path string) ([]string, error) { @@ -13,8 +15,9 @@ func (controller Controller) DeleteObj(path string) ([]string, error) { var resp *Response if models.PathHasLayer(path) { + var pathSplit models.Path filters := map[string]string{} - pathSplit, err := controller.SplitPath(path) + pathSplit, err = controller.SplitPath(path) if err != nil { return nil, err } @@ -41,3 +44,34 @@ func (controller Controller) DeleteObj(path string) ([]string, error) { } return paths, nil } + +func (controller Controller) UnsetAttribute(path string, attr string) error { + obj, err := controller.GetObject(path) + if err != nil { + return err + } + delete(obj, "id") + delete(obj, "lastUpdated") + delete(obj, "createdDate") + attributes, hasAttributes := obj["attributes"].(map[string]any) + if !hasAttributes { + return fmt.Errorf("object has no attributes") + } + if vconfigAttr, found := strings.CutPrefix(attr, VIRTUALCONFIG+"."); found { + if len(vconfigAttr) < 1 { + return fmt.Errorf("invalid attribute name") + } else if vAttrs, ok := attributes[VIRTUALCONFIG].(map[string]any); !ok { + return fmt.Errorf("object has no " + VIRTUALCONFIG) + } else { + delete(vAttrs, vconfigAttr) + } + } else { + delete(attributes, attr) + } + url, err := controller.ObjectUrl(path, 0) + if err != nil { + return err + } + _, err = controller.API.Request("PUT", url, obj, http.StatusOK) + return err +} diff --git a/CLI/controllers/delete_test.go b/CLI/controllers/delete_test.go index 069b88f97..eeae465ae 100644 --- a/CLI/controllers/delete_test.go +++ b/CLI/controllers/delete_test.go @@ -9,6 +9,8 @@ import ( "github.com/stretchr/testify/assert" ) +const testRackObjPath = "/api/hierarchy_objects/BASIC.A.R1.A01" + func TestDeleteTag(t *testing.T) { controller, mockAPI, _, _ := test_utils.NewControllerWithMocks(t) @@ -28,3 +30,45 @@ func TestDeleteTag(t *testing.T) { _, err := controller.DeleteObj(path) assert.Nil(t, err) } + +// Tests UnsetAttribute +func TestUnsetAttributeObjectNotFound(t *testing.T) { + controller, mockAPI, _ := layersSetup(t) + + test_utils.MockObjectNotFound(mockAPI, testRackObjPath) + + err := controller.UnsetAttribute("/Physical/BASIC/A/R1/A01", "color") + assert.NotNil(t, err) + assert.Equal(t, "object not found", err.Error()) +} + +func TestUnsetAttributeWorks(t *testing.T) { + controller, mockAPI, _ := layersSetup(t) + rack := map[string]any{ + "category": "rack", + "id": "BASIC.A.R1.A01", + "name": "A01", + "parentId": "BASIC.A.R1", + "domain": "test-domain", + "description": "", + "attributes": map[string]any{ + "height": "47", + "heightUnit": "U", + "rotation": `[45, 45, 45]`, + "posXYZ": `[4.6666666666667, -2, 0]`, + "posXYUnit": "m", + "size": `[1, 1]`, + "sizeUnit": "cm", + "color": "00ED00", + }, + } + updatedRack := test_utils.CopyMap(rack) + delete(updatedRack["attributes"].(map[string]any), "color") + delete(updatedRack, "id") + + test_utils.MockGetObject(mockAPI, rack) + test_utils.MockPutObject(mockAPI, updatedRack, updatedRack) + + err := controller.UnsetAttribute("/Physical/BASIC/A/R1/A01", "color") + assert.Nil(t, err) +} diff --git a/CLI/controllers/draw.go b/CLI/controllers/draw.go index 781d2e97f..e5ec5a0a9 100644 --- a/CLI/controllers/draw.go +++ b/CLI/controllers/draw.go @@ -1,6 +1,7 @@ package controllers import ( + "cli/models" "fmt" "strconv" ) @@ -107,3 +108,59 @@ func (controller Controller) Undraw(path string) error { return nil } + +func IsCategoryAttrDrawable(category string, attr string) bool { + templateJson := State.DrawableJsons[category] + if templateJson == nil { + return true + } + + switch attr { + case "id", "name", "category", "parentID", + "description", "domain", "parentid", "parentId", "tags": + if valBool, ok := templateJson[attr].(bool); ok { + return valBool + } + return false + default: + if attributes, ok := templateJson["attributes"].(map[string]interface{}); ok { + if valBool, ok := attributes[attr].(bool); ok { + return valBool + } + } + return false + } +} + +func (controller Controller) IsEntityDrawable(path string) (bool, error) { + obj, err := controller.GetObject(path) + if err != nil { + return false, err + } + if catInf, ok := obj["category"]; ok { + if category, ok := catInf.(string); ok { + return IsDrawableEntity(category), nil + } + } + return false, nil +} + +func IsDrawableEntity(x string) bool { + entInt := models.EntityStrToInt(x) + + for idx := range State.DrawableObjs { + if State.DrawableObjs[idx] == entInt { + return true + } + } + return false +} + +func (controller Controller) IsAttrDrawable(path string, attr string) (bool, error) { + obj, err := controller.GetObject(path) + if err != nil { + return false, err + } + category := obj["category"].(string) + return IsCategoryAttrDrawable(category, attr), nil +} diff --git a/CLI/controllers/draw_test.go b/CLI/controllers/draw_test.go new file mode 100644 index 000000000..b61f10f88 --- /dev/null +++ b/CLI/controllers/draw_test.go @@ -0,0 +1,104 @@ +package controllers_test + +import ( + "cli/controllers" + "cli/models" + test_utils "cli/test" + "testing" + + "github.com/stretchr/testify/assert" +) + +// Tests IsEntityDrawable +func TestIsEntityDrawableObjectNotFound(t *testing.T) { + controller, mockAPI, _ := layersSetup(t) + + test_utils.MockObjectNotFound(mockAPI, testRackObjPath) + + isDrawable, err := controller.IsEntityDrawable(models.PhysicalPath + "BASIC/A/R1/A01") + assert.False(t, isDrawable) + assert.NotNil(t, err) + assert.Equal(t, "object not found", err.Error()) +} + +func TestIsEntityDrawable(t *testing.T) { + tests := []struct { + name string + drawableObjects []int + expectedIsDrawable bool + }{ + {"CategoryIsNotDrawable", []int{models.EntityStrToInt("device")}, false}, + {"CategoryIsDrawable", []int{models.EntityStrToInt("rack")}, true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + controller, mockAPI, _ := layersSetup(t) + controllers.State.DrawableObjs = tt.drawableObjects + + test_utils.MockGetObject(mockAPI, rack1) + + isDrawable, err := controller.IsEntityDrawable(models.PhysicalPath + "BASIC/A/R1/A01") + assert.Equal(t, tt.expectedIsDrawable, isDrawable) + assert.Nil(t, err) + }) + } +} + +// Tests IsAttrDrawable (and IsCategoryAttrDrawable) +func TestIsAttrDrawableObjectNotFound(t *testing.T) { + controller, mockAPI, _ := layersSetup(t) + path := testRackObjPath + + test_utils.MockObjectNotFound(mockAPI, path) + + isAttrDrawable, err := controller.IsAttrDrawable(models.PhysicalPath+"BASIC/A/R1/A01", "color") + assert.False(t, isAttrDrawable) + assert.NotNil(t, err) + assert.Equal(t, "object not found", err.Error()) +} + +func TestIsAttrDrawableTemplateJsonIsNil(t *testing.T) { + controller, mockAPI, _ := layersSetup(t) + controllers.State.DrawableObjs = []int{models.EntityStrToInt("rack")} + + controllers.State.DrawableJsons = map[string]map[string]any{ + "rack": nil, + } + + test_utils.MockGetObject(mockAPI, rack1) + + isAttrDrawable, err := controller.IsAttrDrawable(models.PhysicalPath+"BASIC/A/R1/A01", "color") + assert.True(t, isAttrDrawable) + assert.Nil(t, err) +} + +func TestIsAttrDrawable(t *testing.T) { + tests := []struct { + name string + attributeDrawable string + attributeNonDrawable string + }{ + {"SpecialAttribute", "name", "description"}, + {"SpecialAttribute", "color", "height"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + controller, mockAPI, _ := layersSetup(t) + controllers.State.DrawableObjs = []int{models.EntityStrToInt("rack")} + + controllers.State.DrawableJsons = test_utils.GetTestDrawableJson() + + test_utils.MockGetObject(mockAPI, rack1) + isAttrDrawable, err := controller.IsAttrDrawable(models.PhysicalPath+"BASIC/A/R1/A01", tt.attributeDrawable) + assert.True(t, isAttrDrawable) + assert.Nil(t, err) + + test_utils.MockGetObject(mockAPI, rack1) + isAttrDrawable, err = controller.IsAttrDrawable(models.PhysicalPath+"BASIC/A/R1/A01", tt.attributeNonDrawable) + assert.False(t, isAttrDrawable) + assert.Nil(t, err) + }) + } +} diff --git a/CLI/controllers/get.go b/CLI/controllers/get.go index 03988be40..5ae90a665 100644 --- a/CLI/controllers/get.go +++ b/CLI/controllers/get.go @@ -3,9 +3,11 @@ package controllers import ( "cli/models" "cli/utils" + "cli/views" "errors" "fmt" "net/http" + "strconv" "strings" ) @@ -62,7 +64,7 @@ func (controller Controller) ParseWildcardResponse(resp *Response, pathStr strin return nil, nil, err } - objs := infArrToMapStrinfArr(objsAny) + objs := utils.AnyArrToMapArr(objsAny) paths := []string{} for _, obj := range objs { var suffix string @@ -123,3 +125,70 @@ func (controller Controller) PollObjectWithChildren(path string, depth int) (map return obj, nil } + +func (controller Controller) GetByAttr(path string, u interface{}) error { + obj, err := controller.GetObjectWithChildren(path, 1) + if err != nil { + return err + } + cat := obj["category"].(string) + if cat != "rack" { + return fmt.Errorf("command may only be performed on rack objects") + } + children := obj["children"].([]any) + devices := utils.AnyArrToMapArr(children) + switch u.(type) { + case int: + for i := range devices { + if attr, ok := devices[i]["attributes"].(map[string]interface{}); ok { + uStr := strconv.Itoa(u.(int)) + if attr["height"] == uStr { + views.DisplayJson("", devices[i]) + return nil //What if the user placed multiple devices at same height? + } + } + } + if State.DebugLvl > NONE { + println("The 'U' you provided does not correspond to any device in this rack") + } + default: //String + for i := range devices { + if attr, ok := devices[i]["attributes"].(map[string]interface{}); ok { + if attr["slot"] == u.(string) { + views.DisplayJson("", devices[i]) + return nil //What if the user placed multiple devices at same slot? + } + } + } + if State.DebugLvl > NONE { + println("The slot you provided does not correspond to any device in this rack") + } + } + return nil +} + +func (controller Controller) GetSlot(rack map[string]any, location string) (map[string]any, error) { + templateAny, ok := rack["attributes"].(map[string]any)["template"] + if !ok { + return nil, nil + } + template := templateAny.(string) + if template == "" { + return nil, nil + } + resp, err := controller.API.Request("GET", "/api/obj_templates/"+template, nil, http.StatusOK) + if err != nil { + return nil, err + } + slots, ok := resp.Body["data"].(map[string]any)["slots"] + if !ok { + return nil, nil + } + for _, slotAny := range slots.([]any) { + slot := slotAny.(map[string]any) + if slot["location"] == location { + return slot, nil + } + } + return nil, fmt.Errorf("the slot %s does not exist", location) +} diff --git a/CLI/controllers/initController.go b/CLI/controllers/init.go similarity index 99% rename from CLI/controllers/initController.go rename to CLI/controllers/init.go index cb4162a41..18a1d023a 100755 --- a/CLI/controllers/initController.go +++ b/CLI/controllers/init.go @@ -42,7 +42,7 @@ func InitDebugLevel(verbose string) { } func PingAPI() bool { - _, err := models.Send("", State.APIURL, "", nil) + _, err := Send("", State.APIURL, "", nil) return err == nil } diff --git a/CLI/controllers/layers_test.go b/CLI/controllers/layer_test.go similarity index 100% rename from CLI/controllers/layers_test.go rename to CLI/controllers/layer_test.go diff --git a/CLI/controllers/link.go b/CLI/controllers/link.go new file mode 100644 index 000000000..1939f277a --- /dev/null +++ b/CLI/controllers/link.go @@ -0,0 +1,45 @@ +package controllers + +import ( + "cli/models" + "fmt" + "net/http" + "strings" +) + +func (controller Controller) LinkObject(source string, destination string, attrs []string, values []any, slots []string) error { + sourceUrl, err := controller.ObjectUrl(source, 0) + if err != nil { + return err + } + destPath, err := controller.SplitPath(destination) + if err != nil { + return err + } + if !strings.HasPrefix(sourceUrl, "/api/stray_objects/") { + return fmt.Errorf("only stray objects can be linked") + } + payload := map[string]any{"parentId": destPath.ObjectID} + + if slots != nil { + if slots, err = models.CheckExpandStrVector(slots); err != nil { + return err + } + payload["slot"] = slots + } + + _, err = controller.API.Request("PATCH", sourceUrl+"/link", payload, http.StatusOK) + if err != nil { + return err + } + return nil +} + +func (controller Controller) UnlinkObject(path string) error { + sourceUrl, err := controller.ObjectUrl(path, 0) + if err != nil { + return err + } + _, err = controller.API.Request("PATCH", sourceUrl+"/unlink", nil, http.StatusOK) + return err +} diff --git a/CLI/controllers/link_test.go b/CLI/controllers/link_test.go new file mode 100644 index 000000000..6a661f97f --- /dev/null +++ b/CLI/controllers/link_test.go @@ -0,0 +1,99 @@ +package controllers_test + +import ( + "cli/models" + test_utils "cli/test" + "testing" + + "github.com/stretchr/testify/assert" +) + +// Tests LinkObject +func TestLinkObjectErrorNotStaryObject(t *testing.T) { + controller, _, _ := layersSetup(t) + + err := controller.LinkObject(models.PhysicalPath+"BASIC/A/R1/A01", models.PhysicalPath+"BASIC/A/R1/A01", []string{}, []any{}, []string{}) + assert.NotNil(t, err) + assert.Equal(t, "only stray objects can be linked", err.Error()) +} + +func TestLinkObjectWithoutSlots(t *testing.T) { + controller, mockAPI, _ := layersSetup(t) + + strayDevice := test_utils.CopyMap(chassis) + delete(strayDevice, "id") + delete(strayDevice, "parentId") + response := map[string]any{"message": "successfully linked"} + body := map[string]any{"parentId": "BASIC.A.R1.A01", "slot": []string{}} + + test_utils.MockUpdateObject(mockAPI, body, response) + + slots := []string{} + attributes := []string{} + values := []any{} + for key, value := range strayDevice["attributes"].(map[string]any) { + attributes = append(attributes, key) + values = append(values, value) + } + err := controller.LinkObject(models.StrayPath+"chT", models.PhysicalPath+"BASIC/A/R1/A01", attributes, values, slots) + assert.Nil(t, err) +} + +func TestLinkObjectWithInvalidSlots(t *testing.T) { + controller, _, _ := layersSetup(t) + + strayDevice := test_utils.CopyMap(chassis) + delete(strayDevice, "id") + delete(strayDevice, "parentId") + + slots := []string{"slot01..slot03", "slot4"} + attributes := []string{} + values := []any{} + for key, value := range strayDevice["attributes"].(map[string]any) { + attributes = append(attributes, key) + values = append(values, value) + } + err := controller.LinkObject(models.StrayPath+"chT", models.PhysicalPath+"BASIC/A/R1/A01", attributes, values, slots) + assert.NotNil(t, err) + assert.Equal(t, "Invalid device syntax: .. can only be used in a single element vector", err.Error()) +} + +func TestLinkObjectWithValidSlots(t *testing.T) { + controller, mockAPI, _ := layersSetup(t) + + strayDevice := test_utils.CopyMap(chassis) + delete(strayDevice, "id") + delete(strayDevice, "parentId") + response := map[string]any{"message": "successfully linked"} + body := map[string]any{"parentId": "BASIC.A.R1.A01", "slot": []string{"slot01"}} + + test_utils.MockUpdateObject(mockAPI, body, response) + + slots := []string{"slot01"} + attributes := []string{} + values := []any{} + for key, value := range strayDevice["attributes"].(map[string]any) { + attributes = append(attributes, key) + values = append(values, value) + } + err := controller.LinkObject(models.StrayPath+"chT", models.PhysicalPath+"BASIC/A/R1/A01", attributes, values, slots) + assert.Nil(t, err) +} + +// Tests UnlinkObject +func TestUnlinkObjectWithInvalidPath(t *testing.T) { + controller, _, _ := layersSetup(t) + + err := controller.UnlinkObject("/invalid/path") + assert.NotNil(t, err) + assert.Equal(t, "invalid object path", err.Error()) +} + +func TestUnlinkObjectWithValidPath(t *testing.T) { + controller, mockAPI, _ := layersSetup(t) + + test_utils.MockUpdateObject(mockAPI, nil, map[string]any{"message": "successfully unlinked"}) + + err := controller.UnlinkObject(models.PhysicalPath + "BASIC/A/R1/A01") + assert.Nil(t, err) +} diff --git a/CLI/controllers/ls.go b/CLI/controllers/ls.go index c46af25f5..57c7184f7 100644 --- a/CLI/controllers/ls.go +++ b/CLI/controllers/ls.go @@ -2,6 +2,7 @@ package controllers import ( "cli/models" + "cli/views" "errors" "fmt" "net/http" @@ -180,3 +181,12 @@ func addAutomaticLayers(rootNode *HierarchyNode) { } } } + +func LSEnterprise() error { + resp, err := API.Request("GET", "/api/stats", nil, http.StatusOK) + if err != nil { + return err + } + views.DisplayJson("", resp.Body) + return nil +} diff --git a/CLI/controllers/ogree3d.go b/CLI/controllers/ogree3d.go index e34cd5cf8..557d067bf 100644 --- a/CLI/controllers/ogree3d.go +++ b/CLI/controllers/ogree3d.go @@ -168,3 +168,59 @@ func (ogree3D *ogree3DPortImpl) InformOptional(caller string, entity int, data m } return nil } + +func Connect3D(url string) error { + return Ogree3D.Connect(url, *State.Terminal) +} + +func Disconnect3D() { + Ogree3D.InformOptional("Disconnect3d", -1, map[string]interface{}{"type": "logout", "data": ""}) + Ogree3D.Disconnect() +} + +// This func is used for when the user wants to filter certain +// attributes from being sent/displayed to Unity viewer client +func GenerateFilteredJson(data map[string]interface{}) map[string]interface{} { + if category, ok := data["category"].(string); ok && models.EntityStrToInt(category) != -1 { + //Start the filtration + ans := map[string]interface{}{} + attrs := map[string]interface{}{} + for key := range data { + if key == "attributes" { + for attrName, attrValue := range data[key].(map[string]interface{}) { + copyAttrIfDrawable(attrs, category, attrName, attrValue) + } + } else { + copyAttrIfDrawable(ans, category, key, data[key]) + } + } + if len(attrs) > 0 { + ans["attributes"] = attrs + } + return ans + } + return data //Nothing will be filtered +} + +func copyAttrIfDrawable(attrs map[string]interface{}, category, attrName string, attrValue any) { + if IsCategoryAttrDrawable(category, attrName) { + attrs[attrName] = attrValue + } +} + +func IsInObjForUnity(entityStr string) bool { + entInt := models.EntityStrToInt(entityStr) + return IsEntityTypeForOGrEE3D(entInt) +} + +func IsEntityTypeForOGrEE3D(entityType int) bool { + if entityType != -1 { + for idx := range State.ObjsForUnity { + if State.ObjsForUnity[idx] == entityType { + return true + } + } + } + + return false +} diff --git a/CLI/controllers/ogree3d_test.go b/CLI/controllers/ogree3d_test.go index 7dd238d03..0b799ccb7 100644 --- a/CLI/controllers/ogree3d_test.go +++ b/CLI/controllers/ogree3d_test.go @@ -1,7 +1,9 @@ -package controllers +package controllers_test import ( + "cli/controllers" "cli/readline" + test_utils "cli/test" "fmt" "net" "sync" @@ -13,79 +15,79 @@ import ( func init() { rl, _ := readline.New("") - State.Terminal = &rl + controllers.State.Terminal = &rl } func TestConnect3DReturnsErrorIfProvidedURLIsInvalid(t *testing.T) { - err := Connect3D("not.valid") + err := controllers.Connect3D("not.valid") assert.ErrorContains(t, err, "OGrEE-3D URL is not valid: not.valid") - assert.False(t, Ogree3D.IsConnected()) - assert.NotEqual(t, Ogree3D.URL(), "not.valid") + assert.False(t, controllers.Ogree3D.IsConnected()) + assert.NotEqual(t, controllers.Ogree3D.URL(), "not.valid") } func TestConnect3DDoesNotConnectIfOgree3DIsUnreachable(t *testing.T) { - err := Connect3D("localhost:3000") + err := controllers.Connect3D("localhost:3000") assert.ErrorContains(t, err, "OGrEE-3D is not reachable caused by OGrEE-3D (localhost:3000) unreachable\ndial tcp") assert.ErrorContains(t, err, "connect: connection refused") - assert.False(t, Ogree3D.IsConnected()) - assert.Equal(t, Ogree3D.URL(), "localhost:3000") + assert.False(t, controllers.Ogree3D.IsConnected()) + assert.Equal(t, controllers.Ogree3D.URL(), "localhost:3000") } func TestConnect3DConnectsToProvidedURL(t *testing.T) { fakeOgree3D(t, "3000") - err := Connect3D("localhost:3000") + err := controllers.Connect3D("localhost:3000") assert.Nil(t, err) - assert.True(t, Ogree3D.IsConnected()) - assert.Equal(t, Ogree3D.URL(), "localhost:3000") + assert.True(t, controllers.Ogree3D.IsConnected()) + assert.Equal(t, controllers.Ogree3D.URL(), "localhost:3000") } func TestConnect3DConnectsToStateOgreeURLIfNotProvidedURL(t *testing.T) { fakeOgree3D(t, "3000") - Ogree3D.SetURL("localhost:3000") - err := Connect3D("") + controllers.Ogree3D.SetURL("localhost:3000") + err := controllers.Connect3D("") assert.Nil(t, err) - assert.True(t, Ogree3D.IsConnected()) - assert.Equal(t, Ogree3D.URL(), "localhost:3000") + assert.True(t, controllers.Ogree3D.IsConnected()) + assert.Equal(t, controllers.Ogree3D.URL(), "localhost:3000") } func TestConnect3DReturnsErrorIfAlreadyConnectedAndNotUrlProvided(t *testing.T) { fakeOgree3D(t, "3000") - err := Connect3D("localhost:3000") + err := controllers.Connect3D("localhost:3000") require.Nil(t, err) - require.True(t, Ogree3D.IsConnected()) + require.True(t, controllers.Ogree3D.IsConnected()) - err = Connect3D("") + err = controllers.Connect3D("") assert.ErrorContains(t, err, "already connected to OGrEE-3D url: localhost:3000") - assert.True(t, Ogree3D.IsConnected()) + assert.True(t, controllers.Ogree3D.IsConnected()) } func TestConnect3DReturnsErrorIfAlreadyConnectedAndSameUrlProvided(t *testing.T) { fakeOgree3D(t, "3000") - err := Connect3D("localhost:3000") + err := controllers.Connect3D("localhost:3000") require.Nil(t, err) - require.True(t, Ogree3D.IsConnected()) + require.True(t, controllers.Ogree3D.IsConnected()) - err = Connect3D("localhost:3000") + err = controllers.Connect3D("localhost:3000") assert.ErrorContains(t, err, "already connected to OGrEE-3D url: localhost:3000") - assert.True(t, Ogree3D.IsConnected()) + assert.True(t, controllers.Ogree3D.IsConnected()) } func TestConnect3DTriesToConnectIfAlreadyConnectedAndDifferentUrlProvided(t *testing.T) { wg := fakeOgree3D(t, "3000") - err := Connect3D("localhost:3000") + err := controllers.Connect3D("localhost:3000") require.Nil(t, err) - require.True(t, Ogree3D.IsConnected()) + require.True(t, controllers.Ogree3D.IsConnected()) - err = Connect3D("localhost:5000") + err = controllers.Connect3D("localhost:5000") assert.ErrorContains(t, err, "OGrEE-3D is not reachable caused by OGrEE-3D (localhost:5000) unreachable\ndial tcp") assert.ErrorContains(t, err, "connect: connection refused") - assert.False(t, Ogree3D.IsConnected()) - assert.Equal(t, Ogree3D.URL(), "localhost:5000") + assert.False(t, controllers.Ogree3D.IsConnected()) + assert.Equal(t, controllers.Ogree3D.URL(), "localhost:5000") wg.Wait() } @@ -93,63 +95,63 @@ func TestConnect3DTriesToConnectIfAlreadyConnectedAndDifferentUrlProvided(t *tes func TestConnect3DConnectsIfAlreadyConnectedAndDifferentUrlProvidedIsReachable(t *testing.T) { wg := fakeOgree3D(t, "3000") - err := Connect3D("localhost:3000") + err := controllers.Connect3D("localhost:3000") require.Nil(t, err) - require.True(t, Ogree3D.IsConnected()) + require.True(t, controllers.Ogree3D.IsConnected()) fakeOgree3D(t, "5000") - err = Connect3D("localhost:5000") + err = controllers.Connect3D("localhost:5000") assert.Nil(t, err) - assert.True(t, Ogree3D.IsConnected()) - assert.Equal(t, Ogree3D.URL(), "localhost:5000") + assert.True(t, controllers.Ogree3D.IsConnected()) + assert.Equal(t, controllers.Ogree3D.URL(), "localhost:5000") wg.Wait() } func TestInformOgree3DOptionalDoesNothingIfOgree3DNotConnected(t *testing.T) { - require.False(t, Ogree3D.IsConnected()) - err := Ogree3D.InformOptional("Interact", -1, map[string]any{}) + require.False(t, controllers.Ogree3D.IsConnected()) + err := controllers.Ogree3D.InformOptional("Interact", -1, map[string]any{}) assert.Nil(t, err) } func TestInformOgree3DOptionalSendDataWhenOgree3DIsConnected(t *testing.T) { fakeOgree3D(t, "3000") - err := Connect3D("localhost:3000") + err := controllers.Connect3D("localhost:3000") require.Nil(t, err) - require.True(t, Ogree3D.IsConnected()) + require.True(t, controllers.Ogree3D.IsConnected()) - err = Ogree3D.InformOptional("Interact", -1, map[string]any{}) + err = controllers.Ogree3D.InformOptional("Interact", -1, map[string]any{}) assert.Nil(t, err) } func TestInformOgree3DFailsIfOgree3DNotReachable(t *testing.T) { - require.False(t, Ogree3D.IsConnected()) - Ogree3D.SetURL("localhost:3000") - err := Ogree3D.Inform("Interact", -1, map[string]any{}) + require.False(t, controllers.Ogree3D.IsConnected()) + controllers.Ogree3D.SetURL("localhost:3000") + err := controllers.Ogree3D.Inform("Interact", -1, map[string]any{}) assert.ErrorContains(t, err, "OGrEE-3D is not reachable caused by OGrEE-3D (localhost:3000) unreachable\ndial tcp") assert.ErrorContains(t, err, "connect: connection refused") } func TestInformOgree3DEstablishConnectionIfOgree3DIsReachable(t *testing.T) { - require.False(t, Ogree3D.IsConnected()) + require.False(t, controllers.Ogree3D.IsConnected()) - Ogree3D.SetURL("localhost:3000") + controllers.Ogree3D.SetURL("localhost:3000") fakeOgree3D(t, "3000") - err := Ogree3D.Inform("Interact", -1, map[string]any{}) + err := controllers.Ogree3D.Inform("Interact", -1, map[string]any{}) assert.Nil(t, err) } func TestInformOgree3DSendsDataIfEstablishConnectionWithOgree3DAlreadyEstablished(t *testing.T) { fakeOgree3D(t, "3000") - err := Connect3D("localhost:3000") + err := controllers.Connect3D("localhost:3000") require.Nil(t, err) - require.True(t, Ogree3D.IsConnected()) + require.True(t, controllers.Ogree3D.IsConnected()) - err = Ogree3D.Inform("Interact", -1, map[string]any{}) + err = controllers.Ogree3D.Inform("Interact", -1, map[string]any{}) assert.Nil(t, err) } @@ -183,10 +185,36 @@ func fakeOgree3D(t *testing.T, port string) *sync.WaitGroup { t.Cleanup(func() { ln.Close() - if Ogree3D.IsConnected() { - Ogree3D.Disconnect() + if controllers.Ogree3D.IsConnected() { + controllers.Ogree3D.Disconnect() } }) return &wg } + +// Test GenerateFilteredJson +func TestGenerateFilteredJson(t *testing.T) { + controllers.State.DrawableJsons = test_utils.GetTestDrawableJson() + + object := map[string]any{ + "name": "rack", + "parentId": "site.building.room", + "category": "rack", + "description": "", + "domain": "domain", + "attributes": map[string]any{ + "color": "aaaaaa", + }, + } + + filteredObject := controllers.GenerateFilteredJson(object) + + assert.Contains(t, filteredObject, "name") + assert.Contains(t, filteredObject, "parentId") + assert.Contains(t, filteredObject, "category") + assert.Contains(t, filteredObject, "domain") + assert.NotContains(t, filteredObject, "description") + assert.Contains(t, filteredObject, "attributes") + assert.Contains(t, filteredObject["attributes"], "color") +} diff --git a/CLI/controllers/path.go b/CLI/controllers/path.go new file mode 100644 index 000000000..991932bf1 --- /dev/null +++ b/CLI/controllers/path.go @@ -0,0 +1,137 @@ +package controllers + +import ( + "cli/models" + "fmt" + "path" + "strings" +) + +func PWD() string { + println(State.CurrPath) + return State.CurrPath +} + +func (controller Controller) UnfoldPath(path string) ([]string, error) { + if strings.Contains(path, "*") || models.PathHasLayer(path) { + _, subpaths, err := controller.GetObjectsWildcard(path, nil, nil) + return subpaths, err + } + + if path == "_" { + return State.ClipBoard, nil + } + + return []string{path}, nil +} + +func (controller Controller) SplitPath(pathStr string) (models.Path, error) { + for _, prefix := range models.PathPrefixes { + if strings.HasPrefix(pathStr, string(prefix)) { + var id string + if prefix == models.VirtualObjsPath && strings.HasPrefix(pathStr, prefix+"#") { + // virtual root layer, keep the virtual node + id = pathStr[1:] + } else { + id = pathStr[len(prefix):] + } + id = strings.ReplaceAll(id, "/", ".") + + var layer models.Layer + var err error + + id, layer, err = controller.GetLayer(id) + if err != nil { + return models.Path{}, err + } + + return models.Path{ + Prefix: prefix, + ObjectID: id, + Layer: layer, + }, nil + } + } + + return models.Path{}, fmt.Errorf("invalid object path") +} + +func (controller Controller) GetParentFromPath(path string, ent int, isValidate bool) (string, map[string]any, error) { + var parent map[string]any + parentId := "" + if ent == models.SITE || ent == models.STRAY_DEV { + // no parent + return parentId, parent, nil + } + + if isValidate { + parentId = models.GetObjectIDFromPath(path) + } else { + var err error + parent, err = controller.PollObject(path) + if err != nil { + return parentId, nil, err + } + if parent == nil && (ent != models.DOMAIN || path != "/Organisation/Domain") && + ent != models.VIRTUALOBJ { + return parentId, nil, fmt.Errorf("parent not found") + } + if parent != nil { + parentId = parent["id"].(string) + } + } + + return parentId, parent, nil +} + +func TranslatePath(p string, acceptSelection bool) string { + if p == "" { + p = "." + } + if p == "_" && acceptSelection { + return "_" + } + if p == "-" { + return State.PrevPath + } + var outputWords []string + if p[0] != '/' { + outputBase := State.CurrPath + if p[0] == '-' { + outputBase = State.PrevPath + } + + outputWords = strings.Split(outputBase, "/")[1:] + if len(outputWords) == 1 && outputWords[0] == "" { + outputWords = outputWords[0:0] + } + } else { + p = p[1:] + } + inputWords := strings.Split(p, "/") + for i, word := range inputWords { + if word == "." || (i == 0 && word == "-") { + continue + } else if word == ".." { + if len(outputWords) > 0 { + outputWords = outputWords[:len(outputWords)-1] + } + } else { + outputWords = append(outputWords, word) + } + } + translatePathShortcuts(outputWords) + return path.Clean("/" + strings.Join(outputWords, "/")) +} + +func translatePathShortcuts(outputWords []string) { + if len(outputWords) > 0 { + if outputWords[0] == "P" { + outputWords[0] = "Physical" + } else if outputWords[0] == "L" { + outputWords[0] = "Logical" + } else if outputWords[0] == "O" { + outputWords[0] = "Organisation" + } + } +} diff --git a/CLI/controllers/path_test.go b/CLI/controllers/path_test.go new file mode 100644 index 000000000..b390cd0fa --- /dev/null +++ b/CLI/controllers/path_test.go @@ -0,0 +1,38 @@ +package controllers_test + +import ( + "cli/controllers" + test_utils "cli/test" + "testing" + + "github.com/stretchr/testify/assert" +) + +// Test UnfoldPath +func TestUnfoldPath(t *testing.T) { + controller, mockAPI, _, _ := test_utils.SetMainEnvironmentMock(t) + wildcardPath := "/Physical/site/building/room/rack*" + firstRackPath := "/Physical/site/building/room/rack1" + secondRackPath := "/Physical/site/building/room/rack2" + rack1 := test_utils.GetEntity("rack", "rack1", "site.building.room", "") + rack2 := test_utils.GetEntity("rack", "rack2", "site.building.room", "") + test_utils.MockGetObjects(mockAPI, "id=site.building.room.rack*&namespace=physical.hierarchy", []any{rack1, rack2}) + controllers.State.ClipBoard = []string{firstRackPath} + tests := []struct { + name string + path string + expectedValue []string + }{ + {"StringWithStar", wildcardPath, []string{firstRackPath, secondRackPath}}, + {"Clipboard", "_", controllers.State.ClipBoard}, + {"SimplePath", secondRackPath, []string{secondRackPath}}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + results, err := controller.UnfoldPath(tt.path) + assert.Nil(t, err) + assert.Equal(t, tt.expectedValue, results) + }) + } +} diff --git a/CLI/controllers/responseSchemaController.go b/CLI/controllers/responseSchemaController.go deleted file mode 100644 index a76f2a40b..000000000 --- a/CLI/controllers/responseSchemaController.go +++ /dev/null @@ -1,91 +0,0 @@ -package controllers - -//Auxillary functions for parsing and verifying -//that the API responses are valid according -//to the specification - -import ( - l "cli/logger" - "encoding/json" - "fmt" - "io" - "net/http" - "os" -) - -type Response struct { - Status int - message string - Body map[string]any -} - -func ParseResponseClean(response *http.Response) (*Response, error) { - bodyBytes, err := io.ReadAll(response.Body) - if err != nil { - return nil, err - } - defer response.Body.Close() - responseBody := map[string]interface{}{} - message := "" - if len(bodyBytes) > 0 { - err = json.Unmarshal(bodyBytes, &responseBody) - if err != nil { - return nil, fmt.Errorf("cannot unmarshal json : \n%s", string(bodyBytes)) - } - message, _ = responseBody["message"].(string) - } - return &Response{response.StatusCode, message, responseBody}, nil -} - -func ParseResponse(resp *http.Response, e error, purpose string) map[string]interface{} { - ans := map[string]interface{}{} - if e != nil { - l.GetWarningLogger().Println("Error while sending "+purpose+" to server: ", e) - if State.DebugLvl > 0 { - if State.DebugLvl > ERROR { - println(e.Error()) - } - println("There was an error!") - } - return nil - } - defer resp.Body.Close() - bodyBytes, err := io.ReadAll(resp.Body) - if err != nil { - if State.DebugLvl > 0 { - println("Error: " + err.Error()) - } - - l.GetErrorLogger().Println("Error while trying to read server response: ", err) - if purpose == "POST" || purpose == "Search" { - os.Exit(-1) - } - return nil - } - json.Unmarshal(bodyBytes, &ans) - return ans -} - -func LoadArrFromResp(resp map[string]interface{}, idx string) []interface{} { - if data, ok := resp["data"].(map[string]interface{}); ok { - if objs, ok1 := data[idx].([]interface{}); ok1 { - return objs - } - } - return nil -} - -func LoadObjectFromInf(x interface{}) (map[string]interface{}, bool) { - object, ok := x.(map[string]interface{}) - return object, ok -} - -// Convert []interface{} array to -// []map[string]interface{} array -func infArrToMapStrinfArr(x []interface{}) []map[string]interface{} { - ans := []map[string]interface{}{} - for i := range x { - ans = append(ans, x[i].(map[string]interface{})) - } - return ans -} diff --git a/CLI/controllers/select.go b/CLI/controllers/select.go index b4f8f1745..3d3138d24 100644 --- a/CLI/controllers/select.go +++ b/CLI/controllers/select.go @@ -1,10 +1,5 @@ package controllers -import ( - "fmt" - "strings" -) - func (controller Controller) Select(path string) ([]string, error) { var paths []string var err error @@ -24,36 +19,3 @@ func (controller Controller) Select(path string) ([]string, error) { return controller.SetClipBoard(paths) } - -func (controller Controller) SetClipBoard(x []string) ([]string, error) { - State.ClipBoard = x - var data map[string]interface{} - - if len(x) == 0 { //This means deselect - data = map[string]interface{}{"type": "select", "data": "[]"} - err := controller.Ogree3D.InformOptional("SetClipBoard", -1, data) - if err != nil { - return nil, fmt.Errorf("cannot reset clipboard : %s", err.Error()) - } - } else { - //Verify paths - arr := []string{} - for _, val := range x { - obj, err := controller.GetObject(val) - if err != nil { - return nil, err - } - id, ok := obj["id"].(string) - if ok { - arr = append(arr, id) - } - } - serialArr := "[\"" + strings.Join(arr, "\",\"") + "\"]" - data = map[string]interface{}{"type": "select", "data": serialArr} - err := controller.Ogree3D.InformOptional("SetClipBoard", -1, data) - if err != nil { - return nil, fmt.Errorf("cannot set clipboard : %s", err.Error()) - } - } - return State.ClipBoard, nil -} diff --git a/CLI/controllers/sendSchemaController.go b/CLI/controllers/sendSchemaController.go deleted file mode 100644 index 7fb04930e..000000000 --- a/CLI/controllers/sendSchemaController.go +++ /dev/null @@ -1,192 +0,0 @@ -package controllers - -// Auxillary functions for parsing and validation of data -// before the CLI sends off to API - -import ( - l "cli/logger" - "cli/models" - "fmt" - "strconv" - "strings" -) - -func serialiseVector(attr map[string]interface{}, want string) []float64 { - if vector, ok := attr[want].([]float64); ok { - if want == "size" && len(vector) == 3 { - attr["height"] = vector[2] - vector = vector[:len(vector)-1] - } else if want == "posXYZ" && len(vector) == 2 { - vector = append(vector, 0) - } - return vector - } else { - return []float64{} - } -} - -// Auxillary function for serialiseAttr -// to help verify the posXY and size attributes -// have correct lengths before they get serialised -func stringSplitter(want, separator, attribute string) []string { - arr := strings.Split(want, separator) - switch attribute { - case "posXYZ": - if len(arr) != 2 && len(arr) != 3 { - return nil - } - case "posXY": - if len(arr) != 2 { - return nil - } - case "size": - if len(arr) != 3 { - return nil - } - } - return arr -} - -func MergeMaps(x, y map[string]interface{}, overwrite bool) { - for i := range y { - //Conflict case - if _, ok := x[i]; ok { - if overwrite { - l.GetWarningLogger().Println("Conflict while merging maps") - if State.DebugLvl > 1 { - println("Conflict while merging data, resorting to overwriting!") - } - - x[i] = y[i] - } - } else { - x[i] = y[i] - } - - } -} - -// This func is used for when the user wants to filter certain -// attributes from being sent/displayed to Unity viewer client -func GenerateFilteredJson(x map[string]interface{}) map[string]interface{} { - ans := map[string]interface{}{} - attrs := map[string]interface{}{} - if catInf, ok := x["category"]; ok { - if cat, ok := catInf.(string); ok { - if models.EntityStrToInt(cat) != -1 { - - //Start the filtration - for i := range x { - if i == "attributes" { - for idx := range x[i].(map[string]interface{}) { - if IsCategoryAttrDrawable(x["category"].(string), idx) { - attrs[idx] = x[i].(map[string]interface{})[idx] - } - } - } else { - if IsCategoryAttrDrawable(x["category"].(string), i) { - ans[i] = x[i] - } - } - } - if len(attrs) > 0 { - ans["attributes"] = attrs - } - return ans - } - } - } - return x //Nothing will be filtered -} - -// Helper func is used to check if sizeU is numeric -// this is necessary since the OCLI command for creating a device -// needs to distinguish if the parameter is a valid sizeU or template -func checkNumeric(x interface{}) bool { - switch x.(type) { - case int, float64, float32: - return true - default: - return false - } -} - -// Helper func that safely deletes a string key in a map -func DeleteAttr(x map[string]interface{}, key string) { - if _, ok := x[key]; ok { - delete(x, key) - } -} - -// Helper func that safely copies a value in a map -func CopyAttr(dest, source map[string]interface{}, key string) bool { - if _, ok := source[key]; ok { - dest[key] = source[key] - return true - } - return false -} - -// Used for update commands to ensure all data sent to API -// are in string format -func Stringify(x interface{}) string { - switch xArr := x.(type) { - case string: - return x.(string) - case int: - return strconv.Itoa(x.(int)) - case float32, float64: - return strconv.FormatFloat(float64(x.(float64)), 'f', -1, 64) - case bool: - return strconv.FormatBool(x.(bool)) - case []string: - return strings.Join(x.([]string), ",") - case []interface{}: - var arrStr []string - for i := range xArr { - arrStr = append(arrStr, Stringify(xArr[i])) - } - return "[" + strings.Join(arrStr, ",") + "]" - case []float64: - var arrStr []string - for i := range xArr { - arrStr = append(arrStr, Stringify(xArr[i])) - } - return "[" + strings.Join(arrStr, ",") + "]" - } - return "" -} - -// ExpandStrVector: allow usage of .. on device slot and group content vector -// converting [slot01..slot03] on [slot01,slot02,slot03] -func ExpandStrVector(slotVector []string) ([]string, error) { - slots := []string{} - for _, slot := range slotVector { - if strings.Contains(slot, "..") { - if len(slotVector) != 1 { - return nil, fmt.Errorf("Invalid device syntax: .. can only be used in a single element vector") - } - parts := strings.Split(slot, "..") - if len(parts) != 2 || - (parts[0][:len(parts[0])-1] != parts[1][:len(parts[1])-1]) { - l.GetWarningLogger().Println("Invalid device syntax encountered") - return nil, fmt.Errorf("Invalid device syntax: incorrect use of .. for slot") - } else { - start, errS := strconv.Atoi(string(parts[0][len(parts[0])-1])) - end, errE := strconv.Atoi(string(parts[1][len(parts[1])-1])) - if errS != nil || errE != nil { - l.GetWarningLogger().Println("Invalid device syntax encountered") - return nil, fmt.Errorf("Invalid device syntax: incorrect use of .. for slot") - } else { - prefix := parts[0][:len(parts[0])-1] - for i := start; i <= end; i++ { - slots = append(slots, prefix+strconv.Itoa(i)) - } - } - } - } else { - slots = append(slots, slot) - } - } - return slots, nil -} diff --git a/CLI/controllers/sendSchemaController_test.go b/CLI/controllers/sendSchemaController_test.go deleted file mode 100644 index b1f0b3390..000000000 --- a/CLI/controllers/sendSchemaController_test.go +++ /dev/null @@ -1,110 +0,0 @@ -package controllers_test - -import ( - "cli/controllers" - l "cli/logger" - test_utils "cli/test" - "testing" - - "github.com/stretchr/testify/assert" -) - -func init() { - l.InitLogs() -} - -func TestMergeMaps(t *testing.T) { - x := map[string]any{ - "a": "10", - "b": "11", - } - y := map[string]any{ - "b": "25", - "c": "40", - } - testMap := test_utils.CopyMap(x) - controllers.MergeMaps(testMap, y, false) - assert.Contains(t, testMap, "a") - assert.Contains(t, testMap, "b") - assert.Contains(t, testMap, "c") - assert.Equal(t, x["a"], testMap["a"]) - assert.Equal(t, x["b"], testMap["b"]) - assert.Equal(t, y["c"], testMap["c"]) - - testMap = test_utils.CopyMap(x) - controllers.MergeMaps(testMap, y, true) - assert.Contains(t, testMap, "a") - assert.Contains(t, testMap, "b") - assert.Contains(t, testMap, "c") - assert.Equal(t, x["a"], testMap["a"]) - assert.Equal(t, y["b"], testMap["b"]) - assert.Equal(t, y["c"], testMap["c"]) -} - -func TestGenerateFilteredJson(t *testing.T) { - controllers.State.DrawableJsons = test_utils.GetTestDrawableJson() - - object := map[string]any{ - "name": "rack", - "parentId": "site.building.room", - "category": "rack", - "description": "", - "domain": "domain", - "attributes": map[string]any{ - "color": "aaaaaa", - }, - } - - filteredObject := controllers.GenerateFilteredJson(object) - - assert.Contains(t, filteredObject, "name") - assert.Contains(t, filteredObject, "parentId") - assert.Contains(t, filteredObject, "category") - assert.Contains(t, filteredObject, "domain") - assert.NotContains(t, filteredObject, "description") - assert.Contains(t, filteredObject, "attributes") - assert.Contains(t, filteredObject["attributes"], "color") -} - -func TestStringify(t *testing.T) { - assert.Equal(t, "text", controllers.Stringify("text")) - assert.Equal(t, "35", controllers.Stringify(35)) - assert.Equal(t, "35", controllers.Stringify(35.0)) - assert.Equal(t, "true", controllers.Stringify(true)) - assert.Equal(t, "hello,world", controllers.Stringify([]string{"hello", "world"})) - assert.Equal(t, "[45,21]", controllers.Stringify([]float64{45, 21})) - assert.Equal(t, "[hello,5,[world,450]]", controllers.Stringify([]any{"hello", 5, []any{"world", 450}})) - assert.Equal(t, "", controllers.Stringify(map[string]any{"hello": 5})) -} - -func TestExpandSlotVector(t *testing.T) { - slots, err := controllers.ExpandStrVector([]string{"slot1..slot3", "slot4"}) - assert.Nil(t, slots) - assert.NotNil(t, err) - assert.ErrorContains(t, err, "Invalid device syntax: .. can only be used in a single element vector") - - slots, err = controllers.ExpandStrVector([]string{"slot1..slot3..slot7"}) - assert.Nil(t, slots) - assert.NotNil(t, err) - assert.ErrorContains(t, err, "Invalid device syntax: incorrect use of .. for slot") - - slots, err = controllers.ExpandStrVector([]string{"slot1..slots3"}) - assert.Nil(t, slots) - assert.NotNil(t, err) - assert.ErrorContains(t, err, "Invalid device syntax: incorrect use of .. for slot") - - slots, err = controllers.ExpandStrVector([]string{"slot1..slotE"}) - assert.Nil(t, slots) - assert.NotNil(t, err) - assert.ErrorContains(t, err, "Invalid device syntax: incorrect use of .. for slot") - - slots, err = controllers.ExpandStrVector([]string{"slot1..slot3"}) - assert.Nil(t, err) - assert.NotNil(t, slots) - assert.EqualValues(t, []string{"slot1", "slot2", "slot3"}, slots) - - slots, err = controllers.ExpandStrVector([]string{"slot1", "slot3"}) - assert.Nil(t, err) - assert.NotNil(t, slots) - assert.EqualValues(t, []string{"slot1", "slot3"}, slots) -} diff --git a/CLI/controllers/shell.go b/CLI/controllers/shell.go new file mode 100644 index 000000000..a122df459 --- /dev/null +++ b/CLI/controllers/shell.go @@ -0,0 +1,253 @@ +package controllers + +import ( + "cli/commands" + l "cli/logger" + "cli/models" + "cli/readline" + "cli/utils" + "fmt" + "net/http" + "os" + "os/exec" + "runtime" + "strings" + "time" +) + +var BuildTime string +var BuildHash string +var BuildTree string +var GitCommitDate string + +var State ShellState + +type ShellState struct { + Prompt string + BlankPrompt string + Customer string //Tenant name + CurrPath string + CurrDomain string + PrevPath string + ClipBoard []string + Hierarchy *HierarchyNode + ConfigPath string //Holds file path of '.env' + HistoryFilePath string //Holds file path of '.history' + User User + APIURL string + APIKEY string + FilterDisplay bool //Set whether or not to send attributes to unity + ObjsForUnity []int //Deciding what objects should be sent to unity + DrawThreshold int //Number of objects to be sent at a time to unity + DrawableObjs []int //Indicate which objs drawable in unity + DrawableJsons map[string]map[string]interface{} + DebugLvl int + Terminal **readline.Instance + Timeout time.Duration + DynamicSymbolTable map[string]interface{} + FuncTable map[string]interface{} + DryRun bool + DryRunErrors []error +} + +func Clear() { + switch runtime.GOOS { + case "windows": + cmd := exec.Command("cmd", "/c", "cls") + cmd.Stdout = os.Stdout + cmd.Run() + default: + fmt.Printf("\033[2J\033[H") + } +} + +// Function is an abstraction of a normal exit +func Exit() { + //writeHistoryOnExit(&State.sessionBuffer) + //runtime.Goexit() + os.Exit(0) +} + +func Help(entry string) { + var path string + entry = strings.TrimSpace(entry) + switch entry { + case "ls", "pwd", "print", "printf", "cd", "tree", "get", "clear", + "lsog", "grep", "for", "while", "if", "env", + "cmds", "var", "unset", "selection", commands.Connect3D, commands.Disconnect3D, "camera", "ui", "hc", "drawable", + "link", "unlink", "draw", "getu", "getslot", "undraw", + "lsenterprise", commands.Cp: + path = "./other/man/" + entry + ".txt" + + case ">": + path = "./other/man/focus.txt" + + case "+": + path = "./other/man/plus.txt" + + case "=": + path = "./other/man/equal.txt" + + case "-": + path = "./other/man/minus.txt" + + case ".template": + path = "./other/man/template.txt" + + case ".cmds": + path = "./other/man/cmds.txt" + + case ".var": + path = "./other/man/var.txt" + + case "lsobj", "lsten", "lssite", commands.LsBuilding, "lsroom", "lsrack", + "lsdev", "lsac", "lscorridor", "lspanel", "lscabinet": + path = "./other/man/lsobj.txt" + + default: + path = "./other/man/default.txt" + } + text, e := os.ReadFile(utils.ExeDir() + "/" + path) + if e != nil { + println("Manual Page not found!") + } else { + println(string(text)) + } + +} + +func ShowClipBoard() []string { + if State.ClipBoard != nil { + for _, k := range State.ClipBoard { + println(k) + } + return State.ClipBoard + } + return nil +} + +func (controller Controller) SetClipBoard(x []string) ([]string, error) { + State.ClipBoard = x + var data map[string]interface{} + + if len(x) == 0 { //This means deselect + data = map[string]interface{}{"type": "select", "data": "[]"} + err := controller.Ogree3D.InformOptional("SetClipBoard", -1, data) + if err != nil { + return nil, fmt.Errorf("cannot reset clipboard : %s", err.Error()) + } + } else { + //Verify paths + arr := []string{} + for _, val := range x { + obj, err := controller.GetObject(val) + if err != nil { + return nil, err + } + id, ok := obj["id"].(string) + if ok { + arr = append(arr, id) + } + } + serialArr := "[\"" + strings.Join(arr, "\",\"") + "\"]" + data = map[string]interface{}{"type": "select", "data": serialArr} + err := controller.Ogree3D.InformOptional("SetClipBoard", -1, data) + if err != nil { + return nil, fmt.Errorf("cannot set clipboard : %s", err.Error()) + } + } + return State.ClipBoard, nil +} + +// Displays environment variable values +// and user defined variables and funcs +func Env(userVars, userFuncs map[string]interface{}) { + fmt.Println("Filter: ", State.FilterDisplay) + fmt.Println() + fmt.Println("Objects Unity shall be informed of upon update:") + for _, k := range State.ObjsForUnity { + fmt.Println(k) + } + fmt.Println() + fmt.Println("Objects Unity shall draw:") + for _, k := range State.DrawableObjs { + fmt.Println(models.EntityToString(k)) + } + + fmt.Println() + fmt.Println("Currently defined user variables:") + for name, k := range userVars { + if k != nil { + fmt.Println("Name:", name, " Value: ", k) + } + + } + + fmt.Println() + fmt.Println("Currently defined user functions:") + for name := range userFuncs { + fmt.Println("Name:", name) + } +} + +func SetEnv(arg string, val interface{}) { + switch arg { + case "Filter": + if _, ok := val.(bool); !ok { + msg := "Can only assign bool values for " + arg + " Env Var" + l.GetWarningLogger().Println(msg) + if State.DebugLvl > 0 { + println(msg) + } + } else { + if arg == "Filter" { + State.FilterDisplay = val.(bool) + } + + println(arg + " Display Environment variable set") + } + + default: + println(arg + " is not an environment variable") + } +} + +func LSOG() error { + fmt.Println("********************************************") + fmt.Println("OGREE Shell Information") + fmt.Println("********************************************") + + fmt.Println("USER EMAIL:", State.User.Email) + fmt.Println("API URL:", State.APIURL+"/api/") + fmt.Println("OGrEE-3D URL:", Ogree3D.URL()) + fmt.Println("OGrEE-3D connected: ", Ogree3D.IsConnected()) + fmt.Println("BUILD DATE:", BuildTime) + fmt.Println("BUILD TREE:", BuildTree) + fmt.Println("BUILD HASH:", BuildHash) + fmt.Println("COMMIT DATE: ", GitCommitDate) + fmt.Println("CONFIG FILE PATH: ", State.ConfigPath) + fmt.Println("LOG PATH:", "./log.txt") + fmt.Println("HISTORY FILE PATH:", State.HistoryFilePath) + fmt.Println("DEBUG LEVEL: ", State.DebugLvl) + + fmt.Printf("\n\n") + fmt.Println("********************************************") + fmt.Println("API Information") + fmt.Println("********************************************") + + //Get API Information here + resp, err := API.Request("GET", "/api/version", nil, http.StatusOK) + if err != nil { + return err + } + apiInfo, ok := resp.Body["data"].(map[string]any) + if !ok { + return fmt.Errorf("invalid response from API on GET /api/version") + } + fmt.Println("BUILD DATE:", apiInfo["BuildDate"]) + fmt.Println("BUILD TREE:", apiInfo["BuildTree"]) + fmt.Println("BUILD HASH:", apiInfo["BuildHash"]) + fmt.Println("COMMIT DATE: ", apiInfo["CommitDate"]) + fmt.Println("CUSTOMER: ", apiInfo["Customer"]) + return nil +} diff --git a/CLI/controllers/shell_test.go b/CLI/controllers/shell_test.go new file mode 100644 index 000000000..2a3dc1cc0 --- /dev/null +++ b/CLI/controllers/shell_test.go @@ -0,0 +1,26 @@ +package controllers_test + +import ( + "cli/controllers" + test_utils "cli/test" + "strings" + "testing" + + "github.com/stretchr/testify/assert" +) + +// Test PWD +func TestPWD(t *testing.T) { + controller, mockAPI, _ := layersSetup(t) + controller.CD("/") + location := controllers.PWD() + assert.Equal(t, "/", location) + + test_utils.MockGetObject(mockAPI, rack1) + path := "/Physical/" + strings.Replace(rack1["id"].(string), ".", "/", -1) + err := controller.CD(path) + assert.Nil(t, err) + + location = controllers.PWD() + assert.Equal(t, path, location) +} diff --git a/CLI/controllers/sortController.go b/CLI/controllers/sortController.go deleted file mode 100644 index 1eae30515..000000000 --- a/CLI/controllers/sortController.go +++ /dev/null @@ -1,115 +0,0 @@ -package controllers - -import "sort" - -/* -// Ensure it satisfies sort.Interface -func (d Deals) Len() int { return len(d) } -func (d Deals) Less(i, j int) bool { return d[i].Id < d[j].Id } -func (d Deals) Swap(i, j int) { d[i], d[j] = d[j], d[i] } -*/ - -type sortable interface { - GetData() []interface{} - Print() -} - -// Helper Struct for sorting -type SortableMArr struct { - data []interface{} - attr string //Desired attr the user wants to use for sorting - isNested bool //If the attr is in "attributes" map -} - -func (s SortableMArr) GetData() []interface{} { return s.data } -func (s SortableMArr) Len() int { return len(s.data) } -func (s SortableMArr) Swap(i, j int) { s.data[i], s.data[j] = s.data[j], s.data[i] } -func (s SortableMArr) Less(i, j int) bool { - var lKey string - var rKey string - var lmap map[string]interface{} - var rmap map[string]interface{} - - //Check if the attribute is in the 'attributes' map - if s.isNested { - lKey = determineStrKey(s.data[i].(map[string]interface{})["attributes"].(map[string]interface{}), []string{s.attr}) - rKey = determineStrKey(s.data[j].(map[string]interface{})["attributes"].(map[string]interface{}), []string{s.attr}) - lmap = s.data[i].(map[string]interface{})["attributes"].(map[string]interface{}) - rmap = s.data[j].(map[string]interface{})["attributes"].(map[string]interface{}) - } else { - lKey = determineStrKey(s.data[i].(map[string]interface{}), []string{s.attr}) - rKey = determineStrKey(s.data[j].(map[string]interface{}), []string{s.attr}) - lmap = s.data[i].(map[string]interface{}) - rmap = s.data[j].(map[string]interface{}) - } - - //We want the objs with non existing attribute at the end of the array - if lKey == "" && rKey != "" { - return false - } - - if rKey == "" && lKey != "" { - return true - } - - lH := lmap[s.attr] - rH := rmap[s.attr] - - //We must ensure that they are strings, non strings will be - //placed at the end of the array - var lOK, rOK bool - _, lOK = lH.(string) - _, rOK = rH.(string) - - if !lOK && rOK || lH == nil && rH != nil { - return false - } - - if lOK && !rOK || lH != nil && rH == nil { - return true - } - - if lH == nil && rH == nil { - return false - } - - return lH.(string) < rH.(string) - -} - -func (s SortableMArr) Print() { - objs := s.GetData() - if s.isNested { - for i := range objs { - attr := objs[i].(map[string]interface{})["attributes"].(map[string]interface{})[s.attr] - if attr == nil { - attr = "NULL" - } - println(s.attr, ":", - attr.(string), - " Name: ", objs[i].(map[string]interface{})["name"].(string)) - } - } else { - for i := range objs { - println(s.attr, ":", objs[i].(map[string]interface{})[s.attr], - " Name: ", objs[i].(map[string]interface{})["name"].(string)) - } - } - -} - -func SortObjects(objs []interface{}, attr string) *SortableMArr { - var x SortableMArr - var nested bool - switch attr { - case "id", "name", "category", "parentID", - "description", "domain", "parentid", "parentId": - nested = false - default: - nested = true - } - - x = SortableMArr{objs, attr, nested} - sort.Sort(x) - return &x -} diff --git a/CLI/controllers/stateController.go b/CLI/controllers/stateController.go deleted file mode 100755 index 98fc31d1f..000000000 --- a/CLI/controllers/stateController.go +++ /dev/null @@ -1,76 +0,0 @@ -package controllers - -import ( - "cli/models" - "cli/readline" - "time" -) - -var BuildTime string -var BuildHash string -var BuildTree string -var GitCommitDate string -var State ShellState - -type User struct { - Email string - ID string -} - -type ShellState struct { - Prompt string - BlankPrompt string - Customer string //Tenant name - CurrPath string - CurrDomain string - PrevPath string - ClipBoard []string - Hierarchy *HierarchyNode - ConfigPath string //Holds file path of '.env' - HistoryFilePath string //Holds file path of '.history' - User User - APIURL string - APIKEY string - FilterDisplay bool //Set whether or not to send attributes to unity - ObjsForUnity []int //Deciding what objects should be sent to unity - DrawThreshold int //Number of objects to be sent at a time to unity - DrawableObjs []int //Indicate which objs drawable in unity - DrawableJsons map[string]map[string]interface{} - DebugLvl int - Terminal **readline.Instance - Timeout time.Duration - DynamicSymbolTable map[string]interface{} - FuncTable map[string]interface{} -} - -func IsInObjForUnity(entityStr string) bool { - entInt := models.EntityStrToInt(entityStr) - return IsEntityTypeForOGrEE3D(entInt) -} - -func IsEntityTypeForOGrEE3D(entityType int) bool { - if entityType != -1 { - for idx := range State.ObjsForUnity { - if State.ObjsForUnity[idx] == entityType { - return true - } - } - } - - return false -} - -func IsDrawableEntity(x string) bool { - entInt := models.EntityStrToInt(x) - - for idx := range State.DrawableObjs { - if State.DrawableObjs[idx] == entInt { - return true - } - } - return false -} - -func GetKey() string { - return State.APIKEY -} diff --git a/CLI/controllers/template.go b/CLI/controllers/template.go index 45e4afacb..a4a68ad4b 100644 --- a/CLI/controllers/template.go +++ b/CLI/controllers/template.go @@ -2,6 +2,7 @@ package controllers import ( "cli/models" + "cli/utils" "errors" "fmt" "net/http" @@ -49,120 +50,112 @@ func (controller Controller) GetTemplate(name string, entity int) (map[string]an return template, nil } +func (controller Controller) ApplyTemplateIfExists(attr, data map[string]any, ent int, isValidate bool) (bool, error) { + if _, hasTemplate := attr["template"]; hasTemplate { + if isValidate { + return true, nil + } + // apply template + return true, controller.ApplyTemplate(attr, data, ent) + } + return false, nil +} + +func (controller Controller) ApplyTemplateOrSetSize(attr, data map[string]any, ent int, isValidate bool) (bool, error) { + if hasTemplate, err := controller.ApplyTemplateIfExists(attr, data, ent, + isValidate); !hasTemplate { + // apply user input + return hasTemplate, models.SetSize(attr) + } else { + return hasTemplate, err + } +} + // If user provided templates, get the JSON // and parse into templates func (controller Controller) ApplyTemplate(attr, data map[string]interface{}, ent int) error { - if templateName, hasTemplate := attr["template"].(string); hasTemplate { - tmpl, err := controller.GetTemplate(templateName, ent) - if err != nil { - return err - } + tmpl, err := controller.GetTemplate(attr["template"].(string), ent) + if err != nil { + return err + } - key := determineStrKey(tmpl, []string{"sizeWDHmm", "sizeWDHm"}) - - if sizeInf, hasSize := tmpl[key].([]any); hasSize && len(sizeInf) == 3 { - attr["size"] = sizeInf[:2] - attr["height"] = sizeInf[2] - CopyAttr(attr, tmpl, "shape") - - if ent == models.DEVICE { - attr["sizeUnit"] = "mm" - attr["heightUnit"] = "mm" - if tmpx, ok := tmpl["attributes"]; ok { - if x, ok := tmpx.(map[string]interface{}); ok { - if tmp, ok := x["type"]; ok { - if t, ok := tmp.(string); ok { - if t == "chassis" || t == "server" { - res := 0 - if val, ok := sizeInf[2].(float64); ok { - res = int((val / 1000) / RACKUNIT) - } else if val, ok := sizeInf[2].(int); ok { - res = int((float64(val) / 1000) / RACKUNIT) - } else { - return errors.New("invalid size vector on given template") - } - attr["sizeU"] = res + key := determineStrKey(tmpl, []string{"sizeWDHmm", "sizeWDHm"}) + + if sizeInf, hasSize := tmpl[key].([]any); hasSize && len(sizeInf) == 3 { + attr["size"] = sizeInf[:2] + attr["height"] = sizeInf[2] + utils.CopyMapVal(attr, tmpl, "shape") + + if ent == models.DEVICE { + if tmpx, ok := tmpl["attributes"]; ok { + if x, ok := tmpx.(map[string]interface{}); ok { + if tmp, ok := x["type"]; ok { + if t, ok := tmp.(string); ok { + if t == "chassis" || t == "server" { + res := 0 + if val, ok := sizeInf[2].(float64); ok { + res = int((val / 1000) / RACKUNIT) + } else if val, ok := sizeInf[2].(int); ok { + res = int((float64(val) / 1000) / RACKUNIT) + } else { + return errors.New("invalid size vector on given template") } + attr["sizeU"] = res } } } } + } - } else if ent == models.ROOM { - attr["sizeUnit"] = "m" - attr["heightUnit"] = "m" - - //Copy additional Room specific attributes - CopyAttr(attr, tmpl, "technicalArea") - if _, ok := attr["technicalArea"]; ok { - attr["technical"] = attr["technicalArea"] - delete(attr, "technicalArea") - } - - CopyAttr(attr, tmpl, "axisOrientation") - - CopyAttr(attr, tmpl, "reservedArea") - if _, ok := attr["reservedArea"]; ok { - attr["reserved"] = attr["reservedArea"] - delete(attr, "reservedArea") - } + } else if ent == models.ROOM { + //Copy additional Room specific attributes + utils.CopyMapVal(attr, tmpl, "technicalArea") + if _, ok := attr["technicalArea"]; ok { + attr["technical"] = attr["technicalArea"] + delete(attr, "technicalArea") + } - CopyAttr(attr, tmpl, "separators") - CopyAttr(attr, tmpl, "pillars") - CopyAttr(attr, tmpl, "floorUnit") - CopyAttr(attr, tmpl, "tiles") - CopyAttr(attr, tmpl, "rows") - CopyAttr(attr, tmpl, "aisles") - CopyAttr(attr, tmpl, "vertices") - CopyAttr(attr, tmpl, "colors") - CopyAttr(attr, tmpl, "tileAngle") - - } else if ent == models.BLDG { - attr["sizeUnit"] = "m" - attr["heightUnit"] = "m" - - } else { - attr["sizeUnit"] = "mm" - attr["heightUnit"] = "mm" + utils.CopyMapVal(attr, tmpl, "reservedArea") + if _, ok := attr["reservedArea"]; ok { + attr["reserved"] = attr["reservedArea"] + delete(attr, "reservedArea") } - //Copy Description - if _, ok := tmpl["description"]; ok { - if descTable, ok := tmpl["description"].([]interface{}); ok { - data["description"] = descTable[0] - for _, desc := range descTable[1:] { - data["description"] = data["description"].(string) + "\n" + desc.(string) - } - } else { - data["description"] = tmpl["description"] - } - } else { - data["description"] = "" + for _, attrName := range []string{"axisOrientation", "separators", + "pillars", "floorUnit", "tiles", "rows", "aisles", + "vertices", "colors", "tileAngle"} { + utils.CopyMapVal(attr, tmpl, attrName) } - //fbxModel section - if check := CopyAttr(attr, tmpl, "fbxModel"); !check { - if ent != models.BLDG { - attr["fbxModel"] = "" - } + } else { + attr["sizeUnit"] = "mm" + attr["heightUnit"] = "mm" + } + + //Copy Description + if _, ok := tmpl["description"]; ok { + data["description"] = tmpl["description"] + } + + //fbxModel section + if check := utils.CopyMapVal(attr, tmpl, "fbxModel"); !check { + if ent != models.BLDG { + attr["fbxModel"] = "" } + } - //Copy orientation if available - CopyAttr(attr, tmpl, "orientation") + //Copy orientation if available + utils.CopyMapVal(attr, tmpl, "orientation") - //Merge attributes if available - if tmplAttrsInf, ok := tmpl["attributes"]; ok { - if tmplAttrs, ok := tmplAttrsInf.(map[string]interface{}); ok { - MergeMaps(attr, tmplAttrs, false) - } + //Merge attributes if available + if tmplAttrsInf, ok := tmpl["attributes"]; ok { + if tmplAttrs, ok := tmplAttrsInf.(map[string]interface{}); ok { + utils.MergeMaps(attr, tmplAttrs, false) } - } else { - println("Warning, invalid size value in template.") - return errors.New("invalid size vector on given template") } } else { - //Serialise size and posXY if given - attr["size"] = serialiseVector(attr, "size") + println("Warning, invalid size value in template.") + return errors.New("invalid size vector on given template") } return nil diff --git a/CLI/controllers/ui.go b/CLI/controllers/ui.go new file mode 100644 index 000000000..857762caf --- /dev/null +++ b/CLI/controllers/ui.go @@ -0,0 +1,109 @@ +package controllers + +import ( + "cli/models" + "fmt" +) + +func (controller Controller) UIDelay(time float64) error { + subdata := map[string]interface{}{"command": "delay", "data": time} + data := map[string]interface{}{"type": "ui", "data": subdata} + if State.DebugLvl > WARNING { + Disp(data) + } + + return controller.Ogree3D.Inform("HandleUI", -1, data) +} + +func (controller Controller) UIToggle(feature string, enable bool) error { + subdata := map[string]interface{}{"command": feature, "data": enable} + data := map[string]interface{}{"type": "ui", "data": subdata} + if State.DebugLvl > WARNING { + Disp(data) + } + + return controller.Ogree3D.Inform("HandleUI", -1, data) +} + +func (controller Controller) UIHighlight(path string) error { + obj, err := controller.GetObject(path) + if err != nil { + return err + } + + subdata := map[string]interface{}{"command": "highlight", "data": obj["id"]} + data := map[string]interface{}{"type": "ui", "data": subdata} + if State.DebugLvl > WARNING { + Disp(data) + } + + return controller.Ogree3D.Inform("HandleUI", -1, data) +} + +func (controller Controller) UIClearCache() error { + subdata := map[string]interface{}{"command": "clearcache", "data": ""} + data := map[string]interface{}{"type": "ui", "data": subdata} + if State.DebugLvl > WARNING { + Disp(data) + } + + return controller.Ogree3D.Inform("HandleUI", -1, data) +} + +func (controller Controller) CameraMove(command string, position []float64, rotation []float64) error { + subdata := map[string]interface{}{"command": command} + subdata["position"] = map[string]interface{}{"x": position[0], "y": position[1], "z": position[2]} + subdata["rotation"] = map[string]interface{}{"x": rotation[0], "y": rotation[1]} + data := map[string]interface{}{"type": "camera", "data": subdata} + if State.DebugLvl > WARNING { + Disp(data) + } + + return controller.Ogree3D.Inform("HandleUI", -1, data) +} + +func (controller Controller) CameraWait(time float64) error { + subdata := map[string]interface{}{"command": "wait"} + subdata["position"] = map[string]interface{}{"x": 0, "y": 0, "z": 0} + subdata["rotation"] = map[string]interface{}{"x": 999, "y": time} + data := map[string]interface{}{"type": "camera", "data": subdata} + if State.DebugLvl > WARNING { + Disp(data) + } + + return controller.Ogree3D.Inform("HandleUI", -1, data) +} + +func (controller Controller) FocusUI(path string) error { + var id string + if path != "" { + obj, err := controller.GetObject(path) + if err != nil { + return err + } + category := models.EntityStrToInt(obj["category"].(string)) + if !models.IsPhysical(path) || category == models.SITE || category == models.BLDG || category == models.ROOM { + msg := "You cannot focus on this object. Note you cannot" + + " focus on Sites, Buildings and Rooms. " + + "For more information please refer to the help doc (man >)" + return fmt.Errorf(msg) + } + id = obj["id"].(string) + } else { + id = "" + } + + data := map[string]interface{}{"type": "focus", "data": id} + err := controller.Ogree3D.Inform("FocusUI", -1, data) + if err != nil { + return err + } + + if path != "" { + return controller.CD(path) + } else { + fmt.Println("Focus is now empty") + } + + return nil +} diff --git a/CLI/controllers/ui_test.go b/CLI/controllers/ui_test.go new file mode 100644 index 000000000..3503c86b4 --- /dev/null +++ b/CLI/controllers/ui_test.go @@ -0,0 +1,178 @@ +package controllers_test + +import ( + test_utils "cli/test" + "strings" + "testing" + + "github.com/stretchr/testify/assert" +) + +// Test UI (UIDelay, UIToggle, UIHighlight) +func TestUIDelay(t *testing.T) { + controller, _, ogree3D := layersSetup(t) + // ogree3D. + time := 15.0 + data := map[string]interface{}{ + "type": "ui", + "data": map[string]interface{}{ + "command": "delay", + "data": time, + }, + } + ogree3D.On("Inform", "HandleUI", -1, data).Return(nil).Once() // The inform should be called once + err := controller.UIDelay(time) + assert.Nil(t, err) +} + +func TestUIToggle(t *testing.T) { + controller, _, ogree3D := layersSetup(t) + // ogree3D. + feature := "feature" + enable := true + data := map[string]interface{}{ + "type": "ui", + "data": map[string]interface{}{ + "command": feature, + "data": enable, + }, + } + + ogree3D.On("Inform", "HandleUI", -1, data).Return(nil).Once() // The inform should be called once + err := controller.UIToggle(feature, enable) + assert.Nil(t, err) +} + +func TestUIHighlightObjectNotFound(t *testing.T) { + controller, mockAPI, ogree3D := layersSetup(t) + path := testRackObjPath + + test_utils.MockObjectNotFound(mockAPI, path) + + data := map[string]interface{}{ + "type": "ui", + "data": map[string]interface{}{ + "command": "highlight", + "data": "BASIC.A.R1.A01", + }, + } + + ogree3D.AssertNotCalled(t, "HandleUI", -1, data) + err := controller.UIHighlight("/Physical/BASIC/A/R1/A01") + assert.NotNil(t, err) + assert.Equal(t, "object not found", err.Error()) +} + +func TestUIHighlightWorks(t *testing.T) { + controller, mockAPI, ogree3D := layersSetup(t) + data := map[string]interface{}{ + "type": "ui", + "data": map[string]interface{}{ + "command": "highlight", + "data": rack1["id"], + }, + } + + test_utils.MockGetObject(mockAPI, rack1) + ogree3D.On("Inform", "HandleUI", -1, data).Return(nil).Once() // The inform should be called once + err := controller.UIHighlight("/Physical/BASIC/A/R1/A01") + assert.Nil(t, err) +} + +func TestUIClearCache(t *testing.T) { + controller, _, ogree3D := layersSetup(t) + data := map[string]interface{}{ + "type": "ui", + "data": map[string]interface{}{ + "command": "clearcache", + "data": "", + }, + } + + ogree3D.On("Inform", "HandleUI", -1, data).Return(nil).Once() // The inform should be called once + err := controller.UIClearCache() + assert.Nil(t, err) +} + +func TestCameraMove(t *testing.T) { + controller, _, ogree3D := layersSetup(t) + data := map[string]interface{}{ + "type": "camera", + "data": map[string]interface{}{ + "command": "move", + "position": map[string]interface{}{"x": 0.0, "y": 1.0, "z": 2.0}, + "rotation": map[string]interface{}{"x": 0.0, "y": 0.0}, + }, + } + + ogree3D.On("Inform", "HandleUI", -1, data).Return(nil).Once() // The inform should be called once + err := controller.CameraMove("move", []float64{0, 1, 2}, []float64{0, 0}) + assert.Nil(t, err) +} + +func TestCameraWait(t *testing.T) { + controller, _, ogree3D := layersSetup(t) + time := 15.0 + data := map[string]interface{}{ + "type": "camera", + "data": map[string]interface{}{ + "command": "wait", + "position": map[string]interface{}{"x": 0, "y": 0, "z": 0}, + "rotation": map[string]interface{}{"x": 999, "y": time}, + }, + } + + ogree3D.On("Inform", "HandleUI", -1, data).Return(nil).Once() // The inform should be called once + err := controller.CameraWait(time) + assert.Nil(t, err) +} + +func TestFocusUIObjectNotFound(t *testing.T) { + controller, mockAPI, ogree3D := layersSetup(t) + + test_utils.MockObjectNotFound(mockAPI, "/api/hierarchy_objects/"+rack1["id"].(string)) + err := controller.FocusUI("/Physical/" + strings.Replace(rack1["id"].(string), ".", "/", -1)) + ogree3D.AssertNotCalled(t, "Inform", "mock.Anything", "mock.Anything", "mock.Anything") + assert.NotNil(t, err) + assert.Equal(t, "object not found", err.Error()) +} + +func TestFocusUIEmptyPath(t *testing.T) { + controller, mockAPI, ogree3D := layersSetup(t) + data := map[string]interface{}{ + "type": "focus", + "data": "", + } + + ogree3D.On("Inform", "FocusUI", -1, data).Return(nil).Once() // The inform should be called once + err := controller.FocusUI("") + mockAPI.AssertNotCalled(t, "Request", "GET", "mock.Anything", "mock.Anything", "mock.Anything") + assert.Nil(t, err) +} + +func TestFocusUIErrorWithRoom(t *testing.T) { + controller, mockAPI, ogree3D := layersSetup(t) + errorMessage := "You cannot focus on this object. Note you cannot focus on Sites, Buildings and Rooms. " + errorMessage += "For more information please refer to the help doc (man >)" + + test_utils.MockGetObject(mockAPI, roomWithoutChildren) + err := controller.FocusUI("/Physical/" + strings.Replace(roomWithoutChildren["id"].(string), ".", "/", -1)) + ogree3D.AssertNotCalled(t, "Inform", "mock.Anything", "mock.Anything", "mock.Anything") + assert.NotNil(t, err) + assert.Equal(t, errorMessage, err.Error()) +} + +func TestFocusUIWorks(t *testing.T) { + controller, mockAPI, ogree3D := layersSetup(t) + data := map[string]interface{}{ + "type": "focus", + "data": rack1["id"], + } + + ogree3D.On("Inform", "FocusUI", -1, data).Return(nil).Once() // The inform should be called once + // Get Object will be called two times: Once in FocusUI and a second time in FocusUI->CD->Tree + test_utils.MockGetObject(mockAPI, rack1) + test_utils.MockGetObject(mockAPI, rack1) + err := controller.FocusUI("/Physical/" + strings.Replace(rack1["id"].(string), ".", "/", -1)) + assert.Nil(t, err) +} diff --git a/CLI/controllers/user.go b/CLI/controllers/user.go new file mode 100644 index 000000000..b23bf69d4 --- /dev/null +++ b/CLI/controllers/user.go @@ -0,0 +1,107 @@ +package controllers + +import ( + "cli/readline" + "fmt" + "math/rand" + "net/http" +) + +type User struct { + Email string + ID string +} + +func (controller Controller) CreateUser(email string, role string, domain string) error { + password := randPassword(14) + response, err := controller.API.Request( + "POST", + "/api/users", + map[string]any{ + "email": email, + "password": password, + "roles": map[string]any{ + domain: role, + }, + }, + http.StatusCreated, + ) + if err != nil { + return err + } + println(response.message) + println("password:" + password) + return nil +} + +func (controller Controller) AddRole(email string, role string, domain string) error { + response, err := controller.API.Request("GET", "/api/users", nil, http.StatusOK) + if err != nil { + return err + } + userList, userListOk := response.Body["data"].([]any) + if !userListOk { + return fmt.Errorf("response contains no user list") + } + userID := "" + for _, user := range userList { + userMap, ok := user.(map[string]any) + if !ok { + continue + } + userEmail, emailOk := userMap["email"].(string) + id, idOk := userMap["_id"].(string) + if emailOk && idOk && userEmail == email { + userID = id + break + } + } + if userID == "" { + return fmt.Errorf("user not found") + } + response, err = controller.API.Request("PATCH", fmt.Sprintf("/api/users/%s", userID), + map[string]any{ + "roles": map[string]any{ + domain: role, + }, + }, + http.StatusOK, + ) + if err != nil { + return err + } + println(response.message) + return nil +} + +func ChangePassword() error { + currentPassword, err := readline.Password("Current password: ") + if err != nil { + return err + } + newPassword, err := readline.Password("New password: ") + if err != nil { + return err + } + response, err := API.Request("POST", "/api/users/password/change", + map[string]any{ + "currentPassword": string(currentPassword), + "newPassword": string(newPassword), + }, + http.StatusOK, + ) + if err != nil { + return err + } + println(response.message) + return nil +} + +func randPassword(n int) string { + const passChars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" + b := make([]byte, n) + for i := range b { + b[i] = passChars[rand.Intn(len(passChars))] + } + return string(b) +} diff --git a/CLI/controllers/user_test.go b/CLI/controllers/user_test.go new file mode 100644 index 000000000..56ed1b7ae --- /dev/null +++ b/CLI/controllers/user_test.go @@ -0,0 +1,94 @@ +package controllers_test + +import ( + "cli/controllers" + "errors" + "testing" + + "github.com/stretchr/testify/assert" +) + +// Tests CreateUser +func TestCreateUserInvalidEmail(t *testing.T) { + controller, mockAPI, _ := layersSetup(t) + + mockAPI.On( + "Request", "POST", + "/api/users", + "mock.Anything", 201, + ).Return( + &controllers.Response{ + Body: map[string]any{ + "message": "A valid email address is required", + }, + Status: 400, + }, errors.New("[Response From API] A valid email address is required"), + ).Once() + + err := controller.CreateUser("email", "manager", "*") + assert.NotNil(t, err) + assert.Equal(t, "[Response From API] A valid email address is required", err.Error()) +} + +func TestCreateUserWorks(t *testing.T) { + controller, mockAPI, _ := layersSetup(t) + + mockAPI.On("Request", "POST", + "/api/users", + "mock.Anything", 201, + ).Return( + &controllers.Response{ + Body: map[string]any{ + "message": "Account has been created", + }, + }, nil, + ).Once() + + err := controller.CreateUser("email@email.com", "manager", "*") + assert.Nil(t, err) +} + +// Tests AddRole +func TestAddRoleUserNotFound(t *testing.T) { + controller, mockAPI, _ := layersSetup(t) + + mockAPI.On("Request", "GET", "/api/users", "mock.Anything", 200).Return( + &controllers.Response{ + Body: map[string]any{ + "data": []any{}, + }, + }, nil, + ).Once() + + err := controller.AddRole("email@email.com", "manager", "*") + assert.NotNil(t, err) + assert.Equal(t, "user not found", err.Error()) +} + +func TestAddRoleWorks(t *testing.T) { + controller, mockAPI, _ := layersSetup(t) + + mockAPI.On("Request", "GET", "/api/users", "mock.Anything", 200).Return( + &controllers.Response{ + Body: map[string]any{ + "data": []any{ + map[string]any{ + "_id": "507f1f77bcf86cd799439011", + "email": "email@email.com", + }, + }, + }, + }, nil, + ).Once() + + mockAPI.On("Request", "PATCH", "/api/users/507f1f77bcf86cd799439011", "mock.Anything", 200).Return( + &controllers.Response{ + Body: map[string]any{ + "message": "successfully updated user roles", + }, + }, nil, + ).Once() + + err := controller.AddRole("email@email.com", "manager", "*") + assert.Nil(t, err) +} diff --git a/CLI/controllers/utils.go b/CLI/controllers/utils.go new file mode 100644 index 000000000..134f24262 --- /dev/null +++ b/CLI/controllers/utils.go @@ -0,0 +1,64 @@ +package controllers + +//This file has a collection of utility functions used in the +//controller package +//And const definitions used throughout the controllers package +import ( + "encoding/json" +) + +// Debug Level Declaration +const ( + NONE = iota + ERROR + WARNING + INFO + DEBUG +) + +const RACKUNIT = .04445 //meter +const VIRTUALCONFIG = "virtual_config" + +// displays contents of maps +func Disp(x map[string]interface{}) { + + jx, _ := json.Marshal(x) + + println("JSON: ", string(jx)) +} + +// Returns true/false if exists and true/false if attr +// is in "attributes" maps +func AttrIsInObj(obj map[string]interface{}, attr string) (bool, bool) { + if _, ok := obj[attr]; ok { + return ok, false + } + + if hasAttr, _ := AttrIsInObj(obj, "attributes"); hasAttr { + if objAttributes, ok := obj["attributes"].(map[string]interface{}); ok { + _, ok := objAttributes[attr] + return ok, true + } + } + + return false, false +} + +type ErrorWithInternalError struct { + UserError error + InternalError error +} + +func (err ErrorWithInternalError) Error() string { + return err.UserError.Error() + " caused by " + err.InternalError.Error() +} + +// Utility functions +func determineStrKey(x map[string]interface{}, possible []string) string { + for idx := range possible { + if _, ok := x[possible[idx]]; ok { + return possible[idx] + } + } + return "" //The code should not reach this point! +} diff --git a/CLI/main.go b/CLI/main.go index eee027d2f..041e2ddd6 100644 --- a/CLI/main.go +++ b/CLI/main.go @@ -4,29 +4,13 @@ import ( "cli/config" c "cli/controllers" l "cli/logger" + "cli/parser" "cli/readline" "fmt" "os" "strings" ) -func SetPrompt(user string) string { - c.State.Prompt = "\u001b[1m\u001b[32m" + user + "@" + c.State.Customer + ":" - c.State.BlankPrompt = user + "@" + c.State.Customer + ":" - - c.State.Prompt += "\u001b[37;1m" + c.State.CurrPath - c.State.BlankPrompt += c.State.CurrPath - - if c.State.CurrDomain != "" { - c.State.Prompt += "\u001b[36m" + " [" + c.State.CurrDomain + "]" - c.State.BlankPrompt += " [" + c.State.CurrDomain + "]" - } - - c.State.Prompt += "\u001b[32m>\u001b[0m " - c.State.BlankPrompt += "> " - return c.State.Prompt -} - func main() { conf := config.ReadConfig() @@ -60,7 +44,7 @@ func main() { return } - err = InitVars(conf.Variables) + err = parser.InitVars(conf.Variables) if err != nil { println("Error while initializing variables :", err.Error()) return @@ -69,7 +53,7 @@ func main() { userShort := strings.Split(c.State.User.Email, "@")[0] rl, err := readline.NewEx(&readline.Config{ - Prompt: SetPrompt(userShort), + Prompt: parser.SetPrompt(userShort), HistoryFile: c.State.HistoryFilePath, AutoComplete: GetPrefixCompleter(), InterruptPrompt: "^C", @@ -89,16 +73,16 @@ func main() { //Execute Script if provided as arg and exit if conf.Script != "" { if strings.Contains(conf.Script, ".ocli") { - LoadFile(conf.Script) + parser.LoadFile(conf.Script) os.Exit(0) } } err = c.Ogree3D.Connect("", rl) if err != nil { - manageError(err, false) + parser.ManageError(err, false) } //Pass control to repl.go - Start(rl, userShort) + parser.Start(rl, userShort) } diff --git a/CLI/models/attributes.go b/CLI/models/attributes.go new file mode 100644 index 000000000..d5920d18f --- /dev/null +++ b/CLI/models/attributes.go @@ -0,0 +1,193 @@ +package models + +// Auxillary functions for parsing and validation of data +// before the CLI sends off to API + +import ( + l "cli/logger" + "fmt" + "strconv" + "strings" +) + +type EntityAttributes map[string]any + +var BldgBaseAttrs = EntityAttributes{ + "posXYUnit": "m", + "sizeUnit": "m", + "heightUnit": "m", +} + +var RoomBaseAttrs = EntityAttributes{ + "floorUnit": "t", + "posXYUnit": "m", + "sizeUnit": "m", + "heightUnit": "m", +} + +var RackBaseAttrs = EntityAttributes{ + "sizeUnit": "cm", + "heightUnit": "U", +} + +var DeviceBaseAttrs = EntityAttributes{ + "orientation": "front", + "sizeUnit": "mm", + "heightUnit": "mm", +} + +var GenericBaseAttrs = EntityAttributes{ + "sizeUnit": "cm", + "heightUnit": "cm", +} + +var CorridorBaseAttrs = GenericBaseAttrs + +var BaseAttrs = map[int]EntityAttributes{ + BLDG: BldgBaseAttrs, + ROOM: RoomBaseAttrs, + RACK: RackBaseAttrs, + DEVICE: DeviceBaseAttrs, + GENERIC: GenericBaseAttrs, + CORRIDOR: CorridorBaseAttrs, +} + +const referToWikiMsg = " Please refer to the wiki or manual reference" + + " for more details on how to create objects " + + "using this syntax" + +func SetPosAttr(ent int, attr EntityAttributes) error { + switch ent { + case BLDG, ROOM: + return SetPosXY(attr) + case RACK, CORRIDOR, GENERIC: + return SetPosXYZ(attr) + default: + return fmt.Errorf("invalid entity for pos attribution") + } +} + +func SetPosXY(attr EntityAttributes) error { + attr["posXY"] = SerialiseVector(attr, "posXY") + if posXY, ok := attr["posXY"].([]float64); !ok || len(posXY) != 2 { + l.GetErrorLogger().Println( + "User gave invalid posXY value") + return fmt.Errorf("invalid posXY attribute provided." + + " \nIt must be an array/list/vector with 2 elements." + + referToWikiMsg) + } + return nil +} + +func SetPosXYZ(attr EntityAttributes) error { + attr["posXYZ"] = SerialiseVector(attr, "posXYZ") + if posXY, ok := attr["posXYZ"].([]float64); !ok || len(posXY) != 3 { + l.GetErrorLogger().Println( + "User gave invalid pos value") + return fmt.Errorf("invalid pos attribute provided." + + " \nIt must be an array/list/vector with 2 or 3 elements." + + referToWikiMsg) + } + return nil +} + +func SetSize(attr map[string]any) error { + attr["size"] = SerialiseVector(attr, "size") + if _, ok := attr["size"].([]any); !ok { + if size, ok := attr["size"].([]float64); !ok || len(size) == 0 { + l.GetErrorLogger().Println( + "User gave invalid size value") + return fmt.Errorf("invalid size attribute provided." + + " \nIt must be an array/list/vector with 3 elements." + + referToWikiMsg) + + } + } + return nil +} + +func SerialiseVector(attr map[string]interface{}, want string) []float64 { + if vector, ok := attr[want].([]float64); ok { + if want == "size" && len(vector) == 3 { + attr["height"] = vector[2] + vector = vector[:len(vector)-1] + } else if want == "posXYZ" && len(vector) == 2 { + vector = append(vector, 0) + } + return vector + } else { + return []float64{} + } +} + +func SetDeviceSizeUIfExists(attr EntityAttributes) { + if sizeU, ok := attr["sizeU"]; ok { + //Convert block + //And Set height + if sizeUInt, ok := sizeU.(int); ok { + attr["sizeU"] = sizeUInt + attr["height"] = float64(sizeUInt) * 44.5 + } else if sizeUFloat, ok := sizeU.(float64); ok { + attr["sizeU"] = sizeUFloat + attr["height"] = sizeUFloat * 44.5 + } + } +} + +func SetDeviceSlotOrPosU(attr EntityAttributes) error { + //Process the posU/slot attribute + if x, ok := attr["posU/slot"].([]string); ok && len(x) > 0 { + delete(attr, "posU/slot") + if posU, err := strconv.Atoi(x[0]); len(x) == 1 && err == nil { + attr["posU"] = posU + } else { + if slots, err := CheckExpandStrVector(x); err != nil { + return err + } else { + attr["slot"] = slots + } + } + } + return nil +} + +// CheckExpandStrVector: allow usage of .. on device slot and group content vector +// converting [slot01..slot03] on [slot01,slot02,slot03] +func CheckExpandStrVector(slotVector []string) ([]string, error) { + slots := []string{} + for _, slot := range slotVector { + if strings.Contains(slot, "..") { + if len(slotVector) != 1 { + return nil, fmt.Errorf("Invalid device syntax: .. can only be used in a single element vector") + } + return expandStrToVector(slot) + } else { + slots = append(slots, slot) + } + } + return slots, nil +} + +func expandStrToVector(slot string) ([]string, error) { + slots := []string{} + errMsg := "Invalid device syntax: incorrect use of .. for slot" + parts := strings.Split(slot, "..") + if len(parts) != 2 || + (parts[0][:len(parts[0])-1] != parts[1][:len(parts[1])-1]) { + l.GetWarningLogger().Println(errMsg) + return nil, fmt.Errorf(errMsg) + } else { + start, errS := strconv.Atoi(string(parts[0][len(parts[0])-1])) + end, errE := strconv.Atoi(string(parts[1][len(parts[1])-1])) + if errS != nil || errE != nil { + l.GetWarningLogger().Println(errMsg) + return nil, fmt.Errorf(errMsg) + } else { + prefix := parts[0][:len(parts[0])-1] + for i := start; i <= end; i++ { + slots = append(slots, prefix+strconv.Itoa(i)) + } + return slots, nil + } + } +} diff --git a/CLI/models/attributes_test.go b/CLI/models/attributes_test.go new file mode 100644 index 000000000..84829f110 --- /dev/null +++ b/CLI/models/attributes_test.go @@ -0,0 +1,45 @@ +package models_test + +import ( + l "cli/logger" + "cli/models" + "testing" + + "github.com/stretchr/testify/assert" +) + +func init() { + l.InitLogs() +} + +func TestExpandSlotVector(t *testing.T) { + slots, err := models.CheckExpandStrVector([]string{"slot1..slot3", "slot4"}) + assert.Nil(t, slots) + assert.NotNil(t, err) + assert.ErrorContains(t, err, "Invalid device syntax: .. can only be used in a single element vector") + + slots, err = models.CheckExpandStrVector([]string{"slot1..slot3..slot7"}) + assert.Nil(t, slots) + assert.NotNil(t, err) + assert.ErrorContains(t, err, "Invalid device syntax: incorrect use of .. for slot") + + slots, err = models.CheckExpandStrVector([]string{"slot1..slots3"}) + assert.Nil(t, slots) + assert.NotNil(t, err) + assert.ErrorContains(t, err, "Invalid device syntax: incorrect use of .. for slot") + + slots, err = models.CheckExpandStrVector([]string{"slot1..slotE"}) + assert.Nil(t, slots) + assert.NotNil(t, err) + assert.ErrorContains(t, err, "Invalid device syntax: incorrect use of .. for slot") + + slots, err = models.CheckExpandStrVector([]string{"slot1..slot3"}) + assert.Nil(t, err) + assert.NotNil(t, slots) + assert.EqualValues(t, []string{"slot1", "slot2", "slot3"}, slots) + + slots, err = models.CheckExpandStrVector([]string{"slot1", "slot3"}) + assert.Nil(t, err) + assert.NotNil(t, slots) + assert.EqualValues(t, []string{"slot1", "slot3"}, slots) +} diff --git a/CLI/models/com.go b/CLI/models/com.go deleted file mode 100755 index 374ba6489..000000000 --- a/CLI/models/com.go +++ /dev/null @@ -1,22 +0,0 @@ -package models - -import ( - "bytes" - "encoding/json" - "net/http" -) - -// Function helps with API Requests -func Send(method, URL, key string, data map[string]any) (*http.Response, error) { - client := &http.Client{} - dataJSON, err := json.Marshal(data) - if err != nil { - return nil, err - } - req, err := http.NewRequest(method, URL, bytes.NewBuffer(dataJSON)) - if err != nil { - return nil, err - } - req.Header.Set("Authorization", "Bearer "+key) - return client.Do(req) -} diff --git a/CLI/models/entity.go b/CLI/models/entity.go index 0e6c1ced2..de092c950 100644 --- a/CLI/models/entity.go +++ b/CLI/models/entity.go @@ -1,5 +1,11 @@ package models +import ( + l "cli/logger" + "fmt" + pathutil "path" +) + const ( SITE = iota BLDG @@ -128,3 +134,18 @@ func GetParentOfEntity(ent int) int { func EntityCreationMustBeInformed(entity int) bool { return entity != TAG } + +func SetObjectBaseData(entity int, path string, data map[string]any) error { + name := pathutil.Base(path) + if name == "." || name == "" { + l.GetWarningLogger().Println("Invalid path name provided for OCLI object creation") + return fmt.Errorf("invalid path name provided for OCLI object creation") + } + data["name"] = name + data["category"] = EntityToString(entity) + data["description"] = "" + if _, hasAttributes := data["attributes"].(map[string]any); !hasAttributes { + data["attributes"] = map[string]any{} + } + return nil +} diff --git a/CLI/models/path.go b/CLI/models/path.go index e416950d5..b99760647 100644 --- a/CLI/models/path.go +++ b/CLI/models/path.go @@ -155,6 +155,17 @@ func PhysicalIDToPath(id string) string { return PhysicalPath + strings.ReplaceAll(id, ".", "/") } +func GetObjectIDFromPath(pathStr string) string { + for _, prefix := range PathPrefixes { + if strings.HasPrefix(pathStr, string(prefix)) { + id := pathStr[len(prefix):] + id = strings.ReplaceAll(id, "/", ".") + return id + } + } + return "" +} + // Removes last "amount" elements from the "path" func PathRemoveLast(path string, amount int) string { pathSplit := SplitPath(path) diff --git a/CLI/other/man/cmds.txt b/CLI/other/man/cmds.txt index 380ddb757..259b7def4 100644 --- a/CLI/other/man/cmds.txt +++ b/CLI/other/man/cmds.txt @@ -1,10 +1,16 @@ -USAGE: .cmds: [PATH] +USAGE: .cmds: [PATH] [OPTIONS] Loads script file and executes OGREE commands in file NOTE Complete path must be provided. At this time it is preferable to enclose the path in quotes. +OPTIONS + -d + Specifies dry run mode. + Commands will only be validated, not effectively executed. + EXAMPLE .cmds: ../../scripts/ocliScript - .cmds: "path/to/scriptFile/ocliScript.ocli" \ No newline at end of file + .cmds: "path/to/scriptFile/ocliScript.ocli" + .cmds: ocliScriptToDryRun -d \ No newline at end of file diff --git a/CLI/ast.go b/CLI/parser/ast.go similarity index 90% rename from CLI/ast.go rename to CLI/parser/ast.go index 36c14319c..7ccae2af0 100644 --- a/CLI/ast.go +++ b/CLI/parser/ast.go @@ -1,4 +1,4 @@ -package main +package parser import ( "cli/config" @@ -22,6 +22,8 @@ func InitVars(variables []config.Vardef) (err error) { }() cmd.State.DynamicSymbolTable = make(map[string]interface{}) cmd.State.FuncTable = make(map[string]interface{}) + cmd.State.DryRun = false + cmd.State.DryRunErrors = []error{} for _, v := range variables { var varNode node switch val := v.Value.(type) { @@ -152,6 +154,9 @@ func (n *focusNode) execute() (interface{}, error) { if err != nil { return nil, err } + if cmd.State.DryRun { + return nil, nil + } return nil, cmd.C.FocusUI(path) } @@ -164,6 +169,9 @@ func (n *cdNode) execute() (interface{}, error) { if err != nil { return nil, err } + if cmd.State.DryRun { + return nil, nil + } return nil, cmd.C.CD(path) } @@ -196,6 +204,10 @@ func (n *lsNode) execute() (interface{}, error) { return nil, err } + if cmd.State.DryRun { + return nil, nil + } + objects, err := cmd.C.Ls(path, filters, recursive) if err != nil { return nil, err @@ -242,7 +254,11 @@ func (n *getUNode) execute() (interface{}, error) { return nil, err } if u < 0 { - return nil, fmt.Errorf("The U value must be positive") + return nil, fmt.Errorf("the U value must be positive") + } + + if cmd.State.DryRun { + return nil, nil } return nil, cmd.C.GetByAttr(path, u) @@ -263,6 +279,9 @@ func (n *getSlotNode) execute() (interface{}, error) { return nil, err } + if cmd.State.DryRun { + return nil, nil + } return nil, cmd.C.GetByAttr(path, slot) } @@ -275,9 +294,28 @@ func (n *loadNode) execute() (interface{}, error) { if err != nil { return nil, err } - //Usually functions from 'controller' pkg are called - //But in this case we are calling a function from 'main' pkg - return nil, LoadFile(path) + + isDryRun := false + path, isDryRun = strings.CutSuffix(path, " -d") + if isDryRun { + // set dry run state + path = strings.TrimSpace(path) + cmd.State.DryRun = true + cmd.State.DryRunErrors = []error{} + // run ocli file + LoadFile(path) + + // print result + views.PrintDryRunErrors(cmd.State.DryRunErrors) + + cmd.State.DryRun = false + cmd.State.DryRunErrors = []error{} + return nil, nil + } else { + //Usually functions from 'controller' pkg are called + //But in this case we are calling a function from 'parser' pkg + return nil, LoadFile(path) + } } type loadTemplateNode struct { @@ -293,6 +331,9 @@ func (n *loadTemplateNode) execute() (interface{}, error) { if data == nil { return nil, fmt.Errorf("cannot read json file : %s", path) } + if cmd.State.DryRun { + return nil, nil + } return path, cmd.C.LoadTemplate(data) } @@ -318,6 +359,9 @@ func (n *deleteObjNode) execute() (interface{}, error) { if err != nil { return nil, err } + if cmd.State.DryRun { + return nil, nil + } paths, err := cmd.C.DeleteObj(path) if err != nil { return nil, err @@ -338,6 +382,9 @@ type deleteSelectionNode struct{} func (n *deleteSelectionNode) execute() (interface{}, error) { var errBuilder strings.Builder deleted := 0 + if cmd.State.DryRun { + return nil, nil + } if cmd.State.ClipBoard != nil { for _, obj := range cmd.State.ClipBoard { _, err := cmd.C.DeleteObj(obj) @@ -366,6 +413,9 @@ func (n *deleteAttrNode) execute() (interface{}, error) { if err != nil { return nil, err } + if cmd.State.DryRun { + return nil, nil + } return nil, cmd.C.UnsetAttribute(path, n.attr) } @@ -432,6 +482,10 @@ func (n *getObjectNode) execute() (interface{}, error) { return nil, err } + if cmd.State.DryRun { + return nil, nil + } + objs, _, err := cmd.C.GetObjectsWildcard(path, filters, recursive) if err != nil { return nil, err @@ -476,6 +530,10 @@ func (n *selectObjectNode) execute() (interface{}, error) { return nil, err } + if cmd.State.DryRun { + return nil, nil + } + selection, err := cmd.C.Select(path) if err != nil { return nil, err @@ -757,6 +815,9 @@ func (n *updateObjNode) execute() (interface{}, error) { } values = append(values, val) } + if cmd.State.DryRun { + return nil, nil + } paths, err := cmd.C.UnfoldPath(path) if err != nil { return nil, err @@ -828,7 +889,7 @@ func updateAttributes(path, attributeName string, values []any) (map[string]any, vecStr = append(vecStr, value.(string)) } var err error - if vecStr, err = controllers.ExpandStrVector(vecStr); err != nil { + if vecStr, err = models.CheckExpandStrVector(vecStr); err != nil { return nil, err } attributes = map[string]any{attributeName: vecStr} @@ -860,6 +921,9 @@ func (n *treeNode) execute() (interface{}, error) { if err != nil { return nil, err } + if cmd.State.DryRun { + return nil, nil + } root, err := cmd.C.Tree(path, n.depth) if err != nil { return nil, err @@ -886,6 +950,9 @@ func (n *drawNode) execute() (interface{}, error) { return nil, err } + if cmd.State.DryRun { + return nil, nil + } return nil, cmd.C.Draw(path, n.depth, n.force) } @@ -895,6 +962,9 @@ type undrawNode struct { func (n *undrawNode) execute() (interface{}, error) { if n.path == nil { + if cmd.State.DryRun { + return nil, nil + } return nil, cmd.C.Undraw("") } @@ -903,18 +973,27 @@ func (n *undrawNode) execute() (interface{}, error) { return nil, err } + if cmd.State.DryRun { + return nil, nil + } return nil, cmd.C.Undraw(path) } type lsogNode struct{} func (n *lsogNode) execute() (interface{}, error) { + if cmd.State.DryRun { + return nil, nil + } return nil, cmd.LSOG() } type lsenterpriseNode struct{} func (n *lsenterpriseNode) execute() (interface{}, error) { + if cmd.State.DryRun { + return nil, nil + } return nil, cmd.LSEnterprise() } @@ -960,6 +1039,9 @@ func (n *selectChildrenNode) execute() (interface{}, error) { if err != nil { return nil, err } + if cmd.State.DryRun { + return nil, nil + } v, err := cmd.C.SetClipBoard(paths) if err != nil { return nil, err @@ -1023,7 +1105,7 @@ func (n *createDomainNode) execute() (interface{}, error) { attributes := map[string]interface{}{"attributes": map[string]interface{}{"color": color}} - return nil, cmd.C.CreateObject(path, models.DOMAIN, attributes) + return nil, cmd.C.CreateObject(path, models.DOMAIN, attributes, cmd.State.DryRun) } type createSiteNode struct { @@ -1036,7 +1118,7 @@ func (n *createSiteNode) execute() (interface{}, error) { return nil, err } - return nil, cmd.C.CreateObject(path, models.SITE, map[string]any{}) + return nil, cmd.C.CreateObject(path, models.SITE, map[string]any{}, cmd.State.DryRun) } type createBuildingNode struct { @@ -1064,7 +1146,7 @@ func (n *createBuildingNode) execute() (interface{}, error) { addSizeOrTemplate(n.sizeOrTemplate, attributes, models.BLDG) - return nil, cmd.C.CreateObject(path, models.BLDG, map[string]any{"attributes": attributes}) + return nil, cmd.C.CreateObject(path, models.BLDG, map[string]any{"attributes": attributes}, cmd.State.DryRun) } type createRoomNode struct { @@ -1122,7 +1204,8 @@ func (n *createRoomNode) execute() (interface{}, error) { } } - return nil, cmd.C.CreateObject(path, models.ROOM, map[string]any{"attributes": attributes}) + return nil, cmd.C.CreateObject(path, models.ROOM, + map[string]any{"attributes": attributes}, cmd.State.DryRun) } type createRackNode struct { @@ -1158,7 +1241,8 @@ func (n *createRackNode) execute() (interface{}, error) { addSizeOrTemplate(n.sizeOrTemplate, attributes, models.RACK) - return nil, cmd.C.CreateObject(path, models.RACK, map[string]any{"attributes": attributes}) + return nil, cmd.C.CreateObject(path, models.RACK, + map[string]any{"attributes": attributes}, cmd.State.DryRun) } type createGenericNode struct { @@ -1210,7 +1294,8 @@ func (n *createGenericNode) execute() (interface{}, error) { addSizeOrTemplate(n.sizeOrTemplate, attributes, models.GENERIC) - return nil, cmd.C.CreateObject(path, models.GENERIC, map[string]any{"attributes": attributes}) + return nil, cmd.C.CreateObject(path, models.GENERIC, + map[string]any{"attributes": attributes}, cmd.State.DryRun) } type createDeviceNode struct { @@ -1263,7 +1348,8 @@ func (n *createDeviceNode) execute() (interface{}, error) { attributes["orientation"] = side } - return nil, cmd.C.CreateObject(path, models.DEVICE, map[string]any{"attributes": attributes}) + return nil, cmd.C.CreateObject(path, models.DEVICE, + map[string]any{"attributes": attributes}, cmd.State.DryRun) } type createVirtualNode struct { @@ -1306,7 +1392,8 @@ func (n *createVirtualNode) execute() (interface{}, error) { attributes[controllers.VIRTUALCONFIG].(map[string]any)["role"] = role } - return nil, cmd.C.CreateObject(path, models.VIRTUALOBJ, map[string]any{"attributes": attributes}) + return nil, cmd.C.CreateObject(path, models.VIRTUALOBJ, + map[string]any{"attributes": attributes}, cmd.State.DryRun) } type createGroupNode struct { @@ -1331,7 +1418,7 @@ func (n *createGroupNode) execute() (interface{}, error) { } data["attributes"] = map[string]interface{}{"content": objs} - return nil, cmd.C.CreateObject(path, models.GROUP, data) + return nil, cmd.C.CreateObject(path, models.GROUP, data, cmd.State.DryRun) } type createTagNode struct { @@ -1350,6 +1437,9 @@ func (n *createTagNode) execute() (interface{}, error) { return nil, err } + if cmd.State.DryRun { + return nil, nil + } return nil, cmd.C.CreateTag(slug, color) } @@ -1375,6 +1465,9 @@ func (n *createLayerNode) execute() (interface{}, error) { return nil, err } + if cmd.State.DryRun { + return nil, nil + } return nil, cmd.C.CreateLayer(slug, applicability, filterValue) } @@ -1419,7 +1512,8 @@ func (n *createCorridorNode) execute() (interface{}, error) { } attributes := map[string]any{"posXYZ": pos, "posXYUnit": unit, "rotation": rotation, "size": size, "temperature": temp} - return nil, cmd.C.CreateObject(path, models.CORRIDOR, map[string]any{"attributes": attributes}) + return nil, cmd.C.CreateObject(path, models.CORRIDOR, + map[string]any{"attributes": attributes}, cmd.State.DryRun) } type createOrphanNode struct { @@ -1440,7 +1534,8 @@ func (n *createOrphanNode) execute() (interface{}, error) { attributes := map[string]any{"template": template} - return nil, cmd.C.CreateObject(path, models.STRAY_DEV, map[string]any{"attributes": attributes}) + return nil, cmd.C.CreateObject(path, models.STRAY_DEV, + map[string]any{"attributes": attributes}, cmd.State.DryRun) } type createUserNode struct { @@ -1463,6 +1558,9 @@ func (n *createUserNode) execute() (interface{}, error) { return nil, err } + if cmd.State.DryRun { + return nil, nil + } return nil, cmd.C.CreateUser(email, role, domain) } @@ -1486,12 +1584,18 @@ func (n *addRoleNode) execute() (interface{}, error) { return nil, err } + if cmd.State.DryRun { + return nil, nil + } return nil, cmd.C.AddRole(email, role, domain) } type changePasswordNode struct{} func (n *changePasswordNode) execute() (interface{}, error) { + if cmd.State.DryRun { + return nil, nil + } return nil, cmd.ChangePassword() } @@ -1500,12 +1604,18 @@ type connect3DNode struct { } func (n *connect3DNode) execute() (interface{}, error) { + if cmd.State.DryRun { + return nil, nil + } return nil, cmd.Connect3D(n.url) } type disconnect3DNode struct{} func (n *disconnect3DNode) execute() (interface{}, error) { + if cmd.State.DryRun { + return nil, nil + } cmd.Disconnect3D() return nil, nil } @@ -1515,6 +1625,9 @@ type uiDelayNode struct { } func (n *uiDelayNode) execute() (interface{}, error) { + if cmd.State.DryRun { + return nil, nil + } return nil, cmd.C.UIDelay(n.time) } @@ -1524,6 +1637,9 @@ type uiToggleNode struct { } func (n *uiToggleNode) execute() (interface{}, error) { + if cmd.State.DryRun { + return nil, nil + } return nil, cmd.C.UIToggle(n.feature, n.enable) } @@ -1536,6 +1652,9 @@ func (n *uiHighlightNode) execute() (interface{}, error) { if err != nil { return nil, err } + if cmd.State.DryRun { + return nil, nil + } return nil, cmd.C.UIHighlight(path) } @@ -1543,6 +1662,9 @@ type uiClearCacheNode struct { } func (n *uiClearCacheNode) execute() (interface{}, error) { + if cmd.State.DryRun { + return nil, nil + } return nil, cmd.C.UIClearCache() } @@ -1562,6 +1684,9 @@ func (n *cameraMoveNode) execute() (interface{}, error) { return nil, err } + if cmd.State.DryRun { + return nil, nil + } return nil, cmd.C.CameraMove(n.command, position, rotation) } @@ -1612,6 +1737,9 @@ func (n *linkObjectNode) execute() (interface{}, error) { } } + if cmd.State.DryRun { + return nil, nil + } return nil, cmd.C.LinkObject(source, dest, n.attrs, values, slots) } @@ -1624,6 +1752,9 @@ func (n *unlinkObjectNode) execute() (interface{}, error) { if err != nil { return nil, err } + if cmd.State.DryRun { + return nil, nil + } return nil, cmd.C.UnlinkObject(source) } @@ -1691,25 +1822,27 @@ func (a *assignNode) execute() (interface{}, error) { // Validate format for cmd [room]:areas=[r1,r2,r3,r4]@[t1,t2,t3,t4] func validateAreas(areas map[string]interface{}) error { - if reserved, ok := areas["reserved"].([]float64); ok { - if tech, ok := areas["technical"].([]float64); ok { - if len(reserved) == 4 && len(tech) == 4 { - return nil - } else { - if len(reserved) != 4 && len(tech) == 4 { - return errorResponder("reserved", "4", false) - } else if len(tech) != 4 && len(reserved) == 4 { - return errorResponder("technical", "4", false) - } else { //Both invalid - return errorResponder("reserved and technical", "4", true) - } - } - } else { + reserved, hasReserved := areas["reserved"].([]float64) + if !hasReserved { + return errorResponder("reserved", "4", false) + } + tech, hasTechnical := areas["technical"].([]float64) + if !hasTechnical { + return errorResponder("technical", "4", false) + } + + if len(reserved) == 4 && len(tech) == 4 { + return nil + } else { + if len(reserved) != 4 && len(tech) == 4 { + return errorResponder("reserved", "4", false) + } else if len(tech) != 4 && len(reserved) == 4 { return errorResponder("technical", "4", false) + } else { //Both invalid + return errorResponder("reserved and technical", "4", true) } - } else { - return errorResponder("reserved", "4", false) } + } type cpNode struct { @@ -1728,5 +1861,8 @@ func (n *cpNode) execute() (interface{}, error) { return nil, err } + if cmd.State.DryRun { + return nil, nil + } return nil, cmd.C.Cp(source, dest) } diff --git a/CLI/ast_test.go b/CLI/parser/ast_test.go similarity index 99% rename from CLI/ast_test.go rename to CLI/parser/ast_test.go index 7a43d3ebc..85565e11a 100644 --- a/CLI/ast_test.go +++ b/CLI/parser/ast_test.go @@ -1,4 +1,4 @@ -package main +package parser import ( "cli/controllers" @@ -175,7 +175,7 @@ func TestGetUNodeExecute(t *testing.T) { assert.Nil(t, value) assert.NotNil(t, err) - assert.ErrorContains(t, err, "The U value must be positive") + assert.ErrorContains(t, err, "the U value must be positive") uNode = getUNode{ path: &pathNode{path: &valueNode{"/Physical/site/building/room/rack"}}, diff --git a/CLI/astbool.go b/CLI/parser/astbool.go similarity index 99% rename from CLI/astbool.go rename to CLI/parser/astbool.go index 6c07dc7ac..afa312728 100644 --- a/CLI/astbool.go +++ b/CLI/parser/astbool.go @@ -1,4 +1,4 @@ -package main +package parser import ( "fmt" diff --git a/CLI/astbool_test.go b/CLI/parser/astbool_test.go similarity index 99% rename from CLI/astbool_test.go rename to CLI/parser/astbool_test.go index 7f6046c45..4d649012e 100644 --- a/CLI/astbool_test.go +++ b/CLI/parser/astbool_test.go @@ -1,4 +1,4 @@ -package main +package parser import ( "testing" diff --git a/CLI/astflow.go b/CLI/parser/astflow.go similarity index 94% rename from CLI/astflow.go rename to CLI/parser/astflow.go index dd72b3aa8..d43211ed4 100644 --- a/CLI/astflow.go +++ b/CLI/parser/astflow.go @@ -1,4 +1,4 @@ -package main +package parser import ( c "cli/controllers" @@ -61,7 +61,6 @@ type forNode struct { body node } -// ToDo: this expression is not possible to obtain. Add it to parser func (n *forNode) execute() (interface{}, error) { _, err := n.init.execute() if err != nil { @@ -93,7 +92,6 @@ type forArrayNode struct { body node } -// ToDo: this expression is not possible to obtain. Add it to parser func (n *forArrayNode) execute() (interface{}, error) { val, err := n.arr.execute() if err != nil { diff --git a/CLI/astflow_test.go b/CLI/parser/astflow_test.go similarity index 99% rename from CLI/astflow_test.go rename to CLI/parser/astflow_test.go index 84291c472..d32fae752 100644 --- a/CLI/astflow_test.go +++ b/CLI/parser/astflow_test.go @@ -1,4 +1,4 @@ -package main +package parser import ( "cli/controllers" diff --git a/CLI/astnum.go b/CLI/parser/astnum.go similarity index 89% rename from CLI/astnum.go rename to CLI/parser/astnum.go index 8845afa47..5dab5966b 100644 --- a/CLI/astnum.go +++ b/CLI/parser/astnum.go @@ -1,9 +1,11 @@ -package main +package parser import ( "fmt" ) +const ZeroDivisionErrMeg = "cannot divide by 0" + type arithNode struct { op string left node @@ -33,12 +35,12 @@ func (a *arithNode) execute() (interface{}, error) { return leftIntVal * rightIntVal, nil case "/": if rightIntVal == 0 { - return nil, fmt.Errorf("cannot divide by 0") + return nil, fmt.Errorf(ZeroDivisionErrMeg) } return float64(leftIntVal) / float64(rightIntVal), nil case "\\": if rightIntVal == 0 { - return nil, fmt.Errorf("cannot divide by 0") + return nil, fmt.Errorf(ZeroDivisionErrMeg) } return leftIntVal / rightIntVal, nil case "%": @@ -62,7 +64,7 @@ func (a *arithNode) execute() (interface{}, error) { return leftFloatVal * rightFloatVal, nil case "/": if rightFloatVal == 0. { - return nil, fmt.Errorf("cannot divide by 0") + return nil, fmt.Errorf(ZeroDivisionErrMeg) } return leftFloatVal / rightFloatVal, nil default: diff --git a/CLI/astnum_test.go b/CLI/parser/astnum_test.go similarity index 93% rename from CLI/astnum_test.go rename to CLI/parser/astnum_test.go index 65e6d92f1..3d25efe19 100644 --- a/CLI/astnum_test.go +++ b/CLI/parser/astnum_test.go @@ -1,4 +1,4 @@ -package main +package parser import ( "testing" @@ -36,7 +36,7 @@ func TestArithNodeExecute(t *testing.T) { valNode = arithNode{"/", &valueNode{10}, &valueNode{0}} _, err = valNode.execute() assert.NotNil(t, err) - assert.ErrorContains(t, err, "cannot divide by 0") + assert.ErrorContains(t, err, ZeroDivisionErrMeg) valNode = arithNode{"/", &valueNode{10.0}, &valueNode{4}} value, err = valNode.execute() @@ -46,7 +46,7 @@ func TestArithNodeExecute(t *testing.T) { valNode = arithNode{"/", &valueNode{10.0}, &valueNode{0}} _, err = valNode.execute() assert.NotNil(t, err) - assert.ErrorContains(t, err, "cannot divide by 0") + assert.ErrorContains(t, err, ZeroDivisionErrMeg) // integer division valNode = arithNode{"\\", &valueNode{10}, &valueNode{4}} @@ -57,7 +57,7 @@ func TestArithNodeExecute(t *testing.T) { valNode = arithNode{"\\", &valueNode{10}, &valueNode{0}} _, err = valNode.execute() assert.NotNil(t, err) - assert.ErrorContains(t, err, "cannot divide by 0") + assert.ErrorContains(t, err, ZeroDivisionErrMeg) valNode = arithNode{"%", &valueNode{10}, &valueNode{4}} value, err = valNode.execute() diff --git a/CLI/aststr.go b/CLI/parser/aststr.go similarity index 98% rename from CLI/aststr.go rename to CLI/parser/aststr.go index b8f7a4e68..71e2b08df 100644 --- a/CLI/aststr.go +++ b/CLI/parser/aststr.go @@ -1,4 +1,4 @@ -package main +package parser import ( c "cli/controllers" diff --git a/CLI/aststr_test.go b/CLI/parser/aststr_test.go similarity index 98% rename from CLI/aststr_test.go rename to CLI/parser/aststr_test.go index e07579301..b063b0b72 100644 --- a/CLI/aststr_test.go +++ b/CLI/parser/aststr_test.go @@ -1,4 +1,4 @@ -package main +package parser import ( "cli/models" diff --git a/CLI/astutil.go b/CLI/parser/astutil.go similarity index 99% rename from CLI/astutil.go rename to CLI/parser/astutil.go index 71a276f64..f4c0b36e5 100644 --- a/CLI/astutil.go +++ b/CLI/parser/astutil.go @@ -1,4 +1,4 @@ -package main +package parser import ( cmd "cli/controllers" diff --git a/CLI/astutil_test.go b/CLI/parser/astutil_test.go similarity index 99% rename from CLI/astutil_test.go rename to CLI/parser/astutil_test.go index 075aaa38d..e17745458 100644 --- a/CLI/astutil_test.go +++ b/CLI/parser/astutil_test.go @@ -1,4 +1,4 @@ -package main +package parser import ( "cli/models" diff --git a/CLI/lexer.go b/CLI/parser/lexer.go similarity index 99% rename from CLI/lexer.go rename to CLI/parser/lexer.go index c1687a549..9e152929f 100644 --- a/CLI/lexer.go +++ b/CLI/parser/lexer.go @@ -1,4 +1,4 @@ -package main +package parser import ( "strconv" diff --git a/CLI/lexer_test.go b/CLI/parser/lexer_test.go similarity index 99% rename from CLI/lexer_test.go rename to CLI/parser/lexer_test.go index 5dc4c314d..fe351408b 100644 --- a/CLI/lexer_test.go +++ b/CLI/parser/lexer_test.go @@ -1,4 +1,4 @@ -package main +package parser import ( "testing" diff --git a/CLI/ocli.go b/CLI/parser/ocli.go similarity index 66% rename from CLI/ocli.go rename to CLI/parser/ocli.go index 7c91e6c4f..8205f7b28 100644 --- a/CLI/ocli.go +++ b/CLI/parser/ocli.go @@ -1,4 +1,4 @@ -package main +package parser //This file loads and executes OCLI script files @@ -82,24 +82,34 @@ func parseFile(path string) ([]parsedLine, error) { return result, nil } -type stackTraceError struct { +type StackTraceError struct { err error history string } -func newStackTraceError(err error, filename string, line string, lineNumber int) *stackTraceError { - stackErr := &stackTraceError{err: err} +func newStackTraceError(err error, filename string, line string, lineNumber int) *StackTraceError { + stackErr := &StackTraceError{err: err} stackErr.extend(filename, line, lineNumber) return stackErr } -func (s *stackTraceError) extend(filename string, line string, lineNumber int) { +func convertToStackTraceError(err error, filename string, line string, lineNumber int) *StackTraceError { + stackTraceErr, ok := err.(*StackTraceError) + if ok { + stackTraceErr.extend(filename, line, lineNumber) + } else { + stackTraceErr = newStackTraceError(err, filename, line, lineNumber) + } + return stackTraceErr +} + +func (s *StackTraceError) extend(filename string, line string, lineNumber int) { trace := fmt.Sprintf(" File \"%s\", line %d\n", filename, lineNumber) trace += " " + line + "\n" s.history = trace + s.history } -func (s *stackTraceError) Error() string { +func (s *StackTraceError) Error() string { msg := "Stack trace (most recent call last):\n" return msg + s.history + "Error : " + s.err.Error() } @@ -107,29 +117,38 @@ func (s *stackTraceError) Error() string { func LoadFile(path string) error { filename := filepath.Base(path) file, err := parseFile(path) - if err != nil { + if err != nil && !c.State.DryRun { + fmt.Println(err) return err } for i := range file { fmt.Println(file[i].line) _, err := file[i].root.execute() if err != nil { - errMsg := err.Error() - if strings.Contains(errMsg, "Duplicate") || strings.Contains(errMsg, "duplicate") { - l.GetWarningLogger().Println(errMsg) - if c.State.DebugLvl > c.NONE { - fmt.Println(errMsg) - } + if ok := isDuplicateErr(err); ok { + // do not interrupt ocli execution continue } - stackTraceErr, ok := err.(*stackTraceError) - if ok { - stackTraceErr.extend(filename, file[i].line, file[i].lineNumber) + stackTraceErr := convertToStackTraceError(err, filename, file[i].line, file[i].lineNumber) + if c.State.DryRun { + fmt.Println(stackTraceErr) + c.State.DryRunErrors = append(c.State.DryRunErrors, stackTraceErr) } else { - stackTraceErr = newStackTraceError(err, filename, file[i].line, file[i].lineNumber) + return stackTraceErr } - return stackTraceErr } } - return nil + return err +} + +func isDuplicateErr(err error) bool { + errMsg := err.Error() + if strings.Contains(errMsg, "Duplicate") || strings.Contains(errMsg, "duplicate") { + l.GetWarningLogger().Println(errMsg) + if c.State.DebugLvl > c.NONE { + fmt.Println(errMsg) + } + return true + } + return false } diff --git a/CLI/ocli_test.go b/CLI/parser/ocli_test.go similarity index 97% rename from CLI/ocli_test.go rename to CLI/parser/ocli_test.go index 854a216da..8dff68870 100644 --- a/CLI/ocli_test.go +++ b/CLI/parser/ocli_test.go @@ -1,4 +1,4 @@ -package main +package parser import ( "cli/controllers" @@ -120,7 +120,7 @@ func TestLoadFileError(t *testing.T) { errorMessage string }{ {"ParseError", "siteName=siteB\n", &fileParseError{}, "Syntax errors were found in the file: " + filename + "\nThe following commands were invalid\n LINE#: 1\tCOMMAND:siteName=siteB"}, - {"StackError", ".var: i = eval 10/0\n", &stackTraceError{}, "Stack trace (most recent call last):\n File \"" + filename + "\", line 1\n .var: i = eval 10/0\nError : cannot divide by 0"}, + {"StackError", ".var: i = eval 10/0\n", &StackTraceError{}, "Stack trace (most recent call last):\n File \"" + filename + "\", line 1\n .var: i = eval 10/0\nError : " + ZeroDivisionErrMeg}, } for _, tt := range tests { diff --git a/CLI/parser.go b/CLI/parser/parser.go similarity index 99% rename from CLI/parser.go rename to CLI/parser/parser.go index e7552ef17..1492495c2 100644 --- a/CLI/parser.go +++ b/CLI/parser/parser.go @@ -1,4 +1,4 @@ -package main +package parser import ( "cli/commands" @@ -313,7 +313,7 @@ loop: } } if trim { - s = strings.Trim(s, " \n") + s = strings.Trim(s, " \t\n") } if len(subExpr) == 0 { return &valueNode{s} diff --git a/CLI/parser_test.go b/CLI/parser/parser_test.go similarity index 99% rename from CLI/parser_test.go rename to CLI/parser/parser_test.go index c2eb01f77..d2a7d2818 100644 --- a/CLI/parser_test.go +++ b/CLI/parser/parser_test.go @@ -1,4 +1,4 @@ -package main +package parser import ( "cli/models" diff --git a/CLI/repl.go b/CLI/parser/repl.go similarity index 67% rename from CLI/repl.go rename to CLI/parser/repl.go index ca49f27da..126e6e6ef 100644 --- a/CLI/repl.go +++ b/CLI/parser/repl.go @@ -1,4 +1,4 @@ -package main +package parser //This file inits the State and //manages the interpreter and REPL @@ -28,14 +28,14 @@ func InterpretLine(str string) { } _, err := root.execute() if err != nil { - manageError(err, true) + ManageError(err, true) } } -func manageError(err error, addErrorPrefix bool) { +func ManageError(err error, addErrorPrefix bool) { l.GetErrorLogger().Println(err.Error()) if c.State.DebugLvl > c.NONE { - if traceErr, ok := err.(*stackTraceError); ok { + if traceErr, ok := err.(*StackTraceError); ok { fmt.Println(traceErr.Error()) } else if errWithInternalErr, ok := err.(c.ErrorWithInternalError); ok { printError(errWithInternalErr.UserError, addErrorPrefix) @@ -70,3 +70,20 @@ func Start(rl *readline.Instance, user string) { rl.SetPrompt(SetPrompt(user)) } } + +func SetPrompt(user string) string { + c.State.Prompt = "\u001b[1m\u001b[32m" + user + "@" + c.State.Customer + ":" + c.State.BlankPrompt = user + "@" + c.State.Customer + ":" + + c.State.Prompt += "\u001b[37;1m" + c.State.CurrPath + c.State.BlankPrompt += c.State.CurrPath + + if c.State.CurrDomain != "" { + c.State.Prompt += "\u001b[36m" + " [" + c.State.CurrDomain + "]" + c.State.BlankPrompt += " [" + c.State.CurrDomain + "]" + } + + c.State.Prompt += "\u001b[32m>\u001b[0m " + c.State.BlankPrompt += "> " + return c.State.Prompt +} diff --git a/CLI/utils/util.go b/CLI/utils/util.go index 42c146127..584034543 100755 --- a/CLI/utils/util.go +++ b/CLI/utils/util.go @@ -8,6 +8,7 @@ import ( "path/filepath" "reflect" "strconv" + "strings" ) func ExeDir() string { @@ -213,6 +214,10 @@ func IsFloat(x interface{}) bool { return ok || ok2 } +func IsNumeric(x interface{}) bool { + return IsInt(x) || IsFloat(x) +} + func CompareVals(val1 any, val2 any) (bool, bool) { val1Float, err1 := ValToFloat(val1, "") val2Float, err2 := ValToFloat(val2, "") @@ -240,8 +245,8 @@ func NameOrSlug(obj map[string]any) string { panic("child has no name/slug") } -func ObjectAttr(obj map[string]any, attr string) (any, bool) { - val, ok := obj[attr] +func GetValFromObj(obj map[string]any, key string) (any, bool) { + val, ok := obj[key] if ok { return val, true } @@ -249,9 +254,70 @@ func ObjectAttr(obj map[string]any, attr string) (any, bool) { if !ok { return nil, false } - val, ok = attributes[attr] + val, ok = attributes[key] if !ok { return nil, false } return val, true } + +// Helper func that safely copies a value in a map +func CopyMapVal(dest, source map[string]interface{}, key string) bool { + if _, ok := source[key]; ok { + dest[key] = source[key] + return true + } + return false +} + +// Convert []interface{} array to +// []map[string]interface{} array +func AnyArrToMapArr(x []interface{}) []map[string]interface{} { + ans := []map[string]interface{}{} + for i := range x { + ans = append(ans, x[i].(map[string]interface{})) + } + return ans +} + +func Stringify(x interface{}) string { + switch xArr := x.(type) { + case string: + return x.(string) + case int: + return strconv.Itoa(x.(int)) + case float32, float64: + return strconv.FormatFloat(float64(x.(float64)), 'f', -1, 64) + case bool: + return strconv.FormatBool(x.(bool)) + case []string: + return strings.Join(x.([]string), ",") + case []interface{}: + var arrStr []string + for i := range xArr { + arrStr = append(arrStr, Stringify(xArr[i])) + } + return "[" + strings.Join(arrStr, ",") + "]" + case []float64: + var arrStr []string + for i := range xArr { + arrStr = append(arrStr, Stringify(xArr[i])) + } + return "[" + strings.Join(arrStr, ",") + "]" + } + return "" +} + +func MergeMaps(x, y map[string]interface{}, overwrite bool) { + for i := range y { + //Conflict case + if _, ok := x[i]; ok { + if overwrite { + x[i] = y[i] + } + } else { + x[i] = y[i] + } + + } +} diff --git a/CLI/utils/util_test.go b/CLI/utils/util_test.go index ec3044889..dabc66aed 100644 --- a/CLI/utils/util_test.go +++ b/CLI/utils/util_test.go @@ -1,6 +1,7 @@ package utils_test import ( + test_utils "cli/test" "cli/utils" "testing" @@ -304,11 +305,11 @@ func TestObjectAttr(t *testing.T) { object := map[string]any{ "name": "my-name", } - value, ok := utils.ObjectAttr(object, "name") + value, ok := utils.GetValFromObj(object, "name") assert.True(t, ok) assert.Equal(t, object["name"], value) - value, ok = utils.ObjectAttr(object, "color") + value, ok = utils.GetValFromObj(object, "color") assert.False(t, ok) assert.Nil(t, value) @@ -316,11 +317,50 @@ func TestObjectAttr(t *testing.T) { "color": "blue", } - value, ok = utils.ObjectAttr(object, "color") + value, ok = utils.GetValFromObj(object, "color") assert.True(t, ok) assert.Equal(t, object["attributes"].(map[string]any)["color"], value) - value, ok = utils.ObjectAttr(object, "other") + value, ok = utils.GetValFromObj(object, "other") assert.False(t, ok) assert.Nil(t, value) } + +func TestStringify(t *testing.T) { + assert.Equal(t, "text", utils.Stringify("text")) + assert.Equal(t, "35", utils.Stringify(35)) + assert.Equal(t, "35", utils.Stringify(35.0)) + assert.Equal(t, "true", utils.Stringify(true)) + assert.Equal(t, "hello,world", utils.Stringify([]string{"hello", "world"})) + assert.Equal(t, "[45,21]", utils.Stringify([]float64{45, 21})) + assert.Equal(t, "[hello,5,[world,450]]", utils.Stringify([]any{"hello", 5, []any{"world", 450}})) + assert.Equal(t, "", utils.Stringify(map[string]any{"hello": 5})) +} + +func TestMergeMaps(t *testing.T) { + x := map[string]any{ + "a": "10", + "b": "11", + } + y := map[string]any{ + "b": "25", + "c": "40", + } + testMap := test_utils.CopyMap(x) + utils.MergeMaps(testMap, y, false) + assert.Contains(t, testMap, "a") + assert.Contains(t, testMap, "b") + assert.Contains(t, testMap, "c") + assert.Equal(t, x["a"], testMap["a"]) + assert.Equal(t, x["b"], testMap["b"]) + assert.Equal(t, y["c"], testMap["c"]) + + testMap = test_utils.CopyMap(x) + utils.MergeMaps(testMap, y, true) + assert.Contains(t, testMap, "a") + assert.Contains(t, testMap, "b") + assert.Contains(t, testMap, "c") + assert.Equal(t, x["a"], testMap["a"]) + assert.Equal(t, y["b"], testMap["b"]) + assert.Equal(t, y["c"], testMap["c"]) +} diff --git a/CLI/views/dryrun.go b/CLI/views/dryrun.go new file mode 100644 index 000000000..49cf29d1a --- /dev/null +++ b/CLI/views/dryrun.go @@ -0,0 +1,20 @@ +package views + +import "fmt" + +func PrintDryRunErrors(dryRunErrors []error) { + // print error quantity + fmt.Println("####################") + errCountMsg := fmt.Sprint("Errors found: ", len(dryRunErrors)) + if len(dryRunErrors) > 0 { + fmt.Println("\033[31m" + errCountMsg + "\033[0m") + } else { + fmt.Println("\u001b[32m" + errCountMsg + "\u001b[0m") + } + + // print error recap + for idx, err := range dryRunErrors { + fmt.Println("\033[31m# Error", idx, "\033[0m") + fmt.Println(err) + } +} diff --git a/CLI/views/ls.go b/CLI/views/ls.go index 98f3cfd94..60de5c09d 100644 --- a/CLI/views/ls.go +++ b/CLI/views/ls.go @@ -97,7 +97,7 @@ func LsWithFormat(objects []map[string]any, sortAttr string, relativePath *Relat attrVals := []any{getObjectNameOrPath(obj, relativePath)} for _, attr := range attributes { - attrVal, hasAttr := utils.ObjectAttr(obj, attr) + attrVal, hasAttr := utils.GetValFromObj(obj, attr) if !hasAttr { attrVal = "-" } @@ -131,7 +131,7 @@ func SortObjects(objects []map[string]any, sortAttr string) ([]map[string]any, e orderObjectsBy(objects, idOrName) } else { objects = pie.Filter(objects, func(object map[string]any) bool { - _, hasAttr := utils.ObjectAttr(object, sortAttr) + _, hasAttr := utils.GetValFromObj(object, sortAttr) return hasAttr }) @@ -140,8 +140,8 @@ func SortObjects(objects []map[string]any, sortAttr string) ([]map[string]any, e } sort.Slice(objects, func(i, j int) bool { - vali, _ := utils.ObjectAttr(objects[i], sortAttr) - valj, _ := utils.ObjectAttr(objects[j], sortAttr) + vali, _ := utils.GetValFromObj(objects[i], sortAttr) + valj, _ := utils.GetValFromObj(objects[j], sortAttr) res, _ := utils.CompareVals(vali, valj) return res }) @@ -176,8 +176,8 @@ func isObjectLayer(object map[string]any) bool { func objectsAreSortable(objects []map[string]any, attr string) bool { for i := 1; i < len(objects); i++ { - val0, _ := utils.ObjectAttr(objects[0], attr) - vali, _ := utils.ObjectAttr(objects[i], attr) + val0, _ := utils.GetValFromObj(objects[0], attr) + vali, _ := utils.GetValFromObj(objects[i], attr) _, comparable := utils.CompareVals(val0, vali) if !comparable { return false