Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[ENG-2320] Reduce cyclomatic complexity of the browse function #119

Open
wants to merge 10 commits into
base: master
Choose a base branch
from
149 changes: 8 additions & 141 deletions opcua_plugin/browse.go
Original file line number Diff line number Diff line change
Expand Up @@ -133,157 +133,24 @@ func browse(ctx context.Context, n NodeBrowser, path string, level int, logger L
return
}

var newPath string
if path == "" {
newPath = sanitize(browseName.Name)
} else {
newPath = path + "." + sanitize(browseName.Name)
newPath := sanitize(browseName.Name)
if path != "" {
newPath = path + "." + newPath
}

var def = NodeDef{
NodeID: n.ID(),
Path: newPath,
NodeID: n.ID(),
Path: newPath,
ParentNodeID: parentNodeId,
}

switch err := attrs[0].Status; {
case errors.Is(err, ua.StatusOK):
if attrs[0].Value == nil {
sendError(ctx, errors.New("node class is nil"), errChan, logger)
return
} else {
def.NodeClass = ua.NodeClass(attrs[0].Value.Int())
}
case errors.Is(err, ua.StatusBadSecurityModeInsufficient):
return
case errors.Is(err, ua.StatusBadNotReadable): // fallback option to not throw an error (this is "normal" for some servers)
logger.Warnf("Tried to browse node: %s but got access denied on getting the NodeClass, do not subscribe to it, continuing browsing its children...\n", path)
def.NodeClass = ua.NodeClassObject // by setting it as an object, we will not subscribe to it
// no need to return here, as we can continue without the NodeClass for browsing
default:
sendError(ctx, err, errChan, logger)
return
}

switch err := attrs[1].Status; {
case errors.Is(err, ua.StatusOK):
if attrs[1].Value == nil {
sendError(ctx, errors.New("browse name is nil"), errChan, logger)
return
} else {
def.BrowseName = attrs[1].Value.String()
}
case errors.Is(err, ua.StatusBadSecurityModeInsufficient):
return
case errors.Is(err, ua.StatusBadNotReadable): // fallback option to not throw an error (this is "normal" for some servers)
logger.Warnf("Tried to browse node: %s but got access denied on getting the BrowseName, skipping...\n", path)
return // We need to return here, as we can't continue without the BrowseName (we need it at least for the path when browsing the children)
default:
sendError(ctx, err, errChan, logger)
return
}

switch err := attrs[2].Status; {
case errors.Is(err, ua.StatusOK):
if attrs[2].Value == nil {
def.Description = "" // this can happen for example in Kepware v6, where the description is OPCUAType_Null
} else {
def.Description = attrs[2].Value.String()
}
case errors.Is(err, ua.StatusBadAttributeIDInvalid):
// ignore
case errors.Is(err, ua.StatusBadSecurityModeInsufficient):
return
case errors.Is(err, ua.StatusBadNotReadable): // fallback option to not throw an error (this is "normal" for some servers)
logger.Warnf("Tried to browse node: %s but got access denied on getting the Description, do not subscribe to it, continuing browsing its children...\n", path)
def.NodeClass = ua.NodeClassObject // by setting it as an object, we will not subscribe to it
// no need to return here, as we can continue without the Description
default:
sendError(ctx, err, errChan, logger)
return
}

switch err := attrs[3].Status; {
case errors.Is(err, ua.StatusOK):
if attrs[3].Value == nil {
sendError(ctx, errors.New("access level is nil"), errChan, logger)
return
} else {
def.AccessLevel = ua.AccessLevelType(attrs[3].Value.Int())
}
case errors.Is(err, ua.StatusBadAttributeIDInvalid):
// ignore
case errors.Is(err, ua.StatusBadSecurityModeInsufficient):
return
case errors.Is(err, ua.StatusBadNotReadable): // fallback option to not throw an error (this is "normal" for some servers)
logger.Warnf("Tried to browse node: %s but got access denied on getting the AccessLevel, continuing...\n", path)
// no need to return here, as we can continue without the AccessLevel for browsing
default:
sendError(ctx, err, errChan, logger)
return
}

// if AccessLevel exists and it is set to None
if def.AccessLevel == ua.AccessLevelTypeNone && errors.Is(err, ua.StatusOK) {
logger.Warnf("Tried to browse node: %s but access level is None ('access denied'). Do not subscribe to it, continuing browsing its children...\n", path)
def.NodeClass = ua.NodeClassObject // by setting it as an object, we will not subscribe to it
// we need to continue here, as we still want to browse the children of this node
}

switch err := attrs[4].Status; {
case errors.Is(err, ua.StatusOK):
if attrs[4].Value == nil {
// This is not an error, it can happen for some OPC UA servers...
// in oru case it is the amine amaach opcua simulator
// if the data type is nil, we simpy ignore it
// errChan <- errors.New("data type is nil")
logger.Debugf("ignoring node: %s as its datatype is nil...\n", path)
return
} else {
switch v := attrs[4].Value.NodeID().IntID(); v {
case id.DateTime:
def.DataType = "time.Time"
case id.Boolean:
def.DataType = "bool"
case id.SByte:
def.DataType = "int8"
case id.Int16:
def.DataType = "int16"
case id.Int32:
def.DataType = "int32"
case id.Byte:
def.DataType = "byte"
case id.UInt16:
def.DataType = "uint16"
case id.UInt32:
def.DataType = "uint32"
case id.UtcTime:
def.DataType = "time.Time"
case id.String:
def.DataType = "string"
case id.Float:
def.DataType = "float32"
case id.Double:
def.DataType = "float64"
default:
def.DataType = attrs[4].Value.NodeID().String()
}
}

case errors.Is(err, ua.StatusBadAttributeIDInvalid):
// ignore
case errors.Is(err, ua.StatusBadSecurityModeInsufficient):
return
case errors.Is(err, ua.StatusBadNotReadable): // fallback option to not throw an error (this is "normal" for some servers)
logger.Warnf("Tried to browse node: %s but got access denied on getting the DataType, do not subscribe to it, continuing browsing its children...\n", path)
def.NodeClass = ua.NodeClassObject // by setting it as an object, we will not subscribe to it
// no need to return here, as we can continue without the DataType
default:
// def.NodeClass, def.BrowseName, def.Description, def.AccessLevel, def.DataType are set in the processNodeAttributes function
if err := processNodeAttributes(attrs, &def, newPath, logger); err != nil {
sendError(ctx, err, errChan, logger)
return
}

logger.Debugf("%d: def.Path:%s def.NodeClass:%s\n", level, def.Path, def.NodeClass)
def.ParentNodeID = parentNodeId

browseChildrenV2 := func(refType uint32) error {
children, err := n.Children(ctx, refType, ua.NodeClassVariable|ua.NodeClassObject)
Expand Down
183 changes: 183 additions & 0 deletions opcua_plugin/node_attributes.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
package opcua_plugin

import (
"errors"
"fmt"

"github.com/gopcua/opcua/id"
"github.com/gopcua/opcua/ua"
)

// AttributeHandler is the struct that handles the attributes of a node
type AttributeHandler struct {
// Using more idiomatic names
handleOK func(value *ua.Variant) error // handleOK is the function that handles the OK status
handleNotReadable func() // handleNotReadable is the function that handles the NotReadable status
ignoreInvalidAttr bool // ignoreInvalidAttr is the flag that determines if invalid attributes should be ignored
requiresValue bool // requiresValue is the flag that determines if a value is required
affectsNodeClass bool // affectsNodeClass is the flag that determines if the node class should be affected
}

// handleAttributeStatus is the function that handles the attributes of a node
func handleAttributeStatus(
name string,
attr *ua.DataValue,
def *NodeDef,
path string,
logger Logger,
handler AttributeHandler,
) error {
if attr == nil {
return fmt.Errorf("attribute is nil")
}

if attr.Status == ua.StatusOK {
if attr.Value == nil && handler.requiresValue {
return fmt.Errorf("attribute value is nil for %s", name)
}
return handleOKStatus(attr.Value, handler)
}

if attr.Status == ua.StatusBadSecurityModeInsufficient {
return errors.New("insufficient security mode")
}

if attr.Status == ua.StatusBadAttributeIDInvalid {
if handler.ignoreInvalidAttr {
return nil
}
return fmt.Errorf("invalid attribute ID")
}

if attr.Status == ua.StatusBadNotReadable {
handleNotReadable(def, handler, path, logger)
return nil
}

return attr.Status
}

// handleOKStatus processes successful attribute reads
func handleOKStatus(value *ua.Variant, handler AttributeHandler) error {
if handler.handleOK != nil && value != nil {
return handler.handleOK(value)
}
return nil
}

// handleNotReadable processes non-readable attributes
func handleNotReadable(def *NodeDef, handler AttributeHandler, path string, logger Logger) {
if handler.affectsNodeClass {
def.NodeClass = ua.NodeClassObject
}
if handler.handleNotReadable != nil {
handler.handleNotReadable()
}
logger.Warnf("Access denied for node: %s, continuing...\n", path)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

be careful about warning, they will prevent deployment in the management console

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The existing code in master branch uses logger.Warnf for this scenario and the status quo is maintained.

}
kanapuli marked this conversation as resolved.
Show resolved Hide resolved

// processNodeAttributes processes all attributes for a node
// This function is used to process the attributes of a node and set the NodeDef struct
// def.NodeClass, def.BrowseName, def.Description, def.AccessLevel, def.DataType are set in the processNodeAttributes function
func processNodeAttributes(attrs []*ua.DataValue, def *NodeDef, path string, logger Logger) error {
// NodeClass (attrs[0])
nodeClassHandler := AttributeHandler{
handleOK: func(value *ua.Variant) error {
def.NodeClass = ua.NodeClass(value.Int())
return nil
},
requiresValue: true,
affectsNodeClass: true,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe explain for those edge cases with a short comment why it requires a value and it affects the node class. for nodeclass, quite obvious, for description maybe not that obvious

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comments are added now

}
if err := handleAttributeStatus("NodeClass", attrs[0], def, path, logger, nodeClassHandler); err != nil {
return err
}

// BrowseName (attrs[1])
browseNameHandler := AttributeHandler{
handleOK: func(value *ua.Variant) error {
def.BrowseName = value.String()
return nil
},
requiresValue: true,
}
if err := handleAttributeStatus("BrowseName", attrs[1], def, path, logger, browseNameHandler); err != nil {
return err
}

// Description (attrs[2])
descriptionHandler := AttributeHandler{
handleOK: func(value *ua.Variant) error {
if value != nil {
def.Description = value.String()
} else {
def.Description = ""
}
return nil
},
ignoreInvalidAttr: true,
affectsNodeClass: true,
}
if err := handleAttributeStatus("Description", attrs[2], def, path, logger, descriptionHandler); err != nil {
return err
}

// AccessLevel (attrs[3])
accessLevelHandler := AttributeHandler{
handleOK: func(value *ua.Variant) error {
def.AccessLevel = ua.AccessLevelType(value.Int())
return nil
},
ignoreInvalidAttr: true,
}
if err := handleAttributeStatus("AccessLevel", attrs[3], def, path, logger, accessLevelHandler); err != nil {
return err
}

// Check AccessLevel None
if def.AccessLevel == ua.AccessLevelTypeNone {
logger.Warnf("Node %s has AccessLevel None, marking as Object\n", path)
def.NodeClass = ua.NodeClassObject
}

// DataType (attrs[4])
dataTypeHandler := AttributeHandler{
handleOK: func(value *ua.Variant) error {
if value == nil {
logger.Debugf("ignoring node: %s as its datatype is nil...\n", path)
return fmt.Errorf("datatype is nil")
}
def.DataType = getDataTypeString(value.NodeID().IntID())
return nil
},
ignoreInvalidAttr: true,
affectsNodeClass: true,
}
if err := handleAttributeStatus("DataType", attrs[4], def, path, logger, dataTypeHandler); err != nil {
return err
}

return nil
}

var dataTypeMap = map[uint32]string{
id.DateTime: "time.Time",
id.Boolean: "bool",
id.SByte: "int8",
id.Int16: "int16",
id.Int32: "int32",
id.Byte: "byte",
id.UInt16: "uint16",
id.UInt32: "uint32",
id.UtcTime: "time.Time",
id.String: "string",
id.Float: "float32",
id.Double: "float64",
}

func getDataTypeString(typeID uint32) string {
if dtype, ok := dataTypeMap[typeID]; ok {
return dtype
}
return fmt.Sprintf("ns=%d;i=%d", 0, typeID)
}
2 changes: 1 addition & 1 deletion opcua_plugin/opcua_unittest_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@
}

Expect(errs).Should(HaveLen(1))
Expect(errs[0].Error()).To(Equal("opcua: node class is nil"))
Expect(errs[0].Error()).To(Equal("attribute value is nil for NodeClass"))
})

It("should browse the root node with no children with right attributes", func() {
Expand Down Expand Up @@ -316,7 +316,7 @@

Context("When browsing a folder structure with HasTypeDefinition and HasChild references", func() {
It("should browse through ABC folder and return the ProcessValue variable", func() {
Skip("fake opc ua node browser cannot handle this, as children wiull always return all referencednodes independent of the nodeclass")

Check notice on line 319 in opcua_plugin/opcua_unittest_test.go

View workflow job for this annotation

GitHub Actions / go-test-nodered-js

It 01/30/25 13:19:17.16
// Create ABC folder node
abcFolder := createMockVariableNode(1234, "ABC")
abcFolder.attributes = append(abcFolder.attributes, getDataValueForNodeClass(ua.NodeClassObject))
Expand Down
Loading