diff --git a/general/g.mapsets/Makefile b/general/g.mapsets/Makefile index 016bbd6cc05..6672dda2804 100644 --- a/general/g.mapsets/Makefile +++ b/general/g.mapsets/Makefile @@ -3,7 +3,7 @@ MODULE_TOPDIR = ../.. PGM = g.mapsets -LIBES = $(GISLIB) +LIBES = $(PARSONLIB) $(GISLIB) DEPENDENCIES = $(GISDEP) include $(MODULE_TOPDIR)/include/Make/Module.make diff --git a/general/g.mapsets/g.mapsets.html b/general/g.mapsets/g.mapsets.html index d1e10adbe2d..cd04a944dae 100644 --- a/general/g.mapsets/g.mapsets.html +++ b/general/g.mapsets/g.mapsets.html @@ -118,6 +118,22 @@

Print available mapsets

PERMANENT user1 user2 +Mapsets can be also printed out as json by setting the format option to "json" (format="json"). + +
+
+    g.mapsets format="json" -l
+
+    {
+      "mapsets": [
+        "PERMANENT",
+        "user1",
+        "user2"
+      ]
+    }
+  
+
+

Add new mapset

Add mapset 'user2' to the current mapset search path diff --git a/general/g.mapsets/list.c b/general/g.mapsets/list.c index f8dfabbdc5e..0bea28ddda8 100644 --- a/general/g.mapsets/list.c +++ b/general/g.mapsets/list.c @@ -3,27 +3,51 @@ #include #include #include "local_proto.h" +#include + +// Function to initialize a JSON object with a mapsets array +static JSON_Object *initialize_json_object() +{ + JSON_Value *root_value = json_value_init_object(); + if (!root_value) { + G_fatal_error(_("Failed to initialize JSON object. Out of memory?")); + } + + JSON_Object *root_object = json_value_get_object(root_value); + json_object_set_value(root_object, "mapsets", json_value_init_array()); + + JSON_Array *mapsets = json_object_get_array(root_object, "mapsets"); + if (!mapsets) { + json_value_free(root_value); + G_fatal_error(_("Failed to initialize mapsets array. Out of memory?")); + } + + return root_object; +} + +// Function to serialize and print JSON object +static void serialize_and_print_json_object(JSON_Value *root_value) +{ + char *serialized_string = json_serialize_to_string_pretty(root_value); + if (!serialized_string) { + json_value_free(root_value); + G_fatal_error(_("Failed to serialize JSON to pretty format.")); + } + + fprintf(stdout, "%s\n", serialized_string); + json_free_serialized_string(serialized_string); + json_value_free(root_value); +} void list_available_mapsets(const char **mapset_name, int nmapsets, const char *fs) { - int n; - G_message(_("Available mapsets:")); - for (n = 0; n < nmapsets; n++) { + for (int n = 0; n < nmapsets; n++) { fprintf(stdout, "%s", mapset_name[n]); if (n < nmapsets - 1) { - if (strcmp(fs, "newline") == 0) - fprintf(stdout, "\n"); - else if (strcmp(fs, "space") == 0) - fprintf(stdout, " "); - else if (strcmp(fs, "comma") == 0) - fprintf(stdout, ","); - else if (strcmp(fs, "tab") == 0) - fprintf(stdout, "\t"); - else - fprintf(stdout, "%s", fs); + fprintf(stdout, "%s", fs); } } fprintf(stdout, "\n"); @@ -31,25 +55,45 @@ void list_available_mapsets(const char **mapset_name, int nmapsets, void list_accessible_mapsets(const char *fs) { - int n; const char *name; G_message(_("Accessible mapsets:")); - for (n = 0; (name = G_get_mapset_name(n)); n++) { + + for (int n = 0; (name = G_get_mapset_name(n)); n++) { /* match each mapset to its numeric equivalent */ fprintf(stdout, "%s", name); if (G_get_mapset_name(n + 1)) { - if (strcmp(fs, "newline") == 0) - fprintf(stdout, "\n"); - else if (strcmp(fs, "space") == 0) - fprintf(stdout, " "); - else if (strcmp(fs, "comma") == 0) - fprintf(stdout, ","); - else if (strcmp(fs, "tab") == 0) - fprintf(stdout, "\t"); - else - fprintf(stdout, "%s", fs); + fprintf(stdout, "%s", fs); } } fprintf(stdout, "\n"); } + +// Lists all accessible mapsets in JSON format +void list_accessible_mapsets_json() +{ + const char *name; + JSON_Object *root_object = initialize_json_object(); + JSON_Array *mapsets = json_object_get_array(root_object, "mapsets"); + + for (int n = 0; (name = G_get_mapset_name(n)); n++) { + json_array_append_string(mapsets, name); + } + + serialize_and_print_json_object( + json_object_get_wrapping_value(root_object)); +} + +// Lists available mapsets from a provided array in JSON format +void list_avaliable_mapsets_json(const char **mapset_names, int nmapsets) +{ + JSON_Object *root_object = initialize_json_object(); + JSON_Array *mapsets = json_object_get_array(root_object, "mapsets"); + + for (int n = 0; n < nmapsets; n++) { + json_array_append_string(mapsets, mapset_names[n]); + } + + serialize_and_print_json_object( + json_object_get_wrapping_value(root_object)); +} diff --git a/general/g.mapsets/local_proto.h b/general/g.mapsets/local_proto.h index badbf2fabc2..5a050b0aedb 100644 --- a/general/g.mapsets/local_proto.h +++ b/general/g.mapsets/local_proto.h @@ -5,3 +5,5 @@ const char *substitute_mapset(const char *); /* list.c */ void list_available_mapsets(const char **, int, const char *); void list_accessible_mapsets(const char *); +void list_avaliable_mapsets_json(const char **, int); +void list_accessible_mapsets_json(); diff --git a/general/g.mapsets/main.c b/general/g.mapsets/main.c index 3d79f1f9432..55c8c81f646 100644 --- a/general/g.mapsets/main.c +++ b/general/g.mapsets/main.c @@ -9,9 +9,10 @@ * Markus Neteler , * Moritz Lennert , * Martin Landa , - * Huidae Cho + * Huidae Cho , + * Corey White * PURPOSE: set current mapset path - * COPYRIGHT: (C) 1994-2009, 2012 by the GRASS Development Team + * COPYRIGHT: (C) 1994-2009, 2012-2024 by the GRASS Development Team * * This program is free software under the GNU General * Public License (>=v2). Read the file COPYING that @@ -33,6 +34,28 @@ #define OP_ADD 2 #define OP_REM 3 +enum OutputFormat { PLAIN, JSON }; + +void fatal_error_option_value_excludes_flag(struct Option *option, + struct Flag *excluded, + const char *because) +{ + if (!excluded->answer) + return; + G_fatal_error(_("The flag -%c is not allowed with %s=%s. %s"), + excluded->key, option->key, option->answer, because); +} + +void fatal_error_option_value_excludes_option(struct Option *option, + struct Option *excluded, + const char *because) +{ + if (!excluded->answer) + return; + G_fatal_error(_("The option %s is not allowed with %s=%s. %s"), + excluded->key, option->key, option->answer, because); +} + static void append_mapset(char **, const char *); int main(int argc, char *argv[]) @@ -45,15 +68,15 @@ int main(int argc, char *argv[]) int no_tokens; FILE *fp; char path_buf[GPATH_MAX]; - char *path, *fs; + char *path, *fsep; int operation, nchoices; - + enum OutputFormat format; char **mapset_name; int nmapsets; struct GModule *module; struct _opt { - struct Option *mapset, *op, *fs; + struct Option *mapset, *op, *format, *fsep; struct Flag *print, *list, *dialog; } opt; @@ -82,10 +105,20 @@ int main(int argc, char *argv[]) opt.op->description = _("Operation to be performed"); opt.op->answer = "add"; - opt.fs = G_define_standard_option(G_OPT_F_SEP); - opt.fs->label = _("Field separator for printing (-l and -p flags)"); - opt.fs->answer = "space"; - opt.fs->guisection = _("Print"); + opt.format = G_define_option(); + opt.format->key = "format"; + opt.format->type = TYPE_STRING; + opt.format->required = YES; + opt.format->label = _("Output format for printing (-l and -p flags)"); + opt.format->options = "plain,json"; + opt.format->descriptions = "plain;Configurable plain text output;" + "json;JSON (JavaScript Object Notation);"; + opt.format->answer = "plain"; + opt.format->guisection = _("Print"); + + opt.fsep = G_define_standard_option(G_OPT_F_SEP); + opt.fsep->answer = NULL; + opt.fsep->guisection = _("Print"); opt.list = G_define_flag(); opt.list->key = 'l'; @@ -130,7 +163,27 @@ int main(int argc, char *argv[]) } } - fs = G_option_to_separator(opt.fs); + if (strcmp(opt.format->answer, "json") == 0) + format = JSON; + else + format = PLAIN; + if (format == JSON) { + fatal_error_option_value_excludes_option( + opt.format, opt.fsep, _("Separator is part of the format.")); + } + + /* the field separator */ + if (opt.fsep->answer) { + fsep = G_option_to_separator(opt.fsep); + } + else { + /* A different separator is needed to for each format and output. */ + if (format == PLAIN) { + fsep = G_store(" "); + } + else + fsep = NULL; /* Something like a separator is part of the format. */ + } /* list available mapsets */ if (opt.list->answer) { @@ -141,7 +194,13 @@ int main(int argc, char *argv[]) if (opt.mapset->answer) G_warning(_("Option <%s> ignored"), opt.mapset->key); mapset_name = get_available_mapsets(&nmapsets); - list_available_mapsets((const char **)mapset_name, nmapsets, fs); + if (format == JSON) { + list_avaliable_mapsets_json((const char **)mapset_name, nmapsets); + } + else { + list_available_mapsets((const char **)mapset_name, nmapsets, fsep); + } + exit(EXIT_SUCCESS); } @@ -150,7 +209,13 @@ int main(int argc, char *argv[]) G_warning(_("Flag -%c ignored"), opt.dialog->key); if (opt.mapset->answer) G_warning(_("Option <%s> ignored"), opt.mapset->key); - list_accessible_mapsets(fs); + if (format == JSON) { + list_accessible_mapsets_json(); + } + else { + list_accessible_mapsets(fsep); + } + exit(EXIT_SUCCESS); } diff --git a/general/g.mapsets/tests/conftest.py b/general/g.mapsets/tests/conftest.py new file mode 100644 index 00000000000..ca58228607c --- /dev/null +++ b/general/g.mapsets/tests/conftest.py @@ -0,0 +1,35 @@ +"""Fixtures for Jupyter tests + +Fixture for grass.jupyter.TimeSeries test + +Fixture for ReprojectionRenderer test with simple GRASS location, raster, vector. +""" + + +from types import SimpleNamespace + +import grass.script as gs +import pytest + +TEST_MAPSETS = ["PERMANENT", "test1", "test2", "test3"] +ACCESSIBLE_MAPSETS = ["test3", "PERMANENT"] + + +@pytest.fixture(scope="module") +def simple_dataset(tmp_path_factory): + """Start a session and create a test mapsets + Returns object with attributes about the dataset. + """ + tmp_path = tmp_path_factory.mktemp("simple_dataset") + location = "test" + gs.core._create_location_xy(tmp_path, location) # pylint: disable=protected-access + with gs.setup.init(tmp_path / location): + gs.run_command("g.proj", flags="c", epsg=26917) + gs.run_command("g.region", s=0, n=80, w=0, e=120, b=0, t=50, res=10, res3=10) + # Create Mock Mapsets + for mapset in TEST_MAPSETS: + gs.run_command("g.mapset", location=location, mapset=mapset, flags="c") + + yield SimpleNamespace( + mapsets=TEST_MAPSETS, accessible_mapsets=ACCESSIBLE_MAPSETS + ) diff --git a/general/g.mapsets/tests/g_mapsets_list_format_test.py b/general/g.mapsets/tests/g_mapsets_list_format_test.py new file mode 100644 index 00000000000..79555085bb0 --- /dev/null +++ b/general/g.mapsets/tests/g_mapsets_list_format_test.py @@ -0,0 +1,68 @@ +############################################################################ +# +# MODULE: Test of g.mapsets +# AUTHOR(S): Corey White +# PURPOSE: Test parsing and structure of CSV and JSON outputs +# COPYRIGHT: (C) 2022 by Corey White the GRASS Development Team +# +# This program is free software under the GNU General Public +# License (>=v2). Read the file COPYING that comes with GRASS +# for details. +# +############################################################################# + +"""Test parsing and structure of CSV and JSON outputs from g.mapsets""" + +import json +import pytest +import grass.script as gs +from grass.script import utils as gutils + +SEPARATORS = ["newline", "space", "comma", "tab", "pipe", ","] + + +def _check_parsed_list(mapsets, text, sep="|"): + """Asserts to run on for each separator""" + parsed_list = text.splitlines() if sep == "\n" else text.split(sep) + mapsets_len = len(mapsets) + + assert len(parsed_list) == mapsets_len + assert text == sep.join(mapsets) + "\n" + + +@pytest.mark.parametrize("separator", SEPARATORS) +def test_plain_list_output(simple_dataset, separator): + """Test that the separators are properly applied with list flag""" + mapsets = simple_dataset.mapsets + text = gs.read_command("g.mapsets", format="plain", separator=separator, flags="l") + _check_parsed_list(mapsets, text, gutils.separator(separator)) + + +@pytest.mark.parametrize("separator", SEPARATORS) +def test_plain_print_output(simple_dataset, separator): + """Test that the separators are properly applied with print flag""" + mapsets = simple_dataset.accessible_mapsets + text = gs.read_command("g.mapsets", format="plain", separator=separator, flags="p") + _check_parsed_list(mapsets, text, gutils.separator(separator)) + + +def test_json_list_ouput(simple_dataset): + """JSON format""" + text = gs.read_command("g.mapsets", format="json", flags="l") + data = json.loads(text) + assert list(data.keys()) == ["mapsets"] + assert isinstance(data["mapsets"], list) + assert len(data["mapsets"]) == 4 + for mapset in simple_dataset.mapsets: + assert mapset in data["mapsets"] + + +def test_json_print_ouput(simple_dataset): + """JSON format""" + text = gs.read_command("g.mapsets", format="json", flags="p") + data = json.loads(text) + assert list(data.keys()) == ["mapsets"] + assert isinstance(data["mapsets"], list) + assert len(data["mapsets"]) == 2 + for mapset in simple_dataset.accessible_mapsets: + assert mapset in data["mapsets"]