diff --git a/changelog/@unreleased/pr-2213.v2.yml b/changelog/@unreleased/pr-2213.v2.yml new file mode 100644 index 000000000..a79225343 --- /dev/null +++ b/changelog/@unreleased/pr-2213.v2.yml @@ -0,0 +1,6 @@ +type: improvement +improvement: + description: Java generator supports ignoring external type imports using the `externalFallbackTypes` + generator option + links: + - https://github.com/palantir/conjure-java/pull/2213 diff --git a/conjure-java-core/src/integrationInput/java/com/palantir/product/external/AliasToExternal.java b/conjure-java-core/src/integrationInput/java/com/palantir/product/external/AliasToExternal.java new file mode 100644 index 000000000..f87bf7ff7 --- /dev/null +++ b/conjure-java-core/src/integrationInput/java/com/palantir/product/external/AliasToExternal.java @@ -0,0 +1,55 @@ +package com.palantir.product.external; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; +import com.palantir.logsafe.Preconditions; +import com.palantir.logsafe.Safe; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import javax.annotation.processing.Generated; + +@Safe +@Generated("com.palantir.conjure.java.types.AliasGenerator") +public final class AliasToExternal implements Comparable { + private final @Safe String value; + + private AliasToExternal(@Nonnull @Safe String value) { + this.value = Preconditions.checkNotNull(value, "value cannot be null"); + } + + @JsonValue + public @Safe String get() { + return value; + } + + @Override + @Safe + public String toString() { + return value.toString(); + } + + @Override + public boolean equals(@Nullable Object other) { + return this == other + || (other instanceof AliasToExternal && this.value.equals(((AliasToExternal) other).value)); + } + + @Override + public int hashCode() { + return value.hashCode(); + } + + @Override + public int compareTo(AliasToExternal other) { + return value.compareTo(other.get()); + } + + public static AliasToExternal valueOf(@Safe String value) { + return of(value); + } + + @JsonCreator(mode = JsonCreator.Mode.DELEGATING) + public static AliasToExternal of(@Nonnull @Safe String value) { + return new AliasToExternal(value); + } +} diff --git a/conjure-java-core/src/integrationInput/java/com/palantir/product/external/DialogueServiceUsingExternalTypesEndpoints.java b/conjure-java-core/src/integrationInput/java/com/palantir/product/external/DialogueServiceUsingExternalTypesEndpoints.java new file mode 100644 index 000000000..0104a50e7 --- /dev/null +++ b/conjure-java-core/src/integrationInput/java/com/palantir/product/external/DialogueServiceUsingExternalTypesEndpoints.java @@ -0,0 +1,49 @@ +package com.palantir.product.external; + +import com.google.common.collect.ListMultimap; +import com.palantir.dialogue.Endpoint; +import com.palantir.dialogue.HttpMethod; +import com.palantir.dialogue.PathTemplate; +import com.palantir.dialogue.UrlBuilder; +import java.lang.Override; +import java.lang.String; +import java.util.Optional; +import javax.annotation.processing.Generated; + +@Generated("com.palantir.conjure.java.services.dialogue.DialogueEndpointsGenerator") +enum DialogueServiceUsingExternalTypesEndpoints implements Endpoint { + external { + private final PathTemplate pathTemplate = + PathTemplate.builder().fixed("external").variable("path").build(); + + @Override + public void renderPath(ListMultimap params, UrlBuilder url) { + pathTemplate.fill(params, url); + } + + @Override + public HttpMethod httpMethod() { + return HttpMethod.PUT; + } + + @Override + public String serviceName() { + return "ServiceUsingExternalTypes"; + } + + @Override + public String endpointName() { + return "external"; + } + + @Override + public String version() { + return VERSION; + } + }; + + private static final String VERSION = Optional.ofNullable(DialogueServiceUsingExternalTypesEndpoints.class + .getPackage() + .getImplementationVersion()) + .orElse("0.0.0"); +} diff --git a/conjure-java-core/src/integrationInput/java/com/palantir/product/external/ObjectWithExternalField.java b/conjure-java-core/src/integrationInput/java/com/palantir/product/external/ObjectWithExternalField.java new file mode 100644 index 000000000..d6740d6ff --- /dev/null +++ b/conjure-java-core/src/integrationInput/java/com/palantir/product/external/ObjectWithExternalField.java @@ -0,0 +1,112 @@ +package com.palantir.product.external; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonSetter; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.palantir.logsafe.Preconditions; +import com.palantir.logsafe.Safe; +import com.palantir.logsafe.SafeArg; +import com.palantir.logsafe.exceptions.SafeIllegalArgumentException; +import java.util.ArrayList; +import java.util.List; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import javax.annotation.processing.Generated; + +@Safe +@JsonDeserialize(builder = ObjectWithExternalField.Builder.class) +@Generated("com.palantir.conjure.java.types.BeanGenerator") +public final class ObjectWithExternalField { + private final String field; + + private ObjectWithExternalField(String field) { + validateFields(field); + this.field = field; + } + + @JsonProperty("field") + @Safe + public String getField() { + return this.field; + } + + @Override + public boolean equals(@Nullable Object other) { + return this == other || (other instanceof ObjectWithExternalField && equalTo((ObjectWithExternalField) other)); + } + + private boolean equalTo(ObjectWithExternalField other) { + return this.field.equals(other.field); + } + + @Override + public int hashCode() { + return this.field.hashCode(); + } + + @Override + @Safe + public String toString() { + return "ObjectWithExternalField{field: " + field + '}'; + } + + public static ObjectWithExternalField of(@Safe String field) { + return builder().field(field).build(); + } + + private static void validateFields(String field) { + List missingFields = null; + missingFields = addFieldIfMissing(missingFields, field, "field"); + if (missingFields != null) { + throw new SafeIllegalArgumentException( + "Some required fields have not been set", SafeArg.of("missingFields", missingFields)); + } + } + + private static List addFieldIfMissing(List prev, Object fieldValue, String fieldName) { + List missingFields = prev; + if (fieldValue == null) { + if (missingFields == null) { + missingFields = new ArrayList<>(1); + } + missingFields.add(fieldName); + } + return missingFields; + } + + public static Builder builder() { + return new Builder(); + } + + @Generated("com.palantir.conjure.java.types.BeanBuilderGenerator") + public static final class Builder { + boolean _buildInvoked; + + private @Safe String field; + + private Builder() {} + + public Builder from(ObjectWithExternalField other) { + checkNotBuilt(); + field(other.getField()); + return this; + } + + @JsonSetter("field") + public Builder field(@Nonnull @Safe String field) { + checkNotBuilt(); + this.field = Preconditions.checkNotNull(field, "field cannot be null"); + return this; + } + + public ObjectWithExternalField build() { + checkNotBuilt(); + this._buildInvoked = true; + return new ObjectWithExternalField(field); + } + + private void checkNotBuilt() { + Preconditions.checkState(!_buildInvoked, "Build has already been called"); + } + } +} diff --git a/conjure-java-core/src/integrationInput/java/com/palantir/product/external/ServiceUsingExternalTypesAsync.java b/conjure-java-core/src/integrationInput/java/com/palantir/product/external/ServiceUsingExternalTypesAsync.java new file mode 100644 index 000000000..707790343 --- /dev/null +++ b/conjure-java-core/src/integrationInput/java/com/palantir/product/external/ServiceUsingExternalTypesAsync.java @@ -0,0 +1,84 @@ +package com.palantir.product.external; + +import com.google.common.util.concurrent.ListenableFuture; +import com.palantir.conjure.java.lib.internal.ClientEndpoint; +import com.palantir.dialogue.Channel; +import com.palantir.dialogue.ConjureRuntime; +import com.palantir.dialogue.Deserializer; +import com.palantir.dialogue.DialogueService; +import com.palantir.dialogue.DialogueServiceFactory; +import com.palantir.dialogue.Endpoint; +import com.palantir.dialogue.EndpointChannel; +import com.palantir.dialogue.EndpointChannelFactory; +import com.palantir.dialogue.PlainSerDe; +import com.palantir.dialogue.Request; +import com.palantir.dialogue.Serializer; +import com.palantir.dialogue.TypeMarker; +import com.palantir.logsafe.Safe; +import java.lang.Long; +import java.lang.Override; +import java.lang.String; +import java.util.List; +import java.util.Map; +import javax.annotation.processing.Generated; + +@Generated("com.palantir.conjure.java.services.dialogue.DialogueInterfaceGenerator") +@DialogueService(ServiceUsingExternalTypesAsync.Factory.class) +public interface ServiceUsingExternalTypesAsync { + /** @apiNote {@code PUT /external/{path}} */ + @ClientEndpoint(method = "PUT", path = "/external/{path}") + ListenableFuture> external(@Safe String path, @Safe List body); + + /** Creates an asynchronous/non-blocking client for a ServiceUsingExternalTypes service. */ + static ServiceUsingExternalTypesAsync of(EndpointChannelFactory _endpointChannelFactory, ConjureRuntime _runtime) { + return new ServiceUsingExternalTypesAsync() { + private final PlainSerDe _plainSerDe = _runtime.plainSerDe(); + + private final Serializer> externalSerializer = + _runtime.bodySerDe().serializer(new TypeMarker>() {}); + + private final EndpointChannel externalChannel = + _endpointChannelFactory.endpoint(DialogueServiceUsingExternalTypesEndpoints.external); + + private final Deserializer> externalDeserializer = + _runtime.bodySerDe().deserializer(new TypeMarker>() {}); + + @Override + public ListenableFuture> external(String path, List body) { + Request.Builder _request = Request.builder(); + _request.putPathParams("path", _plainSerDe.serializeString(path)); + _request.body(externalSerializer.serialize(body)); + return _runtime.clients().call(externalChannel, _request.build(), externalDeserializer); + } + + @Override + public String toString() { + return "ServiceUsingExternalTypesAsync{_endpointChannelFactory=" + _endpointChannelFactory + + ", runtime=" + _runtime + '}'; + } + }; + } + + /** Creates an asynchronous/non-blocking client for a ServiceUsingExternalTypes service. */ + static ServiceUsingExternalTypesAsync of(Channel _channel, ConjureRuntime _runtime) { + if (_channel instanceof EndpointChannelFactory) { + return of((EndpointChannelFactory) _channel, _runtime); + } + return of( + new EndpointChannelFactory() { + @Override + public EndpointChannel endpoint(Endpoint endpoint) { + return _runtime.clients().bind(_channel, endpoint); + } + }, + _runtime); + } + + final class Factory implements DialogueServiceFactory { + @Override + public ServiceUsingExternalTypesAsync create( + EndpointChannelFactory endpointChannelFactory, ConjureRuntime runtime) { + return ServiceUsingExternalTypesAsync.of(endpointChannelFactory, runtime); + } + } +} diff --git a/conjure-java-core/src/integrationInput/java/com/palantir/product/external/ServiceUsingExternalTypesBlocking.java b/conjure-java-core/src/integrationInput/java/com/palantir/product/external/ServiceUsingExternalTypesBlocking.java new file mode 100644 index 000000000..f6b9785de --- /dev/null +++ b/conjure-java-core/src/integrationInput/java/com/palantir/product/external/ServiceUsingExternalTypesBlocking.java @@ -0,0 +1,84 @@ +package com.palantir.product.external; + +import com.palantir.conjure.java.lib.internal.ClientEndpoint; +import com.palantir.dialogue.Channel; +import com.palantir.dialogue.ConjureRuntime; +import com.palantir.dialogue.Deserializer; +import com.palantir.dialogue.DialogueService; +import com.palantir.dialogue.DialogueServiceFactory; +import com.palantir.dialogue.Endpoint; +import com.palantir.dialogue.EndpointChannel; +import com.palantir.dialogue.EndpointChannelFactory; +import com.palantir.dialogue.PlainSerDe; +import com.palantir.dialogue.Request; +import com.palantir.dialogue.Serializer; +import com.palantir.dialogue.TypeMarker; +import com.palantir.logsafe.Safe; +import java.lang.Long; +import java.lang.Override; +import java.lang.String; +import java.util.List; +import java.util.Map; +import javax.annotation.processing.Generated; + +@Generated("com.palantir.conjure.java.services.dialogue.DialogueInterfaceGenerator") +@DialogueService(ServiceUsingExternalTypesBlocking.Factory.class) +public interface ServiceUsingExternalTypesBlocking { + /** @apiNote {@code PUT /external/{path}} */ + @ClientEndpoint(method = "PUT", path = "/external/{path}") + Map external(@Safe String path, @Safe List body); + + /** Creates a synchronous/blocking client for a ServiceUsingExternalTypes service. */ + static ServiceUsingExternalTypesBlocking of( + EndpointChannelFactory _endpointChannelFactory, ConjureRuntime _runtime) { + return new ServiceUsingExternalTypesBlocking() { + private final PlainSerDe _plainSerDe = _runtime.plainSerDe(); + + private final Serializer> externalSerializer = + _runtime.bodySerDe().serializer(new TypeMarker>() {}); + + private final EndpointChannel externalChannel = + _endpointChannelFactory.endpoint(DialogueServiceUsingExternalTypesEndpoints.external); + + private final Deserializer> externalDeserializer = + _runtime.bodySerDe().deserializer(new TypeMarker>() {}); + + @Override + public Map external(String path, List body) { + Request.Builder _request = Request.builder(); + _request.putPathParams("path", _plainSerDe.serializeString(path)); + _request.body(externalSerializer.serialize(body)); + return _runtime.clients().callBlocking(externalChannel, _request.build(), externalDeserializer); + } + + @Override + public String toString() { + return "ServiceUsingExternalTypesBlocking{_endpointChannelFactory=" + _endpointChannelFactory + + ", runtime=" + _runtime + '}'; + } + }; + } + + /** Creates an asynchronous/non-blocking client for a ServiceUsingExternalTypes service. */ + static ServiceUsingExternalTypesBlocking of(Channel _channel, ConjureRuntime _runtime) { + if (_channel instanceof EndpointChannelFactory) { + return of((EndpointChannelFactory) _channel, _runtime); + } + return of( + new EndpointChannelFactory() { + @Override + public EndpointChannel endpoint(Endpoint endpoint) { + return _runtime.clients().bind(_channel, endpoint); + } + }, + _runtime); + } + + final class Factory implements DialogueServiceFactory { + @Override + public ServiceUsingExternalTypesBlocking create( + EndpointChannelFactory endpointChannelFactory, ConjureRuntime runtime) { + return ServiceUsingExternalTypesBlocking.of(endpointChannelFactory, runtime); + } + } +} diff --git a/conjure-java-core/src/integrationInput/java/com/palantir/product/external/UnionWithExternal.java b/conjure-java-core/src/integrationInput/java/com/palantir/product/external/UnionWithExternal.java new file mode 100644 index 000000000..2eb230014 --- /dev/null +++ b/conjure-java-core/src/integrationInput/java/com/palantir/product/external/UnionWithExternal.java @@ -0,0 +1,274 @@ +package com.palantir.product.external; + +import com.fasterxml.jackson.annotation.JsonAnyGetter; +import com.fasterxml.jackson.annotation.JsonAnySetter; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonSetter; +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.fasterxml.jackson.annotation.JsonTypeName; +import com.fasterxml.jackson.annotation.JsonValue; +import com.palantir.logsafe.Preconditions; +import com.palantir.logsafe.Safe; +import com.palantir.logsafe.SafeArg; +import com.palantir.logsafe.exceptions.SafeIllegalArgumentException; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.function.BiFunction; +import java.util.function.Function; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import javax.annotation.processing.Generated; + +/** A type which can either be a StringExample, a set of strings, or an integer. */ +@Generated("com.palantir.conjure.java.types.UnionGenerator") +public final class UnionWithExternal { + private final Base value; + + @JsonCreator(mode = JsonCreator.Mode.DELEGATING) + private UnionWithExternal(Base value) { + this.value = value; + } + + @JsonValue + private Base getValue() { + return value; + } + + public static UnionWithExternal field(@Safe String value) { + return new UnionWithExternal(new FieldWrapper(value)); + } + + public static UnionWithExternal unknown(@Safe String type, Object value) { + switch (Preconditions.checkNotNull(type, "Type is required")) { + case "field": + throw new SafeIllegalArgumentException( + "Unknown type cannot be created as the provided type is known: field"); + default: + return new UnionWithExternal(new UnknownWrapper(type, Collections.singletonMap(type, value))); + } + } + + public T accept(Visitor visitor) { + return value.accept(visitor); + } + + @Override + public boolean equals(@Nullable Object other) { + return this == other || (other instanceof UnionWithExternal && equalTo((UnionWithExternal) other)); + } + + private boolean equalTo(UnionWithExternal other) { + return this.value.equals(other.value); + } + + @Override + public int hashCode() { + return this.value.hashCode(); + } + + @Override + public String toString() { + return "UnionWithExternal{value: " + value + '}'; + } + + public interface Visitor { + T visitField(@Safe String value); + + T visitUnknown(@Safe String unknownType, Object unknownValue); + + static FieldStageVisitorBuilder builder() { + return new VisitorBuilder(); + } + } + + private static final class VisitorBuilder + implements FieldStageVisitorBuilder, UnknownStageVisitorBuilder, Completed_StageVisitorBuilder { + private Function<@Safe String, T> fieldVisitor; + + private BiFunction<@Safe String, Object, T> unknownVisitor; + + @Override + public UnknownStageVisitorBuilder field(@Nonnull Function<@Safe String, T> fieldVisitor) { + Preconditions.checkNotNull(fieldVisitor, "fieldVisitor cannot be null"); + this.fieldVisitor = fieldVisitor; + return this; + } + + @Override + public Completed_StageVisitorBuilder unknown(@Nonnull BiFunction<@Safe String, Object, T> unknownVisitor) { + Preconditions.checkNotNull(unknownVisitor, "unknownVisitor cannot be null"); + this.unknownVisitor = unknownVisitor; + return this; + } + + @Override + public Completed_StageVisitorBuilder unknown(@Nonnull Function<@Safe String, T> unknownVisitor) { + Preconditions.checkNotNull(unknownVisitor, "unknownVisitor cannot be null"); + this.unknownVisitor = (unknownType, _unknownValue) -> unknownVisitor.apply(unknownType); + return this; + } + + @Override + public Completed_StageVisitorBuilder throwOnUnknown() { + this.unknownVisitor = (unknownType, _unknownValue) -> { + throw new SafeIllegalArgumentException( + "Unknown variant of the 'UnionWithExternal' union", SafeArg.of("unknownType", unknownType)); + }; + return this; + } + + @Override + public Visitor build() { + final Function<@Safe String, T> fieldVisitor = this.fieldVisitor; + final BiFunction<@Safe String, Object, T> unknownVisitor = this.unknownVisitor; + return new Visitor() { + @Override + public T visitField(@Safe String value) { + return fieldVisitor.apply(value); + } + + @Override + public T visitUnknown(String unknownType, Object unknownValue) { + return unknownVisitor.apply(unknownType, unknownValue); + } + }; + } + } + + public interface FieldStageVisitorBuilder { + UnknownStageVisitorBuilder field(@Nonnull Function<@Safe String, T> fieldVisitor); + } + + public interface UnknownStageVisitorBuilder { + Completed_StageVisitorBuilder unknown(@Nonnull BiFunction<@Safe String, Object, T> unknownVisitor); + + Completed_StageVisitorBuilder unknown(@Nonnull Function<@Safe String, T> unknownVisitor); + + Completed_StageVisitorBuilder throwOnUnknown(); + } + + public interface Completed_StageVisitorBuilder { + Visitor build(); + } + + @JsonTypeInfo( + use = JsonTypeInfo.Id.NAME, + include = JsonTypeInfo.As.EXISTING_PROPERTY, + property = "type", + visible = true, + defaultImpl = UnknownWrapper.class) + @JsonSubTypes(@JsonSubTypes.Type(FieldWrapper.class)) + @JsonIgnoreProperties(ignoreUnknown = true) + private interface Base { + T accept(Visitor visitor); + } + + @JsonTypeName("field") + private static final class FieldWrapper implements Base { + private final String value; + + @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) + private FieldWrapper(@JsonSetter("field") @Nonnull String value) { + Preconditions.checkNotNull(value, "field cannot be null"); + this.value = value; + } + + @JsonProperty(value = "type", index = 0) + private String getType() { + return "field"; + } + + @JsonProperty("field") + private String getValue() { + return value; + } + + @Override + public T accept(Visitor visitor) { + return visitor.visitField(value); + } + + @Override + public boolean equals(@Nullable Object other) { + return this == other || (other instanceof FieldWrapper && equalTo((FieldWrapper) other)); + } + + private boolean equalTo(FieldWrapper other) { + return this.value.equals(other.value); + } + + @Override + public int hashCode() { + return this.value.hashCode(); + } + + @Override + public String toString() { + return "FieldWrapper{value: " + value + '}'; + } + } + + private static final class UnknownWrapper implements Base { + private final String type; + + private final Map value; + + @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) + private UnknownWrapper(@JsonProperty("type") String type) { + this(type, new HashMap()); + } + + private UnknownWrapper(@Nonnull String type, @Nonnull Map value) { + Preconditions.checkNotNull(type, "type cannot be null"); + Preconditions.checkNotNull(value, "value cannot be null"); + this.type = type; + this.value = value; + } + + @JsonProperty + private String getType() { + return type; + } + + @JsonAnyGetter + private Map getValue() { + return value; + } + + @JsonAnySetter + private void put(String key, Object val) { + value.put(key, val); + } + + @Override + public T accept(Visitor visitor) { + return visitor.visitUnknown(type, value.get(type)); + } + + @Override + public boolean equals(@Nullable Object other) { + return this == other || (other instanceof UnknownWrapper && equalTo((UnknownWrapper) other)); + } + + private boolean equalTo(UnknownWrapper other) { + return this.type.equals(other.type) && this.value.equals(other.value); + } + + @Override + public int hashCode() { + int hash = 1; + hash = 31 * hash + this.type.hashCode(); + hash = 31 * hash + this.value.hashCode(); + return hash; + } + + @Override + public String toString() { + return "UnknownWrapper{type: " + type + ", value: " + value + '}'; + } + } +} diff --git a/conjure-java-core/src/main/java/com/palantir/conjure/java/ExternalImportFilter.java b/conjure-java-core/src/main/java/com/palantir/conjure/java/ExternalImportFilter.java new file mode 100644 index 000000000..0c0f1f4bc --- /dev/null +++ b/conjure-java-core/src/main/java/com/palantir/conjure/java/ExternalImportFilter.java @@ -0,0 +1,234 @@ +/* + * (c) Copyright 2024 Palantir Technologies Inc. All rights reserved. + * + * 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 + * + * 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 com.palantir.conjure.java; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Lists; +import com.palantir.conjure.spec.AliasDefinition; +import com.palantir.conjure.spec.ArgumentDefinition; +import com.palantir.conjure.spec.ConjureDefinition; +import com.palantir.conjure.spec.EndpointDefinition; +import com.palantir.conjure.spec.EnumDefinition; +import com.palantir.conjure.spec.ErrorDefinition; +import com.palantir.conjure.spec.ExternalReference; +import com.palantir.conjure.spec.FieldDefinition; +import com.palantir.conjure.spec.ListType; +import com.palantir.conjure.spec.LogSafety; +import com.palantir.conjure.spec.MapType; +import com.palantir.conjure.spec.ObjectDefinition; +import com.palantir.conjure.spec.OptionalType; +import com.palantir.conjure.spec.PrimitiveType; +import com.palantir.conjure.spec.ServiceDefinition; +import com.palantir.conjure.spec.SetType; +import com.palantir.conjure.spec.Type; +import com.palantir.conjure.spec.Type.Visitor; +import com.palantir.conjure.spec.TypeDefinition; +import com.palantir.conjure.spec.TypeName; +import com.palantir.conjure.spec.UnionDefinition; +import com.palantir.logsafe.Safe; +import com.palantir.logsafe.SafeArg; +import com.palantir.logsafe.exceptions.SafeIllegalStateException; +import java.util.Optional; + +/** Utility to filter a {@link ConjureDefinition} into an equivalent definition with no external type imports. */ +final class ExternalImportFilter { + + private final Options options; + + ExternalImportFilter(Options options) { + this.options = options; + } + + ConjureDefinition filter(ConjureDefinition definition) { + if (options.externalFallbackTypes()) { + return ConjureDefinition.builder() + .from(definition) + .errors(Lists.transform(definition.getErrors(), this::filter)) + .types(Lists.transform(definition.getTypes(), this::filter)) + .services(Lists.transform(definition.getServices(), this::filter)) + .build(); + } else { + return definition; + } + } + + private ErrorDefinition filter(ErrorDefinition definition) { + return ErrorDefinition.builder() + .from(definition) + .safeArgs(Lists.transform(definition.getSafeArgs(), this::filter)) + .unsafeArgs(Lists.transform(definition.getUnsafeArgs(), this::filter)) + .build(); + } + + private TypeDefinition filter(TypeDefinition definition) { + return definition.accept(new TypeDefinition.Visitor<>() { + @Override + public TypeDefinition visitAlias(AliasDefinition value) { + TypeWithSafety typeWithSafety = filter(value.getAlias()); + return TypeDefinition.alias(AliasDefinition.builder() + .from(value) + .alias(typeWithSafety.type()) + .safety(typeWithSafety.safety().or(value::getSafety)) + .build()); + } + + @Override + public TypeDefinition visitEnum(EnumDefinition value) { + return TypeDefinition.enum_(value); + } + + @Override + public TypeDefinition visitObject(ObjectDefinition value) { + return TypeDefinition.object(ObjectDefinition.builder() + .from(value) + .fields(Lists.transform(value.getFields(), ExternalImportFilter.this::filter)) + .build()); + } + + @Override + public TypeDefinition visitUnion(UnionDefinition value) { + return TypeDefinition.union(UnionDefinition.builder() + .from(value) + .union(Lists.transform(value.getUnion(), ExternalImportFilter.this::filter)) + .build()); + } + + @Override + public TypeDefinition visitUnknown(@Safe String unknownType) { + throw new SafeIllegalStateException("Unknown type", SafeArg.of("unknownType", unknownType)); + } + }); + } + + private ServiceDefinition filter(ServiceDefinition definition) { + return ServiceDefinition.builder() + .from(definition) + .endpoints(Lists.transform(definition.getEndpoints(), this::filter)) + .build(); + } + + private EndpointDefinition filter(EndpointDefinition definition) { + return EndpointDefinition.builder() + .from(definition) + // Remove markers, which are required to be external type imports + .markers(ImmutableList.of()) + .args(Lists.transform(definition.getArgs(), this::filter)) + .build(); + } + + private ArgumentDefinition filter(ArgumentDefinition definition) { + TypeWithSafety typeWithSafety = filter(definition.getType()); + return ArgumentDefinition.builder() + .from(definition) + // Remove markers, which are required to be external type imports + .markers(ImmutableList.of()) + .type(typeWithSafety.type()) + .safety(typeWithSafety.safety().or(definition::getSafety)) + .build(); + } + + private FieldDefinition filter(FieldDefinition definition) { + TypeWithSafety typeWithSafety = filter(definition.getType()); + return FieldDefinition.builder() + .from(definition) + .type(typeWithSafety.type()) + .safety(typeWithSafety.safety().or(definition::getSafety)) + .build(); + } + + private TypeWithSafety filter(Type definition) { + return definition.accept(TypeFilter.INSTANCE).orElseGet(() -> new TypeWithSafety(definition, Optional.empty())); + } + + private enum TypeFilter implements Visitor> { + INSTANCE; + + @Override + public Optional visitPrimitive(PrimitiveType _value) { + return Optional.empty(); + } + + @Override + public Optional visitOptional(OptionalType value) { + return value.getItemType() + .accept(this) + .map(result -> new TypeWithSafety(Type.optional(OptionalType.of(result.type())), result.safety())); + } + + @Override + public Optional visitList(ListType value) { + return value.getItemType() + .accept(this) + .map(result -> new TypeWithSafety(Type.list(ListType.of(result.type())), result.safety())); + } + + @Override + public Optional visitSet(SetType value) { + return value.getItemType() + .accept(this) + .map(result -> new TypeWithSafety(Type.set(SetType.of(result.type())), result.safety())); + } + + @Override + public Optional visitMap(MapType value) { + Optional keyType = value.getKeyType().accept(this); + Optional valueType = value.getValueType().accept(this); + if (keyType.isEmpty() && valueType.isEmpty()) { + return Optional.empty(); + } + return Optional.of(new TypeWithSafety( + Type.map(MapType.of( + keyType.map(TypeWithSafety::type).orElseGet(value::getKeyType), + valueType.map(TypeWithSafety::type).orElseGet(value::getValueType))), + // Safety on maps is tricky due to the rules we apply around what may and may not be marked + Optional.empty())); + } + + @Override + public Optional visitReference(TypeName _value) { + return Optional.empty(); + } + + @Override + public Optional visitExternal(ExternalReference value) { + return Optional.of(new TypeWithSafety(value.getFallback(), value.getSafety())); + } + + @Override + public Optional visitUnknown(@Safe String _unknownType) { + return Optional.empty(); + } + } + + private static final class TypeWithSafety { + private final Type type; + private final Optional safety; + + TypeWithSafety(Type type, Optional safety) { + this.type = type; + this.safety = safety; + } + + Type type() { + return type; + } + + Optional safety() { + return safety; + } + } +} diff --git a/conjure-java-core/src/main/java/com/palantir/conjure/java/GenerationCoordinator.java b/conjure-java-core/src/main/java/com/palantir/conjure/java/GenerationCoordinator.java index b5b71f018..91eaa5304 100644 --- a/conjure-java-core/src/main/java/com/palantir/conjure/java/GenerationCoordinator.java +++ b/conjure-java-core/src/main/java/com/palantir/conjure/java/GenerationCoordinator.java @@ -30,10 +30,16 @@ public class GenerationCoordinator { private final Executor executor; private final Set generators; + private final Options options; - public GenerationCoordinator(Executor executor, Set generators) { + public GenerationCoordinator(Executor executor, Set generators, Options options) { this.executor = executor; this.generators = generators; + this.options = options; + } + + public GenerationCoordinator(Executor executor, Set generators) { + this(executor, generators, Options.empty()); } /** @@ -41,8 +47,9 @@ public GenerationCoordinator(Executor executor, Set generators) { * the instance's service and type generators. */ public List emit(ConjureDefinition conjureDefinition, File outputDir) { + ConjureDefinition definition = new ExternalImportFilter(options).filter(conjureDefinition); return MoreStreams.inCompletionOrder( - generators.stream().flatMap(generator -> generator.generate(conjureDefinition)), + generators.stream().flatMap(generator -> generator.generate(definition)), f -> Goethe.formatAndEmit(f, outputDir.toPath()), executor, Runtime.getRuntime().availableProcessors()) diff --git a/conjure-java-core/src/main/java/com/palantir/conjure/java/Options.java b/conjure-java-core/src/main/java/com/palantir/conjure/java/Options.java index 398aa0e8a..ca5c878aa 100644 --- a/conjure-java-core/src/main/java/com/palantir/conjure/java/Options.java +++ b/conjure-java-core/src/main/java/com/palantir/conjure/java/Options.java @@ -163,6 +163,12 @@ default boolean jetbrainsContractAnnotations() { return false; } + /** When set, external type imports are generated as their fallback types. */ + @Value.Default + default boolean externalFallbackTypes() { + return false; + } + Optional packagePrefix(); Optional apiVersion(); diff --git a/conjure-java-core/src/test/java/com/palantir/conjure/java/types/ExternalTypeFallbackTests.java b/conjure-java-core/src/test/java/com/palantir/conjure/java/types/ExternalTypeFallbackTests.java new file mode 100644 index 000000000..e0e7f2e61 --- /dev/null +++ b/conjure-java-core/src/test/java/com/palantir/conjure/java/types/ExternalTypeFallbackTests.java @@ -0,0 +1,79 @@ +/* + * (c) Copyright 2018 Palantir Technologies Inc. All rights reserved. + * + * 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 + * + * 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 com.palantir.conjure.java.types; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import com.google.common.util.concurrent.MoreExecutors; +import com.palantir.conjure.defs.Conjure; +import com.palantir.conjure.java.GenerationCoordinator; +import com.palantir.conjure.java.Options; +import com.palantir.conjure.java.services.dialogue.DialogueServiceGenerator; +import com.palantir.conjure.spec.ConjureDefinition; +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.List; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +public final class ExternalTypeFallbackTests { + + private static final String REFERENCE_FILES_FOLDER = "src/integrationInput/java"; + + @TempDir + public File tempDir; + + @Test + public void testExternalFallbackTypes() throws IOException { + Options options = Options.builder() + .useImmutableBytes(true) + .strictObjects(true) + .nonNullCollections(true) + .excludeEmptyOptionals(true) + .unionsWithUnknownValues(true) + .jetbrainsContractAnnotations(true) + .externalFallbackTypes(true) + .build(); + ConjureDefinition def = Conjure.parse(ImmutableList.of(new File("src/test/resources/external-types.yml"))); + List files = new GenerationCoordinator( + MoreExecutors.directExecutor(), + ImmutableSet.of(new ObjectGenerator(options), new DialogueServiceGenerator(options)), + options) + .emit(def, tempDir); + + assertThatFilesAreTheSame(files, REFERENCE_FILES_FOLDER); + } + + private void assertThatFilesAreTheSame(List files, String referenceFilesFolder) throws IOException { + for (Path file : files) { + Path relativized = tempDir.toPath().relativize(file); + Path expectedFile = Paths.get(referenceFilesFolder, relativized.toString()); + if (Boolean.valueOf(System.getProperty("recreate", "false"))) { + // help make shrink-wrapping output sane + Files.createDirectories(expectedFile.getParent()); + Files.deleteIfExists(expectedFile); + Files.copy(file, expectedFile); + } + assertThat(file).hasSameContentAs(expectedFile); + } + } +} diff --git a/conjure-java-core/src/test/resources/external-types.yml b/conjure-java-core/src/test/resources/external-types.yml new file mode 100644 index 000000000..6a25e9ca8 --- /dev/null +++ b/conjure-java-core/src/test/resources/external-types.yml @@ -0,0 +1,39 @@ +types: + imports: + ExternalLong: + base-type: string + safety: safe + external: + java: java.lang.Long + definitions: + default-package: com.palantir.product.external + errors: + ErrorWithExternalRef: + namespace: Conjure + code: INVALID_ARGUMENT + docs: Invalid Conjure type definition. + safe-args: + arg: ExternalLong + objects: + ObjectWithExternalField: + fields: + field: ExternalLong + AliasToExternal: + alias: ExternalLong + UnionWithExternal: + docs: A type which can either be a StringExample, a set of strings, or an integer. + union: + field: ExternalLong +services: + ServiceUsingExternalTypes: + name: ServiceUsingExternalTypes + default-auth: none + base-path: / + package: com.palantir.product.external + endpoints: + external: + http: PUT /external/{path} + args: + path: ExternalLong + body: list + returns: map \ No newline at end of file diff --git a/conjure-java/src/main/java/com/palantir/conjure/java/cli/ConjureJavaCli.java b/conjure-java/src/main/java/com/palantir/conjure/java/cli/ConjureJavaCli.java index 533e999cc..101a7ee20 100644 --- a/conjure-java/src/main/java/com/palantir/conjure/java/cli/ConjureJavaCli.java +++ b/conjure-java/src/main/java/com/palantir/conjure/java/cli/ConjureJavaCli.java @@ -223,6 +223,12 @@ public static final class GenerateCommand implements Runnable { description = "Union visitors expose the values of unknowns in addition to their types.") private boolean unionsWithUnknownValues; + @CommandLine.Option( + names = "--externalFallbackTypes", + defaultValue = "false", + description = "Java external type imports are generated using their fallback type.") + private boolean externalFallbackTypes; + @SuppressWarnings("unused") @CommandLine.Unmatched private List unmatchedOptions; @@ -253,7 +259,7 @@ public void run() { if (config.generateDialogue()) { generatorBuilder.add(new DialogueServiceGenerator(config.options())); } - new GenerationCoordinator(executor, generatorBuilder.build()) + new GenerationCoordinator(executor, generatorBuilder.build(), config.options()) .emit(conjureDefinition, config.outputDirectory()); } catch (IOException e) { throw new SafeRuntimeException("Error parsing definition", e); @@ -290,6 +296,7 @@ CliConfiguration getConfiguration() { .excludeEmptyOptionals(excludeEmptyOptionals) .excludeEmptyCollections(excludeEmptyCollections) .unionsWithUnknownValues(unionsWithUnknownValues) + .externalFallbackTypes(externalFallbackTypes) .build()) .build(); } diff --git a/readme.md b/readme.md index ea533b3f2..1db1a6f2a 100644 --- a/readme.md +++ b/readme.md @@ -44,6 +44,8 @@ The recommended way to use conjure-java is via a build tool like [gradle-conjure --jakartaPackages Generates jax-rs annotated interfaces which use the newer 'jakarta` packages instead of the legacy 'javax' packages. + --externalFallbackTypes + Java external type imports are generated using their fallback type. ### Known Tag Values