Skip to content

Commit

Permalink
handle 'AsIs' as length-1 JSON arrays
Browse files Browse the repository at this point in the history
  • Loading branch information
coolbutuseless committed Sep 14, 2023
1 parent 68afc32 commit 7bafff5
Show file tree
Hide file tree
Showing 7 changed files with 142 additions and 66 deletions.
5 changes: 5 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@
* removed `vectors_to_df`
* replaced with `obj_of_arrs_to_df`
* added `arr_of_obs_to_df`
* length-1 vectors marked with class `AsIs` (using a call to `I()`) will
never be unboxed i.e. will always be serialized as a JSON []-array with
one element.
* Added parse option `length1_array_asis`. If `TRUE` then automatically add
the class `AsIs` to the object object.

# yyjsonr 0.1.9 2023-09-13

Expand Down
18 changes: 11 additions & 7 deletions R/json-opts.R
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,8 @@ write_flag <- list(
#' then array of mixed string/numeric types will be promoted to all
#' string values and returned as an atonic character vector. Set this to \code{TRUE}
#' if you want to emulate the behaviour of \code{jsonlite::fromJSON()}
#' @param length1_array_asis logical. Should JSON arrays with length = 1 be
#' marked with class \code{AsIs}. Default: FALSE
#'
#' @seealso [read_flag()]
#' @return Named list of options
Expand All @@ -201,6 +203,7 @@ opts_read_json <- function(
missing_list_elem = c('null', 'na'),
obj_of_arrs_to_df = TRUE,
arr_of_objs_to_df = TRUE,
length1_array_asis = FALSE,
str_specials = c('string', 'special'),
num_specials = c('special', 'string'),
promote_num_to_string = FALSE,
Expand All @@ -209,13 +212,14 @@ opts_read_json <- function(

structure(
list(
int64 = match.arg(int64),
missing_list_elem = match.arg(missing_list_elem),
obj_of_arrs_to_df = isTRUE(obj_of_arrs_to_df),
arr_of_objs_to_df = isTRUE(arr_of_objs_to_df),
str_specials = match.arg(str_specials),
num_specials = match.arg(num_specials),
yyjson_read_flag = as.integer(yyjson_read_flag)
int64 = match.arg(int64),
missing_list_elem = match.arg(missing_list_elem),
obj_of_arrs_to_df = isTRUE(obj_of_arrs_to_df),
arr_of_objs_to_df = isTRUE(arr_of_objs_to_df),
length1_array_asis = isTRUE(length1_array_asis),
str_specials = match.arg(str_specials),
num_specials = match.arg(num_specials),
yyjson_read_flag = as.integer(yyjson_read_flag)
),
class = "opts_read_json"
)
Expand Down
4 changes: 4 additions & 0 deletions man/opts_read_json.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

13 changes: 12 additions & 1 deletion src/R-yyjson-parse.c
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ parse_options create_parse_options(SEXP parse_opts_) {
.missing_list_elem = MISSING_AS_NULL,
.obj_of_arrs_to_df = true,
.arr_of_objs_to_df = true,
.length1_array_asis = false,
.str_specials = STR_SPECIALS_AS_STRING,
.num_specials = NUM_SPECIALS_AS_SPECIAL,
.promote_num_to_string = false,
Expand All @@ -47,7 +48,9 @@ parse_options create_parse_options(SEXP parse_opts_) {
const char *opt_name = CHAR(STRING_ELT(nms_, i));
SEXP val_ = VECTOR_ELT(parse_opts_, i);

if (strcmp(opt_name, "int64") == 0) {
if (strcmp(opt_name, "length1_array_asis") == 0) {
opt.length1_array_asis = asLogical(val_);
} else if (strcmp(opt_name, "int64") == 0) {
const char *val = CHAR(STRING_ELT(val_, 0));
opt.int64 = strcmp(val, "string") == 0 ? INT64_AS_STR : INT64_AS_BIT64;
} else if (strcmp(opt_name, "missing_list_elem") == 0) {
Expand Down Expand Up @@ -1129,6 +1132,14 @@ SEXP json_array_as_robj(yyjson_val *arr, parse_options *opt) {
default:
error("json_array_as_robj(). Ooops\n");
}

//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Tag a length-1 array as class = 'AsIs'
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
if (opt->length1_array_asis && length(res_) == 1 && !inherits(res_, "Integer64")) {
setAttrib(res_, R_ClassSymbol, mkString("AsIs"));
}

} else if (ctn_bitset == CTN_ARR) {
unsigned int sexp_type = get_best_sexp_type_for_matrix(arr, opt);
if (sexp_type != 0) {
Expand Down
1 change: 1 addition & 0 deletions src/R-yyjson-parse.h
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ typedef struct {
unsigned int missing_list_elem;
bool obj_of_arrs_to_df;
bool arr_of_objs_to_df;
bool length1_array_asis;
unsigned int str_specials;
unsigned int num_specials;
bool promote_num_to_string;
Expand Down
120 changes: 62 additions & 58 deletions src/R-yyjson-serialize.c
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ yyjson_mut_val *scalar_date_to_json_val(SEXP vec_, unsigned int idx, yyjson_mut_
double ndays = 0;
if (isReal(vec_)) {
ndays = REAL(vec_)[idx];

if (!R_FINITE(ndays)) {
return yyjson_mut_null(doc);
}
Expand Down Expand Up @@ -206,7 +206,7 @@ yyjson_mut_val *scalar_posixct_to_json_val(SEXP vec_, unsigned int idx, yyjson_m

if (isReal(vec_)) {
seconds = REAL(vec_)[idx];

if (!R_FINITE(seconds)) {
return yyjson_mut_null(doc);
}
Expand Down Expand Up @@ -308,7 +308,7 @@ yyjson_mut_val *scalar_double_to_json_val(double rdbl, yyjson_mut_doc *doc, seri
// Scalar STRSRXP to JSON value
//===========================================================================
yyjson_mut_val *scalar_strsxp_to_json_val(SEXP str_, unsigned int idx, yyjson_mut_doc *doc, serialize_options *opt) {

yyjson_mut_val *val;

SEXP charsxp_ = STRING_ELT(str_, idx);
Expand All @@ -321,7 +321,7 @@ yyjson_mut_val *scalar_strsxp_to_json_val(SEXP str_, unsigned int idx, yyjson_mu
} else {
val = yyjson_mut_strcpy(doc, CHAR(charsxp_));
}

return val;
}

Expand Down Expand Up @@ -356,13 +356,13 @@ yyjson_mut_val *vector_lglsxp_to_json_array(SEXP vec_, yyjson_mut_doc *doc, seri
// Serialize factor to JSON []-array
//===========================================================================
yyjson_mut_val *vector_factor_to_json_array(SEXP vec_, yyjson_mut_doc *doc, serialize_options *opt) {

yyjson_mut_val *arr = yyjson_mut_arr(doc);

for (int i = 0; i < length(vec_); i++) {
yyjson_mut_arr_append(arr, scalar_factor_to_json_val(vec_, i, doc, opt));
}

return arr;
}

Expand Down Expand Up @@ -546,19 +546,19 @@ yyjson_mut_val *vector_to_json_array(SEXP vec_, yyjson_mut_doc *doc, serialize_o
// Write a matrix as an array of arrays in column-major order
//===========================================================================
yyjson_mut_val *matrix_to_col_major_array(SEXP mat_, unsigned int offset, yyjson_mut_doc *doc, serialize_options *opt) {

SEXP dims_ = getAttrib(mat_, R_DimSymbol);
unsigned int nrow = INTEGER(dims_)[0];
unsigned int ncol = INTEGER(dims_)[1];

// 'Offset' is used for array processing where we want to start processing
// elements at an index other than 0. i.e. the n-th layer of the 3-d array

//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Allocate an array within the document
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
yyjson_mut_val *arr = yyjson_mut_arr(doc);

switch(TYPEOF(mat_)) {
case LGLSXP: {
int32_t *ptr = INTEGER(mat_) + offset;
Expand Down Expand Up @@ -595,19 +595,19 @@ yyjson_mut_val *matrix_to_col_major_array(SEXP mat_, unsigned int offset, yyjson
break;
case STRSXP: {
for (unsigned int row = 0; row < nrow; row++) {
yyjson_mut_val *row_arr = yyjson_mut_arr(doc);
for (unsigned int col = 0; col < ncol; col++) {
yyjson_mut_arr_append(row_arr, scalar_strsxp_to_json_val(mat_, offset + row + col * nrow, doc, opt));
}
yyjson_mut_arr_append(arr, row_arr);
yyjson_mut_val *row_arr = yyjson_mut_arr(doc);
for (unsigned int col = 0; col < ncol; col++) {
yyjson_mut_arr_append(row_arr, scalar_strsxp_to_json_val(mat_, offset + row + col * nrow, doc, opt));
}
yyjson_mut_arr_append(arr, row_arr);
}
}
break;
default:
error("matrix_to_col_major_array(). Unhandled type: %s", type2char(TYPEOF(mat_)));
}


return arr;
}

Expand Down Expand Up @@ -708,7 +708,7 @@ yyjson_mut_val *unnamed_list_to_json_array(SEXP list_, yyjson_mut_doc *doc, seri
return arr;
}


//===========================================================================
// # # # # # #
// # # # # #
Expand Down Expand Up @@ -1017,7 +1017,7 @@ yyjson_mut_val *data_frame_to_json_array_of_arrays(SEXP df_, yyjson_mut_doc *doc


yyjson_mut_val *data_frame_to_json_array_of_objects(SEXP df_, yyjson_mut_doc *doc, serialize_options *opt) {

//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Sanity check
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Expand All @@ -1029,13 +1029,13 @@ yyjson_mut_val *data_frame_to_json_array_of_objects(SEXP df_, yyjson_mut_doc *do
if (isNull(getAttrib(df_, R_NamesSymbol))) {
return data_frame_to_json_array_of_arrays(df_, doc, opt);
}

// Create an array
yyjson_mut_val *arr = yyjson_mut_arr(doc);

// get size of data.frame
unsigned int nrows = length(VECTOR_ELT(df_, 0)); // length of first column

//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// For each row
// create an object
Expand All @@ -1058,9 +1058,9 @@ yyjson_mut_val *data_frame_to_json_array_of_objects(SEXP df_, yyjson_mut_doc *do
// }

for (int row = 0; row < nrows; row++) {

yyjson_mut_val *obj = data_frame_row_to_json_object(df_, col_type, row, -1, doc, opt);

// Add row obj to array
yyjson_mut_arr_append(arr, obj);
}
Expand Down Expand Up @@ -1110,40 +1110,44 @@ yyjson_mut_val *serialize_core(SEXP robj_, yyjson_mut_doc *doc, serialize_option
}
val = dim3_matrix_to_col_major_array(robj_, doc, opt);
} else if (opt->auto_unbox && isVectorAtomic(robj_) && length(robj_) == 1) {
switch(TYPEOF(robj_)) {
case LGLSXP:
val = scalar_logical_to_json_val(asLogical(robj_), doc, opt);
break;
case INTSXP:
if (isFactor(robj_)) {
val = scalar_factor_to_json_val(robj_, 0, doc, opt);
} else if (inherits(robj_, "Date")) {
val = scalar_date_to_json_val(robj_, 0, doc, opt);
} else if (inherits(robj_, "POSIXct")) {
val = scalar_posixct_to_json_val(robj_, 0, doc, opt);
} else {
val = scalar_integer_to_json_val(asInteger(robj_), doc, opt);
}
break;
case REALSXP:
if (inherits(robj_, "Date")) {
val = scalar_date_to_json_val(robj_, 0, doc, opt);
} else if (inherits(robj_, "POSIXct")) {
val = scalar_posixct_to_json_val(robj_, 0, doc, opt);
} else if (inherits(robj_, "integer64")) {
val = scalar_integer64_to_json_val(robj_, 0, doc, opt);
} else {
val = scalar_double_to_json_val(asReal(robj_), doc, opt);
if (inherits(robj_, "AsIs")) {
val = vector_to_json_array(robj_, doc, opt);
} else {
switch(TYPEOF(robj_)) {
case LGLSXP:
val = scalar_logical_to_json_val(asLogical(robj_), doc, opt);
break;
case INTSXP:
if (isFactor(robj_)) {
val = scalar_factor_to_json_val(robj_, 0, doc, opt);
} else if (inherits(robj_, "Date")) {
val = scalar_date_to_json_val(robj_, 0, doc, opt);
} else if (inherits(robj_, "POSIXct")) {
val = scalar_posixct_to_json_val(robj_, 0, doc, opt);
} else {
val = scalar_integer_to_json_val(asInteger(robj_), doc, opt);
}
break;
case REALSXP:
if (inherits(robj_, "Date")) {
val = scalar_date_to_json_val(robj_, 0, doc, opt);
} else if (inherits(robj_, "POSIXct")) {
val = scalar_posixct_to_json_val(robj_, 0, doc, opt);
} else if (inherits(robj_, "integer64")) {
val = scalar_integer64_to_json_val(robj_, 0, doc, opt);
} else {
val = scalar_double_to_json_val(asReal(robj_), doc, opt);
}
break;
case STRSXP:
val = scalar_strsxp_to_json_val(robj_, 0, doc, opt);
break;
case RAWSXP:
val = scalar_rawsxp_to_json_val(robj_, 0, doc, opt);
break;
default:
error("Unhandled scalar SEXP: %s\n", type2char(TYPEOF(robj_)));
}
break;
case STRSXP:
val = scalar_strsxp_to_json_val(robj_, 0, doc, opt);
break;
case RAWSXP:
val = scalar_rawsxp_to_json_val(robj_, 0, doc, opt);
break;
default:
error("Unhandled scalar SEXP: %s\n", type2char(TYPEOF(robj_)));
}
} else if (isVectorAtomic(robj_)) {
val = vector_to_json_array(robj_, doc, opt);
Expand All @@ -1170,11 +1174,11 @@ yyjson_mut_val *serialize_core(SEXP robj_, yyjson_mut_doc *doc, serialize_option
// Serialize R object to JSON string. Callable from R
//===========================================================================
SEXP serialize_to_str_(SEXP robj_, SEXP serialize_opts_) {


serialize_options opt = parse_serialize_options(serialize_opts_);


//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Create a mutable document.
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Expand Down
47 changes: 47 additions & 0 deletions tests/testthat/test-unboxing-AsIs-handling.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@


test_that("AsIs handling works", {
library(jsonlite)

#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
#' I() forces scalar values to be written as JSON-array
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
l1 <- list(a = 1, b = I(2))
expect_equal(
write_json_str(l1, auto_unbox = TRUE),
'{"a":1.0,"b":[2.0]}'
)
expect_equal(
write_json_str(l1, auto_unbox = FALSE),
'{"a":[1.0],"b":[2.0]}'
)

#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
#' Unboxing doesn't affect vectors with length > 1
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
l2 <- list(a = 1, b = I(c(2, 3)))
expect_equal(
write_json_str (l2, auto_unbox = TRUE),
'{"a":1.0,"b":[2.0,3.0]}'
)
expect_equal(
write_json_str (l2, auto_unbox = FALSE),
'{"a":[1.0],"b":[2.0,3.0]}'
)

#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
#'
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
s <- '{"a":1,"b":[2]}'

x <- read_json_str(s)
expect_identical(x, list(a = 1L, b = 2L))


x <- read_json_str(s, length1_array_asis = TRUE)
expect_identical(x, list(a = 1L, b = structure(2L, class = 'AsIs')))

s2 <- write_json_str(x, auto_unbox = TRUE)
expect_identical(s, s2)

})

0 comments on commit 7bafff5

Please sign in to comment.