From 822627d97b09197cfd2f496dad18fb2967893f51 Mon Sep 17 00:00:00 2001 From: Helder Betiol <37706737+helderbetiol@users.noreply.github.com> Date: Fri, 12 Jul 2024 10:58:14 +0200 Subject: [PATCH] Add breakers to racks (#492) * feat(cli, api) add breakers * fix(cli) tests * fix(cli) tests * refactor(cli) update attributes * feat(cli) avoid sep/pillar/breaker in wrong obj * fix(cli) better doc and test * fix(cli) minor test improvement * fix(cli) add tests * fix(cli,api) update tests * fix(cli) check len value for update interact --- API/models/model.go | 5 +- API/models/schemas/rack_schema.json | 34 ++- API/models/tag.go | 21 ++ API/models/tag_test.go | 18 ++ API/repository/base.go | 3 +- CLI/controllers/delete.go | 6 +- CLI/controllers/interact.go | 68 +++++ CLI/controllers/interact_test.go | 46 ++-- CLI/controllers/layer.go | 6 +- CLI/controllers/update.go | 286 +++++++++++++++++++- CLI/controllers/update_test.go | 389 +++++++++++++++++++++++++++- CLI/controllers/utils.go | 2 - CLI/models/attributes.go | 46 ++++ CLI/models/attributes_rack.go | 66 +++++ CLI/models/attributes_room.go | 65 +++++ CLI/models/attributes_test.go | 8 + CLI/models/entity.go | 13 + CLI/parser/ast.go | 348 +------------------------ CLI/parser/ast_test.go | 170 ------------ CLI/parser/astutil.go | 19 -- CLI/parser/astutil_test.go | 8 - CLI/parser/parser.go | 14 +- CLI/test/mocks.go | 4 + CLI/views/get.go | 12 +- 24 files changed, 1067 insertions(+), 590 deletions(-) create mode 100644 CLI/models/attributes_rack.go create mode 100644 CLI/models/attributes_room.go diff --git a/API/models/model.go b/API/models/model.go index 2458f23c1..2fdd1435d 100644 --- a/API/models/model.go +++ b/API/models/model.go @@ -14,6 +14,7 @@ import ( "strings" "time" + "github.com/elliotchance/pie/v2" "go.mongodb.org/mongo-driver/mongo" "go.mongodb.org/mongo-driver/mongo/options" @@ -21,6 +22,8 @@ import ( "go.mongodb.org/mongo-driver/bson/primitive" ) +var AttrsWithInnerObj = []string{"pillars", "separators", "breakers"} + // Helper functions func domainHasObjects(domain string) bool { @@ -90,7 +93,7 @@ func updateOldObjWithPatch(old map[string]interface{}, patch map[string]interfac for k, v := range patch { switch patchValueCasted := v.(type) { case map[string]interface{}: - if k == "pillars" || k == "separators" { + if pie.Contains(AttrsWithInnerObj, k) { old[k] = v } else { switch oldValueCasted := old[k].(type) { diff --git a/API/models/schemas/rack_schema.json b/API/models/schemas/rack_schema.json index 8aa3d989f..7b6c4e946 100644 --- a/API/models/schemas/rack_schema.json +++ b/API/models/schemas/rack_schema.json @@ -22,7 +22,11 @@ }, "posXYUnit": { "type": "string", - "enum": ["m", "t", "f"] + "enum": [ + "m", + "t", + "f" + ] }, "posZUnit": { "type": "string", @@ -44,6 +48,32 @@ }, "clearance": { "$ref": "refs/types.json#/definitions/clearanceVector" + }, + "breakers": { + "type": "object", + "additionalProperties": { + "type": "object", + "required": [ + "powerpanel" + ], + "properties": { + "powerpanel": { + "type": "string" + }, + "circuit": { + "type": "string" + }, + "type": { + "type": "string" + }, + "tag": { + "type": "string" + }, + "intensity": { + "type": "number" + } + } + } } }, "required": [ @@ -63,7 +93,7 @@ "height": 47, "heightUnit": "U", "rotation": [45, 45, 45], - "posXYZ": [4.6666666666667, -2, 0], + "posXYZ": [4.6666666666667, -2, 0], "posXYUnit": "m", "size": [80, 100.532442], "sizeUnit": "cm", diff --git a/API/models/tag.go b/API/models/tag.go index 8700c09ff..7f8422c31 100644 --- a/API/models/tag.go +++ b/API/models/tag.go @@ -87,11 +87,32 @@ func addAndRemoveFromTags(entity int, objectID string, object map[string]interfa delete(object, "tags-") } + + // check tags for rack breakers + if err := VerifyTagForRackBreaker(object); err != nil { + return err + } } return nil } +func VerifyTagForRackBreaker(object map[string]interface{}) *u.Error { + if breakers, ok := object["attributes"].(map[string]any)["breakers"].(map[string]any); ok { + tagsToCheck := []any{} + for _, breaker := range breakers { + if tag, ok := breaker.(map[string]any)["tag"]; ok { + tagsToCheck = append(tagsToCheck, tag) + } + } + err := verifyTagList(tagsToCheck) + if err != nil { + return err + } + } + return nil +} + // Deletes tag with slug "slug" func DeleteTag(slug string) *u.Error { tag, err := repository.GetTagBySlug(slug) diff --git a/API/models/tag_test.go b/API/models/tag_test.go index 4d4578c76..375d37a30 100644 --- a/API/models/tag_test.go +++ b/API/models/tag_test.go @@ -467,6 +467,24 @@ func TestCreateObjectWithTagsAsStringReturnsError(t *testing.T) { assert.ErrorContains(t, err, "JSON body doesn't validate with the expected JSON schema") } +func TestVerifyTagForRackBreakerWorks(t *testing.T) { + err := createTag("exists") + require.Nil(t, err) + rack := test_utils.GetEntityMap("rack", "rack-breaker-tags", "", integration.TestDBName) + rack["attributes"].(map[string]any)["breakers"] = map[string]any{"mybreaker": map[string]any{"tag": "exists"}} + err = models.VerifyTagForRackBreaker(rack) + assert.Nil(t, err) +} + +func TestVerifyTagForRackBreakerError(t *testing.T) { + rack := test_utils.GetEntityMap("rack", "rack-breaker-tags", "", integration.TestDBName) + rack["attributes"].(map[string]any)["breakers"] = map[string]any{"mybreaker": map[string]any{"tag": "not-exists"}} + err := models.VerifyTagForRackBreaker(rack) + assert.NotNil(t, err) + assert.Equal(t, u.ErrBadFormat, err.Type) + assert.Equal(t, "Tag \"not-exists\" not found", err.Message) +} + func createTag(slug string) *u.Error { _, err := models.CreateEntity( u.TAG, diff --git a/API/repository/base.go b/API/repository/base.go index 5154ab53e..2fc4b29ae 100644 --- a/API/repository/base.go +++ b/API/repository/base.go @@ -48,7 +48,8 @@ func ConnectToDB(host, port, user, pass, dbName, tenantName string) error { func SetupDB(db *mongo.Database) error { // Indexes creation // Enforce unique children - for _, entity := range []int{u.DOMAIN, u.SITE, u.BLDG, u.ROOM, u.RACK, u.DEVICE, u.AC, u.PWRPNL, u.CABINET, u.CORRIDOR, u.GROUP, u.STRAYOBJ, u.GENERIC} { + for _, entity := range []int{u.DOMAIN, u.SITE, u.BLDG, u.ROOM, u.RACK, u.DEVICE, u.AC, + u.PWRPNL, u.CABINET, u.CORRIDOR, u.GROUP, u.STRAYOBJ, u.GENERIC, u.VIRTUALOBJ} { if err := createUniqueIndex(db, u.EntityToString(entity), bson.M{"id": 1}); err != nil { return err } diff --git a/CLI/controllers/delete.go b/CLI/controllers/delete.go index e8e584c2f..b0333f1f2 100644 --- a/CLI/controllers/delete.go +++ b/CLI/controllers/delete.go @@ -57,11 +57,11 @@ func (controller Controller) UnsetAttribute(path string, attr string) error { if !hasAttributes { return fmt.Errorf("object has no attributes") } - if vconfigAttr, found := strings.CutPrefix(attr, VIRTUALCONFIG+"."); found { + if vconfigAttr, found := strings.CutPrefix(attr, VirtualConfigAttr+"."); 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 if vAttrs, ok := attributes[VirtualConfigAttr].(map[string]any); !ok { + return fmt.Errorf("object has no " + VirtualConfigAttr) } else { delete(vAttrs, vconfigAttr) } diff --git a/CLI/controllers/interact.go b/CLI/controllers/interact.go index 973198137..154ef6237 100644 --- a/CLI/controllers/interact.go +++ b/CLI/controllers/interact.go @@ -1,6 +1,7 @@ package controllers import ( + "cli/utils" "fmt" "strings" ) @@ -101,3 +102,70 @@ func (controller Controller) InteractObject(path string, keyword string, val int //-1 since its not neccessary to check for filtering return Ogree3D.InformOptional("Interact", -1, ans) } + +func (controller Controller) UpdateInteract(path, attrName string, values []any, hasSharpe bool) error { + if attrName != "labelFont" && len(values) != 1 { + return fmt.Errorf("only 1 value expected") + } + switch attrName { + case "displayContent", "alpha", "tilesName", "tilesColor", "U", "slots", "localCS": + return controller.SetBooleanInteractAttribute(path, values, attrName, hasSharpe) + case "label": + return controller.SetLabel(path, values, hasSharpe) + case "labelFont": + return controller.SetLabelFont(path, values) + case "labelBackground": + return controller.SetLabelBackground(path, values) + } + return nil +} + +func (controller Controller) SetLabel(path string, values []any, hasSharpe bool) error { + value, err := utils.ValToString(values[0], "value") + if err != nil { + return err + } + return controller.InteractObject(path, "label", value, hasSharpe) +} + +func (controller Controller) SetLabelFont(path string, values []any) error { + msg := "The font can only be bold or italic" + + " or be in the form of color@[colorValue]." + + "\n\nFor more information please refer to: " + + "\nhttps://github.com/ditrit/OGrEE-3D/wiki/CLI-langage#interact-with-objects" + + switch len(values) { + case 1: + if values[0] != "bold" && values[0] != "italic" { + return fmt.Errorf(msg) + } + return controller.InteractObject(path, "labelFont", values[0], false) + case 2: + if values[0] != "color" { + return fmt.Errorf(msg) + } + c, ok := utils.ValToColor(values[1]) + if !ok { + return fmt.Errorf("please provide a valid 6 length hex value for the color") + } + return controller.InteractObject(path, "labelFont", "color@"+c, false) + default: + return fmt.Errorf(msg) + } +} + +func (controller Controller) SetLabelBackground(path string, values []any) error { + c, ok := utils.ValToColor(values[0]) + if !ok { + return fmt.Errorf("please provide a valid 6 length hex value for the color") + } + return controller.InteractObject(path, "labelBackground", c, false) +} + +func (controller Controller) SetBooleanInteractAttribute(path string, values []any, attrName string, hasSharpe bool) error { + boolVal, err := utils.ValToBool(values[0], attrName) + if err != nil { + return err + } + return controller.InteractObject(path, attrName, boolVal, hasSharpe) +} diff --git a/CLI/controllers/interact_test.go b/CLI/controllers/interact_test.go index 5548751d5..4fb772051 100644 --- a/CLI/controllers/interact_test.go +++ b/CLI/controllers/interact_test.go @@ -71,44 +71,44 @@ func interactLabelTestSetup(t *testing.T) (cmd.Controller, *mocks.APIPort, *mock } func TestLabelNotStringReturnsError(t *testing.T) { - err := cmd.C.InteractObject("/Physical/BASIC/A/R1", "label", 1, false) + err := cmd.C.UpdateInteract("/Physical/BASIC/A/R1", "label", []any{1}, false) assert.NotNil(t, err) assert.Errorf(t, err, "The label value must be a string") } func TestNonExistingAttrReturnsError(t *testing.T) { controller, _, _ := interactLabelTestSetup(t) - err := controller.InteractObject("/Physical/BASIC/A/R1/A01", "label", "abc", true) + err := controller.UpdateInteract("/Physical/BASIC/A/R1/A01", "label", []any{"abc"}, true) assert.NotNil(t, err) assert.Errorf(t, err, "The specified attribute 'abc' does not exist in the object. \nPlease view the object (ie. $> get) and try again") } -func TestInteractObject(t *testing.T) { +func TestUpdateInteract(t *testing.T) { tests := []struct { name string path string keyword string - value interface{} + value []interface{} fromAttr bool }{ - {"LabelStringOk", "/Physical/BASIC/A/R1/A01", "label", "string", false}, - {"LabelSingleAttrOk", "/Physical/BASIC/A/R1/A01", "label", "name", true}, - {"LabelStringWithOneAttrOk", "/Physical/BASIC/A/R1/A01", "label", "My name is #name", false}, - {"LabelStringWithMultipleAttrOk", "/Physical/BASIC/A/R1/A01", "label", "My name is #name and I am a #category", false}, - {"LabelSingleAttrAndStringOk", "/Physical/BASIC/A/R1/A01", "label", "name is my name", true}, - {"LabelSingleAttrAndStringWithAttrOk", "/Physical/BASIC/A/R1/A01", "label", "name\n#id", true}, - {"FontItalicOk", "/Physical/BASIC/A/R1/A01", "labelFont", "italic", false}, - {"LabelFontBoldOk", "/Physical/BASIC/A/R1/A01", "labelFont", "bold", false}, - {"LabelColorOk", "/Physical/BASIC/A/R1/A01", "labelFont", "color@C0FFEE", false}, - {"LabelBackgroundOk", "/Physical/BASIC/A/R1/A01", "labelBackground", "C0FFEE", false}, - {"ContentOk", "/Physical/BASIC/A/R1/A01", "displayContent", true, false}, - {"AlphaOk", "/Physical/BASIC/A/R1/A01", "alpha", true, false}, + {"LabelStringOk", "/Physical/BASIC/A/R1/A01", "label", []any{"string"}, false}, + {"LabelSingleAttrOk", "/Physical/BASIC/A/R1/A01", "label", []any{"name"}, true}, + {"LabelStringWithOneAttrOk", "/Physical/BASIC/A/R1/A01", "label", []any{"My name is #name"}, false}, + {"LabelStringWithMultipleAttrOk", "/Physical/BASIC/A/R1/A01", "label", []any{"My name is #name and I am a #category"}, false}, + {"LabelSingleAttrAndStringOk", "/Physical/BASIC/A/R1/A01", "label", []any{"name is my name"}, true}, + {"LabelSingleAttrAndStringWithAttrOk", "/Physical/BASIC/A/R1/A01", "label", []any{"name\n#id"}, true}, + {"FontItalicOk", "/Physical/BASIC/A/R1/A01", "labelFont", []any{"italic"}, false}, + {"LabelFontBoldOk", "/Physical/BASIC/A/R1/A01", "labelFont", []any{"bold"}, false}, + {"LabelColorOk", "/Physical/BASIC/A/R1/A01", "labelFont", []any{"color", "C0FFEE"}, false}, + {"LabelBackgroundOk", "/Physical/BASIC/A/R1/A01", "labelBackground", []any{"C0FFEE"}, false}, + {"ContentOk", "/Physical/BASIC/A/R1/A01", "displayContent", []any{true}, false}, + {"AlphaOk", "/Physical/BASIC/A/R1/A01", "alpha", []any{true}, false}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { controller, _, _ := interactLabelTestSetup(t) - err := controller.InteractObject(tt.path, tt.keyword, tt.value, tt.fromAttr) + err := controller.UpdateInteract(tt.path, tt.keyword, tt.value, tt.fromAttr) assert.Nil(t, err) }) } @@ -135,8 +135,18 @@ func TestInteractObjectWithMock(t *testing.T) { controller, mockAPI, _ := interactTestSetup(t) test_utils.MockGetObject(mockAPI, tt.mockObject) - err := controller.InteractObject(tt.path, tt.keyword, tt.value, tt.fromAttr) + err := controller.UpdateInteract(tt.path, tt.keyword, []any{tt.value}, tt.fromAttr) assert.Nil(t, err) }) } } + +func TestSetLabel(t *testing.T) { + controller, mockAPI, _, _ := test_utils.NewControllerWithMocks(t) + + room := test_utils.GetEntity("rack", "rack", "site.building.room", "domain") + test_utils.MockGetObject(mockAPI, room) + err := controller.SetLabel("/Physical/site/building/room/rack", []any{"myLabel"}, false) + + assert.Nil(t, err) +} diff --git a/CLI/controllers/layer.go b/CLI/controllers/layer.go index 2fe335b3a..f9e1327eb 100644 --- a/CLI/controllers/layer.go +++ b/CLI/controllers/layer.go @@ -165,11 +165,11 @@ func (controller Controller) UpdateLayer(path string, attributeName string, valu return err } - _, err = controller.UpdateObj(path, map[string]any{attributeName: applicability}, false) + _, err = controller.PatchObj(path, map[string]any{attributeName: applicability}, false) case models.LayerFiltersAdd: - _, err = controller.UpdateObj(path, map[string]any{models.LayerFilters: "& (" + value.(string) + ")"}, false) + _, err = controller.PatchObj(path, map[string]any{models.LayerFilters: "& (" + value.(string) + ")"}, false) default: - _, err = controller.UpdateObj(path, map[string]any{attributeName: value}, false) + _, err = controller.PatchObj(path, map[string]any{attributeName: value}, false) if attributeName == "slug" { State.Hierarchy.Children["Logical"].Children["Layers"].IsCached = false } diff --git a/CLI/controllers/update.go b/CLI/controllers/update.go index fcb2e85ad..1ada948ef 100644 --- a/CLI/controllers/update.go +++ b/CLI/controllers/update.go @@ -2,10 +2,57 @@ package controllers import ( "cli/models" + "cli/utils" + "fmt" "net/http" + "regexp" + "strings" ) -func (controller Controller) UpdateObj(pathStr string, data map[string]any, withRecursive bool) (map[string]any, error) { +const invalidAttrNameMsg = "invalid attribute name" + +// InnerAttrObjs +const ( + PillarAttr = "pillar" + SeparatorAttr = "separator" + BreakerAttr = "breaker" +) +const VirtualConfigAttr = "virtual_config" + +func (controller Controller) UpdateObject(path, attr string, values []any) error { + var err error + switch attr { + case "areas": + _, err = controller.UpdateRoomAreas(path, values) + case "separators+", "pillars+", "breakers+": + _, err = controller.AddInnerAtrObj(strings.TrimSuffix(attr, "s+"), path, values) + case "pillars-", "separators-", "breakers-": + _, err = controller.DeleteInnerAttrObj(path, strings.TrimSuffix(attr, "-"), values[0].(string)) + case "vlinks+", "vlinks-": + _, err = controller.UpdateVirtualLink(path, attr, values[0].(string)) + case "domain", "tags+", "tags-": + isRecursive := len(values) > 1 && values[1] == "recursive" + _, err = controller.PatchObj(path, map[string]any{attr: values[0]}, isRecursive) + case "tags", "separators", "pillars", "vlinks", "breakers": + err = fmt.Errorf( + "object's %[1]s can not be updated directly, please use %[1]s+= and %[1]s-=", + attr, + ) + case "description": + _, err = controller.UpdateDescription(path, attr, values) + case VirtualConfigAttr: + err = controller.AddVirtualConfig(path, values) + default: + if strings.Contains(attr, ".") { + err = controller.UpdateInnerAtrObj(path, attr, values) + } else { + _, err = controller.UpdateAttributes(path, attr, values) + } + } + return err +} + +func (controller Controller) PatchObj(pathStr string, data map[string]any, withRecursive bool) (map[string]any, error) { obj, err := controller.GetObject(pathStr) if err != nil { return nil, err @@ -44,3 +91,240 @@ func (controller Controller) UpdateObj(pathStr string, data map[string]any, with return resp.Body, nil } + +// [obj]:[attributeName]=[values] +func (controller Controller) UpdateAttributes(path, attributeName string, values []any) (map[string]any, error) { + var attributes map[string]any + if attributeName == "slot" || attributeName == "content" { + vecStr := []string{} + for _, value := range values { + vecStr = append(vecStr, value.(string)) + } + var err error + if vecStr, err = models.CheckExpandStrVector(vecStr); err != nil { + return nil, err + } + attributes = map[string]any{attributeName: vecStr} + } else { + if len(values) > 1 { + return nil, fmt.Errorf("attributes can only be assigned a single value") + } + attributes = map[string]any{attributeName: values[0]} + } + + return controller.PatchObj(path, map[string]any{"attributes": attributes}, false) +} + +// [obj]:description=values +func (controller Controller) UpdateDescription(path string, attr string, values []any) (map[string]any, error) { + if len(values) != 1 { + return nil, fmt.Errorf("a single value is expected to update a description") + } + newDesc, err := utils.ValToString(values[0], "description") + if err != nil { + return nil, err + } + data := map[string]any{"description": newDesc} + return controller.PatchObj(path, data, false) +} + +// vlinks+ and vlinks- +func (controller Controller) UpdateVirtualLink(path string, attr string, value string) (map[string]any, error) { + if len(value) == 0 { + return nil, fmt.Errorf("an empty string is not valid") + } + + obj, err := controller.GetObject(path) + if err != nil { + return nil, err + } else if obj["category"] != models.EntityToString(models.VIRTUALOBJ) { + return nil, fmt.Errorf("only virtual objects can have vlinks") + } + + vlinks, hasVlinks := obj["attributes"].(map[string]any)["vlinks"].([]any) + if attr == "vlinks+" { + if !hasVlinks { + vlinks = []any{value} + } else { + vlinks = append(vlinks, value) + } + } else if attr == "vlinks-" { + if !hasVlinks { + return nil, fmt.Errorf("no vlinks defined for this object") + } + vlinks, err = removeVirtualLink(vlinks, value) + if err != nil { + return nil, err + } + } else { + return nil, fmt.Errorf("invalid vlink update command") + } + + data := map[string]any{"vlinks": vlinks} + return controller.PatchObj(path, map[string]any{"attributes": data}, false) +} + +func removeVirtualLink(vlinks []any, vlinkToRemove string) ([]any, error) { + for i, vlink := range vlinks { + if vlink == vlinkToRemove { + vlinks = append(vlinks[:i], vlinks[i+1:]...) + return vlinks, nil + } + } + return nil, fmt.Errorf("vlink to remove not found") +} + +// only to delete pillars, separators and breakers +func (controller Controller) DeleteInnerAttrObj(path, attribute, name string) (map[string]any, error) { + obj, err := controller.GetObject(path) + if err != nil { + return nil, err + } + attributes := obj["attributes"].(map[string]any) + attrMap, ok := attributes[attribute].(map[string]any) + if !ok || attrMap[name] == nil { + return nil, fmt.Errorf("%s %s does not exist", attribute, name) + } + delete(attrMap, name) + attributes[attribute] = attrMap + fmt.Println(attributes) + return controller.PatchObj(path, map[string]any{"attributes": attributes}, false) +} + +// only to add pillars, separators and breakers +func (controller Controller) AddInnerAtrObj(attrName, path string, values []any) (map[string]any, error) { + // get object + obj, err := controller.GetObject(path) + if err != nil { + return nil, err + } + attr := obj["attributes"].(map[string]any) + if (attrName == PillarAttr || attrName == SeparatorAttr) && obj["category"] != models.EntityToString(models.ROOM) { + return nil, fmt.Errorf("this attribute can only be added to rooms") + } else if attrName == BreakerAttr && obj["category"] != models.EntityToString(models.RACK) { + return nil, fmt.Errorf("this attribute can only be added to racks") + } + + // check and create attr + var name string + var newAttrObject any + if attrName == PillarAttr { + name, newAttrObject, err = models.ValuesToPillar(values) + } else if attrName == SeparatorAttr { + name, newAttrObject, err = models.ValuesToSeparator(values) + } else if attrName == BreakerAttr { + name, newAttrObject, err = models.ValuesToBreaker(values) + } + if err != nil { + return nil, err + } + + // add attr to object + var keyExist bool + attr[attrName+"s"], keyExist = AddToMap(attr[attrName+"s"], name, newAttrObject) + obj, err = controller.PatchObj(path, map[string]any{"attributes": attr}, false) + if err != nil { + return nil, err + } + if keyExist { + fmt.Printf(attrName+" %s replaced\n", name) + } + return obj, nil +} + +// [device]:virtual_config=type@clusterId@role +func (controller Controller) AddVirtualConfig(path string, values []any) error { + if len(values) < 1 { + return fmt.Errorf("invalid virtual_cofig values") + } + vconfig := map[string]any{"type": values[0]} + if len(values) > 1 { + vconfig["clusterId"] = values[1] + } + if len(values) > 2 { + vconfig["role"] = values[2] + } + + attributes := map[string]any{VirtualConfigAttr: vconfig} + _, err := controller.PatchObj(path, map[string]any{"attributes": attributes}, false) + return err +} + +// [room]:areas=[r1,r2,r3,r4]@[t1,t2,t3,t4] +func (controller Controller) UpdateRoomAreas(path string, values []any) (map[string]any, error) { + if attributes, e := models.SetRoomAreas(values); e != nil { + return nil, e + } else { + return controller.PatchObj(path, map[string]any{"attributes": attributes}, false) + } +} + +func (controller Controller) UpdateInnerAtrObj(path, attr string, values []any) error { + if regexp.MustCompile(`^breakers.([\w-]+).([\w-]+)$`).MatchString(attr) { + return controller.UpdateRackBreakerData(path, attr, values) + } else if regexp.MustCompile(`^virtual_config.([\w-]+)$`).MatchString(attr) { + return controller.UpdateVirtualConfig(path, attr, values) + } else { + return fmt.Errorf(invalidAttrNameMsg) + } +} + +// [rack]:breakers.breakerName.attribute=value +func (controller Controller) UpdateRackBreakerData(path, attr string, values []any) error { + // format attribute + attrs := strings.Split(attr, ".") // breakers.name.attribute + if len(attrs) != 3 { + return fmt.Errorf(invalidAttrNameMsg) + } + // get rack and modify breakers + obj, err := controller.GetObject(path) + if err != nil { + return err + } + attributes := obj["attributes"].(map[string]any) + breakers, hasBreakers := attributes["breakers"].(map[string]any) + notFoundErr := fmt.Errorf("rack does not have specified breaker") + if !hasBreakers { + return notFoundErr + } + breaker, hasBreaker := breakers[attrs[1]].(map[string]any) + if !hasBreaker { + return notFoundErr + } + breaker[attrs[2]] = values[0] + _, err = controller.PatchObj(path, map[string]any{"attributes": attributes}, false) + return err +} + +// [obj]:virtual_config.attr=value +func (controller Controller) UpdateVirtualConfig(path, attr string, values []any) error { + vconfigAttr, _ := strings.CutPrefix(attr, VirtualConfigAttr+".") + if len(vconfigAttr) < 1 { + return fmt.Errorf(invalidAttrNameMsg) + } + + // get object and modify virtual config + obj, err := controller.GetObject(path) + if err != nil { + return err + } + attributes := obj["attributes"].(map[string]any) + vconfig, hasVconfig := attributes[VirtualConfigAttr].(map[string]any) + if !hasVconfig { + return fmt.Errorf("object does not have virtual config") + } + vconfig[vconfigAttr] = values[0] + _, err = controller.PatchObj(path, map[string]any{"attributes": attributes}, false) + return err +} + +// Helpers +func AddToMap[T any](mapToAdd any, key string, val T) (map[string]any, bool) { + attrMap, ok := mapToAdd.(map[string]any) + if !ok { + attrMap = map[string]any{} + } + _, keyExist := attrMap[key] + attrMap[key] = val + return attrMap, keyExist +} diff --git a/CLI/controllers/update_test.go b/CLI/controllers/update_test.go index 27a7fa068..dec440257 100644 --- a/CLI/controllers/update_test.go +++ b/CLI/controllers/update_test.go @@ -1,8 +1,11 @@ package controllers_test import ( + "cli/controllers" "cli/models" test_utils "cli/test" + "maps" + "strings" "testing" "github.com/stretchr/testify/assert" @@ -32,7 +35,7 @@ func TestUpdateTagColor(t *testing.T) { } test_utils.MockUpdateObject(mockAPI, dataUpdate, dataUpdated) - _, err := controller.UpdateObj(path, dataUpdate, false) + _, err := controller.PatchObj(path, dataUpdate, false) assert.Nil(t, err) } @@ -60,12 +63,11 @@ func TestUpdateTagSlug(t *testing.T) { } test_utils.MockUpdateObject(mockAPI, dataUpdate, dataUpdated) - _, err := controller.UpdateObj(path, dataUpdate, false) + _, err := controller.PatchObj(path, dataUpdate, false) assert.Nil(t, err) } //endregion tags - // region device's sizeU // Test an update of a device's sizeU with heightUnit == mm @@ -92,7 +94,7 @@ func TestUpdateDeviceSizeUmm(t *testing.T) { device["attributes"].(map[string]any)["height"] = 44.45 test_utils.MockUpdateObject(mockAPI, mockDataUpdate, device) - _, err := controller.UpdateObj(path, dataUpdate, false) + _, err := controller.PatchObj(path, dataUpdate, false) assert.Nil(t, err) } @@ -122,12 +124,11 @@ func TestUpdateDeviceSizeUcm(t *testing.T) { device["attributes"].(map[string]any)["height"] = 4.445 test_utils.MockUpdateObject(mockAPI, mockDataUpdate, device) - _, err := controller.UpdateObj(path, dataUpdate, false) + _, err := controller.PatchObj(path, dataUpdate, false) assert.Nil(t, err) } // endregion sizeU - // region device's height // Test an update of a device's height with heightUnit == mm @@ -154,7 +155,7 @@ func TestUpdateDeviceheightmm(t *testing.T) { device["attributes"].(map[string]any)["height"] = 44.45 test_utils.MockUpdateObject(mockAPI, mockDataUpdate, device) - _, err := controller.UpdateObj(path, dataUpdate, false) + _, err := controller.PatchObj(path, dataUpdate, false) assert.Nil(t, err) } @@ -183,8 +184,380 @@ func TestUpdateDeviceheightcm(t *testing.T) { device["attributes"].(map[string]any)["height"] = 4.445 test_utils.MockUpdateObject(mockAPI, mockDataUpdate, device) - _, err := controller.UpdateObj(path, dataUpdate, false) + _, err := controller.PatchObj(path, dataUpdate, false) + assert.Nil(t, err) +} + +// endregion +// region update attribute + +func TestUpdateDeviceDescription(t *testing.T) { + controller, mockAPI, _, _ := test_utils.NewControllerWithMocks(t) + device := test_utils.CopyMap(chassis) + device["description"] = "my old description" + updatedDevice := test_utils.CopyMap(device) + updatedDevice["description"] = "my new description" + dataUpdate := map[string]any{"description": "my new description"} + + path := "/Physical/" + strings.Replace(device["id"].(string), ".", "/", -1) + + test_utils.MockGetObject(mockAPI, device) + test_utils.MockUpdateObject(mockAPI, dataUpdate, updatedDevice) + + result, err := controller.UpdateDescription(path, "description", []any{updatedDevice["description"]}) + assert.Nil(t, err) + assert.Equal(t, result["data"].(map[string]any)["description"], updatedDevice["description"]) +} + +func TestUpdateDeviceAttribute(t *testing.T) { + controller, mockAPI, _, _ := test_utils.NewControllerWithMocks(t) + device := test_utils.CopyMap(chassis) + updatedDevice := test_utils.CopyMap(device) + updatedDevice["attributes"].(map[string]any)["slot"] = []any{"slot1"} + dataUpdate := map[string]any{"attributes": map[string]any{"slot": []string{"slot1"}}} + + path := "/Physical/" + strings.Replace(device["id"].(string), ".", "/", -1) + + test_utils.MockGetObject(mockAPI, device) + test_utils.MockUpdateObject(mockAPI, dataUpdate, updatedDevice) + + result, err := controller.UpdateAttributes(path, "slot", updatedDevice["attributes"].(map[string]any)["slot"].([]any)) + assert.Nil(t, err) + assert.Equal(t, result["data"].(map[string]any)["attributes"], updatedDevice["attributes"]) +} + +func TestUpdateGroupContent(t *testing.T) { + controller, mockAPI, _, _ := test_utils.NewControllerWithMocks(t) + group := test_utils.CopyMap(rackGroup) + group["attributes"] = map[string]any{ + "content": "A,B", + } + newValue := "A,B,C" + updatedGroup := test_utils.CopyMap(group) + updatedGroup["attributes"].(map[string]any)["content"] = newValue + dataUpdate := updatedGroup["attributes"].(map[string]any) + + path := "/Physical/" + strings.Replace(group["id"].(string), ".", "/", -1) + + test_utils.MockGetObject(mockAPI, group) + test_utils.MockUpdateObject(mockAPI, dataUpdate, updatedGroup) + + controllers.State.ObjsForUnity = controllers.SetObjsForUnity([]string{"all"}) + + result, err := controller.PatchObj(path, dataUpdate, false) + assert.Nil(t, err) + assert.Equal(t, result["data"].(map[string]any)["attributes"].(map[string]any)["content"].(string), newValue) +} + +// endregion +// region update virtual + +func TestAddVirtualConfig(t *testing.T) { + controller, mockAPI, _, _ := test_utils.NewControllerWithMocks(t) + device := test_utils.CopyMap(chassis) + updatedDevice := test_utils.CopyMap(device) + vconfig := map[string]any{"type": "node", "clusterId": "mycluster", "role": "proxmox"} + updatedDevice["attributes"].(map[string]any)[controllers.VirtualConfigAttr] = vconfig + dataUpdate := map[string]any{"attributes": map[string]any{controllers.VirtualConfigAttr: vconfig}} + + path := "/Physical/" + strings.Replace(device["id"].(string), ".", "/", -1) + + test_utils.MockGetObject(mockAPI, device) + test_utils.MockUpdateObject(mockAPI, dataUpdate, updatedDevice) + + err := controller.UpdateObject(path, controllers.VirtualConfigAttr, []any{"node", "mycluster", "proxmox"}) + assert.Nil(t, err) +} + +func TestUpdateVirtualConfigData(t *testing.T) { + controller, mockAPI, _, _ := test_utils.NewControllerWithMocks(t) + // original device + device := test_utils.CopyMap(chassis) + vconfig := map[string]any{"type": "node", "clusterId": "mycluster", "role": "proxmox"} + device["attributes"].(map[string]any)[controllers.VirtualConfigAttr] = test_utils.CopyMap(vconfig) + // updated device + updatedDevice := test_utils.CopyMap(device) + vconfig["type"] = "host" + updatedDevice["attributes"].(map[string]any)[controllers.VirtualConfigAttr] = vconfig + // update data + dataUpdate := map[string]any{"attributes": updatedDevice["attributes"]} + + path := "/Physical/" + strings.Replace(device["id"].(string), ".", "/", -1) + + test_utils.MockGetObject(mockAPI, device) + test_utils.MockGetObject(mockAPI, device) + test_utils.MockUpdateObject(mockAPI, dataUpdate, updatedDevice) + + err := controller.UpdateObject(path, controllers.VirtualConfigAttr+".type", []any{"host"}) + assert.Nil(t, err) +} + +func TestUpdateVirtualLink(t *testing.T) { + controller, mockAPI, _, _ := test_utils.NewControllerWithMocks(t) + vobj := test_utils.CopyMap(vobjCluster) + updatedDevice := test_utils.CopyMap(vobj) + updatedDevice["attributes"].(map[string]any)["vlinks"] = []any{"device"} + dataUpdate := map[string]any{"attributes": map[string]any{"vlinks": []any{"device"}}} + + path := "/Logical/VirtualObjects/" + strings.Replace(vobj["id"].(string), ".", "/", -1) + + // Add vlink + test_utils.MockGetVirtualObject(mockAPI, vobj) + test_utils.MockGetVirtualObject(mockAPI, vobj) + test_utils.MockUpdateObject(mockAPI, dataUpdate, updatedDevice) + + result, err := controller.UpdateVirtualLink(path, "vlinks+", "device") + assert.Nil(t, err) + assert.Equal(t, result["data"].(map[string]any)["attributes"], updatedDevice["attributes"]) + + // Remove vlink + test_utils.MockGetVirtualObject(mockAPI, updatedDevice) + test_utils.MockGetVirtualObject(mockAPI, updatedDevice) + dataUpdate = map[string]any{"attributes": map[string]any{"vlinks": []any{}}} + test_utils.MockUpdateObject(mockAPI, dataUpdate, vobj) + + result, err = controller.UpdateVirtualLink(path, "vlinks-", "device") assert.Nil(t, err) + assert.Equal(t, result["data"].(map[string]any)["attributes"], vobj["attributes"]) } // endregion +// region update inner attr object + +func TestUpdateRackBreakerData(t *testing.T) { + controller, mockAPI, _, _ := test_utils.NewControllerWithMocks(t) + // original device + rack := test_utils.GetEntity("rack", "rack", "site.building.room", "domain") + path := "/Physical/site/building/room/rack" + breakers := map[string]any{"break1": map[string]any{"powerpanel": "panel1"}} + rack["attributes"].(map[string]any)[controllers.BreakerAttr+"s"] = test_utils.CopyMap(breakers) + // updated device + updatedRack := test_utils.CopyMap(rack) + breakers["break1"].(map[string]any)["powerpanel"] = "panel2" + updatedRack["attributes"].(map[string]any)[controllers.BreakerAttr+"s"] = breakers + // update data + dataUpdate := map[string]any{"attributes": updatedRack["attributes"]} + + test_utils.MockGetObject(mockAPI, rack) + test_utils.MockGetObject(mockAPI, rack) + test_utils.MockUpdateObject(mockAPI, dataUpdate, updatedRack) + + err := controller.UpdateObject(path, controllers.BreakerAttr+"s.break1.powerpanel", []any{"panel2"}) + assert.Nil(t, err) +} + +func TestAddInnerAtrObjWorks(t *testing.T) { + controller, mockAPI, _, _ := test_utils.NewControllerWithMocks(t) + tests := []struct { + name string + addFunction func(string, string, []any) error + attr string + values []any + newAttributes map[string]any + }{ + {"AddRoomSeparator", controller.UpdateObject, controllers.SeparatorAttr, []any{"mySeparator", []float64{1., 2.}, []float64{1., 2.}, "wireframe"}, map[string]interface{}{"separators": map[string]interface{}{"mySeparator": models.Separator{StartPos: []float64{1, 2}, EndPos: []float64{1, 2}, Type: "wireframe"}}}}, + {"AddRoomPillar", controller.UpdateObject, controllers.PillarAttr, []any{"myPillar", []float64{1., 2.}, []float64{1., 2.}, 2.5}, map[string]interface{}{"pillars": map[string]interface{}{"myPillar": models.Pillar{CenterXY: []float64{1, 2}, SizeXY: []float64{1, 2}, Rotation: 2.5}}}}, + {"AddRackBraker", controller.UpdateObject, controllers.BreakerAttr, []any{"myBreaker", "powerpanel"}, map[string]interface{}{"breakers": map[string]interface{}{"myBreaker": models.Breaker{Powerpanel: "powerpanel"}}}}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var targetObj map[string]any + var target string + if tt.attr == controllers.BreakerAttr { + targetObj = test_utils.GetEntity("rack", "rack", "site.building.room", "domain") + target = "/Physical/site/building/room/rack" + } else { + targetObj = test_utils.GetEntity("room", "room", "site.building", "domain") + target = "/Physical/site/building/room" + } + + test_utils.MockGetObject(mockAPI, targetObj) + test_utils.MockGetObject(mockAPI, targetObj) + + targetObj["attributes"] = tt.newAttributes + test_utils.MockUpdateObject(mockAPI, map[string]interface{}{"attributes": tt.newAttributes}, targetObj) + + err := tt.addFunction(target, tt.attr+"s+", tt.values) + assert.Nil(t, err) + }) + } +} + +func TestAddInnerAtrObjTargetError(t *testing.T) { + controller, mockAPI, _, _ := test_utils.NewControllerWithMocks(t) + tests := []struct { + name string + addFunction func(string, string, []any) (map[string]any, error) + attr string + values []any + errorMessage string + }{ + {"AddRoomSeparator", controller.AddInnerAtrObj, "separator", []any{"mySeparator"}, "this attribute can only be added to rooms"}, + {"AddRoomPillar", controller.AddInnerAtrObj, "pillar", []any{"myPillar"}, "this attribute can only be added to rooms"}, + {"AddRackBraker", controller.AddInnerAtrObj, "breaker", []any{"myBreaker"}, "this attribute can only be added to racks"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var targetObj map[string]any + var target string + if tt.attr != controllers.BreakerAttr { // inverted compared to the right one + targetObj = test_utils.GetEntity("rack", "rack", "site.building.room", "domain") + target = "/Physical/site/building/room/rack" + } else { + targetObj = test_utils.GetEntity("room", "room", "site.building", "domain") + target = "/Physical/site/building/room" + } + + test_utils.MockGetObject(mockAPI, targetObj) + obj, err := tt.addFunction(tt.attr, target, tt.values) + + assert.Nil(t, obj) + assert.NotNil(t, err) + assert.ErrorContains(t, err, tt.errorMessage) + }) + } +} + +func TestAddInnerAtrObjFormatError(t *testing.T) { + controller, mockAPI, _, _ := test_utils.NewControllerWithMocks(t) + tests := []struct { + name string + addFunction func(string, string, []any) (map[string]any, error) + attr string + values []any + errorMessage string + }{ + {"AddRoomSeparator", controller.AddInnerAtrObj, "separator", []any{"mySeparator"}, "4 values (name, startPos, endPos, type) expected to add a separator"}, + {"AddRoomPillar", controller.AddInnerAtrObj, "pillar", []any{"myPillar"}, "4 values (name, centerXY, sizeXY, rotation) expected to add a pillar"}, + {"AddRackBraker", controller.AddInnerAtrObj, "breaker", []any{"myBreaker"}, "at least 2 values (name and powerpanel) expected to add a breaker"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var targetObj map[string]any + var target string + if tt.attr == controllers.BreakerAttr { + targetObj = test_utils.GetEntity("rack", "rack", "site.building.room", "domain") + target = "/Physical/site/building/room/rack" + } else { + targetObj = test_utils.GetEntity("room", "room", "site.building", "domain") + target = "/Physical/site/building/room" + } + test_utils.MockGetObject(mockAPI, targetObj) + + obj, err := tt.addFunction(tt.attr, target, tt.values) + + assert.Nil(t, obj) + assert.NotNil(t, err) + assert.ErrorContains(t, err, tt.errorMessage) + }) + } +} + +func TestDeleteInnerAtrObjWithError(t *testing.T) { + tests := []struct { + name string + attributeName string + separatorName string + errorMessage string + }{ + {"InvalidArgument", "other", "separator", "others separator does not exist"}, + {"SeparatorDoesNotExist", "separator", "mySeparator", "separators mySeparator does not exist"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + controller, mockAPI, _, _ := test_utils.NewControllerWithMocks(t) + + room := test_utils.GetEntity("room", "room", "site.building", "domain") + test_utils.MockGetObject(mockAPI, room) + obj, err := controller.DeleteInnerAttrObj("/Physical/site/building/room", tt.attributeName+"s", tt.separatorName) + + assert.Nil(t, obj) + assert.NotNil(t, err) + assert.ErrorContains(t, err, tt.errorMessage) + }) + } +} + +func TestDeleteInnerAtrObjWorks(t *testing.T) { + controller, mockAPI, _, _ := test_utils.NewControllerWithMocks(t) + tests := []struct { + name string + addFunction func(string, string, []any) (map[string]any, error) + attr string + // values []any + currentAttributes map[string]any + }{ + {"DeleteRoomSeparator", controller.AddInnerAtrObj, controllers.SeparatorAttr, map[string]interface{}{"separators": map[string]interface{}{"myseparator": models.Separator{StartPos: []float64{1, 2}, EndPos: []float64{1, 2}, Type: "wireframe"}}}}, + {"DeleteRoomPillar", controller.AddInnerAtrObj, controllers.PillarAttr, map[string]interface{}{"pillars": map[string]interface{}{"mypillar": models.Pillar{CenterXY: []float64{1, 2}, SizeXY: []float64{1, 2}, Rotation: 2.5}}}}, + {"DeleteRackBraker", controller.AddInnerAtrObj, controllers.BreakerAttr, map[string]interface{}{"breakers": map[string]interface{}{"mybreaker": models.Breaker{Powerpanel: "powerpanel"}}}}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var targetObj map[string]any + var target string + if tt.attr == controllers.BreakerAttr { + targetObj = test_utils.GetEntity("rack", "rack", "site.building.room", "domain") + target = "/Physical/site/building/room/rack" + } else { + targetObj = test_utils.GetEntity("room", "room", "site.building", "domain") + target = "/Physical/site/building/room" + } + + maps.Copy(targetObj["attributes"].(map[string]any), tt.currentAttributes) + // room["attributes"].(map[string]any)["pillars"] = map[string]interface{}{"myPillar": models.Pillar{CenterXY: []float64{1, 2}, SizeXY: []float64{1, 2}, Rotation: 2.5}} + + updatedTarget := maps.Clone(targetObj) + updatedTarget["attributes"] = map[string]any{tt.attr + "s": map[string]interface{}{}} + + test_utils.MockGetObject(mockAPI, targetObj) + test_utils.MockGetObject(mockAPI, targetObj) + test_utils.MockUpdateObject(mockAPI, map[string]interface{}{"attributes": map[string]interface{}{tt.attr + "s": map[string]interface{}{}}}, updatedTarget) + err := controller.UpdateObject(target, tt.attr+"s-", []any{"my" + tt.attr}) + + assert.Nil(t, err) + }) + } +} + +// endregion +// region room areas + +func TestSetRoomAreas(t *testing.T) { + controller, mockAPI, _, _ := test_utils.NewControllerWithMocks(t) + + room := test_utils.GetEntity("room", "room", "site.building", "domain") + + roomResponse := test_utils.GetEntity("room", "room", "site.building", "domain") + test_utils.MockGetObject(mockAPI, room) + + roomResponse["attributes"] = map[string]any{ + "reserved": []float64{1, 2, 3, 4}, + "technical": []float64{1, 2, 3, 4}, + } + test_utils.MockUpdateObject(mockAPI, map[string]interface{}{"attributes": map[string]interface{}{"reserved": []float64{1, 2, 3, 4}, "technical": []float64{1, 2, 3, 4}}}, roomResponse) + + reservedArea := []float64{1, 2, 3, 4} + technicalArea := []float64{1, 2, 3, 4} + value, err := controller.UpdateRoomAreas("/Physical/site/building/room", []any{reservedArea, technicalArea}) + + assert.Nil(t, err) + assert.NotNil(t, value) +} + +// endregion + +func TestAddToMap(t *testing.T) { + newMap, replaced := controllers.AddToMap[int](map[string]any{"a": 3}, "b", 10) + + assert.Equal(t, map[string]any{"a": 3, "b": 10}, newMap) + assert.False(t, replaced) + + newMap, replaced = controllers.AddToMap[int](newMap, "b", 15) + assert.Equal(t, map[string]any{"a": 3, "b": 15}, newMap) + assert.True(t, replaced) +} diff --git a/CLI/controllers/utils.go b/CLI/controllers/utils.go index 5d4881914..dd7d137db 100644 --- a/CLI/controllers/utils.go +++ b/CLI/controllers/utils.go @@ -16,8 +16,6 @@ const ( DEBUG ) -const VIRTUALCONFIG = "virtual_config" - // displays contents of maps func Disp(x map[string]interface{}) { diff --git a/CLI/models/attributes.go b/CLI/models/attributes.go index 17cfef80d..fd0dc2a78 100644 --- a/CLI/models/attributes.go +++ b/CLI/models/attributes.go @@ -194,6 +194,52 @@ func expandStrToVector(slot string) ([]string, error) { } } +// Validate for cmd [room]:areas=[r1,r2,r3,r4]@[t1,t2,t3,t4] +func SetRoomAreas(values []any) (map[string]any, error) { + if len(values) != 2 { + return nil, fmt.Errorf("2 values (reserved, technical) expected to set room areas") + } + areas := map[string]any{"reserved": values[0], "technical": values[1]} + reserved, hasReserved := areas["reserved"].([]float64) + if !hasReserved { + return nil, ErrorResponder("reserved", "4", false) + } + tech, hasTechnical := areas["technical"].([]float64) + if !hasTechnical { + return nil, ErrorResponder("technical", "4", false) + } + + if len(reserved) == 4 && len(tech) == 4 { + return areas, nil + } else { + if len(reserved) != 4 && len(tech) == 4 { + return nil, ErrorResponder("reserved", "4", false) + } else if len(tech) != 4 && len(reserved) == 4 { + return nil, ErrorResponder("technical", "4", false) + } else { //Both invalid + return nil, ErrorResponder("reserved and technical", "4", true) + } + } +} + +// errResponder helper func for specialUpdateNode +// used for separator, pillar err msgs and validateRoomAreas() +func ErrorResponder(attr, numElts string, multi bool) error { + var errorMsg string + if multi { + errorMsg = "Invalid " + attr + " attributes provided." + + " They must be arrays/lists/vectors with " + numElts + " elements." + } else { + errorMsg = "Invalid " + attr + " attribute provided." + + " It must be an array/list/vector with " + numElts + " elements." + } + + segment := " Please refer to the wiki or manual reference" + + " for more details on how to create objects " + + "using this syntax" + + return fmt.Errorf(errorMsg + segment) +} func MapStringAny(value any) (map[string]any, error) { m, ok := value.(map[string]any) if ok { diff --git a/CLI/models/attributes_rack.go b/CLI/models/attributes_rack.go new file mode 100644 index 000000000..e71c641ae --- /dev/null +++ b/CLI/models/attributes_rack.go @@ -0,0 +1,66 @@ +package models + +import ( + "cli/utils" + "fmt" + "strconv" +) + +// Rack inner attributes +type Breaker struct { + Powerpanel string `json:"powerpanel"` + Type string `json:"type,omitempty"` + Circuit string `json:"circuit,omitempty"` + Intensity float64 `json:"intensity,omitempty"` + Tag string `json:"tag,omitempty"` +} + +func ValuesToBreaker(values []any) (string, Breaker, error) { + nMandatory := 2 + // mandatory name + mandatoryErr := fmt.Errorf("at least %d values (name and powerpanel) expected to add a breaker", nMandatory) + if len(values) < nMandatory { + return "", Breaker{}, mandatoryErr + } + name, err := utils.ValToString(values[0], "name") + if err != nil { + return name, Breaker{}, err + } + powerpanel, err := utils.ValToString(values[1], "powerpanel") + if err != nil { + return name, Breaker{}, err + } + if len(name) <= 0 || len(powerpanel) <= 0 { + return name, Breaker{}, mandatoryErr + } + var breakerType string + var circuit string + var intensityStr string + var tag string + for index, receiver := range []*string{&breakerType, &circuit, &intensityStr, &tag} { + err = setOptionalParam(index+nMandatory, values, receiver) + if err != nil { + return name, Breaker{}, err + } + } + var intensity float64 + if intensity, err = strconv.ParseFloat(intensityStr, + 64); intensityStr != "" && (err != nil || intensity <= 0) { + return name, Breaker{}, fmt.Errorf("invalid value for intensity, it should be a positive number") + } + + return name, Breaker{Powerpanel: powerpanel, Type: breakerType, + Circuit: circuit, Intensity: intensity, Tag: tag}, nil +} + +// Helpers +func setOptionalParam(index int, values []any, receiver *string) error { + if len(values) > index { + value, err := utils.ValToString(values[index], fmt.Sprintf("optional %d", index)) + if err != nil { + return err + } + *receiver = value + } + return nil +} diff --git a/CLI/models/attributes_room.go b/CLI/models/attributes_room.go new file mode 100644 index 000000000..1939d63ed --- /dev/null +++ b/CLI/models/attributes_room.go @@ -0,0 +1,65 @@ +package models + +import ( + "cli/utils" + "fmt" +) + +// Room inner attributes +type Pillar struct { + CenterXY []float64 `json:"centerXY"` + SizeXY []float64 `json:"sizeXY"` + Rotation float64 `json:"rotation"` +} + +func ValuesToPillar(values []any) (string, Pillar, error) { + if len(values) != 4 { + return "", Pillar{}, fmt.Errorf("4 values (name, centerXY, sizeXY, rotation) expected to add a pillar") + } + name, err := utils.ValToString(values[0], "name") + if err != nil { + return name, Pillar{}, err + } + centerXY, err := utils.ValToVec(values[1], 2, "centerXY") + if err != nil { + return name, Pillar{}, err + } + sizeXY, err := utils.ValToVec(values[2], 2, "sizeXY") + if err != nil { + return name, Pillar{}, err + } + rotation, err := utils.ValToFloat(values[3], "rotation") + if err != nil { + return name, Pillar{}, err + } + return name, Pillar{centerXY, sizeXY, rotation}, nil +} + +type Separator struct { + StartPos []float64 `json:"startPosXYm"` + EndPos []float64 `json:"endPosXYm"` + Type string `json:"type"` +} + +func ValuesToSeparator(values []any) (string, Separator, error) { + if len(values) != 4 { + return "", Separator{}, fmt.Errorf("4 values (name, startPos, endPos, type) expected to add a separator") + } + name, err := utils.ValToString(values[0], "name") + if err != nil { + return name, Separator{}, err + } + startPos, err := utils.ValToVec(values[1], 2, "startPos") + if err != nil { + return name, Separator{}, err + } + endPos, err := utils.ValToVec(values[2], 2, "endPos") + if err != nil { + return name, Separator{}, err + } + sepType, err := utils.ValToString(values[3], "separator type") + if err != nil { + return name, Separator{}, err + } + return name, Separator{startPos, endPos, sepType}, nil +} diff --git a/CLI/models/attributes_test.go b/CLI/models/attributes_test.go index 51cc21254..efd862f55 100644 --- a/CLI/models/attributes_test.go +++ b/CLI/models/attributes_test.go @@ -43,3 +43,11 @@ func TestExpandSlotVector(t *testing.T) { assert.NotNil(t, slots) assert.EqualValues(t, []string{"slot1", "slot3"}, slots) } + +func TestErrorResponder(t *testing.T) { + err := models.ErrorResponder("reserved", "4", false) + assert.ErrorContains(t, err, "Invalid reserved attribute provided. It must be an array/list/vector with 4 elements. Please refer to the wiki or manual reference for more details on how to create objects using this syntax") + + err = models.ErrorResponder("reserved", "4", true) + assert.ErrorContains(t, err, "Invalid reserved attributes provided. They must be arrays/lists/vectors with 4 elements. Please refer to the wiki or manual reference for more details on how to create objects using this syntax") +} diff --git a/CLI/models/entity.go b/CLI/models/entity.go index de092c950..cf15191da 100644 --- a/CLI/models/entity.go +++ b/CLI/models/entity.go @@ -4,6 +4,7 @@ import ( l "cli/logger" "fmt" pathutil "path" + "time" ) const ( @@ -28,6 +29,18 @@ const ( VIRTUALOBJ ) +type Entity struct { + Category string `json:"category"` + Description string `json:"description"` + Domain string `json:"domain"` + CreatedDate *time.Time `json:"createdDate,omitempty"` + LastUpdated *time.Time `json:"lastUpdated,omitempty"` + Name string `json:"name"` + Id string `json:"id,omitempty"` + ParentId string `json:"parentId,omitempty"` + Attributes EntityAttributes `json:"attributes"` +} + func EntityToString(entity int) string { switch entity { case DOMAIN: diff --git a/CLI/parser/ast.go b/CLI/parser/ast.go index d42b170b9..b1944262e 100644 --- a/CLI/parser/ast.go +++ b/CLI/parser/ast.go @@ -6,7 +6,6 @@ import ( "cli/models" "cli/utils" "cli/views" - "encoding/json" "errors" "fmt" "path/filepath" @@ -544,250 +543,6 @@ func (n *selectObjectNode) execute() (interface{}, error) { return nil, nil } -func setRoomAreas(path string, values []any) (map[string]any, error) { - if len(values) != 2 { - return nil, fmt.Errorf("2 values (reserved, technical) expected to set room areas") - } - attributes := map[string]any{"reserved": values[0], "technical": values[1]} - if e := validateAreas(attributes); e != nil { - return nil, e - } - return cmd.C.UpdateObj(path, map[string]any{"attributes": attributes}, false) -} - -func setLabel(path string, values []any, hasSharpe bool) (map[string]any, error) { - if len(values) != 1 { - return nil, fmt.Errorf("only 1 value expected") - } - value, err := utils.ValToString(values[0], "value") - if err != nil { - return nil, err - } - return nil, cmd.C.InteractObject(path, "label", value, hasSharpe) -} - -func setLabelFont(path string, values []any) (map[string]any, error) { - msg := "The font can only be bold or italic" + - " or be in the form of color@[colorValue]." + - "\n\nFor more information please refer to: " + - "\nhttps://github.com/ditrit/OGrEE-3D/wiki/CLI-langage#interact-with-objects" - - switch len(values) { - case 1: - if values[0] != "bold" && values[0] != "italic" { - return nil, fmt.Errorf(msg) - } - return nil, cmd.C.InteractObject(path, "labelFont", values[0], false) - case 2: - if values[0] != "color" { - return nil, fmt.Errorf(msg) - } - c, ok := utils.ValToColor(values[1]) - if !ok { - return nil, fmt.Errorf("please provide a valid 6 length hex value for the color") - } - return nil, cmd.C.InteractObject(path, "labelFont", "color@"+c, false) - default: - return nil, fmt.Errorf(msg) - } -} - -func setLabelBackground(path string, values []any) (map[string]any, error) { - if len(values) != 1 { - return nil, fmt.Errorf("only 1 value expected") - } - c, ok := utils.ValToColor(values[0]) - if !ok { - return nil, fmt.Errorf("please provide a valid 6 length hex value for the color") - } - return nil, cmd.C.InteractObject(path, "labelBackground", c, false) -} - -func addToMap[T any](mapToAdd any, key string, val T) (map[string]any, bool) { - attrMap, ok := mapToAdd.(map[string]any) - if !ok { - attrMap = map[string]any{} - } - _, keyExist := attrMap[key] - attrMap[key] = val - return attrMap, keyExist -} - -func removeFromStringMap[T any](stringMap string, key string) (string, bool) { - m := map[string]T{} - if stringMap != "" { - json.Unmarshal([]byte(stringMap), &m) - } - _, ok := m[key] - if !ok { - return stringMap, false - } - delete(m, key) - mBytes, _ := json.Marshal(m) - return string(mBytes), true -} - -type Separator struct { - StartPos []float64 `json:"startPosXYm"` - EndPos []float64 `json:"endPosXYm"` - Type string `json:"type"` -} - -func addRoomSeparator(path string, values []any) (map[string]any, error) { - if len(values) != 4 { - return nil, fmt.Errorf("4 values (name, startPos, endPos, type) expected to add a separator") - } - name, err := utils.ValToString(values[0], "name") - if err != nil { - return nil, err - } - startPos, err := utils.ValToVec(values[1], 2, "startPos") - if err != nil { - return nil, err - } - endPos, err := utils.ValToVec(values[2], 2, "endPos") - if err != nil { - return nil, err - } - sepType, err := utils.ValToString(values[3], "separator type") - if err != nil { - return nil, err - } - obj, err := cmd.C.GetObject(path) - if err != nil { - return nil, err - } - attr := obj["attributes"].(map[string]any) - newSeparator := Separator{startPos, endPos, sepType} - var keyExist bool - attr["separators"], keyExist = addToMap[Separator](attr["separators"], name, newSeparator) - obj, err = cmd.C.UpdateObj(path, map[string]any{"attributes": attr}, false) - if err != nil { - return nil, err - } - if keyExist { - fmt.Printf("Separator %s replaced\n", name) - } - return obj, nil -} - -type Pillar struct { - CenterXY []float64 `json:"centerXY"` - SizeXY []float64 `json:"sizeXY"` - Rotation float64 `json:"rotation"` -} - -func addRoomPillar(path string, values []any) (map[string]any, error) { - if len(values) != 4 { - return nil, fmt.Errorf("4 values (name, centerXY, sizeXY, rotation) expected to add a pillar") - } - name, err := utils.ValToString(values[0], "name") - if err != nil { - return nil, err - } - centerXY, err := utils.ValToVec(values[1], 2, "centerXY") - if err != nil { - return nil, err - } - sizeXY, err := utils.ValToVec(values[2], 2, "sizeXY") - if err != nil { - return nil, err - } - rotation, err := utils.ValToFloat(values[3], "rotation") - if err != nil { - return nil, err - } - obj, err := cmd.C.GetObject(path) - if err != nil { - return nil, err - } - attr := obj["attributes"].(map[string]any) - newPillar := Pillar{centerXY, sizeXY, rotation} - var keyExist bool - attr["pillars"], keyExist = addToMap[Pillar](attr["pillars"], name, newPillar) - obj, err = cmd.C.UpdateObj(path, map[string]any{"attributes": attr}, false) - if err != nil { - return nil, err - } - if keyExist { - fmt.Printf("Pillar %s replaced\n", name) - } - return obj, nil -} - -// attribute must be "separator" or "pillar" -func deleteRoomPillarOrSeparator(path, attribute, name string) (map[string]any, error) { - obj, err := cmd.C.GetObject(path) - if err != nil { - return nil, err - } - attributes := obj["attributes"].(map[string]any) - attrMap, ok := attributes[attribute+"s"].(map[string]any) - if !ok || attrMap[name] == nil { - return nil, fmt.Errorf("%s %s does not exist", attribute, name) - } - delete(attrMap, name) - attributes[attribute+"s"] = attrMap - return cmd.C.UpdateObj(path, map[string]any{"attributes": attributes}, false) -} - -func updateDescription(path string, attr string, values []any) (map[string]any, error) { - if len(values) != 1 { - return nil, fmt.Errorf("a single value is expected to update a description") - } - newDesc, err := utils.ValToString(values[0], "description") - if err != nil { - return nil, err - } - data := map[string]any{"description": newDesc} - return cmd.C.UpdateObj(path, data, false) -} - -func updateVirtualLink(path string, attr string, value string) (map[string]any, error) { - if len(value) == 0 { - return nil, fmt.Errorf("an empty string is not valid") - } - - obj, err := cmd.C.GetObject(path) - if err != nil { - return nil, err - } else if obj["category"] != models.EntityToString(models.VIRTUALOBJ) { - return nil, fmt.Errorf("only virtual objects can have vlinks") - } - - vlinks, hasVlinks := obj["attributes"].(map[string]any)["vlinks"].([]any) - if attr == "vlinks+" { - if !hasVlinks { - vlinks = []any{value} - } else { - vlinks = append(vlinks, value) - } - } else if attr == "vlinks-" { - if !hasVlinks { - return nil, fmt.Errorf("no vlinks defined for this object") - } - vlinks, err = removeVirtualLink(vlinks, value) - if err != nil { - return nil, err - } - } else { - return nil, fmt.Errorf("invalid vlink update command") - } - - data := map[string]any{"vlinks": vlinks} - return cmd.C.UpdateObj(path, map[string]any{"attributes": data}, false) -} - -func removeVirtualLink(vlinks []any, vlinkToRemove string) ([]any, error) { - for i, vlink := range vlinks { - if vlink == vlinkToRemove { - vlinks = append(vlinks[:i], vlinks[i+1:]...) - return vlinks, nil - } - } - return nil, fmt.Errorf("vlink to remove not found") -} - type updateObjNode struct { path node attr string @@ -825,51 +580,17 @@ func (n *updateObjNode) execute() (interface{}, error) { var err error if models.IsTag(path) { if n.attr == "slug" || n.attr == "color" || n.attr == "description" { - _, err = cmd.C.UpdateObj(path, map[string]any{n.attr: values[0]}, false) + _, err = cmd.C.PatchObj(path, map[string]any{n.attr: values[0]}, false) } } else if models.IsLayer(path) { err = cmd.C.UpdateLayer(path, n.attr, values[0]) } else { switch n.attr { - case "displayContent", "alpha", "tilesName", "tilesColor", "U", "slots", "localCS": - var boolVal bool - boolVal, err = utils.ValToBool(values[0], n.attr) - if err != nil { - return nil, err - } - err = cmd.C.InteractObject(path, n.attr, boolVal, n.hasSharpe) - case "areas": - _, err = setRoomAreas(path, values) - case "label": - _, err = setLabel(path, values, n.hasSharpe) - case "labelFont": - _, err = setLabelFont(path, values) - case "labelBackground": - _, err = setLabelBackground(path, values) - case "separators+": - _, err = addRoomSeparator(path, values) - case "pillars+": - _, err = addRoomPillar(path, values) - case "separators-": - _, err = deleteRoomPillarOrSeparator(path, "separator", values[0].(string)) - case "pillars-": - _, err = deleteRoomPillarOrSeparator(path, "pillar", values[0].(string)) - case "vlinks+", "vlinks-": - _, err = updateVirtualLink(path, n.attr, values[0].(string)) - case "domain", "tags+", "tags-": - isRecursive := len(values) > 1 && values[1] == "recursive" - _, err = cmd.C.UpdateObj(path, map[string]any{n.attr: values[0]}, isRecursive) - case "tags", "separators", "pillars", "vlinks": - err = fmt.Errorf( - "object's %[1]s can not be updated directly, please use %[1]s+= and %[1]s-=", - n.attr, - ) + case "displayContent", "alpha", "tilesName", "tilesColor", "U", + "slots", "localCS", "label", "labelFont", "labelBackground": + err = cmd.C.UpdateInteract(path, n.attr, values, n.hasSharpe) default: - if n.attr == "description" { - _, err = updateDescription(path, n.attr, values) - } else { - _, err = updateAttributes(path, n.attr, values) - } + err = cmd.C.UpdateObject(path, n.attr, values) } } @@ -880,36 +601,6 @@ func (n *updateObjNode) execute() (interface{}, error) { return nil, nil } -func updateAttributes(path, attributeName string, values []any) (map[string]any, error) { - var attributes map[string]any - if attributeName == "slot" || attributeName == "content" { - vecStr := []string{} - for _, value := range values { - vecStr = append(vecStr, value.(string)) - } - var err error - if vecStr, err = models.CheckExpandStrVector(vecStr); err != nil { - return nil, err - } - attributes = map[string]any{attributeName: vecStr} - } else { - if len(values) > 1 { - return nil, fmt.Errorf("attributes can only be assigned a single value") - } - if vconfigAttr, found := strings.CutPrefix(attributeName, cmd.VIRTUALCONFIG+"."); found { - if len(vconfigAttr) < 1 { - return nil, fmt.Errorf("invalid attribute name") - } - vAttr := map[string]any{vconfigAttr: values[0]} - attributes = map[string]any{cmd.VIRTUALCONFIG: vAttr} - } else { - attributes = map[string]any{attributeName: values[0]} - } - } - - return cmd.C.UpdateObj(path, map[string]any{"attributes": attributes}, false) -} - type treeNode struct { path node depth int @@ -1368,7 +1059,7 @@ func (n *createVirtualNode) execute() (interface{}, error) { if err != nil { return nil, err } - attributes := map[string]any{cmd.VIRTUALCONFIG: map[string]any{"type": vtype}} + attributes := map[string]any{cmd.VirtualConfigAttr: map[string]any{"type": vtype}} if n.vlinks != nil { vlinks := []string{} @@ -1388,7 +1079,7 @@ func (n *createVirtualNode) execute() (interface{}, error) { if err != nil { return nil, err } - attributes[cmd.VIRTUALCONFIG].(map[string]any)["role"] = role + attributes[cmd.VirtualConfigAttr].(map[string]any)["role"] = role } return nil, cmd.C.CreateObject(path, models.VIRTUALOBJ, @@ -1819,31 +1510,6 @@ func (a *assignNode) execute() (interface{}, error) { return nil, fmt.Errorf("Invalid type to assign variable %s", a.variable) } -// Validate format for cmd [room]:areas=[r1,r2,r3,r4]@[t1,t2,t3,t4] -func validateAreas(areas map[string]interface{}) error { - 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) - } - } - -} - type cpNode struct { source node dest node diff --git a/CLI/parser/ast_test.go b/CLI/parser/ast_test.go index 0864a03cd..7a106bb7d 100644 --- a/CLI/parser/ast_test.go +++ b/CLI/parser/ast_test.go @@ -10,7 +10,6 @@ import ( "time" "github.com/stretchr/testify/assert" - "golang.org/x/exp/maps" ) func TestValueNodeExecute(t *testing.T) { @@ -354,175 +353,6 @@ func TestSelectObjectNodeExecuteReset(t *testing.T) { assert.Len(t, controllers.State.ClipBoard, 0) } -func TestSetRoomAreas(t *testing.T) { - _, mockAPI, _, _ := test_utils.SetMainEnvironmentMock(t) - - room := test_utils.GetEntity("room", "room", "site.building", "domain") - - roomResponse := test_utils.GetEntity("room", "room", "site.building", "domain") - test_utils.MockGetObject(mockAPI, room) - - roomResponse["attributes"] = map[string]any{ - "reserved": []float64{1, 2, 3, 4}, - "technical": []float64{1, 2, 3, 4}, - } - test_utils.MockUpdateObject(mockAPI, map[string]interface{}{"attributes": map[string]interface{}{"reserved": []float64{1, 2, 3, 4}, "technical": []float64{1, 2, 3, 4}}}, roomResponse) - - reservedArea := []float64{1, 2, 3, 4} - technicalArea := []float64{1, 2, 3, 4} - value, err := setRoomAreas("/Physical/site/building/room", []any{reservedArea, technicalArea}) - - assert.Nil(t, err) - assert.NotNil(t, value) -} - -func TestSetLabel(t *testing.T) { - _, mockAPI, _, _ := test_utils.SetMainEnvironmentMock(t) - - room := test_utils.GetEntity("rack", "rack", "site.building.room", "domain") - test_utils.MockGetObject(mockAPI, room) - value, err := setLabel("/Physical/site/building/room/rack", []any{"myLabel"}, false) - - assert.Nil(t, err) - assert.Nil(t, value) -} - -func TestAddToMap(t *testing.T) { - newMap, replaced := addToMap[int](map[string]any{"a": 3}, "b", 10) - - assert.Equal(t, map[string]any{"a": 3, "b": 10}, newMap) - assert.False(t, replaced) - - newMap, replaced = addToMap[int](newMap, "b", 15) - assert.Equal(t, map[string]any{"a": 3, "b": 15}, newMap) - assert.True(t, replaced) -} - -func TestRemoveFromStringMap(t *testing.T) { - newMap, deleted := removeFromStringMap[int]("{\"a\":3,\"b\":10}", "b") - - assert.Equal(t, "{\"a\":3}", newMap) - assert.True(t, deleted) - - newMap, deleted = removeFromStringMap[int](newMap, "b") - assert.Equal(t, "{\"a\":3}", newMap) - assert.False(t, deleted) -} - -func TestAddRoomSeparatorOrPillarError(t *testing.T) { - tests := []struct { - name string - addFunction func(string, []any) (map[string]any, error) - values []any - errorMessage string - }{ - {"AddRoomSeparator", addRoomSeparator, []any{"mySeparator"}, "4 values (name, startPos, endPos, type) expected to add a separator"}, - {"AddRoomPillar", addRoomPillar, []any{"myPillar"}, "4 values (name, centerXY, sizeXY, rotation) expected to add a pillar"}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - obj, err := tt.addFunction("/Physical/site/building/room", tt.values) - - assert.Nil(t, obj) - assert.NotNil(t, err) - assert.ErrorContains(t, err, tt.errorMessage) - }) - } -} - -func TestAddRoomSeparatorOrPillarWorks(t *testing.T) { - tests := []struct { - name string - addFunction func(string, []any) (map[string]any, error) - values []any - newAttributes map[string]any - }{ - {"AddRoomSeparator", addRoomSeparator, []any{"mySeparator", []float64{1., 2.}, []float64{1., 2.}, "wireframe"}, map[string]interface{}{"separators": map[string]interface{}{"mySeparator": Separator{StartPos: []float64{1, 2}, EndPos: []float64{1, 2}, Type: "wireframe"}}}}, - {"AddRoomPillar", addRoomPillar, []any{"myPillar", []float64{1., 2.}, []float64{1., 2.}, 2.5}, map[string]interface{}{"pillars": map[string]interface{}{"myPillar": Pillar{CenterXY: []float64{1, 2}, SizeXY: []float64{1, 2}, Rotation: 2.5}}}}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - _, mockAPI, _, _ := test_utils.SetMainEnvironmentMock(t) - room := test_utils.GetEntity("room", "room", "site.building", "domain") - - test_utils.MockGetObject(mockAPI, room) - test_utils.MockGetObject(mockAPI, room) - - room["attributes"] = tt.newAttributes - test_utils.MockUpdateObject(mockAPI, map[string]interface{}{"attributes": tt.newAttributes}, room) - - obj, err := tt.addFunction("/Physical/site/building/room", tt.values) - assert.NotNil(t, obj) - assert.Nil(t, err) - }) - } -} - -func TestDeleteRoomPillarOrSeparatorWithError(t *testing.T) { - tests := []struct { - name string - attributeName string - separatorName string - errorMessage string - }{ - {"InvalidArgument", "other", "separator", "other separator does not exist"}, - {"SeparatorDoesNotExist", "separator", "mySeparator", "separator mySeparator does not exist"}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - _, mockAPI, _, _ := test_utils.SetMainEnvironmentMock(t) - - room := test_utils.GetEntity("room", "room", "site.building", "domain") - test_utils.MockGetObject(mockAPI, room) - obj, err := deleteRoomPillarOrSeparator("/Physical/site/building/room", tt.attributeName, tt.separatorName) - - assert.Nil(t, obj) - assert.NotNil(t, err) - assert.ErrorContains(t, err, tt.errorMessage) - }) - } -} - -func TestDeleteRoomPillarOrSeparatorSeparator(t *testing.T) { - _, mockAPI, _, _ := test_utils.SetMainEnvironmentMock(t) - - room := test_utils.GetEntity("room", "room", "site.building", "domain") - room["attributes"].(map[string]any)["separators"] = map[string]interface{}{"mySeparator": Separator{StartPos: []float64{1, 2}, EndPos: []float64{1, 2}, Type: "wireframe"}} - - updatedRoom := test_utils.GetEntity("room", "room", "site.building", "domain") - updatedRoom["attributes"] = map[string]any{"separators": map[string]interface{}{}} - - test_utils.MockGetObject(mockAPI, room) - test_utils.MockGetObject(mockAPI, room) - - test_utils.MockUpdateObject(mockAPI, map[string]interface{}{"attributes": map[string]interface{}{"separators": map[string]interface{}{}}}, updatedRoom) - obj, err := deleteRoomPillarOrSeparator("/Physical/site/building/room", "separator", "mySeparator") - - assert.Nil(t, err) - assert.NotNil(t, obj) -} - -func TestDeleteRoomPillarOrSeparatorPillar(t *testing.T) { - _, mockAPI, _, _ := test_utils.SetMainEnvironmentMock(t) - - room := test_utils.GetEntity("room", "room", "site.building", "domain") - room["attributes"].(map[string]any)["pillars"] = map[string]interface{}{"myPillar": Pillar{CenterXY: []float64{1, 2}, SizeXY: []float64{1, 2}, Rotation: 2.5}} - - updatedRoom := maps.Clone(room) - updatedRoom["attributes"] = map[string]any{"pillars": map[string]interface{}{}} - - test_utils.MockGetObject(mockAPI, room) - test_utils.MockGetObject(mockAPI, room) - test_utils.MockUpdateObject(mockAPI, map[string]interface{}{"attributes": map[string]interface{}{"pillars": map[string]interface{}{}}}, updatedRoom) - obj, err := deleteRoomPillarOrSeparator("/Physical/site/building/room", "pillar", "myPillar") - - assert.Nil(t, err) - assert.NotNil(t, obj) -} - func TestUpdateObjNodeExecuteUpdateDescription(t *testing.T) { _, mockAPI, _, _ := test_utils.SetMainEnvironmentMock(t) diff --git a/CLI/parser/astutil.go b/CLI/parser/astutil.go index f4c0b36e5..229881350 100644 --- a/CLI/parser/astutil.go +++ b/CLI/parser/astutil.go @@ -113,25 +113,6 @@ func evalNodeArr[elt comparable](arr *[]node, x []elt) ([]elt, error) { return x, nil } -// errResponder helper func for specialUpdateNode -// used for separator, pillar err msgs and validateAreas() -func errorResponder(attr, numElts string, multi bool) error { - var errorMsg string - if multi { - errorMsg = "Invalid " + attr + " attributes provided." + - " They must be arrays/lists/vectors with " + numElts + " elements." - } else { - errorMsg = "Invalid " + attr + " attribute provided." + - " It must be an array/list/vector with " + numElts + " elements." - } - - segment := " Please refer to the wiki or manual reference" + - " for more details on how to create objects " + - "using this syntax" - - return fmt.Errorf(errorMsg + segment) -} - func filtersToMapString(filters map[string]node) (map[string]string, error) { filtersString := map[string]string{} diff --git a/CLI/parser/astutil_test.go b/CLI/parser/astutil_test.go index e17745458..12c52fdb3 100644 --- a/CLI/parser/astutil_test.go +++ b/CLI/parser/astutil_test.go @@ -151,14 +151,6 @@ func TestEvalNodeArr(t *testing.T) { assert.ErrorContains(t, err, "Error unexpected element") } -func TestErrorResponder(t *testing.T) { - err := errorResponder("reserved", "4", false) - assert.ErrorContains(t, err, "Invalid reserved attribute provided. It must be an array/list/vector with 4 elements. Please refer to the wiki or manual reference for more details on how to create objects using this syntax") - - err = errorResponder("reserved", "4", true) - assert.ErrorContains(t, err, "Invalid reserved attributes provided. They must be arrays/lists/vectors with 4 elements. Please refer to the wiki or manual reference for more details on how to create objects using this syntax") -} - func TestFiltersToMapString(t *testing.T) { filters := map[string]node{ "tag": &valueNode{"my-tag"}, diff --git a/CLI/parser/parser.go b/CLI/parser/parser.go index 1492495c2..998c3732e 100644 --- a/CLI/parser/parser.go +++ b/CLI/parser/parser.go @@ -217,12 +217,13 @@ func (p *parser) parseSimpleWord(name string) string { } } -func (p *parser) parseComplexWord(name string) string { +func (p *parser) parseComplexWord(name string, extraAcceptedChar ...byte) string { + extraAcceptedChar = append([]byte{'-', '+', '_'}, extraAcceptedChar...) p.skipWhiteSpaces() defer un(trace(p, name)) for { c := p.next() - if isAlphaNumeric(c) || c == '-' || c == '+' || c == '_' { + if isAlphaNumeric(c) || pie.Contains(extraAcceptedChar, c) { continue } p.backward(1) @@ -867,7 +868,7 @@ func (p *parser) parseDelete() node { path := p.parsePath("") if p.parseExact(":") { attr := p.parseComplexWord("attribute") - if attr == c.VIRTUALCONFIG { + if attr == c.VirtualConfigAttr { p.expect(".") extraAttr := p.parseComplexWord("attribute") attr = attr + "." + extraAttr @@ -1324,12 +1325,7 @@ func (p *parser) parseUpdate() node { p.skipWhiteSpaces() p.expect(":") p.skipWhiteSpaces() - attr := p.parseComplexWord("attribute") - if attr == c.VIRTUALCONFIG { - p.expect(".") - extraAttr := p.parseComplexWord("attribute") - attr = attr + "." + extraAttr - } + attr := p.parseComplexWord("attribute", '.') p.skipWhiteSpaces() p.expect("=") p.skipWhiteSpaces() diff --git a/CLI/test/mocks.go b/CLI/test/mocks.go index 39253157c..800f2e8f4 100644 --- a/CLI/test/mocks.go +++ b/CLI/test/mocks.go @@ -52,6 +52,10 @@ func MockGetObjects(mockAPI *mocks.APIPort, queryParams string, result []any) { mockResponseWithParams(mockAPI, http.MethodGet, "/api/objects", queryParams, nil, http.StatusOK, RemoveChildrenFromList(result)) } +func MockGetVirtualObject(mockAPI *mocks.APIPort, object map[string]any) { + mockResponse(mockAPI, http.MethodGet, "/api/virtual_objs/"+object["id"].(string), nil, http.StatusOK, RemoveChildren(object)) +} + func MockGetVirtualObjects(mockAPI *mocks.APIPort, queryParams string, result []any) { mockResponseWithParams(mockAPI, http.MethodGet, "/api/virtual_objs", queryParams, nil, http.StatusOK, RemoveChildrenFromList(result)) } diff --git a/CLI/views/get.go b/CLI/views/get.go index 192e2c22d..7cfd19ed7 100644 --- a/CLI/views/get.go +++ b/CLI/views/get.go @@ -5,6 +5,8 @@ import ( "encoding/json" "fmt" "sort" + + "github.com/elliotchance/pie/v2" ) func Object(path string, obj map[string]any) { @@ -16,6 +18,7 @@ func Object(path string, obj map[string]any) { } func DisplayJson(indent string, jsonMap map[string]any) { + keysWithObjectsValue := []string{"attributes", "breakers", "pillars", "separators", "virtual_config"} defaultIndent := " " // sort keys in alphabetical order keys := make([]string, 0, len(jsonMap)) @@ -27,11 +30,12 @@ func DisplayJson(indent string, jsonMap map[string]any) { // print map println("{") for _, key := range keys { - if key == "attributes" { - print(defaultIndent + "\"" + key + "\": ") - DisplayJson(defaultIndent, jsonMap[key].(map[string]any)) + thisLevelIndent := indent + defaultIndent + if pie.Contains(keysWithObjectsValue, key) { + print(thisLevelIndent + "\"" + key + "\": ") + DisplayJson(thisLevelIndent, jsonMap[key].(map[string]any)) } else { - print(indent + defaultIndent + "\"" + key + "\": ") + print(thisLevelIndent + "\"" + key + "\": ") if value, err := json.Marshal(jsonMap[key]); err != nil { fmt.Println(err) } else {