diff --git a/temporal/CMakeLists.txt b/temporal/CMakeLists.txt index e368b7b5f2f..8013228cb86 100644 --- a/temporal/CMakeLists.txt +++ b/temporal/CMakeLists.txt @@ -63,7 +63,7 @@ if(WITH_DOCS) generate_docs(temporalintro TARGET ALL_TEMPORAL_MODULES) endif() -build_program_in_subdir(t.connect DEPENDS grass_gis grass_temporal) +build_program_in_subdir(t.connect DEPENDS grass_gis grass_temporal grass_parson) foreach(temporal_DIR ${temporal_script_modules_list}) build_script_in_subdir(${temporal_DIR}) diff --git a/temporal/t.connect/Makefile b/temporal/t.connect/Makefile index bdd51ef3b7c..f11b93d785e 100644 --- a/temporal/t.connect/Makefile +++ b/temporal/t.connect/Makefile @@ -1,6 +1,6 @@ MODULE_TOPDIR = ../.. -LIBES = $(TEMPORALLIB) $(GISLIB) +LIBES = $(TEMPORALLIB) $(GISLIB) $(PARSONLIB) DEPENDENCIES = $(TEMPORALDEP) $(GISDEP) PGM = t.connect diff --git a/temporal/t.connect/main.c b/temporal/t.connect/main.c index 6aca5cc8115..28bffee5dcf 100644 --- a/temporal/t.connect/main.c +++ b/temporal/t.connect/main.c @@ -18,14 +18,21 @@ #include #include #include +#include + +enum OutputFormat { PLAIN, SHELL, JSON }; int main(int argc, char *argv[]) { dbConnection conn; struct Flag *print, *check_set_default, *def, *sh; - struct Option *driver, *database; + struct Option *driver, *database, *format_opt; struct GModule *module; + enum OutputFormat format; + G_JSON_Value *root_value = NULL; + G_JSON_Object *root_object = NULL; + /* Initialize the GIS calls */ G_gisinit(argv[0]); @@ -42,6 +49,15 @@ int main(int argc, char *argv[]) print->description = _("Print current connection parameters and exit"); print->guisection = _("Print"); + format_opt = G_define_standard_option(G_OPT_F_FORMAT); + format_opt->options = "plain,shell,json"; + format_opt->required = NO; + format_opt->answer = "plain"; + format_opt->descriptions = _("plain;Plain text output;" + "shell;Shell script style output;" + "json;JSON (JavaScript Object Notation)"); + format_opt->guisection = _("Print"); + check_set_default = G_define_flag(); check_set_default->key = 'c'; check_set_default->description = @@ -56,9 +72,12 @@ int main(int argc, char *argv[]) sh = G_define_flag(); sh->key = 'g'; + sh->label = _("Print current connection parameters using shell style " + "and exit [deprecated]"); sh->description = - _("Print current connection parameter in shell style and exit"); - sh->guisection = _("Set"); + _("This flag is deprecated and will be removed in a future " + "release. Use format=shell instead."); + sh->guisection = _("Print"); driver = G_define_standard_option(G_OPT_DB_DRIVER); driver->options = "sqlite,pg"; @@ -72,31 +91,102 @@ int main(int argc, char *argv[]) if (G_parser(argc, argv)) exit(EXIT_FAILURE); + if (format_opt->answer != NULL) { + if (strcmp(format_opt->answer, "json") == 0) { + format = JSON; + root_value = G_json_value_init_object(); + if (root_value == NULL) { + G_fatal_error( + _("Failed to initialize JSON object. Out of memory?")); + } + root_object = G_json_object(root_value); + } + else if (strcmp(format_opt->answer, "shell") == 0) { + format = SHELL; + } + else { + format = PLAIN; + } + } + else { + format = PLAIN; + } + + if (sh->answer) { + G_verbose_message( + _("Flag 'g' is deprecated and will be removed in a future " + "release. Please use format=shell instead.")); + if (format == JSON) { + /* Free memory before failing to prevent memory leaks */ + if (root_value) + G_json_value_free(root_value); + G_fatal_error(_("The -g flag cannot be used with format=json. " + "Please select only one output format.")); + } + format = SHELL; + print->answer = 1; + } + + if (format != PLAIN && !print->answer) { + G_fatal_error( + _("The -p flag is required when using the format option.")); + } + if (print->answer) { - if (sh->answer) { - if (tgis_get_connection(&conn) == DB_OK) { + if (tgis_get_connection(&conn) == DB_OK) { + switch (format) { + case SHELL: fprintf(stdout, "driver=%s\n", conn.driverName ? conn.driverName : ""); fprintf(stdout, "database=%s\n", conn.databaseName ? conn.databaseName : ""); - } - else - G_fatal_error(_("Temporal GIS database connection not defined. " - "Run t.connect.")); - } - else { - /* get and print connection */ - if (tgis_get_connection(&conn) == DB_OK) { + break; + + case PLAIN: fprintf(stdout, "driver:%s\n", conn.driverName ? conn.driverName : ""); fprintf(stdout, "database:%s\n", conn.databaseName ? conn.databaseName : ""); + break; + + case JSON: + if (conn.driverName) + G_json_object_set_string(root_object, "driver", + conn.driverName); + else + G_json_object_set_null(root_object, "driver"); + + if (conn.databaseName) + G_json_object_set_string(root_object, "database", + conn.databaseName); + else + G_json_object_set_null(root_object, "database"); + break; + } + } + else { + if (root_value) + G_json_value_free(root_value); + G_fatal_error(_("Temporal GIS database connection not defined. " + "Run t.connect.")); + } + + if (format == JSON) { + char *json_string = G_json_serialize_to_string_pretty(root_value); + if (!json_string) { + G_json_value_free(root_value); + G_fatal_error(_("Failed to serialize JSON to pretty format.")); } - else - G_fatal_error(_("Temporal GIS database connection not defined. " - "Run t.connect.")); + + fputs(json_string, stdout); + fputc('\n', stdout); + + G_json_free_serialized_string(json_string); + G_json_value_free(root_value); } + fflush(stdout); + exit(EXIT_SUCCESS); } diff --git a/temporal/t.connect/t.connect.md b/temporal/t.connect/t.connect.md index c8ff153e6b3..fa04ed27b21 100644 --- a/temporal/t.connect/t.connect.md +++ b/temporal/t.connect/t.connect.md @@ -5,10 +5,11 @@ default setting is that the temporal database of type *sqlite3* is located in the current mapset directory. The **-p** flag will display the current temporal database connection -parameters. +parameters. Use the **format** option to specify the output format: `plain`, + `shell`, or `json` — with `plain` being the default. -The **-pg** flag will display the current temporal database connection -parameters using shell style. +The **-g** flag is deprecated and will be removed in a future release. Please +use **format=shell** option with the **-p** flag instead. The **-c** flag will silently check if the temporal database connection parameters have been set, and if not will set them to use GRASS's diff --git a/temporal/t.connect/tests/test_t_connect.py b/temporal/t.connect/tests/test_t_connect.py index c968fea5c30..e200b8e70c2 100644 --- a/temporal/t.connect/tests/test_t_connect.py +++ b/temporal/t.connect/tests/test_t_connect.py @@ -1,6 +1,8 @@ """Test current baseline functionality of t.connect""" -from grass.tools import Tools +import pytest + +from grass.tools import ToolError, Tools def test_no_connection(empty_session): @@ -28,8 +30,35 @@ def test_print_shell(t_connect_session): """Check that the shell format prints correctly.""" tools = Tools(session=t_connect_session.session) - result = tools.t_connect(flags="pg") + result = tools.t_connect(flags="p", format="shell") lines = result.text_split("\n") assert "driver=sqlite" in lines assert "database=$GISDBASE/$LOCATION_NAME/$MAPSET/tgis/sqlite.db" in lines + + +def test_print_json(t_connect_session): + """Check that the JSON format parses correctly and matches connection values.""" + tools = Tools(session=t_connect_session.session) + + result = tools.t_connect(flags="p", format="json") + + assert result["driver"] == "sqlite" + assert result["database"] == "$GISDBASE/$LOCATION_NAME/$MAPSET/tgis/sqlite.db" + + +@pytest.mark.parametrize("output_format", ["shell", "json"]) +def test_missing_print_flag(t_connect_session, output_format): + """Ensure omitting the -p flag when requesting a format triggers a fatal error.""" + tools = Tools(session=t_connect_session.session) + + with pytest.raises(ToolError): + tools.t_connect(format=output_format) + + +def test_incompatible_flags(t_connect_session): + """Ensure combining the deprecated -g flag with format=json triggers a fatal error.""" + tools = Tools(session=t_connect_session.session) + + with pytest.raises(ToolError): + tools.t_connect(flags="g", format="json")