Skip to content

Commit

Permalink
Add breakers to racks (#492)
Browse files Browse the repository at this point in the history
* 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
helderbetiol authored Jul 12, 2024
1 parent 7d102a1 commit 822627d
Showing 24 changed files with 1,067 additions and 590 deletions.
5 changes: 4 additions & 1 deletion API/models/model.go
Original file line number Diff line number Diff line change
@@ -14,13 +14,16 @@ import (
"strings"
"time"

"github.com/elliotchance/pie/v2"
"go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options"

"go.mongodb.org/mongo-driver/bson"
"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) {
34 changes: 32 additions & 2 deletions API/models/schemas/rack_schema.json
Original file line number Diff line number Diff line change
@@ -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",
21 changes: 21 additions & 0 deletions API/models/tag.go
Original file line number Diff line number Diff line change
@@ -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)
18 changes: 18 additions & 0 deletions API/models/tag_test.go
Original file line number Diff line number Diff line change
@@ -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,
3 changes: 2 additions & 1 deletion API/repository/base.go
Original file line number Diff line number Diff line change
@@ -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
}
6 changes: 3 additions & 3 deletions CLI/controllers/delete.go
Original file line number Diff line number Diff line change
@@ -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)
}
68 changes: 68 additions & 0 deletions CLI/controllers/interact.go
Original file line number Diff line number Diff line change
@@ -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)
}
46 changes: 28 additions & 18 deletions CLI/controllers/interact_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
6 changes: 3 additions & 3 deletions CLI/controllers/layer.go
Original file line number Diff line number Diff line change
@@ -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
}
286 changes: 285 additions & 1 deletion CLI/controllers/update.go
Original file line number Diff line number Diff line change
@@ -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
}
389 changes: 381 additions & 8 deletions CLI/controllers/update_test.go

Large diffs are not rendered by default.

2 changes: 0 additions & 2 deletions CLI/controllers/utils.go
Original file line number Diff line number Diff line change
@@ -16,8 +16,6 @@ const (
DEBUG
)

const VIRTUALCONFIG = "virtual_config"

// displays contents of maps
func Disp(x map[string]interface{}) {

46 changes: 46 additions & 0 deletions CLI/models/attributes.go
Original file line number Diff line number Diff line change
@@ -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 {
66 changes: 66 additions & 0 deletions CLI/models/attributes_rack.go
Original file line number Diff line number Diff line change
@@ -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
}
65 changes: 65 additions & 0 deletions CLI/models/attributes_room.go
Original file line number Diff line number Diff line change
@@ -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
}
8 changes: 8 additions & 0 deletions CLI/models/attributes_test.go
Original file line number Diff line number Diff line change
@@ -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")
}
13 changes: 13 additions & 0 deletions CLI/models/entity.go
Original file line number Diff line number Diff line change
@@ -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:
348 changes: 7 additions & 341 deletions CLI/parser/ast.go

Large diffs are not rendered by default.

170 changes: 0 additions & 170 deletions CLI/parser/ast_test.go
Original file line number Diff line number Diff line change
@@ -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)

19 changes: 0 additions & 19 deletions CLI/parser/astutil.go
Original file line number Diff line number Diff line change
@@ -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{}

8 changes: 0 additions & 8 deletions CLI/parser/astutil_test.go
Original file line number Diff line number Diff line change
@@ -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"},
14 changes: 5 additions & 9 deletions CLI/parser/parser.go
Original file line number Diff line number Diff line change
@@ -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()
4 changes: 4 additions & 0 deletions CLI/test/mocks.go
Original file line number Diff line number Diff line change
@@ -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))
}
12 changes: 8 additions & 4 deletions CLI/views/get.go
Original file line number Diff line number Diff line change
@@ -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 {

0 comments on commit 822627d

Please sign in to comment.