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

g.mapsets: Add JSON output #2542

Merged
merged 31 commits into from
Apr 11, 2024
Merged
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
032620a
g.mapsets: fixed indenting and added json flag in preperation to add…
cwhite911 Aug 27, 2022
dfbf8cf
Added format options to list mapsets as plain, vertical, csv, or json
cwhite911 Aug 27, 2022
3cd14cc
g.mapsets: Added start testing listing mapsets with different formats
cwhite911 Aug 27, 2022
53e3905
g.mapsets: Reformated python with flake8 and black
cwhite911 Aug 27, 2022
0f05302
g.mapsets: Fixed indent conflict
cwhite911 Aug 27, 2022
5415520
Merge remote-tracking branch 'upstream/main' into g.mapset-json
cwhite911 Aug 27, 2022
a904c05
Fixed implicit declaration of function errors
cwhite911 Aug 27, 2022
d3da3fd
Fixed issues found in code review
cwhite911 Aug 28, 2022
8173965
Added print flag to json output and added tests
cwhite911 Aug 28, 2022
34dd435
Simplified tests
cwhite911 Aug 31, 2022
5fd4e05
Merge branch 'main' into g.mapset-json
echoix Mar 21, 2024
e5e834c
Apply clang format suggestions
echoix Mar 21, 2024
5622b70
Merge branch 'main' into g.mapset-json
cwhite911 Apr 4, 2024
fb21b77
Updated code to use parson and fixed tests
Apr 4, 2024
76dbf30
Updated docs
Apr 4, 2024
4e6b53b
Fixed mapsets array initalization issue
Apr 4, 2024
8163581
Fixed bug setting JSON_ARRAY
Apr 4, 2024
bdceb3d
Removed unused parameter
Apr 4, 2024
db0252e
Update general/g.mapsets/main.c
cwhite911 Apr 5, 2024
6c18a50
Updated default separator to space
Apr 5, 2024
8e99fec
Update general/g.mapsets/main.c
cwhite911 Apr 7, 2024
8961692
Update general/g.mapsets/main.c
cwhite911 Apr 7, 2024
06d04a6
Update general/g.mapsets/main.c
cwhite911 Apr 7, 2024
cf061e6
Merge branch 'main' into g.mapset-json
cwhite911 Apr 7, 2024
ace2374
Removed unneeded logic
Apr 7, 2024
26f47c3
Update general/g.mapsets/list.c
cwhite911 Apr 8, 2024
e7f815a
Update general/g.mapsets/list.c
cwhite911 Apr 8, 2024
4b8f58b
Merge branch 'main' into g.mapset-json
cwhite911 Apr 8, 2024
4d0794c
Implmented suggestion from code review
Apr 8, 2024
6c8509a
Removed duplicate code
Apr 8, 2024
4463bf1
Refactored duplicate code and removed some comments
Apr 10, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion general/g.mapsets/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ MODULE_TOPDIR = ../..

PGM = g.mapsets

LIBES = $(GISLIB)
LIBES = $(PARSONLIB) $(GISLIB)
DEPENDENCIES = $(GISDEP)

include $(MODULE_TOPDIR)/include/Make/Module.make
Expand Down
16 changes: 16 additions & 0 deletions general/g.mapsets/g.mapsets.html
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,22 @@ <h3>Print available mapsets</h3>
PERMANENT user1 user2
</pre></div>

Mapsets can be also printed out as json by setting the format option to "json" (<b>format="json"</b>).

<div class="code">
<pre>
g.mapsets format="json" -l

{
"mapsets": [
"PERMANENT",
"user1",
"user2"
]
}
</pre>
</div>

<h3>Add new mapset</h3>

Add mapset 'user2' to the current mapset search path
Expand Down
81 changes: 81 additions & 0 deletions general/g.mapsets/list.c
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
#include <grass/gis.h>
#include <grass/glocale.h>
#include "local_proto.h"
#include <grass/parson.h>

void list_available_mapsets(const char **mapset_name, int nmapsets,
const char *fs)
Expand Down Expand Up @@ -35,6 +36,7 @@ void list_accessible_mapsets(const char *fs)
const char *name;

G_message(_("Accessible mapsets:"));

for (n = 0; (name = G_get_mapset_name(n)); n++) {
/* match each mapset to its numeric equivalent */
fprintf(stdout, "%s", name);
Expand All @@ -53,3 +55,82 @@ void list_accessible_mapsets(const char *fs)
}
fprintf(stdout, "\n");
}

void list_accessible_mapsets_json()
{
int n;
const char *name;
char *serialized_string = NULL;
JSON_Value *root_value = NULL;
JSON_Object *root_object = NULL;
JSON_Array *mapsets = NULL;

root_value = json_value_init_object();
root_object = json_value_get_object(root_value);

// Create and add mapsets array to root object
json_object_set_value(root_object, "mapsets", json_value_init_array());
mapsets = json_object_get_array(root_object, "mapsets");

// Check that memory was allocated to root json object
if (root_value == NULL) {
cwhite911 marked this conversation as resolved.
Show resolved Hide resolved
G_fatal_error(_("Failed to initialize JSON. Out of memory?"));
}

// Check that memory was allocated to mapsets array
if (mapsets == NULL) {
G_fatal_error(_("Failed to initialize JSON array. Out of memory?"));
}

// Add mapsets to mapsets array
for (n = 0; (name = G_get_mapset_name(n)); n++) {
cwhite911 marked this conversation as resolved.
Show resolved Hide resolved
// Append mapset name to mapsets array
json_array_append_string(mapsets, name);
}

// Serialize root object to string and print it to stdout
serialized_string = json_serialize_to_string_pretty(root_value);
puts(serialized_string);
cwhite911 marked this conversation as resolved.
Show resolved Hide resolved

// Free memory
json_free_serialized_string(serialized_string);
json_value_free(root_value);
}

void list_avaliable_mapsets_json(const char **mapset_name, int nmapsets)
{
int n;
char *serialized_string = NULL;
JSON_Value *root_value = NULL;
JSON_Object *root_object = NULL;
JSON_Array *mapsets = NULL;

root_value = json_value_init_object();
root_object = json_value_get_object(root_value);

// Create mapsets array
json_object_set_value(root_object, "mapsets", json_value_init_array());
mapsets = json_object_get_array(root_object, "mapsets");

// Check that memory was allocated to root json object
if (root_value == NULL) {
G_fatal_error(_("Failed to initialize JSON. Out of memory?"));
}

if (mapsets == NULL) {
G_fatal_error(_("Failed to initialize JSON array. Out of memory?"));
}

// Append mapsets to mapsets array
for (n = 0; n < nmapsets; n++) {
json_array_append_string(mapsets, mapset_name[n]);
}

// Serialize root object to string and print it to stdout
serialized_string = json_serialize_to_string_pretty(root_value);
puts(serialized_string);

// Free memory
json_free_serialized_string(serialized_string);
json_value_free(root_value);
}
2 changes: 2 additions & 0 deletions general/g.mapsets/local_proto.h
Original file line number Diff line number Diff line change
Expand Up @@ -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();
89 changes: 77 additions & 12 deletions general/g.mapsets/main.c
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,10 @@
* Markus Neteler <neteler itc.it>,
* Moritz Lennert <mlennert club.worldonline.be>,
* Martin Landa <landa.martin gmail.com>,
* Huidae Cho <grass4u gmail.com>
* Huidae Cho <grass4u gmail.com>,
* Corey White <smortopahri gmail.com>
* 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
Expand All @@ -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[])
Expand All @@ -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;

Expand Down Expand Up @@ -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 = _("Format");

opt.fsep = G_define_standard_option(G_OPT_F_SEP);
opt.fsep->answer = NULL;
opt.fsep->guisection = _("Format");
cwhite911 marked this conversation as resolved.
Show resolved Hide resolved

opt.list = G_define_flag();
opt.list->key = 'l';
Expand Down Expand Up @@ -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("space");
cwhite911 marked this conversation as resolved.
Show resolved Hide resolved
}
else
fsep = NULL; /* Something like a separator is part of the format. */
}

/* list available mapsets */
if (opt.list->answer) {
Expand All @@ -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);
}

Expand All @@ -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(fsep);
cwhite911 marked this conversation as resolved.
Show resolved Hide resolved
}
else {
list_accessible_mapsets(fsep);
}

exit(EXIT_SUCCESS);
}

Expand Down
35 changes: 35 additions & 0 deletions general/g.mapsets/tests/conftest.py
Original file line number Diff line number Diff line change
@@ -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
)
68 changes: 68 additions & 0 deletions general/g.mapsets/tests/g_mapsets_list_format_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
############################################################################
#
# MODULE: Test of g.mapsets
# AUTHOR(S): Corey White <smortopahri gmail com>
# 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"]
Loading