diff --git a/clients/algoliasearch-client-python/algoliasearch/http/api_response.py b/clients/algoliasearch-client-python/algoliasearch/http/api_response.py index aa7c7de7bb2..ed19413aa69 100644 --- a/clients/algoliasearch-client-python/algoliasearch/http/api_response.py +++ b/clients/algoliasearch-client-python/algoliasearch/http/api_response.py @@ -65,9 +65,14 @@ def deserialize(klass: Any = None, data: Any = None) -> Any: if hasattr(klass, "__origin__") and klass.__origin__ is list: sub_kls = klass.__args__[0] - arr = json.loads(data) + arr = json.loads(data) if isinstance(data, str) else data return [ApiResponse.deserialize(sub_kls, sub_data) for sub_data in arr] + if hasattr(klass, "__origin__") and klass.__origin__ is dict: + sub_kls = klass.__args__[1] + obj = json.loads(data) if isinstance(data, str) else data + return {k: ApiResponse.deserialize(sub_kls, v) for k, v in obj.items()} + if isinstance(klass, str): if klass.startswith("List["): sub_kls = match(r"List\[(.*)]", klass) diff --git a/clients/algoliasearch-client-scala/src/main/scala/algoliasearch/config/HttpRequest.scala b/clients/algoliasearch-client-scala/src/main/scala/algoliasearch/config/HttpRequest.scala index 7236f1179aa..93e4c045368 100644 --- a/clients/algoliasearch-client-scala/src/main/scala/algoliasearch/config/HttpRequest.scala +++ b/clients/algoliasearch-client-scala/src/main/scala/algoliasearch/config/HttpRequest.scala @@ -100,6 +100,13 @@ object HttpRequest { this } + def withHeader(key: String, value: Option[Any]): HttpRequest.Builder = { + value match { + case Some(param) => withHeader(key, param) + case None => this + } + } + def withHeaders(headers: Map[String, Any]): HttpRequest.Builder = { for ((key, value) <- headers) withHeader(key, value) diff --git a/config/generation.config.mjs b/config/generation.config.mjs index c2f950db91e..beee2e7c9f0 100644 --- a/config/generation.config.mjs +++ b/config/generation.config.mjs @@ -28,6 +28,7 @@ export const patterns = [ 'tests/output/csharp/src/Algolia.Search.Tests.csproj', '!tests/output/csharp/src/TimeoutIntegrationTests.cs', + '!tests/output/csharp/src/Utils/**', // Dart '!clients/algoliasearch-client-dart/**', @@ -64,6 +65,7 @@ export const patterns = [ 'tests/output/java/build.gradle', '!tests/output/java/src/test/java/com/algolia/manual/**', + '!tests/output/java/src/test/java/com/algolia/utils/**', // JavaScript '!clients/algoliasearch-client-javascript/*', @@ -92,6 +94,8 @@ export const patterns = [ 'clients/algoliasearch-client-kotlin/client/src/commonMain/kotlin/com/algolia/client/api/**', 'clients/algoliasearch-client-kotlin/client/src/commonMain/kotlin/com/algolia/client/model/**', + '!tests/output/kotlin/src/commonTest/kotlin/com/algolia/utils/TestHelpers.kt', + // PHP '!clients/algoliasearch-client-php/**', 'clients/algoliasearch-client-php/lib/Api/*', diff --git a/templates/csharp/tests/e2e/e2e.mustache b/templates/csharp/tests/e2e/e2e.mustache index 1b443e5a7ca..f1638ddd362 100644 --- a/templates/csharp/tests/e2e/e2e.mustache +++ b/templates/csharp/tests/e2e/e2e.mustache @@ -6,7 +6,6 @@ using Algolia.Search.Serializer; using Algolia.Search.Tests.Utils; using Xunit; using System.Text.Json; -using Quibble.Xunit; using dotenv.net; {{#isSearchClient}} using Action = Algolia.Search.Models.Search.Action; @@ -50,6 +49,7 @@ public class {{client}}RequestTestsE2E } + {{#blocksE2E}} {{#tests}} [Fact(DisplayName = "{{{testName}}}")] @@ -64,7 +64,7 @@ public class {{client}}RequestTestsE2E {{/statusCode}} {{#body}} - JsonAssert.EqualOverrideDefault("{{#lambda.escapeQuotes}}{{{.}}}{{/lambda.escapeQuotes}}", JsonSerializer.Serialize(resp, JsonConfig.Options), new JsonDiffConfig(true)); + TestHelpers.LenientJsonAssert("{{#lambda.escapeQuotes}}{{{.}}}{{/lambda.escapeQuotes}}", JsonSerializer.Serialize(resp, JsonConfig.Options)); {{/body}} } catch (Exception e) { diff --git a/templates/go/api.mustache b/templates/go/api.mustache index 6c6371a4cb2..4fe7dd0259c 100644 --- a/templates/go/api.mustache +++ b/templates/go/api.mustache @@ -386,11 +386,11 @@ func (c *APIClient) NewApi{{{nickname}}}Request({{#requiredParams}} {{paramName} {{#allParams}} {{^required}} -// With{{#lambda.titlecase}}{{baseName}}{{/lambda.titlecase}} adds the {{paramName}} to the {{#structPrefix}}{{&classname}}{{/structPrefix}}{{^structPrefix}}Api{{/structPrefix}}{{operationId}}Request and returns the request for chaining. +// With{{#nameInPascalCase}}{{nameInPascalCase}}{{/nameInPascalCase}}{{^nameInPascalCase}}{{#lambda.titlecase}}{{baseName}}{{/lambda.titlecase}}{{/nameInPascalCase}} adds the {{paramName}} to the {{#structPrefix}}{{&classname}}{{/structPrefix}}{{^structPrefix}}Api{{/structPrefix}}{{operationId}}Request and returns the request for chaining. {{#isDeprecated}} // Deprecated {{/isDeprecated}} -func (r {{#structPrefix}}{{&classname}}{{/structPrefix}}{{^structPrefix}}Api{{/structPrefix}}{{operationId}}Request) With{{#lambda.titlecase}}{{baseName}}{{/lambda.titlecase}}({{paramName}} {{^isFreeFormObject}}{{^isArray}}{{^isMap}}{{^isPrimitiveType}}{{^isEnumRef}}*{{/isEnumRef}}{{/isPrimitiveType}}{{/isMap}}{{/isArray}}{{/isFreeFormObject}}{{{dataType}}}) {{#structPrefix}}{{&classname}}{{/structPrefix}}{{^structPrefix}}Api{{/structPrefix}}{{operationId}}Request { +func (r {{#structPrefix}}{{&classname}}{{/structPrefix}}{{^structPrefix}}Api{{/structPrefix}}{{operationId}}Request) With{{#nameInPascalCase}}{{nameInPascalCase}}{{/nameInPascalCase}}{{^nameInPascalCase}}{{#lambda.titlecase}}{{baseName}}{{/lambda.titlecase}}{{/nameInPascalCase}}({{paramName}} {{^isFreeFormObject}}{{^isArray}}{{^isMap}}{{^isPrimitiveType}}{{^isEnumRef}}*{{/isEnumRef}}{{/isPrimitiveType}}{{/isMap}}{{/isArray}}{{/isFreeFormObject}}{{{dataType}}}) {{#structPrefix}}{{&classname}}{{/structPrefix}}{{^structPrefix}}Api{{/structPrefix}}{{operationId}}Request { r.{{paramName}} = {{#isPrimitiveType}}{{^isMap}}&{{/isMap}}{{/isPrimitiveType}}{{paramName}} return r } diff --git a/templates/go/search_helpers.mustache b/templates/go/search_helpers.mustache index a297bec029f..67b2618cee3 100644 --- a/templates/go/search_helpers.mustache +++ b/templates/go/search_helpers.mustache @@ -802,7 +802,13 @@ func (c *APIClient) SaveObjectsWithTransformation(indexName string, objects []ma return nil, reportError("`region` must be provided at client instantiation before calling this method.") } - return c.ingestionTransporter.ChunkedPush(indexName, objects, ingestion.Action(ACTION_ADD_OBJECT), nil, toIngestionChunkedBatchOptions(opts)...) //nolint:wrapcheck + //nolint:wrapcheck + return c.ingestionTransporter.ChunkedPush( + indexName, + objects, + ingestion.Action(ACTION_ADD_OBJECT), + nil, + toIngestionChunkedBatchOptions(opts)...) } /* @@ -836,5 +842,11 @@ func (c *APIClient) PartialUpdateObjectsWithTransformation(indexName string, obj action = ACTION_PARTIAL_UPDATE_OBJECT_NO_CREATE } - return c.ingestionTransporter.ChunkedPush(indexName, objects, ingestion.Action(action), nil, toIngestionChunkedBatchOptions(partialUpdateObjectsToChunkedBatchOptions(opts))...) //nolint:wrapcheck + //nolint:wrapcheck + return c.ingestionTransporter.ChunkedPush( + indexName, + objects, + ingestion.Action(action), + nil, + toIngestionChunkedBatchOptions(partialUpdateObjectsToChunkedBatchOptions(opts))...) } \ No newline at end of file diff --git a/templates/go/tests/method.mustache b/templates/go/tests/method.mustache index d2819805aef..2231a58f529 100644 --- a/templates/go/tests/method.mustache +++ b/templates/go/tests/method.mustache @@ -1,2 +1,2 @@ client.{{#lambda.titlecase}}{{method}}{{/lambda.titlecase}}({{#hasParams}}{{^isHelper}}client.NewApi{{#lambda.titlecase}}{{method}}{{/lambda.titlecase}}Request({{/isHelper}} - {{#parametersWithDataType}}{{#required}}{{> tests/generateParams}},{{/required}}{{/parametersWithDataType}} {{^isHelper}}){{#parametersWithDataType}}{{^required}}.With{{#lambda.pascalcase}}{{{key}}}{{/lambda.pascalcase}}({{> tests/generateParams}}){{#-last}},{{/-last}}{{/required}}{{/parametersWithDataType}}{{/isHelper}}{{#isHelper}}{{#parametersWithDataType}}{{^required}}{{> tests/generateParams}},{{/required}}{{/parametersWithDataType}}{{/isHelper}}{{/hasParams}}{{#requestOptions}}{{#queryParameters.parametersWithDataType}}{{clientPrefix}}.WithQueryParam("{{{key}}}", {{> tests/generateInnerParams}}),{{/queryParameters.parametersWithDataType}}{{#headers.parametersWithDataType}}{{clientPrefix}}.WithHeaderParam("{{{key}}}", {{> tests/generateInnerParams}}),{{/headers.parametersWithDataType}} {{#timeouts.read}} ,{{clientPrefix}}.WithReadTimeout({{.}} * time.Millisecond), {{/timeouts.read}} {{#timeouts.write}} ,{{clientPrefix}}.WithWriteTimeout({{.}} * time.Millisecond), {{/timeouts.write}} {{#timeouts.connect}} ,{{clientPrefix}}.WithConnectTimeout({{.}} * time.Millisecond), {{/timeouts.connect}} {{/requestOptions}}) \ No newline at end of file + {{#parametersWithDataType}}{{#required}}{{> tests/generateParams}},{{/required}}{{/parametersWithDataType}} {{^isHelper}}){{#parametersWithDataType}}{{^required}}{{^isNull}}.With{{#lambda.pascalcase}}{{{key}}}{{/lambda.pascalcase}}({{> tests/generateParams}}){{/isNull}}{{#-last}},{{/-last}}{{/required}}{{/parametersWithDataType}}{{/isHelper}}{{#isHelper}}{{#parametersWithDataType}}{{^required}}{{> tests/generateParams}},{{/required}}{{/parametersWithDataType}}{{/isHelper}}{{/hasParams}}{{#requestOptions}}{{#queryParameters.parametersWithDataType}}{{clientPrefix}}.WithQueryParam("{{{key}}}", {{> tests/generateInnerParams}}),{{/queryParameters.parametersWithDataType}}{{#headers.parametersWithDataType}}{{clientPrefix}}.WithHeaderParam("{{{key}}}", {{> tests/generateInnerParams}}),{{/headers.parametersWithDataType}} {{#timeouts.read}} ,{{clientPrefix}}.WithReadTimeout({{.}} * time.Millisecond), {{/timeouts.read}} {{#timeouts.write}} ,{{clientPrefix}}.WithWriteTimeout({{.}} * time.Millisecond), {{/timeouts.write}} {{#timeouts.connect}} ,{{clientPrefix}}.WithConnectTimeout({{.}} * time.Millisecond), {{/timeouts.connect}} {{/requestOptions}}) \ No newline at end of file diff --git a/templates/java/tests/e2e/e2e.mustache b/templates/java/tests/e2e/e2e.mustache index 3ef00c05013..196647c9e12 100644 --- a/templates/java/tests/e2e/e2e.mustache +++ b/templates/java/tests/e2e/e2e.mustache @@ -12,8 +12,7 @@ import io.github.cdimascio.dotenv.Dotenv; import java.util.*; import java.time.Duration; import org.junit.jupiter.api.*; -import org.skyscreamer.jsonassert.JSONAssert; -import org.skyscreamer.jsonassert.JSONCompareMode; +import com.algolia.utils.TestHelpers; @TestInstance(TestInstance.Lifecycle.PER_CLASS) class {{client}}RequestsTestsE2E { @@ -45,7 +44,7 @@ class {{client}}RequestsTestsE2E { {{returnType}} res = {{> tests/method}}; {{#response}} {{#body}} - assertDoesNotThrow(() -> JSONAssert.assertEquals("{{#lambda.escapeQuotes}}{{{body}}}{{/lambda.escapeQuotes}}", json.writeValueAsString(res), JSONCompareMode.LENIENT)); + assertDoesNotThrow(() -> TestHelpers.lenientJsonAssert("{{#lambda.escapeQuotes}}{{{body}}}{{/lambda.escapeQuotes}}", json.writeValueAsString(res))); {{/body}} {{/response}} } diff --git a/templates/kotlin/builder_function.mustache b/templates/kotlin/builder_function.mustache index 67056984cd9..c5edebc751f 100644 --- a/templates/kotlin/builder_function.mustache +++ b/templates/kotlin/builder_function.mustache @@ -8,7 +8,7 @@ */ public fun {{vendorExtensions.x-one-of-explicit-name}}( {{#vars}} - {{{name}}}: {{> data_class_field_type}}{{^required}}? = null{{/required}}, + {{{name}}}: {{> data_class_field_type}}{{#required}}{{#isNullable}}? = null{{/isNullable}}{{/required}}{{^required}}? = null{{/required}}, {{/vars}} ): {{classname}} = {{vendorExtensions.x-fully-qualified-classname}} ( {{#vars}} diff --git a/templates/kotlin/data_class_field.mustache b/templates/kotlin/data_class_field.mustache index 5a4a65f2f6a..70336d084fc 100644 --- a/templates/kotlin/data_class_field.mustache +++ b/templates/kotlin/data_class_field.mustache @@ -4,4 +4,4 @@ {{#deprecated}} @Deprecated(message = "This property is deprecated.") {{/deprecated}} - @SerialName(value = "{{{vendorExtensions.x-base-name-literal}}}") {{#isInherited}}override {{/isInherited}}val {{{name}}}: {{> data_class_field_type}}{{^required}}? = null{{/required}} + @SerialName(value = "{{{vendorExtensions.x-base-name-literal}}}") {{#isInherited}}override {{/isInherited}}val {{{name}}}: {{> data_class_field_type}}{{#required}}{{#isNullable}}? = null{{/isNullable}}{{/required}}{{^required}}? = null{{/required}} diff --git a/templates/kotlin/json_object_field.mustache b/templates/kotlin/json_object_field.mustache index fcced575c14..99d08309c14 100644 --- a/templates/kotlin/json_object_field.mustache +++ b/templates/kotlin/json_object_field.mustache @@ -4,4 +4,4 @@ {{#deprecated}} @Deprecated(message = "This property is deprecated.") {{/deprecated}} - {{#isInherited}}override {{/isInherited}}val {{{name}}}: {{> data_class_field_type}}{{^required}}? = null{{/required}} + {{#isInherited}}override {{/isInherited}}val {{{name}}}: {{> data_class_field_type}}{{#required}}{{#isNullable}}? = null{{/isNullable}}{{/required}}{{^required}}? = null{{/required}} diff --git a/templates/kotlin/tests/e2e/e2e.mustache b/templates/kotlin/tests/e2e/e2e.mustache index 1d02f27f596..1d95fbf0ac2 100644 --- a/templates/kotlin/tests/e2e/e2e.mustache +++ b/templates/kotlin/tests/e2e/e2e.mustache @@ -6,8 +6,6 @@ import com.algolia.client.model.{{import}}.* import com.algolia.client.configuration.* import com.algolia.client.transport.* import com.algolia.utils.* -import org.skyscreamer.jsonassert.JSONAssert; -import org.skyscreamer.jsonassert.JSONCompareMode; import io.ktor.http.* import io.github.cdimascio.dotenv.Dotenv import kotlinx.coroutines.test.* @@ -44,7 +42,7 @@ class {{clientPrefix}}Test { {{#response}} {{#body}} response = { - JSONAssert.assertEquals("{{#lambda.escapeQuotes}}{{{body}}}{{/lambda.escapeQuotes}}", Json.encodeToString(it), JSONCompareMode.LENIENT) + lenientJsonAssert("{{#lambda.escapeQuotes}}{{{body}}}{{/lambda.escapeQuotes}}", Json.encodeToString(it)) }, {{/body}} {{/response}} diff --git a/templates/kotlin/tests/request_param.mustache b/templates/kotlin/tests/request_param.mustache index 0146bf2f787..de0f8367fa4 100644 --- a/templates/kotlin/tests/request_param.mustache +++ b/templates/kotlin/tests/request_param.mustache @@ -1 +1 @@ -{{^hasAdditionalProperties}}{{#lambda.camelcase}}{{{key}}}{{/lambda.camelcase}} = {{> tests/param_value}},{{/hasAdditionalProperties}}{{#hasAdditionalProperties}}{{> tests/param_value}},{{/hasAdditionalProperties}} \ No newline at end of file +{{^isNull}}{{^hasAdditionalProperties}}{{#lambda.camelcase}}{{{key}}}{{/lambda.camelcase}} = {{> tests/param_value}},{{/hasAdditionalProperties}}{{#hasAdditionalProperties}}{{> tests/param_value}},{{/hasAdditionalProperties}}{{/isNull}}{{#isNull}}{{#required}}{{^hasAdditionalProperties}}{{#lambda.camelcase}}{{{key}}}{{/lambda.camelcase}} = {{> tests/param_value}},{{/hasAdditionalProperties}}{{#hasAdditionalProperties}}{{> tests/param_value}},{{/hasAdditionalProperties}}{{/required}}{{/isNull}} \ No newline at end of file diff --git a/templates/scala/tests/request_param.mustache b/templates/scala/tests/request_param.mustache index 0c5bacece1f..31030d474f4 100644 --- a/templates/scala/tests/request_param.mustache +++ b/templates/scala/tests/request_param.mustache @@ -1 +1 @@ -{{^hasAdditionalProperties}}{{#lambda.identifier}}{{key}}{{/lambda.identifier}} = {{> tests/param_optional}}{{/hasAdditionalProperties}}{{#hasAdditionalProperties}}{{> tests/param_value}}{{/hasAdditionalProperties}} \ No newline at end of file +{{^hasAdditionalProperties}}{{#isNull}}{{^required}}{{#lambda.identifier}}{{key}}{{/lambda.identifier}} = None{{/required}}{{#required}}{{#lambda.identifier}}{{key}}{{/lambda.identifier}} = {{> tests/param_optional}}{{/required}}{{/isNull}}{{^isNull}}{{#lambda.identifier}}{{key}}{{/lambda.identifier}} = {{> tests/param_optional}}{{/isNull}}{{/hasAdditionalProperties}}{{#hasAdditionalProperties}}{{> tests/param_value}}{{/hasAdditionalProperties}} \ No newline at end of file diff --git a/templates/swift/tests/paramValue.mustache b/templates/swift/tests/paramValue.mustache index 90462b8d176..27fe45cdaa4 100644 --- a/templates/swift/tests/paramValue.mustache +++ b/templates/swift/tests/paramValue.mustache @@ -1 +1 @@ -{{#isVerbatim}}{{{value}}}{{/isVerbatim}}{{#isObject}}{{objectName}}({{^hasAdditionalProperties}}{{#value}}{{> tests/generateParams}}{{^-last}}, {{/-last}}{{/value}}{{/hasAdditionalProperties}}{{#hasAdditionalProperties}}from: [{{#value}}"{{key}}": AnyCodable({{> tests/paramValue }}){{^-last}}, {{/-last}}{{/value}}]{{/hasAdditionalProperties}}){{/isObject}}{{#isString}}{{#isAnyType}}AnyCodable({{/isAnyType}}"{{#lambda.escapeQuotes}}{{{value}}}{{/lambda.escapeQuotes}}"{{#isAnyType}}){{/isAnyType}}{{/isString}}{{#isNumber}}{{#isLong}}Int64({{/isLong}}{{{value}}}{{#isLong}}){{/isLong}}{{/isNumber}}{{#isBoolean}}{{{value}}}{{/isBoolean}}{{#isEnum}}{{objectName}}.{{#lambda.identifier}}{{#lambda.camelcase}}{{value}}{{/lambda.camelcase}}{{/lambda.identifier}}{{/isEnum}}{{#isArray}}[{{#value}}{{> tests/generateParams}}{{^-last}}, {{/-last}}{{/value}}]{{/isArray}}{{#isMap}}Map[{{{.}}}]{{/isMap}}{{#isFreeFormObject}}{{#isAnyType}}[{{#value}}{{#entrySet}}"{{key}}": "{{{value}}}"{{/entrySet}}{{/value}}]{{/isAnyType}}{{^isAnyType}}{{^value}}[String: AnyCodable](){{/value}}{{#value}}{{#-first}}[{{/-first}}{{> tests/generateParams}}{{^-last}}, {{/-last}}{{#-last}}]{{/-last}}{{/value}}{{/isAnyType}}{{/isFreeFormObject}}{{#isNull}}{{#inClientTest}}TestNull{{{objectName}}}(){{/inClientTest}}{{/isNull}} \ No newline at end of file +{{#isVerbatim}}{{{value}}}{{/isVerbatim}}{{#isObject}}{{objectName}}({{^hasAdditionalProperties}}{{#value}}{{> tests/generateParams}}{{^-last}}, {{/-last}}{{/value}}{{/hasAdditionalProperties}}{{#hasAdditionalProperties}}from: [{{#value}}"{{key}}": AnyCodable({{> tests/paramValue }}){{^-last}}, {{/-last}}{{/value}}]{{/hasAdditionalProperties}}){{/isObject}}{{#isString}}{{#isAnyType}}AnyCodable({{/isAnyType}}"{{#lambda.escapeQuotes}}{{{value}}}{{/lambda.escapeQuotes}}"{{#isAnyType}}){{/isAnyType}}{{/isString}}{{#isNumber}}{{#isLong}}Int64({{/isLong}}{{{value}}}{{#isLong}}){{/isLong}}{{/isNumber}}{{#isBoolean}}{{{value}}}{{/isBoolean}}{{#isEnum}}{{objectName}}.{{#lambda.identifier}}{{#lambda.camelcase}}{{value}}{{/lambda.camelcase}}{{/lambda.identifier}}{{/isEnum}}{{#isArray}}[{{#value}}{{> tests/generateParams}}{{^-last}}, {{/-last}}{{/value}}]{{/isArray}}{{#isMap}}Map[{{{.}}}]{{/isMap}}{{#isFreeFormObject}}{{#isAnyType}}[{{#value}}{{#entrySet}}"{{key}}": "{{{value}}}"{{/entrySet}}{{/value}}]{{/isAnyType}}{{^isAnyType}}{{^value}}[String: AnyCodable](){{/value}}{{#value}}{{#-first}}[{{/-first}}{{> tests/generateParams}}{{^-last}}, {{/-last}}{{#-last}}]{{/-last}}{{/value}}{{/isAnyType}}{{/isFreeFormObject}}{{#isNull}}{{#inClientTest}}TestNull{{{objectName}}}(){{/inClientTest}}{{^inClientTest}}nil{{/inClientTest}}{{/isNull}} \ No newline at end of file diff --git a/tests/output/csharp/src/Utils/TestHelpers.cs b/tests/output/csharp/src/Utils/TestHelpers.cs new file mode 100644 index 00000000000..7264da6a005 --- /dev/null +++ b/tests/output/csharp/src/Utils/TestHelpers.cs @@ -0,0 +1,54 @@ +using System.Text.Json; +using System.Text.Json.Nodes; +using Quibble.Xunit; + +namespace Algolia.Search.Tests.Utils; + +public static class TestHelpers +{ + /// + /// Asserts that the serialized response contains at least the expected JSON structure. + /// Extra keys in objects and extra elements in arrays (beyond expected indices) are ignored. + /// Mirrors the union-based e2e assertion used by JS, Python, Ruby, PHP, Go, and Swift clients. + /// + public static void LenientJsonAssert(string expected, string actual) + { + var expectedNode = JsonNode.Parse(expected); + var actualNode = JsonNode.Parse(actual); + var unionNode = Union(expectedNode, actualNode); + var unionJson = JsonSerializer.Serialize(unionNode); + JsonAssert.EqualOverrideDefault(expected, unionJson, new JsonDiffConfig(true)); + } + + /// + /// Recursively intersects the structure of with the values of + /// . Only keys/indices present in expected are kept. + /// + private static JsonNode Union(JsonNode expected, JsonNode received) + { + if (expected is JsonObject expectedObj && received is JsonObject receivedObj) + { + var result = new JsonObject(); + foreach (var prop in expectedObj) + { + if (receivedObj[prop.Key] != null) + { + result[prop.Key] = Union(prop.Value, receivedObj[prop.Key]); + } + } + return result; + } + + if (expected is JsonArray expectedArr && received is JsonArray receivedArr) + { + var result = new JsonArray(); + for (int i = 0; i < expectedArr.Count && i < receivedArr.Count; i++) + { + result.Add(Union(expectedArr[i], receivedArr[i])); + } + return result; + } + + return received.DeepClone(); + } +} diff --git a/tests/output/java/src/test/java/com/algolia/utils/TestHelpers.java b/tests/output/java/src/test/java/com/algolia/utils/TestHelpers.java new file mode 100644 index 00000000000..9b214fde036 --- /dev/null +++ b/tests/output/java/src/test/java/com/algolia/utils/TestHelpers.java @@ -0,0 +1,54 @@ +package com.algolia.utils; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import org.skyscreamer.jsonassert.JSONAssert; +import org.skyscreamer.jsonassert.JSONCompareMode; + +public class TestHelpers { + + private static final ObjectMapper mapper = new ObjectMapper(); + + /** + * Asserts that the serialized response contains at least the expected JSON structure. Extra keys + * in objects and extra elements in arrays (beyond expected indices) are ignored. Mirrors the + * union-based e2e assertion used by JS, Python, Ruby, PHP, Go, Swift, and C# clients. + */ + public static void lenientJsonAssert(String expected, String actual) throws Exception { + JsonNode expectedNode = mapper.readTree(expected); + JsonNode actualNode = mapper.readTree(actual); + JsonNode unionNode = union(expectedNode, actualNode); + String unionJson = mapper.writeValueAsString(unionNode); + JSONAssert.assertEquals(expected, unionJson, JSONCompareMode.LENIENT); + } + + /** + * Recursively intersects the structure of {@code expected} with the values of {@code received}. + * Only keys/indices present in expected are kept. + */ + private static JsonNode union(JsonNode expected, JsonNode received) { + if (expected.isObject() && received.isObject()) { + ObjectNode result = mapper.createObjectNode(); + expected + .fieldNames() + .forEachRemaining(key -> { + if (received.has(key)) { + result.set(key, union(expected.get(key), received.get(key))); + } + }); + return result; + } + + if (expected.isArray() && received.isArray()) { + ArrayNode result = mapper.createArrayNode(); + for (int i = 0; i < expected.size() && i < received.size(); i++) { + result.add(union(expected.get(i), received.get(i))); + } + return result; + } + + return received.deepCopy(); + } +} diff --git a/tests/output/kotlin/src/commonTest/kotlin/com/algolia/utils/TestHelpers.kt b/tests/output/kotlin/src/commonTest/kotlin/com/algolia/utils/TestHelpers.kt new file mode 100644 index 00000000000..47ddbf160bf --- /dev/null +++ b/tests/output/kotlin/src/commonTest/kotlin/com/algolia/utils/TestHelpers.kt @@ -0,0 +1,46 @@ +package com.algolia.utils + +import kotlinx.serialization.json.* +import org.skyscreamer.jsonassert.JSONAssert +import org.skyscreamer.jsonassert.JSONCompareMode + +/** + * Asserts that the serialized response contains at least the expected JSON structure. Extra keys in + * objects and extra elements in arrays (beyond expected indices) are ignored. Mirrors the + * union-based e2e assertion used by JS, Python, Ruby, PHP, Go, Swift, and C# clients. + */ +public fun lenientJsonAssert(expected: String, actual: String) { + val expectedNode = Json.parseToJsonElement(expected) + val actualNode = Json.parseToJsonElement(actual) + val unionNode = union(expectedNode, actualNode) + val unionJson = Json.encodeToString(JsonElement.serializer(), unionNode) + JSONAssert.assertEquals(expected, unionJson, JSONCompareMode.LENIENT) +} + +/** + * Recursively intersects the structure of [expected] with the values of [received]. Only + * keys/indices present in expected are kept. + */ +private fun union(expected: JsonElement, received: JsonElement): JsonElement { + if (expected is JsonObject && received is JsonObject) { + return buildJsonObject { + for ((key, value) in expected) { + if (key in received) { + put(key, union(value, received[key]!!)) + } + } + } + } + + if (expected is JsonArray && received is JsonArray) { + return buildJsonArray { + for (i in expected.indices) { + if (i < received.size) { + add(union(expected[i], received[i])) + } + } + } + } + + return received +}