diff --git a/openapi-generics-server-starter/src/main/java/io/github/blueprintplatform/openapi/generics/server/autoconfigure/OpenApiGenericsAutoConfiguration.java b/openapi-generics-server-starter/src/main/java/io/github/blueprintplatform/openapi/generics/server/autoconfigure/OpenApiGenericsAutoConfiguration.java
index 031e7560..cc662be5 100644
--- a/openapi-generics-server-starter/src/main/java/io/github/blueprintplatform/openapi/generics/server/autoconfigure/OpenApiGenericsAutoConfiguration.java
+++ b/openapi-generics-server-starter/src/main/java/io/github/blueprintplatform/openapi/generics/server/autoconfigure/OpenApiGenericsAutoConfiguration.java
@@ -2,11 +2,11 @@
import io.github.blueprintplatform.openapi.generics.server.autoconfigure.properties.OpenApiGenericsProperties;
import io.github.blueprintplatform.openapi.generics.server.core.introspection.*;
-import io.github.blueprintplatform.openapi.generics.server.core.introspection.container.DefaultSupportedContainerTypesResolver;
-import io.github.blueprintplatform.openapi.generics.server.core.introspection.container.SupportedContainerTypesResolver;
+import io.github.blueprintplatform.openapi.generics.server.core.introspection.container.resolver.ConfiguredContainerTypesResolver;
+import io.github.blueprintplatform.openapi.generics.server.core.introspection.container.resolver.DefaultSupportedContainerTypesResolver;
+import io.github.blueprintplatform.openapi.generics.server.core.introspection.container.resolver.SupportedContainerTypesResolver;
import io.github.blueprintplatform.openapi.generics.server.core.pipeline.OpenApiPipelineOrchestrator;
import io.github.blueprintplatform.openapi.generics.server.core.schema.ContractSchemaExclusionApplier;
-import io.github.blueprintplatform.openapi.generics.server.core.schema.WrapperSchemaEnricher;
import io.github.blueprintplatform.openapi.generics.server.core.schema.WrapperSchemaProcessor;
import io.github.blueprintplatform.openapi.generics.server.core.validation.OpenApiContractGuard;
import io.github.blueprintplatform.openapi.generics.server.mvc.MvcResponseTypeDiscoveryStrategy;
@@ -20,7 +20,13 @@
import org.springframework.context.annotation.Bean;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
-@AutoConfiguration
+/**
+ * Main auto-configuration for OpenAPI Generics server support.
+ *
+ *
Registers response type discovery, generic response introspection, contract guarding, and the
+ * OpenAPI customization pipeline.
+ */
+@AutoConfiguration(after = OpenApiGenericsSchemaAutoConfiguration.class)
@ConditionalOnClass(OpenApiCustomizer.class)
@ConditionalOnWebApplication
@EnableConfigurationProperties(OpenApiGenericsProperties.class)
@@ -40,11 +46,19 @@ public SupportedContainerTypesResolver supportedContainerTypesResolver() {
return new DefaultSupportedContainerTypesResolver();
}
+ @Bean
+ @ConditionalOnMissingBean
+ public ConfiguredContainerTypesResolver configuredContainerTypesResolver() {
+ return new ConfiguredContainerTypesResolver();
+ }
+
@Bean
@ConditionalOnMissingBean
public ResponseIntrospectionPolicyResolver responseIntrospectionPolicyResolver(
- SupportedContainerTypesResolver supportedContainerTypesResolver) {
- return new ResponseIntrospectionPolicyResolver(supportedContainerTypesResolver);
+ SupportedContainerTypesResolver supportedContainerTypesResolver,
+ ConfiguredContainerTypesResolver configuredContainerTypesResolver) {
+ return new ResponseIntrospectionPolicyResolver(
+ supportedContainerTypesResolver, configuredContainerTypesResolver);
}
@Bean
@@ -66,12 +80,6 @@ public ContractSchemaExclusionApplier schemaGenerationControlMarker() {
return new ContractSchemaExclusionApplier();
}
- @Bean
- @ConditionalOnMissingBean
- public WrapperSchemaProcessor wrapperSchemaProcessor(WrapperSchemaEnricher enricher) {
- return new WrapperSchemaProcessor(enricher);
- }
-
@Bean
@ConditionalOnMissingBean
public OpenApiContractGuard openApiContractGuard() {
diff --git a/openapi-generics-server-starter/src/main/java/io/github/blueprintplatform/openapi/generics/server/autoconfigure/OpenApiGenericsMissingDependencyAutoConfiguration.java b/openapi-generics-server-starter/src/main/java/io/github/blueprintplatform/openapi/generics/server/autoconfigure/OpenApiGenericsMissingDependencyAutoConfiguration.java
index a424cff4..36a5b290 100644
--- a/openapi-generics-server-starter/src/main/java/io/github/blueprintplatform/openapi/generics/server/autoconfigure/OpenApiGenericsMissingDependencyAutoConfiguration.java
+++ b/openapi-generics-server-starter/src/main/java/io/github/blueprintplatform/openapi/generics/server/autoconfigure/OpenApiGenericsMissingDependencyAutoConfiguration.java
@@ -48,20 +48,16 @@ public class OpenApiGenericsMissingDependencyAutoConfiguration {
This starter activates only when Springdoc is present.
- To enable OpenAPI customization, add one of the following:
-
- - org.springdoc:springdoc-openapi-starter-webmvc-ui
- - org.springdoc:springdoc-openapi-starter-webflux-ui
+ To enable OpenAPI customization, add dependency on Springdoc OpenAPI in your build configuration:
+ - For Maven:
+
+ org.springdoc
+ springdoc-openapi-starter-webmvc-ui
+
-------------------------------------------------------------------------
""";
- /**
- * Logs a warning message indicating that Springdoc is missing.
- *
- *
This method is invoked after bean initialization and provides a clear diagnostic message to
- * the user.
- */
@PostConstruct
public void logWarning() {
log.warn(MESSAGE);
diff --git a/openapi-generics-server-starter/src/main/java/io/github/blueprintplatform/openapi/generics/server/autoconfigure/OpenApiGenericsSchemaAutoConfiguration.java b/openapi-generics-server-starter/src/main/java/io/github/blueprintplatform/openapi/generics/server/autoconfigure/OpenApiGenericsSchemaAutoConfiguration.java
index d09f2f38..3142aef2 100644
--- a/openapi-generics-server-starter/src/main/java/io/github/blueprintplatform/openapi/generics/server/autoconfigure/OpenApiGenericsSchemaAutoConfiguration.java
+++ b/openapi-generics-server-starter/src/main/java/io/github/blueprintplatform/openapi/generics/server/autoconfigure/OpenApiGenericsSchemaAutoConfiguration.java
@@ -1,12 +1,11 @@
package io.github.blueprintplatform.openapi.generics.server.autoconfigure;
-import io.github.blueprintplatform.openapi.generics.server.core.schema.WrapperSchemaEnricher;
-import io.github.blueprintplatform.openapi.generics.server.core.schema.extractor.ContentArrayItemExtractor;
-import io.github.blueprintplatform.openapi.generics.server.core.schema.extractor.DirectArrayItemExtractor;
-import io.github.blueprintplatform.openapi.generics.server.core.schema.resolver.ComponentContainerSchemaResolver;
-import io.github.blueprintplatform.openapi.generics.server.core.schema.resolver.WrapperPayloadArraySchemaResolver;
-import io.github.blueprintplatform.openapi.generics.server.core.schema.strategy.*;
-import java.util.List;
+import io.github.blueprintplatform.openapi.generics.server.core.schema.WrapperSchemaProcessor;
+import io.github.blueprintplatform.openapi.generics.server.core.schema.enrichment.ContainerSchemaMetadataResolver;
+import io.github.blueprintplatform.openapi.generics.server.core.schema.enrichment.WrapperSchemaEnricher;
+import io.github.blueprintplatform.openapi.generics.server.core.schema.extraction.ArrayItemReferenceExtractor;
+import io.github.blueprintplatform.openapi.generics.server.core.schema.resolution.ComponentContainerSchemaResolver;
+import io.github.blueprintplatform.openapi.generics.server.core.schema.resolution.WrapperPayloadArraySchemaResolver;
import org.springdoc.core.customizers.OpenApiCustomizer;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
@@ -14,21 +13,21 @@
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.context.annotation.Bean;
+/**
+ * Schema auto-configuration for OpenAPI Generics server support.
+ *
+ *
Registers schema resolution and enrichment components used to process projected generic
+ * wrapper schemas.
+ */
@AutoConfiguration(before = OpenApiGenericsAutoConfiguration.class)
@ConditionalOnClass(OpenApiCustomizer.class)
@ConditionalOnWebApplication
public class OpenApiGenericsSchemaAutoConfiguration {
@Bean
- @ConditionalOnMissingBean(ContentArrayItemExtractor.class)
- public ContentArrayItemExtractor contentArrayItemExtractor() {
- return new ContentArrayItemExtractor();
- }
-
- @Bean
- @ConditionalOnMissingBean(DirectArrayItemExtractor.class)
- public DirectArrayItemExtractor directArrayItemExtractor() {
- return new DirectArrayItemExtractor();
+ @ConditionalOnMissingBean(ArrayItemReferenceExtractor.class)
+ public ArrayItemReferenceExtractor arrayItemReferenceExtractor() {
+ return new ArrayItemReferenceExtractor();
}
@Bean
@@ -44,41 +43,27 @@ public WrapperPayloadArraySchemaResolver wrapperPayloadArraySchemaResolver() {
}
@Bean
- @ConditionalOnMissingBean(PageContainerSchemaStrategy.class)
- public PageContainerSchemaStrategy pageContainerSchemaStrategy(
- ComponentContainerSchemaResolver componentContainerSchemaResolver,
- ContentArrayItemExtractor contentArrayItemExtractor) {
- return new PageContainerSchemaStrategy(
- componentContainerSchemaResolver, contentArrayItemExtractor);
- }
-
- @Bean
- @ConditionalOnMissingBean(ListContainerSchemaStrategy.class)
- public ListContainerSchemaStrategy listContainerSchemaStrategy(
- WrapperPayloadArraySchemaResolver wrapperPayloadArraySchemaResolver,
- DirectArrayItemExtractor directArrayItemExtractor) {
- return new ListContainerSchemaStrategy(
- wrapperPayloadArraySchemaResolver, directArrayItemExtractor);
- }
-
- @Bean
- @ConditionalOnMissingBean(SetContainerSchemaStrategy.class)
- public SetContainerSchemaStrategy setContainerSchemaStrategy(
+ @ConditionalOnMissingBean
+ public ContainerSchemaMetadataResolver containerSchemaMetadataResolver(
WrapperPayloadArraySchemaResolver wrapperPayloadArraySchemaResolver,
- DirectArrayItemExtractor directArrayItemExtractor) {
- return new SetContainerSchemaStrategy(
- wrapperPayloadArraySchemaResolver, directArrayItemExtractor);
+ ComponentContainerSchemaResolver componentContainerSchemaResolver,
+ ArrayItemReferenceExtractor arrayItemReferenceExtractor) {
+ return new ContainerSchemaMetadataResolver(
+ wrapperPayloadArraySchemaResolver,
+ componentContainerSchemaResolver,
+ arrayItemReferenceExtractor);
}
@Bean
@ConditionalOnMissingBean
- public ContainerSchemaRegistry containerSchemaRegistry(List strategies) {
- return new ContainerSchemaRegistry(strategies);
+ public WrapperSchemaEnricher wrapperSchemaEnricher(
+ ContainerSchemaMetadataResolver containerSchemaMetadataResolver) {
+ return new WrapperSchemaEnricher(containerSchemaMetadataResolver);
}
@Bean
@ConditionalOnMissingBean
- public WrapperSchemaEnricher wrapperSchemaEnricher(ContainerSchemaRegistry registry) {
- return new WrapperSchemaEnricher(registry);
+ public WrapperSchemaProcessor wrapperSchemaProcessor(WrapperSchemaEnricher enricher) {
+ return new WrapperSchemaProcessor(enricher);
}
}
diff --git a/openapi-generics-server-starter/src/main/java/io/github/blueprintplatform/openapi/generics/server/autoconfigure/properties/ContainerProperties.java b/openapi-generics-server-starter/src/main/java/io/github/blueprintplatform/openapi/generics/server/autoconfigure/properties/ContainerProperties.java
new file mode 100644
index 00000000..11c381cd
--- /dev/null
+++ b/openapi-generics-server-starter/src/main/java/io/github/blueprintplatform/openapi/generics/server/autoconfigure/properties/ContainerProperties.java
@@ -0,0 +1,12 @@
+package io.github.blueprintplatform.openapi.generics.server.autoconfigure.properties;
+
+/**
+ * Configuration describing a custom generic container contract.
+ *
+ * Each configured container becomes eligible for deterministic generic reconstruction during
+ * OpenAPI projection.
+ *
+ * @param type fully-qualified generic container class name
+ * @param itemProperty JSON property containing the generic item collection
+ */
+public record ContainerProperties(String type, String itemProperty) {}
diff --git a/openapi-generics-server-starter/src/main/java/io/github/blueprintplatform/openapi/generics/server/autoconfigure/properties/OpenApiGenericsProperties.java b/openapi-generics-server-starter/src/main/java/io/github/blueprintplatform/openapi/generics/server/autoconfigure/properties/OpenApiGenericsProperties.java
index 557d036c..8b9cf340 100644
--- a/openapi-generics-server-starter/src/main/java/io/github/blueprintplatform/openapi/generics/server/autoconfigure/properties/OpenApiGenericsProperties.java
+++ b/openapi-generics-server-starter/src/main/java/io/github/blueprintplatform/openapi/generics/server/autoconfigure/properties/OpenApiGenericsProperties.java
@@ -1,6 +1,7 @@
package io.github.blueprintplatform.openapi.generics.server.autoconfigure.properties;
import jakarta.validation.Valid;
+import java.util.List;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.validation.annotation.Validated;
@@ -8,7 +9,9 @@
* Configuration properties for the OpenAPI Generics server starter.
*
* @param envelope envelope-related configuration
+ * @param containers custom generic container contract configuration
*/
@Validated
@ConfigurationProperties(prefix = "openapi-generics")
-public record OpenApiGenericsProperties(@Valid EnvelopeProperties envelope) {}
+public record OpenApiGenericsProperties(
+ @Valid EnvelopeProperties envelope, @Valid List containers) {}
diff --git a/openapi-generics-server-starter/src/main/java/io/github/blueprintplatform/openapi/generics/server/core/introspection/ResponseIntrospectionPolicy.java b/openapi-generics-server-starter/src/main/java/io/github/blueprintplatform/openapi/generics/server/core/introspection/ResponseIntrospectionPolicy.java
index 7480a42e..f1fd5cf8 100644
--- a/openapi-generics-server-starter/src/main/java/io/github/blueprintplatform/openapi/generics/server/core/introspection/ResponseIntrospectionPolicy.java
+++ b/openapi-generics-server-starter/src/main/java/io/github/blueprintplatform/openapi/generics/server/core/introspection/ResponseIntrospectionPolicy.java
@@ -1,36 +1,16 @@
package io.github.blueprintplatform.openapi.generics.server.core.introspection;
-import io.github.blueprintplatform.openapi.generics.server.core.introspection.container.SupportedContainerType;
+import io.github.blueprintplatform.openapi.generics.server.core.introspection.container.descriptor.SupportedContainerDescriptor;
import java.util.Set;
/**
- * Immutable configuration describing how response types should be interpreted by the introspection
- * pipeline.
+ * Defines the active response introspection policy.
*
- * This policy defines:
- *
- *
- * - Which envelope type should be recognized during response analysis
- *
- Which property inside the envelope represents the payload
- *
- Which container types are supported for generic reconstruction
- *
- *
- * For the default platform configuration this typically corresponds to:
- *
- *
{@code
- * ServiceResponse
- * ServiceResponse>
- * ServiceResponse>
- * }
- *
- * Additional containers may be contributed through the {@link SupportedContainerType} model
- * without changing the core introspection algorithm.
- *
- * @param envelopeType envelope type to detect (for example {@code ServiceResponse})
- * @param payloadPropertyName property representing the payload within the envelope
- * @param supportedContainers supported generic container definitions
+ * @param envelopeType active response envelope type
+ * @param payloadPropertyName JSON property carrying the envelope payload
+ * @param supportedContainers supported generic container contracts
*/
public record ResponseIntrospectionPolicy(
Class> envelopeType,
String payloadPropertyName,
- Set supportedContainers) {}
+ Set supportedContainers) {}
diff --git a/openapi-generics-server-starter/src/main/java/io/github/blueprintplatform/openapi/generics/server/core/introspection/ResponseIntrospectionPolicyResolver.java b/openapi-generics-server-starter/src/main/java/io/github/blueprintplatform/openapi/generics/server/core/introspection/ResponseIntrospectionPolicyResolver.java
index f2c3909e..f4963380 100644
--- a/openapi-generics-server-starter/src/main/java/io/github/blueprintplatform/openapi/generics/server/core/introspection/ResponseIntrospectionPolicyResolver.java
+++ b/openapi-generics-server-starter/src/main/java/io/github/blueprintplatform/openapi/generics/server/core/introspection/ResponseIntrospectionPolicyResolver.java
@@ -3,7 +3,9 @@
import io.github.blueprintplatform.openapi.generics.contract.envelope.ServiceResponse;
import io.github.blueprintplatform.openapi.generics.server.autoconfigure.properties.EnvelopeProperties;
import io.github.blueprintplatform.openapi.generics.server.autoconfigure.properties.OpenApiGenericsProperties;
-import io.github.blueprintplatform.openapi.generics.server.core.introspection.container.SupportedContainerTypesResolver;
+import io.github.blueprintplatform.openapi.generics.server.core.introspection.container.descriptor.SupportedContainerDescriptor;
+import io.github.blueprintplatform.openapi.generics.server.core.introspection.container.resolver.ConfiguredContainerTypesResolver;
+import io.github.blueprintplatform.openapi.generics.server.core.introspection.container.resolver.SupportedContainerTypesResolver;
import io.github.blueprintplatform.openapi.generics.server.core.schema.constant.PropertyNames;
import java.lang.reflect.Field;
import java.lang.reflect.GenericArrayType;
@@ -11,56 +13,61 @@
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.lang.reflect.TypeVariable;
+import java.util.LinkedHashSet;
+import java.util.Set;
/**
- * Resolves and validates the response envelope type used for contract introspection.
+ * Resolves the response introspection policy from built-in defaults and application configuration.
*
- * Supports both the default {@code ServiceResponse} and custom envelopes (BYOE), ensuring
- * they comply with strict structural constraints:
- *
- *
- * - Must be a concrete class
- *
- Must declare exactly one type parameter
- *
- Must contain exactly one direct payload field of type T
- *
- Nested generic payloads are not supported
- *
- *
- * Produces a {@link ResponseIntrospectionPolicy} used by the introspection pipeline.
+ *
Determines the response envelope contract, payload property, and supported generic container
+ * types used during response type introspection.
*/
public class ResponseIntrospectionPolicyResolver {
private final SupportedContainerTypesResolver supportedContainerTypesResolver;
+ private final ConfiguredContainerTypesResolver configuredContainerTypesResolver;
public ResponseIntrospectionPolicyResolver(
- SupportedContainerTypesResolver supportedContainerTypesResolver) {
+ SupportedContainerTypesResolver supportedContainerTypesResolver,
+ ConfiguredContainerTypesResolver configuredContainerTypesResolver) {
this.supportedContainerTypesResolver = supportedContainerTypesResolver;
+ this.configuredContainerTypesResolver = configuredContainerTypesResolver;
}
public ResponseIntrospectionPolicy resolve(OpenApiGenericsProperties properties) {
String configuredType = extractConfiguredEnvelopeType(properties);
+ Set supportedContainers = resolveSupportedContainers(properties);
if (configuredType == null) {
return new ResponseIntrospectionPolicy(
- ServiceResponse.class, PropertyNames.DATA, supportedContainerTypesResolver.resolve());
+ ServiceResponse.class, PropertyNames.DATA, supportedContainers);
}
Class> envelopeType = resolveExternalEnvelopeType(configuredType);
String payloadPropertyName = validateExternalEnvelopeType(envelopeType);
- return new ResponseIntrospectionPolicy(
- envelopeType, payloadPropertyName, supportedContainerTypesResolver.resolve());
+ return new ResponseIntrospectionPolicy(envelopeType, payloadPropertyName, supportedContainers);
}
- private String extractConfiguredEnvelopeType(OpenApiGenericsProperties properties) {
- if (properties == null) {
- return null;
+ private Set resolveSupportedContainers(
+ OpenApiGenericsProperties properties) {
+
+ Set containers =
+ new LinkedHashSet<>(supportedContainerTypesResolver.resolve());
+
+ if (properties != null) {
+ containers.addAll(configuredContainerTypesResolver.resolve(properties.containers()));
}
- EnvelopeProperties envelope = properties.envelope();
- if (envelope == null) {
+ return Set.copyOf(containers);
+ }
+
+ private String extractConfiguredEnvelopeType(OpenApiGenericsProperties properties) {
+ if (properties == null || properties.envelope() == null) {
return null;
}
+ EnvelopeProperties envelope = properties.envelope();
String type = envelope.type();
return (type == null || type.isBlank()) ? null : type;
}
@@ -76,14 +83,14 @@ private Class> resolveExternalEnvelopeType(String configuredType) {
+ configuredType
+ "'. Expected fully-qualified class name (e.g. com.example.ApiResponse)");
}
+
try {
return Class.forName(configuredType);
} catch (ClassNotFoundException e) {
throw new IllegalStateException(
"Configured envelope class not found: '"
+ configuredType
- + "'. "
- + "Ensure the class exists and is on the application classpath.",
+ + "'. Ensure the class exists and is on the application classpath.",
e);
}
}
@@ -124,7 +131,6 @@ private TypeVariable> validateSingleTypeParameter(Class> envelopeType) {
private String validateSingleDirectPayloadSlot(
Class> envelopeType, TypeVariable> payloadTypeParameter) {
-
String payloadPropertyName = null;
for (Field field : envelopeType.getDeclaredFields()) {
@@ -137,13 +143,16 @@ private String validateSingleDirectPayloadSlot(
"contains unsupported nested generic payload slot in field '"
+ field.getName()
+ "'");
- } else if (kind == PayloadSlotKind.DIRECT) {
+ }
+
+ if (kind == PayloadSlotKind.DIRECT) {
if (payloadPropertyName != null) {
throw invalidEnvelope(
envelopeType,
"must declare exactly one direct payload field of type "
+ payloadTypeParameter.getName());
}
+
payloadPropertyName = field.getName();
}
}
diff --git a/openapi-generics-server-starter/src/main/java/io/github/blueprintplatform/openapi/generics/server/core/introspection/ResponseTypeDescriptor.java b/openapi-generics-server-starter/src/main/java/io/github/blueprintplatform/openapi/generics/server/core/introspection/ResponseTypeDescriptor.java
index 2d6ca683..5d4947b4 100644
--- a/openapi-generics-server-starter/src/main/java/io/github/blueprintplatform/openapi/generics/server/core/introspection/ResponseTypeDescriptor.java
+++ b/openapi-generics-server-starter/src/main/java/io/github/blueprintplatform/openapi/generics/server/core/introspection/ResponseTypeDescriptor.java
@@ -1,13 +1,13 @@
package io.github.blueprintplatform.openapi.generics.server.core.introspection;
-import io.github.blueprintplatform.openapi.generics.server.core.introspection.container.SupportedContainerType;
+import io.github.blueprintplatform.openapi.generics.server.core.introspection.container.descriptor.SupportedContainerDescriptor;
import java.util.Objects;
/**
* Describes a supported response shape discovered during introspection.
*
* For container responses, {@code dataRefName} follows the OpenAPI schema name produced by
- * springdoc, {@code containerType} preserves the Java container identity discovered during
+ * springdoc, {@code container} preserves the Java container identity discovered during
* introspection, and {@code itemRefName} represents the contained item schema name.
*/
public final class ResponseTypeDescriptor {
@@ -15,19 +15,19 @@ public final class ResponseTypeDescriptor {
private final Class> envelopeType;
private final String payloadPropertyName;
private final String dataRefName;
- private final SupportedContainerType containerType;
+ private final SupportedContainerDescriptor container;
private final String itemRefName;
private ResponseTypeDescriptor(
Class> envelopeType,
String payloadPropertyName,
String dataRefName,
- SupportedContainerType containerType,
+ SupportedContainerDescriptor container,
String itemRefName) {
this.envelopeType = envelopeType;
this.payloadPropertyName = payloadPropertyName;
this.dataRefName = dataRefName;
- this.containerType = containerType;
+ this.container = container;
this.itemRefName = itemRefName;
}
@@ -39,13 +39,13 @@ public static ResponseTypeDescriptor simple(
public static ResponseTypeDescriptor container(
Class> envelopeType,
String payloadPropertyName,
- SupportedContainerType containerType,
+ SupportedContainerDescriptor container,
String itemRefName) {
return new ResponseTypeDescriptor(
envelopeType,
payloadPropertyName,
- containerType.schemaName() + itemRefName,
- containerType,
+ container.schemaName() + itemRefName,
+ container,
itemRefName);
}
@@ -61,16 +61,16 @@ public String dataRefName() {
return dataRefName;
}
- public SupportedContainerType containerType() {
- return containerType;
+ public SupportedContainerDescriptor container() {
+ return container;
}
public String containerName() {
- return containerType != null ? containerType.containerName() : null;
+ return container != null ? container.containerName() : null;
}
public String containerTypeName() {
- return containerType != null ? containerType.containerTypeName() : null;
+ return container != null ? container.containerTypeName() : null;
}
public String itemRefName() {
@@ -78,7 +78,7 @@ public String itemRefName() {
}
public boolean isContainer() {
- return containerType != null && itemRefName != null;
+ return container != null && itemRefName != null;
}
@Override
@@ -88,13 +88,13 @@ public boolean equals(Object o) {
return Objects.equals(envelopeType, that.envelopeType)
&& Objects.equals(payloadPropertyName, that.payloadPropertyName)
&& Objects.equals(dataRefName, that.dataRefName)
- && Objects.equals(containerType, that.containerType)
+ && Objects.equals(container, that.container)
&& Objects.equals(itemRefName, that.itemRefName);
}
@Override
public int hashCode() {
- return Objects.hash(envelopeType, payloadPropertyName, dataRefName, containerType, itemRefName);
+ return Objects.hash(envelopeType, payloadPropertyName, dataRefName, container, itemRefName);
}
@Override
diff --git a/openapi-generics-server-starter/src/main/java/io/github/blueprintplatform/openapi/generics/server/core/introspection/ResponseTypeIntrospector.java b/openapi-generics-server-starter/src/main/java/io/github/blueprintplatform/openapi/generics/server/core/introspection/ResponseTypeIntrospector.java
index 6f674bf9..4e57bce1 100644
--- a/openapi-generics-server-starter/src/main/java/io/github/blueprintplatform/openapi/generics/server/core/introspection/ResponseTypeIntrospector.java
+++ b/openapi-generics-server-starter/src/main/java/io/github/blueprintplatform/openapi/generics/server/core/introspection/ResponseTypeIntrospector.java
@@ -1,6 +1,6 @@
package io.github.blueprintplatform.openapi.generics.server.core.introspection;
-import io.github.blueprintplatform.openapi.generics.server.core.introspection.container.SupportedContainerType;
+import io.github.blueprintplatform.openapi.generics.server.core.introspection.container.descriptor.SupportedContainerDescriptor;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.Optional;
@@ -15,41 +15,10 @@
import org.springframework.web.context.request.async.WebAsyncTask;
/**
- * Extracts contract-aware response type metadata from controller return types.
+ * Introspects generic response types and produces descriptors used during OpenAPI projection.
*
- *
Unwraps framework-level wrappers (for example {@code ResponseEntity}, {@code CompletionStage},
- * {@code Future}, {@code DeferredResult}, and {@code WebAsyncTask}) before analyzing the actual
- * contract response shape.
- *
- *
Produces a {@link ResponseTypeDescriptor} only for response structures that are explicitly
- * supported by the active {@link ResponseIntrospectionPolicy}.
- *
- *
For the default platform envelope, supported shapes are:
- *
- *
- * - {@code ServiceResponse}
- *
- {@code ServiceResponse>}
- *
- {@code ServiceResponse
>}
- *
- *
- * For custom BYOE envelopes, supported shapes are limited to:
- *
- *
- * - {@code YourEnvelope}
- *
- *
- * Nested container payloads are intentionally unsupported, including:
- *
- *
- * - {@code ServiceResponse
>>}
- * - {@code ServiceResponse>>}
- *
- {@code YourEnvelope>}
- *
- {@code YourEnvelope
>}
- *
- *
- * Enum payloads are supported only when published as reusable OpenAPI schema components (for
- * example via {@code @Schema(enumAsRef = true)}). Inline enum schemas are ignored because they do
- * not produce stable component identities required by the projection pipeline.
+ *
Recognizes the configured response envelope, supported generic container contracts, and
+ * payload types after unwrapping common asynchronous response wrappers.
*/
public final class ResponseTypeIntrospector {
@@ -58,7 +27,7 @@ public final class ResponseTypeIntrospector {
private static final String SCHEMA_ANNOTATION = "io.swagger.v3.oas.annotations.media.Schema";
private final Class> envelopeType;
- private final Set supportedContainers;
+ private final Set supportedContainers;
private final String payloadPropertyName;
public ResponseTypeIntrospector(ResponseIntrospectionPolicy policy) {
@@ -87,12 +56,14 @@ public Optional extract(ResolvableType type) {
descriptorOpt.map(Object::toString).orElse(""),
descriptorOpt.map(ResponseTypeDescriptor::dataRefName).orElse(""));
}
+
return descriptorOpt;
}
private ResolvableType unwrap(ResolvableType type) {
for (int i = 0; i < MAX_UNWRAP_DEPTH; i++) {
Class> raw = type.resolve();
+
if (raw == null || envelopeType.isAssignableFrom(raw)) {
return type;
}
@@ -145,13 +116,13 @@ private Optional buildDescriptor(ResolvableType dataType
private Optional buildContainerDescriptor(
ResolvableType dataType, Class> raw) {
-
- for (SupportedContainerType containerType : supportedContainers) {
- if (!containerType.matches(raw)) {
+ for (SupportedContainerDescriptor container : supportedContainers) {
+ if (!container.matches(raw)) {
continue;
}
ResolvableType itemType = safeGeneric(dataType);
+
if (itemType.hasGenerics()) {
return Optional.empty();
}
@@ -163,7 +134,7 @@ private Optional buildContainerDescriptor(
return Optional.of(
ResponseTypeDescriptor.container(
- envelopeType, payloadPropertyName, containerType, itemRaw.getSimpleName()));
+ envelopeType, payloadPropertyName, container, itemRaw.getSimpleName()));
}
return Optional.empty();
@@ -203,6 +174,7 @@ private ResolvableType safeGeneric(ResolvableType type) {
if (!type.hasGenerics()) {
return ResolvableType.NONE;
}
+
return type.getGeneric(0);
}
diff --git a/openapi-generics-server-starter/src/main/java/io/github/blueprintplatform/openapi/generics/server/core/introspection/container/DefaultSupportedContainerTypesResolver.java b/openapi-generics-server-starter/src/main/java/io/github/blueprintplatform/openapi/generics/server/core/introspection/container/DefaultSupportedContainerTypesResolver.java
deleted file mode 100644
index c5332fdc..00000000
--- a/openapi-generics-server-starter/src/main/java/io/github/blueprintplatform/openapi/generics/server/core/introspection/container/DefaultSupportedContainerTypesResolver.java
+++ /dev/null
@@ -1,23 +0,0 @@
-package io.github.blueprintplatform.openapi.generics.server.core.introspection.container;
-
-import static io.github.blueprintplatform.openapi.generics.server.core.schema.constant.ContainerNames.*;
-
-import io.github.blueprintplatform.openapi.generics.contract.paging.Page;
-import java.util.LinkedHashSet;
-import java.util.List;
-import java.util.Set;
-
-public final class DefaultSupportedContainerTypesResolver
- implements SupportedContainerTypesResolver {
-
- @Override
- public Set resolve() {
- Set containers = new LinkedHashSet<>();
-
- containers.add(new SupportedContainerType(Page.class, PAGE, PAGE));
- containers.add(new SupportedContainerType(List.class, LIST, LIST));
- containers.add(new SupportedContainerType(Set.class, SET, SET));
-
- return Set.copyOf(containers);
- }
-}
diff --git a/openapi-generics-server-starter/src/main/java/io/github/blueprintplatform/openapi/generics/server/core/introspection/container/SupportedContainerType.java b/openapi-generics-server-starter/src/main/java/io/github/blueprintplatform/openapi/generics/server/core/introspection/container/SupportedContainerType.java
deleted file mode 100644
index 14ba9d2a..00000000
--- a/openapi-generics-server-starter/src/main/java/io/github/blueprintplatform/openapi/generics/server/core/introspection/container/SupportedContainerType.java
+++ /dev/null
@@ -1,52 +0,0 @@
-package io.github.blueprintplatform.openapi.generics.server.core.introspection.container;
-
-import java.util.Objects;
-
-/**
- * Describes a supported generic container type recognized by the projection pipeline.
- *
- * A container definition separates three distinct concerns:
- *
- *
- * - Java type discovered during response introspection
- *
- Schema name used for OpenAPI schema identification and projection
- *
- Container name used as the semantic identifier exposed through vendor extensions
- *
- *
- * For built-in containers, schema and semantic names are often identical:
- *
- *
- * Page -> schemaName=Page, containerName=Page
- * List -> schemaName=List, containerName=List
- * Set -> schemaName=Set, containerName=Set
- *
- *
- * The Java container type is preserved separately so projection metadata can expose the fully
- * qualified container identity without relying on schema names or naming conventions.
- *
- * @param type raw Java container type discovered during introspection
- * @param schemaName canonical schema identifier used during projection
- * @param containerName semantic container identifier exposed through vendor extensions
- */
-public record SupportedContainerType(Class> type, String schemaName, String containerName) {
-
- public SupportedContainerType {
- Objects.requireNonNull(type, "type must not be null");
-
- if (schemaName == null || schemaName.isBlank()) {
- throw new IllegalArgumentException("schemaName must not be null or blank");
- }
-
- if (containerName == null || containerName.isBlank()) {
- throw new IllegalArgumentException("containerName must not be null or blank");
- }
- }
-
- public String containerTypeName() {
- return type.getName();
- }
-
- public boolean matches(Class> candidate) {
- return candidate != null && type.isAssignableFrom(candidate);
- }
-}
diff --git a/openapi-generics-server-starter/src/main/java/io/github/blueprintplatform/openapi/generics/server/core/introspection/container/SupportedContainerTypesResolver.java b/openapi-generics-server-starter/src/main/java/io/github/blueprintplatform/openapi/generics/server/core/introspection/container/SupportedContainerTypesResolver.java
deleted file mode 100644
index 90b3541e..00000000
--- a/openapi-generics-server-starter/src/main/java/io/github/blueprintplatform/openapi/generics/server/core/introspection/container/SupportedContainerTypesResolver.java
+++ /dev/null
@@ -1,8 +0,0 @@
-package io.github.blueprintplatform.openapi.generics.server.core.introspection.container;
-
-import java.util.Set;
-
-public interface SupportedContainerTypesResolver {
-
- Set resolve();
-}
diff --git a/openapi-generics-server-starter/src/main/java/io/github/blueprintplatform/openapi/generics/server/core/introspection/container/descriptor/ContainerMatchMode.java b/openapi-generics-server-starter/src/main/java/io/github/blueprintplatform/openapi/generics/server/core/introspection/container/descriptor/ContainerMatchMode.java
new file mode 100644
index 00000000..9a9acb77
--- /dev/null
+++ b/openapi-generics-server-starter/src/main/java/io/github/blueprintplatform/openapi/generics/server/core/introspection/container/descriptor/ContainerMatchMode.java
@@ -0,0 +1,11 @@
+package io.github.blueprintplatform.openapi.generics.server.core.introspection.container.descriptor;
+
+/** Defines how discovered Java response types are matched against supported containers. */
+public enum ContainerMatchMode {
+
+ /** Candidate type must be exactly the configured container type. */
+ EXACT,
+
+ /** Candidate type may be assignable to the configured container type. */
+ ASSIGNABLE
+}
diff --git a/openapi-generics-server-starter/src/main/java/io/github/blueprintplatform/openapi/generics/server/core/introspection/container/descriptor/ContainerShape.java b/openapi-generics-server-starter/src/main/java/io/github/blueprintplatform/openapi/generics/server/core/introspection/container/descriptor/ContainerShape.java
new file mode 100644
index 00000000..f62eca74
--- /dev/null
+++ b/openapi-generics-server-starter/src/main/java/io/github/blueprintplatform/openapi/generics/server/core/introspection/container/descriptor/ContainerShape.java
@@ -0,0 +1,11 @@
+package io.github.blueprintplatform.openapi.generics.server.core.introspection.container.descriptor;
+
+/** Describes how a generic container is represented in the OpenAPI schema. */
+public enum ContainerShape {
+
+ /** Container is represented directly as an array, for example List or Set. */
+ DIRECT_ARRAY,
+
+ /** Container is represented as an object containing an item collection property. */
+ OBJECT_WITH_ITEM_ARRAY
+}
diff --git a/openapi-generics-server-starter/src/main/java/io/github/blueprintplatform/openapi/generics/server/core/introspection/container/descriptor/ContainerSource.java b/openapi-generics-server-starter/src/main/java/io/github/blueprintplatform/openapi/generics/server/core/introspection/container/descriptor/ContainerSource.java
new file mode 100644
index 00000000..5b2ffd8a
--- /dev/null
+++ b/openapi-generics-server-starter/src/main/java/io/github/blueprintplatform/openapi/generics/server/core/introspection/container/descriptor/ContainerSource.java
@@ -0,0 +1,11 @@
+package io.github.blueprintplatform.openapi.generics.server.core.introspection.container.descriptor;
+
+/** Identifies where a supported container definition comes from. */
+public enum ContainerSource {
+
+ /** Container provided by OpenAPI Generics itself. */
+ BUILT_IN,
+
+ /** Container explicitly configured by the application. */
+ CONFIGURED
+}
diff --git a/openapi-generics-server-starter/src/main/java/io/github/blueprintplatform/openapi/generics/server/core/introspection/container/descriptor/SupportedContainerDescriptor.java b/openapi-generics-server-starter/src/main/java/io/github/blueprintplatform/openapi/generics/server/core/introspection/container/descriptor/SupportedContainerDescriptor.java
new file mode 100644
index 00000000..90f5a944
--- /dev/null
+++ b/openapi-generics-server-starter/src/main/java/io/github/blueprintplatform/openapi/generics/server/core/introspection/container/descriptor/SupportedContainerDescriptor.java
@@ -0,0 +1,70 @@
+package io.github.blueprintplatform.openapi.generics.server.core.introspection.container.descriptor;
+
+import java.util.Objects;
+
+/**
+ * Describes a supported generic container contract recognized by the projection pipeline.
+ *
+ * A container descriptor separates Java identity, OpenAPI schema identity, semantic identity,
+ * and schema-shape behavior. This allows built-in containers and configured BYOC containers to pass
+ * through the same deterministic introspection and projection pipeline.
+ *
+ * @param type raw Java container type discovered during introspection
+ * @param schemaName canonical schema identifier used during projection
+ * @param containerName semantic container identifier exposed through vendor extensions
+ * @param shape OpenAPI schema shape of the container
+ * @param itemPropertyName JSON property containing the generic item collection for object
+ * containers
+ * @param source descriptor source
+ * @param matchMode Java type matching policy
+ */
+public record SupportedContainerDescriptor(
+ Class> type,
+ String schemaName,
+ String containerName,
+ ContainerShape shape,
+ String itemPropertyName,
+ ContainerSource source,
+ ContainerMatchMode matchMode) {
+
+ public SupportedContainerDescriptor {
+ Objects.requireNonNull(type, "type must not be null");
+ Objects.requireNonNull(shape, "shape must not be null");
+ Objects.requireNonNull(source, "source must not be null");
+ Objects.requireNonNull(matchMode, "matchMode must not be null");
+
+ if (schemaName == null || schemaName.isBlank()) {
+ throw new IllegalArgumentException("schemaName must not be null or blank");
+ }
+
+ if (containerName == null || containerName.isBlank()) {
+ throw new IllegalArgumentException("containerName must not be null or blank");
+ }
+
+ if (shape == ContainerShape.OBJECT_WITH_ITEM_ARRAY
+ && (itemPropertyName == null || itemPropertyName.isBlank())) {
+ throw new IllegalArgumentException(
+ "itemPropertyName must not be null or blank for object containers");
+ }
+
+ if (shape == ContainerShape.DIRECT_ARRAY && itemPropertyName != null) {
+ throw new IllegalArgumentException(
+ "itemPropertyName must be null for direct array containers");
+ }
+ }
+
+ public String containerTypeName() {
+ return type.getName();
+ }
+
+ public boolean matches(Class> candidate) {
+ if (candidate == null) {
+ return false;
+ }
+
+ return switch (matchMode) {
+ case EXACT -> type.equals(candidate);
+ case ASSIGNABLE -> type.isAssignableFrom(candidate);
+ };
+ }
+}
diff --git a/openapi-generics-server-starter/src/main/java/io/github/blueprintplatform/openapi/generics/server/core/introspection/container/resolver/ConfiguredContainerTypesResolver.java b/openapi-generics-server-starter/src/main/java/io/github/blueprintplatform/openapi/generics/server/core/introspection/container/resolver/ConfiguredContainerTypesResolver.java
new file mode 100644
index 00000000..39d1ee83
--- /dev/null
+++ b/openapi-generics-server-starter/src/main/java/io/github/blueprintplatform/openapi/generics/server/core/introspection/container/resolver/ConfiguredContainerTypesResolver.java
@@ -0,0 +1,172 @@
+package io.github.blueprintplatform.openapi.generics.server.core.introspection.container.resolver;
+
+import io.github.blueprintplatform.openapi.generics.server.autoconfigure.properties.ContainerProperties;
+import io.github.blueprintplatform.openapi.generics.server.core.introspection.container.descriptor.ContainerMatchMode;
+import io.github.blueprintplatform.openapi.generics.server.core.introspection.container.descriptor.ContainerShape;
+import io.github.blueprintplatform.openapi.generics.server.core.introspection.container.descriptor.ContainerSource;
+import io.github.blueprintplatform.openapi.generics.server.core.introspection.container.descriptor.SupportedContainerDescriptor;
+import java.lang.reflect.Field;
+import java.lang.reflect.Modifier;
+import java.lang.reflect.ParameterizedType;
+import java.lang.reflect.Type;
+import java.lang.reflect.TypeVariable;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Resolves application-configured generic container contracts into supported container descriptors.
+ *
+ *
Configured containers are validated eagerly so invalid BYOC definitions fail fast during
+ * application startup.
+ */
+public final class ConfiguredContainerTypesResolver {
+
+ public static final String ITEM_PROPERTY = "item-property '";
+
+ public Set resolve(List properties) {
+ if (properties == null || properties.isEmpty()) {
+ return Set.of();
+ }
+
+ Set containers = new LinkedHashSet<>();
+
+ for (ContainerProperties property : properties) {
+ if (property == null) {
+ continue;
+ }
+
+ Class> containerType = resolveContainerClass(property.type());
+
+ validateConcreteContainer(containerType);
+ TypeVariable> itemTypeParameter = validateSingleTypeParameter(containerType);
+ validateItemProperty(containerType, property.itemProperty(), itemTypeParameter);
+
+ String simpleName = containerType.getSimpleName();
+
+ containers.add(
+ new SupportedContainerDescriptor(
+ containerType,
+ simpleName,
+ simpleName,
+ ContainerShape.OBJECT_WITH_ITEM_ARRAY,
+ property.itemProperty(),
+ ContainerSource.CONFIGURED,
+ ContainerMatchMode.EXACT));
+ }
+
+ return Set.copyOf(containers);
+ }
+
+ private Class> resolveContainerClass(String configuredType) {
+ if (configuredType == null || configuredType.isBlank()) {
+ throw new IllegalStateException("Container type must not be null or blank");
+ }
+
+ if (!configuredType.contains(".")) {
+ throw new IllegalStateException(
+ "Invalid container type '"
+ + configuredType
+ + "'. Expected fully-qualified class name (e.g. com.example.Paging)");
+ }
+
+ try {
+ return Class.forName(configuredType);
+ } catch (ClassNotFoundException e) {
+ throw new IllegalStateException(
+ "Configured container class not found: '"
+ + configuredType
+ + "'. Ensure the class exists and is on the application classpath.",
+ e);
+ }
+ }
+
+ private void validateConcreteContainer(Class> containerType) {
+ if (containerType.isInterface()) {
+ throw invalidContainer(containerType, "must be a concrete class or record, not an interface");
+ } else if (containerType.isEnum()) {
+ throw invalidContainer(containerType, "must be a class or record, not an enum");
+ } else if (containerType.isAnnotation()) {
+ throw invalidContainer(containerType, "must be a class or record, not an annotation");
+ } else if (containerType.isArray()) {
+ throw invalidContainer(containerType, "must be a class or record, not an array");
+ } else if (containerType.isPrimitive()) {
+ throw invalidContainer(containerType, "must be a class or record, not a primitive");
+ } else if (Modifier.isAbstract(containerType.getModifiers())) {
+ throw invalidContainer(containerType, "must be concrete, not abstract");
+ }
+ }
+
+ private TypeVariable> validateSingleTypeParameter(Class> containerType) {
+ TypeVariable>[] typeParameters = containerType.getTypeParameters();
+
+ if (typeParameters.length != 1) {
+ throw invalidContainer(containerType, "must declare exactly one type parameter");
+ }
+
+ return typeParameters[0];
+ }
+
+ private void validateItemProperty(
+ Class> containerType, String itemProperty, TypeVariable> itemTypeParameter) {
+ if (itemProperty == null || itemProperty.isBlank()) {
+ throw invalidContainer(
+ containerType, ITEM_PROPERTY + itemProperty + "' must not be null or blank");
+ }
+
+ Field field = findDeclaredField(containerType, itemProperty);
+
+ if (field == null) {
+ throw invalidContainer(containerType, ITEM_PROPERTY + itemProperty + "' does not exist");
+ }
+
+ if (Modifier.isStatic(field.getModifiers()) || field.isSynthetic()) {
+ throw invalidContainer(
+ containerType, ITEM_PROPERTY + itemProperty + "' must be an instance field");
+ }
+
+ Type fieldType = field.getGenericType();
+
+ if (!(fieldType instanceof ParameterizedType parameterizedType)) {
+ throw invalidContainer(
+ containerType, ITEM_PROPERTY + itemProperty + "' must be List or Set");
+ }
+
+ Type rawType = parameterizedType.getRawType();
+
+ if (!(rawType == List.class || rawType == Set.class)) {
+ throw invalidContainer(
+ containerType, ITEM_PROPERTY + itemProperty + "' must be List or Set");
+ }
+
+ Type[] arguments = parameterizedType.getActualTypeArguments();
+
+ if (arguments.length != 1 || !sameTypeVariable(arguments[0], itemTypeParameter)) {
+ throw invalidContainer(
+ containerType,
+ ITEM_PROPERTY + itemProperty + "' must use the container type parameter directly");
+ }
+ }
+
+ private Field findDeclaredField(Class> type, String name) {
+ try {
+ return type.getDeclaredField(name);
+ } catch (NoSuchFieldException ignored) {
+ return null;
+ }
+ }
+
+ private boolean sameTypeVariable(Type candidate, TypeVariable> expected) {
+ if (!(candidate instanceof TypeVariable> typeVariable)) {
+ return false;
+ }
+
+ return typeVariable.getGenericDeclaration() == expected.getGenericDeclaration()
+ && typeVariable.getName().equals(expected.getName());
+ }
+
+ private IllegalStateException invalidContainer(Class> containerType, String reason) {
+ return new IllegalStateException(
+ "Unsupported container type '" + containerType.getName() + "': " + reason);
+ }
+}
diff --git a/openapi-generics-server-starter/src/main/java/io/github/blueprintplatform/openapi/generics/server/core/introspection/container/resolver/DefaultSupportedContainerTypesResolver.java b/openapi-generics-server-starter/src/main/java/io/github/blueprintplatform/openapi/generics/server/core/introspection/container/resolver/DefaultSupportedContainerTypesResolver.java
new file mode 100644
index 00000000..1cf4dc04
--- /dev/null
+++ b/openapi-generics-server-starter/src/main/java/io/github/blueprintplatform/openapi/generics/server/core/introspection/container/resolver/DefaultSupportedContainerTypesResolver.java
@@ -0,0 +1,55 @@
+package io.github.blueprintplatform.openapi.generics.server.core.introspection.container.resolver;
+
+import static io.github.blueprintplatform.openapi.generics.server.core.schema.constant.ContainerNames.*;
+import static io.github.blueprintplatform.openapi.generics.server.core.schema.constant.SchemaConstants.PROPERTY_CONTENT;
+
+import io.github.blueprintplatform.openapi.generics.contract.paging.Page;
+import io.github.blueprintplatform.openapi.generics.server.core.introspection.container.descriptor.ContainerMatchMode;
+import io.github.blueprintplatform.openapi.generics.server.core.introspection.container.descriptor.ContainerShape;
+import io.github.blueprintplatform.openapi.generics.server.core.introspection.container.descriptor.ContainerSource;
+import io.github.blueprintplatform.openapi.generics.server.core.introspection.container.descriptor.SupportedContainerDescriptor;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Set;
+
+/** Provides descriptor definitions for the built-in generic container contracts. */
+public final class DefaultSupportedContainerTypesResolver
+ implements SupportedContainerTypesResolver {
+
+ @Override
+ public Set resolve() {
+ Set containers = new LinkedHashSet<>();
+
+ containers.add(
+ new SupportedContainerDescriptor(
+ Page.class,
+ PAGE,
+ PAGE,
+ ContainerShape.OBJECT_WITH_ITEM_ARRAY,
+ PROPERTY_CONTENT,
+ ContainerSource.BUILT_IN,
+ ContainerMatchMode.EXACT));
+
+ containers.add(
+ new SupportedContainerDescriptor(
+ List.class,
+ LIST,
+ LIST,
+ ContainerShape.DIRECT_ARRAY,
+ null,
+ ContainerSource.BUILT_IN,
+ ContainerMatchMode.ASSIGNABLE));
+
+ containers.add(
+ new SupportedContainerDescriptor(
+ Set.class,
+ SET,
+ SET,
+ ContainerShape.DIRECT_ARRAY,
+ null,
+ ContainerSource.BUILT_IN,
+ ContainerMatchMode.ASSIGNABLE));
+
+ return Set.copyOf(containers);
+ }
+}
diff --git a/openapi-generics-server-starter/src/main/java/io/github/blueprintplatform/openapi/generics/server/core/introspection/container/resolver/SupportedContainerTypesResolver.java b/openapi-generics-server-starter/src/main/java/io/github/blueprintplatform/openapi/generics/server/core/introspection/container/resolver/SupportedContainerTypesResolver.java
new file mode 100644
index 00000000..6ab4356f
--- /dev/null
+++ b/openapi-generics-server-starter/src/main/java/io/github/blueprintplatform/openapi/generics/server/core/introspection/container/resolver/SupportedContainerTypesResolver.java
@@ -0,0 +1,9 @@
+package io.github.blueprintplatform.openapi.generics.server.core.introspection.container.resolver;
+
+import io.github.blueprintplatform.openapi.generics.server.core.introspection.container.descriptor.SupportedContainerDescriptor;
+import java.util.Set;
+
+public interface SupportedContainerTypesResolver {
+
+ Set resolve();
+}
diff --git a/openapi-generics-server-starter/src/main/java/io/github/blueprintplatform/openapi/generics/server/core/pipeline/OpenApiPipelineOrchestrator.java b/openapi-generics-server-starter/src/main/java/io/github/blueprintplatform/openapi/generics/server/core/pipeline/OpenApiPipelineOrchestrator.java
index c90ac967..eccfb9cb 100644
--- a/openapi-generics-server-starter/src/main/java/io/github/blueprintplatform/openapi/generics/server/core/pipeline/OpenApiPipelineOrchestrator.java
+++ b/openapi-generics-server-starter/src/main/java/io/github/blueprintplatform/openapi/generics/server/core/pipeline/OpenApiPipelineOrchestrator.java
@@ -15,30 +15,12 @@
import org.slf4j.LoggerFactory;
/**
- * Orchestrates the full OpenAPI projection pipeline for contract-aware responses.
+ * Orchestrates the OpenAPI projection pipeline for generic response contracts.
*
- * This is the single entry point that coordinates all processing steps:
+ *
Coordinates response type discovery, generic type introspection, wrapper schema processing,
+ * contract schema exclusion, and final OpenAPI validation.
*
- *
- * - Discover response types from the application layer
- *
- Extract contract-aware descriptors via introspection
- *
- Generate wrapper schemas (default or BYOE)
- *
- Mark non-authoritative schemas to be ignored
- *
- Validate final OpenAPI contract integrity
- *
- *
- * The pipeline is executed exactly once per OpenAPI instance.
- *
- *
Design principles:
- *
- *
- * - Deterministic execution order
- *
- Contract-first enforcement (no drift allowed)
- *
- OpenAPI is treated as a projection, not a source of truth
- *
- *
- * Acts as the integration point between discovery, schema generation, control marking, and
- * contract validation phases.
+ *
The pipeline is executed once per OpenAPI instance.
*/
public class OpenApiPipelineOrchestrator {
diff --git a/openapi-generics-server-starter/src/main/java/io/github/blueprintplatform/openapi/generics/server/core/schema/WrapperSchemaEnricher.java b/openapi-generics-server-starter/src/main/java/io/github/blueprintplatform/openapi/generics/server/core/schema/WrapperSchemaEnricher.java
deleted file mode 100644
index 71d5a689..00000000
--- a/openapi-generics-server-starter/src/main/java/io/github/blueprintplatform/openapi/generics/server/core/schema/WrapperSchemaEnricher.java
+++ /dev/null
@@ -1,105 +0,0 @@
-package io.github.blueprintplatform.openapi.generics.server.core.schema;
-
-import io.github.blueprintplatform.openapi.generics.server.core.introspection.ResponseTypeDescriptor;
-import io.github.blueprintplatform.openapi.generics.server.core.schema.constant.VendorExtensions;
-import io.github.blueprintplatform.openapi.generics.server.core.schema.strategy.ContainerSchemaRegistry;
-import io.github.blueprintplatform.openapi.generics.server.core.schema.strategy.ContainerSchemaStrategy;
-import io.swagger.v3.oas.models.OpenAPI;
-import io.swagger.v3.oas.models.media.Schema;
-import java.util.Map;
-
-/**
- * Enriches projected wrapper schemas with container metadata required for generic reconstruction.
- *
- *
This component resolves the container represented by a wrapper payload schema and applies the
- * corresponding OpenAPI Generics vendor extensions to the wrapper schema.
- *
- *
The actual container-specific behavior is delegated to {@link ContainerSchemaStrategy}
- * implementations registered in {@link ContainerSchemaRegistry}.
- */
-public class WrapperSchemaEnricher {
-
- private final ContainerSchemaRegistry containerSchemaRegistry;
-
- public WrapperSchemaEnricher(ContainerSchemaRegistry containerSchemaRegistry) {
- this.containerSchemaRegistry = containerSchemaRegistry;
- }
-
- @SuppressWarnings("rawtypes")
- public void enrich(OpenAPI openApi, String wrapperName, ResponseTypeDescriptor descriptor) {
- Map schemas = getSchemas(openApi);
-
- if (schemas.isEmpty()
- || wrapperName == null
- || descriptor == null
- || !descriptor.isContainer()) {
- return;
- }
-
- ContainerSchemaMetadata metadata = resolveContainerMetadata(schemas, wrapperName, descriptor);
-
- if (metadata == null) {
- return;
- }
-
- applyContainerMetadata(metadata);
- }
-
- @SuppressWarnings("rawtypes")
- private ContainerSchemaMetadata resolveContainerMetadata(
- Map schemas, String wrapperName, ResponseTypeDescriptor descriptor) {
- ContainerSchemaStrategy strategy =
- containerSchemaRegistry.findByContainerType(descriptor.containerType());
-
- if (strategy == null) {
- return null;
- }
-
- Schema> containerSchema =
- strategy
- .resolver()
- .resolve(
- schemas, descriptor.dataRefName(), wrapperName, descriptor.payloadPropertyName());
-
- if (containerSchema == null) {
- return null;
- }
-
- String itemName = strategy.extractor().extractItemName(containerSchema);
-
- if (itemName == null) {
- return null;
- }
-
- Schema> wrapper = schemas.get(wrapperName);
-
- if (wrapper == null) {
- return null;
- }
-
- return new ContainerSchemaMetadata(
- wrapper, strategy.containerName(), descriptor.containerTypeName(), itemName);
- }
-
- private void applyContainerMetadata(ContainerSchemaMetadata metadata) {
- metadata.wrapper().addExtension(VendorExtensions.DATA_CONTAINER, metadata.containerName());
- metadata
- .wrapper()
- .addExtension(VendorExtensions.DATA_CONTAINER_TYPE, metadata.containerTypeName());
- metadata.wrapper().addExtension(VendorExtensions.DATA_ITEM, metadata.itemName());
- }
-
- @SuppressWarnings("rawtypes")
- private Map getSchemas(OpenAPI openApi) {
- if (openApi == null
- || openApi.getComponents() == null
- || openApi.getComponents().getSchemas() == null) {
- return Map.of();
- }
-
- return openApi.getComponents().getSchemas();
- }
-
- private record ContainerSchemaMetadata(
- Schema> wrapper, String containerName, String containerTypeName, String itemName) {}
-}
diff --git a/openapi-generics-server-starter/src/main/java/io/github/blueprintplatform/openapi/generics/server/core/schema/WrapperSchemaProcessor.java b/openapi-generics-server-starter/src/main/java/io/github/blueprintplatform/openapi/generics/server/core/schema/WrapperSchemaProcessor.java
index b1eaaf6f..80583145 100644
--- a/openapi-generics-server-starter/src/main/java/io/github/blueprintplatform/openapi/generics/server/core/schema/WrapperSchemaProcessor.java
+++ b/openapi-generics-server-starter/src/main/java/io/github/blueprintplatform/openapi/generics/server/core/schema/WrapperSchemaProcessor.java
@@ -1,6 +1,7 @@
package io.github.blueprintplatform.openapi.generics.server.core.schema;
import io.github.blueprintplatform.openapi.generics.server.core.introspection.ResponseTypeDescriptor;
+import io.github.blueprintplatform.openapi.generics.server.core.schema.enrichment.WrapperSchemaEnricher;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.media.Schema;
import java.util.Map;
diff --git a/openapi-generics-server-starter/src/main/java/io/github/blueprintplatform/openapi/generics/server/core/schema/enrichment/ContainerSchemaMetadata.java b/openapi-generics-server-starter/src/main/java/io/github/blueprintplatform/openapi/generics/server/core/schema/enrichment/ContainerSchemaMetadata.java
new file mode 100644
index 00000000..269d32fa
--- /dev/null
+++ b/openapi-generics-server-starter/src/main/java/io/github/blueprintplatform/openapi/generics/server/core/schema/enrichment/ContainerSchemaMetadata.java
@@ -0,0 +1,14 @@
+package io.github.blueprintplatform.openapi.generics.server.core.schema.enrichment;
+
+import io.swagger.v3.oas.models.media.Schema;
+
+/**
+ * Metadata resolved from a container schema and applied to the projected wrapper schema.
+ *
+ * @param wrapper wrapper schema receiving vendor extensions
+ * @param containerName semantic container name
+ * @param containerTypeName fully-qualified Java container type
+ * @param itemName contained item schema name
+ */
+public record ContainerSchemaMetadata(
+ Schema> wrapper, String containerName, String containerTypeName, String itemName) {}
diff --git a/openapi-generics-server-starter/src/main/java/io/github/blueprintplatform/openapi/generics/server/core/schema/enrichment/ContainerSchemaMetadataResolver.java b/openapi-generics-server-starter/src/main/java/io/github/blueprintplatform/openapi/generics/server/core/schema/enrichment/ContainerSchemaMetadataResolver.java
new file mode 100644
index 00000000..fb0abcec
--- /dev/null
+++ b/openapi-generics-server-starter/src/main/java/io/github/blueprintplatform/openapi/generics/server/core/schema/enrichment/ContainerSchemaMetadataResolver.java
@@ -0,0 +1,99 @@
+package io.github.blueprintplatform.openapi.generics.server.core.schema.enrichment;
+
+import io.github.blueprintplatform.openapi.generics.server.core.introspection.ResponseTypeDescriptor;
+import io.github.blueprintplatform.openapi.generics.server.core.introspection.container.descriptor.ContainerShape;
+import io.github.blueprintplatform.openapi.generics.server.core.introspection.container.descriptor.SupportedContainerDescriptor;
+import io.github.blueprintplatform.openapi.generics.server.core.schema.extraction.ArrayItemReferenceExtractor;
+import io.github.blueprintplatform.openapi.generics.server.core.schema.resolution.ComponentContainerSchemaResolver;
+import io.github.blueprintplatform.openapi.generics.server.core.schema.resolution.WrapperPayloadArraySchemaResolver;
+import io.swagger.v3.oas.models.media.Schema;
+import java.util.Map;
+
+/**
+ * Resolves the container schema metadata required to enrich projected wrapper schemas.
+ *
+ * Extracts the container type, item collection, and payload metadata used for generic client
+ * reconstruction.
+ */
+public class ContainerSchemaMetadataResolver {
+
+ private final WrapperPayloadArraySchemaResolver wrapperPayloadArraySchemaResolver;
+ private final ComponentContainerSchemaResolver componentContainerSchemaResolver;
+ private final ArrayItemReferenceExtractor arrayItemReferenceExtractor;
+
+ public ContainerSchemaMetadataResolver(
+ WrapperPayloadArraySchemaResolver wrapperPayloadArraySchemaResolver,
+ ComponentContainerSchemaResolver componentContainerSchemaResolver,
+ ArrayItemReferenceExtractor arrayItemReferenceExtractor) {
+ this.wrapperPayloadArraySchemaResolver = wrapperPayloadArraySchemaResolver;
+ this.componentContainerSchemaResolver = componentContainerSchemaResolver;
+ this.arrayItemReferenceExtractor = arrayItemReferenceExtractor;
+ }
+
+ @SuppressWarnings("rawtypes")
+ public ContainerSchemaMetadata resolve(
+ Map schemas, String wrapperName, ResponseTypeDescriptor descriptor) {
+ if (schemas == null || schemas.isEmpty() || wrapperName == null || descriptor == null) {
+ return null;
+ }
+
+ SupportedContainerDescriptor container = descriptor.container();
+ if (container == null) {
+ return null;
+ }
+
+ Schema> wrapper = schemas.get(wrapperName);
+ if (wrapper == null) {
+ return null;
+ }
+
+ Schema> containerSchema = resolveContainerSchema(schemas, wrapperName, descriptor, container);
+ if (containerSchema == null) {
+ return null;
+ }
+
+ Schema> itemArraySchema = resolveItemArraySchema(containerSchema, container);
+ String itemName = arrayItemReferenceExtractor.extractItemName(itemArraySchema);
+
+ if (itemName == null) {
+ return null;
+ }
+
+ return new ContainerSchemaMetadata(
+ wrapper, container.containerName(), container.containerTypeName(), itemName);
+ }
+
+ @SuppressWarnings("rawtypes")
+ private Schema> resolveContainerSchema(
+ Map schemas,
+ String wrapperName,
+ ResponseTypeDescriptor descriptor,
+ SupportedContainerDescriptor container) {
+ if (container.shape() == ContainerShape.DIRECT_ARRAY) {
+ return wrapperPayloadArraySchemaResolver.resolve(
+ schemas, descriptor.dataRefName(), wrapperName, descriptor.payloadPropertyName());
+ }
+
+ if (container.shape() == ContainerShape.OBJECT_WITH_ITEM_ARRAY) {
+ return componentContainerSchemaResolver.resolve(
+ schemas, descriptor.dataRefName(), wrapperName, descriptor.payloadPropertyName());
+ }
+
+ return null;
+ }
+
+ @SuppressWarnings("rawtypes")
+ private Schema> resolveItemArraySchema(
+ Schema> containerSchema, SupportedContainerDescriptor container) {
+ if (container.shape() == ContainerShape.DIRECT_ARRAY) {
+ return containerSchema;
+ }
+
+ Map properties = containerSchema.getProperties();
+ if (properties == null) {
+ return null;
+ }
+
+ return properties.get(container.itemPropertyName());
+ }
+}
diff --git a/openapi-generics-server-starter/src/main/java/io/github/blueprintplatform/openapi/generics/server/core/schema/enrichment/WrapperSchemaEnricher.java b/openapi-generics-server-starter/src/main/java/io/github/blueprintplatform/openapi/generics/server/core/schema/enrichment/WrapperSchemaEnricher.java
new file mode 100644
index 00000000..a07e23be
--- /dev/null
+++ b/openapi-generics-server-starter/src/main/java/io/github/blueprintplatform/openapi/generics/server/core/schema/enrichment/WrapperSchemaEnricher.java
@@ -0,0 +1,59 @@
+package io.github.blueprintplatform.openapi.generics.server.core.schema.enrichment;
+
+import io.github.blueprintplatform.openapi.generics.server.core.introspection.ResponseTypeDescriptor;
+import io.github.blueprintplatform.openapi.generics.server.core.schema.constant.VendorExtensions;
+import io.swagger.v3.oas.models.OpenAPI;
+import io.swagger.v3.oas.models.media.Schema;
+import java.util.Map;
+
+/**
+ * Enriches projected wrapper schemas with container metadata for generic client reconstruction.
+ *
+ * Adds vendor extensions describing the generic container contract and its payload item type.
+ */
+public class WrapperSchemaEnricher {
+ private final ContainerSchemaMetadataResolver metadataResolver;
+
+ public WrapperSchemaEnricher(ContainerSchemaMetadataResolver metadataResolver) {
+ this.metadataResolver = metadataResolver;
+ }
+
+ @SuppressWarnings("rawtypes")
+ public void enrich(OpenAPI openApi, String wrapperName, ResponseTypeDescriptor descriptor) {
+ Map schemas = getSchemas(openApi);
+
+ if (schemas.isEmpty()
+ || wrapperName == null
+ || descriptor == null
+ || !descriptor.isContainer()) {
+ return;
+ }
+
+ ContainerSchemaMetadata metadata = metadataResolver.resolve(schemas, wrapperName, descriptor);
+
+ if (metadata == null) {
+ return;
+ }
+
+ applyContainerMetadata(metadata);
+ }
+
+ private void applyContainerMetadata(ContainerSchemaMetadata metadata) {
+ metadata.wrapper().addExtension(VendorExtensions.DATA_CONTAINER, metadata.containerName());
+ metadata
+ .wrapper()
+ .addExtension(VendorExtensions.DATA_CONTAINER_TYPE, metadata.containerTypeName());
+ metadata.wrapper().addExtension(VendorExtensions.DATA_ITEM, metadata.itemName());
+ }
+
+ @SuppressWarnings("rawtypes")
+ private Map getSchemas(OpenAPI openApi) {
+ if (openApi == null
+ || openApi.getComponents() == null
+ || openApi.getComponents().getSchemas() == null) {
+ return Map.of();
+ }
+
+ return openApi.getComponents().getSchemas();
+ }
+}
diff --git a/openapi-generics-server-starter/src/main/java/io/github/blueprintplatform/openapi/generics/server/core/schema/extractor/DirectArrayItemExtractor.java b/openapi-generics-server-starter/src/main/java/io/github/blueprintplatform/openapi/generics/server/core/schema/extraction/ArrayItemReferenceExtractor.java
similarity index 52%
rename from openapi-generics-server-starter/src/main/java/io/github/blueprintplatform/openapi/generics/server/core/schema/extractor/DirectArrayItemExtractor.java
rename to openapi-generics-server-starter/src/main/java/io/github/blueprintplatform/openapi/generics/server/core/schema/extraction/ArrayItemReferenceExtractor.java
index 3c3fbde6..e484bab5 100644
--- a/openapi-generics-server-starter/src/main/java/io/github/blueprintplatform/openapi/generics/server/core/schema/extractor/DirectArrayItemExtractor.java
+++ b/openapi-generics-server-starter/src/main/java/io/github/blueprintplatform/openapi/generics/server/core/schema/extraction/ArrayItemReferenceExtractor.java
@@ -1,4 +1,4 @@
-package io.github.blueprintplatform.openapi.generics.server.core.schema.extractor;
+package io.github.blueprintplatform.openapi.generics.server.core.schema.extraction;
import static io.github.blueprintplatform.openapi.generics.server.core.schema.constant.SchemaConstants.COMPONENT_SCHEMA_REF_PREFIX;
import static io.github.blueprintplatform.openapi.generics.server.core.schema.constant.SchemaConstants.TYPE_ARRAY;
@@ -7,31 +7,16 @@
import io.swagger.v3.oas.models.media.JsonSchema;
import io.swagger.v3.oas.models.media.Schema;
-/**
- * Extracts item type from List style schemas. List is usually represented as a direct array
- * in OpenAPI.
- */
-public class DirectArrayItemExtractor implements ItemExtractor {
+/** Extracts the referenced component schema name from OpenAPI array item definitions. */
+public class ArrayItemReferenceExtractor {
- @SuppressWarnings("rawtypes")
- @Override
- public String extractItemName(Schema> containerSchema) {
- if (containerSchema == null) return null;
+ public String extractItemName(Schema> arraySchema) {
+ Schema> items = resolveItems(arraySchema);
- Schema> items = null;
-
- if (containerSchema instanceof ArraySchema arraySchema) {
- items = arraySchema.getItems();
- } else if (TYPE_ARRAY.equals(containerSchema.getType())) {
- items = containerSchema.getItems();
- } else if (containerSchema instanceof JsonSchema jsonSchema
- && jsonSchema.getTypes() != null
- && jsonSchema.getTypes().contains(TYPE_ARRAY)) {
- items = jsonSchema.getItems();
+ if (items == null) {
+ return null;
}
- if (items == null) return null;
-
String itemRef = items.get$ref();
if (itemRef == null || !itemRef.startsWith(COMPONENT_SCHEMA_REF_PREFIX)) {
return null;
@@ -39,4 +24,26 @@ public String extractItemName(Schema> containerSchema) {
return itemRef.substring(COMPONENT_SCHEMA_REF_PREFIX.length());
}
+
+ private Schema> resolveItems(Schema> schema) {
+ if (schema == null) {
+ return null;
+ }
+
+ if (schema instanceof ArraySchema arraySchema) {
+ return arraySchema.getItems();
+ }
+
+ if (TYPE_ARRAY.equals(schema.getType())) {
+ return schema.getItems();
+ }
+
+ if (schema instanceof JsonSchema jsonSchema
+ && jsonSchema.getTypes() != null
+ && jsonSchema.getTypes().contains(TYPE_ARRAY)) {
+ return jsonSchema.getItems();
+ }
+
+ return null;
+ }
}
diff --git a/openapi-generics-server-starter/src/main/java/io/github/blueprintplatform/openapi/generics/server/core/schema/extractor/ContentArrayItemExtractor.java b/openapi-generics-server-starter/src/main/java/io/github/blueprintplatform/openapi/generics/server/core/schema/extractor/ContentArrayItemExtractor.java
deleted file mode 100644
index 77c950f0..00000000
--- a/openapi-generics-server-starter/src/main/java/io/github/blueprintplatform/openapi/generics/server/core/schema/extractor/ContentArrayItemExtractor.java
+++ /dev/null
@@ -1,48 +0,0 @@
-package io.github.blueprintplatform.openapi.generics.server.core.schema.extractor;
-
-import static io.github.blueprintplatform.openapi.generics.server.core.schema.constant.SchemaConstants.*;
-
-import io.swagger.v3.oas.models.media.ArraySchema;
-import io.swagger.v3.oas.models.media.JsonSchema;
-import io.swagger.v3.oas.models.media.Schema;
-import java.util.Map;
-
-/**
- * Extracts item type from Page style schemas. Page schema usually contains a "content" property
- * which is an array.
- */
-public class ContentArrayItemExtractor implements ItemExtractor {
-
- @SuppressWarnings("rawtypes")
- @Override
- public String extractItemName(Schema> containerSchema) {
- if (containerSchema == null) return null;
-
- Map properties = containerSchema.getProperties();
- if (properties == null) return null;
-
- Schema> content = properties.get(PROPERTY_CONTENT);
- if (content == null) return null;
-
- Schema> items = null;
-
- if (content instanceof ArraySchema arraySchema) {
- items = arraySchema.getItems();
- } else if (TYPE_ARRAY.equals(content.getType())) {
- items = content.getItems();
- } else if (content instanceof JsonSchema jsonSchema
- && jsonSchema.getTypes() != null
- && jsonSchema.getTypes().contains(TYPE_ARRAY)) {
- items = jsonSchema.getItems();
- }
-
- if (items == null) return null;
-
- String itemRef = items.get$ref();
- if (itemRef == null || !itemRef.startsWith(COMPONENT_SCHEMA_REF_PREFIX)) {
- return null;
- }
-
- return itemRef.substring(COMPONENT_SCHEMA_REF_PREFIX.length());
- }
-}
diff --git a/openapi-generics-server-starter/src/main/java/io/github/blueprintplatform/openapi/generics/server/core/schema/extractor/ItemExtractor.java b/openapi-generics-server-starter/src/main/java/io/github/blueprintplatform/openapi/generics/server/core/schema/extractor/ItemExtractor.java
deleted file mode 100644
index 5073ad38..00000000
--- a/openapi-generics-server-starter/src/main/java/io/github/blueprintplatform/openapi/generics/server/core/schema/extractor/ItemExtractor.java
+++ /dev/null
@@ -1,19 +0,0 @@
-package io.github.blueprintplatform.openapi.generics.server.core.schema.extractor;
-
-import io.swagger.v3.oas.models.media.Schema;
-
-/**
- * Strategy interface for extracting the item type name from a container schema. Used by
- * WrapperSchemaEnricher to support different container types (List, Page, Slice, etc.) in a clean
- * and extensible way.
- */
-public interface ItemExtractor {
-
- /**
- * Extracts the simple name of the item type inside the container.
- *
- * @param containerSchema the schema representing the container
- * @return the simple name of the item type, or null if it cannot be determined
- */
- String extractItemName(Schema> containerSchema);
-}
diff --git a/openapi-generics-server-starter/src/main/java/io/github/blueprintplatform/openapi/generics/server/core/schema/resolver/ComponentContainerSchemaResolver.java b/openapi-generics-server-starter/src/main/java/io/github/blueprintplatform/openapi/generics/server/core/schema/resolution/ComponentContainerSchemaResolver.java
similarity index 68%
rename from openapi-generics-server-starter/src/main/java/io/github/blueprintplatform/openapi/generics/server/core/schema/resolver/ComponentContainerSchemaResolver.java
rename to openapi-generics-server-starter/src/main/java/io/github/blueprintplatform/openapi/generics/server/core/schema/resolution/ComponentContainerSchemaResolver.java
index 9656b83a..ed988801 100644
--- a/openapi-generics-server-starter/src/main/java/io/github/blueprintplatform/openapi/generics/server/core/schema/resolver/ComponentContainerSchemaResolver.java
+++ b/openapi-generics-server-starter/src/main/java/io/github/blueprintplatform/openapi/generics/server/core/schema/resolution/ComponentContainerSchemaResolver.java
@@ -1,12 +1,16 @@
-package io.github.blueprintplatform.openapi.generics.server.core.schema.resolver;
+package io.github.blueprintplatform.openapi.generics.server.core.schema.resolution;
import static io.github.blueprintplatform.openapi.generics.server.core.schema.constant.SchemaConstants.*;
-import io.swagger.v3.oas.models.media.*;
+import io.swagger.v3.oas.models.media.ArraySchema;
+import io.swagger.v3.oas.models.media.ComposedSchema;
+import io.swagger.v3.oas.models.media.JsonSchema;
+import io.swagger.v3.oas.models.media.Schema;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;
+/** Resolves component-based container schemas referenced by projected wrapper payloads. */
public class ComponentContainerSchemaResolver implements ContainerSchemaResolver {
@SuppressWarnings("rawtypes")
@@ -28,17 +32,25 @@ public Schema> resolve(
private Schema> resolveContainerSchema(
Map schemas, Schema> schema, Set visited) {
- if (schema == null) return null;
+ if (schema == null) {
+ return null;
+ }
Schema> current = dereferenceIfNeeded(schemas, schema, visited);
- if (current == null) return null;
+ if (current == null) {
+ return null;
+ }
- if (isContainerLike(current)) return current;
+ if (isContainerLike(current)) {
+ return current;
+ }
if (current instanceof ComposedSchema composed && composed.getAllOf() != null) {
for (Schema> candidate : composed.getAllOf()) {
Schema> resolved = resolveContainerSchema(schemas, candidate, visited);
- if (resolved != null) return resolved;
+ if (resolved != null) {
+ return resolved;
+ }
}
}
@@ -50,23 +62,30 @@ private Schema> dereferenceIfNeeded(
Map schemas, Schema> schema, Set visited) {
String ref = schema.get$ref();
- if (ref == null || !ref.startsWith(COMPONENT_SCHEMA_REF_PREFIX)) return schema;
+ if (ref == null || !ref.startsWith(COMPONENT_SCHEMA_REF_PREFIX)) {
+ return schema;
+ }
String name = ref.substring(COMPONENT_SCHEMA_REF_PREFIX.length());
- if (!visited.add(name)) return null;
+ if (!visited.add(name)) {
+ return null;
+ }
return schemas.get(name);
}
private boolean isContainerLike(Schema> schema) {
- return schema instanceof ObjectSchema
- || TYPE_OBJECT.equals(schema.getType())
- || (schema.getProperties() != null && !schema.getProperties().isEmpty())
- || isArrayLike(schema);
+ return hasProperties(schema) || isArrayLike(schema);
+ }
+
+ private boolean hasProperties(Schema> schema) {
+ return schema.getProperties() != null && !schema.getProperties().isEmpty();
}
private boolean isArrayLike(Schema> schema) {
- if (schema == null) return false;
+ if (schema == null) {
+ return false;
+ }
return schema instanceof ArraySchema
|| TYPE_ARRAY.equals(schema.getType())
diff --git a/openapi-generics-server-starter/src/main/java/io/github/blueprintplatform/openapi/generics/server/core/schema/resolver/ContainerSchemaResolver.java b/openapi-generics-server-starter/src/main/java/io/github/blueprintplatform/openapi/generics/server/core/schema/resolution/ContainerSchemaResolver.java
similarity index 94%
rename from openapi-generics-server-starter/src/main/java/io/github/blueprintplatform/openapi/generics/server/core/schema/resolver/ContainerSchemaResolver.java
rename to openapi-generics-server-starter/src/main/java/io/github/blueprintplatform/openapi/generics/server/core/schema/resolution/ContainerSchemaResolver.java
index ec597086..10078027 100644
--- a/openapi-generics-server-starter/src/main/java/io/github/blueprintplatform/openapi/generics/server/core/schema/resolver/ContainerSchemaResolver.java
+++ b/openapi-generics-server-starter/src/main/java/io/github/blueprintplatform/openapi/generics/server/core/schema/resolution/ContainerSchemaResolver.java
@@ -1,4 +1,4 @@
-package io.github.blueprintplatform.openapi.generics.server.core.schema.resolver;
+package io.github.blueprintplatform.openapi.generics.server.core.schema.resolution;
import io.swagger.v3.oas.models.media.Schema;
import java.util.Map;
diff --git a/openapi-generics-server-starter/src/main/java/io/github/blueprintplatform/openapi/generics/server/core/schema/resolver/WrapperPayloadArraySchemaResolver.java b/openapi-generics-server-starter/src/main/java/io/github/blueprintplatform/openapi/generics/server/core/schema/resolution/WrapperPayloadArraySchemaResolver.java
similarity index 92%
rename from openapi-generics-server-starter/src/main/java/io/github/blueprintplatform/openapi/generics/server/core/schema/resolver/WrapperPayloadArraySchemaResolver.java
rename to openapi-generics-server-starter/src/main/java/io/github/blueprintplatform/openapi/generics/server/core/schema/resolution/WrapperPayloadArraySchemaResolver.java
index 7ffc0206..101bb68a 100644
--- a/openapi-generics-server-starter/src/main/java/io/github/blueprintplatform/openapi/generics/server/core/schema/resolver/WrapperPayloadArraySchemaResolver.java
+++ b/openapi-generics-server-starter/src/main/java/io/github/blueprintplatform/openapi/generics/server/core/schema/resolution/WrapperPayloadArraySchemaResolver.java
@@ -1,4 +1,4 @@
-package io.github.blueprintplatform.openapi.generics.server.core.schema.resolver;
+package io.github.blueprintplatform.openapi.generics.server.core.schema.resolution;
import static io.github.blueprintplatform.openapi.generics.server.core.schema.constant.SchemaConstants.TYPE_ARRAY;
@@ -7,6 +7,7 @@
import io.swagger.v3.oas.models.media.Schema;
import java.util.Map;
+/** Resolves array payload schemas defined directly on projected wrapper properties. */
public class WrapperPayloadArraySchemaResolver implements ContainerSchemaResolver {
@SuppressWarnings("rawtypes")
diff --git a/openapi-generics-server-starter/src/main/java/io/github/blueprintplatform/openapi/generics/server/core/schema/strategy/ContainerSchemaRegistry.java b/openapi-generics-server-starter/src/main/java/io/github/blueprintplatform/openapi/generics/server/core/schema/strategy/ContainerSchemaRegistry.java
deleted file mode 100644
index fb78a299..00000000
--- a/openapi-generics-server-starter/src/main/java/io/github/blueprintplatform/openapi/generics/server/core/schema/strategy/ContainerSchemaRegistry.java
+++ /dev/null
@@ -1,26 +0,0 @@
-package io.github.blueprintplatform.openapi.generics.server.core.schema.strategy;
-
-import io.github.blueprintplatform.openapi.generics.server.core.introspection.container.SupportedContainerType;
-import java.util.LinkedHashMap;
-import java.util.List;
-import java.util.Map;
-
-public class ContainerSchemaRegistry {
-
- private final Map strategies =
- new LinkedHashMap<>();
-
- public ContainerSchemaRegistry(List strategies) {
- if (strategies != null) {
- strategies.forEach(strategy -> this.strategies.put(strategy.containerType(), strategy));
- }
- }
-
- public ContainerSchemaStrategy findByContainerType(SupportedContainerType containerType) {
- if (containerType == null) {
- return null;
- }
-
- return strategies.get(containerType);
- }
-}
diff --git a/openapi-generics-server-starter/src/main/java/io/github/blueprintplatform/openapi/generics/server/core/schema/strategy/ContainerSchemaStrategy.java b/openapi-generics-server-starter/src/main/java/io/github/blueprintplatform/openapi/generics/server/core/schema/strategy/ContainerSchemaStrategy.java
deleted file mode 100644
index 74a5fd2c..00000000
--- a/openapi-generics-server-starter/src/main/java/io/github/blueprintplatform/openapi/generics/server/core/schema/strategy/ContainerSchemaStrategy.java
+++ /dev/null
@@ -1,18 +0,0 @@
-package io.github.blueprintplatform.openapi.generics.server.core.schema.strategy;
-
-import io.github.blueprintplatform.openapi.generics.server.core.introspection.container.SupportedContainerType;
-import io.github.blueprintplatform.openapi.generics.server.core.schema.extractor.ItemExtractor;
-import io.github.blueprintplatform.openapi.generics.server.core.schema.resolver.ContainerSchemaResolver;
-
-public interface ContainerSchemaStrategy {
-
- SupportedContainerType containerType();
-
- ContainerSchemaResolver resolver();
-
- ItemExtractor extractor();
-
- default String containerName() {
- return containerType().containerName();
- }
-}
diff --git a/openapi-generics-server-starter/src/main/java/io/github/blueprintplatform/openapi/generics/server/core/schema/strategy/ListContainerSchemaStrategy.java b/openapi-generics-server-starter/src/main/java/io/github/blueprintplatform/openapi/generics/server/core/schema/strategy/ListContainerSchemaStrategy.java
deleted file mode 100644
index 80c26884..00000000
--- a/openapi-generics-server-starter/src/main/java/io/github/blueprintplatform/openapi/generics/server/core/schema/strategy/ListContainerSchemaStrategy.java
+++ /dev/null
@@ -1,20 +0,0 @@
-package io.github.blueprintplatform.openapi.generics.server.core.schema.strategy;
-
-import static io.github.blueprintplatform.openapi.generics.server.core.schema.constant.ContainerNames.LIST;
-
-import io.github.blueprintplatform.openapi.generics.server.core.introspection.container.SupportedContainerType;
-import io.github.blueprintplatform.openapi.generics.server.core.schema.extractor.ItemExtractor;
-import io.github.blueprintplatform.openapi.generics.server.core.schema.resolver.ContainerSchemaResolver;
-import java.util.List;
-
-public record ListContainerSchemaStrategy(ContainerSchemaResolver resolver, ItemExtractor extractor)
- implements ContainerSchemaStrategy {
-
- private static final SupportedContainerType CONTAINER_TYPE =
- new SupportedContainerType(List.class, LIST, LIST);
-
- @Override
- public SupportedContainerType containerType() {
- return CONTAINER_TYPE;
- }
-}
diff --git a/openapi-generics-server-starter/src/main/java/io/github/blueprintplatform/openapi/generics/server/core/schema/strategy/PageContainerSchemaStrategy.java b/openapi-generics-server-starter/src/main/java/io/github/blueprintplatform/openapi/generics/server/core/schema/strategy/PageContainerSchemaStrategy.java
deleted file mode 100644
index 8b22433c..00000000
--- a/openapi-generics-server-starter/src/main/java/io/github/blueprintplatform/openapi/generics/server/core/schema/strategy/PageContainerSchemaStrategy.java
+++ /dev/null
@@ -1,20 +0,0 @@
-package io.github.blueprintplatform.openapi.generics.server.core.schema.strategy;
-
-import static io.github.blueprintplatform.openapi.generics.server.core.schema.constant.ContainerNames.PAGE;
-
-import io.github.blueprintplatform.openapi.generics.contract.paging.Page;
-import io.github.blueprintplatform.openapi.generics.server.core.introspection.container.SupportedContainerType;
-import io.github.blueprintplatform.openapi.generics.server.core.schema.extractor.ItemExtractor;
-import io.github.blueprintplatform.openapi.generics.server.core.schema.resolver.ContainerSchemaResolver;
-
-public record PageContainerSchemaStrategy(ContainerSchemaResolver resolver, ItemExtractor extractor)
- implements ContainerSchemaStrategy {
-
- private static final SupportedContainerType CONTAINER_TYPE =
- new SupportedContainerType(Page.class, PAGE, PAGE);
-
- @Override
- public SupportedContainerType containerType() {
- return CONTAINER_TYPE;
- }
-}
diff --git a/openapi-generics-server-starter/src/main/java/io/github/blueprintplatform/openapi/generics/server/core/schema/strategy/SetContainerSchemaStrategy.java b/openapi-generics-server-starter/src/main/java/io/github/blueprintplatform/openapi/generics/server/core/schema/strategy/SetContainerSchemaStrategy.java
deleted file mode 100644
index dc4c54fb..00000000
--- a/openapi-generics-server-starter/src/main/java/io/github/blueprintplatform/openapi/generics/server/core/schema/strategy/SetContainerSchemaStrategy.java
+++ /dev/null
@@ -1,20 +0,0 @@
-package io.github.blueprintplatform.openapi.generics.server.core.schema.strategy;
-
-import static io.github.blueprintplatform.openapi.generics.server.core.schema.constant.ContainerNames.SET;
-
-import io.github.blueprintplatform.openapi.generics.server.core.introspection.container.SupportedContainerType;
-import io.github.blueprintplatform.openapi.generics.server.core.schema.extractor.ItemExtractor;
-import io.github.blueprintplatform.openapi.generics.server.core.schema.resolver.ContainerSchemaResolver;
-import java.util.Set;
-
-public record SetContainerSchemaStrategy(ContainerSchemaResolver resolver, ItemExtractor extractor)
- implements ContainerSchemaStrategy {
-
- private static final SupportedContainerType CONTAINER_TYPE =
- new SupportedContainerType(Set.class, SET, SET);
-
- @Override
- public SupportedContainerType containerType() {
- return CONTAINER_TYPE;
- }
-}
diff --git a/openapi-generics-server-starter/src/test/java/io/github/blueprintplatform/openapi/generics/server/autoconfigure/OpenApiGenericsAutoConfigurationTest.java b/openapi-generics-server-starter/src/test/java/io/github/blueprintplatform/openapi/generics/server/autoconfigure/OpenApiGenericsAutoConfigurationTest.java
index c21c6314..5b1e2e19 100644
--- a/openapi-generics-server-starter/src/test/java/io/github/blueprintplatform/openapi/generics/server/autoconfigure/OpenApiGenericsAutoConfigurationTest.java
+++ b/openapi-generics-server-starter/src/test/java/io/github/blueprintplatform/openapi/generics/server/autoconfigure/OpenApiGenericsAutoConfigurationTest.java
@@ -8,12 +8,14 @@
import io.github.blueprintplatform.openapi.generics.server.core.introspection.ResponseTypeIntrospector;
import io.github.blueprintplatform.openapi.generics.server.core.pipeline.OpenApiPipelineOrchestrator;
import io.github.blueprintplatform.openapi.generics.server.core.schema.ContractSchemaExclusionApplier;
-import io.github.blueprintplatform.openapi.generics.server.core.schema.WrapperSchemaEnricher;
import io.github.blueprintplatform.openapi.generics.server.core.schema.WrapperSchemaProcessor;
-import io.github.blueprintplatform.openapi.generics.server.core.schema.strategy.ContainerSchemaRegistry;
+import io.github.blueprintplatform.openapi.generics.server.core.schema.enrichment.ContainerSchemaMetadataResolver;
+import io.github.blueprintplatform.openapi.generics.server.core.schema.enrichment.WrapperSchemaEnricher;
+import io.github.blueprintplatform.openapi.generics.server.core.schema.extraction.ArrayItemReferenceExtractor;
+import io.github.blueprintplatform.openapi.generics.server.core.schema.resolution.ComponentContainerSchemaResolver;
+import io.github.blueprintplatform.openapi.generics.server.core.schema.resolution.WrapperPayloadArraySchemaResolver;
import io.github.blueprintplatform.openapi.generics.server.core.validation.OpenApiContractGuard;
import io.github.blueprintplatform.openapi.generics.server.mvc.MvcResponseTypeDiscoveryStrategy;
-import java.util.List;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
@@ -45,7 +47,10 @@ void shouldRegisterAllDefaultBeans() {
assertThat(context).hasSingleBean(ResponseIntrospectionPolicy.class);
assertThat(context).hasSingleBean(ResponseTypeIntrospector.class);
assertThat(context).hasSingleBean(ContractSchemaExclusionApplier.class);
- assertThat(context).hasSingleBean(ContainerSchemaRegistry.class);
+ assertThat(context).hasSingleBean(ArrayItemReferenceExtractor.class);
+ assertThat(context).hasSingleBean(ComponentContainerSchemaResolver.class);
+ assertThat(context).hasSingleBean(WrapperPayloadArraySchemaResolver.class);
+ assertThat(context).hasSingleBean(ContainerSchemaMetadataResolver.class);
assertThat(context).hasSingleBean(WrapperSchemaEnricher.class);
assertThat(context).hasSingleBean(WrapperSchemaProcessor.class);
assertThat(context).hasSingleBean(OpenApiContractGuard.class);
@@ -73,7 +78,10 @@ void shouldNotLoadWhenSpringdocMissing() {
.withClassLoader(new FilteredClassLoader(OpenApiCustomizer.class))
.run(
context -> {
- assertThat(context).doesNotHaveBean(ContainerSchemaRegistry.class);
+ assertThat(context).doesNotHaveBean(ArrayItemReferenceExtractor.class);
+ assertThat(context).doesNotHaveBean(ComponentContainerSchemaResolver.class);
+ assertThat(context).doesNotHaveBean(WrapperPayloadArraySchemaResolver.class);
+ assertThat(context).doesNotHaveBean(ContainerSchemaMetadataResolver.class);
assertThat(context).doesNotHaveBean(WrapperSchemaEnricher.class);
assertThat(context).doesNotHaveBean(OpenApiPipelineOrchestrator.class);
assertThat(context).doesNotHaveBean("openApiGenericsCustomizer");
@@ -143,7 +151,11 @@ OpenApiCustomizer openApiGenericsCustomizer() {
static class CustomEnricherConfig {
static final WrapperSchemaEnricher CUSTOM_ENRICHER =
- new WrapperSchemaEnricher(new ContainerSchemaRegistry(List.of()));
+ new WrapperSchemaEnricher(
+ new ContainerSchemaMetadataResolver(
+ new WrapperPayloadArraySchemaResolver(),
+ new ComponentContainerSchemaResolver(),
+ new ArrayItemReferenceExtractor()));
@Bean
WrapperSchemaEnricher wrapperSchemaEnricher() {
diff --git a/openapi-generics-server-starter/src/test/java/io/github/blueprintplatform/openapi/generics/server/autoconfigure/OpenApiGenericsSchemaAutoConfigurationTest.java b/openapi-generics-server-starter/src/test/java/io/github/blueprintplatform/openapi/generics/server/autoconfigure/OpenApiGenericsSchemaAutoConfigurationTest.java
new file mode 100644
index 00000000..faddcf21
--- /dev/null
+++ b/openapi-generics-server-starter/src/test/java/io/github/blueprintplatform/openapi/generics/server/autoconfigure/OpenApiGenericsSchemaAutoConfigurationTest.java
@@ -0,0 +1,227 @@
+package io.github.blueprintplatform.openapi.generics.server.autoconfigure;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import io.github.blueprintplatform.openapi.generics.server.core.schema.WrapperSchemaProcessor;
+import io.github.blueprintplatform.openapi.generics.server.core.schema.enrichment.ContainerSchemaMetadataResolver;
+import io.github.blueprintplatform.openapi.generics.server.core.schema.enrichment.WrapperSchemaEnricher;
+import io.github.blueprintplatform.openapi.generics.server.core.schema.extraction.ArrayItemReferenceExtractor;
+import io.github.blueprintplatform.openapi.generics.server.core.schema.resolution.ComponentContainerSchemaResolver;
+import io.github.blueprintplatform.openapi.generics.server.core.schema.resolution.WrapperPayloadArraySchemaResolver;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Tag;
+import org.junit.jupiter.api.Test;
+import org.springdoc.core.customizers.OpenApiCustomizer;
+import org.springframework.boot.autoconfigure.AutoConfigurations;
+import org.springframework.boot.test.context.FilteredClassLoader;
+import org.springframework.boot.test.context.runner.WebApplicationContextRunner;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+@Tag("unit")
+@DisplayName("Unit Test: OpenApiGenericsSchemaAutoConfiguration")
+class OpenApiGenericsSchemaAutoConfigurationTest {
+
+ private final WebApplicationContextRunner contextRunner =
+ new WebApplicationContextRunner()
+ .withConfiguration(AutoConfigurations.of(OpenApiGenericsSchemaAutoConfiguration.class));
+
+ @Test
+ @DisplayName("should register schema infrastructure beans when Springdoc is present")
+ void shouldRegisterSchemaInfrastructureBeans() {
+ contextRunner.run(
+ context -> {
+ assertThat(context).hasSingleBean(ArrayItemReferenceExtractor.class);
+ assertThat(context).hasSingleBean(ComponentContainerSchemaResolver.class);
+ assertThat(context).hasSingleBean(WrapperPayloadArraySchemaResolver.class);
+ assertThat(context).hasSingleBean(ContainerSchemaMetadataResolver.class);
+ assertThat(context).hasSingleBean(WrapperSchemaEnricher.class);
+ assertThat(context).hasSingleBean(WrapperSchemaProcessor.class);
+ });
+ }
+
+ @Test
+ @DisplayName("should not load schema auto-configuration when Springdoc is missing")
+ void shouldNotLoadWhenSpringdocMissing() {
+ contextRunner
+ .withClassLoader(new FilteredClassLoader(OpenApiCustomizer.class))
+ .run(
+ context -> {
+ assertThat(context).doesNotHaveBean(ArrayItemReferenceExtractor.class);
+ assertThat(context).doesNotHaveBean(ComponentContainerSchemaResolver.class);
+ assertThat(context).doesNotHaveBean(WrapperPayloadArraySchemaResolver.class);
+ assertThat(context).doesNotHaveBean(ContainerSchemaMetadataResolver.class);
+ assertThat(context).doesNotHaveBean(WrapperSchemaEnricher.class);
+ assertThat(context).doesNotHaveBean(WrapperSchemaProcessor.class);
+ });
+ }
+
+ @Test
+ @DisplayName("should back off when user provides custom ArrayItemReferenceExtractor")
+ void shouldBackOffForCustomArrayItemReferenceExtractor() {
+ contextRunner
+ .withUserConfiguration(CustomArrayItemReferenceExtractorConfig.class)
+ .run(
+ context -> {
+ ArrayItemReferenceExtractor extractor =
+ context.getBean(ArrayItemReferenceExtractor.class);
+
+ assertThat(extractor)
+ .isSameAs(CustomArrayItemReferenceExtractorConfig.CUSTOM_EXTRACTOR);
+ });
+ }
+
+ @Test
+ @DisplayName("should back off when user provides custom ComponentContainerSchemaResolver")
+ void shouldBackOffForCustomComponentContainerSchemaResolver() {
+ contextRunner
+ .withUserConfiguration(CustomComponentContainerSchemaResolverConfig.class)
+ .run(
+ context -> {
+ ComponentContainerSchemaResolver resolver =
+ context.getBean(ComponentContainerSchemaResolver.class);
+
+ assertThat(resolver)
+ .isSameAs(CustomComponentContainerSchemaResolverConfig.CUSTOM_RESOLVER);
+ });
+ }
+
+ @Test
+ @DisplayName("should back off when user provides custom WrapperPayloadArraySchemaResolver")
+ void shouldBackOffForCustomWrapperPayloadArraySchemaResolver() {
+ contextRunner
+ .withUserConfiguration(CustomWrapperPayloadArraySchemaResolverConfig.class)
+ .run(
+ context -> {
+ WrapperPayloadArraySchemaResolver resolver =
+ context.getBean(WrapperPayloadArraySchemaResolver.class);
+
+ assertThat(resolver)
+ .isSameAs(CustomWrapperPayloadArraySchemaResolverConfig.CUSTOM_RESOLVER);
+ });
+ }
+
+ @Test
+ @DisplayName("should back off when user provides custom ContainerSchemaMetadataResolver")
+ void shouldBackOffForCustomContainerSchemaMetadataResolver() {
+ contextRunner
+ .withUserConfiguration(CustomContainerSchemaMetadataResolverConfig.class)
+ .run(
+ context -> {
+ ContainerSchemaMetadataResolver resolver =
+ context.getBean(ContainerSchemaMetadataResolver.class);
+
+ assertThat(resolver)
+ .isSameAs(CustomContainerSchemaMetadataResolverConfig.CUSTOM_RESOLVER);
+ });
+ }
+
+ @Test
+ @DisplayName("should back off when user provides custom WrapperSchemaEnricher")
+ void shouldBackOffForCustomWrapperSchemaEnricher() {
+ contextRunner
+ .withUserConfiguration(CustomWrapperSchemaEnricherConfig.class)
+ .run(
+ context -> {
+ WrapperSchemaEnricher enricher = context.getBean(WrapperSchemaEnricher.class);
+
+ assertThat(enricher).isSameAs(CustomWrapperSchemaEnricherConfig.CUSTOM_ENRICHER);
+ });
+ }
+
+ @Test
+ @DisplayName("should back off when user provides custom WrapperSchemaProcessor")
+ void shouldBackOffForCustomWrapperSchemaProcessor() {
+ contextRunner
+ .withUserConfiguration(CustomWrapperSchemaProcessorConfig.class)
+ .run(
+ context -> {
+ WrapperSchemaProcessor processor = context.getBean(WrapperSchemaProcessor.class);
+
+ assertThat(processor).isSameAs(CustomWrapperSchemaProcessorConfig.CUSTOM_PROCESSOR);
+ });
+ }
+
+ @Configuration
+ static class CustomArrayItemReferenceExtractorConfig {
+
+ static final ArrayItemReferenceExtractor CUSTOM_EXTRACTOR = new ArrayItemReferenceExtractor();
+
+ @Bean
+ ArrayItemReferenceExtractor arrayItemReferenceExtractor() {
+ return CUSTOM_EXTRACTOR;
+ }
+ }
+
+ @Configuration
+ static class CustomComponentContainerSchemaResolverConfig {
+
+ static final ComponentContainerSchemaResolver CUSTOM_RESOLVER =
+ new ComponentContainerSchemaResolver();
+
+ @Bean
+ ComponentContainerSchemaResolver componentContainerSchemaResolver() {
+ return CUSTOM_RESOLVER;
+ }
+ }
+
+ @Configuration
+ static class CustomWrapperPayloadArraySchemaResolverConfig {
+
+ static final WrapperPayloadArraySchemaResolver CUSTOM_RESOLVER =
+ new WrapperPayloadArraySchemaResolver();
+
+ @Bean
+ WrapperPayloadArraySchemaResolver wrapperPayloadArraySchemaResolver() {
+ return CUSTOM_RESOLVER;
+ }
+ }
+
+ @Configuration
+ static class CustomContainerSchemaMetadataResolverConfig {
+
+ static final ContainerSchemaMetadataResolver CUSTOM_RESOLVER =
+ new ContainerSchemaMetadataResolver(
+ new WrapperPayloadArraySchemaResolver(),
+ new ComponentContainerSchemaResolver(),
+ new ArrayItemReferenceExtractor());
+
+ @Bean
+ ContainerSchemaMetadataResolver containerSchemaMetadataResolver() {
+ return CUSTOM_RESOLVER;
+ }
+ }
+
+ @Configuration
+ static class CustomWrapperSchemaEnricherConfig {
+
+ static final WrapperSchemaEnricher CUSTOM_ENRICHER =
+ new WrapperSchemaEnricher(
+ new ContainerSchemaMetadataResolver(
+ new WrapperPayloadArraySchemaResolver(),
+ new ComponentContainerSchemaResolver(),
+ new ArrayItemReferenceExtractor()));
+
+ @Bean
+ WrapperSchemaEnricher wrapperSchemaEnricher() {
+ return CUSTOM_ENRICHER;
+ }
+ }
+
+ @Configuration
+ static class CustomWrapperSchemaProcessorConfig {
+
+ static final WrapperSchemaProcessor CUSTOM_PROCESSOR =
+ new WrapperSchemaProcessor(
+ new WrapperSchemaEnricher(
+ new ContainerSchemaMetadataResolver(
+ new WrapperPayloadArraySchemaResolver(),
+ new ComponentContainerSchemaResolver(),
+ new ArrayItemReferenceExtractor())));
+
+ @Bean
+ WrapperSchemaProcessor wrapperSchemaProcessor() {
+ return CUSTOM_PROCESSOR;
+ }
+ }
+}
diff --git a/openapi-generics-server-starter/src/test/java/io/github/blueprintplatform/openapi/generics/server/core/introspection/ResponseIntrospectionPolicyResolverTest.java b/openapi-generics-server-starter/src/test/java/io/github/blueprintplatform/openapi/generics/server/core/introspection/ResponseIntrospectionPolicyResolverTest.java
index 5d151ab0..93226414 100644
--- a/openapi-generics-server-starter/src/test/java/io/github/blueprintplatform/openapi/generics/server/core/introspection/ResponseIntrospectionPolicyResolverTest.java
+++ b/openapi-generics-server-starter/src/test/java/io/github/blueprintplatform/openapi/generics/server/core/introspection/ResponseIntrospectionPolicyResolverTest.java
@@ -4,10 +4,15 @@
import io.github.blueprintplatform.openapi.generics.contract.envelope.ServiceResponse;
import io.github.blueprintplatform.openapi.generics.contract.paging.Page;
+import io.github.blueprintplatform.openapi.generics.server.autoconfigure.properties.ContainerProperties;
import io.github.blueprintplatform.openapi.generics.server.autoconfigure.properties.EnvelopeProperties;
import io.github.blueprintplatform.openapi.generics.server.autoconfigure.properties.OpenApiGenericsProperties;
-import io.github.blueprintplatform.openapi.generics.server.core.introspection.container.DefaultSupportedContainerTypesResolver;
-import io.github.blueprintplatform.openapi.generics.server.core.introspection.container.SupportedContainerType;
+import io.github.blueprintplatform.openapi.generics.server.core.introspection.container.descriptor.ContainerMatchMode;
+import io.github.blueprintplatform.openapi.generics.server.core.introspection.container.descriptor.ContainerShape;
+import io.github.blueprintplatform.openapi.generics.server.core.introspection.container.descriptor.ContainerSource;
+import io.github.blueprintplatform.openapi.generics.server.core.introspection.container.descriptor.SupportedContainerDescriptor;
+import io.github.blueprintplatform.openapi.generics.server.core.introspection.container.resolver.ConfiguredContainerTypesResolver;
+import io.github.blueprintplatform.openapi.generics.server.core.introspection.container.resolver.DefaultSupportedContainerTypesResolver;
import java.util.List;
import java.util.Set;
import org.junit.jupiter.api.DisplayName;
@@ -19,29 +24,84 @@
class ResponseIntrospectionPolicyResolverTest {
private final ResponseIntrospectionPolicyResolver resolver =
- new ResponseIntrospectionPolicyResolver(new DefaultSupportedContainerTypesResolver());
+ new ResponseIntrospectionPolicyResolver(
+ new DefaultSupportedContainerTypesResolver(), new ConfiguredContainerTypesResolver());
- private static Set defaultContainers() {
+ private static Set defaultContainers() {
return Set.of(
- new SupportedContainerType(Page.class, "Page", "Page"),
- new SupportedContainerType(List.class, "List", "List"),
- new SupportedContainerType(Set.class, "Set", "Set"));
+ new SupportedContainerDescriptor(
+ Page.class,
+ "Page",
+ "Page",
+ ContainerShape.OBJECT_WITH_ITEM_ARRAY,
+ "content",
+ ContainerSource.BUILT_IN,
+ ContainerMatchMode.EXACT),
+ new SupportedContainerDescriptor(
+ List.class,
+ "List",
+ "List",
+ ContainerShape.DIRECT_ARRAY,
+ null,
+ ContainerSource.BUILT_IN,
+ ContainerMatchMode.ASSIGNABLE),
+ new SupportedContainerDescriptor(
+ Set.class,
+ "Set",
+ "Set",
+ ContainerShape.DIRECT_ARRAY,
+ null,
+ ContainerSource.BUILT_IN,
+ ContainerMatchMode.ASSIGNABLE));
}
@Test
@DisplayName("resolve -> should use supported container resolver for default policy")
void resolve_shouldUseSupportedContainerResolverForDefaultPolicy() {
-
- SupportedContainerType pageContainer = new SupportedContainerType(Page.class, "Page", "Page");
+ SupportedContainerDescriptor pageContainer =
+ new SupportedContainerDescriptor(
+ Page.class,
+ "Page",
+ "Page",
+ ContainerShape.OBJECT_WITH_ITEM_ARRAY,
+ "content",
+ ContainerSource.BUILT_IN,
+ ContainerMatchMode.EXACT);
ResponseIntrospectionPolicyResolver customResolver =
- new ResponseIntrospectionPolicyResolver(() -> Set.of(pageContainer));
+ new ResponseIntrospectionPolicyResolver(
+ () -> Set.of(pageContainer), new ConfiguredContainerTypesResolver());
ResponseIntrospectionPolicy policy = customResolver.resolve(null);
assertEquals(Set.of(pageContainer), policy.supportedContainers());
}
+ @Test
+ @DisplayName("resolve -> should include configured containers in default policy")
+ void resolve_shouldIncludeConfiguredContainersInDefaultPolicy() {
+ OpenApiGenericsProperties properties =
+ new OpenApiGenericsProperties(
+ null, List.of(new ContainerProperties(Paging.class.getName(), "content")));
+
+ ResponseIntrospectionPolicy policy = resolver.resolve(properties);
+
+ SupportedContainerDescriptor expected =
+ new SupportedContainerDescriptor(
+ Paging.class,
+ "Paging",
+ "Paging",
+ ContainerShape.OBJECT_WITH_ITEM_ARRAY,
+ "content",
+ ContainerSource.CONFIGURED,
+ ContainerMatchMode.EXACT);
+
+ assertEquals(ServiceResponse.class, policy.envelopeType());
+ assertEquals("data", policy.payloadPropertyName());
+ assertTrue(policy.supportedContainers().containsAll(defaultContainers()));
+ assertTrue(policy.supportedContainers().contains(expected));
+ }
+
@Test
@DisplayName("resolve -> should return default policy when properties are null")
void resolve_shouldReturnDefaultPolicy_whenPropertiesNull() {
@@ -55,7 +115,7 @@ void resolve_shouldReturnDefaultPolicy_whenPropertiesNull() {
@Test
@DisplayName("resolve -> should return default policy when envelope is missing")
void resolve_shouldReturnDefaultPolicy_whenEnvelopeMissing() {
- OpenApiGenericsProperties properties = new OpenApiGenericsProperties(null);
+ OpenApiGenericsProperties properties = new OpenApiGenericsProperties(null, null);
ResponseIntrospectionPolicy policy = resolver.resolve(properties);
@@ -68,7 +128,7 @@ void resolve_shouldReturnDefaultPolicy_whenEnvelopeMissing() {
@DisplayName("resolve -> should return default policy when envelope type is blank")
void resolve_shouldReturnDefaultPolicy_whenEnvelopeTypeBlank() {
OpenApiGenericsProperties properties =
- new OpenApiGenericsProperties(new EnvelopeProperties(" "));
+ new OpenApiGenericsProperties(new EnvelopeProperties(" "), null);
ResponseIntrospectionPolicy policy = resolver.resolve(properties);
@@ -81,7 +141,7 @@ void resolve_shouldReturnDefaultPolicy_whenEnvelopeTypeBlank() {
@DisplayName("resolve -> should resolve custom envelope with direct payload field")
void resolve_shouldResolveCustomEnvelope_whenValid() {
OpenApiGenericsProperties properties =
- new OpenApiGenericsProperties(new EnvelopeProperties(ValidEnvelope.class.getName()));
+ new OpenApiGenericsProperties(new EnvelopeProperties(ValidEnvelope.class.getName()), null);
ResponseIntrospectionPolicy policy = resolver.resolve(properties);
@@ -90,11 +150,37 @@ void resolve_shouldResolveCustomEnvelope_whenValid() {
assertEquals(defaultContainers(), policy.supportedContainers());
}
+ @Test
+ @DisplayName("resolve -> should include configured containers in custom envelope policy")
+ void resolve_shouldIncludeConfiguredContainersInCustomEnvelopePolicy() {
+ OpenApiGenericsProperties properties =
+ new OpenApiGenericsProperties(
+ new EnvelopeProperties(ValidEnvelope.class.getName()),
+ List.of(new ContainerProperties(Paging.class.getName(), "content")));
+
+ ResponseIntrospectionPolicy policy = resolver.resolve(properties);
+
+ SupportedContainerDescriptor expected =
+ new SupportedContainerDescriptor(
+ Paging.class,
+ "Paging",
+ "Paging",
+ ContainerShape.OBJECT_WITH_ITEM_ARRAY,
+ "content",
+ ContainerSource.CONFIGURED,
+ ContainerMatchMode.EXACT);
+
+ assertEquals(ValidEnvelope.class, policy.envelopeType());
+ assertEquals("payload", policy.payloadPropertyName());
+ assertTrue(policy.supportedContainers().containsAll(defaultContainers()));
+ assertTrue(policy.supportedContainers().contains(expected));
+ }
+
@Test
@DisplayName("resolve -> should reject non fqcn envelope type")
void resolve_shouldRejectNonFqcnEnvelopeType() {
OpenApiGenericsProperties properties =
- new OpenApiGenericsProperties(new EnvelopeProperties("ApiResponse"));
+ new OpenApiGenericsProperties(new EnvelopeProperties("ApiResponse"), null);
IllegalStateException ex =
assertThrows(IllegalStateException.class, () -> resolver.resolve(properties));
@@ -106,7 +192,7 @@ void resolve_shouldRejectNonFqcnEnvelopeType() {
@DisplayName("resolve -> should reject missing envelope class")
void resolve_shouldRejectMissingEnvelopeClass() {
OpenApiGenericsProperties properties =
- new OpenApiGenericsProperties(new EnvelopeProperties("com.example.DoesNotExist"));
+ new OpenApiGenericsProperties(new EnvelopeProperties("com.example.DoesNotExist"), null);
IllegalStateException ex =
assertThrows(IllegalStateException.class, () -> resolver.resolve(properties));
@@ -119,7 +205,7 @@ void resolve_shouldRejectMissingEnvelopeClass() {
void resolve_shouldRejectInterfaceEnvelope() {
OpenApiGenericsProperties properties =
new OpenApiGenericsProperties(
- new EnvelopeProperties(InvalidEnvelopeInterface.class.getName()));
+ new EnvelopeProperties(InvalidEnvelopeInterface.class.getName()), null);
IllegalStateException ex =
assertThrows(IllegalStateException.class, () -> resolver.resolve(properties));
@@ -131,7 +217,8 @@ void resolve_shouldRejectInterfaceEnvelope() {
@DisplayName("resolve -> should reject abstract envelope")
void resolve_shouldRejectAbstractEnvelope() {
OpenApiGenericsProperties properties =
- new OpenApiGenericsProperties(new EnvelopeProperties(AbstractEnvelope.class.getName()));
+ new OpenApiGenericsProperties(
+ new EnvelopeProperties(AbstractEnvelope.class.getName()), null);
IllegalStateException ex =
assertThrows(IllegalStateException.class, () -> resolver.resolve(properties));
@@ -144,7 +231,7 @@ void resolve_shouldRejectAbstractEnvelope() {
void resolve_shouldRejectRecordEnvelope() {
OpenApiGenericsProperties properties =
new OpenApiGenericsProperties(
- new EnvelopeProperties(InvalidEnvelopeRecord.class.getName()));
+ new EnvelopeProperties(InvalidEnvelopeRecord.class.getName()), null);
IllegalStateException ex =
assertThrows(IllegalStateException.class, () -> resolver.resolve(properties));
@@ -157,7 +244,7 @@ void resolve_shouldRejectRecordEnvelope() {
void resolve_shouldRejectEnvelopeWithMultipleTypeParameters() {
OpenApiGenericsProperties properties =
new OpenApiGenericsProperties(
- new EnvelopeProperties(InvalidEnvelopeMultipleTypes.class.getName()));
+ new EnvelopeProperties(InvalidEnvelopeMultipleTypes.class.getName()), null);
IllegalStateException ex =
assertThrows(IllegalStateException.class, () -> resolver.resolve(properties));
@@ -170,7 +257,7 @@ void resolve_shouldRejectEnvelopeWithMultipleTypeParameters() {
void resolve_shouldRejectEnvelopeWithoutDirectPayloadField() {
OpenApiGenericsProperties properties =
new OpenApiGenericsProperties(
- new EnvelopeProperties(InvalidEnvelopeNoPayload.class.getName()));
+ new EnvelopeProperties(InvalidEnvelopeNoPayload.class.getName()), null);
IllegalStateException ex =
assertThrows(IllegalStateException.class, () -> resolver.resolve(properties));
@@ -183,7 +270,7 @@ void resolve_shouldRejectEnvelopeWithoutDirectPayloadField() {
void resolve_shouldRejectEnvelopeWithMultipleDirectPayloadFields() {
OpenApiGenericsProperties properties =
new OpenApiGenericsProperties(
- new EnvelopeProperties(InvalidEnvelopeMultiplePayloads.class.getName()));
+ new EnvelopeProperties(InvalidEnvelopeMultiplePayloads.class.getName()), null);
IllegalStateException ex =
assertThrows(IllegalStateException.class, () -> resolver.resolve(properties));
@@ -196,7 +283,7 @@ void resolve_shouldRejectEnvelopeWithMultipleDirectPayloadFields() {
void resolve_shouldRejectEnvelopeWithNestedPayloadField() {
OpenApiGenericsProperties properties =
new OpenApiGenericsProperties(
- new EnvelopeProperties(InvalidEnvelopeNestedPayload.class.getName()));
+ new EnvelopeProperties(InvalidEnvelopeNestedPayload.class.getName()), null);
IllegalStateException ex =
assertThrows(IllegalStateException.class, () -> resolver.resolve(properties));
@@ -208,7 +295,8 @@ void resolve_shouldRejectEnvelopeWithNestedPayloadField() {
@DisplayName("resolve -> should reject enum envelope")
void resolve_shouldRejectEnumEnvelope() {
OpenApiGenericsProperties properties =
- new OpenApiGenericsProperties(new EnvelopeProperties(InvalidEnvelopeEnum.class.getName()));
+ new OpenApiGenericsProperties(
+ new EnvelopeProperties(InvalidEnvelopeEnum.class.getName()), null);
IllegalStateException ex =
assertThrows(IllegalStateException.class, () -> resolver.resolve(properties));
@@ -221,7 +309,7 @@ void resolve_shouldRejectEnumEnvelope() {
void resolve_shouldRejectAnnotationEnvelope() {
OpenApiGenericsProperties properties =
new OpenApiGenericsProperties(
- new EnvelopeProperties(InvalidEnvelopeAnnotation.class.getName()));
+ new EnvelopeProperties(InvalidEnvelopeAnnotation.class.getName()), null);
IllegalStateException ex =
assertThrows(IllegalStateException.class, () -> resolver.resolve(properties));
@@ -234,7 +322,7 @@ void resolve_shouldRejectAnnotationEnvelope() {
void resolve_shouldRejectEnvelopeWithZeroTypeParameters() {
OpenApiGenericsProperties properties =
new OpenApiGenericsProperties(
- new EnvelopeProperties(InvalidEnvelopeNoGenerics.class.getName()));
+ new EnvelopeProperties(InvalidEnvelopeNoGenerics.class.getName()), null);
IllegalStateException ex =
assertThrows(IllegalStateException.class, () -> resolver.resolve(properties));
@@ -247,7 +335,7 @@ void resolve_shouldRejectEnvelopeWithZeroTypeParameters() {
void resolve_shouldIgnoreStaticFields() {
OpenApiGenericsProperties properties =
new OpenApiGenericsProperties(
- new EnvelopeProperties(EnvelopeWithStaticField.class.getName()));
+ new EnvelopeProperties(EnvelopeWithStaticField.class.getName()), null);
ResponseIntrospectionPolicy policy = resolver.resolve(properties);
@@ -260,7 +348,7 @@ void resolve_shouldIgnoreStaticFields() {
void resolve_shouldDetectNestedPayloadInGenericArray() {
OpenApiGenericsProperties properties =
new OpenApiGenericsProperties(
- new EnvelopeProperties(InvalidEnvelopeGenericArrayPayload.class.getName()));
+ new EnvelopeProperties(InvalidEnvelopeGenericArrayPayload.class.getName()), null);
IllegalStateException ex =
assertThrows(IllegalStateException.class, () -> resolver.resolve(properties));
@@ -273,7 +361,7 @@ void resolve_shouldDetectNestedPayloadInGenericArray() {
void resolve_shouldDetectDeeplyNestedPayload() {
OpenApiGenericsProperties properties =
new OpenApiGenericsProperties(
- new EnvelopeProperties(InvalidEnvelopeDeeplyNestedPayload.class.getName()));
+ new EnvelopeProperties(InvalidEnvelopeDeeplyNestedPayload.class.getName()), null);
IllegalStateException ex =
assertThrows(IllegalStateException.class, () -> resolver.resolve(properties));
@@ -286,7 +374,7 @@ void resolve_shouldDetectDeeplyNestedPayload() {
void resolve_shouldAcceptEnvelopeWithUnrelatedGenerics() {
OpenApiGenericsProperties properties =
new OpenApiGenericsProperties(
- new EnvelopeProperties(EnvelopeWithUnrelatedGenericField.class.getName()));
+ new EnvelopeProperties(EnvelopeWithUnrelatedGenericField.class.getName()), null);
ResponseIntrospectionPolicy policy = resolver.resolve(properties);
@@ -297,13 +385,22 @@ void resolve_shouldAcceptEnvelopeWithUnrelatedGenerics() {
@Test
@DisplayName("resolve -> should use supported container resolver for custom envelope policy")
void resolve_shouldUseSupportedContainerResolverForCustomEnvelopePolicy() {
- SupportedContainerType setContainer = new SupportedContainerType(Set.class, "Set", "Set");
+ SupportedContainerDescriptor setContainer =
+ new SupportedContainerDescriptor(
+ Set.class,
+ "Set",
+ "Set",
+ ContainerShape.DIRECT_ARRAY,
+ null,
+ ContainerSource.BUILT_IN,
+ ContainerMatchMode.ASSIGNABLE);
ResponseIntrospectionPolicyResolver customResolver =
- new ResponseIntrospectionPolicyResolver(() -> Set.of(setContainer));
+ new ResponseIntrospectionPolicyResolver(
+ () -> Set.of(setContainer), new ConfiguredContainerTypesResolver());
OpenApiGenericsProperties properties =
- new OpenApiGenericsProperties(new EnvelopeProperties(ValidEnvelope.class.getName()));
+ new OpenApiGenericsProperties(new EnvelopeProperties(ValidEnvelope.class.getName()), null);
ResponseIntrospectionPolicy policy = customResolver.resolve(properties);
@@ -361,6 +458,7 @@ static final class InvalidEnvelopeNoGenerics {
}
static final class EnvelopeWithStaticField {
+ static String ignored;
T payload;
}
@@ -374,6 +472,10 @@ static final class InvalidEnvelopeDeeplyNestedPayload {
static final class EnvelopeWithUnrelatedGenericField {
T payload;
- java.util.List tags; // String is not T, should not be classified as payload
+ java.util.List tags;
+ }
+
+ static final class Paging {
+ List content;
}
}
diff --git a/openapi-generics-server-starter/src/test/java/io/github/blueprintplatform/openapi/generics/server/core/introspection/ResponseTypeIntrospectorTest.java b/openapi-generics-server-starter/src/test/java/io/github/blueprintplatform/openapi/generics/server/core/introspection/ResponseTypeIntrospectorTest.java
index c119e31b..9d4e922c 100644
--- a/openapi-generics-server-starter/src/test/java/io/github/blueprintplatform/openapi/generics/server/core/introspection/ResponseTypeIntrospectorTest.java
+++ b/openapi-generics-server-starter/src/test/java/io/github/blueprintplatform/openapi/generics/server/core/introspection/ResponseTypeIntrospectorTest.java
@@ -4,7 +4,10 @@
import io.github.blueprintplatform.openapi.generics.contract.envelope.ServiceResponse;
import io.github.blueprintplatform.openapi.generics.contract.paging.Page;
-import io.github.blueprintplatform.openapi.generics.server.core.introspection.container.SupportedContainerType;
+import io.github.blueprintplatform.openapi.generics.server.core.introspection.container.descriptor.ContainerMatchMode;
+import io.github.blueprintplatform.openapi.generics.server.core.introspection.container.descriptor.ContainerShape;
+import io.github.blueprintplatform.openapi.generics.server.core.introspection.container.descriptor.ContainerSource;
+import io.github.blueprintplatform.openapi.generics.server.core.introspection.container.descriptor.SupportedContainerDescriptor;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Future;
@@ -20,11 +23,32 @@
@DisplayName("Unit Test: ResponseTypeIntrospector")
class ResponseTypeIntrospectorTest {
+ private static final SupportedContainerDescriptor PAGE_CONTAINER =
+ new SupportedContainerDescriptor(
+ Page.class,
+ "Page",
+ "Page",
+ ContainerShape.OBJECT_WITH_ITEM_ARRAY,
+ "content",
+ ContainerSource.BUILT_IN,
+ ContainerMatchMode.EXACT);
+
+ private static final SupportedContainerDescriptor PAGING_CONTAINER =
+ new SupportedContainerDescriptor(
+ Paging.class,
+ "Paging",
+ "Paging",
+ ContainerShape.OBJECT_WITH_ITEM_ARRAY,
+ "content",
+ ContainerSource.CONFIGURED,
+ ContainerMatchMode.EXACT);
+
private static final ResponseIntrospectionPolicy DEFAULT_POLICY =
+ new ResponseIntrospectionPolicy(ServiceResponse.class, "data", Set.of(PAGE_CONTAINER));
+
+ private static final ResponseIntrospectionPolicy CUSTOM_CONTAINER_POLICY =
new ResponseIntrospectionPolicy(
- ServiceResponse.class,
- "data",
- Set.of(new SupportedContainerType(Page.class, "Page", "Page")));
+ ServiceResponse.class, "data", Set.of(PAGE_CONTAINER, PAGING_CONTAINER));
private final ResponseTypeIntrospector introspector =
new ResponseTypeIntrospector(DEFAULT_POLICY);
@@ -60,6 +84,30 @@ void extract_shouldReturnContainerDescriptor_forPageEnvelope() {
assertEquals("data", descriptor.payloadPropertyName());
assertEquals("PageCustomerDto", descriptor.dataRefName());
assertEquals("Page", descriptor.containerName());
+ assertEquals(Page.class.getName(), descriptor.containerTypeName());
+ assertEquals("CustomerDto", descriptor.itemRefName());
+ assertTrue(descriptor.isContainer());
+ }
+
+ @Test
+ @DisplayName("extract -> should return container descriptor for configured container")
+ void extract_shouldReturnContainerDescriptor_forConfiguredContainer() {
+ ResponseTypeIntrospector customIntrospector =
+ new ResponseTypeIntrospector(CUSTOM_CONTAINER_POLICY);
+
+ ResolvableType pagingType =
+ ResolvableType.forClassWithGenerics(
+ Paging.class, ResolvableType.forClass(CustomerDto.class));
+
+ ResolvableType type = ResolvableType.forClassWithGenerics(ServiceResponse.class, pagingType);
+
+ ResponseTypeDescriptor descriptor = customIntrospector.extract(type).orElseThrow();
+
+ assertEquals(ServiceResponse.class, descriptor.envelopeType());
+ assertEquals("data", descriptor.payloadPropertyName());
+ assertEquals("PagingCustomerDto", descriptor.dataRefName());
+ assertEquals("Paging", descriptor.containerName());
+ assertEquals(Paging.class.getName(), descriptor.containerTypeName());
assertEquals("CustomerDto", descriptor.itemRefName());
assertTrue(descriptor.isContainer());
}
@@ -160,6 +208,18 @@ void extract_shouldReturnEmpty_forUnsupportedNestedGenericPayload() {
assertTrue(introspector.extract(type).isEmpty());
}
+ @Test
+ @DisplayName("extract -> should return empty for unregistered generic container")
+ void extract_shouldReturnEmpty_forUnregisteredGenericContainer() {
+ ResolvableType pagingType =
+ ResolvableType.forClassWithGenerics(
+ Paging.class, ResolvableType.forClass(CustomerDto.class));
+
+ ResolvableType type = ResolvableType.forClassWithGenerics(ServiceResponse.class, pagingType);
+
+ assertTrue(introspector.extract(type).isEmpty());
+ }
+
@Test
@DisplayName("extract -> should return empty when container item type is unresolved")
void extract_shouldReturnEmpty_whenContainerItemTypeUnresolved() {
@@ -172,4 +232,8 @@ void extract_shouldReturnEmpty_whenContainerItemTypeUnresolved() {
private static final class CustomerDto {}
private static final class Wrapper {}
+
+ private static final class Paging {
+ java.util.List content;
+ }
}
diff --git a/openapi-generics-server-starter/src/test/java/io/github/blueprintplatform/openapi/generics/server/core/pipeline/OpenApiPipelineOrchestratorTest.java b/openapi-generics-server-starter/src/test/java/io/github/blueprintplatform/openapi/generics/server/core/pipeline/OpenApiPipelineOrchestratorTest.java
index 99497267..28dc5dd7 100644
--- a/openapi-generics-server-starter/src/test/java/io/github/blueprintplatform/openapi/generics/server/core/pipeline/OpenApiPipelineOrchestratorTest.java
+++ b/openapi-generics-server-starter/src/test/java/io/github/blueprintplatform/openapi/generics/server/core/pipeline/OpenApiPipelineOrchestratorTest.java
@@ -9,7 +9,10 @@
import io.github.blueprintplatform.openapi.generics.server.core.introspection.ResponseTypeDescriptor;
import io.github.blueprintplatform.openapi.generics.server.core.introspection.ResponseTypeDiscoveryStrategy;
import io.github.blueprintplatform.openapi.generics.server.core.introspection.ResponseTypeIntrospector;
-import io.github.blueprintplatform.openapi.generics.server.core.introspection.container.SupportedContainerType;
+import io.github.blueprintplatform.openapi.generics.server.core.introspection.container.descriptor.ContainerMatchMode;
+import io.github.blueprintplatform.openapi.generics.server.core.introspection.container.descriptor.ContainerShape;
+import io.github.blueprintplatform.openapi.generics.server.core.introspection.container.descriptor.ContainerSource;
+import io.github.blueprintplatform.openapi.generics.server.core.introspection.container.descriptor.SupportedContainerDescriptor;
import io.github.blueprintplatform.openapi.generics.server.core.schema.ContractSchemaExclusionApplier;
import io.github.blueprintplatform.openapi.generics.server.core.schema.WrapperSchemaProcessor;
import io.github.blueprintplatform.openapi.generics.server.core.validation.OpenApiContractGuard;
@@ -50,7 +53,14 @@ void run_shouldExecuteFullPipeline() {
ResponseTypeDescriptor.container(
ServiceResponse.class,
"data",
- new SupportedContainerType(Page.class, PAGE, PAGE),
+ new SupportedContainerDescriptor(
+ Page.class,
+ PAGE,
+ PAGE,
+ ContainerShape.OBJECT_WITH_ITEM_ARRAY,
+ "content",
+ ContainerSource.BUILT_IN,
+ ContainerMatchMode.EXACT),
"OrderDto");
when(discoveryStrategy.discover()).thenReturn(Set.of(type1, type2));
diff --git a/openapi-generics-server-starter/src/test/java/io/github/blueprintplatform/openapi/generics/server/core/schema/ContractSchemaExclusionApplierTest.java b/openapi-generics-server-starter/src/test/java/io/github/blueprintplatform/openapi/generics/server/core/schema/ContractSchemaExclusionApplierTest.java
index 63c471da..75cf2b46 100644
--- a/openapi-generics-server-starter/src/test/java/io/github/blueprintplatform/openapi/generics/server/core/schema/ContractSchemaExclusionApplierTest.java
+++ b/openapi-generics-server-starter/src/test/java/io/github/blueprintplatform/openapi/generics/server/core/schema/ContractSchemaExclusionApplierTest.java
@@ -6,7 +6,10 @@
import io.github.blueprintplatform.openapi.generics.contract.envelope.ServiceResponse;
import io.github.blueprintplatform.openapi.generics.contract.paging.Page;
import io.github.blueprintplatform.openapi.generics.server.core.introspection.ResponseTypeDescriptor;
-import io.github.blueprintplatform.openapi.generics.server.core.introspection.container.SupportedContainerType;
+import io.github.blueprintplatform.openapi.generics.server.core.introspection.container.descriptor.ContainerMatchMode;
+import io.github.blueprintplatform.openapi.generics.server.core.introspection.container.descriptor.ContainerShape;
+import io.github.blueprintplatform.openapi.generics.server.core.introspection.container.descriptor.ContainerSource;
+import io.github.blueprintplatform.openapi.generics.server.core.introspection.container.descriptor.SupportedContainerDescriptor;
import io.github.blueprintplatform.openapi.generics.server.core.schema.constant.VendorExtensions;
import io.swagger.v3.oas.models.Components;
import io.swagger.v3.oas.models.OpenAPI;
@@ -67,7 +70,14 @@ void apply_shouldIgnoreContainerSchema_forContainerResponse() {
ResponseTypeDescriptor.container(
ServiceResponse.class,
"data",
- new SupportedContainerType(Page.class, PAGE, PAGE),
+ new SupportedContainerDescriptor(
+ Page.class,
+ PAGE,
+ PAGE,
+ ContainerShape.OBJECT_WITH_ITEM_ARRAY,
+ "content",
+ ContainerSource.BUILT_IN,
+ ContainerMatchMode.EXACT),
"CustomerDto");
marker.apply(openApi, Set.of(descriptor));
diff --git a/openapi-generics-server-starter/src/test/java/io/github/blueprintplatform/openapi/generics/server/core/schema/WrapperSchemaEnricherTest.java b/openapi-generics-server-starter/src/test/java/io/github/blueprintplatform/openapi/generics/server/core/schema/WrapperSchemaEnricherTest.java
deleted file mode 100644
index 771909f3..00000000
--- a/openapi-generics-server-starter/src/test/java/io/github/blueprintplatform/openapi/generics/server/core/schema/WrapperSchemaEnricherTest.java
+++ /dev/null
@@ -1,476 +0,0 @@
-package io.github.blueprintplatform.openapi.generics.server.core.schema;
-
-import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertNull;
-
-import io.github.blueprintplatform.openapi.generics.contract.paging.Page;
-import io.github.blueprintplatform.openapi.generics.server.core.introspection.ResponseTypeDescriptor;
-import io.github.blueprintplatform.openapi.generics.server.core.introspection.container.SupportedContainerType;
-import io.github.blueprintplatform.openapi.generics.server.core.schema.constant.VendorExtensions;
-import io.github.blueprintplatform.openapi.generics.server.core.schema.extractor.ContentArrayItemExtractor;
-import io.github.blueprintplatform.openapi.generics.server.core.schema.extractor.DirectArrayItemExtractor;
-import io.github.blueprintplatform.openapi.generics.server.core.schema.resolver.ComponentContainerSchemaResolver;
-import io.github.blueprintplatform.openapi.generics.server.core.schema.resolver.WrapperPayloadArraySchemaResolver;
-import io.github.blueprintplatform.openapi.generics.server.core.schema.strategy.ContainerSchemaRegistry;
-import io.github.blueprintplatform.openapi.generics.server.core.schema.strategy.ContainerSchemaStrategy;
-import io.github.blueprintplatform.openapi.generics.server.core.schema.strategy.ListContainerSchemaStrategy;
-import io.github.blueprintplatform.openapi.generics.server.core.schema.strategy.PageContainerSchemaStrategy;
-import io.github.blueprintplatform.openapi.generics.server.core.schema.strategy.SetContainerSchemaStrategy;
-import io.swagger.v3.oas.models.Components;
-import io.swagger.v3.oas.models.OpenAPI;
-import io.swagger.v3.oas.models.media.ArraySchema;
-import io.swagger.v3.oas.models.media.ComposedSchema;
-import io.swagger.v3.oas.models.media.JsonSchema;
-import io.swagger.v3.oas.models.media.ObjectSchema;
-import io.swagger.v3.oas.models.media.Schema;
-import java.util.LinkedHashMap;
-import java.util.Map;
-import java.util.Set;
-import org.junit.jupiter.api.DisplayName;
-import org.junit.jupiter.api.Tag;
-import org.junit.jupiter.api.Test;
-
-@Tag("unit")
-@DisplayName("Unit Test: WrapperSchemaEnricher")
-class WrapperSchemaEnricherTest {
-
- @Test
- @DisplayName("enrich -> should add container extensions for default Page schema")
- void enrich_shouldAddContainerExtensions_forPageSchema() {
- WrapperSchemaEnricher enricher = defaultEnricher();
-
- OpenAPI openApi =
- openApi(
- schema("PageCustomerDto", pageSchemaWithArrayContentRef("CustomerDto")),
- schema("ServiceResponsePageCustomerDto", new ObjectSchema()));
-
- enricher.enrich(openApi, "ServiceResponsePageCustomerDto", pageDescriptor("CustomerDto"));
-
- Schema> wrapper = openApi.getComponents().getSchemas().get("ServiceResponsePageCustomerDto");
-
- assertContainerMetadata(wrapper, "Page", Page.class.getName(), "CustomerDto");
- }
-
- @Test
- @DisplayName("enrich -> should support List container")
- void enrich_shouldSupportListContainer() {
- WrapperSchemaEnricher enricher = defaultEnricher();
-
- ArraySchema listSchema = arraySchemaWithItemRef("CustomerDto");
-
- ObjectSchema wrapperSchema = new ObjectSchema();
- wrapperSchema.addProperty("data", listSchema);
-
- OpenAPI openApi =
- openApi(
- schema("ListCustomerDto", listSchema),
- schema("ServiceResponseListCustomerDto", wrapperSchema));
-
- enricher.enrich(openApi, "ServiceResponseListCustomerDto", listDescriptor("CustomerDto"));
-
- Schema> wrapper = openApi.getComponents().getSchemas().get("ServiceResponseListCustomerDto");
-
- assertContainerMetadata(wrapper, "List", java.util.List.class.getName(), "CustomerDto");
- }
-
- @Test
- @DisplayName("enrich -> should resolve allOf object-like schema")
- void enrich_shouldResolveAllOfSchema() {
- WrapperSchemaEnricher enricher = defaultEnricher();
-
- ComposedSchema pageComposed = new ComposedSchema();
- pageComposed.addAllOfItem(pageSchemaWithArrayContentRef("CustomerDto"));
-
- OpenAPI openApi =
- openApi(
- schema("PageCustomerDto", pageComposed),
- schema("ServiceResponsePageCustomerDto", new ObjectSchema()));
-
- enricher.enrich(openApi, "ServiceResponsePageCustomerDto", pageDescriptor("CustomerDto"));
-
- Schema> wrapper = openApi.getComponents().getSchemas().get("ServiceResponsePageCustomerDto");
-
- assertContainerMetadata(wrapper, "Page", Page.class.getName(), "CustomerDto");
- }
-
- @Test
- @DisplayName("enrich -> should support custom container strategy")
- void enrich_shouldSupportCustomContainerStrategy() {
- ContainerSchemaStrategy sliceStrategy =
- new ContainerSchemaStrategy() {
-
- private final SupportedContainerType containerType =
- new SupportedContainerType(Slice.class, "Slice", "Slice");
-
- private final ComponentContainerSchemaResolver resolver =
- new ComponentContainerSchemaResolver();
-
- private final DirectArrayItemExtractor extractor = new DirectArrayItemExtractor();
-
- @Override
- public SupportedContainerType containerType() {
- return containerType;
- }
-
- @Override
- public ComponentContainerSchemaResolver resolver() {
- return resolver;
- }
-
- @Override
- public DirectArrayItemExtractor extractor() {
- return extractor;
- }
- };
-
- WrapperSchemaEnricher enricher =
- new WrapperSchemaEnricher(new ContainerSchemaRegistry(java.util.List.of(sliceStrategy)));
-
- ArraySchema sliceSchema = arraySchemaWithItemRef("CustomerDto");
-
- OpenAPI openApi =
- openApi(
- schema("SliceCustomerDto", sliceSchema),
- schema("ApiResponseSliceCustomerDto", new ObjectSchema()));
-
- enricher.enrich(
- openApi,
- "ApiResponseSliceCustomerDto",
- containerDescriptor(Slice.class, "Slice", "CustomerDto"));
-
- Schema> wrapper = openApi.getComponents().getSchemas().get("ApiResponseSliceCustomerDto");
-
- assertContainerMetadata(wrapper, "Slice", Slice.class.getName(), "CustomerDto");
- }
-
- @Test
- @DisplayName("enrich -> should ignore unsupported container name")
- void enrich_shouldIgnoreUnsupportedContainerName() {
- WrapperSchemaEnricher enricher = defaultEnricher();
-
- ObjectSchema unsupportedSchema = new ObjectSchema();
- unsupportedSchema.addProperty("content", arraySchemaWithItemRef("CustomerDto"));
-
- OpenAPI openApi =
- openApi(
- schema("FooBarCustomerDto", unsupportedSchema),
- schema("ServiceResponseFooBarCustomerDto", new ObjectSchema()));
-
- enricher.enrich(
- openApi,
- "ServiceResponseFooBarCustomerDto",
- containerDescriptor(FooBar.class, "FooBar", "CustomerDto"));
-
- Schema> wrapper =
- openApi.getComponents().getSchemas().get("ServiceResponseFooBarCustomerDto");
-
- assertNull(wrapper.getExtensions());
- }
-
- @Test
- @DisplayName("enrich -> should ignore when wrapper schema is missing")
- void enrich_shouldIgnore_whenWrapperSchemaMissing() {
- WrapperSchemaEnricher enricher = defaultEnricher();
-
- OpenAPI openApi =
- openApi(schema("PageCustomerDto", pageSchemaWithArrayContentRef("CustomerDto")));
-
- assertDoesNotThrow(
- () -> enricher.enrich(openApi, "MissingWrapper", pageDescriptor("CustomerDto")));
- }
-
- @Test
- @DisplayName("enrich -> should ignore when data schema is missing")
- void enrich_shouldIgnore_whenDataSchemaMissing() {
- WrapperSchemaEnricher enricher = defaultEnricher();
-
- OpenAPI openApi = openApi(schema("ServiceResponsePageCustomerDto", new ObjectSchema()));
-
- assertDoesNotThrow(
- () ->
- enricher.enrich(
- openApi,
- "ServiceResponsePageCustomerDto",
- containerDescriptor(MissingData.class, "MissingData", "CustomerDto")));
- }
-
- @Test
- @DisplayName("enrich -> should ignore when content property is missing")
- void enrich_shouldIgnore_whenContentPropertyMissing() {
- WrapperSchemaEnricher enricher = defaultEnricher();
-
- ObjectSchema pageSchema = new ObjectSchema();
- pageSchema.addProperty("totalElements", new Schema<>().type("integer"));
-
- OpenAPI openApi =
- openApi(
- schema("PageCustomerDto", pageSchema),
- schema("ServiceResponsePageCustomerDto", new ObjectSchema()));
-
- enricher.enrich(openApi, "ServiceResponsePageCustomerDto", pageDescriptor("CustomerDto"));
-
- Schema> wrapper = openApi.getComponents().getSchemas().get("ServiceResponsePageCustomerDto");
-
- assertNull(wrapper.getExtensions());
- }
-
- @Test
- @DisplayName("enrich -> should extract item type from JsonSchema array content")
- void enrich_shouldExtractItemType_fromJsonSchemaArray() {
- WrapperSchemaEnricher enricher = defaultEnricher();
-
- JsonSchema content = new JsonSchema();
- content.setTypes(Set.of("array"));
- content.setItems(new Schema<>().$ref("#/components/schemas/CustomerDto"));
-
- ObjectSchema pageSchema = new ObjectSchema();
- pageSchema.addProperty("content", content);
-
- OpenAPI openApi =
- openApi(
- schema("PageCustomerDto", pageSchema),
- schema("ServiceResponsePageCustomerDto", new ObjectSchema()));
-
- enricher.enrich(openApi, "ServiceResponsePageCustomerDto", pageDescriptor("CustomerDto"));
-
- Schema> wrapper = openApi.getComponents().getSchemas().get("ServiceResponsePageCustomerDto");
-
- assertContainerMetadata(wrapper, "Page", Page.class.getName(), "CustomerDto");
- }
-
- @Test
- @DisplayName("enrich -> should do nothing for null inputs")
- void enrich_shouldDoNothing_forNullInputs() {
- WrapperSchemaEnricher enricher = defaultEnricher();
-
- assertDoesNotThrow(() -> enricher.enrich(null, "Wrapper", pageDescriptor("CustomerDto")));
-
- OpenAPI openApi =
- openApi(
- schema("PageCustomerDto", pageSchemaWithArrayContentRef("CustomerDto")),
- schema("ServiceResponsePageCustomerDto", new ObjectSchema()));
-
- assertDoesNotThrow(() -> enricher.enrich(openApi, null, pageDescriptor("CustomerDto")));
- assertDoesNotThrow(() -> enricher.enrich(openApi, "ServiceResponsePageCustomerDto", null));
- assertDoesNotThrow(
- () ->
- enricher.enrich(
- openApi,
- "ServiceResponsePageCustomerDto",
- ResponseTypeDescriptor.simple(ServiceResponse.class, "data", "CustomerDto")));
- }
-
- @Test
- @DisplayName("enrich -> should do nothing when components are null")
- void enrich_shouldDoNothing_whenComponentsNull() {
- WrapperSchemaEnricher enricher = defaultEnricher();
- OpenAPI openApi = new OpenAPI();
-
- assertDoesNotThrow(
- () ->
- enricher.enrich(
- openApi, "ServiceResponsePageCustomerDto", pageDescriptor("CustomerDto")));
- }
-
- @Test
- @DisplayName("enrich -> should do nothing when schemas map is empty")
- void enrich_shouldDoNothing_whenSchemasEmpty() {
- WrapperSchemaEnricher enricher = defaultEnricher();
- OpenAPI openApi = new OpenAPI().components(new Components());
-
- assertDoesNotThrow(
- () ->
- enricher.enrich(
- openApi, "ServiceResponsePageCustomerDto", pageDescriptor("CustomerDto")));
- }
-
- @Test
- @DisplayName("constructor -> should work with custom strategy registry")
- void constructor_shouldWorkWithCustomStrategyRegistry() {
- ContainerSchemaStrategy customListStrategy =
- new ContainerSchemaStrategy() {
-
- private final SupportedContainerType containerType =
- new SupportedContainerType(CustomList.class, "CustomList", "CustomList");
-
- private final ComponentContainerSchemaResolver resolver =
- new ComponentContainerSchemaResolver();
-
- private final DirectArrayItemExtractor extractor = new DirectArrayItemExtractor();
-
- @Override
- public SupportedContainerType containerType() {
- return containerType;
- }
-
- @Override
- public ComponentContainerSchemaResolver resolver() {
- return resolver;
- }
-
- @Override
- public DirectArrayItemExtractor extractor() {
- return extractor;
- }
- };
-
- WrapperSchemaEnricher enricher =
- new WrapperSchemaEnricher(
- new ContainerSchemaRegistry(java.util.List.of(customListStrategy)));
-
- ArraySchema customListSchema = arraySchemaWithItemRef("CustomerDto");
-
- OpenAPI openApi =
- openApi(
- schema("CustomListCustomerDto", customListSchema),
- schema("ServiceResponseCustomListCustomerDto", new ObjectSchema()));
-
- enricher.enrich(
- openApi,
- "ServiceResponseCustomListCustomerDto",
- containerDescriptor(CustomList.class, "CustomList", "CustomerDto"));
-
- Schema> wrapper =
- openApi.getComponents().getSchemas().get("ServiceResponseCustomListCustomerDto");
-
- assertContainerMetadata(wrapper, "CustomList", CustomList.class.getName(), "CustomerDto");
- }
-
- @Test
- @DisplayName("enrich -> should use custom payload property for array container")
- void enrich_shouldUseCustomPayloadProperty_forArrayContainer() {
- WrapperSchemaEnricher enricher = defaultEnricher();
-
- ArraySchema listSchema = arraySchemaWithItemRef("CustomerDto");
-
- ObjectSchema wrapperSchema = new ObjectSchema();
- wrapperSchema.addProperty("payload", listSchema);
-
- OpenAPI openApi =
- openApi(
- schema("ListCustomerDto", listSchema),
- schema("ApiResponseListCustomerDto", wrapperSchema));
-
- enricher.enrich(
- openApi, "ApiResponseListCustomerDto", listDescriptor("CustomerDto", "payload"));
-
- Schema> wrapper = openApi.getComponents().getSchemas().get("ApiResponseListCustomerDto");
-
- assertContainerMetadata(wrapper, "List", java.util.List.class.getName(), "CustomerDto");
- }
-
- @Test
- @DisplayName("enrich -> should support Set container")
- void enrich_shouldSupportSetContainer() {
- WrapperSchemaEnricher enricher = defaultEnricher();
-
- ArraySchema setSchema = arraySchemaWithItemRef("CustomerDto");
-
- ObjectSchema wrapperSchema = new ObjectSchema();
- wrapperSchema.addProperty("data", setSchema);
-
- OpenAPI openApi =
- openApi(
- schema("SetCustomerDto", setSchema),
- schema("ServiceResponseSetCustomerDto", wrapperSchema));
-
- enricher.enrich(openApi, "ServiceResponseSetCustomerDto", setDescriptor("CustomerDto"));
-
- Schema> wrapper = openApi.getComponents().getSchemas().get("ServiceResponseSetCustomerDto");
-
- assertContainerMetadata(wrapper, "Set", java.util.Set.class.getName(), "CustomerDto");
- }
-
- private WrapperSchemaEnricher defaultEnricher() {
- return new WrapperSchemaEnricher(
- new ContainerSchemaRegistry(
- java.util.List.of(
- new PageContainerSchemaStrategy(
- new ComponentContainerSchemaResolver(), new ContentArrayItemExtractor()),
- new ListContainerSchemaStrategy(
- new WrapperPayloadArraySchemaResolver(), new DirectArrayItemExtractor()),
- new SetContainerSchemaStrategy(
- new WrapperPayloadArraySchemaResolver(), new DirectArrayItemExtractor()))));
- }
-
- private ResponseTypeDescriptor pageDescriptor(String itemRefName) {
- return containerDescriptor(Page.class, "Page", itemRefName);
- }
-
- private ResponseTypeDescriptor listDescriptor(String itemRefName) {
- return listDescriptor(itemRefName, "data");
- }
-
- private ResponseTypeDescriptor listDescriptor(String itemRefName, String payloadPropertyName) {
- return containerDescriptor(java.util.List.class, "List", itemRefName, payloadPropertyName);
- }
-
- private ResponseTypeDescriptor setDescriptor(String itemRefName) {
- return containerDescriptor(java.util.Set.class, "Set", itemRefName);
- }
-
- private ResponseTypeDescriptor containerDescriptor(
- Class> containerClass, String containerName, String itemRefName) {
- return containerDescriptor(containerClass, containerName, itemRefName, "data");
- }
-
- private ResponseTypeDescriptor containerDescriptor(
- Class> containerClass,
- String containerName,
- String itemRefName,
- String payloadPropertyName) {
- SupportedContainerType containerType =
- new SupportedContainerType(containerClass, containerName, containerName);
-
- return ResponseTypeDescriptor.container(
- ServiceResponse.class, payloadPropertyName, containerType, itemRefName);
- }
-
- private void assertContainerMetadata(
- Schema> wrapper, String containerName, String containerTypeName, String itemName) {
- assertEquals(containerName, wrapper.getExtensions().get(VendorExtensions.DATA_CONTAINER));
- assertEquals(
- containerTypeName, wrapper.getExtensions().get(VendorExtensions.DATA_CONTAINER_TYPE));
- assertEquals(itemName, wrapper.getExtensions().get(VendorExtensions.DATA_ITEM));
- }
-
- private OpenAPI openApi(NamedSchema... namedSchemas) {
- Map schemas = new LinkedHashMap<>();
-
- for (NamedSchema namedSchema : namedSchemas) {
- schemas.put(namedSchema.name, namedSchema.schema);
- }
-
- return new OpenAPI().components(new Components().schemas(schemas));
- }
-
- private NamedSchema schema(String name, Schema> schema) {
- schema.setName(name);
- return new NamedSchema(name, schema);
- }
-
- private ObjectSchema pageSchemaWithArrayContentRef(String itemRefName) {
- ObjectSchema pageSchema = new ObjectSchema();
- pageSchema.addProperty("content", arraySchemaWithItemRef(itemRefName));
- return pageSchema;
- }
-
- private ArraySchema arraySchemaWithItemRef(String itemRefName) {
- ArraySchema schema = new ArraySchema();
- schema.setItems(new Schema<>().$ref("#/components/schemas/" + itemRefName));
- return schema;
- }
-
- private record NamedSchema(String name, Schema> schema) {}
-
- private static final class ServiceResponse {}
-
- private static final class Slice {}
-
- private static final class FooBar {}
-
- private static final class MissingData {}
-
- private static final class CustomList {}
-}
diff --git a/openapi-generics-server-starter/src/test/java/io/github/blueprintplatform/openapi/generics/server/core/schema/WrapperSchemaProcessorTest.java b/openapi-generics-server-starter/src/test/java/io/github/blueprintplatform/openapi/generics/server/core/schema/WrapperSchemaProcessorTest.java
index eebede95..e1f2bb7d 100644
--- a/openapi-generics-server-starter/src/test/java/io/github/blueprintplatform/openapi/generics/server/core/schema/WrapperSchemaProcessorTest.java
+++ b/openapi-generics-server-starter/src/test/java/io/github/blueprintplatform/openapi/generics/server/core/schema/WrapperSchemaProcessorTest.java
@@ -7,8 +7,12 @@
import io.github.blueprintplatform.openapi.generics.contract.envelope.ServiceResponse;
import io.github.blueprintplatform.openapi.generics.contract.paging.Page;
import io.github.blueprintplatform.openapi.generics.server.core.introspection.ResponseTypeDescriptor;
-import io.github.blueprintplatform.openapi.generics.server.core.introspection.container.SupportedContainerType;
+import io.github.blueprintplatform.openapi.generics.server.core.introspection.container.descriptor.ContainerMatchMode;
+import io.github.blueprintplatform.openapi.generics.server.core.introspection.container.descriptor.ContainerShape;
+import io.github.blueprintplatform.openapi.generics.server.core.introspection.container.descriptor.ContainerSource;
+import io.github.blueprintplatform.openapi.generics.server.core.introspection.container.descriptor.SupportedContainerDescriptor;
import io.github.blueprintplatform.openapi.generics.server.core.schema.constant.VendorExtensions;
+import io.github.blueprintplatform.openapi.generics.server.core.schema.enrichment.WrapperSchemaEnricher;
import io.swagger.v3.oas.models.Components;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.media.Schema;
@@ -76,7 +80,14 @@ void process_shouldDelegateContainerEnrichment_forDefaultEnvelopeContainerRespon
ResponseTypeDescriptor.container(
ServiceResponse.class,
"data",
- new SupportedContainerType(Page.class, PAGE, PAGE),
+ new SupportedContainerDescriptor(
+ Page.class,
+ PAGE,
+ PAGE,
+ ContainerShape.OBJECT_WITH_ITEM_ARRAY,
+ "content",
+ ContainerSource.BUILT_IN,
+ ContainerMatchMode.EXACT),
"CustomerDto");
processor.process(openApi, descriptor);
@@ -120,8 +131,15 @@ void process_shouldDelegateContainerEnrichment_forCustomEnvelopeContainerRespons
ResponseTypeDescriptor descriptor =
ResponseTypeDescriptor.container(
ApiResponse.class,
- "payload",
- new SupportedContainerType(Page.class, PAGE, PAGE),
+ "data",
+ new SupportedContainerDescriptor(
+ Page.class,
+ PAGE,
+ PAGE,
+ ContainerShape.OBJECT_WITH_ITEM_ARRAY,
+ "content",
+ ContainerSource.BUILT_IN,
+ ContainerMatchMode.EXACT),
"CustomerDto");
processor.process(openApi, descriptor);
diff --git a/openapi-generics-server-starter/src/test/java/io/github/blueprintplatform/openapi/generics/server/core/schema/enrichment/WrapperSchemaEnricherTest.java b/openapi-generics-server-starter/src/test/java/io/github/blueprintplatform/openapi/generics/server/core/schema/enrichment/WrapperSchemaEnricherTest.java
new file mode 100644
index 00000000..f2010eac
--- /dev/null
+++ b/openapi-generics-server-starter/src/test/java/io/github/blueprintplatform/openapi/generics/server/core/schema/enrichment/WrapperSchemaEnricherTest.java
@@ -0,0 +1,342 @@
+package io.github.blueprintplatform.openapi.generics.server.core.schema.enrichment;
+
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+import io.github.blueprintplatform.openapi.generics.contract.paging.Page;
+import io.github.blueprintplatform.openapi.generics.server.core.introspection.ResponseTypeDescriptor;
+import io.github.blueprintplatform.openapi.generics.server.core.introspection.container.descriptor.ContainerMatchMode;
+import io.github.blueprintplatform.openapi.generics.server.core.introspection.container.descriptor.ContainerShape;
+import io.github.blueprintplatform.openapi.generics.server.core.introspection.container.descriptor.ContainerSource;
+import io.github.blueprintplatform.openapi.generics.server.core.introspection.container.descriptor.SupportedContainerDescriptor;
+import io.github.blueprintplatform.openapi.generics.server.core.schema.constant.VendorExtensions;
+import io.github.blueprintplatform.openapi.generics.server.core.schema.extraction.ArrayItemReferenceExtractor;
+import io.github.blueprintplatform.openapi.generics.server.core.schema.resolution.ComponentContainerSchemaResolver;
+import io.github.blueprintplatform.openapi.generics.server.core.schema.resolution.WrapperPayloadArraySchemaResolver;
+import io.swagger.v3.oas.models.Components;
+import io.swagger.v3.oas.models.OpenAPI;
+import io.swagger.v3.oas.models.media.ArraySchema;
+import io.swagger.v3.oas.models.media.ObjectSchema;
+import io.swagger.v3.oas.models.media.Schema;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Tag;
+import org.junit.jupiter.api.Test;
+
+@Tag("unit")
+@DisplayName("Unit Test: WrapperSchemaEnricher")
+class WrapperSchemaEnricherTest {
+
+ @Test
+ @DisplayName("enrich -> should do nothing when openApi is null")
+ void enrich_shouldDoNothing_whenOpenApiIsNull() {
+ WrapperSchemaEnricher enricher = defaultEnricher();
+
+ assertDoesNotThrow(
+ () ->
+ enricher.enrich(null, "ServiceResponsePageCustomerDto", pageDescriptor("CustomerDto")));
+ }
+
+ @Test
+ @DisplayName("enrich -> should do nothing when wrapper name is null")
+ void enrich_shouldDoNothing_whenWrapperNameIsNull() {
+ WrapperSchemaEnricher enricher = defaultEnricher();
+
+ OpenAPI openApi =
+ openApi(
+ schema("PageCustomerDto", objectContainerSchema("content", "CustomerDto")),
+ schema("ServiceResponsePageCustomerDto", new ObjectSchema()));
+
+ assertDoesNotThrow(() -> enricher.enrich(openApi, null, pageDescriptor("CustomerDto")));
+ }
+
+ @Test
+ @DisplayName("enrich -> should do nothing when descriptor is null")
+ void enrich_shouldDoNothing_whenDescriptorIsNull() {
+ WrapperSchemaEnricher enricher = defaultEnricher();
+
+ OpenAPI openApi =
+ openApi(
+ schema("PageCustomerDto", objectContainerSchema("content", "CustomerDto")),
+ schema("ServiceResponsePageCustomerDto", new ObjectSchema()));
+
+ assertDoesNotThrow(() -> enricher.enrich(openApi, "ServiceResponsePageCustomerDto", null));
+ }
+
+ @Test
+ @DisplayName("enrich -> should ignore when wrapper schema is missing")
+ void enrich_shouldIgnore_whenWrapperSchemaMissing() {
+ WrapperSchemaEnricher enricher = defaultEnricher();
+
+ OpenAPI openApi =
+ openApi(schema("PageCustomerDto", objectContainerSchema("content", "CustomerDto")));
+
+ assertDoesNotThrow(
+ () -> enricher.enrich(openApi, "MissingWrapper", pageDescriptor("CustomerDto")));
+ }
+
+ @Test
+ @DisplayName("enrich -> should ignore when direct array payload property is missing")
+ void enrich_shouldIgnore_whenDirectArrayPayloadPropertyMissing() {
+ WrapperSchemaEnricher enricher = defaultEnricher();
+
+ ArraySchema listSchema = arraySchemaWithItemRef("CustomerDto");
+
+ ObjectSchema wrapper = new ObjectSchema();
+ wrapper.addProperty("payload", listSchema);
+
+ OpenAPI openApi =
+ openApi(
+ schema("ListCustomerDto", listSchema),
+ schema("ServiceResponseListCustomerDto", wrapper));
+
+ enricher.enrich(openApi, "ServiceResponseListCustomerDto", listDescriptor("CustomerDto"));
+
+ Schema> result = openApi.getComponents().getSchemas().get("ServiceResponseListCustomerDto");
+
+ assertNull(result.getExtensions());
+ }
+
+ @Test
+ @DisplayName("enrich -> should ignore non-container descriptor")
+ void enrich_shouldIgnoreNonContainerDescriptor() {
+ WrapperSchemaEnricher enricher = defaultEnricher();
+
+ OpenAPI openApi = openApi(schema("ServiceResponseCustomerDto", new ObjectSchema()));
+
+ ResponseTypeDescriptor descriptor =
+ ResponseTypeDescriptor.simple(ServiceResponse.class, "data", "CustomerDto");
+
+ assertDoesNotThrow(() -> enricher.enrich(openApi, "ServiceResponseCustomerDto", descriptor));
+
+ Schema> wrapper = openApi.getComponents().getSchemas().get("ServiceResponseCustomerDto");
+
+ assertNull(wrapper.getExtensions());
+ }
+
+ @Test
+ @DisplayName("enrich -> should add metadata for object container")
+ void enrich_shouldAddMetadata_forObjectContainer() {
+ WrapperSchemaEnricher enricher = defaultEnricher();
+
+ OpenAPI openApi =
+ openApi(
+ schema("PageCustomerDto", objectContainerSchema("content", "CustomerDto")),
+ schema("ServiceResponsePageCustomerDto", new ObjectSchema()));
+
+ enricher.enrich(openApi, "ServiceResponsePageCustomerDto", pageDescriptor("CustomerDto"));
+
+ Schema> wrapper = openApi.getComponents().getSchemas().get("ServiceResponsePageCustomerDto");
+
+ assertContainerMetadata(wrapper, "Page", Page.class.getName(), "CustomerDto");
+ }
+
+ @Test
+ @DisplayName("enrich -> should add metadata for direct array container")
+ void enrich_shouldAddMetadata_forDirectArrayContainer() {
+ WrapperSchemaEnricher enricher = defaultEnricher();
+
+ ArraySchema arraySchema = arraySchemaWithItemRef("CustomerDto");
+
+ ObjectSchema wrapperSchema = new ObjectSchema();
+ wrapperSchema.addProperty("data", arraySchema);
+
+ OpenAPI openApi =
+ openApi(
+ schema("ListCustomerDto", arraySchema),
+ schema("ServiceResponseListCustomerDto", wrapperSchema));
+
+ enricher.enrich(openApi, "ServiceResponseListCustomerDto", listDescriptor("CustomerDto"));
+
+ Schema> wrapper = openApi.getComponents().getSchemas().get("ServiceResponseListCustomerDto");
+
+ assertContainerMetadata(wrapper, "List", java.util.List.class.getName(), "CustomerDto");
+ }
+
+ @Test
+ @DisplayName("enrich -> should add metadata for configured object container")
+ void enrich_shouldAddMetadata_forConfiguredObjectContainer() {
+ WrapperSchemaEnricher enricher = defaultEnricher();
+
+ OpenAPI openApi =
+ openApi(
+ schema("PagingCustomerDto", objectContainerSchema("items", "CustomerDto")),
+ schema("ServiceResponsePagingCustomerDto", new ObjectSchema()));
+
+ enricher.enrich(
+ openApi,
+ "ServiceResponsePagingCustomerDto",
+ objectContainerDescriptor(Paging.class, "Paging", "items", "CustomerDto"));
+
+ Schema> wrapper =
+ openApi.getComponents().getSchemas().get("ServiceResponsePagingCustomerDto");
+
+ assertContainerMetadata(wrapper, "Paging", Paging.class.getName(), "CustomerDto");
+ }
+
+ @Test
+ @DisplayName("enrich -> should ignore when metadata cannot be resolved")
+ void enrich_shouldIgnore_whenMetadataCannotBeResolved() {
+ WrapperSchemaEnricher enricher = defaultEnricher();
+
+ OpenAPI openApi =
+ openApi(
+ schema("PageCustomerDto", new ObjectSchema()),
+ schema("ServiceResponsePageCustomerDto", new ObjectSchema()));
+
+ assertDoesNotThrow(
+ () ->
+ enricher.enrich(
+ openApi, "ServiceResponsePageCustomerDto", pageDescriptor("CustomerDto")));
+
+ Schema> wrapper = openApi.getComponents().getSchemas().get("ServiceResponsePageCustomerDto");
+
+ assertNull(wrapper.getExtensions());
+ }
+
+ @Test
+ @DisplayName("enrich -> should resolve object container through component ref")
+ void enrich_shouldResolveObjectContainer_throughComponentRef() {
+ WrapperSchemaEnricher enricher = defaultEnricher();
+
+ Schema> pageRef = new Schema<>().$ref("#/components/schemas/PageCustomerDto");
+
+ OpenAPI openApi =
+ openApi(
+ schema("PageCustomerDto", objectContainerSchema("content", "CustomerDto")),
+ schema("ServiceResponsePageCustomerDto", pageRef));
+
+ enricher.enrich(openApi, "ServiceResponsePageCustomerDto", pageDescriptor("CustomerDto"));
+
+ Schema> wrapper = openApi.getComponents().getSchemas().get("ServiceResponsePageCustomerDto");
+
+ assertContainerMetadata(wrapper, "Page", Page.class.getName(), "CustomerDto");
+ }
+
+ @Test
+ @DisplayName("enrich -> should ignore when array item is not component ref")
+ void enrich_shouldIgnore_whenArrayItemIsNotComponentRef() {
+ WrapperSchemaEnricher enricher = defaultEnricher();
+
+ ArraySchema arraySchema = new ArraySchema();
+ arraySchema.setItems(new Schema<>().type("string"));
+
+ ObjectSchema wrapperSchema = new ObjectSchema();
+ wrapperSchema.addProperty("data", arraySchema);
+
+ OpenAPI openApi =
+ openApi(
+ schema("ListString", arraySchema), schema("ServiceResponseListString", wrapperSchema));
+
+ enricher.enrich(openApi, "ServiceResponseListString", listDescriptor("String"));
+
+ Schema> wrapper = openApi.getComponents().getSchemas().get("ServiceResponseListString");
+
+ assertNull(wrapper.getExtensions());
+ }
+
+ @Test
+ @DisplayName("enrich -> should ignore when configured item property does not exist")
+ void enrich_shouldIgnore_whenConfiguredItemPropertyDoesNotExist() {
+ WrapperSchemaEnricher enricher = defaultEnricher();
+
+ OpenAPI openApi =
+ openApi(
+ schema("PagingCustomerDto", objectContainerSchema("content", "CustomerDto")),
+ schema("ServiceResponsePagingCustomerDto", new ObjectSchema()));
+
+ enricher.enrich(
+ openApi,
+ "ServiceResponsePagingCustomerDto",
+ objectContainerDescriptor(Paging.class, "Paging", "items", "CustomerDto"));
+
+ Schema> wrapper =
+ openApi.getComponents().getSchemas().get("ServiceResponsePagingCustomerDto");
+
+ assertNull(wrapper.getExtensions());
+ }
+
+ private WrapperSchemaEnricher defaultEnricher() {
+ return new WrapperSchemaEnricher(
+ new ContainerSchemaMetadataResolver(
+ new WrapperPayloadArraySchemaResolver(),
+ new ComponentContainerSchemaResolver(),
+ new ArrayItemReferenceExtractor()));
+ }
+
+ private ResponseTypeDescriptor pageDescriptor(String itemRefName) {
+ return objectContainerDescriptor(Page.class, "Page", "content", itemRefName);
+ }
+
+ private ResponseTypeDescriptor listDescriptor(String itemRefName) {
+ SupportedContainerDescriptor container =
+ new SupportedContainerDescriptor(
+ java.util.List.class,
+ "List",
+ "List",
+ ContainerShape.DIRECT_ARRAY,
+ null,
+ ContainerSource.BUILT_IN,
+ ContainerMatchMode.ASSIGNABLE);
+
+ return ResponseTypeDescriptor.container(ServiceResponse.class, "data", container, itemRefName);
+ }
+
+ private ResponseTypeDescriptor objectContainerDescriptor(
+ Class> containerClass, String containerName, String itemPropertyName, String itemRefName) {
+ SupportedContainerDescriptor container =
+ new SupportedContainerDescriptor(
+ containerClass,
+ containerName,
+ containerName,
+ ContainerShape.OBJECT_WITH_ITEM_ARRAY,
+ itemPropertyName,
+ ContainerSource.CONFIGURED,
+ ContainerMatchMode.EXACT);
+
+ return ResponseTypeDescriptor.container(ServiceResponse.class, "data", container, itemRefName);
+ }
+
+ private ObjectSchema objectContainerSchema(String itemPropertyName, String itemRefName) {
+ ObjectSchema schema = new ObjectSchema();
+ schema.addProperty(itemPropertyName, arraySchemaWithItemRef(itemRefName));
+ return schema;
+ }
+
+ private ArraySchema arraySchemaWithItemRef(String itemRefName) {
+ ArraySchema schema = new ArraySchema();
+ schema.setItems(new Schema<>().$ref("#/components/schemas/" + itemRefName));
+ return schema;
+ }
+
+ private void assertContainerMetadata(
+ Schema> wrapper, String containerName, String containerTypeName, String itemName) {
+ assertEquals(containerName, wrapper.getExtensions().get(VendorExtensions.DATA_CONTAINER));
+ assertEquals(
+ containerTypeName, wrapper.getExtensions().get(VendorExtensions.DATA_CONTAINER_TYPE));
+ assertEquals(itemName, wrapper.getExtensions().get(VendorExtensions.DATA_ITEM));
+ }
+
+ private OpenAPI openApi(NamedSchema... namedSchemas) {
+ Map schemas = new LinkedHashMap<>();
+
+ for (NamedSchema namedSchema : namedSchemas) {
+ schemas.put(namedSchema.name, namedSchema.schema);
+ }
+
+ return new OpenAPI().components(new Components().schemas(schemas));
+ }
+
+ private NamedSchema schema(String name, Schema> schema) {
+ schema.setName(name);
+ return new NamedSchema(name, schema);
+ }
+
+ private record NamedSchema(String name, Schema> schema) {}
+
+ private static final class ServiceResponse {}
+
+ private static final class Paging {}
+}
diff --git a/openapi-generics-server-starter/src/test/java/io/github/blueprintplatform/openapi/generics/server/core/schema/resolution/ComponentContainerSchemaResolverTest.java b/openapi-generics-server-starter/src/test/java/io/github/blueprintplatform/openapi/generics/server/core/schema/resolution/ComponentContainerSchemaResolverTest.java
new file mode 100644
index 00000000..7ed9b1fe
--- /dev/null
+++ b/openapi-generics-server-starter/src/test/java/io/github/blueprintplatform/openapi/generics/server/core/schema/resolution/ComponentContainerSchemaResolverTest.java
@@ -0,0 +1,211 @@
+package io.github.blueprintplatform.openapi.generics.server.core.schema.resolution;
+
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+
+import io.swagger.v3.oas.models.media.ArraySchema;
+import io.swagger.v3.oas.models.media.ComposedSchema;
+import io.swagger.v3.oas.models.media.JsonSchema;
+import io.swagger.v3.oas.models.media.ObjectSchema;
+import io.swagger.v3.oas.models.media.Schema;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Tag;
+import org.junit.jupiter.api.Test;
+
+@Tag("unit")
+@DisplayName("Unit Test: ComponentContainerSchemaResolver")
+class ComponentContainerSchemaResolverTest {
+
+ private final ComponentContainerSchemaResolver resolver = new ComponentContainerSchemaResolver();
+
+ @Test
+ @DisplayName("resolve -> should return null when data ref name does not exist")
+ void resolve_shouldReturnNull_whenDataRefNameDoesNotExist() {
+ Schema> result = resolver.resolve(Map.of(), "MissingDto", "WrapperDto", "data");
+
+ assertNull(result);
+ }
+
+ @Test
+ @DisplayName("resolve -> should resolve object container schema")
+ void resolve_shouldResolveObjectContainerSchema() {
+ ObjectSchema container = new ObjectSchema();
+ container.addProperty("content", new ArraySchema());
+
+ Schema> result =
+ resolver.resolve(
+ schemas("PageCustomerDto", container), "PageCustomerDto", "WrapperDto", "data");
+
+ assertSame(container, result);
+ }
+
+ @Test
+ @DisplayName("resolve -> should return null for object type without properties")
+ void resolve_shouldReturnNull_forObjectTypeWithoutProperties() {
+ Schema> container = new Schema<>().type("object");
+
+ Schema> result =
+ resolver.resolve(
+ schemas("PagingCustomerDto", container), "PagingCustomerDto", "WrapperDto", "data");
+
+ assertNull(result);
+ }
+
+ @Test
+ @DisplayName("resolve -> should resolve schema with properties")
+ void resolve_shouldResolveSchemaWithProperties() {
+ Schema> container = new Schema<>();
+ container.addProperty("items", new ArraySchema());
+
+ Schema> result =
+ resolver.resolve(
+ schemas("WindowCustomerDto", container), "WindowCustomerDto", "WrapperDto", "data");
+
+ assertSame(container, result);
+ }
+
+ @Test
+ @DisplayName("resolve -> should resolve array schema")
+ void resolve_shouldResolveArraySchema() {
+ ArraySchema container = new ArraySchema();
+
+ Schema> result =
+ resolver.resolve(
+ schemas("ListCustomerDto", container), "ListCustomerDto", "WrapperDto", "data");
+
+ assertSame(container, result);
+ }
+
+ @Test
+ @DisplayName("resolve -> should resolve schema with array type")
+ void resolve_shouldResolveSchemaWithArrayType() {
+ Schema> container = new Schema<>().type("array");
+
+ Schema> result =
+ resolver.resolve(
+ schemas("ListCustomerDto", container), "ListCustomerDto", "WrapperDto", "data");
+
+ assertSame(container, result);
+ }
+
+ @Test
+ @DisplayName("resolve -> should resolve json schema with array type")
+ void resolve_shouldResolveJsonSchemaWithArrayType() {
+ JsonSchema container = new JsonSchema();
+ container.setTypes(java.util.Set.of("array"));
+
+ Schema> result =
+ resolver.resolve(
+ schemas("ListCustomerDto", container), "ListCustomerDto", "WrapperDto", "data");
+
+ assertSame(container, result);
+ }
+
+ @Test
+ @DisplayName("resolve -> should dereference component schema")
+ void resolve_shouldDereferenceComponentSchema() {
+ ObjectSchema container = new ObjectSchema();
+ container.addProperty("content", new ArraySchema());
+
+ Schema> ref = new Schema<>().$ref("#/components/schemas/PageCustomerDto");
+
+ Map schemas =
+ schemas(
+ "PageCustomerDto", container,
+ "PageCustomerDtoRef", ref);
+
+ Schema> result = resolver.resolve(schemas, "PageCustomerDtoRef", "WrapperDto", "data");
+
+ assertSame(container, result);
+ }
+
+ @Test
+ @DisplayName("resolve -> should resolve container inside allOf")
+ void resolve_shouldResolveContainerInsideAllOf() {
+ ObjectSchema container = new ObjectSchema();
+ container.addProperty("content", new ArraySchema());
+
+ ComposedSchema composed = new ComposedSchema();
+ composed.setAllOf(List.of(new ObjectSchema(), container));
+
+ Schema> result =
+ resolver.resolve(
+ schemas("PageCustomerDto", composed), "PageCustomerDto", "WrapperDto", "data");
+
+ assertSame(container, result);
+ }
+
+ @Test
+ @DisplayName("resolve -> should resolve referenced container inside allOf")
+ void resolve_shouldResolveReferencedContainerInsideAllOf() {
+ ObjectSchema container = new ObjectSchema();
+ container.addProperty("content", new ArraySchema());
+
+ ComposedSchema composed = new ComposedSchema();
+ composed.setAllOf(List.of(new Schema<>().$ref("#/components/schemas/PageCustomerDto")));
+
+ Map schemas =
+ schemas(
+ "PageCustomerDto", container,
+ "ComposedPageCustomerDto", composed);
+
+ Schema> result = resolver.resolve(schemas, "ComposedPageCustomerDto", "WrapperDto", "data");
+
+ assertSame(container, result);
+ }
+
+ @Test
+ @DisplayName("resolve -> should return null when schema is not container-like")
+ void resolve_shouldReturnNull_whenSchemaIsNotContainerLike() {
+ Schema> scalar = new Schema<>().type("string");
+
+ Schema> result =
+ resolver.resolve(schemas("StringDto", scalar), "StringDto", "WrapperDto", "data");
+
+ assertNull(result);
+ }
+
+ @Test
+ @DisplayName("resolve -> should return null when component ref is missing")
+ void resolve_shouldReturnNull_whenComponentRefIsMissing() {
+ Schema> ref = new Schema<>().$ref("#/components/schemas/MissingDto");
+
+ Schema> result =
+ resolver.resolve(schemas("BrokenRefDto", ref), "BrokenRefDto", "WrapperDto", "data");
+
+ assertNull(result);
+ }
+
+ @Test
+ @DisplayName("resolve -> should return null when component ref cycle is detected")
+ void resolve_shouldReturnNull_whenComponentRefCycleIsDetected() {
+ Schema> first = new Schema<>().$ref("#/components/schemas/SecondDto");
+ Schema> second = new Schema<>().$ref("#/components/schemas/FirstDto");
+
+ Map schemas =
+ schemas(
+ "FirstDto", first,
+ "SecondDto", second);
+
+ Schema> result = resolver.resolve(schemas, "FirstDto", "WrapperDto", "data");
+
+ assertNull(result);
+ }
+
+ private Map schemas(String name, Schema> schema) {
+ Map schemas = new LinkedHashMap<>();
+ schemas.put(name, schema);
+ return schemas;
+ }
+
+ private Map schemas(
+ String firstName, Schema> firstSchema, String secondName, Schema> secondSchema) {
+ Map schemas = new LinkedHashMap<>();
+ schemas.put(firstName, firstSchema);
+ schemas.put(secondName, secondSchema);
+ return schemas;
+ }
+}
diff --git a/openapi-generics-server-starter/src/test/java/io/github/blueprintplatform/openapi/generics/server/core/validation/OpenApiContractGuardTest.java b/openapi-generics-server-starter/src/test/java/io/github/blueprintplatform/openapi/generics/server/core/validation/OpenApiContractGuardTest.java
index 65e92ac0..5702fb82 100644
--- a/openapi-generics-server-starter/src/test/java/io/github/blueprintplatform/openapi/generics/server/core/validation/OpenApiContractGuardTest.java
+++ b/openapi-generics-server-starter/src/test/java/io/github/blueprintplatform/openapi/generics/server/core/validation/OpenApiContractGuardTest.java
@@ -10,7 +10,10 @@
import io.github.blueprintplatform.openapi.generics.contract.envelope.ServiceResponse;
import io.github.blueprintplatform.openapi.generics.contract.paging.Page;
import io.github.blueprintplatform.openapi.generics.server.core.introspection.ResponseTypeDescriptor;
-import io.github.blueprintplatform.openapi.generics.server.core.introspection.container.SupportedContainerType;
+import io.github.blueprintplatform.openapi.generics.server.core.introspection.container.descriptor.ContainerMatchMode;
+import io.github.blueprintplatform.openapi.generics.server.core.introspection.container.descriptor.ContainerShape;
+import io.github.blueprintplatform.openapi.generics.server.core.introspection.container.descriptor.ContainerSource;
+import io.github.blueprintplatform.openapi.generics.server.core.introspection.container.descriptor.SupportedContainerDescriptor;
import io.swagger.v3.oas.models.Components;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.media.ComposedSchema;
@@ -51,7 +54,14 @@ void validate_shouldPass_forValidContainerWrapper() {
ResponseTypeDescriptor.container(
ServiceResponse.class,
"data",
- new SupportedContainerType(Page.class, PAGE, PAGE),
+ new SupportedContainerDescriptor(
+ Page.class,
+ PAGE,
+ PAGE,
+ ContainerShape.OBJECT_WITH_ITEM_ARRAY,
+ "content",
+ ContainerSource.BUILT_IN,
+ ContainerMatchMode.EXACT),
"CustomerDto");
Schema> wrapper = wrapperWithAllOfPayload("data");
@@ -205,7 +215,14 @@ void validate_shouldFail_whenContainerExtensionsMissing() {
ResponseTypeDescriptor.container(
ServiceResponse.class,
"data",
- new SupportedContainerType(Page.class, PAGE, PAGE),
+ new SupportedContainerDescriptor(
+ Page.class,
+ PAGE,
+ PAGE,
+ ContainerShape.OBJECT_WITH_ITEM_ARRAY,
+ "content",
+ ContainerSource.BUILT_IN,
+ ContainerMatchMode.EXACT),
"CustomerDto");
Schema> wrapper = wrapperWithAllOfPayload("data");
@@ -227,7 +244,14 @@ void validate_shouldFail_whenContainerInvalid() {
ResponseTypeDescriptor.container(
ServiceResponse.class,
"data",
- new SupportedContainerType(Page.class, PAGE, PAGE),
+ new SupportedContainerDescriptor(
+ Page.class,
+ PAGE,
+ PAGE,
+ ContainerShape.OBJECT_WITH_ITEM_ARRAY,
+ "content",
+ ContainerSource.BUILT_IN,
+ ContainerMatchMode.EXACT),
"CustomerDto");
Schema> wrapper = wrapperWithAllOfPayload("data");
@@ -251,9 +275,15 @@ void validate_shouldFail_whenItemInvalid() {
ResponseTypeDescriptor.container(
ServiceResponse.class,
"data",
- new SupportedContainerType(Page.class, PAGE, PAGE),
+ new SupportedContainerDescriptor(
+ Page.class,
+ PAGE,
+ PAGE,
+ ContainerShape.OBJECT_WITH_ITEM_ARRAY,
+ "content",
+ ContainerSource.BUILT_IN,
+ ContainerMatchMode.EXACT),
"CustomerDto");
-
Schema> wrapper = wrapperWithAllOfPayload("data");
wrapper.addExtension(API_WRAPPER, true);
wrapper.addExtension(API_WRAPPER_DATATYPE, "PageCustomerDto");
diff --git a/samples/type-coverage/byoe-response/README.md b/samples/type-coverage/byoe-response/README.md
index f38d4d03..3f91b889 100644
--- a/samples/type-coverage/byoe-response/README.md
+++ b/samples/type-coverage/byoe-response/README.md
@@ -1,10 +1,10 @@
# byoe-response-type-coverage
-> End-to-end verification sample for OpenAPI projection, client generation, and runtime reconstruction using a custom response envelope.
+> End-to-end verification sample for OpenAPI projection, client generation, and runtime reconstruction of user-owned `ApiResponse` contracts.
This sample demonstrates the **Bring Your Own Envelope (BYOE)** capabilities of OpenAPI Generics.
-Instead of using the platform-provided envelope implementation, the producer exposes endpoints using a completely user-owned generic response contract.
+Instead of using the platform-provided response envelope, the producer exposes endpoints using a completely application-owned generic contract:
```java
ApiResponse
@@ -12,95 +12,59 @@ ApiResponse
The sample verifies that OpenAPI Generics can:
-- project custom generic envelopes into OpenAPI
-- reconstruct generic type information during code generation
-- generate strongly typed clients
-- deserialize responses back into the original contract shape
-- support scalar, value, object, collection, and paged payloads
+- project user-owned generic response envelopes into OpenAPI
+- reconstruct generic type information during client generation
+- generate strongly typed Java clients
+- deserialize responses back into the original application contract
+- support scalar, value, object, collection, built-in page, and application-owned generic container payloads
without requiring changes to the original response contract.
---
-# Table of Contents
+## Table of Contents
-- [Purpose](#purpose)
-- [Architecture](#architecture)
+- [What This Sample Validates](#what-this-sample-validates)
- [Modules](#modules)
-- [Custom Envelope](#custom-envelope)
-- [Supported Response Shapes](#supported-response-shapes)
-- [Verification Matrix](#verification-matrix)
+- [User-Owned Envelope](#user-owned-envelope)
+- [Covered Response Shapes](#covered-response-shapes)
- [Running the Sample](#running-the-sample)
- [Verification Endpoints](#verification-endpoints)
-- [BYOE Projection Flow](#byoe-projection-flow)
-- [What This Sample Protects](#what-this-sample-protects)
+- [Mental Model](#mental-model)
---
-# Purpose
+## What This Sample Validates
-The goal of this sample is to validate the complete BYOE lifecycle:
+The sample exercises the complete OpenAPI Generics flow:
```text
User-Owned Contract
- ↓
+ ↓
Spring Endpoint
- ↓
+ ↓
OpenAPI Projection
- ↓
+ ↓
Generated Client
- ↓
+ ↓
Consumer Runtime
```
The consumer uses only generated client artifacts.
-No DTO adapters.
+No manual DTO mapping.
No manual envelope reconstruction.
-No custom deserialization code.
-
-The generated client must reconstruct the original generic contract automatically.
-
----
-
-# Architecture
-
-```text
-Producer
- │
- ▼
-OpenAPI Projection
- │
- ▼
-Generated Client
- │
- ▼
-Consumer
-```
-
-The producer publishes endpoints using:
-
-```java
-ApiResponse
-```
+No manual generic container reconstruction.
-The OpenAPI document contains projected wrapper schemas.
-
-The OpenAPI Generics code generator reconstructs the original generic contract.
-
-The generated client returns:
-
-```java
-ApiResponse
-```
+No custom deserialization code.
-instead of generated wrapper DTOs.
+The generated client automatically reconstructs the original application contract together with any nested generic payload types.
---
-# Modules
+## Modules
```text
byoe-response-type-coverage
@@ -111,18 +75,17 @@ byoe-response-type-coverage
```
| Module | Responsibility |
-|----------|----------|
-| contract | User-owned ApiResponse contract |
-| producer | Publishes ApiResponse-based endpoints |
+|----------|----------------|
+| contract | User-owned `ApiResponse` and generic payload container contracts |
+| producer | Publishes `ApiResponse`-based endpoints |
| client | Generated Java client |
-| consumer | Consumes generated client |
-| consumer-api | Verification endpoints for runtime testing |
+| consumer | Uses the generated client and exposes verification endpoints |
---
-# Custom Envelope
+## User-Owned Envelope
-The sample uses a completely user-owned envelope.
+The sample uses a completely application-owned response envelope.
```java
public class ApiResponse {
@@ -149,120 +112,81 @@ public record ApiError(
These classes are not provided by OpenAPI Generics.
-OpenAPI Generics only projects and reconstructs them.
-
-The ownership of the contract remains entirely with the application.
+OpenAPI Generics projects and reconstructs them while preserving the original application contract.
---
-# Supported Response Shapes
+## Covered Response Shapes
-## Scalar Payloads
+### Scalar Payloads
```java
ApiResponse
-
ApiResponse
-
ApiResponse
-
ApiResponse
-
ApiResponse
```
----
-
-## Value Payloads
+### Value Payloads
```java
ApiResponse
-
ApiResponse
-
ApiResponse
-
ApiResponse
```
----
-
-## Object Payloads
+### Object Payloads
```java
ApiResponse
-
ApiResponse
```
-Including:
-
-- nested DTOs
-- enums
-- collections
-- maps
-- temporal types
-
----
-
-## Collection Payloads
+### List Payloads
```java
ApiResponse>
-
ApiResponse>
+```
-ApiResponse>
+### Set Payloads
+```java
+ApiResponse>
ApiResponse>
```
----
-
-## Paged Payloads
+### Built-in Page Payloads
```java
ApiResponse>
-
ApiResponse>
```
-Using:
+using the built-in platform `Page` contract.
+
+### Application-owned Generic Container Payloads
```java
-io.github.blueprintplatform.openapi.generics.contract.paging.Page
-```
+ApiResponse>
+ApiResponse>
----
+ApiResponse>
+ApiResponse>
+```
-# Verification Matrix
-
-| Category | Verified |
-|----------|----------|
-| Scalar types | ✓ |
-| UUID | ✓ |
-| LocalDate | ✓ |
-| OffsetDateTime | ✓ |
-| Enum payloads | ✓ |
-| DTO payloads | ✓ |
-| Nested DTO graphs | ✓ |
-| List payloads | ✓ |
-| Set payloads | ✓ |
-| Page payloads | ✓ |
-| Generated client reconstruction | ✓ |
-| Runtime deserialization | ✓ |
-| Consumer integration | ✓ |
-| User-owned envelope | ✓ |
+These payloads verify that nested application-owned generic containers are projected into OpenAPI and reconstructed by the generated client without requiring custom serialization, mapping, or adapter code.
---
-# Running the Sample
+## Running the Sample
-## Start Producer
+### Start Producer
```bash
cd producer
-
mvn spring-boot:run
```
@@ -284,13 +208,10 @@ OpenAPI document:
http://localhost:8076/type-coverage/byoe-response/v3/api-docs.yaml
```
----
-
-## Start Consumer
+### Start Consumer
```bash
cd consumer
-
mvn spring-boot:run
```
@@ -304,16 +225,14 @@ http://localhost:8077/type-coverage/byoe-response-consumer
## Quick Smoke Test
-After starting both producer and consumer, a small subset of endpoints can be called directly to verify that the generated client reconstructs the expected generic response types correctly.
+After starting both producer and consumer, a representative subset of endpoints can be called directly to verify that the generated client reconstructs the expected generic response types correctly.
-The goal of these requests is not to validate business behavior.
-
-They provide a fast end-to-end sanity check for:
+These requests provide a fast end-to-end verification of:
- OpenAPI projection
-- custom envelope projection
- generated client generation
-- generic wrapper reconstruction
+- user-owned envelope reconstruction
+- nested generic payload reconstruction
- runtime deserialization
- consumer integration
@@ -327,9 +246,13 @@ curl http://localhost:8076/type-coverage/byoe-response/types/lists/summaries
curl http://localhost:8076/type-coverage/byoe-response/types/sets/statuses
curl http://localhost:8076/type-coverage/byoe-response/types/pages/summaries
+
+curl http://localhost:8076/type-coverage/byoe-response/types/paging/summaries
+
+curl http://localhost:8076/type-coverage/byoe-response/types/windows/summaries
```
-Expected generic shapes:
+Expected generic response shapes:
```java
ApiResponse
@@ -339,6 +262,10 @@ ApiResponse>
ApiResponse>
ApiResponse>
+
+ApiResponse>
+
+ApiResponse>
```
### Consumer Verification
@@ -351,154 +278,104 @@ curl http://localhost:8077/type-coverage/byoe-response-consumer/types/lists/summ
curl http://localhost:8077/type-coverage/byoe-response-consumer/types/sets/statuses
curl http://localhost:8077/type-coverage/byoe-response-consumer/types/pages/summaries
-```
-
-The consumer uses only generated client artifacts.
-
-No manual DTO mapping, custom deserialization, or envelope reconstruction exists in the consumer application.
-Successful responses verify that generic type information survives the complete pipeline:
+curl http://localhost:8077/type-coverage/byoe-response-consumer/types/paging/summaries
-```text
-User-Owned Contract
- ↓
-OpenAPI Projection
- ↓
-Generated Client
- ↓
-Consumer Runtime
+curl http://localhost:8077/type-coverage/byoe-response-consumer/types/windows/summaries
```
-and is reconstructed back into the original contract shape:
+The consumer uses only generated client artifacts.
-```java
-ApiResponse
-```
+No manual DTO mapping, custom deserialization, envelope reconstruction, or generic container reconstruction exists in the consumer application.
---
-# Verification Endpoints
+## Verification Endpoints
-## Scalars
+### Scalars
```text
/types/scalars/string
-
/types/scalars/boolean
-
/types/scalars/integer
-
/types/scalars/long
-
/types/scalars/decimal
```
----
-
-## Values
+### Values
```text
/types/values/uuid
-
/types/values/date
-
/types/values/datetime
-
/types/values/enum
```
----
-
-## Objects
+### Objects
```text
/types/objects/address
-
/types/objects/profile
```
----
-
-## Lists
+### Lists
```text
/types/lists/summaries
-
/types/lists/statuses
```
----
-
-## Sets
+### Sets
```text
/types/sets/summaries
-
/types/sets/statuses
```
----
-
-## Pages
+### Built-in Pages
```text
/types/pages/summaries
-
/types/pages/statuses
```
----
-
-# BYOE Projection Flow
-
-The producer starts with:
-
-```java
-ApiResponse>
-```
-
-Projection phase:
+### Application-owned Generic Containers
```text
-ApiResponse>
- ↓
-ApiResponsePageTypeSummaryDto
-```
-
-Generated client phase:
+/types/paging/summaries
+/types/paging/statuses
-```text
-ApiResponsePageTypeSummaryDto
- ↓
-ApiResponse>
+/types/windows/summaries
+/types/windows/statuses
```
-Consumer phase:
+These endpoints verify that the generated client correctly reconstructs:
```java
-ApiResponse>
+ApiResponse
+ApiResponse>
+ApiResponse>
+ApiResponse>
+ApiResponse>
+ApiResponse>
```
-The wrapper DTO exists only as a projection artifact.
-
-The consumer works exclusively with the original generic contract.
+across primitive, value, enum, DTO, collection, built-in page, and application-owned generic container payloads.
---
-# What This Sample Protects
-
-This sample serves as a regression suite for BYOE support.
-
-It validates that future changes do not break:
+## Mental Model
-- custom envelope projection
-- generic payload reconstruction
-- collection reconstruction
-- page reconstruction
-- generated client typing
-- runtime deserialization
-- consumer integration
-
-The sample is intentionally designed as an end-to-end verification pipeline rather than a unit-level feature demonstration.
+```text
+ApiResponse>
+ ↓
+OpenAPI Projection
+ ↓
+Generated Client
+ ↓
+Consumer Deserialization
+ ↓
+ApiResponse>
+```
-Its purpose is to guarantee that user-owned response contracts continue to work without modification across future OpenAPI Generics releases.
\ No newline at end of file
+The same reconstruction flow applies to every supported response shape, including application-owned generic payload containers nested inside the user-owned response envelope.
\ No newline at end of file
diff --git a/samples/type-coverage/byoe-response/client/src/main/java/io/github/blueprintplatform/samples/typecoverage/client/adapter/TypeCoverageClientAdapter.java b/samples/type-coverage/byoe-response/client/src/main/java/io/github/blueprintplatform/samples/typecoverage/client/adapter/TypeCoverageClientAdapter.java
index 945ea819..c6950d15 100644
--- a/samples/type-coverage/byoe-response/client/src/main/java/io/github/blueprintplatform/samples/typecoverage/client/adapter/TypeCoverageClientAdapter.java
+++ b/samples/type-coverage/byoe-response/client/src/main/java/io/github/blueprintplatform/samples/typecoverage/client/adapter/TypeCoverageClientAdapter.java
@@ -6,6 +6,8 @@
import io.github.blueprintplatform.samples.typecoverage.client.generated.dto.TypeProfileDto;
import io.github.blueprintplatform.samples.typecoverage.client.generated.dto.TypeSummaryDto;
import io.github.blueprintplatform.samples.typecoverage.contract.ApiResponse;
+import io.github.blueprintplatform.samples.typecoverage.contract.Paging;
+import io.github.blueprintplatform.samples.typecoverage.contract.Window;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.OffsetDateTime;
@@ -48,4 +50,12 @@ public interface TypeCoverageClientAdapter {
ApiResponse> pagedSummaries();
ApiResponse> pagedStatuses();
+
+ ApiResponse> pagingSummaries();
+
+ ApiResponse> pagingStatuses();
+
+ ApiResponse> windowSummaries();
+
+ ApiResponse> windowStatuses();
}
diff --git a/samples/type-coverage/byoe-response/client/src/main/java/io/github/blueprintplatform/samples/typecoverage/client/adapter/config/TypeCoverageApiClientConfig.java b/samples/type-coverage/byoe-response/client/src/main/java/io/github/blueprintplatform/samples/typecoverage/client/adapter/config/TypeCoverageApiClientConfig.java
index 0a8e06fd..da9a9ed0 100644
--- a/samples/type-coverage/byoe-response/client/src/main/java/io/github/blueprintplatform/samples/typecoverage/client/adapter/config/TypeCoverageApiClientConfig.java
+++ b/samples/type-coverage/byoe-response/client/src/main/java/io/github/blueprintplatform/samples/typecoverage/client/adapter/config/TypeCoverageApiClientConfig.java
@@ -1,11 +1,6 @@
package io.github.blueprintplatform.samples.typecoverage.client.adapter.config;
-import io.github.blueprintplatform.samples.typecoverage.client.generated.api.ListPayloadControllerApi;
-import io.github.blueprintplatform.samples.typecoverage.client.generated.api.ObjectPayloadControllerApi;
-import io.github.blueprintplatform.samples.typecoverage.client.generated.api.PagedPayloadControllerApi;
-import io.github.blueprintplatform.samples.typecoverage.client.generated.api.ScalarPayloadControllerApi;
-import io.github.blueprintplatform.samples.typecoverage.client.generated.api.SetPayloadControllerApi;
-import io.github.blueprintplatform.samples.typecoverage.client.generated.api.ValuePayloadControllerApi;
+import io.github.blueprintplatform.samples.typecoverage.client.generated.api.*;
import io.github.blueprintplatform.samples.typecoverage.client.generated.invoker.ApiClient;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
@@ -55,4 +50,14 @@ SetPayloadControllerApi setPayloadControllerApi(ApiClient typeCoverageApiClient)
PagedPayloadControllerApi pagedPayloadControllerApi(ApiClient typeCoverageApiClient) {
return new PagedPayloadControllerApi(typeCoverageApiClient);
}
+
+ @Bean
+ PagingPayloadControllerApi pagingPayloadControllerApi(ApiClient typeCoverageApiClient) {
+ return new PagingPayloadControllerApi(typeCoverageApiClient);
+ }
+
+ @Bean
+ WindowPayloadControllerApi windowPayloadControllerApi(ApiClient typeCoverageApiClient) {
+ return new WindowPayloadControllerApi(typeCoverageApiClient);
+ }
}
diff --git a/samples/type-coverage/byoe-response/client/src/main/java/io/github/blueprintplatform/samples/typecoverage/client/adapter/impl/TypeCoverageClientAdapterImpl.java b/samples/type-coverage/byoe-response/client/src/main/java/io/github/blueprintplatform/samples/typecoverage/client/adapter/impl/TypeCoverageClientAdapterImpl.java
index 9ea85ef4..954d5d19 100644
--- a/samples/type-coverage/byoe-response/client/src/main/java/io/github/blueprintplatform/samples/typecoverage/client/adapter/impl/TypeCoverageClientAdapterImpl.java
+++ b/samples/type-coverage/byoe-response/client/src/main/java/io/github/blueprintplatform/samples/typecoverage/client/adapter/impl/TypeCoverageClientAdapterImpl.java
@@ -2,17 +2,14 @@
import io.github.blueprintplatform.openapi.generics.contract.paging.Page;
import io.github.blueprintplatform.samples.typecoverage.client.adapter.TypeCoverageClientAdapter;
-import io.github.blueprintplatform.samples.typecoverage.client.generated.api.ListPayloadControllerApi;
-import io.github.blueprintplatform.samples.typecoverage.client.generated.api.ObjectPayloadControllerApi;
-import io.github.blueprintplatform.samples.typecoverage.client.generated.api.PagedPayloadControllerApi;
-import io.github.blueprintplatform.samples.typecoverage.client.generated.api.ScalarPayloadControllerApi;
-import io.github.blueprintplatform.samples.typecoverage.client.generated.api.SetPayloadControllerApi;
-import io.github.blueprintplatform.samples.typecoverage.client.generated.api.ValuePayloadControllerApi;
+import io.github.blueprintplatform.samples.typecoverage.client.generated.api.*;
import io.github.blueprintplatform.samples.typecoverage.client.generated.dto.AddressDto;
import io.github.blueprintplatform.samples.typecoverage.client.generated.dto.CoverageStatus;
import io.github.blueprintplatform.samples.typecoverage.client.generated.dto.TypeProfileDto;
import io.github.blueprintplatform.samples.typecoverage.client.generated.dto.TypeSummaryDto;
import io.github.blueprintplatform.samples.typecoverage.contract.ApiResponse;
+import io.github.blueprintplatform.samples.typecoverage.contract.Paging;
+import io.github.blueprintplatform.samples.typecoverage.contract.Window;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.OffsetDateTime;
@@ -30,20 +27,26 @@ public class TypeCoverageClientAdapterImpl implements TypeCoverageClientAdapter
private final ListPayloadControllerApi listApi;
private final SetPayloadControllerApi setApi;
private final PagedPayloadControllerApi pagedApi;
+ private final PagingPayloadControllerApi pagingApi;
+ private final WindowPayloadControllerApi windowApi;
public TypeCoverageClientAdapterImpl(
- ScalarPayloadControllerApi scalarApi,
- ValuePayloadControllerApi valueApi,
- ObjectPayloadControllerApi objectApi,
- ListPayloadControllerApi listApi,
- SetPayloadControllerApi setApi,
- PagedPayloadControllerApi pagedApi) {
+ ScalarPayloadControllerApi scalarApi,
+ ValuePayloadControllerApi valueApi,
+ ObjectPayloadControllerApi objectApi,
+ ListPayloadControllerApi listApi,
+ SetPayloadControllerApi setApi,
+ PagedPayloadControllerApi pagedApi,
+ PagingPayloadControllerApi pagingApi,
+ WindowPayloadControllerApi windowApi) {
this.scalarApi = scalarApi;
this.valueApi = valueApi;
this.objectApi = objectApi;
this.listApi = listApi;
this.setApi = setApi;
this.pagedApi = pagedApi;
+ this.pagingApi = pagingApi;
+ this.windowApi = windowApi;
}
@Override
@@ -130,4 +133,24 @@ public ApiResponse> pagedSummaries() {
public ApiResponse> pagedStatuses() {
return pagedApi.pagedStatuses();
}
+
+ @Override
+ public ApiResponse> pagingSummaries() {
+ return pagingApi.pagingSummaries();
+ }
+
+ @Override
+ public ApiResponse> pagingStatuses() {
+ return pagingApi.pagingStatuses();
+ }
+
+ @Override
+ public ApiResponse> windowSummaries() {
+ return windowApi.windowSummaries();
+ }
+
+ @Override
+ public ApiResponse> windowStatuses() {
+ return windowApi.windowStatuses();
+ }
}
diff --git a/samples/type-coverage/byoe-response/consumer/src/main/java/io/github/blueprintplatform/samples/typecoverage/consumer/api/controller/TypeCoverageConsumerController.java b/samples/type-coverage/byoe-response/consumer/src/main/java/io/github/blueprintplatform/samples/typecoverage/consumer/api/controller/TypeCoverageConsumerController.java
index c50c9c23..c43a2d2b 100644
--- a/samples/type-coverage/byoe-response/consumer/src/main/java/io/github/blueprintplatform/samples/typecoverage/consumer/api/controller/TypeCoverageConsumerController.java
+++ b/samples/type-coverage/byoe-response/consumer/src/main/java/io/github/blueprintplatform/samples/typecoverage/consumer/api/controller/TypeCoverageConsumerController.java
@@ -7,6 +7,8 @@
import io.github.blueprintplatform.samples.typecoverage.client.generated.dto.TypeSummaryDto;
import io.github.blueprintplatform.samples.typecoverage.consumer.service.TypeCoverageConsumerService;
import io.github.blueprintplatform.samples.typecoverage.contract.ApiResponse;
+import io.github.blueprintplatform.samples.typecoverage.contract.Paging;
+import io.github.blueprintplatform.samples.typecoverage.contract.Window;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.OffsetDateTime;
@@ -113,4 +115,24 @@ public ResponseEntity>> pagedSummaries() {
public ResponseEntity>> pagedStatuses() {
return ResponseEntity.ok(service.pagedStatuses());
}
+
+ @GetMapping("/paging/summaries")
+ public ResponseEntity>> pagingSummaries() {
+ return ResponseEntity.ok(service.pagingSummaries());
+ }
+
+ @GetMapping("/paging/statuses")
+ public ResponseEntity>> pagingStatuses() {
+ return ResponseEntity.ok(service.pagingStatuses());
+ }
+
+ @GetMapping("/windows/summaries")
+ public ResponseEntity>> windowSummaries() {
+ return ResponseEntity.ok(service.windowSummaries());
+ }
+
+ @GetMapping("/windows/statuses")
+ public ResponseEntity>> windowStatuses() {
+ return ResponseEntity.ok(service.windowStatuses());
+ }
}
diff --git a/samples/type-coverage/byoe-response/consumer/src/main/java/io/github/blueprintplatform/samples/typecoverage/consumer/service/TypeCoverageConsumerService.java b/samples/type-coverage/byoe-response/consumer/src/main/java/io/github/blueprintplatform/samples/typecoverage/consumer/service/TypeCoverageConsumerService.java
index 7adf12d5..f760c344 100644
--- a/samples/type-coverage/byoe-response/consumer/src/main/java/io/github/blueprintplatform/samples/typecoverage/consumer/service/TypeCoverageConsumerService.java
+++ b/samples/type-coverage/byoe-response/consumer/src/main/java/io/github/blueprintplatform/samples/typecoverage/consumer/service/TypeCoverageConsumerService.java
@@ -6,6 +6,8 @@
import io.github.blueprintplatform.samples.typecoverage.client.generated.dto.TypeProfileDto;
import io.github.blueprintplatform.samples.typecoverage.client.generated.dto.TypeSummaryDto;
import io.github.blueprintplatform.samples.typecoverage.contract.ApiResponse;
+import io.github.blueprintplatform.samples.typecoverage.contract.Paging;
+import io.github.blueprintplatform.samples.typecoverage.contract.Window;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.OffsetDateTime;
@@ -48,4 +50,12 @@ public interface TypeCoverageConsumerService {
ApiResponse> pagedSummaries();
ApiResponse> pagedStatuses();
+
+ ApiResponse> pagingSummaries();
+
+ ApiResponse> pagingStatuses();
+
+ ApiResponse> windowSummaries();
+
+ ApiResponse> windowStatuses();
}
diff --git a/samples/type-coverage/byoe-response/consumer/src/main/java/io/github/blueprintplatform/samples/typecoverage/consumer/service/impl/TypeCoverageConsumerServiceImpl.java b/samples/type-coverage/byoe-response/consumer/src/main/java/io/github/blueprintplatform/samples/typecoverage/consumer/service/impl/TypeCoverageConsumerServiceImpl.java
index 4ac87372..ada3d2b4 100644
--- a/samples/type-coverage/byoe-response/consumer/src/main/java/io/github/blueprintplatform/samples/typecoverage/consumer/service/impl/TypeCoverageConsumerServiceImpl.java
+++ b/samples/type-coverage/byoe-response/consumer/src/main/java/io/github/blueprintplatform/samples/typecoverage/consumer/service/impl/TypeCoverageConsumerServiceImpl.java
@@ -8,6 +8,8 @@
import io.github.blueprintplatform.samples.typecoverage.client.generated.dto.TypeSummaryDto;
import io.github.blueprintplatform.samples.typecoverage.consumer.service.TypeCoverageConsumerService;
import io.github.blueprintplatform.samples.typecoverage.contract.ApiResponse;
+import io.github.blueprintplatform.samples.typecoverage.contract.Paging;
+import io.github.blueprintplatform.samples.typecoverage.contract.Window;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.OffsetDateTime;
@@ -109,4 +111,24 @@ public ApiResponse> pagedSummaries() {
public ApiResponse> pagedStatuses() {
return adapter.pagedStatuses();
}
+
+ @Override
+ public ApiResponse> pagingSummaries() {
+ return adapter.pagingSummaries();
+ }
+
+ @Override
+ public ApiResponse> pagingStatuses() {
+ return adapter.pagingStatuses();
+ }
+
+ @Override
+ public ApiResponse> windowSummaries() {
+ return adapter.windowSummaries();
+ }
+
+ @Override
+ public ApiResponse> windowStatuses() {
+ return adapter.windowStatuses();
+ }
}
diff --git a/samples/type-coverage/byoe-response/contract/src/main/java/io/github/blueprintplatform/samples/typecoverage/contract/Paging.java b/samples/type-coverage/byoe-response/contract/src/main/java/io/github/blueprintplatform/samples/typecoverage/contract/Paging.java
new file mode 100644
index 00000000..e27e22ac
--- /dev/null
+++ b/samples/type-coverage/byoe-response/contract/src/main/java/io/github/blueprintplatform/samples/typecoverage/contract/Paging.java
@@ -0,0 +1,35 @@
+package io.github.blueprintplatform.samples.typecoverage.contract;
+
+import java.util.List;
+
+/**
+ * User-owned paged container contract used to verify BYOC container reconstruction.
+ *
+ * @param content current page items
+ * @param page zero-based page index
+ * @param size requested page size
+ * @param totalElements total available element count
+ * @param totalPages total available page count
+ * @param hasNext whether another page exists
+ * @param item type
+ */
+public record Paging(
+ List content, int page, int size, long totalElements, int totalPages, boolean hasNext) {
+
+ public Paging {
+ content = content == null ? List.of() : List.copyOf(content);
+ }
+
+ public static Paging of(List content, int page, int size, long totalElements) {
+ int safePage = Math.max(page, 0);
+ int safeSize = Math.max(size, 1);
+
+ long totalPagesLong = totalElements <= 0L ? 0L : ((totalElements + safeSize - 1L) / safeSize);
+
+ int totalPages = totalPagesLong > Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) totalPagesLong;
+
+ boolean hasNext = safePage < totalPages - 1;
+
+ return new Paging<>(content, safePage, safeSize, totalElements, totalPages, hasNext);
+ }
+}
diff --git a/samples/type-coverage/byoe-response/contract/src/main/java/io/github/blueprintplatform/samples/typecoverage/contract/Window.java b/samples/type-coverage/byoe-response/contract/src/main/java/io/github/blueprintplatform/samples/typecoverage/contract/Window.java
new file mode 100644
index 00000000..97209cea
--- /dev/null
+++ b/samples/type-coverage/byoe-response/contract/src/main/java/io/github/blueprintplatform/samples/typecoverage/contract/Window.java
@@ -0,0 +1,22 @@
+package io.github.blueprintplatform.samples.typecoverage.contract;
+
+import java.util.List;
+
+/**
+ * User-owned cursor/window container contract used to verify BYOC container reconstruction.
+ *
+ * @param items current window items
+ * @param nextCursor cursor for the next window, if available
+ * @param hasNext whether another window exists
+ * @param item type
+ */
+public record Window(List items, String nextCursor, boolean hasNext) {
+
+ public Window {
+ items = items == null ? List.of() : List.copyOf(items);
+ }
+
+ public static Window of(List items, String nextCursor, boolean hasNext) {
+ return new Window<>(items, nextCursor, hasNext);
+ }
+}
diff --git a/samples/type-coverage/byoe-response/producer/pom.xml b/samples/type-coverage/byoe-response/producer/pom.xml
index b3bd1e45..590aa4f8 100644
--- a/samples/type-coverage/byoe-response/producer/pom.xml
+++ b/samples/type-coverage/byoe-response/producer/pom.xml
@@ -53,6 +53,11 @@
spring-boot-starter-validation
+
+ org.springframework.data
+ spring-data-commons
+
+
org.springdoc
springdoc-openapi-starter-webmvc-ui
diff --git a/samples/type-coverage/byoe-response/producer/src/main/java/io/github/blueprintplatform/samples/typecoverage/api/controller/PagingPayloadController.java b/samples/type-coverage/byoe-response/producer/src/main/java/io/github/blueprintplatform/samples/typecoverage/api/controller/PagingPayloadController.java
new file mode 100644
index 00000000..6430ac83
--- /dev/null
+++ b/samples/type-coverage/byoe-response/producer/src/main/java/io/github/blueprintplatform/samples/typecoverage/api/controller/PagingPayloadController.java
@@ -0,0 +1,42 @@
+package io.github.blueprintplatform.samples.typecoverage.api.controller;
+
+import io.github.blueprintplatform.samples.typecoverage.api.dto.CoverageStatus;
+import io.github.blueprintplatform.samples.typecoverage.api.dto.TypeSummaryDto;
+import io.github.blueprintplatform.samples.typecoverage.contract.ApiResponse;
+import io.github.blueprintplatform.samples.typecoverage.contract.Paging;
+import java.util.List;
+import java.util.UUID;
+import org.springframework.http.MediaType;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+@RestController
+@RequestMapping(value = "/types/paging", produces = MediaType.APPLICATION_JSON_VALUE)
+public class PagingPayloadController {
+
+ @GetMapping("/summaries")
+ public ResponseEntity>> pagingSummaries() {
+ var content =
+ List.of(
+ new TypeSummaryDto(
+ UUID.fromString("55555555-5555-5555-5555-555555555555"),
+ "BYOE-PAGING-001",
+ CoverageStatus.ACTIVE),
+ new TypeSummaryDto(
+ UUID.fromString("66666666-6666-6666-6666-666666666666"),
+ "BYOE-PAGING-002",
+ CoverageStatus.EXPERIMENTAL));
+
+ return ResponseEntity.ok(ApiResponse.ok(Paging.of(content, 0, 2, 2)));
+ }
+
+ @GetMapping("/statuses")
+ public ResponseEntity>> pagingStatuses() {
+ var content =
+ List.of(CoverageStatus.ACTIVE, CoverageStatus.PASSIVE, CoverageStatus.EXPERIMENTAL);
+
+ return ResponseEntity.ok(ApiResponse.ok(Paging.of(content, 0, 3, 3)));
+ }
+}
diff --git a/samples/type-coverage/byoe-response/producer/src/main/java/io/github/blueprintplatform/samples/typecoverage/api/controller/WindowPayloadController.java b/samples/type-coverage/byoe-response/producer/src/main/java/io/github/blueprintplatform/samples/typecoverage/api/controller/WindowPayloadController.java
new file mode 100644
index 00000000..61940685
--- /dev/null
+++ b/samples/type-coverage/byoe-response/producer/src/main/java/io/github/blueprintplatform/samples/typecoverage/api/controller/WindowPayloadController.java
@@ -0,0 +1,41 @@
+package io.github.blueprintplatform.samples.typecoverage.api.controller;
+
+import io.github.blueprintplatform.samples.typecoverage.api.dto.CoverageStatus;
+import io.github.blueprintplatform.samples.typecoverage.api.dto.TypeSummaryDto;
+import io.github.blueprintplatform.samples.typecoverage.contract.ApiResponse;
+import io.github.blueprintplatform.samples.typecoverage.contract.Window;
+import java.util.List;
+import java.util.UUID;
+import org.springframework.http.MediaType;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+@RestController
+@RequestMapping(value = "/types/windows", produces = MediaType.APPLICATION_JSON_VALUE)
+public class WindowPayloadController {
+
+ @GetMapping("/summaries")
+ public ResponseEntity>> windowSummaries() {
+ var items =
+ List.of(
+ new TypeSummaryDto(
+ UUID.fromString("77777777-7777-7777-7777-777777777777"),
+ "BYOE-WINDOW-001",
+ CoverageStatus.ACTIVE),
+ new TypeSummaryDto(
+ UUID.fromString("88888888-8888-8888-8888-888888888888"),
+ "BYOE-WINDOW-002",
+ CoverageStatus.EXPERIMENTAL));
+
+ return ResponseEntity.ok(ApiResponse.ok(Window.of(items, "next-window-token", true)));
+ }
+
+ @GetMapping("/statuses")
+ public ResponseEntity>> windowStatuses() {
+ var items = List.of(CoverageStatus.ACTIVE, CoverageStatus.PASSIVE, CoverageStatus.EXPERIMENTAL);
+
+ return ResponseEntity.ok(ApiResponse.ok(Window.of(items, null, false)));
+ }
+}
diff --git a/samples/type-coverage/byoe-response/producer/src/main/java/io/github/blueprintplatform/samples/typecoverage/openapi/OpenApiConstants.java b/samples/type-coverage/byoe-response/producer/src/main/java/io/github/blueprintplatform/samples/typecoverage/openapi/OpenApiConstants.java
index 2c93257c..2d873677 100644
--- a/samples/type-coverage/byoe-response/producer/src/main/java/io/github/blueprintplatform/samples/typecoverage/openapi/OpenApiConstants.java
+++ b/samples/type-coverage/byoe-response/producer/src/main/java/io/github/blueprintplatform/samples/typecoverage/openapi/OpenApiConstants.java
@@ -5,9 +5,26 @@ public final class OpenApiConstants {
public static final String TITLE = "BYOE Response Type Coverage API";
public static final String DESCRIPTION =
- "Type coverage sample validating contract projection and client reconstruction for ApiResponse, ApiResponse>, ApiResponse>, and ApiResponse>.";
+ """
+ Type coverage sample validating OpenAPI projection, generated client reconstruction,
+ and runtime deserialization for custom response envelopes.
+
+ Verified response shapes include:
+
+ - ApiResponse
+ - ApiResponse>
+ - ApiResponse>
+ - ApiResponse>
+ - ApiResponse>
+ - ApiResponse>
+
+ Demonstrates Bring Your Own Envelope (BYOE) and Bring Your Own Container (BYOC)
+ support by reconstructing both built-in and application-defined generic response
+ contracts across OpenAPI projection, generated clients, and runtime deserialization.
+ """;
- public static final String SERVER_DESCRIPTION = "Local BYOE response type coverage producer";
+ public static final String SERVER_DESCRIPTION =
+ "Local BYOE/BYOC response type coverage producer";
private OpenApiConstants() {}
-}
+}
\ No newline at end of file
diff --git a/samples/type-coverage/byoe-response/producer/src/main/resources/application.yml b/samples/type-coverage/byoe-response/producer/src/main/resources/application.yml
index 5148f718..28600990 100644
--- a/samples/type-coverage/byoe-response/producer/src/main/resources/application.yml
+++ b/samples/type-coverage/byoe-response/producer/src/main/resources/application.yml
@@ -28,6 +28,11 @@ app:
openapi-generics:
envelope:
type: io.github.blueprintplatform.samples.typecoverage.contract.ApiResponse
+ containers:
+ - type: io.github.blueprintplatform.samples.typecoverage.contract.Paging
+ item-property: content
+ - type: io.github.blueprintplatform.samples.typecoverage.contract.Window
+ item-property: items
springdoc:
default-consumes-media-type: application/json
diff --git a/samples/type-coverage/byoe-response/spec/byoe-response-coverage.yaml b/samples/type-coverage/byoe-response/spec/byoe-response-coverage.yaml
index 3952fe5c..f767b6ca 100644
--- a/samples/type-coverage/byoe-response/spec/byoe-response-coverage.yaml
+++ b/samples/type-coverage/byoe-response/spec/byoe-response-coverage.yaml
@@ -1,13 +1,51 @@
openapi: 3.1.0
info:
title: BYOE Response Type Coverage API
- description: "Type coverage sample validating contract projection and client reconstruction\
- \ for ApiResponse, ApiResponse>, ApiResponse>, and ApiResponse>."
+ description: |
+ Type coverage sample validating OpenAPI projection, generated client reconstruction,
+ and runtime deserialization for custom response envelopes.
+
+ Verified response shapes include:
+
+ - ApiResponse
+ - ApiResponse>
+ - ApiResponse>
+ - ApiResponse>
+ - ApiResponse>
+ - ApiResponse>
+
+ Demonstrates Bring Your Own Envelope (BYOE) and Bring Your Own Container (BYOC)
+ support by reconstructing both built-in and application-defined generic response
+ contracts across OpenAPI projection, generated clients, and runtime deserialization.
version: 1.2.0-SNAPSHOT
servers:
- url: http://localhost:8076/type-coverage/byoe-response
- description: Local BYOE response type coverage producer
+ description: Local BYOE/BYOC response type coverage producer
paths:
+ /types/windows/summaries:
+ get:
+ tags:
+ - window-payload-controller
+ operationId: windowSummaries
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/ApiResponseWindowTypeSummaryDto"
+ /types/windows/statuses:
+ get:
+ tags:
+ - window-payload-controller
+ operationId: windowStatuses
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/ApiResponseWindowCoverageStatus"
/types/values/uuid:
get:
tags:
@@ -140,6 +178,30 @@ paths:
application/json:
schema:
$ref: "#/components/schemas/ApiResponseBoolean"
+ /types/paging/summaries:
+ get:
+ tags:
+ - paging-payload-controller
+ operationId: pagingSummaries
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/ApiResponsePagingTypeSummaryDto"
+ /types/paging/statuses:
+ get:
+ tags:
+ - paging-payload-controller
+ operationId: pagingStatuses
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/ApiResponsePagingCoverageStatus"
/types/pages/summaries:
get:
tags:
@@ -222,6 +284,84 @@ components:
message:
type: string
x-ignore-model: true
+ ApiResponseWindowTypeSummaryDto:
+ type: object
+ properties:
+ status:
+ type: integer
+ format: int32
+ message:
+ type: string
+ data:
+ $ref: "#/components/schemas/WindowTypeSummaryDto"
+ errors:
+ type: array
+ items:
+ $ref: "#/components/schemas/ApiError"
+ x-api-wrapper: true
+ x-api-wrapper-datatype: WindowTypeSummaryDto
+ x-data-container: Window
+ x-data-container-type: io.github.blueprintplatform.samples.typecoverage.contract.Window
+ x-data-item: TypeSummaryDto
+ CoverageStatus:
+ type: string
+ enum:
+ - ACTIVE
+ - PASSIVE
+ - EXPERIMENTAL
+ TypeSummaryDto:
+ type: object
+ properties:
+ id:
+ type: string
+ format: uuid
+ code:
+ type: string
+ status:
+ $ref: "#/components/schemas/CoverageStatus"
+ WindowTypeSummaryDto:
+ type: object
+ properties:
+ items:
+ type: array
+ items:
+ $ref: "#/components/schemas/TypeSummaryDto"
+ nextCursor:
+ type: string
+ hasNext:
+ type: boolean
+ x-ignore-model: true
+ ApiResponseWindowCoverageStatus:
+ type: object
+ properties:
+ status:
+ type: integer
+ format: int32
+ message:
+ type: string
+ data:
+ $ref: "#/components/schemas/WindowCoverageStatus"
+ errors:
+ type: array
+ items:
+ $ref: "#/components/schemas/ApiError"
+ x-api-wrapper: true
+ x-api-wrapper-datatype: WindowCoverageStatus
+ x-data-container: Window
+ x-data-container-type: io.github.blueprintplatform.samples.typecoverage.contract.Window
+ x-data-item: CoverageStatus
+ WindowCoverageStatus:
+ type: object
+ properties:
+ items:
+ type: array
+ items:
+ $ref: "#/components/schemas/CoverageStatus"
+ nextCursor:
+ type: string
+ hasNext:
+ type: boolean
+ x-ignore-model: true
ApiResponseUUID:
type: object
properties:
@@ -255,12 +395,6 @@ components:
$ref: "#/components/schemas/ApiError"
x-api-wrapper: true
x-api-wrapper-datatype: CoverageStatus
- CoverageStatus:
- type: string
- enum:
- - ACTIVE
- - PASSIVE
- - EXPERIMENTAL
ApiResponseOffsetDateTime:
type: object
properties:
@@ -317,16 +451,6 @@ components:
x-data-container: Set
x-data-container-type: java.util.Set
x-data-item: TypeSummaryDto
- TypeSummaryDto:
- type: object
- properties:
- id:
- type: string
- format: uuid
- code:
- type: string
- status:
- $ref: "#/components/schemas/CoverageStatus"
ApiResponseSetCoverageStatus:
type: object
properties:
@@ -431,6 +555,88 @@ components:
$ref: "#/components/schemas/ApiError"
x-api-wrapper: true
x-api-wrapper-datatype: Boolean
+ ApiResponsePagingTypeSummaryDto:
+ type: object
+ properties:
+ status:
+ type: integer
+ format: int32
+ message:
+ type: string
+ data:
+ $ref: "#/components/schemas/PagingTypeSummaryDto"
+ errors:
+ type: array
+ items:
+ $ref: "#/components/schemas/ApiError"
+ x-api-wrapper: true
+ x-api-wrapper-datatype: PagingTypeSummaryDto
+ x-data-container: Paging
+ x-data-container-type: io.github.blueprintplatform.samples.typecoverage.contract.Paging
+ x-data-item: TypeSummaryDto
+ PagingTypeSummaryDto:
+ type: object
+ properties:
+ content:
+ type: array
+ items:
+ $ref: "#/components/schemas/TypeSummaryDto"
+ page:
+ type: integer
+ format: int32
+ size:
+ type: integer
+ format: int32
+ totalElements:
+ type: integer
+ format: int64
+ totalPages:
+ type: integer
+ format: int32
+ hasNext:
+ type: boolean
+ x-ignore-model: true
+ ApiResponsePagingCoverageStatus:
+ type: object
+ properties:
+ status:
+ type: integer
+ format: int32
+ message:
+ type: string
+ data:
+ $ref: "#/components/schemas/PagingCoverageStatus"
+ errors:
+ type: array
+ items:
+ $ref: "#/components/schemas/ApiError"
+ x-api-wrapper: true
+ x-api-wrapper-datatype: PagingCoverageStatus
+ x-data-container: Paging
+ x-data-container-type: io.github.blueprintplatform.samples.typecoverage.contract.Paging
+ x-data-item: CoverageStatus
+ PagingCoverageStatus:
+ type: object
+ properties:
+ content:
+ type: array
+ items:
+ $ref: "#/components/schemas/CoverageStatus"
+ page:
+ type: integer
+ format: int32
+ size:
+ type: integer
+ format: int32
+ totalElements:
+ type: integer
+ format: int64
+ totalPages:
+ type: integer
+ format: int32
+ hasNext:
+ type: boolean
+ x-ignore-model: true
ApiResponsePageTypeSummaryDto:
type: object
properties:
diff --git a/samples/type-coverage/service-response/producer/pom.xml b/samples/type-coverage/service-response/producer/pom.xml
index 3e21115e..77a55540 100644
--- a/samples/type-coverage/service-response/producer/pom.xml
+++ b/samples/type-coverage/service-response/producer/pom.xml
@@ -52,11 +52,6 @@
${springdoc-openapi-starter.version}
-
- org.springframework.data
- spring-data-commons
-
-
org.springframework.boot
spring-boot-configuration-processor