Files
infix/src/webui/internal/schema/manager.go
T
Joachim Wiberg 545299ec7f webui: advanced configure using a yang tree
Navigating interfaces now work, and the tree follows the accordion style
like the rest of the UI.

Also, some updates to the curated pages: online help, lists of leafrefs
instead of hard-coded list options.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-06-05 16:30:54 +02:00

900 lines
27 KiB
Go

package schema
import (
"fmt"
"log"
"os"
"path/filepath"
"reflect"
"regexp"
"sort"
"strings"
"github.com/openconfig/goyang/pkg/yang"
)
// Manager holds a fully processed goyang module set and provides schema queries.
// All modules are loaded before Process() is called so that cross-module
// identityref resolution and augments work correctly.
type Manager struct {
ms *yang.Modules
}
// Load parses all .yang files in yangDir and returns a Manager.
// Errors from Process() that are non-fatal (e.g. unresolved augments for
// modules that were not downloaded) are logged but do not abort loading.
func Load(yangDir string) (*Manager, error) {
ms := yang.NewModules()
ms.Path = []string{yangDir}
entries, err := os.ReadDir(yangDir)
if err != nil {
return nil, fmt.Errorf("schema: read yang dir: %w", err)
}
for _, e := range entries {
if e.IsDir() || !strings.HasSuffix(e.Name(), ".yang") {
continue
}
if err := ms.Read(filepath.Join(yangDir, e.Name())); err != nil {
// Non-fatal: goyang may still register the module partially.
log.Printf("schema: parse (non-fatal) %s: %v", e.Name(), err)
}
}
errs := ms.Process()
for _, err := range errs {
// goyang v1.6.3 has known YANG 1.1 gaps (must/when substatements,
// duplicate augments). These are non-fatal; log at debug level.
log.Printf("schema: process (non-fatal): %v", err)
}
return &Manager{ms: ms}, nil
}
// Children returns the direct config-relevant child Nodes of the node at path.
// Use path "/" for the top-level module list.
// choice/case nodes are inlined transparently (their children are promoted).
func (m *Manager) Children(path string) ([]*Node, error) {
if path == "" || path == "/" {
return m.topLevelNodes(), nil
}
e, err := m.entryAt(path)
if err != nil {
return nil, err
}
if e.Dir == nil {
return nil, nil
}
return dirToNodes(e.Dir, path), nil
}
// ChildrenAll is like Children but includes config:false nodes, making it
// suitable for the operational/status tree where state data reuses read-write
// schema nodes. Only RPCs, notifications, anydata/anyxml, and deprecated or
// obsolete nodes are excluded.
func (m *Manager) ChildrenAll(path string) ([]*Node, error) {
if path == "" || path == "/" {
return m.topLevelNodesAll(), nil
}
e, err := m.entryAt(path)
if err != nil {
return nil, err
}
if e.Dir == nil {
return nil, nil
}
return dirToNodesAll(e.Dir, path), nil
}
// NodeAt returns a Node for the YANG schema node at path (without children).
func (m *Manager) NodeAt(path string) (*Node, error) {
if path == "" || path == "/" {
return &Node{Path: "/", Name: "", Kind: "container", Config: true}, nil
}
e, err := m.entryAt(path)
if err != nil {
return nil, err
}
return entryToNode(e, path), nil
}
// IdentitiesOf returns the names of all identities derived (directly or
// transitively) from baseName. baseName may be "name" or "module:name".
// Searches across all loaded modules.
func (m *Manager) IdentitiesOf(baseName string) []string {
// Strip module prefix if present; search by identity name.
_, localName := splitPrefix(baseName)
for _, mod := range m.ms.Modules {
for _, id := range mod.Identities() {
if id.Name == localName {
var names []string
for _, v := range id.Values {
names = append(names, v.Name)
}
sort.Strings(names)
return names
}
}
}
return nil
}
// ResolveLeafref converts a YANG leafref path expression to an absolute schema
// path that can be fetched via RESTCONF. contextPath is the schema path of the
// leaf that holds the leafref (needed for relative `../` resolution).
// Returns empty string if the path cannot be resolved.
func (m *Manager) ResolveLeafref(leafrefPath, contextPath string) string {
if strings.HasPrefix(leafrefPath, "/") {
return normalizeLeafrefPath(leafrefPath)
}
// Relative path: resolve `../` against the context, stripping the leaf itself.
parts := strings.Split(strings.TrimPrefix(contextPath, "/"), "/")
if len(parts) > 0 {
parts = parts[:len(parts)-1] // remove the leaf; start from its parent
}
for _, seg := range strings.Split(leafrefPath, "/") {
switch seg {
case "..":
if len(parts) > 0 {
parts = parts[:len(parts)-1]
}
case ".", "":
// skip
default:
parts = append(parts, stripPredicate(seg))
}
}
if len(parts) == 0 {
return ""
}
return "/" + strings.Join(parts, "/")
}
// normalizeLeafrefPath strips XPath predicates from each segment of an
// absolute YANG leafref path so it can be used as a schema lookup path.
func normalizeLeafrefPath(p string) string {
segs := strings.Split(strings.TrimPrefix(p, "/"), "/")
for i, s := range segs {
segs[i] = stripPredicate(s)
}
return "/" + strings.Join(segs, "/")
}
// canonicalizeLeafrefPath rewrites a YANG leafref path into the RESTCONF
// canonical form (RFC 8040 §3.5.3): each segment's prefix alias is
// resolved to its canonical module name, and the "module:" qualifier
// is dropped from segments whose module matches the preceding
// qualified segment. Unknown prefixes are left untouched.
//
// Example: "/if:interfaces/if:interface/if:name" with the `if` prefix
// pointing at `ietf-interfaces` becomes
// "/ietf-interfaces:interfaces/interface/name", which is what
// nodeSegment() produces for ordinary paths and what RESTCONF expects.
func canonicalizeLeafrefPath(node yang.Node, p string) string {
if node == nil || p == "" {
return p
}
segs := strings.Split(p, "/")
var lastMod string
for i, seg := range segs {
if seg == "" || seg == ".." || seg == "." {
continue
}
bare, pred := seg, ""
if b := strings.IndexByte(seg, '['); b >= 0 {
bare, pred = seg[:b], seg[b:]
}
colon := strings.IndexByte(bare, ':')
if colon < 0 {
// Unprefixed segments inherit the prevailing module.
continue
}
prefix, rest := bare[:colon], bare[colon+1:]
mod := yang.FindModuleByPrefix(node, prefix)
if mod == nil {
continue
}
modName := mod.Name
if mod.BelongsTo != nil {
modName = mod.BelongsTo.Name
}
if modName == lastMod {
segs[i] = rest + pred
} else {
segs[i] = modName + ":" + rest + pred
lastMod = modName
}
}
return strings.Join(segs, "/")
}
// derefSiblingPattern captures the `deref(.)/../<sibling>` XPath idiom
// used by Infix YANG must statements to constrain a leafref's target
// to objects that carry a specific sibling node (e.g. `bridge-port/
// bridge`'s must says the referenced interface must have a `bridge`
// container — i.e. it has to be a bridge). The capture group is the
// local sibling name; any prefix (`tt:type`) is matched non-capturing
// so callers don't have to strip it.
var derefSiblingPattern = regexp.MustCompile(`deref\s*\(\s*\.\s*\)\s*/\s*\.\.\s*/\s*(?:[a-zA-Z][\w-]*:)?([a-zA-Z][\w-]*)`)
// leafrefSiblingFilter looks at the leaf's must statements (only Leaf
// nodes carry must in goyang's Entry tree) for the deref(.)/../X
// pattern and returns X, or "" when no such constraint exists.
func leafrefSiblingFilter(e *yang.Entry) string {
leaf, ok := e.Node.(*yang.Leaf)
if !ok {
return ""
}
for _, m := range leaf.Must {
if m == nil {
continue
}
if match := derefSiblingPattern.FindStringSubmatch(m.Name); len(match) == 2 {
return match[1]
}
}
return ""
}
// Default returns the YANG default value for the leaf at path, if any.
func (m *Manager) Default(path string) (string, bool) {
e, err := m.entryAt(path)
if err != nil {
return "", false
}
return e.SingleDefaultValue()
}
// ModuleName returns the module name for the schema node at path.
func (m *Manager) ModuleName(path string) (string, error) {
e, err := m.entryAt(path)
if err != nil {
return "", err
}
return e.InstantiatingModule()
}
// ModuleQualifiedName returns "module:name" for the schema node at path.
// This is the JSON object key used when PUT/PATCHing that node directly via
// RESTCONF (RFC 7951 §4 — namespace-qualified name at module boundaries).
func (m *Manager) ModuleQualifiedName(path string) (string, error) {
e, err := m.entryAt(path)
if err != nil {
return "", err
}
modName, err := e.InstantiatingModule()
if err != nil {
return "", err
}
return modName + ":" + e.Name, nil
}
// internalOperationalTopNodes lists bare node names that must not appear at
// the top level of the operational/status tree. These are all transport- or
// server-monitoring nodes of no interest to end users. They are already
// absent from the configure tree because they are config:false; this list
// applies the same exclusion to topLevelNodesAll.
var internalOperationalTopNodes = map[string]bool{
"netconf": true, // NETCONF session/lock monitoring
"netconf-state": true, // ietf-netconf-monitoring (RFC 6022)
"notification": true, // nc-notifications stream container
"restconf-state": true, // ietf-restconf-monitoring (RFC 8527)
"supported-algorithms": true, // SSH/TLS algorithm capability advertisement
"system-capabilities": true, // ietf-system-capabilities (RFC 9196)
}
// internalModules is the deny-list of YANG modules that are infrastructure-only
// and must not appear in the user-facing configure tree.
// Mirrors sr_module_is_internal() from klish-plugin-sysrepo/src/pline.c.
// Note: ietf-netconf-acm is intentionally NOT listed here — Infix exposes NACM
// configuration to users.
var internalModules = map[string]bool{
// libyang built-ins
"ietf-yang-metadata": true,
"yang": true,
"ietf-inet-types": true,
"ietf-yang-types": true,
// YANG library / schema mount
"ietf-datastores": true,
"ietf-yang-schema-mount": true,
"ietf-yang-library": true,
// NETCONF infrastructure
"ietf-netconf": true,
"ietf-netconf-with-defaults": true,
"ietf-origin": true,
"ietf-netconf-notifications": true,
// sysrepo internals
"sysrepo": true,
"sysrepo-monitoring": true,
"sysrepo-plugind": true,
// Infix test/debug subtree and meta-data — not user-facing
"infix-test": true,
"infix-meta": true,
// NETCONF server / transport infrastructure — managed by the system, not users
"ietf-netconf-server": true,
"libnetconf2-netconf-server": true,
"ietf-truststore": true,
// Notification subscriptions and filters — not user-configurable via WebUI
"ietf-subscribed-notifications": true,
// ACL, key-chains, network-instances — not exposed in WebUI yet
"ietf-access-control-list": true,
"ietf-key-chain": true,
"ietf-network-instance": true,
}
// topLevelNodes returns a Node for each config-relevant top-level schema node
// across all loaded modules. Submodules, versioned aliases, internal modules,
// and config:false top-level nodes are all excluded.
func (m *Manager) topLevelNodes() []*Node {
var nodes []*Node
for key, mod := range m.ms.Modules {
if strings.Contains(key, "@") {
continue // versioned alias (e.g. "ieee802-dot1ab-lldp@2022-03-15") — duplicate
}
if mod.BelongsTo != nil {
continue // submodule — content appears under the parent module
}
if internalModules[key] {
continue
}
e := yang.ToEntry(mod)
if e == nil || e.Dir == nil {
continue
}
for _, child := range sortedEntries(e.Dir) {
if isNonConfigNode(child) {
continue
}
nodePath := "/" + key + ":" + child.Name
if isContainerList(child) {
if lc := listChildOf(child); lc != nil {
nodes = append(nodes, entryToNode(lc, nodePath+"/"+nodeSegment(lc, key)))
}
continue
}
nodes = append(nodes, entryToNode(child, nodePath))
}
}
// Sort by node name (not module-qualified path) for a clean alphabetical list.
sort.Slice(nodes, func(i, j int) bool { return nodes[i].Name < nodes[j].Name })
return nodes
}
// entryAt resolves a RESTCONF-style path to a goyang Entry.
// Path format: /module:top-node/child[key='val']/grandchild
// Key predicates ([key='val']) are stripped — they carry instance identity,
// not schema identity. choice/case nodes are skipped during traversal.
func (m *Manager) entryAt(path string) (*yang.Entry, error) {
path = strings.TrimPrefix(path, "/")
parts := strings.SplitN(path, "/", 2)
head := stripPredicate(parts[0])
moduleName, nodeName := splitPrefix(head)
if moduleName == "" {
return nil, fmt.Errorf("schema: path must start with module prefix: %s", path)
}
mod, ok := m.ms.Modules[moduleName]
if !ok {
return nil, fmt.Errorf("schema: module not found: %s", moduleName)
}
root := yang.ToEntry(mod)
if root == nil {
return nil, fmt.Errorf("schema: no entry for module: %s", moduleName)
}
e := findInDir(root.Dir, nodeName)
if e == nil {
return nil, fmt.Errorf("schema: %s not found in %s", nodeName, moduleName)
}
if len(parts) == 1 {
return e, nil
}
// Traverse remaining path segments, stripping key predicates.
for _, seg := range strings.Split(parts[1], "/") {
if seg == "" {
continue
}
_, localName := splitPrefix(stripPredicate(seg))
child := findInDir(e.Dir, localName)
if child == nil {
return nil, fmt.Errorf("schema: %s not found under %s", localName, e.Name)
}
e = child
}
return e, nil
}
// stripPredicate removes a key predicate from a path segment.
// Handles both RESTCONF ("interface=eth0" → "interface") and
// XPath ("interface[name='eth0']" → "interface") forms.
func stripPredicate(seg string) string {
if i := strings.IndexByte(seg, '['); i >= 0 {
return seg[:i]
}
if i := strings.IndexByte(seg, '='); i >= 0 {
return seg[:i]
}
return seg
}
// findInDir looks up name in a dir map, transparently descending into
// choice/case nodes which are not part of the RESTCONF path.
func findInDir(dir map[string]*yang.Entry, name string) *yang.Entry {
if dir == nil {
return nil
}
// Direct match — but if it is a choice/case, descend into it because YANG
// commonly names a case identically to the leaf it contains (e.g. the
// ietf-system timezone-name case wrapping the timezone-name leaf).
if e, ok := dir[name]; ok {
if !e.IsChoice() && !e.IsCase() {
return e
}
if found := findInDir(e.Dir, name); found != nil {
return found
}
}
// Search inside all choice/case children.
for _, e := range dir {
if e.IsChoice() || e.IsCase() {
if found := findInDir(e.Dir, name); found != nil {
return found
}
}
}
return nil
}
// isContainerList returns true for containers that wrap exactly one list and
// nothing else — the classic YANG container/list idiom (e.g. /interfaces wrapping
// /interfaces/interface). Mirrors klysc_is_container_list() in klish-plugin-sysrepo
// exactly: ALL direct children are inspected, including config:false ones. Any
// non-list child (a leaf, another container, …) prevents collapsing. This is why
// "hardware" does not collapse — it has a config:false "last-change" leaf alongside
// "component", so the default case fires and returns false.
//
// Hard exceptions:
// - "static-routes" always collapses (ietf-routing, asymmetric naming)
// - "mdb" never collapses
// - "ipv4", "ipv6" never collapse (would confuse routing subtrees)
func isContainerList(e *yang.Entry) bool {
if !e.IsContainer() || e.Dir == nil {
return false
}
if e.Name == "mdb" || e.Name == "ipv4" || e.Name == "ipv6" {
return false
}
if e.Name == "static-routes" {
return true
}
listCount := 0
for _, child := range e.Dir {
if !child.IsList() {
return false // any non-list child (even config:false) prevents collapse
}
listCount++
}
return listCount == 1
}
// listChildOf returns the single list child of a collapsible container (panics if
// called on a non-collapsible container — callers must guard with isContainerList).
func listChildOf(e *yang.Entry) *yang.Entry {
for _, child := range e.Dir {
if child.IsList() && !isNonConfigNode(child) {
return child
}
}
return nil
}
// listChildOfAll is like listChildOf but uses the data-node filter (includes
// config:false lists), used by dirToNodesAll / the operational status tree.
func listChildOfAll(e *yang.Entry) *yang.Entry {
for _, child := range e.Dir {
if child.IsList() && !isNonDataNode(child) {
return child
}
}
return nil
}
// topLevelNodesAll is like topLevelNodes but includes config:false top-level
// nodes, for use by the operational status tree.
func (m *Manager) topLevelNodesAll() []*Node {
var nodes []*Node
for key, mod := range m.ms.Modules {
if strings.Contains(key, "@") {
continue
}
if mod.BelongsTo != nil {
continue
}
if internalModules[key] {
continue
}
e := yang.ToEntry(mod)
if e == nil || e.Dir == nil {
continue
}
for _, child := range sortedEntries(e.Dir) {
if isNonDataNode(child) {
continue
}
if internalOperationalTopNodes[child.Name] {
continue
}
nodePath := "/" + key + ":" + child.Name
if isContainerList(child) {
if lc := listChildOfAll(child); lc != nil {
nodes = append(nodes, entryToNode(lc, nodePath+"/"+nodeSegment(lc, key)))
}
continue
}
nodes = append(nodes, entryToNode(child, nodePath))
}
}
sort.Slice(nodes, func(i, j int) bool { return nodes[i].Name < nodes[j].Name })
return nodes
}
// dirToNodesAll is like dirToNodes but uses isNonDataNode, including
// config:false leaves and containers in the result.
func dirToNodesAll(dir map[string]*yang.Entry, parentPath string) []*Node {
parentMod := extractModuleFromPath(parentPath)
var nodes []*Node
for _, e := range sortedEntries(dir) {
if e.IsChoice() || e.IsCase() {
caseWhen := extractWhen(e)
for _, child := range dirToNodesAll(e.Dir, parentPath) {
if caseWhen != "" && child.When == "" {
child.When = caseWhen
}
nodes = append(nodes, child)
}
continue
}
if isNonDataNode(e) {
continue
}
nodePath := parentPath + "/" + nodeSegment(e, parentMod)
if isContainerList(e) {
if lc := listChildOfAll(e); lc != nil {
nodes = append(nodes, entryToNode(lc, nodePath+"/"+nodeSegment(lc, parentMod)))
}
continue
}
nodes = append(nodes, entryToNode(e, nodePath))
}
return nodes
}
// dirToNodes converts a goyang Dir map to a sorted slice of Nodes.
// choice/case children are inlined (their contents promoted to this level).
// Collapsible container-list wrappers are transparent: the list child is surfaced
// directly under the parent with the full (un-collapsed) RESTCONF path.
// RPC, notification, anydata, anyxml and config:false nodes are excluded.
//
// RESTCONF requires module qualification ("module:name") whenever a node's
// module differs from its parent's — the common case being augmented nodes.
// e.g. infix-lldp augments ieee802-dot1ab-lldp:lldp, so the path is
// /ieee802-dot1ab-lldp:lldp/infix-lldp:enabled, not /…/enabled.
func dirToNodes(dir map[string]*yang.Entry, parentPath string) []*Node {
parentMod := extractModuleFromPath(parentPath)
var nodes []*Node
for _, e := range sortedEntries(dir) {
if e.IsChoice() || e.IsCase() {
// Extract when from the choice/case before inlining its children.
// Cases contributed by augments carry the augment's when (e.g.
// lag-port only for ethernetCsmacd, bridge-port only for bridge).
// Propagate that constraint to each promoted child that has no when of
// its own; if the child already has one, leave it alone.
caseWhen := extractWhen(e)
for _, child := range dirToNodes(e.Dir, parentPath) {
if caseWhen != "" && child.When == "" {
child.When = caseWhen
}
nodes = append(nodes, child)
}
continue
}
if isNonConfigNode(e) {
continue
}
nodePath := parentPath + "/" + nodeSegment(e, parentMod)
if isContainerList(e) {
if lc := listChildOf(e); lc != nil {
// Container is collapsed; qualify list child against parentMod.
nodes = append(nodes, entryToNode(lc, nodePath+"/"+nodeSegment(lc, parentMod)))
}
continue
}
nodes = append(nodes, entryToNode(e, nodePath))
}
return nodes
}
// nodeSegment returns the path segment for e, qualified as "module:name" when
// e's instantiating module differs from parentMod (RESTCONF RFC 8040 §3.5.3).
func nodeSegment(e *yang.Entry, parentMod string) string {
mod, err := e.InstantiatingModule()
if err != nil || mod == parentMod {
return e.Name
}
return mod + ":" + e.Name
}
// extractModuleFromPath returns the module from the rightmost module-qualified
// segment in a RESTCONF path, e.g. "/ieee802-dot1ab-lldp:lldp/port" → "ieee802-dot1ab-lldp".
func extractModuleFromPath(path string) string {
segs := strings.Split(strings.TrimPrefix(path, "/"), "/")
for i := len(segs) - 1; i >= 0; i-- {
seg := stripPredicate(segs[i])
if j := strings.IndexByte(seg, ':'); j > 0 {
return seg[:j]
}
}
return ""
}
// isNonConfigNode returns true for schema nodes that do not belong in the
// configuration tree: RPCs, notifications, anydata/anyxml, config:false subtrees,
// and nodes with YANG status deprecated or obsolete.
// goyang propagates config:false from parent to children during Process(), so a
// single check at each level is sufficient to prune entire read-only subtrees.
func isNonConfigNode(e *yang.Entry) bool {
return e.RPC != nil ||
e.Kind == yang.NotificationEntry ||
e.Kind == yang.AnyDataEntry ||
e.Kind == yang.AnyXMLEntry ||
e.Config == yang.TSFalse ||
isDeprecatedOrObsolete(e)
}
// isNonDataNode is like isNonConfigNode but keeps config:false nodes.
// Used by ChildrenAll / the operational status tree which shows all data —
// operational state reuses read-write schema nodes alongside state-only leaves.
func isNonDataNode(e *yang.Entry) bool {
return e.RPC != nil ||
e.Kind == yang.NotificationEntry ||
e.Kind == yang.AnyDataEntry ||
e.Kind == yang.AnyXMLEntry ||
isDeprecatedOrObsolete(e)
}
// entryPresence returns the YANG presence statement string for e, or "".
// Presence is stored on the underlying Container Node via reflection because
// yang.Entry does not expose it directly.
func entryPresence(e *yang.Entry) string {
if e.Node == nil {
return ""
}
v := reflect.ValueOf(e.Node)
if v.Kind() == reflect.Ptr {
v = v.Elem()
}
f := v.FieldByName("Presence")
if !f.IsValid() || f.Kind() != reflect.Ptr || f.IsNil() {
return ""
}
name := f.Elem().FieldByName("Name")
if !name.IsValid() {
return ""
}
return name.String()
}
// isDeprecatedOrObsolete returns true when the YANG node carries
// "status deprecated" or "status obsolete". goyang does not expose status
// directly on Entry, so we reach through to the underlying Node via reflection.
// All concrete yang node types (Container, Leaf, List, …) have Status *Value.
func isDeprecatedOrObsolete(e *yang.Entry) bool {
if e.Node == nil {
return false
}
v := reflect.ValueOf(e.Node)
if v.Kind() == reflect.Ptr {
v = v.Elem()
}
f := v.FieldByName("Status")
if !f.IsValid() || f.Kind() != reflect.Ptr || f.IsNil() {
return false
}
name := f.Elem().FieldByName("Name")
if !name.IsValid() {
return false
}
s := name.String()
return s == "deprecated" || s == "obsolete"
}
// entryToNode converts a goyang Entry to a schema Node (no children populated).
func entryToNode(e *yang.Entry, path string) *Node {
n := &Node{
Path: path,
Name: e.Name,
Kind: entryKind(e),
Description: e.Description,
Config: e.Config != yang.TSFalse,
Mandatory: e.Mandatory == yang.TSTrue,
When: extractWhen(e),
Presence: entryPresence(e),
}
if def, ok := e.SingleDefaultValue(); ok {
n.Default = def
}
if strings.Contains(e.Key, " ") || e.Key != "" {
n.Keys = strings.Fields(e.Key)
}
if e.IsLeaf() || e.IsLeafList() {
n.Type = yangTypeInfo(e)
}
return n
}
// prefixInXPath matches a "prefix:X" token where X is a letter or underscore,
// the first character of an identifier. The matched letter is included so
// ReplaceAllStringFunc can reattach it after resolving the prefix.
var prefixInXPath = regexp.MustCompile(`[a-zA-Z][a-zA-Z0-9_\-]*:[a-zA-Z_]`)
// extractWhen returns the pre-resolved YANG when expression for e, or "".
// It first checks e itself (when directly on the node), then checks parent
// augments — the Infix convention is to put when on the augment, not on the
// top-level container inside the augment.
func extractWhen(e *yang.Entry) string {
if xpath, ok := e.GetWhenXPath(); ok && xpath != "" {
return resolveWhenPrefixes(e.Node, xpath)
}
// Check parent's augment list: the augment may carry the when expression
// even though the individual container inside it does not.
if e.Parent == nil {
return ""
}
for _, aug := range e.Parent.Augmented {
if _, found := aug.Dir[e.Name]; !found {
continue
}
if xpath, ok := aug.GetWhenXPath(); ok && xpath != "" {
return resolveWhenPrefixes(aug.Node, xpath)
}
}
return ""
}
// resolveWhenPrefixes replaces "prefix:x" tokens in an XPath expression with
// the canonical "module:x" form using FindModuleByPrefix on the given node.
// Unknown prefixes (not imported by the node's module) are left unchanged.
func resolveWhenPrefixes(node yang.Node, xpath string) string {
return prefixInXPath.ReplaceAllStringFunc(xpath, func(m string) string {
colon := strings.IndexByte(m, ':')
prefix := m[:colon]
rest := m[colon+1:] // the single identifier-start character
mod := yang.FindModuleByPrefix(node, prefix)
if mod == nil {
return m
}
modName := mod.Name
if mod.BelongsTo != nil {
modName = mod.BelongsTo.Name
}
return modName + ":" + rest
})
}
// entryKind maps a goyang Entry to a kind string.
func entryKind(e *yang.Entry) string {
switch {
case e.IsLeaf():
return "leaf"
case e.IsLeafList():
return "leaf-list"
case e.IsList():
return "list"
case e.IsContainer():
return "container"
case e.IsChoice():
return "choice"
case e.IsCase():
return "case"
case e.Kind == yang.AnyDataEntry:
return "anydata"
case e.Kind == yang.AnyXMLEntry:
return "anyxml"
case e.RPC != nil:
return "rpc"
default:
return "unknown"
}
}
// yangTypeInfo builds a TypeInfo from a leaf's YangType.
func yangTypeInfo(e *yang.Entry) *TypeInfo {
t := e.Type
if t == nil {
return nil
}
info := &TypeInfo{
Kind: yang.TypeKindToName[t.Kind],
}
switch t.Kind {
case yang.Yenum:
if t.Enum != nil {
info.Enums = t.Enum.Names()
sort.Strings(info.Enums)
}
case yang.Yidentityref:
if t.IdentityBase != nil {
for _, v := range t.IdentityBase.Values {
name := v.Name
if root := yang.RootNode(v); root != nil {
modName := root.Name
if root.Kind() == "submodule" && root.BelongsTo != nil {
modName = root.BelongsTo.Name
}
name = modName + ":" + v.Name
}
info.Identities = append(info.Identities, name)
}
sort.Strings(info.Identities)
}
case yang.Yleafref:
info.Leafref = canonicalizeLeafrefPath(e.Node, t.Path)
info.LeafrefSibling = leafrefSiblingFilter(e)
case yang.Yunion:
for _, sub := range t.Type {
if sub.Kind == yang.Yenum && sub.Enum != nil {
info.Enums = append(info.Enums, sub.Enum.Names()...)
}
}
if len(info.Enums) > 0 {
sort.Strings(info.Enums)
}
}
if len(t.Pattern) > 0 {
info.Pattern = strings.Join(t.Pattern, "|")
}
if len(t.Range) > 0 {
info.Range = t.Range.String()
}
return info
}
// splitPrefix splits "module:name" into ("module", "name").
// If there is no prefix, it returns ("", name).
func splitPrefix(s string) (prefix, name string) {
if i := strings.Index(s, ":"); i >= 0 {
return s[:i], s[i+1:]
}
return "", s
}
// sortedEntries returns the entries in a Dir map sorted by name.
func sortedEntries(dir map[string]*yang.Entry) []*yang.Entry {
keys := make([]string, 0, len(dir))
for k := range dir {
keys = append(keys, k)
}
sort.Strings(keys)
result := make([]*yang.Entry, 0, len(keys))
for _, k := range keys {
result = append(result, dir[k])
}
return result
}