public class NormalizedNodeWriter implements Closeable, Flushable {
private final NormalizedNodeStreamWriter writer;
- private NormalizedNodeWriter(final NormalizedNodeStreamWriter writer) {
+ protected NormalizedNodeWriter(final NormalizedNodeStreamWriter writer) {
this.writer = Preconditions.checkNotNull(writer);
}
* @return NormalizedNodeWriter this
* @throws IOException when thrown from the backing writer.
*/
- public final NormalizedNodeWriter write(final NormalizedNode<?, ?> node) throws IOException {
+ public NormalizedNodeWriter write(final NormalizedNode<?, ?> node) throws IOException {
if (wasProcessedAsCompositeNode(node)) {
return this;
}
* @param children Child nodes
* @return Best estimate of the collection size required to hold all the children.
*/
- static final int childSizeHint(final Iterable<?> children) {
+ protected static final int childSizeHint(final Iterable<?> children) {
return (children instanceof Collection) ? ((Collection<?>) children).size() : UNKNOWN_SIZE;
}
- private boolean wasProcessAsSimpleNode(final NormalizedNode<?, ?> node) throws IOException {
+ protected boolean wasProcessAsSimpleNode(final NormalizedNode<?, ?> node) throws IOException {
if (node instanceof LeafSetEntryNode) {
final LeafSetEntryNode<?> nodeAsLeafList = (LeafSetEntryNode<?>)node;
final QName name = nodeAsLeafList.getIdentifier().getNodeType();
* @return True
* @throws IOException when the writer reports it
*/
- protected final boolean writeChildren(final Iterable<? extends NormalizedNode<?, ?>> children) throws IOException {
+ protected boolean writeChildren(final Iterable<? extends NormalizedNode<?, ?>> children) throws IOException {
for (final NormalizedNode<?, ?> child : children) {
write(child);
}
return writeChildren(node.getValue());
}
- private boolean wasProcessedAsCompositeNode(final NormalizedNode<?, ?> node) throws IOException {
+ protected boolean wasProcessedAsCompositeNode(final NormalizedNode<?, ?> node) throws IOException {
if (node instanceof ContainerNode) {
final ContainerNode n = (ContainerNode) node;
if(writer instanceof NormalizedNodeStreamAttributeWriter) {
private final DataNodeContainer root;
private SchemaTracker(final SchemaContext context, final SchemaPath path) {
- SchemaNode current = Preconditions.checkNotNull(context);
- for (final QName qname : path.getPathFromRoot()) {
- SchemaNode child;
- if(current instanceof DataNodeContainer) {
- child = ((DataNodeContainer) current).getDataChildByName(qname);
-
- if (child == null && current instanceof SchemaContext) {
- child = tryFindGroupings((SchemaContext) current, qname).orNull();
- }
-
- if(child == null && current instanceof SchemaContext) {
- child = tryFindNotification((SchemaContext) current, qname)
- .or(tryFindRpc(((SchemaContext) current), qname)).orNull();
- }
- } else if (current instanceof ChoiceSchemaNode) {
- child = ((ChoiceSchemaNode) current).getCaseNodeByName(qname);
- } else if (current instanceof RpcDefinition) {
- switch (qname.getLocalName()) {
- case "input":
- child = ((RpcDefinition) current).getInput();
- break;
- case "output":
- child = ((RpcDefinition) current).getOutput();
- break;
- default:
- child = null;
- break;
- }
- } else {
- throw new IllegalArgumentException(String.format("Schema node %s does not allow children.", current));
- }
- current = child;
- }
+ SchemaNode current = SchemaUtils.findParentSchemaOnPath(context, path);
Preconditions.checkArgument(current instanceof DataNodeContainer,"Schema path must point to container or list or an rpc input/output. Supplied path %s pointed to: %s",path,current);
root = (DataNodeContainer) current;
}
- private static Optional<SchemaNode> tryFindGroupings(final SchemaContext ctx, final QName qname) {
- return Optional.<SchemaNode> fromNullable(Iterables.find(ctx.getGroupings(), new SchemaNodePredicate(qname), null));
- }
-
- private static Optional<SchemaNode> tryFindRpc(final SchemaContext ctx, final QName qname) {
- return Optional.<SchemaNode>fromNullable(Iterables.find(ctx.getOperations(), new SchemaNodePredicate(qname), null));
- }
-
- private static Optional<SchemaNode> tryFindNotification(final SchemaContext ctx, final QName qname) {
- return Optional.<SchemaNode>fromNullable(Iterables.find(ctx.getNotifications(), new SchemaNodePredicate(qname), null));
- }
-
/**
* Create a new writer with the specified context as its root.
*
return schemaStack.pop();
}
- private static final class SchemaNodePredicate implements Predicate<SchemaNode> {
- private final QName qname;
-
- public SchemaNodePredicate(final QName qname) {
- this.qname = qname;
- }
-
- @Override
- public boolean apply(final SchemaNode input) {
- return input.getQName().equals(qname);
- }
- }
}
--- /dev/null
+/*
+ * Copyright (c) 2016 Cisco Systems, Inc. and others. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.opendaylight.yangtools.yang.data.impl.schema;
+
+import com.google.common.collect.ArrayListMultimap;
+import java.io.IOException;
+import java.util.Collection;
+import java.util.List;
+import java.util.Objects;
+import org.opendaylight.yangtools.yang.common.QName;
+import org.opendaylight.yangtools.yang.data.api.schema.AugmentationNode;
+import org.opendaylight.yangtools.yang.data.api.schema.DataContainerChild;
+import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode;
+import org.opendaylight.yangtools.yang.data.api.schema.stream.NormalizedNodeStreamWriter;
+import org.opendaylight.yangtools.yang.data.api.schema.stream.NormalizedNodeWriter;
+import org.opendaylight.yangtools.yang.model.api.ChoiceCaseNode;
+import org.opendaylight.yangtools.yang.model.api.ChoiceSchemaNode;
+import org.opendaylight.yangtools.yang.model.api.DataNodeContainer;
+import org.opendaylight.yangtools.yang.model.api.DataSchemaNode;
+import org.opendaylight.yangtools.yang.model.api.ListSchemaNode;
+import org.opendaylight.yangtools.yang.model.api.SchemaContext;
+import org.opendaylight.yangtools.yang.model.api.SchemaNode;
+import org.opendaylight.yangtools.yang.model.api.SchemaPath;
+
+/**
+ * This is an iterator over a {@link NormalizedNode}. Unlike {@link NormalizedNodeWriter},
+ * this iterates over elements in order as they are defined in .yang file.
+ */
+public class SchemaOrderedNormalizedNodeWriter extends NormalizedNodeWriter {
+
+ private final SchemaContext schemaContext;
+ private final SchemaNode root;
+ private final NormalizedNodeStreamWriter writer;
+
+ private SchemaNode currentSchemaNode;
+
+ /**
+ * Create a new writer backed by a {@link NormalizedNodeStreamWriter}.
+ * @param writer Back-end writer
+ * @param schemaContext Schema context
+ * @param path path
+ */
+ public SchemaOrderedNormalizedNodeWriter(final NormalizedNodeStreamWriter writer, final SchemaContext schemaContext, final SchemaPath path) {
+ super(writer);
+ this.writer = writer;
+ this.schemaContext = schemaContext;
+ this.root = SchemaUtils.findParentSchemaOnPath(schemaContext, path);
+ }
+
+ @Override
+ public SchemaOrderedNormalizedNodeWriter write(final NormalizedNode<?, ?> node) throws IOException {
+ if (Objects.equals(root, schemaContext)) {
+ currentSchemaNode = schemaContext.getDataChildByName(node.getNodeType());
+ } else {
+ currentSchemaNode = root;
+ }
+ return write(node, currentSchemaNode);
+
+ }
+
+ /**
+ * Iterate over the provided collection and emit write
+ * events to the encapsulated {@link NormalizedNodeStreamWriter}.
+ *
+ * @param nodes nodes
+ * @return NormalizedNodeWriter this
+ * @throws IOException when thrown from the backing writer.
+ */
+ public SchemaOrderedNormalizedNodeWriter write(final Collection<DataContainerChild<?,?>> nodes) throws IOException {
+ currentSchemaNode = root;
+ if (writeChildren(nodes, currentSchemaNode, false)) {
+ return this;
+ }
+
+ throw new IllegalStateException("It wasn't possible to serialize nodes " + nodes);
+
+ }
+
+ private SchemaOrderedNormalizedNodeWriter write(final NormalizedNode<?, ?> node, final SchemaNode dataSchemaNode) throws IOException {
+
+ //Set current schemaNode
+ try (SchemaNodeSetter sns = new SchemaNodeSetter(dataSchemaNode)) {
+ if (node == null) {
+ return this;
+ }
+
+ if (wasProcessedAsCompositeNode(node)) {
+ return this;
+ }
+
+ if (wasProcessAsSimpleNode(node)) {
+ return this;
+ }
+ }
+
+ throw new IllegalStateException("It wasn't possible to serialize node " + node);
+ }
+
+ private void write(final List<NormalizedNode<?, ?>> nodes, final SchemaNode dataSchemaNode) throws IOException {
+ for (NormalizedNode<?, ?> node : nodes) {
+ write(node, dataSchemaNode);
+ }
+ }
+
+ protected boolean writeChildren(final Iterable<? extends NormalizedNode<?, ?>> children) throws IOException {
+ return writeChildren(children, currentSchemaNode, true);
+ }
+
+ private boolean writeChildren(final Iterable<? extends NormalizedNode<?, ?>> children, final SchemaNode parentSchemaNode, boolean endParent) throws IOException {
+ //Augmentations cannot be gotten with node.getChild so create our own structure with augmentations resolved
+ ArrayListMultimap<QName, NormalizedNode<?, ?>> qNameToNodes = ArrayListMultimap.create();
+ for (NormalizedNode<?, ?> child : children) {
+ if (child instanceof AugmentationNode) {
+ qNameToNodes.putAll(resolveAugmentations(child));
+ } else {
+ qNameToNodes.put(child.getNodeType(), child);
+ }
+ }
+
+ if (parentSchemaNode instanceof DataNodeContainer) {
+ if (parentSchemaNode instanceof ListSchemaNode && qNameToNodes.containsKey(parentSchemaNode.getQName())) {
+ write(qNameToNodes.get(parentSchemaNode.getQName()), parentSchemaNode);
+ } else {
+ for (DataSchemaNode schemaNode : ((DataNodeContainer) parentSchemaNode).getChildNodes()) {
+ write(qNameToNodes.get(schemaNode.getQName()), schemaNode);
+ }
+ }
+ } else if(parentSchemaNode instanceof ChoiceSchemaNode) {
+ for (ChoiceCaseNode ccNode : ((ChoiceSchemaNode) parentSchemaNode).getCases()) {
+ for (DataSchemaNode dsn : ccNode.getChildNodes()) {
+ if (qNameToNodes.containsKey(dsn.getQName())) {
+ write(qNameToNodes.get(dsn.getQName()), dsn);
+ }
+ }
+ }
+ } else {
+ for (NormalizedNode<?, ?> child : children) {
+ writeLeaf(child);
+ }
+ }
+ if (endParent) {
+ writer.endNode();
+ }
+ return true;
+ }
+
+ private SchemaOrderedNormalizedNodeWriter writeLeaf(final NormalizedNode<?, ?> node) throws IOException {
+ if (wasProcessAsSimpleNode(node)) {
+ return this;
+ }
+
+ throw new IllegalStateException("It wasn't possible to serialize node " + node);
+ }
+
+ private ArrayListMultimap<QName, NormalizedNode<?, ?>> resolveAugmentations(final NormalizedNode<?, ?> child) {
+ final ArrayListMultimap<QName, NormalizedNode<?, ?>> resolvedAugs = ArrayListMultimap.create();
+ for (NormalizedNode<?, ?> node : ((AugmentationNode) child).getValue()) {
+ if (node instanceof AugmentationNode) {
+ resolvedAugs.putAll(resolveAugmentations(node));
+ } else {
+ resolvedAugs.put(node.getNodeType(), node);
+ }
+ }
+ return resolvedAugs;
+ }
+
+
+ private class SchemaNodeSetter implements AutoCloseable {
+
+ private final SchemaNode previousSchemaNode;
+
+ /**
+ * Sets current schema node new value and store old value for later restore
+ */
+ public SchemaNodeSetter(final SchemaNode schemaNode) {
+ previousSchemaNode = SchemaOrderedNormalizedNodeWriter.this.currentSchemaNode;
+ SchemaOrderedNormalizedNodeWriter.this.currentSchemaNode = schemaNode;
+ }
+
+ /**
+ * Restore previous schema node
+ */
+ @Override
+ public void close() {
+ SchemaOrderedNormalizedNodeWriter.this.currentSchemaNode = previousSchemaNode;
+ }
+ }
+
+}
\ No newline at end of file
import com.google.common.base.Function;
import com.google.common.base.Optional;
import com.google.common.base.Preconditions;
+import com.google.common.base.Predicate;
import com.google.common.collect.Collections2;
import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Iterables;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import java.util.Collection;
import org.opendaylight.yangtools.yang.model.api.ChoiceSchemaNode;
import org.opendaylight.yangtools.yang.model.api.DataNodeContainer;
import org.opendaylight.yangtools.yang.model.api.DataSchemaNode;
+import org.opendaylight.yangtools.yang.model.api.RpcDefinition;
+import org.opendaylight.yangtools.yang.model.api.SchemaContext;
+import org.opendaylight.yangtools.yang.model.api.SchemaNode;
+import org.opendaylight.yangtools.yang.model.api.SchemaPath;
public final class SchemaUtils {
return new AugmentationIdentifier(ImmutableSet.copyOf(qnames));
}
+ /**
+ * Finds schema node for given path in schema context.
+ * @param schemaContext schema context
+ * @param path path
+ * @return schema node on path
+ */
+ public static SchemaNode findParentSchemaOnPath(final SchemaContext schemaContext, final SchemaPath path) {
+ SchemaNode current = Preconditions.checkNotNull(schemaContext);
+ for (final QName qname : path.getPathFromRoot()) {
+ SchemaNode child;
+ if(current instanceof DataNodeContainer) {
+ child = ((DataNodeContainer) current).getDataChildByName(qname);
+
+ if (child == null && current instanceof SchemaContext) {
+ child = tryFindGroupings((SchemaContext) current, qname).orNull();
+ }
+
+ if(child == null && current instanceof SchemaContext) {
+ child = tryFindNotification((SchemaContext) current, qname)
+ .or(tryFindRpc(((SchemaContext) current), qname)).orNull();
+ }
+ } else if (current instanceof ChoiceSchemaNode) {
+ child = ((ChoiceSchemaNode) current).getCaseNodeByName(qname);
+ } else if (current instanceof RpcDefinition) {
+ switch (qname.getLocalName()) {
+ case "input":
+ child = ((RpcDefinition) current).getInput();
+ break;
+ case "output":
+ child = ((RpcDefinition) current).getOutput();
+ break;
+ default:
+ child = null;
+ break;
+ }
+ } else {
+ throw new IllegalArgumentException(String.format("Schema node %s does not allow children.", current));
+ }
+ current = child;
+ }
+ return current;
+ }
+
+ private static Optional<SchemaNode> tryFindGroupings(final SchemaContext ctx, final QName qname) {
+ return Optional.<SchemaNode> fromNullable(Iterables.find(ctx.getGroupings(), new SchemaNodePredicate(qname), null));
+ }
+
+ private static Optional<SchemaNode> tryFindRpc(final SchemaContext ctx, final QName qname) {
+ return Optional.<SchemaNode>fromNullable(Iterables.find(ctx.getOperations(), new SchemaNodePredicate(qname), null));
+ }
+
+ private static Optional<SchemaNode> tryFindNotification(final SchemaContext ctx, final QName qname) {
+ return Optional.<SchemaNode>fromNullable(Iterables.find(ctx.getNotifications(), new SchemaNodePredicate(qname), null));
+ }
+
+ private static final class SchemaNodePredicate implements Predicate<SchemaNode> {
+ private final QName qname;
+
+ public SchemaNodePredicate(final QName qname) {
+ this.qname = qname;
+ }
+
+ @Override
+ public boolean apply(final SchemaNode input) {
+ return input.getQName().equals(qname);
+ }
+ }
+
}
--- /dev/null
+/*
+ * Copyright (c) 2016 Cisco Systems, Inc. and others. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.opendaylight.yangtools.yang.data.impl.schema;
+
+import java.io.File;
+import java.io.StringWriter;
+import java.net.URISyntaxException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import javax.xml.stream.XMLOutputFactory;
+import javax.xml.stream.XMLStreamWriter;
+import org.custommonkey.xmlunit.Diff;
+import org.custommonkey.xmlunit.XMLAssert;
+import org.custommonkey.xmlunit.XMLUnit;
+import org.junit.Before;
+import org.junit.Test;
+import org.opendaylight.yangtools.yang.common.QName;
+import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
+import org.opendaylight.yangtools.yang.data.api.schema.DataContainerChild;
+import org.opendaylight.yangtools.yang.data.api.schema.MapEntryNode;
+import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode;
+import org.opendaylight.yangtools.yang.data.api.schema.stream.NormalizedNodeStreamWriter;
+import org.opendaylight.yangtools.yang.data.api.schema.stream.NormalizedNodeWriter;
+import org.opendaylight.yangtools.yang.data.impl.codec.xml.XMLStreamNormalizedNodeStreamWriter;
+import org.opendaylight.yangtools.yang.model.api.SchemaContext;
+import org.opendaylight.yangtools.yang.model.api.SchemaPath;
+import org.opendaylight.yangtools.yang.parser.impl.YangParserImpl;
+
+public class SchemaOrderedNormalizedNodeWriterTest {
+
+ private static final String EXPECTED_1 =
+ "<root>\n" +
+ " <policy>\n" +
+ " <name>policy1</name>\n" +
+ " <rule>\n" +
+ " <name>rule1</name>\n" +
+ " </rule>\n" +
+ " <rule>\n" +
+ " <name>rule2</name>\n" +
+ " </rule>\n" +
+ " <rule>\n" +
+ " <name>rule3</name>\n" +
+ " </rule>\n" +
+ " <rule>\n" +
+ " <name>rule4</name>\n" +
+ " </rule>\n" +
+ " </policy>\n" +
+ " <policy>\n" +
+ " <name>policy2</name>\n" +
+ " </policy>\n" +
+ "</root>\n";
+
+
+ private static final String EXPECTED_2 = "<root>\n" +
+ " <id>id1</id>\n" +
+ " <cont>\n" +
+ " <content>content1</content>\n" +
+ " </cont>\n" +
+ "</root>";
+
+
+ @Before
+ public void setUp() throws Exception {
+ XMLUnit.setIgnoreWhitespace(true);
+ }
+
+ @Test
+ public void testWrite() throws Exception {
+ final StringWriter stringWriter = new StringWriter();
+ final XMLStreamWriter xmlStreamWriter = XMLOutputFactory.newFactory().createXMLStreamWriter(stringWriter);
+
+ SchemaContext schemaContext = getSchemaContext("/bug-1848.yang");
+ NormalizedNodeStreamWriter writer = XMLStreamNormalizedNodeStreamWriter.create(xmlStreamWriter, schemaContext);
+ SchemaOrderedNormalizedNodeWriter nnw = new SchemaOrderedNormalizedNodeWriter(writer, schemaContext, SchemaPath.ROOT);
+
+ List<MapEntryNode> rule1Names = new ArrayList<>();
+ rule1Names.add(ImmutableNodes.mapEntry(createQName("foo", "rule"), createQName("foo", "name"), "rule1"));
+ rule1Names.add(ImmutableNodes.mapEntry(createQName("foo", "rule"), createQName("foo", "name"), "rule2"));
+
+ List<MapEntryNode> rule2Names = new ArrayList<>();
+ rule1Names.add(ImmutableNodes.mapEntry(createQName("foo", "rule"), createQName("foo", "name"), "rule3"));
+ rule1Names.add(ImmutableNodes.mapEntry(createQName("foo", "rule"), createQName("foo", "name"), "rule4"));
+
+ DataContainerChild<?, ?> rules1 = Builders.orderedMapBuilder()
+ .withNodeIdentifier(getNodeIdentifier("foo", "rule"))
+ .withValue(rule1Names)
+ .build();
+ DataContainerChild<?, ?> rules2 = Builders.orderedMapBuilder()
+ .withNodeIdentifier(getNodeIdentifier("foo", "rule"))
+ .withValue(rule2Names)
+ .build();
+
+ List<MapEntryNode> policyNodes = new ArrayList<>();
+
+
+ final MapEntryNode pn1 = ImmutableNodes
+ .mapEntryBuilder(createQName("foo", "policy"), createQName("foo", "name"), "policy1")
+ .withChild(rules1)
+ .build();
+ final MapEntryNode pn2 = ImmutableNodes
+ .mapEntryBuilder(createQName("foo", "policy"), createQName("foo", "name"), "policy2")
+ .withChild(rules2)
+ .build();
+ policyNodes.add(pn1);
+ policyNodes.add(pn2);
+
+
+
+ DataContainerChild<?, ?> policy = Builders.orderedMapBuilder()
+ .withNodeIdentifier(getNodeIdentifier("foo", "policy"))
+ .withValue(policyNodes)
+ .build();
+ NormalizedNode<?, ?> root = Builders.containerBuilder()
+ .withNodeIdentifier(getNodeIdentifier("foo", "root"))
+ .withChild(policy).build();
+ nnw.write(root);
+
+ XMLAssert.assertXMLIdentical(new Diff(EXPECTED_1, stringWriter.toString()), true);
+ }
+
+ @Test
+ public void testWriteOrder() throws Exception {
+ final StringWriter stringWriter = new StringWriter();
+ final XMLStreamWriter xmlStreamWriter = XMLOutputFactory.newFactory().createXMLStreamWriter(stringWriter);
+ SchemaContext schemaContext = getSchemaContext("/bug-1848-2.yang");
+ NormalizedNodeStreamWriter writer = XMLStreamNormalizedNodeStreamWriter.create(xmlStreamWriter, schemaContext);
+
+ NormalizedNodeWriter nnw = new SchemaOrderedNormalizedNodeWriter(writer, schemaContext, SchemaPath.ROOT);
+
+ DataContainerChild<?, ?> cont = Builders.containerBuilder()
+ .withNodeIdentifier(getNodeIdentifier("order", "cont"))
+ .withChild(ImmutableNodes.leafNode(createQName("order", "content"), "content1"))
+ .build();
+
+ NormalizedNode<?, ?> root = Builders.containerBuilder()
+ .withNodeIdentifier(getNodeIdentifier("order", "root"))
+ .withChild(cont)
+ .withChild(ImmutableNodes.leafNode(createQName("order", "id"), "id1"))
+ .build();
+
+ nnw.write(root);
+ System.out.println(stringWriter.toString());
+ System.out.println(EXPECTED_2);
+
+ XMLAssert.assertXMLIdentical(new Diff(EXPECTED_2, stringWriter.toString()), true);
+ }
+
+ private SchemaContext getSchemaContext(String filePath) throws URISyntaxException {
+ File file = new File(getClass().getResource(filePath).toURI());
+ return YangParserImpl.getInstance().parseFiles(Collections.singletonList(file));
+ }
+
+ private YangInstanceIdentifier.NodeIdentifier getNodeIdentifier(String ns, String name) {
+ return YangInstanceIdentifier.NodeIdentifier.create(createQName(ns, name));
+ }
+
+ private QName createQName(String ns, String name) {
+ return QName.create(ns, "2016-02-17", name);
+ }
+
+}
\ No newline at end of file
--- /dev/null
+module order {
+ namespace "order";
+ prefix "order";
+ revision "2016-02-17";
+ container root {
+ leaf id {
+ type string;
+ }
+ container cont {
+ leaf content {
+ type string;
+ }
+ }
+
+ }
+}
\ No newline at end of file
--- /dev/null
+module foo {
+ namespace "foo";
+ prefix "foo";
+ revision "2016-02-17";
+ container root {
+ list policy {
+ key name;
+ leaf name { type string; }
+ list rule {
+ key name;
+ leaf name { type string; }
+ }
+ }
+ }
+}
\ No newline at end of file