diff --git a/ballerina/json_api.bal b/ballerina/json_api.bal index b6cf13b..6426c17 100644 --- a/ballerina/json_api.bal +++ b/ballerina/json_api.bal @@ -56,9 +56,19 @@ public isolated function parseStream(stream s, Options options = # # + v - Source anydata value # + return - representation of `v` as value of type json -public isolated function toJson(anydata v) +public isolated function toJson(anydata v) returns json|Error = @java:Method {'class: "io.ballerina.lib.data.jsondata.json.Native"} external; +# Prettifies the provided JSON value. +# +# + value - The `json` value to be prettified +# + indentation - The number of spaces for an indentation +# + return - The prettified `json` as a string +public isolated function prettify(json value, int indentation = 4) returns string { + string indent = getIndentation(indentation); + return prettifyJson(value, indent, 0); +} + # Represent the options that can be used to modify the behaviour of the projection. # # + allowDataProjection - enable or disable projection diff --git a/ballerina/tests/prettify_test.bal b/ballerina/tests/prettify_test.bal new file mode 100644 index 0000000..c853c5e --- /dev/null +++ b/ballerina/tests/prettify_test.bal @@ -0,0 +1,224 @@ +// Copyright (c) 2024, WSO2 LLC. (https://www.wso2.com). +// +// WSO2 LLC. licenses this file to you under the Apache License, +// Version 2.0 (the "License"); you may not use this file except +// in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +import ballerina/test; + +@test:Config { + groups: ["prettify", "string"] +} +function testStringValue() returns error? { + json value = "Sam"; + string actual = prettify(value); + string expected = check getStringContentFromFile("string_value.json"); + test:assertEquals(actual, expected); +} + +@test:Config { + groups: ["prettify", "int"] +} +function testIntValue() returns error? { + json value = 515; + string actual = prettify(value); + string expected = check getStringContentFromFile("int_value.json"); + test:assertEquals(actual, expected); +} + +@test:Config { + groups: ["prettify", "boolean"] +} +function testBooleanValue() returns error? { + json value = false; + string actual = prettify(value); + string expected = check getStringContentFromFile("boolean_value.json"); + test:assertEquals(actual, expected); +} + +@test:Config { + groups: ["prettify", "null"] +} +function testNullValue() returns error? { + json value = null; + string actual = prettify(value); + string expected = " null"; + test:assertEquals(actual, expected); +} + +@test:Config { + groups: ["prettify", "array"] +} +function testStringArray() returns error? { + json value = ["sam", "bam", "tan"]; + string actual = prettify(value); + string expected = check getStringContentFromFile("string_array.json"); + test:assertEquals(actual, expected); +} + +@test:Config { + groups: ["prettify", "array"] +} +function testEmptyArray() returns error? { + json value = []; + string actual = prettify(value); + string expected = check getStringContentFromFile("empty_array.json"); + test:assertEquals(actual, expected); +} + +@test:Config { + groups: ["prettify", "map"] +} +function testEmptyMap() returns error? { + json value = {}; + string actual = prettify(value); + string expected = check getStringContentFromFile("empty_map.json"); + test:assertEquals(actual, expected); +} + +@test:Config { + groups: ["prettify", "map"] +} +function testArrayOfEmptyMaps() returns error? { + json value = [ + {}, + {}, + {} + ]; + string actual = prettify(value); + string expected = check getStringContentFromFile("array_of_empty_maps.json"); + test:assertEquals(actual, expected); +} + +@test:Config { + groups: ["prettify", "map"] +} +function testMapWithStringField() returns error? { + json value = { + name: "Walter White" + }; + string actual = prettify(value); + string expected = check getStringContentFromFile("map_with_string_field.json"); + test:assertEquals(actual, expected); +} + +@test:Config { + groups: ["prettify", "map"] +} +function testMapWithMultipleStringFields() returns error? { + json value = { + name: "Walter White", + subject: "Chemistry", + city: "Albequerque" + }; + string actual = prettify(value); + string expected = check getStringContentFromFile("map_with_multiple_string_fields.json"); + test:assertEquals(actual, expected); +} + +@test:Config { + groups: ["prettify", "map"] +} +function testMap() returns error? { + json value = { + person: { + name: "Walter White", + age: 51, + address: { + number: 308, + street: "Negra Arroyo Lane", + city: "Albequerque" + } + } + }; + string actual = prettify(value); + string expected = check getStringContentFromFile("map.json"); + test:assertEquals(actual, expected); +} + +@test:Config { + groups: ["prettify", "map"] +} +function testArrayOfMap() returns error? { + json value = [ + { + name: "Walter White", + age: 51, + address: { + number: 308, + street: "Negra Arroyo Lane", + city: "Albequerque" + } + }, + { + name: "Jesse Pinkman", + age: 26, + address: { + number: 9809, + street: "Margo Street", + city: "Albequerque" + } + } + ]; + string actual = prettify(value); + string expected = check getStringContentFromFile("array_of_map.json"); + test:assertEquals(actual, expected); +} + +@test:Config { + groups: ["prettify", "array", "map"] +} +function testComplexExample() returns error? { + json value = check getJsonContentFromFile("complex_example.json"); + string actual = prettify(value); + string expected = check getStringContentFromFile("complex_example.json"); + test:assertEquals(actual, expected); +} + +@test:Config { + groups: ["prettify", "array", "map"] +} +function testComplexExampleWithCustomIndentation() returns error? { + json value = check getJsonContentFromFile("complex_example.json"); + string actual = prettify(value, 2); + string expected = check getStringContentFromFile("complex_example_with_custom_indentation.json"); + test:assertEquals(actual, expected); +} + +@test:Config { + groups: ["prettify", "array", "map"] +} +function testComplexExampleWithCustomIndentationInvalidTest() returns error? { + json value = check getJsonContentFromFile("complex_example.json"); + string actual = prettify(value, 2); + string expected = check getStringContentFromFile("complex_example.json"); + test:assertNotEquals(actual, expected); +} + +@test:Config { + groups: ["prettify", "null"] +} +function testJsonValueWithNull() returns error? { + json value = { + name: "Walter White", + age: null, + address: { + number: 308, + street: (), + city: "Albequerque" + } + }; + string actual = prettify(value); + string expected = check getStringContentFromFile("json_value_with_null.json"); + test:assertEquals(actual, expected); +} diff --git a/ballerina/tests/resources/expected_results/array_of_empty_maps.json b/ballerina/tests/resources/expected_results/array_of_empty_maps.json new file mode 100644 index 0000000..46133e6 --- /dev/null +++ b/ballerina/tests/resources/expected_results/array_of_empty_maps.json @@ -0,0 +1,5 @@ +[ + {}, + {}, + {} +] diff --git a/ballerina/tests/resources/expected_results/array_of_map.json b/ballerina/tests/resources/expected_results/array_of_map.json new file mode 100644 index 0000000..6f8c3b8 --- /dev/null +++ b/ballerina/tests/resources/expected_results/array_of_map.json @@ -0,0 +1,20 @@ +[ + { + "name": "Walter White", + "age": 51, + "address": { + "number": 308, + "street": "Negra Arroyo Lane", + "city": "Albequerque" + } + }, + { + "name": "Jesse Pinkman", + "age": 26, + "address": { + "number": 9809, + "street": "Margo Street", + "city": "Albequerque" + } + } +] diff --git a/ballerina/tests/resources/expected_results/boolean_value.json b/ballerina/tests/resources/expected_results/boolean_value.json new file mode 100644 index 0000000..c508d53 --- /dev/null +++ b/ballerina/tests/resources/expected_results/boolean_value.json @@ -0,0 +1 @@ +false diff --git a/ballerina/tests/resources/expected_results/complex_example.json b/ballerina/tests/resources/expected_results/complex_example.json new file mode 100644 index 0000000..c8403ff --- /dev/null +++ b/ballerina/tests/resources/expected_results/complex_example.json @@ -0,0 +1,87 @@ +{ + "colors": [ + { + "color": "black", + "category": "hue", + "type": "primary", + "code": { + "rgba": [ + 255, + 255, + 255, + 1 + ], + "hex": "#000" + } + }, + { + "color": "white", + "category": "value", + "code": { + "rgba": [ + 0, + 0, + 0, + 1 + ], + "hex": "#FFF" + } + }, + { + "color": "red", + "category": "hue", + "type": "primary", + "code": { + "rgba": [ + 255, + 0, + 0, + 1 + ], + "hex": "#FF0" + } + }, + { + "color": "blue", + "category": "hue", + "type": "primary", + "code": { + "rgba": [ + 0, + 0, + 255, + 1 + ], + "hex": "#00F" + } + }, + { + "color": "yellow", + "category": "hue", + "type": "primary", + "code": { + "rgba": [ + 255, + 255, + 0, + 1 + ], + "hex": "#FF0" + } + }, + { + "color": "green", + "category": "hue", + "type": "secondary", + "code": { + "rgba": [ + 0, + 255, + 0, + 1 + ], + "hex": "#0F0" + } + } + ] +} diff --git a/ballerina/tests/resources/expected_results/complex_example_with_custom_indentation.json b/ballerina/tests/resources/expected_results/complex_example_with_custom_indentation.json new file mode 100644 index 0000000..ea29bd3 --- /dev/null +++ b/ballerina/tests/resources/expected_results/complex_example_with_custom_indentation.json @@ -0,0 +1,87 @@ +{ + "colors": [ + { + "color": "black", + "category": "hue", + "type": "primary", + "code": { + "rgba": [ + 255, + 255, + 255, + 1 + ], + "hex": "#000" + } + }, + { + "color": "white", + "category": "value", + "code": { + "rgba": [ + 0, + 0, + 0, + 1 + ], + "hex": "#FFF" + } + }, + { + "color": "red", + "category": "hue", + "type": "primary", + "code": { + "rgba": [ + 255, + 0, + 0, + 1 + ], + "hex": "#FF0" + } + }, + { + "color": "blue", + "category": "hue", + "type": "primary", + "code": { + "rgba": [ + 0, + 0, + 255, + 1 + ], + "hex": "#00F" + } + }, + { + "color": "yellow", + "category": "hue", + "type": "primary", + "code": { + "rgba": [ + 255, + 255, + 0, + 1 + ], + "hex": "#FF0" + } + }, + { + "color": "green", + "category": "hue", + "type": "secondary", + "code": { + "rgba": [ + 0, + 255, + 0, + 1 + ], + "hex": "#0F0" + } + } + ] +} diff --git a/ballerina/tests/resources/expected_results/empty_array.json b/ballerina/tests/resources/expected_results/empty_array.json new file mode 100644 index 0000000..fe51488 --- /dev/null +++ b/ballerina/tests/resources/expected_results/empty_array.json @@ -0,0 +1 @@ +[] diff --git a/ballerina/tests/resources/expected_results/empty_map.json b/ballerina/tests/resources/expected_results/empty_map.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/ballerina/tests/resources/expected_results/empty_map.json @@ -0,0 +1 @@ +{} diff --git a/ballerina/tests/resources/expected_results/int_value.json b/ballerina/tests/resources/expected_results/int_value.json new file mode 100644 index 0000000..3cda32f --- /dev/null +++ b/ballerina/tests/resources/expected_results/int_value.json @@ -0,0 +1 @@ +515 diff --git a/ballerina/tests/resources/expected_results/json_value_with_null.json b/ballerina/tests/resources/expected_results/json_value_with_null.json new file mode 100644 index 0000000..d853a0a --- /dev/null +++ b/ballerina/tests/resources/expected_results/json_value_with_null.json @@ -0,0 +1,9 @@ +{ + "name": "Walter White", + "age": null, + "address": { + "number": 308, + "street": null, + "city": "Albequerque" + } +} diff --git a/ballerina/tests/resources/expected_results/map.json b/ballerina/tests/resources/expected_results/map.json new file mode 100644 index 0000000..670754a --- /dev/null +++ b/ballerina/tests/resources/expected_results/map.json @@ -0,0 +1,11 @@ +{ + "person": { + "name": "Walter White", + "age": 51, + "address": { + "number": 308, + "street": "Negra Arroyo Lane", + "city": "Albequerque" + } + } +} diff --git a/ballerina/tests/resources/expected_results/map_with_multiple_string_fields.json b/ballerina/tests/resources/expected_results/map_with_multiple_string_fields.json new file mode 100644 index 0000000..579d119 --- /dev/null +++ b/ballerina/tests/resources/expected_results/map_with_multiple_string_fields.json @@ -0,0 +1,5 @@ +{ + "name": "Walter White", + "subject": "Chemistry", + "city": "Albequerque" +} diff --git a/ballerina/tests/resources/expected_results/map_with_string_field.json b/ballerina/tests/resources/expected_results/map_with_string_field.json new file mode 100644 index 0000000..9363606 --- /dev/null +++ b/ballerina/tests/resources/expected_results/map_with_string_field.json @@ -0,0 +1,3 @@ +{ + "name": "Walter White" +} diff --git a/ballerina/tests/resources/expected_results/string_array.json b/ballerina/tests/resources/expected_results/string_array.json new file mode 100644 index 0000000..3e5eed7 --- /dev/null +++ b/ballerina/tests/resources/expected_results/string_array.json @@ -0,0 +1,5 @@ +[ + "sam", + "bam", + "tan" +] diff --git a/ballerina/tests/resources/expected_results/string_value.json b/ballerina/tests/resources/expected_results/string_value.json new file mode 100644 index 0000000..6b5dec0 --- /dev/null +++ b/ballerina/tests/resources/expected_results/string_value.json @@ -0,0 +1 @@ +"Sam" diff --git a/ballerina/tests/resources/input_files/complex_example.json b/ballerina/tests/resources/input_files/complex_example.json new file mode 100644 index 0000000..c8403ff --- /dev/null +++ b/ballerina/tests/resources/input_files/complex_example.json @@ -0,0 +1,87 @@ +{ + "colors": [ + { + "color": "black", + "category": "hue", + "type": "primary", + "code": { + "rgba": [ + 255, + 255, + 255, + 1 + ], + "hex": "#000" + } + }, + { + "color": "white", + "category": "value", + "code": { + "rgba": [ + 0, + 0, + 0, + 1 + ], + "hex": "#FFF" + } + }, + { + "color": "red", + "category": "hue", + "type": "primary", + "code": { + "rgba": [ + 255, + 0, + 0, + 1 + ], + "hex": "#FF0" + } + }, + { + "color": "blue", + "category": "hue", + "type": "primary", + "code": { + "rgba": [ + 0, + 0, + 255, + 1 + ], + "hex": "#00F" + } + }, + { + "color": "yellow", + "category": "hue", + "type": "primary", + "code": { + "rgba": [ + 255, + 255, + 0, + 1 + ], + "hex": "#FF0" + } + }, + { + "color": "green", + "category": "hue", + "type": "secondary", + "code": { + "rgba": [ + 0, + 255, + 0, + 1 + ], + "hex": "#0F0" + } + } + ] +} diff --git a/ballerina/tests/utils.bal b/ballerina/tests/utils.bal new file mode 100644 index 0000000..b0e9d58 --- /dev/null +++ b/ballerina/tests/utils.bal @@ -0,0 +1,28 @@ +// Copyright (c) 2024, WSO2 LLC. (https://www.wso2.com). +// +// WSO2 LLC. licenses this file to you under the Apache License, +// Version 2.0 (the "License"); you may not use this file except +// in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +import ballerina/file; +import ballerina/io; + +isolated function getStringContentFromFile(string fileName) returns string|error { + string path = check file:joinPath("tests", "resources", "expected_results", fileName); + return io:fileReadString(path); +} + +isolated function getJsonContentFromFile(string fileName) returns json|error { + string path = check file:joinPath("tests", "resources", "input_files", fileName); + return io:fileReadJson(path); +} diff --git a/ballerina/utils.bal b/ballerina/utils.bal new file mode 100644 index 0000000..f036785 --- /dev/null +++ b/ballerina/utils.bal @@ -0,0 +1,108 @@ +// Copyright (c) 2024, WSO2 LLC. (https://www.wso2.com). +// +// WSO2 LLC. licenses this file to you under the Apache License, +// Version 2.0 (the "License"); you may not use this file except +// in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +isolated function prettifyJson(json value, string indentation, int level, boolean isMapField = false) returns string { + if value == () { + return " null"; + } else if value is map { + return prettifyJsonMap(value, indentation, level, isMapField); + } else if value is json[] { + return prettifyJsonArray(value, indentation, level, isMapField); + } else { + return prettifyJsonField(value, indentation, level, isMapField); + } +} + +isolated function getIndentation(int indentation) returns string { + string result = ""; + foreach int i in 0 ..< indentation { + result += " "; + } + return result; +} + +isolated function getIndentationForLevel(string indentation, int level) returns string { + string result = ""; + foreach int i in 0 ..< level { + result += indentation; + } + return result; +} + +isolated function getInitialIndentation(string indentation, int level, boolean isMapField) returns string { + if isMapField { + return " "; + } + return getIndentationForLevel(indentation, level); +} + +isolated function prettifyJsonMap(map value, string indentation, int level, boolean isMapField) returns string { + string initialIndentation = getInitialIndentation(indentation, level, isMapField); + string result = string `${initialIndentation}{`; + boolean isEmptyMap = value.keys().length() == 0; + if !isEmptyMap { + result += "\n"; + } + + int fieldLevel = level + 1; + string fieldIndentation = getIndentationForLevel(indentation, fieldLevel); + int length = value.length(); + int i = 1; + foreach string key in value.keys() { + string fieldValue = prettifyJson(value.get(key), indentation, fieldLevel, true); + string line = string `${fieldIndentation}"${key}":${fieldValue}`; + result += line; + if i != length { + result += ","; + } + result += "\n"; + i += 1; + } + + if !isEmptyMap { + result += getIndentationForLevel(indentation, level); + } + result += "}"; + return result; +} + +isolated function prettifyJsonArray(json[] array, string indentation, int level, boolean isMapField) returns string { + string initialIndentation = getInitialIndentation(indentation, level, isMapField); + string result = string `${initialIndentation}[`; + + boolean isEmptyArray = array.length() == 0; + if !isEmptyArray { + result += "\n"; + } + + int elementLevel = level + 1; + string[] elements = []; + foreach json value in array { + elements.push(prettifyJson(value, indentation, elementLevel)); + } + string separator = ",\n"; + result += 'string:'join(separator, ...elements); + + if !isEmptyArray { + result += "\n" + getIndentationForLevel(indentation, level); + } + return string `${result}]`; +} + +isolated function prettifyJsonField(json value, string indentation, int level, boolean isMapField) returns string { + string initialIndentation = getInitialIndentation(indentation, level, isMapField); + return string `${initialIndentation}${value.toJsonString()}`; +}