mirror of
https://github.com/kernelkit/infix.git
synced 2026-07-22 01:13:00 +02:00
Merge pull request #1457 from kernelkit/web
This commit is contained in:
@@ -0,0 +1,20 @@
|
||||
name: 'Setup MkDocs'
|
||||
description: "Install MkDocs and the plugins required to build the Infix User's Guide (post-build.sh bundles it into WebUI images)."
|
||||
|
||||
# Mirrors the dependency set in .github/workflows/docs.yml. Kept in-repo
|
||||
# (rather than an external action) so any checkout of a commit needing
|
||||
# docs-in-image can reproduce it without an out-of-tree dependency.
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- name: Install MkDocs and plugins
|
||||
shell: bash
|
||||
run: |
|
||||
command -v pipx >/dev/null 2>&1 || python3 -m pip install --user pipx
|
||||
python3 -m pipx install --force mkdocs
|
||||
python3 -m pipx inject mkdocs \
|
||||
mkdocs-material pymdown-extensions mkdocs-callouts mike mkdocs-to-pdf mkdocs-glightbox
|
||||
# Make the pipx-installed mkdocs visible to later steps (the build,
|
||||
# where post-build.sh invokes it).
|
||||
python3 -m pipx environment --value PIPX_BIN_DIR >> "$GITHUB_PATH" 2>/dev/null \
|
||||
|| echo "$HOME/.local/bin" >> "$GITHUB_PATH"
|
||||
@@ -45,6 +45,9 @@ jobs:
|
||||
target: ${{ matrix.target }}
|
||||
enabled: ${{ inputs.use_cache }}
|
||||
|
||||
# WebUI images bundle the mkdocs User's Guide via post-build.sh.
|
||||
- uses: ./.github/actions/setup-mkdocs
|
||||
|
||||
- name: Configure & Build
|
||||
env:
|
||||
INFIX_RELEASE: ${{ steps.vars.outputs.ver }}
|
||||
|
||||
@@ -124,6 +124,9 @@ jobs:
|
||||
with:
|
||||
target: ${{ env.TARGET }}
|
||||
|
||||
# WebUI images bundle the mkdocs User's Guide via post-build.sh.
|
||||
- uses: ./.github/actions/setup-mkdocs
|
||||
|
||||
- name: Configure ${{ env.TARGET }}
|
||||
run: |
|
||||
make ${{ env.TARGET }}_defconfig
|
||||
|
||||
@@ -120,3 +120,29 @@ grep -qsE '^/bin/clish$$' "$TARGET_DIR/etc/shells" \
|
||||
if [ "$BR2_PACKAGE_HOST_PYTHON_YANGDOC" = "y" ]; then
|
||||
mkyangdoc "$BINARIES_DIR/yangdoc.html"
|
||||
fi
|
||||
|
||||
# Bundle the mkdocs User's Guide into the rootfs, served by the WebUI's
|
||||
# nginx at /guide/. Only when the WebUI is present (nothing serves it
|
||||
# otherwise, and it keeps minimal images small) and mkdocs is on the build
|
||||
# host. Best-effort: a failed build warns but does not abort the image
|
||||
# build, and the WebUI hides its User Guide entry when the docs are absent.
|
||||
mkuserguide()
|
||||
{
|
||||
local cfg dst
|
||||
cfg="$(readlink -f "$common/../..")/mkdocs.yml"
|
||||
dst="$TARGET_DIR/var/www/guide"
|
||||
|
||||
if ! command -v mkdocs >/dev/null 2>&1; then
|
||||
ixmsg "mkdocs not found, skipping User's Guide bundling"
|
||||
return
|
||||
fi
|
||||
ixmsg "Building User's Guide into $dst"
|
||||
if ! mkdocs build -f "$cfg" -d "$dst" --clean --quiet; then
|
||||
ixmsg "WARNING: mkdocs build failed, shipping without on-device User's Guide"
|
||||
rm -rf "$dst"
|
||||
fi
|
||||
}
|
||||
|
||||
if [ "$BR2_PACKAGE_WEBUI" = "y" ]; then
|
||||
mkuserguide
|
||||
fi
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
RESTCONF_URL=https://127.0.0.1/restconf
|
||||
INSECURE_TLS=1
|
||||
# Spool firmware uploads (and any other temp files) to persistent storage
|
||||
# instead of the RAM-backed /tmp to enable push-upgrades on low-memory systems.
|
||||
TMPDIR=/var/tmp
|
||||
@@ -0,0 +1 @@
|
||||
../restconf.app
|
||||
@@ -0,0 +1,3 @@
|
||||
allow 127.0.0.1;
|
||||
allow ::1;
|
||||
deny all;
|
||||
@@ -0,0 +1 @@
|
||||
restconf-access-local.conf
|
||||
@@ -1,5 +1,6 @@
|
||||
# /telemetry/optics is for streaming (not used atm)
|
||||
location ~ ^/(restconf|yang|.well-known)/ {
|
||||
include /etc/nginx/restconf-access.conf;
|
||||
grpc_pass grpc://[::1]:10080;
|
||||
grpc_set_header Host $host;
|
||||
grpc_set_header X-Real-IP $remote_addr;
|
||||
|
||||
@@ -175,7 +175,6 @@ BR2_PACKAGE_FIREWALL=y
|
||||
BR2_PACKAGE_IITO=y
|
||||
BR2_PACKAGE_KEYACK=y
|
||||
BR2_PACKAGE_KLISH_PLUGIN_INFIX=y
|
||||
BR2_PACKAGE_LANDING=y
|
||||
BR2_PACKAGE_LOWDOWN=y
|
||||
BR2_PACKAGE_MCD=y
|
||||
BR2_PACKAGE_MDNS_ALIAS=y
|
||||
@@ -187,6 +186,8 @@ BR2_PACKAGE_PODMAN_DRIVER_DEVICEMAPPER=y
|
||||
BR2_PACKAGE_PODMAN_DRIVER_VFS=y
|
||||
BR2_PACKAGE_TETRIS=y
|
||||
BR2_PACKAGE_ROUSETTE=y
|
||||
# BR2_PACKAGE_LANDING is not set
|
||||
BR2_PACKAGE_WEBUI=y
|
||||
BR2_PACKAGE_RAUC_INSTALLATION_STATUS=y
|
||||
BR2_PACKAGE_HOST_PYTHON_YANGDOC=y
|
||||
BR2_PACKAGE_PCIUTILS=y
|
||||
|
||||
@@ -144,6 +144,7 @@ BR2_PACKAGE_FIREWALL=y
|
||||
BR2_PACKAGE_IITO=y
|
||||
BR2_PACKAGE_KEYACK=y
|
||||
BR2_PACKAGE_KLISH_PLUGIN_INFIX=y
|
||||
BR2_PACKAGE_LANDING=y
|
||||
BR2_PACKAGE_LOWDOWN=y
|
||||
BR2_PACKAGE_MCD=y
|
||||
BR2_PACKAGE_MDNS_ALIAS=y
|
||||
|
||||
@@ -162,7 +162,6 @@ BR2_PACKAGE_FIREWALL=y
|
||||
BR2_PACKAGE_IITO=y
|
||||
BR2_PACKAGE_KEYACK=y
|
||||
BR2_PACKAGE_KLISH_PLUGIN_INFIX=y
|
||||
BR2_PACKAGE_LANDING=y
|
||||
BR2_PACKAGE_LOWDOWN=y
|
||||
BR2_PACKAGE_MCD=y
|
||||
BR2_PACKAGE_MDNS_ALIAS=y
|
||||
@@ -170,6 +169,8 @@ BR2_PACKAGE_NETBROWSE=y
|
||||
BR2_PACKAGE_ONIEPROM=y
|
||||
BR2_PACKAGE_TETRIS=y
|
||||
BR2_PACKAGE_ROUSETTE=y
|
||||
# BR2_PACKAGE_LANDING is not set
|
||||
BR2_PACKAGE_WEBUI=y
|
||||
BR2_PACKAGE_RAUC_INSTALLATION_STATUS=y
|
||||
BR2_PACKAGE_HOST_PYTHON_YANGDOC=y
|
||||
IMAGE_ITB_AUX=y
|
||||
|
||||
@@ -142,6 +142,7 @@ BR2_PACKAGE_FIREWALL=y
|
||||
BR2_PACKAGE_IITO=y
|
||||
BR2_PACKAGE_KEYACK=y
|
||||
BR2_PACKAGE_KLISH_PLUGIN_INFIX=y
|
||||
BR2_PACKAGE_LANDING=y
|
||||
BR2_PACKAGE_LOWDOWN=y
|
||||
BR2_PACKAGE_MCD=y
|
||||
BR2_PACKAGE_MDNS_ALIAS=y
|
||||
|
||||
@@ -194,7 +194,6 @@ BR2_PACKAGE_FIREWALL=y
|
||||
BR2_PACKAGE_IITO=y
|
||||
BR2_PACKAGE_KEYACK=y
|
||||
BR2_PACKAGE_KLISH_PLUGIN_INFIX=y
|
||||
BR2_PACKAGE_LANDING=y
|
||||
BR2_PACKAGE_LOWDOWN=y
|
||||
BR2_PACKAGE_MCD=y
|
||||
BR2_PACKAGE_MDNS_ALIAS=y
|
||||
@@ -206,6 +205,8 @@ BR2_PACKAGE_PODMAN_DRIVER_DEVICEMAPPER=y
|
||||
BR2_PACKAGE_PODMAN_DRIVER_VFS=y
|
||||
BR2_PACKAGE_TETRIS=y
|
||||
BR2_PACKAGE_ROUSETTE=y
|
||||
# BR2_PACKAGE_LANDING is not set
|
||||
BR2_PACKAGE_WEBUI=y
|
||||
BR2_PACKAGE_RAUC_INSTALLATION_STATUS=y
|
||||
BR2_PACKAGE_HOST_PYTHON_YANGDOC=y
|
||||
BR2_PACKAGE_PCIUTILS=y
|
||||
|
||||
@@ -169,7 +169,6 @@ BR2_PACKAGE_FIREWALL=y
|
||||
BR2_PACKAGE_IITO=y
|
||||
BR2_PACKAGE_KEYACK=y
|
||||
BR2_PACKAGE_KLISH_PLUGIN_INFIX=y
|
||||
BR2_PACKAGE_LANDING=y
|
||||
BR2_PACKAGE_LOWDOWN=y
|
||||
BR2_PACKAGE_MCD=y
|
||||
BR2_PACKAGE_MDNS_ALIAS=y
|
||||
@@ -181,6 +180,8 @@ BR2_PACKAGE_PODMAN_DRIVER_DEVICEMAPPER=y
|
||||
BR2_PACKAGE_PODMAN_DRIVER_VFS=y
|
||||
BR2_PACKAGE_TETRIS=y
|
||||
BR2_PACKAGE_ROUSETTE=y
|
||||
# BR2_PACKAGE_LANDING is not set
|
||||
BR2_PACKAGE_WEBUI=y
|
||||
BR2_PACKAGE_RAUC_INSTALLATION_STATUS=y
|
||||
BR2_PACKAGE_HOST_PYTHON_YANGDOC=y
|
||||
BR2_PACKAGE_PCIUTILS=y
|
||||
|
||||
@@ -141,6 +141,7 @@ BR2_PACKAGE_FIREWALL=y
|
||||
BR2_PACKAGE_IITO=y
|
||||
BR2_PACKAGE_KEYACK=y
|
||||
BR2_PACKAGE_KLISH_PLUGIN_INFIX=y
|
||||
BR2_PACKAGE_LANDING=y
|
||||
BR2_PACKAGE_LOWDOWN=y
|
||||
BR2_PACKAGE_MCD=y
|
||||
BR2_PACKAGE_MDNS_ALIAS=y
|
||||
|
||||
+16
-7
@@ -4,11 +4,15 @@ Change Log
|
||||
All notable changes to the project are documented in this file.
|
||||
|
||||
[v26.06.0][UNRELEASED]
|
||||
--------------
|
||||
-------------------------
|
||||
|
||||
### Changes
|
||||
|
||||
- Upgrade Linux kernel to 6.18.35 (LTS)
|
||||
- Add basic web interface: static status pages and a tree view of operational
|
||||
status. Curated configuration pages for some common tasks and a YANG tree
|
||||
editor for the rest. Also includes a maintenance section for firmware
|
||||
upgrade, backup & restore, and more
|
||||
- Add Wi-Fi roaming for fast, seamless handoff between access points that
|
||||
share an SSID: 802.11k, 802.11v and 802.11r (over-the-air FT). See the
|
||||
[Wi-Fi][wifi] guide for details
|
||||
@@ -21,11 +25,15 @@ All notable changes to the project are documented in this file.
|
||||
|
||||
### Fixes
|
||||
|
||||
- Firewall masquerade no longer enables the global IPv4/IPv6 forwarding
|
||||
sysctls. You must now enable IP forwarding explicitly on the interfaces
|
||||
that should route traffic; enabling NAT alone is no longer enough
|
||||
- Enabling IP masquerading in the firewall no longer enables IP forwarding on
|
||||
all interfaces. This has been an issue ever since the firewall support was
|
||||
introduced in v25.10.0
|
||||
- Fix file permission regression in `/cfg/startup-config.cfg`, causing the
|
||||
default `admin` user no permission to read or write the file from shell
|
||||
- Fix admin url shown for HTTP/HTTPS links in <https://network.local> browser,
|
||||
used pre-conflict resolution hostname.local, instead of hostname-2.local
|
||||
|
||||
[wifi]: wifi.md
|
||||
[wifi]: https://www.kernelkit.org/infix/latest/wifi/
|
||||
|
||||
[v26.05.0][] - 2026-05-29
|
||||
-------------------------
|
||||
@@ -84,9 +92,9 @@ All notable changes to the project are documented in this file.
|
||||
show`, which were not expanded, so `show interface` and other operational
|
||||
reads failed. Ranges are now expanded and listed correctly
|
||||
|
||||
[ethernet]: ethernet.md#restricting-advertised-link-modes
|
||||
[ethernet]: https://www.kernelkit.org/infix/latest/ethernet/#restricting-advertised-link-modes
|
||||
[BPI-R3]: https://docs.banana-pi.org/en/BPI-R3/BananaPi_BPI-R3
|
||||
[AcerConnectVero]: ../board/aarch64/acer-connect-vero-w6m/
|
||||
[AcerConnectVero]: https://github.com/kernelkit/infix/tree/main/board/aarch64/acer-connect-vero-w6m
|
||||
|
||||
[v26.04.0][] - 2026-04-30
|
||||
-------------------------
|
||||
@@ -2160,6 +2168,7 @@ Supported YANG models in addition to those used by sysrepo and netopeer:
|
||||
|
||||
[buildroot]: https://buildroot.org/
|
||||
[UNRELEASED]: https://github.com/kernelkit/infix/compare/v26.05.0...HEAD
|
||||
[v26.06.0]: https://github.com/kernelkit/infix/compare/v26.05.0...v26.06.0
|
||||
[v26.05.0]: https://github.com/kernelkit/infix/compare/v26.04.0...v26.05.0
|
||||
[v26.04.0]: https://github.com/kernelkit/infix/compare/v26.03.0...v26.04.0
|
||||
[v26.03.0]: https://github.com/kernelkit/infix/compare/v26.02.0...v26.03.0
|
||||
|
||||
+13
-5
@@ -122,13 +122,21 @@ recommend using `pipx` to install the necessary tooling:
|
||||
```bash
|
||||
$ sudo apt install pipx
|
||||
$ pipx install mkdocs
|
||||
$ pipx inject mkdocs mkdocs-material pymdown-extensions mkdocs-callouts mike mkdocs-to-pdf
|
||||
$ pipx inject mkdocs mkdocs-material pymdown-extensions mkdocs-callouts mike mkdocs-to-pdf mkdocs-glightbox
|
||||
```
|
||||
|
||||
The last two packages, `mike` and `mkdocs-to-pdf`, are used for online
|
||||
versioning and PDF generation by GitHub Actions, but since they are in
|
||||
the `mkdocs.yml` file, everyone who wants to preview the documentation
|
||||
have to install all the tooling.
|
||||
The `mike` and `mkdocs-to-pdf` packages are used for online versioning
|
||||
and PDF generation by GitHub Actions, but since every plugin is listed
|
||||
in `mkdocs.yml`, anyone who wants to preview the documentation has to
|
||||
install all the tooling.
|
||||
|
||||
> [!IMPORTANT]
|
||||
> MkDocs is also required to **build a WebUI image**. The build bundles
|
||||
> the User's Guide into the image (served on-device at `/guide/`), so
|
||||
> `make` runs `mkdocs build` from `post-build.sh` when the `webui`
|
||||
> package is selected. If MkDocs is missing the build still succeeds,
|
||||
> but the image ships without the on-device guide. Minimal images and
|
||||
> any build without the `webui` package skip this step entirely.
|
||||
|
||||
Preview with:
|
||||
|
||||
|
||||
@@ -42,6 +42,7 @@ source "$BR2_EXTERNAL_INFIX_PATH/package/tetris/Config.in"
|
||||
source "$BR2_EXTERNAL_INFIX_PATH/package/libyang-cpp/Config.in"
|
||||
source "$BR2_EXTERNAL_INFIX_PATH/package/sysrepo-cpp/Config.in"
|
||||
source "$BR2_EXTERNAL_INFIX_PATH/package/rousette/Config.in"
|
||||
source "$BR2_EXTERNAL_INFIX_PATH/package/webui/Config.in"
|
||||
source "$BR2_EXTERNAL_INFIX_PATH/package/nghttp2-asio/Config.in"
|
||||
source "$BR2_EXTERNAL_INFIX_PATH/package/date-cpp/Config.in"
|
||||
source "$BR2_EXTERNAL_INFIX_PATH/package/rauc-installation-status/Config.in"
|
||||
|
||||
@@ -14,6 +14,8 @@ define LANDING_INSTALL_TARGET_CMDS
|
||||
mkdir -p $(TARGET_DIR)/usr/html/
|
||||
cp $(@D)/*.html $(TARGET_DIR)/usr/html/
|
||||
cp $(@D)/*.png $(TARGET_DIR)/usr/html/
|
||||
$(INSTALL) -D -m 0644 $(LANDING_PKGDIR)/default.conf \
|
||||
$(TARGET_DIR)/etc/nginx/available/default.conf
|
||||
endef
|
||||
|
||||
$(eval $(generic-package))
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Loading…</title>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="refresh" content="3">
|
||||
<style>
|
||||
html { color-scheme: light dark; font-family: Tahoma, Verdana, Arial, sans-serif; }
|
||||
body { max-width: 32em; margin: 4em auto; text-align: center; }
|
||||
h1 { font-weight: 500; margin-bottom: 0.5em; }
|
||||
p { color: #888; margin: 0.5em 0; }
|
||||
.spinner {
|
||||
display: inline-block;
|
||||
width: 1.5em;
|
||||
height: 1.5em;
|
||||
margin-top: 1em;
|
||||
border: 3px solid currentColor;
|
||||
border-top-color: transparent;
|
||||
border-radius: 50%;
|
||||
animation: rot 1s linear infinite;
|
||||
}
|
||||
@keyframes rot { to { transform: rotate(360deg); } }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Loading…</h1>
|
||||
<p>The device is finishing its startup. This page refreshes automatically.</p>
|
||||
<p><span class="spinner" aria-hidden="true"></span></p>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,9 @@
|
||||
config BR2_PACKAGE_WEBUI
|
||||
bool "webui"
|
||||
depends on BR2_PACKAGE_HOST_GO_TARGET_ARCH_SUPPORTS
|
||||
depends on BR2_PACKAGE_ROUSETTE
|
||||
depends on !BR2_PACKAGE_LANDING
|
||||
help
|
||||
Web management interface for Infix, a Go+HTMX application
|
||||
that provides browser-based configuration and monitoring
|
||||
via RESTCONF.
|
||||
@@ -0,0 +1,44 @@
|
||||
# Throttle POST /login so a brute-force attempt can't pin the box on
|
||||
# the bcrypt-shaped credential check inside rousette/PAM. GET is left
|
||||
# unmetered: page loads after a 401-driven HX-Redirect shouldn't eat
|
||||
# into the budget.
|
||||
#
|
||||
# Zone is 32k (~500 IPs) — the minimum nginx accepts via shared-memory
|
||||
# allocation. With the cache full an attacker rotating source addresses
|
||||
# still tops out at ~2500 attempts/min total, all serialised through
|
||||
# bcrypt one at a time.
|
||||
#
|
||||
# 429 instead of the default 503 keeps the rate-limit response out of
|
||||
# the /50x.html error_page rewrite below (which auto-refreshes — would
|
||||
# loop a throttled client straight back to /login).
|
||||
map $request_method $webui_login_key {
|
||||
POST $binary_remote_addr;
|
||||
default "";
|
||||
}
|
||||
limit_req_zone $webui_login_key zone=webui_login:32k rate=5r/m;
|
||||
limit_req_status 429;
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
server_name _;
|
||||
return 301 https://$host$request_uri;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 443 ssl;
|
||||
listen [::]:443 ssl;
|
||||
server_name _;
|
||||
include ssl.conf;
|
||||
|
||||
# 404 also points at /50x.html: the page is a "Loading…" screen
|
||||
# with a meta-refresh, so the early-boot window where the Go
|
||||
# backend isn't up yet (and any other transient 404 / 5xx) self-
|
||||
# recovers as soon as upstream comes back.
|
||||
error_page 404 500 502 503 504 /50x.html;
|
||||
location = /50x.html {
|
||||
root html;
|
||||
}
|
||||
|
||||
include /etc/nginx/app/*.conf;
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
# Shared proxy-pass shape for the webui upstream. Nested locations that
|
||||
# declare their own proxy_* directives don't inherit from the outer block,
|
||||
# so each location includes this file rather than relying on inheritance.
|
||||
|
||||
proxy_pass http://127.0.0.1:10000;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_redirect off;
|
||||
@@ -0,0 +1,72 @@
|
||||
# Must be at server scope, not on an inner location: client_max_body_size
|
||||
# does not inherit into a nested location that declares its own proxy_pass,
|
||||
# and the http-level 1m default would silently apply and reject bundle
|
||||
# uploads with 413.
|
||||
client_max_body_size 256m;
|
||||
|
||||
location / {
|
||||
include /etc/nginx/webui-proxy.conf;
|
||||
}
|
||||
|
||||
# burst=3 nodelay: three POSTs land back-to-back; the next needs a
|
||||
# fresh token (~12 s at 5r/m) or 429s.
|
||||
location = /login {
|
||||
limit_req zone=webui_login burst=3 nodelay;
|
||||
include /etc/nginx/webui-proxy.conf;
|
||||
}
|
||||
|
||||
# Keep proxy_request_buffering on (the default). Streaming the body races
|
||||
# the Go handler's response close, producing RST instead of FIN at
|
||||
# Content-Length and a client-visible 502. nginx spools the body to
|
||||
# /var/cache/nginx/client-body during the upload instead.
|
||||
location = /software/upload {
|
||||
proxy_read_timeout 600s;
|
||||
include /etc/nginx/webui-proxy.conf;
|
||||
}
|
||||
|
||||
# SSE progress stream: RAUC's Progress D-Bus property doesn't change while
|
||||
# it's writing a slot, so the upstream goes minutes without a frame. The
|
||||
# default 60 s proxy_read_timeout closes the stream and the browser sees
|
||||
# a transient error. Raise to cover a slow image write end-to-end.
|
||||
location = /software/progress {
|
||||
proxy_read_timeout 1800s;
|
||||
proxy_buffering off;
|
||||
include /etc/nginx/webui-proxy.conf;
|
||||
}
|
||||
|
||||
# SSE live-tail stream for Maintenance > Logs. Same buffering / timeout
|
||||
# concerns as the software progress stream: nginx must not buffer the
|
||||
# event frames, and the connection has to stay open through quiet logs.
|
||||
# The Go side sends a 15 s heartbeat, so 600 s of read timeout is plenty
|
||||
# of safety margin.
|
||||
location ~ ^/maintenance/logs/[^/]+/tail$ {
|
||||
proxy_read_timeout 600s;
|
||||
proxy_buffering off;
|
||||
include /etc/nginx/webui-proxy.conf;
|
||||
}
|
||||
|
||||
# Support bundle collection runs `support collect`, which emits nothing
|
||||
# until it finishes (~50 s) and then sends the whole archive at once.
|
||||
# The upstream is silent for that window, so the default 60 s
|
||||
# proxy_read_timeout would 504 just as the bundle is ready. Buffering
|
||||
# stays on — it's a plain file download, not a stream.
|
||||
location = /maintenance/support-bundle {
|
||||
proxy_read_timeout 300s;
|
||||
include /etc/nginx/webui-proxy.conf;
|
||||
}
|
||||
|
||||
# On-device User's Guide: the mkdocs site bundled into the rootfs at
|
||||
# /var/www/guide by post-build.sh. Served as static files (no upstream
|
||||
# call), public like the published guide. Present only when the build
|
||||
# host had mkdocs; the WebUI hides its User Guide entry when it's absent.
|
||||
location /guide/ {
|
||||
alias /var/www/guide/;
|
||||
index index.html;
|
||||
}
|
||||
|
||||
# Liveness probe — nginx-only, no upstream call. Used by the watchdog
|
||||
# div in base.html and the reboot-overlay poller.
|
||||
location = /device-status {
|
||||
access_log off;
|
||||
return 204;
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
################################################################################
|
||||
#
|
||||
# webui
|
||||
#
|
||||
################################################################################
|
||||
|
||||
WEBUI_VERSION = 1.0
|
||||
WEBUI_SITE_METHOD = local
|
||||
WEBUI_SITE = $(BR2_EXTERNAL_INFIX_PATH)/src/webui
|
||||
WEBUI_GOMOD = infix/webui
|
||||
WEBUI_LICENSE = MIT
|
||||
WEBUI_LICENSE_FILES = LICENSE
|
||||
WEBUI_REDISTRIBUTE = NO
|
||||
|
||||
define WEBUI_INSTALL_EXTRA
|
||||
$(INSTALL) -D -m 0644 $(WEBUI_PKGDIR)/webui.svc \
|
||||
$(FINIT_D)/available/webui.conf
|
||||
$(INSTALL) -D -m 0644 $(WEBUI_PKGDIR)/webui.conf \
|
||||
$(TARGET_DIR)/etc/nginx/app/webui.conf
|
||||
$(INSTALL) -D -m 0644 $(WEBUI_PKGDIR)/webui-proxy.conf \
|
||||
$(TARGET_DIR)/etc/nginx/webui-proxy.conf
|
||||
$(INSTALL) -D -m 0644 $(WEBUI_PKGDIR)/default.conf \
|
||||
$(TARGET_DIR)/etc/nginx/available/default.conf
|
||||
$(INSTALL) -D -m 0644 $(WEBUI_PKGDIR)/50x.html \
|
||||
$(TARGET_DIR)/usr/html/50x.html
|
||||
endef
|
||||
WEBUI_POST_INSTALL_TARGET_HOOKS += WEBUI_INSTALL_EXTRA
|
||||
|
||||
$(eval $(golang-package))
|
||||
@@ -0,0 +1,3 @@
|
||||
service <!> name:webui log:prio:daemon.info,tag:webui \
|
||||
[2345] env:-/etc/default/webui webui -listen 127.0.0.1:10000 \
|
||||
-- Web management interface
|
||||
+8
-1
@@ -138,9 +138,16 @@ static void set_owner(const char *fn, const char *user)
|
||||
if (chown(fn, -1, gid) && errno != EPERM) {
|
||||
const struct group *gr = getgrgid(gid);
|
||||
|
||||
warn("setting group owner %s (%d) on %s",
|
||||
warn("failed setting group owner %s (%d) on %s",
|
||||
gr ? gr->gr_name : "<unknown>", gid, fn);
|
||||
}
|
||||
|
||||
/* Make sure the group we just set can actually read/write the file.
|
||||
* umask alone can't: the datastore export goes through a 0600 mkstemp
|
||||
* temp and cp(1) propagates that mode to the destination.
|
||||
*/
|
||||
if (chmod(fn, 0660) && errno != EPERM)
|
||||
warn("failed setting mode 0660 on %s", fn);
|
||||
}
|
||||
|
||||
static const char *infix_ds(const char *text, const struct infix_ds **ds)
|
||||
|
||||
@@ -19,6 +19,8 @@
|
||||
#define GENERATE_STRING(STRING) #STRING,
|
||||
|
||||
#define NGINX_SSL_CONF "/etc/nginx/ssl.conf"
|
||||
#define NGINX_RESTCONF_ACCESS "/etc/nginx/restconf-access.conf"
|
||||
#define NGINX_RESTCONF_ACCESS_LOCAL "restconf-access-local.conf"
|
||||
#define AVAHI_SVC_PATH "/etc/avahi/services"
|
||||
|
||||
#define LLDP_CONFIG "/etc/lldpd.d/confd.conf"
|
||||
@@ -557,7 +559,30 @@ static int restconf_change(sr_session_ctx_t *session, struct lyd_node *config, s
|
||||
|
||||
ena = lydx_is_enabled(srv, "enabled") &&
|
||||
lydx_is_enabled(lydx_get_xpathf(config, WEB_XPATH), "enabled");
|
||||
svc_enable(ena, restconf, "restconf");
|
||||
|
||||
/*
|
||||
* restconf.app is permanently installed in nginx/app/ so rousette is
|
||||
* always reachable from loopback (required by the WebUI). External
|
||||
* access is gated by an `include` of restconf-access.conf — empty
|
||||
* means no allow/deny rules apply and nginx's default of `allow all`
|
||||
* takes effect; populated means loopback-only.
|
||||
*
|
||||
* The shipped state is a symlink to restconf-access-local.conf (which
|
||||
* holds the loopback rules), so the rules are defined in exactly one
|
||||
* place. erase() before re-creating because fopen("w") on a symlink
|
||||
* follows the link and would clobber the template; the empty-file
|
||||
* path needs a real file, and the loopback path needs a fresh
|
||||
* symlink that may have been replaced on a previous transition.
|
||||
*/
|
||||
erase(NGINX_RESTCONF_ACCESS);
|
||||
if (ena) {
|
||||
FILE *fp = fopen(NGINX_RESTCONF_ACCESS, "w");
|
||||
if (fp)
|
||||
fclose(fp);
|
||||
} else if (symlink(NGINX_RESTCONF_ACCESS_LOCAL, NGINX_RESTCONF_ACCESS)) {
|
||||
ERRNO("failed restoring %s", NGINX_RESTCONF_ACCESS);
|
||||
}
|
||||
mdns_records(ena ? MDNS_ADD : MDNS_DELETE, restconf);
|
||||
finit_reload("nginx");
|
||||
|
||||
return put(cfg);
|
||||
@@ -703,13 +728,16 @@ static int web_change(sr_session_ctx_t *session, struct lyd_node *config, struct
|
||||
|
||||
/* Web master on/off: propagate to nginx and all sub-services */
|
||||
if (lydx_get_xpathf(diff, WEB_XPATH "/enabled")) {
|
||||
int rc_ena = ena && lydx_is_enabled(lydx_get_xpathf(config, WEB_RESTCONF_XPATH), "enabled");
|
||||
int nb_ena = ena && lydx_is_enabled(lydx_get_xpathf(config, WEB_NETBROWSE_XPATH), "enabled");
|
||||
|
||||
svc_enable(ena && lydx_is_enabled(lydx_get_xpathf(config, WEB_CONSOLE_XPATH), "enabled"),
|
||||
ttyd, "ttyd");
|
||||
svc_enable(nb_ena, netbrowse, "netbrowse");
|
||||
svc_enable(ena && lydx_is_enabled(lydx_get_xpathf(config, WEB_RESTCONF_XPATH), "enabled"),
|
||||
restconf, "restconf");
|
||||
/* Rousette follows web/enabled; external access is gated separately via restconf/enabled */
|
||||
ena ? finit_enable("restconf") : finit_disable("restconf");
|
||||
ena ? finit_enable("webui") : finit_disable("webui");
|
||||
mdns_records(rc_ena ? MDNS_ADD : MDNS_DELETE, restconf);
|
||||
svc_enable(ena, web, "nginx");
|
||||
mdns_alias_conf(nb_ena);
|
||||
finit_reload("mdns-alias");
|
||||
|
||||
+27
-2
@@ -5,6 +5,7 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/url"
|
||||
"os/exec"
|
||||
"sort"
|
||||
"strings"
|
||||
@@ -221,7 +222,7 @@ func scan() map[string]Host {
|
||||
|
||||
var url string
|
||||
if meta.adminurl != "" {
|
||||
url = meta.adminurl
|
||||
url = rebaseURLHost(meta.adminurl, link)
|
||||
} else if info.urlTemplate != "" {
|
||||
url = strings.NewReplacer(
|
||||
"{address}", address,
|
||||
@@ -340,7 +341,7 @@ func parseOperational(root *opRoot) map[string]Host {
|
||||
|
||||
var url string
|
||||
if meta.adminurl != "" {
|
||||
url = meta.adminurl
|
||||
url = rebaseURLHost(meta.adminurl, link)
|
||||
} else if info.urlTemplate != "" {
|
||||
url = strings.NewReplacer(
|
||||
"{address}", addr,
|
||||
@@ -398,6 +399,30 @@ func scanAuto() map[string]Host {
|
||||
return parseOperational(root)
|
||||
}
|
||||
|
||||
// rebaseURLHost rewrites the host of raw to host, keeping scheme, port, and
|
||||
// path. It corrects stale adminurl TXT records: avahi resolves hostname
|
||||
// conflicts at runtime by appending -2, -3, … but the advertiser writes the
|
||||
// adminurl with its configured hostname and never updates it, so a device
|
||||
// renamed infix-2.local still advertises adminurl=http://infix.local — which
|
||||
// resolves to the *other* device. The mDNS-resolved host is authoritative.
|
||||
// A no-op when raw is unparseable, hostless, or already points at host (the
|
||||
// common single-device case).
|
||||
func rebaseURLHost(raw, host string) string {
|
||||
if host == "" {
|
||||
return raw
|
||||
}
|
||||
u, err := url.Parse(raw)
|
||||
if err != nil || u.Host == "" {
|
||||
return raw
|
||||
}
|
||||
if port := u.Port(); port != "" {
|
||||
u.Host = host + ":" + port
|
||||
} else {
|
||||
u.Host = host
|
||||
}
|
||||
return u.String()
|
||||
}
|
||||
|
||||
// decode handles avahi's DNS-SD escape sequences in service names:
|
||||
// - \DDD decimal value 0-255, e.g. \058 → ':'
|
||||
// - \X literal character X, e.g. \. → '.'
|
||||
|
||||
@@ -47,10 +47,10 @@ int main(int argc, char **argv)
|
||||
json_object_set_new(json, "last-error", json_string(strval));
|
||||
props = rauc_installer_get_progress(rauc);
|
||||
if(props) {
|
||||
GVariant *val;
|
||||
gint32 pct;
|
||||
progress = json_object();
|
||||
g_variant_get(props, "(@isi)", &val, &strval, NULL);
|
||||
json_object_set_new(progress, "percentage", json_string(g_variant_print(val, FALSE)));
|
||||
g_variant_get(props, "(isi)", &pct, &strval, NULL);
|
||||
json_object_set_new(progress, "percentage", json_integer(pct));
|
||||
json_object_set_new(progress, "message", json_string(strval));
|
||||
json_object_set_new(json, "progress", progress);
|
||||
}
|
||||
|
||||
@@ -203,10 +203,11 @@ def link(ifname):
|
||||
if supported:
|
||||
eth["infix-ethernet-interface:supported-pmd-types"] = supported
|
||||
|
||||
# Suppress when advertised == supported — that's the default
|
||||
# "advertise everything" state with no diagnostic value.
|
||||
# Always emit when the PHY is advertising something — even when it equals
|
||||
# the full supported set, displaying the list alongside supported is
|
||||
# useful for "show interfaces detail" and the WebUI ethernet card.
|
||||
advertised = _ethtool_modes_to_pmd_identities(data.get("advertised-link-modes"))
|
||||
if advertised and set(advertised) != set(supported):
|
||||
if advertised:
|
||||
eth["auto-negotiation"]["infix-ethernet-interface:advertised-pmd-types"] = advertised
|
||||
|
||||
speed_bps = None
|
||||
|
||||
@@ -257,9 +257,16 @@ def add_software(out):
|
||||
insert(out, "infix-system:software", software)
|
||||
|
||||
def add_hostname(out):
|
||||
hostname = HOST.run(tuple(["hostname"]))
|
||||
hostname = HOST.run(tuple(["hostname"]))
|
||||
out["hostname"] = hostname.strip()
|
||||
|
||||
def add_contact_location(out):
|
||||
for name in ("contact", "location"):
|
||||
data = HOST.run_json(("copy", "running", "-x", f"/system/{name}"), {})
|
||||
val = data.get("ietf-system:system", {}).get(name)
|
||||
if val:
|
||||
out[name] = val
|
||||
|
||||
def add_timezone(out):
|
||||
path = HOST.run(tuple("realpath /etc/localtime".split()), "")
|
||||
timezone = None
|
||||
@@ -415,9 +422,14 @@ def add_resource_usage(out):
|
||||
except (FileNotFoundError, ValueError):
|
||||
pass
|
||||
|
||||
# Filesystem usage
|
||||
# Filesystem usage. /run and /tmp are tmpfs (RAM-backed) and easily the
|
||||
# scarcest writable storage on small embedded boards — surface them so
|
||||
# operators can tell when the box is about to start dropping logs or
|
||||
# failing to spool config diffs. / is the read-only rootfs (squashfs);
|
||||
# always 100 % used by design but kept in the list so a flag/limit
|
||||
# consumer can recognise it.
|
||||
filesystems = []
|
||||
for mount in ["/", "/var", "/cfg"]:
|
||||
for mount in ["/", "/var", "/cfg", "/run", "/tmp"]:
|
||||
try:
|
||||
result = HOST.run_multiline(["df", "-k", mount], [])
|
||||
if len(result) > 1:
|
||||
@@ -448,6 +460,7 @@ def operational():
|
||||
out_state = out["ietf-system:system-state"]
|
||||
out_system = out["ietf-system:system"]
|
||||
add_hostname(out_system)
|
||||
add_contact_location(out_system)
|
||||
add_users(out_system)
|
||||
add_timezone(out_system)
|
||||
add_software(out_state)
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
webui
|
||||
@@ -0,0 +1,19 @@
|
||||
Copyright (c) 2026 The KernelKit Authors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,15 @@
|
||||
BINARY = webui
|
||||
GOARCH ?= $(shell go env GOARCH)
|
||||
GOOS ?= $(shell go env GOOS)
|
||||
|
||||
build:
|
||||
CGO_ENABLED=0 GOOS=$(GOOS) GOARCH=$(GOARCH) \
|
||||
go build -ldflags="-s -w" -o $(BINARY) .
|
||||
|
||||
dev: build
|
||||
go run . --listen :10000 --insecure-tls $(ARGS)
|
||||
|
||||
clean:
|
||||
rm -f $(BINARY)
|
||||
|
||||
.PHONY: build dev clean
|
||||
@@ -0,0 +1,119 @@
|
||||
# Infix WebUI
|
||||
|
||||
A lightweight web management interface for [Infix][1] network devices,
|
||||
built with Go and [htmx][2].
|
||||
|
||||
The WebUI communicates with the device over [RESTCONF][3] (RFC 8040),
|
||||
presenting the same operational data available through the Infix CLI in
|
||||
a browser-friendly format.
|
||||
|
||||
## Features
|
||||
|
||||
- **Dashboard** -- system info, hardware, sensors, and interface summary
|
||||
with bridge member grouping
|
||||
- **Interfaces** -- list with status, addresses, and per-type detail;
|
||||
click through to a detail page with live-updating counters, WiFi
|
||||
station table, scan results, WireGuard peers, and ethernet frame
|
||||
statistics
|
||||
- **Firewall** -- zone-to-zone policy matrix
|
||||
- **Keystore** -- symmetric and asymmetric key display
|
||||
- **Firmware** -- slot overview, install from URL with live progress
|
||||
- **Reboot** -- two-phase status polling (wait down, wait up)
|
||||
- **Config download** -- startup datastore as JSON
|
||||
|
||||
|
||||
## Building
|
||||
|
||||
Requires Go 1.22 or later.
|
||||
|
||||
```sh
|
||||
make build
|
||||
```
|
||||
|
||||
Produces a statically linked `webui` binary with all templates,
|
||||
CSS, and JS embedded.
|
||||
|
||||
Cross-compile for the target:
|
||||
|
||||
```sh
|
||||
GOOS=linux GOARCH=arm64 make build
|
||||
```
|
||||
|
||||
|
||||
## Running
|
||||
|
||||
```sh
|
||||
./webui --restconf https://192.168.0.1/restconf --listen :10000
|
||||
```
|
||||
|
||||
| **Flag** | **Default** | **Description** |
|
||||
|-------------------|-----------------------------------|-------------------------------------------|
|
||||
| `--listen` | `:10000` | Address to listen on |
|
||||
| `--restconf` | `http://localhost:8080/restconf` | RESTCONF base URL of the device |
|
||||
| `--insecure-tls` | `false` | Disable TLS certificate verification |
|
||||
|
||||
The RESTCONF URL can also be set via the `RESTCONF_URL` environment
|
||||
variable.
|
||||
|
||||
|
||||
## Development
|
||||
|
||||
Point `RESTCONF_URL` at a running Infix device and start the dev
|
||||
server:
|
||||
|
||||
```sh
|
||||
make dev ARGS="--restconf https://192.168.0.1/restconf"
|
||||
```
|
||||
|
||||
This runs `go run .` on port 10000 with `--insecure-tls` already set.
|
||||
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
Browser ──htmx──▶ Go server ──RESTCONF──▶ Infix device (rousette/sysrepo)
|
||||
```
|
||||
|
||||
- **Single binary** -- templates, CSS, JS, and images are embedded via
|
||||
`go:embed`
|
||||
- **Server-side rendering** -- Go `html/template` with per-page parsing
|
||||
to avoid `{{define "content"}}` collisions
|
||||
- **htmx SPA navigation** -- sidebar links use `hx-get` / `hx-target`
|
||||
for partial page updates with `hx-push-url` for browser history
|
||||
- **Stateless sessions** -- AES-256-GCM encrypted cookies carry
|
||||
credentials (needed for every RESTCONF call); no server-side session
|
||||
store
|
||||
- **Live polling** -- counters update every 5s, firmware progress every
|
||||
3s, all via htmx triggers
|
||||
|
||||
```
|
||||
main.go Entry point, flags, embedded FS
|
||||
internal/
|
||||
auth/ Login, logout, session (AES-GCM cookies)
|
||||
restconf/ HTTP client (Get, GetRaw, Post, PostJSON)
|
||||
handlers/ Page handlers
|
||||
dashboard.go Dashboard, hardware, sensors
|
||||
interfaces.go Interface list, detail, counters
|
||||
firewall.go Zone matrix
|
||||
keystore.go Key display
|
||||
system.go Firmware, reboot, config download
|
||||
server/
|
||||
server.go Route registration, template wiring, middleware
|
||||
templates/
|
||||
layouts/ base.html (shell), sidebar.html
|
||||
pages/ Per-page templates (one per route)
|
||||
fragments/ htmx partial fragments
|
||||
static/
|
||||
css/style.css All styles
|
||||
js/htmx.min.js htmx library
|
||||
img/ Logo, favicon
|
||||
```
|
||||
|
||||
|
||||
## License
|
||||
|
||||
See [LICENSE](LICENSE).
|
||||
|
||||
[1]: https://github.com/kernelkit/infix
|
||||
[2]: https://htmx.org
|
||||
[3]: https://datatracker.ietf.org/doc/html/rfc8040
|
||||
@@ -0,0 +1,17 @@
|
||||
module infix/webui
|
||||
|
||||
go 1.22.0
|
||||
|
||||
toolchain go1.22.2
|
||||
|
||||
require (
|
||||
github.com/google/go-cmp v0.7.0 // indirect
|
||||
github.com/openconfig/goyang v1.6.3 // indirect
|
||||
github.com/pborman/getopt v1.1.0 // indirect
|
||||
)
|
||||
|
||||
// Local fork of goyang with YANG 1.1 fixes:
|
||||
// - Uses.Augment: *Augment → []*Augment (multiple augments per uses)
|
||||
// - Value: add Reference field (when { reference "..."; })
|
||||
// - Input/Output: add Must field (must statements in rpc input/output)
|
||||
replace github.com/openconfig/goyang => ./internal/goyang
|
||||
@@ -0,0 +1,6 @@
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/openconfig/goyang v1.6.3 h1:9nWXBwd6b4+nZr8ni7O4zUXVhrVMXCLFz8os5YWFuo4=
|
||||
github.com/openconfig/goyang v1.6.3/go.mod h1:5WolITjek1NF8yrNERyVZ7jqjOClJTpO8p/+OwmETM4=
|
||||
github.com/pborman/getopt v1.1.0 h1:eJ3aFZroQqq0bWmraivjQNt6Dmm5M0h2JcDW38/Azb0=
|
||||
github.com/pborman/getopt v1.1.0/go.mod h1:FxXoW1Re00sQG/+KIkuSqRL/LwQgSkv7uyac+STFsbk=
|
||||
@@ -0,0 +1,173 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"html/template"
|
||||
"log"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"infix/webui/internal/handlers"
|
||||
"infix/webui/internal/restconf"
|
||||
"infix/webui/internal/security"
|
||||
)
|
||||
|
||||
const cookieName = "session"
|
||||
|
||||
// LoginHandler serves the login page and processes login/logout requests.
|
||||
type LoginHandler struct {
|
||||
Store *SessionStore
|
||||
RC *restconf.Client
|
||||
Template *template.Template
|
||||
// OnLogin is called after every successful login with a context that
|
||||
// carries the authenticated user's credentials. It is invoked in the
|
||||
// foreground, so implementations should start their own goroutines for
|
||||
// slow work. May be nil.
|
||||
OnLogin func(ctx context.Context)
|
||||
}
|
||||
|
||||
type loginData struct {
|
||||
Error string
|
||||
CsrfToken string
|
||||
}
|
||||
|
||||
// ShowLogin renders the login page (GET /login).
|
||||
func (h *LoginHandler) ShowLogin(w http.ResponseWriter, r *http.Request) {
|
||||
h.renderLogin(w, r, "")
|
||||
}
|
||||
|
||||
// DoLogin validates credentials against RESTCONF and creates a session (POST /login).
|
||||
func (h *LoginHandler) DoLogin(w http.ResponseWriter, r *http.Request) {
|
||||
if err := r.ParseForm(); err != nil {
|
||||
h.renderLogin(w, r, "Invalid request.")
|
||||
return
|
||||
}
|
||||
|
||||
username := r.FormValue("username")
|
||||
password := r.FormValue("password")
|
||||
|
||||
if username == "" || password == "" {
|
||||
h.renderLogin(w, r, "Username and password are required.")
|
||||
return
|
||||
}
|
||||
|
||||
// Verify credentials by making a RESTCONF call with Basic Auth.
|
||||
err := h.RC.CheckAuth(username, password)
|
||||
if err != nil {
|
||||
log.Printf("login failed for %q: %v", username, err)
|
||||
var authErr *restconf.AuthError
|
||||
if errors.As(err, &authErr) {
|
||||
h.renderLogin(w, r, "Invalid username or password.")
|
||||
} else {
|
||||
h.renderLogin(w, r, "Unable to reach the device. Please try again later.")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Build an authenticated context for post-login work.
|
||||
ctx := restconf.ContextWithCredentials(r.Context(), restconf.Credentials{
|
||||
Username: username,
|
||||
Password: password,
|
||||
})
|
||||
|
||||
// Probe optional features once at login and bake into the session.
|
||||
caps := handlers.DetectCapabilities(ctx, h.RC)
|
||||
// The external web-app shortcuts (console/ttyd, netbrowse) are
|
||||
// config-gated; fold them into the same feature map so templates gate
|
||||
// on .Capabilities.Has "console" / "netbrowse".
|
||||
console, netbrowse := handlers.DetectWebShortcuts(ctx, h.RC)
|
||||
caps.Features()["console"] = console
|
||||
caps.Features()["netbrowse"] = netbrowse
|
||||
// The User's Guide is bundled at build time (a filesystem check, not
|
||||
// config); gate the Help entry on its presence.
|
||||
caps.Features()["docs"] = handlers.DetectDocs()
|
||||
|
||||
// Trigger any post-login hooks (e.g. schema sync) with full credentials.
|
||||
if h.OnLogin != nil {
|
||||
h.OnLogin(ctx)
|
||||
}
|
||||
|
||||
token, csrfToken, err := h.Store.Create(username, password, caps.Features())
|
||||
if err != nil {
|
||||
log.Printf("session create error: %v", err)
|
||||
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
Name: cookieName,
|
||||
Value: token,
|
||||
Path: "/",
|
||||
HttpOnly: true,
|
||||
Secure: security.IsSecureRequest(r),
|
||||
SameSite: http.SameSiteLaxMode,
|
||||
})
|
||||
security.EnsureToken(w, r, csrfToken)
|
||||
|
||||
fullRedirect(w, r, "/")
|
||||
}
|
||||
|
||||
// DoLogout destroys the session and redirects to the login page (POST /logout).
|
||||
func (h *LoginHandler) DoLogout(w http.ResponseWriter, r *http.Request) {
|
||||
if c, err := r.Cookie(cookieName); err == nil {
|
||||
h.Store.Delete(c.Value)
|
||||
}
|
||||
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
Name: cookieName,
|
||||
Value: "",
|
||||
Path: "/",
|
||||
MaxAge: -1,
|
||||
HttpOnly: true,
|
||||
Secure: security.IsSecureRequest(r),
|
||||
SameSite: http.SameSiteLaxMode,
|
||||
})
|
||||
security.ClearToken(w, r)
|
||||
|
||||
fullRedirect(w, r, "/login")
|
||||
}
|
||||
|
||||
// SetSessionTimeout updates the current session's idle timeout from the
|
||||
// client's Auto-logout menu. The "timeout" form value is seconds; 0 means
|
||||
// never expire ("Off"). The store caps the upper bound.
|
||||
func (h *LoginHandler) SetSessionTimeout(w http.ResponseWriter, r *http.Request) {
|
||||
c, err := r.Cookie(cookieName)
|
||||
if err != nil {
|
||||
http.Error(w, "no session", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
secs, err := strconv.Atoi(r.FormValue("timeout"))
|
||||
if err != nil || secs < 0 {
|
||||
http.Error(w, "bad timeout", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
h.Store.SetTimeout(c.Value, time.Duration(secs)*time.Second)
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// fullRedirect forces a full page navigation. When the request comes
|
||||
// from htmx (boosted form) we use HX-Redirect so the browser does a
|
||||
// real page load instead of an AJAX swap — this is essential for the
|
||||
// login/logout transition where the page layout changes completely.
|
||||
func fullRedirect(w http.ResponseWriter, r *http.Request, url string) {
|
||||
if r.Header.Get("HX-Request") == "true" {
|
||||
w.Header().Set("HX-Redirect", url)
|
||||
return
|
||||
}
|
||||
http.Redirect(w, r, url, http.StatusSeeOther)
|
||||
}
|
||||
|
||||
func (h *LoginHandler) renderLogin(w http.ResponseWriter, r *http.Request, errMsg string) {
|
||||
data := loginData{
|
||||
Error: errMsg,
|
||||
CsrfToken: security.TokenFromContext(r.Context()),
|
||||
}
|
||||
if err := h.Template.ExecuteTemplate(w, "login.html", data); err != nil {
|
||||
log.Printf("template error: %v", err)
|
||||
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package auth
|
||||
@@ -0,0 +1,154 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package auth
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"infix/webui/internal/security"
|
||||
)
|
||||
|
||||
const sessionTimeout = 1 * time.Hour
|
||||
const maxSessionTimeout = 24 * time.Hour
|
||||
|
||||
// sessionEntry is the in-memory state of a single authenticated
|
||||
// session. The user's password is kept alongside the token so the
|
||||
// webui can re-authenticate to RESTCONF on the user's behalf on each
|
||||
// request; it is never written to disk or surfaced over the wire.
|
||||
type sessionEntry struct {
|
||||
username string
|
||||
password string
|
||||
csrfToken string
|
||||
features map[string]bool
|
||||
lastSeenAt time.Time
|
||||
timeout time.Duration // idle timeout; 0 = never expire ("Off")
|
||||
}
|
||||
|
||||
// expired reports whether an entry has passed its idle timeout. A zero
|
||||
// timeout means the session never idle-expires — the user chose "Off".
|
||||
func expired(e *sessionEntry) bool {
|
||||
return e.timeout > 0 && time.Since(e.lastSeenAt) > e.timeout
|
||||
}
|
||||
|
||||
// SessionStore issues opaque random session tokens backed by an
|
||||
// in-memory map. Nothing is persisted: a webui restart drops every
|
||||
// active session and the UI's 401 handler surfaces a fresh login
|
||||
// page.
|
||||
type SessionStore struct {
|
||||
mu sync.RWMutex
|
||||
sessions map[string]*sessionEntry
|
||||
}
|
||||
|
||||
// NewSessionStore returns a fresh store and starts a janitor
|
||||
// goroutine that sweeps expired entries once a minute.
|
||||
func NewSessionStore() *SessionStore {
|
||||
s := &SessionStore{
|
||||
sessions: make(map[string]*sessionEntry),
|
||||
}
|
||||
go s.janitor()
|
||||
return s
|
||||
}
|
||||
|
||||
// Create issues a session for the given credentials, returning the
|
||||
// session token (cookie value) and a freshly minted CSRF token.
|
||||
func (s *SessionStore) Create(username, password string, features map[string]bool) (string, string, error) {
|
||||
token, err := security.RandomToken()
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("session token: %w", err)
|
||||
}
|
||||
csrf, err := security.RandomToken()
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("csrf token: %w", err)
|
||||
}
|
||||
|
||||
s.mu.Lock()
|
||||
s.sessions[token] = &sessionEntry{
|
||||
username: username,
|
||||
password: password,
|
||||
csrfToken: csrf,
|
||||
features: features,
|
||||
lastSeenAt: time.Now(),
|
||||
timeout: sessionTimeout,
|
||||
}
|
||||
s.mu.Unlock()
|
||||
return token, csrf, nil
|
||||
}
|
||||
|
||||
// Lookup returns the credentials associated with a session token, or
|
||||
// ok=false if the token is unknown or expired. An expired entry is
|
||||
// reaped as a side effect.
|
||||
func (s *SessionStore) Lookup(token string) (username, password, csrf string, features map[string]bool, ok bool) {
|
||||
s.mu.RLock()
|
||||
e, present := s.sessions[token]
|
||||
if present && !expired(e) {
|
||||
username, password, csrf, features = e.username, e.password, e.csrfToken, e.features
|
||||
s.mu.RUnlock()
|
||||
return username, password, csrf, features, true
|
||||
}
|
||||
s.mu.RUnlock()
|
||||
|
||||
// Slow path: the entry was either missing or expired at read
|
||||
// time. Re-check under the write lock — Refresh may have
|
||||
// updated lastSeenAt in the gap — and only delete if still
|
||||
// expired.
|
||||
if present {
|
||||
s.mu.Lock()
|
||||
if e, ok := s.sessions[token]; ok && expired(e) {
|
||||
delete(s.sessions, token)
|
||||
}
|
||||
s.mu.Unlock()
|
||||
}
|
||||
return "", "", "", nil, false
|
||||
}
|
||||
|
||||
// SetTimeout updates a session's idle timeout (0 = never expire) so the
|
||||
// server-side expiry tracks the client's Auto-logout menu. Out-of-range
|
||||
// values — including a negative from integer overflow on a garbage request —
|
||||
// are capped at maxSessionTimeout so a session can't be pinned open
|
||||
// indefinitely; 0 still means never. No-op if the token is unknown.
|
||||
func (s *SessionStore) SetTimeout(token string, d time.Duration) {
|
||||
if d < 0 || d > maxSessionTimeout {
|
||||
d = maxSessionTimeout
|
||||
}
|
||||
s.mu.Lock()
|
||||
if e, ok := s.sessions[token]; ok {
|
||||
e.timeout = d
|
||||
}
|
||||
s.mu.Unlock()
|
||||
}
|
||||
|
||||
// Refresh extends a session's lifetime by resetting its last-seen
|
||||
// timestamp. Called by the auth middleware on each user-driven
|
||||
// request so an active session doesn't expire mid-use. No-op if the
|
||||
// token is unknown.
|
||||
func (s *SessionStore) Refresh(token string) {
|
||||
s.mu.Lock()
|
||||
if e, ok := s.sessions[token]; ok {
|
||||
e.lastSeenAt = time.Now()
|
||||
}
|
||||
s.mu.Unlock()
|
||||
}
|
||||
|
||||
// Delete revokes a session. Called by logout and by Lookup on
|
||||
// expiration.
|
||||
func (s *SessionStore) Delete(token string) {
|
||||
s.mu.Lock()
|
||||
delete(s.sessions, token)
|
||||
s.mu.Unlock()
|
||||
}
|
||||
|
||||
func (s *SessionStore) janitor() {
|
||||
t := time.NewTicker(1 * time.Minute)
|
||||
defer t.Stop()
|
||||
for range t.C {
|
||||
s.mu.Lock()
|
||||
for token, e := range s.sessions {
|
||||
if expired(e) {
|
||||
delete(s.sessions, token)
|
||||
}
|
||||
}
|
||||
s.mu.Unlock()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
# To get started with Dependabot version updates, you'll need to specify which
|
||||
# package ecosystems to update and where the package manifests are located.
|
||||
# Please see the documentation for all configuration options:
|
||||
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
|
||||
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "gomod" # See documentation for possible values
|
||||
directory: "/" # Location of package manifests
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
- package-ecosystem: "github-actions" # See documentation for possible values
|
||||
directory: "/" # Location of package manifests
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
@@ -0,0 +1,53 @@
|
||||
---
|
||||
#########################
|
||||
#########################
|
||||
## Golang Linter rules ##
|
||||
#########################
|
||||
#########################
|
||||
|
||||
# configure golangci-lint
|
||||
# see https://github.com/golangci/golangci-lint/blob/master/.golangci.example.yml
|
||||
run:
|
||||
timeout: 10m
|
||||
issues:
|
||||
exclude-rules:
|
||||
- path: _test\.go
|
||||
linters:
|
||||
- dupl
|
||||
- gosec
|
||||
- goconst
|
||||
new: true
|
||||
linters:
|
||||
enable:
|
||||
- gosec
|
||||
- unconvert
|
||||
- goconst
|
||||
- goimports
|
||||
- gofmt
|
||||
- gocritic
|
||||
- govet
|
||||
- revive
|
||||
- staticcheck
|
||||
- unconvert
|
||||
- unparam
|
||||
- unused
|
||||
- wastedassign
|
||||
- whitespace
|
||||
linters-settings:
|
||||
errcheck:
|
||||
# report about assignment of errors to blank identifier: `num, _ := strconv.Atoi(numStr)`;
|
||||
# default is false: such cases aren't reported by default.
|
||||
check-blank: true
|
||||
govet:
|
||||
# report about shadowed variables
|
||||
check-shadowing: false
|
||||
maligned:
|
||||
# print struct with more effective memory layout or not, false by default
|
||||
suggest-new: true
|
||||
gocritic:
|
||||
disabled-checks:
|
||||
- singleCaseSwitch
|
||||
- appendAssign
|
||||
revive:
|
||||
ignore-generated-header: true
|
||||
severity: warning
|
||||
@@ -0,0 +1,59 @@
|
||||
---
|
||||
###########################################
|
||||
# These are the rules used for #
|
||||
# linting all the yaml files in the stack #
|
||||
# NOTE: #
|
||||
# You can disable line with: #
|
||||
# # yamllint disable-line #
|
||||
###########################################
|
||||
rules:
|
||||
braces:
|
||||
level: warning
|
||||
min-spaces-inside: 0
|
||||
max-spaces-inside: 0
|
||||
min-spaces-inside-empty: 1
|
||||
max-spaces-inside-empty: 5
|
||||
brackets:
|
||||
level: warning
|
||||
min-spaces-inside: 0
|
||||
max-spaces-inside: 0
|
||||
min-spaces-inside-empty: 1
|
||||
max-spaces-inside-empty: 5
|
||||
colons:
|
||||
level: warning
|
||||
max-spaces-before: 0
|
||||
max-spaces-after: 1
|
||||
commas:
|
||||
level: warning
|
||||
max-spaces-before: 0
|
||||
min-spaces-after: 1
|
||||
max-spaces-after: 1
|
||||
comments: disable
|
||||
comments-indentation: disable
|
||||
document-end: disable
|
||||
document-start:
|
||||
level: warning
|
||||
present: true
|
||||
empty-lines:
|
||||
level: warning
|
||||
max: 2
|
||||
max-start: 0
|
||||
max-end: 0
|
||||
hyphens:
|
||||
level: warning
|
||||
max-spaces-after: 1
|
||||
indentation:
|
||||
level: warning
|
||||
spaces: consistent
|
||||
indent-sequences: true
|
||||
check-multi-line-strings: false
|
||||
key-duplicates: enable
|
||||
line-length:
|
||||
level: warning
|
||||
max: 120
|
||||
allow-non-breakable-words: true
|
||||
allow-non-breakable-inline-mappings: true
|
||||
new-line-at-end-of-file: disable
|
||||
new-lines:
|
||||
type: unix
|
||||
trailing-spaces: disable
|
||||
@@ -0,0 +1,15 @@
|
||||
name: Go
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
pull_request:
|
||||
schedule:
|
||||
- cron: "0 0 * * *"
|
||||
|
||||
jobs:
|
||||
go:
|
||||
uses: openconfig/common-ci/.github/workflows/go.yml@125b6b58286d116b216e45c33cb859f547965d61
|
||||
|
||||
linter:
|
||||
uses: openconfig/common-ci/.github/workflows/linter.yml@125b6b58286d116b216e45c33cb859f547965d61
|
||||
@@ -0,0 +1 @@
|
||||
tags
|
||||
@@ -0,0 +1,9 @@
|
||||
# This is the official list of goyang authors for copyright purposes.
|
||||
# This file is distinct from the CONTRIBUTORS files.
|
||||
# See the latter for an explanation.
|
||||
|
||||
# Names should be added to this file as:
|
||||
# Name or Organization <email address>
|
||||
# The email address is not required for organizations.
|
||||
|
||||
Google Inc.
|
||||
@@ -0,0 +1,25 @@
|
||||
Want to contribute? Great! First, read this page (including the small print at the end).
|
||||
|
||||
### Before you contribute
|
||||
Before we can use your code, you must sign the
|
||||
[Google Individual Contributor License Agreement](https://developers.google.com/open-source/cla/individual?csw=1)
|
||||
(CLA), which you can do online. The CLA is necessary mainly because you own the
|
||||
copyright to your changes, even after your contribution becomes part of our
|
||||
codebase, so we need your permission to use and distribute your code. We also
|
||||
need to be sure of various other things—for instance that you'll tell us if you
|
||||
know that your code infringes on other people's patents. You don't have to sign
|
||||
the CLA until after you've submitted your code for review and a member has
|
||||
approved it, but you must do it before we can put your code into our codebase.
|
||||
Before you start working on a larger contribution, you should get in touch with
|
||||
us first through the issue tracker with your idea so that we can help out and
|
||||
possibly guide you. Coordinating up front makes it much easier to avoid
|
||||
frustration later on.
|
||||
|
||||
### Code reviews
|
||||
All submissions, including submissions by project members, require review. We
|
||||
use Github pull requests for this purpose.
|
||||
|
||||
### The small print
|
||||
Contributions made by corporations are covered by a different agreement than
|
||||
the one above, the
|
||||
[Software Grant and Corporate Contributor License Agreement](https://cla.developers.google.com/about/google-corporate).
|
||||
@@ -0,0 +1,15 @@
|
||||
# People who have agreed to one of the CLAs and can contribute patches.
|
||||
# The AUTHORS file lists the copyright holders; this file
|
||||
# lists people. For example, Google employees are listed here
|
||||
# but not in AUTHORS, because Google holds the copyright.
|
||||
#
|
||||
# https://developers.google.com/open-source/cla/individual
|
||||
# https://developers.google.com/open-source/cla/corporate
|
||||
#
|
||||
# Names should be added to this file as:
|
||||
# Name <email address>
|
||||
|
||||
Paul Borman <borman@google.com>
|
||||
Andrew Fort <afort@arista.com>
|
||||
Rob Shakir <robjs@google.com>
|
||||
Sean Condon <sean@opennetworking.org>
|
||||
@@ -0,0 +1,14 @@
|
||||
// Copyright 2015 Google Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
@@ -0,0 +1,202 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "{}"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright {yyyy} {name of copyright owner}
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||

|
||||
[](https://coveralls.io/github/openconfig/goyang?branch=master)
|
||||
|
||||
Current support for `goyang` is for the [latest 3 Go releases](https://golang.org/project/#release).
|
||||
|
||||
# goyang
|
||||
YANG parser and compiler for Go programs.
|
||||
|
||||
The yang package (pkg/yang) is used to convert a YANG schema into either an
|
||||
in memory abstract syntax trees (ast) or more fully resolved, in memory, "Entry"
|
||||
trees. An Entry tree consists only of Entry structures and has had
|
||||
augmentation, imports, and includes all applied.
|
||||
|
||||
goyang is a sample program that uses the yang (pkg/yang) package.
|
||||
|
||||
goyang uses the yang package to create an in-memory tree representation of
|
||||
schemas defined in YANG and then dumps out the contents in several forms.
|
||||
The forms include:
|
||||
|
||||
* tree - a simple tree representation
|
||||
* types - list understood types extracted from the schema
|
||||
|
||||
The yang package, and the goyang program, are not complete and are a work in
|
||||
progress.
|
||||
|
||||
For more complex output types, such as Go structs, and protobuf messages
|
||||
please use the [openconfig/ygot](https://github.com/openconfig/ygot) package,
|
||||
which uses this package as its backend.
|
||||
|
||||
### Getting started
|
||||
|
||||
To build goyang, ensure you have go language tools installed
|
||||
(available at [golang.org](https://golang.org/dl)) and that the `GOPATH`
|
||||
environment variable is set to your Go workspace.
|
||||
|
||||
1. `go get github.com/openconfig/goyang`
|
||||
* This will download goyang code and dependencies into the src
|
||||
subdirectory in your workspace.
|
||||
|
||||
2. `cd <workspace>/src/github.com/openconfig/goyang`
|
||||
|
||||
3. `go build`
|
||||
|
||||
* This will build the goyang binary and place it in the bin
|
||||
subdirectory in your workspace.
|
||||
|
||||
### Contributing to goyang
|
||||
|
||||
goyang is still a work-in-progress and we welcome contributions. Please see
|
||||
the `CONTRIBUTING` file for information about how to contribute to the codebase.
|
||||
|
||||
### Disclaimer
|
||||
|
||||
This is not an official Google product.
|
||||
@@ -0,0 +1,12 @@
|
||||
module github.com/openconfig/goyang
|
||||
|
||||
go 1.22.0
|
||||
|
||||
toolchain go1.24.1
|
||||
|
||||
require (
|
||||
github.com/google/go-cmp v0.7.0
|
||||
github.com/kylelemons/godebug v1.1.0
|
||||
github.com/openconfig/gnmi v0.14.1
|
||||
github.com/pborman/getopt v1.1.0
|
||||
)
|
||||
@@ -0,0 +1,8 @@
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
||||
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||
github.com/openconfig/gnmi v0.14.1 h1:qKMuFvhIRR2/xxCOsStPQ25aKpbMDdWr3kI+nP9bhMs=
|
||||
github.com/openconfig/gnmi v0.14.1/go.mod h1:whr6zVq9PCU8mV1D0K9v7Ajd3+swoN6Yam9n8OH3eT0=
|
||||
github.com/pborman/getopt v1.1.0 h1:eJ3aFZroQqq0bWmraivjQNt6Dmm5M0h2JcDW38/Azb0=
|
||||
github.com/pborman/getopt v1.1.0/go.mod h1:FxXoW1Re00sQG/+KIkuSqRL/LwQgSkv7uyac+STFsbk=
|
||||
@@ -0,0 +1,112 @@
|
||||
// Copyright 2015 Google Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Package indent indents lines of text.
|
||||
package indent
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// String returns s with each line in s prefixed by indent.
|
||||
func String(indent, s string) string {
|
||||
if indent == "" || s == "" {
|
||||
return s
|
||||
}
|
||||
lines := strings.SplitAfter(s, "\n")
|
||||
if len(lines[len(lines)-1]) == 0 {
|
||||
lines = lines[:len(lines)-1]
|
||||
}
|
||||
return strings.Join(append([]string{""}, lines...), indent)
|
||||
}
|
||||
|
||||
// Bytes returns b with each line in b prefixed by indent.
|
||||
func Bytes(indent, b []byte) []byte {
|
||||
if len(indent) == 0 || len(b) == 0 {
|
||||
return b
|
||||
}
|
||||
lines := bytes.SplitAfter(b, []byte{'\n'})
|
||||
if len(lines[len(lines)-1]) == 0 {
|
||||
lines = lines[:len(lines)-1]
|
||||
}
|
||||
return bytes.Join(append([][]byte{{}}, lines...), indent)
|
||||
}
|
||||
|
||||
// NewWriter returns an io.Writer that prefixes the lines written to it with
|
||||
// indent and then writes them to w. The writer returns the number of bytes
|
||||
// written to the underlying Writer.
|
||||
func NewWriter(w io.Writer, indent string) io.Writer {
|
||||
if indent == "" {
|
||||
return w
|
||||
}
|
||||
return &iw{
|
||||
w: w,
|
||||
prefix: []byte(indent),
|
||||
}
|
||||
}
|
||||
|
||||
type iw struct {
|
||||
w io.Writer
|
||||
prefix []byte
|
||||
partial bool // true if next line's indent already written
|
||||
}
|
||||
|
||||
// Write implements io.Writer.
|
||||
func (w *iw) Write(buf []byte) (int, error) {
|
||||
if len(buf) == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
lines := bytes.SplitAfter(buf, []byte{'\n'})
|
||||
if len(lines[len(lines)-1]) == 0 {
|
||||
lines = lines[:len(lines)-1]
|
||||
}
|
||||
if !w.partial {
|
||||
lines = append([][]byte{{}}, lines...)
|
||||
}
|
||||
joined := bytes.Join(lines, w.prefix)
|
||||
w.partial = joined[len(joined)-1] != '\n'
|
||||
|
||||
n, err := w.w.Write(joined)
|
||||
if err != nil {
|
||||
return actualWrittenSize(n, len(w.prefix), lines), err
|
||||
}
|
||||
|
||||
return len(buf), nil
|
||||
}
|
||||
|
||||
func actualWrittenSize(underlay, prefix int, lines [][]byte) int {
|
||||
actual := 0
|
||||
remain := underlay
|
||||
for _, line := range lines {
|
||||
if len(line) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
addition := remain - prefix
|
||||
if addition <= 0 {
|
||||
return actual
|
||||
}
|
||||
|
||||
if addition <= len(line) {
|
||||
return actual + addition
|
||||
}
|
||||
|
||||
actual += len(line)
|
||||
remain -= prefix + len(line)
|
||||
}
|
||||
|
||||
return actual
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
// Copyright 2015 Google Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package indent
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"testing"
|
||||
)
|
||||
|
||||
var tests = []struct {
|
||||
prefix, in, out string
|
||||
}{
|
||||
{
|
||||
"", "", "",
|
||||
}, {
|
||||
"--", "", "",
|
||||
}, {
|
||||
"", "x\nx", "x\nx",
|
||||
}, {
|
||||
"--", "x", "--x",
|
||||
}, {
|
||||
"--", "\n", "--\n",
|
||||
}, {
|
||||
"--", "\n\n", "--\n--\n",
|
||||
}, {
|
||||
"--", "x\n", "--x\n",
|
||||
}, {
|
||||
"--", "\nx", "--\n--x",
|
||||
}, {
|
||||
"--", "two\nlines\n", "--two\n--lines\n",
|
||||
}, {
|
||||
"--", "\nempty\nfirst\n", "--\n--empty\n--first\n",
|
||||
}, {
|
||||
"--", "empty\nlast\n\n", "--empty\n--last\n--\n",
|
||||
}, {
|
||||
"--", "empty\n\nmiddle\n", "--empty\n--\n--middle\n",
|
||||
},
|
||||
}
|
||||
|
||||
func TestIndent(t *testing.T) {
|
||||
for x, tt := range tests {
|
||||
out := String(tt.prefix, tt.in)
|
||||
if out != tt.out {
|
||||
t.Errorf("#%d: got %q, want %q", x, out, tt.out)
|
||||
}
|
||||
bout := string(Bytes([]byte(tt.prefix), []byte(tt.in)))
|
||||
if bout != out {
|
||||
t.Errorf("#%d: Bytes got %q\n String got %q", x, bout, out)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestWriter(t *testing.T) {
|
||||
Test:
|
||||
for x, tt := range tests {
|
||||
for size := 1; size < 64; size <<= 1 {
|
||||
var b bytes.Buffer
|
||||
w := NewWriter(&b, tt.prefix)
|
||||
data := []byte(tt.in)
|
||||
for len(data) > size {
|
||||
if _, err := w.Write(data[:size]); err != nil {
|
||||
t.Errorf("#%d: %v", x, err)
|
||||
continue Test
|
||||
}
|
||||
data = data[size:]
|
||||
}
|
||||
if _, err := w.Write(data); err != nil {
|
||||
t.Errorf("#%d/%d: %v", x, size, err)
|
||||
continue Test
|
||||
}
|
||||
|
||||
out := b.String()
|
||||
if out != tt.out {
|
||||
t.Errorf("#%d/%d: got %q, want %q", x, size, out, tt.out)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestWrittenSize(t *testing.T) {
|
||||
for x, tt := range tests {
|
||||
var b bytes.Buffer
|
||||
w := NewWriter(&b, tt.prefix)
|
||||
data := []byte(tt.in)
|
||||
if n, _ := w.Write(data); n != len(data) {
|
||||
t.Errorf("#%d: got %d, want %d", x, n, len(data))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestWrittenSizeWithError(t *testing.T) {
|
||||
table := []struct {
|
||||
prefix string
|
||||
input string
|
||||
underlay int
|
||||
expected int
|
||||
}{
|
||||
{"--", "two\nlines\n", 0, 0},
|
||||
{"--", "two\nlines\n", 1, 0}, // -
|
||||
{"--", "two\nlines\n", 2, 0}, // -
|
||||
{"--", "two\nlines\n", 3, 1}, // t
|
||||
{"--", "two\nlines\n", 4, 2}, // w
|
||||
{"--", "two\nlines\n", 5, 3}, // o
|
||||
{"--", "two\nlines\n", 6, 4}, // \n
|
||||
{"--", "two\nlines\n", 7, 4}, // -
|
||||
{"--", "two\nlines\n", 8, 4}, // -
|
||||
{"--", "two\nlines\n", 9, 5}, // l
|
||||
{"--", "two\nlines\n", 10, 6}, // i
|
||||
{"--", "two\nlines\n", 11, 7}, // n
|
||||
{"--", "two\nlines\n", 12, 8}, // e
|
||||
{"--", "two\nlines\n", 13, 9}, // s
|
||||
{"--", "two\nlines\n", 14, 10}, // \n
|
||||
{"--", "two\nlines\n", 15, 10}, // -
|
||||
{"--", "two\nlines\n", 16, 10}, // -
|
||||
}
|
||||
|
||||
for _, d := range table {
|
||||
uw := errorWriter{d.underlay}
|
||||
w := NewWriter(uw, d.prefix)
|
||||
data := []byte(d.input)
|
||||
if n, _ := w.Write(data); n != d.expected {
|
||||
t.Errorf("underlay: %d, got %d, want %d, err: ", d.underlay, n, d.expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type errorWriter struct {
|
||||
ret int
|
||||
}
|
||||
|
||||
func (w errorWriter) Write(buf []byte) (int, error) {
|
||||
return w.ret, errors.New("error")
|
||||
}
|
||||
@@ -0,0 +1,461 @@
|
||||
// Copyright 2015 Google Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package yang
|
||||
|
||||
// This file implements BuildAST() and its associated helper structs and
|
||||
// functions for constructing an AST of Nodes from a Statement tree. This
|
||||
// function also populates all typedefs into a type cache.
|
||||
//
|
||||
// The initTypes function generates the helper struct and functions that
|
||||
// recursively fill in the various Node structures defined in yang.go.
|
||||
// BuildAST() then uses those functions to convert raw parsed Statements into
|
||||
// an AST.
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func init() {
|
||||
// Initialize the global variables `typeMap` and `nameMap`.
|
||||
// By doing this, we are making the assumption that all modules will be
|
||||
// parsed according to the type hierarchy rooted at `meta`, and thus
|
||||
// all input YANG modules will be parsed in this manner.
|
||||
initTypes(reflect.TypeOf(&meta{}))
|
||||
}
|
||||
|
||||
// A yangStatement contains all information needed to build a particular
|
||||
// type of statement into an AST node.
|
||||
type yangStatement struct {
|
||||
// funcs is the map of YANG field names to the function that populates
|
||||
// the statement into the AST node.
|
||||
funcs map[string]func(*Statement, reflect.Value, reflect.Value, *typeDictionary) error
|
||||
// required is a list of fields that must be present in the statement.
|
||||
required []string
|
||||
// sRequired maps a statement name to a list of required sub-field
|
||||
// names. The statement name can be an alias of the primary field type.
|
||||
// e.g. If a field is required by statement type foo, then only foo
|
||||
// should have the field. If bar is an alias of foo, it must not
|
||||
// have this field.
|
||||
sRequired map[string][]string
|
||||
// addext is the function to handle possible extensions.
|
||||
addext func(*Statement, reflect.Value, reflect.Value) error
|
||||
}
|
||||
|
||||
// newYangStatement creates a new yangStatement.
|
||||
func newYangStatement() *yangStatement {
|
||||
return &yangStatement{
|
||||
funcs: make(map[string]func(*Statement, reflect.Value, reflect.Value, *typeDictionary) error),
|
||||
sRequired: make(map[string][]string),
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
// The following maps are built up at init time.
|
||||
// typeMap provides a lookup from a Node type to the corresponding
|
||||
// yangStatement.
|
||||
typeMap = map[reflect.Type]*yangStatement{}
|
||||
// nameMap provides a lookup from a keyword string to the corresponding
|
||||
// concrete type implementing the Node interface (see yang.go).
|
||||
nameMap = map[string]reflect.Type{}
|
||||
|
||||
// The following are helper types used by the implementation.
|
||||
statementType = reflect.TypeOf(&Statement{})
|
||||
nilValue = reflect.ValueOf(nil)
|
||||
// nodeType is the reflect.Type of the Node interface.
|
||||
nodeType = reflect.TypeOf((*Node)(nil)).Elem()
|
||||
)
|
||||
|
||||
// meta is a collection of top-level statements. There is no actual
|
||||
// statement named "meta". All other statements are a sub-statement of one
|
||||
// of the meta statements.
|
||||
type meta struct {
|
||||
Module []*Module `yang:"module"`
|
||||
}
|
||||
|
||||
// aliases is a map of "aliased" names, that is, two types of statements
|
||||
// that parse (nearly) the same.
|
||||
// NOTE: This only works for root-level aliasing for now, which is good enough
|
||||
// for module/submodule. This is because yangStatement.funcs doesn't store the
|
||||
// handler function for aliased fields, and sRequired also may only store the
|
||||
// correct values when processing a root-level statement due to aliasing. These
|
||||
// issues would need to be fixed in order to support aliasing for non-top-level
|
||||
// statements.
|
||||
var aliases = map[string]string{
|
||||
"submodule": "module",
|
||||
}
|
||||
|
||||
// buildASTWithTypeDict creates an AST for the input statement, and returns its
|
||||
// root node. It also takes as input a type dictionary into which any
|
||||
// encountered typedefs within the statement are cached.
|
||||
func buildASTWithTypeDict(stmt *Statement, types *typeDictionary) (Node, error) {
|
||||
v, err := build(stmt, nilValue, types)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return v.Interface().(Node), nil
|
||||
}
|
||||
|
||||
// build builds and returns an AST from the statement stmt and with parent node
|
||||
// parent. It also takes as input a type dictionary types into which any
|
||||
// encountered typedefs within the statement are cached. The type of value
|
||||
// returned depends on the keyword in stmt (see yang.go). It returns an error
|
||||
// if it cannot build the statement into its corresponding Node type.
|
||||
func build(stmt *Statement, parent reflect.Value, types *typeDictionary) (v reflect.Value, err error) {
|
||||
defer func() {
|
||||
// If we are returning a real Node then call addTypedefs
|
||||
// if the node possibly contains typedefs.
|
||||
// Cache these in the typedef cache for look-ups.
|
||||
if err != nil || v == nilValue {
|
||||
return
|
||||
}
|
||||
if t, ok := v.Interface().(Typedefer); ok {
|
||||
types.addTypedefs(t)
|
||||
}
|
||||
}()
|
||||
keyword := stmt.Keyword
|
||||
if k, ok := aliases[stmt.Keyword]; ok {
|
||||
keyword = k
|
||||
}
|
||||
t := nameMap[keyword]
|
||||
y := typeMap[t]
|
||||
// Keep track of which substatements are present in the statement.
|
||||
found := map[string]bool{}
|
||||
|
||||
// Get the struct type we are pointing to.
|
||||
t = t.Elem()
|
||||
// v is a pointer to the instantiated structure we are building.
|
||||
v = reflect.New(t)
|
||||
|
||||
// Handle special cases that are not actually substatements:
|
||||
|
||||
if fn := y.funcs["Name"]; fn != nil {
|
||||
// Name uses stmt directly.
|
||||
if err := fn(stmt, v, parent, types); err != nil {
|
||||
return nilValue, err
|
||||
}
|
||||
}
|
||||
if fn := y.funcs["Statement"]; fn != nil {
|
||||
// Statement uses stmt directly.
|
||||
if err := fn(stmt, v, parent, types); err != nil {
|
||||
return nilValue, err
|
||||
}
|
||||
}
|
||||
if fn := y.funcs["Parent"]; fn != nil {
|
||||
// parent is the parent node, which is nilValue (reflect.ValueOf(nil)) if there is none.
|
||||
// parent.IsValid will return false when parent is a nil interface
|
||||
// parent.IsValid will true if parent references a concrete type
|
||||
// (even if it is nil).
|
||||
if parent.IsValid() {
|
||||
if err := fn(stmt, v, parent, types); err != nil {
|
||||
return nilValue, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Now handle the substatements
|
||||
|
||||
for _, ss := range stmt.statements {
|
||||
found[ss.Keyword] = true
|
||||
fn := y.funcs[ss.Keyword]
|
||||
switch {
|
||||
case fn != nil:
|
||||
// Normal case, the keyword is known.
|
||||
if err := fn(ss, v, parent, types); err != nil {
|
||||
return nilValue, err
|
||||
}
|
||||
case len(strings.Split(ss.Keyword, ":")) == 2:
|
||||
// Keyword is not known but it has a prefix so it might
|
||||
// be an extension.
|
||||
if y.addext == nil {
|
||||
return nilValue, fmt.Errorf("%s: no extension function", ss.Location())
|
||||
}
|
||||
y.addext(ss, v, parent)
|
||||
default:
|
||||
return nilValue, fmt.Errorf("%s: unknown %s field: %s", ss.Location(), stmt.Keyword, ss.Keyword)
|
||||
}
|
||||
}
|
||||
|
||||
// Make sure all of our required field are there.
|
||||
for _, r := range y.required {
|
||||
if !found[r] {
|
||||
return nilValue, fmt.Errorf("%s: missing required %s field: %s", stmt.Location(), stmt.Keyword, r)
|
||||
}
|
||||
}
|
||||
|
||||
// Make sure required fields based on our keyword are there (module vs submodule)
|
||||
for _, r := range y.sRequired[stmt.Keyword] {
|
||||
if !found[r] {
|
||||
return nilValue, fmt.Errorf("%s: missing required %s field: %s", stmt.Location(), stmt.Keyword, r)
|
||||
}
|
||||
}
|
||||
|
||||
// Make sure we don't have any field set that is required by a different keyword.
|
||||
for n, or := range y.sRequired {
|
||||
if n == stmt.Keyword {
|
||||
continue
|
||||
}
|
||||
for _, r := range or {
|
||||
if found[r] {
|
||||
return nilValue, fmt.Errorf("%s: unknown %s field: %s", stmt.Location(), stmt.Keyword, r)
|
||||
}
|
||||
}
|
||||
}
|
||||
return v, nil
|
||||
}
|
||||
|
||||
// initTypes creates the functions necessary to build a Statement into the
|
||||
// given the type "at" based on its possible substatements. at must implement
|
||||
// Node, with its concrete type being a pointer to a struct defined in yang.go.
|
||||
//
|
||||
// This function also builds up the functions to populate the input type
|
||||
// dictionary types with any encountered typedefs within the statement.
|
||||
//
|
||||
// For each field of the struct with a yang tag (e.g., `yang:"command"`), a
|
||||
// function is created with "command" as its unique ID. The complete map of
|
||||
// builder functions for at is then added to the typeMap map with at as the
|
||||
// key. The idea is to call these builder functions for each substatement
|
||||
// encountered.
|
||||
//
|
||||
// The functions have the form:
|
||||
//
|
||||
// func fn(ss *Statement, v, p reflect.Value, types *typeDictionary) error
|
||||
//
|
||||
// Given stmt as a Statement of type at, ss is a substatement of stmt (in a few
|
||||
// exceptional cases, ss is the Statement itself). v must have the same type
|
||||
// as at and is the structure being filled in. p is the parent Node, or nil.
|
||||
// types is the type dictionary cache of the current set of modules being parsed,
|
||||
// which is used for looking up typedefs. p is only used to set the Parent
|
||||
// field of a Node. For example, given the following structure and variables:
|
||||
//
|
||||
// type Include struct {
|
||||
// Name string `yang:"Name"`
|
||||
// Source *Statement `yang:"Statement"`
|
||||
// Parent Node `yang:"Parent"`
|
||||
// Extensions []*Statement `yang:"Ext"`
|
||||
// RevisionDate *Value `yang:"revision-date"`
|
||||
// }
|
||||
//
|
||||
// var inc = &Include{}
|
||||
// var vInc = reflect.ValueOf(inc)
|
||||
// var tInc = reflect.TypeOf(inc)
|
||||
//
|
||||
// Functions are created for each fields and named Name, Statement, Parent, Ext,
|
||||
// and revision-date.
|
||||
//
|
||||
// The function built for RevisionDate will be called for any substatement,
|
||||
// ds, of stmt that has the keyword "revision-date" along with the value of
|
||||
// vInc and its parent:
|
||||
//
|
||||
// typeMap[tInc]["revision-date"](ss, vInc, parent, types)
|
||||
//
|
||||
// Normal fields are all processed this same way.
|
||||
//
|
||||
// The other 4 fields are special. In the case of Name, Statement, and Parent,
|
||||
// the function is passed stmt, rather than ss, as these fields are not filled in
|
||||
// by substatements.
|
||||
//
|
||||
// The Name command must set its field to the Statement's argument. The
|
||||
// Statement command must set its field to the Statement itself. The
|
||||
// Parent command must set its field with the Node of its parent (the
|
||||
// parent parameter).
|
||||
//
|
||||
// The Ext command is unique and must decode into a []*Statement. This is a
|
||||
// slice of all statements that use unknown keywords with a prefix (in a valid
|
||||
// .yang file these should be the extensions).
|
||||
//
|
||||
// The Field can have attributes delimited by a ','. The only
|
||||
// supported attributes are:
|
||||
//
|
||||
// nomerge: Do not merge this field
|
||||
// required: This field must be populated
|
||||
// required=KIND: This field must be populated if the keyword is KIND
|
||||
// otherwise this field must not be present.
|
||||
// (This is to support merging Module and SubModule).
|
||||
//
|
||||
// If at contains substructures, initTypes recurses on the substructures.
|
||||
func initTypes(at reflect.Type) {
|
||||
if at.Kind() != reflect.Ptr || at.Elem().Kind() != reflect.Struct {
|
||||
panic(fmt.Sprintf("interface not a struct pointer, is %v", at))
|
||||
}
|
||||
if typeMap[at] != nil {
|
||||
return // we already defined this type
|
||||
}
|
||||
|
||||
y := newYangStatement()
|
||||
typeMap[at] = y
|
||||
t := at.Elem()
|
||||
for i := 0; i != t.NumField(); i++ {
|
||||
i := i
|
||||
f := t.Field(i)
|
||||
yang := f.Tag.Get("yang")
|
||||
if yang == "" {
|
||||
continue
|
||||
}
|
||||
parts := strings.Split(yang, ",")
|
||||
name := parts[0]
|
||||
if a, ok := aliases[name]; ok {
|
||||
name = a
|
||||
}
|
||||
|
||||
const reqe = "required="
|
||||
for _, p := range parts[1:] {
|
||||
switch {
|
||||
case p == "nomerge":
|
||||
case p == "required":
|
||||
y.required = append(y.required, name)
|
||||
case strings.HasPrefix(p, reqe):
|
||||
p = p[len(reqe):]
|
||||
y.sRequired[p] = append(y.sRequired[p], name)
|
||||
default:
|
||||
panic(f.Name + ": unknown tag: " + p)
|
||||
}
|
||||
}
|
||||
|
||||
// Ext means this is where we squirrel away extensions
|
||||
if name == "Ext" {
|
||||
// stmt is the extension to put into v at for field f.
|
||||
y.addext = func(stmt *Statement, v, _ reflect.Value) error {
|
||||
if v.Type() != at {
|
||||
panic(fmt.Sprintf("given type %s, need type %s", v.Type(), at))
|
||||
}
|
||||
fv := v.Elem().Field(i)
|
||||
fv.Set(reflect.Append(fv, reflect.ValueOf(stmt)))
|
||||
return nil
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// descend runs initType on dt if it has not already done so.
|
||||
descend := func(name string, dt reflect.Type) {
|
||||
switch nameMap[name] {
|
||||
case nil:
|
||||
nameMap[name] = dt
|
||||
initTypes(dt) // Make sure that structure type is included
|
||||
case dt:
|
||||
default:
|
||||
panic("redeclared type " + name)
|
||||
}
|
||||
}
|
||||
|
||||
// Create a function, fn, that will build the field from a
|
||||
// Statement. These functions are used when actually making
|
||||
// an AST from a Statement Tree.
|
||||
var fn func(*Statement, reflect.Value, reflect.Value, *typeDictionary) error
|
||||
|
||||
// The field can be a pointer, a slice or a string
|
||||
switch f.Type.Kind() {
|
||||
default:
|
||||
panic(fmt.Sprintf("invalid type: %v", f.Type.Kind()))
|
||||
|
||||
case reflect.Interface:
|
||||
// The only case of this should be the "Parent" field.
|
||||
if name != "Parent" {
|
||||
panic(fmt.Sprintf("interface field is %s, not Parent", name))
|
||||
}
|
||||
fn = func(stmt *Statement, v, p reflect.Value, types *typeDictionary) error {
|
||||
if !p.Type().Implements(nodeType) {
|
||||
panic(fmt.Sprintf("invalid interface: %v", f.Type.Kind()))
|
||||
}
|
||||
v.Elem().Field(i).Set(p)
|
||||
return nil
|
||||
}
|
||||
case reflect.String:
|
||||
// The only case of this should be the "Name" field
|
||||
if name != "Name" {
|
||||
panic(fmt.Sprintf("string field is %s, not Name", name))
|
||||
}
|
||||
fn = func(stmt *Statement, v, _ reflect.Value, types *typeDictionary) error {
|
||||
if v.Type() != at {
|
||||
panic(fmt.Sprintf("got type %v, want %v", v.Type(), at))
|
||||
}
|
||||
fv := v.Elem().Field(i)
|
||||
if fv.String() != "" {
|
||||
return errors.New(stmt.Keyword + ": already set")
|
||||
}
|
||||
|
||||
v.Elem().Field(i).SetString(stmt.Argument)
|
||||
return nil
|
||||
}
|
||||
|
||||
case reflect.Ptr:
|
||||
if f.Type == statementType {
|
||||
// The only case of this should be the
|
||||
// "Statement" field
|
||||
if name != "Statement" {
|
||||
panic(fmt.Sprintf("string field is %s, not Statement", name))
|
||||
}
|
||||
fn = func(stmt *Statement, v, _ reflect.Value, types *typeDictionary) error {
|
||||
if v.Type() != at {
|
||||
panic(fmt.Sprintf("got type %v, want %v", v.Type(), at))
|
||||
}
|
||||
v.Elem().Field(i).Set(reflect.ValueOf(stmt))
|
||||
return nil
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
// Make sure our field type is also setup.
|
||||
descend(name, f.Type)
|
||||
|
||||
fn = func(stmt *Statement, v, p reflect.Value, types *typeDictionary) error {
|
||||
if v.Type() != at {
|
||||
panic(fmt.Sprintf("given type %s, need type %s", v.Type(), at))
|
||||
}
|
||||
fv := v.Elem().Field(i)
|
||||
if !fv.IsNil() {
|
||||
return errors.New(stmt.Keyword + ": already set")
|
||||
}
|
||||
|
||||
// Use build to build the value for this field.
|
||||
sv, err := build(stmt, v, types)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
v.Elem().Field(i).Set(sv)
|
||||
return nil
|
||||
}
|
||||
|
||||
case reflect.Slice:
|
||||
// A slice at this point is always a slice of
|
||||
// substructures. We may see the same keyword multiple
|
||||
// times, each time we see it we just append to the
|
||||
// slice.
|
||||
st := f.Type.Elem()
|
||||
switch st.Kind() {
|
||||
default:
|
||||
panic(fmt.Sprintf("invalid type: %v", st.Kind()))
|
||||
case reflect.Ptr:
|
||||
descend(name, st)
|
||||
fn = func(stmt *Statement, v, p reflect.Value, types *typeDictionary) error {
|
||||
if v.Type() != at {
|
||||
panic(fmt.Sprintf("given type %s, need type %s", v.Type(), at))
|
||||
}
|
||||
sv, err := build(stmt, v, types)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fv := v.Elem().Field(i)
|
||||
fv.Set(reflect.Append(fv, sv))
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
y.funcs[name] = fn
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,538 @@
|
||||
// Copyright 2015 Google Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package yang
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type MainNode struct {
|
||||
Name string `yang:"Name,nomerge"`
|
||||
Source *Statement `yang:"Statement,nomerge"`
|
||||
Parent Node `yang:"Parent,nomerge"`
|
||||
Extensions []*Statement `yang:"Ext"`
|
||||
|
||||
Field *Value `yang:"field"`
|
||||
Slice []*Value `yang:"slice"`
|
||||
ChildNode *SubNode `yang:"child_node"`
|
||||
ChildSlice []*SubNode `yang:"child_slice"`
|
||||
ReqNode *ReqNode `yang:"req_node"`
|
||||
MainField *Value `yang:"main_field,required=main_node"`
|
||||
AltField *Value `yang:"alt_field,required=alt_node"`
|
||||
}
|
||||
|
||||
func (m *MainNode) Kind() string {
|
||||
if m.AltField != nil {
|
||||
return "alt_node"
|
||||
}
|
||||
return "main_node"
|
||||
}
|
||||
|
||||
func (m *MainNode) ParentNode() Node { return m.Parent }
|
||||
func (m *MainNode) NName() string { return m.Name }
|
||||
func (m *MainNode) Statement() *Statement { return m.Source }
|
||||
func (m *MainNode) Exts() []*Statement { return m.Extensions }
|
||||
|
||||
func (m *MainNode) checkEqual(n Node) string {
|
||||
o, ok := n.(*MainNode)
|
||||
if !ok {
|
||||
return fmt.Sprintf("expected *MainNode, got %T", n)
|
||||
}
|
||||
if m.Name != o.Name {
|
||||
return fmt.Sprintf("got name %s, want %s", o.Name, m.Name)
|
||||
}
|
||||
if s := m.Source.checkEqual(o.Source); s != "" {
|
||||
return s
|
||||
}
|
||||
if (m.Field == nil) != (o.Field == nil) {
|
||||
if m.Field == nil {
|
||||
return "unexpected field entry"
|
||||
}
|
||||
return "missing expected field entry"
|
||||
}
|
||||
if m.Field != nil {
|
||||
if m.Field.Name != o.Field.Name {
|
||||
return fmt.Sprintf("got field of %s, want %s", o.Field.Name, m.Field.Name)
|
||||
}
|
||||
}
|
||||
if len(m.Slice) != len(o.Slice) {
|
||||
return fmt.Sprintf("got slice of %d, want slice of %d", len(o.Slice), len(m.Slice))
|
||||
}
|
||||
for x, s1 := range m.Slice {
|
||||
s2 := o.Slice[x]
|
||||
if s1.Name != s2.Name {
|
||||
return fmt.Sprintf("slice[%d] got %s, want %s", x, s2.Name, s1.Name)
|
||||
}
|
||||
}
|
||||
if (m.ChildNode == nil) != (o.ChildNode == nil) {
|
||||
if m.ChildNode == nil {
|
||||
return "unexpected child_node entry"
|
||||
}
|
||||
return "missing expected child_node entry"
|
||||
}
|
||||
if m.ChildNode != nil {
|
||||
if s := m.ChildNode.checkEqual(o.ChildNode); s != "" {
|
||||
return fmt.Sprintf("child_node: %s", s)
|
||||
}
|
||||
}
|
||||
if len(m.ChildSlice) != len(o.ChildSlice) {
|
||||
return fmt.Sprintf("got child_slice of %d, want slice of %d", len(o.ChildSlice), len(m.ChildSlice))
|
||||
}
|
||||
for x, s1 := range m.ChildSlice {
|
||||
s2 := o.ChildSlice[x]
|
||||
if s := s1.checkEqual(s2); s != "" {
|
||||
return fmt.Sprintf("child_slice[%d]: %s", x, s)
|
||||
}
|
||||
}
|
||||
if (m.ReqNode == nil) != (o.ReqNode == nil) {
|
||||
if m.ReqNode == nil {
|
||||
return "unexpected req_node entry"
|
||||
}
|
||||
return "missing expected req_node entry"
|
||||
}
|
||||
if m.ReqNode != nil {
|
||||
if s := m.ReqNode.checkEqual(o.ReqNode); s != "" {
|
||||
return fmt.Sprintf("req_node: %s", s)
|
||||
}
|
||||
}
|
||||
if (m.AltField == nil) != (o.AltField == nil) {
|
||||
if m.AltField == nil {
|
||||
return "unexpected alt_field entry"
|
||||
}
|
||||
return "missing expected alt_field entry"
|
||||
}
|
||||
if m.AltField != nil {
|
||||
if m.AltField.Name != o.AltField.Name {
|
||||
return fmt.Sprintf("got alt_field of %s, want %s", o.AltField.Name, m.AltField.Name)
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
type SubNode struct {
|
||||
Name string `yang:"Name,nomerge"`
|
||||
Source *Statement `yang:"Statement,nomerge"`
|
||||
Parent Node `yang:"Parent,nomerge"`
|
||||
Extensions []*Statement `yang:"Ext"`
|
||||
|
||||
SubField *Value `yang:"sub_field"`
|
||||
}
|
||||
|
||||
func (SubNode) Kind() string { return "sub_node" }
|
||||
func (s *SubNode) ParentNode() Node { return s.Parent }
|
||||
func (s *SubNode) NName() string { return s.Name }
|
||||
func (s *SubNode) Statement() *Statement { return s.Source }
|
||||
func (s *SubNode) Exts() []*Statement { return s.Extensions }
|
||||
|
||||
func (s *SubNode) checkEqual(o *SubNode) string {
|
||||
if s.Name != o.Name {
|
||||
return fmt.Sprintf("got name %s, want %s", o.Name, s.Name)
|
||||
}
|
||||
if s := s.Source.checkEqual(o.Source); s != "" {
|
||||
return s
|
||||
}
|
||||
if (s.SubField == nil) != (o.SubField == nil) {
|
||||
if s.SubField == nil {
|
||||
return "unexpected sub_field entry"
|
||||
}
|
||||
return "missing expected sub_field entry"
|
||||
}
|
||||
if s.SubField != nil {
|
||||
if s.SubField.Name != o.SubField.Name {
|
||||
return fmt.Sprintf("got sub_field of %s, want %s", o.SubField.Name, s.SubField.Name)
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
type ReqNode struct {
|
||||
Name string `yang:"Name,nomerge"`
|
||||
Source *Statement `yang:"Statement,nomerge"`
|
||||
Parent Node `yang:"Parent,nomerge"`
|
||||
|
||||
ReqField *Value `yang:"req_field,required"`
|
||||
AltReqField *Value `yang:"alt_req_field,required=alt_req_node"`
|
||||
Field *Value `yang:"field"`
|
||||
}
|
||||
|
||||
func (s *ReqNode) Kind() string {
|
||||
return "req_node"
|
||||
}
|
||||
func (s *ReqNode) ParentNode() Node { return s.Parent }
|
||||
func (s *ReqNode) NName() string { return s.Name }
|
||||
func (s *ReqNode) Statement() *Statement { return s.Source }
|
||||
func (m *ReqNode) Exts() []*Statement { return nil }
|
||||
|
||||
func (s *ReqNode) checkEqual(o *ReqNode) string {
|
||||
if s.Name != o.Name {
|
||||
return fmt.Sprintf("got name %s, want %s", o.Name, s.Name)
|
||||
}
|
||||
if s := s.Source.checkEqual(o.Source); s != "" {
|
||||
return s
|
||||
}
|
||||
if (s.ReqField == nil) != (o.ReqField == nil) {
|
||||
if s.ReqField == nil {
|
||||
return "unexpected req_field entry"
|
||||
}
|
||||
return "missing expected req_field entry"
|
||||
}
|
||||
if s.ReqField != nil {
|
||||
if s.ReqField.Name != o.ReqField.Name {
|
||||
return fmt.Sprintf("got req_field of %s, want %s", o.ReqField.Name, s.ReqField.Name)
|
||||
}
|
||||
}
|
||||
if (s.AltReqField == nil) != (o.AltReqField == nil) {
|
||||
if s.AltReqField == nil {
|
||||
return "unexpected alt_req_field entry"
|
||||
}
|
||||
return "missing expected alt_req_field entry"
|
||||
}
|
||||
if s.AltReqField != nil {
|
||||
if s.AltReqField.Name != o.AltReqField.Name {
|
||||
return fmt.Sprintf("got alt_req_field of %s, want %s", o.AltReqField.Name, s.AltReqField.Name)
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (s *Statement) checkEqual(o *Statement) string {
|
||||
if (s == nil) != (o == nil) {
|
||||
var b bytes.Buffer
|
||||
if s == nil {
|
||||
o.Write(&b, "")
|
||||
return fmt.Sprintf("unexpected Statement entry\n%s", &b)
|
||||
}
|
||||
s.Write(&b, "")
|
||||
return fmt.Sprintf("missing expected Statement entry\n%s", &b)
|
||||
}
|
||||
if s == nil {
|
||||
return ""
|
||||
}
|
||||
var b1, b2 bytes.Buffer
|
||||
s.Write(&b1, "")
|
||||
o.Write(&b2, "")
|
||||
ss := b1.String()
|
||||
os := b2.String()
|
||||
if ss != os {
|
||||
return fmt.Sprintf("got statement:\n%swant:\n%s", os, ss)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func TestAST(t *testing.T) {
|
||||
// Teach the AST parser about our testing nodes
|
||||
type meta struct {
|
||||
MainNode []*MainNode `yang:"main_node"`
|
||||
}
|
||||
|
||||
old_aliases := aliases
|
||||
aliases = map[string]string{
|
||||
"alt_node": "main_node",
|
||||
}
|
||||
|
||||
for _, tt := range []struct {
|
||||
line int
|
||||
in string
|
||||
out *MainNode
|
||||
err string
|
||||
}{
|
||||
{
|
||||
line: line(),
|
||||
in: `
|
||||
main_node the_node {
|
||||
// This test is testing to make sure unknown statements, that
|
||||
// might be extensions, are properly put in the Extensions slice.
|
||||
// When an extension is used, it must be of the form "prefix:name".
|
||||
// See https://tools.ietf.org/html/rfc6020#section-7.17
|
||||
ex:ext1 value1;
|
||||
ex:ext2 value2;
|
||||
main_field foo;
|
||||
}
|
||||
`,
|
||||
out: &MainNode{
|
||||
Source: SA("main_node", "the_node",
|
||||
SA("ex:ext1", "value1"),
|
||||
SA("ex:ext2", "value2"),
|
||||
SA("main_field", "foo")),
|
||||
Name: "the_node",
|
||||
Extensions: []*Statement{
|
||||
SA("ex:ext1", "value1"),
|
||||
SA("ex:ext2", "value2"),
|
||||
},
|
||||
MainField: &Value{
|
||||
Name: "foo",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
line: line(),
|
||||
in: `
|
||||
main_node the_node {
|
||||
// This test tests fields, slices, and sub-statements.
|
||||
field field_value;
|
||||
slice sl1;
|
||||
slice sl2;
|
||||
child_node the_child {
|
||||
sub_field val1;
|
||||
}
|
||||
child_slice element1 {
|
||||
sub_field el1;
|
||||
}
|
||||
child_slice element2 {
|
||||
sub_field el2;
|
||||
}
|
||||
main_field foo;
|
||||
}`,
|
||||
out: &MainNode{
|
||||
Source: SA("main_node", "the_node",
|
||||
SA("field", "field_value"),
|
||||
SA("slice", "sl1"),
|
||||
SA("slice", "sl2"),
|
||||
SA("child_node", "the_child",
|
||||
SA("sub_field", "val1")),
|
||||
SA("child_slice", "element1",
|
||||
SA("sub_field", "el1")),
|
||||
SA("child_slice", "element2",
|
||||
SA("sub_field", "el2")),
|
||||
SA("main_field", "foo"),
|
||||
),
|
||||
Name: "the_node",
|
||||
Field: &Value{
|
||||
Name: "field_value",
|
||||
},
|
||||
Slice: []*Value{
|
||||
{
|
||||
Name: "sl1",
|
||||
},
|
||||
{
|
||||
Name: "sl2",
|
||||
},
|
||||
},
|
||||
ChildNode: &SubNode{
|
||||
Source: SA("child_node", "the_child",
|
||||
SA("sub_field", "val1")),
|
||||
Name: "the_child",
|
||||
SubField: &Value{
|
||||
Name: "val1",
|
||||
},
|
||||
},
|
||||
ChildSlice: []*SubNode{
|
||||
{
|
||||
Source: SA("child_slice", "element1",
|
||||
SA("sub_field", "el1")),
|
||||
Name: "element1",
|
||||
SubField: &Value{
|
||||
Name: "el1",
|
||||
},
|
||||
},
|
||||
{
|
||||
Source: SA("child_slice", "element2",
|
||||
SA("sub_field", "el2")),
|
||||
Name: "element2",
|
||||
SubField: &Value{
|
||||
Name: "el2",
|
||||
},
|
||||
},
|
||||
},
|
||||
MainField: &Value{
|
||||
Name: "foo",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
line: line(),
|
||||
in: `
|
||||
// This test tests for the presence of a required field.
|
||||
// main_node requires the field named "main_field".
|
||||
main_node the_node {
|
||||
main_field value1 {
|
||||
}
|
||||
}
|
||||
`,
|
||||
out: &MainNode{
|
||||
Source: SA("main_node", "the_node",
|
||||
SA("main_field", "value1"),
|
||||
),
|
||||
Name: "the_node",
|
||||
MainField: &Value{
|
||||
Name: "value1",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
line: line(),
|
||||
in: `
|
||||
// This test tests for the presence of a required= field.
|
||||
// alt_node requires the field named "alt_field".
|
||||
alt_node the_node {
|
||||
alt_field value2 {
|
||||
}
|
||||
}
|
||||
`,
|
||||
out: &MainNode{
|
||||
Source: SA("alt_node", "the_node",
|
||||
SA("alt_field", "value2"),
|
||||
),
|
||||
Name: "the_node",
|
||||
AltField: &Value{
|
||||
Name: "value2",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
line: line(),
|
||||
in: `
|
||||
main_node the_node {
|
||||
// This test tests that extensions are rejected when the node is not
|
||||
// supposed to contain them.
|
||||
req_node value1 {
|
||||
req_field foo {
|
||||
}
|
||||
ex:ext1 value1;
|
||||
ex:ext2 value2;
|
||||
}
|
||||
}
|
||||
`,
|
||||
err: `ast.yang:8:3: no extension function`,
|
||||
},
|
||||
{
|
||||
line: line(),
|
||||
in: `
|
||||
main_node the_node {
|
||||
// This test tests for the presence of a required field.
|
||||
// req_node requires the field named "req_field".
|
||||
req_node value1 {
|
||||
req_field foo {
|
||||
}
|
||||
}
|
||||
main_field foo;
|
||||
}
|
||||
`,
|
||||
out: &MainNode{
|
||||
Source: SA("main_node", "the_node",
|
||||
SA("req_node", "value1",
|
||||
SA("req_field", "foo")),
|
||||
SA("main_field", "foo"),
|
||||
),
|
||||
Name: "the_node",
|
||||
ReqNode: &ReqNode{
|
||||
Source: SA("req_node", "value1",
|
||||
SA("req_field", "foo")),
|
||||
Name: "value1",
|
||||
ReqField: &Value{
|
||||
Name: "foo",
|
||||
},
|
||||
},
|
||||
MainField: &Value{
|
||||
Name: "foo",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
line: line(),
|
||||
in: `
|
||||
main_node the_node {
|
||||
// This test tests that the absence of a required field fails.
|
||||
// req_node requires the field named "req_field".
|
||||
req_node value1 {
|
||||
}
|
||||
main_field foo;
|
||||
}
|
||||
`,
|
||||
err: `ast.yang:5:2: missing required req_node field: req_field`,
|
||||
},
|
||||
{
|
||||
line: line(),
|
||||
in: `
|
||||
main_node the_node {
|
||||
// This test tests that the absence of a required field.
|
||||
// main_node requires the field named "main_field".
|
||||
req_node value1 {
|
||||
req_field foo {
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
err: `ast.yang:2:1: missing required main_node field: main_field`,
|
||||
},
|
||||
{
|
||||
line: line(),
|
||||
in: `
|
||||
// This test tests that the alt_field, specified with
|
||||
// required=alt_node, causes the AST construction to error when a
|
||||
// main_node contains it.
|
||||
main_node the_node {
|
||||
main_field foo;
|
||||
alt_field foo;
|
||||
}
|
||||
`,
|
||||
err: `ast.yang:5:1: unknown main_node field: alt_field`,
|
||||
},
|
||||
{
|
||||
line: line(),
|
||||
in: `
|
||||
// This test tests that required=alt_node enforces that
|
||||
// alt_node must contain it.
|
||||
alt_node the_node {
|
||||
main_field foo;
|
||||
alt_field foo;
|
||||
}
|
||||
`,
|
||||
err: `ast.yang:4:1: unknown alt_node field: main_field`,
|
||||
},
|
||||
{
|
||||
line: line(),
|
||||
in: `
|
||||
// This test tests that required=alt_node enforces that
|
||||
// alt_node must contain it.
|
||||
alt_node the_node {
|
||||
}
|
||||
`,
|
||||
err: `ast.yang:4:1: missing required alt_node field: alt_field`,
|
||||
},
|
||||
} {
|
||||
ss, err := Parse(tt.in, "ast.yang")
|
||||
if err != nil {
|
||||
t.Errorf("%d: %v", tt.line, err)
|
||||
continue
|
||||
}
|
||||
if len(ss) != 1 {
|
||||
t.Errorf("%d: got %d results, want 1", tt.line, len(ss))
|
||||
continue
|
||||
}
|
||||
|
||||
typeDict := newTypeDictionary()
|
||||
initTypes(reflect.TypeOf(&meta{}))
|
||||
|
||||
ast, err := buildASTWithTypeDict(ss[0], typeDict)
|
||||
switch {
|
||||
case err == nil && tt.err == "":
|
||||
if s := tt.out.checkEqual(ast); s != "" {
|
||||
t.Errorf("%d: %s", tt.line, s)
|
||||
}
|
||||
case err == nil:
|
||||
t.Errorf("%d: did not get expected error %s", tt.line, tt.err)
|
||||
case tt.err == "":
|
||||
t.Errorf("%d: %v", tt.line, err)
|
||||
case err.Error() != tt.err:
|
||||
t.Errorf("%d: got error %v, want %s", tt.line, err, tt.err)
|
||||
}
|
||||
}
|
||||
|
||||
aliases = old_aliases
|
||||
}
|
||||
@@ -0,0 +1,571 @@
|
||||
// Copyright 2015 Google Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package yang
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestBGP simply makes sure we are able to parse a version of Anees's
|
||||
// BGP model. We don't actually attempt to validate we got the right
|
||||
// AST. ast_test.go will test smaller peices to make sure the basics
|
||||
// of BuildAST produce expected results.
|
||||
func TestBGP(t *testing.T) {
|
||||
ss, err := Parse(bgp, "bgp.yang")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(ss) != 1 {
|
||||
t.Fatalf("got %d results, want 1", len(ss))
|
||||
}
|
||||
typeDict := newTypeDictionary()
|
||||
initTypes(reflect.TypeOf(&meta{}))
|
||||
if _, err := buildASTWithTypeDict(ss[0], typeDict); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
var bgp = `
|
||||
module google-bgp {
|
||||
|
||||
yang-version "1";
|
||||
|
||||
// namespace
|
||||
namespace "http://google.com/yang/google-bgp-protocol-cfg";
|
||||
|
||||
prefix "gbgp";
|
||||
|
||||
// import some basic types -- no other dependency on
|
||||
// in-progress models in draft status
|
||||
import ietf-inet-types { prefix inet; }
|
||||
|
||||
|
||||
// meta
|
||||
organization "Google, Inc.";
|
||||
|
||||
contact
|
||||
"Google, Inc.
|
||||
1600 Amphitheatre Way
|
||||
Mountain View, CA 94043";
|
||||
|
||||
description
|
||||
"This module describes a YANG model for BGP protocol
|
||||
configuration.It is a limited subset of all of the configuration
|
||||
parameters available in the variety of vendor implementations,
|
||||
hence it is expected that it would be augmented with vendor-
|
||||
specific configuration data as needed.Additional modules or
|
||||
submodules to handle other aspects of BGP configuration,
|
||||
including policy, VRFs, and additional address families are also
|
||||
expected.";
|
||||
|
||||
revision "2014-07-07" {
|
||||
description
|
||||
"Initial revision";
|
||||
reference "TBD";
|
||||
}
|
||||
|
||||
|
||||
identity afi-type {
|
||||
description
|
||||
"base identity type for BGP address family identifiers (AFI)";
|
||||
reference "IETF RFC 4760";
|
||||
}
|
||||
|
||||
identity safi-type {
|
||||
description
|
||||
"base identity type for BGP subsequent address family
|
||||
identifiers (SAFI)";
|
||||
reference "IETF RFC 4760";
|
||||
}
|
||||
|
||||
identity ipv4-afi {
|
||||
base afi-type;
|
||||
description
|
||||
"IPv4 AF identifier";
|
||||
}
|
||||
|
||||
identity ipv6-afi {
|
||||
base afi-type;
|
||||
description
|
||||
"IPv6 AF identifier";
|
||||
}
|
||||
|
||||
identity unicast-safi {
|
||||
base safi-type;
|
||||
description
|
||||
"unicast SAFI identifier";
|
||||
}
|
||||
|
||||
identity labeled-unicast-safi {
|
||||
base safi-type;
|
||||
description
|
||||
"labeled unicast SAFI identifier";
|
||||
reference "RFC 3107 - Carrying Label Information in BGP-4";
|
||||
}
|
||||
|
||||
|
||||
typedef peer-group-type {
|
||||
type enumeration {
|
||||
enum INTERNAL {
|
||||
description "internal (iBGP) peer";
|
||||
}
|
||||
enum EXTERNAL {
|
||||
description "external (eBGP) peer";
|
||||
}
|
||||
}
|
||||
description
|
||||
"labels a peer as explicitly internal or external";
|
||||
}
|
||||
|
||||
|
||||
typedef remove-private-as-option {
|
||||
type enumeration {
|
||||
enum ALL {
|
||||
description "remove all private ASes in the path";
|
||||
}
|
||||
enum REPLACE {
|
||||
description "replace private ASes with local AS";
|
||||
}
|
||||
}
|
||||
description
|
||||
"set of options for configuring how private AS path numbers
|
||||
are removed from advertisements";
|
||||
}
|
||||
|
||||
typedef percentage {
|
||||
type uint8 {
|
||||
range "0..100";
|
||||
}
|
||||
description
|
||||
"Integer indicating a percentage value";
|
||||
}
|
||||
|
||||
typedef rr-cluster-id-type {
|
||||
type union {
|
||||
type uint32;
|
||||
type inet:ipv4-address;
|
||||
}
|
||||
description
|
||||
"union type for route reflector cluster ids:
|
||||
option 1: 4-byte number
|
||||
option 2: IP address";
|
||||
}
|
||||
|
||||
grouping bgp-common-configuration {
|
||||
description "Common configuration across neighbors, groups,
|
||||
etc.";
|
||||
|
||||
leaf description {
|
||||
type string;
|
||||
description
|
||||
"A textual description of the peer or group";
|
||||
}
|
||||
container use-multiple-paths {
|
||||
description
|
||||
"Configuration of BGP multipath to enable load sharing across
|
||||
multiple paths to peers.";
|
||||
leaf allow-multiple-as {
|
||||
type boolean;
|
||||
default "false";
|
||||
description
|
||||
"Allow multipath to use paths from different neighboring
|
||||
ASes. The default is to only consider multiple paths from
|
||||
the same neighboring AS.";
|
||||
}
|
||||
leaf maximum-paths {
|
||||
type uint32;
|
||||
default 1;
|
||||
description
|
||||
"Maximum number of parallel paths to consider when using
|
||||
BGP multipath. The default is to use a single path.";
|
||||
reference "draft-ietf-idr-add-paths-09.txt";
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
grouping bgp-group-common-configuration {
|
||||
description "Configuration items that are applied at the peer
|
||||
group level";
|
||||
}
|
||||
|
||||
grouping bgp-group-neighbor-common-configuration {
|
||||
description "Configuration options for peer and group context";
|
||||
|
||||
leaf auth-password {
|
||||
type string;
|
||||
description
|
||||
"Configures an authentication password for use with
|
||||
neighboring devices.";
|
||||
}
|
||||
|
||||
container timers {
|
||||
description "Configuration of various BGP timers";
|
||||
|
||||
leaf hold-time {
|
||||
type decimal64 {
|
||||
fraction-digits 2;
|
||||
}
|
||||
default 90;
|
||||
// hold-time should typically be set to 3x the
|
||||
// keepalive-interval -- create a constraint for this?
|
||||
description
|
||||
"Time interval in seconds that a BGP session will be
|
||||
considered active in the absence of keepalive or other
|
||||
messages from the peer";
|
||||
reference
|
||||
"RFC 1771 - A Border Gateway Protocol 4";
|
||||
}
|
||||
|
||||
leaf keepalive-interval {
|
||||
type decimal64 {
|
||||
fraction-digits 2;
|
||||
}
|
||||
default 30;
|
||||
description
|
||||
"Time interval in seconds between transmission of keepalive
|
||||
messages to the neighbor. Typically set to 1/3 the
|
||||
hold-time.";
|
||||
}
|
||||
|
||||
leaf advertisement-interval {
|
||||
type decimal64 {
|
||||
fraction-digits 2;
|
||||
}
|
||||
default 30;
|
||||
description
|
||||
"Mininum time interval in seconds between transmission
|
||||
of BGP updates to neighbors";
|
||||
reference
|
||||
"RFC 1771 - A Border Gateway Protocol 4";
|
||||
}
|
||||
|
||||
leaf connect-retry {
|
||||
type decimal64 {
|
||||
fraction-digits 2;
|
||||
}
|
||||
default 30;
|
||||
description
|
||||
"Time interval in seconds between attempts to establish a
|
||||
session with the peer.";
|
||||
}
|
||||
}
|
||||
|
||||
container ebgp-multihop {
|
||||
description
|
||||
"Configure multihop BGP for peers that are not directly
|
||||
connected";
|
||||
|
||||
leaf multihop-ttl {
|
||||
type uint8;
|
||||
default 1;
|
||||
description
|
||||
"Time-to-live for multihop BGP sessions. The default
|
||||
value of 1 is for directly connected peers (i.e.,
|
||||
multihop disabled";
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
container route-reflector {
|
||||
description
|
||||
"Configure the local router as a route-reflector
|
||||
server";
|
||||
leaf route-reflector-clusterid {
|
||||
type rr-cluster-id-type;
|
||||
description
|
||||
"route-reflector cluster id to use when local router is
|
||||
configured as a route reflector. Commonly set at the group
|
||||
level, but allows a different cluster
|
||||
id to be set for each neighbor.";
|
||||
}
|
||||
|
||||
leaf route-reflector-client {
|
||||
type boolean;
|
||||
default "false";
|
||||
description
|
||||
"configure the neighbor as a route reflector client";
|
||||
}
|
||||
}
|
||||
|
||||
leaf remove-private-as {
|
||||
// could also make this a container with a flag to enable
|
||||
// remove-private and separate option. here, option implies
|
||||
// remove-private is enabled.
|
||||
type remove-private-as-option;
|
||||
description
|
||||
"Remove private AS numbers from updates sent to peers";
|
||||
}
|
||||
|
||||
|
||||
container bgp-logging-options {
|
||||
description
|
||||
"Configure various tracing/logging options for BGP peers
|
||||
or groups. Expected that additional vendor-specific log
|
||||
options would augment this container";
|
||||
|
||||
leaf log-neighbor-state-changes {
|
||||
type boolean;
|
||||
default "true";
|
||||
description
|
||||
"Configure logging of peer state changes. Default is
|
||||
to enable logging of peer state changes.";
|
||||
}
|
||||
}
|
||||
|
||||
container transport-options {
|
||||
description
|
||||
"Transport protocol options for BGP sessions";
|
||||
|
||||
leaf tcp-mss {
|
||||
type uint16;
|
||||
description
|
||||
"Sets the max segment size for BGP TCP sessions";
|
||||
}
|
||||
|
||||
leaf passive-mode {
|
||||
type boolean;
|
||||
description
|
||||
"Wait for peers to issue requests to open a BGP session,
|
||||
rather than initiating sessions from the local router";
|
||||
}
|
||||
}
|
||||
|
||||
leaf local-address {
|
||||
type inet:ip-address;
|
||||
description
|
||||
"Set the local IP (either IPv4 or IPv6) address to use for
|
||||
the session when sending BGP update messages";
|
||||
}
|
||||
|
||||
leaf route-flap-damping {
|
||||
type boolean;
|
||||
description
|
||||
"Enable route flap damping";
|
||||
}
|
||||
}
|
||||
|
||||
grouping bgp-address-family-common-configuration {
|
||||
description "Configuration options per address family context";
|
||||
|
||||
list address-family {
|
||||
|
||||
key "afi-name";
|
||||
description
|
||||
"Per address-family configuration, uniquely identified by AF
|
||||
name";
|
||||
leaf afi-name {
|
||||
type identityref {
|
||||
base "afi-type";
|
||||
}
|
||||
description
|
||||
"Address family names are drawn from the afi-type base
|
||||
identity, which has specific address family types as
|
||||
derived identities";
|
||||
}
|
||||
|
||||
list subsequent-address-family {
|
||||
|
||||
key "safi-name";
|
||||
description
|
||||
"Per subsequent address family configuration, under a
|
||||
specific address family";
|
||||
|
||||
leaf safi-name {
|
||||
// do we need to specify which SAFIs are possible within
|
||||
// each AF? with the current set of AF/SAFI, all are
|
||||
/// applicable
|
||||
type identityref {
|
||||
base "safi-type";
|
||||
}
|
||||
description
|
||||
"Within each address family, subsequent address family
|
||||
names are drawn from the subsequent-address-family base
|
||||
identity";
|
||||
}
|
||||
|
||||
|
||||
container prefix-limit {
|
||||
description
|
||||
"Configure the maximum number of prefixes that will be
|
||||
accepted from a peer";
|
||||
|
||||
leaf max-prefixes {
|
||||
type uint32;
|
||||
description
|
||||
"Maximum number of prefixes that will be accepted from
|
||||
the neighbor";
|
||||
}
|
||||
|
||||
leaf shutdown-threshold-pct {
|
||||
type percentage;
|
||||
description
|
||||
"Threshold on number of prefixes that can be received
|
||||
from a neighbor before generation of warning messages
|
||||
or log entries. Expressed as a percentage of
|
||||
max-prefixes.";
|
||||
}
|
||||
|
||||
leaf restart-timer {
|
||||
type decimal64 {
|
||||
fraction-digits 2;
|
||||
}
|
||||
units "seconds";
|
||||
description
|
||||
"Time interval in seconds after which the BGP session
|
||||
is reestablished after being torn down due to exceeding
|
||||
the max-prefixes limit.";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
container bgp {
|
||||
description "Top-level configuration data for the BGP router";
|
||||
|
||||
container global {
|
||||
description
|
||||
"Top-level bgp protocol options applied across peer-groups,
|
||||
neighbors, and address families";
|
||||
|
||||
leaf as {
|
||||
type inet:as-number;
|
||||
mandatory "true";
|
||||
description
|
||||
"Local autonomous system number of the router. Uses
|
||||
the as-number type defined in RFC 6991";
|
||||
}
|
||||
leaf router-id {
|
||||
type inet:ipv4-address;
|
||||
description
|
||||
"Router id of the router, expressed as an
|
||||
IPv4 address";
|
||||
// there is a typedef for this in draft module ietf-routing
|
||||
// but it does not use an appropriate type
|
||||
}
|
||||
container route-selection-options {
|
||||
description
|
||||
"Set of configuration options that govern best
|
||||
path selection";
|
||||
leaf always-compare-med {
|
||||
type boolean;
|
||||
default "false";
|
||||
description
|
||||
"Compare multi-exit discriminator (MED) value from
|
||||
different ASes when selecting the best route. The
|
||||
default behavior is to only compare MEDs for paths
|
||||
received from the same AS.";
|
||||
}
|
||||
leaf ignore-as-path {
|
||||
type boolean;
|
||||
default "false";
|
||||
description
|
||||
"Ignore the AS path length when selecting the best path.
|
||||
The default is to use the AS path length and prefer paths
|
||||
with shorter length.";
|
||||
}
|
||||
leaf external-compare-router-id {
|
||||
type boolean;
|
||||
default "true";
|
||||
description
|
||||
"When comparing similar routes received from external
|
||||
BGP peers, use the router-id as a criterion to select
|
||||
the active path. The default is to use the router-id to
|
||||
select among similar routes.";
|
||||
}
|
||||
leaf advertise-inactive-routes {
|
||||
type boolean;
|
||||
default "false";
|
||||
description
|
||||
"Advertise inactive routes to external peers. The
|
||||
default is to only advertise active routes.";
|
||||
}
|
||||
}
|
||||
container default-route-distance {
|
||||
description
|
||||
"Administrative distance (or preference) assigned to
|
||||
routes received from different sources
|
||||
(external, internal, and local.)";
|
||||
leaf external-route-distance {
|
||||
type uint8 {
|
||||
range "1..255";
|
||||
}
|
||||
description
|
||||
"Administrative distance for routes learned from external
|
||||
BGP (eBGP)";
|
||||
}
|
||||
leaf internal-route-distance {
|
||||
type uint8 {
|
||||
range "1..255";
|
||||
}
|
||||
description
|
||||
"Administrative distance for routes learned from internal
|
||||
BGP (iBGP)";
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
uses bgp-address-family-common-configuration;
|
||||
|
||||
list peer-group {
|
||||
key "group-name";
|
||||
description
|
||||
"List of peer-groups, uniquely identified by the peer group
|
||||
names";
|
||||
leaf group-name {
|
||||
type string;
|
||||
description "Name of the peer group";
|
||||
}
|
||||
leaf group-type {
|
||||
type peer-group-type;
|
||||
description
|
||||
"Explicitly designate the peer group as internal (iBGP)
|
||||
or external (eBGP)";
|
||||
}
|
||||
uses bgp-common-configuration;
|
||||
uses bgp-address-family-common-configuration;
|
||||
uses bgp-group-neighbor-common-configuration;
|
||||
}
|
||||
|
||||
list neighbor {
|
||||
key "neighbor-address";
|
||||
description
|
||||
"List of BGP peers, uniquely identified by neighbor address";
|
||||
leaf neighbor-address {
|
||||
type inet:ip-address;
|
||||
description
|
||||
"Address of the BGP peer, either IPv4 or IPv6";
|
||||
}
|
||||
|
||||
leaf peer-as {
|
||||
type inet:as-number;
|
||||
mandatory "true";
|
||||
description
|
||||
"AS number of the peer";
|
||||
|
||||
}
|
||||
uses bgp-common-configuration;
|
||||
uses bgp-address-family-common-configuration;
|
||||
uses bgp-group-neighbor-common-configuration;
|
||||
}
|
||||
|
||||
}
|
||||
}`
|
||||
@@ -0,0 +1,94 @@
|
||||
// Copyright 2015 Google Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package yang
|
||||
|
||||
var knownWords = map[string]string{
|
||||
"Ietf": "IETF",
|
||||
}
|
||||
|
||||
// Is c an ASCII lower-case letter?
|
||||
func isASCIILower(c byte) bool {
|
||||
return 'a' <= c && c <= 'z'
|
||||
}
|
||||
|
||||
// Is c an ASCII digit?
|
||||
func isASCIIDigit(c byte) bool {
|
||||
return '0' <= c && c <= '9'
|
||||
}
|
||||
|
||||
// CamelCase returns a CamelCased name for a YANG identifier.
|
||||
// Currently this supports the output being used for a Go or proto identifier.
|
||||
// Dash and dot are first converted to underscore, and then any underscores
|
||||
// before a lower-case letter are removed, and the letter converted to
|
||||
// upper-case. Any input characters not part of the YANG identifier
|
||||
// specification (https://tools.ietf.org/html/rfc7950#section-6.2) are treated
|
||||
// as lower-case characters.
|
||||
// The first letter is always upper-case in order to be an exported name in Go.
|
||||
// There is a remote possibility of this rewrite causing a name collision, but
|
||||
// it's so remote we're prepared to pretend it's nonexistent - since the C++
|
||||
// generator lowercases names, it's extremely unlikely to have two fields with
|
||||
// different capitalizations. In short, _my_field-name_2 becomes XMyFieldName_2.
|
||||
func CamelCase(s string) string {
|
||||
if s == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
fix := func(c byte) byte {
|
||||
if c == '-' || c == '.' {
|
||||
return '_'
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
t := make([]byte, 0, 32)
|
||||
i := 0
|
||||
if fix(s[0]) == '_' {
|
||||
// Need a capital letter; drop the '_'.
|
||||
t = append(t, 'X')
|
||||
i++
|
||||
}
|
||||
|
||||
// Invariant: if the next letter is lower case, it must be converted
|
||||
// to upper case.
|
||||
// That is, we process a word at a time, where words are marked by _ or
|
||||
// upper case letter. Digits are treated as words.
|
||||
for ; i < len(s); i++ {
|
||||
c := fix(s[i])
|
||||
if c == '_' && i+1 < len(s) && isASCIILower(s[i+1]) {
|
||||
continue // Skip the underscore in s.
|
||||
}
|
||||
if isASCIIDigit(c) {
|
||||
t = append(t, c)
|
||||
continue
|
||||
}
|
||||
// Assume we have a letter now - if not, it's a bogus identifier.
|
||||
// The next word is a sequence of characters that must start upper case.
|
||||
if isASCIILower(c) {
|
||||
c ^= ' ' // Make it a capital letter.
|
||||
}
|
||||
start := len(t)
|
||||
t = append(t, c) // Guaranteed not lower case.
|
||||
// Accept lower case sequence that follows.
|
||||
for i+1 < len(s) && isASCIILower(s[i+1]) {
|
||||
i++
|
||||
t = append(t, s[i])
|
||||
}
|
||||
// If the word turns out to be a special word, then use that instead.
|
||||
if kn := knownWords[string(t[start:])]; kn != "" {
|
||||
t = append(t[:start], []byte(kn)...)
|
||||
}
|
||||
}
|
||||
return string(t)
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
// Copyright 2015 Google Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package yang
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestCamelCase(t *testing.T) {
|
||||
tests := []struct {
|
||||
in, want string
|
||||
}{
|
||||
{"one", "One"},
|
||||
{"one_two", "OneTwo"},
|
||||
{"__one__two__three__four", "XOne_Two_Three_Four"},
|
||||
{"one.two.three", "OneTwoThree"},
|
||||
{"one.two.three.", "OneTwoThree_"},
|
||||
{"_my_field_name_2", "XMyFieldName_2"},
|
||||
{"Something_Capped", "Something_Capped"},
|
||||
{"_Foo-bar", "XFooBar"},
|
||||
{"my_Name", "My_Name"},
|
||||
{"OneTwo", "OneTwo"},
|
||||
{"_", "X"},
|
||||
{"_a_", "XA_"},
|
||||
{"ietf-interface", "IETFInterface"},
|
||||
{"ietf-interface-1", "IETFInterface_1"},
|
||||
{"out-unicast.pkts", "OutUnicastPkts"},
|
||||
// Invalid input conversion behaviours:
|
||||
{"one/two", "One/two"},
|
||||
{"/one/two", "/one/two"},
|
||||
{"one:two", "One:two"},
|
||||
{"::one::two", "::one::two"},
|
||||
{"one|two", "One|two"},
|
||||
{"one||two", "One||two"},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
if got := CamelCase(tc.in); got != tc.want {
|
||||
t.Errorf("CamelCase(%q) = %q, want %q", tc.in, got, tc.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
// Copyright 2015 Google Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Package yang is used to parse .yang files (see RFC 6020).
|
||||
//
|
||||
// A generic yang statements takes one of the forms:
|
||||
//
|
||||
// keyword [argument] ;
|
||||
// keyword [argument] { [statement [...]] }
|
||||
//
|
||||
// At the lowest level, package yang returns a simple tree of statements via the
|
||||
// Parse function. The Parse function makes no attempt to determine the
|
||||
// validity of the source, other than checking for generic syntax errors.
|
||||
//
|
||||
// At it's simplest, the GetModule function is used. The GetModule function
|
||||
// searches the current directory, and any directory added to the Path variable,
|
||||
// for a matching .yang source file by appending .yang to the name of the
|
||||
// module:
|
||||
//
|
||||
// // Get the tree for the module module-name by looking for the source
|
||||
// // file named module-name.yang.
|
||||
// e, errs := yang.GetModule("module-name" [, optional sources...])
|
||||
// if len(errs) > 0 {
|
||||
// for _, err := range errs {
|
||||
// fmt.Fprintln(os.Stderr, err)
|
||||
// }
|
||||
// os.Exit(1)
|
||||
// }
|
||||
//
|
||||
// // e is the Entry tree for "module-name"
|
||||
//
|
||||
// More complicated uses cases should use NewModules and then some combination
|
||||
// of Modules.GetModule, Modules.Read, Modules.Parse, and Modules.GetErrors.
|
||||
//
|
||||
// The GetErrors method is mandatory, however, both yang.GetModule and
|
||||
// Modules.GetModule automatically call Modules.GetErrors.
|
||||
package yang
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,167 @@
|
||||
// Copyright 2015 Google Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package yang
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var (
|
||||
// revisionDateSuffixRegex matches on the revision-date portion of a YANG
|
||||
// file's name.
|
||||
revisionDateSuffixRegex = regexp.MustCompile(`^@\d{4}-\d{2}-\d{2}\.yang$`)
|
||||
)
|
||||
|
||||
// PathsWithModules returns all paths under and including the
|
||||
// root containing files with a ".yang" extension, as well as
|
||||
// any error encountered
|
||||
func PathsWithModules(root string) (paths []string, err error) {
|
||||
pm := map[string]bool{}
|
||||
filepath.Walk(root, func(p string, info os.FileInfo, e error) error {
|
||||
err = e
|
||||
if err == nil {
|
||||
if info == nil {
|
||||
return nil
|
||||
}
|
||||
if !info.IsDir() && strings.HasSuffix(p, ".yang") {
|
||||
dir := filepath.Dir(p)
|
||||
if !pm[dir] {
|
||||
pm[dir] = true
|
||||
paths = append(paths, dir)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// AddPath adds the directories specified in p, a colon separated list
|
||||
// of directory names, to Path, if they are not already in Path. Using
|
||||
// multiple arguments is also supported.
|
||||
func (ms *Modules) AddPath(paths ...string) {
|
||||
for _, path := range paths {
|
||||
for _, p := range strings.Split(path, ":") {
|
||||
if !ms.pathMap[p] {
|
||||
ms.pathMap[p] = true
|
||||
ms.Path = append(ms.Path, p)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// readFile makes testing of findFile easier.
|
||||
var readFile = ioutil.ReadFile
|
||||
|
||||
// scanDir makes testing of findFile easier.
|
||||
var scanDir = findInDir
|
||||
|
||||
// findFile returns the name and contents of the .yang file associated with
|
||||
// name, or an error. If name is a module name rather than a file name (it does
|
||||
// not have a .yang extension and there is no / in name), .yang is appended to
|
||||
// the the name. The directory that the .yang file is found in is added to Path
|
||||
// if not already in Path. If a file is not found by exact match, directories
|
||||
// are scanned for "name@revision-date.yang" files, the latest (sorted by
|
||||
// YYYY-MM-DD revision-date) of these will be selected.
|
||||
//
|
||||
// If a path has the form dir/... then dir and all direct or indirect
|
||||
// subdirectories of dir are searched.
|
||||
//
|
||||
// The current directory (.) is always checked first, no matter the value of
|
||||
// Path.
|
||||
func (ms *Modules) findFile(name string) (string, string, error) {
|
||||
slash := strings.Index(name, "/")
|
||||
if slash < 0 && !strings.HasSuffix(name, ".yang") {
|
||||
name += ".yang"
|
||||
if best := scanDir(".", name, false); best != "" {
|
||||
// we found a matching candidate in the local directory
|
||||
name = best
|
||||
}
|
||||
}
|
||||
|
||||
switch data, err := readFile(name); true {
|
||||
case err == nil:
|
||||
ms.AddPath(filepath.Dir(name))
|
||||
return name, string(data), nil
|
||||
case slash >= 0:
|
||||
// If there are any /'s in the name then don't search Path.
|
||||
return "", "", fmt.Errorf("no such file: %s", name)
|
||||
}
|
||||
|
||||
for _, dir := range ms.Path {
|
||||
var n string
|
||||
if filepath.Base(dir) == "..." {
|
||||
n = scanDir(filepath.Dir(dir), name, true)
|
||||
} else {
|
||||
n = scanDir(dir, name, false)
|
||||
}
|
||||
if n == "" {
|
||||
continue
|
||||
}
|
||||
if data, err := readFile(n); err == nil {
|
||||
return n, string(data), nil
|
||||
}
|
||||
}
|
||||
return "", "", fmt.Errorf("no such file: %s", name)
|
||||
}
|
||||
|
||||
// findInDir looks for a file named name in dir or any of its subdirectories if
|
||||
// recurse is true. if recurse is false, scan only the directory dir.
|
||||
// If no matching file is found, an empty string is returned.
|
||||
//
|
||||
// The file SHOULD have the following name, per
|
||||
// https://tools.ietf.org/html/rfc7950#section-5.2:
|
||||
// module-or-submodule-name ['@' revision-date] '.yang'
|
||||
// where revision-date = 4DIGIT "-" 2DIGIT "-" 2DIGIT
|
||||
//
|
||||
// If a perfect name match is found, then that file's path is returned.
|
||||
// Else if file(s) with otherwise matching names but which contain a
|
||||
// revision-date pattern exactly matching the above are found, then path of the
|
||||
// one with the latest date is returned.
|
||||
func findInDir(dir, name string, recurse bool) string {
|
||||
fis, err := ioutil.ReadDir(dir)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
var revisions []string
|
||||
mname := strings.TrimSuffix(name, ".yang")
|
||||
for _, fi := range fis {
|
||||
switch {
|
||||
case !fi.IsDir():
|
||||
if fn := fi.Name(); fn == name {
|
||||
return filepath.Join(dir, name)
|
||||
} else if strings.HasPrefix(fn, mname) && revisionDateSuffixRegex.MatchString(strings.TrimPrefix(fn, mname)) {
|
||||
revisions = append(revisions, fn)
|
||||
}
|
||||
case recurse:
|
||||
if n := findInDir(filepath.Join(dir, fi.Name()), name, recurse); n != "" {
|
||||
return n
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(revisions) == 0 {
|
||||
return ""
|
||||
}
|
||||
sort.Strings(revisions)
|
||||
return filepath.Join(dir, revisions[len(revisions)-1])
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
// Copyright 2015 Google Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package yang
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestFindFile(t *testing.T) {
|
||||
sep := string(os.PathSeparator)
|
||||
|
||||
for _, tt := range []struct {
|
||||
name string
|
||||
path []string
|
||||
check []string
|
||||
}{
|
||||
{
|
||||
name: "one",
|
||||
check: []string{"one.yang"},
|
||||
},
|
||||
{
|
||||
name: "./two",
|
||||
check: []string{"./two"},
|
||||
},
|
||||
{
|
||||
name: "three.yang",
|
||||
check: []string{"three.yang"},
|
||||
},
|
||||
{
|
||||
name: "four",
|
||||
path: []string{"dir1", "dir2"},
|
||||
check: []string{"four.yang", "dir1" + sep + "four.yang", "dir2" + sep + "four.yang"},
|
||||
},
|
||||
} {
|
||||
var checked []string
|
||||
ms := NewModules()
|
||||
ms.Path = tt.path
|
||||
readFile = func(path string) ([]byte, error) {
|
||||
checked = append(checked, path)
|
||||
return nil, errors.New("no such file")
|
||||
}
|
||||
scanDir = func(dir, name string, recurse bool) string {
|
||||
return filepath.Join(dir, name)
|
||||
}
|
||||
if _, _, err := ms.findFile(tt.name); err == nil {
|
||||
t.Errorf("%s unexpectedly succeeded", tt.name)
|
||||
continue
|
||||
}
|
||||
if !reflect.DeepEqual(tt.check, checked) {
|
||||
t.Errorf("%s: got %v, want %v", tt.name, checked, tt.check)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestScanForPathsAndAddModules(t *testing.T) {
|
||||
// disable any readFile mock setup by other tests
|
||||
readFile = ioutil.ReadFile
|
||||
|
||||
// Scan the directory tree for YANG modules
|
||||
paths, err := PathsWithModules("../../testdata")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// we should have seen two directories being testdata and
|
||||
// testdata/subdir.
|
||||
if len(paths) != 2 {
|
||||
t.Errorf("got %d paths imported, want 2", len(paths))
|
||||
}
|
||||
ms := NewModules()
|
||||
// add the paths found in the scan to the module path
|
||||
ms.AddPath(paths...)
|
||||
|
||||
// confirm we can load the four modules that exist in
|
||||
// the two paths we scanned.
|
||||
modules := []string{"aug", "base", "other", "subdir1"}
|
||||
for _, name := range modules {
|
||||
if _, err := ms.GetModule(name); err != nil {
|
||||
t.Errorf("getting %s: %v", name, err)
|
||||
}
|
||||
}
|
||||
|
||||
// however, a sub module is not a valid argument to GetModule.
|
||||
if _, err := ms.GetModule("sub"); err == nil {
|
||||
t.Error("want an error when loading 'sub', got nil")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestFindInDir(t *testing.T) {
|
||||
testDir := "testdata/find-file-test"
|
||||
|
||||
tests := []struct {
|
||||
desc string
|
||||
inDir string
|
||||
inName string
|
||||
inRecurse bool
|
||||
want string
|
||||
}{{
|
||||
desc: "file not found",
|
||||
inDir: testDir,
|
||||
inName: "green.yang",
|
||||
inRecurse: true,
|
||||
want: "",
|
||||
}, {
|
||||
desc: "input directory does not exist",
|
||||
inDir: filepath.Join(testDir, "dne"),
|
||||
inName: "red.yang",
|
||||
inRecurse: true,
|
||||
want: "",
|
||||
}, {
|
||||
desc: "exact match",
|
||||
inDir: testDir,
|
||||
inName: "blue.yang",
|
||||
inRecurse: false,
|
||||
want: filepath.Join(testDir, "blue.yang"),
|
||||
}, {
|
||||
desc: "exact match, recursive",
|
||||
inDir: testDir,
|
||||
inName: "blue.yang",
|
||||
inRecurse: true,
|
||||
want: filepath.Join(testDir, "blue.yang"),
|
||||
}, {
|
||||
desc: "exact match with non-standard name",
|
||||
inDir: testDir,
|
||||
inName: "non-standard.name",
|
||||
inRecurse: false,
|
||||
want: filepath.Join(testDir, "non-standard.name"),
|
||||
}, {
|
||||
desc: "revision match without recursion, and ignoring invalid revision",
|
||||
inDir: testDir,
|
||||
inName: "red.yang",
|
||||
inRecurse: false,
|
||||
want: filepath.Join(testDir, "red@2010-10-10.yang"),
|
||||
}, {
|
||||
desc: "revision match with recursion",
|
||||
inDir: testDir,
|
||||
inName: "red.yang",
|
||||
inRecurse: true,
|
||||
want: filepath.Join(testDir, "dir", "dirdir", "red@2022-02-22.yang"),
|
||||
}}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.desc, func(t *testing.T) {
|
||||
if got, want := findInDir(tt.inDir, tt.inName, tt.inRecurse), tt.want; got != want {
|
||||
t.Errorf("got: %q, want: %q", got, want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
// Copyright 2015 Google Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package yang
|
||||
|
||||
// This file has functions that search the AST for specified nodes.
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// localPrefix returns the local prefix used by the containing (sub)module to
|
||||
// refer to its own module.
|
||||
func localPrefix(n Node) string {
|
||||
return RootNode(n).GetPrefix()
|
||||
}
|
||||
|
||||
// trimLocalPrefix trims the current module's prefix from the given name. If the
|
||||
// name is not prefixed with the local module's prefix or is unprefixed
|
||||
// entirely, then the same string is returned unchanged.
|
||||
func trimLocalPrefix(n Node, name string) string {
|
||||
pfx := localPrefix(n)
|
||||
if pfx != "" {
|
||||
pfx += ":"
|
||||
}
|
||||
return strings.TrimPrefix(name, pfx)
|
||||
}
|
||||
|
||||
// FindGrouping finds the grouping named name according to YANG namespace rules
|
||||
// using the input node as the initial context node. The seen parameter
|
||||
// provides a list of the modules previously seen by FindGrouping during
|
||||
// traversal. If the named grouping cannot be found, nil is returned.
|
||||
//
|
||||
// FindGrouping works by recursively looking through the context node's parent
|
||||
// nodes for grouping fields, or in included or imported submodules/modules for
|
||||
// externally-defined groupings. Note that any prefix in the name must match
|
||||
// the module prefix of its import statement in the context node's module.
|
||||
func FindGrouping(n Node, name string, seen map[string]bool) *Grouping {
|
||||
name = trimLocalPrefix(n, name)
|
||||
for n != nil {
|
||||
// Grab the Grouping field of the underlying structure. n is
|
||||
// always a pointer to a structure,
|
||||
e := reflect.ValueOf(n).Elem()
|
||||
v := e.FieldByName("Grouping")
|
||||
if v.IsValid() {
|
||||
for _, g := range v.Interface().([]*Grouping) {
|
||||
if g.Name == name {
|
||||
return g
|
||||
}
|
||||
}
|
||||
}
|
||||
v = e.FieldByName("Import")
|
||||
if v.IsValid() {
|
||||
for _, i := range v.Interface().([]*Import) {
|
||||
// If the prefix matches the import statement,
|
||||
// then search for the trimmed name in that module.
|
||||
pname := strings.TrimPrefix(name, i.Prefix.Name+":")
|
||||
if pname == name {
|
||||
continue
|
||||
}
|
||||
if g := FindGrouping(i.Module, pname, seen); g != nil {
|
||||
return g
|
||||
}
|
||||
}
|
||||
}
|
||||
v = e.FieldByName("Include")
|
||||
if v.IsValid() {
|
||||
for _, i := range v.Interface().([]*Include) {
|
||||
if seen[i.Module.Name] {
|
||||
// Prevent infinite loops in the case that we have already looked at
|
||||
// this submodule. This occurs where submodules have include statements
|
||||
// in them, or there is a circular dependency.
|
||||
continue
|
||||
}
|
||||
seen[i.Module.Name] = true
|
||||
if g := FindGrouping(i.Module, name, seen); g != nil {
|
||||
return g
|
||||
}
|
||||
}
|
||||
}
|
||||
n = n.ParentNode()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,358 @@
|
||||
package yang
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestFindGrouping(t *testing.T) {
|
||||
tests := []struct {
|
||||
desc string
|
||||
inMods map[string]string
|
||||
inNode func(*Modules) (Node, error)
|
||||
inName string
|
||||
wantGroupNodePath string
|
||||
// wantCannotFound indicates that the grouping cannot be found.
|
||||
wantCannotFound bool
|
||||
}{{
|
||||
desc: "grouping within module",
|
||||
inMods: map[string]string{
|
||||
"dev": `
|
||||
module dev {
|
||||
prefix d;
|
||||
namespace "urn:d";
|
||||
|
||||
revision 01-01-01 { description "the start of time"; }
|
||||
|
||||
grouping g { leaf a { type string; } }
|
||||
|
||||
container c { leaf b { type string; } }
|
||||
}`,
|
||||
},
|
||||
inNode: func(ms *Modules) (Node, error) {
|
||||
return FindNode(ms.Modules["dev"], "c")
|
||||
},
|
||||
inName: "g",
|
||||
wantGroupNodePath: "/dev/g",
|
||||
}, {
|
||||
desc: "nested grouping within module",
|
||||
inMods: map[string]string{
|
||||
"dev": `
|
||||
module dev {
|
||||
prefix d;
|
||||
namespace "urn:d";
|
||||
|
||||
revision 01-01-01 { description "the start of time"; }
|
||||
|
||||
grouping g { grouping gg { leaf a { type string; } } uses gg; }
|
||||
|
||||
container c { leaf b { type string; } }
|
||||
}`,
|
||||
},
|
||||
inNode: func(ms *Modules) (Node, error) {
|
||||
return FindNode(ms.Modules["dev"], "g")
|
||||
},
|
||||
inName: "gg",
|
||||
wantGroupNodePath: "/dev/g/gg",
|
||||
}, {
|
||||
desc: "grouping that uses another grouping both within the same module",
|
||||
inMods: map[string]string{
|
||||
"dev": `
|
||||
module dev {
|
||||
prefix d;
|
||||
namespace "urn:d";
|
||||
|
||||
revision 01-01-01 { description "the start of time"; }
|
||||
|
||||
grouping gg { leaf a { type string; } }
|
||||
|
||||
grouping g { uses gg; }
|
||||
|
||||
container c { leaf b { type string; } }
|
||||
}`,
|
||||
},
|
||||
inNode: func(ms *Modules) (Node, error) {
|
||||
return FindNode(ms.Modules["dev"], "g")
|
||||
},
|
||||
inName: "gg",
|
||||
wantGroupNodePath: "/dev/gg",
|
||||
}, {
|
||||
desc: "grouping in included submodule",
|
||||
inMods: map[string]string{
|
||||
"dev": `
|
||||
module dev {
|
||||
prefix d;
|
||||
namespace "urn:d";
|
||||
include sys;
|
||||
|
||||
container c { leaf b { type string; } }
|
||||
|
||||
revision 01-01-01 { description "the start of time"; }
|
||||
}`,
|
||||
"sys": `
|
||||
submodule sys {
|
||||
belongs-to dev {
|
||||
prefix "d";
|
||||
}
|
||||
|
||||
revision 01-01-01 { description "the start of time"; }
|
||||
|
||||
grouping g { leaf a { type string; } }
|
||||
}`,
|
||||
},
|
||||
inNode: func(ms *Modules) (Node, error) {
|
||||
return FindNode(ms.Modules["dev"], "c")
|
||||
},
|
||||
inName: "g",
|
||||
wantGroupNodePath: "/sys/g",
|
||||
}, {
|
||||
desc: "grouping in indirectly-included submodule",
|
||||
inMods: map[string]string{
|
||||
"dev": `
|
||||
module dev {
|
||||
prefix d;
|
||||
namespace "urn:d";
|
||||
include sys;
|
||||
|
||||
revision 01-01-01 { description "the start of time"; }
|
||||
|
||||
container c { leaf b { type string; } }
|
||||
}`,
|
||||
"sys": `
|
||||
submodule sys {
|
||||
belongs-to dev {
|
||||
prefix "d";
|
||||
}
|
||||
include sysdb;
|
||||
|
||||
revision 01-01-01 { description "the start of time"; }
|
||||
}`,
|
||||
"sysdb": `
|
||||
submodule sysdb {
|
||||
belongs-to dev {
|
||||
prefix "d";
|
||||
}
|
||||
|
||||
revision 01-01-01 { description "the start of time"; }
|
||||
|
||||
grouping g { leaf a { type string; } }
|
||||
}`,
|
||||
},
|
||||
inNode: func(ms *Modules) (Node, error) {
|
||||
return FindNode(ms.Modules["dev"], "c")
|
||||
},
|
||||
inName: "g",
|
||||
wantGroupNodePath: "/sysdb/g",
|
||||
}, {
|
||||
desc: "grouping in indirectly-included submodule with node in submodule",
|
||||
inMods: map[string]string{
|
||||
"dev": `
|
||||
module dev {
|
||||
prefix d;
|
||||
namespace "urn:d";
|
||||
include sys;
|
||||
|
||||
revision 01-01-01 { description "the start of time"; }
|
||||
}`,
|
||||
"sys": `
|
||||
submodule sys {
|
||||
belongs-to dev {
|
||||
prefix "d";
|
||||
}
|
||||
include sysdb;
|
||||
|
||||
revision 01-01-01 { description "the start of time"; }
|
||||
|
||||
container c { leaf b { type string; } }
|
||||
}`,
|
||||
"sysdb": `
|
||||
submodule sysdb {
|
||||
belongs-to dev {
|
||||
prefix "d";
|
||||
}
|
||||
|
||||
revision 01-01-01 { description "the start of time"; }
|
||||
|
||||
grouping g { leaf a { type string; } }
|
||||
}`,
|
||||
},
|
||||
inNode: func(ms *Modules) (Node, error) {
|
||||
return FindNode(ms.SubModules["sys"], "c")
|
||||
},
|
||||
inName: "g",
|
||||
wantGroupNodePath: "/sysdb/g",
|
||||
}, {
|
||||
desc: "grouping in submodule",
|
||||
inMods: map[string]string{
|
||||
"dev": `
|
||||
module dev {
|
||||
prefix d;
|
||||
namespace "urn:d";
|
||||
import sysdb { prefix "s"; }
|
||||
|
||||
revision 01-01-01 { description "the start of time"; }
|
||||
|
||||
container c { leaf b { type string; } uses s:g; }
|
||||
}`,
|
||||
"sysdb": `
|
||||
module sysdb {
|
||||
prefix sd;
|
||||
namespace "urn:sd";
|
||||
|
||||
revision 01-01-01 { description "the start of time"; }
|
||||
|
||||
grouping g { leaf a { type string; } }
|
||||
}`,
|
||||
},
|
||||
inNode: func(ms *Modules) (Node, error) {
|
||||
return FindNode(ms.Modules["dev"], "c")
|
||||
},
|
||||
inName: "s:g",
|
||||
wantGroupNodePath: "/sysdb/g",
|
||||
}, {
|
||||
desc: "grouping that uses another grouping both in different modules",
|
||||
inMods: map[string]string{
|
||||
"dev": `
|
||||
module dev {
|
||||
prefix d;
|
||||
namespace "urn:d";
|
||||
import dev2 { prefix "de2"; }
|
||||
|
||||
revision 01-01-01 { description "the start of time"; }
|
||||
|
||||
container c { leaf l { type string; } uses de2:g; }
|
||||
}`,
|
||||
"dev2": `
|
||||
module dev2 {
|
||||
prefix d2;
|
||||
namespace "urn:d2";
|
||||
import dev3 { prefix "de3"; }
|
||||
|
||||
revision 01-01-01 { description "the start of time"; }
|
||||
|
||||
grouping g { leaf a { type string; } uses de3:gg; }
|
||||
}`,
|
||||
"dev3": `
|
||||
module dev3 {
|
||||
prefix d3;
|
||||
namespace "urn:d3";
|
||||
|
||||
revision 01-01-01 { description "the start of time"; }
|
||||
|
||||
grouping gg { leaf b { type string; } }
|
||||
}`,
|
||||
},
|
||||
inNode: func(ms *Modules) (Node, error) {
|
||||
return FindNode(ms.Modules["dev2"], "g")
|
||||
},
|
||||
inName: "de3:gg",
|
||||
wantGroupNodePath: "/dev3/gg",
|
||||
}, {
|
||||
desc: "grouping that uses another grouping both in different modules but prefix is wrong",
|
||||
inMods: map[string]string{
|
||||
"dev": `
|
||||
module dev {
|
||||
prefix d;
|
||||
namespace "urn:d";
|
||||
import dev2 { prefix "de2"; }
|
||||
|
||||
revision 01-01-01 { description "the start of time"; }
|
||||
|
||||
container c { leaf l { type string; } uses de2:g; }
|
||||
}`,
|
||||
"dev2": `
|
||||
module dev2 {
|
||||
prefix d2;
|
||||
namespace "urn:d2";
|
||||
import dev3 { prefix "de3"; }
|
||||
|
||||
revision 01-01-01 { description "the start of time"; }
|
||||
|
||||
grouping g { leaf a { type string; } uses de3:gg; }
|
||||
}`,
|
||||
"dev3": `
|
||||
module dev3 {
|
||||
prefix dev3;
|
||||
namespace "urn:dev3";
|
||||
|
||||
revision 01-01-01 { description "the start of time"; }
|
||||
|
||||
grouping gg { leaf b { type string; } }
|
||||
}`,
|
||||
},
|
||||
inNode: func(ms *Modules) (Node, error) {
|
||||
return FindNode(ms.Modules["dev2"], "g")
|
||||
},
|
||||
inName: "d3:gg",
|
||||
wantCannotFound: true,
|
||||
}, {
|
||||
desc: "grouping that uses another grouping both in different modules but uses wrong context node",
|
||||
inMods: map[string]string{
|
||||
"dev": `
|
||||
module dev {
|
||||
prefix d;
|
||||
namespace "urn:d";
|
||||
import dev2 { prefix "de2"; }
|
||||
|
||||
revision 01-01-01 { description "the start of time"; }
|
||||
|
||||
container c { leaf l { type string; } uses de2:g; }
|
||||
}`,
|
||||
"dev2": `
|
||||
module dev2 {
|
||||
prefix d2;
|
||||
namespace "urn:d2";
|
||||
import dev3 { prefix "dev3"; }
|
||||
|
||||
revision 01-01-01 { description "the start of time"; }
|
||||
|
||||
grouping g { leaf a { type string; } uses dev3:gg; }
|
||||
}`,
|
||||
"dev3": `
|
||||
module dev3 {
|
||||
prefix dev3;
|
||||
namespace "urn:dev3";
|
||||
|
||||
revision 01-01-01 { description "the start of time"; }
|
||||
|
||||
grouping gg { leaf b { type string; } }
|
||||
}`,
|
||||
},
|
||||
inNode: func(ms *Modules) (Node, error) {
|
||||
return FindNode(ms.Modules["dev"], "c")
|
||||
},
|
||||
inName: "dev3:gg",
|
||||
wantCannotFound: true,
|
||||
}}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.desc, func(t *testing.T) {
|
||||
ms := NewModules()
|
||||
|
||||
for n, m := range tt.inMods {
|
||||
if err := ms.Parse(m, n); err != nil {
|
||||
t.Fatalf("cannot parse module %s, err: %v", n, err)
|
||||
}
|
||||
}
|
||||
|
||||
if errs := ms.Process(); errs != nil {
|
||||
t.Fatalf("cannot process modules: %v", errs)
|
||||
}
|
||||
|
||||
seen := map[string]bool{}
|
||||
node, err := tt.inNode(ms)
|
||||
if err != nil {
|
||||
t.Fatalf("cannot find input node: %v", err)
|
||||
}
|
||||
g := FindGrouping(node, tt.inName, seen)
|
||||
if got, want := g == nil, tt.wantCannotFound; got != want {
|
||||
t.Fatalf("got grouping: %v, wantCannotFound: %v", got, want)
|
||||
}
|
||||
if tt.wantCannotFound {
|
||||
return
|
||||
}
|
||||
if got, want := NodePath(g), tt.wantGroupNodePath; got != want {
|
||||
t.Errorf("found grouping path doesn't match expected, got: %s, want: %s", got, want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
// Copyright 2016 Google Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package yang
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// This file implements data structures and functions that relate to the
|
||||
// identity type.
|
||||
|
||||
// identityDictionary stores a set of identities across all parsed Modules that
|
||||
// have been resolved to be identified by their module and name.
|
||||
type identityDictionary struct {
|
||||
mu sync.Mutex
|
||||
// dict is a global cache of identities keyed by
|
||||
// modulename:identityname, where modulename is the full name of the
|
||||
// module to which the identity belongs. If the identity were defined
|
||||
// in a submodule, then the parent module name is used instead.
|
||||
dict map[string]resolvedIdentity
|
||||
}
|
||||
|
||||
// resolvedIdentity is an Identity that has been disambiguated.
|
||||
type resolvedIdentity struct {
|
||||
Module *Module
|
||||
Identity *Identity
|
||||
}
|
||||
|
||||
// isEmpty determines whether the resolvedIdentity struct value is populated.
|
||||
func (r resolvedIdentity) isEmpty() bool {
|
||||
return r.Module == nil && r.Identity == nil
|
||||
}
|
||||
|
||||
// newResolvedIdentity creates a resolved identity from an identity and its
|
||||
// associated module, and returns the prefixed name (Prefix:IdentityName)
|
||||
// along with the resolved identity.
|
||||
func newResolvedIdentity(m *Module, i *Identity) (string, *resolvedIdentity) {
|
||||
r := &resolvedIdentity{
|
||||
Module: m,
|
||||
Identity: i,
|
||||
}
|
||||
return i.modulePrefixedName(), r
|
||||
}
|
||||
|
||||
func appendIfNotIn(ids []*Identity, chk *Identity) []*Identity {
|
||||
for _, id := range ids {
|
||||
if id == chk {
|
||||
return ids
|
||||
}
|
||||
}
|
||||
return append(ids, chk)
|
||||
}
|
||||
|
||||
// addChildren adds identity r and all of its children to ids
|
||||
// deterministically.
|
||||
func addChildren(r *Identity, ids []*Identity) []*Identity {
|
||||
ids = appendIfNotIn(ids, r)
|
||||
|
||||
// Iterate through the values of r.
|
||||
for _, ch := range r.Values {
|
||||
ids = addChildren(ch, ids)
|
||||
}
|
||||
return ids
|
||||
}
|
||||
|
||||
// findIdentityBase returns the resolved identity that is corresponds to the
|
||||
// baseStr string in the context of the module/submodule mod.
|
||||
func (mod *Module) findIdentityBase(baseStr string) (*resolvedIdentity, []error) {
|
||||
var base resolvedIdentity
|
||||
var ok bool
|
||||
var errs []error
|
||||
|
||||
basePrefix, baseName := getPrefix(baseStr)
|
||||
rootPrefix := mod.GetPrefix()
|
||||
source := Source(mod)
|
||||
typeDict := mod.Modules.typeDict
|
||||
|
||||
switch basePrefix {
|
||||
case "", rootPrefix:
|
||||
// This is a local identity which is defined within the current
|
||||
// module
|
||||
keyName := fmt.Sprintf("%s:%s", module(mod).Name, baseName)
|
||||
base, ok = typeDict.identities.dict[keyName]
|
||||
if !ok {
|
||||
errs = append(errs, fmt.Errorf("%s: can't resolve the local base %s as %s", source, baseStr, keyName))
|
||||
}
|
||||
default:
|
||||
// This is an identity which is defined within another module
|
||||
extmod := FindModuleByPrefix(mod, basePrefix)
|
||||
if extmod == nil {
|
||||
errs = append(errs,
|
||||
fmt.Errorf("%s: can't find external module with prefix %s", source, basePrefix))
|
||||
break
|
||||
}
|
||||
// The identity we are looking for is modulename:basename.
|
||||
if id, ok := typeDict.identities.dict[fmt.Sprintf("%s:%s", module(extmod).Name, baseName)]; ok {
|
||||
base = id
|
||||
break
|
||||
}
|
||||
|
||||
// Error if we did not find the identity that had the name specified in
|
||||
// the module it was expected to be in.
|
||||
if base.isEmpty() {
|
||||
errs = append(errs, fmt.Errorf("%s: can't resolve remote base %s", source, baseStr))
|
||||
}
|
||||
}
|
||||
return &base, errs
|
||||
}
|
||||
|
||||
func (ms *Modules) resolveIdentities() []error {
|
||||
defer ms.typeDict.identities.mu.Unlock()
|
||||
ms.typeDict.identities.mu.Lock()
|
||||
|
||||
var errs []error
|
||||
|
||||
// Across all modules, read the identity values that have been extracted
|
||||
// from them, and compile them into a "fully resolved" map that means that
|
||||
// we can look them up based on the 'real' prefix of the module and the
|
||||
// name of the identity.
|
||||
for _, mod := range ms.Modules {
|
||||
for _, i := range mod.Identities() {
|
||||
keyName, r := newResolvedIdentity(mod, i)
|
||||
ms.typeDict.identities.dict[keyName] = *r
|
||||
}
|
||||
|
||||
// Hoist up all identities in our included submodules.
|
||||
// We could just do a range on ms.SubModules, but that
|
||||
// might process a submodule that no module included.
|
||||
for _, in := range mod.Include {
|
||||
if in.Module == nil {
|
||||
continue
|
||||
}
|
||||
for _, i := range in.Module.Identities() {
|
||||
keyName, r := newResolvedIdentity(in.Module, i)
|
||||
ms.typeDict.identities.dict[keyName] = *r
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Now, we want to create for all identities a view of all of their children.
|
||||
// A child identity here means an inherited identity.
|
||||
//
|
||||
// We start by finding the direct children of all identities using the
|
||||
// 'base' statement.
|
||||
for _, i := range ms.typeDict.identities.dict {
|
||||
if i.Identity.Base != nil {
|
||||
// This identity inherits from one or more other identities.
|
||||
|
||||
root := RootNode(i.Identity)
|
||||
for _, b := range i.Identity.Base {
|
||||
base, baseErr := root.findIdentityBase(b.asString())
|
||||
|
||||
if baseErr != nil {
|
||||
errs = append(errs, baseErr...)
|
||||
continue
|
||||
}
|
||||
|
||||
// Build up a list of direct children of this identity.
|
||||
base.Identity.Values = append(base.Identity.Values, i.Identity)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Now, we can find all transitive identities by recursively populating
|
||||
// the children of each identity.
|
||||
for _, i := range ms.typeDict.identities.dict {
|
||||
newValues := []*Identity{}
|
||||
for _, j := range i.Identity.Values {
|
||||
newValues = addChildren(j, newValues)
|
||||
}
|
||||
sort.SliceStable(newValues, func(j, k int) bool {
|
||||
return newValues[j].Name < newValues[k].Name
|
||||
})
|
||||
i.Identity.Values = newValues
|
||||
}
|
||||
|
||||
return errs
|
||||
}
|
||||
@@ -0,0 +1,802 @@
|
||||
// Copyright 2016 Google Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package yang
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/openconfig/gnmi/errdiff"
|
||||
)
|
||||
|
||||
// inputModule is a mock input YANG module.
|
||||
type inputModule struct {
|
||||
name string // The filename of the YANG module.
|
||||
content string // The contents of the YANG module.
|
||||
}
|
||||
|
||||
type idrefOut struct {
|
||||
module string // The module that the identityref is within.
|
||||
name string // The name of the identityref.
|
||||
values []string // Names of the identities that the identityref relates to.
|
||||
}
|
||||
|
||||
// identityOut is the output for a particular identity within the test case.
|
||||
type identityOut struct {
|
||||
module string // The module that the identity is within.
|
||||
name string // The name of the identity.
|
||||
baseNames []string // The base(s) of the identity as string(s).
|
||||
values []string // The string names of derived identities.
|
||||
}
|
||||
|
||||
// identityTestCase is a test case for a module which contains identities.
|
||||
type identityTestCase struct {
|
||||
name string
|
||||
in []inputModule // The set of input modules for the test
|
||||
identities []identityOut // Slice of the identity values expected
|
||||
idrefs []idrefOut // Slice of identityref results expected
|
||||
wantErrSubstr string // wanErrSubstr is a substring of the wanted error.
|
||||
}
|
||||
|
||||
// getBaseNamesFrom is a utility function for getting the base name(s) of an identity
|
||||
func getBaseNamesFrom(i *Identity) []string {
|
||||
baseNames := []string{}
|
||||
for _, base := range i.Base {
|
||||
baseNames = append(baseNames, base.Name)
|
||||
}
|
||||
return baseNames
|
||||
}
|
||||
|
||||
// Test cases for basic identity extraction.
|
||||
var basicTestCases = []identityTestCase{
|
||||
{
|
||||
name: "basic-test-case-1: Check identity is found in module.",
|
||||
in: []inputModule{
|
||||
{
|
||||
name: "idtest-one",
|
||||
content: `
|
||||
module idtest-one {
|
||||
namespace "urn:idone";
|
||||
prefix "idone";
|
||||
|
||||
identity TEST_ID;
|
||||
}
|
||||
`},
|
||||
},
|
||||
identities: []identityOut{
|
||||
{module: "idtest-one", name: "TEST_ID"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "basic-test-case-2: Check identity with base is found in module.",
|
||||
in: []inputModule{
|
||||
{
|
||||
name: "idtest-two",
|
||||
content: `
|
||||
module idtest-two {
|
||||
namespace "urn:idtwo";
|
||||
prefix "idone";
|
||||
|
||||
identity TEST_ID;
|
||||
identity TEST_ID_TWO;
|
||||
identity TEST_CHILD {
|
||||
base TEST_ID;
|
||||
}
|
||||
}
|
||||
`},
|
||||
},
|
||||
identities: []identityOut{
|
||||
{module: "idtest-two", name: "TEST_ID"},
|
||||
{module: "idtest-two", name: "TEST_ID_TWO"},
|
||||
{module: "idtest-two", name: "TEST_CHILD", baseNames: []string{"TEST_ID"}},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "basic-test-case-3: Check identity with multiple bases.",
|
||||
in: []inputModule{
|
||||
{
|
||||
name: "idtest-three",
|
||||
content: `
|
||||
module idtest-three {
|
||||
namespace "urn:idthree";
|
||||
prefix "idthree";
|
||||
|
||||
identity BASE_ONE;
|
||||
identity BASE_TWO;
|
||||
identity TEST_CHILD_WITH_MULTIPLE_BASES {
|
||||
base BASE_ONE;
|
||||
base BASE_TWO;
|
||||
}
|
||||
}
|
||||
`},
|
||||
},
|
||||
identities: []identityOut{
|
||||
{module: "idtest-three", name: "BASE_ONE"},
|
||||
{module: "idtest-three", name: "BASE_TWO"},
|
||||
{module: "idtest-three", name: "TEST_CHILD_WITH_MULTIPLE_BASES", baseNames: []string{"BASE_ONE", "BASE_TWO"}},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "basic-test-case-4: Check identity base is found from submodule.",
|
||||
in: []inputModule{
|
||||
{
|
||||
name: "idtest-one",
|
||||
content: `
|
||||
module idtest-one {
|
||||
namespace "urn:idone";
|
||||
prefix "idone";
|
||||
|
||||
include "idtest-one-sub";
|
||||
|
||||
identity TEST_ID_DERIVED {
|
||||
base TEST_ID;
|
||||
}
|
||||
}
|
||||
`},
|
||||
{
|
||||
name: "idtest-one-sub",
|
||||
content: `
|
||||
submodule idtest-one-sub {
|
||||
belongs-to idtest-one {
|
||||
prefix "idone";
|
||||
}
|
||||
|
||||
identity TEST_ID;
|
||||
}
|
||||
`},
|
||||
},
|
||||
identities: []identityOut{
|
||||
{module: "idtest-one", name: "TEST_ID"},
|
||||
{module: "idtest-one", name: "TEST_ID_DERIVED", baseNames: []string{"TEST_ID"}},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "basic-test-case-5: Check identity base is found from module.",
|
||||
in: []inputModule{
|
||||
{
|
||||
name: "idtest-one",
|
||||
content: `
|
||||
module idtest-one {
|
||||
namespace "urn:idone";
|
||||
prefix "idone";
|
||||
|
||||
include "idtest-one-sub";
|
||||
|
||||
identity TEST_ID;
|
||||
}
|
||||
`},
|
||||
{
|
||||
name: "idtest-one-sub",
|
||||
content: `
|
||||
submodule idtest-one-sub {
|
||||
belongs-to idtest-one {
|
||||
prefix "idone";
|
||||
}
|
||||
|
||||
identity TEST_ID_DERIVED {
|
||||
base TEST_ID;
|
||||
}
|
||||
}
|
||||
`},
|
||||
},
|
||||
identities: []identityOut{
|
||||
{module: "idtest-one", name: "TEST_ID_DERIVED", baseNames: []string{"TEST_ID"}},
|
||||
{module: "idtest-one", name: "TEST_ID"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Test the ability to extract identities from a module with the correct base
|
||||
// statements.
|
||||
func TestIdentityExtract(t *testing.T) {
|
||||
for _, tt := range basicTestCases {
|
||||
ms := NewModules()
|
||||
for _, mod := range tt.in {
|
||||
_ = ms.Parse(mod.content, mod.name)
|
||||
}
|
||||
|
||||
for _, ti := range tt.identities {
|
||||
_, err := ms.GetModule(ti.module)
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("Could not parse module : %s", ti.module)
|
||||
continue
|
||||
}
|
||||
|
||||
foundIdentity := false
|
||||
var thisID *Identity
|
||||
for _, ri := range ms.typeDict.identities.dict {
|
||||
moduleName := module(ri.Module).Name
|
||||
if ri.Identity.Name == ti.name && moduleName == ti.module {
|
||||
foundIdentity = true
|
||||
thisID = ri.Identity
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !foundIdentity {
|
||||
t.Errorf("Could not find identity %s in module %s, identity dict:\n%+v", ti.name, ti.module, ms.typeDict.identities.dict)
|
||||
continue
|
||||
}
|
||||
|
||||
actualBaseNames := getBaseNamesFrom(thisID)
|
||||
if len(ti.baseNames) > 0 {
|
||||
if diff := cmp.Diff(actualBaseNames, ti.baseNames); diff != "" {
|
||||
t.Errorf("(-got, +want):\n%s", diff)
|
||||
}
|
||||
} else {
|
||||
if thisID.Base != nil {
|
||||
t.Errorf("Identity %s had unexpected base(s) %s", thisID.Name,
|
||||
actualBaseNames)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Test cases for validating that identities can be resolved correctly.
|
||||
var treeTestCases = []identityTestCase{
|
||||
{
|
||||
name: "tree-test-case-0: Validate identity resolution across submodules",
|
||||
in: []inputModule{
|
||||
{
|
||||
name: "base.yang",
|
||||
content: `
|
||||
module base {
|
||||
namespace "urn:base";
|
||||
prefix "base";
|
||||
|
||||
include side;
|
||||
|
||||
identity REMOTE_BASE;
|
||||
}
|
||||
`},
|
||||
{
|
||||
name: "remote.yang",
|
||||
content: `
|
||||
submodule side {
|
||||
belongs-to base {
|
||||
prefix "r";
|
||||
}
|
||||
|
||||
identity LOCAL_REMOTE_BASE {
|
||||
base r:REMOTE_BASE;
|
||||
}
|
||||
}
|
||||
`},
|
||||
},
|
||||
identities: []identityOut{
|
||||
{
|
||||
module: "base",
|
||||
name: "REMOTE_BASE",
|
||||
values: []string{"LOCAL_REMOTE_BASE"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "tree-test-case-1: Validate identity resolution across modules",
|
||||
in: []inputModule{
|
||||
{
|
||||
name: "base.yang",
|
||||
content: `
|
||||
module base {
|
||||
namespace "urn:base";
|
||||
prefix "base";
|
||||
|
||||
import remote { prefix "r"; }
|
||||
import remote2 { prefix "r2"; }
|
||||
|
||||
identity LOCAL_REMOTE_BASE {
|
||||
base r:REMOTE_BASE;
|
||||
}
|
||||
|
||||
identity LOCAL_REMOTE_BASE2 {
|
||||
base r2:REMOTE_BASE2;
|
||||
}
|
||||
}
|
||||
`},
|
||||
{
|
||||
name: "remote.yang",
|
||||
content: `
|
||||
module remote {
|
||||
namespace "urn:remote";
|
||||
prefix "r";
|
||||
|
||||
identity REMOTE_BASE;
|
||||
}
|
||||
`},
|
||||
{
|
||||
name: "remote2.yang",
|
||||
content: `
|
||||
module remote2 {
|
||||
namespace "urn:remote2";
|
||||
prefix "remote";
|
||||
|
||||
identity REMOTE_BASE2;
|
||||
}
|
||||
`},
|
||||
},
|
||||
identities: []identityOut{
|
||||
{
|
||||
module: "remote",
|
||||
name: "REMOTE_BASE",
|
||||
values: []string{"LOCAL_REMOTE_BASE"},
|
||||
},
|
||||
{
|
||||
module: "remote2",
|
||||
name: "REMOTE_BASE2",
|
||||
values: []string{"LOCAL_REMOTE_BASE2"},
|
||||
},
|
||||
{
|
||||
module: "base",
|
||||
name: "LOCAL_REMOTE_BASE",
|
||||
baseNames: []string{"r:REMOTE_BASE"},
|
||||
},
|
||||
{
|
||||
module: "base",
|
||||
name: "LOCAL_REMOTE_BASE2",
|
||||
baseNames: []string{"r2:REMOTE_BASE2"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "tree-test-case-2: Multi-level inheritance validation.",
|
||||
in: []inputModule{
|
||||
{
|
||||
name: "base.yang",
|
||||
content: `
|
||||
module base {
|
||||
namespace "urn:base";
|
||||
prefix "base";
|
||||
|
||||
identity GREATGRANDFATHER;
|
||||
identity GRANDFATHER {
|
||||
base "GREATGRANDFATHER";
|
||||
}
|
||||
identity FATHER {
|
||||
base "GRANDFATHER";
|
||||
}
|
||||
identity SON {
|
||||
base "FATHER";
|
||||
}
|
||||
identity UNCLE {
|
||||
base "GRANDFATHER";
|
||||
}
|
||||
identity BROTHER {
|
||||
base "FATHER";
|
||||
}
|
||||
identity GREATUNCLE {
|
||||
base "GREATGRANDFATHER";
|
||||
}
|
||||
}
|
||||
`},
|
||||
},
|
||||
identities: []identityOut{
|
||||
{
|
||||
module: "base",
|
||||
name: "GREATGRANDFATHER",
|
||||
values: []string{
|
||||
"BROTHER", // Order is alphabetical
|
||||
"FATHER",
|
||||
"GRANDFATHER",
|
||||
"GREATUNCLE",
|
||||
"SON",
|
||||
"UNCLE",
|
||||
},
|
||||
},
|
||||
{
|
||||
module: "base",
|
||||
name: "GRANDFATHER",
|
||||
baseNames: []string{"GREATGRANDFATHER"},
|
||||
values: []string{"BROTHER", "FATHER", "SON", "UNCLE"},
|
||||
},
|
||||
{
|
||||
module: "base",
|
||||
name: "GREATUNCLE",
|
||||
baseNames: []string{"GREATGRANDFATHER"},
|
||||
},
|
||||
{
|
||||
module: "base",
|
||||
name: "FATHER",
|
||||
baseNames: []string{"GRANDFATHER"},
|
||||
values: []string{"BROTHER", "SON"},
|
||||
},
|
||||
{
|
||||
module: "base",
|
||||
name: "UNCLE",
|
||||
baseNames: []string{"GRANDFATHER"},
|
||||
},
|
||||
{
|
||||
module: "base",
|
||||
name: "BROTHER",
|
||||
baseNames: []string{"FATHER"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "tree-test-case-3",
|
||||
in: []inputModule{
|
||||
{
|
||||
name: "base.yang",
|
||||
content: `
|
||||
module base {
|
||||
namespace "urn:base";
|
||||
prefix "base";
|
||||
|
||||
identity BASE;
|
||||
identity NOTBASE {
|
||||
base BASE;
|
||||
}
|
||||
|
||||
leaf idref {
|
||||
type identityref {
|
||||
base "BASE";
|
||||
}
|
||||
}
|
||||
}
|
||||
`},
|
||||
},
|
||||
identities: []identityOut{
|
||||
{
|
||||
module: "base",
|
||||
name: "BASE",
|
||||
values: []string{"NOTBASE"},
|
||||
},
|
||||
{
|
||||
module: "base",
|
||||
name: "NOTBASE",
|
||||
baseNames: []string{"BASE"},
|
||||
},
|
||||
},
|
||||
idrefs: []idrefOut{
|
||||
{
|
||||
module: "base",
|
||||
name: "idref",
|
||||
values: []string{"NOTBASE"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "tree-test-case-4",
|
||||
in: []inputModule{
|
||||
{
|
||||
name: "base.yang",
|
||||
content: `
|
||||
module base4 {
|
||||
namespace "urn:base";
|
||||
prefix "base4";
|
||||
|
||||
identity BASE4;
|
||||
identity CHILD4 {
|
||||
base BASE4;
|
||||
}
|
||||
|
||||
typedef t {
|
||||
type identityref {
|
||||
base BASE4;
|
||||
}
|
||||
}
|
||||
|
||||
leaf tref {
|
||||
type t;
|
||||
}
|
||||
}
|
||||
`},
|
||||
},
|
||||
identities: []identityOut{
|
||||
{
|
||||
module: "base4",
|
||||
name: "BASE4",
|
||||
values: []string{"CHILD4"},
|
||||
},
|
||||
{
|
||||
module: "base4",
|
||||
name: "CHILD4",
|
||||
baseNames: []string{"BASE4"},
|
||||
},
|
||||
},
|
||||
idrefs: []idrefOut{
|
||||
{
|
||||
module: "base4",
|
||||
name: "tref",
|
||||
values: []string{"CHILD4"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "tree-test-case-5",
|
||||
in: []inputModule{
|
||||
{
|
||||
name: "base.yang",
|
||||
content: `
|
||||
module base5 {
|
||||
namespace "urn:base";
|
||||
prefix "base5";
|
||||
|
||||
identity BASE5A;
|
||||
identity BASE5B;
|
||||
|
||||
identity FIVE_ONE {
|
||||
base BASE5A;
|
||||
}
|
||||
|
||||
identity FIVE_TWO {
|
||||
base BASE5B;
|
||||
}
|
||||
|
||||
leaf union {
|
||||
type union {
|
||||
type identityref {
|
||||
base BASE5A;
|
||||
}
|
||||
type identityref {
|
||||
base BASE5B;
|
||||
}
|
||||
}
|
||||
}
|
||||
}`},
|
||||
},
|
||||
identities: []identityOut{
|
||||
{
|
||||
module: "base5",
|
||||
name: "BASE5A",
|
||||
values: []string{"FIVE_ONE"},
|
||||
},
|
||||
{
|
||||
module: "base5",
|
||||
name: "BASE5B",
|
||||
values: []string{"FIVE_TWO"},
|
||||
},
|
||||
},
|
||||
idrefs: []idrefOut{
|
||||
{
|
||||
module: "base5",
|
||||
name: "union",
|
||||
values: []string{"FIVE_ONE", "FIVE_TWO"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "identity's base can't be found",
|
||||
in: []inputModule{
|
||||
{
|
||||
name: "idtest",
|
||||
content: `
|
||||
module idtest{
|
||||
namespace "urn:idtwo";
|
||||
prefix "idone";
|
||||
|
||||
identity TEST_ID_TWO;
|
||||
identity TEST_CHILD {
|
||||
base TEST_ID;
|
||||
}
|
||||
}
|
||||
`},
|
||||
},
|
||||
identities: []identityOut{
|
||||
{module: "idtest", name: "TEST_ID2"},
|
||||
},
|
||||
wantErrSubstr: "can't resolve the local base",
|
||||
},
|
||||
{
|
||||
name: "identity's base can't be found in remote",
|
||||
in: []inputModule{
|
||||
{
|
||||
name: "remote.yang",
|
||||
content: `
|
||||
module remote {
|
||||
namespace "urn:remote";
|
||||
prefix "remote";
|
||||
|
||||
identity REMOTE_BASE_ESCAPE;
|
||||
}
|
||||
`},
|
||||
{
|
||||
name: "base.yang",
|
||||
content: `
|
||||
module base {
|
||||
namespace "urn:base";
|
||||
prefix "base";
|
||||
|
||||
import remote { prefix "r"; }
|
||||
|
||||
identity LOCAL_REMOTE_BASE {
|
||||
base r:REMOTE_BASE;
|
||||
}
|
||||
}
|
||||
`},
|
||||
},
|
||||
identities: []identityOut{
|
||||
{module: "base", name: "LOCAL_REMOTE_BASE"},
|
||||
},
|
||||
wantErrSubstr: "can't resolve remote base",
|
||||
},
|
||||
{
|
||||
name: "identity's base's module can't be found",
|
||||
in: []inputModule{
|
||||
{
|
||||
name: "remote.yang",
|
||||
content: `
|
||||
module remote {
|
||||
namespace "urn:remote";
|
||||
prefix "remote";
|
||||
|
||||
identity REMOTE_BASE;
|
||||
}
|
||||
`},
|
||||
{
|
||||
name: "base.yang",
|
||||
content: `
|
||||
module base {
|
||||
namespace "urn:base";
|
||||
prefix "base";
|
||||
|
||||
import remote { prefix "r"; }
|
||||
|
||||
identity LOCAL_REMOTE_BASE {
|
||||
base roe:REMOTE_BASE;
|
||||
}
|
||||
}
|
||||
`},
|
||||
},
|
||||
identities: []identityOut{
|
||||
{module: "base", name: "LOCAL_REMOTE_BASE"},
|
||||
},
|
||||
wantErrSubstr: "can't find external module",
|
||||
},
|
||||
}
|
||||
|
||||
// TestIdentityTree - check inheritance of identities from local and remote
|
||||
// sources. The Values of an Identity correspond to the values that are
|
||||
// referenced by that identity, which need to be inherited.
|
||||
func TestIdentityTree(t *testing.T) {
|
||||
for _, tt := range treeTestCases {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
ms := NewModules()
|
||||
|
||||
for _, mod := range tt.in {
|
||||
_ = ms.Parse(mod.content, mod.name)
|
||||
}
|
||||
|
||||
errs := ms.Process()
|
||||
|
||||
var err error
|
||||
switch len(errs) {
|
||||
case 1:
|
||||
err = errs[0]
|
||||
if diff := errdiff.Substring(err, tt.wantErrSubstr); diff != "" {
|
||||
t.Fatalf("%s", diff)
|
||||
}
|
||||
return
|
||||
case 0:
|
||||
if diff := errdiff.Substring(err, tt.wantErrSubstr); diff != "" {
|
||||
t.Fatalf("%s", diff)
|
||||
}
|
||||
default:
|
||||
t.Fatalf("got multiple errors: %v", errs)
|
||||
}
|
||||
|
||||
// Walk through the identities that are defined in the test case output
|
||||
// and validate that they exist, and their base and values are as expected.
|
||||
for _, chkID := range tt.identities {
|
||||
m, errs := ms.GetModule(chkID.module)
|
||||
if errs != nil {
|
||||
t.Errorf("Couldn't find expected module: %v", errs)
|
||||
continue
|
||||
}
|
||||
|
||||
var foundID *Identity
|
||||
for _, i := range m.Identities {
|
||||
if i.Name == chkID.name {
|
||||
foundID = i
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if foundID == nil {
|
||||
t.Errorf("Couldn't find identity %s in module %s", chkID.name,
|
||||
chkID.module)
|
||||
continue
|
||||
}
|
||||
|
||||
if len(chkID.baseNames) > 0 {
|
||||
actualBaseNames := getBaseNamesFrom(foundID)
|
||||
if diff := cmp.Diff(actualBaseNames, chkID.baseNames); diff != "" {
|
||||
t.Errorf("(-got, +want):\n%s", diff)
|
||||
}
|
||||
}
|
||||
|
||||
valueMap := make(map[string]bool)
|
||||
|
||||
for i, val := range chkID.values {
|
||||
valueMap[val] = false
|
||||
// Check that IsDefined returns the right result
|
||||
if !foundID.IsDefined(val) {
|
||||
t.Errorf("Couldn't find defined value %s for %s", val, chkID.name)
|
||||
}
|
||||
|
||||
// Check that the values are sorted in a consistent order
|
||||
if foundID.Values[i].Name != val {
|
||||
t.Errorf("Invalid order for value #%d. Expecting %s Got %s", i, foundID.Values[i].Name, val)
|
||||
}
|
||||
// Check that GetValue returns the right Identity
|
||||
idval := foundID.GetValue(val)
|
||||
if idval == nil {
|
||||
t.Errorf("Couldn't GetValue(%s) for %s", val, chkID.name)
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure that IsDefined does not return false positives
|
||||
if foundID.IsDefined("DoesNotExist") {
|
||||
t.Errorf("Non-existent value IsDefined for %s", foundID.Name)
|
||||
}
|
||||
|
||||
if foundID.GetValue("DoesNotExist") != nil {
|
||||
t.Errorf("Non-existent value GetValue not nil for %s", foundID.Name)
|
||||
}
|
||||
|
||||
for _, chkv := range foundID.Values {
|
||||
_, ok := valueMap[chkv.Name]
|
||||
if !ok {
|
||||
t.Errorf("Found unexpected value %s for %s", chkv.Name, chkID.name)
|
||||
continue
|
||||
}
|
||||
valueMap[chkv.Name] = true
|
||||
}
|
||||
|
||||
for k, v := range valueMap {
|
||||
if v == false {
|
||||
t.Errorf("Could not find identity %s for %s", k, chkID.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, idr := range tt.idrefs {
|
||||
m, errs := ms.GetModule(idr.module)
|
||||
if errs != nil {
|
||||
t.Errorf("Couldn't find expected module %s: %v", idr.module, errs)
|
||||
continue
|
||||
}
|
||||
|
||||
if _, ok := m.Dir[idr.name]; !ok {
|
||||
t.Errorf("Could not find expected identity, got: nil, want: %v", idr.name)
|
||||
continue
|
||||
}
|
||||
|
||||
identity := m.Dir[idr.name]
|
||||
var vals []*Identity
|
||||
switch len(identity.Type.Type) {
|
||||
case 0:
|
||||
vals = identity.Type.IdentityBase.Values
|
||||
default:
|
||||
for _, b := range identity.Type.Type {
|
||||
if b.IdentityBase != nil {
|
||||
vals = append(vals, b.IdentityBase.Values...)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var valNames []string
|
||||
for _, v := range vals {
|
||||
valNames = append(valNames, v.Name)
|
||||
}
|
||||
|
||||
if diff := cmp.Diff(idr.values, valNames); diff != "" {
|
||||
t.Errorf("Identity %s did not have expected values, (-got, +want):\n%s", idr.name, diff)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,522 @@
|
||||
// Copyright 2015 Google Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package yang
|
||||
|
||||
// This file implements the lexical tokenization of yang. The lexer returns
|
||||
// a series of tokens with one of the following codes:
|
||||
//
|
||||
// tError // an error was encountered
|
||||
// tEOF // end-of-file
|
||||
// tString // A de-quoted string (e.g., "\"bob\"" becomes "bob")
|
||||
// tUnquoted // An un-quoted string
|
||||
// '{'
|
||||
// ';'
|
||||
// '}'
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"reflect"
|
||||
"runtime"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
const (
|
||||
eof = 0x7fffffff // end of file, also an invalid rune
|
||||
maxErrors = 8
|
||||
tooMany = "too many errors...\n"
|
||||
)
|
||||
|
||||
// stateFn represents a state in the lexer as a function, returning the next
|
||||
// state the lexer should move to.
|
||||
type stateFn func(*lexer) stateFn
|
||||
|
||||
// A lexer holds the internal state of the lexer.
|
||||
type lexer struct {
|
||||
errout io.Writer // destination for errors, defaults to os.Stderr
|
||||
errcnt int // number of errors encountered
|
||||
|
||||
file string // name of file we are processing
|
||||
input string // contents of the file
|
||||
start int // start position in input of unconsumed data.
|
||||
pos int // current position in the input.
|
||||
line int // the current line number (1's based)
|
||||
col int // the current column number (0 based, add 1 before displaying)
|
||||
|
||||
debug bool // set to true to include internal debugging
|
||||
inPattern bool // set when parsing the argument to a pattern
|
||||
items chan *token // channel of scanned items.
|
||||
tcol int // column with tabs expanded (for multi-line strings)
|
||||
scol int // starting col of current token
|
||||
sline int // starting line of current token
|
||||
state stateFn // current state of the lexer
|
||||
width int // width of last rune read from input.
|
||||
}
|
||||
|
||||
// A code is a token code. Single character tokens (i.e., punctuation)
|
||||
// are represented by their unicode code point.
|
||||
type code int
|
||||
|
||||
const (
|
||||
tEOF = code(-1 - iota) // Reached end of file
|
||||
tError // An error
|
||||
tString // A dequoted string
|
||||
tUnquoted // A non-quoted string
|
||||
)
|
||||
|
||||
// String returns c as a string.
|
||||
func (c code) String() string {
|
||||
switch c {
|
||||
case tError:
|
||||
return "Error"
|
||||
case tString:
|
||||
return "String"
|
||||
case tUnquoted:
|
||||
return "Unquoted"
|
||||
}
|
||||
if c < 0 || c > '~' {
|
||||
return fmt.Sprintf("%d", c)
|
||||
}
|
||||
return fmt.Sprintf("'%c'", c)
|
||||
}
|
||||
|
||||
// A token represents one lexical unit read from the input.
|
||||
// Line and Col are both 1's based.
|
||||
type token struct {
|
||||
code code
|
||||
Text string // the actual text of the token
|
||||
File string // the source file the token is from
|
||||
Line int // the source line number the token is from
|
||||
Col int // the source column number the token is from (8 space tabs)
|
||||
}
|
||||
|
||||
// Code returns the code of t. If t is nil, tEOF is returned.
|
||||
func (t *token) Code() code {
|
||||
if t == nil {
|
||||
return tEOF
|
||||
}
|
||||
return t.code
|
||||
}
|
||||
|
||||
// String returns the location, code, and text of t as a string.
|
||||
func (t *token) String() string {
|
||||
var s []string
|
||||
if t.File != "" {
|
||||
s = append(s, t.File+":")
|
||||
}
|
||||
if t.Line != 0 {
|
||||
s = append(s, fmt.Sprintf("%d:%d:", t.Line, t.Col))
|
||||
}
|
||||
if t.Text == "" {
|
||||
s = append(s, fmt.Sprintf(" %v", t.code))
|
||||
} else {
|
||||
s = append(s, " ", t.Text)
|
||||
}
|
||||
return strings.Join(s, "")
|
||||
}
|
||||
|
||||
// A note on writing to errout. Errors should always be written to errout
|
||||
// in a single Write call. The test code makes this assumption for testing
|
||||
// expected errors.
|
||||
|
||||
// newLexer returns a new lexer, importing into it the provided input and path.
|
||||
// The provided path should indicate where the source originated.
|
||||
func newLexer(input, path string) *lexer {
|
||||
// Force input to be newline terminated.
|
||||
if len(input) > 0 && input[len(input)-1] != '\n' {
|
||||
input += "\n"
|
||||
}
|
||||
return &lexer{
|
||||
file: path,
|
||||
input: input,
|
||||
line: 1, // humans start with 1
|
||||
items: make(chan *token, maxErrors),
|
||||
state: lexGround,
|
||||
errout: os.Stderr,
|
||||
}
|
||||
}
|
||||
|
||||
// NextToken returns the next token from the input, returning nil on EOF.
|
||||
func (l *lexer) NextToken() *token {
|
||||
for {
|
||||
select {
|
||||
case item := <-l.items:
|
||||
return item
|
||||
default:
|
||||
if l.state == nil {
|
||||
return nil
|
||||
}
|
||||
if l.debug {
|
||||
name := runtime.FuncForPC(reflect.ValueOf(l.state).Pointer()).Name()
|
||||
name = name[strings.LastIndex(name, ".")+1:]
|
||||
name = strings.TrimPrefix(name, "lex")
|
||||
input := l.input[l.pos:]
|
||||
if len(input) > 8 {
|
||||
input = input[:8] + "..."
|
||||
}
|
||||
fmt.Fprintf(os.Stderr, "%d:%d: state %s %q\n", l.line, l.col+1, name, input)
|
||||
}
|
||||
l.state = l.state(l)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// emit emits the currently parsed token marked with code c using emitText.
|
||||
func (l *lexer) emit(c code) {
|
||||
l.emitText(c, l.input[l.start:l.pos])
|
||||
}
|
||||
|
||||
// emitText emits text as a token marked with c.
|
||||
// All input up to the current cursor (pos) is consumed.
|
||||
func (l *lexer) emitText(c code, text string) {
|
||||
if l.debug {
|
||||
fmt.Fprintf(os.Stderr, "%v: %q\n", c, text)
|
||||
}
|
||||
select {
|
||||
case l.items <- &token{
|
||||
code: c,
|
||||
Text: text,
|
||||
File: l.file,
|
||||
Line: l.sline,
|
||||
Col: l.scol + 1,
|
||||
}:
|
||||
default:
|
||||
}
|
||||
l.consume()
|
||||
}
|
||||
|
||||
// consume consumes all input to the current cursor.
|
||||
func (l *lexer) consume() {
|
||||
l.start = l.pos
|
||||
}
|
||||
|
||||
// backup steps back one rune. It can be called only immediately after a call
|
||||
// of next. Backing up over a tab will set tcol to the last position of the
|
||||
// tab, not where the tab started. This is okay as when we call next again it
|
||||
// will move tcol back to where it was before backup was called.
|
||||
func (l *lexer) backup() {
|
||||
l.pos -= l.width
|
||||
if l.width > 0 {
|
||||
l.col--
|
||||
l.tcol--
|
||||
if l.col < 0 {
|
||||
// We must have backuped up over a newline.
|
||||
// Don't bother to figure out the column number
|
||||
// as the next call to next will reset it to 0.
|
||||
l.line--
|
||||
l.col = 0
|
||||
l.tcol = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// peek returns but does not move past the next rune in the input. backup
|
||||
// is not supported over peeked characters.
|
||||
func (l *lexer) peek() rune {
|
||||
rune := l.next()
|
||||
l.backup()
|
||||
return rune
|
||||
}
|
||||
|
||||
// next returns the next rune in the input. If next encounters the end of input
|
||||
// then it will return eof.
|
||||
func (l *lexer) next() (rune rune) {
|
||||
if l.pos >= len(l.input) {
|
||||
l.width = 0
|
||||
return eof
|
||||
}
|
||||
// l.width is what limits more than a single backup.
|
||||
rune, l.width = utf8.DecodeRuneInString(l.input[l.pos:])
|
||||
l.pos += l.width
|
||||
switch rune {
|
||||
case '\n':
|
||||
l.line++
|
||||
l.col = 0
|
||||
l.tcol = 0
|
||||
case '\t':
|
||||
l.tcol = (l.tcol + 8) & ^7
|
||||
l.col++ // should this be l.width?
|
||||
default:
|
||||
l.tcol++
|
||||
l.col++ // should this be l.width?
|
||||
}
|
||||
return rune
|
||||
}
|
||||
|
||||
// acceptRun moves the cursor forward up to, but not including, the first rune
|
||||
// not found in the valid set. It returns true if any runes were accepted.
|
||||
func (l *lexer) acceptRun(valid string) bool {
|
||||
ret := false
|
||||
for strings.ContainsRune(valid, l.next()) {
|
||||
ret = true
|
||||
}
|
||||
l.backup()
|
||||
return ret
|
||||
}
|
||||
|
||||
// skipTo moves the cursor up to, but not including, s.
|
||||
// Returns whether s was found in the remaining input.
|
||||
func (l *lexer) skipTo(s string) bool {
|
||||
if x := strings.Index(l.input[l.pos:], s); x >= 0 {
|
||||
l.updateCursor(x)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// updateCursor moves the cursor forward n bytes. updateCursor does not
|
||||
// correctly handle tabs. This is okay as it is only used by skipTo, and skipTo
|
||||
// is never used to skip to an initial " (which is the only time that tcol is
|
||||
// necessary, as per YANG's multi-line quoted string requirement).
|
||||
func (l *lexer) updateCursor(n int) {
|
||||
s := l.input[l.pos : l.pos+n]
|
||||
l.pos += n
|
||||
// we could get away without updating width at all because backup is
|
||||
// only promised to work after a call to next.
|
||||
l.width = n
|
||||
|
||||
if c := strings.Count(s, "\n"); c > 0 {
|
||||
l.line += c
|
||||
l.col = 0
|
||||
}
|
||||
l.col += utf8.RuneCountInString(s[strings.LastIndex(s, "\n")+1:])
|
||||
}
|
||||
|
||||
// Errorf writes an error on l.errout and increments the error count.
|
||||
// If too many errors (8) are encountered then lexing will stop and
|
||||
// eof is returned as the next token.
|
||||
func (l *lexer) Errorf(f string, v ...interface{}) {
|
||||
buf := &bytes.Buffer{}
|
||||
|
||||
if l.debug {
|
||||
// For internal debugging, print the file and line number
|
||||
// of the call to Errorf
|
||||
_, name, line, _ := runtime.Caller(1)
|
||||
|
||||
fmt.Fprintf(buf, "%s:%d: ", name, line)
|
||||
}
|
||||
fmt.Fprintf(buf, "%s:%d:%d: ", l.file, l.line, l.col+1)
|
||||
fmt.Fprintf(buf, f, v...)
|
||||
b := buf.Bytes()
|
||||
if b[len(b)-1] != '\n' {
|
||||
buf.Write([]byte{'\n'})
|
||||
}
|
||||
l.emit(tError)
|
||||
l.adderror(buf.Bytes())
|
||||
}
|
||||
|
||||
func (l *lexer) ErrorfAt(line, col int, f string, v ...interface{}) {
|
||||
oline, ocol := l.line, l.col
|
||||
defer func() {
|
||||
l.line, l.col = oline, ocol
|
||||
}()
|
||||
l.line, l.col = line, col
|
||||
l.Errorf(f, v...)
|
||||
}
|
||||
|
||||
// adderror writes out the error string err and increases the error count.
|
||||
// If more than maxErrors are encountered, a "too many errors" message is
|
||||
// displayed and processing stops (by clearing the input).
|
||||
func (l *lexer) adderror(err []byte) {
|
||||
if l.errcnt == maxErrors {
|
||||
l.pos = 0
|
||||
l.start = 0
|
||||
l.input = ""
|
||||
l.errout.Write([]byte(tooMany))
|
||||
l.errcnt++
|
||||
return
|
||||
} else if l.errcnt == maxErrors+1 {
|
||||
return
|
||||
}
|
||||
l.errout.Write(err)
|
||||
l.errcnt++
|
||||
}
|
||||
|
||||
// Below are all the states
|
||||
|
||||
// lexGround is the state when the lexer is not in the middle of a token. The
|
||||
// ground state is left once the start of a token is found. Pure comment lines
|
||||
// leave the lexer in the ground state.
|
||||
func lexGround(l *lexer) stateFn {
|
||||
l.acceptRun(" \t\r\n") // Skip leading spaces
|
||||
l.consume()
|
||||
l.sline = l.line
|
||||
l.scol = l.col
|
||||
|
||||
switch c := l.peek(); c {
|
||||
case eof:
|
||||
return nil
|
||||
case ';', '{', '}':
|
||||
l.next()
|
||||
l.emit(code(c))
|
||||
return lexGround
|
||||
case '\'':
|
||||
l.next()
|
||||
l.consume() // Toss the leading '
|
||||
if !l.skipTo("'") {
|
||||
l.ErrorfAt(l.line, l.col-1, `missing closing '`)
|
||||
return nil
|
||||
}
|
||||
l.emit(tString)
|
||||
l.next() // Either EOF or the matching '
|
||||
return lexGround
|
||||
case '"':
|
||||
l.next()
|
||||
return lexQString
|
||||
case '/':
|
||||
l.next()
|
||||
switch l.peek() {
|
||||
case '/':
|
||||
// Start of a // comment
|
||||
if !l.skipTo("\n") {
|
||||
// Here "\n" should always be found, since we force all
|
||||
// input to be "\n" terminated.
|
||||
l.ErrorfAt(l.line, l.col-1, `lexer internal error: all lines should be newline-terminated.`)
|
||||
return nil
|
||||
}
|
||||
return lexGround
|
||||
case '*':
|
||||
// Start of a /* comment
|
||||
if !l.skipTo("*/") {
|
||||
l.ErrorfAt(l.line, l.col-1, `missing closing */`)
|
||||
return nil
|
||||
}
|
||||
// Now actually skip the */
|
||||
l.next()
|
||||
l.next()
|
||||
return lexGround
|
||||
default:
|
||||
return lexUnquoted
|
||||
}
|
||||
case '+':
|
||||
l.next()
|
||||
switch l.peek() {
|
||||
case '"', '\'':
|
||||
l.emit(tUnquoted)
|
||||
return lexGround
|
||||
default:
|
||||
return lexUnquoted
|
||||
}
|
||||
default:
|
||||
return lexUnquoted
|
||||
}
|
||||
}
|
||||
|
||||
// From the YANG standard:
|
||||
//
|
||||
// If the double-quoted string contains a line break followed by space
|
||||
// or tab characters that are used to indent the text according to the
|
||||
// layout in the YANG file, this leading whitespace is stripped from the
|
||||
// string, up to and including the column of the double quote character,
|
||||
// or to the first non-whitespace character, whichever occurs first. In
|
||||
// this process, a tab character is treated as 8 space characters.
|
||||
//
|
||||
// If the double-quoted string contains space or tab characters before a
|
||||
// line break, this trailing whitespace is stripped from the string.
|
||||
|
||||
// lexQString handles double quoted strings, see the above text on how they
|
||||
// work. The leading " has already been parsed.
|
||||
func lexQString(l *lexer) stateFn {
|
||||
indent := l.tcol // the column our text starts on
|
||||
over := true // set to false when we are not past the indent
|
||||
|
||||
// Keep track of where the starting quote was
|
||||
line, col := l.line, l.col-1
|
||||
|
||||
var text []byte
|
||||
for {
|
||||
// l.next can return non-8bit unicode code points.
|
||||
// c cannot be treated as only a single byte.
|
||||
switch c := l.next(); c {
|
||||
case eof:
|
||||
l.ErrorfAt(line, col, `missing closing "`)
|
||||
return nil
|
||||
case '"':
|
||||
l.emitText(tString, string(text))
|
||||
|
||||
return lexGround
|
||||
case '\n':
|
||||
Loop:
|
||||
// Trim trailing white space from the line.
|
||||
for i := len(text); i > 0; {
|
||||
i--
|
||||
switch text[i] {
|
||||
case ' ', '\t':
|
||||
text = text[:i]
|
||||
default:
|
||||
break Loop
|
||||
}
|
||||
}
|
||||
text = append(text, []byte(string(c))...)
|
||||
over = false
|
||||
case ' ', '\t':
|
||||
// Ignore leading white space up to our indent.
|
||||
if !over && l.tcol <= indent {
|
||||
break
|
||||
}
|
||||
over = true
|
||||
text = append(text, []byte(string(c))...)
|
||||
case '\\':
|
||||
switch c = l.next(); c {
|
||||
case 'n':
|
||||
c = '\n'
|
||||
case 't':
|
||||
c = '\t'
|
||||
case '"':
|
||||
case '\\':
|
||||
default:
|
||||
// Strings are use both in descriptions and
|
||||
// in patterns. In strings only \n, \t, \"
|
||||
// and \\ are defined. In patterns the \
|
||||
// can either mean to escape the character
|
||||
// (e..g., \{) or to be part of of a special
|
||||
// sequence such as \S.
|
||||
if !l.inPattern {
|
||||
l.ErrorfAt(l.line, l.col-2, `invalid escape sequence: \`+string(c))
|
||||
}
|
||||
text = append(text, '\\')
|
||||
}
|
||||
fallthrough
|
||||
default:
|
||||
over = true
|
||||
text = append(text, []byte(string(c))...)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// lexUnquoted reads one identifier/number/un-quoted-string/...
|
||||
//
|
||||
// From https://tools.ietf.org/html/rfc7950#section-6.1.3:
|
||||
// An unquoted string is any sequence of characters that does not
|
||||
// contain any space, tab, carriage return, or line feed characters, a
|
||||
// single or double quote character, a semicolon (";"), braces ("{" or
|
||||
// "}"), or comment sequences ("//", "/*", or "*/").
|
||||
func lexUnquoted(l *lexer) stateFn {
|
||||
for {
|
||||
switch c := l.peek(); c {
|
||||
// TODO: Support detection of comment immediately following an
|
||||
// unquoted string, likely through supporting two peeks instead
|
||||
// of just one.
|
||||
case ' ', '\r', '\n', '\t', ';', '"', '\'', '{', '}', eof:
|
||||
l.emit(tUnquoted)
|
||||
return lexGround
|
||||
default:
|
||||
l.next()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,309 @@
|
||||
// Copyright 2015 Google Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package yang
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"runtime"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// line returns the line number from which it was called.
|
||||
// Used to mark where test entries are in the source.
|
||||
func line() int {
|
||||
_, _, line, _ := runtime.Caller(1)
|
||||
return line
|
||||
|
||||
}
|
||||
|
||||
// Equal returns true if t and tt are equal (have the same code and text),
|
||||
// false if not.
|
||||
func (t *token) Equal(tt *token) bool {
|
||||
return t.code == tt.code && t.Text == tt.Text
|
||||
}
|
||||
|
||||
// T Creates a new token from the provided code and string.
|
||||
func T(c code, text string) *token { return &token{code: c, Text: text} }
|
||||
|
||||
func TestLex(t *testing.T) {
|
||||
Tests:
|
||||
for _, tt := range []struct {
|
||||
line int
|
||||
in string
|
||||
tokens []*token
|
||||
}{
|
||||
{line(), "", nil},
|
||||
{line(), "bob", []*token{
|
||||
T(tUnquoted, "bob"),
|
||||
}},
|
||||
{line(), "bob //bob", []*token{
|
||||
T(tUnquoted, "bob"),
|
||||
}},
|
||||
{line(), "/the/path", []*token{
|
||||
T(tUnquoted, "/the/path"),
|
||||
}},
|
||||
{line(), "+the/path", []*token{
|
||||
T(tUnquoted, "+the/path"),
|
||||
}},
|
||||
{line(), "+the+path", []*token{
|
||||
T(tUnquoted, "+the+path"),
|
||||
}},
|
||||
{line(), "+ the/path", []*token{
|
||||
T(tUnquoted, "+"),
|
||||
T(tUnquoted, "the/path"),
|
||||
}},
|
||||
{line(), "{bob}", []*token{
|
||||
T('{', "{"),
|
||||
T(tUnquoted, "bob"),
|
||||
T('}', "}"),
|
||||
}},
|
||||
{line(), "bob;fred", []*token{
|
||||
T(tUnquoted, "bob"),
|
||||
T(';', ";"),
|
||||
T(tUnquoted, "fred"),
|
||||
}},
|
||||
{line(), "\t bob\t; fred ", []*token{
|
||||
T(tUnquoted, "bob"),
|
||||
T(';', ";"),
|
||||
T(tUnquoted, "fred"),
|
||||
}},
|
||||
{line(), `
|
||||
bob;
|
||||
fred
|
||||
`, []*token{
|
||||
T(tUnquoted, "bob"),
|
||||
T(';', ";"),
|
||||
T(tUnquoted, "fred"),
|
||||
}},
|
||||
{line(), `
|
||||
// This is a comment
|
||||
bob;
|
||||
fred
|
||||
`, []*token{
|
||||
T(tUnquoted, "bob"),
|
||||
T(';', ";"),
|
||||
T(tUnquoted, "fred"),
|
||||
}},
|
||||
{line(), `
|
||||
/* This is a comment */
|
||||
bob;
|
||||
fred
|
||||
`, []*token{
|
||||
T(tUnquoted, "bob"),
|
||||
T(';', ";"),
|
||||
T(tUnquoted, "fred"),
|
||||
}},
|
||||
{line(), `
|
||||
/*
|
||||
* This is a comment
|
||||
*/
|
||||
bob;
|
||||
fred
|
||||
`, []*token{
|
||||
T(tUnquoted, "bob"),
|
||||
T(';', ";"),
|
||||
T(tUnquoted, "fred"),
|
||||
}},
|
||||
{line(), `
|
||||
bob; // This is bob
|
||||
fred // This is fred
|
||||
`, []*token{
|
||||
T(tUnquoted, "bob"),
|
||||
T(';', ";"),
|
||||
T(tUnquoted, "fred"),
|
||||
}},
|
||||
{line(), `
|
||||
pattern '[a-zA-Z0-9!#$%&'+"'"+'*+/=?^_` + "`" + `{|}~-]+';
|
||||
`, []*token{
|
||||
T(tUnquoted, "pattern"),
|
||||
T(tString, "[a-zA-Z0-9!#$%&"),
|
||||
T(tUnquoted, "+"),
|
||||
T(tString, "'"),
|
||||
T(tUnquoted, "+"),
|
||||
T(tString, "*+/=?^_`{|}~-]+"),
|
||||
T(';', ";"),
|
||||
}},
|
||||
{line(), `
|
||||
// tab indent both lines
|
||||
"Broken
|
||||
line"
|
||||
`, []*token{
|
||||
T(tString, "Broken\nline"),
|
||||
}},
|
||||
{line(), `
|
||||
// tab indent both lines, trailing spaces and tabs
|
||||
"Broken
|
||||
line"
|
||||
`, []*token{
|
||||
T(tString, "Broken\nline"),
|
||||
}},
|
||||
{line(), `
|
||||
// tab indent first line, spaces and tab second line
|
||||
"Broken
|
||||
line"
|
||||
`, []*token{
|
||||
T(tString, "Broken\nline"),
|
||||
}},
|
||||
{line(), `
|
||||
// tab indent first line, spaces second linfe
|
||||
"Broken
|
||||
line"
|
||||
`, []*token{
|
||||
T(tString, "Broken\nline"),
|
||||
}},
|
||||
{line(), `
|
||||
// extra space in second line
|
||||
"Broken
|
||||
space"
|
||||
`, []*token{
|
||||
T(tString, "Broken\n space"),
|
||||
}},
|
||||
{line(), `
|
||||
// spaces first line, tab on second
|
||||
"Broken
|
||||
space"
|
||||
`, []*token{
|
||||
T(tString, "Broken\nspace"),
|
||||
}},
|
||||
{line(), `
|
||||
// Odd indenting
|
||||
"Broken
|
||||
space"
|
||||
`, []*token{
|
||||
T(tString, "Broken\nspace"),
|
||||
}},
|
||||
{line(), `
|
||||
// Odd indenting
|
||||
"Broken \t
|
||||
space with trailing space"
|
||||
`, []*token{
|
||||
T(tString, "Broken\nspace with trailing space"),
|
||||
}},
|
||||
} {
|
||||
l := newLexer(tt.in, "")
|
||||
// l.debug = true
|
||||
for i := 0; ; i++ {
|
||||
token := l.NextToken()
|
||||
if token == nil {
|
||||
if len(tt.tokens) != i {
|
||||
t.Errorf("%d: got %d tokens, want %d", tt.line, i, len(tt.tokens))
|
||||
}
|
||||
continue Tests
|
||||
}
|
||||
if len(tt.tokens) > i && !token.Equal(tt.tokens[i]) {
|
||||
t.Errorf("%d, %d: got (%v, %q) want (%v, %q)", tt.line, i, token.code, token.Text, tt.tokens[i].code, tt.tokens[i].Text)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestLexErrors(t *testing.T) {
|
||||
for _, tt := range []struct {
|
||||
line int
|
||||
in string
|
||||
errcnt int
|
||||
errs string
|
||||
}{
|
||||
{line(),
|
||||
`1: "no closing quote`,
|
||||
1,
|
||||
`test.yang:1:4: missing closing "
|
||||
`,
|
||||
},
|
||||
{line(),
|
||||
`1: on another line
|
||||
2: there is "no closing quote\"`,
|
||||
1,
|
||||
`test.yang:2:13: missing closing "
|
||||
`,
|
||||
},
|
||||
{line(),
|
||||
`1:
|
||||
2: "Mares eat oats,"
|
||||
3: "And does eat oats,"
|
||||
4: "But little lambs eat ivy,"
|
||||
5: "and if I were a little lamb,"
|
||||
6: "I'ld eat ivy too.
|
||||
5: So saith the sage.`,
|
||||
1,
|
||||
`test.yang:6:4: missing closing "
|
||||
`,
|
||||
},
|
||||
{line(),
|
||||
`1:
|
||||
2: "Quoted string"
|
||||
3: "Missing quote
|
||||
4: "Another quoted string"
|
||||
`,
|
||||
1,
|
||||
`test.yang:4:26: missing closing "
|
||||
`,
|
||||
},
|
||||
{line(),
|
||||
`1:
|
||||
2: 'Quoted string'
|
||||
3: 'Missing quote
|
||||
4: 'Another quoted string'
|
||||
`,
|
||||
1,
|
||||
`test.yang:4:26: missing closing '
|
||||
`,
|
||||
},
|
||||
{line(),
|
||||
`1: "Quoted string\"
|
||||
2: Missing end-quote\q`,
|
||||
2,
|
||||
`test.yang:2:21: invalid escape sequence: \q
|
||||
test.yang:1:4: missing closing "
|
||||
`,
|
||||
},
|
||||
{line(),
|
||||
`/* This is a comment
|
||||
without an ending.
|
||||
`,
|
||||
1,
|
||||
`test.yang:1:1: missing closing */
|
||||
`,
|
||||
},
|
||||
{line(),
|
||||
// Two errors too many.
|
||||
`yang-version 1.1;description "\/\/\/\/\/\/\/\/\/\/";`,
|
||||
9,
|
||||
`test.yang:1:31: invalid escape sequence: \/
|
||||
test.yang:1:33: invalid escape sequence: \/
|
||||
test.yang:1:35: invalid escape sequence: \/
|
||||
test.yang:1:37: invalid escape sequence: \/
|
||||
test.yang:1:39: invalid escape sequence: \/
|
||||
test.yang:1:41: invalid escape sequence: \/
|
||||
test.yang:1:43: invalid escape sequence: \/
|
||||
test.yang:1:45: invalid escape sequence: \/
|
||||
` + tooMany,
|
||||
},
|
||||
} {
|
||||
l := newLexer(tt.in, "test.yang")
|
||||
errbuf := &bytes.Buffer{}
|
||||
l.errout = errbuf
|
||||
for l.NextToken() != nil {
|
||||
|
||||
}
|
||||
if l.errcnt != tt.errcnt {
|
||||
t.Errorf("%d: got %d errors, want %v", tt.line, l.errcnt, tt.errcnt)
|
||||
}
|
||||
errs := errbuf.String()
|
||||
if errs != tt.errs {
|
||||
t.Errorf("%d: got errors:\n%s\nwant:\n%s", tt.line, errs, tt.errs)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,832 @@
|
||||
// Copyright 2017 Google Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package yang
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/kylelemons/godebug/pretty"
|
||||
)
|
||||
|
||||
func TestMarshalJSON(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
in *Entry
|
||||
want string
|
||||
wantErr bool
|
||||
}{{
|
||||
name: "simple leaf entry",
|
||||
in: &Entry{
|
||||
Name: "leaf",
|
||||
Node: &Leaf{
|
||||
Name: "leaf",
|
||||
},
|
||||
Description: "This is a fake leaf.",
|
||||
Default: []string{"default-leaf-value"},
|
||||
Errors: []error{fmt.Errorf("error one")},
|
||||
Kind: LeafEntry,
|
||||
Config: TSTrue,
|
||||
Prefix: &Value{
|
||||
Name: "ModulePrefix",
|
||||
Source: &Statement{
|
||||
Keyword: "prefix",
|
||||
Argument: "ModulePrefix",
|
||||
HasArgument: true,
|
||||
},
|
||||
},
|
||||
Type: &YangType{
|
||||
Name: "string",
|
||||
Kind: Ystring,
|
||||
Default: "string-value",
|
||||
},
|
||||
Annotation: map[string]interface{}{
|
||||
"fish": struct{ Side string }{"chips"},
|
||||
},
|
||||
},
|
||||
want: `{
|
||||
"Name": "leaf",
|
||||
"Description": "This is a fake leaf.",
|
||||
"Default": [
|
||||
"default-leaf-value"
|
||||
],
|
||||
"Kind": 0,
|
||||
"Config": 1,
|
||||
"Prefix": {
|
||||
"Name": "ModulePrefix",
|
||||
"Source": {
|
||||
"Keyword": "prefix",
|
||||
"HasArgument": true,
|
||||
"Argument": "ModulePrefix"
|
||||
}
|
||||
},
|
||||
"Type": {
|
||||
"Name": "string",
|
||||
"Kind": 18,
|
||||
"Default": "string-value"
|
||||
},
|
||||
"Annotation": {
|
||||
"fish": {
|
||||
"Side": "chips"
|
||||
}
|
||||
}
|
||||
}`,
|
||||
}, {
|
||||
name: "simple container entry with parent",
|
||||
in: &Entry{
|
||||
Name: "container",
|
||||
Node: &Container{
|
||||
Name: "container",
|
||||
},
|
||||
Kind: DirectoryEntry,
|
||||
Config: TSFalse,
|
||||
Prefix: &Value{
|
||||
Name: "ModulePrefix",
|
||||
Source: &Statement{
|
||||
Keyword: "prefix",
|
||||
Argument: "ModulePrefix",
|
||||
HasArgument: true,
|
||||
},
|
||||
},
|
||||
Dir: map[string]*Entry{
|
||||
"child": {
|
||||
Name: "leaf",
|
||||
Node: &Leaf{
|
||||
Name: "leaf",
|
||||
},
|
||||
Kind: LeafEntry,
|
||||
Config: TSUnset,
|
||||
Prefix: &Value{
|
||||
Name: "ModulePrefix",
|
||||
Source: &Statement{
|
||||
Keyword: "prefix",
|
||||
Argument: "ModulePrefix",
|
||||
HasArgument: true,
|
||||
},
|
||||
},
|
||||
Type: &YangType{
|
||||
Name: "union",
|
||||
Type: []*YangType{{
|
||||
Name: "string",
|
||||
Pattern: []string{"^a.*$"},
|
||||
Kind: Ystring,
|
||||
Length: YangRange{{
|
||||
Min: FromInt(10),
|
||||
Max: FromInt(20),
|
||||
}},
|
||||
}},
|
||||
},
|
||||
},
|
||||
},
|
||||
Augments: []*Entry{{
|
||||
Name: "augment",
|
||||
Node: &Leaf{
|
||||
Name: "leaf",
|
||||
},
|
||||
Kind: LeafEntry,
|
||||
Config: TSFalse,
|
||||
Prefix: &Value{
|
||||
Name: "ModulePrefix",
|
||||
Source: &Statement{
|
||||
Keyword: "prefix",
|
||||
Argument: "ModulePrefix",
|
||||
HasArgument: true,
|
||||
},
|
||||
},
|
||||
}},
|
||||
Augmented: []*Entry{{
|
||||
Name: "augmented",
|
||||
Node: &Leaf{
|
||||
Name: "leaf",
|
||||
},
|
||||
Kind: LeafEntry,
|
||||
Config: TSTrue,
|
||||
Prefix: &Value{
|
||||
Name: "ModulePrefix",
|
||||
Source: &Statement{
|
||||
Keyword: "prefix",
|
||||
Argument: "ModulePrefix",
|
||||
HasArgument: true,
|
||||
},
|
||||
},
|
||||
}},
|
||||
Uses: []*UsesStmt{{
|
||||
Uses: &Uses{
|
||||
Name: "grouping",
|
||||
},
|
||||
Grouping: &Entry{
|
||||
Name: "grouping",
|
||||
Node: &Grouping{
|
||||
Name: "grouping",
|
||||
Leaf: []*Leaf{{
|
||||
Name: "groupingLeaf",
|
||||
}},
|
||||
},
|
||||
Config: TSFalse,
|
||||
Prefix: &Value{
|
||||
Name: "ModulePrefix",
|
||||
Source: &Statement{
|
||||
Keyword: "prefix",
|
||||
Argument: "ModulePrefix",
|
||||
HasArgument: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
}},
|
||||
},
|
||||
want: `{
|
||||
"Name": "container",
|
||||
"Kind": 1,
|
||||
"Config": 2,
|
||||
"Prefix": {
|
||||
"Name": "ModulePrefix",
|
||||
"Source": {
|
||||
"Keyword": "prefix",
|
||||
"HasArgument": true,
|
||||
"Argument": "ModulePrefix"
|
||||
}
|
||||
},
|
||||
"Dir": {
|
||||
"child": {
|
||||
"Name": "leaf",
|
||||
"Kind": 0,
|
||||
"Config": 0,
|
||||
"Prefix": {
|
||||
"Name": "ModulePrefix",
|
||||
"Source": {
|
||||
"Keyword": "prefix",
|
||||
"HasArgument": true,
|
||||
"Argument": "ModulePrefix"
|
||||
}
|
||||
},
|
||||
"Type": {
|
||||
"Name": "union",
|
||||
"Kind": 0,
|
||||
"Type": [
|
||||
{
|
||||
"Name": "string",
|
||||
"Kind": 18,
|
||||
"Length": [
|
||||
{
|
||||
"Min": {
|
||||
"Value": 10,
|
||||
"FractionDigits": 0,
|
||||
"Negative": false
|
||||
},
|
||||
"Max": {
|
||||
"Value": 20,
|
||||
"FractionDigits": 0,
|
||||
"Negative": false
|
||||
}
|
||||
}
|
||||
],
|
||||
"Pattern": [
|
||||
"^a.*$"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"Augments": [
|
||||
{
|
||||
"Name": "augment",
|
||||
"Kind": 0,
|
||||
"Config": 2,
|
||||
"Prefix": {
|
||||
"Name": "ModulePrefix",
|
||||
"Source": {
|
||||
"Keyword": "prefix",
|
||||
"HasArgument": true,
|
||||
"Argument": "ModulePrefix"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"Augmented": [
|
||||
{
|
||||
"Name": "augmented",
|
||||
"Kind": 0,
|
||||
"Config": 1,
|
||||
"Prefix": {
|
||||
"Name": "ModulePrefix",
|
||||
"Source": {
|
||||
"Keyword": "prefix",
|
||||
"HasArgument": true,
|
||||
"Argument": "ModulePrefix"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"Uses": [
|
||||
{
|
||||
"Uses": {
|
||||
"Name": "grouping"
|
||||
},
|
||||
"Grouping": {
|
||||
"Name": "grouping",
|
||||
"Kind": 0,
|
||||
"Config": 2,
|
||||
"Prefix": {
|
||||
"Name": "ModulePrefix",
|
||||
"Source": {
|
||||
"Keyword": "prefix",
|
||||
"HasArgument": true,
|
||||
"Argument": "ModulePrefix"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}`,
|
||||
}, {
|
||||
name: "Entry with list and leaflist",
|
||||
in: &Entry{
|
||||
Name: "list",
|
||||
Kind: DirectoryEntry,
|
||||
Config: TSUnset,
|
||||
Dir: map[string]*Entry{
|
||||
"leaf": {
|
||||
Name: "string",
|
||||
Kind: LeafEntry,
|
||||
},
|
||||
"leaf-list": {
|
||||
Name: "leaf-list",
|
||||
ListAttr: &ListAttr{
|
||||
MaxElements: 18446744073709551615,
|
||||
MinElements: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
ListAttr: &ListAttr{
|
||||
MaxElements: 42,
|
||||
MinElements: 48,
|
||||
},
|
||||
Identities: []*Identity{{
|
||||
Name: "ID_ONE",
|
||||
}},
|
||||
Exts: []*Statement{{
|
||||
Keyword: "some-extension:ext",
|
||||
Argument: "ext-value",
|
||||
HasArgument: true,
|
||||
}},
|
||||
},
|
||||
want: `{
|
||||
"Name": "list",
|
||||
"Kind": 1,
|
||||
"Config": 0,
|
||||
"Dir": {
|
||||
"leaf": {
|
||||
"Name": "string",
|
||||
"Kind": 0,
|
||||
"Config": 0
|
||||
},
|
||||
"leaf-list": {
|
||||
"Name": "leaf-list",
|
||||
"Kind": 0,
|
||||
"Config": 0,
|
||||
"ListAttr": {
|
||||
"MinElements": 0,
|
||||
"MaxElements": 18446744073709551615,
|
||||
"OrderedBy": null,
|
||||
"OrderedByUser": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"Exts": [
|
||||
{
|
||||
"Keyword": "some-extension:ext",
|
||||
"HasArgument": true,
|
||||
"Argument": "ext-value"
|
||||
}
|
||||
],
|
||||
"ListAttr": {
|
||||
"MinElements": 48,
|
||||
"MaxElements": 42,
|
||||
"OrderedBy": null,
|
||||
"OrderedByUser": false
|
||||
},
|
||||
"Identities": [
|
||||
{
|
||||
"Name": "ID_ONE"
|
||||
}
|
||||
]
|
||||
}`,
|
||||
}}
|
||||
|
||||
for _, tt := range tests {
|
||||
got, err := json.MarshalIndent(tt.in, "", " ")
|
||||
if err != nil {
|
||||
if !tt.wantErr {
|
||||
t.Errorf("%s: json.MarshalIndent(%v, ...): got unexpected error: %v", tt.name, tt.in, err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if diff := pretty.Compare(string(got), tt.want); diff != "" {
|
||||
t.Errorf("%s: jsonMarshalIndent(%v, ...): did not get expected JSON, diff(-got,+want):\n%s", tt.name, tt.in, diff)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseAndMarshal(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
in []inputModule
|
||||
want map[string]string
|
||||
}{{
|
||||
name: "simple single module",
|
||||
in: []inputModule{{
|
||||
name: "test.yang",
|
||||
content: `module test {
|
||||
prefix "t";
|
||||
namespace "urn:t";
|
||||
|
||||
typedef foobar {
|
||||
type string {
|
||||
length "10";
|
||||
}
|
||||
}
|
||||
|
||||
identity "BASE";
|
||||
identity "DERIVED" { base "BASE"; }
|
||||
|
||||
container test {
|
||||
list a {
|
||||
key "k";
|
||||
min-elements 10;
|
||||
max-elements "unbounded";
|
||||
leaf k { type string; }
|
||||
|
||||
leaf bar {
|
||||
type foobar;
|
||||
}
|
||||
}
|
||||
|
||||
leaf d {
|
||||
type decimal64 {
|
||||
fraction-digits 8;
|
||||
}
|
||||
}
|
||||
|
||||
leaf-list zip {
|
||||
type string;
|
||||
}
|
||||
|
||||
leaf-list zip2 {
|
||||
max-elements 1000;
|
||||
type string;
|
||||
}
|
||||
|
||||
leaf x {
|
||||
type union {
|
||||
type string;
|
||||
type identityref {
|
||||
base "BASE";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}`,
|
||||
}},
|
||||
want: map[string]string{
|
||||
"test": `{
|
||||
"Name": "test",
|
||||
"Kind": 1,
|
||||
"Config": 0,
|
||||
"Prefix": {
|
||||
"Name": "t",
|
||||
"Source": {
|
||||
"Keyword": "prefix",
|
||||
"HasArgument": true,
|
||||
"Argument": "t"
|
||||
}
|
||||
},
|
||||
"Dir": {
|
||||
"test": {
|
||||
"Name": "test",
|
||||
"Kind": 1,
|
||||
"Config": 0,
|
||||
"Prefix": {
|
||||
"Name": "t",
|
||||
"Source": {
|
||||
"Keyword": "prefix",
|
||||
"HasArgument": true,
|
||||
"Argument": "t"
|
||||
}
|
||||
},
|
||||
"Dir": {
|
||||
"a": {
|
||||
"Name": "a",
|
||||
"Kind": 1,
|
||||
"Config": 0,
|
||||
"Prefix": {
|
||||
"Name": "t",
|
||||
"Source": {
|
||||
"Keyword": "prefix",
|
||||
"HasArgument": true,
|
||||
"Argument": "t"
|
||||
}
|
||||
},
|
||||
"Dir": {
|
||||
"bar": {
|
||||
"Name": "bar",
|
||||
"Kind": 0,
|
||||
"Config": 0,
|
||||
"Prefix": {
|
||||
"Name": "t",
|
||||
"Source": {
|
||||
"Keyword": "prefix",
|
||||
"HasArgument": true,
|
||||
"Argument": "t"
|
||||
}
|
||||
},
|
||||
"Type": {
|
||||
"Name": "foobar",
|
||||
"Kind": 18,
|
||||
"Length": [
|
||||
{
|
||||
"Min": {
|
||||
"Value": 10,
|
||||
"FractionDigits": 0,
|
||||
"Negative": false
|
||||
},
|
||||
"Max": {
|
||||
"Value": 10,
|
||||
"FractionDigits": 0,
|
||||
"Negative": false
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"k": {
|
||||
"Name": "k",
|
||||
"Kind": 0,
|
||||
"Config": 0,
|
||||
"Prefix": {
|
||||
"Name": "t",
|
||||
"Source": {
|
||||
"Keyword": "prefix",
|
||||
"HasArgument": true,
|
||||
"Argument": "t"
|
||||
}
|
||||
},
|
||||
"Type": {
|
||||
"Name": "string",
|
||||
"Kind": 18
|
||||
}
|
||||
}
|
||||
},
|
||||
"Key": "k",
|
||||
"ListAttr": {
|
||||
"MinElements": 10,
|
||||
"MaxElements": 18446744073709551615,
|
||||
"OrderedBy": null,
|
||||
"OrderedByUser": false
|
||||
}
|
||||
},
|
||||
"d": {
|
||||
"Name": "d",
|
||||
"Kind": 0,
|
||||
"Config": 0,
|
||||
"Prefix": {
|
||||
"Name": "t",
|
||||
"Source": {
|
||||
"Keyword": "prefix",
|
||||
"HasArgument": true,
|
||||
"Argument": "t"
|
||||
}
|
||||
},
|
||||
"Type": {
|
||||
"Name": "decimal64",
|
||||
"Kind": 12,
|
||||
"FractionDigits": 8,
|
||||
"Range": [
|
||||
{
|
||||
"Min": {
|
||||
"Value": 9223372036854775808,
|
||||
"FractionDigits": 8,
|
||||
"Negative": true
|
||||
},
|
||||
"Max": {
|
||||
"Value": 9223372036854775807,
|
||||
"FractionDigits": 8,
|
||||
"Negative": false
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"x": {
|
||||
"Name": "x",
|
||||
"Kind": 0,
|
||||
"Config": 0,
|
||||
"Prefix": {
|
||||
"Name": "t",
|
||||
"Source": {
|
||||
"Keyword": "prefix",
|
||||
"HasArgument": true,
|
||||
"Argument": "t"
|
||||
}
|
||||
},
|
||||
"Type": {
|
||||
"Name": "union",
|
||||
"Kind": 19,
|
||||
"Type": [
|
||||
{
|
||||
"Name": "string",
|
||||
"Kind": 18
|
||||
},
|
||||
{
|
||||
"Name": "identityref",
|
||||
"Kind": 15,
|
||||
"IdentityBase": {
|
||||
"Name": "BASE",
|
||||
"Values": [
|
||||
{
|
||||
"Name": "DERIVED"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"zip": {
|
||||
"Name": "zip",
|
||||
"Kind": 0,
|
||||
"Config": 0,
|
||||
"Prefix": {
|
||||
"Name": "t",
|
||||
"Source": {
|
||||
"Keyword": "prefix",
|
||||
"HasArgument": true,
|
||||
"Argument": "t"
|
||||
}
|
||||
},
|
||||
"Type": {
|
||||
"Name": "string",
|
||||
"Kind": 18
|
||||
},
|
||||
"ListAttr": {
|
||||
"MinElements": 0,
|
||||
"MaxElements": 18446744073709551615,
|
||||
"OrderedBy": null,
|
||||
"OrderedByUser": false
|
||||
}
|
||||
},
|
||||
"zip2": {
|
||||
"Name": "zip2",
|
||||
"Kind": 0,
|
||||
"Config": 0,
|
||||
"Prefix": {
|
||||
"Name": "t",
|
||||
"Source": {
|
||||
"Keyword": "prefix",
|
||||
"HasArgument": true,
|
||||
"Argument": "t"
|
||||
}
|
||||
},
|
||||
"Type": {
|
||||
"Name": "string",
|
||||
"Kind": 18
|
||||
},
|
||||
"ListAttr": {
|
||||
"MinElements": 0,
|
||||
"MaxElements": 1000,
|
||||
"OrderedBy": null,
|
||||
"OrderedByUser": false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Identities": [
|
||||
{
|
||||
"Name": "BASE",
|
||||
"Values": [
|
||||
{
|
||||
"Name": "DERIVED"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"Name": "DERIVED"
|
||||
}
|
||||
],
|
||||
"extra-unstable": {
|
||||
"namespace": [
|
||||
{
|
||||
"Name": "urn:t",
|
||||
"Source": {
|
||||
"Keyword": "namespace",
|
||||
"HasArgument": true,
|
||||
"Argument": "urn:t"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}`,
|
||||
},
|
||||
}, {
|
||||
name: "multiple modules with extension",
|
||||
in: []inputModule{{
|
||||
name: "ext.yang",
|
||||
content: `module ext {
|
||||
prefix "e";
|
||||
namespace "urn:e";
|
||||
|
||||
extension foobar {
|
||||
argument "baz";
|
||||
}
|
||||
}`,
|
||||
}, {
|
||||
name: "test.yang",
|
||||
content: `module test {
|
||||
prefix "t";
|
||||
namespace "urn:t";
|
||||
|
||||
import ext { prefix ext; }
|
||||
|
||||
leaf t {
|
||||
type string;
|
||||
ext:foobar "marked";
|
||||
}
|
||||
}`,
|
||||
}},
|
||||
want: map[string]string{
|
||||
"test": `{
|
||||
"Name": "test",
|
||||
"Kind": 1,
|
||||
"Config": 0,
|
||||
"Prefix": {
|
||||
"Name": "t",
|
||||
"Source": {
|
||||
"Keyword": "prefix",
|
||||
"HasArgument": true,
|
||||
"Argument": "t"
|
||||
}
|
||||
},
|
||||
"Dir": {
|
||||
"t": {
|
||||
"Name": "t",
|
||||
"Kind": 0,
|
||||
"Config": 0,
|
||||
"Prefix": {
|
||||
"Name": "t",
|
||||
"Source": {
|
||||
"Keyword": "prefix",
|
||||
"HasArgument": true,
|
||||
"Argument": "t"
|
||||
}
|
||||
},
|
||||
"Type": {
|
||||
"Name": "string",
|
||||
"Kind": 18
|
||||
},
|
||||
"Exts": [
|
||||
{
|
||||
"Keyword": "ext:foobar",
|
||||
"HasArgument": true,
|
||||
"Argument": "marked"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"extra-unstable": {
|
||||
"namespace": [
|
||||
{
|
||||
"Name": "urn:t",
|
||||
"Source": {
|
||||
"Keyword": "namespace",
|
||||
"HasArgument": true,
|
||||
"Argument": "urn:t"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}`,
|
||||
"ext": `{
|
||||
"Name": "ext",
|
||||
"Kind": 1,
|
||||
"Config": 0,
|
||||
"Prefix": {
|
||||
"Name": "e",
|
||||
"Source": {
|
||||
"Keyword": "prefix",
|
||||
"HasArgument": true,
|
||||
"Argument": "e"
|
||||
}
|
||||
},
|
||||
"extra-unstable": {
|
||||
"extension": [
|
||||
{
|
||||
"Name": "foobar",
|
||||
"Argument": {
|
||||
"Name": "baz"
|
||||
}
|
||||
}
|
||||
],
|
||||
"namespace": [
|
||||
{
|
||||
"Name": "urn:e",
|
||||
"Source": {
|
||||
"Keyword": "namespace",
|
||||
"HasArgument": true,
|
||||
"Argument": "urn:e"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}`,
|
||||
},
|
||||
}}
|
||||
|
||||
for _, tt := range tests {
|
||||
ms := NewModules()
|
||||
|
||||
for _, mod := range tt.in {
|
||||
if err := ms.Parse(mod.content, mod.name); err != nil {
|
||||
t.Errorf("%s: ms.Parse(..., %v): parsing error with module: %v", tt.name, mod.name, err)
|
||||
continue
|
||||
}
|
||||
|
||||
if errs := ms.Process(); len(errs) != 0 {
|
||||
t.Errorf("%s: ms.Process(): could not parse modules: %v", tt.name, errs)
|
||||
continue
|
||||
}
|
||||
|
||||
entries := make(map[string]*Entry)
|
||||
for _, m := range ms.Modules {
|
||||
if _, ok := entries[m.Name]; !ok {
|
||||
entries[m.Name] = ToEntry(m)
|
||||
|
||||
got, err := json.MarshalIndent(entries[m.Name], "", " ")
|
||||
if err != nil {
|
||||
t.Errorf("%s: json.MarshalIndent(...): got unexpected error: %v", tt.name, err)
|
||||
continue
|
||||
}
|
||||
|
||||
if diff := cmp.Diff(string(got), tt.want[m.Name]); diff != "" {
|
||||
t.Errorf("%s: json.MarshalIndent(...): did not get expected JSON, diff(-got,+want):\n%s", tt.name, diff)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,466 @@
|
||||
// Copyright 2015 Google Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package yang
|
||||
|
||||
// This file implements the Modules type. This includes the processing of
|
||||
// include and import statements, which must be done prior to turning the
|
||||
// module into an Entry tree.
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// Modules contains information about all the top level modules and
|
||||
// submodules that are read into it via its Read method.
|
||||
type Modules struct {
|
||||
Modules map[string]*Module // All "module" nodes
|
||||
SubModules map[string]*Module // All "submodule" nodes
|
||||
includes map[*Module]bool // Modules we have already done include on
|
||||
nsMu sync.Mutex // nsMu protects the byNS map.
|
||||
byNS map[string]*Module // Cache of namespace lookup
|
||||
typeDict *typeDictionary // Cache for type definitions.
|
||||
entryCacheMu sync.RWMutex // entryCacheMu protects the entryCache map.
|
||||
// entryCache is used to prevent unnecessary recursion into previously
|
||||
// converted nodes. To access the map, use the get/set/ClearEntryCache()
|
||||
// thread-safe functions.
|
||||
entryCache map[Node]*Entry
|
||||
// mergedSubmodule is used to prevent re-parsing a submodule that has already
|
||||
// been merged into a particular entity when circular dependencies are being
|
||||
// ignored. The keys of the map are a string that is formed by concatenating
|
||||
// the name of the including (sub)module and the included submodule.
|
||||
mergedSubmodule map[string]bool
|
||||
// ParseOptions sets the options for the current YANG module parsing. It can be
|
||||
// directly set by the caller to influence how goyang will behave in the presence
|
||||
// of certain exceptional cases.
|
||||
ParseOptions Options
|
||||
// Path is the list of directories to look for .yang files in.
|
||||
Path []string
|
||||
// pathMap is used to prevent adding dups in Path.
|
||||
pathMap map[string]bool
|
||||
}
|
||||
|
||||
// NewModules returns a newly created and initialized Modules.
|
||||
func NewModules() *Modules {
|
||||
ms := &Modules{
|
||||
Modules: map[string]*Module{},
|
||||
SubModules: map[string]*Module{},
|
||||
includes: map[*Module]bool{},
|
||||
byNS: map[string]*Module{},
|
||||
typeDict: newTypeDictionary(),
|
||||
mergedSubmodule: map[string]bool{},
|
||||
entryCache: map[Node]*Entry{},
|
||||
pathMap: map[string]bool{},
|
||||
}
|
||||
return ms
|
||||
}
|
||||
|
||||
// Read reads the named yang module into ms. The name can be the name of an
|
||||
// actual .yang file or a module/submodule name (the base name of a .yang file,
|
||||
// e.g., foo.yang is named foo). An error is returned if the file is not
|
||||
// found or there was an error parsing the file.
|
||||
func (ms *Modules) Read(name string) error {
|
||||
name, data, err := ms.findFile(name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return ms.Parse(data, name)
|
||||
}
|
||||
|
||||
// Parse parses data as YANG source and adds it to ms. The name should reflect
|
||||
// the source of data.
|
||||
// Note: If an error is returned, valid modules might still have been added to
|
||||
// the Modules cache.
|
||||
func (ms *Modules) Parse(data, name string) error {
|
||||
ss, err := Parse(data, name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, s := range ss {
|
||||
n, err := buildASTWithTypeDict(s, ms.typeDict)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := ms.add(n); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetModule returns the Entry of the module named by name. GetModule will
|
||||
// search for and read the file named name + ".yang" if it cannot satisfy the
|
||||
// request from what it has currently read.
|
||||
//
|
||||
// GetModule is a convenience function for calling Read and Process, and
|
||||
// then looking up the module name. It is safe to call Read and Process prior
|
||||
// to calling GetModule.
|
||||
func (ms *Modules) GetModule(name string) (*Entry, []error) {
|
||||
if ms.Modules[name] == nil {
|
||||
if err := ms.Read(name); err != nil {
|
||||
return nil, []error{err}
|
||||
}
|
||||
if ms.Modules[name] == nil {
|
||||
return nil, []error{fmt.Errorf("module not found: %s", name)}
|
||||
}
|
||||
}
|
||||
// Make sure that the modules have all been processed and have no
|
||||
// errors.
|
||||
if errs := ms.Process(); len(errs) != 0 {
|
||||
return nil, errs
|
||||
}
|
||||
return ToEntry(ms.Modules[name]), nil
|
||||
}
|
||||
|
||||
// GetModule optionally reads in a set of YANG source files, named by sources,
|
||||
// and then returns the Entry for the module named module. If sources is
|
||||
// missing, or the named module is not yet known, GetModule searches for name
|
||||
// with the suffix ".yang". GetModule either returns an Entry or returns
|
||||
// one or more errors.
|
||||
//
|
||||
// GetModule is a convenience function for calling NewModules, Read, and Process,
|
||||
// and then looking up the module name.
|
||||
func GetModule(name string, sources ...string) (*Entry, []error) {
|
||||
var errs []error
|
||||
ms := NewModules()
|
||||
for _, source := range sources {
|
||||
if err := ms.Read(source); err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
}
|
||||
if len(errs) > 0 {
|
||||
return nil, errs
|
||||
}
|
||||
return ms.GetModule(name)
|
||||
}
|
||||
|
||||
// add adds Node n to ms. n must be assignable to *Module (i.e., it is a
|
||||
// "module" or "submodule"). An error is returned if n is a duplicate of
|
||||
// a name already added, or n is not assignable to *Module.
|
||||
func (ms *Modules) add(n Node) error {
|
||||
var m map[string]*Module
|
||||
|
||||
name := n.NName()
|
||||
kind := n.Kind()
|
||||
switch kind {
|
||||
case "module":
|
||||
m = ms.Modules
|
||||
case "submodule":
|
||||
m = ms.SubModules
|
||||
default:
|
||||
return fmt.Errorf("not a module or submodule: %s is of type %s", name, kind)
|
||||
}
|
||||
|
||||
mod := n.(*Module)
|
||||
fullName := mod.FullName()
|
||||
mod.Modules = ms
|
||||
|
||||
if o := m[fullName]; o != nil {
|
||||
return fmt.Errorf("duplicate %s %s at %s and %s", kind, fullName, Source(o), Source(n))
|
||||
}
|
||||
m[fullName] = mod
|
||||
if fullName == name {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Add us to the map if:
|
||||
// name has not been added before
|
||||
// fullname is a more recent version of the entry.
|
||||
if o := m[name]; o == nil || o.FullName() < fullName {
|
||||
m[name] = mod
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// FindModule returns the Module/Submodule specified by n, which must be a
|
||||
// *Include or *Import. If n is a *Include then a submodule is returned. If n
|
||||
// is a *Import then a module is returned.
|
||||
func (ms *Modules) FindModule(n Node) *Module {
|
||||
name := n.NName()
|
||||
rev := name
|
||||
var m map[string]*Module
|
||||
|
||||
switch i := n.(type) {
|
||||
case *Include:
|
||||
m = ms.SubModules
|
||||
if i.RevisionDate != nil {
|
||||
rev = name + "@" + i.RevisionDate.Name
|
||||
}
|
||||
// TODO(borman): we should check the BelongsTo field below?
|
||||
case *Import:
|
||||
m = ms.Modules
|
||||
if i.RevisionDate != nil {
|
||||
rev = name + "@" + i.RevisionDate.Name
|
||||
}
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
if n := m[rev]; n != nil {
|
||||
return n
|
||||
}
|
||||
if n := m[name]; n != nil {
|
||||
return n
|
||||
}
|
||||
|
||||
// Try to read first a module by revision
|
||||
if err := ms.Read(rev); err != nil {
|
||||
// if failed, try to read a module by its bare name
|
||||
if err := ms.Read(name); err != nil {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
if n := m[rev]; n != nil {
|
||||
return n
|
||||
}
|
||||
return m[name]
|
||||
}
|
||||
|
||||
// FindModuleByNamespace either returns the Module specified by the namespace
|
||||
// or returns an error.
|
||||
func (ms *Modules) FindModuleByNamespace(ns string) (*Module, error) {
|
||||
// Protect the byNS map from concurrent accesses
|
||||
ms.nsMu.Lock()
|
||||
defer ms.nsMu.Unlock()
|
||||
|
||||
if m, ok := ms.byNS[ns]; ok {
|
||||
return m, nil
|
||||
}
|
||||
var found *Module
|
||||
for _, m := range ms.Modules {
|
||||
if m.Namespace.Name == ns {
|
||||
switch {
|
||||
case m == found:
|
||||
case found != nil:
|
||||
return nil, fmt.Errorf("namespace %s matches two or more modules (%s, %s)",
|
||||
ns, found.Name, m.Name)
|
||||
default:
|
||||
found = m
|
||||
}
|
||||
}
|
||||
}
|
||||
if found == nil {
|
||||
return nil, fmt.Errorf("%q: no such namespace", ns)
|
||||
}
|
||||
// Don't cache negative results because new modules could be added.
|
||||
ms.byNS[ns] = found
|
||||
return found, nil
|
||||
}
|
||||
|
||||
// process satisfies all include and import statements and verifies that all
|
||||
// link ref paths reference a known node. If an import or include references
|
||||
// a [sub]module that is not already known, Process will search for a .yang
|
||||
// file that contains it, returning an error if not found. An error is also
|
||||
// returned if there is an unknown link ref path or other parsing errors.
|
||||
//
|
||||
// Process must be called once all the source modules have been read in and
|
||||
// prior to converting Node tree into an Entry tree.
|
||||
func (ms *Modules) process() []error {
|
||||
var mods []*Module
|
||||
var errs []error
|
||||
|
||||
// Collect the list of modules we know about now so when we range
|
||||
// below we don't pick up new modules. We assume the user tells
|
||||
// us explicitly which modules they are interested in.
|
||||
for _, m := range ms.Modules {
|
||||
mods = append(mods, m)
|
||||
}
|
||||
for _, m := range mods {
|
||||
if err := ms.include(m); err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Resolve identities before resolving typedefs, otherwise when we resolve a
|
||||
// typedef that has an identityref within it, then the identity dictionary
|
||||
// has not yet been built.
|
||||
errs = append(errs, ms.resolveIdentities()...)
|
||||
// Append any errors found trying to resolve typedefs
|
||||
errs = append(errs, ms.typeDict.resolveTypedefs()...)
|
||||
|
||||
return errs
|
||||
}
|
||||
|
||||
// Process processes all the modules and submodules that have been read into
|
||||
// ms. While processing, if an include or import is found for which there
|
||||
// is no matching module, Process attempts to locate the source file (using
|
||||
// Path) and automatically load them. If a file cannot be found then an
|
||||
// error is returned. When looking for a source file, Process searches for a
|
||||
// file using the module's or submodule's name with ".yang" appended. After
|
||||
// searching the current directory, the directories in Path are searched.
|
||||
//
|
||||
// Process builds Entry trees for each modules and submodules in ms. These
|
||||
// trees are accessed using the ToEntry function. Process does augmentation
|
||||
// on Entry trees once all the modules and submodules in ms have been built.
|
||||
// Following augmentation, Process inserts implied case statements. I.e.,
|
||||
//
|
||||
// choice interface-type {
|
||||
// container ethernet { ... }
|
||||
// }
|
||||
//
|
||||
// has a case statement inserted to become:
|
||||
//
|
||||
// choice interface-type {
|
||||
// case ethernet {
|
||||
// container ethernet { ... }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// Process may return multiple errors if multiple errors were encountered
|
||||
// while processing. Even though multiple errors may be returned, this does
|
||||
// not mean these are all the errors. Process will terminate processing early
|
||||
// based on the type and location of the error.
|
||||
func (ms *Modules) Process() []error {
|
||||
// Reset globals that may remain stale if multiple Process() calls are
|
||||
// made by the same caller.
|
||||
ms.mergedSubmodule = map[string]bool{}
|
||||
ms.ClearEntryCache()
|
||||
|
||||
errs := ms.process()
|
||||
if len(errs) > 0 {
|
||||
return errorSort(errs)
|
||||
}
|
||||
|
||||
for _, m := range ms.Modules {
|
||||
errs = append(errs, ToEntry(m).GetErrors()...)
|
||||
}
|
||||
for _, m := range ms.SubModules {
|
||||
errs = append(errs, ToEntry(m).GetErrors()...)
|
||||
}
|
||||
|
||||
if len(errs) > 0 {
|
||||
return errorSort(errs)
|
||||
}
|
||||
|
||||
// Now handle all the augments. We don't have a good way to know
|
||||
// what order to process them in, so repeat until no progress is made
|
||||
|
||||
mods := make([]*Module, 0, len(ms.Modules)+len(ms.SubModules))
|
||||
for _, m := range ms.Modules {
|
||||
mods = append(mods, m)
|
||||
}
|
||||
for _, m := range ms.SubModules {
|
||||
mods = append(mods, m)
|
||||
}
|
||||
for len(mods) > 0 {
|
||||
var processed int
|
||||
for i := 0; i < len(mods); {
|
||||
m := mods[i]
|
||||
p, s := ToEntry(m).Augment(false)
|
||||
processed += p
|
||||
if s == 0 {
|
||||
mods[i] = mods[len(mods)-1]
|
||||
mods = mods[:len(mods)-1]
|
||||
continue
|
||||
}
|
||||
i++
|
||||
}
|
||||
if processed == 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Now fix up all the choice statements to add in the missing case
|
||||
// statements.
|
||||
for _, m := range ms.Modules {
|
||||
ToEntry(m).FixChoice()
|
||||
}
|
||||
for _, m := range ms.SubModules {
|
||||
ToEntry(m).FixChoice()
|
||||
}
|
||||
|
||||
// Go through any modules that have remaining augments and collect
|
||||
// the errors.
|
||||
for _, m := range mods {
|
||||
ToEntry(m).Augment(true)
|
||||
errs = append(errs, ToEntry(m).GetErrors()...)
|
||||
}
|
||||
|
||||
// The deviation statement is only valid under a module or submodule,
|
||||
// which allows us to avoid having to process it within ToEntry, and
|
||||
// rather we can just walk all modules and submodules *after* entries
|
||||
// are resolved. This means we do not need to concern ourselves that
|
||||
// an entry does not exist.
|
||||
dvP := map[string]bool{} // cache the modules we've handled since we have both modname and modname@revision-date
|
||||
for _, devmods := range []map[string]*Module{ms.Modules, ms.SubModules} {
|
||||
for _, m := range devmods {
|
||||
e := ToEntry(m)
|
||||
if !dvP[e.Name] {
|
||||
errs = append(errs, e.ApplyDeviate(ms.ParseOptions.DeviateOptions)...)
|
||||
dvP[e.Name] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return errorSort(errs)
|
||||
}
|
||||
|
||||
// include resolves all the include and import statements for m. It returns
|
||||
// an error if m, or recursively, any of the modules it includes or imports,
|
||||
// reference a module that cannot be found.
|
||||
func (ms *Modules) include(m *Module) error {
|
||||
if ms.includes[m] {
|
||||
return nil
|
||||
}
|
||||
ms.includes[m] = true
|
||||
|
||||
// First process any includes in this module.
|
||||
for _, i := range m.Include {
|
||||
im := ms.FindModule(i)
|
||||
if im == nil {
|
||||
return fmt.Errorf("no such submodule: %s", i.Name)
|
||||
}
|
||||
// Process the include statements in our included module.
|
||||
if err := ms.include(im); err != nil {
|
||||
return err
|
||||
}
|
||||
i.Module = im
|
||||
}
|
||||
|
||||
// Next process any imports in this module. Imports are used
|
||||
// when searching.
|
||||
for _, i := range m.Import {
|
||||
im := ms.FindModule(i)
|
||||
if im == nil {
|
||||
return fmt.Errorf("no such module: %s", i.Name)
|
||||
}
|
||||
// Process the include statements in our included module.
|
||||
if err := ms.include(im); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
i.Module = im
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ms *Modules) getEntryCache(n Node) *Entry {
|
||||
ms.entryCacheMu.RLock()
|
||||
defer ms.entryCacheMu.RUnlock()
|
||||
return ms.entryCache[n]
|
||||
}
|
||||
|
||||
func (ms *Modules) setEntryCache(n Node, e *Entry) {
|
||||
ms.entryCacheMu.Lock()
|
||||
defer ms.entryCacheMu.Unlock()
|
||||
ms.entryCache[n] = e
|
||||
}
|
||||
|
||||
// ClearEntryCache clears the entryCache containing previously converted nodes
|
||||
// used by the ToEntry function.
|
||||
func (ms *Modules) ClearEntryCache() {
|
||||
ms.entryCacheMu.Lock()
|
||||
defer ms.entryCacheMu.Unlock()
|
||||
ms.entryCache = map[Node]*Entry{}
|
||||
}
|
||||
@@ -0,0 +1,414 @@
|
||||
// Copyright 2016 Google Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package yang
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/openconfig/gnmi/errdiff"
|
||||
)
|
||||
|
||||
var testdataFindModulesText = map[string]string{
|
||||
"foo": `module foo { prefix "foo"; namespace "urn:foo"; }`,
|
||||
"bar": `module bar { prefix "bar"; namespace "urn:bar"; }`,
|
||||
"baz": `module baz { prefix "baz"; namespace "urn:baz"; }`,
|
||||
"dup-pre-one": `module dup-pre-one { prefix duplicate; namespace urn:duplicate:one; }`,
|
||||
"dup-pre-two": `module dup-pre-two { prefix duplicate; namespace urn:duplicate:two; }`,
|
||||
"dup-ns-one": `module dup-ns-one { prefix ns-one; namespace urn:duplicate; }`,
|
||||
"dup-ns-two": `module dup-ns-two { prefix ns-two; namespace urn:duplicate; }`,
|
||||
}
|
||||
|
||||
func TestDupModule(t *testing.T) {
|
||||
tests := []struct {
|
||||
desc string
|
||||
inModules map[string]string
|
||||
wantErr bool
|
||||
}{{
|
||||
desc: "two modules with the same name",
|
||||
inModules: map[string]string{
|
||||
"foo": `module foo { prefix "foo"; namespace "urn:foo"; }`,
|
||||
"bar": `module foo { prefix "foo"; namespace "urn:foo"; }`,
|
||||
},
|
||||
wantErr: true,
|
||||
}}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.desc, func(t *testing.T) {
|
||||
ms := NewModules()
|
||||
var err error
|
||||
for name, modtext := range tt.inModules {
|
||||
if err = ms.Parse(modtext, name+".yang"); err != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
if gotErr := err != nil; gotErr != tt.wantErr {
|
||||
t.Fatalf("wantErr: %v, got error: %v", tt.wantErr, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func testModulesForTestdataModulesText(t *testing.T) *Modules {
|
||||
ms := NewModules()
|
||||
for name, modtext := range testdataFindModulesText {
|
||||
if err := ms.Parse(modtext, name+".yang"); err != nil {
|
||||
t.Fatalf("error importing testdataFindModulesText[%q]: %v", name, err)
|
||||
}
|
||||
}
|
||||
if errs := ms.Process(); errs != nil {
|
||||
for _, err := range errs {
|
||||
t.Errorf("error: %v", err)
|
||||
}
|
||||
t.Fatalf("fatal error(s) calling Process()")
|
||||
}
|
||||
return ms
|
||||
}
|
||||
|
||||
func testModulesFindByCommonHandler(t *testing.T, i int, got, want *Module, wantError string, err error) {
|
||||
if err != nil {
|
||||
if wantError != "" {
|
||||
if !strings.Contains(err.Error(), wantError) {
|
||||
t.Errorf("[%d] want error containing %q, got %q",
|
||||
i, wantError, err.Error())
|
||||
}
|
||||
} else {
|
||||
t.Errorf("[%d] unexpected error: %v", i, err)
|
||||
}
|
||||
} else if wantError != "" {
|
||||
t.Errorf("[%d] want error containing %q, got nil", i, wantError)
|
||||
} else if want != got {
|
||||
t.Errorf("[%d] want module %#v, got %#v", i, want, got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestModulesFindByNamespace(t *testing.T) {
|
||||
ms := testModulesForTestdataModulesText(t)
|
||||
|
||||
for i, tc := range []struct {
|
||||
namespace string
|
||||
want *Module
|
||||
wantError string
|
||||
}{
|
||||
{
|
||||
namespace: "does-not-exist",
|
||||
wantError: `"does-not-exist": no such namespace`,
|
||||
},
|
||||
{
|
||||
namespace: "urn:foo",
|
||||
want: ms.Modules["foo"],
|
||||
},
|
||||
{
|
||||
namespace: "urn:bar",
|
||||
want: ms.Modules["bar"],
|
||||
},
|
||||
{
|
||||
namespace: "urn:baz",
|
||||
want: ms.Modules["baz"],
|
||||
},
|
||||
{
|
||||
namespace: "urn:duplicate",
|
||||
wantError: "namespace urn:duplicate matches two or more modules (dup-ns-",
|
||||
},
|
||||
} {
|
||||
got, err := ms.FindModuleByNamespace(tc.namespace)
|
||||
testModulesFindByCommonHandler(t, i, got, tc.want, tc.wantError, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestModuleLinkage(t *testing.T) {
|
||||
tests := []struct {
|
||||
desc string
|
||||
inMods map[string]string
|
||||
wantErrSubstr string
|
||||
}{{
|
||||
desc: "invalid import",
|
||||
inMods: map[string]string{
|
||||
"dev": `
|
||||
module dev {
|
||||
prefix d;
|
||||
namespace "urn:d";
|
||||
import sys { prefix sys; }
|
||||
|
||||
revision 01-01-01 { description "the start of time"; }
|
||||
|
||||
deviation /sys:sys/sys:hostname {
|
||||
deviate not-supported;
|
||||
}
|
||||
}`,
|
||||
},
|
||||
wantErrSubstr: "no such module",
|
||||
}, {
|
||||
desc: "valid include",
|
||||
inMods: map[string]string{
|
||||
"dev": `
|
||||
module dev {
|
||||
prefix d;
|
||||
namespace "urn:d";
|
||||
include sys;
|
||||
|
||||
revision 01-01-01 { description "the start of time"; }
|
||||
}`,
|
||||
"sys": `
|
||||
submodule sys {
|
||||
belongs-to dev {
|
||||
prefix "d";
|
||||
}
|
||||
|
||||
revision 01-01-01 { description "the start of time"; }
|
||||
|
||||
container sys { leaf hostname { type string; } }
|
||||
}`,
|
||||
},
|
||||
}, {
|
||||
desc: "invalid include",
|
||||
inMods: map[string]string{
|
||||
"dev": `
|
||||
module dev {
|
||||
prefix d;
|
||||
namespace "urn:d";
|
||||
include sys;
|
||||
|
||||
revision 01-01-01 { description "the start of time"; }
|
||||
}`,
|
||||
"sysdb": `
|
||||
submodule sysdb {
|
||||
belongs-to dev {
|
||||
prefix "d";
|
||||
}
|
||||
|
||||
revision 01-01-01 { description "the start of time"; }
|
||||
|
||||
container sys { leaf hostname { type string; } }
|
||||
}`,
|
||||
},
|
||||
wantErrSubstr: "no such submodule",
|
||||
}, {
|
||||
desc: "valid include in submodule",
|
||||
inMods: map[string]string{
|
||||
"dev": `
|
||||
module dev {
|
||||
prefix d;
|
||||
namespace "urn:d";
|
||||
include sys;
|
||||
|
||||
revision 01-01-01 { description "the start of time"; }
|
||||
}`,
|
||||
"sys": `
|
||||
submodule sys {
|
||||
belongs-to dev {
|
||||
prefix "d";
|
||||
}
|
||||
include sysdb;
|
||||
|
||||
revision 01-01-01 { description "the start of time"; }
|
||||
|
||||
container sys { leaf hostname { type string; } }
|
||||
}`,
|
||||
"sysdb": `
|
||||
submodule sysdb {
|
||||
belongs-to dev {
|
||||
prefix "d";
|
||||
}
|
||||
|
||||
revision 01-01-01 { description "the start of time"; }
|
||||
|
||||
container sysdb { leaf hostname { type string; } }
|
||||
}`,
|
||||
},
|
||||
}, {
|
||||
desc: "invalid include in submodule",
|
||||
inMods: map[string]string{
|
||||
"dev": `
|
||||
module dev {
|
||||
prefix d;
|
||||
namespace "urn:d";
|
||||
include sys;
|
||||
|
||||
revision 01-01-01 { description "the start of time"; }
|
||||
}`,
|
||||
"sys": `
|
||||
submodule sys {
|
||||
belongs-to dev {
|
||||
prefix "d";
|
||||
}
|
||||
include sysdb;
|
||||
|
||||
revision 01-01-01 { description "the start of time"; }
|
||||
|
||||
container sys { leaf hostname { type string; } }
|
||||
}`,
|
||||
"syyysdb": `
|
||||
submodule syyysdb {
|
||||
belongs-to dev {
|
||||
prefix "d";
|
||||
}
|
||||
|
||||
revision 01-01-01 { description "the start of time"; }
|
||||
|
||||
container sysdb { leaf hostname { type string; } }
|
||||
}`,
|
||||
},
|
||||
wantErrSubstr: "no such submodule",
|
||||
}, {
|
||||
desc: "valid import in submodule",
|
||||
inMods: map[string]string{
|
||||
"dev": `
|
||||
module dev {
|
||||
prefix d;
|
||||
namespace "urn:d";
|
||||
include sys;
|
||||
|
||||
revision 01-01-01 { description "the start of time"; }
|
||||
}`,
|
||||
"sys": `
|
||||
submodule sys {
|
||||
belongs-to dev {
|
||||
prefix "d";
|
||||
}
|
||||
import sysdb {
|
||||
prefix "sd";
|
||||
}
|
||||
|
||||
revision 01-01-01 { description "the start of time"; }
|
||||
|
||||
container sys { leaf hostname { type string; } }
|
||||
}`,
|
||||
"sysdb": `
|
||||
module sysdb {
|
||||
prefix sd;
|
||||
namespace "urn:sd";
|
||||
|
||||
revision 01-01-01 { description "the start of time"; }
|
||||
|
||||
container sysdb { leaf hostname { type string; } }
|
||||
}`,
|
||||
},
|
||||
}, {
|
||||
desc: "invalid import in submodule",
|
||||
inMods: map[string]string{
|
||||
"dev": `
|
||||
module dev {
|
||||
prefix d;
|
||||
namespace "urn:d";
|
||||
include sys;
|
||||
|
||||
revision 01-01-01 { description "the start of time"; }
|
||||
}`,
|
||||
"sys": `
|
||||
submodule sys {
|
||||
belongs-to dev {
|
||||
prefix "d";
|
||||
}
|
||||
import sysdb {
|
||||
prefix "sd";
|
||||
}
|
||||
|
||||
revision 01-01-01 { description "the start of time"; }
|
||||
|
||||
container sys { leaf hostname { type string; } }
|
||||
}`,
|
||||
"syyysdb": `
|
||||
module syyysdb {
|
||||
prefix sd;
|
||||
namespace "urn:sd";
|
||||
|
||||
revision 01-01-01 { description "the start of time"; }
|
||||
|
||||
container sysdb { leaf hostname { type string; } }
|
||||
}`,
|
||||
},
|
||||
wantErrSubstr: "no such module",
|
||||
}}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.desc, func(t *testing.T) {
|
||||
ms := NewModules()
|
||||
|
||||
for n, m := range tt.inMods {
|
||||
if err := ms.Parse(m, n); err != nil {
|
||||
t.Fatalf("cannot parse module %s, err: %v", n, err)
|
||||
}
|
||||
}
|
||||
|
||||
errs := ms.Process()
|
||||
var err error
|
||||
switch len(errs) {
|
||||
case 1:
|
||||
err = errs[0]
|
||||
fallthrough
|
||||
case 0:
|
||||
if diff := errdiff.Substring(err, tt.wantErrSubstr); diff != "" {
|
||||
t.Fatalf("%s", diff)
|
||||
}
|
||||
default:
|
||||
t.Fatalf("got multiple errors: %v", errs)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestModulesTotalProcess(t *testing.T) {
|
||||
tests := []struct {
|
||||
desc string
|
||||
inMods map[string]string
|
||||
wantErr bool
|
||||
}{{
|
||||
desc: "import with deviation",
|
||||
inMods: map[string]string{
|
||||
"dev": `
|
||||
module dev {
|
||||
prefix d;
|
||||
namespace "urn:d";
|
||||
import sys { prefix sys; }
|
||||
|
||||
revision 01-01-01 { description "the start of time"; }
|
||||
|
||||
deviation /sys:sys/sys:hostname {
|
||||
deviate not-supported;
|
||||
}
|
||||
}`,
|
||||
"sys": `
|
||||
module sys {
|
||||
prefix s;
|
||||
namespace "urn:s";
|
||||
|
||||
revision 01-01-01 { description "the start of time"; }
|
||||
|
||||
container sys { leaf hostname { type string; } }
|
||||
}`,
|
||||
},
|
||||
}}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.desc, func(t *testing.T) {
|
||||
ms := NewModules()
|
||||
|
||||
for n, m := range tt.inMods {
|
||||
if err := ms.Parse(m, n); err != nil {
|
||||
t.Fatalf("cannot parse module %s, err: %v", n, err)
|
||||
}
|
||||
}
|
||||
|
||||
errs := ms.Process()
|
||||
switch {
|
||||
case len(errs) == 0 && tt.wantErr:
|
||||
t.Fatalf("did not get expected errors, got: %v, wantErr: %v", errs, tt.wantErr)
|
||||
case len(errs) != 0 && !tt.wantErr:
|
||||
t.Fatalf("got unexpected errors, got: %v, wantErr: %v", errs, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,388 @@
|
||||
// Copyright 2015 Google Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package yang
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"github.com/openconfig/goyang/pkg/indent"
|
||||
)
|
||||
|
||||
// A Node contains a yang statement and all attributes and sub-statements.
|
||||
// Only pointers to structures should implement Node.
|
||||
type Node interface {
|
||||
// Kind returns the kind of yang statement (the keyword).
|
||||
Kind() string
|
||||
// NName returns the node's name (the argument)
|
||||
NName() string
|
||||
// Statement returns the original Statement of this Node.
|
||||
Statement() *Statement
|
||||
// ParentNode returns the parent of this Node, or nil if the
|
||||
// Node has no parent.
|
||||
ParentNode() Node
|
||||
// Exts returns the list of extension statements found.
|
||||
Exts() []*Statement
|
||||
}
|
||||
|
||||
// A Typedefer is a Node that defines typedefs.
|
||||
type Typedefer interface {
|
||||
Node
|
||||
Typedefs() []*Typedef
|
||||
}
|
||||
|
||||
// An ErrorNode is a node that only contains an error.
|
||||
type ErrorNode struct {
|
||||
Parent Node `yang:"Parent,nomerge"`
|
||||
|
||||
Error error
|
||||
}
|
||||
|
||||
func (ErrorNode) Kind() string { return "error" }
|
||||
func (s *ErrorNode) ParentNode() Node { return s.Parent }
|
||||
func (s *ErrorNode) NName() string { return "error" }
|
||||
func (s *ErrorNode) Statement() *Statement { return &Statement{} }
|
||||
func (s *ErrorNode) Exts() []*Statement { return nil }
|
||||
|
||||
// isRPCNode is a terrible hack to return back that a path points into
|
||||
// an RPC and we should ignore it.
|
||||
var isRPCNode = &ErrorNode{Error: errors.New("rpc is unsupported")}
|
||||
|
||||
// Source returns the location of the source where n was defined.
|
||||
func Source(n Node) string {
|
||||
if n != nil && n.Statement() != nil {
|
||||
return n.Statement().Location()
|
||||
}
|
||||
return "unknown"
|
||||
}
|
||||
|
||||
// getPrefix returns the prefix and base name of s. If s has no prefix
|
||||
// then the returned prefix is "".
|
||||
func getPrefix(s string) (string, string) {
|
||||
f := strings.SplitN(s, ":", 2)
|
||||
if len(f) == 1 {
|
||||
return "", s
|
||||
}
|
||||
return f[0], f[1]
|
||||
}
|
||||
|
||||
// Prefix notes for types:
|
||||
//
|
||||
// If there is prefix, look in nodes ancestors.
|
||||
//
|
||||
// If prefix matches the module's prefix statement, look in nodes ancestors.
|
||||
//
|
||||
// If prefix matches the submodule's belongs-t statement, look in nodes
|
||||
// ancestors.
|
||||
//
|
||||
// Finally, look in the module imported with prefix.
|
||||
|
||||
// FindModuleByPrefix finds the module or submodule with the provided prefix
|
||||
// relative to where n was defined. If the prefix cannot be resolved then nil
|
||||
// is returned.
|
||||
func FindModuleByPrefix(n Node, prefix string) *Module {
|
||||
if n == nil {
|
||||
return nil
|
||||
}
|
||||
mod := RootNode(n)
|
||||
|
||||
if prefix == "" || prefix == mod.GetPrefix() {
|
||||
return mod
|
||||
}
|
||||
|
||||
for _, i := range mod.Import {
|
||||
if prefix == i.Prefix.Name {
|
||||
return mod.Modules.FindModule(i)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// MatchingExtensions returns the subset of the given node's extensions
|
||||
// that match the given module and identifier.
|
||||
func MatchingExtensions(n Node, module, identifier string) ([]*Statement, error) {
|
||||
return matchingExtensions(n, n.Exts(), module, identifier)
|
||||
}
|
||||
|
||||
// MatchingEntryExtensions returns the subset of the given entry's extensions
|
||||
// that match the given module and identifier.
|
||||
func MatchingEntryExtensions(e *Entry, module, identifier string) ([]*Statement, error) {
|
||||
return matchingExtensions(e.Node, e.Exts, module, identifier)
|
||||
}
|
||||
|
||||
// matchingEntryExtensions returns the subset of the given node's extensions
|
||||
// that match the given module and identifier.
|
||||
func matchingExtensions(n Node, exts []*Statement, module, identifier string) ([]*Statement, error) {
|
||||
var matchingExtensions []*Statement
|
||||
for _, ext := range exts {
|
||||
names := strings.SplitN(ext.Keyword, ":", 2)
|
||||
mod := FindModuleByPrefix(n, names[0])
|
||||
if mod == nil {
|
||||
return nil, fmt.Errorf("matchingExtensions: module prefix %q not found", names[0])
|
||||
}
|
||||
if len(names) == 2 && names[1] == identifier && mod.Name == module {
|
||||
matchingExtensions = append(matchingExtensions, ext)
|
||||
}
|
||||
}
|
||||
return matchingExtensions, nil
|
||||
}
|
||||
|
||||
// RootNode returns the submodule or module that n was defined in.
|
||||
func RootNode(n Node) *Module {
|
||||
for ; n.ParentNode() != nil; n = n.ParentNode() {
|
||||
}
|
||||
if mod, ok := n.(*Module); ok {
|
||||
return mod
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// module returns the Module to which n belongs. If n resides in a submodule,
|
||||
// the belonging module will be returned.
|
||||
// If n is nil or a module could not be find, nil is returned.
|
||||
func module(n Node) *Module {
|
||||
m := RootNode(n)
|
||||
if m.Kind() == "submodule" {
|
||||
m = m.Modules.Modules[m.BelongsTo.Name]
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
// NodePath returns the full path of the node from the module name.
|
||||
func NodePath(n Node) string {
|
||||
var path string
|
||||
for n != nil {
|
||||
path = "/" + n.NName() + path
|
||||
n = n.ParentNode()
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
// FindNode finds the node referenced by path relative to n. If path does not
|
||||
// reference a node then nil is returned (i.e. path not found). The path looks
|
||||
// similar to an XPath but currently has no wildcarding. For example:
|
||||
// "/if:interfaces/if:interface" and "../config".
|
||||
func FindNode(n Node, path string) (Node, error) {
|
||||
if path == "" {
|
||||
return n, nil
|
||||
}
|
||||
// / is not a valid path, it needs a module name
|
||||
if path == "/" {
|
||||
return nil, fmt.Errorf("invalid path %q", path)
|
||||
}
|
||||
// Paths do not end in /'s
|
||||
if path[len(path)-1] == '/' {
|
||||
return nil, fmt.Errorf("invalid path %q", path)
|
||||
}
|
||||
|
||||
parts := strings.Split(path, "/")
|
||||
|
||||
// An absolute path has a leading component of "".
|
||||
// We need to discover which module they are part of
|
||||
// based on our imports.
|
||||
if parts[0] == "" {
|
||||
parts = parts[1:]
|
||||
|
||||
// TODO(borman): merge this with FindModuleByPrefix?
|
||||
// The base is always a module
|
||||
mod := RootNode(n)
|
||||
n = mod
|
||||
prefix, _ := getPrefix(parts[0])
|
||||
if mod.Kind() == "submodule" {
|
||||
m := mod.Modules.Modules[mod.BelongsTo.Name]
|
||||
if m == nil {
|
||||
return nil, fmt.Errorf("%s: unknown module %s", m.Name, mod.BelongsTo.Name)
|
||||
}
|
||||
if prefix == "" || prefix == mod.BelongsTo.Prefix.Name {
|
||||
goto processing
|
||||
}
|
||||
mod = m
|
||||
}
|
||||
|
||||
if prefix == "" || prefix == mod.Prefix.Name {
|
||||
goto processing
|
||||
}
|
||||
|
||||
for _, i := range mod.Import {
|
||||
if prefix == i.Prefix.Name {
|
||||
n = i.Module
|
||||
goto processing
|
||||
}
|
||||
}
|
||||
// We didn't find a matching prefix.
|
||||
return nil, fmt.Errorf("unknown prefix: %q", prefix)
|
||||
processing:
|
||||
// At this point, n should be pointing to the Module node
|
||||
// of module we are rooted in
|
||||
}
|
||||
|
||||
for _, part := range parts {
|
||||
// If we encounter an RPC node in our search then we
|
||||
// return the magic isRPCNode Node which just contains
|
||||
// an error that it is an RPC node. isRPCNode is a singleton
|
||||
// and can be checked against.
|
||||
if n.Kind() == "rpc" {
|
||||
return isRPCNode, nil
|
||||
}
|
||||
if part == ".." {
|
||||
Loop:
|
||||
for {
|
||||
n = n.ParentNode()
|
||||
if n == nil {
|
||||
return nil, fmt.Errorf(".. with no parent")
|
||||
}
|
||||
// choice, leaf, and case nodes
|
||||
// are "invisible" when doing ".."
|
||||
// up the tree.
|
||||
switch n.Kind() {
|
||||
case "choice", "leaf", "case":
|
||||
default:
|
||||
break Loop
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
// For now just strip off any prefix
|
||||
// TODO(borman): fix this
|
||||
_, spart := getPrefix(part)
|
||||
n = ChildNode(n, spart)
|
||||
if n == nil {
|
||||
return nil, fmt.Errorf("%s: no such element", part)
|
||||
}
|
||||
}
|
||||
return n, nil
|
||||
}
|
||||
|
||||
// ChildNode finds n's child node named name. It returns nil if the node
|
||||
// could not be found. ChildNode looks at every direct Node pointer in
|
||||
// n as well as every node in all slices of Node pointers. Names must
|
||||
// be non-ambiguous, otherwise ChildNode has a non-deterministic result.
|
||||
func ChildNode(n Node, name string) Node {
|
||||
v := reflect.ValueOf(n).Elem()
|
||||
t := v.Type()
|
||||
nf := t.NumField()
|
||||
|
||||
Loop:
|
||||
for i := 0; i < nf; i++ {
|
||||
ft := t.Field(i)
|
||||
yang := ft.Tag.Get("yang")
|
||||
if yang == "" {
|
||||
continue
|
||||
}
|
||||
parts := strings.Split(yang, ",")
|
||||
for _, p := range parts[1:] {
|
||||
if p == "nomerge" {
|
||||
continue Loop
|
||||
}
|
||||
}
|
||||
|
||||
f := v.Field(i)
|
||||
if !f.IsValid() || f.IsNil() {
|
||||
continue
|
||||
}
|
||||
|
||||
check := func(n Node) Node {
|
||||
if n.NName() == name {
|
||||
return n
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if parts[0] == "uses" {
|
||||
check = func(n Node) Node {
|
||||
uname := n.NName()
|
||||
// unrooted uses are rooted at root
|
||||
if !strings.HasPrefix(uname, "/") {
|
||||
uname = "/" + uname
|
||||
}
|
||||
if n, _ = FindNode(n, uname); n != nil {
|
||||
return ChildNode(n, name)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
switch ft.Type.Kind() {
|
||||
case reflect.Ptr:
|
||||
if n = check(f.Interface().(Node)); n != nil {
|
||||
return n
|
||||
}
|
||||
case reflect.Slice:
|
||||
sl := f.Len()
|
||||
for i := 0; i < sl; i++ {
|
||||
n = f.Index(i).Interface().(Node)
|
||||
if n = check(n); n != nil {
|
||||
return n
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// PrintNode prints node n to w, recursively.
|
||||
// TODO(borman): display more information
|
||||
func PrintNode(w io.Writer, n Node) {
|
||||
v := reflect.ValueOf(n).Elem()
|
||||
t := v.Type()
|
||||
nf := t.NumField()
|
||||
fmt.Fprintf(w, "%s [%s]\n", n.NName(), n.Kind())
|
||||
Loop:
|
||||
for i := 0; i < nf; i++ {
|
||||
ft := t.Field(i)
|
||||
yang := ft.Tag.Get("yang")
|
||||
if yang == "" {
|
||||
continue
|
||||
}
|
||||
parts := strings.Split(yang, ",")
|
||||
for _, p := range parts[1:] {
|
||||
if p == "nomerge" {
|
||||
continue Loop
|
||||
}
|
||||
}
|
||||
|
||||
// Skip uppercase elements.
|
||||
if parts[0][0] >= 'A' && parts[0][0] <= 'Z' {
|
||||
continue
|
||||
}
|
||||
|
||||
f := v.Field(i)
|
||||
if !f.IsValid() || f.IsNil() {
|
||||
continue
|
||||
}
|
||||
|
||||
switch ft.Type.Kind() {
|
||||
case reflect.Ptr:
|
||||
n = f.Interface().(Node)
|
||||
if v, ok := n.(*Value); ok {
|
||||
fmt.Fprintf(w, "%s = %s\n", ft.Name, v.Name)
|
||||
} else {
|
||||
PrintNode(indent.NewWriter(w, " "), n)
|
||||
}
|
||||
case reflect.Slice:
|
||||
sl := f.Len()
|
||||
for i := 0; i < sl; i++ {
|
||||
n = f.Index(i).Interface().(Node)
|
||||
if v, ok := n.(*Value); ok {
|
||||
fmt.Fprintf(w, "%s[%d] = %s\n", ft.Name, i, v.Name)
|
||||
} else {
|
||||
PrintNode(indent.NewWriter(w, " "), n)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,622 @@
|
||||
// Copyright 2019 Google Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package yang
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/openconfig/gnmi/errdiff"
|
||||
)
|
||||
|
||||
func TestNodePath(t *testing.T) {
|
||||
tests := []struct {
|
||||
desc string
|
||||
in Node
|
||||
want string
|
||||
}{{
|
||||
desc: "basic",
|
||||
in: &Leaf{
|
||||
Name: "bar",
|
||||
Parent: &Container{
|
||||
Name: "c",
|
||||
Parent: &List{
|
||||
Name: "b",
|
||||
Parent: &Module{
|
||||
Name: "foo",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
want: "/foo/b/c/bar",
|
||||
}, {
|
||||
desc: "nil input node",
|
||||
in: nil,
|
||||
want: "",
|
||||
}, {
|
||||
desc: "single node",
|
||||
in: &Module{
|
||||
Name: "foo",
|
||||
},
|
||||
want: "/foo",
|
||||
}}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.desc, func(t *testing.T) {
|
||||
if diff := cmp.Diff(NodePath(tt.in), tt.want); diff != "" {
|
||||
t.Errorf("(-got, +want):\n%s", diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestNode provides a framework for processing tests that can check particular
|
||||
// nodes being added to the grammar. It can be used to ensure that particular
|
||||
// statement combinations are supported, especially where they are opaque to
|
||||
// the YANG library.
|
||||
func TestNode(t *testing.T) {
|
||||
tests := []struct {
|
||||
desc string
|
||||
inFn func(*Modules) (Node, error)
|
||||
inModules map[string]string
|
||||
wantNode func(Node) error
|
||||
wantErrSubstr string
|
||||
}{{
|
||||
desc: "import reference statement",
|
||||
inFn: func(ms *Modules) (Node, error) {
|
||||
|
||||
const module = "test"
|
||||
m, ok := ms.Modules[module]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("can't find module %q", module)
|
||||
}
|
||||
|
||||
if len(m.Import) == 0 {
|
||||
return nil, fmt.Errorf("node %v is missing imports", m)
|
||||
}
|
||||
|
||||
return m.Import[0], nil
|
||||
},
|
||||
inModules: map[string]string{
|
||||
"test": `
|
||||
module test {
|
||||
prefix "t";
|
||||
namespace "urn:t";
|
||||
|
||||
import foo {
|
||||
prefix "f";
|
||||
reference "bar";
|
||||
}
|
||||
}
|
||||
`,
|
||||
"foo": `
|
||||
module foo {
|
||||
prefix "f";
|
||||
namespace "urn:f";
|
||||
}
|
||||
`,
|
||||
},
|
||||
wantNode: func(n Node) error {
|
||||
is, ok := n.(*Import)
|
||||
if !ok {
|
||||
return fmt.Errorf("got node: %v, want type: import", n)
|
||||
}
|
||||
|
||||
switch {
|
||||
case is.Reference == nil:
|
||||
return errors.New("did not get expected reference, got: nil, want: *yang.Statement")
|
||||
case is.Reference.Statement().Argument != "bar":
|
||||
return fmt.Errorf("did not get expected reference, got: %v, want: 'bar'", is.Reference.Statement())
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}, {
|
||||
desc: "get submodule from prefix in submodule",
|
||||
inFn: func(ms *Modules) (Node, error) {
|
||||
|
||||
m, ok := ms.SubModules["foo"]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("can't find submodule in %v", ms)
|
||||
}
|
||||
|
||||
if m.BelongsTo == nil {
|
||||
return nil, fmt.Errorf("node %v is missing belongs-to", m)
|
||||
}
|
||||
|
||||
return m.BelongsTo, nil
|
||||
},
|
||||
inModules: map[string]string{
|
||||
"test": `
|
||||
module test {
|
||||
prefix "t";
|
||||
namespace "urn:t";
|
||||
|
||||
include foo {
|
||||
revision-date 2008-01-01;
|
||||
}
|
||||
}
|
||||
`,
|
||||
"foo": `
|
||||
submodule foo {
|
||||
belongs-to test {
|
||||
prefix "t";
|
||||
}
|
||||
}
|
||||
`,
|
||||
},
|
||||
wantNode: func(n Node) error {
|
||||
is, ok := n.(*BelongsTo)
|
||||
if !ok {
|
||||
return fmt.Errorf("got node: %v, want type: belongs-to", n)
|
||||
}
|
||||
|
||||
switch {
|
||||
case is.Prefix == nil:
|
||||
return errors.New("did not get expected reference, got: nil, want: *yang.Statement")
|
||||
case is.Prefix.Statement().Argument != "t":
|
||||
return fmt.Errorf("did not get expected reference, got: %v, want: 't'", is.Prefix.Statement())
|
||||
}
|
||||
|
||||
m := FindModuleByPrefix(is, is.Prefix.Statement().Argument)
|
||||
if m == nil {
|
||||
return fmt.Errorf("can't find module from submodule's belongs-to prefix value")
|
||||
}
|
||||
if want := "foo"; m.Name != want {
|
||||
return fmt.Errorf("module from submodule's belongs-to prefix value doesn't match, got %q, want %q", m.Name, want)
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}, {
|
||||
desc: "import statement from submodule",
|
||||
inFn: func(ms *Modules) (Node, error) {
|
||||
|
||||
m, ok := ms.SubModules["foo"]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("can't find submodule in %v", ms)
|
||||
}
|
||||
|
||||
if len(m.Import) == 0 {
|
||||
return nil, fmt.Errorf("node %v is missing import statement", m)
|
||||
}
|
||||
|
||||
return m.Import[0], nil
|
||||
},
|
||||
inModules: map[string]string{
|
||||
"test": `
|
||||
module test {
|
||||
prefix "t";
|
||||
namespace "urn:t";
|
||||
|
||||
include foo {
|
||||
revision-date 2008-01-01;
|
||||
}
|
||||
|
||||
typedef t {
|
||||
type string;
|
||||
}
|
||||
}
|
||||
`,
|
||||
"foo": `
|
||||
submodule foo {
|
||||
belongs-to test {
|
||||
prefix "t";
|
||||
}
|
||||
|
||||
import test2 {
|
||||
prefix "t2";
|
||||
description "test2 module";
|
||||
}
|
||||
}
|
||||
`,
|
||||
"test2": `
|
||||
module test2 {
|
||||
prefix "t2";
|
||||
namespace "urn:t2";
|
||||
}
|
||||
`,
|
||||
},
|
||||
wantNode: func(n Node) error {
|
||||
is, ok := n.(*Import)
|
||||
if !ok {
|
||||
return fmt.Errorf("got node: %v, want type: belongs-to", n)
|
||||
}
|
||||
|
||||
switch {
|
||||
case is.Prefix == nil:
|
||||
return errors.New("did not get expected reference, got: nil, want: *yang.Statement")
|
||||
case is.Prefix.Statement().Argument != "t2":
|
||||
return fmt.Errorf("did not get expected reference, got: %v, want: 't'", is.Prefix.Statement())
|
||||
}
|
||||
|
||||
m := FindModuleByPrefix(is, is.Prefix.Statement().Argument)
|
||||
if m == nil {
|
||||
return fmt.Errorf("can't find module from submodule's import prefix value")
|
||||
}
|
||||
if want := "test2"; m.Name != want {
|
||||
return fmt.Errorf("module from submodule's import prefix value doesn't match, got %q, want %q", m.Name, want)
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}, {
|
||||
desc: "import description statement",
|
||||
inFn: func(ms *Modules) (Node, error) {
|
||||
|
||||
const module = "test"
|
||||
m, ok := ms.Modules[module]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("can't find module %q", module)
|
||||
}
|
||||
|
||||
if len(m.Import) == 0 {
|
||||
return nil, fmt.Errorf("node %v is missing imports", m)
|
||||
}
|
||||
|
||||
return m.Import[0], nil
|
||||
},
|
||||
inModules: map[string]string{
|
||||
"test": `
|
||||
module test {
|
||||
prefix "t";
|
||||
namespace "urn:t";
|
||||
|
||||
import foo {
|
||||
prefix "f";
|
||||
description "foo module";
|
||||
}
|
||||
}
|
||||
`,
|
||||
"foo": `
|
||||
module foo {
|
||||
prefix "f";
|
||||
namespace "urn:f";
|
||||
}
|
||||
`,
|
||||
},
|
||||
wantNode: func(n Node) error {
|
||||
is, ok := n.(*Import)
|
||||
if !ok {
|
||||
return fmt.Errorf("got node: %v, want type: import", n)
|
||||
}
|
||||
|
||||
switch {
|
||||
case is.Description == nil:
|
||||
return errors.New("did not get expected reference, got: nil, want: *yang.Statement")
|
||||
case is.Description.Statement().Argument != "foo module":
|
||||
return fmt.Errorf("did not get expected reference, got: '%v', want: 'foo module'", is.Description.Statement().Argument)
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}, {
|
||||
desc: "Test matchingExtensions",
|
||||
inFn: func(ms *Modules) (Node, error) {
|
||||
|
||||
module := "test"
|
||||
m, ok := ms.Modules[module]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("can't find module %q", module)
|
||||
}
|
||||
|
||||
if len(m.Leaf) == 0 {
|
||||
return nil, fmt.Errorf("node %v is missing imports", m)
|
||||
}
|
||||
|
||||
module = "foo"
|
||||
if _, ok := ms.Modules[module]; !ok {
|
||||
return nil, fmt.Errorf("can't find module %q", module)
|
||||
}
|
||||
|
||||
return m.Leaf[0].Type, nil
|
||||
},
|
||||
inModules: map[string]string{
|
||||
"test": `
|
||||
module test {
|
||||
prefix "t";
|
||||
namespace "urn:t";
|
||||
|
||||
import foo {
|
||||
prefix "f";
|
||||
description "foo module";
|
||||
}
|
||||
|
||||
import foo2 {
|
||||
prefix "f2";
|
||||
description "foo2 module";
|
||||
}
|
||||
|
||||
leaf test-leaf {
|
||||
type string {
|
||||
pattern 'alpha';
|
||||
// Test different modules and different ext names.
|
||||
f:bar 'boo';
|
||||
f2:bar 'boo2';
|
||||
|
||||
f:bar 'coo';
|
||||
f2:bar 'coo2';
|
||||
|
||||
f:far 'doo';
|
||||
f2:far 'doo2';
|
||||
|
||||
f:bar 'foo';
|
||||
f2:bar 'foo2';
|
||||
|
||||
f:far 'goo';
|
||||
f2:far 'goo2';
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
"foo": `
|
||||
module foo {
|
||||
prefix "f";
|
||||
namespace "urn:f";
|
||||
|
||||
extension bar {
|
||||
argument "baz";
|
||||
}
|
||||
|
||||
extension far {
|
||||
argument "baz";
|
||||
}
|
||||
}
|
||||
`,
|
||||
"foo2": `
|
||||
module foo2 {
|
||||
prefix "f2";
|
||||
namespace "urn:f2";
|
||||
|
||||
extension bar {
|
||||
argument "baz";
|
||||
}
|
||||
|
||||
extension far {
|
||||
argument "baz";
|
||||
}
|
||||
}
|
||||
`,
|
||||
},
|
||||
wantNode: func(n Node) error {
|
||||
n, ok := n.(*Type)
|
||||
if !ok {
|
||||
return fmt.Errorf("got node: %v, want type: Leaf", n)
|
||||
}
|
||||
|
||||
var bars []string
|
||||
matches, err := matchingExtensions(n, n.Exts(), "foo", "bar")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, ext := range matches {
|
||||
bars = append(bars, ext.Argument)
|
||||
}
|
||||
|
||||
if diff := cmp.Diff(bars, []string{"boo", "coo", "foo"}); diff != "" {
|
||||
return fmt.Errorf("matchingExtensions (-got, +want):\n%s", diff)
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}, {
|
||||
desc: "Test matchingExtensions when module is not found",
|
||||
inFn: func(ms *Modules) (Node, error) {
|
||||
|
||||
module := "test"
|
||||
m, ok := ms.Modules[module]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("can't find module %q", module)
|
||||
}
|
||||
|
||||
if len(m.Leaf) == 0 {
|
||||
return nil, fmt.Errorf("node %v is missing imports", m)
|
||||
}
|
||||
|
||||
module = "foo"
|
||||
if _, ok := ms.Modules[module]; !ok {
|
||||
return nil, fmt.Errorf("can't find module %q", module)
|
||||
}
|
||||
|
||||
return m.Leaf[0].Type, nil
|
||||
},
|
||||
inModules: map[string]string{
|
||||
"test": `
|
||||
module test {
|
||||
prefix "t";
|
||||
namespace "urn:t";
|
||||
|
||||
import foo {
|
||||
prefix "f";
|
||||
description "foo module";
|
||||
}
|
||||
|
||||
leaf test-leaf {
|
||||
type string {
|
||||
pattern 'alpha';
|
||||
not-found:bar 'foo';
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
"foo": `
|
||||
module foo {
|
||||
prefix "f";
|
||||
namespace "urn:f";
|
||||
|
||||
extension bar {
|
||||
argument "baz";
|
||||
}
|
||||
|
||||
extension far {
|
||||
argument "baz";
|
||||
}
|
||||
}
|
||||
`,
|
||||
},
|
||||
wantNode: func(n Node) error {
|
||||
n, ok := n.(*Type)
|
||||
if !ok {
|
||||
return fmt.Errorf("got node: %v, want type: Leaf", n)
|
||||
}
|
||||
|
||||
var bars []string
|
||||
matches, err := matchingExtensions(n, n.Exts(), "foo", "bar")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, ext := range matches {
|
||||
bars = append(bars, ext.Argument)
|
||||
}
|
||||
|
||||
if diff := cmp.Diff(bars, []string{"boo", "coo", "foo"}); diff != "" {
|
||||
return fmt.Errorf("matchingExtensions (-got, +want):\n%s", diff)
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
wantErrSubstr: `module prefix "not-found" not found`,
|
||||
}}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.desc, func(t *testing.T) {
|
||||
ms := NewModules()
|
||||
|
||||
for n, m := range tt.inModules {
|
||||
if err := ms.Parse(m, n); err != nil {
|
||||
t.Errorf("error parsing module %s, got: %v, want: nil", n, err)
|
||||
}
|
||||
}
|
||||
|
||||
errs := ms.Process()
|
||||
var err error
|
||||
if len(errs) > 1 {
|
||||
t.Fatalf("Got more than 1 error: %v", errs)
|
||||
} else if len(errs) == 1 {
|
||||
err = errs[0]
|
||||
}
|
||||
if diff := errdiff.Substring(err, tt.wantErrSubstr); diff != "" {
|
||||
t.Errorf("Did not get expected error: %s", diff)
|
||||
}
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
node, err := tt.inFn(ms)
|
||||
if err != nil {
|
||||
t.Fatalf("cannot run in function, %v", err)
|
||||
}
|
||||
|
||||
if err := tt.wantNode(node); err != nil {
|
||||
t.Fatalf("failed check function, %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestModulesFindByPrefix(t *testing.T) {
|
||||
// Some examples of where prefixes might be used are in the following
|
||||
// YANG statements: extension, uses, augment, deviation, type, leafref.
|
||||
// Not all are put into the test here, since the logic is the same for
|
||||
// each.
|
||||
modules := map[string]string{
|
||||
"foo": `module foo { prefix "foo"; namespace "urn:foo"; include bar; leaf leafref { type leafref { path "../foo:leaf"; } } uses foo:lg; }`,
|
||||
"bar": `submodule bar { belongs-to foo { prefix "bar"; } container c { uses bar:lg; } grouping lg { leaf leaf { type string; } } }`,
|
||||
"baz": `module baz { prefix "foo"; namespace "urn:foo"; import foo { prefix f; } extension e; uses f:lg; foo:e; }`,
|
||||
}
|
||||
|
||||
ms := NewModules()
|
||||
for name, modtext := range modules {
|
||||
if err := ms.Parse(modtext, name+".yang"); err != nil {
|
||||
t.Fatalf("error parsing module %q: %v", name, err)
|
||||
}
|
||||
}
|
||||
if errs := ms.Process(); errs != nil {
|
||||
for _, err := range errs {
|
||||
t.Errorf("error: %v", err)
|
||||
}
|
||||
t.Fatalf("fatal error(s) calling Process()")
|
||||
}
|
||||
|
||||
for _, tt := range []struct {
|
||||
desc string
|
||||
node Node
|
||||
prefix string
|
||||
want *Module
|
||||
}{
|
||||
{
|
||||
desc: "nil node",
|
||||
node: nil,
|
||||
prefix: "does-not-exist",
|
||||
want: nil,
|
||||
},
|
||||
{
|
||||
desc: "module foo",
|
||||
node: ms.Modules["foo"],
|
||||
prefix: "foo",
|
||||
want: ms.Modules["foo"],
|
||||
},
|
||||
{
|
||||
desc: "submodule bar",
|
||||
node: ms.SubModules["bar"],
|
||||
prefix: "bar",
|
||||
want: ms.SubModules["bar"],
|
||||
},
|
||||
{
|
||||
desc: "module baz",
|
||||
node: ms.Modules["baz"],
|
||||
prefix: "foo",
|
||||
want: ms.Modules["baz"],
|
||||
},
|
||||
{
|
||||
desc: "foo leafref",
|
||||
node: ms.Modules["foo"].Leaf[0].Type,
|
||||
prefix: "foo",
|
||||
want: ms.Modules["foo"],
|
||||
},
|
||||
{
|
||||
desc: "foo uses",
|
||||
node: ms.Modules["foo"].Uses[0],
|
||||
prefix: "foo",
|
||||
want: ms.Modules["foo"],
|
||||
},
|
||||
{
|
||||
desc: "bar uses",
|
||||
node: ms.SubModules["bar"].Container[0].Uses[0],
|
||||
prefix: "bar",
|
||||
want: ms.SubModules["bar"],
|
||||
},
|
||||
{
|
||||
desc: "baz uses",
|
||||
node: ms.Modules["baz"].Uses[0],
|
||||
prefix: "f",
|
||||
want: ms.Modules["foo"],
|
||||
},
|
||||
{
|
||||
desc: "baz extension",
|
||||
node: ms.Modules["baz"],
|
||||
prefix: "foo",
|
||||
want: ms.Modules["baz"],
|
||||
},
|
||||
} {
|
||||
t.Run(tt.desc, func(t *testing.T) {
|
||||
if got := FindModuleByPrefix(tt.node, tt.prefix); got != tt.want {
|
||||
t.Errorf("got: %v, want: %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
// Copyright 2017 Google Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package yang
|
||||
|
||||
// Options defines the options that should be used when parsing YANG modules,
|
||||
// including specific overrides for potentially problematic YANG constructs.
|
||||
type Options struct {
|
||||
// IgnoreSubmoduleCircularDependencies specifies whether circular dependencies
|
||||
// between submodules. Setting this value to true will ensure that this
|
||||
// package will explicitly ignore the case where a submodule will include
|
||||
// itself through a circular reference.
|
||||
IgnoreSubmoduleCircularDependencies bool
|
||||
// StoreUses controls whether the Uses field of each YANG entry should be
|
||||
// populated. Setting this value to true will cause each Entry which is
|
||||
// generated within the schema to store the logical grouping from which it
|
||||
// is derived.
|
||||
StoreUses bool
|
||||
// DeviateOptions contains options for how deviations are handled.
|
||||
DeviateOptions DeviateOptions
|
||||
}
|
||||
|
||||
// DeviateOptions contains options for how deviations are handled.
|
||||
type DeviateOptions struct {
|
||||
// IgnoreDeviateNotSupported indicates to the parser to retain nodes
|
||||
// that are marked with "deviate not-supported". An example use case is
|
||||
// where the user wants to interact with different targets that have
|
||||
// different support for a leaf without having to use a second instance
|
||||
// of an AST.
|
||||
IgnoreDeviateNotSupported bool
|
||||
}
|
||||
|
||||
// IsDeviateOpt ensures that DeviateOptions satisfies the DeviateOpt interface.
|
||||
func (DeviateOptions) IsDeviateOpt() {}
|
||||
|
||||
// DeviateOpt is an interface that can be used in function arguments.
|
||||
type DeviateOpt interface {
|
||||
IsDeviateOpt()
|
||||
}
|
||||
|
||||
func hasIgnoreDeviateNotSupported(opts []DeviateOpt) bool {
|
||||
for _, o := range opts {
|
||||
if opt, ok := o.(DeviateOptions); ok {
|
||||
return opt.IgnoreDeviateNotSupported
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,338 @@
|
||||
// Copyright 2015 Google Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package yang
|
||||
|
||||
// This file implements Parse, which parses the input as generic YANG and
|
||||
// returns a slice of base Statements (which in turn may contain more
|
||||
// Statements, i.e., a slice of Statement trees.)
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// a parser is used to parse the contents of a single .yang file.
|
||||
type parser struct {
|
||||
lex *lexer
|
||||
errout *bytes.Buffer
|
||||
tokens []*token // stack of pushed tokens (for backing up)
|
||||
|
||||
// Depth of statements in nested braces
|
||||
statementDepth int
|
||||
|
||||
// hitBrace is returned when we encounter a '}'. The statement location
|
||||
// is updated with the location of the '}'. The brace may be legitimate
|
||||
// but only the caller will know if it is. That is, the brace may be
|
||||
// closing our parent or may be an error (we didn't expect it).
|
||||
// hitBrace is updated with the file, line, and column of the brace's
|
||||
// location.
|
||||
hitBrace *Statement
|
||||
}
|
||||
|
||||
// Statement is a generic YANG statement that may have sub-statements.
|
||||
// It implements the Node interface.
|
||||
//
|
||||
// Within the parser, it represents a non-terminal token.
|
||||
// From https://tools.ietf.org/html/rfc7950#section-6.3:
|
||||
// statement = keyword [argument] (";" / "{" *statement "}")
|
||||
// The argument is a string.
|
||||
type Statement struct {
|
||||
Keyword string
|
||||
HasArgument bool
|
||||
Argument string
|
||||
statements []*Statement
|
||||
|
||||
file string
|
||||
line int // 1's based line number
|
||||
col int // 1's based column number
|
||||
}
|
||||
|
||||
func (s *Statement) NName() string { return s.Argument }
|
||||
func (s *Statement) Kind() string { return s.Keyword }
|
||||
func (s *Statement) Statement() *Statement { return s }
|
||||
func (s *Statement) ParentNode() Node { return nil }
|
||||
func (s *Statement) Exts() []*Statement { return nil }
|
||||
|
||||
// Arg returns the optional argument to s. It returns false if s has no
|
||||
// argument.
|
||||
func (s *Statement) Arg() (string, bool) { return s.Argument, s.HasArgument }
|
||||
|
||||
// SubStatements returns a slice of Statements found in s.
|
||||
func (s *Statement) SubStatements() []*Statement { return s.statements }
|
||||
|
||||
// Location returns the location in the source where s was defined.
|
||||
func (s *Statement) Location() string {
|
||||
switch {
|
||||
case s.file == "" && s.line == 0:
|
||||
return "unknown"
|
||||
case s.file == "":
|
||||
return fmt.Sprintf("line %d:%d", s.line, s.col)
|
||||
case s.line == 0:
|
||||
return s.file
|
||||
default:
|
||||
return fmt.Sprintf("%s:%d:%d", s.file, s.line, s.col)
|
||||
}
|
||||
}
|
||||
|
||||
// Write writes the tree in s to w, each line indented by ident. Children
|
||||
// nodes are indented further by a tab. Typically indent is "" at the top
|
||||
// level. Write is intended to display the contents of Statement, but
|
||||
// not necessarily reproduce the input of Statement.
|
||||
func (s *Statement) Write(w io.Writer, indent string) error {
|
||||
if s.Keyword == "" {
|
||||
// We are just a collection of statements at the top level.
|
||||
for _, s := range s.statements {
|
||||
if err := s.Write(w, indent); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
parts := []string{fmt.Sprintf("%s%s", indent, s.Keyword)}
|
||||
if s.HasArgument {
|
||||
args := strings.Split(s.Argument, "\n")
|
||||
if len(args) == 1 {
|
||||
parts = append(parts, fmt.Sprintf(" %q", s.Argument))
|
||||
} else {
|
||||
parts = append(parts, ` "`, args[0], "\n")
|
||||
i := fmt.Sprintf("%*s", len(s.Keyword)+1, "")
|
||||
for x, p := range args[1:] {
|
||||
s := fmt.Sprintf("%q", p)
|
||||
s = s[1 : len(s)-1]
|
||||
parts = append(parts, indent, " ", i, s)
|
||||
if x == len(args[1:])-1 {
|
||||
// last part just needs the closing "
|
||||
parts = append(parts, `"`)
|
||||
} else {
|
||||
parts = append(parts, "\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(s.statements) == 0 {
|
||||
_, err := fmt.Fprintf(w, "%s;\n", strings.Join(parts, ""))
|
||||
return err
|
||||
}
|
||||
if _, err := fmt.Fprintf(w, "%s {\n", strings.Join(parts, "")); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, s := range s.statements {
|
||||
if err := s.Write(w, indent+"\t"); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if _, err := fmt.Fprintf(w, "%s}\n", indent); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ignoreMe is an error recovery token used by the parser in order
|
||||
// to continue processing for other errors in the file.
|
||||
var ignoreMe = &Statement{}
|
||||
|
||||
// Parse parses the input as generic YANG and returns the statements parsed.
|
||||
// The path parameter should be the source name where input was read from (e.g.,
|
||||
// the file name the input was read from). If one more more errors are
|
||||
// encountered, nil and an error are returned. The error's text includes all
|
||||
// errors encountered.
|
||||
func Parse(input, path string) ([]*Statement, error) {
|
||||
var statements []*Statement
|
||||
p := &parser{
|
||||
lex: newLexer(input, path),
|
||||
errout: &bytes.Buffer{},
|
||||
hitBrace: &Statement{},
|
||||
}
|
||||
p.lex.errout = p.errout
|
||||
Loop:
|
||||
for {
|
||||
switch ns := p.nextStatement(); ns {
|
||||
case nil:
|
||||
break Loop
|
||||
case p.hitBrace:
|
||||
fmt.Fprintf(p.errout, "%s:%d:%d: unexpected %c\n", ns.file, ns.line, ns.col, '}')
|
||||
default:
|
||||
statements = append(statements, ns)
|
||||
}
|
||||
}
|
||||
|
||||
p.checkStatementDepthIsZero()
|
||||
|
||||
if p.errout.Len() == 0 {
|
||||
return statements, nil
|
||||
}
|
||||
return nil, errors.New(strings.TrimSpace(p.errout.String()))
|
||||
}
|
||||
|
||||
// push pushes tokens t back on the input stream so they will be the next
|
||||
// tokens returned by next. The tokens list is a LIFO so the final token
|
||||
// listed to push will be the next token returned.
|
||||
func (p *parser) push(t ...*token) {
|
||||
p.tokens = append(p.tokens, t...)
|
||||
}
|
||||
|
||||
// pop returns the last token pushed, or nil if the token stack is empty.
|
||||
func (p *parser) pop() *token {
|
||||
if n := len(p.tokens); n > 0 {
|
||||
n--
|
||||
defer func() { p.tokens = p.tokens[:n] }()
|
||||
return p.tokens[n]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// next returns the next token from the lexer. If the next token is a
|
||||
// concatenated string, it returns the concatenated string as the token.
|
||||
func (p *parser) next() *token {
|
||||
if t := p.pop(); t != nil {
|
||||
return t
|
||||
}
|
||||
// next returns the next unprocessed lexer token.
|
||||
next := func() *token {
|
||||
for {
|
||||
if t := p.lex.NextToken(); t.Code() != tError {
|
||||
return t
|
||||
}
|
||||
}
|
||||
}
|
||||
t := next()
|
||||
if t.Code() != tString {
|
||||
return t
|
||||
}
|
||||
// Process string concatenation (both single and double quote).
|
||||
// See https://tools.ietf.org/html/rfc7950#section-6.1.3.1
|
||||
// The lexer trimmed the quotes already.
|
||||
for {
|
||||
nt := next()
|
||||
switch nt.Code() {
|
||||
case tEOF:
|
||||
return t
|
||||
case tUnquoted:
|
||||
if nt.Text != "+" {
|
||||
p.push(nt)
|
||||
return t
|
||||
}
|
||||
default:
|
||||
p.push(nt)
|
||||
return t
|
||||
}
|
||||
// Invariant: nt is a + sign.
|
||||
nnt := next()
|
||||
switch nnt.Code() {
|
||||
case tEOF:
|
||||
p.push(nt)
|
||||
return t
|
||||
case tString:
|
||||
// Accumulate the concatenation.
|
||||
t.Text += nnt.Text
|
||||
default:
|
||||
p.push(nnt, nt)
|
||||
return t
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// nextStatement returns the next statement in the input, which may in turn
|
||||
// recurse to read sub statements.
|
||||
// nil is returned when EOF has been reached, or is reached halfway through
|
||||
// parsing the next statement (with associated syntax errors printed to
|
||||
// errout).
|
||||
func (p *parser) nextStatement() *Statement {
|
||||
t := p.next()
|
||||
switch t.Code() {
|
||||
case tEOF:
|
||||
return nil
|
||||
case '}':
|
||||
p.statementDepth -= 1
|
||||
p.hitBrace.file = t.File
|
||||
p.hitBrace.line = t.Line
|
||||
p.hitBrace.col = t.Col
|
||||
return p.hitBrace
|
||||
case tUnquoted:
|
||||
default:
|
||||
fmt.Fprintf(p.errout, "%v: keyword token not an unquoted string\n", t)
|
||||
return ignoreMe
|
||||
}
|
||||
// Invariant: t represents a keyword token.
|
||||
|
||||
s := &Statement{
|
||||
Keyword: t.Text,
|
||||
file: t.File,
|
||||
line: t.Line,
|
||||
col: t.Col,
|
||||
}
|
||||
|
||||
// The keyword "pattern" must be treated specially. When
|
||||
// parsing the argument for "pattern", escape sequences
|
||||
// must be expanded differently.
|
||||
p.lex.inPattern = t.Text == "pattern"
|
||||
t = p.next()
|
||||
p.lex.inPattern = false
|
||||
switch t.Code() {
|
||||
case tString, tUnquoted:
|
||||
s.HasArgument = true
|
||||
s.Argument = t.Text
|
||||
t = p.next()
|
||||
}
|
||||
|
||||
switch t.Code() {
|
||||
case tEOF:
|
||||
fmt.Fprintf(p.errout, "%s: unexpected EOF\n", s.file)
|
||||
return nil
|
||||
case ';':
|
||||
return s
|
||||
case '{':
|
||||
p.statementDepth += 1
|
||||
for {
|
||||
switch ns := p.nextStatement(); ns {
|
||||
case nil:
|
||||
// Signal EOF reached.
|
||||
return nil
|
||||
case p.hitBrace:
|
||||
return s
|
||||
default:
|
||||
s.statements = append(s.statements, ns)
|
||||
}
|
||||
}
|
||||
default:
|
||||
fmt.Fprintf(p.errout, "%v: syntax error, expected ';' or '{'\n", t)
|
||||
return ignoreMe
|
||||
}
|
||||
}
|
||||
|
||||
// checkStatementDepthIsZero checks that we aren't missing closing
|
||||
// braces. Note: the parser will error out for the case where we
|
||||
// start with an unmatched close brace, i.e. depth < 0
|
||||
//
|
||||
// This test should only be done if there are no other errors as
|
||||
// we may exit early due to those errors -- and therefore there *might*
|
||||
// not really be a mismatched brace issue.
|
||||
func (p *parser) checkStatementDepthIsZero() {
|
||||
if p.errout.Len() > 0 || p.statementDepth == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
plural := ""
|
||||
if p.statementDepth > 1 {
|
||||
plural = "s"
|
||||
}
|
||||
fmt.Fprintf(p.errout, "%s:%d:%d: missing %d closing brace%s\n",
|
||||
p.lex.file, p.lex.line, p.lex.col, p.statementDepth, plural)
|
||||
}
|
||||
@@ -0,0 +1,539 @@
|
||||
// Copyright 2015 Google Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package yang
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func (s1 *Statement) equal(s2 *Statement) bool {
|
||||
if s1.Keyword != s2.Keyword ||
|
||||
s1.HasArgument != s2.HasArgument ||
|
||||
s1.Argument != s2.Argument ||
|
||||
len(s1.statements) != len(s2.statements) {
|
||||
return false
|
||||
}
|
||||
|
||||
for x, ss := range s1.statements {
|
||||
if !ss.equal(s2.statements[x]) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// SA returns a statement with an argument and optional substatements.
|
||||
func SA(k, a string, ss ...*Statement) *Statement {
|
||||
return &Statement{
|
||||
Keyword: k,
|
||||
Argument: a,
|
||||
HasArgument: true,
|
||||
statements: ss,
|
||||
}
|
||||
}
|
||||
|
||||
// S returns a statement with no argument and optional substatements.
|
||||
func S(k string, ss ...*Statement) *Statement {
|
||||
return &Statement{
|
||||
Keyword: k,
|
||||
statements: ss,
|
||||
}
|
||||
}
|
||||
|
||||
func TestParse(t *testing.T) {
|
||||
for _, tt := range []struct {
|
||||
line int
|
||||
in string
|
||||
out []*Statement
|
||||
err string
|
||||
}{
|
||||
{line: line()},
|
||||
{line: line(), in: `
|
||||
foo;
|
||||
`,
|
||||
out: []*Statement{
|
||||
S("foo"),
|
||||
},
|
||||
},
|
||||
{line: line(), in: `
|
||||
foo {}
|
||||
`,
|
||||
out: []*Statement{
|
||||
S("foo"),
|
||||
},
|
||||
},
|
||||
{line: line(), in: `
|
||||
foo "";
|
||||
`,
|
||||
out: []*Statement{
|
||||
SA("foo", ""),
|
||||
},
|
||||
},
|
||||
{line: line(), in: `
|
||||
foo bar;
|
||||
`,
|
||||
out: []*Statement{
|
||||
SA("foo", "bar"),
|
||||
},
|
||||
},
|
||||
{line: line(), in: `
|
||||
foo "bar";
|
||||
`,
|
||||
out: []*Statement{
|
||||
SA("foo", "bar"),
|
||||
},
|
||||
},
|
||||
{line: line(), in: `
|
||||
foo "\\ \S \n";
|
||||
`,
|
||||
err: `test.yang:2:9: invalid escape sequence: \S`,
|
||||
},
|
||||
{line: line(), in: `
|
||||
pattern "\\ \S \n";
|
||||
`,
|
||||
out: []*Statement{
|
||||
SA("pattern", `\ \S
|
||||
`),
|
||||
},
|
||||
},
|
||||
{line: line(), in: `
|
||||
foo '\\ \S \n';
|
||||
`,
|
||||
out: []*Statement{
|
||||
SA("foo", `\\ \S \n`),
|
||||
},
|
||||
},
|
||||
{line: line(), in: `
|
||||
pattern '\\ \S \n';
|
||||
`,
|
||||
out: []*Statement{
|
||||
SA("pattern", `\\ \S \n`),
|
||||
},
|
||||
},
|
||||
{line: line(), in: `
|
||||
foo "bar" + "baz";
|
||||
`,
|
||||
out: []*Statement{
|
||||
SA("foo", "barbaz"),
|
||||
},
|
||||
},
|
||||
{line: line(), in: `
|
||||
foo "bar" + "+" + "baz";
|
||||
`,
|
||||
out: []*Statement{
|
||||
SA("foo", "bar+baz"),
|
||||
},
|
||||
},
|
||||
{line: line(), in: `
|
||||
foo "bar"
|
||||
`,
|
||||
err: `test.yang: unexpected EOF`,
|
||||
},
|
||||
{line: line(), in: `
|
||||
foo "bar" + "baz"
|
||||
`,
|
||||
err: `test.yang: unexpected EOF`,
|
||||
},
|
||||
{line: line(), in: `
|
||||
foo "bar" baz;
|
||||
`,
|
||||
err: `test.yang:2:11: baz: syntax error, expected ';' or '{'
|
||||
test.yang:2:14: ;: keyword token not an unquoted string`,
|
||||
},
|
||||
{line: line(), in: `
|
||||
foo "bar" + baz;
|
||||
`,
|
||||
err: `test.yang:2:11: +: syntax error, expected ';' or '{'`,
|
||||
},
|
||||
{line: line(), in: `
|
||||
foo "bar" +
|
||||
`,
|
||||
err: `test.yang:2:11: +: syntax error, expected ';' or '{'`,
|
||||
},
|
||||
{line: line(), in: `
|
||||
foo "bar";
|
||||
`,
|
||||
out: []*Statement{
|
||||
SA("foo", "bar"),
|
||||
},
|
||||
},
|
||||
{line: line(), in: `
|
||||
foo "bar" {}
|
||||
`,
|
||||
out: []*Statement{
|
||||
SA("foo", "bar"),
|
||||
},
|
||||
},
|
||||
{line: line(), in: `
|
||||
foo 'bar' + 'baz';
|
||||
`,
|
||||
out: []*Statement{
|
||||
SA("foo", "barbaz"),
|
||||
},
|
||||
},
|
||||
{line: line(), in: `
|
||||
foo 'bar' + '+' + 'baz';
|
||||
`,
|
||||
out: []*Statement{
|
||||
SA("foo", "bar+baz"),
|
||||
},
|
||||
},
|
||||
{line: line(), in: `
|
||||
foo 'bar'
|
||||
`,
|
||||
err: `test.yang: unexpected EOF`,
|
||||
},
|
||||
{line: line(), in: `
|
||||
foo 'bar' + 'baz'
|
||||
`,
|
||||
err: `test.yang: unexpected EOF`,
|
||||
},
|
||||
{line: line(), in: `
|
||||
foo 'bar' baz;
|
||||
`,
|
||||
err: `test.yang:2:11: baz: syntax error, expected ';' or '{'
|
||||
test.yang:2:14: ;: keyword token not an unquoted string`,
|
||||
},
|
||||
{line: line(), in: `
|
||||
foo 'bar' + baz;
|
||||
`,
|
||||
err: `test.yang:2:11: +: syntax error, expected ';' or '{'`,
|
||||
},
|
||||
{line: line(), in: `
|
||||
foo 'bar' +
|
||||
`,
|
||||
err: `test.yang:2:11: +: syntax error, expected ';' or '{'`,
|
||||
},
|
||||
{line: line(), in: `
|
||||
foo 'bar';
|
||||
`,
|
||||
out: []*Statement{
|
||||
SA("foo", "bar"),
|
||||
},
|
||||
},
|
||||
{line: line(), in: `
|
||||
foo 'bar' {}
|
||||
`,
|
||||
out: []*Statement{
|
||||
SA("foo", "bar"),
|
||||
},
|
||||
},
|
||||
{line: line(), in: `
|
||||
foo bar;
|
||||
red black;
|
||||
`,
|
||||
out: []*Statement{
|
||||
SA("foo", "bar"),
|
||||
SA("red", "black"),
|
||||
},
|
||||
},
|
||||
{line: line(), in: `
|
||||
foo {
|
||||
key value;
|
||||
}
|
||||
`,
|
||||
out: []*Statement{
|
||||
S("foo",
|
||||
SA("key", "value"),
|
||||
),
|
||||
},
|
||||
},
|
||||
{line: line(), in: `
|
||||
foo {
|
||||
key value;
|
||||
}
|
||||
`,
|
||||
out: []*Statement{
|
||||
S("foo",
|
||||
SA("key", "value"),
|
||||
),
|
||||
},
|
||||
},
|
||||
{line: line(), in: `
|
||||
foo {
|
||||
key "value1 value2
|
||||
|
||||
value3";
|
||||
}
|
||||
`,
|
||||
out: []*Statement{
|
||||
S("foo",
|
||||
SA("key", "value1 value2\n\n value3"),
|
||||
),
|
||||
},
|
||||
},
|
||||
{line: line(), in: `
|
||||
foo {
|
||||
key value;
|
||||
key2;
|
||||
}
|
||||
`,
|
||||
out: []*Statement{
|
||||
S("foo",
|
||||
SA("key", "value"),
|
||||
S("key2"),
|
||||
),
|
||||
},
|
||||
},
|
||||
{line: line(), in: `
|
||||
foo1 {
|
||||
key value1;
|
||||
}
|
||||
foo2 {
|
||||
key value2;
|
||||
}
|
||||
foo3 value3;
|
||||
`,
|
||||
out: []*Statement{
|
||||
S("foo1",
|
||||
SA("key", "value1"),
|
||||
),
|
||||
S("foo2",
|
||||
SA("key", "value2"),
|
||||
),
|
||||
SA("foo3", "value3"),
|
||||
},
|
||||
},
|
||||
{line: line(), in: `
|
||||
foo1 {
|
||||
key value1;
|
||||
foo2 {
|
||||
key value2;
|
||||
}
|
||||
}
|
||||
`,
|
||||
out: []*Statement{
|
||||
S("foo1",
|
||||
SA("key", "value1"),
|
||||
S("foo2",
|
||||
SA("key", "value2"),
|
||||
),
|
||||
),
|
||||
},
|
||||
},
|
||||
{line: line(), in: `
|
||||
foo1 {
|
||||
key value1;
|
||||
foo2 {
|
||||
pattern '[a-zA-Z0-9!#$%&'+"'"+'*+/=?^_` + "`" + `{|}~-]+'
|
||||
+ '(\.[a-zA-Z0-9!#$%&'+"'"+'*+/=?^_` + "`" + `{|}~-]+)*'
|
||||
+ '@'
|
||||
+ '[a-zA-Z0-9!#$%&'+"'"+'*+/=?^_` + "`" + `{|}~-]+'
|
||||
+ '(\.[a-zA-Z0-9!#$%&'+"'"+'*+/=?^_` + "`" + `{|}~-]+)*';
|
||||
}
|
||||
}
|
||||
`,
|
||||
out: []*Statement{
|
||||
S("foo1",
|
||||
SA("key", "value1"),
|
||||
S("foo2",
|
||||
SA("pattern", "[a-zA-Z0-9!#$%&'*+/=?^_`{|}~-]+(\\.[a-zA-Z0-9!#$%&'*+/=?^_`{|}~-]+)*@[a-zA-Z0-9!#$%&'*+/=?^_`{|}~-]+(\\.[a-zA-Z0-9!#$%&'*+/=?^_`{|}~-]+)*"),
|
||||
),
|
||||
),
|
||||
},
|
||||
},
|
||||
{line: line(), in: `
|
||||
}
|
||||
`,
|
||||
err: `test.yang:2:2: unexpected }`,
|
||||
},
|
||||
{line: line(), in: `
|
||||
id
|
||||
`,
|
||||
err: `test.yang: unexpected EOF`,
|
||||
},
|
||||
{line: line(), in: `
|
||||
{
|
||||
`,
|
||||
err: `test.yang:2:4: {: keyword token not an unquoted string`,
|
||||
},
|
||||
{line: line(), in: `
|
||||
;
|
||||
`,
|
||||
err: `test.yang:2:1: ;: keyword token not an unquoted string`,
|
||||
},
|
||||
{line: line(), in: `
|
||||
statement one two { }
|
||||
`,
|
||||
err: `test.yang:2:15: two: syntax error, expected ';' or '{'
|
||||
test.yang:2:19: {: keyword token not an unquoted string
|
||||
test.yang:2:21: unexpected }`,
|
||||
},
|
||||
{line: line(), in: `
|
||||
}
|
||||
foo {
|
||||
key: "value";
|
||||
}
|
||||
`,
|
||||
err: `test.yang:2:5: unexpected }`,
|
||||
},
|
||||
{line: line(), in: `
|
||||
{
|
||||
something: "bad";
|
||||
}
|
||||
foo {
|
||||
key: "\Value";
|
||||
key2: "value2";
|
||||
bar {
|
||||
key3: "value\3;
|
||||
}
|
||||
}`,
|
||||
err: `test.yang:2:1: {: keyword token not an unquoted string
|
||||
test.yang:4:1: unexpected }
|
||||
test.yang:6:8: invalid escape sequence: \V
|
||||
test.yang:9:15: invalid escape sequence: \3
|
||||
test.yang:9:9: missing closing "
|
||||
test.yang: unexpected EOF`,
|
||||
},
|
||||
{line: line(), in: `
|
||||
module base {
|
||||
container top-missing-close-brace {
|
||||
leaf my-leaf {
|
||||
type string;
|
||||
}
|
||||
}
|
||||
`,
|
||||
err: "test.yang:8:0: missing 1 closing brace",
|
||||
},
|
||||
{line: line(), in: `
|
||||
module base {
|
||||
container top-missing-close-brace {
|
||||
leaf my-leaf {
|
||||
type string;
|
||||
}
|
||||
`,
|
||||
err: "test.yang:7:0: missing 2 closing braces",
|
||||
},
|
||||
} {
|
||||
s, err := Parse(tt.in, "test.yang")
|
||||
if (s == nil) != (tt.out == nil) {
|
||||
if s == nil {
|
||||
t.Errorf("%d: did not get expected statements: %v", tt.line, tt.out)
|
||||
} else {
|
||||
t.Errorf("%d: get unexpected statements: %v", tt.line, s)
|
||||
}
|
||||
}
|
||||
switch {
|
||||
case err == nil && tt.err == "":
|
||||
case tt.err == "":
|
||||
t.Errorf("%d: unexpected error %v", tt.line, err)
|
||||
continue
|
||||
case err == nil:
|
||||
t.Errorf("%d: did not get expected error %v", tt.line, tt.err)
|
||||
continue
|
||||
case err.Error() == tt.err:
|
||||
continue
|
||||
default:
|
||||
t.Errorf("%d: got error:\n%s\nwant:\n%s", tt.line, err, tt.err)
|
||||
continue
|
||||
}
|
||||
s1 := &Statement{statements: s}
|
||||
s2 := &Statement{statements: tt.out}
|
||||
if !s1.equal(s2) {
|
||||
t.Errorf("%d: got:\n%v\nwant:\n%v", tt.line, s1, s2)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestWrite(t *testing.T) {
|
||||
Testing:
|
||||
for _, tt := range []struct {
|
||||
line int
|
||||
in string
|
||||
out string
|
||||
}{
|
||||
{line: line(),
|
||||
in: `key arg { substatement; }`,
|
||||
out: `key "arg" {
|
||||
substatement;
|
||||
}
|
||||
`,
|
||||
},
|
||||
{line: line(),
|
||||
in: `key { substatement { key arg; }}`,
|
||||
out: `key {
|
||||
substatement {
|
||||
key "arg";
|
||||
}
|
||||
}
|
||||
`,
|
||||
},
|
||||
{line: line(),
|
||||
in: `
|
||||
module base {
|
||||
namespace "urn:mod";
|
||||
prefix "base";
|
||||
|
||||
typedef base-type { type int32; }
|
||||
|
||||
grouping base-group {
|
||||
description
|
||||
"The base-group is used to test the
|
||||
'uses' statement below. This description
|
||||
is here to simply include a multi-line
|
||||
string as an example of multi-line strings";
|
||||
leaf base-group-leaf {
|
||||
config false;
|
||||
type string;
|
||||
}
|
||||
}
|
||||
uses base-group;
|
||||
}
|
||||
`, out: `module "base" {
|
||||
namespace "urn:mod";
|
||||
prefix "base";
|
||||
typedef "base-type" {
|
||||
type "int32";
|
||||
}
|
||||
grouping "base-group" {
|
||||
description "The base-group is used to test the
|
||||
'uses' statement below. This description
|
||||
is here to simply include a multi-line
|
||||
string as an example of multi-line strings";
|
||||
leaf "base-group-leaf" {
|
||||
config "false";
|
||||
type "string";
|
||||
}
|
||||
}
|
||||
uses "base-group";
|
||||
}
|
||||
`,
|
||||
},
|
||||
} {
|
||||
in := tt.in
|
||||
// Run twice. The first time we are parsing tt.in, the second
|
||||
// time we are parsing the output from the first parsing.
|
||||
for i := 0; i < 2; i++ {
|
||||
s, err := Parse(in, "test.yang")
|
||||
if err != nil {
|
||||
t.Errorf("%d: unexpected error %v", tt.line, err)
|
||||
continue Testing
|
||||
}
|
||||
if len(s) != 1 {
|
||||
t.Errorf("%d: got %d statements, expected 1", tt.line, len(s))
|
||||
continue Testing
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
s[0].Write(&buf, "")
|
||||
out := buf.String()
|
||||
if out != tt.out {
|
||||
t.Errorf("%d: got:\n%swant:\n%s", tt.line, out, tt.out)
|
||||
continue Testing
|
||||
}
|
||||
in = out
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
module deviate {
|
||||
prefix "d";
|
||||
namespace "urn:d";
|
||||
|
||||
grouping substmts {
|
||||
leaf config {
|
||||
type string;
|
||||
config true;
|
||||
}
|
||||
leaf default {
|
||||
type string;
|
||||
default "fish";
|
||||
}
|
||||
leaf mandatory {
|
||||
type string;
|
||||
mandatory false;
|
||||
}
|
||||
leaf-list max-elements {
|
||||
type string;
|
||||
max-elements 1000;
|
||||
}
|
||||
leaf-list min-elements {
|
||||
type string;
|
||||
min-elements 1000;
|
||||
}
|
||||
leaf-list max-and-min-elements {
|
||||
type string;
|
||||
max-elements 1024;
|
||||
min-elements 1;
|
||||
}
|
||||
leaf type {
|
||||
type string;
|
||||
}
|
||||
// TODO(robjs): unique for deviation
|
||||
leaf units {
|
||||
type uint16;
|
||||
units "nanofish per millenium";
|
||||
}
|
||||
}
|
||||
|
||||
container target {
|
||||
container delete {
|
||||
uses substmts;
|
||||
}
|
||||
}
|
||||
|
||||
deviation /target/delete/config {
|
||||
deviate delete {
|
||||
config true;
|
||||
}
|
||||
}
|
||||
|
||||
deviation /target/delete/default {
|
||||
deviate delete {
|
||||
default "fish";
|
||||
}
|
||||
}
|
||||
|
||||
deviation /target/delete/mandatory {
|
||||
deviate delete {
|
||||
mandatory false;
|
||||
}
|
||||
}
|
||||
|
||||
deviation /target/delete/min-elements {
|
||||
deviate delete {
|
||||
min-elements 1000;
|
||||
}
|
||||
}
|
||||
|
||||
deviation /target/delete/max-elements {
|
||||
deviate delete {
|
||||
max-elements 1000;
|
||||
}
|
||||
}
|
||||
|
||||
deviation /target/delete/max-and-min-elements {
|
||||
deviate delete {
|
||||
max-elements 1024;
|
||||
min-elements 1;
|
||||
}
|
||||
}
|
||||
|
||||
deviation /target/delete/units {
|
||||
deviate delete {
|
||||
units "nanofish per millenium";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
module deviate {
|
||||
prefix "d";
|
||||
namespace "urn:d";
|
||||
|
||||
grouping substmts {
|
||||
container child {
|
||||
leaf zzz { type string; }
|
||||
}
|
||||
}
|
||||
|
||||
container target {
|
||||
uses substmts;
|
||||
}
|
||||
|
||||
list target-list {
|
||||
key "k";
|
||||
|
||||
leaf k { type string; }
|
||||
uses substmts;
|
||||
}
|
||||
|
||||
leaf a-leaf { type string; }
|
||||
leaf a-leaflist { type string; }
|
||||
|
||||
leaf survivor { type string; }
|
||||
|
||||
deviation /target {
|
||||
deviate not-supported;
|
||||
}
|
||||
|
||||
deviation /target-list {
|
||||
deviate not-supported;
|
||||
}
|
||||
|
||||
deviation /a-leaf {
|
||||
deviate not-supported;
|
||||
}
|
||||
|
||||
deviation /a-leaflist {
|
||||
deviate not-supported;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
module deviate {
|
||||
prefix "d";
|
||||
namespace "urn:d";
|
||||
|
||||
grouping substmts {
|
||||
leaf config {
|
||||
type string;
|
||||
config true;
|
||||
}
|
||||
leaf default {
|
||||
type string;
|
||||
default "fish";
|
||||
}
|
||||
leaf-list default-list {
|
||||
type string;
|
||||
default "fish";
|
||||
default "sticks";
|
||||
}
|
||||
leaf mandatory {
|
||||
type string;
|
||||
mandatory false;
|
||||
}
|
||||
leaf-list max-elements {
|
||||
type string;
|
||||
max-elements 1000;
|
||||
}
|
||||
leaf-list min-elements {
|
||||
type string;
|
||||
min-elements 1000;
|
||||
}
|
||||
leaf-list max-and-min-elements {
|
||||
type string;
|
||||
max-elements 1024;
|
||||
min-elements 1;
|
||||
}
|
||||
leaf type {
|
||||
type string;
|
||||
}
|
||||
// TODO(robjs): unique for deviation
|
||||
leaf units {
|
||||
type uint16;
|
||||
units "nanofish per millenium";
|
||||
}
|
||||
}
|
||||
|
||||
container target {
|
||||
container replace {
|
||||
uses substmts;
|
||||
}
|
||||
}
|
||||
|
||||
deviation /target/replace/config {
|
||||
deviate replace {
|
||||
config false;
|
||||
}
|
||||
}
|
||||
|
||||
deviation /target/replace/default {
|
||||
deviate replace {
|
||||
default "a default value";
|
||||
}
|
||||
}
|
||||
|
||||
deviation /target/replace/default-list {
|
||||
deviate replace {
|
||||
default "nematodes";
|
||||
}
|
||||
}
|
||||
|
||||
deviation /target/replace/mandatory {
|
||||
deviate replace {
|
||||
mandatory true;
|
||||
}
|
||||
}
|
||||
|
||||
deviation /target/replace/min-elements {
|
||||
deviate replace {
|
||||
min-elements 42;
|
||||
}
|
||||
}
|
||||
|
||||
deviation /target/replace/max-elements {
|
||||
deviate replace {
|
||||
max-elements 42;
|
||||
}
|
||||
}
|
||||
|
||||
deviation /target/replace/max-and-min-elements {
|
||||
deviate replace {
|
||||
max-elements 42;
|
||||
min-elements 42;
|
||||
}
|
||||
}
|
||||
|
||||
deviation /target/replace/type {
|
||||
deviate replace {
|
||||
type uint16;
|
||||
}
|
||||
}
|
||||
|
||||
deviation /target/replace/units {
|
||||
deviate replace {
|
||||
units "fish per second";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
module deviate {
|
||||
prefix "d";
|
||||
namespace "urn:d";
|
||||
|
||||
typedef derived-string {
|
||||
type string;
|
||||
default "barnacles";
|
||||
}
|
||||
|
||||
grouping substmts {
|
||||
leaf config { type string; }
|
||||
leaf default { type string; }
|
||||
leaf default-typedef { type derived-string; }
|
||||
leaf-list default-list { type string; default "foo"; default "bar"; }
|
||||
leaf-list default-list-typedef-default { type derived-string; }
|
||||
leaf mandatory { type string; }
|
||||
leaf-list max-elements { type string; }
|
||||
leaf-list min-elements { type string; }
|
||||
leaf-list max-and-min-elements { type string; }
|
||||
leaf type { type string; }
|
||||
// TODO(robjs): unique requires a list target
|
||||
leaf units { type uint16; }
|
||||
}
|
||||
|
||||
container target {
|
||||
container add {
|
||||
uses substmts;
|
||||
}
|
||||
}
|
||||
|
||||
deviation /target/add/config {
|
||||
deviate add {
|
||||
config false;
|
||||
}
|
||||
}
|
||||
|
||||
deviation /target/add/default {
|
||||
deviate add {
|
||||
default "a default value";
|
||||
}
|
||||
}
|
||||
|
||||
deviation /target/add/default-list {
|
||||
deviate add {
|
||||
default "foo";
|
||||
// TODO(wenovus): support multiple default statements for deviate.
|
||||
//default "baz";
|
||||
}
|
||||
}
|
||||
|
||||
deviation /target/add/mandatory {
|
||||
deviate add {
|
||||
mandatory true;
|
||||
}
|
||||
}
|
||||
|
||||
deviation /target/add/min-elements {
|
||||
deviate add {
|
||||
min-elements 42;
|
||||
}
|
||||
}
|
||||
|
||||
deviation /target/add/max-elements {
|
||||
deviate add {
|
||||
max-elements 42;
|
||||
}
|
||||
}
|
||||
|
||||
deviation /target/add/max-and-min-elements {
|
||||
deviate add {
|
||||
max-elements 42;
|
||||
min-elements 42;
|
||||
}
|
||||
}
|
||||
|
||||
deviation /target/add/units {
|
||||
deviate add {
|
||||
units "fish per second";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,425 @@
|
||||
// Copyright 2015 Google Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package yang
|
||||
|
||||
// This file implements the functions relating to types and typedefs.
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"regexp/syntax"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// A typeDictionary is a dictionary of all Typedefs defined in all Typedefers.
|
||||
// A map of Nodes is used rather than a map of Typedefers to simplify usage
|
||||
// when traversing up a Node tree.
|
||||
type typeDictionary struct {
|
||||
mu sync.Mutex
|
||||
dict map[Node]map[string]*Typedef
|
||||
// identities contains a dictionary of resolved identities.
|
||||
identities identityDictionary
|
||||
}
|
||||
|
||||
func newTypeDictionary() *typeDictionary {
|
||||
return &typeDictionary{
|
||||
dict: map[Node]map[string]*Typedef{},
|
||||
identities: identityDictionary{dict: map[string]resolvedIdentity{}},
|
||||
}
|
||||
}
|
||||
|
||||
// add adds an entry to the typeDictionary d.
|
||||
func (d *typeDictionary) add(n Node, name string, td *Typedef) {
|
||||
defer d.mu.Unlock()
|
||||
d.mu.Lock()
|
||||
if d.dict[n] == nil {
|
||||
d.dict[n] = map[string]*Typedef{}
|
||||
}
|
||||
d.dict[n][name] = td
|
||||
}
|
||||
|
||||
// find returns the Typedef name define in node n, or nil.
|
||||
func (d *typeDictionary) find(n Node, name string) *Typedef {
|
||||
defer d.mu.Unlock()
|
||||
d.mu.Lock()
|
||||
if d.dict[n] == nil {
|
||||
return nil
|
||||
}
|
||||
return d.dict[n][name]
|
||||
}
|
||||
|
||||
// findExternal finds the externally-defined typedef name in a module imported
|
||||
// by n's root with the specified prefix.
|
||||
func (d *typeDictionary) findExternal(n Node, prefix, name string) (*Typedef, error) {
|
||||
root := FindModuleByPrefix(n, prefix)
|
||||
if root == nil {
|
||||
return nil, fmt.Errorf("%s: unknown prefix: %s for type %s", Source(n), prefix, name)
|
||||
}
|
||||
if td := d.find(root, name); td != nil {
|
||||
return td, nil
|
||||
}
|
||||
if prefix != "" {
|
||||
name = prefix + ":" + name
|
||||
}
|
||||
return nil, fmt.Errorf("%s: unknown type %s", Source(n), name)
|
||||
}
|
||||
|
||||
// typedefs returns a slice of all typedefs in d.
|
||||
func (d *typeDictionary) typedefs() []*Typedef {
|
||||
var tds []*Typedef
|
||||
defer d.mu.Unlock()
|
||||
d.mu.Lock()
|
||||
for _, dict := range d.dict {
|
||||
for _, td := range dict {
|
||||
tds = append(tds, td)
|
||||
}
|
||||
}
|
||||
return tds
|
||||
}
|
||||
|
||||
// addTypedefs is called from BuildAST after each Typedefer is defined. There
|
||||
// are no error conditions in this process as it is simply used to build up the
|
||||
// typedef dictionary.
|
||||
func (d *typeDictionary) addTypedefs(t Typedefer) {
|
||||
for _, td := range t.Typedefs() {
|
||||
d.add(t, td.Name, td)
|
||||
}
|
||||
}
|
||||
|
||||
// resolveTypedefs is called after all of modules and submodules have been read,
|
||||
// as well as their imports and includes. It resolves all typedefs found in all
|
||||
// modules and submodules read in.
|
||||
func (d *typeDictionary) resolveTypedefs() []error {
|
||||
var errs []error
|
||||
|
||||
// When resolve typedefs, we may need to look up other typedefs.
|
||||
// We gather all typedefs into a slice so we don't deadlock on
|
||||
// typeDict.
|
||||
for _, td := range d.typedefs() {
|
||||
errs = append(errs, td.resolve(d)...)
|
||||
}
|
||||
return errs
|
||||
}
|
||||
|
||||
// resolve creates a YangType for t, if not already done. Resolving t
|
||||
// requires resolving the Type that t is based on.
|
||||
func (t *Typedef) resolve(d *typeDictionary) []error {
|
||||
// If we have no parent we are a base type and
|
||||
// are already resolved.
|
||||
if t.Parent == nil || t.YangType != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if errs := t.Type.resolve(d); len(errs) != 0 {
|
||||
return errs
|
||||
}
|
||||
|
||||
// Make a copy of the YangType we are based on and then
|
||||
// update it with local information.
|
||||
y := *t.Type.YangType
|
||||
y.Name = t.Name
|
||||
y.Base = t.Type
|
||||
|
||||
if t.Units != nil {
|
||||
y.Units = t.Units.Name
|
||||
}
|
||||
if t.Default != nil {
|
||||
y.HasDefault = true
|
||||
y.Default = t.Default.Name
|
||||
}
|
||||
|
||||
if t.Type.IdentityBase != nil {
|
||||
// We need to copy over the IdentityBase statement if the type has one
|
||||
if idBase, err := RootNode(t).findIdentityBase(t.Type.IdentityBase.Name); err == nil {
|
||||
y.IdentityBase = idBase.Identity
|
||||
} else {
|
||||
return []error{fmt.Errorf("could not resolve identity base for typedef: %s", t.Type.IdentityBase.Name)}
|
||||
}
|
||||
}
|
||||
|
||||
// If we changed something, we are the new root.
|
||||
if y.Root == t.Type.YangType || !y.Equal(y.Root) {
|
||||
y.Root = &y
|
||||
}
|
||||
t.YangType = &y
|
||||
return nil
|
||||
}
|
||||
|
||||
// resolve resolves Type t, as well as the underlying typedef for t. If t
|
||||
// cannot be resolved then one or more errors are returned.
|
||||
func (t *Type) resolve(d *typeDictionary) (errs []error) {
|
||||
if t.YangType != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// If t.Name is a base type then td will not be nil, otherwise
|
||||
// td will be nil and of type *Typedef.
|
||||
td := BaseTypedefs[t.Name]
|
||||
|
||||
prefix, name := getPrefix(t.Name)
|
||||
root := RootNode(t)
|
||||
rootPrefix := root.GetPrefix()
|
||||
|
||||
source := "unknown"
|
||||
check:
|
||||
switch {
|
||||
case td != nil:
|
||||
source = "builtin"
|
||||
// This was a base type
|
||||
case prefix == "" || rootPrefix == prefix:
|
||||
source = "local"
|
||||
// If we have no prefix, or the prefix is what we call our own
|
||||
// root, then we look in our ancestors for a typedef of name.
|
||||
for n := Node(t); n != nil; n = n.ParentNode() {
|
||||
if td = d.find(n, name); td != nil {
|
||||
break check
|
||||
}
|
||||
}
|
||||
// We need to check our sub-modules as well
|
||||
for _, in := range root.Include {
|
||||
if td = d.find(in.Module, name); td != nil {
|
||||
break check
|
||||
}
|
||||
}
|
||||
var pname string
|
||||
switch {
|
||||
case prefix == "", prefix == root.Prefix.Name:
|
||||
pname = root.Prefix.Name + ":" + t.Name
|
||||
default:
|
||||
pname = fmt.Sprintf("%s[%s]:%s", prefix, root.Prefix.Name, t.Name)
|
||||
}
|
||||
|
||||
return []error{fmt.Errorf("%s: unknown type: %s", Source(t), pname)}
|
||||
|
||||
default:
|
||||
source = "imported"
|
||||
// prefix is not local to our module, so we have to go find
|
||||
// what module it is part of and if it is defined at the top
|
||||
// level of that module.
|
||||
var err error
|
||||
td, err = d.findExternal(t, prefix, name)
|
||||
if err != nil {
|
||||
return []error{err}
|
||||
}
|
||||
}
|
||||
if errs := td.resolve(d); len(errs) > 0 {
|
||||
return errs
|
||||
}
|
||||
|
||||
// Make a copy of the typedef we are based on so we can
|
||||
// augment it.
|
||||
if td.YangType == nil {
|
||||
return []error{fmt.Errorf("%s: no YangType defined for %s %s", Source(td), source, td.Name)}
|
||||
}
|
||||
y := *td.YangType
|
||||
|
||||
y.Base = td.Type
|
||||
t.YangType = &y
|
||||
|
||||
if v := t.RequireInstance; v != nil {
|
||||
b, err := v.asBool()
|
||||
if err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
y.OptionalInstance = !b
|
||||
}
|
||||
if v := t.Path; v != nil {
|
||||
y.Path = v.asString()
|
||||
}
|
||||
isDecimal64 := y.Kind == Ydecimal64 && (t.Name == "decimal64" || y.FractionDigits != 0)
|
||||
switch {
|
||||
case isDecimal64 && y.FractionDigits != 0:
|
||||
if t.FractionDigits != nil {
|
||||
return append(errs, fmt.Errorf("%s: overriding of fraction-digits not allowed", Source(t)))
|
||||
}
|
||||
// FractionDigits already set via type inheritance.
|
||||
case isDecimal64:
|
||||
// If we are directly of type decimal64 then we must specify
|
||||
// fraction-digits in the range from 1-18.
|
||||
i, err := t.FractionDigits.asRangeInt(1, 18)
|
||||
if err != nil {
|
||||
errs = append(errs, fmt.Errorf("%s: %v", Source(t), err))
|
||||
}
|
||||
y.FractionDigits = int(i)
|
||||
// We only know to how to populate Range after knowing the
|
||||
// fractional digit value.
|
||||
y.Range = YangRange{{
|
||||
Number{Value: AbsMinInt64, Negative: true, FractionDigits: uint8(i)},
|
||||
Number{Value: MaxInt64, FractionDigits: uint8(i)},
|
||||
}}
|
||||
case t.FractionDigits != nil:
|
||||
errs = append(errs, fmt.Errorf("%s: fraction-digits only allowed for decimal64 values", Source(t)))
|
||||
case y.Kind == Yidentityref:
|
||||
if source != "builtin" {
|
||||
// This is a typedef that refers to an identityref, so we want to simply
|
||||
// maintain the base that the typedef resolution provided
|
||||
break
|
||||
}
|
||||
|
||||
if t.IdentityBase == nil {
|
||||
errs = append(errs, fmt.Errorf("%s: an identityref must specify a base", Source(t)))
|
||||
break
|
||||
}
|
||||
|
||||
root := RootNode(t.Parent)
|
||||
resolvedBase, baseErr := root.findIdentityBase(t.IdentityBase.Name)
|
||||
if baseErr != nil {
|
||||
errs = append(errs, baseErr...)
|
||||
break
|
||||
}
|
||||
|
||||
if resolvedBase.Identity == nil {
|
||||
errs = append(errs, fmt.Errorf("%s: identity has a null base", t.IdentityBase.Name))
|
||||
break
|
||||
}
|
||||
y.IdentityBase = resolvedBase.Identity
|
||||
}
|
||||
|
||||
if t.Range != nil {
|
||||
yr, err := y.Range.parseChildRanges(t.Range.Name, isDecimal64, uint8(y.FractionDigits))
|
||||
switch {
|
||||
case err != nil:
|
||||
errs = append(errs, fmt.Errorf("%s: bad range: %v", Source(t.Range), err))
|
||||
case yr.Equal(y.Range):
|
||||
default:
|
||||
y.Range = yr
|
||||
}
|
||||
}
|
||||
|
||||
if t.Length != nil {
|
||||
parentRange := Uint64Range
|
||||
if y.Length != nil {
|
||||
parentRange = y.Length
|
||||
}
|
||||
yr, err := parentRange.parseChildRanges(t.Length.Name, false, 0)
|
||||
switch {
|
||||
case err != nil:
|
||||
errs = append(errs, fmt.Errorf("%s: bad length: %v", Source(t.Length), err))
|
||||
case yr.Equal(y.Length):
|
||||
default:
|
||||
for _, r := range yr {
|
||||
if r.Min.Negative {
|
||||
errs = append(errs, fmt.Errorf("%s: negative length: %v", Source(t.Length), yr))
|
||||
break
|
||||
}
|
||||
}
|
||||
y.Length = yr
|
||||
}
|
||||
}
|
||||
|
||||
set := func(e *EnumType, name string, value *Value) error {
|
||||
if value == nil {
|
||||
return e.SetNext(name)
|
||||
}
|
||||
n, err := ParseInt(value.Name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
i, err := n.Int()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return e.Set(name, i)
|
||||
}
|
||||
|
||||
if len(t.Enum) > 0 {
|
||||
enum := NewEnumType()
|
||||
for _, e := range t.Enum {
|
||||
if err := set(enum, e.Name, e.Value); err != nil {
|
||||
errs = append(errs, fmt.Errorf("%s: %v", Source(e), err))
|
||||
}
|
||||
}
|
||||
y.Enum = enum
|
||||
}
|
||||
|
||||
if len(t.Bit) > 0 {
|
||||
bit := NewBitfield()
|
||||
for _, e := range t.Bit {
|
||||
if err := set(bit, e.Name, e.Position); err != nil {
|
||||
errs = append(errs, fmt.Errorf("%s: %v", Source(e), err))
|
||||
}
|
||||
}
|
||||
y.Bit = bit
|
||||
}
|
||||
|
||||
// Append any newly found patterns to the end of the list of patterns.
|
||||
// Patterns are ANDed according to section 9.4.6. If all the patterns
|
||||
// declared by t were also declared by the type t is based on, then
|
||||
// no patterns are added.
|
||||
seenPatterns := map[string]bool{}
|
||||
for _, p := range y.Pattern {
|
||||
seenPatterns[p] = true
|
||||
}
|
||||
seenPOSIXPatterns := map[string]bool{}
|
||||
for _, p := range y.POSIXPattern {
|
||||
seenPOSIXPatterns[p] = true
|
||||
}
|
||||
|
||||
// First parse out the pattern statements.
|
||||
// These patterns are not checked because there is no support for W3C regexes by Go.
|
||||
for _, pv := range t.Pattern {
|
||||
if !seenPatterns[pv.Name] {
|
||||
seenPatterns[pv.Name] = true
|
||||
y.Pattern = append(y.Pattern, pv.Name)
|
||||
}
|
||||
}
|
||||
|
||||
// Then, parse out the posix-pattern statements, if they exist.
|
||||
// A YANG module could make use of either or both, so we deal with each separately.
|
||||
posixPatterns, err := MatchingExtensions(t, "openconfig-extensions", "posix-pattern")
|
||||
if err != nil {
|
||||
return []error{err}
|
||||
}
|
||||
|
||||
checkPattern := func(n Node, p string, flags syntax.Flags) {
|
||||
if _, err := syntax.Parse(p, flags); err != nil {
|
||||
if re, ok := err.(*syntax.Error); ok {
|
||||
// Error adds "error parsing regexp" to
|
||||
// the error, re.Code is the real error.
|
||||
err = errors.New(re.Code.String())
|
||||
}
|
||||
errs = append(errs, fmt.Errorf("%s: bad pattern: %v: %s", Source(n), err, p))
|
||||
}
|
||||
}
|
||||
for _, ext := range posixPatterns {
|
||||
checkPattern(ext, ext.Argument, syntax.POSIX)
|
||||
if !seenPOSIXPatterns[ext.Argument] {
|
||||
seenPOSIXPatterns[ext.Argument] = true
|
||||
y.POSIXPattern = append(y.POSIXPattern, ext.Argument)
|
||||
}
|
||||
}
|
||||
|
||||
// I don't know of an easy way to use a type as a key to a map,
|
||||
// so we have to check equality the hard way.
|
||||
looking:
|
||||
for _, ut := range t.Type {
|
||||
errs = append(errs, ut.resolve(d)...)
|
||||
if ut.YangType != nil {
|
||||
for _, yt := range y.Type {
|
||||
if ut.YangType.Equal(yt) {
|
||||
continue looking
|
||||
}
|
||||
}
|
||||
y.Type = append(y.Type, ut.YangType)
|
||||
}
|
||||
}
|
||||
|
||||
// If we changed something, we are the new root.
|
||||
if !y.Equal(y.Root) {
|
||||
y.Root = &y
|
||||
}
|
||||
|
||||
return errs
|
||||
}
|
||||
@@ -0,0 +1,700 @@
|
||||
// Copyright 2015 Google Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package yang
|
||||
|
||||
// This module contains all the builtin types as well as types related
|
||||
// to types (such as ranges, enums, etc).
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// This file handles interpretation of types
|
||||
|
||||
// These are the default ranges defined by the YANG standard.
|
||||
var (
|
||||
Int8Range = mustParseRangesInt("-128..127")
|
||||
Int16Range = mustParseRangesInt("-32768..32767")
|
||||
Int32Range = mustParseRangesInt("-2147483648..2147483647")
|
||||
Int64Range = mustParseRangesInt("-9223372036854775808..9223372036854775807")
|
||||
|
||||
Uint8Range = mustParseRangesInt("0..255")
|
||||
Uint16Range = mustParseRangesInt("0..65535")
|
||||
Uint32Range = mustParseRangesInt("0..4294967295")
|
||||
Uint64Range = mustParseRangesInt("0..18446744073709551615")
|
||||
)
|
||||
|
||||
const (
|
||||
// MaxInt64 corresponds to the maximum value of a signed int64.
|
||||
MaxInt64 = 1<<63 - 1
|
||||
// MinInt64 corresponds to the maximum value of a signed int64.
|
||||
MinInt64 = -1 << 63
|
||||
// Min/MaxDecimal64 are the max/min decimal64 values.
|
||||
MinDecimal64 float64 = -922337203685477580.8
|
||||
MaxDecimal64 float64 = 922337203685477580.7
|
||||
// AbsMinInt64 is the absolute value of MinInt64.
|
||||
AbsMinInt64 = 1 << 63
|
||||
// MaxEnum is the maximum value of an enumeration.
|
||||
MaxEnum = 1<<31 - 1
|
||||
// MinEnum is the minimum value of an enumeration.
|
||||
MinEnum = -1 << 31
|
||||
// MaxBitfieldSize is the maximum number of bits in a bitfield.
|
||||
MaxBitfieldSize = 1 << 32
|
||||
// MaxFractionDigits is the maximum number of fractional digits as per RFC6020 Section 9.3.4.
|
||||
MaxFractionDigits uint8 = 18
|
||||
|
||||
space18 = "000000000000000000" // used for prepending 0's
|
||||
)
|
||||
|
||||
// A Number is either an integer the range of [-(1<<64) - 1, (1<<64)-1], or a
|
||||
// YANG decimal conforming to https://tools.ietf.org/html/rfc6020#section-9.3.4.
|
||||
type Number struct {
|
||||
// Absolute value of the number.
|
||||
Value uint64
|
||||
// Number of fractional digits.
|
||||
// 0 means it's an integer. For decimal64 it falls within [1, 18].
|
||||
FractionDigits uint8
|
||||
// Negative indicates whether the number is negative.
|
||||
Negative bool
|
||||
}
|
||||
|
||||
// IsDecimal reports whether n is a decimal number.
|
||||
func (n Number) IsDecimal() bool {
|
||||
return n.FractionDigits != 0
|
||||
}
|
||||
|
||||
// String returns n as a string in decimal.
|
||||
func (n Number) String() string {
|
||||
out := strconv.FormatUint(n.Value, 10)
|
||||
|
||||
if n.IsDecimal() {
|
||||
if fd := int(n.FractionDigits); fd > 0 {
|
||||
ofd := len(out) - fd
|
||||
if ofd <= 0 {
|
||||
// We want 0.1 not .1
|
||||
out = space18[:-ofd+1] + out
|
||||
ofd = 1
|
||||
}
|
||||
out = out[:ofd] + "." + out[ofd:]
|
||||
}
|
||||
}
|
||||
if n.Negative {
|
||||
out = "-" + out
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
// Int returns n as an int64. It returns an error if n overflows an int64 or
|
||||
// the number is decimal.
|
||||
func (n Number) Int() (int64, error) {
|
||||
if n.IsDecimal() {
|
||||
return 0, errors.New("called Int() on decimal64 value")
|
||||
}
|
||||
if n.Negative {
|
||||
return -int64(n.Value), nil
|
||||
}
|
||||
if n.Value <= MaxInt64 {
|
||||
return int64(n.Value), nil
|
||||
}
|
||||
return 0, errors.New("signed integer overflow")
|
||||
}
|
||||
|
||||
// addQuantum adds the smallest quantum to n without checking overflow.
|
||||
func (n Number) addQuantum(i uint64) Number {
|
||||
switch n.Negative {
|
||||
case true:
|
||||
if n.Value <= i {
|
||||
n.Value = i - n.Value
|
||||
n.Negative = false
|
||||
} else {
|
||||
n.Value -= i
|
||||
}
|
||||
case false:
|
||||
n.Value += i
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
// Less returns true if n is less than m. Panics if n and m are a mix of integer
|
||||
// and decimal.
|
||||
func (n Number) Less(m Number) bool {
|
||||
switch {
|
||||
case n.Negative && !m.Negative:
|
||||
return true
|
||||
case !n.Negative && m.Negative:
|
||||
return false
|
||||
}
|
||||
|
||||
nt, mt := n.Trunc(), m.Trunc()
|
||||
lt := nt < mt
|
||||
if nt == mt {
|
||||
nf, mf := n.frac(), m.frac()
|
||||
if nf == mf {
|
||||
return false
|
||||
}
|
||||
lt = nf < mf
|
||||
}
|
||||
|
||||
if n.Negative {
|
||||
return !lt
|
||||
}
|
||||
return lt
|
||||
}
|
||||
|
||||
// Equal returns true if n is equal to m.
|
||||
func (n Number) Equal(m Number) bool {
|
||||
return !n.Less(m) && !m.Less(n)
|
||||
}
|
||||
|
||||
// Trunc returns the whole part of abs(n) as a signed integer.
|
||||
func (n Number) Trunc() uint64 {
|
||||
nv := n.Value
|
||||
e := pow10(n.FractionDigits)
|
||||
return nv / e
|
||||
}
|
||||
|
||||
// frac returns the fraction part with a precision of 18 fractional digits.
|
||||
// E.g. if n is 3.1 then n.frac() returns 100,000,000,000,000,000
|
||||
func (n Number) frac() uint64 {
|
||||
frac := n.FractionDigits
|
||||
i := n.Trunc() * pow10(frac)
|
||||
return (n.Value - i) * pow10(uint8(18-frac))
|
||||
}
|
||||
|
||||
// YRange is a single range of consecutive numbers, inclusive.
|
||||
type YRange struct {
|
||||
Min Number
|
||||
Max Number
|
||||
}
|
||||
|
||||
// Valid returns false if r is not a valid range (min > max).
|
||||
func (r YRange) Valid() bool {
|
||||
return !r.Max.Less(r.Min)
|
||||
}
|
||||
|
||||
// String returns r as a string using YANG notation, either a simple
|
||||
// value if min == max or min..max.
|
||||
func (r YRange) String() string {
|
||||
if r.Min.Equal(r.Max) {
|
||||
return r.Min.String()
|
||||
}
|
||||
return r.Min.String() + ".." + r.Max.String()
|
||||
}
|
||||
|
||||
// Equal compares whether two YRanges are equal.
|
||||
func (r YRange) Equal(s YRange) bool {
|
||||
return r.Min.Equal(s.Min) && r.Max.Equal(s.Max)
|
||||
}
|
||||
|
||||
// A YangRange is a set of non-overlapping ranges.
|
||||
type YangRange []YRange
|
||||
|
||||
// String returns the ranges r using YANG notation. Individual ranges
|
||||
// are separated by pipes (|).
|
||||
func (r YangRange) String() string {
|
||||
s := make([]string, len(r))
|
||||
for i, r := range r {
|
||||
s[i] = r.String()
|
||||
}
|
||||
return strings.Join(s, "|")
|
||||
}
|
||||
|
||||
func (r YangRange) Len() int { return len(r) }
|
||||
func (r YangRange) Swap(i, j int) { r[i], r[j] = r[j], r[i] }
|
||||
func (r YangRange) Less(i, j int) bool {
|
||||
switch {
|
||||
case r[i].Min.Less(r[j].Min):
|
||||
return true
|
||||
case r[j].Min.Less(r[i].Min):
|
||||
return false
|
||||
default:
|
||||
return r[i].Max.Less(r[j].Max)
|
||||
}
|
||||
}
|
||||
|
||||
// Validate returns an error if r has either an invalid range or has
|
||||
// overlapping ranges.
|
||||
// r is expected to be sorted use YangRange.Sort()
|
||||
func (r YangRange) Validate() error {
|
||||
if !sort.IsSorted(r) {
|
||||
return errors.New("range not sorted")
|
||||
}
|
||||
switch {
|
||||
case len(r) == 0:
|
||||
return nil
|
||||
case !r[0].Valid():
|
||||
return errors.New("invalid number")
|
||||
}
|
||||
p := r[0]
|
||||
|
||||
for _, n := range r[1:] {
|
||||
if n.Min.Less(p.Max) {
|
||||
return errors.New("overlapping ranges")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Sort r. Must be called before Validate and coalesce if unsorted
|
||||
func (r YangRange) Sort() {
|
||||
sort.Sort(r)
|
||||
}
|
||||
|
||||
// Equal returns true if ranges r and q are identically equivalent.
|
||||
// TODO(borman): should we coalesce ranges in the comparison?
|
||||
func (r YangRange) Equal(q YangRange) bool {
|
||||
if len(r) != len(q) {
|
||||
return false
|
||||
}
|
||||
for i, r := range r {
|
||||
if !r.Equal(q[i]) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// Contains returns true if all possible values in s are also possible values
|
||||
// in r. An empty range is assumed to be min..max when it is the receiver
|
||||
// argument.
|
||||
func (r YangRange) Contains(s YangRange) bool {
|
||||
if len(r) == 0 || len(s) == 0 {
|
||||
return true
|
||||
}
|
||||
|
||||
// Check if every range in s is subsumed under r.
|
||||
// Both range lists should be in order and non-adjacent (coalesced).
|
||||
ri := 0
|
||||
for _, ss := range s {
|
||||
for r[ri].Max.Less(ss.Min) {
|
||||
ri++
|
||||
if ri == len(r) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
if ss.Min.Less(r[ri].Min) || r[ri].Max.Less(ss.Max) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// FromInt creates a Number from an int64.
|
||||
func FromInt(i int64) Number {
|
||||
if i < 0 {
|
||||
return Number{Negative: true, Value: uint64(-i)}
|
||||
}
|
||||
return Number{Value: uint64(i)}
|
||||
}
|
||||
|
||||
// FromUint creates a Number from a uint64.
|
||||
func FromUint(i uint64) Number {
|
||||
return Number{Value: i}
|
||||
}
|
||||
|
||||
// FromFloat creates a Number from a float64. Input values with absolute value
|
||||
// outside the boundaries specified for the decimal64 value specified in
|
||||
// RFC6020/RFC7950 are clamped down to the closest boundary value.
|
||||
func FromFloat(f float64) Number {
|
||||
if f > MaxDecimal64 {
|
||||
return Number{
|
||||
Value: FromInt(MaxInt64).Value,
|
||||
FractionDigits: 1,
|
||||
}
|
||||
}
|
||||
if f < MinDecimal64 {
|
||||
return Number{
|
||||
Negative: true,
|
||||
Value: FromInt(MaxInt64).Value,
|
||||
FractionDigits: 1,
|
||||
}
|
||||
}
|
||||
|
||||
// Per RFC7950/6020, fraction-digits must be at least 1.
|
||||
fracDig := uint8(1)
|
||||
f *= 10.0
|
||||
for ; Frac(f) != 0.0 && fracDig <= MaxFractionDigits; fracDig++ {
|
||||
f *= 10.0
|
||||
}
|
||||
negative := false
|
||||
if f < 0 {
|
||||
negative = true
|
||||
f = -f
|
||||
}
|
||||
v := uint64(f)
|
||||
|
||||
return Number{Negative: negative, Value: v, FractionDigits: fracDig}
|
||||
}
|
||||
|
||||
// ParseInt returns s as a Number with FractionDigits=0.
|
||||
// octal, or hexadecimal using the standard prefix notations (e.g., 0 and 0x)
|
||||
func ParseInt(s string) (Number, error) {
|
||||
s = strings.TrimSpace(s)
|
||||
var n Number
|
||||
switch s {
|
||||
case "":
|
||||
return n, errors.New("converting empty string to number")
|
||||
case "+", "-":
|
||||
return n, errors.New("sign with no value")
|
||||
}
|
||||
|
||||
ns := s
|
||||
switch s[0] {
|
||||
case '+':
|
||||
ns = s[1:]
|
||||
case '-':
|
||||
n.Negative = true
|
||||
ns = s[1:]
|
||||
}
|
||||
|
||||
var err error
|
||||
n.Value, err = strconv.ParseUint(ns, 0, 64)
|
||||
return n, err
|
||||
}
|
||||
|
||||
// ParseDecimal returns s as a Number with a non-zero FractionDigits.
|
||||
// octal, or hexadecimal using the standard prefix notations (e.g., 0 and 0x)
|
||||
func ParseDecimal(s string, fracDigRequired uint8) (n Number, err error) {
|
||||
s = strings.TrimSpace(s)
|
||||
switch s {
|
||||
case "":
|
||||
return n, errors.New("converting empty string to number")
|
||||
case "+", "-":
|
||||
return n, errors.New("sign with no value")
|
||||
}
|
||||
|
||||
return decimalValueFromString(s, fracDigRequired)
|
||||
}
|
||||
|
||||
// decimalValueFromString returns a decimal Number representation of numStr.
|
||||
// fracDigRequired is used to set the number of fractional digits, which must
|
||||
// be at least the greatest precision seen in numStr.
|
||||
// which must be between 1 and 18.
|
||||
// numStr must conform to Section 9.3.4.
|
||||
func decimalValueFromString(numStr string, fracDigRequired uint8) (n Number, err error) {
|
||||
if fracDigRequired > MaxFractionDigits || fracDigRequired < 1 {
|
||||
return n, fmt.Errorf("invalid number of fraction digits %d > max of %d, minimum 1", fracDigRequired, MaxFractionDigits)
|
||||
}
|
||||
|
||||
s := numStr
|
||||
dx := strings.Index(s, ".")
|
||||
var fracDig uint8
|
||||
if dx >= 0 {
|
||||
fracDig = uint8(len(s) - 1 - dx)
|
||||
// remove first decimal, if dx > 1, will fail ParseInt below
|
||||
s = s[:dx] + s[dx+1:]
|
||||
}
|
||||
|
||||
if fracDig > fracDigRequired {
|
||||
return n, fmt.Errorf("%s has too much precision, expect <= %d fractional digits", s, fracDigRequired)
|
||||
}
|
||||
|
||||
s += space18[:fracDigRequired-fracDig]
|
||||
|
||||
v, err := strconv.ParseInt(s, 10, 64)
|
||||
if err != nil {
|
||||
return n, fmt.Errorf("%s is not a valid decimal number: %s", numStr, err)
|
||||
}
|
||||
|
||||
negative := false
|
||||
if v < 0 {
|
||||
negative = true
|
||||
v = -v
|
||||
}
|
||||
|
||||
return Number{Value: uint64(v), FractionDigits: fracDigRequired, Negative: negative}, nil
|
||||
}
|
||||
|
||||
// ParseRangesInt parses s into a series of ranges. Each individual range is in s
|
||||
// is separated by the pipe character (|). The min and max value of a range
|
||||
// are separated by "..". An error is returned if the range is invalid. The
|
||||
// output range is sorted and coalesced.
|
||||
func ParseRangesInt(s string) (YangRange, error) {
|
||||
return YangRange{}.parseChildRanges(s, false, 0)
|
||||
}
|
||||
|
||||
// ParseRangesDecimal parses s into a series of ranges. Each individual range is in s
|
||||
// is separated by the pipe character (|). The min and max value of a range
|
||||
// are separated by "..". An error is returned if the range is invalid. The
|
||||
// output range is sorted and coalesced.
|
||||
func ParseRangesDecimal(s string, fracDigRequired uint8) (YangRange, error) {
|
||||
return YangRange{}.parseChildRanges(s, true, fracDigRequired)
|
||||
}
|
||||
|
||||
// parseChildRanges parses a child ranges statement 's' into a series of ranges
|
||||
// based on an already-parsed parent YangRange. Each individual range is in s
|
||||
// is separated by the pipe character (|). The min and max value of a range are
|
||||
// separated by "..". An error is returned if the child ranges are not
|
||||
// equally-limiting or more limiting than the parent range
|
||||
// (rfc7950#section-9.2.5). The output range is sorted and coalesced.
|
||||
// fracDigRequired is ignored when decimal=false.
|
||||
func (y YangRange) parseChildRanges(s string, decimal bool, fracDigRequired uint8) (YangRange, error) {
|
||||
parseNumber := func(s string) (Number, error) {
|
||||
switch {
|
||||
case s == "max":
|
||||
if len(y) == 0 {
|
||||
return Number{}, errors.New("cannot resolve 'max' keyword using an empty YangRange parent object")
|
||||
}
|
||||
max := y[len(y)-1].Max
|
||||
max.FractionDigits = fracDigRequired
|
||||
return max, nil
|
||||
case s == "min":
|
||||
if len(y) == 0 {
|
||||
return Number{}, errors.New("cannot resolve 'min' keyword using an empty YangRange parent object")
|
||||
}
|
||||
min := y[0].Min
|
||||
min.FractionDigits = fracDigRequired
|
||||
return min, nil
|
||||
case decimal:
|
||||
return ParseDecimal(s, fracDigRequired)
|
||||
default:
|
||||
return ParseInt(s)
|
||||
}
|
||||
}
|
||||
|
||||
parts := strings.Split(s, "|")
|
||||
r := make(YangRange, len(parts))
|
||||
for i, s := range parts {
|
||||
parts := strings.Split(s, "..")
|
||||
min, err := parseNumber(strings.TrimSpace(parts[0]))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var max Number
|
||||
switch len(parts) {
|
||||
case 1:
|
||||
max = min
|
||||
case 2:
|
||||
if max, err = parseNumber(strings.TrimSpace(parts[1])); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
default:
|
||||
return nil, fmt.Errorf("too many '..' in %s", s)
|
||||
}
|
||||
if max.Less(min) {
|
||||
return nil, fmt.Errorf("range boundaries out of order (%s less than %s): %s", max, min, s)
|
||||
}
|
||||
r[i] = YRange{min, max}
|
||||
}
|
||||
r.Sort()
|
||||
r = coalesce(r)
|
||||
|
||||
if !y.Contains(r) {
|
||||
return nil, fmt.Errorf("%v not within %v", s, y)
|
||||
}
|
||||
|
||||
if err := r.Validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return r, nil
|
||||
}
|
||||
|
||||
// coalesce coalesces r into as few ranges as possible. For example,
|
||||
// 1..5|6..10 would become 1..10. r is assumed to be sorted.
|
||||
func coalesce(r YangRange) YangRange {
|
||||
// coalesce the ranges if we have more than 1.
|
||||
if len(r) < 2 {
|
||||
return r
|
||||
}
|
||||
cr := make(YangRange, len(r))
|
||||
i := 0
|
||||
cr[i] = r[0]
|
||||
for _, r1 := range r[1:] {
|
||||
// r1.Min is always at least as large as cr[i].Min
|
||||
// Cases are:
|
||||
// r1 is contained in cr[i]
|
||||
// r1 starts inside of cr[i]
|
||||
// r1.Min cr[i].Max+1
|
||||
// r1 is beyond cr[i]
|
||||
if cr[i].Max.addQuantum(1).Less(r1.Min) {
|
||||
// r1 starts after cr[i], this is a new range
|
||||
i++
|
||||
cr[i] = r1
|
||||
} else if cr[i].Max.Less(r1.Max) {
|
||||
cr[i].Max = r1.Max
|
||||
}
|
||||
}
|
||||
return cr[:i+1]
|
||||
}
|
||||
|
||||
func mustParseRangesInt(s string) YangRange {
|
||||
r, err := ParseRangesInt(s)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
func mustParseRangesDecimal(s string, fracDigRequired uint8) YangRange {
|
||||
r, err := ParseRangesDecimal(s, fracDigRequired)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
// Frac returns the fractional part of f.
|
||||
func Frac(f float64) float64 {
|
||||
return f - math.Trunc(f)
|
||||
}
|
||||
|
||||
// pow10 returns 10^e without checking for overflow.
|
||||
func pow10(e uint8) uint64 {
|
||||
var out uint64 = 1
|
||||
for i := uint8(0); i < e; i++ {
|
||||
out *= 10
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// A EnumType represents a mapping of strings to integers. It is used both
|
||||
// for enumerations as well as bitfields.
|
||||
type EnumType struct {
|
||||
last int64 // maximum value assigned thus far
|
||||
min int64 // minimum value allowed
|
||||
max int64 // maximum value allowed
|
||||
unique bool // numeric values must be unique (enums)
|
||||
ToString map[int64]string `json:",omitempty"` // map of enum entries by value (integer)
|
||||
ToInt map[string]int64 `json:",omitempty"` // map of enum entries by name (string)
|
||||
}
|
||||
|
||||
// NewEnumType returns an initialized EnumType.
|
||||
func NewEnumType() *EnumType {
|
||||
return &EnumType{
|
||||
last: -1, // +1 will start at 0
|
||||
min: MinEnum,
|
||||
max: MaxEnum,
|
||||
unique: true,
|
||||
ToString: map[int64]string{},
|
||||
ToInt: map[string]int64{},
|
||||
}
|
||||
}
|
||||
|
||||
// NewBitfield returns an EnumType initialized as a bitfield. Multiple string
|
||||
// values may map to the same numeric values. Numeric values must be small
|
||||
// non-negative integers.
|
||||
func NewBitfield() *EnumType {
|
||||
return &EnumType{
|
||||
last: -1, // +1 will start at 0
|
||||
min: 0,
|
||||
max: MaxBitfieldSize - 1,
|
||||
ToString: map[int64]string{},
|
||||
ToInt: map[string]int64{},
|
||||
}
|
||||
}
|
||||
|
||||
// Set sets name in e to the provided value. Set returns an error if the value
|
||||
// is invalid, name is already signed, or when used as an enum rather than a
|
||||
// bitfield, the value has previousl been used. When two different names are
|
||||
// assigned to the same value, the conversion from value to name will result in
|
||||
// the most recently assigned name.
|
||||
func (e *EnumType) Set(name string, value int64) error {
|
||||
if _, ok := e.ToInt[name]; ok {
|
||||
return fmt.Errorf("field %s already assigned", name)
|
||||
}
|
||||
if oname, ok := e.ToString[value]; e.unique && ok {
|
||||
return fmt.Errorf("fields %s and %s conflict on value %d", name, oname, value)
|
||||
}
|
||||
if value < e.min {
|
||||
return fmt.Errorf("value %d for %s too small (minimum is %d)", value, name, e.min)
|
||||
}
|
||||
if value > e.max {
|
||||
return fmt.Errorf("value %d for %s too large (maximum is %d)", value, name, e.max)
|
||||
}
|
||||
e.ToString[value] = name
|
||||
e.ToInt[name] = value
|
||||
if value >= e.last {
|
||||
e.last = value
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetNext sets the name in e using the next possible value that is greater than
|
||||
// all previous values.
|
||||
func (e *EnumType) SetNext(name string) error {
|
||||
if e.last == MaxEnum {
|
||||
return fmt.Errorf("enum %q must specify a value since previous enum is the maximum value allowed", name)
|
||||
}
|
||||
return e.Set(name, e.last+1)
|
||||
}
|
||||
|
||||
// Name returns the name in e associated with value. The empty string is
|
||||
// returned if no name has been assigned to value.
|
||||
func (e *EnumType) Name(value int64) string { return e.ToString[value] }
|
||||
|
||||
// Value returns the value associated with name in e associated. 0 is returned
|
||||
// if name is not in e, or if it is the first value in an unnumbered enum. Use
|
||||
// IsDefined to definitively confirm name is in e.
|
||||
func (e *EnumType) Value(name string) int64 { return e.ToInt[name] }
|
||||
|
||||
// IsDefined returns true if name is defined in e, else false.
|
||||
func (e *EnumType) IsDefined(name string) bool {
|
||||
_, defined := e.ToInt[name]
|
||||
return defined
|
||||
}
|
||||
|
||||
// Names returns the sorted list of enum string names.
|
||||
func (e *EnumType) Names() []string {
|
||||
names := make([]string, len(e.ToInt))
|
||||
i := 0
|
||||
for name := range e.ToInt {
|
||||
names[i] = name
|
||||
i++
|
||||
}
|
||||
sort.Strings(names)
|
||||
return names
|
||||
}
|
||||
|
||||
type int64Slice []int64
|
||||
|
||||
func (p int64Slice) Len() int { return len(p) }
|
||||
func (p int64Slice) Less(i, j int) bool { return p[i] < p[j] }
|
||||
func (p int64Slice) Swap(i, j int) { p[i], p[j] = p[j], p[i] }
|
||||
|
||||
// Values returns the sorted list of enum values.
|
||||
func (e *EnumType) Values() []int64 {
|
||||
values := make([]int64, len(e.ToInt))
|
||||
i := 0
|
||||
for _, value := range e.ToInt {
|
||||
values[i] = value
|
||||
i++
|
||||
}
|
||||
sort.Sort(int64Slice(values))
|
||||
return values
|
||||
}
|
||||
|
||||
// NameMap returns a map of names to values.
|
||||
func (e *EnumType) NameMap() map[string]int64 {
|
||||
m := make(map[string]int64, len(e.ToInt))
|
||||
for name, value := range e.ToInt {
|
||||
m[name] = value
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
// ValueMap returns a map of values to names.
|
||||
func (e *EnumType) ValueMap() map[int64]string {
|
||||
m := make(map[int64]string, len(e.ToString))
|
||||
for name, value := range e.ToString {
|
||||
m[name] = value
|
||||
}
|
||||
return m
|
||||
}
|
||||
@@ -0,0 +1,915 @@
|
||||
// Copyright 2015 Google Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package yang
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/openconfig/gnmi/errdiff"
|
||||
)
|
||||
|
||||
const (
|
||||
maxUint64 uint64 = 18446744073709551615
|
||||
maxUint32 = 0xFFFFFFFF
|
||||
maxUint16 = 0xFFFF
|
||||
maxUint8 = 0xFF
|
||||
maxInt32 = 1<<31 - 1
|
||||
minInt32 = -1 << 31
|
||||
maxInt16 = 1<<15 - 1
|
||||
minInt16 = -1 << 15
|
||||
maxInt8 = 1<<7 - 1
|
||||
minInt8 = -1 << 7
|
||||
)
|
||||
|
||||
// R is a test helper for creating an int-based YRange.
|
||||
func R(a, b int64) YRange {
|
||||
return YRange{FromInt(a), FromInt(b)}
|
||||
}
|
||||
|
||||
// Rf is a test helper for creating a float-based YRange.
|
||||
func Rf(a, b int64, fracDig uint8) YRange {
|
||||
n1 := Number{Value: uint64(a), FractionDigits: fracDig}
|
||||
n2 := Number{Value: uint64(b), FractionDigits: fracDig}
|
||||
if a < 0 {
|
||||
n1.Value = uint64(-a)
|
||||
n1.Negative = true
|
||||
}
|
||||
if b < 0 {
|
||||
n2.Value = uint64(-b)
|
||||
n2.Negative = true
|
||||
}
|
||||
return YRange{n1, n2}
|
||||
}
|
||||
|
||||
func TestFromFloat(t *testing.T) {
|
||||
tests := []struct {
|
||||
desc string
|
||||
in float64
|
||||
want Number
|
||||
}{{
|
||||
desc: "positive - no decimals",
|
||||
in: 10.0,
|
||||
want: Number{
|
||||
Negative: false,
|
||||
Value: 10,
|
||||
FractionDigits: 0,
|
||||
},
|
||||
}, {
|
||||
desc: "positive - decimals",
|
||||
in: 10.15,
|
||||
want: Number{
|
||||
Negative: false,
|
||||
Value: 1015,
|
||||
FractionDigits: 2,
|
||||
},
|
||||
}, {
|
||||
desc: "negative - no decimals",
|
||||
in: -10.0,
|
||||
want: Number{
|
||||
Negative: true,
|
||||
Value: 10,
|
||||
FractionDigits: 0,
|
||||
},
|
||||
}, {
|
||||
desc: "negative - decimals",
|
||||
in: -10.15,
|
||||
want: Number{
|
||||
Negative: true,
|
||||
Value: 1015,
|
||||
FractionDigits: 2,
|
||||
},
|
||||
}}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.desc, func(t *testing.T) {
|
||||
if got := FromFloat(tt.in); !cmp.Equal(got, tt.want) {
|
||||
t.Fatalf("FromFloat(%v): did not get expected value, got: %+v, want: %+v", tt.in, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNumberInt(t *testing.T) {
|
||||
tests := []struct {
|
||||
desc string
|
||||
in Number
|
||||
want int64
|
||||
wantErr bool
|
||||
}{{
|
||||
desc: "zero",
|
||||
in: FromInt(0),
|
||||
want: 0,
|
||||
}, {
|
||||
desc: "positive",
|
||||
in: FromInt(42),
|
||||
want: 42,
|
||||
}, {
|
||||
desc: "negative",
|
||||
in: FromInt(-42),
|
||||
want: -42,
|
||||
}, {
|
||||
desc: "decimal",
|
||||
in: FromFloat(42),
|
||||
wantErr: true,
|
||||
}, {
|
||||
desc: "overflow",
|
||||
in: FromUint(maxUint64),
|
||||
wantErr: true,
|
||||
}}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.desc, func(t *testing.T) {
|
||||
got, err := tt.in.Int()
|
||||
if got != tt.want {
|
||||
t.Errorf("got: %v, want: %v", got, tt.want)
|
||||
}
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("gotErr: %v, wantErr: %v", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRangeEqual(t *testing.T) {
|
||||
tests := []struct {
|
||||
desc string
|
||||
inBaseRange YangRange
|
||||
inTestRange YangRange
|
||||
want bool
|
||||
}{{
|
||||
desc: "empty range equals empty range",
|
||||
want: true,
|
||||
}, {
|
||||
desc: "test range is default",
|
||||
inBaseRange: YangRange{R(1, 2)}, want: false,
|
||||
}, {
|
||||
desc: "base range is default",
|
||||
inTestRange: YangRange{R(1, 2)}, want: false,
|
||||
}, {
|
||||
desc: "equal ranges",
|
||||
inBaseRange: YangRange{R(1, 2)},
|
||||
inTestRange: YangRange{R(1, 2)},
|
||||
want: true,
|
||||
}, {
|
||||
desc: "wider base range",
|
||||
inBaseRange: YangRange{R(1, 3)},
|
||||
inTestRange: YangRange{R(1, 2)},
|
||||
want: false,
|
||||
}, {
|
||||
desc: "equal ranges with multiple subranges",
|
||||
inBaseRange: YangRange{R(1, 2), R(4, 5)},
|
||||
inTestRange: YangRange{R(1, 2), R(4, 5)},
|
||||
want: true,
|
||||
}, {
|
||||
desc: "multiple subranges with one unequal",
|
||||
inBaseRange: YangRange{R(1, 2), R(4, 6)},
|
||||
inTestRange: YangRange{R(1, 2), R(4, 5)},
|
||||
want: false,
|
||||
}, {
|
||||
desc: "extra subrange in base range",
|
||||
inBaseRange: YangRange{R(1, 2)},
|
||||
inTestRange: YangRange{R(1, 2), R(4, 5)},
|
||||
want: false,
|
||||
}, {
|
||||
desc: "extra subrange in test range",
|
||||
inBaseRange: YangRange{R(1, 2), R(4, 5)},
|
||||
inTestRange: YangRange{R(1, 2)},
|
||||
want: false,
|
||||
}}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.desc, func(t *testing.T) {
|
||||
if want := tt.inBaseRange.Equal(tt.inTestRange); want != tt.want {
|
||||
t.Errorf("got %v, want %v", want, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRangeContains(t *testing.T) {
|
||||
tests := []struct {
|
||||
desc string
|
||||
inBaseRange YangRange
|
||||
inTestRange YangRange
|
||||
want bool
|
||||
}{{
|
||||
desc: "empty range contained in empty range",
|
||||
want: true,
|
||||
}, {
|
||||
desc: "empty range contained in non-empty range",
|
||||
inBaseRange: YangRange{R(1, 2)},
|
||||
want: true,
|
||||
}, {
|
||||
desc: "non-empty range contained in empty range",
|
||||
inTestRange: YangRange{R(1, 2)},
|
||||
want: true,
|
||||
}, {
|
||||
desc: "equal ranges contain",
|
||||
inBaseRange: YangRange{R(1, 2)},
|
||||
inTestRange: YangRange{R(1, 2)},
|
||||
want: true,
|
||||
}, {
|
||||
desc: "superset contains",
|
||||
inBaseRange: YangRange{R(1, 5)},
|
||||
inTestRange: YangRange{R(2, 3)},
|
||||
want: true,
|
||||
}, {
|
||||
desc: "subset doesn't contain",
|
||||
inBaseRange: YangRange{R(2, 3)},
|
||||
inTestRange: YangRange{R(1, 5)},
|
||||
want: false,
|
||||
}, {
|
||||
desc: "contain subranges",
|
||||
inBaseRange: YangRange{R(1, 10)},
|
||||
inTestRange: YangRange{R(1, 2), R(4, 5), R(7, 10)},
|
||||
want: true,
|
||||
}, {
|
||||
desc: "subranges leaks out",
|
||||
inBaseRange: YangRange{R(1, 10)},
|
||||
inTestRange: YangRange{R(1, 2), R(7, 11)},
|
||||
want: false,
|
||||
}, {
|
||||
desc: "subranges containing a subset",
|
||||
inBaseRange: YangRange{R(1, 9), R(11, 19), R(21, 29)},
|
||||
inTestRange: YangRange{R(23, 25)},
|
||||
want: true,
|
||||
}, {
|
||||
desc: "subranges containing a single valued range",
|
||||
inBaseRange: YangRange{R(1, 9), R(11, 19), R(21, 29)},
|
||||
inTestRange: YangRange{R(23, 23)},
|
||||
want: true,
|
||||
}, {
|
||||
desc: "subranges doesn't contain a single outside value",
|
||||
inBaseRange: YangRange{R(1, 9), R(11, 19), R(21, 29)},
|
||||
inTestRange: YangRange{R(20, 20)},
|
||||
want: false,
|
||||
}, {
|
||||
desc: "smaller range doesn't contain min..max",
|
||||
inBaseRange: YangRange{R(1, 10)},
|
||||
inTestRange: YangRange{R(MinInt64, MaxInt64)},
|
||||
want: false,
|
||||
}, {
|
||||
desc: "full range contains any",
|
||||
inBaseRange: YangRange{R(MinInt64, MaxInt64)},
|
||||
inTestRange: YangRange{R(1, 10)},
|
||||
want: true,
|
||||
}, {
|
||||
desc: "smaller range doesn't contain min..a|b..max",
|
||||
inBaseRange: YangRange{R(1024, 65535)},
|
||||
inTestRange: YangRange{R(MinInt64, 4096), R(5120, MaxInt64)},
|
||||
want: false,
|
||||
}, {
|
||||
desc: "ranges don't overlap with max word used",
|
||||
inBaseRange: YangRange{R(1024, 65535)},
|
||||
inTestRange: YangRange{R(-999999, 4096), R(5120, MaxInt64)},
|
||||
want: false,
|
||||
}, {
|
||||
desc: "ranges don't overlap with min word used",
|
||||
inBaseRange: YangRange{R(1024, 65535)},
|
||||
inTestRange: YangRange{R(MinInt64, 4096), R(5120, 999999)},
|
||||
want: false,
|
||||
}}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.desc, func(t *testing.T) {
|
||||
if got := tt.inBaseRange.Contains(tt.inTestRange); got != tt.want {
|
||||
t.Errorf("got %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseRangesInt(t *testing.T) {
|
||||
tests := []struct {
|
||||
desc string
|
||||
inParentRange YangRange
|
||||
in string
|
||||
want YangRange
|
||||
wantErrSubstring string
|
||||
}{{
|
||||
desc: "small numbers, coalescing",
|
||||
in: "0|2..3|4..5",
|
||||
want: YangRange{R(0, 0), R(2, 5)},
|
||||
}, {
|
||||
desc: "small numbers, out of order, coalescing",
|
||||
in: "4..5|0|2..3",
|
||||
want: YangRange{R(0, 0), R(2, 5)},
|
||||
}, {
|
||||
desc: "invalid input: too many ..s",
|
||||
in: "0|2..3|4..5..6",
|
||||
wantErrSubstring: "too many '..' in 4..5..6",
|
||||
}, {
|
||||
desc: "invalid input: range boundaries out of order",
|
||||
in: "0|2..3|5..4",
|
||||
wantErrSubstring: "range boundaries out of order",
|
||||
}, {
|
||||
desc: "range with min",
|
||||
inParentRange: Int64Range,
|
||||
in: "min..0|2..3|4..5",
|
||||
want: YangRange{R(MinInt64, 0), R(2, 5)},
|
||||
}, {
|
||||
desc: "range with min but without parent range",
|
||||
in: "min..0|2..3|4..5",
|
||||
wantErrSubstring: "empty YangRange parent object",
|
||||
}, {
|
||||
desc: "range with max",
|
||||
inParentRange: Int32Range,
|
||||
in: "min..0|2..3|4..5|7..max",
|
||||
want: YangRange{R(minInt32, 0), R(2, 5), R(7, maxInt32)},
|
||||
}, {
|
||||
desc: "coalescing from min to max for uint64",
|
||||
inParentRange: Uint64Range,
|
||||
in: "min..0|1..max",
|
||||
want: YangRange{YRange{FromInt(0), FromUint(maxUint64)}},
|
||||
}, {
|
||||
desc: "coalescing from min to max for uint32",
|
||||
inParentRange: Uint32Range,
|
||||
in: "min..0|1..max",
|
||||
want: YangRange{R(0, maxUint32)},
|
||||
}, {
|
||||
desc: "coalescing from min to max for uint16",
|
||||
inParentRange: Uint16Range,
|
||||
in: "min..0|1..max",
|
||||
want: YangRange{R(0, maxUint16)},
|
||||
}, {
|
||||
desc: "coalescing from min to max for uint8",
|
||||
inParentRange: Uint8Range,
|
||||
in: "min..0|1..max",
|
||||
want: YangRange{R(0, maxUint8)},
|
||||
}, {
|
||||
desc: "coalescing from min to max for int64",
|
||||
inParentRange: Int64Range,
|
||||
in: "min..0|1..max",
|
||||
want: YangRange{R(MinInt64, MaxInt64)},
|
||||
}, {
|
||||
desc: "coalescing from min to max for int32",
|
||||
inParentRange: Int32Range,
|
||||
in: "min..0|1..max",
|
||||
want: YangRange{R(minInt32, maxInt32)},
|
||||
}, {
|
||||
desc: "coalescing from min to max for int16",
|
||||
inParentRange: Int16Range,
|
||||
in: "min..0|1..max",
|
||||
want: YangRange{R(minInt16, maxInt16)},
|
||||
}, {
|
||||
desc: "coalescing from min to max for int8",
|
||||
inParentRange: Int8Range,
|
||||
in: "min..0|1..max",
|
||||
want: YangRange{R(minInt8, maxInt8)},
|
||||
}, {
|
||||
desc: "spelling error",
|
||||
inParentRange: Int64Range,
|
||||
in: "mean..0|1..max",
|
||||
wantErrSubstring: "invalid syntax",
|
||||
}, {
|
||||
desc: "big numbers, coalescing",
|
||||
in: "0..69|4294967294|4294967295",
|
||||
want: YangRange{R(0, 69), R(4294967294, 4294967295)},
|
||||
}, {
|
||||
desc: "no ranges",
|
||||
in: "250|500|1000",
|
||||
want: YangRange{R(250, 250), R(500, 500), R(1000, 1000)},
|
||||
}, {
|
||||
desc: "no ranges unsorted",
|
||||
in: "1000|500|250",
|
||||
want: YangRange{R(250, 250), R(500, 500), R(1000, 1000)},
|
||||
}, {
|
||||
desc: "negative numbers",
|
||||
in: "-31..-1|1..31",
|
||||
want: YangRange{R(-31, -1), R(1, 31)},
|
||||
}, {
|
||||
desc: "spaces",
|
||||
in: "-22 | -15 | -7 | 0",
|
||||
want: YangRange{R(-22, -22), R(-15, -15), R(-7, -7), R(0, 0)},
|
||||
}}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.desc, func(t *testing.T) {
|
||||
got, err := tt.inParentRange.parseChildRanges(tt.in, false, 0)
|
||||
if err != nil {
|
||||
if diff := errdiff.Substring(err, tt.wantErrSubstring); diff != "" {
|
||||
t.Fatalf("did not get expected error, %s", diff)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if diff := cmp.Diff(tt.want, got); diff != "" {
|
||||
t.Errorf("parseChildRanges (-want, +got):\n%s", diff)
|
||||
}
|
||||
|
||||
if tt.inParentRange == nil {
|
||||
if got, err = ParseRangesInt(tt.in); err != nil {
|
||||
t.Fatalf("ParseRangesInt: unexpected error: %v", err)
|
||||
}
|
||||
if diff := cmp.Diff(tt.want, got); diff != "" {
|
||||
t.Errorf("ParseRangesInt (-want, +got):\n%s", diff)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCoalesce(t *testing.T) {
|
||||
for x, tt := range []struct {
|
||||
in, out YangRange
|
||||
}{
|
||||
{},
|
||||
{YangRange{R(1, 4)}, YangRange{R(1, 4)}},
|
||||
{YangRange{R(1, 2), R(3, 4)}, YangRange{R(1, 4)}},
|
||||
{YangRange{Rf(10, 25, 1), Rf(30, 40, 1)}, YangRange{Rf(10, 25, 1), Rf(30, 40, 1)}},
|
||||
{YangRange{Rf(10, 29, 1), Rf(30, 40, 1)}, YangRange{Rf(10, 40, 1)}},
|
||||
{YangRange{R(1, 2), R(2, 4)}, YangRange{R(1, 4)}},
|
||||
{YangRange{R(1, 2), R(4, 5)}, YangRange{R(1, 2), R(4, 5)}},
|
||||
{YangRange{R(1, 3), R(2, 5)}, YangRange{R(1, 5)}},
|
||||
{YangRange{R(1, 10), R(2, 5)}, YangRange{R(1, 10)}},
|
||||
{YangRange{R(1, 10), R(1, 2), R(4, 5), R(7, 8)}, YangRange{R(1, 10)}},
|
||||
{YangRange{Rf(1, 10, 3), Rf(1, 2, 3), Rf(4, 5, 3), Rf(7, 8, 3)}, YangRange{Rf(1, 10, 3)}},
|
||||
} {
|
||||
out := coalesce(tt.in)
|
||||
if !out.Equal(tt.out) {
|
||||
t.Errorf("#%d: got %v, want %v", x, out, tt.out)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestYangRangeSort(t *testing.T) {
|
||||
for x, tt := range []struct {
|
||||
in, out YangRange
|
||||
}{
|
||||
{YangRange{}, YangRange{}},
|
||||
{YangRange{R(1, 4), R(6, 10)}, YangRange{R(1, 4), R(6, 10)}},
|
||||
{YangRange{R(6, 10), R(1, 4)}, YangRange{R(1, 4), R(6, 10)}},
|
||||
{YangRange{Rf(10, 25, 1), Rf(30, 40, 1)}, YangRange{Rf(10, 25, 1), Rf(30, 40, 1)}},
|
||||
{YangRange{Rf(30, 40, 1), Rf(10, 25, 1)}, YangRange{Rf(10, 25, 1), Rf(30, 40, 1)}},
|
||||
{YangRange{R(1, 2)}, YangRange{R(1, 2)}},
|
||||
{YangRange{R(1, 2), R(4, 5)}, YangRange{R(1, 2), R(4, 5)}},
|
||||
{YangRange{R(1, 3), R(2, 5)}, YangRange{R(1, 3), R(2, 5)}},
|
||||
{YangRange{R(1, 10), R(2, 5)}, YangRange{R(1, 10), R(2, 5)}},
|
||||
{YangRange{R(1, 10), R(1, 2), R(4, 5), R(7, 8)}, YangRange{R(1, 2), R(1, 10), R(4, 5), R(7, 8)}},
|
||||
} {
|
||||
tt.in.Sort()
|
||||
if !tt.in.Equal(tt.out) {
|
||||
t.Errorf("#%d: got %v, want %v", x, tt.in, tt.out)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseRangesDecimal(t *testing.T) {
|
||||
rangeMax := mustParseRangesDecimal("-922337203685477580.8..922337203685477580.7", 1)
|
||||
rangeRestricted := mustParseRangesDecimal("-42..42|100", 5)
|
||||
|
||||
tests := []struct {
|
||||
desc string
|
||||
inParentRange YangRange
|
||||
in string
|
||||
inFracDig uint8
|
||||
want YangRange
|
||||
wantErrSubstring string
|
||||
}{{
|
||||
desc: "min..max fraction-digits 1",
|
||||
inParentRange: rangeMax,
|
||||
in: "min..max",
|
||||
inFracDig: 1,
|
||||
want: YangRange{Rf(MinInt64, MaxInt64, 1)},
|
||||
}, {
|
||||
desc: "min..max fraction-digits 2",
|
||||
inParentRange: rangeMax,
|
||||
in: "min..max",
|
||||
inFracDig: 2,
|
||||
want: YangRange{Rf(MinInt64, MaxInt64, 2)},
|
||||
}, {
|
||||
desc: "min..max no parent range",
|
||||
in: "min..max",
|
||||
inFracDig: 2,
|
||||
want: YangRange{Rf(MinInt64, MaxInt64, 2)},
|
||||
wantErrSubstring: "empty YangRange parent object",
|
||||
}, {
|
||||
desc: "min..max on fragmented range",
|
||||
inParentRange: rangeRestricted,
|
||||
in: "min..max",
|
||||
inFracDig: 5,
|
||||
wantErrSubstring: "not within",
|
||||
}, {
|
||||
desc: "small decimals",
|
||||
inParentRange: rangeMax,
|
||||
in: "0.0|2.0..30.0|1.34..1.99",
|
||||
inFracDig: 2,
|
||||
want: YangRange{Rf(0, 0, 2), Rf(134, 3000, 2)},
|
||||
}, {
|
||||
desc: "small decimals on restricted range",
|
||||
inParentRange: rangeRestricted,
|
||||
in: "0.0|2.0..30.0|1.34..1.99999",
|
||||
inFracDig: 5,
|
||||
want: YangRange{Rf(0, 0, 5), Rf(134000, 3000000, 5)},
|
||||
}, {
|
||||
desc: "small decimals with coalescing",
|
||||
inParentRange: rangeMax,
|
||||
in: "0.0|2.0..30.0",
|
||||
inFracDig: 1,
|
||||
want: YangRange{Rf(0, 0, 1), Rf(20, 300, 1)},
|
||||
}, {
|
||||
desc: "fractional digit cannot be too high",
|
||||
in: "0.0|2.0..30.0",
|
||||
inFracDig: 19,
|
||||
wantErrSubstring: "invalid number of fraction digits",
|
||||
}, {
|
||||
desc: "fractional digit cannot be 0",
|
||||
in: "0.0|2.0..30.0",
|
||||
inFracDig: 0,
|
||||
wantErrSubstring: "invalid number of fraction digits",
|
||||
}, {
|
||||
desc: "big decimals",
|
||||
in: "0.0..69|4294967294.1234|4294967295.1234",
|
||||
inFracDig: 4,
|
||||
want: YangRange{Rf(0, 690000, 4), Rf(42949672941234, 42949672941234, 4), Rf(42949672951234, 42949672951234, 4)},
|
||||
}, {
|
||||
desc: "small decimals, out of order",
|
||||
in: "4.0..5.55|0|2.32..3.23",
|
||||
inFracDig: 3,
|
||||
want: YangRange{Rf(0, 0, 3), Rf(2320, 3230, 3), Rf(4000, 5550, 3)},
|
||||
}, {
|
||||
desc: "invalid input: too many ..s",
|
||||
in: "4.0..5.55..6.66|0|2.32..3.23",
|
||||
inFracDig: 3,
|
||||
wantErrSubstring: "too many '..'",
|
||||
}, {
|
||||
desc: "invalid input: range boundaries out of order",
|
||||
in: "5..4.0|0|2.32..3.23",
|
||||
inFracDig: 3,
|
||||
wantErrSubstring: "range boundaries out of order",
|
||||
}, {
|
||||
desc: "range with min",
|
||||
inParentRange: rangeMax,
|
||||
in: "4.0..5.55|min..0|2.32..3.23",
|
||||
inFracDig: 3,
|
||||
want: YangRange{Rf(MinInt64, 0, 3), Rf(2320, 3230, 3), Rf(4000, 5550, 3)},
|
||||
}, {
|
||||
desc: "range with max",
|
||||
inParentRange: rangeMax,
|
||||
in: "4.0..max|min..0|2.32..3.23",
|
||||
inFracDig: 3,
|
||||
want: YangRange{Rf(MinInt64, 0, 3), Rf(2320, 3230, 3), Rf(4000, MaxInt64, 3)},
|
||||
}, {
|
||||
desc: "coalescing from min to max",
|
||||
inParentRange: rangeMax,
|
||||
in: "min..0.9|1..max",
|
||||
inFracDig: 1,
|
||||
want: YangRange{Rf(MinInt64, MaxInt64, 1)},
|
||||
}, {
|
||||
desc: "spelling error",
|
||||
inParentRange: rangeMax,
|
||||
in: "min..0.9|1..masks",
|
||||
inFracDig: 1,
|
||||
wantErrSubstring: "invalid syntax",
|
||||
}, {
|
||||
desc: "no ranges",
|
||||
in: "250.55|500.0|1000",
|
||||
inFracDig: 2,
|
||||
want: YangRange{Rf(25055, 25055, 2), Rf(50000, 50000, 2), Rf(100000, 100000, 2)},
|
||||
}, {
|
||||
desc: "no ranges unsorted",
|
||||
in: "1000|500.0|250.55",
|
||||
inFracDig: 2,
|
||||
want: YangRange{Rf(25055, 25055, 2), Rf(50000, 50000, 2), Rf(100000, 100000, 2)},
|
||||
}, {
|
||||
desc: "negative decimals",
|
||||
in: "-31.2..-1.5|1.5..31.2",
|
||||
inFracDig: 1,
|
||||
want: YangRange{Rf(-312, -15, 1), Rf(15, 312, 1)},
|
||||
}, {
|
||||
desc: "spaces",
|
||||
in: "-22.5 | -15 | -7.5 | 0",
|
||||
inFracDig: 1,
|
||||
want: YangRange{Rf(-225, -225, 1), Rf(-150, -150, 1), Rf(-75, -75, 1), Rf(0, 0, 1)},
|
||||
}}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.desc, func(t *testing.T) {
|
||||
got, err := tt.inParentRange.parseChildRanges(tt.in, true, tt.inFracDig)
|
||||
if err != nil {
|
||||
if diff := errdiff.Substring(err, tt.wantErrSubstring); diff != "" {
|
||||
t.Fatalf("did not get expected error, %s", diff)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if diff := cmp.Diff(tt.want, got); diff != "" {
|
||||
t.Errorf("(-want, +got):\n%s", diff)
|
||||
}
|
||||
|
||||
if tt.inParentRange == nil {
|
||||
if got, err = ParseRangesDecimal(tt.in, tt.inFracDig); err != nil {
|
||||
t.Fatalf("ParseRangesDecimal: unexpected error: %v", err)
|
||||
}
|
||||
if diff := cmp.Diff(tt.want, got); diff != "" {
|
||||
t.Errorf("ParseRangesDecimal (-want, +got):\n%s", diff)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAdd(t *testing.T) {
|
||||
tests := []struct {
|
||||
desc string
|
||||
inVal Number
|
||||
inAdd uint64
|
||||
want Number
|
||||
}{{
|
||||
desc: "add one to integer",
|
||||
inVal: FromInt(1),
|
||||
inAdd: 1,
|
||||
want: FromInt(2),
|
||||
}, {
|
||||
desc: "add one to decimal64",
|
||||
inVal: FromFloat(1.0),
|
||||
inAdd: 1,
|
||||
want: FromFloat(1.1),
|
||||
}, {
|
||||
desc: "negative int becomes positive",
|
||||
inVal: FromInt(-2),
|
||||
inAdd: 3,
|
||||
want: FromInt(1),
|
||||
}, {
|
||||
desc: "negative int stays negative",
|
||||
inVal: FromInt(-3),
|
||||
inAdd: 1,
|
||||
want: FromInt(-2),
|
||||
}, {
|
||||
desc: "negative decimal becomes positive",
|
||||
inVal: FromFloat(-2),
|
||||
inAdd: 35,
|
||||
want: FromFloat(1.5),
|
||||
}, {
|
||||
desc: "negative decimal stays negative",
|
||||
inVal: FromFloat(-42.22),
|
||||
inAdd: 4122,
|
||||
want: FromFloat(-1.0),
|
||||
}, {
|
||||
desc: "explicitly set fraction digits",
|
||||
inVal: Number{Value: 10000, FractionDigits: 5},
|
||||
inAdd: 1,
|
||||
want: Number{Value: 10001, FractionDigits: 5},
|
||||
}, {
|
||||
desc: "explicitly set fraction digits - negative",
|
||||
inVal: Number{Value: 0, FractionDigits: 3},
|
||||
inAdd: 42,
|
||||
want: FromFloat(0.042),
|
||||
}}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.desc, func(t *testing.T) {
|
||||
got := tt.inVal.addQuantum(tt.inAdd)
|
||||
if !cmp.Equal(got, tt.want) {
|
||||
t.Fatalf("did get expected result, got: %s, want: %s", got.String(), tt.want.String())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseInt(t *testing.T) {
|
||||
tests := []struct {
|
||||
desc string
|
||||
inStr string
|
||||
want Number
|
||||
wantErrSubstring string
|
||||
}{{
|
||||
desc: "invalid string supplied",
|
||||
inStr: "fish",
|
||||
wantErrSubstring: "valid syntax",
|
||||
}, {
|
||||
desc: "negative int",
|
||||
inStr: "-42",
|
||||
want: FromInt(-42),
|
||||
}, {
|
||||
desc: "positive int",
|
||||
inStr: "42",
|
||||
want: FromInt(42),
|
||||
}, {
|
||||
desc: "positive int with plus sign",
|
||||
inStr: "+42",
|
||||
want: FromInt(42),
|
||||
}, {
|
||||
desc: "zero",
|
||||
inStr: "0",
|
||||
want: FromInt(0),
|
||||
}, {
|
||||
desc: "min",
|
||||
inStr: "min",
|
||||
wantErrSubstring: "invalid syntax",
|
||||
}, {
|
||||
desc: "max",
|
||||
inStr: "max",
|
||||
wantErrSubstring: "invalid syntax",
|
||||
}, {
|
||||
desc: "just a sign",
|
||||
inStr: "-",
|
||||
wantErrSubstring: "sign with no value",
|
||||
}}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.desc, func(t *testing.T) {
|
||||
got, err := ParseInt(tt.inStr)
|
||||
if err != nil {
|
||||
if diff := errdiff.Substring(err, tt.wantErrSubstring); diff != "" {
|
||||
t.Fatalf("did not get expected error, %s", diff)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if !cmp.Equal(got, tt.want) {
|
||||
t.Errorf("did not get expected Number, got: %s, want: %s", got, tt.want)
|
||||
}
|
||||
|
||||
if got.IsDecimal() {
|
||||
t.Errorf("Got decimal value instead of int: %v", got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseDecimal(t *testing.T) {
|
||||
tests := []struct {
|
||||
desc string
|
||||
inStr string
|
||||
inFracDig uint8
|
||||
skipFractionDigitsCheck bool
|
||||
want Number
|
||||
wantErrSubstring string
|
||||
}{{
|
||||
desc: "too few fractional digits",
|
||||
inStr: "1.000",
|
||||
inFracDig: 0,
|
||||
wantErrSubstring: "invalid number of fraction digits",
|
||||
}, {
|
||||
desc: "too many fraction digits",
|
||||
inStr: "1.000",
|
||||
inFracDig: 24,
|
||||
wantErrSubstring: "invalid number of fraction digits",
|
||||
}, {
|
||||
desc: "more digits supplied",
|
||||
inStr: "1.14242",
|
||||
inFracDig: 2,
|
||||
wantErrSubstring: "has too much precision",
|
||||
}, {
|
||||
desc: "single digit precision",
|
||||
inStr: "1.1",
|
||||
inFracDig: 1,
|
||||
want: Number{Value: 11, FractionDigits: 1},
|
||||
}, {
|
||||
desc: "max precision",
|
||||
inStr: "0.100000000000000000",
|
||||
inFracDig: 18,
|
||||
skipFractionDigitsCheck: true,
|
||||
want: FromFloat(0.1),
|
||||
}, {
|
||||
desc: "max precision but not supplied",
|
||||
inStr: "0.1",
|
||||
inFracDig: 4,
|
||||
skipFractionDigitsCheck: true,
|
||||
want: FromFloat(0.1),
|
||||
}, {
|
||||
desc: "invalid string supplied",
|
||||
inStr: "fish",
|
||||
inFracDig: 17,
|
||||
wantErrSubstring: "not a valid decimal number",
|
||||
}, {
|
||||
desc: "negative number",
|
||||
inStr: "-42.0",
|
||||
inFracDig: 1,
|
||||
want: FromFloat(-42),
|
||||
}}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.desc, func(t *testing.T) {
|
||||
got, err := ParseDecimal(tt.inStr, tt.inFracDig)
|
||||
if err != nil {
|
||||
if diff := errdiff.Substring(err, tt.wantErrSubstring); diff != "" {
|
||||
t.Fatalf("did not get expected error, %s", diff)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if !cmp.Equal(got, tt.want) {
|
||||
t.Errorf("did not get expected Number, got: %s, want: %s", got, tt.want)
|
||||
}
|
||||
|
||||
if !tt.skipFractionDigitsCheck {
|
||||
if got, want := got.FractionDigits, tt.want.FractionDigits; got != want {
|
||||
t.Errorf("fractional digits not equal, got: %d, want: %d", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
if !got.IsDecimal() {
|
||||
t.Errorf("Got non-decimal value: %v", got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNumberString(t *testing.T) {
|
||||
tests := []struct {
|
||||
desc string
|
||||
in Number
|
||||
want string
|
||||
}{{
|
||||
desc: "min",
|
||||
in: FromInt(MinInt64),
|
||||
want: "-9223372036854775808",
|
||||
}, {
|
||||
desc: "max",
|
||||
in: FromInt(MaxInt64),
|
||||
want: "9223372036854775807",
|
||||
}, {
|
||||
desc: "integer",
|
||||
in: Number{Value: 1},
|
||||
want: "1",
|
||||
}, {
|
||||
desc: "negative integer",
|
||||
in: Number{Value: 1, Negative: true},
|
||||
want: "-1",
|
||||
}, {
|
||||
desc: "decimal, fractional digits = 1",
|
||||
in: Number{Value: 1, FractionDigits: 1},
|
||||
want: "0.1",
|
||||
}, {
|
||||
desc: "decimal, fractional digits = 18",
|
||||
in: Number{Value: 123456789012345678, FractionDigits: 18},
|
||||
want: "0.123456789012345678",
|
||||
}, {
|
||||
desc: "negative decimal",
|
||||
in: Number{Value: 100, FractionDigits: 2, Negative: true},
|
||||
want: "-1.00",
|
||||
}}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.desc, func(t *testing.T) {
|
||||
if got := tt.in.String(); got != tt.want {
|
||||
t.Fatalf("did not get expected number, got: %s, want: %s", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnumToJson(t *testing.T) {
|
||||
tests := []struct {
|
||||
desc string
|
||||
in *EnumType
|
||||
want string
|
||||
wantErr bool
|
||||
}{{
|
||||
"empty enum to JSON",
|
||||
&EnumType{
|
||||
last: -1, // +1 will start at 0
|
||||
min: 0,
|
||||
max: MaxBitfieldSize - 1,
|
||||
ToString: map[int64]string{},
|
||||
ToInt: map[string]int64{},
|
||||
},
|
||||
`{}`,
|
||||
false,
|
||||
}, {
|
||||
"2 value enum to JSON",
|
||||
&EnumType{
|
||||
last: 2,
|
||||
min: 0,
|
||||
max: MaxBitfieldSize - 1,
|
||||
ToString: map[int64]string{
|
||||
1: "value1",
|
||||
2: "value2",
|
||||
},
|
||||
ToInt: map[string]int64{
|
||||
"value1": 1,
|
||||
"value2": 2,
|
||||
},
|
||||
},
|
||||
`{"ToString":{"1":"value1","2":"value2"},"ToInt":{"value1":1,"value2":2}}`,
|
||||
false,
|
||||
}}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.desc, func(t *testing.T) {
|
||||
got, err := json.Marshal(tt.in)
|
||||
if string(got) != tt.want {
|
||||
t.Errorf("got: %v, want: %v", string(got), tt.want)
|
||||
}
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("gotErr: %v, wantErr: %v", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,330 @@
|
||||
// Copyright 2021 Google Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package yang
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
)
|
||||
|
||||
var (
|
||||
// TypeKindFromName maps the string name used in a YANG file to the enumerated
|
||||
// TypeKind used in this library.
|
||||
TypeKindFromName = map[string]TypeKind{
|
||||
"none": Ynone,
|
||||
"int8": Yint8,
|
||||
"int16": Yint16,
|
||||
"int32": Yint32,
|
||||
"int64": Yint64,
|
||||
"uint8": Yuint8,
|
||||
"uint16": Yuint16,
|
||||
"uint32": Yuint32,
|
||||
"uint64": Yuint64,
|
||||
"binary": Ybinary,
|
||||
"bits": Ybits,
|
||||
"boolean": Ybool,
|
||||
"decimal64": Ydecimal64,
|
||||
"empty": Yempty,
|
||||
"enumeration": Yenum,
|
||||
"identityref": Yidentityref,
|
||||
"instance-identifier": YinstanceIdentifier,
|
||||
"leafref": Yleafref,
|
||||
"string": Ystring,
|
||||
"union": Yunion,
|
||||
}
|
||||
|
||||
// TypeKindToName maps the enumerated type used in this library to the string
|
||||
// used in a YANG file.
|
||||
TypeKindToName = map[TypeKind]string{
|
||||
Ynone: "none",
|
||||
Yint8: "int8",
|
||||
Yint16: "int16",
|
||||
Yint32: "int32",
|
||||
Yint64: "int64",
|
||||
Yuint8: "uint8",
|
||||
Yuint16: "uint16",
|
||||
Yuint32: "uint32",
|
||||
Yuint64: "uint64",
|
||||
Ybinary: "binary",
|
||||
Ybits: "bits",
|
||||
Ybool: "boolean",
|
||||
Ydecimal64: "decimal64",
|
||||
Yempty: "empty",
|
||||
Yenum: "enumeration",
|
||||
Yidentityref: "identityref",
|
||||
YinstanceIdentifier: "instance-identifier",
|
||||
Yleafref: "leafref",
|
||||
Ystring: "string",
|
||||
Yunion: "union",
|
||||
}
|
||||
|
||||
// BaseTypedefs is a map of all base types to the Typedef structure manufactured
|
||||
// for the type.
|
||||
BaseTypedefs = map[string]*Typedef{}
|
||||
|
||||
baseTypes = map[string]*YangType{
|
||||
"int8": {
|
||||
Name: "int8",
|
||||
Kind: Yint8,
|
||||
Range: Int8Range,
|
||||
},
|
||||
"int16": {
|
||||
Name: "int16",
|
||||
Kind: Yint16,
|
||||
Range: Int16Range,
|
||||
},
|
||||
"int32": {
|
||||
Name: "int32",
|
||||
Kind: Yint32,
|
||||
Range: Int32Range,
|
||||
},
|
||||
"int64": {
|
||||
Name: "int64",
|
||||
Kind: Yint64,
|
||||
Range: Int64Range,
|
||||
},
|
||||
"uint8": {
|
||||
Name: "uint8",
|
||||
Kind: Yuint8,
|
||||
Range: Uint8Range,
|
||||
},
|
||||
"uint16": {
|
||||
Name: "uint16",
|
||||
Kind: Yuint16,
|
||||
Range: Uint16Range,
|
||||
},
|
||||
"uint32": {
|
||||
Name: "uint32",
|
||||
Kind: Yuint32,
|
||||
Range: Uint32Range,
|
||||
},
|
||||
"uint64": {
|
||||
Name: "uint64",
|
||||
Kind: Yuint64,
|
||||
Range: Uint64Range,
|
||||
},
|
||||
|
||||
"decimal64": {
|
||||
Name: "decimal64",
|
||||
Kind: Ydecimal64,
|
||||
},
|
||||
"string": {
|
||||
Name: "string",
|
||||
Kind: Ystring,
|
||||
},
|
||||
"boolean": {
|
||||
Name: "boolean",
|
||||
Kind: Ybool,
|
||||
},
|
||||
"enumeration": {
|
||||
Name: "enumeration",
|
||||
Kind: Yenum,
|
||||
},
|
||||
"bits": {
|
||||
Name: "bits",
|
||||
Kind: Ybits,
|
||||
},
|
||||
"binary": {
|
||||
Name: "binary",
|
||||
Kind: Ybinary,
|
||||
},
|
||||
"leafref": {
|
||||
Name: "leafref",
|
||||
Kind: Yleafref,
|
||||
},
|
||||
"identityref": {
|
||||
Name: "identityref",
|
||||
Kind: Yidentityref,
|
||||
},
|
||||
"empty": {
|
||||
Name: "empty",
|
||||
Kind: Yempty,
|
||||
},
|
||||
"union": {
|
||||
Name: "union",
|
||||
Kind: Yunion,
|
||||
},
|
||||
"instance-identifier": {
|
||||
Name: "instance-identifier",
|
||||
Kind: YinstanceIdentifier,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
// Install builtin types as know types
|
||||
func init() {
|
||||
for k, v := range baseTypes {
|
||||
// Base types are always their own root
|
||||
v.Root = v
|
||||
BaseTypedefs[k] = v.typedef()
|
||||
}
|
||||
}
|
||||
|
||||
// TypeKind is the enumeration of the base types available in YANG. It
|
||||
// is analogous to reflect.Kind.
|
||||
type TypeKind uint
|
||||
|
||||
func (k TypeKind) String() string {
|
||||
if s := TypeKindToName[k]; s != "" {
|
||||
return s
|
||||
}
|
||||
return fmt.Sprintf("unknown-type-%d", k)
|
||||
}
|
||||
|
||||
const (
|
||||
// Ynone represents the invalid (unset) type.
|
||||
Ynone = TypeKind(iota)
|
||||
// Yint8 is an int in the range [-128, 127].
|
||||
Yint8
|
||||
// Yint16 is an int in the range [-32768, 32767].
|
||||
Yint16
|
||||
// Yint32 is an int in the range [-2147483648, 2147483647].
|
||||
Yint32
|
||||
// Yint64 is an int in the range [-9223372036854775808, 9223372036854775807]
|
||||
Yint64
|
||||
// Yuint8 is an int in the range [0, 255]
|
||||
Yuint8
|
||||
// Yuint16 is an int in the range [0, 65535]
|
||||
Yuint16
|
||||
// Yuint32 is an int in the range [0, 4294967295]
|
||||
Yuint32
|
||||
// Yuint64 is an int in the range [0, 18446744073709551615]
|
||||
Yuint64
|
||||
|
||||
// Ybinary stores arbitrary data.
|
||||
Ybinary
|
||||
// Ybits is a named set of bits or flags.
|
||||
Ybits
|
||||
// Ybool is true or false.
|
||||
Ybool
|
||||
// Ydecimal64 is a signed decimal number.
|
||||
Ydecimal64
|
||||
// Yempty has no associated value.
|
||||
Yempty
|
||||
// Yenum stores enumerated strings.
|
||||
Yenum
|
||||
// Yidentityref stores an extensible enumeration.
|
||||
Yidentityref
|
||||
// YinstanceIdentifier stores a reference to a data tree node.
|
||||
YinstanceIdentifier
|
||||
// Yleafref stores a reference to a leaf instance.
|
||||
Yleafref
|
||||
// Ystring is a human readable string.
|
||||
Ystring
|
||||
// Yunion is a choice of types.
|
||||
Yunion
|
||||
)
|
||||
|
||||
// A YangType is the internal representation of a type in YANG. It may
|
||||
// refer to either a builtin type or type specified with typedef. Not
|
||||
// all fields in YangType are used for all types.
|
||||
type YangType struct {
|
||||
Name string
|
||||
Kind TypeKind // Ynone if not a base type
|
||||
Base *Type `json:"-"` // Base type for non-builtin types
|
||||
IdentityBase *Identity `json:",omitempty"` // Base statement for a type using identityref
|
||||
Root *YangType `json:"-"` // root of this type that is the same
|
||||
Bit *EnumType `json:",omitempty"` // bit position, "status" is lost
|
||||
Enum *EnumType `json:",omitempty"` // enum name to value, "status" is lost
|
||||
Units string `json:",omitempty"` // units to be used for this type
|
||||
Default string `json:",omitempty"` // default value, if any
|
||||
HasDefault bool `json:",omitempty"` // whether the type has a default.
|
||||
FractionDigits int `json:",omitempty"` // decimal64 fixed point precision
|
||||
Length YangRange `json:",omitempty"` // this should be processed by section 12
|
||||
OptionalInstance bool `json:",omitempty"` // !require-instances which defaults to true
|
||||
Path string `json:",omitempty"` // the path in a leafref
|
||||
Pattern []string `json:",omitempty"` // limiting XSD-TYPES expressions on strings
|
||||
POSIXPattern []string `json:",omitempty"` // limiting POSIX ERE on strings (specified by openconfig-extensions:posix-pattern)
|
||||
Range YangRange `json:",omitempty"` // range for integers
|
||||
Type []*YangType `json:",omitempty"` // for unions
|
||||
}
|
||||
|
||||
// Equal returns true if y and t describe the same type.
|
||||
func (y *YangType) Equal(t *YangType) bool {
|
||||
switch {
|
||||
case y == t:
|
||||
return true
|
||||
case y == nil || t == nil:
|
||||
return false
|
||||
case
|
||||
// Don't check the Name, it contains no information
|
||||
y.Kind != t.Kind,
|
||||
y.Units != t.Units,
|
||||
y.Default != t.Default,
|
||||
y.HasDefault != t.HasDefault,
|
||||
y.FractionDigits != t.FractionDigits,
|
||||
y.IdentityBase != t.IdentityBase,
|
||||
len(y.Length) != len(t.Length),
|
||||
!y.Length.Equal(t.Length),
|
||||
y.OptionalInstance != t.OptionalInstance,
|
||||
y.Path != t.Path,
|
||||
!ssEqual(y.Pattern, t.Pattern),
|
||||
!ssEqual(y.POSIXPattern, t.POSIXPattern),
|
||||
len(y.Range) != len(t.Range),
|
||||
!y.Range.Equal(t.Range),
|
||||
!tsEqual(y.Type, t.Type),
|
||||
!cmp.Equal(y.Enum, t.Enum, cmp.Comparer(func(t, u EnumType) bool {
|
||||
return cmp.Equal(t.unique, u.unique) && cmp.Equal(t.ToInt, u.ToInt) && cmp.Equal(t.ToString, u.ToString)
|
||||
})):
|
||||
|
||||
return false
|
||||
}
|
||||
// TODO(borman): Base, Bit
|
||||
return true
|
||||
}
|
||||
|
||||
// typedef returns a Typedef created from y for insertion into the BaseTypedefs
|
||||
// map.
|
||||
func (y *YangType) typedef() *Typedef {
|
||||
return &Typedef{
|
||||
Name: y.Name,
|
||||
Source: &Statement{},
|
||||
Type: &Type{
|
||||
Name: y.Name,
|
||||
Source: &Statement{},
|
||||
YangType: y,
|
||||
},
|
||||
YangType: y,
|
||||
}
|
||||
}
|
||||
|
||||
// ssEqual returns true if the two slices are equivalent.
|
||||
func ssEqual(s1, s2 []string) bool {
|
||||
if len(s1) != len(s2) {
|
||||
return false
|
||||
}
|
||||
for x, s := range s1 {
|
||||
if s != s2[x] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// tsEqual returns true if the two Type slices are identical.
|
||||
func tsEqual(t1, t2 []*YangType) bool {
|
||||
if len(t1) != len(t2) {
|
||||
return false
|
||||
}
|
||||
// For now we compare absolute pointers.
|
||||
// This may be wrong.
|
||||
for x, t := range t1 {
|
||||
if !t.Equal(t2[x]) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user