Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
4 changes: 4 additions & 0 deletions config/generation.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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/**',
Expand Down Expand Up @@ -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/*',
Expand Down Expand Up @@ -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/*',
Expand Down
4 changes: 2 additions & 2 deletions templates/csharp/tests/e2e/e2e.mustache
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -50,6 +49,7 @@ public class {{client}}RequestTestsE2E

}


{{#blocksE2E}}
{{#tests}}
[Fact(DisplayName = "{{{testName}}}")]
Expand All @@ -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)
{
Expand Down
4 changes: 2 additions & 2 deletions templates/go/api.mustache
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
16 changes: 14 additions & 2 deletions templates/go/search_helpers.mustache
Original file line number Diff line number Diff line change
Expand Up @@ -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)...)
}

/*
Expand Down Expand Up @@ -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))...)
}
2 changes: 1 addition & 1 deletion templates/go/tests/method.mustache
Original file line number Diff line number Diff line change
@@ -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}})
{{#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}})
5 changes: 2 additions & 3 deletions templates/java/tests/e2e/e2e.mustache
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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}}
}
Expand Down
2 changes: 1 addition & 1 deletion templates/kotlin/builder_function.mustache
Original file line number Diff line number Diff line change
Expand Up @@ -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}}
Expand Down
2 changes: 1 addition & 1 deletion templates/kotlin/data_class_field.mustache
Original file line number Diff line number Diff line change
Expand Up @@ -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}}
2 changes: 1 addition & 1 deletion templates/kotlin/json_object_field.mustache
Original file line number Diff line number Diff line change
Expand Up @@ -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}}
4 changes: 1 addition & 3 deletions templates/kotlin/tests/e2e/e2e.mustache
Original file line number Diff line number Diff line change
Expand Up @@ -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.*
Expand Down Expand Up @@ -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}}
Expand Down
2 changes: 1 addition & 1 deletion templates/kotlin/tests/request_param.mustache
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{{^hasAdditionalProperties}}{{#lambda.camelcase}}{{{key}}}{{/lambda.camelcase}} = {{> tests/param_value}},{{/hasAdditionalProperties}}{{#hasAdditionalProperties}}{{> tests/param_value}},{{/hasAdditionalProperties}}
{{^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}}
2 changes: 1 addition & 1 deletion templates/scala/tests/request_param.mustache
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{{^hasAdditionalProperties}}{{#lambda.identifier}}{{key}}{{/lambda.identifier}} = {{> tests/param_optional}}{{/hasAdditionalProperties}}{{#hasAdditionalProperties}}{{> tests/param_value}}{{/hasAdditionalProperties}}
{{^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}}
2 changes: 1 addition & 1 deletion templates/swift/tests/paramValue.mustache
Original file line number Diff line number Diff line change
@@ -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}}
{{#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}}
54 changes: 54 additions & 0 deletions tests/output/csharp/src/Utils/TestHelpers.cs
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// 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.
/// </summary>
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));
}

/// <summary>
/// Recursively intersects the structure of <paramref name="expected"/> with the values of
/// <paramref name="received"/>. Only keys/indices present in expected are kept.
/// </summary>
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();
}
}
Original file line number Diff line number Diff line change
@@ -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();
}
}
Loading
Loading