diff --git a/architecture-rules/pom.xml b/architecture-rules/pom.xml new file mode 100644 index 000000000..2da601b64 --- /dev/null +++ b/architecture-rules/pom.xml @@ -0,0 +1,36 @@ + + + 4.0.0 + + + com.sap.ai.sdk + sdk-parent + 1.20.0-SNAPSHOT + + + architecture-rules + Architecture rules + Shared ArchUnit-based architecture rules for the SAP AI SDK for Java. + + + ${project.basedir}/../ + 0% + 0% + 0% + 0% + 0% + 0% + + + + + com.tngtech.archunit + archunit + 1.4.1 + + + com.google.code.findbugs + jsr305 + + + diff --git a/architecture-rules/src/main/java/com/sap/ai/sdk/architecture/InheritGeneratedModel.java b/architecture-rules/src/main/java/com/sap/ai/sdk/architecture/InheritGeneratedModel.java new file mode 100644 index 000000000..e2ca4c38e --- /dev/null +++ b/architecture-rules/src/main/java/com/sap/ai/sdk/architecture/InheritGeneratedModel.java @@ -0,0 +1,90 @@ +package com.sap.ai.sdk.architecture; + +import static com.tngtech.archunit.lang.SimpleConditionEvent.violated; +import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.classes; + +import com.tngtech.archunit.base.DescribedPredicate; +import com.tngtech.archunit.core.domain.JavaClass; +import com.tngtech.archunit.core.importer.ClassFileImporter; +import com.tngtech.archunit.core.importer.ImportOption; +import com.tngtech.archunit.lang.ArchCondition; +import com.tngtech.archunit.lang.ConditionEvents; +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import javax.annotation.Nonnull; + +/** + * Runs the global architecture rule that restricts inheritance from packages ending with {@code + * .model}. + */ +public final class InheritGeneratedModel { + private static final DescribedPredicate MODEL_PACKAGE = + JavaClass.Predicates.resideInAPackage("com.sap.ai.sdk..model"); + private static final String MSG_VIOLATION = + "%s inherits from a type in a package matching 'com.sap.ai.sdk..model' while not residing in such a package itself. Use @AllowModelInheritance(reason = \"...\") for approved exceptions."; + + private static final DescribedPredicate NOT_IN_MODEL_PACKAGE = + DescribedPredicate.describe( + "not residing in a package matching com.sap.ai.sdk..model", + input -> !MODEL_PACKAGE.test(input)); + + private static final DescribedPredicate NOT_ALLOWLISTED = + DescribedPredicate.describe( + "not annotated with @AllowModelInheritance", + input -> !input.isAnnotatedWith(AllowModelInheritance.class)); + + private static final ArchCondition NOT_INHERIT_FROM_MODEL_TYPES = + new ArchCondition<>("not extend or implement com.sap.ai.sdk..model types") { + @Override + public void check(final JavaClass item, final ConditionEvents events) { + final var superClass = item.getRawSuperclass(); + final var extendsModel = superClass.isPresent() && MODEL_PACKAGE.test(superClass.get()); + if (extendsModel || item.getRawInterfaces().stream().anyMatch(MODEL_PACKAGE)) { + events.add(violated(item, MSG_VIOLATION.formatted(item.getFullName()))); + } + } + }; + + private InheritGeneratedModel() {} + + /** + * Executes the inheritance rule for the package configured via {@code + * model.inheritance.basePackage}. + * + * @param args Unused command line arguments. + */ + public static void main(final String[] args) { + final var targetPackage = System.getProperty("model.inheritance.basePackage", ""); + final var importedClasses = + new ClassFileImporter() + .withImportOption(ImportOption.Predefined.DO_NOT_INCLUDE_TESTS) + .importPackages(targetPackage); + + classes() + .that(NOT_IN_MODEL_PACKAGE) + .and(NOT_ALLOWLISTED) + .should(NOT_INHERIT_FROM_MODEL_TYPES) + .check(importedClasses); + } + + /** + * Marks approved exceptions to the model inheritance rule. + * + *

Types annotated with this are excluded from the ArchUnit check. + */ + @Documented + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.TYPE) + public @interface AllowModelInheritance { + /** + * Explains why the exception is needed. + * + * @return The reason for the exception. + */ + @Nonnull + String reason() default ""; + } +} diff --git a/foundation-models/openai/pom.xml b/foundation-models/openai/pom.xml index 37b8d64bb..6c6860f2f 100644 --- a/foundation-models/openai/pom.xml +++ b/foundation-models/openai/pom.xml @@ -66,6 +66,10 @@ com.sap.ai.sdk core + + com.sap.ai.sdk + architecture-rules + com.google.code.findbugs jsr305 diff --git a/foundation-models/openai/src/main/java/com/sap/ai/sdk/foundationmodels/openai/model/OpenAiChatCompletionChoice.java b/foundation-models/openai/src/main/java/com/sap/ai/sdk/foundationmodels/openai/model/OpenAiChatCompletionChoice.java index 83fea5566..65c43a7d4 100644 --- a/foundation-models/openai/src/main/java/com/sap/ai/sdk/foundationmodels/openai/model/OpenAiChatCompletionChoice.java +++ b/foundation-models/openai/src/main/java/com/sap/ai/sdk/foundationmodels/openai/model/OpenAiChatCompletionChoice.java @@ -1,6 +1,7 @@ package com.sap.ai.sdk.foundationmodels.openai.model; import com.fasterxml.jackson.annotation.JsonProperty; +import com.sap.ai.sdk.architecture.InheritGeneratedModel.AllowModelInheritance; import com.sap.ai.sdk.foundationmodels.openai.model.OpenAiChatMessage.OpenAiChatAssistantMessage; import javax.annotation.Nonnull; import lombok.AccessLevel; @@ -15,6 +16,8 @@ @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) @Deprecated +@AllowModelInheritance( + reason = "Maintains backward-compatible wrapper inheritance for OpenAI model types.") public class OpenAiChatCompletionChoice extends OpenAiCompletionChoice { /** Completion chat message. */ @JsonProperty("message") diff --git a/orchestration/pom.xml b/orchestration/pom.xml index 4859f791d..9f4850f28 100644 --- a/orchestration/pom.xml +++ b/orchestration/pom.xml @@ -50,6 +50,10 @@ com.sap.ai.sdk core + + com.sap.ai.sdk + architecture-rules + com.sap.cloud.sdk.cloudplatform diff --git a/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationChatCompletionDelta.java b/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationChatCompletionDelta.java index c9b2c882d..0b766651b 100644 --- a/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationChatCompletionDelta.java +++ b/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationChatCompletionDelta.java @@ -1,5 +1,6 @@ package com.sap.ai.sdk.orchestration; +import com.sap.ai.sdk.architecture.InheritGeneratedModel.AllowModelInheritance; import com.sap.ai.sdk.core.common.StreamedDelta; import com.sap.ai.sdk.orchestration.model.CompletionPostResponseStreaming; import javax.annotation.Nonnull; @@ -8,6 +9,8 @@ /** Orchestration chat completion output delta for streaming. */ @SuppressWarnings("PMD.PublicClassExtendsModelType") +@AllowModelInheritance( + reason = "Maintains backward-compatible wrapper inheritance for orchestration model types.") public class OrchestrationChatCompletionDelta extends CompletionPostResponseStreaming implements StreamedDelta { diff --git a/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationTemplate.java b/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationTemplate.java index d57082c12..dffb5e30f 100644 --- a/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationTemplate.java +++ b/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationTemplate.java @@ -5,6 +5,7 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; +import com.sap.ai.sdk.architecture.InheritGeneratedModel.AllowModelInheritance; import com.sap.ai.sdk.orchestration.model.ChatCompletionTool; import com.sap.ai.sdk.orchestration.model.ChatMessage; import com.sap.ai.sdk.orchestration.model.PromptTemplatingModuleConfigPrompt; @@ -39,6 +40,8 @@ @With @AllArgsConstructor(access = AccessLevel.PRIVATE) @NoArgsConstructor(force = true, access = AccessLevel.PACKAGE) +@AllowModelInheritance( + reason = "Maintains backward-compatible wrapper inheritance for orchestration model types.") public class OrchestrationTemplate extends TemplateConfig { /** Please use {@link #withMessages(Message...)} instead. */ diff --git a/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationTemplateReference.java b/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationTemplateReference.java index 62fd0e067..667dca9e4 100644 --- a/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationTemplateReference.java +++ b/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationTemplateReference.java @@ -1,5 +1,6 @@ package com.sap.ai.sdk.orchestration; +import com.sap.ai.sdk.architecture.InheritGeneratedModel.AllowModelInheritance; import com.sap.ai.sdk.orchestration.model.PromptTemplatingModuleConfigPrompt; import com.sap.ai.sdk.orchestration.model.TemplateRef; import com.sap.ai.sdk.orchestration.model.TemplateRefByID; @@ -20,6 +21,8 @@ @EqualsAndHashCode(callSuper = true) @Value @AllArgsConstructor(access = AccessLevel.PROTECTED) +@AllowModelInheritance( + reason = "Maintains backward-compatible wrapper inheritance for orchestration model types.") public class OrchestrationTemplateReference extends TemplateConfig { @SuppressWarnings("PMD.LombokGetterSetterExposesModelType") @Nonnull diff --git a/pom.xml b/pom.xml index 4f93896ec..d384285da 100644 --- a/pom.xml +++ b/pom.xml @@ -31,6 +31,7 @@ core orchestration + architecture-rules core-services/batch core-services/document-grounding core-services/prompt-registry @@ -70,6 +71,7 @@ 3.2.0 5.23.0 3.28.2 + 1.4.1 4.38.0 2.22.0 1.5.34 @@ -221,6 +223,11 @@ orchestration ${project.version} + + com.sap.ai.sdk + architecture-rules + ${project.version} + com.sap.ai.sdk batch @@ -650,6 +657,44 @@ + + org.codehaus.mojo + exec-maven-plugin + 3.6.2 + + + com.sap.ai.sdk + architecture-rules + ${project.version} + + + com.tngtech.archunit + archunit + ${archunit.version} + + + + + check-model-inheritance + + java + + process-classes + + true + true + compile + com.sap.ai.sdk.architecture.InheritGeneratedModel + + + model.inheritance.basePackage + ${project.groupId} + + + + + + org.apache.maven.plugins