api: fromjson/tojson enum flag support

Represent enum flags as JSON arrays (as these can have multiple values).
Add unit tests.

Type: improvement
Change-Id: I680c5b6f76ef6f05f360e2f3b9c4cbb927e15d7d
Signed-off-by: Ole Troan <ot@cisco.com>
diff --git a/src/tools/vppapigen/vppapigen_c.py b/src/tools/vppapigen/vppapigen_c.py
index 44f86be..66e0c2f 100644
--- a/src/tools/vppapigen/vppapigen_c.py
+++ b/src/tools/vppapigen/vppapigen_c.py
@@ -187,7 +187,21 @@
         write('}\n')
 
     _dispatch['Enum'] = print_enum
-    _dispatch['EnumFlag'] = print_enum
+
+    def print_enum_flag(self, o):
+        '''Create cJSON object (string) for VPP API enum'''
+        write = self.stream.write
+        write('static inline cJSON *vl_api_{name}_t_tojson '
+              '(vl_api_{name}_t a) {{\n'.format(name=o.name))
+        write('    cJSON *array = cJSON_CreateArray();\n')
+
+        for b in o.block:
+            write('    if (a & {})\n'.format(b[0]))
+            write('       cJSON_AddItemToArray(array, cJSON_CreateString("{}"));\n'.format(b[0]))
+        write('    return array;\n')
+        write('}\n')
+
+    _dispatch['EnumFlag'] = print_enum_flag
 
     def print_typedef(self, o):
         '''Create cJSON (dictionary) object from VPP API typedef'''
@@ -456,7 +470,27 @@
         write('}\n')
 
     _dispatch['Enum'] = print_enum
-    _dispatch['EnumFlag'] = print_enum
+
+    def print_enum_flag(self, o):
+        '''Convert to JSON enum(string) to VPP API enum (int)'''
+        write = self.stream.write
+        write('static inline void *vl_api_{n}_t_fromjson '
+              '(void *mp, int *len, cJSON *o, vl_api_{n}_t *a) {{\n'
+              .format(n=o.name))
+        write('   int i;\n')
+        write('   *a = 0;\n')
+        write('   for (i = 0; i < cJSON_GetArraySize(o); i++) {\n')
+        write('       cJSON *e = cJSON_GetArrayItem(o, i);\n')
+        write('       char *p = cJSON_GetStringValue(e);\n')
+        write('       if (!p) return 0;\n')
+        for b in o.block:
+            write('       if (strcmp(p, "{}") == 0) *a |= {};\n'
+                  .format(b[0], b[1]))
+        write('    }\n')
+        write('   return mp;\n')
+        write('}\n')
+
+    _dispatch['EnumFlag'] = print_enum_flag
 
     def print_typedef(self, o):
         '''Convert from JSON object to VPP API binary representation'''
diff --git a/src/vat2/CMakeLists.txt b/src/vat2/CMakeLists.txt
index 690267c..73538b4 100644
--- a/src/vat2/CMakeLists.txt
+++ b/src/vat2/CMakeLists.txt
@@ -31,6 +31,29 @@
   rt m dl crypto
 )
 
+#
+# Unit test code. Call generator directly to avoid it being installed
+#set_source_files_properties(vat2_test.c PROPERTIES
+#  COMPILE_FLAGS " -fsanitize=address"
+#)
+
+vpp_generate_api_c_header (test/vat2_test.api)
+add_vpp_executable(test_vat2 ENABLE_EXPORTS NO_INSTALL
+  SOURCES
+  test/vat2_test.c
+  jsonconvert.c
+
+  DEPENDS api_headers
+
+  LINK_LIBRARIES
+  vlibmemoryclient
+  svm
+  vppinfra
+  vppapiclient
+  Threads::Threads
+  rt m dl crypto
+)
+#target_link_options(test_vat2 PUBLIC "LINKER:-fsanitize=address")
 ##############################################################################
 # vat2 headers
 ##############################################################################
diff --git a/src/vat2/jsonconvert.c b/src/vat2/jsonconvert.c
index 3aeaeed..ec06628 100644
--- a/src/vat2/jsonconvert.c
+++ b/src/vat2/jsonconvert.c
@@ -314,12 +314,12 @@
   char *p = cJSON_GetStringValue(o);
   if (!p) return 0;
   unformat_init_string (&input, p, strlen(p));
-  if (a->af == ADDRESS_IP4)
-    unformat(&input, "%U", unformat_ip4_address, &a->un.ip4);
-  else if (a->af == ADDRESS_IP6)
-    unformat(&input, "%U", unformat_ip6_address, &a->un.ip6);
+  if (unformat (&input, "%U", unformat_ip4_address, &a->un.ip4))
+    a->af = ADDRESS_IP4;
+  else if (unformat (&input, "%U", unformat_ip6_address, &a->un.ip6))
+    a->af = ADDRESS_IP6;
   else
-    return 0;
+    return (0);
   return mp;
 }
 
@@ -328,14 +328,17 @@
   unformat_input_t input;
 
   char *p = cJSON_GetStringValue(o);
+
   if (!p) return 0;
   unformat_init_string (&input, p, strlen(p));
-  if (a->address.af == ADDRESS_IP4)
-    unformat(&input, "%U/%d", unformat_ip4_address, &a->address.un.ip4, &a->len);
-  else if (a->address.af == ADDRESS_IP6)
-    unformat(&input, "%U/%d", unformat_ip6_address, &a->address.un.ip6, &a->len);
+  int plen;
+  if (unformat (&input, "%U/%d", unformat_ip4_address, &a->address.un.ip4, &plen))
+    a->address.af = ADDRESS_IP4;
+  else if (unformat (&input, "%U/%d", unformat_ip6_address, &a->address.un.ip6, &plen))
+    a->address.af = ADDRESS_IP6;
   else
-    return 0;
+    return (0);
+  a->len = plen;
   return mp;
 }
 
diff --git a/src/vat2/test/vat2_test.api b/src/vat2/test/vat2_test.api
new file mode 100644
index 0000000..6a2c94d
--- /dev/null
+++ b/src/vat2/test/vat2_test.api
@@ -0,0 +1,30 @@
+/*
+ * Copyright (c) 2020 Cisco and/or its affiliates.
+ * 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.
+ */
+
+import "vnet/ip/ip_types.api";
+
+autoreply define test_prefix {
+  vl_api_prefix_t pref;
+};
+
+enumflag test_enumflags {
+  RED = 0x1,
+  BLUE = 0x2,
+  GREEN = 0x4,
+};
+
+autoreply define test_enum {
+  vl_api_test_enumflags_t flags;
+};
diff --git a/src/vat2/test/vat2_test.c b/src/vat2/test/vat2_test.c
new file mode 100644
index 0000000..fe788f1
--- /dev/null
+++ b/src/vat2/test/vat2_test.c
@@ -0,0 +1,130 @@
+/*
+ * Copyright (c) 2020 Cisco and/or its affiliates.
+ * 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.
+ */
+
+#include <stdio.h>
+#include <assert.h>
+#include <vlibapi/api.h>
+#include "vat2/test/vat2_test.api_types.h"
+#include "vat2/test/vat2_test.api_tojson.h"
+#include "vat2/test/vat2_test.api_fromjson.h"
+
+typedef cJSON *(* tojson_fn_t)(void *);
+typedef void *(* fromjson_fn_t)(cJSON *o, int *len);
+
+static void
+test (tojson_fn_t tojson, fromjson_fn_t fromjson, cJSON *o, bool should_fail)
+{
+  // convert JSON object to API
+  int len = 0;
+  void *mp = (fromjson)(o, &len);
+  assert(mp);
+
+  // convert API to JSON
+  cJSON *o2 = (tojson)(mp);
+  assert(o2);
+
+  if (should_fail)
+    assert(!cJSON_Compare(o, o2, 1));
+  else
+    assert(cJSON_Compare(o, o2, 1));
+  char *s2 = cJSON_Print(o2);
+  assert(s2);
+
+  char *in = cJSON_Print(o);
+  printf("%s\n%s\n", in, s2);
+
+  free(in);
+  free(mp);
+  cJSON_Delete(o2);
+  free(s2);
+}
+
+struct msgs {
+  char *name;
+  tojson_fn_t tojson;
+  fromjson_fn_t fromjson;
+};
+struct tests {
+  char *s;
+  bool should_fail;
+};
+
+uword *function_by_name_tojson;
+uword *function_by_name_fromjson;
+static void
+register_functions(struct msgs msgs[], int n)
+{
+  int i;
+  function_by_name_tojson = hash_create_string (0, sizeof (uword));
+  function_by_name_fromjson = hash_create_string (0, sizeof (uword));
+  for (i = 0; i < n; i++) {
+    hash_set_mem(function_by_name_tojson, msgs[i].name, msgs[i].tojson);
+    hash_set_mem(function_by_name_fromjson, msgs[i].name, msgs[i].fromjson);
+  }
+}
+
+static void
+runtest (char *s, bool should_fail)
+{
+  cJSON *o = cJSON_Parse(s);
+  assert(o);
+  char *name = cJSON_GetStringValue(cJSON_GetObjectItem(o, "_msgname"));
+  assert(name);
+
+  uword *p = hash_get_mem(function_by_name_tojson, name);
+  assert(p);
+  tojson_fn_t tojson = (tojson_fn_t)p[0];
+
+  p = hash_get_mem(function_by_name_fromjson, name);
+  assert(p);
+  fromjson_fn_t fromjson = (fromjson_fn_t)p[0];
+
+  test(tojson, fromjson, o, should_fail);
+  cJSON_Delete(o);
+}
+
+struct msgs msgs[] = {
+{
+  .name = "test_prefix",
+  .tojson = (tojson_fn_t)vl_api_test_prefix_t_tojson,
+  .fromjson = (fromjson_fn_t)vl_api_test_prefix_t_fromjson,
+},
+{
+  .name = "test_enum",
+  .tojson = (tojson_fn_t)vl_api_test_enum_t_tojson,
+  .fromjson = (fromjson_fn_t)vl_api_test_enum_t_fromjson,
+},
+};
+
+struct tests tests[] = {
+  {.s = "{\"_msgname\": \"test_prefix\", \"pref\": \"2001:db8::/64\"}"},
+  {.s = "{\"_msgname\": \"test_prefix\", \"pref\": \"192.168.10.0/24\"}"},
+  {.s = "{\"_msgname\": \"test_enum\", \"flags\": [\"RED\", \"BLUE\"]}"},
+  {.s = "{\"_msgname\": \"test_enum\", \"flags\": [\"BLACK\", \"BLUE\"]}",
+   .should_fail = 1},
+};
+
+int main (int argc, char **argv)
+{
+  clib_mem_init (0, 64 << 20);
+  int n = sizeof(msgs)/sizeof(msgs[0]);
+  register_functions(msgs, n);
+
+  int i;
+  n = sizeof(tests)/sizeof(tests[0]);
+  for (i = 0; i < n; i++) {
+    runtest(tests[i].s, tests[i].should_fail);
+  }
+}