mirror of
https://github.com/kernelkit/infix.git
synced 2026-08-02 13:53:01 +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 }}
|
target: ${{ matrix.target }}
|
||||||
enabled: ${{ inputs.use_cache }}
|
enabled: ${{ inputs.use_cache }}
|
||||||
|
|
||||||
|
# WebUI images bundle the mkdocs User's Guide via post-build.sh.
|
||||||
|
- uses: ./.github/actions/setup-mkdocs
|
||||||
|
|
||||||
- name: Configure & Build
|
- name: Configure & Build
|
||||||
env:
|
env:
|
||||||
INFIX_RELEASE: ${{ steps.vars.outputs.ver }}
|
INFIX_RELEASE: ${{ steps.vars.outputs.ver }}
|
||||||
|
|||||||
@@ -124,6 +124,9 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
target: ${{ env.TARGET }}
|
target: ${{ env.TARGET }}
|
||||||
|
|
||||||
|
# WebUI images bundle the mkdocs User's Guide via post-build.sh.
|
||||||
|
- uses: ./.github/actions/setup-mkdocs
|
||||||
|
|
||||||
- name: Configure ${{ env.TARGET }}
|
- name: Configure ${{ env.TARGET }}
|
||||||
run: |
|
run: |
|
||||||
make ${{ env.TARGET }}_defconfig
|
make ${{ env.TARGET }}_defconfig
|
||||||
|
|||||||
@@ -120,3 +120,29 @@ grep -qsE '^/bin/clish$$' "$TARGET_DIR/etc/shells" \
|
|||||||
if [ "$BR2_PACKAGE_HOST_PYTHON_YANGDOC" = "y" ]; then
|
if [ "$BR2_PACKAGE_HOST_PYTHON_YANGDOC" = "y" ]; then
|
||||||
mkyangdoc "$BINARIES_DIR/yangdoc.html"
|
mkyangdoc "$BINARIES_DIR/yangdoc.html"
|
||||||
fi
|
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)
|
# /telemetry/optics is for streaming (not used atm)
|
||||||
location ~ ^/(restconf|yang|.well-known)/ {
|
location ~ ^/(restconf|yang|.well-known)/ {
|
||||||
|
include /etc/nginx/restconf-access.conf;
|
||||||
grpc_pass grpc://[::1]:10080;
|
grpc_pass grpc://[::1]:10080;
|
||||||
grpc_set_header Host $host;
|
grpc_set_header Host $host;
|
||||||
grpc_set_header X-Real-IP $remote_addr;
|
grpc_set_header X-Real-IP $remote_addr;
|
||||||
|
|||||||
@@ -175,7 +175,6 @@ BR2_PACKAGE_FIREWALL=y
|
|||||||
BR2_PACKAGE_IITO=y
|
BR2_PACKAGE_IITO=y
|
||||||
BR2_PACKAGE_KEYACK=y
|
BR2_PACKAGE_KEYACK=y
|
||||||
BR2_PACKAGE_KLISH_PLUGIN_INFIX=y
|
BR2_PACKAGE_KLISH_PLUGIN_INFIX=y
|
||||||
BR2_PACKAGE_LANDING=y
|
|
||||||
BR2_PACKAGE_LOWDOWN=y
|
BR2_PACKAGE_LOWDOWN=y
|
||||||
BR2_PACKAGE_MCD=y
|
BR2_PACKAGE_MCD=y
|
||||||
BR2_PACKAGE_MDNS_ALIAS=y
|
BR2_PACKAGE_MDNS_ALIAS=y
|
||||||
@@ -187,6 +186,8 @@ BR2_PACKAGE_PODMAN_DRIVER_DEVICEMAPPER=y
|
|||||||
BR2_PACKAGE_PODMAN_DRIVER_VFS=y
|
BR2_PACKAGE_PODMAN_DRIVER_VFS=y
|
||||||
BR2_PACKAGE_TETRIS=y
|
BR2_PACKAGE_TETRIS=y
|
||||||
BR2_PACKAGE_ROUSETTE=y
|
BR2_PACKAGE_ROUSETTE=y
|
||||||
|
# BR2_PACKAGE_LANDING is not set
|
||||||
|
BR2_PACKAGE_WEBUI=y
|
||||||
BR2_PACKAGE_RAUC_INSTALLATION_STATUS=y
|
BR2_PACKAGE_RAUC_INSTALLATION_STATUS=y
|
||||||
BR2_PACKAGE_HOST_PYTHON_YANGDOC=y
|
BR2_PACKAGE_HOST_PYTHON_YANGDOC=y
|
||||||
BR2_PACKAGE_PCIUTILS=y
|
BR2_PACKAGE_PCIUTILS=y
|
||||||
|
|||||||
@@ -144,6 +144,7 @@ BR2_PACKAGE_FIREWALL=y
|
|||||||
BR2_PACKAGE_IITO=y
|
BR2_PACKAGE_IITO=y
|
||||||
BR2_PACKAGE_KEYACK=y
|
BR2_PACKAGE_KEYACK=y
|
||||||
BR2_PACKAGE_KLISH_PLUGIN_INFIX=y
|
BR2_PACKAGE_KLISH_PLUGIN_INFIX=y
|
||||||
|
BR2_PACKAGE_LANDING=y
|
||||||
BR2_PACKAGE_LOWDOWN=y
|
BR2_PACKAGE_LOWDOWN=y
|
||||||
BR2_PACKAGE_MCD=y
|
BR2_PACKAGE_MCD=y
|
||||||
BR2_PACKAGE_MDNS_ALIAS=y
|
BR2_PACKAGE_MDNS_ALIAS=y
|
||||||
|
|||||||
@@ -162,7 +162,6 @@ BR2_PACKAGE_FIREWALL=y
|
|||||||
BR2_PACKAGE_IITO=y
|
BR2_PACKAGE_IITO=y
|
||||||
BR2_PACKAGE_KEYACK=y
|
BR2_PACKAGE_KEYACK=y
|
||||||
BR2_PACKAGE_KLISH_PLUGIN_INFIX=y
|
BR2_PACKAGE_KLISH_PLUGIN_INFIX=y
|
||||||
BR2_PACKAGE_LANDING=y
|
|
||||||
BR2_PACKAGE_LOWDOWN=y
|
BR2_PACKAGE_LOWDOWN=y
|
||||||
BR2_PACKAGE_MCD=y
|
BR2_PACKAGE_MCD=y
|
||||||
BR2_PACKAGE_MDNS_ALIAS=y
|
BR2_PACKAGE_MDNS_ALIAS=y
|
||||||
@@ -170,6 +169,8 @@ BR2_PACKAGE_NETBROWSE=y
|
|||||||
BR2_PACKAGE_ONIEPROM=y
|
BR2_PACKAGE_ONIEPROM=y
|
||||||
BR2_PACKAGE_TETRIS=y
|
BR2_PACKAGE_TETRIS=y
|
||||||
BR2_PACKAGE_ROUSETTE=y
|
BR2_PACKAGE_ROUSETTE=y
|
||||||
|
# BR2_PACKAGE_LANDING is not set
|
||||||
|
BR2_PACKAGE_WEBUI=y
|
||||||
BR2_PACKAGE_RAUC_INSTALLATION_STATUS=y
|
BR2_PACKAGE_RAUC_INSTALLATION_STATUS=y
|
||||||
BR2_PACKAGE_HOST_PYTHON_YANGDOC=y
|
BR2_PACKAGE_HOST_PYTHON_YANGDOC=y
|
||||||
IMAGE_ITB_AUX=y
|
IMAGE_ITB_AUX=y
|
||||||
|
|||||||
@@ -142,6 +142,7 @@ BR2_PACKAGE_FIREWALL=y
|
|||||||
BR2_PACKAGE_IITO=y
|
BR2_PACKAGE_IITO=y
|
||||||
BR2_PACKAGE_KEYACK=y
|
BR2_PACKAGE_KEYACK=y
|
||||||
BR2_PACKAGE_KLISH_PLUGIN_INFIX=y
|
BR2_PACKAGE_KLISH_PLUGIN_INFIX=y
|
||||||
|
BR2_PACKAGE_LANDING=y
|
||||||
BR2_PACKAGE_LOWDOWN=y
|
BR2_PACKAGE_LOWDOWN=y
|
||||||
BR2_PACKAGE_MCD=y
|
BR2_PACKAGE_MCD=y
|
||||||
BR2_PACKAGE_MDNS_ALIAS=y
|
BR2_PACKAGE_MDNS_ALIAS=y
|
||||||
|
|||||||
@@ -194,7 +194,6 @@ BR2_PACKAGE_FIREWALL=y
|
|||||||
BR2_PACKAGE_IITO=y
|
BR2_PACKAGE_IITO=y
|
||||||
BR2_PACKAGE_KEYACK=y
|
BR2_PACKAGE_KEYACK=y
|
||||||
BR2_PACKAGE_KLISH_PLUGIN_INFIX=y
|
BR2_PACKAGE_KLISH_PLUGIN_INFIX=y
|
||||||
BR2_PACKAGE_LANDING=y
|
|
||||||
BR2_PACKAGE_LOWDOWN=y
|
BR2_PACKAGE_LOWDOWN=y
|
||||||
BR2_PACKAGE_MCD=y
|
BR2_PACKAGE_MCD=y
|
||||||
BR2_PACKAGE_MDNS_ALIAS=y
|
BR2_PACKAGE_MDNS_ALIAS=y
|
||||||
@@ -206,6 +205,8 @@ BR2_PACKAGE_PODMAN_DRIVER_DEVICEMAPPER=y
|
|||||||
BR2_PACKAGE_PODMAN_DRIVER_VFS=y
|
BR2_PACKAGE_PODMAN_DRIVER_VFS=y
|
||||||
BR2_PACKAGE_TETRIS=y
|
BR2_PACKAGE_TETRIS=y
|
||||||
BR2_PACKAGE_ROUSETTE=y
|
BR2_PACKAGE_ROUSETTE=y
|
||||||
|
# BR2_PACKAGE_LANDING is not set
|
||||||
|
BR2_PACKAGE_WEBUI=y
|
||||||
BR2_PACKAGE_RAUC_INSTALLATION_STATUS=y
|
BR2_PACKAGE_RAUC_INSTALLATION_STATUS=y
|
||||||
BR2_PACKAGE_HOST_PYTHON_YANGDOC=y
|
BR2_PACKAGE_HOST_PYTHON_YANGDOC=y
|
||||||
BR2_PACKAGE_PCIUTILS=y
|
BR2_PACKAGE_PCIUTILS=y
|
||||||
|
|||||||
@@ -169,7 +169,6 @@ BR2_PACKAGE_FIREWALL=y
|
|||||||
BR2_PACKAGE_IITO=y
|
BR2_PACKAGE_IITO=y
|
||||||
BR2_PACKAGE_KEYACK=y
|
BR2_PACKAGE_KEYACK=y
|
||||||
BR2_PACKAGE_KLISH_PLUGIN_INFIX=y
|
BR2_PACKAGE_KLISH_PLUGIN_INFIX=y
|
||||||
BR2_PACKAGE_LANDING=y
|
|
||||||
BR2_PACKAGE_LOWDOWN=y
|
BR2_PACKAGE_LOWDOWN=y
|
||||||
BR2_PACKAGE_MCD=y
|
BR2_PACKAGE_MCD=y
|
||||||
BR2_PACKAGE_MDNS_ALIAS=y
|
BR2_PACKAGE_MDNS_ALIAS=y
|
||||||
@@ -181,6 +180,8 @@ BR2_PACKAGE_PODMAN_DRIVER_DEVICEMAPPER=y
|
|||||||
BR2_PACKAGE_PODMAN_DRIVER_VFS=y
|
BR2_PACKAGE_PODMAN_DRIVER_VFS=y
|
||||||
BR2_PACKAGE_TETRIS=y
|
BR2_PACKAGE_TETRIS=y
|
||||||
BR2_PACKAGE_ROUSETTE=y
|
BR2_PACKAGE_ROUSETTE=y
|
||||||
|
# BR2_PACKAGE_LANDING is not set
|
||||||
|
BR2_PACKAGE_WEBUI=y
|
||||||
BR2_PACKAGE_RAUC_INSTALLATION_STATUS=y
|
BR2_PACKAGE_RAUC_INSTALLATION_STATUS=y
|
||||||
BR2_PACKAGE_HOST_PYTHON_YANGDOC=y
|
BR2_PACKAGE_HOST_PYTHON_YANGDOC=y
|
||||||
BR2_PACKAGE_PCIUTILS=y
|
BR2_PACKAGE_PCIUTILS=y
|
||||||
|
|||||||
@@ -141,6 +141,7 @@ BR2_PACKAGE_FIREWALL=y
|
|||||||
BR2_PACKAGE_IITO=y
|
BR2_PACKAGE_IITO=y
|
||||||
BR2_PACKAGE_KEYACK=y
|
BR2_PACKAGE_KEYACK=y
|
||||||
BR2_PACKAGE_KLISH_PLUGIN_INFIX=y
|
BR2_PACKAGE_KLISH_PLUGIN_INFIX=y
|
||||||
|
BR2_PACKAGE_LANDING=y
|
||||||
BR2_PACKAGE_LOWDOWN=y
|
BR2_PACKAGE_LOWDOWN=y
|
||||||
BR2_PACKAGE_MCD=y
|
BR2_PACKAGE_MCD=y
|
||||||
BR2_PACKAGE_MDNS_ALIAS=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.
|
All notable changes to the project are documented in this file.
|
||||||
|
|
||||||
[v26.06.0][UNRELEASED]
|
[v26.06.0][UNRELEASED]
|
||||||
--------------
|
-------------------------
|
||||||
|
|
||||||
### Changes
|
### Changes
|
||||||
|
|
||||||
- Upgrade Linux kernel to 6.18.35 (LTS)
|
- 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
|
- 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
|
share an SSID: 802.11k, 802.11v and 802.11r (over-the-air FT). See the
|
||||||
[Wi-Fi][wifi] guide for details
|
[Wi-Fi][wifi] guide for details
|
||||||
@@ -21,11 +25,15 @@ All notable changes to the project are documented in this file.
|
|||||||
|
|
||||||
### Fixes
|
### Fixes
|
||||||
|
|
||||||
- Firewall masquerade no longer enables the global IPv4/IPv6 forwarding
|
- Enabling IP masquerading in the firewall no longer enables IP forwarding on
|
||||||
sysctls. You must now enable IP forwarding explicitly on the interfaces
|
all interfaces. This has been an issue ever since the firewall support was
|
||||||
that should route traffic; enabling NAT alone is no longer enough
|
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
|
[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
|
show`, which were not expanded, so `show interface` and other operational
|
||||||
reads failed. Ranges are now expanded and listed correctly
|
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
|
[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
|
[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/
|
[buildroot]: https://buildroot.org/
|
||||||
[UNRELEASED]: https://github.com/kernelkit/infix/compare/v26.05.0...HEAD
|
[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.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.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
|
[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
|
```bash
|
||||||
$ sudo apt install pipx
|
$ sudo apt install pipx
|
||||||
$ pipx install mkdocs
|
$ 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
|
The `mike` and `mkdocs-to-pdf` packages are used for online versioning
|
||||||
versioning and PDF generation by GitHub Actions, but since they are in
|
and PDF generation by GitHub Actions, but since every plugin is listed
|
||||||
the `mkdocs.yml` file, everyone who wants to preview the documentation
|
in `mkdocs.yml`, anyone who wants to preview the documentation has to
|
||||||
have to install all the tooling.
|
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:
|
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/libyang-cpp/Config.in"
|
||||||
source "$BR2_EXTERNAL_INFIX_PATH/package/sysrepo-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/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/nghttp2-asio/Config.in"
|
||||||
source "$BR2_EXTERNAL_INFIX_PATH/package/date-cpp/Config.in"
|
source "$BR2_EXTERNAL_INFIX_PATH/package/date-cpp/Config.in"
|
||||||
source "$BR2_EXTERNAL_INFIX_PATH/package/rauc-installation-status/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/
|
mkdir -p $(TARGET_DIR)/usr/html/
|
||||||
cp $(@D)/*.html $(TARGET_DIR)/usr/html/
|
cp $(@D)/*.html $(TARGET_DIR)/usr/html/
|
||||||
cp $(@D)/*.png $(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
|
endef
|
||||||
|
|
||||||
$(eval $(generic-package))
|
$(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) {
|
if (chown(fn, -1, gid) && errno != EPERM) {
|
||||||
const struct group *gr = getgrgid(gid);
|
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);
|
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)
|
static const char *infix_ds(const char *text, const struct infix_ds **ds)
|
||||||
|
|||||||
@@ -19,6 +19,8 @@
|
|||||||
#define GENERATE_STRING(STRING) #STRING,
|
#define GENERATE_STRING(STRING) #STRING,
|
||||||
|
|
||||||
#define NGINX_SSL_CONF "/etc/nginx/ssl.conf"
|
#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 AVAHI_SVC_PATH "/etc/avahi/services"
|
||||||
|
|
||||||
#define LLDP_CONFIG "/etc/lldpd.d/confd.conf"
|
#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") &&
|
ena = lydx_is_enabled(srv, "enabled") &&
|
||||||
lydx_is_enabled(lydx_get_xpathf(config, WEB_XPATH), "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");
|
finit_reload("nginx");
|
||||||
|
|
||||||
return put(cfg);
|
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 */
|
/* Web master on/off: propagate to nginx and all sub-services */
|
||||||
if (lydx_get_xpathf(diff, WEB_XPATH "/enabled")) {
|
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");
|
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"),
|
svc_enable(ena && lydx_is_enabled(lydx_get_xpathf(config, WEB_CONSOLE_XPATH), "enabled"),
|
||||||
ttyd, "ttyd");
|
ttyd, "ttyd");
|
||||||
svc_enable(nb_ena, netbrowse, "netbrowse");
|
svc_enable(nb_ena, netbrowse, "netbrowse");
|
||||||
svc_enable(ena && lydx_is_enabled(lydx_get_xpathf(config, WEB_RESTCONF_XPATH), "enabled"),
|
/* Rousette follows web/enabled; external access is gated separately via restconf/enabled */
|
||||||
restconf, "restconf");
|
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");
|
svc_enable(ena, web, "nginx");
|
||||||
mdns_alias_conf(nb_ena);
|
mdns_alias_conf(nb_ena);
|
||||||
finit_reload("mdns-alias");
|
finit_reload("mdns-alias");
|
||||||
|
|||||||
+27
-2
@@ -5,6 +5,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
|
"net/url"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -221,7 +222,7 @@ func scan() map[string]Host {
|
|||||||
|
|
||||||
var url string
|
var url string
|
||||||
if meta.adminurl != "" {
|
if meta.adminurl != "" {
|
||||||
url = meta.adminurl
|
url = rebaseURLHost(meta.adminurl, link)
|
||||||
} else if info.urlTemplate != "" {
|
} else if info.urlTemplate != "" {
|
||||||
url = strings.NewReplacer(
|
url = strings.NewReplacer(
|
||||||
"{address}", address,
|
"{address}", address,
|
||||||
@@ -340,7 +341,7 @@ func parseOperational(root *opRoot) map[string]Host {
|
|||||||
|
|
||||||
var url string
|
var url string
|
||||||
if meta.adminurl != "" {
|
if meta.adminurl != "" {
|
||||||
url = meta.adminurl
|
url = rebaseURLHost(meta.adminurl, link)
|
||||||
} else if info.urlTemplate != "" {
|
} else if info.urlTemplate != "" {
|
||||||
url = strings.NewReplacer(
|
url = strings.NewReplacer(
|
||||||
"{address}", addr,
|
"{address}", addr,
|
||||||
@@ -398,6 +399,30 @@ func scanAuto() map[string]Host {
|
|||||||
return parseOperational(root)
|
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:
|
// decode handles avahi's DNS-SD escape sequences in service names:
|
||||||
// - \DDD decimal value 0-255, e.g. \058 → ':'
|
// - \DDD decimal value 0-255, e.g. \058 → ':'
|
||||||
// - \X literal character X, e.g. \. → '.'
|
// - \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));
|
json_object_set_new(json, "last-error", json_string(strval));
|
||||||
props = rauc_installer_get_progress(rauc);
|
props = rauc_installer_get_progress(rauc);
|
||||||
if(props) {
|
if(props) {
|
||||||
GVariant *val;
|
gint32 pct;
|
||||||
progress = json_object();
|
progress = json_object();
|
||||||
g_variant_get(props, "(@isi)", &val, &strval, NULL);
|
g_variant_get(props, "(isi)", &pct, &strval, NULL);
|
||||||
json_object_set_new(progress, "percentage", json_string(g_variant_print(val, FALSE)));
|
json_object_set_new(progress, "percentage", json_integer(pct));
|
||||||
json_object_set_new(progress, "message", json_string(strval));
|
json_object_set_new(progress, "message", json_string(strval));
|
||||||
json_object_set_new(json, "progress", progress);
|
json_object_set_new(json, "progress", progress);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -203,10 +203,11 @@ def link(ifname):
|
|||||||
if supported:
|
if supported:
|
||||||
eth["infix-ethernet-interface:supported-pmd-types"] = supported
|
eth["infix-ethernet-interface:supported-pmd-types"] = supported
|
||||||
|
|
||||||
# Suppress when advertised == supported — that's the default
|
# Always emit when the PHY is advertising something — even when it equals
|
||||||
# "advertise everything" state with no diagnostic value.
|
# 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"))
|
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
|
eth["auto-negotiation"]["infix-ethernet-interface:advertised-pmd-types"] = advertised
|
||||||
|
|
||||||
speed_bps = None
|
speed_bps = None
|
||||||
|
|||||||
@@ -257,9 +257,16 @@ def add_software(out):
|
|||||||
insert(out, "infix-system:software", software)
|
insert(out, "infix-system:software", software)
|
||||||
|
|
||||||
def add_hostname(out):
|
def add_hostname(out):
|
||||||
hostname = HOST.run(tuple(["hostname"]))
|
hostname = HOST.run(tuple(["hostname"]))
|
||||||
out["hostname"] = hostname.strip()
|
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):
|
def add_timezone(out):
|
||||||
path = HOST.run(tuple("realpath /etc/localtime".split()), "")
|
path = HOST.run(tuple("realpath /etc/localtime".split()), "")
|
||||||
timezone = None
|
timezone = None
|
||||||
@@ -415,9 +422,14 @@ def add_resource_usage(out):
|
|||||||
except (FileNotFoundError, ValueError):
|
except (FileNotFoundError, ValueError):
|
||||||
pass
|
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 = []
|
filesystems = []
|
||||||
for mount in ["/", "/var", "/cfg"]:
|
for mount in ["/", "/var", "/cfg", "/run", "/tmp"]:
|
||||||
try:
|
try:
|
||||||
result = HOST.run_multiline(["df", "-k", mount], [])
|
result = HOST.run_multiline(["df", "-k", mount], [])
|
||||||
if len(result) > 1:
|
if len(result) > 1:
|
||||||
@@ -448,6 +460,7 @@ def operational():
|
|||||||
out_state = out["ietf-system:system-state"]
|
out_state = out["ietf-system:system-state"]
|
||||||
out_system = out["ietf-system:system"]
|
out_system = out["ietf-system:system"]
|
||||||
add_hostname(out_system)
|
add_hostname(out_system)
|
||||||
|
add_contact_location(out_system)
|
||||||
add_users(out_system)
|
add_users(out_system)
|
||||||
add_timezone(out_system)
|
add_timezone(out_system)
|
||||||
add_software(out_state)
|
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