webui: depend on kernelkit/goyang fork instead of an in-tree copy

The three YANG 1.1 fixes lived as a frozen, one-shot copy of goyang
v1.6.3 under internal/goyang (via a local `replace`) — invisible to
dependency/CVE tracking and impossible to update.  Move them to the
maintained kernelkit/goyang fork (v1.6.3-kkit branch) and depend on it
through go.mod, pinned by commit hash; drop the in-tree copy.  getopt
falls out of the dependency set since it was only used by goyang's CLI,
not pkg/yang.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
This commit is contained in:
Joachim Wiberg
2026-06-17 20:50:01 +02:00
parent 19c820fac2
commit d02f45d777
82 changed files with 20 additions and 21177 deletions
+6 -10
View File
@@ -4,14 +4,10 @@ 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
)
require github.com/openconfig/goyang v1.6.3
// 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
require github.com/google/go-cmp v0.7.0 // indirect
// kernelkit/goyang fork carrying our YANG 1.1 fixes: reference on Value,
// multiple uses-augments, and must in rpc input/output.
replace github.com/openconfig/goyang => github.com/kernelkit/goyang v1.6.4-0.20260617163501-afcacf84230c
+6 -4
View File
@@ -1,6 +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/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=
github.com/kernelkit/goyang v1.6.4-0.20260617163501-afcacf84230c h1:CFApC5asdQoMmQZ1YdP2fDX38K37vObCH8EEKeMFHE8=
github.com/kernelkit/goyang v1.6.4-0.20260617163501-afcacf84230c/go.mod h1:5WolITjek1NF8yrNERyVZ7jqjOClJTpO8p/+OwmETM4=
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=
-15
View File
@@ -1,15 +0,0 @@
# 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"
-53
View File
@@ -1,53 +0,0 @@
---
#########################
#########################
## 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
@@ -1,59 +0,0 @@
---
###########################################
# 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
-15
View File
@@ -1,15 +0,0 @@
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
-1
View File
@@ -1 +0,0 @@
tags
-9
View File
@@ -1,9 +0,0 @@
# 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.
-25
View File
@@ -1,25 +0,0 @@
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).
-15
View File
@@ -1,15 +0,0 @@
# 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>
-14
View File
@@ -1,14 +0,0 @@
// 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.
-202
View File
@@ -1,202 +0,0 @@
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.
-54
View File
@@ -1,54 +0,0 @@
![Go](https://github.com/openconfig/goyang/workflows/Go/badge.svg?branch=master)
[![Coverage Status](https://coveralls.io/repos/github/openconfig/goyang/badge.svg?branch=master)](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.
-12
View File
@@ -1,12 +0,0 @@
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
)
-8
View File
@@ -1,8 +0,0 @@
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=
@@ -1,112 +0,0 @@
// 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
}
@@ -1,146 +0,0 @@
// 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")
}
-461
View File
@@ -1,461 +0,0 @@
// 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
}
}
@@ -1,538 +0,0 @@
// 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
}
@@ -1,571 +0,0 @@
// 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;
}
}
}`
@@ -1,94 +0,0 @@
// 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)
}
@@ -1,53 +0,0 @@
// 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)
}
}
}
-48
View File
@@ -1,48 +0,0 @@
// 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
-167
View File
@@ -1,167 +0,0 @@
// 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])
}
@@ -1,166 +0,0 @@
// 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)
}
})
}
}
@@ -1,96 +0,0 @@
// 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
}
@@ -1,358 +0,0 @@
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)
}
})
}
}
@@ -1,192 +0,0 @@
// 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
}
@@ -1,802 +0,0 @@
// 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)
}
}
})
}
}
-522
View File
@@ -1,522 +0,0 @@
// 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()
}
}
}
@@ -1,309 +0,0 @@
// 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)
}
}
}
@@ -1,832 +0,0 @@
// 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)
}
}
}
}
}
}
@@ -1,466 +0,0 @@
// 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{}
}
@@ -1,414 +0,0 @@
// 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)
}
})
}
}
-388
View File
@@ -1,388 +0,0 @@
// 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)
}
}
}
}
}
@@ -1,622 +0,0 @@
// 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)
}
})
}
}
@@ -1,59 +0,0 @@
// 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
}
-338
View File
@@ -1,338 +0,0 @@
// 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)
}
@@ -1,539 +0,0 @@
// 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
}
}
}
@@ -1,89 +0,0 @@
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";
}
}
}
@@ -1,42 +0,0 @@
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;
}
}
@@ -1,106 +0,0 @@
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";
}
}
}
@@ -1,81 +0,0 @@
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";
}
}
}
-425
View File
@@ -1,425 +0,0 @@
// 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
}
@@ -1,700 +0,0 @@
// 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
}
@@ -1,915 +0,0 @@
// 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
@@ -1,330 +0,0 @@
// 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
}
@@ -1,136 +0,0 @@
// 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)
}
})
}
}
@@ -1,78 +0,0 @@
// 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
}
@@ -1,129 +0,0 @@
// 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)
}
}
})
}
}
@@ -1,6 +0,0 @@
module test-module {
prefix "t";
namespace "urn:t";
leaf valid-leaf { type string; }
}
@@ -1,6 +0,0 @@
module test-module {
prefix "t";
namespace "urn:t";
leaf invalid-leaf { }
}
@@ -1,6 +0,0 @@
module test-module {
prefix "t";
namespace "urn:t";
import imported { prefix "i"; }
}
@@ -1,6 +0,0 @@
module test-module {
prefix "t";
namespace "urn:t";
import import-not-found { prefix "i"; }
}
@@ -1,6 +0,0 @@
module module-one {
prefix "t";
namespace "urn:t";
leaf one { type int8; }
}
@@ -1,6 +0,0 @@
module module-two {
prefix "t";
namespace "urn:t";
leaf two { type int8; }
}
@@ -1,12 +0,0 @@
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";
}
@@ -1,9 +0,0 @@
submodule circular-sub-one {
yang-version "1.1";
belongs-to circular-main { prefix "main"; }
include circular-sub-two;
revision "2025-02-15";
}
@@ -1,9 +0,0 @@
submodule circular-sub-two {
yang-version "1.1";
belongs-to circular-main { prefix "main"; }
include circular-sub-one;
revision "2025-02-15";
}
@@ -1,5 +0,0 @@
module imported {
prefix "imported";
namespace "urn:i";
}
-28
View File
@@ -1,28 +0,0 @@
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
View File
@@ -1,93 +0,0 @@
// 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
View File
@@ -1,30 +0,0 @@
// 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
View File
@@ -1,26 +0,0 @@
// 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; }
}
}
-12
View File
@@ -1,12 +0,0 @@
// 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.";
}
-112
View File
@@ -1,112 +0,0 @@
// 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
}
-135
View File
@@ -1,135 +0,0 @@
// 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)
}
}
-214
View File
@@ -1,214 +0,0 @@
// 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)
}
+6 -6
View File
@@ -626,12 +626,12 @@ type Uses struct {
Extensions []*Statement `yang:"Ext" json:"-"`
Augment []*Augment `yang:"augment" json:",omitempty"`
Description *Value `yang:"description" json:",omitempty"`
IfFeature []*Value `yang:"if-feature" json:"-"`
Refine []*Refine `yang:"refine" json:"-"`
Reference *Value `yang:"reference" json:"-"`
Status *Value `yang:"status" json:"-"`
When *Value `yang:"when" json:",omitempty"`
Description *Value `yang:"description" json:",omitempty"`
IfFeature []*Value `yang:"if-feature" json:"-"`
Refine []*Refine `yang:"refine" json:"-"`
Reference *Value `yang:"reference" json:"-"`
Status *Value `yang:"status" json:"-"`
When *Value `yang:"when" json:",omitempty"`
}
func (Uses) Kind() string { return "uses" }
+2 -4
View File
@@ -5,10 +5,8 @@ github.com/google/go-cmp/cmp/internal/diff
github.com/google/go-cmp/cmp/internal/flags
github.com/google/go-cmp/cmp/internal/function
github.com/google/go-cmp/cmp/internal/value
# github.com/openconfig/goyang v1.6.3 => ./internal/goyang
# github.com/openconfig/goyang v1.6.3 => github.com/kernelkit/goyang v1.6.4-0.20260617163501-afcacf84230c
## explicit; go 1.22.0
github.com/openconfig/goyang/pkg/indent
github.com/openconfig/goyang/pkg/yang
# github.com/pborman/getopt v1.1.0
## explicit; go 1.13
# github.com/openconfig/goyang => ./internal/goyang
# github.com/openconfig/goyang => github.com/kernelkit/goyang v1.6.4-0.20260617163501-afcacf84230c