From 37d994f3b54cd27386aea34a1ad8d6c33cf5ed49 Mon Sep 17 00:00:00 2001 From: Dmitry Grinberg Date: Mon, 2 Mar 2026 19:06:48 +0200 Subject: [PATCH] restconf: Add simple "fields" query parameter support (RFC 8040 Sec 4.8.3) Implement top-level module filtering for GET /restconf/data via the "fields" query parameter as specified in RFC 8040 Section 4.8.3. This simple implementation supports selecting root-level data nodes by module name and node name at the datastore root, separated by semicolons. For example: GET /restconf/data?fields=ietf-interfaces:interfaces%3Bietf-system:system The filter is applied as a post-processing step on the XML tree returned by the backend, removing children that do not match any entry in the fields list. It only takes effect when the request targets the data root (i.e. no api-path is given). Changes: - apps/restconf/restconf_methods_get.c: Add restconf_fields_filter() helper and wire up "fields" query parameter parsing in api_data_get2() - apps/backend/backend_state.c: Advertise urn:ietf:params:restconf:capability:fields:1.0 capability - test/test_restconf_jukebox.sh: Implement RFC 8040 B.3.3 "fields" parameter tests (was previously commented out) - test/test_yang_with_defaults.sh: Update capability assertions to include the new fields:1.0 capability --- apps/backend/backend_state.c | 1 + apps/restconf/restconf_methods_get.c | 84 ++++++++++++++++++++++++++++ test/test_restconf_jukebox.sh | 24 ++++++-- test/test_yang_with_defaults.sh | 4 +- 4 files changed, 107 insertions(+), 6 deletions(-) diff --git a/apps/backend/backend_state.c b/apps/backend/backend_state.c index 34e494037..0ca210d4d 100644 --- a/apps/backend/backend_state.c +++ b/apps/backend/backend_state.c @@ -105,6 +105,7 @@ restconf_client_get_capabilities(clixon_handle h, cprintf(cb, ""); cprintf(cb, "urn:ietf:params:restconf:capability:defaults:1.0?basic-mode=explicit"); cprintf(cb, "urn:ietf:params:restconf:capability:depth:1.0"); + cprintf(cb, "urn:ietf:params:restconf:capability:fields:1.0"); cprintf(cb, "urn:ietf:params:restconf:capability:with-defaults:1.0"); cprintf(cb, ""); if (clixon_xml_parse_string(cbuf_get(cb), YB_PARENT, NULL, &xrstate, NULL) < 0) diff --git a/apps/restconf/restconf_methods_get.c b/apps/restconf/restconf_methods_get.c index 70c5e965e..4b5ad43dd 100644 --- a/apps/restconf/restconf_methods_get.c +++ b/apps/restconf/restconf_methods_get.c @@ -66,6 +66,79 @@ /* Forward */ static int api_data_pagination(clixon_handle h, void *req, char *api_path, int pi, cvec *qvec, int pretty, restconf_media media_out); +/*! Filter top-level data nodes based on "fields" query parameter (RFC 8040 Sec 4.8.3) + * + * Simple implementation: only top-level module:node or module:* selection at data root. + * Removes children of xdata that do not match any entry in the semicolon-separated + * fields list. + * @param[in] xdata XML data root whose children are top-level data nodes + * @param[in] fields_str Value of the "fields" query parameter, e.g. "mod1:node1;mod2:node2" + * @retval 0 OK + * @retval -1 Error + * @see RFC 8040 Sec 4.8.3 + */ +static int +restconf_fields_filter(cxobj *xdata, + char *fields_str) +{ + int retval = -1; + char *str = NULL; + char *s; + char *saveptr; + char *colon; + cxobj *x; + yang_stmt *ynode; + yang_stmt *ymod; + char *modname; + char *nodename; + int i; + int found; + + for (i = xml_child_nr(xdata)-1; i >= 0; i--){ + x = xml_child_i(xdata, i); + if (xml_type(x) != CX_ELMNT) + continue; + ynode = xml_spec(x); + ymod = (ynode != NULL) ? ys_module(ynode) : NULL; + modname = (ymod != NULL) ? yang_argument_get(ymod) : NULL; + nodename = xml_name(x); + found = 0; + if ((str = strdup(fields_str)) == NULL){ + clixon_err(OE_UNIX, errno, "strdup"); + goto done; + } + s = strtok_r(str, ";", &saveptr); + while (s != NULL){ + colon = strchr(s, ':'); + if (colon != NULL){ + *colon = '\0'; + if (modname != NULL && + strcmp(s, modname) == 0 && + strcmp(colon+1, nodename) == 0) + found = 1; + *colon = ':'; + } + else{ + if (modname != NULL && strcmp(s, modname) == 0) + found = 1; + } + if (found) + break; + s = strtok_r(NULL, ";", &saveptr); + } + free(str); + str = NULL; + if (!found) + if (xml_purge(x) < 0) + goto done; + } + retval = 0; + done: + if (str) + free(str); + return retval; +} + /*! Generic GET (both HEAD and GET) * * According to restconf @@ -125,6 +198,7 @@ api_data_get2(clixon_handle h, cxobj *xtop = NULL; yang_stmt *y = NULL; char *defaults = NULL; + char *fields_str = NULL; cvec *nscd = NULL; int ret; @@ -207,6 +281,11 @@ api_data_get2(clixon_handle h, clixon_debug(CLIXON_DBG_RESTCONF, "with_defaults=%s", attr); defaults = attr; } + /* Check for fields attribute: simple root-level module:node filter, RFC 8040 Sec 4.8.3 */ + if ((attr = cvec_find_str(qvec, "fields")) != NULL){ + clixon_debug(CLIXON_DBG_RESTCONF, "fields=%s", attr); + fields_str = attr; + } clixon_debug(CLIXON_DBG_RESTCONF, "path:%s", xpath); if ((ret = clicon_rpc_get(h, xpath, nsc, content, depth, defaults, &xret)) < 0){ @@ -235,6 +314,11 @@ api_data_get2(clixon_handle h, goto done; } if (xpath==NULL || strcmp(xpath,"/")==0){ /* Special case: data root */ + /* Apply fields filter if requested */ + if (fields_str != NULL){ + if (restconf_fields_filter(xret, fields_str) < 0) + goto done; + } switch (media_out){ case YANG_DATA_XML: if (clixon_xml2cbuf(cbx, xret, 0, pretty, NULL, -1, 0) < 0) /* Dont print top object? */ diff --git a/test/test_restconf_jukebox.sh b/test/test_restconf_jukebox.sh index 3e19d0ee9..9f564ebba 100755 --- a/test/test_restconf_jukebox.sh +++ b/test/test_restconf_jukebox.sh @@ -189,13 +189,30 @@ expectpart "$(curl $CURLOPTS -X GET -H 'Accept: application/yang-data+json' $RCP # Maybe this is not correct w [null,null]but I have no good examples new 'B.3.2. "depth" Parameter depth=3' -expectpart "$(curl $CURLOPTS -X GET -H 'Accept: application/yang-data+json' $RCPROTO://localhost/restconf/data/example-jukebox:jukebox?depth=3)" 0 "HTTP/$HVER 200" '{"example-jukebox:jukebox":{"artist":\[null,null\]}}} - ' +expectpart "$(curl $CURLOPTS -X GET -H 'Accept: application/yang-data+json' $RCPROTO://localhost/restconf/data/example-jukebox:jukebox?depth=3)" 0 "HTTP/$HVER 200" '{"example-jukebox:jukebox":{"artist":\[null,null\]}}} + +' new "restconf DELETE whole datastore" expectpart "$(curl $CURLOPTS -X DELETE $RCPROTO://localhost/restconf/data)" 0 "HTTP/$HVER 204" -#new 'B.3.3. "fields" Parameter' +new 'B.3.3. "fields" Parameter: preamble (create system and jukebox data)' +expectpart "$(curl $CURLOPTS -X PATCH -H 'Content-Type: application/yang-data+xml' $RCPROTO://localhost/restconf/data -d 'trueFields Artist')" 0 "HTTP/$HVER 204" + +new 'B.3.3. "fields" Parameter: system only' +expectpart "$(curl $CURLOPTS -X GET -H 'Accept: application/yang-data+json' $RCPROTO://localhost/restconf/data?fields=example-system:system)" 0 "HTTP/$HVER 200" \ + '"example-system:system"' --not-- '"example-jukebox:jukebox"' + +new 'B.3.3. "fields" Parameter: jukebox only' +expectpart "$(curl $CURLOPTS -X GET -H 'Accept: application/yang-data+json' $RCPROTO://localhost/restconf/data?fields=example-jukebox:jukebox)" 0 "HTTP/$HVER 200" \ + '"example-jukebox:jukebox"' --not-- '"example-system:system"' + +new 'B.3.3. "fields" Parameter: two modules' +expectpart "$(curl $CURLOPTS -X GET -H 'Accept: application/yang-data+json' "$RCPROTO://localhost/restconf/data?fields=example-system:system%3Bexample-jukebox:jukebox")" 0 "HTTP/$HVER 200" \ + '"example-system:system"' '"example-jukebox:jukebox"' + +new "restconf DELETE whole datastore (after fields test)" +expectpart "$(curl $CURLOPTS -X DELETE $RCPROTO://localhost/restconf/data)" 0 "HTTP/$HVER 204" new 'B.3.4. "insert" Parameter' JSON="{\"example-jukebox:song\":[{\"index\":1,\"id\":\"/example-jukebox:jukebox/library/artist[name='Foo Fighters']/album[name='Wasting Light']/song[name='Rope']\"}]}" @@ -251,7 +268,6 @@ new 'B.3.5. "insert/point" leaf-list check order (2,4,3,1)' expectpart "$(curl $CURLOPTS -X GET $RCPROTO://localhost/restconf/data/example-jukebox:extra -H 'Accept: application/yang-data+xml')" 0 "HTTP/$HVER 200" '2431' #new "B.2.2. Detect Datastore Resource Entity-Tag Change" # XXX done except entity-changed -#new 'B.3.3. "fields" Parameter' #new 'B.3.6. "filter" Parameter' #new 'B.3.7. "start-time" Parameter' #new 'B.3.8. "stop-time" Parameter' diff --git a/test/test_yang_with_defaults.sh b/test/test_yang_with_defaults.sh index e8ce796ea..eebeed60b 100755 --- a/test/test_yang_with_defaults.sh +++ b/test/test_yang_with_defaults.sh @@ -509,14 +509,14 @@ expectpart "$(curl $CURLOPTS -X GET -H 'Accept: application/yang-data+json' $RCP 0 \ "HTTP/$HVER 200" "Content-Type: application/yang-data+json" \ 'Cache-Control: no-cache' \ -'{"ietf-restconf-monitoring:capabilities":{"capability":\["urn:ietf:params:restconf:capability:defaults:1.0?basic-mode=explicit","urn:ietf:params:restconf:capability:depth:1.0","urn:ietf:params:restconf:capability:with-defaults:1.0"\]}}' +'{"ietf-restconf-monitoring:capabilities":{"capability":\["urn:ietf:params:restconf:capability:defaults:1.0?basic-mode=explicit","urn:ietf:params:restconf:capability:depth:1.0","urn:ietf:params:restconf:capability:fields:1.0","urn:ietf:params:restconf:capability:with-defaults:1.0"\]}}' new "rfc8040 B.1.3. Retrieve the Server Capability Information xml" expectpart "$(curl $CURLOPTS -X GET -H 'Accept: application/yang-data+xml' $RCPROTO://localhost/restconf/data/ietf-restconf-monitoring:restconf-state/capabilities)" \ 0 \ "HTTP/$HVER 200" "Content-Type: application/yang-data+xml" \ 'Cache-Control: no-cache' \ -'urn:ietf:params:restconf:capability:defaults:1.0?basic-mode=expliciturn:ietf:params:restconf:capability:depth:1.0urn:ietf:params:restconf:capability:with-defaults:1.0' +'urn:ietf:params:restconf:capability:defaults:1.0?basic-mode=expliciturn:ietf:params:restconf:capability:depth:1.0urn:ietf:params:restconf:capability:fields:1.0urn:ietf:params:restconf:capability:with-defaults:1.0' new "rfc8040 B.3.9. RESTCONF with-defaults parameter = report-all json" expectpart "$(curl $CURLOPTS -X GET -H 'Accept: application/yang-data+json' $RCPROTO://localhost/restconf/data/example:interfaces/interface=eth1?with-defaults=report-all)" \