diff --git a/vector/v.info/local_proto.h b/vector/v.info/local_proto.h index b1a7d5795cb..100c314dbb4 100644 --- a/vector/v.info/local_proto.h +++ b/vector/v.info/local_proto.h @@ -5,6 +5,7 @@ #define SHELL_BASIC 0x02 #define SHELL_REGION 0x04 #define SHELL_TOPO 0x08 +#define STR_LEN 1024 enum OutputFormat { PLAIN, SHELL, JSON }; @@ -24,3 +25,7 @@ void print_columns(struct Map_info *, const char *, const char *, void print_info(struct Map_info *); void print_shell(struct Map_info *, const char *, enum OutputFormat, JSON_Object *); +void parse_history_line(const char *, char *, char *, char *, char *, char *, + char *, char *); +void add_record_to_json(char *, char *, char *, char *, JSON_Array *, int); +void print_history(struct Map_info *, enum OutputFormat); diff --git a/vector/v.info/main.c b/vector/v.info/main.c index ef1f5c44d06..23dcff0cdec 100644 --- a/vector/v.info/main.c +++ b/vector/v.info/main.c @@ -72,12 +72,7 @@ int main(int argc, char *argv[]) if (hist_flag || col_flag) { if (hist_flag) { - char buf[1001]; - - Vect_hist_rewind(&Map); - while (Vect_hist_read(buf, 1000, &Map) != NULL) { - fprintf(stdout, "%s\n", buf); - } + print_history(&Map, format); } else if (col_flag) { print_columns(&Map, input_opt, field_opt, format); diff --git a/vector/v.info/print.c b/vector/v.info/print.c index ae913bf2e3d..da26553c2d1 100644 --- a/vector/v.info/print.c +++ b/vector/v.info/print.c @@ -771,3 +771,131 @@ void print_info(struct Map_info *Map) divider('+'); fprintf(stdout, "\n"); } + +/*! + \brief Extracts and assigns values from a history line to command, gisdbase, + location, mapset, user, date, and mapset_path based on specific prefixes. + + */ +void parse_history_line(const char *buf, char *command, char *gisdbase, + char *location, char *mapset, char *user, char *date, + char *mapset_path) +{ + if (strncmp(buf, "COMMAND:", 8) == 0) { + sscanf(buf, "COMMAND: %[^\n]", command); + } + else if (strncmp(buf, "GISDBASE:", 9) == 0) { + sscanf(buf, "GISDBASE: %[^\n]", gisdbase); + } + else if (strncmp(buf, "LOCATION:", 9) == 0) { + sscanf(buf, "LOCATION: %s MAPSET: %s USER: %s DATE: %[^\n]", location, + mapset, user, date); + + snprintf(mapset_path, GPATH_MAX, "%s/%s/%s", gisdbase, location, + mapset); + } +} + +/*! + \brief Creates a JSON object with fields for command, user, date, and + mapset_path, appends it to a JSON array. + + */ +void add_record_to_json(char *command, char *user, char *date, + char *mapset_path, JSON_Array *record_array, + int history_number) +{ + + JSON_Value *info_value = json_value_init_object(); + if (info_value == NULL) { + G_fatal_error(_("Failed to initialize JSON object. Out of memory?")); + } + JSON_Object *info_object = json_object(info_value); + + json_object_set_number(info_object, "history_number", history_number); + json_object_set_string(info_object, "command", command); + json_object_set_string(info_object, "mapset_path", mapset_path); + json_object_set_string(info_object, "user", user); + json_object_set_string(info_object, "date", date); + + json_array_append_value(record_array, info_value); +} + +/*! + \brief Reads history entries from a map, formats them based on the specified + output format (PLAIN, SHELL, or JSON), and prints the results. + + */ +void print_history(struct Map_info *Map, enum OutputFormat format) +{ + int history_number = 0; + + char buf[STR_LEN] = {0}; + char command[STR_LEN] = {0}, gisdbase[STR_LEN] = {0}; + char location[STR_LEN] = {0}, mapset[STR_LEN] = {0}; + char user[STR_LEN] = {0}, date[STR_LEN] = {0}; + char mapset_path[GPATH_MAX] = {0}; + + JSON_Value *root_value = NULL, *record_value = NULL; + JSON_Object *root_object = NULL; + JSON_Array *record_array = NULL; + + if (format == JSON) { + root_value = json_value_init_object(); + if (root_value == NULL) { + G_fatal_error( + _("Failed to initialize JSON object. Out of memory?")); + } + root_object = json_object(root_value); + + record_value = json_value_init_array(); + if (record_value == NULL) { + G_fatal_error(_("Failed to initialize JSON array. Out of memory?")); + } + record_array = json_array(record_value); + } + + Vect_hist_rewind(Map); + while (Vect_hist_read(buf, sizeof(buf) - 1, Map) != NULL) { + switch (format) { + case PLAIN: + case SHELL: + fprintf(stdout, "%s\n", buf); + break; + case JSON: + // Parse each line based on its prefix + parse_history_line(buf, command, gisdbase, location, mapset, user, + date, mapset_path); + if (command[0] != '\0' && mapset_path[0] != '\0' && + user[0] != '\0' && date[0] != '\0') { + // Increment history counter + history_number++; + + add_record_to_json(command, user, date, mapset_path, + record_array, history_number); + + // Clear the input strings before processing new + // entries in the history file + command[0] = '\0'; + user[0] = '\0'; + date[0] = '\0'; + mapset_path[0] = '\0'; + } + break; + } + } + + if (format == JSON) { + json_object_set_value(root_object, "records", record_value); + + char *serialized_string = json_serialize_to_string_pretty(root_value); + if (!serialized_string) { + json_value_free(root_value); + G_fatal_error(_("Failed to initialize pretty JSON string.")); + } + puts(serialized_string); + + json_free_serialized_string(serialized_string); + json_value_free(root_value); + } +} diff --git a/vector/v.info/testsuite/test_vinfo.py b/vector/v.info/testsuite/test_vinfo.py index 366859aebae..19da76b2fef 100644 --- a/vector/v.info/testsuite/test_vinfo.py +++ b/vector/v.info/testsuite/test_vinfo.py @@ -12,6 +12,7 @@ class TestVInfo(TestCase): test_vinfo_no_db = "test_vinfo_no_db" test_vinfo_with_db = "test_vinfo_with_db" test_vinfo_with_db_3d = "test_vinfo_with_db_3d" + test_vinfo_with_hist = "test_vinfo_with_hist" # All maps should be tested against these references reference = { @@ -56,6 +57,17 @@ def setUpClass(cls): flags="z", ) + cls.runModule( + "v.random", output=cls.test_vinfo_with_hist, npoints=5, zmin=0, zmax=100 + ) + + # For testing vector history file with multiple commands + cls.runModule( + "v.support", + map=cls.test_vinfo_with_hist, + cmdhist='v.mkgrid map="test_vinfo_with_hist" grid=10,10 type="point"', + ) + cls.runModule("v.timestamp", map=cls.test_vinfo_with_db_3d, date="15 jan 1994") @classmethod @@ -69,6 +81,7 @@ def tearDownClass(cls): cls.test_vinfo_no_db, cls.test_vinfo_with_db, cls.test_vinfo_with_db_3d, + cls.test_vinfo_with_hist, ], ) @@ -271,6 +284,38 @@ def test_json_column(self): self.assertDictEqual(expected_json, result) + def test_json_histroy(self): + """Test the JSON output format of v.info with the history flag, using a history file containing multiple commands.""" + module = SimpleModule( + "v.info", map=self.test_vinfo_with_hist, format="json", flags="h" + ) + self.runModule(module) + result = json.loads(module.outputs.stdout) + + expected_json = { + "records": [ + { + "history_number": 1, + "command": 'v.random output="test_vinfo_with_hist" npoints=5 layer="-1" zmin=0 zmax=100 column_type="double precision"', + }, + { + "history_number": 2, + "command": 'v.mkgrid map="test_vinfo_with_hist" grid=10,10 type="point"', + }, + ] + } + + # The following fields vary depending on the test data's path, + # date, and user. Therefore, only check for their presence in + # the JSON output and not for their exact values. + remove_fields = ["mapset_path", "date", "user"] + for record in result["records"]: + for field in remove_fields: + self.assertIn(field, record) + record.pop(field) + + self.assertDictEqual(expected_json, result) + def test_database_table(self): """Test the database table column and type of the two vector maps with attribute data""" self.assertModuleKeyValue(