diff --git a/spring-cloud-contract-verifier/pom.xml b/spring-cloud-contract-verifier/pom.xml index 1c90d4e529..0b9e84c1db 100644 --- a/spring-cloud-contract-verifier/pom.xml +++ b/spring-cloud-contract-verifier/pom.xml @@ -5,6 +5,7 @@ 4.0.0 2.1.1 + 1.12.1 org.springframework.cloud @@ -275,11 +276,28 @@ spock-junit4 test + + org.spockframework + spock-spring + test + org.springframework.boot spring-boot-resttestclient test + + org.apache.avro + avro + ${avro.version} + test + + + org.apache.avro + avro + ${avro.version} + provided + diff --git a/spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/messaging/noop/NoOpContractVerifierAutoConfiguration.java b/spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/messaging/noop/NoOpContractVerifierAutoConfiguration.java index 1a562bb36d..8eb1bd7081 100644 --- a/spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/messaging/noop/NoOpContractVerifierAutoConfiguration.java +++ b/spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/messaging/noop/NoOpContractVerifierAutoConfiguration.java @@ -19,12 +19,16 @@ import java.util.Map; import java.util.concurrent.TimeUnit; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import org.apache.avro.specific.SpecificRecordBase; import org.jspecify.annotations.Nullable; import tools.jackson.databind.json.JsonMapper; import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.autoconfigure.AutoConfigureOrder; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass; import org.springframework.cloud.contract.verifier.converter.YamlContract; import org.springframework.cloud.contract.verifier.messaging.MessageVerifierReceiver; import org.springframework.cloud.contract.verifier.messaging.MessageVerifierSender; @@ -89,12 +93,30 @@ public ContractVerifierMessaging contractVerifierMessaging() { @Bean @ConditionalOnMissingBean - public ContractVerifierObjectMapper contractVerifierObjectMapper(ObjectProvider jsonMapper) { - JsonMapper mapper = jsonMapper.getIfAvailable(); - if (mapper != null) { + @ConditionalOnMissingClass("org.apache.avro.specific.SpecificRecordBase") + public ContractVerifierObjectMapper contractVerifierObjectMapper( + ObjectProvider jsonMapper) { + JsonMapper mapper = jsonMapper.getIfAvailable(JsonMapper::new); + return new ContractVerifierObjectMapper(mapper); + } + + @Configuration(proxyBeanMethods = false) + @ConditionalOnClass(SpecificRecordBase.class) + public static class AvroContractVerifierObjectMapperConfiguration { + + @Bean + @ConditionalOnMissingBean + public ContractVerifierObjectMapper avroContractVerifierObjectMapper( + ObjectProvider jsonMapper) throws ClassNotFoundException { + JsonMapper mapper = jsonMapper.getIfAvailable(JsonMapper::new).rebuild() + .addMixIn(SpecificRecordBase.class, IgnoreAvroMixin.class).build(); return new ContractVerifierObjectMapper(mapper); } - return new ContractVerifierObjectMapper(); + + @JsonIgnoreProperties({ "schema", "specificData", "classSchema", "conversion" }) + interface IgnoreAvroMixin { + + } } } diff --git a/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/messaging/internal/ContractVerifierObjectMapperAvroSpec.groovy b/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/messaging/internal/ContractVerifierObjectMapperAvroSpec.groovy new file mode 100644 index 0000000000..e2b223a3f6 --- /dev/null +++ b/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/messaging/internal/ContractVerifierObjectMapperAvroSpec.groovy @@ -0,0 +1,38 @@ +/* + * Copyright 2013-present the original author or authors. + * + * Licensed 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 + * + * https://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 org.springframework.cloud.contract.verifier.messaging.internal + +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.cloud.contract.verifier.messaging.noop.NoOpContractVerifierAutoConfiguration +import org.springframework.test.context.ContextConfiguration +import spock.lang.Specification + +@ContextConfiguration(classes = [NoOpContractVerifierAutoConfiguration]) +class ContractVerifierObjectMapperAvroSpec extends Specification { + + @Autowired + ContractVerifierObjectMapper mapper + + def "should convert an Avro-generated object into a json representation"() { + given: + FooAvro input = FooAvro.newBuilder().setFooAvro("barAvro").build() + when: + String result = mapper.writeValueAsString(input) + then: + result == '{"fooAvro":"barAvro"}' + } +} diff --git a/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/messaging/internal/FooAvro.java b/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/messaging/internal/FooAvro.java new file mode 100644 index 0000000000..1b9e9b0b96 --- /dev/null +++ b/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/messaging/internal/FooAvro.java @@ -0,0 +1,341 @@ +/** + * Autogenerated by Avro + * + * DO NOT EDIT DIRECTLY + */ +package org.springframework.cloud.contract.verifier.messaging.internal; + +import org.apache.avro.message.BinaryMessageDecoder; +import org.apache.avro.message.BinaryMessageEncoder; +import org.apache.avro.message.SchemaStore; +import org.apache.avro.specific.SpecificData; + +/** Dummy Avro object for testing purposes */ +@org.apache.avro.specific.AvroGenerated +public class FooAvro extends org.apache.avro.specific.SpecificRecordBase + implements org.apache.avro.specific.SpecificRecord { + private static final long serialVersionUID = -2221379489582530192L; + + public static final org.apache.avro.Schema SCHEMA$ = new org.apache.avro.Schema.Parser().parse( + "{\"type\":\"record\",\"name\":\"FooAvro\",\"namespace\":\"org.springframework.cloud.contract.verifier.messaging.internal\",\"doc\":\"Dummy Avro object for testing purposes\",\"fields\":[{\"name\":\"fooAvro\",\"type\":{\"type\":\"string\",\"avro.java.string\":\"String\"},\"doc\":\"foo field\"}]}"); + + public static org.apache.avro.Schema getClassSchema() { + return SCHEMA$; + } + + private static final SpecificData MODEL$ = new SpecificData(); + + private static final BinaryMessageEncoder ENCODER = new BinaryMessageEncoder<>( + MODEL$, SCHEMA$); + + private static final BinaryMessageDecoder DECODER = new BinaryMessageDecoder<>( + MODEL$, SCHEMA$); + + /** + * Return the BinaryMessageEncoder instance used by this class. + * @return the message encoder used by this class + */ + public static BinaryMessageEncoder getEncoder() { + return ENCODER; + } + + /** + * Return the BinaryMessageDecoder instance used by this class. + * @return the message decoder used by this class + */ + public static BinaryMessageDecoder getDecoder() { + return DECODER; + } + + /** + * Create a new BinaryMessageDecoder instance for this class that uses the specified + * {@link SchemaStore}. + * @param resolver a {@link SchemaStore} used to find schemas by fingerprint + * @return a BinaryMessageDecoder instance for this class backed by the given + * SchemaStore + */ + public static BinaryMessageDecoder createDecoder(SchemaStore resolver) { + return new BinaryMessageDecoder<>(MODEL$, SCHEMA$, resolver); + } + + /** + * Serializes this FooAvro to a ByteBuffer. + * @return a buffer holding the serialized data for this instance + * @throws java.io.IOException if this instance could not be serialized + */ + public java.nio.ByteBuffer toByteBuffer() throws java.io.IOException { + return ENCODER.encode(this); + } + + /** + * Deserializes a FooAvro from a ByteBuffer. + * @param b a byte buffer holding serialized data for an instance of this class + * @return a FooAvro instance decoded from the given buffer + * @throws java.io.IOException if the given bytes could not be deserialized into an + * instance of this class + */ + public static FooAvro fromByteBuffer(java.nio.ByteBuffer b) + throws java.io.IOException { + return DECODER.decode(b); + } + + /** foo field */ + private java.lang.String fooAvro; + + /** + * Default constructor. Note that this does not initialize fields to their default + * values from the schema. If that is desired then one should use + * newBuilder(). + */ + public FooAvro() { + } + + /** + * All-args constructor. + * @param fooAvro foo field + */ + public FooAvro(java.lang.String fooAvro) { + this.fooAvro = fooAvro; + } + + @Override + public org.apache.avro.specific.SpecificData getSpecificData() { + return MODEL$; + } + + @Override + public org.apache.avro.Schema getSchema() { + return SCHEMA$; + } + + // Used by DatumWriter. Applications should not call. + @Override + public java.lang.Object get(int field$) { + switch (field$) { + case 0: + return fooAvro; + default: + throw new IndexOutOfBoundsException("Invalid index: " + field$); + } + } + + // Used by DatumReader. Applications should not call. + @Override + @SuppressWarnings(value = "unchecked") + public void put(int field$, java.lang.Object value$) { + switch (field$) { + case 0: + fooAvro = value$ != null ? value$.toString() : null; + break; + default: + throw new IndexOutOfBoundsException("Invalid index: " + field$); + } + } + + /** + * Gets the value of the 'fooAvro' field. + * @return foo field + */ + public java.lang.String getFooAvro() { + return fooAvro; + } + + /** + * Sets the value of the 'fooAvro' field. foo field + * @param value the value to set. + */ + public void setFooAvro(java.lang.String value) { + this.fooAvro = value; + } + + /** + * Creates a new FooAvro RecordBuilder. + * @return A new FooAvro RecordBuilder + */ + public static org.springframework.cloud.contract.verifier.messaging.internal.FooAvro.Builder newBuilder() { + return new org.springframework.cloud.contract.verifier.messaging.internal.FooAvro.Builder(); + } + + /** + * Creates a new FooAvro RecordBuilder by copying an existing Builder. + * @param other The existing builder to copy. + * @return A new FooAvro RecordBuilder + */ + public static org.springframework.cloud.contract.verifier.messaging.internal.FooAvro.Builder newBuilder( + org.springframework.cloud.contract.verifier.messaging.internal.FooAvro.Builder other) { + if (other == null) { + return new org.springframework.cloud.contract.verifier.messaging.internal.FooAvro.Builder(); + } + else { + return new org.springframework.cloud.contract.verifier.messaging.internal.FooAvro.Builder( + other); + } + } + + /** + * Creates a new FooAvro RecordBuilder by copying an existing FooAvro instance. + * @param other The existing instance to copy. + * @return A new FooAvro RecordBuilder + */ + public static org.springframework.cloud.contract.verifier.messaging.internal.FooAvro.Builder newBuilder( + org.springframework.cloud.contract.verifier.messaging.internal.FooAvro other) { + if (other == null) { + return new org.springframework.cloud.contract.verifier.messaging.internal.FooAvro.Builder(); + } + else { + return new org.springframework.cloud.contract.verifier.messaging.internal.FooAvro.Builder( + other); + } + } + + /** + * RecordBuilder for FooAvro instances. + */ + @org.apache.avro.specific.AvroGenerated + public static class Builder + extends org.apache.avro.specific.SpecificRecordBuilderBase + implements org.apache.avro.data.RecordBuilder { + + /** foo field */ + private java.lang.String fooAvro; + + /** Creates a new Builder */ + private Builder() { + super(SCHEMA$, MODEL$); + } + + /** + * Creates a Builder by copying an existing Builder. + * @param other The existing Builder to copy. + */ + private Builder( + org.springframework.cloud.contract.verifier.messaging.internal.FooAvro.Builder other) { + super(other); + if (isValidValue(fields()[0], other.fooAvro)) { + this.fooAvro = data().deepCopy(fields()[0].schema(), other.fooAvro); + fieldSetFlags()[0] = other.fieldSetFlags()[0]; + } + } + + /** + * Creates a Builder by copying an existing FooAvro instance + * @param other The existing instance to copy. + */ + private Builder( + org.springframework.cloud.contract.verifier.messaging.internal.FooAvro other) { + super(SCHEMA$, MODEL$); + if (isValidValue(fields()[0], other.fooAvro)) { + this.fooAvro = data().deepCopy(fields()[0].schema(), other.fooAvro); + fieldSetFlags()[0] = true; + } + } + + /** + * Gets the value of the 'fooAvro' field. foo field + * @return The value. + */ + public java.lang.String getFooAvro() { + return fooAvro; + } + + /** + * Sets the value of the 'fooAvro' field. foo field + * @param value The value of 'fooAvro'. + * @return This builder. + */ + public org.springframework.cloud.contract.verifier.messaging.internal.FooAvro.Builder setFooAvro( + java.lang.String value) { + validate(fields()[0], value); + this.fooAvro = value; + fieldSetFlags()[0] = true; + return this; + } + + /** + * Checks whether the 'fooAvro' field has been set. foo field + * @return True if the 'fooAvro' field has been set, false otherwise. + */ + public boolean hasFooAvro() { + return fieldSetFlags()[0]; + } + + /** + * Clears the value of the 'fooAvro' field. foo field + * @return This builder. + */ + public org.springframework.cloud.contract.verifier.messaging.internal.FooAvro.Builder clearFooAvro() { + fooAvro = null; + fieldSetFlags()[0] = false; + return this; + } + + @Override + @SuppressWarnings("unchecked") + public FooAvro build() { + try { + FooAvro record = new FooAvro(); + record.fooAvro = fieldSetFlags()[0] ? + this.fooAvro : + (java.lang.String) defaultValue(fields()[0]); + return record; + } + catch (org.apache.avro.AvroMissingFieldException e) { + throw e; + } + catch (java.lang.Exception e) { + throw new org.apache.avro.AvroRuntimeException(e); + } + } + } + + @SuppressWarnings("unchecked") + private static final org.apache.avro.io.DatumWriter WRITER$ = (org.apache.avro.io.DatumWriter) MODEL$.createDatumWriter( + SCHEMA$); + + @Override + public void writeExternal(java.io.ObjectOutput out) throws java.io.IOException { + WRITER$.write(this, SpecificData.getEncoder(out)); + } + + @SuppressWarnings("unchecked") + private static final org.apache.avro.io.DatumReader READER$ = (org.apache.avro.io.DatumReader) MODEL$.createDatumReader( + SCHEMA$); + + @Override + public void readExternal(java.io.ObjectInput in) throws java.io.IOException { + READER$.read(this, SpecificData.getDecoder(in)); + } + + @Override + protected boolean hasCustomCoders() { + return true; + } + + @Override + public void customEncode(org.apache.avro.io.Encoder out) throws java.io.IOException { + out.writeString(this.fooAvro); + + } + + @Override + public void customDecode(org.apache.avro.io.ResolvingDecoder in) + throws java.io.IOException { + org.apache.avro.Schema.Field[] fieldOrder = in.readFieldOrderIfDiff(); + if (fieldOrder == null) { + this.fooAvro = in.readString(); + + } + else { + for (int i = 0; i < 1; i++) { + switch (fieldOrder[i].pos()) { + case 0: + this.fooAvro = in.readString(); + break; + + default: + throw new java.io.IOException("Corrupt ResolvingDecoder."); + } + } + } + } +} \ No newline at end of file