]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
table: Improve mangling of JSON field names
authorAdrian Vovk <adrianvovk@gmail.com>
Wed, 3 Jul 2024 21:57:42 +0000 (17:57 -0400)
committerAdrian Vovk <adrianvovk@gmail.com>
Wed, 17 Jul 2024 18:15:43 +0000 (14:15 -0400)
First, when displaying JSON we convert dashes into underscores. We want
to avoid using dashes in JSON field names in new code, because some
JSON parsers don't support dashes very well.

Second, we make the first character of every word lower-case. This
better matches our JSON field name style, and makes the automatic
JSON name mangling a lot more useful for vertical tables, where fields
are given a display name. For example, "Foo Bar" would be converted into
"foo_bar" instead of "Foo_Bar", which much better matches our style.
We don't make the whole string lowercase to support cases like:
"fooBar" should stay as "fooBar".

Some situations don't behave quite perfectly, such as "Foo BarBaz" gets
converted into "foo_barBaz", or all-caps headings get mangled
incorrectly. In these situations, the JSON field should be overridden
manually. In most cases, or at least more cases than before, this
heuristic does good enough.

src/shared/format-table.c
src/shared/format-table.h
src/test/test-format-table.c

index e538d9641470f0e181c334cf64931b5c891bd60b..ee641d1144429801ee5081a37dc65b6a15f5afdb 100644 (file)
@@ -2895,18 +2895,39 @@ static int table_data_to_json(TableData *d, sd_json_variant **ret) {
         }
 }
 
-static char* string_to_json_field_name(const char *f) {
+char* table_mangle_to_json_field_name(const char *str) {
         /* Tries to make a string more suitable as JSON field name. There are no strict rules defined what a
-         * field name can be hence this is a bit vague and black magic. Right now we only convert spaces to
-         * underscores and leave everything as is. */
+         * field name can be hence this is a bit vague and black magic. Here's what we do:
+         *  - Convert spaces to underscores
+         *  - Convert dashes to underscores (some JSON parsers don't like dealing with dashes)
+         *  - Convert most other symbols to underscores (for similar reasons)
+         *  - Make the first letter of each word lowercase (unless it looks like the whole word is uppercase)
+         */
 
-        char *c = strdup(f);
+        bool new_word = true;
+        char *c;
+
+        assert(str);
+
+        c = strdup(str);
         if (!c)
                 return NULL;
 
-        for (char *x = c; *x; x++)
-                if (isspace(*x))
+        for (char *x = c; *x; x++) {
+                if (!strchr(ALPHANUMERICAL, *x)) {
                         *x = '_';
+                        new_word = true;
+                        continue;
+                }
+
+                if (new_word) {
+                        if (ascii_tolower(*(x + 1)) == *(x + 1)) /* Heuristic: if next char is upper-case
+                                                                  * then we assume the whole word is all-caps
+                                                                  * and avoid lowercasing it. */
+                                *x = ascii_tolower(*x);
+                        new_word = false;
+                }
+        }
 
         return c;
 }
@@ -2927,7 +2948,7 @@ static int table_make_json_field_name(Table *t, TableData *d, char **ret) {
                         return -ENOMEM;
         }
 
-        mangled = string_to_json_field_name(n);
+        mangled = table_mangle_to_json_field_name(n);
         if (!mangled)
                 return -ENOMEM;
 
index 15c0547b944f0f814a708d68f89b6adb5ccefeec..bb2eb707599e6b0d1f0aa39c4e4cc59121b645e0 100644 (file)
@@ -166,6 +166,7 @@ int table_print_json(Table *t, FILE *f, sd_json_format_flags_t json_flags);
 
 int table_print_with_pager(Table *t, sd_json_format_flags_t json_format_flags, PagerFlags pager_flags, bool show_header);
 
+char* table_mangle_to_json_field_name(const char *str);
 int table_set_json_field_name(Table *t, size_t idx, const char *name);
 
 #define table_log_add_error(r) \
index fa34c29b02c817747825d13e969bc3005e9df593..41621dbb5b36621ac181e8a865514dea3364e333 100644 (file)
@@ -404,6 +404,38 @@ TEST(json) {
         assert_se(sd_json_variant_equal(v, w));
 }
 
+TEST(json_mangling) {
+        static const struct {
+                const char *arg;
+                const char *exp;
+        } cases[] = {
+                /* Not Mangled */
+                { "foo", "foo" },
+                { "foo_bar", "foo_bar" },
+                { "fooBar", "fooBar" },
+                { "fooBar123", "fooBar123" },
+                { "foo_bar123", "foo_bar123" },
+                { ALPHANUMERICAL, ALPHANUMERICAL },
+                { "_123", "_123" },
+
+                /* Mangled */
+                { "Foo Bar", "foo_bar" },
+                { "Foo-Bar", "foo_bar" },
+                { "Foo@Bar", "foo_bar" },
+                { "Foo (Bar)", "foo__bar_"},
+                { "MixedCase ALLCAPS", "mixedCase_ALLCAPS" },
+                { "_X", "_x" },
+                { "_Foo", "_foo" },
+        };
+
+        FOREACH_ELEMENT(i, cases) {
+                _cleanup_free_ char *ret = NULL;
+                assert_se(ret = table_mangle_to_json_field_name(i->arg));
+                printf("\"%s\" -> \"%s\"\n", i->arg, ret);
+                assert_se(streq(ret, i->exp));
+        }
+}
+
 TEST(table) {
         _cleanup_(table_unrefp) Table *t = NULL;
         _cleanup_free_ char *formatted = NULL;