Skip to content

Commit

Permalink
Allow requests to be serialized as nothing instead of an empty object (
Browse files Browse the repository at this point in the history
  • Loading branch information
swallez committed Jan 23, 2025
1 parent 819df34 commit 222047b
Show file tree
Hide file tree
Showing 9 changed files with 367 additions and 13 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -760,5 +760,6 @@ protected static void setupCountRequestDeserializer(ObjectDeserializer<CountRequ
}
return params;

}, SimpleEndpoint.emptyMap(), true, CountResponse._DESERIALIZER);
}, SimpleEndpoint.emptyMap(), SimpleEndpoint.nonEmptyJsonObject(SimpleEndpoint.returnSelf()),
CountResponse._DESERIALIZER);
}
Original file line number Diff line number Diff line change
Expand Up @@ -704,7 +704,8 @@ protected static void setupExplainRequestDeserializer(ObjectDeserializer<Explain
}
return params;

}, SimpleEndpoint.emptyMap(), true, ExplainResponse._DESERIALIZER);
}, SimpleEndpoint.emptyMap(), SimpleEndpoint.nonEmptyJsonObject(SimpleEndpoint.returnSelf()),
ExplainResponse._DESERIALIZER);

/**
* Create an "{@code explain}" endpoint.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -698,5 +698,6 @@ protected static void setupValidateQueryRequestDeserializer(ObjectDeserializer<V
}
return params;

}, SimpleEndpoint.emptyMap(), true, ValidateQueryResponse._DESERIALIZER);
}, SimpleEndpoint.emptyMap(), SimpleEndpoint.nonEmptyJsonObject(SimpleEndpoint.returnSelf()),
ValidateQueryResponse._DESERIALIZER);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. 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.
*/

package co.elastic.clients.json;

import jakarta.json.JsonValue;
import jakarta.json.stream.JsonGenerator;

import java.math.BigDecimal;
import java.math.BigInteger;

/**
* A JSON generator that delegates to another generator.
* <p>
* All convenience methods that accept a property name and an event (value, start object, start array) call separately
* {@link #writeKey(String)} and the same method without the key name. This is meant to facilitate overloading
* of methods.
*/
public class DelegatingJsonGenerator implements JsonGenerator {
protected final JsonGenerator generator;

public DelegatingJsonGenerator(JsonGenerator generator) {
this.generator = generator;
}

public JsonGenerator unwrap() {
return generator;
};

@Override
public JsonGenerator writeStartObject() {
generator.writeStartObject();
return this;
}

@Override
public JsonGenerator writeKey(String s) {
generator.writeKey(s);
return this;
}

@Override
public JsonGenerator writeStartArray() {
generator.writeStartArray();
return this;
}

@Override
public JsonGenerator writeEnd() {
generator.writeEnd();
return this;
}

@Override
public JsonGenerator write(JsonValue jsonValue) {
generator.write(jsonValue);
return this;
}

@Override
public JsonGenerator write(String s) {
generator.write(s);
return this;
}

@Override
public JsonGenerator write(BigDecimal bigDecimal) {
generator.write(bigDecimal);
return this;
}

@Override
public JsonGenerator write(BigInteger bigInteger) {
generator.write(bigInteger);
return this;
}

@Override
public JsonGenerator write(int i) {
generator.write(i);
return this;
}

@Override
public JsonGenerator write(long l) {
generator.write(l);
return this;
}

@Override
public JsonGenerator write(double v) {
generator.write(v);
return this;
}

@Override
public JsonGenerator write(boolean b) {
generator.write(b);
return this;
}

@Override
public JsonGenerator writeNull() {
generator.writeNull();
return this;
}

@Override
public void close() {
generator.close();
}

@Override
public void flush() {
generator.flush();
}

//----- Convenience key+value methods

@Override
public final JsonGenerator writeStartObject(String s) {
this.writeKey(s);
return this.writeStartObject();
}

@Override
public final JsonGenerator writeStartArray(String s) {
this.writeKey(s);
return this.writeStartArray();
}

@Override
public final JsonGenerator write(String s, JsonValue jsonValue) {
this.writeKey(s);
return this.write(jsonValue);
}

@Override
public final JsonGenerator write(String s, String s1) {
this.writeKey(s);
return this.write(s1);
}

@Override
public final JsonGenerator write(String s, BigInteger bigInteger) {
this.writeKey(s);
return this.write(bigInteger);
}

@Override
public final JsonGenerator write(String s, BigDecimal bigDecimal) {
this.writeKey(s);
return this.write(bigDecimal);
}

@Override
public final JsonGenerator write(String s, int i) {
this.writeKey(s);
return this.write(i);
}

@Override
public final JsonGenerator write(String s, long l) {
this.writeKey(s);
return this.write(l);
}

@Override
public final JsonGenerator write(String s, double v) {
this.writeKey(s);
return this.write(v);
}

@Override
public final JsonGenerator write(String s, boolean b) {
this.writeKey(s);
return this.write(b);
}

@Override
public final JsonGenerator writeNull(String s) {
this.writeKey(s);
return this.writeNull();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@

import co.elastic.clients.json.BufferingJsonGenerator;
import co.elastic.clients.json.BufferingJsonpMapper;
import co.elastic.clients.json.DelegatingJsonGenerator;
import co.elastic.clients.json.JsonpDeserializer;
import co.elastic.clients.json.JsonpDeserializerBase;
import co.elastic.clients.json.JsonpMapper;
Expand Down Expand Up @@ -95,16 +96,23 @@ protected <T> JsonpDeserializer<T> getDefaultDeserializer(Type type) {
@Override
public <T> void serialize(T value, JsonGenerator generator) {

if (!(generator instanceof JacksonJsonpGenerator)) {
throw new IllegalArgumentException("Jackson's ObjectMapper can only be used with the JacksonJsonpProvider");
}

JsonpSerializer<T> serializer = findSerializer(value);
if (serializer != null) {
serializer.serialize(value, generator, this);
return;
}

// Delegating generators are used in higher levels of serialization (e.g. filter empty top-level objects).
// At this point the object is not a JsonpSerializable and we can assume we're in a nested property holding
// a user-provided type and can unwrap to find the underlying non-delegating generator.
while (generator instanceof DelegatingJsonGenerator) {
generator = ((DelegatingJsonGenerator) generator).unwrap();
}

if (!(generator instanceof JacksonJsonpGenerator)) {
throw new IllegalArgumentException("Jackson's ObjectMapper can only be used with the JacksonJsonpProvider");
}

com.fasterxml.jackson.core.JsonGenerator jkGenerator = ((JacksonJsonpGenerator)generator).jacksonGenerator();
try {
objectMapper.writeValue(jkGenerator, value);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -264,9 +264,24 @@ private <RequestT, ResponseT, ErrorT> TransportHttpClient.Request prepareTranspo
NoCopyByteArrayOutputStream baos = new NoCopyByteArrayOutputStream();
JsonGenerator generator = mapper.jsonProvider().createGenerator(baos);
mapper.serialize(body, generator);
generator.close();
bodyBuffers = Collections.singletonList(baos.asByteBuffer());
headers = JsonContentTypeHeaders;

// Some generators (e.g. Parsson) throw an exception if we close a generator
// that hasn't received any event. In that case, we ignore the exception
RuntimeException closeException = null;
try {
generator.close();
} catch (RuntimeException e) {
closeException = e;
}

if (baos.size() > 0) {
if (closeException != null) {
// We got some content and close failed
throw closeException;
}
bodyBuffers = Collections.singletonList(baos.asByteBuffer());
headers = JsonContentTypeHeaders;
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ static <T, U> Function<T, U> returnNull() {
* that the input and output generic parameters are different, making it suitable for use in a wider range of use cases.
*/
@SuppressWarnings("unchecked")
static <T, U> Function<T, U> returnSelf() {
public static <T, U> Function<T, U> returnSelf() {
return (Function<T, U>) RETURN_SELF;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,12 @@
package co.elastic.clients.transport.endpoints;

import co.elastic.clients.elasticsearch._types.ErrorResponse;
import co.elastic.clients.json.DelegatingJsonGenerator;
import co.elastic.clients.json.JsonpDeserializer;
import co.elastic.clients.json.JsonpMapper;
import co.elastic.clients.json.JsonpSerializable;
import co.elastic.clients.transport.JsonEndpoint;
import jakarta.json.stream.JsonGenerator;

import java.util.Map;
import java.util.function.Function;
Expand Down Expand Up @@ -52,7 +56,7 @@ public SimpleEndpoint(
Function<RequestT, Map<String, String>> pathParameters,
Function<RequestT, Map<String, String>> queryParameters,
Function<RequestT, Map<String, String>> headers,
boolean hasResponseBody,
boolean hasRequestBody,
JsonpDeserializer<ResponseT> responseParser
) {
this(
Expand All @@ -62,7 +66,7 @@ public SimpleEndpoint(
pathParameters,
queryParameters,
headers,
hasResponseBody ? returnSelf() : returnNull(),
hasRequestBody ? returnSelf() : returnNull(),
responseParser
);
}
Expand All @@ -86,4 +90,56 @@ public <NewResponseT> SimpleEndpoint<RequestT, NewResponseT> withResponseDeseria
newResponseParser
);
}

/**
* Wraps a function's result with a serializable object that will serialize to nothing if the wrapped
* object's serialization has no property, i.e. it will either produce an empty object or nothing.
*/
public static <T, U extends JsonpSerializable> Function<T, Object> nonEmptyJsonObject(Function<T, U> getter) {
return (x -> x == null ? null : new NonEmptySerializable(getter.apply(x)));
}

private static final class NonEmptySerializable implements JsonpSerializable {
private final Object value;

NonEmptySerializable(Object value) {
this.value = value;
}

@Override
public void serialize(JsonGenerator generator, JsonpMapper mapper) {
// Track the first property to start the top-level object, and end it if needed in close()
JsonGenerator filter = new DelegatingJsonGenerator(generator) {
boolean gotKey = false;

@Override
public JsonGenerator writeStartObject() {
if (gotKey) {
super.writeStartObject();
}
return this;
}

@Override
public JsonGenerator writeKey(String s) {
if (!gotKey) {
gotKey = true;
super.writeStartObject();
}
super.writeKey(s);
return this;
}

@Override
public JsonGenerator writeEnd() {
if (gotKey) {
super.writeEnd();
}
return this;
}
};

mapper.serialize(value, filter);
}
}
}
Loading

0 comments on commit 222047b

Please sign in to comment.