diff --git a/README.md b/README.md index f397374a..3c3e1cb7 100644 --- a/README.md +++ b/README.md @@ -201,7 +201,8 @@ CREATE TABLE my_table (my_json JSON); ```java connection.createStatement("INSERT INTO my_table (my_json) VALUES($1)") .bind("$1", Json.of("{\"hello\": \"world\"}")).execute(); -``` +``` + **Consume JSON** ```java @@ -215,7 +216,7 @@ connection.createStatement("SELECT my_json FROM my_table") ```java connection.createStatement("INSERT INTO my_table (my_json) VALUES($1::JSON)") .bind("$1", "{\"hello\": \"world\"}").execute(); -``` +``` **Consume JSON as scalar type** @@ -251,7 +252,6 @@ On application shutdown, `close()` the `ReplicationStream`. Note that a connection is busy once the replication is active and a connection can have at most one active replication stream. ```java - Mono replicationMono = connectionFactory.replication(); // later: @@ -275,6 +275,35 @@ Flux replicationStream = replicationConnection.startReplication(replicationRe }); ``` +## Postgres Enum Types + +Applications may make use of Postgres enumerated types by using `EnumCodec` to map custom types to Java `enum` types. +`EnumCodec` requires the Postgres OID and the Java to map enum values to the Postgres protocol and to materialize Enum instances from Postgres results. +You can configure a `CodecRegistrar` through `EnumCodec.builder()` for one or more enumeration type mappings. Make sure to use different Java enum types otherwise the driver is not able to distinguish between Postgres OIDs. + +Example: + +**SQL:** + +```sql +CREATE TYPE my_enum AS ENUM ('FIRST', 'SECOND'); +``` + +**Java Model:** + +```java +enum MyEnumType { + FIRST, SECOND; +} +``` + +**Codec Registration:** + +```java +PostgresqlConnectionConfiguration.builder() + .codecRegistrar(EnumCodec.builder().withEnum("my_enum", MyEnumType.class).build()); +``` + ## Data Type Mapping This reference table shows the type mapping between [PostgreSQL][p] and Java data types: @@ -293,6 +322,7 @@ This reference table shows the type mapping between [PostgreSQL][p] and Java dat | [`circle`][psql-circle-ref] | Not yet supported.| | [`date`][psql-date-ref] | [`LocalDate`][java-ld-ref]| | [`double precision`][psql-floating-point-ref] | [**`Double`**][java-double-ref], [`Float`][java-float-ref], [`Boolean`][java-boolean-ref], [`Byte`][java-byte-ref], [`Short`][java-short-ref], [`Integer`][java-integer-ref], [`Long`][java-long-ref], [`BigDecimal`][java-bigdecimal-ref], [`BigInteger`][java-biginteger-ref]| +| [enumerated types][psql-enum-ref] | Client code `Enum` types through `EnumCodec`| | [`hstore`][psql-hstore-ref] | [**`Map`**][java-map-ref]| | [`inet`][psql-inet-ref] | [**`InetAddress`**][java-inet-ref]| | [`integer`][psql-integer-ref] | [**`Integer`**][java-integer-ref], [`Boolean`][java-boolean-ref], [`Byte`][java-byte-ref], [`Short`][java-short-ref], [`Long`][java-long-ref], [`BigDecimal`][java-bigdecimal-ref], [`BigInteger`][java-biginteger-ref]| @@ -345,6 +375,7 @@ Support for the following single-dimensional arrays (read and write): [psql-circle-ref]: https://www.postgresql.org/docs/current/datatype-geometric.html#DATATYPE-CIRCLE [psql-date-ref]: https://www.postgresql.org/docs/current/datatype-datetime.html [psql-floating-point-ref]: https://www.postgresql.org/docs/current/datatype-numeric.html#DATATYPE-FLOAT +[psql-enum-ref]: https://www.postgresql.org/docs/current/datatype-enum.html [psql-hstore-ref]: https://www.postgresql.org/docs/current/hstore.html [psql-inet-ref]: https://www.postgresql.org/docs/current/datatype-net-types.html#DATATYPE-INET [psql-integer-ref]: https://www.postgresql.org/docs/current/datatype-numeric.html#DATATYPE-INT @@ -408,7 +439,7 @@ This driver accepts the following extensions: Extensions can be registered programmatically using `PostgresConnectionConfiguration` or discovered using Java's `ServiceLoader` mechanism (from `META-INF/services/io.r2dbc.postgresql.extension.Extension`). -The driver ships with built-in dynamic codecs (e.g. `hstore`) that are registered during the connection handshake depending on their availability while connecting. Note that Postgres extensions registered after a connection was established require a reconnect to initialize the codec. +The driver ships with built-in dynamic codecs (e.g. `hstore`) that are registered during the connection handshake depending on their availability while connecting. Note that Postgres extensions registered after a connection was established require a reconnect to initialize the codec. ## Logging If SL4J is on the classpath, it will be used. Otherwise, there are two possible fallbacks: Console or `java.util.logging.Logger`). By default, the Console fallback is used. To use the JDK loggers, set the `reactor.logging.fallback` System property to `JDK`. diff --git a/src/main/java/io/r2dbc/postgresql/PostgresqlConnectionConfiguration.java b/src/main/java/io/r2dbc/postgresql/PostgresqlConnectionConfiguration.java index 1dd8fbec..721a49ae 100644 --- a/src/main/java/io/r2dbc/postgresql/PostgresqlConnectionConfiguration.java +++ b/src/main/java/io/r2dbc/postgresql/PostgresqlConnectionConfiguration.java @@ -370,6 +370,7 @@ public Builder connectTimeout(@Nullable Duration connectTimeout) { /** * Registers a {@link CodecRegistrar} that can contribute extension {@link Codec}s. + * Calling this method adds a {@link CodecRegistrar} and does not replace existing {@link Extension}s. * * @param codecRegistrar registrar to contribute codecs * @return this {@link Builder} @@ -400,6 +401,7 @@ public Builder enableSsl() { /** * Registers a {@link Extension} to extend driver functionality. + * Calling this method adds a {@link Extension} and does not replace existing {@link Extension}s. * * @param extension extension to extend driver functionality * @return this {@link Builder} diff --git a/src/main/java/io/r2dbc/postgresql/codec/EnumCodec.java b/src/main/java/io/r2dbc/postgresql/codec/EnumCodec.java new file mode 100644 index 00000000..aa0efb5e --- /dev/null +++ b/src/main/java/io/r2dbc/postgresql/codec/EnumCodec.java @@ -0,0 +1,184 @@ +/* + * Copyright 2020 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 io.r2dbc.postgresql.codec; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.r2dbc.postgresql.client.Parameter; +import io.r2dbc.postgresql.extension.CodecRegistrar; +import io.r2dbc.postgresql.message.Format; +import io.r2dbc.postgresql.util.Assert; +import io.r2dbc.postgresql.util.ByteBufUtils; +import reactor.core.publisher.Mono; +import reactor.util.Logger; +import reactor.util.Loggers; +import reactor.util.annotation.Nullable; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import static io.r2dbc.postgresql.client.Parameter.NULL_VALUE; +import static io.r2dbc.postgresql.message.Format.FORMAT_TEXT; + +/** + * Codec to map Postgres {@code enumerated} types to Java {@link Enum} values. + * This codec uses {@link Enum#name()} to map Postgres enum values as these are represented as string values. + *

Note that enum values are case-sensitive. + * + * @param enum type + * @since 0.8.4 + */ +public final class EnumCodec> implements Codec { + + private static final Logger logger = Loggers.getLogger(EnumCodec.class); + + private final ByteBufAllocator byteBufAllocator; + + private final Class type; + + private final int oid; + + public EnumCodec(ByteBufAllocator byteBufAllocator, Class type, int oid) { + this.byteBufAllocator = Assert.requireNonNull(byteBufAllocator, "byteBufAllocator must not be null"); + this.type = Assert.requireNonNull(type, "type must not be null"); + this.oid = oid; + } + + @Override + public boolean canDecode(int dataType, Format format, Class type) { + Assert.requireNonNull(type, "type must not be null"); + return this.type.equals(type) && dataType == this.oid; + } + + @Override + public boolean canEncode(Object value) { + Assert.requireNonNull(value, "value must not be null"); + return this.type.isInstance(value); + } + + @Override + public boolean canEncodeNull(Class type) { + Assert.requireNonNull(type, "type must not be null"); + return this.type.equals(type); + } + + @Override + public T decode(@Nullable ByteBuf buffer, int dataType, Format format, Class type) { + if (buffer == null) { + return null; + } + + return Enum.valueOf(this.type, ByteBufUtils.decode(buffer)); + } + + @Override + public Parameter encode(Object value) { + + Assert.requireNonNull(value, "value must not be null"); + + return new Parameter(FORMAT_TEXT, this.oid, Mono.fromSupplier(() -> ByteBufUtils.encode(this.byteBufAllocator, this.type.cast(value).name()))); + } + + @Override + public Parameter encodeNull() { + return new Parameter(Format.FORMAT_BINARY, this.oid, NULL_VALUE); + } + + @Override + public Class type() { + return this.type; + } + + /** + * Create a new {@link Builder} to build a {@link CodecRegistrar} to dynamically register Postgres {@code enum} types to {@link Enum} values. + * + * @return a new builder. + */ + public static EnumCodec.Builder builder() { + return new Builder(); + } + + /** + * Builder for {@link CodecRegistrar} to register {@link EnumCodec} for one or more enum type mappings. + */ + public static final class Builder { + + private final Map>> mapping = new LinkedHashMap<>(); + + /** + * Add a Postgres enum type to {@link Enum} mapping. + * + * @param name name of the Postgres enum type + * @param enumClass the corresponding Java type + * @return this {@link Builder} + */ + public Builder withEnum(String name, Class> enumClass) { + Assert.requireNonNull(enumClass, "Enum class must not be null"); + Assert.isTrue(enumClass.isEnum(), String.format("Enum class %s must be an enum type", enumClass.getName())); + + if (this.mapping.containsKey(name)) { + throw new IllegalArgumentException(String.format("Builder contains already a mapping for Postgres type %s", name)); + } + + if (this.mapping.containsValue(enumClass)) { + throw new IllegalArgumentException(String.format("Builder contains already a mapping for Java type %s", enumClass.getName())); + } + + this.mapping.put(Assert.requireNotEmpty(name, "Postgres type name must not be null"), enumClass); + return this; + } + + /** + * Build a {@link CodecRegistrar} to be used with {@code PostgresqlConnectionConfiguration.Builder#codecRegistrar(CodecRegistrar)}. + * The codec registrar registers the codes to be used as part of the connection setup. + * + * @return a new {@link CodecRegistrar}. + */ + @SuppressWarnings({"unchecked", "rawtypes"}) + public CodecRegistrar build() { + + Map>> mapping = new LinkedHashMap<>(this.mapping); + + return (connection, allocator, registry) -> { + + List missing = new ArrayList<>(mapping.keySet()); + return PostgresTypes.from(connection).lookupTypes(mapping.keySet()) + .filter(PostgresTypes.PostgresType::isEnum) + .doOnNext(it -> { + + Class> enumClass = mapping.get(it.getName()); + if (enumClass == null) { + logger.warn(String.format("Cannot find Java type for enum type '%s' with oid %d. Known types are: %s", it.getName(), it.getOid(), mapping)); + return; + } + + missing.remove(it.getName()); + logger.debug(String.format("Registering codec for type '%s' with oid %d using Java enum type '%s'", it.getName(), it.getOid(), enumClass.getName())); + registry.addLast(new EnumCodec(allocator, enumClass, it.getOid())); + }).doOnComplete(() -> { + + if (!missing.isEmpty()) { + logger.warn(String.format("Could not lookup enum types for: %s", missing)); + } + + }).then(); + }; + } + } +} diff --git a/src/main/java/io/r2dbc/postgresql/codec/PostgresTypes.java b/src/main/java/io/r2dbc/postgresql/codec/PostgresTypes.java new file mode 100644 index 00000000..8819d48f --- /dev/null +++ b/src/main/java/io/r2dbc/postgresql/codec/PostgresTypes.java @@ -0,0 +1,247 @@ +/* + * Copyright 2020 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 io.r2dbc.postgresql.codec; + +import io.r2dbc.postgresql.api.PostgresqlConnection; +import io.r2dbc.postgresql.util.Assert; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import java.util.Objects; +import java.util.StringJoiner; +import java.util.regex.Pattern; + +/** + * Utility to look up Postgres types using {@code pg_type}. + * + * @since 0.8.4 + */ +public class PostgresTypes { + + // parameterized with %s for the comparator (=, IN), %s for the actual criteria value and %s for a potential LIMIT 1 statement + private static final String SELECT_PG_TYPE = "SELECT pg_type.oid, typname, typcategory " + + " FROM pg_catalog.pg_type " + + " LEFT " + + " JOIN (select ns.oid as nspoid, ns.nspname, r.r " + + " from pg_namespace as ns " + + " join ( select s.r, (current_schemas(false))[s.r] as nspname " + + " from generate_series(1, array_upper(current_schemas(false), 1)) as s(r) ) as r " + + " using ( nspname ) " + + " ) as sp " + + " ON sp.nspoid = typnamespace " + + " WHERE typname %s %s " + + " ORDER BY sp.r, pg_type.oid DESC %s;"; + + private final static Pattern TYPENAME = Pattern.compile("[a-zA-Z0-9_]+"); + + private final PostgresqlConnection connection; + + private PostgresTypes(PostgresqlConnection connection) { + this.connection = connection; + } + + public static PostgresTypes from(PostgresqlConnection connection) { + return new PostgresTypes(Assert.requireNonNull(connection, "connection must not be null")); + } + + /** + * Lookup Postgres types by {@code typname}. Please note that {@code typname} inlined to use simple statements. Therefore, {@code typname} gets verified against {@link #TYPENAME} to prevent SQL + * injection. + * + * @param typeName the type name. Must comply with the pattern {@code [a-zA-Z0-9_]+} + * @return a mono emitting the {@link PostgresType} if found or {@link Mono#empty()} if not found + */ + public Mono lookupType(String typeName) { + if (!TYPENAME.matcher(Assert.requireNonNull(typeName, "typeName must not be null")).matches()) { + throw new IllegalArgumentException(String.format("Invalid typename %s", typeName)); + } + + return this.connection.createStatement(String.format(SELECT_PG_TYPE, "=", typeName, "LIMIT 1")).execute() + .flatMap(it -> it.map((row, rowMetadata) -> { + return new PostgresType(row.get("oid", Integer.class), row.get("typname", String.class), row.get("typcategory", String.class)); + })).singleOrEmpty(); + } + + public Flux lookupTypes(Iterable typeNames) { + + StringJoiner joiner = new StringJoiner(",", "(", ")"); + + typeNames.forEach(typeName -> { + + if (!TYPENAME.matcher(Assert.requireNonNull(typeName, "typeName must not be null")).matches()) { + throw new IllegalArgumentException(String.format("Invalid typename %s", typeName)); + } + + joiner.add("'" + typeName + "'"); + }); + + return this.connection.createStatement(String.format(SELECT_PG_TYPE, "IN", joiner, "")).execute() + .flatMap(it -> it.map((row, rowMetadata) -> { + return new PostgresType(row.get("oid", Integer.class), row.get("typname", String.class), row.get("typcategory", String.class)); + })); + + } + + public static class PostgresType { + + private final int oid; + + private final String name; + + private final String category; + + public PostgresType(int oid, String name, String category) { + this.oid = oid; + this.name = name; + this.category = category; + } + + public int getOid() { + return this.oid; + } + + public String getName() { + return this.name; + } + + /** + * @return {@code true} if the type is an array type (category code {@code A}) + */ + public boolean isArray() { + return "A".equals(this.category); + } + + /** + * @return {@code true} if the type is a boolean type (category code {@code B}) + */ + public boolean isBoolean() { + return "B".equals(this.category); + } + + /** + * @return {@code true} if the type is a composite type (category code {@code C}) + */ + public boolean isComposite() { + return "C".equals(this.category); + } + + /** + * @return {@code true} if the type is a date/time type (category code {@code D}) + */ + public boolean isDateTime() { + return "D".equals(this.category); + } + + /** + * @return {@code true} if the type is an enum type (category code {@code E}) + */ + public boolean isEnum() { + return "E".equals(this.category); + } + + /** + * @return {@code true} if the type is a geometric type (category code {@code G}) + */ + public boolean isGeometric() { + return "G".equals(this.category); + } + + /** + * @return {@code true} if the type is a network address type (category code {@code I}) + */ + public boolean isNetworkAddress() { + return "I".equals(this.category); + } + + /** + * @return {@code true} if the type is a numeric type (category code {@code N}) + */ + public boolean isNumeric() { + return "N".equals(this.category); + } + + /** + * @return {@code true} if the type is a pseudo-type (category code {@code P}) + */ + public boolean isPseudo() { + return "P".equals(this.category); + } + + /** + * @return {@code true} if the type is a string type (category code {@code S}) + */ + public boolean isString() { + return "S".equals(this.category); + } + + /** + * @return {@code true} if the type is a timespan type (category code {@code T}) + */ + public boolean isTimespan() { + return "T".equals(this.category); + } + + /** + * @return {@code true} if the type is a user-defined type (category code {@code U}) + */ + public boolean isUserDefinedType() { + return "U".equals(this.category); + } + + /** + * @return {@code true} if the type is a bit-string type (category code {@code V}) + */ + public boolean isBitString() { + return "V".equals(this.category); + } + + /** + * @return {@code true} if the type is an unknown type (category code {@code X}) + */ + public boolean isUnknown() { + return "X".equals(this.category); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof PostgresType)) { + return false; + } + PostgresType that = (PostgresType) o; + return this.oid == that.oid && + Objects.equals(this.name, that.name) && + Objects.equals(this.category, that.category); + } + + @Override + public int hashCode() { + return Objects.hash(this.oid, this.name, this.category); + } + + @Override + public String toString() { + return "PostgresType{" + + "oid=" + this.oid + + ", name='" + this.name + '\'' + + ", category='" + this.category + '\'' + + '}'; + } + } +} diff --git a/src/test/java/io/r2dbc/postgresql/AbstractCodecIntegrationTests.java b/src/test/java/io/r2dbc/postgresql/AbstractCodecIntegrationTests.java index ad8adc92..e425ea6f 100644 --- a/src/test/java/io/r2dbc/postgresql/AbstractCodecIntegrationTests.java +++ b/src/test/java/io/r2dbc/postgresql/AbstractCodecIntegrationTests.java @@ -21,6 +21,7 @@ import io.netty.buffer.Unpooled; import io.r2dbc.postgresql.api.PostgresqlResult; import io.r2dbc.postgresql.api.PostgresqlStatement; +import io.r2dbc.postgresql.codec.EnumCodec; import io.r2dbc.postgresql.codec.Json; import io.r2dbc.spi.Blob; import io.r2dbc.spi.Clob; @@ -28,6 +29,7 @@ import org.assertj.core.data.Offset; import org.junit.jupiter.api.Test; import org.reactivestreams.Publisher; +import org.springframework.dao.DataAccessException; import org.springframework.util.StreamUtils; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -64,6 +66,16 @@ abstract class AbstractCodecIntegrationTests extends AbstractIntegrationTests { + @Override + protected void customize(PostgresqlConnectionConfiguration.Builder builder) { + try { + SERVER.getJdbcOperations().execute("CREATE TYPE my_enum AS ENUM ('HELLO', 'WORLD')"); + } catch (DataAccessException e) { + // ignore duplicate types + } + builder.codecRegistrar(EnumCodec.builder().withEnum("my_enum", MyEnum.class).build()); + } + @Test void bigDecimal() { testCodec(BigDecimal.class, new BigDecimal("1000.00"), "NUMERIC"); @@ -190,6 +202,11 @@ void doublePrimitive() { testCodec(Double.class, 100.1, "FLOAT8"); } + @Test + void simpleMappedEnum() { + testCodec(MyEnum.class, MyEnum.HELLO, "my_enum"); + } + @Test void floatPrimitive() { testCodec(Float.class, 100.0f, "INT2"); @@ -567,4 +584,9 @@ private void testCodecReadAs(W toWrite, Class javaTypeToRead, Consumer } } + + enum MyEnum { + HELLO, WORLD, + } + } diff --git a/src/test/java/io/r2dbc/postgresql/CodecBinaryFormatIntegrationTests.java b/src/test/java/io/r2dbc/postgresql/CodecBinaryFormatIntegrationTests.java index 7edbe821..9e8177c7 100644 --- a/src/test/java/io/r2dbc/postgresql/CodecBinaryFormatIntegrationTests.java +++ b/src/test/java/io/r2dbc/postgresql/CodecBinaryFormatIntegrationTests.java @@ -20,6 +20,7 @@ final class CodecBinaryFormatIntegrationTests extends AbstractCodecIntegrationTe @Override protected void customize(PostgresqlConnectionConfiguration.Builder builder) { + super.customize(builder); builder.forceBinary(true); } diff --git a/src/test/java/io/r2dbc/postgresql/codec/EnumCodecIntegrationTests.java b/src/test/java/io/r2dbc/postgresql/codec/EnumCodecIntegrationTests.java new file mode 100644 index 00000000..ffb61205 --- /dev/null +++ b/src/test/java/io/r2dbc/postgresql/codec/EnumCodecIntegrationTests.java @@ -0,0 +1,53 @@ +/* + * Copyright 2020 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 io.r2dbc.postgresql.codec; + +import io.r2dbc.postgresql.AbstractIntegrationTests; +import io.r2dbc.postgresql.PostgresqlConnectionConfiguration; +import io.r2dbc.postgresql.PostgresqlConnectionFactory; +import io.r2dbc.postgresql.api.PostgresqlConnection; +import org.junit.jupiter.api.Test; +import reactor.test.StepVerifier; + +/** + * Integration tests for {@link EnumCodec}. + */ +class EnumCodecIntegrationTests extends AbstractIntegrationTests { + + @Test + void shouldReportUnresolvableTypes() { + + PostgresqlConnectionConfiguration configuration = PostgresqlConnectionConfiguration.builder() + .database(SERVER.getDatabase()) + .host(SERVER.getHost()) + .port(SERVER.getPort()) + .password(SERVER.getPassword()) + .username(SERVER.getUsername()) + .codecRegistrar(EnumCodec.builder().withEnum("do_not_exist", MyEnum.class).build()) + .build(); + + + PostgresqlConnectionFactory connectionFactory = new PostgresqlConnectionFactory(configuration); + connectionFactory.create().flatMap(PostgresqlConnection::close).as(StepVerifier::create).verifyComplete(); + + // we cannot really assert logs so that's up to you. + } + + enum MyEnum { + HELLO; + } +} diff --git a/src/test/java/io/r2dbc/postgresql/codec/EnumCodecUnitTests.java b/src/test/java/io/r2dbc/postgresql/codec/EnumCodecUnitTests.java new file mode 100644 index 00000000..99a96e3f --- /dev/null +++ b/src/test/java/io/r2dbc/postgresql/codec/EnumCodecUnitTests.java @@ -0,0 +1,50 @@ +/* + * Copyright 2020 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 io.r2dbc.postgresql.codec; + +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; + +/** + * Unit tests for {@link EnumCodec}. + */ +class EnumCodecUnitTests { + + @Test + void shouldRejectMultipleMappingForJavaType() { + + EnumCodec.Builder builder = EnumCodec.builder().withEnum("foo", MyEnum.class); + + Assertions.assertThatIllegalArgumentException().isThrownBy(() -> builder.withEnum("bar", MyEnum.class)); + } + + @Test + void shouldRejectMultipleMappingForTypeName() { + + EnumCodec.Builder builder = EnumCodec.builder().withEnum("foo", MyEnum.class); + + Assertions.assertThatIllegalArgumentException().isThrownBy(() -> builder.withEnum("foo", MyOtherEnum.class)); + } + + enum MyEnum { + INSTANCE; + } + + enum MyOtherEnum { + INSTANCE; + } +}