+/*
+ * Copyright (c) 2021 PANTHEON.tech, s.r.o. 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.restconf.nb.rfc8040;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThrows;
+
+import java.text.ParseException;
+import java.util.List;
+import org.junit.Test;
+import org.opendaylight.restconf.nb.rfc8040.ApiPath.ApiIdentifier;
+import org.opendaylight.restconf.nb.rfc8040.FieldsParameter.NodeSelector;
+
+public class FieldsParameterTest {
+ // https://datatracker.ietf.org/doc/html/rfc8040#section-4.8.3:
+ // ";" is used to select multiple nodes. For example, to retrieve only
+ // the "genre" and "year" of an album, use "fields=genre;year".
+ @Test
+ public void testGenreYear() {
+ final var selectors = assertValidFields("genre;year");
+ assertEquals(2, selectors.size());
+
+ var selector = selectors.get(0);
+ assertEquals(List.of(new ApiIdentifier(null, "genre")), selector.path());
+ assertEquals(List.of(), selector.subSelectors());
+
+ selector = selectors.get(1);
+ assertEquals(List.of(new ApiIdentifier(null, "year")), selector.path());
+ assertEquals(List.of(), selector.subSelectors());
+ }
+
+ // https://datatracker.ietf.org/doc/html/rfc8040#section-4.8.3:
+ // "/" is used in a path to retrieve a child node of a node. For
+ // example, to retrieve only the "label" of an album, use
+ // "fields=admin/label".
+ @Test
+ public void testAdminLabel() throws ParseException {
+ final var selectors = assertValidFields("admin/label");
+ assertEquals(1, selectors.size());
+
+ final var selector = selectors.get(0);
+ assertEquals(List.of(new ApiIdentifier(null, "admin"), new ApiIdentifier(null, "label")), selector.path());
+ assertEquals(List.of(), selector.subSelectors());
+ }
+
+ // https://datatracker.ietf.org/doc/html/rfc8040#section-4.8.3:
+ // For example, assume that the target resource is the "album" list. To
+ // retrieve only the "label" and "catalogue-number" of the "admin"
+ // container within an album, use
+ // "fields=admin(label;catalogue-number)".
+ @Test
+ public void testAdminLabelCatalogueNumber() throws ParseException {
+ final var selectors = assertValidFields("admin(label;catalogue-number)");
+ assertEquals(1, selectors.size());
+
+ final var selector = selectors.get(0);
+ assertEquals(List.of(new ApiIdentifier(null, "admin")), selector.path());
+
+ final var subSelectors = selector.subSelectors();
+ assertEquals(2, subSelectors.size());
+
+ var subSelector = subSelectors.get(0);
+ assertEquals(List.of(new ApiIdentifier(null, "label")), subSelector.path());
+ assertEquals(List.of(), subSelector.subSelectors());
+
+
+ subSelector = subSelectors.get(1);
+ assertEquals(List.of(new ApiIdentifier(null, "catalogue-number")), subSelector.path());
+ assertEquals(List.of(), subSelector.subSelectors());
+ }
+
+ // https://datatracker.ietf.org/doc/html/rfc8040#appendix-B.3.3:
+ // In this example, the client is retrieving the datastore resource in
+ // JSON format, but retrieving only the "modules-state/module" list, and
+ // only the "name" and "revision" nodes from each list entry. Note that
+ // the top node returned by the server matches the target resource node
+ // (which is "data" in this example). The "module-set-id" leaf is not
+ // returned because it is not selected in the fields expression.
+ //
+ // GET /restconf/data?fields=ietf-yang-library:modules-state/\
+ // module(name;revision) HTTP/1.1
+ @Test
+ public void testModulesModuleNameRevision() {
+ final var selectors = assertValidFields("ietf-yang-library:modules-state/module(name;revision)");
+ assertEquals(1, selectors.size());
+
+ final var selector = selectors.get(0);
+ assertEquals(
+ List.of(new ApiIdentifier("ietf-yang-library", "modules-state"), new ApiIdentifier(null, "module")),
+ selector.path());
+
+ final var subSelectors = selector.subSelectors();
+ assertEquals(2, subSelectors.size());
+
+ var subSelector = subSelectors.get(0);
+ assertEquals(List.of(new ApiIdentifier(null, "name")), subSelector.path());
+ assertEquals(List.of(), subSelector.subSelectors());
+
+ subSelector = subSelectors.get(1);
+ assertEquals(List.of(new ApiIdentifier(null, "revision")), subSelector.path());
+ assertEquals(List.of(), subSelector.subSelectors());
+ }
+
+ @Test
+ public void testModulesSimple() {
+ final var selectors = assertValidFields("ietf-yang-library:modules-state");
+ assertEquals(1, selectors.size());
+
+ final var selector = selectors.get(0);
+ assertEquals(List.of(new ApiIdentifier("ietf-yang-library", "modules-state")), selector.path());
+ assertEquals(List.of(), selector.subSelectors());
+ }
+
+ @Test
+ public void testUnqualifiedSubQualified() {
+ final var selectors = assertValidFields("a(b:c)");
+ assertEquals(1, selectors.size());
+
+ final var selector = selectors.get(0);
+ assertEquals(List.of(new ApiIdentifier(null, "a")), selector.path());
+
+ final var subSelectors = selector.subSelectors();
+ assertEquals(1, subSelectors.size());
+
+ final var subSelector = subSelectors.get(0);
+ assertEquals(List.of(new ApiIdentifier("b", "c")), subSelector.path());
+ assertEquals(List.of(), subSelector.subSelectors());
+ }
+
+ @Test
+ public void testQualifiedSubUnqualified() {
+ final var selectors = assertValidFields("a:b(c)");
+ assertEquals(1, selectors.size());
+
+ final var selector = selectors.get(0);
+ assertEquals(List.of(new ApiIdentifier("a", "b")), selector.path());
+
+ final var subSelectors = selector.subSelectors();
+ assertEquals(1, subSelectors.size());
+
+ final var subSelector = subSelectors.get(0);
+ assertEquals(List.of(new ApiIdentifier(null, "c")), subSelector.path());
+ assertEquals(List.of(), subSelector.subSelectors());
+ }
+
+ @Test
+ public void testDeepNesting() {
+ final var selectors = assertValidFields("a(b(c(d)));e(f(g(h)));i(j(k(l)))");
+ assertEquals(3, selectors.size());
+ }
+
+ @Test
+ public void testInvalidIdentifier() {
+ assertInvalidFields(".", "Expecting [a-ZA-Z_], not '.'", 0);
+ assertInvalidFields("a+", "Expecting [a-zA-Z_.-/(:;], not '+'", 1);
+ assertInvalidFields("a:.", "Expecting [a-ZA-Z_], not '.'", 2);
+ assertInvalidFields("a:b+", "Expecting [a-zA-Z_.-/(:;], not '+'", 3);
+ assertInvalidFields("a;)", "Expecting [a-ZA-Z_], not ')'", 2);
+ }
+
+ @Test
+ public void testUnexpectedEnds() {
+ assertInvalidFields("a;", "Unexpected end of input", 2);
+ assertInvalidFields("a(", "Unexpected end of input", 2);
+ assertInvalidFields("a(a", "Unexpected end of input", 3);
+ }
+
+ @Test
+ public void testUnexpectedRightParent() {
+ assertInvalidFields("a)", "Expecting ';', not ')'", 1);
+ }
+
+ private static void assertInvalidFields(final String str, final String message, final int errorOffset) {
+ final var ex = assertThrows(ParseException.class, () -> FieldsParameter.parse(str));
+ assertEquals(message, ex.getMessage());
+ assertEquals(errorOffset, ex.getErrorOffset());
+ }
+
+ private static List<NodeSelector> assertValidFields(final String str) {
+ try {
+ return FieldsParameter.parse(str).nodeSelectors();
+ } catch (ParseException e) {
+ throw new AssertionError(e);
+ }
+ }
+}