mirror of
https://github.com/kernelkit/infix.git
synced 2026-07-22 01:13:00 +02:00
webui: advanced configure using a yang tree
Navigating interfaces now work, and the tree follows the accordion style like the rest of the UI. Also, some updates to the curated pages: online help, lists of leafrefs instead of hard-coded list options. Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
This commit is contained in:
+15
-1
@@ -1,3 +1,17 @@
|
||||
module github.com/kernelkit/webui
|
||||
|
||||
go 1.22
|
||||
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=
|
||||
@@ -3,6 +3,7 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"html/template"
|
||||
"log"
|
||||
@@ -20,6 +21,11 @@ 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 {
|
||||
@@ -60,13 +66,20 @@ func (h *LoginHandler) DoLogin(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
// Probe optional features once at login and bake into the session.
|
||||
// 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)
|
||||
|
||||
// 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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
// 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 (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestYangTypeEqual(t *testing.T) {
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
inLeft *YangType
|
||||
inRight *YangType
|
||||
wantEqual bool
|
||||
}{{
|
||||
name: "both-nil",
|
||||
inLeft: nil,
|
||||
inRight: nil,
|
||||
wantEqual: true,
|
||||
}, {
|
||||
name: "one-nil",
|
||||
inLeft: &YangType{
|
||||
Kind: Ydecimal64,
|
||||
FractionDigits: 5,
|
||||
},
|
||||
inRight: nil,
|
||||
wantEqual: false,
|
||||
}, {
|
||||
name: "name-unequal",
|
||||
inLeft: &YangType{
|
||||
Name: "foo",
|
||||
Kind: Ydecimal64,
|
||||
FractionDigits: 5,
|
||||
},
|
||||
inRight: &YangType{
|
||||
Name: "bar",
|
||||
Kind: Ydecimal64,
|
||||
FractionDigits: 5,
|
||||
},
|
||||
wantEqual: true,
|
||||
}, {
|
||||
name: "fraction-digits-unequal",
|
||||
inLeft: &YangType{
|
||||
Name: "foo",
|
||||
Kind: Ydecimal64,
|
||||
FractionDigits: 5,
|
||||
},
|
||||
inRight: &YangType{
|
||||
Name: "foo",
|
||||
Kind: Ydecimal64,
|
||||
FractionDigits: 4,
|
||||
},
|
||||
wantEqual: false,
|
||||
}, {
|
||||
name: "types-unequal",
|
||||
inLeft: &YangType{
|
||||
Name: "foo",
|
||||
Kind: Ydecimal64,
|
||||
FractionDigits: 5,
|
||||
},
|
||||
inRight: &YangType{
|
||||
Name: "foo",
|
||||
Kind: Yint64,
|
||||
},
|
||||
wantEqual: false,
|
||||
}, {
|
||||
name: "defaults-equal",
|
||||
inLeft: &YangType{
|
||||
Name: "foo",
|
||||
Kind: Ystring,
|
||||
Default: "bar",
|
||||
HasDefault: true,
|
||||
},
|
||||
inRight: &YangType{
|
||||
Name: "foo",
|
||||
Kind: Ystring,
|
||||
Default: "bar",
|
||||
HasDefault: true,
|
||||
},
|
||||
wantEqual: true,
|
||||
}, {
|
||||
name: "defaults-unequal",
|
||||
inLeft: &YangType{
|
||||
Name: "foo",
|
||||
Kind: Ystring,
|
||||
Default: "bar",
|
||||
HasDefault: true,
|
||||
},
|
||||
inRight: &YangType{
|
||||
Name: "foo",
|
||||
Kind: Ystring,
|
||||
Default: "baz",
|
||||
HasDefault: true,
|
||||
},
|
||||
wantEqual: false,
|
||||
}, {
|
||||
name: "has-default-unequal",
|
||||
inLeft: &YangType{
|
||||
Name: "foo",
|
||||
Kind: Ystring,
|
||||
Default: "",
|
||||
},
|
||||
inRight: &YangType{
|
||||
Name: "foo",
|
||||
Kind: Ystring,
|
||||
Default: "",
|
||||
HasDefault: true,
|
||||
},
|
||||
wantEqual: false,
|
||||
}}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if gotEqual := tt.inLeft.Equal(tt.inRight); gotEqual != tt.wantEqual {
|
||||
t.Errorf("gotEqual: %v, wantEqual: %v", gotEqual, tt.wantEqual)
|
||||
}
|
||||
// Must be symmetric
|
||||
if reverseEqual := tt.inRight.Equal(tt.inLeft); reverseEqual != tt.wantEqual {
|
||||
t.Errorf("got reverseEqual: %v, wantEqual: %v", reverseEqual, tt.wantEqual)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
// Copyright 2020 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 yangentry contains high-level helpers for using yang.Entry objects.
|
||||
package yangentry
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/openconfig/goyang/pkg/yang"
|
||||
)
|
||||
|
||||
// Parse takes a list of either module/submodule names or .yang file
|
||||
// paths, and a list of include paths. It runs the yang parser on the YANG
|
||||
// files by searching for them in the include paths or in the current
|
||||
// directory, returning a slice of yang.Entry pointers which represent the
|
||||
// parsed top level modules. It also returns a list of errors encountered while
|
||||
// parsing, if any.
|
||||
func Parse(yangfiles, path []string) (map[string]*yang.Entry, []error) {
|
||||
return parse(yangfiles, path, yang.NewModules())
|
||||
}
|
||||
|
||||
// ParseWithOptions takes a list of either module/submodule names or .yang file
|
||||
// paths, a list of include paths, and a set of parse options. It configures the
|
||||
// yang parser with the specified parse options and runs it on the YANG
|
||||
// files by searching for them in the include paths or in the current
|
||||
// directory, returning a slice of yang.Entry pointers which represent the
|
||||
// parsed top level modules. It also returns a list of errors encountered while
|
||||
// parsing, if any.
|
||||
func ParseWithOptions(yangfiles, path []string, parseOptions yang.Options) (map[string]*yang.Entry, []error) {
|
||||
ms := yang.NewModules()
|
||||
ms.ParseOptions = parseOptions
|
||||
|
||||
return parse(yangfiles, path, ms)
|
||||
}
|
||||
|
||||
func parse(yangfiles, path []string, ms *yang.Modules) (map[string]*yang.Entry, []error) {
|
||||
for _, p := range path {
|
||||
ms.AddPath(fmt.Sprintf("%s/...", p))
|
||||
}
|
||||
|
||||
var processErr []error
|
||||
for _, name := range yangfiles {
|
||||
if name == "" {
|
||||
continue
|
||||
}
|
||||
if err := ms.Read(name); err != nil {
|
||||
processErr = append(processErr, err)
|
||||
}
|
||||
}
|
||||
|
||||
if len(processErr) > 0 {
|
||||
return nil, processErr
|
||||
}
|
||||
|
||||
if errs := ms.Process(); len(errs) != 0 {
|
||||
return nil, errs
|
||||
}
|
||||
|
||||
entries := make(map[string]*yang.Entry)
|
||||
for _, m := range ms.Modules {
|
||||
e := yang.ToEntry(m)
|
||||
entries[e.Name] = e
|
||||
}
|
||||
|
||||
return entries, nil
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
// Copyright 2020 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 yangentry
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/openconfig/goyang/pkg/yang"
|
||||
)
|
||||
|
||||
// TestParse tests the Parse function - which takes an input
|
||||
// set of modules and processes them using the goyang compiler into a set of
|
||||
// yang.Entry pointers.
|
||||
func TestParse(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
inFiles []string
|
||||
inPath []string
|
||||
wantErr bool
|
||||
wantMods []string
|
||||
}{{
|
||||
name: "simple valid module",
|
||||
inFiles: []string{"testdata/00-valid-module.yang"},
|
||||
inPath: []string{"testdata"},
|
||||
wantMods: []string{"test-module"},
|
||||
}, {
|
||||
name: "simple valid module without .yang extension",
|
||||
inFiles: []string{"00-valid-module"},
|
||||
inPath: []string{"testdata"},
|
||||
wantMods: []string{"test-module"},
|
||||
}, {
|
||||
name: "simple invalid module",
|
||||
inFiles: []string{"testdata/01-invalid-module.yang"},
|
||||
inPath: []string{"testdata"},
|
||||
wantErr: true,
|
||||
}, {
|
||||
name: "valid import",
|
||||
inFiles: []string{"testdata/02-valid-import.yang"},
|
||||
inPath: []string{"testdata/subdir"},
|
||||
wantMods: []string{"test-module"},
|
||||
}, {
|
||||
name: "invalid import",
|
||||
inFiles: []string{"testdata/03-invalid-import.yang"},
|
||||
inPath: []string{},
|
||||
wantErr: true,
|
||||
}, {
|
||||
name: "two modules",
|
||||
inFiles: []string{"testdata/04-valid-module-one.yang", "testdata/04-valid-module-two.yang"},
|
||||
inPath: []string{},
|
||||
wantMods: []string{"module-one", "module-two"},
|
||||
}, {
|
||||
name: "circular submodule dependency",
|
||||
inFiles: []string{"testdata/05-circular-main.yang"},
|
||||
inPath: []string{"testdata/subdir"},
|
||||
wantErr: true,
|
||||
}}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
entries, errs := Parse(tt.inFiles, tt.inPath)
|
||||
if len(errs) != 0 && !tt.wantErr {
|
||||
t.Fatalf("%s: unexpected error processing modules: %v", tt.name, errs)
|
||||
}
|
||||
|
||||
for _, m := range tt.wantMods {
|
||||
if _, ok := entries[m]; !ok {
|
||||
t.Fatalf("%s: could not find module %s", tt.name, m)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestParseWithOptions tests the ParseWithOptions function - which takes an input
|
||||
// set of modules along with a set of parse options, and processes them using the goyang
|
||||
// compiler into a set of yang.Entry pointers.
|
||||
func TestParseWithOptions(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
inFiles []string
|
||||
inPath []string
|
||||
parseOptions yang.Options
|
||||
wantErr bool
|
||||
wantMods []string
|
||||
}{
|
||||
{
|
||||
name: "circular submodule dependency with default options",
|
||||
inFiles: []string{"testdata/05-circular-main.yang"},
|
||||
inPath: []string{"testdata/subdir"},
|
||||
parseOptions: yang.Options{},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "circular submodule dependency with IgnoreSubmoduleCircularDependencies",
|
||||
inFiles: []string{"testdata/05-circular-main.yang"},
|
||||
inPath: []string{"testdata/subdir"},
|
||||
parseOptions: yang.Options{IgnoreSubmoduleCircularDependencies: true},
|
||||
wantMods: []string{"circular-main"},
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
entries, errs := ParseWithOptions(tt.inFiles, tt.inPath, tt.parseOptions)
|
||||
if len(errs) != 0 && !tt.wantErr {
|
||||
t.Fatalf("%s: unexpected error processing modules: %v", tt.name, errs)
|
||||
}
|
||||
|
||||
for _, m := range tt.wantMods {
|
||||
if _, ok := entries[m]; !ok {
|
||||
t.Fatalf("%s: could not find module %s", tt.name, m)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
module test-module {
|
||||
prefix "t";
|
||||
namespace "urn:t";
|
||||
|
||||
leaf valid-leaf { type string; }
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
module test-module {
|
||||
prefix "t";
|
||||
namespace "urn:t";
|
||||
|
||||
leaf invalid-leaf { }
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
module test-module {
|
||||
prefix "t";
|
||||
namespace "urn:t";
|
||||
|
||||
import imported { prefix "i"; }
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
module test-module {
|
||||
prefix "t";
|
||||
namespace "urn:t";
|
||||
|
||||
import import-not-found { prefix "i"; }
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
module module-one {
|
||||
prefix "t";
|
||||
namespace "urn:t";
|
||||
|
||||
leaf one { type int8; }
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
module module-two {
|
||||
prefix "t";
|
||||
namespace "urn:t";
|
||||
|
||||
leaf two { type int8; }
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
module circular-main {
|
||||
yang-version "1.1";
|
||||
|
||||
namespace "urn:test:circular:main";
|
||||
|
||||
prefix "main";
|
||||
|
||||
include circular-sub-one;
|
||||
include circular-sub-two;
|
||||
|
||||
revision "2025-02-15";
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
submodule circular-sub-one {
|
||||
yang-version "1.1";
|
||||
|
||||
belongs-to circular-main { prefix "main"; }
|
||||
|
||||
include circular-sub-two;
|
||||
|
||||
revision "2025-02-15";
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
submodule circular-sub-two {
|
||||
yang-version "1.1";
|
||||
|
||||
belongs-to circular-main { prefix "main"; }
|
||||
|
||||
include circular-sub-one;
|
||||
|
||||
revision "2025-02-15";
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
module imported {
|
||||
prefix "imported";
|
||||
namespace "urn:i";
|
||||
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
module aug {
|
||||
namespace "yang-sucks";
|
||||
prefix "yang";
|
||||
grouping bgp-neighbor_config {
|
||||
leaf peer-as { type string; }
|
||||
}
|
||||
grouping bgp-neighbors {
|
||||
list neighbor {
|
||||
uses bgp-neighbor-group;
|
||||
}
|
||||
}
|
||||
grouping bgp-neighbor-group {
|
||||
container config {
|
||||
uses bgp-neighbor_config;
|
||||
}
|
||||
}
|
||||
grouping bgp-neighbor-peer-group_config {
|
||||
leaf peer-group { type string; }
|
||||
}
|
||||
augment /bgp/neighbors/neighbor/config {
|
||||
uses bgp-neighbor-peer-group_config;
|
||||
}
|
||||
container bgp {
|
||||
container neighbors {
|
||||
uses bgp-neighbors;
|
||||
}
|
||||
}
|
||||
}
|
||||
+93
@@ -0,0 +1,93 @@
|
||||
// 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.
|
||||
|
||||
// Base test yang module.
|
||||
module base {
|
||||
namespace "urn:mod";
|
||||
prefix "base";
|
||||
|
||||
include sub;
|
||||
import other {
|
||||
prefix bother;
|
||||
}
|
||||
|
||||
// basic type tests
|
||||
typedef base-type { type int32; }
|
||||
leaf base-leaf1 { type base-type; }
|
||||
leaf base-leaf2 { type base:base-type; }
|
||||
leaf base-leaf3 { type bother:other-type; }
|
||||
leaf base-leaf4 { type sub-type; }
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
// test uses and leaf ref
|
||||
container base-container-1 {
|
||||
uses base-group;
|
||||
uses bother:other-group;
|
||||
uses base:sub-group;
|
||||
choice base-choice {
|
||||
case choice-a {
|
||||
leaf base-choice-a1 { type string; }
|
||||
leaf base-choice-a2 {
|
||||
type leafref { path ../base-container-1-leaf; }
|
||||
}
|
||||
}
|
||||
case choice-b {
|
||||
leaf base-choice-b1 { type string; }
|
||||
leaf base-choice-b2 {
|
||||
type leafref { path ../../base-container-2/base-container-2a/base-container-2a-leaf; }
|
||||
}
|
||||
}
|
||||
}
|
||||
leaf base-container-1-leaf { type string; }
|
||||
}
|
||||
|
||||
// container referenced by a leafref above
|
||||
container base-container-2 {
|
||||
container base-container-2a {
|
||||
leaf base-container-2a-leaf { type string; }
|
||||
}
|
||||
}
|
||||
|
||||
// test basic augmenting
|
||||
augment /base-container-1/base-choice/choice-a {
|
||||
leaf base-choice-a3 { type string; }
|
||||
}
|
||||
augment /base-container-1/base-choice {
|
||||
case choice-c {
|
||||
leaf base-choice-c1 { type string; }
|
||||
}
|
||||
}
|
||||
|
||||
// simple extension test
|
||||
extension base-ext {
|
||||
argument base-arg;
|
||||
}
|
||||
container ext-container {
|
||||
config false;
|
||||
leaf ext-container-leaf { type string; }
|
||||
base:base-ext "EXTENSION" {
|
||||
leaf base-ext-leaf { type string; }
|
||||
}
|
||||
}
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
// 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.
|
||||
|
||||
// imported by base.yang.
|
||||
module other {
|
||||
namespace "uri:empty";
|
||||
prefix "otherp";
|
||||
typedef other-type { type string; }
|
||||
|
||||
// This container should not appear in base, even though this file is
|
||||
// imported by base. That is just the YANG is defined.
|
||||
container other-container {
|
||||
leaf other-container-leaf1 { type other-type; }
|
||||
leaf other-container-leaf2 { type otherp:other-type; }
|
||||
}
|
||||
grouping other-group {
|
||||
leaf other-group-leaf { type string; }
|
||||
}
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
// 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.
|
||||
|
||||
// included by base.yang.
|
||||
submodule sub {
|
||||
belongs-to base { prefix "sbase"; }
|
||||
typedef sub-type { type sub-type2; }
|
||||
typedef sub-type2 { type int8; }
|
||||
container sub-container {
|
||||
leaf sub-container-leaf { type sub-type; }
|
||||
}
|
||||
grouping sub-group {
|
||||
leaf sub-group-leaf { type string; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
// A YANG module located in a subdirectory, to test the AddYANGPaths
|
||||
// helper function.
|
||||
|
||||
module subdir1 {
|
||||
yang-version "1";
|
||||
|
||||
namespace "namespace:goes:here";
|
||||
prefix "subdir1";
|
||||
|
||||
description
|
||||
"This module is to be found by test cases.";
|
||||
}
|
||||
@@ -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 main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"sort"
|
||||
|
||||
"github.com/openconfig/goyang/pkg/indent"
|
||||
"github.com/openconfig/goyang/pkg/yang"
|
||||
)
|
||||
|
||||
func init() {
|
||||
register(&formatter{
|
||||
name: "tree",
|
||||
f: doTree,
|
||||
help: "display in a tree format",
|
||||
})
|
||||
}
|
||||
|
||||
func doTree(w io.Writer, entries []*yang.Entry) {
|
||||
for _, e := range entries {
|
||||
Write(w, e)
|
||||
}
|
||||
}
|
||||
|
||||
// Write writes e, formatted, and all of its children, to w.
|
||||
func Write(w io.Writer, e *yang.Entry) {
|
||||
if e.Description != "" {
|
||||
fmt.Fprintln(w)
|
||||
fmt.Fprintln(indent.NewWriter(w, "// "), e.Description)
|
||||
}
|
||||
if len(e.Exts) > 0 {
|
||||
fmt.Fprintf(w, "extensions: {\n")
|
||||
for _, ext := range e.Exts {
|
||||
if n := ext.NName(); n != "" {
|
||||
fmt.Fprintf(w, " %s %s;\n", ext.Kind(), n)
|
||||
} else {
|
||||
fmt.Fprintf(w, " %s;\n", ext.Kind())
|
||||
}
|
||||
}
|
||||
fmt.Fprintln(w, "}")
|
||||
}
|
||||
switch {
|
||||
case e.RPC != nil:
|
||||
fmt.Fprintf(w, "RPC: ")
|
||||
case e.ReadOnly():
|
||||
fmt.Fprintf(w, "RO: ")
|
||||
default:
|
||||
fmt.Fprintf(w, "rw: ")
|
||||
}
|
||||
if e.Type != nil {
|
||||
fmt.Fprintf(w, "%s ", getTypeName(e))
|
||||
}
|
||||
name := e.Name
|
||||
if e.Prefix != nil {
|
||||
name = e.Prefix.Name + ":" + name
|
||||
}
|
||||
switch {
|
||||
case e.Dir == nil && e.ListAttr != nil:
|
||||
fmt.Fprintf(w, "[]%s\n", name)
|
||||
return
|
||||
case e.Dir == nil:
|
||||
fmt.Fprintf(w, "%s\n", name)
|
||||
return
|
||||
case e.ListAttr != nil:
|
||||
fmt.Fprintf(w, "[%s]%s {\n", e.Key, name) //}
|
||||
default:
|
||||
fmt.Fprintf(w, "%s {\n", name) //}
|
||||
}
|
||||
if r := e.RPC; r != nil {
|
||||
if r.Input != nil {
|
||||
Write(indent.NewWriter(w, " "), r.Input)
|
||||
}
|
||||
if r.Output != nil {
|
||||
Write(indent.NewWriter(w, " "), r.Output)
|
||||
}
|
||||
}
|
||||
var names []string
|
||||
for k := range e.Dir {
|
||||
names = append(names, k)
|
||||
}
|
||||
sort.Strings(names)
|
||||
for _, k := range names {
|
||||
Write(indent.NewWriter(w, " "), e.Dir[k])
|
||||
}
|
||||
// { to match the brace below to keep brace matching working
|
||||
fmt.Fprintln(w, "}")
|
||||
}
|
||||
|
||||
func getTypeName(e *yang.Entry) string {
|
||||
if e == nil || e.Type == nil {
|
||||
return ""
|
||||
}
|
||||
// Return our root's type name.
|
||||
// This is should be the builtin type-name
|
||||
// for this entry.
|
||||
return e.Type.Root.Name
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
// 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 main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"github.com/openconfig/goyang/pkg/indent"
|
||||
"github.com/openconfig/goyang/pkg/yang"
|
||||
"github.com/pborman/getopt"
|
||||
)
|
||||
|
||||
var (
|
||||
typesDebug bool
|
||||
typesVerbose bool
|
||||
)
|
||||
|
||||
func init() {
|
||||
flags := getopt.New()
|
||||
register(&formatter{
|
||||
name: "types",
|
||||
f: doTypes,
|
||||
help: "display found types",
|
||||
flags: flags,
|
||||
})
|
||||
flags.BoolVarLong(&typesDebug, "types_debug", 0, "display debug information")
|
||||
flags.BoolVarLong(&typesVerbose, "types_verbose", 0, "include base information")
|
||||
}
|
||||
|
||||
func doTypes(w io.Writer, entries []*yang.Entry) {
|
||||
types := Types{}
|
||||
for _, e := range entries {
|
||||
types.AddEntry(e)
|
||||
}
|
||||
|
||||
for t := range types {
|
||||
printType(w, t, typesVerbose)
|
||||
}
|
||||
if typesDebug {
|
||||
for _, e := range entries {
|
||||
showall(w, e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Types keeps track of all the YangTypes defined.
|
||||
type Types map[*yang.YangType]struct{}
|
||||
|
||||
// AddEntry adds all types defined in e and its descendants to t.
|
||||
func (t Types) AddEntry(e *yang.Entry) {
|
||||
if e == nil {
|
||||
return
|
||||
}
|
||||
if e.Type != nil {
|
||||
t[e.Type.Root] = struct{}{}
|
||||
}
|
||||
for _, d := range e.Dir {
|
||||
t.AddEntry(d)
|
||||
}
|
||||
}
|
||||
|
||||
// printType prints type t in a moderately human readable format to w.
|
||||
func printType(w io.Writer, t *yang.YangType, verbose bool) {
|
||||
if verbose && t.Base != nil {
|
||||
base := yang.Source(t.Base)
|
||||
if base == "unknown" {
|
||||
base = "unnamed type"
|
||||
}
|
||||
fmt.Fprintf(w, "%s: ", base)
|
||||
}
|
||||
fmt.Fprintf(w, "%s", t.Root.Name)
|
||||
if t.Kind.String() != t.Root.Name {
|
||||
fmt.Fprintf(w, "(%s)", t.Kind)
|
||||
}
|
||||
if t.Units != "" {
|
||||
fmt.Fprintf(w, " units=%s", t.Units)
|
||||
}
|
||||
if t.Default != "" {
|
||||
fmt.Fprintf(w, " default=%q", t.Default)
|
||||
}
|
||||
if t.FractionDigits != 0 {
|
||||
fmt.Fprintf(w, " fraction-digits=%d", t.FractionDigits)
|
||||
}
|
||||
if len(t.Length) > 0 {
|
||||
fmt.Fprintf(w, " length=%s", t.Length)
|
||||
}
|
||||
if t.Kind == yang.YinstanceIdentifier && !t.OptionalInstance {
|
||||
fmt.Fprintf(w, " required")
|
||||
}
|
||||
if t.Kind == yang.Yleafref && t.Path != "" {
|
||||
fmt.Fprintf(w, " path=%q", t.Path)
|
||||
}
|
||||
if len(t.Pattern) > 0 {
|
||||
fmt.Fprintf(w, " pattern=%s", strings.Join(t.Pattern, "|"))
|
||||
}
|
||||
b := yang.BaseTypedefs[t.Kind.String()].YangType
|
||||
if len(t.Range) > 0 && !t.Range.Equal(b.Range) {
|
||||
fmt.Fprintf(w, " range=%s", t.Range)
|
||||
}
|
||||
if len(t.Type) > 0 {
|
||||
fmt.Fprintf(w, "{\n")
|
||||
for _, t := range t.Type {
|
||||
printType(indent.NewWriter(w, " "), t, verbose)
|
||||
}
|
||||
fmt.Fprintf(w, "}")
|
||||
}
|
||||
fmt.Fprintf(w, ";\n")
|
||||
}
|
||||
|
||||
func showall(w io.Writer, e *yang.Entry) {
|
||||
if e == nil {
|
||||
return
|
||||
}
|
||||
if e.Type != nil {
|
||||
fmt.Fprintf(w, "\n%s\n ", e.Node.Statement().Location())
|
||||
printType(w, e.Type.Root, false)
|
||||
}
|
||||
for _, d := range e.Dir {
|
||||
showall(w, d)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,214 @@
|
||||
// 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.
|
||||
|
||||
// Program yang parses YANG files, displays errors, and possibly writes
|
||||
// something related to the input on output.
|
||||
//
|
||||
// Usage: yang [--path DIR] [--format FORMAT] [FORMAT OPTIONS] [MODULE] [FILE ...]
|
||||
//
|
||||
// If MODULE is specified (an argument that does not end in .yang), it is taken
|
||||
// as the name of the module to display. Any FILEs specified are read, and the
|
||||
// tree for MODULE is displayed. If MODULE was not defined in FILEs (or no
|
||||
// files were specified), then the file MODULES.yang is read as well. An error
|
||||
// is displayed if no definition for MODULE was found.
|
||||
//
|
||||
// If MODULE is missing, then all base modules read from the FILEs are
|
||||
// displayed. If there are no arguments then standard input is parsed.
|
||||
//
|
||||
// If DIR is specified, it is considered a comma separated list of paths
|
||||
// to append to the search directory. If DIR appears as DIR/... then
|
||||
// DIR and all direct and indirect subdirectories are checked.
|
||||
//
|
||||
// FORMAT, which defaults to "tree", specifies the format of output to produce.
|
||||
// Use "goyang --help" for a list of available formats.
|
||||
//
|
||||
// FORMAT OPTIONS are flags that apply to a specific format. They must follow
|
||||
// --format.
|
||||
//
|
||||
// THIS PROGRAM IS STILL JUST A DEVELOPMENT TOOL.
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"runtime/trace"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/openconfig/goyang/pkg/indent"
|
||||
"github.com/openconfig/goyang/pkg/yang"
|
||||
"github.com/pborman/getopt"
|
||||
)
|
||||
|
||||
// Each format must register a formatter with register. The function f will
|
||||
// be called once with the set of yang Entry trees generated.
|
||||
type formatter struct {
|
||||
name string
|
||||
f func(io.Writer, []*yang.Entry)
|
||||
help string
|
||||
flags *getopt.Set
|
||||
}
|
||||
|
||||
var formatters = map[string]*formatter{}
|
||||
|
||||
func register(f *formatter) {
|
||||
formatters[f.name] = f
|
||||
}
|
||||
|
||||
// exitIfError writes errs to standard error and exits with an exit status of 1.
|
||||
// If errs is empty then exitIfError does nothing and simply returns.
|
||||
func exitIfError(errs []error) {
|
||||
if len(errs) > 0 {
|
||||
for _, err := range errs {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
}
|
||||
stop(1)
|
||||
}
|
||||
}
|
||||
|
||||
var stop = os.Exit
|
||||
|
||||
func main() {
|
||||
var format string
|
||||
formats := make([]string, 0, len(formatters))
|
||||
for k := range formatters {
|
||||
formats = append(formats, k)
|
||||
}
|
||||
sort.Strings(formats)
|
||||
|
||||
var traceP string
|
||||
var help bool
|
||||
var paths []string
|
||||
var ignoreSubmoduleCircularDependencies bool
|
||||
getopt.ListVarLong(&paths, "path", 'p', "comma separated list of directories to add to search path", "DIR[,DIR...]")
|
||||
getopt.StringVarLong(&format, "format", 'f', "format to display: "+strings.Join(formats, ", "), "FORMAT")
|
||||
getopt.StringVarLong(&traceP, "trace", 't', "write trace into to TRACEFILE", "TRACEFILE")
|
||||
getopt.BoolVarLong(&help, "help", 'h', "display help")
|
||||
getopt.BoolVarLong(&ignoreSubmoduleCircularDependencies, "ignore-circdep", 'g', "ignore circular dependencies between submodules")
|
||||
getopt.SetParameters("[FORMAT OPTIONS] [SOURCE] [...]")
|
||||
|
||||
if err := getopt.Getopt(func(o getopt.Option) bool {
|
||||
if o.Name() == "--format" {
|
||||
f, ok := formatters[format]
|
||||
if !ok {
|
||||
fmt.Fprintf(os.Stderr, "%s: invalid format. Choices are %s\n", format, strings.Join(formats, ", "))
|
||||
stop(1)
|
||||
}
|
||||
if f.flags != nil {
|
||||
f.flags.VisitAll(func(o getopt.Option) {
|
||||
getopt.AddOption(o)
|
||||
})
|
||||
}
|
||||
}
|
||||
return true
|
||||
}); err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
getopt.PrintUsage(os.Stderr)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if traceP != "" {
|
||||
fp, err := os.Create(traceP)
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
trace.Start(fp)
|
||||
stop = func(c int) { trace.Stop(); os.Exit(c) }
|
||||
defer func() { trace.Stop() }()
|
||||
}
|
||||
|
||||
if help {
|
||||
getopt.CommandLine.PrintUsage(os.Stderr)
|
||||
fmt.Fprintf(os.Stderr, `
|
||||
SOURCE may be a module name or a .yang file.
|
||||
|
||||
Formats:
|
||||
`)
|
||||
for _, fn := range formats {
|
||||
f := formatters[fn]
|
||||
fmt.Fprintf(os.Stderr, " %s - %s\n", f.name, f.help)
|
||||
if f.flags != nil {
|
||||
f.flags.PrintOptions(indent.NewWriter(os.Stderr, " "))
|
||||
}
|
||||
fmt.Fprintln(os.Stderr)
|
||||
}
|
||||
stop(0)
|
||||
}
|
||||
|
||||
ms := yang.NewModules()
|
||||
ms.ParseOptions.IgnoreSubmoduleCircularDependencies = ignoreSubmoduleCircularDependencies
|
||||
|
||||
for _, path := range paths {
|
||||
expanded, err := yang.PathsWithModules(path)
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
continue
|
||||
}
|
||||
ms.AddPath(expanded...)
|
||||
}
|
||||
|
||||
if format == "" {
|
||||
format = "tree"
|
||||
}
|
||||
if _, ok := formatters[format]; !ok {
|
||||
fmt.Fprintf(os.Stderr, "%s: invalid format. Choices are %s\n", format, strings.Join(formats, ", "))
|
||||
stop(1)
|
||||
|
||||
}
|
||||
|
||||
files := getopt.Args()
|
||||
|
||||
if len(files) == 0 {
|
||||
data, err := ioutil.ReadAll(os.Stdin)
|
||||
if err == nil {
|
||||
err = ms.Parse(string(data), "<STDIN>")
|
||||
}
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
stop(1)
|
||||
}
|
||||
}
|
||||
|
||||
for _, name := range files {
|
||||
if err := ms.Read(name); err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// Process the read files, exiting if any errors were found.
|
||||
exitIfError(ms.Process())
|
||||
|
||||
// Keep track of the top level modules we read in.
|
||||
// Those are the only modules we want to print below.
|
||||
mods := map[string]*yang.Module{}
|
||||
var names []string
|
||||
|
||||
for _, m := range ms.Modules {
|
||||
if mods[m.Name] == nil {
|
||||
mods[m.Name] = m
|
||||
names = append(names, m.Name)
|
||||
}
|
||||
}
|
||||
sort.Strings(names)
|
||||
entries := make([]*yang.Entry, len(names))
|
||||
for x, n := range names {
|
||||
entries[x] = yang.ToEntry(mods[n])
|
||||
}
|
||||
|
||||
formatters[format].f(os.Stdout, entries)
|
||||
}
|
||||
@@ -17,6 +17,7 @@ type PageData struct {
|
||||
PageTitle string
|
||||
ActivePage string
|
||||
Capabilities *Capabilities
|
||||
CfgUnsaved bool // running config differs from startup (Apply was used without ApplyAndSave)
|
||||
}
|
||||
|
||||
func csrfToken(ctx context.Context) string {
|
||||
@@ -24,11 +25,13 @@ func csrfToken(ctx context.Context) string {
|
||||
}
|
||||
|
||||
func newPageData(r *http.Request, page, title string) PageData {
|
||||
_, cookieErr := r.Cookie(cfgUnsavedCookie)
|
||||
return PageData{
|
||||
Username: restconf.CredentialsFromContext(r.Context()).Username,
|
||||
CsrfToken: csrfToken(r.Context()),
|
||||
PageTitle: title,
|
||||
ActivePage: page,
|
||||
Capabilities: CapabilitiesFromContext(r.Context()),
|
||||
CfgUnsaved: cookieErr == nil,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,11 +9,21 @@ import (
|
||||
"github.com/kernelkit/webui/internal/restconf"
|
||||
)
|
||||
|
||||
const cfgUnsavedCookie = "cfg-unsaved"
|
||||
|
||||
// ConfigureHandler manages the candidate datastore lifecycle.
|
||||
type ConfigureHandler struct {
|
||||
RC restconf.Fetcher
|
||||
}
|
||||
|
||||
func setCfgUnsaved(w http.ResponseWriter) {
|
||||
http.SetCookie(w, &http.Cookie{Name: cfgUnsavedCookie, Value: "1", Path: "/", MaxAge: 86400, SameSite: http.SameSiteLaxMode})
|
||||
}
|
||||
|
||||
func clearCfgUnsaved(w http.ResponseWriter) {
|
||||
http.SetCookie(w, &http.Cookie{Name: cfgUnsavedCookie, Value: "", Path: "/", MaxAge: -1, SameSite: http.SameSiteLaxMode})
|
||||
}
|
||||
|
||||
// Enter copies running → candidate, initialising a fresh edit session.
|
||||
// Called when the user opens the Configure accordion.
|
||||
// POST /configure/enter
|
||||
@@ -27,6 +37,7 @@ func (h *ConfigureHandler) Enter(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
// Apply copies candidate → running, activating all staged changes atomically.
|
||||
// Sets the cfg-unsaved cookie so the persistent banner appears until startup is saved.
|
||||
// POST /configure/apply
|
||||
func (h *ConfigureHandler) Apply(w http.ResponseWriter, r *http.Request) {
|
||||
if err := h.RC.CopyDatastore(r.Context(), "candidate", "running"); err != nil {
|
||||
@@ -34,7 +45,8 @@ func (h *ConfigureHandler) Apply(w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, "Could not apply configuration: "+err.Error(), http.StatusBadGateway)
|
||||
return
|
||||
}
|
||||
w.Header().Set("HX-Redirect", "/")
|
||||
setCfgUnsaved(w)
|
||||
w.Header().Set("HX-Refresh", "true")
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
@@ -43,13 +55,14 @@ func (h *ConfigureHandler) Apply(w http.ResponseWriter, r *http.Request) {
|
||||
func (h *ConfigureHandler) Abort(w http.ResponseWriter, r *http.Request) {
|
||||
if err := h.RC.CopyDatastore(r.Context(), "running", "candidate"); err != nil {
|
||||
log.Printf("configure abort: %v", err)
|
||||
// Best-effort reset; redirect regardless so the user can get out.
|
||||
// Best-effort reset; refresh regardless.
|
||||
}
|
||||
w.Header().Set("HX-Redirect", "/")
|
||||
w.Header().Set("HX-Refresh", "true")
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// ApplyAndSave copies candidate → running then running → startup in one step.
|
||||
// Clears the cfg-unsaved cookie.
|
||||
// POST /configure/apply-and-save
|
||||
func (h *ConfigureHandler) ApplyAndSave(w http.ResponseWriter, r *http.Request) {
|
||||
if err := h.RC.CopyDatastore(r.Context(), "candidate", "running"); err != nil {
|
||||
@@ -62,6 +75,21 @@ func (h *ConfigureHandler) ApplyAndSave(w http.ResponseWriter, r *http.Request)
|
||||
http.Error(w, "Could not save configuration: "+err.Error(), http.StatusBadGateway)
|
||||
return
|
||||
}
|
||||
w.Header().Set("HX-Redirect", "/")
|
||||
clearCfgUnsaved(w)
|
||||
w.Header().Set("HX-Refresh", "true")
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// Save copies running → startup, persisting the active configuration.
|
||||
// Clears the cfg-unsaved cookie and does a full-page refresh so the banner disappears.
|
||||
// POST /configure/save
|
||||
func (h *ConfigureHandler) Save(w http.ResponseWriter, r *http.Request) {
|
||||
if err := h.RC.CopyDatastore(r.Context(), "running", "startup"); err != nil {
|
||||
log.Printf("configure save: %v", err)
|
||||
http.Error(w, "Could not save configuration: "+err.Error(), http.StatusBadGateway)
|
||||
return
|
||||
}
|
||||
clearCfgUnsaved(w)
|
||||
w.Header().Set("HX-Refresh", "true")
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/kernelkit/webui/internal/restconf"
|
||||
"github.com/kernelkit/webui/internal/schema"
|
||||
)
|
||||
|
||||
// ─── RESTCONF JSON types (candidate datastore) ────────────────────────────────
|
||||
@@ -70,6 +71,7 @@ type cfgDNSAddrJSON struct {
|
||||
|
||||
type cfgSystemPageData struct {
|
||||
PageData
|
||||
Loading bool // true while YANG schema is still downloading
|
||||
Error string
|
||||
Hostname string
|
||||
Contact string
|
||||
@@ -79,6 +81,11 @@ type cfgSystemPageData struct {
|
||||
DNS cfgDNSJSON
|
||||
MotdBanner string // decoded from YANG binary
|
||||
TextEditor string // e.g. "infix-system:emacs"
|
||||
|
||||
// Schema-enriched fields — only populated when Loading is false.
|
||||
TextEditorOptions []schema.IdentityOption
|
||||
TimezoneOptions []string // bare timezone names for select
|
||||
Desc map[string]string // leaf name → YANG description
|
||||
}
|
||||
|
||||
// ─── Handler ─────────────────────────────────────────────────────────────────
|
||||
@@ -87,6 +94,7 @@ type cfgSystemPageData struct {
|
||||
type ConfigureSystemHandler struct {
|
||||
Template *template.Template
|
||||
RC restconf.Fetcher
|
||||
Schema *schema.Cache
|
||||
}
|
||||
|
||||
const candidatePath = "/ds/ietf-datastores:candidate"
|
||||
@@ -127,6 +135,32 @@ func (h *ConfigureSystemHandler) Overview(w http.ResponseWriter, r *http.Request
|
||||
data.TextEditor = s.TextEditor
|
||||
}
|
||||
|
||||
mgr := h.Schema.Manager()
|
||||
data.Loading = mgr == nil
|
||||
if mgr != nil {
|
||||
const sys = "/ietf-system:system"
|
||||
data.TextEditorOptions = schema.OptionsFor(mgr, sys+"/infix-system:text-editor")
|
||||
if data.TextEditor == "" {
|
||||
for _, opt := range data.TextEditorOptions {
|
||||
if opt.IsDefault {
|
||||
data.TextEditor = opt.Value
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
data.Desc = map[string]string{
|
||||
"hostname": schema.DescriptionOf(mgr, sys+"/hostname"),
|
||||
"contact": schema.DescriptionOf(mgr, sys+"/contact"),
|
||||
"location": schema.DescriptionOf(mgr, sys+"/location"),
|
||||
"timezone": schema.DescriptionOf(mgr, sys+"/clock/timezone-name"),
|
||||
"text-editor": schema.DescriptionOf(mgr, sys+"/infix-system:text-editor"),
|
||||
"motd-banner": schema.DescriptionOf(mgr, sys+"/infix-system:motd-banner"),
|
||||
}
|
||||
for _, opt := range schema.OptionsFor(mgr, sys+"/clock/timezone-name") {
|
||||
data.TimezoneOptions = append(data.TimezoneOptions, schema.StripModulePrefix(opt.Value))
|
||||
}
|
||||
}
|
||||
|
||||
tmplName := "configure-system.html"
|
||||
if r.Header.Get("HX-Request") == "true" {
|
||||
tmplName = "content"
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestUnwrapForPutContainer(t *testing.T) {
|
||||
raw := []byte(`{"infix-services:web":{"certificate":"gencert","enabled":true}}`)
|
||||
var doc map[string]any
|
||||
if err := json.Unmarshal(raw, &doc); err != nil {
|
||||
t.Fatalf("unmarshal: %v", err)
|
||||
}
|
||||
obj, wrap, err := unwrapForPut(doc, "/infix-services:web")
|
||||
if err != nil {
|
||||
t.Fatalf("unwrap: %v", err)
|
||||
}
|
||||
if wrap != "infix-services:web" {
|
||||
t.Errorf("wrap = %q, want %q", wrap, "infix-services:web")
|
||||
}
|
||||
if obj["certificate"] != "gencert" {
|
||||
t.Errorf("certificate = %v, want gencert", obj["certificate"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnwrapForPutListInstance(t *testing.T) {
|
||||
raw := []byte(`{"ietf-interfaces:interfaces":{"interface":[{"name":"lan3","type":"ethernet","infix-interfaces:bridge-port":{"bridge":"br0"}}]}}`)
|
||||
var doc map[string]any
|
||||
if err := json.Unmarshal(raw, &doc); err != nil {
|
||||
t.Fatalf("unmarshal: %v", err)
|
||||
}
|
||||
obj, wrap, err := unwrapForPut(doc, "/ietf-interfaces:interfaces/interface=lan3")
|
||||
if err != nil {
|
||||
t.Fatalf("unwrap: %v", err)
|
||||
}
|
||||
if wrap != "interface" {
|
||||
t.Errorf("wrap = %q, want %q", wrap, "interface")
|
||||
}
|
||||
if obj["name"] != "lan3" {
|
||||
t.Errorf("name = %v, want lan3", obj["name"])
|
||||
}
|
||||
if _, ok := obj["infix-interfaces:bridge-port"]; !ok {
|
||||
t.Errorf("expected bridge-port key in unwrapped entry, got: %v", obj)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnwrapForPutEmpty(t *testing.T) {
|
||||
doc := map[string]any{}
|
||||
if _, _, err := unwrapForPut(doc, "/x"); err == nil {
|
||||
t.Errorf("expected error for empty doc")
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractFieldValuesWithSibling(t *testing.T) {
|
||||
data := []byte(`{
|
||||
"ietf-interfaces:interfaces": {
|
||||
"interface": [
|
||||
{"name": "lan1", "type": "ethernet"},
|
||||
{"name": "br0", "type": "bridge", "infix-interfaces:bridge": {"vlans": {}}},
|
||||
{"name": "br1", "type": "bridge", "infix-interfaces:bridge": {}},
|
||||
{"name": "lan3", "type": "ethernet", "infix-interfaces:bridge-port": {"bridge":"br0"}}
|
||||
]
|
||||
}
|
||||
}`)
|
||||
got := extractFieldValuesWithSibling(data, "name", "bridge")
|
||||
want := []string{"br0", "br1"}
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Errorf("got %v, want %v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
// TestExtractFieldValuesNestedRecord guards against the bug where a
|
||||
// leafref dropdown for /ietf-keystore:keystore/asymmetric-keys/
|
||||
// asymmetric-key/name picked up "self-signed" — which lives in the
|
||||
// nested certificates/certificate[]/name list inside each asymmetric
|
||||
// key entry. The walker has to treat the asymmetric-key map as a
|
||||
// record boundary and not descend into siblings once it has found
|
||||
// the field there.
|
||||
func TestExtractFieldValuesNestedRecord(t *testing.T) {
|
||||
data := []byte(`{
|
||||
"ietf-keystore:keystore": {
|
||||
"asymmetric-keys": {
|
||||
"asymmetric-key": [
|
||||
{
|
||||
"name": "gencert",
|
||||
"certificates": {
|
||||
"certificate": [
|
||||
{"name": "self-signed"}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "genkey",
|
||||
"certificates": {}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}`)
|
||||
got := extractFieldValues(data, "name")
|
||||
want := []string{"gencert", "genkey"}
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Errorf("got %v, want %v", got, want)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,223 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/kernelkit/webui/internal/restconf"
|
||||
"github.com/kernelkit/webui/internal/schema"
|
||||
)
|
||||
|
||||
const candidateDS = "/ds/ietf-datastores:candidate"
|
||||
|
||||
// DataHandler serves GET /api/data — raw RESTCONF JSON for a path.
|
||||
// PUT and DELETE are handled by TreeHandler to share template rendering.
|
||||
type DataHandler struct {
|
||||
RC restconf.Fetcher
|
||||
Schema *schema.Cache
|
||||
}
|
||||
|
||||
// Get serves GET /api/data?path=...
|
||||
// Returns the raw RESTCONF JSON subtree from candidate (falls back to running).
|
||||
func (h *DataHandler) Get(w http.ResponseWriter, r *http.Request) {
|
||||
path := r.URL.Query().Get("path")
|
||||
if path == "" {
|
||||
http.Error(w, "path required", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
data, err := h.RC.GetRaw(r.Context(), candidateDS+path)
|
||||
if err != nil {
|
||||
data, err = h.RC.GetRaw(r.Context(), "/data"+path)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write(data)
|
||||
}
|
||||
|
||||
// navigateToNode traverses a RESTCONF JSON response using path segments to
|
||||
// reach the target node. The server always wraps responses in the full
|
||||
// module-root hierarchy ({"module:root": {"list": [{...}]}}), so we walk each
|
||||
// segment; for list segments with a key predicate (e.g. "interface=eth0") we
|
||||
// enter the array and take the first (only) element.
|
||||
// Returns nil when any segment cannot be found.
|
||||
func navigateToNode(data []byte, path string) json.RawMessage {
|
||||
segs := strings.Split(strings.TrimPrefix(path, "/"), "/")
|
||||
current := json.RawMessage(data)
|
||||
for _, seg := range segs {
|
||||
hasPred := strings.ContainsAny(seg, "[=")
|
||||
_, localName := splitModPrefix(stripModPredicate(seg))
|
||||
var obj map[string]json.RawMessage
|
||||
if err := json.Unmarshal(current, &obj); err != nil {
|
||||
return nil
|
||||
}
|
||||
var found json.RawMessage
|
||||
for k, v := range obj {
|
||||
_, local := splitModPrefix(k)
|
||||
if local == localName {
|
||||
found = v
|
||||
break
|
||||
}
|
||||
}
|
||||
if found == nil {
|
||||
return nil
|
||||
}
|
||||
if hasPred {
|
||||
var arr []json.RawMessage
|
||||
if err := json.Unmarshal(found, &arr); err != nil || len(arr) == 0 {
|
||||
return nil
|
||||
}
|
||||
// Extract key value from predicate: "name=eth0" → "eth0".
|
||||
keyVal := ""
|
||||
if i := strings.IndexByte(seg, '='); i >= 0 {
|
||||
keyVal = seg[i+1:]
|
||||
}
|
||||
matched := arr[0] // fallback: first element
|
||||
if keyVal != "" {
|
||||
for _, elem := range arr {
|
||||
var row map[string]json.RawMessage
|
||||
if json.Unmarshal(elem, &row) != nil {
|
||||
continue
|
||||
}
|
||||
for _, v := range row {
|
||||
var s string
|
||||
if json.Unmarshal(v, &s) == nil && s == keyVal {
|
||||
matched = elem
|
||||
goto nextSeg
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
nextSeg:
|
||||
found = matched
|
||||
}
|
||||
current = found
|
||||
}
|
||||
return current
|
||||
}
|
||||
|
||||
// flattenNodeValues extracts direct scalar leaf values from a JSON object,
|
||||
// returning a map of bare-name → string. Nested objects and arrays (which
|
||||
// represent sub-containers and sub-lists) are silently skipped.
|
||||
func flattenNodeValues(raw json.RawMessage) map[string]string {
|
||||
var obj map[string]json.RawMessage
|
||||
if err := json.Unmarshal(raw, &obj); err != nil {
|
||||
return nil
|
||||
}
|
||||
result := make(map[string]string, len(obj))
|
||||
for k, v := range obj {
|
||||
if len(v) > 0 && v[0] == '[' {
|
||||
// YANG empty type is encoded as [null] in JSON (RFC 7951 §6.9).
|
||||
// Represent presence as "true"; skip real arrays (sub-lists).
|
||||
trimmed := bytes.TrimSpace(v)
|
||||
if bytes.Equal(trimmed, []byte("[null]")) {
|
||||
_, local := splitModPrefix(k)
|
||||
result[local] = "true"
|
||||
}
|
||||
continue
|
||||
}
|
||||
if len(v) > 0 && v[0] == '{' {
|
||||
continue // sub-container — not a direct leaf
|
||||
}
|
||||
_, local := splitModPrefix(k)
|
||||
result[local] = extractScalar(v)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// stripModPredicate removes both a module prefix and a RESTCONF key predicate
|
||||
// from a path segment, e.g. "ietf-interfaces:interface=eth0" → "interface".
|
||||
func stripModPredicate(seg string) string {
|
||||
if i := strings.IndexByte(seg, '['); i >= 0 {
|
||||
seg = seg[:i]
|
||||
}
|
||||
if i := strings.IndexByte(seg, '='); i >= 0 {
|
||||
seg = seg[:i]
|
||||
}
|
||||
return seg
|
||||
}
|
||||
|
||||
// extractLeafValue unwraps the single-key RESTCONF JSON envelope that wraps a
|
||||
// leaf value: {"module:name": <value>} → string representation of <value>.
|
||||
// Recursively unwraps single-key nested objects so that a response like
|
||||
// {"module:parent": {"certificate": "gencert"}} → "gencert".
|
||||
func extractLeafValue(data []byte) string {
|
||||
var m map[string]json.RawMessage
|
||||
if err := json.Unmarshal(data, &m); err != nil {
|
||||
return ""
|
||||
}
|
||||
for _, raw := range m {
|
||||
return extractScalar(raw)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// extractScalar converts a JSON value to a display string.
|
||||
// Single-key objects are recursively unwrapped (RESTCONF sometimes wraps leaf
|
||||
// values in a choice/case or container envelope).
|
||||
func extractScalar(raw json.RawMessage) string {
|
||||
// Plain string.
|
||||
var s string
|
||||
if json.Unmarshal(raw, &s) == nil {
|
||||
return s
|
||||
}
|
||||
// Bool or number (unquoted JSON token).
|
||||
v := string(raw)
|
||||
if v == "true" || v == "false" || (len(v) > 0 && (v[0] == '-' || (v[0] >= '0' && v[0] <= '9'))) {
|
||||
return v
|
||||
}
|
||||
// Single-key object: unwrap one level and recurse.
|
||||
var nested map[string]json.RawMessage
|
||||
if err := json.Unmarshal(raw, &nested); err == nil && len(nested) == 1 {
|
||||
for _, inner := range nested {
|
||||
return extractScalar(inner)
|
||||
}
|
||||
}
|
||||
// Fallback: return the raw token (strips outer quotes if present).
|
||||
if len(v) >= 2 && v[0] == '"' && v[len(v)-1] == '"' {
|
||||
return v[1 : len(v)-1]
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
// coerceLeafValue converts the raw form string to the JSON type that RESTCONF
|
||||
// expects for the leaf based on the schema Node type.
|
||||
func coerceLeafValue(raw string, node *schema.Node) any {
|
||||
if node == nil || node.Type == nil {
|
||||
return raw
|
||||
}
|
||||
switch node.Type.Kind {
|
||||
case "boolean":
|
||||
return raw == "on" || raw == "true"
|
||||
case "int8", "int16", "int32", "int64",
|
||||
"uint8", "uint16", "uint32", "uint64":
|
||||
var n int64
|
||||
if _, err := fmt.Sscanf(raw, "%d", &n); err == nil {
|
||||
return n
|
||||
}
|
||||
case "binary":
|
||||
// Strip whitespace that textarea input may add (trailing newlines, spaces).
|
||||
// If the cleaned value is valid base64, send it as-is; otherwise encode.
|
||||
cleaned := strings.Map(func(r rune) rune {
|
||||
if r == ' ' || r == '\t' || r == '\n' || r == '\r' {
|
||||
return -1
|
||||
}
|
||||
return r
|
||||
}, raw)
|
||||
if _, err := base64.StdEncoding.DecodeString(cleaned); err == nil {
|
||||
return cleaned
|
||||
}
|
||||
return base64.StdEncoding.EncodeToString([]byte(raw))
|
||||
}
|
||||
return raw
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -187,6 +187,40 @@ func (c *Client) writeJSON(ctx context.Context, method, path string, body any) e
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetYANG downloads a YANG module file. YANG files are served outside the
|
||||
// RESTCONF tree at /yang/{name}@{revision}.yang on the device host — mirroring
|
||||
// the approach used by the Infamy test framework (yang_url = base_url + "/yang").
|
||||
func (c *Client) GetYANG(ctx context.Context, name, revision string) ([]byte, error) {
|
||||
// Strip the /restconf suffix to get the host-level base URL.
|
||||
yangBase := c.baseURL
|
||||
if i := strings.LastIndex(yangBase, "/restconf"); i >= 0 {
|
||||
yangBase = yangBase[:i]
|
||||
}
|
||||
|
||||
path := "/yang/" + name
|
||||
if revision != "" {
|
||||
path += "@" + revision
|
||||
}
|
||||
path += ".yang"
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, yangBase+path, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
creds := CredentialsFromContext(ctx)
|
||||
req.SetBasicAuth(creds.Username, creds.Password)
|
||||
|
||||
resp, err := c.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("restconf request failed: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, parseError(resp)
|
||||
}
|
||||
return io.ReadAll(resp.Body)
|
||||
}
|
||||
|
||||
// GetRaw fetches a RESTCONF resource and returns the raw JSON bytes.
|
||||
func (c *Client) GetRaw(ctx context.Context, path string) ([]byte, error) {
|
||||
resp, err := c.doRequest(ctx, http.MethodGet, path)
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// AuthError is returned when RESTCONF rejects credentials (401/403).
|
||||
@@ -35,7 +36,7 @@ func (e *Error) Error() string {
|
||||
|
||||
// parseError reads a RESTCONF error response body and returns an *Error.
|
||||
func parseError(resp *http.Response) error {
|
||||
body, _ := io.ReadAll(io.LimitReader(resp.Body, 4096))
|
||||
body, _ := io.ReadAll(io.LimitReader(resp.Body, 8192))
|
||||
|
||||
re := &Error{StatusCode: resp.StatusCode}
|
||||
|
||||
@@ -45,16 +46,28 @@ func parseError(resp *http.Response) error {
|
||||
Error []struct {
|
||||
ErrorType string `json:"error-type"`
|
||||
ErrorTag string `json:"error-tag"`
|
||||
ErrorPath string `json:"error-path"`
|
||||
ErrorMessage string `json:"error-message"`
|
||||
ErrorInfo any `json:"error-info"`
|
||||
} `json:"error"`
|
||||
} `json:"ietf-restconf:errors"`
|
||||
}
|
||||
|
||||
if json.Unmarshal(body, &envelope) == nil && len(envelope.Errors.Error) > 0 {
|
||||
first := envelope.Errors.Error[0]
|
||||
re.Type = first.ErrorType
|
||||
re.Tag = first.ErrorTag
|
||||
re.Message = first.ErrorMessage
|
||||
var parts []string
|
||||
for _, e := range envelope.Errors.Error {
|
||||
msg := e.ErrorMessage
|
||||
if msg == "" {
|
||||
msg = e.ErrorTag
|
||||
}
|
||||
if e.ErrorPath != "" {
|
||||
msg += " (path: " + e.ErrorPath + ")"
|
||||
}
|
||||
parts = append(parts, msg)
|
||||
}
|
||||
re.Type = envelope.Errors.Error[0].ErrorType
|
||||
re.Tag = envelope.Errors.Error[0].ErrorTag
|
||||
re.Message = strings.Join(parts, "; ")
|
||||
} else {
|
||||
re.Message = http.StatusText(resp.StatusCode)
|
||||
}
|
||||
|
||||
@@ -12,6 +12,9 @@ import (
|
||||
type Fetcher interface {
|
||||
Get(ctx context.Context, path string, target any) error
|
||||
GetRaw(ctx context.Context, path string) ([]byte, error)
|
||||
// GetYANG downloads a YANG module file from /yang/{name}@{revision}.yang
|
||||
// on the device host (outside the /restconf tree).
|
||||
GetYANG(ctx context.Context, name, revision string) ([]byte, error)
|
||||
Post(ctx context.Context, path string) error
|
||||
PostJSON(ctx context.Context, path string, body any) error
|
||||
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
package schema
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/kernelkit/webui/internal/restconf"
|
||||
)
|
||||
|
||||
// ModuleInfo identifies a YANG module by name and revision.
|
||||
type ModuleInfo struct {
|
||||
Name string
|
||||
Revision string
|
||||
}
|
||||
|
||||
func (m ModuleInfo) filename() string {
|
||||
if m.Revision == "" {
|
||||
return m.Name + ".yang"
|
||||
}
|
||||
return m.Name + "@" + m.Revision + ".yang"
|
||||
}
|
||||
|
||||
// rfc7895ModulesState is the RFC 7895 /modules-state response structure.
|
||||
type rfc7895ModulesState struct {
|
||||
ModulesState struct {
|
||||
Module []struct {
|
||||
Name string `json:"name"`
|
||||
Revision string `json:"revision"`
|
||||
Submodule []struct {
|
||||
Name string `json:"name"`
|
||||
Revision string `json:"revision"`
|
||||
} `json:"submodule"`
|
||||
} `json:"module"`
|
||||
} `json:"ietf-yang-library:modules-state"`
|
||||
}
|
||||
|
||||
// rfc8525YangLibrary is the RFC 8525 /yang-library response structure (fallback).
|
||||
type rfc8525YangLibrary struct {
|
||||
YangLibrary struct {
|
||||
ModuleSet []struct {
|
||||
Module []struct {
|
||||
Name string `json:"name"`
|
||||
Revision string `json:"revision"`
|
||||
} `json:"module"`
|
||||
} `json:"module-set"`
|
||||
} `json:"ietf-yang-library:yang-library"`
|
||||
}
|
||||
|
||||
// FetchModules downloads any YANG files not already cached in cacheDir.
|
||||
// It first tries the RFC 7895 modules-state endpoint, then falls back to
|
||||
// the RFC 8525 yang-library endpoint (same as capabilities.go).
|
||||
// Each module and its submodules are downloaded from /yang/{name}@{rev}.yang.
|
||||
func FetchModules(ctx context.Context, rc restconf.Fetcher, cacheDir string) ([]ModuleInfo, error) {
|
||||
if err := os.MkdirAll(cacheDir, 0750); err != nil {
|
||||
return nil, fmt.Errorf("schema: create cache dir: %w", err)
|
||||
}
|
||||
|
||||
modules, err := listModules(ctx, rc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var downloaded []ModuleInfo
|
||||
for _, m := range modules {
|
||||
if err := downloadIfMissing(ctx, rc, cacheDir, m); err != nil {
|
||||
log.Printf("schema: skip %s: %v", m.filename(), err)
|
||||
continue
|
||||
}
|
||||
downloaded = append(downloaded, m)
|
||||
}
|
||||
return downloaded, nil
|
||||
}
|
||||
|
||||
// listModules queries the device for the list of implemented YANG modules.
|
||||
func listModules(ctx context.Context, rc restconf.Fetcher) ([]ModuleInfo, error) {
|
||||
// Try RFC 7895 modules-state first.
|
||||
var ms rfc7895ModulesState
|
||||
if err := rc.Get(ctx, "/data/ietf-yang-library:modules-state", &ms); err == nil {
|
||||
var mods []ModuleInfo
|
||||
for _, m := range ms.ModulesState.Module {
|
||||
mods = append(mods, ModuleInfo{Name: m.Name, Revision: m.Revision})
|
||||
for _, sub := range m.Submodule {
|
||||
mods = append(mods, ModuleInfo{Name: sub.Name, Revision: sub.Revision})
|
||||
}
|
||||
}
|
||||
if len(mods) > 0 {
|
||||
return mods, nil
|
||||
}
|
||||
}
|
||||
|
||||
// Fall back to RFC 8525 yang-library.
|
||||
var yl rfc8525YangLibrary
|
||||
if err := rc.Get(ctx, "/data/ietf-yang-library:yang-library", &yl); err != nil {
|
||||
return nil, fmt.Errorf("schema: list modules: %w", err)
|
||||
}
|
||||
var mods []ModuleInfo
|
||||
for _, ms := range yl.YangLibrary.ModuleSet {
|
||||
for _, m := range ms.Module {
|
||||
mods = append(mods, ModuleInfo{Name: m.Name, Revision: m.Revision})
|
||||
}
|
||||
}
|
||||
return mods, nil
|
||||
}
|
||||
|
||||
// downloadIfMissing fetches a single YANG file from the device if not cached.
|
||||
func downloadIfMissing(ctx context.Context, rc restconf.Fetcher, cacheDir string, m ModuleInfo) error {
|
||||
dest := filepath.Join(cacheDir, m.filename())
|
||||
if _, err := os.Stat(dest); err == nil {
|
||||
return nil // already cached
|
||||
}
|
||||
|
||||
data, err := rc.GetYANG(ctx, m.Name, m.Revision)
|
||||
if err != nil {
|
||||
return fmt.Errorf("GET /yang/%s: %w", m.filename(), err)
|
||||
}
|
||||
|
||||
if err := os.WriteFile(dest, data, 0640); err != nil {
|
||||
return fmt.Errorf("write %s: %w", dest, err)
|
||||
}
|
||||
log.Printf("schema: cached %s", m.filename())
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package schema
|
||||
|
||||
import "strings"
|
||||
|
||||
// IdentityOption is a schema-resolved identity/enum value for use in select dropdowns.
|
||||
type IdentityOption struct {
|
||||
Value string // full identity, e.g. "infix-system:clish" — submitted to RESTCONF
|
||||
Label string // display label with module prefix stripped, e.g. "clish"
|
||||
IsDefault bool // true if this matches the leaf's YANG default
|
||||
}
|
||||
|
||||
// StripModulePrefix strips the "module:" prefix from an identity or enum value.
|
||||
func StripModulePrefix(v string) string {
|
||||
if i := strings.LastIndex(v, ":"); i >= 0 {
|
||||
return v[i+1:]
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
// OptionsFor returns IdentityOption entries for the identityref or enumeration
|
||||
// leaf at path. Returns nil when schema is unavailable or the leaf has no options.
|
||||
func OptionsFor(mgr *Manager, path string) []IdentityOption {
|
||||
if mgr == nil {
|
||||
return nil
|
||||
}
|
||||
node, err := mgr.NodeAt(path)
|
||||
if err != nil || node == nil || node.Type == nil {
|
||||
return nil
|
||||
}
|
||||
values := node.Type.Identities
|
||||
if len(values) == 0 {
|
||||
values = node.Type.Enums
|
||||
}
|
||||
opts := make([]IdentityOption, 0, len(values))
|
||||
for _, v := range values {
|
||||
opts = append(opts, IdentityOption{
|
||||
Value: v,
|
||||
Label: StripModulePrefix(v),
|
||||
IsDefault: node.Default != "" && v == node.Default,
|
||||
})
|
||||
}
|
||||
return opts
|
||||
}
|
||||
|
||||
// DescriptionOf returns the YANG description for the leaf at path, or "".
|
||||
func DescriptionOf(mgr *Manager, path string) string {
|
||||
if mgr == nil {
|
||||
return ""
|
||||
}
|
||||
node, err := mgr.NodeAt(path)
|
||||
if err != nil || node == nil {
|
||||
return ""
|
||||
}
|
||||
return node.Description
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
package schema
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
const testTargetYang = `
|
||||
module test-target {
|
||||
yang-version 1.1;
|
||||
namespace "urn:test:target";
|
||||
prefix tt;
|
||||
|
||||
container interfaces {
|
||||
list interface {
|
||||
key name;
|
||||
leaf name { type string; }
|
||||
leaf type { type string; }
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const testReferYang = `
|
||||
module test-refer {
|
||||
yang-version 1.1;
|
||||
namespace "urn:test:refer";
|
||||
prefix tr;
|
||||
|
||||
import test-target { prefix tt; }
|
||||
|
||||
container config {
|
||||
leaf port {
|
||||
type leafref {
|
||||
path "/tt:interfaces/tt:interface/tt:name";
|
||||
}
|
||||
must "deref(.)/../tt:type" {
|
||||
error-message "target must have type";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
func TestLeafrefCanonicalize(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
mustWrite(t, dir, "test-target.yang", testTargetYang)
|
||||
mustWrite(t, dir, "test-refer.yang", testReferYang)
|
||||
|
||||
mgr, err := Load(dir)
|
||||
if err != nil {
|
||||
t.Fatalf("new manager: %v", err)
|
||||
}
|
||||
|
||||
n, err := mgr.NodeAt("/test-refer:config/port")
|
||||
if err != nil {
|
||||
t.Fatalf("NodeAt: %v", err)
|
||||
}
|
||||
if n == nil || n.Type == nil {
|
||||
t.Fatalf("missing node or type: %+v", n)
|
||||
}
|
||||
t.Logf("Leafref path = %q", n.Type.Leafref)
|
||||
t.Logf("LeafrefSibling = %q", n.Type.LeafrefSibling)
|
||||
|
||||
if strings.Contains(n.Type.Leafref, "tt:") {
|
||||
t.Errorf("Leafref still has unresolved prefix: %q", n.Type.Leafref)
|
||||
}
|
||||
want := "/test-target:interfaces/interface/name"
|
||||
if n.Type.Leafref != want {
|
||||
t.Errorf("Leafref = %q, want %q (RESTCONF canonical, no repeated module prefixes)", n.Type.Leafref, want)
|
||||
}
|
||||
if n.Type.LeafrefSibling != "type" {
|
||||
t.Errorf("LeafrefSibling = %q, want \"type\"", n.Type.LeafrefSibling)
|
||||
}
|
||||
}
|
||||
|
||||
func mustWrite(t *testing.T, dir, name, content string) {
|
||||
if err := os.WriteFile(filepath.Join(dir, name), []byte(content), 0644); err != nil {
|
||||
t.Fatalf("write %s: %v", name, err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestLeafrefParentIsList verifies that NodeAt reports the correct Kind
|
||||
// for each segment of a canonical leafref path so fetchLeafrefValues can
|
||||
// walk up past the list to its container parent (RESTCONF refuses bare
|
||||
// list paths). Mirrors the bridge-port/bridge case where the path
|
||||
// ends in /interfaces/interface/name and we have to GET /interfaces.
|
||||
func TestLeafrefParentIsList(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
mustWrite(t, dir, "test-target.yang", testTargetYang)
|
||||
mgr, err := Load(dir)
|
||||
if err != nil {
|
||||
t.Fatalf("load: %v", err)
|
||||
}
|
||||
cases := []struct{ path, kind string }{
|
||||
{"/test-target:interfaces", "container"},
|
||||
{"/test-target:interfaces/interface", "list"},
|
||||
{"/test-target:interfaces/interface/name", "leaf"},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
n, err := mgr.NodeAt(tc.path)
|
||||
if err != nil || n == nil {
|
||||
t.Fatalf("NodeAt(%q): %v", tc.path, err)
|
||||
}
|
||||
if n.Kind != tc.kind {
|
||||
t.Errorf("NodeAt(%q).Kind = %q, want %q", tc.path, n.Kind, tc.kind)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const testBridgeYang = `
|
||||
module test-br {
|
||||
yang-version 1.1;
|
||||
namespace "urn:test:br";
|
||||
prefix tb;
|
||||
|
||||
import test-target { prefix tt; }
|
||||
|
||||
grouping br-port {
|
||||
leaf bridge {
|
||||
type leafref {
|
||||
path "/tt:interfaces/tt:interface/tt:name";
|
||||
}
|
||||
must "deref(.)/../bridge and not(. = ../../tt:name)";
|
||||
}
|
||||
}
|
||||
|
||||
augment "/tt:interfaces/tt:interface" {
|
||||
container bridge-port {
|
||||
uses br-port;
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
func TestLeafrefBridgeAugment(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
mustWrite(t, dir, "test-target.yang", testTargetYang)
|
||||
mustWrite(t, dir, "test-br.yang", testBridgeYang)
|
||||
mgr, err := Load(dir)
|
||||
if err != nil {
|
||||
t.Fatalf("load: %v", err)
|
||||
}
|
||||
n, err := mgr.NodeAt("/test-target:interfaces/interface/test-br:bridge-port/bridge")
|
||||
if err != nil {
|
||||
t.Logf("first NodeAt failed: %v", err)
|
||||
n, err = mgr.NodeAt("/test-target:interfaces/test-target:interface/test-br:bridge-port/test-br:bridge")
|
||||
if err != nil {
|
||||
t.Fatalf("NodeAt: %v", err)
|
||||
}
|
||||
}
|
||||
if n == nil || n.Type == nil {
|
||||
t.Fatalf("missing: %+v", n)
|
||||
}
|
||||
t.Logf("Leafref = %q", n.Type.Leafref)
|
||||
t.Logf("LeafrefSibling = %q", n.Type.LeafrefSibling)
|
||||
if n.Type.LeafrefSibling != "bridge" {
|
||||
t.Errorf("LeafrefSibling = %q, want \"bridge\"", n.Type.LeafrefSibling)
|
||||
}
|
||||
want := "/test-target:interfaces/interface/name"
|
||||
if n.Type.Leafref != want {
|
||||
t.Errorf("Leafref = %q, want %q", n.Type.Leafref, want)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,899 @@
|
||||
package schema
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/openconfig/goyang/pkg/yang"
|
||||
)
|
||||
|
||||
// Manager holds a fully processed goyang module set and provides schema queries.
|
||||
// All modules are loaded before Process() is called so that cross-module
|
||||
// identityref resolution and augments work correctly.
|
||||
type Manager struct {
|
||||
ms *yang.Modules
|
||||
}
|
||||
|
||||
// Load parses all .yang files in yangDir and returns a Manager.
|
||||
// Errors from Process() that are non-fatal (e.g. unresolved augments for
|
||||
// modules that were not downloaded) are logged but do not abort loading.
|
||||
func Load(yangDir string) (*Manager, error) {
|
||||
ms := yang.NewModules()
|
||||
ms.Path = []string{yangDir}
|
||||
|
||||
entries, err := os.ReadDir(yangDir)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("schema: read yang dir: %w", err)
|
||||
}
|
||||
|
||||
for _, e := range entries {
|
||||
if e.IsDir() || !strings.HasSuffix(e.Name(), ".yang") {
|
||||
continue
|
||||
}
|
||||
if err := ms.Read(filepath.Join(yangDir, e.Name())); err != nil {
|
||||
// Non-fatal: goyang may still register the module partially.
|
||||
log.Printf("schema: parse (non-fatal) %s: %v", e.Name(), err)
|
||||
}
|
||||
}
|
||||
|
||||
errs := ms.Process()
|
||||
for _, err := range errs {
|
||||
// goyang v1.6.3 has known YANG 1.1 gaps (must/when substatements,
|
||||
// duplicate augments). These are non-fatal; log at debug level.
|
||||
log.Printf("schema: process (non-fatal): %v", err)
|
||||
}
|
||||
|
||||
return &Manager{ms: ms}, nil
|
||||
}
|
||||
|
||||
// Children returns the direct config-relevant child Nodes of the node at path.
|
||||
// Use path "/" for the top-level module list.
|
||||
// choice/case nodes are inlined transparently (their children are promoted).
|
||||
func (m *Manager) Children(path string) ([]*Node, error) {
|
||||
if path == "" || path == "/" {
|
||||
return m.topLevelNodes(), nil
|
||||
}
|
||||
e, err := m.entryAt(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if e.Dir == nil {
|
||||
return nil, nil
|
||||
}
|
||||
return dirToNodes(e.Dir, path), nil
|
||||
}
|
||||
|
||||
// ChildrenAll is like Children but includes config:false nodes, making it
|
||||
// suitable for the operational/status tree where state data reuses read-write
|
||||
// schema nodes. Only RPCs, notifications, anydata/anyxml, and deprecated or
|
||||
// obsolete nodes are excluded.
|
||||
func (m *Manager) ChildrenAll(path string) ([]*Node, error) {
|
||||
if path == "" || path == "/" {
|
||||
return m.topLevelNodesAll(), nil
|
||||
}
|
||||
e, err := m.entryAt(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if e.Dir == nil {
|
||||
return nil, nil
|
||||
}
|
||||
return dirToNodesAll(e.Dir, path), nil
|
||||
}
|
||||
|
||||
// NodeAt returns a Node for the YANG schema node at path (without children).
|
||||
func (m *Manager) NodeAt(path string) (*Node, error) {
|
||||
if path == "" || path == "/" {
|
||||
return &Node{Path: "/", Name: "", Kind: "container", Config: true}, nil
|
||||
}
|
||||
e, err := m.entryAt(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return entryToNode(e, path), nil
|
||||
}
|
||||
|
||||
// IdentitiesOf returns the names of all identities derived (directly or
|
||||
// transitively) from baseName. baseName may be "name" or "module:name".
|
||||
// Searches across all loaded modules.
|
||||
func (m *Manager) IdentitiesOf(baseName string) []string {
|
||||
// Strip module prefix if present; search by identity name.
|
||||
_, localName := splitPrefix(baseName)
|
||||
|
||||
for _, mod := range m.ms.Modules {
|
||||
for _, id := range mod.Identities() {
|
||||
if id.Name == localName {
|
||||
var names []string
|
||||
for _, v := range id.Values {
|
||||
names = append(names, v.Name)
|
||||
}
|
||||
sort.Strings(names)
|
||||
return names
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ResolveLeafref converts a YANG leafref path expression to an absolute schema
|
||||
// path that can be fetched via RESTCONF. contextPath is the schema path of the
|
||||
// leaf that holds the leafref (needed for relative `../` resolution).
|
||||
// Returns empty string if the path cannot be resolved.
|
||||
func (m *Manager) ResolveLeafref(leafrefPath, contextPath string) string {
|
||||
if strings.HasPrefix(leafrefPath, "/") {
|
||||
return normalizeLeafrefPath(leafrefPath)
|
||||
}
|
||||
// Relative path: resolve `../` against the context, stripping the leaf itself.
|
||||
parts := strings.Split(strings.TrimPrefix(contextPath, "/"), "/")
|
||||
if len(parts) > 0 {
|
||||
parts = parts[:len(parts)-1] // remove the leaf; start from its parent
|
||||
}
|
||||
for _, seg := range strings.Split(leafrefPath, "/") {
|
||||
switch seg {
|
||||
case "..":
|
||||
if len(parts) > 0 {
|
||||
parts = parts[:len(parts)-1]
|
||||
}
|
||||
case ".", "":
|
||||
// skip
|
||||
default:
|
||||
parts = append(parts, stripPredicate(seg))
|
||||
}
|
||||
}
|
||||
if len(parts) == 0 {
|
||||
return ""
|
||||
}
|
||||
return "/" + strings.Join(parts, "/")
|
||||
}
|
||||
|
||||
// normalizeLeafrefPath strips XPath predicates from each segment of an
|
||||
// absolute YANG leafref path so it can be used as a schema lookup path.
|
||||
func normalizeLeafrefPath(p string) string {
|
||||
segs := strings.Split(strings.TrimPrefix(p, "/"), "/")
|
||||
for i, s := range segs {
|
||||
segs[i] = stripPredicate(s)
|
||||
}
|
||||
return "/" + strings.Join(segs, "/")
|
||||
}
|
||||
|
||||
// canonicalizeLeafrefPath rewrites a YANG leafref path into the RESTCONF
|
||||
// canonical form (RFC 8040 §3.5.3): each segment's prefix alias is
|
||||
// resolved to its canonical module name, and the "module:" qualifier
|
||||
// is dropped from segments whose module matches the preceding
|
||||
// qualified segment. Unknown prefixes are left untouched.
|
||||
//
|
||||
// Example: "/if:interfaces/if:interface/if:name" with the `if` prefix
|
||||
// pointing at `ietf-interfaces` becomes
|
||||
// "/ietf-interfaces:interfaces/interface/name", which is what
|
||||
// nodeSegment() produces for ordinary paths and what RESTCONF expects.
|
||||
func canonicalizeLeafrefPath(node yang.Node, p string) string {
|
||||
if node == nil || p == "" {
|
||||
return p
|
||||
}
|
||||
segs := strings.Split(p, "/")
|
||||
var lastMod string
|
||||
for i, seg := range segs {
|
||||
if seg == "" || seg == ".." || seg == "." {
|
||||
continue
|
||||
}
|
||||
bare, pred := seg, ""
|
||||
if b := strings.IndexByte(seg, '['); b >= 0 {
|
||||
bare, pred = seg[:b], seg[b:]
|
||||
}
|
||||
colon := strings.IndexByte(bare, ':')
|
||||
if colon < 0 {
|
||||
// Unprefixed segments inherit the prevailing module.
|
||||
continue
|
||||
}
|
||||
prefix, rest := bare[:colon], bare[colon+1:]
|
||||
mod := yang.FindModuleByPrefix(node, prefix)
|
||||
if mod == nil {
|
||||
continue
|
||||
}
|
||||
modName := mod.Name
|
||||
if mod.BelongsTo != nil {
|
||||
modName = mod.BelongsTo.Name
|
||||
}
|
||||
if modName == lastMod {
|
||||
segs[i] = rest + pred
|
||||
} else {
|
||||
segs[i] = modName + ":" + rest + pred
|
||||
lastMod = modName
|
||||
}
|
||||
}
|
||||
return strings.Join(segs, "/")
|
||||
}
|
||||
|
||||
// derefSiblingPattern captures the `deref(.)/../<sibling>` XPath idiom
|
||||
// used by Infix YANG must statements to constrain a leafref's target
|
||||
// to objects that carry a specific sibling node (e.g. `bridge-port/
|
||||
// bridge`'s must says the referenced interface must have a `bridge`
|
||||
// container — i.e. it has to be a bridge). The capture group is the
|
||||
// local sibling name; any prefix (`tt:type`) is matched non-capturing
|
||||
// so callers don't have to strip it.
|
||||
var derefSiblingPattern = regexp.MustCompile(`deref\s*\(\s*\.\s*\)\s*/\s*\.\.\s*/\s*(?:[a-zA-Z][\w-]*:)?([a-zA-Z][\w-]*)`)
|
||||
|
||||
// leafrefSiblingFilter looks at the leaf's must statements (only Leaf
|
||||
// nodes carry must in goyang's Entry tree) for the deref(.)/../X
|
||||
// pattern and returns X, or "" when no such constraint exists.
|
||||
func leafrefSiblingFilter(e *yang.Entry) string {
|
||||
leaf, ok := e.Node.(*yang.Leaf)
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
for _, m := range leaf.Must {
|
||||
if m == nil {
|
||||
continue
|
||||
}
|
||||
if match := derefSiblingPattern.FindStringSubmatch(m.Name); len(match) == 2 {
|
||||
return match[1]
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// Default returns the YANG default value for the leaf at path, if any.
|
||||
func (m *Manager) Default(path string) (string, bool) {
|
||||
e, err := m.entryAt(path)
|
||||
if err != nil {
|
||||
return "", false
|
||||
}
|
||||
return e.SingleDefaultValue()
|
||||
}
|
||||
|
||||
// ModuleName returns the module name for the schema node at path.
|
||||
func (m *Manager) ModuleName(path string) (string, error) {
|
||||
e, err := m.entryAt(path)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return e.InstantiatingModule()
|
||||
}
|
||||
|
||||
// ModuleQualifiedName returns "module:name" for the schema node at path.
|
||||
// This is the JSON object key used when PUT/PATCHing that node directly via
|
||||
// RESTCONF (RFC 7951 §4 — namespace-qualified name at module boundaries).
|
||||
func (m *Manager) ModuleQualifiedName(path string) (string, error) {
|
||||
e, err := m.entryAt(path)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
modName, err := e.InstantiatingModule()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return modName + ":" + e.Name, nil
|
||||
}
|
||||
|
||||
// internalOperationalTopNodes lists bare node names that must not appear at
|
||||
// the top level of the operational/status tree. These are all transport- or
|
||||
// server-monitoring nodes of no interest to end users. They are already
|
||||
// absent from the configure tree because they are config:false; this list
|
||||
// applies the same exclusion to topLevelNodesAll.
|
||||
var internalOperationalTopNodes = map[string]bool{
|
||||
"netconf": true, // NETCONF session/lock monitoring
|
||||
"netconf-state": true, // ietf-netconf-monitoring (RFC 6022)
|
||||
"notification": true, // nc-notifications stream container
|
||||
"restconf-state": true, // ietf-restconf-monitoring (RFC 8527)
|
||||
"supported-algorithms": true, // SSH/TLS algorithm capability advertisement
|
||||
"system-capabilities": true, // ietf-system-capabilities (RFC 9196)
|
||||
}
|
||||
|
||||
// internalModules is the deny-list of YANG modules that are infrastructure-only
|
||||
// and must not appear in the user-facing configure tree.
|
||||
// Mirrors sr_module_is_internal() from klish-plugin-sysrepo/src/pline.c.
|
||||
// Note: ietf-netconf-acm is intentionally NOT listed here — Infix exposes NACM
|
||||
// configuration to users.
|
||||
var internalModules = map[string]bool{
|
||||
// libyang built-ins
|
||||
"ietf-yang-metadata": true,
|
||||
"yang": true,
|
||||
"ietf-inet-types": true,
|
||||
"ietf-yang-types": true,
|
||||
// YANG library / schema mount
|
||||
"ietf-datastores": true,
|
||||
"ietf-yang-schema-mount": true,
|
||||
"ietf-yang-library": true,
|
||||
// NETCONF infrastructure
|
||||
"ietf-netconf": true,
|
||||
"ietf-netconf-with-defaults": true,
|
||||
"ietf-origin": true,
|
||||
"ietf-netconf-notifications": true,
|
||||
// sysrepo internals
|
||||
"sysrepo": true,
|
||||
"sysrepo-monitoring": true,
|
||||
"sysrepo-plugind": true,
|
||||
// Infix test/debug subtree and meta-data — not user-facing
|
||||
"infix-test": true,
|
||||
"infix-meta": true,
|
||||
// NETCONF server / transport infrastructure — managed by the system, not users
|
||||
"ietf-netconf-server": true,
|
||||
"libnetconf2-netconf-server": true,
|
||||
"ietf-truststore": true,
|
||||
// Notification subscriptions and filters — not user-configurable via WebUI
|
||||
"ietf-subscribed-notifications": true,
|
||||
// ACL, key-chains, network-instances — not exposed in WebUI yet
|
||||
"ietf-access-control-list": true,
|
||||
"ietf-key-chain": true,
|
||||
"ietf-network-instance": true,
|
||||
}
|
||||
|
||||
// topLevelNodes returns a Node for each config-relevant top-level schema node
|
||||
// across all loaded modules. Submodules, versioned aliases, internal modules,
|
||||
// and config:false top-level nodes are all excluded.
|
||||
func (m *Manager) topLevelNodes() []*Node {
|
||||
var nodes []*Node
|
||||
for key, mod := range m.ms.Modules {
|
||||
if strings.Contains(key, "@") {
|
||||
continue // versioned alias (e.g. "ieee802-dot1ab-lldp@2022-03-15") — duplicate
|
||||
}
|
||||
if mod.BelongsTo != nil {
|
||||
continue // submodule — content appears under the parent module
|
||||
}
|
||||
if internalModules[key] {
|
||||
continue
|
||||
}
|
||||
e := yang.ToEntry(mod)
|
||||
if e == nil || e.Dir == nil {
|
||||
continue
|
||||
}
|
||||
for _, child := range sortedEntries(e.Dir) {
|
||||
if isNonConfigNode(child) {
|
||||
continue
|
||||
}
|
||||
nodePath := "/" + key + ":" + child.Name
|
||||
if isContainerList(child) {
|
||||
if lc := listChildOf(child); lc != nil {
|
||||
nodes = append(nodes, entryToNode(lc, nodePath+"/"+nodeSegment(lc, key)))
|
||||
}
|
||||
continue
|
||||
}
|
||||
nodes = append(nodes, entryToNode(child, nodePath))
|
||||
}
|
||||
}
|
||||
// Sort by node name (not module-qualified path) for a clean alphabetical list.
|
||||
sort.Slice(nodes, func(i, j int) bool { return nodes[i].Name < nodes[j].Name })
|
||||
return nodes
|
||||
}
|
||||
|
||||
// entryAt resolves a RESTCONF-style path to a goyang Entry.
|
||||
// Path format: /module:top-node/child[key='val']/grandchild
|
||||
// Key predicates ([key='val']) are stripped — they carry instance identity,
|
||||
// not schema identity. choice/case nodes are skipped during traversal.
|
||||
func (m *Manager) entryAt(path string) (*yang.Entry, error) {
|
||||
path = strings.TrimPrefix(path, "/")
|
||||
parts := strings.SplitN(path, "/", 2)
|
||||
|
||||
head := stripPredicate(parts[0])
|
||||
moduleName, nodeName := splitPrefix(head)
|
||||
|
||||
if moduleName == "" {
|
||||
return nil, fmt.Errorf("schema: path must start with module prefix: %s", path)
|
||||
}
|
||||
|
||||
mod, ok := m.ms.Modules[moduleName]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("schema: module not found: %s", moduleName)
|
||||
}
|
||||
|
||||
root := yang.ToEntry(mod)
|
||||
if root == nil {
|
||||
return nil, fmt.Errorf("schema: no entry for module: %s", moduleName)
|
||||
}
|
||||
|
||||
e := findInDir(root.Dir, nodeName)
|
||||
if e == nil {
|
||||
return nil, fmt.Errorf("schema: %s not found in %s", nodeName, moduleName)
|
||||
}
|
||||
|
||||
if len(parts) == 1 {
|
||||
return e, nil
|
||||
}
|
||||
|
||||
// Traverse remaining path segments, stripping key predicates.
|
||||
for _, seg := range strings.Split(parts[1], "/") {
|
||||
if seg == "" {
|
||||
continue
|
||||
}
|
||||
_, localName := splitPrefix(stripPredicate(seg))
|
||||
child := findInDir(e.Dir, localName)
|
||||
if child == nil {
|
||||
return nil, fmt.Errorf("schema: %s not found under %s", localName, e.Name)
|
||||
}
|
||||
e = child
|
||||
}
|
||||
return e, nil
|
||||
}
|
||||
|
||||
// stripPredicate removes a key predicate from a path segment.
|
||||
// Handles both RESTCONF ("interface=eth0" → "interface") and
|
||||
// XPath ("interface[name='eth0']" → "interface") forms.
|
||||
func stripPredicate(seg string) string {
|
||||
if i := strings.IndexByte(seg, '['); i >= 0 {
|
||||
return seg[:i]
|
||||
}
|
||||
if i := strings.IndexByte(seg, '='); i >= 0 {
|
||||
return seg[:i]
|
||||
}
|
||||
return seg
|
||||
}
|
||||
|
||||
// findInDir looks up name in a dir map, transparently descending into
|
||||
// choice/case nodes which are not part of the RESTCONF path.
|
||||
func findInDir(dir map[string]*yang.Entry, name string) *yang.Entry {
|
||||
if dir == nil {
|
||||
return nil
|
||||
}
|
||||
// Direct match — but if it is a choice/case, descend into it because YANG
|
||||
// commonly names a case identically to the leaf it contains (e.g. the
|
||||
// ietf-system timezone-name case wrapping the timezone-name leaf).
|
||||
if e, ok := dir[name]; ok {
|
||||
if !e.IsChoice() && !e.IsCase() {
|
||||
return e
|
||||
}
|
||||
if found := findInDir(e.Dir, name); found != nil {
|
||||
return found
|
||||
}
|
||||
}
|
||||
// Search inside all choice/case children.
|
||||
for _, e := range dir {
|
||||
if e.IsChoice() || e.IsCase() {
|
||||
if found := findInDir(e.Dir, name); found != nil {
|
||||
return found
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// isContainerList returns true for containers that wrap exactly one list and
|
||||
// nothing else — the classic YANG container/list idiom (e.g. /interfaces wrapping
|
||||
// /interfaces/interface). Mirrors klysc_is_container_list() in klish-plugin-sysrepo
|
||||
// exactly: ALL direct children are inspected, including config:false ones. Any
|
||||
// non-list child (a leaf, another container, …) prevents collapsing. This is why
|
||||
// "hardware" does not collapse — it has a config:false "last-change" leaf alongside
|
||||
// "component", so the default case fires and returns false.
|
||||
//
|
||||
// Hard exceptions:
|
||||
// - "static-routes" always collapses (ietf-routing, asymmetric naming)
|
||||
// - "mdb" never collapses
|
||||
// - "ipv4", "ipv6" never collapse (would confuse routing subtrees)
|
||||
func isContainerList(e *yang.Entry) bool {
|
||||
if !e.IsContainer() || e.Dir == nil {
|
||||
return false
|
||||
}
|
||||
if e.Name == "mdb" || e.Name == "ipv4" || e.Name == "ipv6" {
|
||||
return false
|
||||
}
|
||||
if e.Name == "static-routes" {
|
||||
return true
|
||||
}
|
||||
listCount := 0
|
||||
for _, child := range e.Dir {
|
||||
if !child.IsList() {
|
||||
return false // any non-list child (even config:false) prevents collapse
|
||||
}
|
||||
listCount++
|
||||
}
|
||||
return listCount == 1
|
||||
}
|
||||
|
||||
// listChildOf returns the single list child of a collapsible container (panics if
|
||||
// called on a non-collapsible container — callers must guard with isContainerList).
|
||||
func listChildOf(e *yang.Entry) *yang.Entry {
|
||||
for _, child := range e.Dir {
|
||||
if child.IsList() && !isNonConfigNode(child) {
|
||||
return child
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// listChildOfAll is like listChildOf but uses the data-node filter (includes
|
||||
// config:false lists), used by dirToNodesAll / the operational status tree.
|
||||
func listChildOfAll(e *yang.Entry) *yang.Entry {
|
||||
for _, child := range e.Dir {
|
||||
if child.IsList() && !isNonDataNode(child) {
|
||||
return child
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// topLevelNodesAll is like topLevelNodes but includes config:false top-level
|
||||
// nodes, for use by the operational status tree.
|
||||
func (m *Manager) topLevelNodesAll() []*Node {
|
||||
var nodes []*Node
|
||||
for key, mod := range m.ms.Modules {
|
||||
if strings.Contains(key, "@") {
|
||||
continue
|
||||
}
|
||||
if mod.BelongsTo != nil {
|
||||
continue
|
||||
}
|
||||
if internalModules[key] {
|
||||
continue
|
||||
}
|
||||
e := yang.ToEntry(mod)
|
||||
if e == nil || e.Dir == nil {
|
||||
continue
|
||||
}
|
||||
for _, child := range sortedEntries(e.Dir) {
|
||||
if isNonDataNode(child) {
|
||||
continue
|
||||
}
|
||||
if internalOperationalTopNodes[child.Name] {
|
||||
continue
|
||||
}
|
||||
nodePath := "/" + key + ":" + child.Name
|
||||
if isContainerList(child) {
|
||||
if lc := listChildOfAll(child); lc != nil {
|
||||
nodes = append(nodes, entryToNode(lc, nodePath+"/"+nodeSegment(lc, key)))
|
||||
}
|
||||
continue
|
||||
}
|
||||
nodes = append(nodes, entryToNode(child, nodePath))
|
||||
}
|
||||
}
|
||||
sort.Slice(nodes, func(i, j int) bool { return nodes[i].Name < nodes[j].Name })
|
||||
return nodes
|
||||
}
|
||||
|
||||
// dirToNodesAll is like dirToNodes but uses isNonDataNode, including
|
||||
// config:false leaves and containers in the result.
|
||||
func dirToNodesAll(dir map[string]*yang.Entry, parentPath string) []*Node {
|
||||
parentMod := extractModuleFromPath(parentPath)
|
||||
var nodes []*Node
|
||||
for _, e := range sortedEntries(dir) {
|
||||
if e.IsChoice() || e.IsCase() {
|
||||
caseWhen := extractWhen(e)
|
||||
for _, child := range dirToNodesAll(e.Dir, parentPath) {
|
||||
if caseWhen != "" && child.When == "" {
|
||||
child.When = caseWhen
|
||||
}
|
||||
nodes = append(nodes, child)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if isNonDataNode(e) {
|
||||
continue
|
||||
}
|
||||
nodePath := parentPath + "/" + nodeSegment(e, parentMod)
|
||||
if isContainerList(e) {
|
||||
if lc := listChildOfAll(e); lc != nil {
|
||||
nodes = append(nodes, entryToNode(lc, nodePath+"/"+nodeSegment(lc, parentMod)))
|
||||
}
|
||||
continue
|
||||
}
|
||||
nodes = append(nodes, entryToNode(e, nodePath))
|
||||
}
|
||||
return nodes
|
||||
}
|
||||
|
||||
// dirToNodes converts a goyang Dir map to a sorted slice of Nodes.
|
||||
// choice/case children are inlined (their contents promoted to this level).
|
||||
// Collapsible container-list wrappers are transparent: the list child is surfaced
|
||||
// directly under the parent with the full (un-collapsed) RESTCONF path.
|
||||
// RPC, notification, anydata, anyxml and config:false nodes are excluded.
|
||||
//
|
||||
// RESTCONF requires module qualification ("module:name") whenever a node's
|
||||
// module differs from its parent's — the common case being augmented nodes.
|
||||
// e.g. infix-lldp augments ieee802-dot1ab-lldp:lldp, so the path is
|
||||
// /ieee802-dot1ab-lldp:lldp/infix-lldp:enabled, not /…/enabled.
|
||||
func dirToNodes(dir map[string]*yang.Entry, parentPath string) []*Node {
|
||||
parentMod := extractModuleFromPath(parentPath)
|
||||
var nodes []*Node
|
||||
for _, e := range sortedEntries(dir) {
|
||||
if e.IsChoice() || e.IsCase() {
|
||||
// Extract when from the choice/case before inlining its children.
|
||||
// Cases contributed by augments carry the augment's when (e.g.
|
||||
// lag-port only for ethernetCsmacd, bridge-port only for bridge).
|
||||
// Propagate that constraint to each promoted child that has no when of
|
||||
// its own; if the child already has one, leave it alone.
|
||||
caseWhen := extractWhen(e)
|
||||
for _, child := range dirToNodes(e.Dir, parentPath) {
|
||||
if caseWhen != "" && child.When == "" {
|
||||
child.When = caseWhen
|
||||
}
|
||||
nodes = append(nodes, child)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if isNonConfigNode(e) {
|
||||
continue
|
||||
}
|
||||
nodePath := parentPath + "/" + nodeSegment(e, parentMod)
|
||||
if isContainerList(e) {
|
||||
if lc := listChildOf(e); lc != nil {
|
||||
// Container is collapsed; qualify list child against parentMod.
|
||||
nodes = append(nodes, entryToNode(lc, nodePath+"/"+nodeSegment(lc, parentMod)))
|
||||
}
|
||||
continue
|
||||
}
|
||||
nodes = append(nodes, entryToNode(e, nodePath))
|
||||
}
|
||||
return nodes
|
||||
}
|
||||
|
||||
// nodeSegment returns the path segment for e, qualified as "module:name" when
|
||||
// e's instantiating module differs from parentMod (RESTCONF RFC 8040 §3.5.3).
|
||||
func nodeSegment(e *yang.Entry, parentMod string) string {
|
||||
mod, err := e.InstantiatingModule()
|
||||
if err != nil || mod == parentMod {
|
||||
return e.Name
|
||||
}
|
||||
return mod + ":" + e.Name
|
||||
}
|
||||
|
||||
// extractModuleFromPath returns the module from the rightmost module-qualified
|
||||
// segment in a RESTCONF path, e.g. "/ieee802-dot1ab-lldp:lldp/port" → "ieee802-dot1ab-lldp".
|
||||
func extractModuleFromPath(path string) string {
|
||||
segs := strings.Split(strings.TrimPrefix(path, "/"), "/")
|
||||
for i := len(segs) - 1; i >= 0; i-- {
|
||||
seg := stripPredicate(segs[i])
|
||||
if j := strings.IndexByte(seg, ':'); j > 0 {
|
||||
return seg[:j]
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// isNonConfigNode returns true for schema nodes that do not belong in the
|
||||
// configuration tree: RPCs, notifications, anydata/anyxml, config:false subtrees,
|
||||
// and nodes with YANG status deprecated or obsolete.
|
||||
// goyang propagates config:false from parent to children during Process(), so a
|
||||
// single check at each level is sufficient to prune entire read-only subtrees.
|
||||
func isNonConfigNode(e *yang.Entry) bool {
|
||||
return e.RPC != nil ||
|
||||
e.Kind == yang.NotificationEntry ||
|
||||
e.Kind == yang.AnyDataEntry ||
|
||||
e.Kind == yang.AnyXMLEntry ||
|
||||
e.Config == yang.TSFalse ||
|
||||
isDeprecatedOrObsolete(e)
|
||||
}
|
||||
|
||||
// isNonDataNode is like isNonConfigNode but keeps config:false nodes.
|
||||
// Used by ChildrenAll / the operational status tree which shows all data —
|
||||
// operational state reuses read-write schema nodes alongside state-only leaves.
|
||||
func isNonDataNode(e *yang.Entry) bool {
|
||||
return e.RPC != nil ||
|
||||
e.Kind == yang.NotificationEntry ||
|
||||
e.Kind == yang.AnyDataEntry ||
|
||||
e.Kind == yang.AnyXMLEntry ||
|
||||
isDeprecatedOrObsolete(e)
|
||||
}
|
||||
|
||||
// entryPresence returns the YANG presence statement string for e, or "".
|
||||
// Presence is stored on the underlying Container Node via reflection because
|
||||
// yang.Entry does not expose it directly.
|
||||
func entryPresence(e *yang.Entry) string {
|
||||
if e.Node == nil {
|
||||
return ""
|
||||
}
|
||||
v := reflect.ValueOf(e.Node)
|
||||
if v.Kind() == reflect.Ptr {
|
||||
v = v.Elem()
|
||||
}
|
||||
f := v.FieldByName("Presence")
|
||||
if !f.IsValid() || f.Kind() != reflect.Ptr || f.IsNil() {
|
||||
return ""
|
||||
}
|
||||
name := f.Elem().FieldByName("Name")
|
||||
if !name.IsValid() {
|
||||
return ""
|
||||
}
|
||||
return name.String()
|
||||
}
|
||||
|
||||
// isDeprecatedOrObsolete returns true when the YANG node carries
|
||||
// "status deprecated" or "status obsolete". goyang does not expose status
|
||||
// directly on Entry, so we reach through to the underlying Node via reflection.
|
||||
// All concrete yang node types (Container, Leaf, List, …) have Status *Value.
|
||||
func isDeprecatedOrObsolete(e *yang.Entry) bool {
|
||||
if e.Node == nil {
|
||||
return false
|
||||
}
|
||||
v := reflect.ValueOf(e.Node)
|
||||
if v.Kind() == reflect.Ptr {
|
||||
v = v.Elem()
|
||||
}
|
||||
f := v.FieldByName("Status")
|
||||
if !f.IsValid() || f.Kind() != reflect.Ptr || f.IsNil() {
|
||||
return false
|
||||
}
|
||||
name := f.Elem().FieldByName("Name")
|
||||
if !name.IsValid() {
|
||||
return false
|
||||
}
|
||||
s := name.String()
|
||||
return s == "deprecated" || s == "obsolete"
|
||||
}
|
||||
|
||||
// entryToNode converts a goyang Entry to a schema Node (no children populated).
|
||||
func entryToNode(e *yang.Entry, path string) *Node {
|
||||
n := &Node{
|
||||
Path: path,
|
||||
Name: e.Name,
|
||||
Kind: entryKind(e),
|
||||
Description: e.Description,
|
||||
Config: e.Config != yang.TSFalse,
|
||||
Mandatory: e.Mandatory == yang.TSTrue,
|
||||
When: extractWhen(e),
|
||||
Presence: entryPresence(e),
|
||||
}
|
||||
|
||||
if def, ok := e.SingleDefaultValue(); ok {
|
||||
n.Default = def
|
||||
}
|
||||
|
||||
if strings.Contains(e.Key, " ") || e.Key != "" {
|
||||
n.Keys = strings.Fields(e.Key)
|
||||
}
|
||||
|
||||
if e.IsLeaf() || e.IsLeafList() {
|
||||
n.Type = yangTypeInfo(e)
|
||||
}
|
||||
|
||||
return n
|
||||
}
|
||||
|
||||
// prefixInXPath matches a "prefix:X" token where X is a letter or underscore,
|
||||
// the first character of an identifier. The matched letter is included so
|
||||
// ReplaceAllStringFunc can reattach it after resolving the prefix.
|
||||
var prefixInXPath = regexp.MustCompile(`[a-zA-Z][a-zA-Z0-9_\-]*:[a-zA-Z_]`)
|
||||
|
||||
// extractWhen returns the pre-resolved YANG when expression for e, or "".
|
||||
// It first checks e itself (when directly on the node), then checks parent
|
||||
// augments — the Infix convention is to put when on the augment, not on the
|
||||
// top-level container inside the augment.
|
||||
func extractWhen(e *yang.Entry) string {
|
||||
if xpath, ok := e.GetWhenXPath(); ok && xpath != "" {
|
||||
return resolveWhenPrefixes(e.Node, xpath)
|
||||
}
|
||||
// Check parent's augment list: the augment may carry the when expression
|
||||
// even though the individual container inside it does not.
|
||||
if e.Parent == nil {
|
||||
return ""
|
||||
}
|
||||
for _, aug := range e.Parent.Augmented {
|
||||
if _, found := aug.Dir[e.Name]; !found {
|
||||
continue
|
||||
}
|
||||
if xpath, ok := aug.GetWhenXPath(); ok && xpath != "" {
|
||||
return resolveWhenPrefixes(aug.Node, xpath)
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// resolveWhenPrefixes replaces "prefix:x" tokens in an XPath expression with
|
||||
// the canonical "module:x" form using FindModuleByPrefix on the given node.
|
||||
// Unknown prefixes (not imported by the node's module) are left unchanged.
|
||||
func resolveWhenPrefixes(node yang.Node, xpath string) string {
|
||||
return prefixInXPath.ReplaceAllStringFunc(xpath, func(m string) string {
|
||||
colon := strings.IndexByte(m, ':')
|
||||
prefix := m[:colon]
|
||||
rest := m[colon+1:] // the single identifier-start character
|
||||
mod := yang.FindModuleByPrefix(node, prefix)
|
||||
if mod == nil {
|
||||
return m
|
||||
}
|
||||
modName := mod.Name
|
||||
if mod.BelongsTo != nil {
|
||||
modName = mod.BelongsTo.Name
|
||||
}
|
||||
return modName + ":" + rest
|
||||
})
|
||||
}
|
||||
|
||||
// entryKind maps a goyang Entry to a kind string.
|
||||
func entryKind(e *yang.Entry) string {
|
||||
switch {
|
||||
case e.IsLeaf():
|
||||
return "leaf"
|
||||
case e.IsLeafList():
|
||||
return "leaf-list"
|
||||
case e.IsList():
|
||||
return "list"
|
||||
case e.IsContainer():
|
||||
return "container"
|
||||
case e.IsChoice():
|
||||
return "choice"
|
||||
case e.IsCase():
|
||||
return "case"
|
||||
case e.Kind == yang.AnyDataEntry:
|
||||
return "anydata"
|
||||
case e.Kind == yang.AnyXMLEntry:
|
||||
return "anyxml"
|
||||
case e.RPC != nil:
|
||||
return "rpc"
|
||||
default:
|
||||
return "unknown"
|
||||
}
|
||||
}
|
||||
|
||||
// yangTypeInfo builds a TypeInfo from a leaf's YangType.
|
||||
func yangTypeInfo(e *yang.Entry) *TypeInfo {
|
||||
t := e.Type
|
||||
if t == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
info := &TypeInfo{
|
||||
Kind: yang.TypeKindToName[t.Kind],
|
||||
}
|
||||
|
||||
switch t.Kind {
|
||||
case yang.Yenum:
|
||||
if t.Enum != nil {
|
||||
info.Enums = t.Enum.Names()
|
||||
sort.Strings(info.Enums)
|
||||
}
|
||||
case yang.Yidentityref:
|
||||
if t.IdentityBase != nil {
|
||||
for _, v := range t.IdentityBase.Values {
|
||||
name := v.Name
|
||||
if root := yang.RootNode(v); root != nil {
|
||||
modName := root.Name
|
||||
if root.Kind() == "submodule" && root.BelongsTo != nil {
|
||||
modName = root.BelongsTo.Name
|
||||
}
|
||||
name = modName + ":" + v.Name
|
||||
}
|
||||
info.Identities = append(info.Identities, name)
|
||||
}
|
||||
sort.Strings(info.Identities)
|
||||
}
|
||||
case yang.Yleafref:
|
||||
info.Leafref = canonicalizeLeafrefPath(e.Node, t.Path)
|
||||
info.LeafrefSibling = leafrefSiblingFilter(e)
|
||||
case yang.Yunion:
|
||||
for _, sub := range t.Type {
|
||||
if sub.Kind == yang.Yenum && sub.Enum != nil {
|
||||
info.Enums = append(info.Enums, sub.Enum.Names()...)
|
||||
}
|
||||
}
|
||||
if len(info.Enums) > 0 {
|
||||
sort.Strings(info.Enums)
|
||||
}
|
||||
}
|
||||
|
||||
if len(t.Pattern) > 0 {
|
||||
info.Pattern = strings.Join(t.Pattern, "|")
|
||||
}
|
||||
|
||||
if len(t.Range) > 0 {
|
||||
info.Range = t.Range.String()
|
||||
}
|
||||
|
||||
return info
|
||||
}
|
||||
|
||||
// splitPrefix splits "module:name" into ("module", "name").
|
||||
// If there is no prefix, it returns ("", name).
|
||||
func splitPrefix(s string) (prefix, name string) {
|
||||
if i := strings.Index(s, ":"); i >= 0 {
|
||||
return s[:i], s[i+1:]
|
||||
}
|
||||
return "", s
|
||||
}
|
||||
|
||||
// sortedEntries returns the entries in a Dir map sorted by name.
|
||||
func sortedEntries(dir map[string]*yang.Entry) []*yang.Entry {
|
||||
keys := make([]string, 0, len(dir))
|
||||
for k := range dir {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
result := make([]*yang.Entry, 0, len(keys))
|
||||
for _, k := range keys {
|
||||
result = append(result, dir[k])
|
||||
}
|
||||
return result
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
package schema
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"sync"
|
||||
|
||||
"github.com/kernelkit/webui/internal/restconf"
|
||||
)
|
||||
|
||||
// Cache holds a lazily-loaded schema Manager and refreshes it at startup.
|
||||
// All methods are safe for concurrent use.
|
||||
type Cache struct {
|
||||
mu sync.RWMutex
|
||||
manager *Manager
|
||||
syncing bool // guarded by mu
|
||||
dir string
|
||||
rc restconf.Fetcher
|
||||
}
|
||||
|
||||
// NewCache creates a Cache.
|
||||
// Call LoadFromCacheBackground at startup, then RefreshBackground after login.
|
||||
func NewCache(rc restconf.Fetcher, dir string) *Cache {
|
||||
return &Cache{rc: rc, dir: dir}
|
||||
}
|
||||
|
||||
// LoadFromCache parses whatever .yang files are already in the cache
|
||||
// directory. It makes no HTTP requests and needs no credentials.
|
||||
// This is fast — suitable for server startup. If the directory is empty
|
||||
// or has too few files to form a useful schema, the Manager is left nil.
|
||||
func (c *Cache) LoadFromCache() error {
|
||||
entries, err := os.ReadDir(c.dir)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return nil // nothing cached yet — that is fine
|
||||
}
|
||||
return err
|
||||
}
|
||||
var count int
|
||||
for _, e := range entries {
|
||||
if !e.IsDir() && len(e.Name()) > 5 { // len(".yang") == 5
|
||||
if e.Name()[len(e.Name())-5:] == ".yang" {
|
||||
count++
|
||||
}
|
||||
}
|
||||
}
|
||||
if count == 0 {
|
||||
return nil // empty cache — wait for first Refresh
|
||||
}
|
||||
|
||||
mgr, err := Load(c.dir)
|
||||
if err != nil {
|
||||
return fmt.Errorf("schema: load from cache: %w", err)
|
||||
}
|
||||
c.mu.Lock()
|
||||
c.manager = mgr
|
||||
c.mu.Unlock()
|
||||
log.Printf("schema: loaded %d cached YANG file(s) from %s", count, c.dir)
|
||||
return nil
|
||||
}
|
||||
|
||||
// LoadFromCacheBackground calls LoadFromCache in a goroutine. Errors are logged.
|
||||
func (c *Cache) LoadFromCacheBackground() {
|
||||
go func() {
|
||||
if err := c.LoadFromCache(); err != nil {
|
||||
log.Printf("schema: cache load failed: %v", err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// Refresh downloads any missing YANG files from the device (credentials must
|
||||
// be present in ctx) and then reloads the schema Manager.
|
||||
// Only one refresh runs at a time; concurrent calls return immediately.
|
||||
func (c *Cache) Refresh(ctx context.Context) error {
|
||||
c.mu.Lock()
|
||||
if c.syncing {
|
||||
c.mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
c.syncing = true
|
||||
c.mu.Unlock()
|
||||
|
||||
defer func() {
|
||||
c.mu.Lock()
|
||||
c.syncing = false
|
||||
c.mu.Unlock()
|
||||
}()
|
||||
|
||||
if _, err := FetchModules(ctx, c.rc, c.dir); err != nil {
|
||||
return fmt.Errorf("schema refresh: fetch: %w", err)
|
||||
}
|
||||
|
||||
mgr, err := Load(c.dir)
|
||||
if err != nil {
|
||||
return fmt.Errorf("schema refresh: load: %w", err)
|
||||
}
|
||||
|
||||
c.mu.Lock()
|
||||
c.manager = mgr
|
||||
c.mu.Unlock()
|
||||
|
||||
log.Printf("schema: refreshed successfully from %s", c.dir)
|
||||
return nil
|
||||
}
|
||||
|
||||
// RefreshBackground calls Refresh in a goroutine. Errors are logged.
|
||||
// The context's values (credentials) are preserved but its cancellation is
|
||||
// detached so the goroutine is not killed when the originating HTTP request
|
||||
// completes.
|
||||
func (c *Cache) RefreshBackground(ctx context.Context) {
|
||||
detached := context.WithoutCancel(ctx)
|
||||
go func() {
|
||||
if err := c.Refresh(detached); err != nil {
|
||||
log.Printf("schema: background refresh failed: %v", err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// Manager returns the current Manager, or nil if not yet loaded.
|
||||
func (c *Cache) Manager() *Manager {
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
return c.manager
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package schema
|
||||
|
||||
// Node is a JSON-serialisable representation of a single YANG schema node.
|
||||
// Children are omitted (nil) by default; use Manager.Children to lazy-load them.
|
||||
type Node struct {
|
||||
Path string `json:"path"`
|
||||
Name string `json:"name"`
|
||||
Kind string `json:"kind"` // container|list|leaf|leaf-list|choice|case|rpc|notification
|
||||
Description string `json:"description,omitempty"`
|
||||
Keys []string `json:"keys,omitempty"`
|
||||
Children []*Node `json:"children,omitempty"`
|
||||
Config bool `json:"config"`
|
||||
Mandatory bool `json:"mandatory"`
|
||||
Default string `json:"default,omitempty"`
|
||||
Type *TypeInfo `json:"type,omitempty"`
|
||||
// When holds the pre-resolved YANG when expression (prefix aliases replaced
|
||||
// by canonical module names). Empty when there is no constraint.
|
||||
When string `json:"when,omitempty"`
|
||||
// Presence is non-empty for presence containers; its value is the YANG
|
||||
// presence statement string (describes what the container's existence means).
|
||||
Presence string `json:"presence,omitempty"`
|
||||
}
|
||||
|
||||
// TypeInfo describes the type of a leaf or leaf-list node.
|
||||
type TypeInfo struct {
|
||||
Kind string `json:"kind"` // string|boolean|int8..uint64|enumeration|identityref|leafref|binary|empty|...
|
||||
Enums []string `json:"enums,omitempty"` // enumeration values
|
||||
Identities []string `json:"identities,omitempty"` // identityref derived identity names
|
||||
Range string `json:"range,omitempty"`
|
||||
Pattern string `json:"pattern,omitempty"`
|
||||
// Leafref is the leafref target path with YANG prefix aliases
|
||||
// resolved to canonical "module:name" form, so it can be fed
|
||||
// straight to mgr.NodeAt / RESTCONF without further mapping.
|
||||
Leafref string `json:"leafref,omitempty"`
|
||||
// LeafrefSibling is the sibling-leaf or sibling-container name
|
||||
// extracted from the leaf's `must "deref(.)/../<X>"` constraint,
|
||||
// if any. Callers filter the dropdown to those leafref targets
|
||||
// whose parent object exposes this sibling — the common idiom
|
||||
// for "this leafref must point at an object of kind <X>".
|
||||
LeafrefSibling string `json:"leafref-sibling,omitempty"`
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package schema
|
||||
|
||||
import "strings"
|
||||
|
||||
// EvaluateWhen evaluates a pre-resolved YANG when XPath expression against
|
||||
// the flat key→value map of the context node's parent data.
|
||||
//
|
||||
// Handles the practical subset used in Infix YANG models:
|
||||
// - derived-from-or-self(path, 'module:identity')
|
||||
// - derived-from(path, 'module:identity')
|
||||
// - combinations joined with 'or' / 'and'
|
||||
//
|
||||
// Conservative: any unrecognised expression or absent data returns true (show).
|
||||
func EvaluateWhen(mgr *Manager, expr string, values map[string]string) bool {
|
||||
if expr == "" || values == nil {
|
||||
return true
|
||||
}
|
||||
return evalOr(mgr, strings.TrimSpace(expr), values)
|
||||
}
|
||||
|
||||
func evalOr(mgr *Manager, expr string, values map[string]string) bool {
|
||||
parts := splitOnKeyword(expr, "or")
|
||||
for _, p := range parts {
|
||||
if evalAnd(mgr, strings.TrimSpace(p), values) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func evalAnd(mgr *Manager, expr string, values map[string]string) bool {
|
||||
parts := splitOnKeyword(expr, "and")
|
||||
for _, p := range parts {
|
||||
if !evalAtom(mgr, strings.TrimSpace(p), values) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func evalAtom(mgr *Manager, expr string, values map[string]string) bool {
|
||||
if expr == "" {
|
||||
return true // empty: conservative
|
||||
}
|
||||
if strings.HasPrefix(expr, "derived-from-or-self(") && strings.HasSuffix(expr, ")") {
|
||||
inner := expr[len("derived-from-or-self(") : len(expr)-1]
|
||||
return evalDerivedFrom(mgr, inner, values, true)
|
||||
}
|
||||
if strings.HasPrefix(expr, "derived-from(") && strings.HasSuffix(expr, ")") {
|
||||
inner := expr[len("derived-from(") : len(expr)-1]
|
||||
return evalDerivedFrom(mgr, inner, values, false)
|
||||
}
|
||||
return true // unknown expression: conservative show
|
||||
}
|
||||
|
||||
func evalDerivedFrom(mgr *Manager, inner string, values map[string]string, orSelf bool) bool {
|
||||
comma := strings.Index(inner, ",")
|
||||
if comma < 0 {
|
||||
return true // malformed: conservative
|
||||
}
|
||||
xpathPath := strings.TrimSpace(inner[:comma])
|
||||
identity := strings.Trim(strings.TrimSpace(inner[comma+1:]), "'\"")
|
||||
|
||||
leafName := xpathLeafName(xpathPath)
|
||||
if leafName == "" {
|
||||
return true // unresolvable path: conservative
|
||||
}
|
||||
|
||||
current := values[leafName]
|
||||
if current == "" {
|
||||
return true // no data: conservative show
|
||||
}
|
||||
return checkIdentity(mgr, current, identity, orSelf)
|
||||
}
|
||||
|
||||
// xpathLeafName extracts the bare leaf name from a simple XPath step.
|
||||
// - "module:name" or "name" → "name"
|
||||
// - "../module:name" → "name" (single parent step)
|
||||
// - "../../../…" or path with "/" → "" (multi-level: conservative)
|
||||
func xpathLeafName(path string) string {
|
||||
if strings.HasPrefix(path, "../") {
|
||||
path = path[3:]
|
||||
// After one parent step, path must be a bare leaf name.
|
||||
if strings.Contains(path, "/") || strings.HasPrefix(path, "..") {
|
||||
return ""
|
||||
}
|
||||
} else if strings.Contains(path, "/") {
|
||||
return "" // forward traversal: conservative
|
||||
}
|
||||
_, local := splitPrefix(strings.TrimSpace(path))
|
||||
return local
|
||||
}
|
||||
|
||||
// checkIdentity reports whether currentValue is equal to or derived from
|
||||
// targetIdentity according to the YANG identity hierarchy.
|
||||
func checkIdentity(mgr *Manager, currentValue, targetIdentity string, orSelf bool) bool {
|
||||
_, currentLocal := splitPrefix(currentValue)
|
||||
_, targetLocal := splitPrefix(targetIdentity)
|
||||
|
||||
if orSelf && currentLocal == targetLocal {
|
||||
return true
|
||||
}
|
||||
for _, d := range mgr.IdentitiesOf(targetLocal) {
|
||||
if d == currentLocal {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// splitOnKeyword splits expr on the XPath keyword kw at parenthesis depth 0,
|
||||
// but NOT when kw is part of a compound name (e.g. "derived-from-or-self"
|
||||
// contains "or"; "derived-from-and-…" would contain "and").
|
||||
// A keyword match is suppressed when immediately preceded by a hyphen.
|
||||
//
|
||||
// The Infix YANG files contain a concatenation bug where the 'or' keyword is
|
||||
// immediately followed by the next function name without a separating space
|
||||
// (e.g. "…') orderived-from-or-self(…)"). This splitter handles that case
|
||||
// by treating any 'or'/'and' that is not preceded by '-' as a keyword.
|
||||
func splitOnKeyword(expr, kw string) []string {
|
||||
n := len(kw)
|
||||
var parts []string
|
||||
depth := 0
|
||||
start := 0
|
||||
for i := 0; i < len(expr); i++ {
|
||||
switch expr[i] {
|
||||
case '(':
|
||||
depth++
|
||||
case ')':
|
||||
depth--
|
||||
}
|
||||
if depth != 0 || i+n > len(expr) || expr[i:i+n] != kw {
|
||||
continue
|
||||
}
|
||||
// Suppress if immediately preceded by '-' (part of compound name).
|
||||
if i > 0 && expr[i-1] == '-' {
|
||||
continue
|
||||
}
|
||||
// Found keyword: save part, trim trailing space before keyword.
|
||||
end := i
|
||||
if end > start && expr[end-1] == ' ' {
|
||||
end--
|
||||
}
|
||||
parts = append(parts, strings.TrimSpace(expr[start:end]))
|
||||
start = i + n
|
||||
if start < len(expr) && expr[start] == ' ' {
|
||||
start++
|
||||
}
|
||||
i = start - 1 // loop will increment
|
||||
}
|
||||
parts = append(parts, strings.TrimSpace(expr[start:]))
|
||||
return parts
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package schema
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestSplitOnKeyword(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
expr string
|
||||
kw string
|
||||
want []string
|
||||
}{
|
||||
{
|
||||
name: "single_term_no_split",
|
||||
expr: "derived-from-or-self(if:type, 'gre')",
|
||||
kw: "or",
|
||||
want: []string{"derived-from-or-self(if:type, 'gre')"},
|
||||
},
|
||||
{
|
||||
name: "two_terms_with_space",
|
||||
expr: "derived-from-or-self(if:type, 'gre') or derived-from-or-self(if:type, 'gretap')",
|
||||
kw: "or",
|
||||
want: []string{
|
||||
"derived-from-or-self(if:type, 'gre')",
|
||||
"derived-from-or-self(if:type, 'gretap')",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "two_terms_no_space_after_or", // Infix YANG concatenation bug
|
||||
expr: "derived-from-or-self(if:type, 'gre') orderived-from-or-self(if:type, 'gretap')",
|
||||
kw: "or",
|
||||
want: []string{
|
||||
"derived-from-or-self(if:type, 'gre')",
|
||||
"derived-from-or-self(if:type, 'gretap')",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "or_inside_parens_not_split",
|
||||
expr: "derived-from-or-self(if:type, 'foo or bar')",
|
||||
kw: "or",
|
||||
want: []string{"derived-from-or-self(if:type, 'foo or bar')"},
|
||||
},
|
||||
{
|
||||
name: "and_keyword",
|
||||
expr: "cond-a and cond-b",
|
||||
kw: "and",
|
||||
want: []string{"cond-a", "cond-b"},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := splitOnKeyword(tt.expr, tt.kw)
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("splitOnKeyword(%q, %q) = %v; want %v", tt.expr, tt.kw, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestXpathLeafName(t *testing.T) {
|
||||
tests := []struct {
|
||||
path string
|
||||
want string
|
||||
}{
|
||||
{"if:type", "type"},
|
||||
{"ietf-interfaces:type", "type"},
|
||||
{"type", "type"},
|
||||
{"../iehw:class", "class"},
|
||||
{"../infix-hardware:class", "class"},
|
||||
{"../../something", ""},
|
||||
{"../../../rt:address-family", ""},
|
||||
{"a/b", ""},
|
||||
{"/abs/path", ""},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
got := xpathLeafName(tt.path)
|
||||
if got != tt.want {
|
||||
t.Errorf("xpathLeafName(%q) = %q; want %q", tt.path, got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestEvaluateWhenConservative(t *testing.T) {
|
||||
// nil values → conservative true
|
||||
if !EvaluateWhen(nil, "derived-from-or-self(if:type, 'foo')", nil) {
|
||||
t.Error("nil values should be conservative true")
|
||||
}
|
||||
// empty expr → true
|
||||
if !EvaluateWhen(nil, "", map[string]string{"type": "bar"}) {
|
||||
t.Error("empty expr should be conservative true")
|
||||
}
|
||||
// nil manager with unknown expr → true
|
||||
if !EvaluateWhen(nil, "unknown-function(x)", map[string]string{"x": "y"}) {
|
||||
t.Error("unknown expr should be conservative true")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEvaluateWhenDerivedFromOrSelf(t *testing.T) {
|
||||
// Minimal mock manager: IdentitiesOf("gre") → ["gretap"]
|
||||
mgr := &Manager{ms: nil} // ms is nil, but IdentitiesOf will handle it gracefully
|
||||
|
||||
// We need a real identity hierarchy. Use a Manager with minimal modules.
|
||||
// Since setting up goyang modules is complex, test via checkIdentity directly.
|
||||
tests := []struct {
|
||||
name string
|
||||
current string
|
||||
target string
|
||||
orSelf bool
|
||||
derived []string // what IdentitiesOf would return; we test checkIdentity
|
||||
want bool
|
||||
}{
|
||||
{"or-self exact match", "gre", "gre", true, nil, true},
|
||||
{"or-self different", "bridge", "gre", true, nil, false},
|
||||
{"derived match", "gretap", "gre", false, []string{"gretap"}, true},
|
||||
{"derived no match", "bridge", "gre", false, []string{"gretap"}, false},
|
||||
{"module-qualified current", "infix-if-type:gre", "gre", true, nil, true},
|
||||
{"module-qualified target", "gre", "infix-if-type:gre", true, nil, true},
|
||||
{"both qualified exact", "infix-if-type:gre", "infix-if-type:gre", true, nil, true},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// Override IdentitiesOf by testing checkIdentity directly with a mock.
|
||||
// Build a mini-mgr that returns tt.derived for any base name.
|
||||
got := checkIdentityTest(tt.current, tt.target, tt.orSelf, tt.derived)
|
||||
if got != tt.want {
|
||||
t.Errorf("checkIdentity(%q, %q, orSelf=%v) = %v; want %v",
|
||||
tt.current, tt.target, tt.orSelf, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
_ = mgr
|
||||
}
|
||||
|
||||
// checkIdentityTest is a test helper that bypasses the Manager.IdentitiesOf call.
|
||||
func checkIdentityTest(currentValue, targetIdentity string, orSelf bool, derivedNames []string) bool {
|
||||
_, currentLocal := splitPrefix(currentValue)
|
||||
_, targetLocal := splitPrefix(targetIdentity)
|
||||
if orSelf && currentLocal == targetLocal {
|
||||
return true
|
||||
}
|
||||
for _, d := range derivedNames {
|
||||
if d == currentLocal {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"html/template"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
@@ -10,12 +11,14 @@ import (
|
||||
"github.com/kernelkit/webui/internal/auth"
|
||||
"github.com/kernelkit/webui/internal/handlers"
|
||||
"github.com/kernelkit/webui/internal/restconf"
|
||||
"github.com/kernelkit/webui/internal/schema"
|
||||
)
|
||||
|
||||
// New creates a fully wired http.Handler with all routes and middleware.
|
||||
func New(
|
||||
store *auth.SessionStore,
|
||||
rc *restconf.Client,
|
||||
schemaCache *schema.Cache,
|
||||
templateFS fs.FS,
|
||||
staticFS fs.FS,
|
||||
) (http.Handler, error) {
|
||||
@@ -101,11 +104,31 @@ func New(
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
yangTreeTmpl, err := template.ParseFS(templateFS,
|
||||
"layouts/*.html",
|
||||
"fragments/configure-toolbar.html",
|
||||
"fragments/yang-tree-node.html",
|
||||
"pages/yang-tree.html")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
yangFuncs := template.FuncMap{"stripPrefix": schema.StripModulePrefix}
|
||||
yangFragTmpl, err := template.New("frag").Funcs(yangFuncs).ParseFS(templateFS,
|
||||
"fragments/yang-tree-node.html",
|
||||
"fragments/yang-node-detail.html",
|
||||
"fragments/yang-leaf-group.html",
|
||||
"fragments/yang-list-table.html")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
login := &auth.LoginHandler{
|
||||
Store: store,
|
||||
RC: rc,
|
||||
Template: loginTmpl,
|
||||
OnLogin: func(ctx context.Context) {
|
||||
schemaCache.RefreshBackground(ctx)
|
||||
},
|
||||
}
|
||||
|
||||
dash := &handlers.DashboardHandler{
|
||||
@@ -146,8 +169,16 @@ func New(
|
||||
services := &handlers.ServicesHandler{Template: servicesTmpl, RC: rc}
|
||||
containers := &handlers.ContainersHandler{Template: containersTmpl, RC: rc}
|
||||
cfg := &handlers.ConfigureHandler{RC: rc}
|
||||
cfgSys := &handlers.ConfigureSystemHandler{Template: cfgSysTmpl, RC: rc}
|
||||
cfgUsers := &handlers.ConfigureUsersHandler{Template: cfgUsersTmpl, RC: rc}
|
||||
cfgSys := &handlers.ConfigureSystemHandler{Template: cfgSysTmpl, RC: rc, Schema: schemaCache}
|
||||
cfgUsers := &handlers.ConfigureUsersHandler{Template: cfgUsersTmpl, RC: rc, Schema: schemaCache}
|
||||
schemaH := &handlers.SchemaHandler{Cache: schemaCache}
|
||||
dataH := &handlers.DataHandler{RC: rc, Schema: schemaCache}
|
||||
treeH := &handlers.TreeHandler{
|
||||
Cache: schemaCache,
|
||||
RC: rc,
|
||||
PageTmpl: yangTreeTmpl,
|
||||
FragTmpl: yangFragTmpl,
|
||||
}
|
||||
|
||||
mux := http.NewServeMux()
|
||||
|
||||
@@ -185,10 +216,11 @@ func New(
|
||||
mux.HandleFunc("GET /containers", containers.Overview)
|
||||
|
||||
// Configure routes.
|
||||
mux.HandleFunc("POST /configure/enter", cfg.Enter)
|
||||
mux.HandleFunc("POST /configure/apply", cfg.Apply)
|
||||
mux.HandleFunc("POST /configure/apply-and-save", cfg.ApplyAndSave)
|
||||
mux.HandleFunc("POST /configure/abort", cfg.Abort)
|
||||
mux.HandleFunc("POST /configure/enter", cfg.Enter)
|
||||
mux.HandleFunc("POST /configure/apply", cfg.Apply)
|
||||
mux.HandleFunc("POST /configure/apply-and-save", cfg.ApplyAndSave)
|
||||
mux.HandleFunc("POST /configure/abort", cfg.Abort)
|
||||
mux.HandleFunc("POST /configure/save", cfg.Save)
|
||||
mux.HandleFunc("GET /configure/system", cfgSys.Overview)
|
||||
mux.HandleFunc("POST /configure/system/identity", cfgSys.SaveIdentity)
|
||||
mux.HandleFunc("POST /configure/system/clock", cfgSys.SaveClock)
|
||||
@@ -205,6 +237,27 @@ func New(
|
||||
mux.HandleFunc("POST /configure/users/groups/{name}/members", cfgUsers.AddGroupMembers)
|
||||
mux.HandleFunc("DELETE /configure/users/groups/{name}/members/{user}", cfgUsers.RemoveGroupMember)
|
||||
|
||||
// Schema API routes (authenticated).
|
||||
mux.HandleFunc("GET /api/schema", schemaH.Schema)
|
||||
mux.HandleFunc("GET /api/schema/children", schemaH.Children)
|
||||
|
||||
// Data API route (authenticated) — raw RESTCONF JSON passthrough.
|
||||
mux.HandleFunc("GET /api/data", dataH.Get)
|
||||
|
||||
// YANG tree UI routes (authenticated).
|
||||
mux.HandleFunc("GET /configure/tree", treeH.Overview)
|
||||
mux.HandleFunc("GET /configure/tree/children", treeH.TreeChildren)
|
||||
mux.HandleFunc("GET /configure/tree/node", treeH.TreeNode)
|
||||
mux.HandleFunc("PUT /configure/tree/node", treeH.SaveLeaf)
|
||||
mux.HandleFunc("DELETE /configure/tree/node", treeH.DeleteLeaf)
|
||||
mux.HandleFunc("PUT /configure/tree/group", treeH.SaveGroup)
|
||||
mux.HandleFunc("GET /configure/tree/list-add", treeH.AddListRowForm)
|
||||
mux.HandleFunc("POST /configure/tree/list-row", treeH.SaveListRow)
|
||||
mux.HandleFunc("DELETE /configure/tree/list-row", treeH.DeleteListRow)
|
||||
mux.HandleFunc("PUT /configure/tree/presence", treeH.TogglePresence)
|
||||
mux.HandleFunc("DELETE /configure/tree/presence", treeH.TogglePresence)
|
||||
mux.HandleFunc("DELETE /configure/tree/container", treeH.DeleteContainer)
|
||||
|
||||
handler := authMiddleware(store, mux)
|
||||
handler = csrfMiddleware(handler)
|
||||
handler = securityHeadersMiddleware(handler)
|
||||
|
||||
@@ -94,9 +94,10 @@ func (m *MockFetcher) PostJSON(_ context.Context, path string, _ any) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MockFetcher) Put(_ context.Context, _ string, _ any) error { return nil }
|
||||
func (m *MockFetcher) Patch(_ context.Context, _ string, _ any) error { return nil }
|
||||
func (m *MockFetcher) Delete(_ context.Context, _ string) error { return nil }
|
||||
func (m *MockFetcher) GetYANG(_ context.Context, _, _ string) ([]byte, error) { return nil, nil }
|
||||
func (m *MockFetcher) Put(_ context.Context, _ string, _ any) error { return nil }
|
||||
func (m *MockFetcher) Patch(_ context.Context, _ string, _ any) error { return nil }
|
||||
func (m *MockFetcher) Delete(_ context.Context, _ string) error { return nil }
|
||||
|
||||
func (m *MockFetcher) GetDatastore(_ context.Context, _ string) (json.RawMessage, error) {
|
||||
return json.RawMessage("{}"), nil
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user