Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

v.info: Add JSON output for history #4989

Merged
merged 14 commits into from
Feb 9, 2025
Merged
5 changes: 5 additions & 0 deletions vector/v.info/local_proto.h
Original file line number Diff line number Diff line change
Expand Up @@ -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 };

Expand All @@ -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 *);
void print_history(struct Map_info *, enum OutputFormat);
7 changes: 1 addition & 6 deletions vector/v.info/main.c
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
122 changes: 122 additions & 0 deletions vector/v.info/print.c
Original file line number Diff line number Diff line change
Expand Up @@ -771,3 +771,125 @@ 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);
}
else {
// 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';
nilason marked this conversation as resolved.
Show resolved Hide resolved
}
}

/*!
\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)
{

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_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)
{
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), Map) != NULL) {
nilason marked this conversation as resolved.
Show resolved Hide resolved
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 (strlen(command) > 0 && strlen(mapset_path) > 0 &&
strlen(user) > 0 && strlen(date) > 0) {
add_record_to_json(command, user, date, mapset_path,
record_array);
}
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);
}
}
43 changes: 43 additions & 0 deletions vector/v.info/testsuite/test_vinfo.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down Expand Up @@ -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
Expand All @@ -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,
],
)

Expand Down Expand Up @@ -271,6 +284,36 @@ 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": [
{
"command": 'v.random output="test_vinfo_with_hist" npoints=5 layer="-1" zmin=0 zmax=100 column_type="double precision"',
},
{
"command": 'v.mkgrid map="test_vinfo_with_hist" grid=10,10 type="point"',
},
]
}

# The following fields vary depending on the Grass sample data's path,
# date, and user. Therefore, only check for their presence in the JSON output
# and not for their exact values.
NishantBansal2003 marked this conversation as resolved.
Show resolved Hide resolved
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(
Expand Down
Loading