diff --git a/README.md b/README.md index 5965a6c7c..9a4ff3ed5 100644 --- a/README.md +++ b/README.md @@ -114,6 +114,12 @@ The system creates an empty configuration. To utilize existing Dial Core configu 1. Import configuration file in the AIDIAL admin panel using special import endpoint +**Compatibility Note** +> +> Since **v0.47.0**, the Admin Panel supports multilingual metadata through the `LocalizedValue` type. +> Exported configurations that contain multiple localized values are compatible only with **AIDIAL Core v0.47.0+**. +> Configurations containing only plain string values remain backward compatible with earlier Core versions. + ## Getting Started ### Run Application with Gradle diff --git a/src/main/java/com/epam/aidial/cfg/client/dto/ApplicationResourceDto.java b/src/main/java/com/epam/aidial/cfg/client/dto/ApplicationResourceDto.java index 9b4a87739..301cd7670 100644 --- a/src/main/java/com/epam/aidial/cfg/client/dto/ApplicationResourceDto.java +++ b/src/main/java/com/epam/aidial/cfg/client/dto/ApplicationResourceDto.java @@ -21,12 +21,12 @@ public class ApplicationResourceDto { private String endpoint; private String responsesEndpoint; private Map interfaces; - private String displayName; + private LocalizedValueDto displayName; private String displayVersion; private String overrideName; private String iconUrl; - private String description; - private String intro; + private LocalizedValueDto description; + private LocalizedValueDto intro; private String reference; private Boolean forwardAuthToken; private List inputAttachmentTypes; diff --git a/src/main/java/com/epam/aidial/cfg/client/dto/DeploymentDataDto.java b/src/main/java/com/epam/aidial/cfg/client/dto/DeploymentDataDto.java index bfd4e5588..00e0b1fd8 100644 --- a/src/main/java/com/epam/aidial/cfg/client/dto/DeploymentDataDto.java +++ b/src/main/java/com/epam/aidial/cfg/client/dto/DeploymentDataDto.java @@ -37,10 +37,11 @@ public class DeploymentDataDto { private String model; private String application; private String toolset; - private String displayName; + private LocalizedValueDto displayName; private String displayVersion; private String iconUrl; - private String description; + private LocalizedValueDto description; + private LocalizedValueDto intro; private String reference; private String owner; private String object; diff --git a/src/main/java/com/epam/aidial/cfg/client/dto/LocalizedValueDto.java b/src/main/java/com/epam/aidial/cfg/client/dto/LocalizedValueDto.java new file mode 100644 index 000000000..73353d4b4 --- /dev/null +++ b/src/main/java/com/epam/aidial/cfg/client/dto/LocalizedValueDto.java @@ -0,0 +1,47 @@ +package com.epam.aidial.cfg.client.dto; + +import com.epam.aidial.cfg.client.dto.databind.LocalizedValueDeserializer; +import com.epam.aidial.cfg.client.dto.databind.LocalizedValueSerializer; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import lombok.EqualsAndHashCode; +import lombok.Getter; + +import java.util.LinkedHashMap; +import java.util.Map; + +@Getter +@JsonDeserialize(using = LocalizedValueDeserializer.class) +@JsonSerialize(using = LocalizedValueSerializer.class) +@EqualsAndHashCode +public final class LocalizedValueDto { + + private final String plainValue; + private final Map localeMap; + + private LocalizedValueDto(String plainValue, Map localeMap) { + this.plainValue = plainValue; + this.localeMap = localeMap; + } + + public static LocalizedValueDto of(String value) { + return value == null ? null : new LocalizedValueDto(value, null); + } + + public static LocalizedValueDto of(Map localeMap) { + return localeMap == null ? null : new LocalizedValueDto(null, new LinkedHashMap<>(localeMap)); + } + + public boolean isMap() { + return localeMap != null; + } + + public boolean isPlain() { + return plainValue != null; + } + + @Override + public String toString() { + return plainValue != null ? plainValue : String.valueOf(localeMap); + } +} diff --git a/src/main/java/com/epam/aidial/cfg/client/dto/ToolSetResourceDto.java b/src/main/java/com/epam/aidial/cfg/client/dto/ToolSetResourceDto.java index b5af3d61b..260a30f4c 100644 --- a/src/main/java/com/epam/aidial/cfg/client/dto/ToolSetResourceDto.java +++ b/src/main/java/com/epam/aidial/cfg/client/dto/ToolSetResourceDto.java @@ -19,11 +19,11 @@ public class ToolSetResourceDto { private String name; private String endpoint; - private String displayName; + private LocalizedValueDto displayName; private String displayVersion; private String iconUrl; - private String description; - private String intro; + private LocalizedValueDto description; + private LocalizedValueDto intro; private String vendorWebsite; private List descriptionKeywords; private Integer maxRetryAttempts; diff --git a/src/main/java/com/epam/aidial/cfg/client/dto/databind/LocalizedValueDeserializer.java b/src/main/java/com/epam/aidial/cfg/client/dto/databind/LocalizedValueDeserializer.java new file mode 100644 index 000000000..78b085743 --- /dev/null +++ b/src/main/java/com/epam/aidial/cfg/client/dto/databind/LocalizedValueDeserializer.java @@ -0,0 +1,38 @@ +package com.epam.aidial.cfg.client.dto.databind; + +import com.epam.aidial.cfg.client.dto.LocalizedValueDto; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonToken; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; + +import java.io.IOException; +import java.util.LinkedHashMap; +import java.util.Map; + +public class LocalizedValueDeserializer extends JsonDeserializer { + + @Override + public LocalizedValueDto deserialize(JsonParser p, DeserializationContext ctx) throws IOException { + if (p.getCurrentToken() == JsonToken.VALUE_STRING) { + return LocalizedValueDto.of(p.getValueAsString()); + } + + if (p.getCurrentToken() == JsonToken.START_OBJECT) { + Map localeMap = new LinkedHashMap<>(); + while (p.nextToken() != JsonToken.END_OBJECT) { + String locale = p.getCurrentName(); + p.nextToken(); + localeMap.put(locale, p.getValueAsString()); + } + return LocalizedValueDto.of(localeMap); + } + + if (p.getCurrentToken() == JsonToken.VALUE_NULL) { + return null; + } + + return ctx.reportInputMismatch(LocalizedValueDto.class, + "Expected a string or a locale-to-value object, got %s", p.getCurrentToken()); + } +} diff --git a/src/main/java/com/epam/aidial/cfg/client/dto/databind/LocalizedValueSerializer.java b/src/main/java/com/epam/aidial/cfg/client/dto/databind/LocalizedValueSerializer.java new file mode 100644 index 000000000..f31bdceb8 --- /dev/null +++ b/src/main/java/com/epam/aidial/cfg/client/dto/databind/LocalizedValueSerializer.java @@ -0,0 +1,30 @@ +package com.epam.aidial.cfg.client.dto.databind; + +import com.epam.aidial.cfg.client.dto.LocalizedValueDto; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; + +import java.io.IOException; + +public class LocalizedValueSerializer extends JsonSerializer { + + @Override + public void serialize(LocalizedValueDto value, JsonGenerator gen, SerializerProvider serializers) throws IOException { + if (!value.isMap()) { + gen.writeString(value.getPlainValue()); + return; + } + + if (value.getLocaleMap().size() == 1) { + gen.writeString(value.getLocaleMap().values().iterator().next()); + return; + } + + gen.writeStartObject(); + for (var entry : value.getLocaleMap().entrySet()) { + gen.writeStringField(entry.getKey(), entry.getValue()); + } + gen.writeEndObject(); + } +} diff --git a/src/main/java/com/epam/aidial/cfg/client/mapper/ApplicationClientMapper.java b/src/main/java/com/epam/aidial/cfg/client/mapper/ApplicationClientMapper.java index 9fc05d62f..99130ec46 100644 --- a/src/main/java/com/epam/aidial/cfg/client/mapper/ApplicationClientMapper.java +++ b/src/main/java/com/epam/aidial/cfg/client/mapper/ApplicationClientMapper.java @@ -4,6 +4,7 @@ import com.epam.aidial.cfg.client.dto.ApplicationResourceDto; import com.epam.aidial.cfg.client.dto.NodeTypeDto; import com.epam.aidial.cfg.dto.ApplicationEximDto; +import com.epam.aidial.cfg.mapper.LocalizedValueMapper; import com.epam.aidial.cfg.model.ApplicationExim; import com.epam.aidial.cfg.model.ApplicationResource; import com.epam.aidial.cfg.model.ApplicationResourceNodeInfo; @@ -20,7 +21,7 @@ import static com.epam.aidial.cfg.client.mapper.CoreMetadataUtils.extractPath; import static com.epam.aidial.cfg.client.mapper.CoreMetadataUtils.parseEncodedVersionedPath; -@Mapper(componentModel = "spring", uses = {RouteMapper.class}) +@Mapper(componentModel = "spring", uses = {RouteMapper.class, LocalizedValueClientMapper.class, LocalizedValueMapper.class}) @Slf4j public abstract class ApplicationClientMapper { public static final String APPLICATIONS_PREFIX = "applications/"; diff --git a/src/main/java/com/epam/aidial/cfg/client/mapper/DeploymentClientMapper.java b/src/main/java/com/epam/aidial/cfg/client/mapper/DeploymentClientMapper.java index d1d1f6ffd..efeb5fa7f 100644 --- a/src/main/java/com/epam/aidial/cfg/client/mapper/DeploymentClientMapper.java +++ b/src/main/java/com/epam/aidial/cfg/client/mapper/DeploymentClientMapper.java @@ -13,7 +13,7 @@ import java.util.List; -@Mapper(componentModel = "spring", uses = RouteMapper.class) +@Mapper(componentModel = "spring", uses = {RouteMapper.class, LocalizedValueClientMapper.class}) public interface DeploymentClientMapper { List toDeploymentDataList(List dtos); diff --git a/src/main/java/com/epam/aidial/cfg/client/mapper/LocalizedValueClientMapper.java b/src/main/java/com/epam/aidial/cfg/client/mapper/LocalizedValueClientMapper.java new file mode 100644 index 000000000..26510bf15 --- /dev/null +++ b/src/main/java/com/epam/aidial/cfg/client/mapper/LocalizedValueClientMapper.java @@ -0,0 +1,23 @@ +package com.epam.aidial.cfg.client.mapper; + +import com.epam.aidial.cfg.client.dto.LocalizedValueDto; +import com.epam.aidial.cfg.model.LocalizedValue; +import org.springframework.stereotype.Component; + +@Component +public class LocalizedValueClientMapper { + + public LocalizedValueDto toDto(LocalizedValue domain) { + if (domain == null) { + return null; + } + return domain.isPlain() ? LocalizedValueDto.of(domain.getPlainValue()) : LocalizedValueDto.of(domain.getLocaleMap()); + } + + public LocalizedValue toDomain(LocalizedValueDto dto) { + if (dto == null) { + return null; + } + return dto.isPlain() ? LocalizedValue.of(dto.getPlainValue()) : LocalizedValue.of(dto.getLocaleMap()); + } +} diff --git a/src/main/java/com/epam/aidial/cfg/client/mapper/ToolSetClientMapper.java b/src/main/java/com/epam/aidial/cfg/client/mapper/ToolSetClientMapper.java index ec34c3eb3..e95f8b444 100644 --- a/src/main/java/com/epam/aidial/cfg/client/mapper/ToolSetClientMapper.java +++ b/src/main/java/com/epam/aidial/cfg/client/mapper/ToolSetClientMapper.java @@ -4,6 +4,7 @@ import com.epam.aidial.cfg.client.dto.ToolSetMetadataDto; import com.epam.aidial.cfg.client.dto.ToolSetResourceDto; import com.epam.aidial.cfg.dto.ToolSetEximDto; +import com.epam.aidial.cfg.mapper.LocalizedValueMapper; import com.epam.aidial.cfg.model.CreateToolSetResource; import com.epam.aidial.cfg.model.NodeType; import com.epam.aidial.cfg.model.ToolSetExim; @@ -19,7 +20,7 @@ import static com.epam.aidial.cfg.client.mapper.CoreMetadataUtils.extractPath; import static com.epam.aidial.cfg.client.mapper.CoreMetadataUtils.parseEncodedVersionedPath; -@Mapper(componentModel = "spring") +@Mapper(componentModel = "spring", uses = {LocalizedValueClientMapper.class, LocalizedValueMapper.class}) @Slf4j public abstract class ToolSetClientMapper { public static final String TOOLSETS_PREFIX = "toolsets/"; diff --git a/src/main/java/com/epam/aidial/cfg/configuration/JsonMapperConfiguration.java b/src/main/java/com/epam/aidial/cfg/configuration/JsonMapperConfiguration.java index 0ead19fbd..910425c53 100644 --- a/src/main/java/com/epam/aidial/cfg/configuration/JsonMapperConfiguration.java +++ b/src/main/java/com/epam/aidial/cfg/configuration/JsonMapperConfiguration.java @@ -1,5 +1,8 @@ package com.epam.aidial.cfg.configuration; +import com.epam.aidial.cfg.configuration.databind.LocalizedValueDeserializer; +import com.epam.aidial.cfg.configuration.databind.LocalizedValueSerializer; +import com.epam.aidial.cfg.domain.model.LocalizedValue; import com.epam.aidial.core.config.CoreCostLimit; import com.epam.aidial.core.config.CoreCostLimitMixinForCoreObjectMapper; import com.epam.aidial.core.config.CoreLimit; @@ -19,6 +22,7 @@ import com.fasterxml.jackson.databind.SerializationFeature; import com.fasterxml.jackson.databind.cfg.EnumFeature; import com.fasterxml.jackson.databind.json.JsonMapper; +import com.fasterxml.jackson.databind.module.SimpleModule; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -73,6 +77,9 @@ private static JsonMapper.Builder createDefaultJsonMapperBuilder() { .addModule(new QueryLanguageModule()) .addModule(new ValidationModule()) .addModule(new JavaTimeModule()) + .addModule(new SimpleModule() + .addSerializer(LocalizedValue.class, new LocalizedValueSerializer()) + .addDeserializer(LocalizedValue.class, new LocalizedValueDeserializer())) .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) .disable(SerializationFeature.FAIL_ON_EMPTY_BEANS); } diff --git a/src/main/java/com/epam/aidial/cfg/configuration/databind/LocalizedValueDeserializer.java b/src/main/java/com/epam/aidial/cfg/configuration/databind/LocalizedValueDeserializer.java new file mode 100644 index 000000000..947199069 --- /dev/null +++ b/src/main/java/com/epam/aidial/cfg/configuration/databind/LocalizedValueDeserializer.java @@ -0,0 +1,38 @@ +package com.epam.aidial.cfg.configuration.databind; + +import com.epam.aidial.cfg.domain.model.LocalizedValue; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonToken; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; + +import java.io.IOException; +import java.util.LinkedHashMap; +import java.util.Map; + +public class LocalizedValueDeserializer extends JsonDeserializer { + + @Override + public LocalizedValue deserialize(JsonParser p, DeserializationContext ctx) throws IOException { + if (p.getCurrentToken() == JsonToken.VALUE_STRING) { + return LocalizedValue.of(p.getValueAsString()); + } + + if (p.getCurrentToken() == JsonToken.START_OBJECT) { + Map localeMap = new LinkedHashMap<>(); + while (p.nextToken() != JsonToken.END_OBJECT) { + String locale = p.getCurrentName(); + p.nextToken(); + localeMap.put(locale, p.getValueAsString()); + } + return LocalizedValue.of(localeMap); + } + + if (p.getCurrentToken() == JsonToken.VALUE_NULL) { + return null; + } + + return ctx.reportInputMismatch(LocalizedValue.class, + "Expected a string or a locale-to-value object, got %s", p.getCurrentToken()); + } +} diff --git a/src/main/java/com/epam/aidial/cfg/configuration/databind/LocalizedValueSerializer.java b/src/main/java/com/epam/aidial/cfg/configuration/databind/LocalizedValueSerializer.java new file mode 100644 index 000000000..490d6a65e --- /dev/null +++ b/src/main/java/com/epam/aidial/cfg/configuration/databind/LocalizedValueSerializer.java @@ -0,0 +1,30 @@ +package com.epam.aidial.cfg.configuration.databind; + +import com.epam.aidial.cfg.domain.model.LocalizedValue; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; + +import java.io.IOException; + +public class LocalizedValueSerializer extends JsonSerializer { + + @Override + public void serialize(LocalizedValue value, JsonGenerator gen, SerializerProvider serializers) throws IOException { + if (!value.isMap()) { + gen.writeString(value.getPlainValue()); + return; + } + + if (value.getLocaleMap().size() == 1) { + gen.writeString(value.getLocaleMap().values().iterator().next()); + return; + } + + gen.writeStartObject(); + for (var entry : value.getLocaleMap().entrySet()) { + gen.writeStringField(entry.getKey(), entry.getValue()); + } + gen.writeEndObject(); + } +} diff --git a/src/main/java/com/epam/aidial/cfg/dao/jpa/ApplicationJpaRepository.java b/src/main/java/com/epam/aidial/cfg/dao/jpa/ApplicationJpaRepository.java index 109ddbd7c..cfdf3e552 100644 --- a/src/main/java/com/epam/aidial/cfg/dao/jpa/ApplicationJpaRepository.java +++ b/src/main/java/com/epam/aidial/cfg/dao/jpa/ApplicationJpaRepository.java @@ -7,13 +7,8 @@ import java.util.List; public interface ApplicationJpaRepository extends JpaRepository { - - boolean existsByDisplayNameAndDisplayVersion(String displayName, String displayVersion); - List findByIdNotIn(Collection ids); - List findAllByValidityStateIsValidTrue(); - List findAllByOrderByDisplayNameAscDisplayVersionAscIdAsc(); List findByValidityStateIsValidTrueOrderByDisplayNameAscDisplayVersionAscIdAsc(); @@ -21,4 +16,6 @@ public interface ApplicationJpaRepository extends JpaRepository findByIdInOrderByDisplayNameAscDisplayVersionAscIdAsc(Collection ids); List findByApplicationContainerIsNotNull(); + + List findByDisplayVersion(String displayVersion); } diff --git a/src/main/java/com/epam/aidial/cfg/dao/jpa/ModelJpaRepository.java b/src/main/java/com/epam/aidial/cfg/dao/jpa/ModelJpaRepository.java index d1cc5956f..ae3b3c62d 100644 --- a/src/main/java/com/epam/aidial/cfg/dao/jpa/ModelJpaRepository.java +++ b/src/main/java/com/epam/aidial/cfg/dao/jpa/ModelJpaRepository.java @@ -9,14 +9,14 @@ public interface ModelJpaRepository extends JpaRepository { - boolean existsByDisplayNameAndDisplayVersion(String displayName, String displayVersion); + @Query("SELECT m FROM ModelEntity m WHERE m.modelContainer IS NOT NULL") + List findByContainerIdIsNotNull(); List findByIdNotIn(Collection ids); - @Query("SELECT m FROM ModelEntity m WHERE m.modelContainer IS NOT NULL") - List findByContainerIdIsNotNull(); + List findByDisplayVersion(String displayVersion); List findAllByOrderByDisplayNameAscDisplayVersionAscIdAsc(); - List findByIdInOrderByDisplayNameAscDisplayVersionAscIdAsc(Collection ids); + List findByIdInOrderByDisplayNameAscDisplayVersionAscIdAsc(Collection names); } diff --git a/src/main/java/com/epam/aidial/cfg/dao/mapper/AddonEntityMapper.java b/src/main/java/com/epam/aidial/cfg/dao/mapper/AddonEntityMapper.java index 0fa97565c..425e41f60 100644 --- a/src/main/java/com/epam/aidial/cfg/dao/mapper/AddonEntityMapper.java +++ b/src/main/java/com/epam/aidial/cfg/dao/mapper/AddonEntityMapper.java @@ -12,7 +12,7 @@ import java.util.List; -@Mapper(componentModel = "spring", uses = {DeploymentEntityMapper.class}) +@Mapper(componentModel = "spring", uses = {DeploymentEntityMapper.class, LocalizedValueEntityMapper.class}) public abstract class AddonEntityMapper { @Autowired diff --git a/src/main/java/com/epam/aidial/cfg/dao/mapper/ApplicationEntityMapper.java b/src/main/java/com/epam/aidial/cfg/dao/mapper/ApplicationEntityMapper.java index 60f2aade3..edd19b0d9 100644 --- a/src/main/java/com/epam/aidial/cfg/dao/mapper/ApplicationEntityMapper.java +++ b/src/main/java/com/epam/aidial/cfg/dao/mapper/ApplicationEntityMapper.java @@ -27,7 +27,7 @@ @Mapper(componentModel = "spring", uses = { DeploymentEntityMapper.class, MapPropertiesMapper.class, DependentRouteEntityMapper.class, FeaturesEntityMapper.class, ValidityStateEntityMapper.class, DeploymentInterfacesEntityMapper.class, - ExternalServicesEntityMapper.class, UriMapper.class + ExternalServicesEntityMapper.class, UriMapper.class, LocalizedValueEntityMapper.class }) public abstract class ApplicationEntityMapper { diff --git a/src/main/java/com/epam/aidial/cfg/dao/mapper/AssistantEntityMapper.java b/src/main/java/com/epam/aidial/cfg/dao/mapper/AssistantEntityMapper.java index ce7297abd..6dc977c64 100644 --- a/src/main/java/com/epam/aidial/cfg/dao/mapper/AssistantEntityMapper.java +++ b/src/main/java/com/epam/aidial/cfg/dao/mapper/AssistantEntityMapper.java @@ -12,7 +12,7 @@ import java.util.List; -@Mapper(componentModel = "spring", uses = {DeploymentEntityMapper.class, MapPropertiesMapper.class}) +@Mapper(componentModel = "spring", uses = {DeploymentEntityMapper.class, MapPropertiesMapper.class, LocalizedValueEntityMapper.class}) public abstract class AssistantEntityMapper { @Autowired diff --git a/src/main/java/com/epam/aidial/cfg/dao/mapper/InterceptorEntityMapper.java b/src/main/java/com/epam/aidial/cfg/dao/mapper/InterceptorEntityMapper.java index 1c676b7b3..b1f0414d1 100644 --- a/src/main/java/com/epam/aidial/cfg/dao/mapper/InterceptorEntityMapper.java +++ b/src/main/java/com/epam/aidial/cfg/dao/mapper/InterceptorEntityMapper.java @@ -25,7 +25,8 @@ import java.util.stream.Stream; @Mapper(componentModel = "spring", uses = { - FeaturesEntityMapper.class, MapPropertiesMapper.class, DeploymentInterfacesEntityMapper.class + FeaturesEntityMapper.class, MapPropertiesMapper.class, DeploymentInterfacesEntityMapper.class, + LocalizedValueEntityMapper.class }) public abstract class InterceptorEntityMapper { diff --git a/src/main/java/com/epam/aidial/cfg/dao/mapper/LocalizedValueEntityMapper.java b/src/main/java/com/epam/aidial/cfg/dao/mapper/LocalizedValueEntityMapper.java new file mode 100644 index 000000000..6feb550d6 --- /dev/null +++ b/src/main/java/com/epam/aidial/cfg/dao/mapper/LocalizedValueEntityMapper.java @@ -0,0 +1,43 @@ +package com.epam.aidial.cfg.dao.mapper; + +import com.epam.aidial.cfg.domain.model.LocalizedValue; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.SneakyThrows; +import org.mapstruct.Mapper; +import org.springframework.beans.factory.annotation.Autowired; + +import java.util.Map; + +@Mapper(componentModel = "spring") +public class LocalizedValueEntityMapper { + + @Autowired + protected ObjectMapper objectMapper; + + @SneakyThrows + public String convertToString(LocalizedValue value) { + if (value == null) { + return null; + } + if (!value.isMap()) { + return value.getPlainValue(); + } + return objectMapper.writeValueAsString(value.getLocaleMap()); + } + + @SneakyThrows + public LocalizedValue convertToLocalizedValue(String value) { + if (value == null) { + return null; + } + if (value.startsWith("{")) { + return LocalizedValue.of( + objectMapper.readValue(value, new TypeReference>() { + }) + ); + } + return LocalizedValue.of(value); + } +} + diff --git a/src/main/java/com/epam/aidial/cfg/dao/mapper/ModelEntityMapper.java b/src/main/java/com/epam/aidial/cfg/dao/mapper/ModelEntityMapper.java index b5fbfe3f0..4307a66b7 100644 --- a/src/main/java/com/epam/aidial/cfg/dao/mapper/ModelEntityMapper.java +++ b/src/main/java/com/epam/aidial/cfg/dao/mapper/ModelEntityMapper.java @@ -26,8 +26,9 @@ @Mapper(componentModel = "spring", uses = { DeploymentEntityMapper.class, MapPropertiesMapper.class, UpstreamEntityMapper.class, PropertiesEntityMapper.class, FeaturesEntityMapper.class, DeploymentInterfacesEntityMapper.class, - UriMapper.class + UriMapper.class, LocalizedValueEntityMapper.class }) + public abstract class ModelEntityMapper { @Autowired diff --git a/src/main/java/com/epam/aidial/cfg/dao/mapper/ToolSetEntityMapper.java b/src/main/java/com/epam/aidial/cfg/dao/mapper/ToolSetEntityMapper.java index 2ac4866c0..f57bcd6d1 100755 --- a/src/main/java/com/epam/aidial/cfg/dao/mapper/ToolSetEntityMapper.java +++ b/src/main/java/com/epam/aidial/cfg/dao/mapper/ToolSetEntityMapper.java @@ -18,7 +18,7 @@ import java.util.List; @Mapper(componentModel = "spring", uses = { - DeploymentEntityMapper.class, MapPropertiesMapper.class, UriMapper.class + DeploymentEntityMapper.class, MapPropertiesMapper.class, UriMapper.class, LocalizedValueEntityMapper.class }) public abstract class ToolSetEntityMapper { diff --git a/src/main/java/com/epam/aidial/cfg/domain/mapper/AddonCoreMapper.java b/src/main/java/com/epam/aidial/cfg/domain/mapper/AddonCoreMapper.java index 8e74c7960..15e33a7d1 100644 --- a/src/main/java/com/epam/aidial/cfg/domain/mapper/AddonCoreMapper.java +++ b/src/main/java/com/epam/aidial/cfg/domain/mapper/AddonCoreMapper.java @@ -14,9 +14,7 @@ @Mapper( componentModel = "spring", - uses = { - DeploymentCoreMapper.class, - } + uses = {DeploymentCoreMapper.class, LocalizedValueCoreMapper.class} ) public abstract class AddonCoreMapper { diff --git a/src/main/java/com/epam/aidial/cfg/domain/mapper/ApplicationCoreMapper.java b/src/main/java/com/epam/aidial/cfg/domain/mapper/ApplicationCoreMapper.java index 3a2d82d32..8a6748d1b 100644 --- a/src/main/java/com/epam/aidial/cfg/domain/mapper/ApplicationCoreMapper.java +++ b/src/main/java/com/epam/aidial/cfg/domain/mapper/ApplicationCoreMapper.java @@ -22,7 +22,7 @@ componentModel = "spring", uses = { DeploymentCoreMapper.class, FeatureCoreMapper.class, RouteCoreMapper.class, - DeploymentInterfaceCoreMapper.class, ExternalServiceCoreMapper.class + DeploymentInterfaceCoreMapper.class, ExternalServiceCoreMapper.class, LocalizedValueCoreMapper.class } ) public abstract class ApplicationCoreMapper { diff --git a/src/main/java/com/epam/aidial/cfg/domain/mapper/AssistantCoreMapper.java b/src/main/java/com/epam/aidial/cfg/domain/mapper/AssistantCoreMapper.java index a83e23594..0e7f975fa 100644 --- a/src/main/java/com/epam/aidial/cfg/domain/mapper/AssistantCoreMapper.java +++ b/src/main/java/com/epam/aidial/cfg/domain/mapper/AssistantCoreMapper.java @@ -14,7 +14,7 @@ import java.util.List; -@Mapper(componentModel = "spring", uses = {DeploymentCoreMapper.class, FeatureCoreMapper.class}) +@Mapper(componentModel = "spring", uses = {DeploymentCoreMapper.class, FeatureCoreMapper.class, LocalizedValueCoreMapper.class}) public abstract class AssistantCoreMapper { public abstract Assistant copy(Assistant assistant); diff --git a/src/main/java/com/epam/aidial/cfg/domain/mapper/ExportConfigMapper.java b/src/main/java/com/epam/aidial/cfg/domain/mapper/ExportConfigMapper.java index c8bf31552..881ec5f5d 100644 --- a/src/main/java/com/epam/aidial/cfg/domain/mapper/ExportConfigMapper.java +++ b/src/main/java/com/epam/aidial/cfg/domain/mapper/ExportConfigMapper.java @@ -16,13 +16,14 @@ import com.epam.aidial.cfg.model.ExportRequest; import com.epam.aidial.cfg.model.FullExportRequest; import com.epam.aidial.cfg.model.SelectedItemsExportRequest; +import com.epam.aidial.cfg.web.facade.mapper.LocalizedValueDtoMapper; import org.mapstruct.BeanMapping; import org.mapstruct.Mapper; import org.mapstruct.SubclassMapping; import static org.mapstruct.SubclassExhaustiveStrategy.RUNTIME_EXCEPTION; -@Mapper(componentModel = "spring") +@Mapper(componentModel = "spring", uses = LocalizedValueDtoMapper.class) public interface ExportConfigMapper { @SubclassMapping(source = FullExportRequestDto.class, target = FullExportRequest.class) diff --git a/src/main/java/com/epam/aidial/cfg/domain/mapper/InterceptorCoreMapper.java b/src/main/java/com/epam/aidial/cfg/domain/mapper/InterceptorCoreMapper.java index b71a07be6..48aeda115 100644 --- a/src/main/java/com/epam/aidial/cfg/domain/mapper/InterceptorCoreMapper.java +++ b/src/main/java/com/epam/aidial/cfg/domain/mapper/InterceptorCoreMapper.java @@ -5,7 +5,7 @@ import org.mapstruct.Mapper; import org.mapstruct.Mapping; -@Mapper(componentModel = "spring", uses = {FeatureCoreMapper.class, DeploymentInterfaceCoreMapper.class}) +@Mapper(componentModel = "spring", uses = {FeatureCoreMapper.class, DeploymentInterfaceCoreMapper.class, LocalizedValueCoreMapper.class}) public interface InterceptorCoreMapper { @Mapping(target = "userRoles", ignore = true) diff --git a/src/main/java/com/epam/aidial/cfg/domain/mapper/LocalizedValueCoreMapper.java b/src/main/java/com/epam/aidial/cfg/domain/mapper/LocalizedValueCoreMapper.java new file mode 100644 index 000000000..1f65b7489 --- /dev/null +++ b/src/main/java/com/epam/aidial/cfg/domain/mapper/LocalizedValueCoreMapper.java @@ -0,0 +1,23 @@ +package com.epam.aidial.cfg.domain.mapper; + +import com.epam.aidial.cfg.domain.model.LocalizedValue; +import com.epam.aidial.core.config.CoreLocalizedValue; +import org.springframework.stereotype.Component; + +@Component +public class LocalizedValueCoreMapper { + + public CoreLocalizedValue toCoreLocalizedValue(LocalizedValue domain) { + if (domain == null) { + return null; + } + return domain.isPlain() ? CoreLocalizedValue.of(domain.getPlainValue()) : CoreLocalizedValue.of(domain.getLocaleMap()); + } + + public LocalizedValue toLocalizedValue(CoreLocalizedValue coreLocalizedValue) { + if (coreLocalizedValue == null) { + return null; + } + return coreLocalizedValue.isPlain() ? LocalizedValue.of(coreLocalizedValue.getPlainValue()) : LocalizedValue.of(coreLocalizedValue.getLocaleMap()); + } +} diff --git a/src/main/java/com/epam/aidial/cfg/domain/mapper/ModelCoreMapper.java b/src/main/java/com/epam/aidial/cfg/domain/mapper/ModelCoreMapper.java index 2deb969d2..77c1abc7e 100644 --- a/src/main/java/com/epam/aidial/cfg/domain/mapper/ModelCoreMapper.java +++ b/src/main/java/com/epam/aidial/cfg/domain/mapper/ModelCoreMapper.java @@ -18,7 +18,7 @@ @Mapper( componentModel = "spring", uses = { - DeploymentCoreMapper.class, FeatureCoreMapper.class, DeploymentInterfaceCoreMapper.class + DeploymentCoreMapper.class, FeatureCoreMapper.class, DeploymentInterfaceCoreMapper.class, LocalizedValueCoreMapper.class } ) public abstract class ModelCoreMapper { diff --git a/src/main/java/com/epam/aidial/cfg/domain/mapper/ToolSetCoreMapper.java b/src/main/java/com/epam/aidial/cfg/domain/mapper/ToolSetCoreMapper.java index ae2d89b07..44631f81b 100644 --- a/src/main/java/com/epam/aidial/cfg/domain/mapper/ToolSetCoreMapper.java +++ b/src/main/java/com/epam/aidial/cfg/domain/mapper/ToolSetCoreMapper.java @@ -17,7 +17,7 @@ @Mapper( componentModel = "spring", uses = { - DeploymentCoreMapper.class, ResourceAuthSettingsCoreMapper.class + DeploymentCoreMapper.class, ResourceAuthSettingsCoreMapper.class, LocalizedValueCoreMapper.class } ) public abstract class ToolSetCoreMapper { diff --git a/src/main/java/com/epam/aidial/cfg/domain/model/Addon.java b/src/main/java/com/epam/aidial/cfg/domain/model/Addon.java index aaf2c3372..83c5c7a6d 100644 --- a/src/main/java/com/epam/aidial/cfg/domain/model/Addon.java +++ b/src/main/java/com/epam/aidial/cfg/domain/model/Addon.java @@ -13,8 +13,8 @@ public class Addon extends RoleBased { private String endpoint; private String iconUrl; - private String description; - private String displayName; + private LocalizedValue description; + private LocalizedValue displayName; private List inputAttachmentTypes; private Integer maxInputAttachments; private Boolean forwardAuthToken; diff --git a/src/main/java/com/epam/aidial/cfg/domain/model/Application.java b/src/main/java/com/epam/aidial/cfg/domain/model/Application.java index 1aa4ede01..dbdc20085 100644 --- a/src/main/java/com/epam/aidial/cfg/domain/model/Application.java +++ b/src/main/java/com/epam/aidial/cfg/domain/model/Application.java @@ -22,9 +22,9 @@ public class Application extends RoleBased { private Map interfaces; private String iconUrl; private String reference; - private String description; - private String intro; - private String displayName; + private LocalizedValue description; + private LocalizedValue intro; + private LocalizedValue displayName; private String displayVersion; private String overrideName; private List inputAttachmentTypes; diff --git a/src/main/java/com/epam/aidial/cfg/domain/model/Assistant.java b/src/main/java/com/epam/aidial/cfg/domain/model/Assistant.java index 6505f9b64..9f0044b34 100644 --- a/src/main/java/com/epam/aidial/cfg/domain/model/Assistant.java +++ b/src/main/java/com/epam/aidial/cfg/domain/model/Assistant.java @@ -13,8 +13,8 @@ public class Assistant extends RoleBased { private String iconUrl; - private String description; - private String displayName; + private LocalizedValue description; + private LocalizedValue displayName; private List inputAttachmentTypes; private Integer maxInputAttachments; private Boolean forwardAuthToken; diff --git a/src/main/java/com/epam/aidial/cfg/domain/model/ExportComponentInfo.java b/src/main/java/com/epam/aidial/cfg/domain/model/ExportComponentInfo.java index 93a4a8c9b..aadba8a03 100644 --- a/src/main/java/com/epam/aidial/cfg/domain/model/ExportComponentInfo.java +++ b/src/main/java/com/epam/aidial/cfg/domain/model/ExportComponentInfo.java @@ -12,8 +12,8 @@ public class ExportComponentInfo { private String name; - private String displayName; + private LocalizedValue displayName; private String displayVersion; - private String description; + private LocalizedValue description; private ExportConfigComponentType type; } diff --git a/src/main/java/com/epam/aidial/cfg/domain/model/Interceptor.java b/src/main/java/com/epam/aidial/cfg/domain/model/Interceptor.java index 6cb44f09b..6126f5731 100644 --- a/src/main/java/com/epam/aidial/cfg/domain/model/Interceptor.java +++ b/src/main/java/com/epam/aidial/cfg/domain/model/Interceptor.java @@ -14,9 +14,9 @@ public class Interceptor { private String endpoint; private Map interfaces; private String iconUrl; - private String description; + private LocalizedValue description; private Set topics; - private String displayName; + private LocalizedValue displayName; private String overrideName; private Boolean forwardAuthToken; private String author; diff --git a/src/main/java/com/epam/aidial/cfg/domain/model/LocalizedValue.java b/src/main/java/com/epam/aidial/cfg/domain/model/LocalizedValue.java new file mode 100644 index 000000000..a11724542 --- /dev/null +++ b/src/main/java/com/epam/aidial/cfg/domain/model/LocalizedValue.java @@ -0,0 +1,86 @@ +package com.epam.aidial.cfg.domain.model; + +import lombok.EqualsAndHashCode; +import lombok.Getter; + +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * Polymorphic domain value type that represents either a plain string or a map of locale codes to localized strings. + * + *

Examples:

+ *
+ * // Plain string
+ * LocalizedValue.of("GPT-4")
+ *
+ * // Locale map
+ * LocalizedValue.of(Map.of("en", "GPT-4", "fr", "GPT-4", "de", "GPT-4"))
+ * 
+ * + * @since 0.47.0 + */ +@Getter +@EqualsAndHashCode +public final class LocalizedValue { + + private final String plainValue; + private final Map localeMap; + + private LocalizedValue(String plainValue, Map localeMap) { + this.plainValue = plainValue; + this.localeMap = localeMap; + } + + /** + * Creates a LocalizedValue from a plain string. + * + * @param value the string value + * @return LocalizedValue instance or null if value is null + */ + public static LocalizedValue of(String value) { + return value == null ? null : new LocalizedValue(value, null); + } + + /** + * Creates a LocalizedValue from a map of locale codes to localized strings. + * + * @param localeMap map of locale codes (e.g., "en") to their localized strings + * @return LocalizedValue instance or null if localeMap is null + */ + public static LocalizedValue of(Map localeMap) { + return localeMap == null ? null : new LocalizedValue(null, new LinkedHashMap<>(localeMap)); + } + + /** + * Checks if this LocalizedValue stores a map of locales. + * + * @return true if this is a locale map, false if it's a plain string + */ + public boolean isMap() { + return localeMap != null; + } + + /** + * Resolves a definite string value for internal (non-localized) Core usages: the requested + * locale if present, else the default locale, else the first available value. + */ + public String resolve(String locale) { + if (plainValue != null) { + return plainValue; + } + if (locale != null && localeMap.containsKey(locale)) { + return localeMap.get(locale); + } + return localeMap.values().stream().findFirst().orElse(null); + } + + @Override + public String toString() { + return plainValue != null ? plainValue : String.valueOf(localeMap); + } + + public boolean isPlain() { + return plainValue != null; + } +} diff --git a/src/main/java/com/epam/aidial/cfg/domain/model/Model.java b/src/main/java/com/epam/aidial/cfg/domain/model/Model.java index 940333276..c6e6eca39 100644 --- a/src/main/java/com/epam/aidial/cfg/domain/model/Model.java +++ b/src/main/java/com/epam/aidial/cfg/domain/model/Model.java @@ -18,9 +18,9 @@ public class Model extends RoleBased { private String endpoint; private String responsesEndpoint; private Map interfaces; - private String description; - private String intro; - private String displayName; + private LocalizedValue description; + private LocalizedValue intro; + private LocalizedValue displayName; private String displayVersion; private String iconUrl; private String reference; diff --git a/src/main/java/com/epam/aidial/cfg/domain/model/ToolSet.java b/src/main/java/com/epam/aidial/cfg/domain/model/ToolSet.java index d677dec52..5b84417a3 100644 --- a/src/main/java/com/epam/aidial/cfg/domain/model/ToolSet.java +++ b/src/main/java/com/epam/aidial/cfg/domain/model/ToolSet.java @@ -18,9 +18,9 @@ public class ToolSet extends SecuredRoleBased { private String endpoint; private String iconUrl; - private String description; - private String intro; - private String displayName; + private LocalizedValue description; + private LocalizedValue intro; + private LocalizedValue displayName; private String vendorWebsite; private Set descriptionKeywords; private Integer maxRetryAttempts; diff --git a/src/main/java/com/epam/aidial/cfg/domain/service/ApplicationService.java b/src/main/java/com/epam/aidial/cfg/domain/service/ApplicationService.java index 87a68784f..310697f3d 100644 --- a/src/main/java/com/epam/aidial/cfg/domain/service/ApplicationService.java +++ b/src/main/java/com/epam/aidial/cfg/domain/service/ApplicationService.java @@ -15,6 +15,7 @@ import com.epam.aidial.cfg.domain.model.Application; import com.epam.aidial.cfg.domain.model.Deployment; import com.epam.aidial.cfg.domain.model.DomainObjectWithHash; +import com.epam.aidial.cfg.domain.model.LocalizedValue; import com.epam.aidial.cfg.domain.model.RoleBased; import com.epam.aidial.cfg.domain.model.RoleLimit; import com.epam.aidial.cfg.domain.model.ToolSet; @@ -26,7 +27,7 @@ import com.epam.aidial.cfg.domain.util.ContainerSourceChangeDetector; import com.epam.aidial.cfg.domain.utils.CoreClientUrlUtils; import com.epam.aidial.cfg.domain.validator.ApplicationValidator; -import com.epam.aidial.cfg.exception.EntityAlreadyExistsException; +import com.epam.aidial.cfg.domain.validator.DisplayNameUniquenessValidator; import com.epam.aidial.cfg.exception.EntityNotFoundException; import com.epam.aidial.cfg.exception.OptimisticLockConflictException; import com.epam.aidial.cfg.service.hashing.HashCalculator; @@ -48,7 +49,7 @@ import java.util.Collections; import java.util.HashSet; import java.util.List; -import java.util.Objects; +import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; @@ -78,6 +79,7 @@ public class ApplicationService { private final ContainerEndpointResolver endpointResolver; private final ApplicationRefreshService applicationRefreshService; private final ToolsClient toolsClient; + private final DisplayNameUniquenessValidator displayNameUniquenessValidator; @Transactional(readOnly = true) public Collection getAllApplications() { @@ -142,7 +144,7 @@ public void createApplication(Application application) { applicationValidator.validateCreation(application); deploymentService.assertDeploymentNotExists(application.getDeployment().getName()); deploymentService.assertInterceptorNotExists(application.getDeployment().getName()); - assertNotExists(application.getDisplayName(), application.getDisplayVersion()); + assertDisplayNameAndDisplayVersionUnique(null, application); resolveEndpointsIfContainerSource(application); Optional.of(application) .map(domainModel -> toEntity(domainModel, new ApplicationEntity())) @@ -171,7 +173,7 @@ private ApplicationEntity performUpdate(String applicationName, Application appl var applicationEntity = applicationJpaRepository.findById(applicationName) .orElseThrow(() -> new EntityNotFoundException(NOT_FOUND_MESSAGE_TEMPLATE.formatted(applicationName))); - assertNewApplicationDisplayNameAndDisplayVersion(applicationEntity, application); + assertDisplayNameAndDisplayVersionUnique(applicationEntity.getDeploymentName(), application); assertNotConcurrencyOverwrite(applicationEntity, hash); resolveEndpointsIfContainerSource(application, applicationEntity); return save(toEntity(application, applicationEntity)); @@ -345,21 +347,13 @@ private void assertExists(String name) { } } - private void assertNotExists(String displayName, String displayVersion) { - if ((displayName != null || displayVersion != null) && applicationJpaRepository.existsByDisplayNameAndDisplayVersion(displayName, displayVersion)) { - throw new EntityAlreadyExistsException("Application with display name: '" + displayName + "' and display version: '" + displayVersion + "' already exists"); - } - } - - private void assertNewApplicationDisplayNameAndDisplayVersion(ApplicationEntity entity, Application domain) { - String displayName = entity.getDisplayName(); - String displayVersion = entity.getDisplayVersion(); - String newDisplayName = domain.getDisplayName(); - String newDisplayVersion = domain.getDisplayVersion(); - - if (!Objects.equals(displayName, newDisplayName) || !Objects.equals(displayVersion, newDisplayVersion)) { - assertNotExists(newDisplayName, newDisplayVersion); - } + private void assertDisplayNameAndDisplayVersionUnique(String currentApplicationName, Application application) { + Map candidates = applicationJpaRepository.findByDisplayVersion(application.getDisplayVersion()).stream() + .map(mapper::toDomain) + .filter(a -> a.getDisplayName() != null) + .collect(Collectors.toMap(a -> a.getDeployment().getName(), Application::getDisplayName)); + displayNameUniquenessValidator.validateUnique("Application", currentApplicationName, + application.getDisplayName(), application.getDisplayVersion(), candidates); } private ApplicationEntity toEntity(Application domain, ApplicationEntity entity) { diff --git a/src/main/java/com/epam/aidial/cfg/domain/service/ModelService.java b/src/main/java/com/epam/aidial/cfg/domain/service/ModelService.java index 32bc25af4..0e7f294e5 100644 --- a/src/main/java/com/epam/aidial/cfg/domain/service/ModelService.java +++ b/src/main/java/com/epam/aidial/cfg/domain/service/ModelService.java @@ -13,6 +13,7 @@ import com.epam.aidial.cfg.dao.model.RoleEntity; import com.epam.aidial.cfg.domain.model.Deployment; import com.epam.aidial.cfg.domain.model.DomainObjectWithHash; +import com.epam.aidial.cfg.domain.model.LocalizedValue; import com.epam.aidial.cfg.domain.model.Model; import com.epam.aidial.cfg.domain.model.RoleBased; import com.epam.aidial.cfg.domain.model.RoleLimit; @@ -22,8 +23,8 @@ import com.epam.aidial.cfg.domain.normalizer.ModelNormalizer; import com.epam.aidial.cfg.domain.util.ContainerEndpointResolver; import com.epam.aidial.cfg.domain.util.ContainerSourceChangeDetector; +import com.epam.aidial.cfg.domain.validator.DisplayNameUniquenessValidator; import com.epam.aidial.cfg.domain.validator.ModelValidator; -import com.epam.aidial.cfg.exception.EntityAlreadyExistsException; import com.epam.aidial.cfg.exception.EntityNotFoundException; import com.epam.aidial.cfg.exception.OptimisticLockConflictException; import com.epam.aidial.cfg.service.hashing.HashCalculator; @@ -42,7 +43,7 @@ import java.util.Collections; import java.util.HashSet; import java.util.List; -import java.util.Objects; +import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; @@ -69,6 +70,7 @@ public class ModelService { private final ModelRefreshService refreshService; private final ContainerEndpointResolver endpointResolver; private final HashCalculator calculator; + private final DisplayNameUniquenessValidator displayNameUniquenessValidator; @Transactional(readOnly = true) public Collection getAll() { @@ -130,7 +132,7 @@ public void createModel(Model model) { modelValidator.validateCreation(model); deploymentService.assertDeploymentNotExists(model.getDeployment().getName()); deploymentService.assertInterceptorNotExists(model.getDeployment().getName()); - assertNotExists(model.getDisplayName(), model.getDisplayVersion()); + assertDisplayNameAndDisplayVersionUnique(null, model); resolveEndpointsIfContainerSource(model); Optional.of(model) .map(domainModel -> toEntity(domainModel, new ModelEntity())) @@ -159,7 +161,7 @@ private ModelEntity performUpdate(String modelName, Model model, String hash) { ModelEntity modelEntity = modelJpaRepository.findById(modelName) .orElseThrow(() -> new EntityNotFoundException(NOT_FOUND_MESSAGE_TEMPLATE.formatted(modelName))); - assertNewModelDisplayNameAndDisplayVersion(modelEntity, model); + assertDisplayNameAndDisplayVersionUnique(modelEntity.getDeploymentName(), model); assertNotConcurrencyOverwrite(modelEntity, hash); resolveEndpointsIfContainerSource(model, modelEntity); return save(toEntity(model, modelEntity)); @@ -259,21 +261,13 @@ private void assertExists(String name) { } } - private void assertNotExists(String displayName, String displayVersion) { - if ((displayName != null || displayVersion != null) && modelJpaRepository.existsByDisplayNameAndDisplayVersion(displayName, displayVersion)) { - throw new EntityAlreadyExistsException("Model with display name: '" + displayName + "' and display version: '" + displayVersion + "' already exists"); - } - } - - private void assertNewModelDisplayNameAndDisplayVersion(ModelEntity entity, Model domain) { - String displayName = entity.getDisplayName(); - String displayVersion = entity.getDisplayVersion(); - String newDisplayName = domain.getDisplayName(); - String newDisplayVersion = domain.getDisplayVersion(); - - if (!Objects.equals(displayName, newDisplayName) || !Objects.equals(displayVersion, newDisplayVersion)) { - assertNotExists(newDisplayName, newDisplayVersion); - } + private void assertDisplayNameAndDisplayVersionUnique(String currentModelName, Model model) { + Map candidates = modelJpaRepository.findByDisplayVersion(model.getDisplayVersion()).stream() + .map(mapper::toDomain) + .filter(m -> m.getDisplayName() != null) + .collect(Collectors.toMap(m -> m.getDeployment().getName(), Model::getDisplayName)); + displayNameUniquenessValidator.validateUnique("Model", currentModelName, + model.getDisplayName(), model.getDisplayVersion(), candidates); } private void assertNotConcurrencyOverwrite(ModelEntity entity, String expectedHash) { diff --git a/src/main/java/com/epam/aidial/cfg/domain/validator/DisplayFieldsValidator.java b/src/main/java/com/epam/aidial/cfg/domain/validator/DisplayFieldsValidator.java index 6992e802a..ab2cd44df 100644 --- a/src/main/java/com/epam/aidial/cfg/domain/validator/DisplayFieldsValidator.java +++ b/src/main/java/com/epam/aidial/cfg/domain/validator/DisplayFieldsValidator.java @@ -1,12 +1,13 @@ package com.epam.aidial.cfg.domain.validator; +import com.epam.aidial.cfg.domain.model.LocalizedValue; import org.apache.commons.lang3.StringUtils; import org.springframework.stereotype.Component; @Component public class DisplayFieldsValidator { - public void validateDisplayNameDisplayVersion(String displayName, + public void validateDisplayNameDisplayVersion(LocalizedValue displayName, String displayVersion, String domainObjectType, String id) { @@ -14,6 +15,17 @@ public void validateDisplayNameDisplayVersion(String displayName, validateDisplayVersion(displayVersion, domainObjectType, id); } + public void validateDisplayName(LocalizedValue displayName, String domainObjectType, String id) { + if (isBlank(displayName)) { + throw new IllegalArgumentException("Display name: '%s' must not be blank for %s with id:'%s'" + .formatted(displayName, domainObjectType, id)); + } + } + + /** + * Overload for non-Deployment types that use plain String displayName. + * Used by Adapter, Route, InterceptorRunner, Role, Key, ApplicationTypeSchema, ExternalService validators. + */ public void validateDisplayName(String displayName, String domainObjectType, String id) { if (StringUtils.isBlank(displayName)) { throw new IllegalArgumentException("Display name: '%s' must not be blank for %s with id:'%s'" @@ -21,6 +33,16 @@ public void validateDisplayName(String displayName, String domainObjectType, Str } } + private boolean isBlank(LocalizedValue value) { + if (value == null) { + return true; + } + if (value.isPlain()) { + return StringUtils.isBlank(value.getPlainValue()); + } + return value.getLocaleMap() == null || value.getLocaleMap().values().stream().allMatch(StringUtils::isBlank); + } + private void validateDisplayVersion(String displayVersion, String domainObjectType, String id) { if (displayVersion != null && StringUtils.isBlank(displayVersion)) { throw new IllegalArgumentException("Display version: '%s' must not be blank for %s with id:'%s'" diff --git a/src/main/java/com/epam/aidial/cfg/domain/validator/DisplayNameUniquenessValidator.java b/src/main/java/com/epam/aidial/cfg/domain/validator/DisplayNameUniquenessValidator.java new file mode 100644 index 000000000..6e1082fc2 --- /dev/null +++ b/src/main/java/com/epam/aidial/cfg/domain/validator/DisplayNameUniquenessValidator.java @@ -0,0 +1,76 @@ +package com.epam.aidial.cfg.domain.validator; + +import com.epam.aidial.cfg.domain.model.LocalizedValue; +import com.epam.aidial.cfg.exception.EntityAlreadyExistsException; +import org.apache.commons.lang3.StringUtils; +import org.springframework.stereotype.Component; + +import java.util.Map; +import java.util.Objects; + +/** + * Validates that {@code displayName} is unique (per candidate with a matching {@code displayVersion}) + * among locales independently: a plain string and each locale of a localized value are only + * compared against the same locale (or a plain value) of other candidates. There is no + * {@code defaultLocale} fallback. + */ +@Component +public class DisplayNameUniquenessValidator { + + /** + * @param currentEntityName the entity being updated, excluded from the check; {@code null} on create + * @param candidateDisplayNames display names of other entities that already share {@code newDisplayVersion}, + * keyed by entity name + */ + public void validateUnique(String domainObjectType, + String currentEntityName, + LocalizedValue newDisplayName, + String newDisplayVersion, + Map candidateDisplayNames) { + if (isBlank(newDisplayName) && StringUtils.isEmpty(newDisplayVersion)) { + return; + } + + for (Map.Entry entry : candidateDisplayNames.entrySet()) { + if (entry.getKey().equals(currentEntityName)) { + continue; + } + if (conflicts(newDisplayName, entry.getValue())) { + throw new EntityAlreadyExistsException(domainObjectType + " with display name: '" + newDisplayName + + "' and display version: '" + newDisplayVersion + "' already exists"); + } + } + } + + private boolean conflicts(LocalizedValue left, LocalizedValue right) { + if (left == null || right == null) { + return false; + } + if (left.isPlain() && right.isPlain()) { + return StringUtils.isNotEmpty(left.getPlainValue()) && Objects.equals(left.getPlainValue(), right.getPlainValue()); + } + if (left.isPlain()) { + return StringUtils.isNotEmpty(left.getPlainValue()) && right.getLocaleMap().containsValue(left.getPlainValue()); + } + if (right.isPlain()) { + return StringUtils.isNotEmpty(right.getPlainValue()) && left.getLocaleMap().containsValue(right.getPlainValue()); + } + for (Map.Entry entry : left.getLocaleMap().entrySet()) { + String value = entry.getValue(); + if (StringUtils.isNotEmpty(value) && Objects.equals(value, right.getLocaleMap().get(entry.getKey()))) { + return true; + } + } + return false; + } + + private boolean isBlank(LocalizedValue value) { + if (value == null) { + return true; + } + if (value.isPlain()) { + return StringUtils.isBlank(value.getPlainValue()); + } + return value.getLocaleMap() == null || value.getLocaleMap().values().stream().allMatch(StringUtils::isBlank); + } +} diff --git a/src/main/java/com/epam/aidial/cfg/dto/AddonDto.java b/src/main/java/com/epam/aidial/cfg/dto/AddonDto.java index bd67620ce..43d57b735 100644 --- a/src/main/java/com/epam/aidial/cfg/dto/AddonDto.java +++ b/src/main/java/com/epam/aidial/cfg/dto/AddonDto.java @@ -1,5 +1,6 @@ package com.epam.aidial.cfg.dto; +import com.epam.aidial.cfg.dto.validation.annotation.NotEmptyLocalizedValue; import jakarta.validation.constraints.NotBlank; import lombok.Data; import lombok.EqualsAndHashCode; @@ -17,9 +18,9 @@ public class AddonDto extends RoleBasedDto { private String endpoint; private String iconUrl; - private String description; - @NotBlank(message = "DisplayName is required") - private String displayName; + private LocalizedValueDto description; + @NotEmptyLocalizedValue(message = "DisplayName is required") + private LocalizedValueDto displayName; private List inputAttachmentTypes; private Integer maxInputAttachments; private Boolean forwardAuthToken; diff --git a/src/main/java/com/epam/aidial/cfg/dto/ApplicationDto.java b/src/main/java/com/epam/aidial/cfg/dto/ApplicationDto.java index 975633592..360fd84fe 100644 --- a/src/main/java/com/epam/aidial/cfg/dto/ApplicationDto.java +++ b/src/main/java/com/epam/aidial/cfg/dto/ApplicationDto.java @@ -3,6 +3,7 @@ import com.epam.aidial.cfg.dto.route.DependentRouteDto; import com.epam.aidial.cfg.dto.source.ApplicationSourceDto; import com.epam.aidial.cfg.dto.validation.annotation.Endpoint; +import com.epam.aidial.cfg.dto.validation.annotation.NotEmptyLocalizedValue; import com.fasterxml.jackson.annotation.JsonAlias; import jakarta.validation.Valid; import jakarta.validation.constraints.NotBlank; @@ -29,13 +30,13 @@ public class ApplicationDto extends RoleBasedDto { private String responsesEndpoint; @Valid private Map interfaces; - @NotBlank(message = "DisplayName is required") - private String displayName; + @NotEmptyLocalizedValue(message = "DisplayName is required") + private LocalizedValueDto displayName; private String displayVersion; private String overrideName; private String iconUrl; - private String description; - private String intro; + private LocalizedValueDto description; + private LocalizedValueDto intro; private String reference; private Boolean forwardAuthToken; private FeaturesDto features = new FeaturesDto(); diff --git a/src/main/java/com/epam/aidial/cfg/dto/ApplicationEximDto.java b/src/main/java/com/epam/aidial/cfg/dto/ApplicationEximDto.java index ee90a5322..4fe02ea62 100644 --- a/src/main/java/com/epam/aidial/cfg/dto/ApplicationEximDto.java +++ b/src/main/java/com/epam/aidial/cfg/dto/ApplicationEximDto.java @@ -19,12 +19,12 @@ public class ApplicationEximDto { private String endpoint; private String responsesEndpoint; private Map interfaces; - private String displayName; + private LocalizedValueDto displayName; private String displayVersion; private String overrideName; private String iconUrl; - private String description; - private String intro; + private LocalizedValueDto description; + private LocalizedValueDto intro; private String reference; private Boolean forwardAuthToken; private List inputAttachmentTypes; diff --git a/src/main/java/com/epam/aidial/cfg/dto/ApplicationInfoDto.java b/src/main/java/com/epam/aidial/cfg/dto/ApplicationInfoDto.java index 42ace2310..0b7257a9d 100644 --- a/src/main/java/com/epam/aidial/cfg/dto/ApplicationInfoDto.java +++ b/src/main/java/com/epam/aidial/cfg/dto/ApplicationInfoDto.java @@ -12,11 +12,11 @@ public class ApplicationInfoDto { private String name; private String endpoint; - private String displayName; + private LocalizedValueDto displayName; private String displayVersion; private String overrideName; private String iconUrl; - private String description; + private LocalizedValueDto description; private Boolean forwardAuthToken; private List inputAttachmentTypes; private Integer maxInputAttachments; diff --git a/src/main/java/com/epam/aidial/cfg/dto/ApplicationResourceDto.java b/src/main/java/com/epam/aidial/cfg/dto/ApplicationResourceDto.java index 453eab610..c4b778741 100644 --- a/src/main/java/com/epam/aidial/cfg/dto/ApplicationResourceDto.java +++ b/src/main/java/com/epam/aidial/cfg/dto/ApplicationResourceDto.java @@ -19,12 +19,12 @@ public class ApplicationResourceDto { private String endpoint; private String responsesEndpoint; private Map interfaces; - private String displayName; + private LocalizedValueDto displayName; private String displayVersion; private String overrideName; private String iconUrl; - private String description; - private String intro; + private LocalizedValueDto description; + private LocalizedValueDto intro; private String reference; private Boolean forwardAuthToken; private List inputAttachmentTypes; diff --git a/src/main/java/com/epam/aidial/cfg/dto/AssistantDto.java b/src/main/java/com/epam/aidial/cfg/dto/AssistantDto.java index 410f41d33..6eea3f85c 100644 --- a/src/main/java/com/epam/aidial/cfg/dto/AssistantDto.java +++ b/src/main/java/com/epam/aidial/cfg/dto/AssistantDto.java @@ -17,9 +17,9 @@ public class AssistantDto extends RoleBasedDto { @NotBlank(message = "Name is required") private String name; private String iconUrl; - private String description; + private LocalizedValueDto description; @NotBlank(message = "DisplayName is required") - private String displayName; + private LocalizedValueDto displayName; private List inputAttachmentTypes; private Integer maxInputAttachments; private Boolean forwardAuthToken; diff --git a/src/main/java/com/epam/aidial/cfg/dto/CreateApplicationResourceDto.java b/src/main/java/com/epam/aidial/cfg/dto/CreateApplicationResourceDto.java index d58c5348b..0f1d49d02 100644 --- a/src/main/java/com/epam/aidial/cfg/dto/CreateApplicationResourceDto.java +++ b/src/main/java/com/epam/aidial/cfg/dto/CreateApplicationResourceDto.java @@ -28,12 +28,12 @@ public class CreateApplicationResourceDto { private String responsesEndpoint; @Valid private Map interfaces; - private String displayName; + private LocalizedValueDto displayName; private String displayVersion; private String overrideName; private String iconUrl; - private String description; - private String intro; + private LocalizedValueDto description; + private LocalizedValueDto intro; private String reference; private Boolean forwardAuthToken; private List inputAttachmentTypes; diff --git a/src/main/java/com/epam/aidial/cfg/dto/CreateToolSetResourceDto.java b/src/main/java/com/epam/aidial/cfg/dto/CreateToolSetResourceDto.java index bf2905d72..d7667b558 100644 --- a/src/main/java/com/epam/aidial/cfg/dto/CreateToolSetResourceDto.java +++ b/src/main/java/com/epam/aidial/cfg/dto/CreateToolSetResourceDto.java @@ -1,6 +1,7 @@ package com.epam.aidial.cfg.dto; import com.epam.aidial.cfg.dto.validation.annotation.Endpoint; +import com.epam.aidial.cfg.dto.validation.annotation.NotEmptyLocalizedValue; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; import lombok.Data; @@ -20,12 +21,12 @@ public class CreateToolSetResourceDto { @NotBlank(message = "Completion endpoint is required") @Endpoint private String endpoint; - @NotBlank(message = "Display name is required") - private String displayName; + @NotEmptyLocalizedValue(message = "Display name is required") + private LocalizedValueDto displayName; private String displayVersion; private String iconUrl; - private String description; - private String intro; + private LocalizedValueDto description; + private LocalizedValueDto intro; private String vendorWebsite; private List descriptionKeywords; private Integer maxRetryAttempts; diff --git a/src/main/java/com/epam/aidial/cfg/dto/DeploymentDataDto.java b/src/main/java/com/epam/aidial/cfg/dto/DeploymentDataDto.java index 5eff11670..aca1cf71b 100644 --- a/src/main/java/com/epam/aidial/cfg/dto/DeploymentDataDto.java +++ b/src/main/java/com/epam/aidial/cfg/dto/DeploymentDataDto.java @@ -12,10 +12,11 @@ public class DeploymentDataDto { private String model; private String application; private String toolset; - private String displayName; + private LocalizedValueDto displayName; private String displayVersion; private String iconUrl; - private String description; + private LocalizedValueDto description; + private LocalizedValueDto intro; private String reference; private String owner; private String object; diff --git a/src/main/java/com/epam/aidial/cfg/dto/ExportApplicationTypeSchemaInfoDto.java b/src/main/java/com/epam/aidial/cfg/dto/ExportApplicationTypeSchemaInfoDto.java index fdf68d375..54f280ef9 100644 --- a/src/main/java/com/epam/aidial/cfg/dto/ExportApplicationTypeSchemaInfoDto.java +++ b/src/main/java/com/epam/aidial/cfg/dto/ExportApplicationTypeSchemaInfoDto.java @@ -19,13 +19,13 @@ public class ExportApplicationTypeSchemaInfoDto extends ExportComponentInfoDto { @JsonProperty("dial:applicationTypeDisplayName") @Override - public String getDisplayName() { + public LocalizedValueDto getDisplayName() { return super.getDisplayName(); } @JsonProperty("dial:applicationTypeDisplayName") @Override - public void setDisplayName(String displayName) { + public void setDisplayName(LocalizedValueDto displayName) { super.setDisplayName(displayName); } } diff --git a/src/main/java/com/epam/aidial/cfg/dto/ExportComponentInfoDto.java b/src/main/java/com/epam/aidial/cfg/dto/ExportComponentInfoDto.java index 20c81024c..245d86d11 100644 --- a/src/main/java/com/epam/aidial/cfg/dto/ExportComponentInfoDto.java +++ b/src/main/java/com/epam/aidial/cfg/dto/ExportComponentInfoDto.java @@ -13,8 +13,8 @@ public class ExportComponentInfoDto { private String name; - private String displayName; + private LocalizedValueDto displayName; private String displayVersion; - private String description; + private LocalizedValueDto description; private ExportConfigComponentType type; } diff --git a/src/main/java/com/epam/aidial/cfg/dto/InterceptorDto.java b/src/main/java/com/epam/aidial/cfg/dto/InterceptorDto.java index 2ad82958f..eeb8cd6fa 100644 --- a/src/main/java/com/epam/aidial/cfg/dto/InterceptorDto.java +++ b/src/main/java/com/epam/aidial/cfg/dto/InterceptorDto.java @@ -2,6 +2,7 @@ import com.epam.aidial.cfg.dto.source.InterceptorSourceDto; import com.epam.aidial.cfg.dto.validation.annotation.Endpoint; +import com.epam.aidial.cfg.dto.validation.annotation.NotEmptyLocalizedValue; import jakarta.validation.Valid; import jakarta.validation.constraints.NotBlank; import lombok.Data; @@ -22,9 +23,9 @@ public class InterceptorDto { @Valid private Map interfaces; private String iconUrl; - private String description; - @NotBlank(message = "DisplayName is required") - private String displayName; + private LocalizedValueDto description; + @NotEmptyLocalizedValue(message = "DisplayName is required") + private LocalizedValueDto displayName; private String overrideName; private Boolean forwardAuthToken; private List entities; diff --git a/src/main/java/com/epam/aidial/cfg/dto/LocalizedValueDto.java b/src/main/java/com/epam/aidial/cfg/dto/LocalizedValueDto.java new file mode 100644 index 000000000..b57b90353 --- /dev/null +++ b/src/main/java/com/epam/aidial/cfg/dto/LocalizedValueDto.java @@ -0,0 +1,50 @@ +package com.epam.aidial.cfg.dto; + +import com.epam.aidial.cfg.dto.databind.LocalizedValueDeserializer; +import com.epam.aidial.cfg.dto.databind.LocalizedValueSerializer; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import lombok.EqualsAndHashCode; +import lombok.Getter; + +import java.util.LinkedHashMap; +import java.util.Map; + +@Getter +@JsonDeserialize(using = LocalizedValueDeserializer.class) +@JsonSerialize(using = LocalizedValueSerializer.class) +@EqualsAndHashCode +public final class LocalizedValueDto { + + private final String plainValue; + private final Map localeMap; + + private LocalizedValueDto(String plainValue, Map localeMap) { + this.plainValue = plainValue; + this.localeMap = localeMap; + } + + public static LocalizedValueDto of(String value) { + return value == null ? null : new LocalizedValueDto(value, null); + } + + public static LocalizedValueDto of(Map localeMap) { + return localeMap == null ? null : new LocalizedValueDto(null, new LinkedHashMap<>(localeMap)); + } + + @JsonIgnore + public boolean isMap() { + return localeMap != null; + } + + @JsonIgnore + public boolean isPlain() { + return plainValue != null; + } + + @Override + public String toString() { + return plainValue != null ? plainValue : String.valueOf(localeMap); + } +} diff --git a/src/main/java/com/epam/aidial/cfg/dto/ModelDto.java b/src/main/java/com/epam/aidial/cfg/dto/ModelDto.java index a5ced5141..9e94300a7 100644 --- a/src/main/java/com/epam/aidial/cfg/dto/ModelDto.java +++ b/src/main/java/com/epam/aidial/cfg/dto/ModelDto.java @@ -2,6 +2,7 @@ import com.epam.aidial.cfg.dto.source.ModelSourceDto; import com.epam.aidial.cfg.dto.validation.annotation.Endpoint; +import com.epam.aidial.cfg.dto.validation.annotation.NotEmptyLocalizedValue; import jakarta.validation.Valid; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.Positive; @@ -29,12 +30,12 @@ public class ModelDto extends RoleBasedDto { private String responsesEndpoint; @Valid private Map interfaces; - @NotBlank(message = "DisplayName is required") - private String displayName; + @NotEmptyLocalizedValue(message = "DisplayName is required") + private LocalizedValueDto displayName; private String displayVersion; private String iconUrl; - private String description; - private String intro; + private LocalizedValueDto description; + private LocalizedValueDto intro; private String reference; private Boolean forwardAuthToken; private FeaturesDto features = new FeaturesDto(); diff --git a/src/main/java/com/epam/aidial/cfg/dto/ModelInfoDto.java b/src/main/java/com/epam/aidial/cfg/dto/ModelInfoDto.java index c13028abc..902d9112e 100644 --- a/src/main/java/com/epam/aidial/cfg/dto/ModelInfoDto.java +++ b/src/main/java/com/epam/aidial/cfg/dto/ModelInfoDto.java @@ -11,8 +11,8 @@ public class ModelInfoDto { private String name; private String overrideName; private String endpoint; - private String displayName; - private String displayVersion; + private LocalizedValueDto displayName; + private LocalizedValueDto displayVersion; private String description; private Boolean forwardAuthToken; private List inputAttachmentTypes; diff --git a/src/main/java/com/epam/aidial/cfg/dto/ToolSetDto.java b/src/main/java/com/epam/aidial/cfg/dto/ToolSetDto.java index 89df71c80..0c4e4ac21 100644 --- a/src/main/java/com/epam/aidial/cfg/dto/ToolSetDto.java +++ b/src/main/java/com/epam/aidial/cfg/dto/ToolSetDto.java @@ -2,6 +2,7 @@ import com.epam.aidial.cfg.dto.source.ToolSetSourceDto; import com.epam.aidial.cfg.dto.validation.annotation.Endpoint; +import com.epam.aidial.cfg.dto.validation.annotation.NotEmptyLocalizedValue; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Positive; @@ -23,10 +24,10 @@ public class ToolSetDto extends RoleBasedDto { @Endpoint private String endpoint; private String iconUrl; - private String description; - private String intro; - @NotBlank(message = "DisplayName is required") - private String displayName; + private LocalizedValueDto description; + private LocalizedValueDto intro; + @NotEmptyLocalizedValue(message = "DisplayName is required") + private LocalizedValueDto displayName; private String vendorWebsite; private TreeSet descriptionKeywords; @Positive(message = "Max retry attempts should be greater than 0") diff --git a/src/main/java/com/epam/aidial/cfg/dto/ToolSetEximDto.java b/src/main/java/com/epam/aidial/cfg/dto/ToolSetEximDto.java index 7c5c07093..4d2e4c17f 100644 --- a/src/main/java/com/epam/aidial/cfg/dto/ToolSetEximDto.java +++ b/src/main/java/com/epam/aidial/cfg/dto/ToolSetEximDto.java @@ -17,11 +17,11 @@ public class ToolSetEximDto { private String folderId; private String version; private String endpoint; - private String displayName; + private LocalizedValueDto displayName; private String displayVersion; private String iconUrl; - private String description; - private String intro; + private LocalizedValueDto description; + private LocalizedValueDto intro; private String vendorWebsite; private List descriptionKeywords; private Integer maxRetryAttempts; diff --git a/src/main/java/com/epam/aidial/cfg/dto/ToolSetResourceDto.java b/src/main/java/com/epam/aidial/cfg/dto/ToolSetResourceDto.java index c8a50d254..b3f1dd5e8 100644 --- a/src/main/java/com/epam/aidial/cfg/dto/ToolSetResourceDto.java +++ b/src/main/java/com/epam/aidial/cfg/dto/ToolSetResourceDto.java @@ -14,11 +14,11 @@ public class ToolSetResourceDto { private String author; private String name; private String endpoint; - private String displayName; + private LocalizedValueDto displayName; private String displayVersion; private String iconUrl; - private String description; - private String intro; + private LocalizedValueDto description; + private LocalizedValueDto intro; private String vendorWebsite; private List descriptionKeywords; private Integer maxRetryAttempts; diff --git a/src/main/java/com/epam/aidial/cfg/dto/databind/LocalizedValueDeserializer.java b/src/main/java/com/epam/aidial/cfg/dto/databind/LocalizedValueDeserializer.java new file mode 100644 index 000000000..30d2be789 --- /dev/null +++ b/src/main/java/com/epam/aidial/cfg/dto/databind/LocalizedValueDeserializer.java @@ -0,0 +1,38 @@ +package com.epam.aidial.cfg.dto.databind; + +import com.epam.aidial.cfg.dto.LocalizedValueDto; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonToken; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; + +import java.io.IOException; +import java.util.LinkedHashMap; +import java.util.Map; + +public class LocalizedValueDeserializer extends JsonDeserializer { + + @Override + public LocalizedValueDto deserialize(JsonParser p, DeserializationContext ctx) throws IOException { + if (p.getCurrentToken() == JsonToken.VALUE_STRING) { + return LocalizedValueDto.of(p.getValueAsString()); + } + + if (p.getCurrentToken() == JsonToken.START_OBJECT) { + Map localeMap = new LinkedHashMap<>(); + while (p.nextToken() != JsonToken.END_OBJECT) { + String locale = p.getCurrentName(); + p.nextToken(); + localeMap.put(locale, p.getValueAsString()); + } + return LocalizedValueDto.of(localeMap); + } + + if (p.getCurrentToken() == JsonToken.VALUE_NULL) { + return null; + } + + return ctx.reportInputMismatch(LocalizedValueDto.class, + "Expected a string or a locale-to-value object, got %s", p.getCurrentToken()); + } +} diff --git a/src/main/java/com/epam/aidial/cfg/dto/databind/LocalizedValueSerializer.java b/src/main/java/com/epam/aidial/cfg/dto/databind/LocalizedValueSerializer.java new file mode 100644 index 000000000..181573ea0 --- /dev/null +++ b/src/main/java/com/epam/aidial/cfg/dto/databind/LocalizedValueSerializer.java @@ -0,0 +1,30 @@ +package com.epam.aidial.cfg.dto.databind; + +import com.epam.aidial.cfg.dto.LocalizedValueDto; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; + +import java.io.IOException; + +public class LocalizedValueSerializer extends JsonSerializer { + + @Override + public void serialize(LocalizedValueDto value, JsonGenerator gen, SerializerProvider serializers) throws IOException { + if (!value.isMap()) { + gen.writeString(value.getPlainValue()); + return; + } + + if (value.getLocaleMap().size() == 1) { + gen.writeString(value.getLocaleMap().values().iterator().next()); + return; + } + + gen.writeStartObject(); + for (var entry : value.getLocaleMap().entrySet()) { + gen.writeStringField(entry.getKey(), entry.getValue()); + } + gen.writeEndObject(); + } +} diff --git a/src/main/java/com/epam/aidial/cfg/dto/validation/annotation/NotEmptyLocalizedValue.java b/src/main/java/com/epam/aidial/cfg/dto/validation/annotation/NotEmptyLocalizedValue.java new file mode 100644 index 000000000..6676b67ba --- /dev/null +++ b/src/main/java/com/epam/aidial/cfg/dto/validation/annotation/NotEmptyLocalizedValue.java @@ -0,0 +1,25 @@ +package com.epam.aidial.cfg.dto.validation.annotation; + +import com.epam.aidial.cfg.dto.validation.validator.NotEmptyLocalizedValueValidator; +import jakarta.validation.Constraint; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +@Constraint(validatedBy = {NotEmptyLocalizedValueValidator.class}) +@Documented +@Retention(RUNTIME) +@Target(FIELD) +public @interface NotEmptyLocalizedValue { + + String message() default "must not be empty"; + + Class[] groups() default {}; + + Class[] payload() default {}; + +} diff --git a/src/main/java/com/epam/aidial/cfg/dto/validation/validator/NotEmptyLocalizedValueValidator.java b/src/main/java/com/epam/aidial/cfg/dto/validation/validator/NotEmptyLocalizedValueValidator.java new file mode 100644 index 000000000..57ffe7cb8 --- /dev/null +++ b/src/main/java/com/epam/aidial/cfg/dto/validation/validator/NotEmptyLocalizedValueValidator.java @@ -0,0 +1,22 @@ +package com.epam.aidial.cfg.dto.validation.validator; + +import com.epam.aidial.cfg.dto.LocalizedValueDto; +import com.epam.aidial.cfg.dto.validation.annotation.NotEmptyLocalizedValue; +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; +import org.apache.commons.lang3.StringUtils; + +public class NotEmptyLocalizedValueValidator implements ConstraintValidator { + + @Override + public boolean isValid(LocalizedValueDto value, ConstraintValidatorContext context) { + if (value == null) { + return false; + } + + return value.isPlain() + ? StringUtils.isNotBlank(value.getPlainValue()) + : value.getLocaleMap() != null + && value.getLocaleMap().values().stream().anyMatch(StringUtils::isNotBlank); + } +} diff --git a/src/main/java/com/epam/aidial/cfg/mapper/ApplicationResourceMapper.java b/src/main/java/com/epam/aidial/cfg/mapper/ApplicationResourceMapper.java index 73fca80f0..c85ea8463 100644 --- a/src/main/java/com/epam/aidial/cfg/mapper/ApplicationResourceMapper.java +++ b/src/main/java/com/epam/aidial/cfg/mapper/ApplicationResourceMapper.java @@ -15,7 +15,7 @@ import org.mapstruct.Mapping; import org.mapstruct.Named; -@Mapper(componentModel = "spring") +@Mapper(componentModel = "spring", uses = LocalizedValueMapper.class) public interface ApplicationResourceMapper { @Mapping(target = "applicationTypeSchemaId", source = "source", qualifiedByName = "toSchemaIdString") diff --git a/src/main/java/com/epam/aidial/cfg/mapper/DeploymentMapper.java b/src/main/java/com/epam/aidial/cfg/mapper/DeploymentMapper.java index 10c04842d..bc46b6191 100644 --- a/src/main/java/com/epam/aidial/cfg/mapper/DeploymentMapper.java +++ b/src/main/java/com/epam/aidial/cfg/mapper/DeploymentMapper.java @@ -13,7 +13,7 @@ import java.util.List; -@Mapper(componentModel = "spring") +@Mapper(componentModel = "spring", uses = LocalizedValueMapper.class) public interface DeploymentMapper { List toDeploymentDataDtoList(List models); diff --git a/src/main/java/com/epam/aidial/cfg/mapper/LocalizedValueMapper.java b/src/main/java/com/epam/aidial/cfg/mapper/LocalizedValueMapper.java new file mode 100644 index 000000000..14888297a --- /dev/null +++ b/src/main/java/com/epam/aidial/cfg/mapper/LocalizedValueMapper.java @@ -0,0 +1,23 @@ +package com.epam.aidial.cfg.mapper; + +import com.epam.aidial.cfg.dto.LocalizedValueDto; +import com.epam.aidial.cfg.model.LocalizedValue; +import org.springframework.stereotype.Component; + +@Component +public class LocalizedValueMapper { + + public LocalizedValueDto toDto(LocalizedValue domain) { + if (domain == null) { + return null; + } + return domain.isPlain() ? LocalizedValueDto.of(domain.getPlainValue()) : LocalizedValueDto.of(domain.getLocaleMap()); + } + + public LocalizedValue toDomain(LocalizedValueDto dto) { + if (dto == null) { + return null; + } + return dto.isPlain() ? LocalizedValue.of(dto.getPlainValue()) : LocalizedValue.of(dto.getLocaleMap()); + } +} diff --git a/src/main/java/com/epam/aidial/cfg/mapper/PublicationMapper.java b/src/main/java/com/epam/aidial/cfg/mapper/PublicationMapper.java index 4feb72a2d..ff67e961c 100644 --- a/src/main/java/com/epam/aidial/cfg/mapper/PublicationMapper.java +++ b/src/main/java/com/epam/aidial/cfg/mapper/PublicationMapper.java @@ -43,7 +43,7 @@ import java.util.List; import java.util.Objects; -@Mapper(componentModel = "spring", uses = ApplicationResourceMapper.class) +@Mapper(componentModel = "spring", uses = {ApplicationResourceMapper.class, LocalizedValueMapper.class}) public interface PublicationMapper { PublicationInfosDto toPromptPublicationInfosDto(PublicationInfos model); diff --git a/src/main/java/com/epam/aidial/cfg/mapper/ToolSetResourceMapper.java b/src/main/java/com/epam/aidial/cfg/mapper/ToolSetResourceMapper.java index 3a2faf30a..39c2131ae 100644 --- a/src/main/java/com/epam/aidial/cfg/mapper/ToolSetResourceMapper.java +++ b/src/main/java/com/epam/aidial/cfg/mapper/ToolSetResourceMapper.java @@ -10,7 +10,7 @@ import com.epam.aidial.cfg.model.ToolSetsExim; import org.mapstruct.Mapper; -@Mapper(componentModel = "spring") +@Mapper(componentModel = "spring", uses = LocalizedValueMapper.class) public interface ToolSetResourceMapper { CreateToolSetResource toCreateToolSetResourceDto(CreateToolSetResourceDto createToolSetResourceDto); diff --git a/src/main/java/com/epam/aidial/cfg/model/ApplicationExim.java b/src/main/java/com/epam/aidial/cfg/model/ApplicationExim.java index e7a78cf8a..a4aa822ff 100644 --- a/src/main/java/com/epam/aidial/cfg/model/ApplicationExim.java +++ b/src/main/java/com/epam/aidial/cfg/model/ApplicationExim.java @@ -19,11 +19,11 @@ public class ApplicationExim { private String endpoint; private String responsesEndpoint; private Map interfaces; - private String displayName; + private LocalizedValue displayName; private String displayVersion; private String overrideName; private String iconUrl; - private String description; + private LocalizedValue description; private String reference; private Boolean forwardAuthToken; private List inputAttachmentTypes; @@ -44,7 +44,7 @@ public class ApplicationExim { private String applicationTypeSchemaId; private Map applicationProperties; private McpResource mcp; - private String intro; + private LocalizedValue intro; private String appIdentity; private boolean allowUserExternalServices; private Map externalServices; diff --git a/src/main/java/com/epam/aidial/cfg/model/ApplicationResource.java b/src/main/java/com/epam/aidial/cfg/model/ApplicationResource.java index 324b2ef4d..71e1bebf3 100644 --- a/src/main/java/com/epam/aidial/cfg/model/ApplicationResource.java +++ b/src/main/java/com/epam/aidial/cfg/model/ApplicationResource.java @@ -23,11 +23,11 @@ public class ApplicationResource { private String endpoint; private String responsesEndpoint; private Map interfaces; - private String displayName; + private LocalizedValue displayName; private String displayVersion; private String overrideName; private String iconUrl; - private String description; + private LocalizedValue description; private String reference; private Boolean forwardAuthToken; private List inputAttachmentTypes; @@ -51,7 +51,7 @@ public class ApplicationResource { private McpResource mcp; private Map externalServices; private String url; - private String intro; + private LocalizedValue intro; private String appIdentity; private boolean allowUserExternalServices; private String catalogSchemaId; diff --git a/src/main/java/com/epam/aidial/cfg/model/CreateApplicationResource.java b/src/main/java/com/epam/aidial/cfg/model/CreateApplicationResource.java index b00ce720f..59fbe7354 100644 --- a/src/main/java/com/epam/aidial/cfg/model/CreateApplicationResource.java +++ b/src/main/java/com/epam/aidial/cfg/model/CreateApplicationResource.java @@ -19,11 +19,11 @@ public class CreateApplicationResource { private String endpoint; private String responsesEndpoint; private Map interfaces; - private String displayName; + private LocalizedValue displayName; private String displayVersion; private String overrideName; private String iconUrl; - private String description; + private LocalizedValue description; private String reference; private Boolean forwardAuthToken; private List inputAttachmentTypes; @@ -43,7 +43,7 @@ public class CreateApplicationResource { private String applicationTypeSchemaId; private Map applicationProperties; private McpResource mcp; - private String intro; + private LocalizedValue intro; private String appIdentity; private boolean allowUserExternalServices; private Map externalServices; diff --git a/src/main/java/com/epam/aidial/cfg/model/CreateToolSetResource.java b/src/main/java/com/epam/aidial/cfg/model/CreateToolSetResource.java index c53c7d9dc..fb2a8e99b 100644 --- a/src/main/java/com/epam/aidial/cfg/model/CreateToolSetResource.java +++ b/src/main/java/com/epam/aidial/cfg/model/CreateToolSetResource.java @@ -17,10 +17,10 @@ public class CreateToolSetResource { private String version; private String folderId; private String endpoint; - private String displayName; + private LocalizedValue displayName; private String displayVersion; private String iconUrl; - private String description; + private LocalizedValue description; private String vendorWebsite; private List descriptionKeywords; private Integer maxRetryAttempts; @@ -30,7 +30,7 @@ public class CreateToolSetResource { private ResourceAuthSettings authSettings; private boolean forwardPerRequestKey; private boolean forwardAuthToken; - private String intro; + private LocalizedValue intro; private String catalogSchemaId; private Map catalogProperties; diff --git a/src/main/java/com/epam/aidial/cfg/model/DeploymentData.java b/src/main/java/com/epam/aidial/cfg/model/DeploymentData.java index 1836fc4d4..ecb6f3b64 100644 --- a/src/main/java/com/epam/aidial/cfg/model/DeploymentData.java +++ b/src/main/java/com/epam/aidial/cfg/model/DeploymentData.java @@ -18,10 +18,11 @@ public class DeploymentData { private String model; private String application; private String toolset; - private String displayName; + private LocalizedValue displayName; private String displayVersion; private String iconUrl; - private String description; + private LocalizedValue description; + private LocalizedValue intro; private String reference; private String owner; private String object; diff --git a/src/main/java/com/epam/aidial/cfg/model/LocalizedValue.java b/src/main/java/com/epam/aidial/cfg/model/LocalizedValue.java new file mode 100644 index 000000000..a324ccc46 --- /dev/null +++ b/src/main/java/com/epam/aidial/cfg/model/LocalizedValue.java @@ -0,0 +1,50 @@ +package com.epam.aidial.cfg.model; + +import com.epam.aidial.cfg.model.databind.LocalizedValueDeserializer; +import com.epam.aidial.cfg.model.databind.LocalizedValueSerializer; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import lombok.EqualsAndHashCode; +import lombok.Getter; + +import java.util.LinkedHashMap; +import java.util.Map; + +@Getter +@JsonDeserialize(using = LocalizedValueDeserializer.class) +@JsonSerialize(using = LocalizedValueSerializer.class) +@EqualsAndHashCode +public final class LocalizedValue { + + private final String plainValue; + private final Map localeMap; + + private LocalizedValue(String plainValue, Map localeMap) { + this.plainValue = plainValue; + this.localeMap = localeMap; + } + + public static LocalizedValue of(String value) { + return value == null ? null : new LocalizedValue(value, null); + } + + public static LocalizedValue of(Map localeMap) { + return localeMap == null ? null : new LocalizedValue(null, new LinkedHashMap<>(localeMap)); + } + + @JsonIgnore + public boolean isMap() { + return localeMap != null; + } + + @JsonIgnore + public boolean isPlain() { + return plainValue != null; + } + + @Override + public String toString() { + return plainValue != null ? plainValue : String.valueOf(localeMap); + } +} diff --git a/src/main/java/com/epam/aidial/cfg/model/ToolSetExim.java b/src/main/java/com/epam/aidial/cfg/model/ToolSetExim.java index e28287126..875ec4e42 100644 --- a/src/main/java/com/epam/aidial/cfg/model/ToolSetExim.java +++ b/src/main/java/com/epam/aidial/cfg/model/ToolSetExim.java @@ -18,10 +18,10 @@ public class ToolSetExim { private String folderId; private String version; private String endpoint; - private String displayName; + private LocalizedValue displayName; private String displayVersion; private String iconUrl; - private String description; + private LocalizedValue description; private String vendorWebsite; private List descriptionKeywords; private Integer maxRetryAttempts; @@ -32,7 +32,7 @@ public class ToolSetExim { private ResourceAuthSettings authSettings; private boolean forwardPerRequestKey; private boolean forwardAuthToken; - private String intro; + private LocalizedValue intro; private String catalogSchemaId; private Map catalogProperties; diff --git a/src/main/java/com/epam/aidial/cfg/model/ToolSetResource.java b/src/main/java/com/epam/aidial/cfg/model/ToolSetResource.java index 4f8590fd8..207ee60a5 100644 --- a/src/main/java/com/epam/aidial/cfg/model/ToolSetResource.java +++ b/src/main/java/com/epam/aidial/cfg/model/ToolSetResource.java @@ -21,10 +21,10 @@ public class ToolSetResource { private String author; private String name; private String endpoint; - private String displayName; + private LocalizedValue displayName; private String displayVersion; private String iconUrl; - private String description; + private LocalizedValue description; private String vendorWebsite; private List descriptionKeywords; private Integer maxRetryAttempts; @@ -36,7 +36,7 @@ public class ToolSetResource { private boolean forwardPerRequestKey; private boolean forwardAuthToken; private String url; - private String intro; + private LocalizedValue intro; private String catalogSchemaId; private Map catalogProperties; diff --git a/src/main/java/com/epam/aidial/cfg/model/databind/LocalizedValueDeserializer.java b/src/main/java/com/epam/aidial/cfg/model/databind/LocalizedValueDeserializer.java new file mode 100644 index 000000000..dee79c890 --- /dev/null +++ b/src/main/java/com/epam/aidial/cfg/model/databind/LocalizedValueDeserializer.java @@ -0,0 +1,38 @@ +package com.epam.aidial.cfg.model.databind; + +import com.epam.aidial.cfg.model.LocalizedValue; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonToken; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; + +import java.io.IOException; +import java.util.LinkedHashMap; +import java.util.Map; + +public class LocalizedValueDeserializer extends JsonDeserializer { + + @Override + public LocalizedValue deserialize(JsonParser p, DeserializationContext ctx) throws IOException { + if (p.getCurrentToken() == JsonToken.VALUE_STRING) { + return LocalizedValue.of(p.getValueAsString()); + } + + if (p.getCurrentToken() == JsonToken.START_OBJECT) { + Map localeMap = new LinkedHashMap<>(); + while (p.nextToken() != JsonToken.END_OBJECT) { + String locale = p.getCurrentName(); + p.nextToken(); + localeMap.put(locale, p.getValueAsString()); + } + return LocalizedValue.of(localeMap); + } + + if (p.getCurrentToken() == JsonToken.VALUE_NULL) { + return null; + } + + return ctx.reportInputMismatch(LocalizedValue.class, + "Expected a string or a locale-to-value object, got %s", p.getCurrentToken()); + } +} diff --git a/src/main/java/com/epam/aidial/cfg/model/databind/LocalizedValueSerializer.java b/src/main/java/com/epam/aidial/cfg/model/databind/LocalizedValueSerializer.java new file mode 100644 index 000000000..bfab8a004 --- /dev/null +++ b/src/main/java/com/epam/aidial/cfg/model/databind/LocalizedValueSerializer.java @@ -0,0 +1,30 @@ +package com.epam.aidial.cfg.model.databind; + +import com.epam.aidial.cfg.model.LocalizedValue; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; + +import java.io.IOException; + +public class LocalizedValueSerializer extends JsonSerializer { + + @Override + public void serialize(LocalizedValue value, JsonGenerator gen, SerializerProvider serializers) throws IOException { + if (!value.isMap()) { + gen.writeString(value.getPlainValue()); + return; + } + + if (value.getLocaleMap().size() == 1) { + gen.writeString(value.getLocaleMap().values().iterator().next()); + return; + } + + gen.writeStartObject(); + for (var entry : value.getLocaleMap().entrySet()) { + gen.writeStringField(entry.getKey(), entry.getValue()); + } + gen.writeEndObject(); + } +} diff --git a/src/main/java/com/epam/aidial/cfg/service/Assertions.java b/src/main/java/com/epam/aidial/cfg/service/Assertions.java deleted file mode 100644 index eaf9147cc..000000000 --- a/src/main/java/com/epam/aidial/cfg/service/Assertions.java +++ /dev/null @@ -1,64 +0,0 @@ -package com.epam.aidial.cfg.service; - -import com.epam.aidial.cfg.exception.ValidationException; -import com.epam.aidial.core.config.Deployment; -import org.apache.commons.lang3.StringUtils; - -import java.util.Map; -import java.util.Objects; - -public class Assertions { - - public static void assertUniqueDisplayName(Map deployments, String newDisplayName) { - if (StringUtils.isEmpty(newDisplayName)) { - return; - } - for (Map.Entry entry : deployments.entrySet()) { - String entityName = entry.getKey(); - Deployment deployment = entry.getValue(); - String currentValue = deployment.getDisplayName(); - if (StringUtils.isNotEmpty(currentValue) && Objects.equals(newDisplayName, currentValue)) { - throw new ValidationException("displayName is not unique. displayName '" + newDisplayName + "' already exists (" + entityName + ")"); - } - } - } - - public static void assertUniqueDisplayNameAndVersion(Map deployments, String newDisplayName, String newDisplayVersion) { - if (StringUtils.isEmpty(newDisplayName) && StringUtils.isEmpty(newDisplayVersion)) { - return; - } - - for (Map.Entry entry : deployments.entrySet()) { - String entityName = entry.getKey(); - Deployment deployment = entry.getValue(); - - String displayName = deployment.getDisplayName(); - String displayVersion = deployment.getDisplayVersion(); - - boolean isEqualDisplayNameAndDisplayVersion = isEqualDisplayNameAndDisplayVersion(displayName, displayVersion, newDisplayName, newDisplayVersion); - - if (StringUtils.isEmpty(newDisplayName) && isEqualDisplayNameAndDisplayVersion) { - throw new ValidationException("displayVersion is not unique. " - + "displayVersion '" + newDisplayVersion + "' already exists (" + entityName + "). " - + "Change or add displayName."); - } - if (StringUtils.isEmpty(newDisplayVersion) && isEqualDisplayNameAndDisplayVersion) { - throw new ValidationException("displayName is not unique. " - + "displayName '" + newDisplayName + "' already exists (" + entityName + "). " - + "Change or add displayVersion."); - } - if (isEqualDisplayNameAndDisplayVersion) { - throw new ValidationException("displayName and displayVersion are not unique. " - + "displayName '" + newDisplayName + "' and displayVersion '" + newDisplayVersion + "' already exists (" + entityName + ")"); - } - } - } - - private static boolean isEqualDisplayNameAndDisplayVersion(String displayName, - String displayVersion, - String newDisplayName, - String newDisplayVersion) { - return Objects.equals(displayName, newDisplayName) && Objects.equals(displayVersion, newDisplayVersion); - } - -} diff --git a/src/main/java/com/epam/aidial/cfg/service/config/transfer/exporter/AdapterExporter.java b/src/main/java/com/epam/aidial/cfg/service/config/transfer/exporter/AdapterExporter.java index 989ef8338..c3f2be43c 100644 --- a/src/main/java/com/epam/aidial/cfg/service/config/transfer/exporter/AdapterExporter.java +++ b/src/main/java/com/epam/aidial/cfg/service/config/transfer/exporter/AdapterExporter.java @@ -4,6 +4,7 @@ import com.epam.aidial.cfg.domain.model.Adapter; import com.epam.aidial.cfg.domain.model.ExportComponentInfo; import com.epam.aidial.cfg.domain.model.ExportConfigComponentType; +import com.epam.aidial.cfg.domain.model.LocalizedValue; import com.epam.aidial.cfg.domain.service.AdapterService; import com.epam.aidial.cfg.model.ExportConfigComponent; import com.epam.aidial.cfg.model.ExportRequest; @@ -66,8 +67,8 @@ protected Collection preview(ExportRequest request) { return getAdapters(request).values().stream() .map(component -> ExportComponentInfo.builder() .name(component.getName()) - .description(component.getDescription()) - .displayName(component.getDisplayName()) + .description(LocalizedValue.of(component.getDescription())) + .displayName(LocalizedValue.of(component.getDisplayName())) .type(ExportConfigComponentType.ADAPTER) .build()) .collect(Collectors.toList()); diff --git a/src/main/java/com/epam/aidial/cfg/service/config/transfer/exporter/ApplicationTypeSchemaExporter.java b/src/main/java/com/epam/aidial/cfg/service/config/transfer/exporter/ApplicationTypeSchemaExporter.java index cfad9855f..c9aa6a49f 100644 --- a/src/main/java/com/epam/aidial/cfg/service/config/transfer/exporter/ApplicationTypeSchemaExporter.java +++ b/src/main/java/com/epam/aidial/cfg/service/config/transfer/exporter/ApplicationTypeSchemaExporter.java @@ -4,6 +4,7 @@ import com.epam.aidial.cfg.domain.model.ApplicationTypeSchema; import com.epam.aidial.cfg.domain.model.ExportApplicationTypeSchemaInfo; import com.epam.aidial.cfg.domain.model.ExportConfigComponentType; +import com.epam.aidial.cfg.domain.model.LocalizedValue; import com.epam.aidial.cfg.domain.service.ApplicationTypeSchemaService; import com.epam.aidial.cfg.model.ExportConfigComponent; import com.epam.aidial.cfg.model.ExportRequest; @@ -77,8 +78,8 @@ protected Collection preview(ExportRequest requ return getApplicationTypeSchemas(request).values().stream() .map(component -> ExportApplicationTypeSchemaInfo.builder() .id(component.getSchemaId()) - .displayName(component.getApplicationTypeDisplayName()) - .description(component.getDescription()) + .displayName(LocalizedValue.of(component.getApplicationTypeDisplayName())) + .description(LocalizedValue.of(component.getDescription())) .type(ExportConfigComponentType.APPLICATION_TYPE_SCHEMA) .build()) .collect(Collectors.toList()); diff --git a/src/main/java/com/epam/aidial/cfg/service/config/transfer/exporter/InterceptorRunnerExporter.java b/src/main/java/com/epam/aidial/cfg/service/config/transfer/exporter/InterceptorRunnerExporter.java index 770f6beb5..cc4a53179 100644 --- a/src/main/java/com/epam/aidial/cfg/service/config/transfer/exporter/InterceptorRunnerExporter.java +++ b/src/main/java/com/epam/aidial/cfg/service/config/transfer/exporter/InterceptorRunnerExporter.java @@ -4,6 +4,7 @@ import com.epam.aidial.cfg.domain.model.ExportComponentInfo; import com.epam.aidial.cfg.domain.model.ExportConfigComponentType; import com.epam.aidial.cfg.domain.model.InterceptorRunner; +import com.epam.aidial.cfg.domain.model.LocalizedValue; import com.epam.aidial.cfg.domain.service.InterceptorRunnerService; import com.epam.aidial.cfg.model.ExportConfigComponent; import com.epam.aidial.cfg.model.ExportRequest; @@ -66,8 +67,8 @@ protected Collection preview(ExportRequest request) { return getInterceptorRunners(request).values().stream() .map(component -> ExportComponentInfo.builder() .name(component.getName()) - .displayName(component.getDisplayName()) - .description(component.getDescription()) + .displayName(LocalizedValue.of(component.getDisplayName())) + .description(LocalizedValue.of(component.getDescription())) .type(ExportConfigComponentType.INTERCEPTOR_RUNNER) .build()) .collect(Collectors.toList()); diff --git a/src/main/java/com/epam/aidial/cfg/service/config/transfer/exporter/KeyExporter.java b/src/main/java/com/epam/aidial/cfg/service/config/transfer/exporter/KeyExporter.java index 2e762d233..5526be9cc 100644 --- a/src/main/java/com/epam/aidial/cfg/service/config/transfer/exporter/KeyExporter.java +++ b/src/main/java/com/epam/aidial/cfg/service/config/transfer/exporter/KeyExporter.java @@ -4,6 +4,7 @@ import com.epam.aidial.cfg.domain.model.ExportConfigComponentType; import com.epam.aidial.cfg.domain.model.ExportKeyInfo; import com.epam.aidial.cfg.domain.model.Key; +import com.epam.aidial.cfg.domain.model.LocalizedValue; import com.epam.aidial.cfg.domain.service.KeyService; import com.epam.aidial.cfg.model.ExportRequest; import com.epam.aidial.cfg.model.FullExportRequest; @@ -81,8 +82,8 @@ protected Collection preview(ExportRequest request) { .expiresAt(component.getExpiresAt()) .keyGeneratedAt(component.getKeyGeneratedAt()) .name(component.getName()) - .displayName(component.getDisplayName()) - .description(component.getDescription()) + .displayName(LocalizedValue.of(component.getDisplayName())) + .description(LocalizedValue.of(component.getDescription())) .type(ExportConfigComponentType.KEY) .build()) .collect(Collectors.toList()); diff --git a/src/main/java/com/epam/aidial/cfg/service/config/transfer/exporter/RoleExporter.java b/src/main/java/com/epam/aidial/cfg/service/config/transfer/exporter/RoleExporter.java index 64d84dc8c..71403c9c9 100644 --- a/src/main/java/com/epam/aidial/cfg/service/config/transfer/exporter/RoleExporter.java +++ b/src/main/java/com/epam/aidial/cfg/service/config/transfer/exporter/RoleExporter.java @@ -3,6 +3,7 @@ import com.epam.aidial.cfg.configuration.logging.LogExecution; import com.epam.aidial.cfg.domain.model.ExportComponentInfo; import com.epam.aidial.cfg.domain.model.ExportConfigComponentType; +import com.epam.aidial.cfg.domain.model.LocalizedValue; import com.epam.aidial.cfg.domain.model.Role; import com.epam.aidial.cfg.domain.model.RoleLimit; import com.epam.aidial.cfg.domain.service.RoleService; @@ -68,8 +69,8 @@ protected Collection preview(ExportRequest request) { return getRoles(request, Set.of()).values().stream() .map(component -> ExportComponentInfo.builder() .name(component.getName()) - .displayName(component.getDisplayName()) - .description(component.getDescription()) + .displayName(LocalizedValue.of(component.getDisplayName())) + .description(LocalizedValue.of(component.getDescription())) .type(ExportConfigComponentType.ROLE) .build()) .collect(Collectors.toList()); diff --git a/src/main/java/com/epam/aidial/cfg/service/config/transfer/exporter/RouteExporter.java b/src/main/java/com/epam/aidial/cfg/service/config/transfer/exporter/RouteExporter.java index dbaef8586..524d11cfd 100644 --- a/src/main/java/com/epam/aidial/cfg/service/config/transfer/exporter/RouteExporter.java +++ b/src/main/java/com/epam/aidial/cfg/service/config/transfer/exporter/RouteExporter.java @@ -4,6 +4,7 @@ import com.epam.aidial.cfg.domain.model.ExportComponentInfo; import com.epam.aidial.cfg.domain.model.ExportConfigComponentType; import com.epam.aidial.cfg.domain.model.ExportFormat; +import com.epam.aidial.cfg.domain.model.LocalizedValue; import com.epam.aidial.cfg.domain.model.Upstream; import com.epam.aidial.cfg.domain.model.route.Route; import com.epam.aidial.cfg.domain.service.RouteService; @@ -70,8 +71,8 @@ protected Collection preview(ExportRequest request) { return getRoutes(request).values().stream() .map(component -> ExportComponentInfo.builder() .name(component.getDeployment().getName()) - .displayName(component.getDisplayName()) - .description(component.getDescription()) + .displayName(LocalizedValue.of(component.getDisplayName())) + .description(LocalizedValue.of(component.getDescription())) .type(ExportConfigComponentType.ROUTE) .build()) .collect(Collectors.toList()); diff --git a/src/main/java/com/epam/aidial/cfg/service/config/transfer/importer/compatibility/backward/transformer/ApplicationToLatestVersionTransformer.java b/src/main/java/com/epam/aidial/cfg/service/config/transfer/importer/compatibility/backward/transformer/ApplicationToLatestVersionTransformer.java index c271b9c16..0dcb92008 100644 --- a/src/main/java/com/epam/aidial/cfg/service/config/transfer/importer/compatibility/backward/transformer/ApplicationToLatestVersionTransformer.java +++ b/src/main/java/com/epam/aidial/cfg/service/config/transfer/importer/compatibility/backward/transformer/ApplicationToLatestVersionTransformer.java @@ -2,10 +2,10 @@ import com.epam.aidial.cfg.configuration.logging.LogExecution; import com.epam.aidial.cfg.domain.model.Application; +import com.epam.aidial.cfg.domain.model.LocalizedValue; import com.epam.aidial.cfg.domain.model.source.ApplicationEndpointsSource; import com.epam.aidial.cfg.domain.model.source.ApplicationSchemaSource; import org.apache.commons.collections4.MapUtils; -import org.apache.commons.lang3.StringUtils; import org.springframework.stereotype.Component; import java.util.Map; @@ -20,8 +20,8 @@ public void transform(Map applications) { } private void transform(Application application) { - if (StringUtils.isBlank(application.getDisplayName())) { - application.setDisplayName(application.getDeployment().getName()); + if (application.getDisplayName() == null) { + application.setDisplayName(LocalizedValue.of(application.getDeployment().getName())); } if (application.getSource() == null) { if (application.getApplicationTypeSchemaId() != null) { diff --git a/src/main/java/com/epam/aidial/cfg/service/config/transfer/importer/compatibility/backward/transformer/InterceptorToLatestVersionTransformer.java b/src/main/java/com/epam/aidial/cfg/service/config/transfer/importer/compatibility/backward/transformer/InterceptorToLatestVersionTransformer.java index d490630f2..a82dec80b 100644 --- a/src/main/java/com/epam/aidial/cfg/service/config/transfer/importer/compatibility/backward/transformer/InterceptorToLatestVersionTransformer.java +++ b/src/main/java/com/epam/aidial/cfg/service/config/transfer/importer/compatibility/backward/transformer/InterceptorToLatestVersionTransformer.java @@ -2,8 +2,8 @@ import com.epam.aidial.cfg.configuration.logging.LogExecution; import com.epam.aidial.cfg.domain.model.Interceptor; +import com.epam.aidial.cfg.domain.model.LocalizedValue; import org.apache.commons.collections4.MapUtils; -import org.apache.commons.lang3.StringUtils; import org.springframework.stereotype.Component; import java.util.Map; @@ -18,8 +18,8 @@ public void transform(Map interceptors) { } private void transform(Interceptor interceptor) { - if (StringUtils.isBlank(interceptor.getDisplayName())) { - interceptor.setDisplayName(interceptor.getName()); + if (interceptor.getDisplayName() == null) { + interceptor.setDisplayName(LocalizedValue.of(interceptor.getName())); } } } diff --git a/src/main/java/com/epam/aidial/cfg/service/config/transfer/importer/compatibility/backward/transformer/ModelToLatestVersionTransformer.java b/src/main/java/com/epam/aidial/cfg/service/config/transfer/importer/compatibility/backward/transformer/ModelToLatestVersionTransformer.java index 08138e08b..f46f5b4c3 100644 --- a/src/main/java/com/epam/aidial/cfg/service/config/transfer/importer/compatibility/backward/transformer/ModelToLatestVersionTransformer.java +++ b/src/main/java/com/epam/aidial/cfg/service/config/transfer/importer/compatibility/backward/transformer/ModelToLatestVersionTransformer.java @@ -1,9 +1,9 @@ package com.epam.aidial.cfg.service.config.transfer.importer.compatibility.backward.transformer; import com.epam.aidial.cfg.configuration.logging.LogExecution; +import com.epam.aidial.cfg.domain.model.LocalizedValue; import com.epam.aidial.cfg.domain.model.Model; import org.apache.commons.collections4.MapUtils; -import org.apache.commons.lang3.StringUtils; import org.springframework.stereotype.Component; import java.util.Map; @@ -18,8 +18,8 @@ public void transform(Map models) { } private void transform(Model model) { - if (StringUtils.isBlank(model.getDisplayName())) { - model.setDisplayName(model.getDeployment().getName()); + if (model.getDisplayName() == null) { + model.setDisplayName(LocalizedValue.of(model.getDeployment().getName())); } } } diff --git a/src/main/java/com/epam/aidial/cfg/service/config/transfer/importer/compatibility/backward/transformer/ToolSetToLatestVersionTransformer.java b/src/main/java/com/epam/aidial/cfg/service/config/transfer/importer/compatibility/backward/transformer/ToolSetToLatestVersionTransformer.java index 4eaffb1fa..056222fb8 100644 --- a/src/main/java/com/epam/aidial/cfg/service/config/transfer/importer/compatibility/backward/transformer/ToolSetToLatestVersionTransformer.java +++ b/src/main/java/com/epam/aidial/cfg/service/config/transfer/importer/compatibility/backward/transformer/ToolSetToLatestVersionTransformer.java @@ -1,9 +1,9 @@ package com.epam.aidial.cfg.service.config.transfer.importer.compatibility.backward.transformer; import com.epam.aidial.cfg.configuration.logging.LogExecution; +import com.epam.aidial.cfg.domain.model.LocalizedValue; import com.epam.aidial.cfg.domain.model.ToolSet; import org.apache.commons.collections4.MapUtils; -import org.apache.commons.lang3.StringUtils; import org.springframework.stereotype.Component; import java.util.Map; @@ -18,8 +18,8 @@ public void transform(Map toolSets) { } private void transform(ToolSet toolSet) { - if (StringUtils.isBlank(toolSet.getDisplayName())) { - toolSet.setDisplayName(toolSet.getDeployment().getName()); + if (toolSet.getDisplayName() == null) { + toolSet.setDisplayName(LocalizedValue.of(toolSet.getDeployment().getName())); } } } diff --git a/src/main/java/com/epam/aidial/cfg/web/facade/mapper/AddonDtoMapper.java b/src/main/java/com/epam/aidial/cfg/web/facade/mapper/AddonDtoMapper.java index efc9cdc9e..5d5822a65 100644 --- a/src/main/java/com/epam/aidial/cfg/web/facade/mapper/AddonDtoMapper.java +++ b/src/main/java/com/epam/aidial/cfg/web/facade/mapper/AddonDtoMapper.java @@ -5,7 +5,7 @@ import org.mapstruct.Mapper; import org.mapstruct.Mapping; -@Mapper(componentModel = "spring", uses = {LimitDtoMapper.class, RoleBasedDtoMapper.class, InstantMapper.class}) +@Mapper(componentModel = "spring", uses = {LimitDtoMapper.class, RoleBasedDtoMapper.class, InstantMapper.class, LocalizedValueDtoMapper.class}) public interface AddonDtoMapper { @RoleBasedDtoMapper.ToDomain diff --git a/src/main/java/com/epam/aidial/cfg/web/facade/mapper/ApplicationDtoMapper.java b/src/main/java/com/epam/aidial/cfg/web/facade/mapper/ApplicationDtoMapper.java index 6be2fb479..baf8cd0b7 100644 --- a/src/main/java/com/epam/aidial/cfg/web/facade/mapper/ApplicationDtoMapper.java +++ b/src/main/java/com/epam/aidial/cfg/web/facade/mapper/ApplicationDtoMapper.java @@ -9,7 +9,7 @@ @Mapper(componentModel = "spring", uses = { LimitDtoMapper.class, RoleBasedDtoMapper.class, InstantMapper.class, FeaturesDtoMapper.class, RouteDtoMapper.class, ValidityStateDtoMapper.class, ApplicationSourceDtoMapper.class, - DeploymentInterfaceDtoMapper.class, ResourceAuthSettingsDtoMapper.class + DeploymentInterfaceDtoMapper.class, ResourceAuthSettingsDtoMapper.class, LocalizedValueDtoMapper.class }) public interface ApplicationDtoMapper { diff --git a/src/main/java/com/epam/aidial/cfg/web/facade/mapper/AssistantDtoMapper.java b/src/main/java/com/epam/aidial/cfg/web/facade/mapper/AssistantDtoMapper.java index 86a440f95..d90520133 100644 --- a/src/main/java/com/epam/aidial/cfg/web/facade/mapper/AssistantDtoMapper.java +++ b/src/main/java/com/epam/aidial/cfg/web/facade/mapper/AssistantDtoMapper.java @@ -5,7 +5,7 @@ import org.mapstruct.Mapper; import org.mapstruct.Mapping; -@Mapper(componentModel = "spring", uses = {LimitDtoMapper.class, RoleBasedDtoMapper.class, InstantMapper.class}) +@Mapper(componentModel = "spring", uses = {LimitDtoMapper.class, RoleBasedDtoMapper.class, InstantMapper.class, LocalizedValueDtoMapper.class}) public interface AssistantDtoMapper { @RoleBasedDtoMapper.ToDomain diff --git a/src/main/java/com/epam/aidial/cfg/web/facade/mapper/InterceptorDtoMapper.java b/src/main/java/com/epam/aidial/cfg/web/facade/mapper/InterceptorDtoMapper.java index 5a238d9f4..b416d3fc8 100644 --- a/src/main/java/com/epam/aidial/cfg/web/facade/mapper/InterceptorDtoMapper.java +++ b/src/main/java/com/epam/aidial/cfg/web/facade/mapper/InterceptorDtoMapper.java @@ -7,7 +7,7 @@ @Mapper(componentModel = "spring", uses = { InstantMapper.class, InterceptorSourceDtoMapper.class, FeaturesDtoMapper.class, - DeploymentInterfaceDtoMapper.class + DeploymentInterfaceDtoMapper.class, LocalizedValueDtoMapper.class }) public interface InterceptorDtoMapper { diff --git a/src/main/java/com/epam/aidial/cfg/web/facade/mapper/LocalizedValueDtoMapper.java b/src/main/java/com/epam/aidial/cfg/web/facade/mapper/LocalizedValueDtoMapper.java new file mode 100644 index 000000000..60b3ff955 --- /dev/null +++ b/src/main/java/com/epam/aidial/cfg/web/facade/mapper/LocalizedValueDtoMapper.java @@ -0,0 +1,23 @@ +package com.epam.aidial.cfg.web.facade.mapper; + +import com.epam.aidial.cfg.domain.model.LocalizedValue; +import com.epam.aidial.cfg.dto.LocalizedValueDto; +import org.springframework.stereotype.Component; + +@Component +public class LocalizedValueDtoMapper { + + public LocalizedValueDto toDto(LocalizedValue domain) { + if (domain == null) { + return null; + } + return domain.isPlain() ? LocalizedValueDto.of(domain.getPlainValue()) : LocalizedValueDto.of(domain.getLocaleMap()); + } + + public LocalizedValue toDomain(LocalizedValueDto dto) { + if (dto == null) { + return null; + } + return dto.isPlain() ? LocalizedValue.of(dto.getPlainValue()) : LocalizedValue.of(dto.getLocaleMap()); + } +} diff --git a/src/main/java/com/epam/aidial/cfg/web/facade/mapper/ModelDtoMapper.java b/src/main/java/com/epam/aidial/cfg/web/facade/mapper/ModelDtoMapper.java index 304e3ab58..f0a2cfb6f 100644 --- a/src/main/java/com/epam/aidial/cfg/web/facade/mapper/ModelDtoMapper.java +++ b/src/main/java/com/epam/aidial/cfg/web/facade/mapper/ModelDtoMapper.java @@ -10,7 +10,7 @@ uses = { LimitDtoMapper.class, UpstreamDtoMapper.class, RoleBasedDtoMapper.class, InstantMapper.class, FeaturesDtoMapper.class, ModelSourceDtoMapper.class, - DeploymentInterfaceDtoMapper.class + DeploymentInterfaceDtoMapper.class, LocalizedValueDtoMapper.class } ) public interface ModelDtoMapper { diff --git a/src/main/java/com/epam/aidial/cfg/web/facade/mapper/ToolSetDtoMapper.java b/src/main/java/com/epam/aidial/cfg/web/facade/mapper/ToolSetDtoMapper.java index a6acaf38d..e8c498541 100644 --- a/src/main/java/com/epam/aidial/cfg/web/facade/mapper/ToolSetDtoMapper.java +++ b/src/main/java/com/epam/aidial/cfg/web/facade/mapper/ToolSetDtoMapper.java @@ -12,7 +12,7 @@ @Mapper(componentModel = "spring", uses = { LimitDtoMapper.class, RoleBasedDtoMapper.class, InstantMapper.class, - ResourceAuthSettingsDtoMapper.class, ToolSetSourceDtoMapper.class + ResourceAuthSettingsDtoMapper.class, ToolSetSourceDtoMapper.class, LocalizedValueDtoMapper.class }) public abstract class ToolSetDtoMapper { diff --git a/src/main/java/com/epam/aidial/core/config/CoreLocalizedValue.java b/src/main/java/com/epam/aidial/core/config/CoreLocalizedValue.java new file mode 100644 index 000000000..d29af7aff --- /dev/null +++ b/src/main/java/com/epam/aidial/core/config/CoreLocalizedValue.java @@ -0,0 +1,47 @@ +package com.epam.aidial.core.config; + +import com.epam.aidial.core.config.databind.CoreLocalizedValueDeserializer; +import com.epam.aidial.core.config.databind.CoreLocalizedValueSerializer; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import lombok.EqualsAndHashCode; +import lombok.Getter; + +import java.util.LinkedHashMap; +import java.util.Map; + +@Getter +@JsonDeserialize(using = CoreLocalizedValueDeserializer.class) +@JsonSerialize(using = CoreLocalizedValueSerializer.class) +@EqualsAndHashCode +public final class CoreLocalizedValue { + + private final String plainValue; + private final Map localeMap; + + private CoreLocalizedValue(String plainValue, Map localeMap) { + this.plainValue = plainValue; + this.localeMap = localeMap; + } + + public static CoreLocalizedValue of(String value) { + return value == null ? null : new CoreLocalizedValue(value, null); + } + + public static CoreLocalizedValue of(Map localeMap) { + return localeMap == null ? null : new CoreLocalizedValue(null, new LinkedHashMap<>(localeMap)); + } + + public boolean isMap() { + return localeMap != null; + } + + public boolean isPlain() { + return plainValue != null; + } + + @Override + public String toString() { + return plainValue != null ? plainValue : String.valueOf(localeMap); + } +} diff --git a/src/main/java/com/epam/aidial/core/config/Deployment.java b/src/main/java/com/epam/aidial/core/config/Deployment.java index 6448cf89a..6c9928dad 100644 --- a/src/main/java/com/epam/aidial/core/config/Deployment.java +++ b/src/main/java/com/epam/aidial/core/config/Deployment.java @@ -18,14 +18,14 @@ public abstract class Deployment extends RoleBasedEntity { @JsonAlias({"responsesEndpoint", "responses_endpoint"}) private String responsesEndpoint; //0.42.0 @JsonAlias({"displayName", "display_name"}) - private String displayName; + private CoreLocalizedValue displayName; //0.47.0 @JsonAlias({"displayVersion", "display_version"}) private String displayVersion; @JsonAlias({"iconUrl", "icon_url"}) private String iconUrl; - private String description; + private CoreLocalizedValue description; //0.47.0 private String reference; - private String intro; //0.46.0 + private CoreLocalizedValue intro; //0.47.0 /** * Forward Http header with authorization token when request is sent to deployment. * Authorization token is NOT forwarded by default. diff --git a/src/main/java/com/epam/aidial/core/config/databind/CoreLocalizedValueDeserializer.java b/src/main/java/com/epam/aidial/core/config/databind/CoreLocalizedValueDeserializer.java new file mode 100644 index 000000000..5f89b1379 --- /dev/null +++ b/src/main/java/com/epam/aidial/core/config/databind/CoreLocalizedValueDeserializer.java @@ -0,0 +1,38 @@ +package com.epam.aidial.core.config.databind; + +import com.epam.aidial.core.config.CoreLocalizedValue; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonToken; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; + +import java.io.IOException; +import java.util.LinkedHashMap; +import java.util.Map; + +public class CoreLocalizedValueDeserializer extends JsonDeserializer { + + @Override + public CoreLocalizedValue deserialize(JsonParser p, DeserializationContext ctx) throws IOException { + if (p.getCurrentToken() == JsonToken.VALUE_STRING) { + return CoreLocalizedValue.of(p.getValueAsString()); + } + + if (p.getCurrentToken() == JsonToken.START_OBJECT) { + Map localeMap = new LinkedHashMap<>(); + while (p.nextToken() != JsonToken.END_OBJECT) { + String locale = p.getCurrentName(); + p.nextToken(); + localeMap.put(locale, p.getValueAsString()); + } + return CoreLocalizedValue.of(localeMap); + } + + if (p.getCurrentToken() == JsonToken.VALUE_NULL) { + return null; + } + + return ctx.reportInputMismatch(CoreLocalizedValue.class, + "Expected a string or a locale-to-value object, got %s", p.getCurrentToken()); + } +} diff --git a/src/main/java/com/epam/aidial/core/config/databind/CoreLocalizedValueSerializer.java b/src/main/java/com/epam/aidial/core/config/databind/CoreLocalizedValueSerializer.java new file mode 100644 index 000000000..37de9493f --- /dev/null +++ b/src/main/java/com/epam/aidial/core/config/databind/CoreLocalizedValueSerializer.java @@ -0,0 +1,30 @@ +package com.epam.aidial.core.config.databind; + +import com.epam.aidial.core.config.CoreLocalizedValue; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; + +import java.io.IOException; + +public class CoreLocalizedValueSerializer extends JsonSerializer { + + @Override + public void serialize(CoreLocalizedValue value, JsonGenerator gen, SerializerProvider serializers) throws IOException { + if (!value.isMap()) { + gen.writeString(value.getPlainValue()); + return; + } + + if (value.getLocaleMap().size() == 1) { + gen.writeString(value.getLocaleMap().values().iterator().next()); + return; + } + + gen.writeStartObject(); + for (var entry : value.getLocaleMap().entrySet()) { + gen.writeStringField(entry.getKey(), entry.getValue()); + } + gen.writeEndObject(); + } +} diff --git a/src/main/resources/core-config-schemas/properties-tracker.md b/src/main/resources/core-config-schemas/properties-tracker.md index dc12e6204..d05c5e8f3 100644 --- a/src/main/resources/core-config-schemas/properties-tracker.md +++ b/src/main/resources/core-config-schemas/properties-tracker.md @@ -23,5 +23,5 @@ This document tracks new properties added to the Core Config schema across diffe | 0.44.0 | - `tokenEndpointAuthMethod` in ResourceAuthSettings, `id` in Upstream | | 0.45.0 | - `maxTokensSupported`, `maxCompletionTokensSupported`,`customTemperatureSupported`,`reasoningEfforts` in Features
- `secretExtraData` in Upstream
- `embeddingDimensions` in Model
- `provider` in ToolSet | | 0.46.0 | - `interfaces` in Model (keys: `openaiChatCompletions`, `openaiResponses`, `anthropicMessages`), Application and Interceptor (key: `openaiChatCompletions` only); each value is an object with required `base_url`
- `externalServices` in Application (map of `displayName`, `description`, `authSettings`), appIdentity and allowUserExternalServices in Application, dynamicallyRegistered in ResourceAuthSettings, intro in Model, Application, ToolSet, vendorWebsite in ToolSet, ToolSetData | -| 0.47.0 | - `catalogSchemaId` in Deployment (String reference to a catalog schema)
- `catalogProperties` in Deployment (curated marketplace/catalog display metadata, validated against catalogSchemaId)
- `overrideName` moved from Model to Deployment (now also applies to Application and Interceptor)
- `cacheRead` in Pricing (cache-read token pricing)
- `cacheWrite` in Pricing (cache-write token pricing) | +| 0.47.0 | - `catalogSchemaId` in Deployment (String reference to a catalog schema)
- `catalogProperties` in Deployment (curated marketplace/catalog display metadata, validated against catalogSchemaId)
- `overrideName` moved from Model to Deployment (now also applies to Application and Interceptor)
- `cacheRead` in Pricing (cache-read token pricing)
- `cacheWrite` in Pricing (cache-write token pricing)
- **Type change**: `displayName`, `description`, `intro` in Deployment changed to `LocalizedValue` type (stores either plain strings or JSON locale maps: `"GPT-4"` or `{"en":"GPT-4","fr":"GPT-4"}`) | diff --git a/src/main/resources/core-config-schemas/schema-v0.47.0.json b/src/main/resources/core-config-schemas/schema-v0.47.0.json index 3257c8533..109b77eb7 100644 --- a/src/main/resources/core-config-schemas/schema-v0.47.0.json +++ b/src/main/resources/core-config-schemas/schema-v0.47.0.json @@ -111,6 +111,25 @@ } } }, + "LocalizedValue": { + "oneOf": [ + { + "type": "string", + "description": "Plain string value (backward compatible)" + }, + { + "type": "object", + "patternProperties": { + "^[a-z]{2}(-[A-Z]{2})?$": { + "type": "string" + } + }, + "additionalProperties": false, + "minProperties": 1, + "description": "Map of locale codes to localized strings (e.g. {\"en\": \"GPT-4\", \"fr\": \"GPT-4\"})" + } + ] + }, "Deployment": { "type": "object", "allOf": [ @@ -132,10 +151,10 @@ } }, "displayName": { - "type": "string" + "$ref": "#/definitions/LocalizedValue" }, "displayVersion": { - "type": "string" + "$ref": "#/definitions/LocalizedValue" }, "iconUrl": { "type": "string" @@ -144,7 +163,7 @@ "type": "string" }, "intro": { - "type": "string" + "$ref": "#/definitions/LocalizedValue" }, "reference": { "type": "string" diff --git a/src/test/java/com/epam/aidial/cfg/dao/listener/validitystate/resolver/ApplicationValidityStateResolverTest.java b/src/test/java/com/epam/aidial/cfg/dao/listener/validitystate/resolver/ApplicationValidityStateResolverTest.java index 4470b420b..a7fa5e419 100644 --- a/src/test/java/com/epam/aidial/cfg/dao/listener/validitystate/resolver/ApplicationValidityStateResolverTest.java +++ b/src/test/java/com/epam/aidial/cfg/dao/listener/validitystate/resolver/ApplicationValidityStateResolverTest.java @@ -11,6 +11,7 @@ import com.epam.aidial.cfg.dao.mapper.ExternalServicesEntityMapperImpl; import com.epam.aidial.cfg.dao.mapper.FeaturesEntityMapperImpl; import com.epam.aidial.cfg.dao.mapper.LimitEntityMapperImpl; +import com.epam.aidial.cfg.dao.mapper.LocalizedValueEntityMapper; import com.epam.aidial.cfg.dao.mapper.MapPropertiesMapperImpl; import com.epam.aidial.cfg.dao.mapper.PropertiesEntityMapperImpl; import com.epam.aidial.cfg.dao.mapper.ResourceAuthSettingsEntityMapperImpl; @@ -58,6 +59,7 @@ PropertiesEntityMapperImpl.class, ApplicationTypeSchemaCoreMapperImpl.class, ApplicationTypeSchemaRouteCoreMapperImpl.class, + LocalizedValueEntityMapper.class, UriMapperImpl.class }) class ApplicationValidityStateResolverTest { diff --git a/src/test/java/com/epam/aidial/cfg/domain/validator/ApplicationValidatorTest.java b/src/test/java/com/epam/aidial/cfg/domain/validator/ApplicationValidatorTest.java index 7df25ab00..f4f50f8e3 100644 --- a/src/test/java/com/epam/aidial/cfg/domain/validator/ApplicationValidatorTest.java +++ b/src/test/java/com/epam/aidial/cfg/domain/validator/ApplicationValidatorTest.java @@ -5,6 +5,7 @@ import com.epam.aidial.cfg.domain.model.Deployment; import com.epam.aidial.cfg.domain.model.DeploymentInterface; import com.epam.aidial.cfg.domain.model.ExternalService; +import com.epam.aidial.cfg.domain.model.LocalizedValue; import com.epam.aidial.cfg.domain.model.Mcp; import com.epam.aidial.cfg.domain.model.ResourceAuthSettings; import com.epam.aidial.cfg.domain.model.source.ApplicationEndpointsSource; @@ -50,7 +51,7 @@ void setUp() { void validateCreation_shouldDelegateToDisplayFieldsValidator() { // given Application application = new Application(); - application.setDisplayName("display name"); + application.setDisplayName(LocalizedValue.of("display name")); application.setDisplayVersion("1.0"); application.setEndpoint("test"); application.setSource(new ApplicationEndpointsSource()); @@ -61,7 +62,7 @@ void validateCreation_shouldDelegateToDisplayFieldsValidator() { applicationValidator.validateCreation(application); // then - verify(displayFieldsValidator).validateDisplayNameDisplayVersion("display name", "1.0", "Application", "text"); + verify(displayFieldsValidator).validateDisplayNameDisplayVersion(LocalizedValue.of("display name"), "1.0", "Application", "text"); } @ParameterizedTest @@ -69,7 +70,7 @@ void validateCreation_shouldDelegateToDisplayFieldsValidator() { void validateCreation_shouldThrowExceptionWhenEndpointIsNotNullButBlank(String endpoint) { // given Application application = new Application(); - application.setDisplayName("display name"); + application.setDisplayName(LocalizedValue.of("display name")); application.setDisplayVersion("1.0"); application.setEndpoint(endpoint); application.setSource(new ApplicationEndpointsSource()); @@ -81,14 +82,15 @@ void validateCreation_shouldThrowExceptionWhenEndpointIsNotNullButBlank(String e .isInstanceOf(IllegalArgumentException.class) .hasMessage("Invalid endpoint: '" + endpoint + "'. Application: text"); - verify(displayFieldsValidator).validateDisplayNameDisplayVersion("display name", "1.0", "Application", "text"); + verify(displayFieldsValidator).validateDisplayNameDisplayVersion( + application.getDisplayName(), application.getDisplayVersion(), "Application", "text"); } @Test void validateCreation_shouldThrowExceptionWhenEndpointsSourceWithNoEndpoints() { // given Application application = new Application(); - application.setDisplayName("display name"); + application.setDisplayName(LocalizedValue.of("display name")); application.setDisplayVersion("1.0"); application.setSource(new ApplicationEndpointsSource()); @@ -100,13 +102,14 @@ void validateCreation_shouldThrowExceptionWhenEndpointsSourceWithNoEndpoints() { .isInstanceOf(IllegalArgumentException.class) .hasMessage("At least application endpoint, MCP endpoint or interfaces must be provided. Application: deploymentName"); - verify(displayFieldsValidator).validateDisplayNameDisplayVersion("display name", "1.0", "Application", "deploymentName"); + verify(displayFieldsValidator).validateDisplayNameDisplayVersion( + application.getDisplayName(), application.getDisplayVersion(), "Application", "deploymentName"); } @Test void validateCreation_shouldNotThrowWhenEndpointsSourceWithOnlyInterfaces() { Application application = new Application(); - application.setDisplayName("display name"); + application.setDisplayName(LocalizedValue.of("display name")); application.setDisplayVersion("1.0"); application.setSource(new ApplicationEndpointsSource()); application.setInterfaces(interfaces("openaiChatCompletions", "http://app.adapter.test.com")); @@ -120,7 +123,7 @@ void validateCreation_shouldNotThrowWhenEndpointsSourceWithOnlyInterfaces() { @Test void validateCreation_shouldThrowWhenApplicationHasUnsupportedInterfaceType() { Application application = new Application(); - application.setDisplayName("display name"); + application.setDisplayName(LocalizedValue.of("display name")); application.setDisplayVersion("1.0"); application.setSource(new ApplicationEndpointsSource()); application.setInterfaces(interfaces("anthropicMessages", "http://app.adapter.test.com")); @@ -136,7 +139,7 @@ void validateCreation_shouldThrowWhenApplicationHasUnsupportedInterfaceType() { @Test void validateCreation_shouldThrowExceptionWhenSchemaSourceWithInterfacesSet() { Application application = new Application(); - application.setDisplayName("text"); + application.setDisplayName(LocalizedValue.of("text")); application.setDisplayVersion("1.0"); application.setInterfaces(interfaces("openaiChatCompletions", "http://app.adapter.test.com")); application.setSource(new ApplicationSchemaSource(URI.create("https://test.com"))); @@ -157,7 +160,7 @@ private static Map interfaces(String type, String b @Test void validateCreation_shouldThrowExceptionWhenSchemaSourceWithNullSchemaId() { Application application = new Application(); - application.setDisplayName("text"); + application.setDisplayName(LocalizedValue.of("text")); application.setDisplayVersion("1.0"); application.setSource(new ApplicationSchemaSource(null)); Deployment deployment = new Deployment("deploymentName"); @@ -172,7 +175,7 @@ void validateCreation_shouldThrowExceptionWhenSchemaSourceWithNullSchemaId() { void validateCreation_shouldThrowExceptionWhenSchemaSourceWithEndpointSet() { // given Application application = new Application(); - application.setDisplayName("text"); + application.setDisplayName(LocalizedValue.of("text")); application.setDisplayVersion("1.0"); application.setEndpoint("test"); application.setSource(new ApplicationSchemaSource(URI.create("https://test.com"))); @@ -188,7 +191,7 @@ void validateCreation_shouldThrowExceptionWhenSchemaSourceWithEndpointSet() { @Test void validateCreation_shouldThrowExceptionWhenSchemaSourceWithMcpProvided() { Application application = new Application(); - application.setDisplayName("text"); + application.setDisplayName(LocalizedValue.of("text")); application.setDisplayVersion("1.0"); application.setSource(new ApplicationSchemaSource(URI.create("https://test.com"))); @@ -207,7 +210,7 @@ void validateCreation_shouldThrowExceptionWhenSchemaSourceWithMcpProvided() { @Test void validateCreation_shouldThrowExceptionWhenSourceIsNull() { Application application = new Application(); - application.setDisplayName("text"); + application.setDisplayName(LocalizedValue.of("text")); application.setDisplayVersion("1.0"); application.setEndpoint("test"); @@ -227,7 +230,7 @@ void validateUpdate_shouldDelegateToDisplayFieldsAndDeploymentValidators() { Deployment deployment = new Deployment(deploymentName); Application application = new Application(); - application.setDisplayName("display name"); + application.setDisplayName(LocalizedValue.of("display name")); application.setDisplayVersion("1.0"); application.setDeployment(deployment); application.setEndpoint("test"); @@ -238,7 +241,8 @@ void validateUpdate_shouldDelegateToDisplayFieldsAndDeploymentValidators() { // then verify(deploymentValidator).validateUpdate(deploymentName, deployment, "Application"); - verify(displayFieldsValidator).validateDisplayNameDisplayVersion("display name", "1.0", "Application", deploymentName); + verify(displayFieldsValidator).validateDisplayNameDisplayVersion( + application.getDisplayName(), application.getDisplayVersion(), "Application", deploymentName); } @ParameterizedTest @@ -246,7 +250,7 @@ void validateUpdate_shouldDelegateToDisplayFieldsAndDeploymentValidators() { void validateUpdate_shouldThrowExceptionWhenEndpointIsNotNullButBlank(String endpoint) { // given Application application = new Application(); - application.setDisplayName("display name"); + application.setDisplayName(LocalizedValue.of("display name")); application.setDisplayVersion("1.0"); application.setEndpoint(endpoint); application.setSource(new ApplicationEndpointsSource()); @@ -259,7 +263,8 @@ void validateUpdate_shouldThrowExceptionWhenEndpointIsNotNullButBlank(String end .isInstanceOf(IllegalArgumentException.class) .hasMessage("Invalid endpoint: '" + endpoint + "'. Application: deploymentName"); - verify(displayFieldsValidator).validateDisplayNameDisplayVersion("display name", "1.0", "Application", "deploymentName"); + verify(displayFieldsValidator).validateDisplayNameDisplayVersion( + application.getDisplayName(), application.getDisplayVersion(), "Application", "deploymentName"); } @Test @@ -271,7 +276,7 @@ void validateUpdate_shouldThrowExceptionWhenEndpointsSourceWithNoEndpoints() { Application application = new Application(); application.setDeployment(deployment); - application.setDisplayName("text"); + application.setDisplayName(LocalizedValue.of("text")); application.setDisplayVersion("1.0"); application.setSource(new ApplicationEndpointsSource()); @@ -285,7 +290,7 @@ void validateUpdate_shouldThrowExceptionWhenEndpointsSourceWithNoEndpoints() { void validateUpdate_shouldThrowExceptionWhenSchemaSourceWithEndpointSet() { // given Application application = new Application(); - application.setDisplayName("text"); + application.setDisplayName(LocalizedValue.of("text")); application.setDisplayVersion("1.0"); application.setEndpoint("test"); application.setSource(new ApplicationSchemaSource(URI.create("https://test.com"))); @@ -337,7 +342,7 @@ void validateCreation_shouldThrowExceptionForInvalidName(String name) { @Test void validateCreation_shouldThrowExceptionWhenExternalServiceHasIncompleteOauthAuthSettings() { Application application = new Application(); - application.setDisplayName("display name"); + application.setDisplayName(LocalizedValue.of("display name")); application.setDisplayVersion("1.0"); application.setEndpoint("test"); application.setSource(new ApplicationEndpointsSource()); @@ -352,7 +357,7 @@ void validateCreation_shouldThrowExceptionWhenExternalServiceHasIncompleteOauthA @Test void validateCreation_shouldThrowExceptionWhenExternalServiceHasIncompleteApiKeyAuthSettings() { Application application = new Application(); - application.setDisplayName("display name"); + application.setDisplayName(LocalizedValue.of("display name")); application.setDisplayVersion("1.0"); application.setEndpoint("test"); application.setSource(new ApplicationEndpointsSource()); @@ -368,7 +373,7 @@ void validateCreation_shouldThrowExceptionWhenExternalServiceHasIncompleteApiKey @Test void validateUpdate_shouldThrowExceptionWhenExternalServiceHasIncompleteOauthAuthSettings() { Application application = new Application(); - application.setDisplayName("display name"); + application.setDisplayName(LocalizedValue.of("display name")); application.setDisplayVersion("1.0"); application.setEndpoint("test"); application.setSource(new ApplicationEndpointsSource()); @@ -393,7 +398,7 @@ void validateCreation_shouldNotThrowWhenExternalServiceAuthSettingsAreValid() { externalService.setAuthSettings(authSettings); Application application = new Application(); - application.setDisplayName("display name"); + application.setDisplayName(LocalizedValue.of("display name")); application.setDisplayVersion("1.0"); application.setEndpoint("test"); application.setSource(new ApplicationEndpointsSource()); @@ -408,7 +413,7 @@ void validateCreation_shouldValidateExternalServiceDisplayName() { ExternalService externalService = externalService(AuthenticationType.NONE); Application application = new Application(); - application.setDisplayName("display name"); + application.setDisplayName(LocalizedValue.of("display name")); application.setDisplayVersion("1.0"); application.setEndpoint("test"); application.setSource(new ApplicationEndpointsSource()); diff --git a/src/test/java/com/epam/aidial/cfg/domain/validator/DisplayFieldsValidatorTest.java b/src/test/java/com/epam/aidial/cfg/domain/validator/DisplayFieldsValidatorTest.java index 0ca29c764..8cc701eb7 100644 --- a/src/test/java/com/epam/aidial/cfg/domain/validator/DisplayFieldsValidatorTest.java +++ b/src/test/java/com/epam/aidial/cfg/domain/validator/DisplayFieldsValidatorTest.java @@ -1,10 +1,13 @@ package com.epam.aidial.cfg.domain.validator; +import com.epam.aidial.cfg.domain.model.LocalizedValue; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.CsvSource; +import java.util.Map; + import static org.assertj.core.api.Assertions.assertThatNoException; import static org.assertj.core.api.Assertions.assertThatThrownBy; @@ -20,44 +23,62 @@ void setUp() { @ParameterizedTest @CsvSource({"''", "' '"}) void validateDisplayNameDisplayVersion_shouldThrowExceptionWhenDisplayNameIsBlank(String displayName) { - assertThatThrownBy(() -> displayFieldsValidator.validateDisplayNameDisplayVersion(displayName, null, "DomainObjectType", "name")) + assertThatThrownBy(() -> displayFieldsValidator.validateDisplayNameDisplayVersion(LocalizedValue.of(displayName), null, "DomainObjectType", "name")) .isInstanceOf(IllegalArgumentException.class) - .hasMessage("Display name: '" + displayName + "'" + " must not be blank for DomainObjectType with id:'name'"); + .hasMessageContaining("must not be blank for DomainObjectType with id:'name'"); } @ParameterizedTest @CsvSource({"''", "' '"}) void validateDisplayNameDisplayVersion_shouldThrowExceptionWhenDisplayVersionIsBlank(String displayVersion) { - assertThatThrownBy(() -> displayFieldsValidator.validateDisplayNameDisplayVersion("test", displayVersion, "DomainObjectType", "name")) + assertThatThrownBy(() -> displayFieldsValidator.validateDisplayNameDisplayVersion(LocalizedValue.of("test"), displayVersion, "DomainObjectType", "name")) .isInstanceOf(IllegalArgumentException.class) - .hasMessage("Display version: '" + displayVersion + "'" + " must not be blank for DomainObjectType with id:'name'"); + .hasMessageContaining("must not be blank for DomainObjectType with id:'name'"); } @ParameterizedTest @CsvSource(value = {"null", "''", "' '"}, nullValues = "null") void validateDisplayName_shouldThrowExceptionWhenDisplayNameIsNullOrBlank(String displayName) { - assertThatThrownBy(() -> displayFieldsValidator.validateDisplayName(displayName, "DomainObjectType", "name")) + assertThatThrownBy(() -> displayFieldsValidator.validateDisplayName(displayName != null ? LocalizedValue.of(displayName) : null, "DomainObjectType", "name")) .isInstanceOf(IllegalArgumentException.class) - .hasMessage("Display name: '" + displayName + "'" + " must not be blank for DomainObjectType with id:'name'"); + .hasMessageContaining("must not be blank for DomainObjectType with id:'name'"); } @Test void validateDisplayNameDisplayVersion_shouldThrowExceptionWhenDisplayNameIsNullAndDisplayVersionIsNotNull() { assertThatThrownBy(() -> displayFieldsValidator.validateDisplayNameDisplayVersion(null, "1.0", "DomainObjectType", "name")) .isInstanceOf(IllegalArgumentException.class) - .hasMessage("Display name: 'null'" + " must not be blank for DomainObjectType with id:'name'"); + .hasMessageContaining("must not be blank for DomainObjectType with id:'name'"); } @Test void validateDisplayNameDisplayVersion_shouldDoNothingWhenDisplayNameIsNotNullAndDisplayVersionIsNull() { - assertThatNoException().isThrownBy(() -> displayFieldsValidator.validateDisplayNameDisplayVersion("text", null, "DomainObjectType", "name")); + assertThatNoException().isThrownBy(() -> displayFieldsValidator.validateDisplayNameDisplayVersion(LocalizedValue.of("text"), null, "DomainObjectType", "name")); } @Test void validateDisplayNameDisplayVersion_shouldDoNothingWhenDisplayNameIsNotNullAndDisplayVersionIsNotNull() { - assertThatNoException().isThrownBy(() -> displayFieldsValidator.validateDisplayNameDisplayVersion("text", "1.0", "DomainObjectType", "name")); + assertThatNoException().isThrownBy(() -> + displayFieldsValidator.validateDisplayNameDisplayVersion( + LocalizedValue.of("text"), "1.0", "DomainObjectType", "name")); + } + + @Test + void validateDisplayName_shouldThrowExceptionWhenAllLocalesAreBlank() { + LocalizedValue displayName = LocalizedValue.of(Map.of("en", "", "fr", " ")); + + assertThatThrownBy(() -> displayFieldsValidator.validateDisplayName(displayName, "DomainObjectType", "name")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("must not be blank for DomainObjectType with id:'name'"); + } + + @Test + void validateDisplayName_shouldDoNothingWhenAtLeastOneLocaleIsNotBlank() { + LocalizedValue displayName = LocalizedValue.of(Map.of("en", "", "fr", "affichage")); + + assertThatNoException().isThrownBy(() -> displayFieldsValidator.validateDisplayName(displayName, "DomainObjectType", "name")); } } \ No newline at end of file diff --git a/src/test/java/com/epam/aidial/cfg/domain/validator/DisplayNameUniquenessValidatorTest.java b/src/test/java/com/epam/aidial/cfg/domain/validator/DisplayNameUniquenessValidatorTest.java new file mode 100644 index 000000000..3d1afcecd --- /dev/null +++ b/src/test/java/com/epam/aidial/cfg/domain/validator/DisplayNameUniquenessValidatorTest.java @@ -0,0 +1,116 @@ +package com.epam.aidial.cfg.domain.validator; + +import com.epam.aidial.cfg.domain.model.LocalizedValue; +import com.epam.aidial.cfg.exception.EntityAlreadyExistsException; +import org.junit.jupiter.api.Test; + +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class DisplayNameUniquenessValidatorTest { + + private final DisplayNameUniquenessValidator validator = new DisplayNameUniquenessValidator(); + + @Test + void testPlainDisplayName_Unique() { + Map candidates = Map.of("existing", LocalizedValue.of("existing display")); + + assertThatCode(() -> validator.validateUnique("Model", null, LocalizedValue.of("new display"), null, candidates)) + .doesNotThrowAnyException(); + } + + @Test + void testPlainDisplayName_Duplicate_Throws() { + Map candidates = Map.of("existing", LocalizedValue.of("display")); + + assertThatThrownBy(() -> validator.validateUnique("Model", null, LocalizedValue.of("display"), null, candidates)) + .isInstanceOf(EntityAlreadyExistsException.class) + .hasMessageContaining("Model with display name"); + } + + @Test + void testLocalizedDisplayName_OneLocale_Duplicate_Throws() { + Map candidates = Map.of("existing", LocalizedValue.of(Map.of("en", "en display"))); + + assertThatThrownBy(() -> validator.validateUnique("Model", null, LocalizedValue.of(Map.of("en", "en display")), null, candidates)) + .isInstanceOf(EntityAlreadyExistsException.class); + } + + @Test + void testLocalizedDisplayName_MultipleLocales_Unique() { + Map candidates = Map.of( + "existing", LocalizedValue.of(Map.of("en", "en display", "fr", "fr display"))); + LocalizedValue newDisplayName = LocalizedValue.of(Map.of("en", "en display new", "fr", "fr display new")); + + assertThatCode(() -> validator.validateUnique("Model", null, newDisplayName, null, candidates)) + .doesNotThrowAnyException(); + } + + @Test + void testTaskExample_ConflictOnlyOnFrLocale() { + // 1. {"en": "en display", "fr": "fr display"} + // 3. {"en": "en display 1", "fr": "fr display"} -- conflicts with 1. on 'fr' + Map candidates = Map.of( + "entity1", LocalizedValue.of(Map.of("en", "en display", "fr", "fr display"))); + + LocalizedValue entity3 = LocalizedValue.of(Map.of("en", "en display 1", "fr", "fr display")); + assertThatThrownBy(() -> validator.validateUnique("Model", null, entity3, null, candidates)) + .isInstanceOf(EntityAlreadyExistsException.class); + } + + @Test + void testTaskExample_PlainValueDoesNotConflict() { + Map candidates = Map.of( + "entity1", LocalizedValue.of(Map.of("en", "en display", "fr", "fr display")), + "entity3", LocalizedValue.of(Map.of("en", "en display 1", "fr", "fr display"))); + + assertThatCode(() -> validator.validateUnique("Model", null, LocalizedValue.of("display"), null, candidates)) + .doesNotThrowAnyException(); + } + + @Test + void testMissingValueForLocale_NoFalseConflict() { + Map candidates = Map.of("existing", LocalizedValue.of(Map.of("en", "en display"))); + LocalizedValue newDisplayName = LocalizedValue.of(Map.of("fr", "fr display")); + + assertThatCode(() -> validator.validateUnique("Model", null, newDisplayName, null, candidates)) + .doesNotThrowAnyException(); + } + + @Test + void testSameValueInDifferentLocales_Allowed() { + Map candidates = Map.of("existing", LocalizedValue.of(Map.of("en", "shared value"))); + LocalizedValue newDisplayName = LocalizedValue.of(Map.of("fr", "shared value")); + + assertThatCode(() -> validator.validateUnique("Model", null, newDisplayName, null, candidates)) + .doesNotThrowAnyException(); + } + + @Test + void testConflictBetweenPlainAndLocalized_Throws() { + Map candidates = Map.of( + "existing", LocalizedValue.of(Map.of("en", "en display", "fr", "shared value"))); + + assertThatThrownBy(() -> validator.validateUnique("Model", null, LocalizedValue.of("shared value"), null, candidates)) + .isInstanceOf(EntityAlreadyExistsException.class); + } + + @Test + void testSelfExclusionOnUpdate_NoThrow() { + LocalizedValue displayName = LocalizedValue.of("unchanged display"); + Map candidates = Map.of("self", displayName); + + assertThatCode(() -> validator.validateUnique("Model", "self", displayName, "1.0.0", candidates)) + .doesNotThrowAnyException(); + } + + @Test + void testBlankNewDisplayNameAndVersion_SkipsValidation() { + Map candidates = Map.of("existing", LocalizedValue.of("existing display")); + + assertThatCode(() -> validator.validateUnique("Model", null, null, null, candidates)) + .doesNotThrowAnyException(); + } +} diff --git a/src/test/java/com/epam/aidial/cfg/domain/validator/ModelValidatorTest.java b/src/test/java/com/epam/aidial/cfg/domain/validator/ModelValidatorTest.java index 63015e7b9..23f09ac47 100644 --- a/src/test/java/com/epam/aidial/cfg/domain/validator/ModelValidatorTest.java +++ b/src/test/java/com/epam/aidial/cfg/domain/validator/ModelValidatorTest.java @@ -2,6 +2,7 @@ import com.epam.aidial.cfg.domain.model.Deployment; import com.epam.aidial.cfg.domain.model.DeploymentInterface; +import com.epam.aidial.cfg.domain.model.LocalizedValue; import com.epam.aidial.cfg.domain.model.Model; import com.epam.aidial.cfg.domain.model.ModelType; import com.epam.aidial.cfg.domain.model.source.ModelAdapterSource; @@ -78,7 +79,7 @@ void setUp() { void validateCreation_shouldDelegateToDisplayFieldsValidator() { // given Model model = new Model(); - model.setDisplayName("display name"); + model.setDisplayName(LocalizedValue.of("display name")); model.setDisplayVersion("1.0"); Deployment deployment = new Deployment("text"); model.setDeployment(deployment); @@ -87,7 +88,7 @@ void validateCreation_shouldDelegateToDisplayFieldsValidator() { modelValidator.validateCreation(model); // then - verify(displayFieldsValidator).validateDisplayNameDisplayVersion("display name", "1.0", "Model", "text"); + verify(displayFieldsValidator).validateDisplayNameDisplayVersion(model.getDisplayName(), model.getDisplayVersion(), "Model", "text"); } @Test @@ -98,7 +99,7 @@ void validateUpdate_shouldDelegateToDisplayFieldsAndDeploymentValidators() { Deployment deployment = new Deployment(deploymentName); Model model = new Model(); - model.setDisplayName("display name"); + model.setDisplayName(LocalizedValue.of("display name")); model.setDisplayVersion("1.0"); model.setDeployment(deployment); @@ -107,7 +108,7 @@ void validateUpdate_shouldDelegateToDisplayFieldsAndDeploymentValidators() { // then verify(deploymentValidator).validateUpdate(deploymentName, deployment, "Model"); - verify(displayFieldsValidator).validateDisplayNameDisplayVersion("display name", "1.0", "Model", "deploymentName"); + verify(displayFieldsValidator).validateDisplayNameDisplayVersion(model.getDisplayName(), model.getDisplayVersion(), "Model", "deploymentName"); } @ParameterizedTest diff --git a/src/test/java/com/epam/aidial/cfg/domain/validator/ToolSetValidatorTest.java b/src/test/java/com/epam/aidial/cfg/domain/validator/ToolSetValidatorTest.java index 26ef96232..0f7333fd6 100644 --- a/src/test/java/com/epam/aidial/cfg/domain/validator/ToolSetValidatorTest.java +++ b/src/test/java/com/epam/aidial/cfg/domain/validator/ToolSetValidatorTest.java @@ -4,6 +4,7 @@ import com.epam.aidial.cfg.client.dto.InferenceTask; import com.epam.aidial.cfg.client.dto.InterceptorDeploymentInfoDto; import com.epam.aidial.cfg.client.dto.McpDeploymentInfoDto; +import com.epam.aidial.cfg.domain.model.LocalizedValue; import com.epam.aidial.cfg.domain.model.SecuredResource; import com.epam.aidial.cfg.domain.model.ToolSet; import com.epam.aidial.cfg.domain.model.source.ToolSetContainerSource; @@ -62,13 +63,13 @@ void setUp() { void validateCreation_shouldDelegateToDeploymentValidator() { // given ToolSet toolSet = new ToolSet(); - toolSet.setDisplayName(TEST_TOOLSET_NAME); + toolSet.setDisplayName(LocalizedValue.of(TEST_TOOLSET_NAME)); SecuredResource deployment = new SecuredResource(TEST_TOOLSET_NAME); toolSet.setDeployment(deployment); // when & then assertThatNoException().isThrownBy(() -> toolSetValidator.validateCreation(toolSet)); - verify(deploymentValidator).validateCreation("ToolSet", toolSet.getDisplayName()); + verify(deploymentValidator).validateCreation("ToolSet", TEST_TOOLSET_NAME); } @ParameterizedTest @@ -76,7 +77,7 @@ void validateCreation_shouldDelegateToDeploymentValidator() { void validateCreation_shouldThrowExceptionForInvalidName(String toolSetName) { // given ToolSet toolSet = new ToolSet(); - toolSet.setDisplayName(toolSetName); + toolSet.setDisplayName(LocalizedValue.of(toolSetName)); SecuredResource deployment = new SecuredResource(toolSetName); toolSet.setDeployment(deployment); @@ -90,7 +91,7 @@ void validateCreation_shouldThrowExceptionForInvalidName(String toolSetName) { void validateCreation_shouldNotThrowExceptionForValidName() { // given ToolSet toolSet = new ToolSet(); - toolSet.setDisplayName(TEST_TOOLSET_NAME); + toolSet.setDisplayName(LocalizedValue.of(TEST_TOOLSET_NAME)); SecuredResource deployment = new SecuredResource(TEST_TOOLSET_NAME); toolSet.setDeployment(deployment); ReflectionTestUtils.setField(toolSetValidator, "toolSetNameValidationPattern", NAME_VALIDATION_PATTERN); @@ -103,7 +104,7 @@ void validateCreation_shouldNotThrowExceptionForValidName() { void validateCreation_withNameValidationPattern_shouldValidateNameAgainstPattern() { // given ToolSet toolSet = new ToolSet(); - toolSet.setDisplayName(TEST_TOOLSET_NAME); + toolSet.setDisplayName(LocalizedValue.of(TEST_TOOLSET_NAME)); SecuredResource deployment = new SecuredResource(TEST_TOOLSET_NAME); toolSet.setDeployment(deployment); ReflectionTestUtils.setField(toolSetValidator, "toolSetNameValidationPattern", NAME_VALIDATION_PATTERN); @@ -117,7 +118,7 @@ void validateCreation_withNameValidationPattern_shouldValidateNameAgainstPattern void validateCreation_withInvalidNameAgainstPattern_shouldThrowException(String invalidName) { // given ToolSet toolSet = new ToolSet(); - toolSet.setDisplayName(invalidName); + toolSet.setDisplayName(LocalizedValue.of(invalidName)); SecuredResource deployment = new SecuredResource(invalidName); toolSet.setDeployment(deployment); ReflectionTestUtils.setField(toolSetValidator, "toolSetNameValidationPattern", NAME_VALIDATION_PATTERN); @@ -132,7 +133,7 @@ void validateCreation_withInvalidNameAgainstPattern_shouldThrowException(String void validateUpdate_shouldDelegateToDeploymentValidator() { // given ToolSet toolSet = new ToolSet(); - toolSet.setDisplayName(TEST_TOOLSET_NAME); + toolSet.setDisplayName(LocalizedValue.of(TEST_TOOLSET_NAME)); SecuredResource deployment = new SecuredResource(TEST_TOOLSET_NAME); toolSet.setDeployment(deployment); @@ -145,7 +146,7 @@ void validateUpdate_shouldDelegateToDeploymentValidator() { void validateToolSetFields_withNullEndpoint_shouldNotThrowException() { // given ToolSet toolSet = new ToolSet(); - toolSet.setDisplayName(TEST_TOOLSET_NAME); + toolSet.setDisplayName(LocalizedValue.of(TEST_TOOLSET_NAME)); toolSet.setEndpoint(null); SecuredResource deployment = new SecuredResource(TEST_TOOLSET_NAME); toolSet.setDeployment(deployment); @@ -158,7 +159,7 @@ void validateToolSetFields_withNullEndpoint_shouldNotThrowException() { void validateToolSetFields_withValidEndpoint_shouldNotThrowException() { // given ToolSet toolSet = new ToolSet(); - toolSet.setDisplayName(TEST_TOOLSET_NAME); + toolSet.setDisplayName(LocalizedValue.of(TEST_TOOLSET_NAME)); toolSet.setEndpoint("https://example.com/api"); SecuredResource deployment = new SecuredResource(TEST_TOOLSET_NAME); toolSet.setDeployment(deployment); @@ -172,7 +173,7 @@ void validateToolSetFields_withValidEndpoint_shouldNotThrowException() { void validateToolSetFields_withBlankEndpoint_shouldThrowException(String blankEndpoint) { // given ToolSet toolSet = new ToolSet(); - toolSet.setDisplayName(TEST_TOOLSET_NAME); + toolSet.setDisplayName(LocalizedValue.of(TEST_TOOLSET_NAME)); toolSet.setEndpoint(blankEndpoint); SecuredResource deployment = new SecuredResource(TEST_TOOLSET_NAME); toolSet.setDeployment(deployment); diff --git a/src/test/java/com/epam/aidial/cfg/dto/validation/annotation/EndpointValidationTest.java b/src/test/java/com/epam/aidial/cfg/dto/validation/annotation/EndpointValidationTest.java index 85b6243f1..9a9809356 100644 --- a/src/test/java/com/epam/aidial/cfg/dto/validation/annotation/EndpointValidationTest.java +++ b/src/test/java/com/epam/aidial/cfg/dto/validation/annotation/EndpointValidationTest.java @@ -5,6 +5,7 @@ import com.epam.aidial.cfg.dto.CreateApplicationResourceDto; import com.epam.aidial.cfg.dto.InterceptorDto; import com.epam.aidial.cfg.dto.InterceptorRunnerDto; +import com.epam.aidial.cfg.dto.LocalizedValueDto; import com.epam.aidial.cfg.dto.ModelDto; import com.epam.aidial.cfg.dto.ToolSetDto; import jakarta.validation.ConstraintViolation; @@ -37,7 +38,7 @@ void init() { void testApplicationDto_ValidEndpoint(String endpoint) { ApplicationDto dto = new ApplicationDto(); dto.setName("test-app"); - dto.setDisplayName("Test App"); + dto.setDisplayName(LocalizedValueDto.of("Test App")); dto.setEndpoint(endpoint); Set> violations = validator.validate(dto); @@ -49,7 +50,7 @@ void testApplicationDto_ValidEndpoint(String endpoint) { void testApplicationDto_InvalidEndpoint(String endpoint) { ApplicationDto dto = new ApplicationDto(); dto.setName("test-app"); - dto.setDisplayName("Test App"); + dto.setDisplayName(LocalizedValueDto.of("Test App")); dto.setEndpoint(endpoint); Set> violations = validator.validate(dto); @@ -93,7 +94,7 @@ void testCreateApplicationResourceDto_InvalidEndpoint(String endpoint) { void testModelDto_ValidEndpoint(String endpoint) { ModelDto dto = new ModelDto(); dto.setName("test-model"); - dto.setDisplayName("Test Model"); + dto.setDisplayName(LocalizedValueDto.of("Test Model")); dto.setEndpoint(endpoint); Set> violations = validator.validate(dto); @@ -105,7 +106,7 @@ void testModelDto_ValidEndpoint(String endpoint) { void testModelDto_InvalidEndpoint(String endpoint) { ModelDto dto = new ModelDto(); dto.setName("test-model"); - dto.setDisplayName("Test Model"); + dto.setDisplayName(LocalizedValueDto.of("Test Model")); dto.setEndpoint(endpoint); Set> violations = validator.validate(dto); @@ -120,7 +121,7 @@ void testModelDto_InvalidEndpoint(String endpoint) { void testInterceptorDto_ValidEndpoint(String endpoint) { InterceptorDto dto = new InterceptorDto(); dto.setName("test-interceptor"); - dto.setDisplayName("Test Interceptor"); + dto.setDisplayName(LocalizedValueDto.of("Test Interceptor")); dto.setEndpoint(endpoint); Set> violations = validator.validate(dto); @@ -132,7 +133,7 @@ void testInterceptorDto_ValidEndpoint(String endpoint) { void testInterceptorDto_InvalidEndpoint(String endpoint) { InterceptorDto dto = new InterceptorDto(); dto.setName("test-interceptor"); - dto.setDisplayName("Test Interceptor"); + dto.setDisplayName(LocalizedValueDto.of("Test Interceptor")); dto.setEndpoint(endpoint); Set> violations = validator.validate(dto); @@ -147,7 +148,7 @@ void testInterceptorDto_InvalidEndpoint(String endpoint) { void testToolSetDto_ValidEndpoint(String endpoint) { ToolSetDto dto = new ToolSetDto(); dto.setName("test-toolset"); - dto.setDisplayName("Test ToolSet"); + dto.setDisplayName(LocalizedValueDto.of("Test ToolSet")); dto.setTransport(ToolSetDto.TransportDto.HTTP); dto.setEndpoint(endpoint); @@ -160,7 +161,7 @@ void testToolSetDto_ValidEndpoint(String endpoint) { void testToolSetDto_InvalidEndpoint(String endpoint) { ToolSetDto dto = new ToolSetDto(); dto.setName("test-toolset"); - dto.setDisplayName("Test ToolSet"); + dto.setDisplayName(LocalizedValueDto.of("Test ToolSet")); dto.setTransport(ToolSetDto.TransportDto.HTTP); dto.setEndpoint(endpoint); diff --git a/src/test/java/com/epam/aidial/cfg/dto/validation/annotation/NotEmptyLocalizedValueValidationTest.java b/src/test/java/com/epam/aidial/cfg/dto/validation/annotation/NotEmptyLocalizedValueValidationTest.java new file mode 100644 index 000000000..b0e1977fa --- /dev/null +++ b/src/test/java/com/epam/aidial/cfg/dto/validation/annotation/NotEmptyLocalizedValueValidationTest.java @@ -0,0 +1,160 @@ +package com.epam.aidial.cfg.dto.validation.annotation; + +import com.epam.aidial.cfg.dto.ApplicationDto; +import com.epam.aidial.cfg.dto.InterceptorDto; +import com.epam.aidial.cfg.dto.LocalizedValueDto; +import com.epam.aidial.cfg.dto.ModelDto; +import com.epam.aidial.cfg.dto.ToolSetDto; +import jakarta.validation.ConstraintViolation; +import jakarta.validation.Validation; +import jakarta.validation.Validator; +import jakarta.validation.ValidatorFactory; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.Map; +import java.util.Set; +import java.util.stream.Stream; + +class NotEmptyLocalizedValueValidationTest { + + private Validator validator; + + @BeforeEach + void init() { + try (ValidatorFactory factory = Validation.buildDefaultValidatorFactory()) { + validator = factory.getValidator(); + } + } + + @ParameterizedTest + @MethodSource("validDisplayNames") + void testApplicationDto_ValidDisplayName(LocalizedValueDto displayName) { + ApplicationDto dto = new ApplicationDto(); + dto.setName("test-app"); + dto.setDisplayName(displayName); + + Set> violations = validator.validate(dto); + Assertions.assertThat(violations.stream() + .anyMatch(v -> v.getPropertyPath().toString().equals("displayName"))) + .isFalse(); + } + + @ParameterizedTest + @MethodSource("invalidDisplayNames") + void testApplicationDto_InvalidDisplayName(LocalizedValueDto displayName) { + ApplicationDto dto = new ApplicationDto(); + dto.setName("test-app"); + dto.setDisplayName(displayName); + + Set> violations = validator.validate(dto); + assertDisplayNameViolation(violations); + } + + @ParameterizedTest + @MethodSource("validDisplayNames") + void testModelDto_ValidDisplayName(LocalizedValueDto displayName) { + ModelDto dto = new ModelDto(); + dto.setName("test-model"); + dto.setDisplayName(displayName); + + Set> violations = validator.validate(dto); + Assertions.assertThat(violations.stream() + .anyMatch(v -> v.getPropertyPath().toString().equals("displayName"))) + .isFalse(); + } + + @ParameterizedTest + @MethodSource("invalidDisplayNames") + void testModelDto_InvalidDisplayName(LocalizedValueDto displayName) { + ModelDto dto = new ModelDto(); + dto.setName("test-model"); + dto.setDisplayName(displayName); + + Set> violations = validator.validate(dto); + assertDisplayNameViolation(violations); + } + + @ParameterizedTest + @MethodSource("validDisplayNames") + void testToolSetDto_ValidDisplayName(LocalizedValueDto displayName) { + ToolSetDto dto = new ToolSetDto(); + dto.setName("test-toolset"); + dto.setTransport(ToolSetDto.TransportDto.HTTP); + dto.setDisplayName(displayName); + + Set> violations = validator.validate(dto); + Assertions.assertThat(violations.stream() + .anyMatch(v -> v.getPropertyPath().toString().equals("displayName"))) + .isFalse(); + } + + @ParameterizedTest + @MethodSource("invalidDisplayNames") + void testToolSetDto_InvalidDisplayName(LocalizedValueDto displayName) { + ToolSetDto dto = new ToolSetDto(); + dto.setName("test-toolset"); + dto.setTransport(ToolSetDto.TransportDto.HTTP); + dto.setDisplayName(displayName); + + Set> violations = validator.validate(dto); + assertDisplayNameViolation(violations); + } + + @ParameterizedTest + @MethodSource("validDisplayNames") + void testInterceptorDto_ValidDisplayName(LocalizedValueDto displayName) { + InterceptorDto dto = new InterceptorDto(); + dto.setName("test-interceptor"); + dto.setDisplayName(displayName); + + Set> violations = validator.validate(dto); + Assertions.assertThat(violations.stream() + .anyMatch(v -> v.getPropertyPath().toString().equals("displayName"))) + .isFalse(); + } + + @ParameterizedTest + @MethodSource("invalidDisplayNames") + void testInterceptorDto_InvalidDisplayName(LocalizedValueDto displayName) { + InterceptorDto dto = new InterceptorDto(); + dto.setName("test-interceptor"); + dto.setDisplayName(displayName); + + Set> violations = validator.validate(dto); + assertDisplayNameViolation(violations); + } + + private static void assertDisplayNameViolation(Set> violations) { + Assertions.assertThat(violations).isNotEmpty(); + Assertions.assertThat(violations.stream() + .filter(v -> v.getPropertyPath().toString().equals("displayName")) + .findFirst()) + .isPresent() + .get() + .extracting(ConstraintViolation::getMessage) + .isEqualTo("DisplayName is required"); + } + + private static Stream validDisplayNames() { + return Stream.of( + Arguments.of(LocalizedValueDto.of("Test Display Name")), + Arguments.of(LocalizedValueDto.of(Map.of("en", "Test Display Name"))), + Arguments.of(LocalizedValueDto.of(Map.of("en", " ", "fr", "Nom de test"))) + ); + } + + private static Stream invalidDisplayNames() { + return Stream.of( + Arguments.of((LocalizedValueDto) null), + Arguments.of(LocalizedValueDto.of("")), + Arguments.of(LocalizedValueDto.of(" ")), + Arguments.of(LocalizedValueDto.of(Map.of())), + Arguments.of(LocalizedValueDto.of(Map.of("en", ""))), + Arguments.of(LocalizedValueDto.of(Map.of("en", " ", "fr", ""))) + ); + } +} diff --git a/src/test/java/com/epam/aidial/cfg/dto/validation/validator/NotEmptyLocalizedValueValidatorTest.java b/src/test/java/com/epam/aidial/cfg/dto/validation/validator/NotEmptyLocalizedValueValidatorTest.java new file mode 100644 index 000000000..cb4121210 --- /dev/null +++ b/src/test/java/com/epam/aidial/cfg/dto/validation/validator/NotEmptyLocalizedValueValidatorTest.java @@ -0,0 +1,60 @@ +package com.epam.aidial.cfg.dto.validation.validator; + +import com.epam.aidial.cfg.dto.LocalizedValueDto; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.Map; +import java.util.stream.Stream; + +class NotEmptyLocalizedValueValidatorTest { + + private NotEmptyLocalizedValueValidator validator; + + @BeforeEach + void init() { + validator = new NotEmptyLocalizedValueValidator(); + } + + @Test + void testIsValid_shouldReturnFalseWhenNull() { + var result = validator.isValid(null, null); + Assertions.assertThat(result).isFalse(); + } + + @ParameterizedTest + @MethodSource("validValues") + void testIsValid_shouldReturnTrueForNonEmptyValues(LocalizedValueDto value) { + var result = validator.isValid(value, null); + Assertions.assertThat(result).isTrue(); + } + + @ParameterizedTest + @MethodSource("invalidValues") + void testIsValid_shouldReturnFalseForEmptyValues(LocalizedValueDto value) { + var result = validator.isValid(value, null); + Assertions.assertThat(result).isFalse(); + } + + private static Stream validValues() { + return Stream.of( + Arguments.of(LocalizedValueDto.of("GPT-4")), + Arguments.of(LocalizedValueDto.of(Map.of("en", "GPT-4"))), + Arguments.of(LocalizedValueDto.of(Map.of("en", " ", "fr", "GPT-4"))) + ); + } + + private static Stream invalidValues() { + return Stream.of( + Arguments.of(LocalizedValueDto.of("")), + Arguments.of(LocalizedValueDto.of(" ")), + Arguments.of(LocalizedValueDto.of(Map.of())), + Arguments.of(LocalizedValueDto.of(Map.of("en", ""))), + Arguments.of(LocalizedValueDto.of(Map.of("en", " ", "fr", ""))) + ); + } +} diff --git a/src/test/java/com/epam/aidial/cfg/functional/config/FunctionalTestConfiguration.java b/src/test/java/com/epam/aidial/cfg/functional/config/FunctionalTestConfiguration.java index 529875276..18672e255 100644 --- a/src/test/java/com/epam/aidial/cfg/functional/config/FunctionalTestConfiguration.java +++ b/src/test/java/com/epam/aidial/cfg/functional/config/FunctionalTestConfiguration.java @@ -6,6 +6,7 @@ import com.epam.aidial.cfg.client.ToolsClient; import com.epam.aidial.cfg.client.mapper.DeploymentClientMapper; import com.epam.aidial.cfg.client.mapper.DeploymentClientMapperImpl; +import com.epam.aidial.cfg.client.mapper.LocalizedValueClientMapper; import com.epam.aidial.cfg.client.mapper.ResourceCredentialClientMapper; import com.epam.aidial.cfg.client.mapper.ResourceCredentialClientMapperImpl; import com.epam.aidial.cfg.client.mapper.RouteMapper; @@ -54,6 +55,7 @@ import com.epam.aidial.cfg.web.facade.AuditActivityFacade; import com.epam.aidial.cfg.web.facade.HistoryFacade; import com.epam.aidial.core.config.Config; +import com.epam.aidial.core.config.CoreLocalizedValue; import com.epam.aidial.core.config.CoreModel; import org.mockito.Mockito; import org.springframework.boot.test.context.TestConfiguration; @@ -125,7 +127,7 @@ public ConfigExportProperties configExportProperties() { public CoreConfigRetriever configSource() { CoreModel model = new CoreModel(); model.setName("testModel"); - model.setDisplayName("testModel displayName"); + model.setDisplayName(CoreLocalizedValue.of("testModel displayName")); model.setEndpoint("https://endpoint1/chat/completions"); Config config = new Config(); @@ -193,6 +195,11 @@ public RouteMapper routeMapper() { return new RouteMapperImpl(); } + @Bean + public LocalizedValueClientMapper localizedValueClientMapper() { + return new LocalizedValueClientMapper(); + } + @Bean public DeploymentClient deploymentClient() { return Mockito.mock(DeploymentClient.class); diff --git a/src/test/java/com/epam/aidial/cfg/functional/tests/AddonFunctionalTest.java b/src/test/java/com/epam/aidial/cfg/functional/tests/AddonFunctionalTest.java index 2f6a3ed2d..1599654a1 100644 --- a/src/test/java/com/epam/aidial/cfg/functional/tests/AddonFunctionalTest.java +++ b/src/test/java/com/epam/aidial/cfg/functional/tests/AddonFunctionalTest.java @@ -1,6 +1,7 @@ package com.epam.aidial.cfg.functional.tests; import com.epam.aidial.cfg.dto.AddonDto; +import com.epam.aidial.cfg.dto.LocalizedValueDto; import com.epam.aidial.cfg.exception.EntityNotFoundException; import com.epam.aidial.cfg.exception.OptimisticLockConflictException; import com.epam.aidial.cfg.features.flag.aspect.FeatureFlagGateEvaluationAspect; @@ -109,13 +110,13 @@ public void shouldSuccessfullyCreateAndUpdateAddon() { AddonDto addonDto = createAddonWithRoleLimitsDto("1"); addonFacade.createAddon(addonDto); AddonDto updatedAddon = createAddonWithRoleLimitsDto("1"); - updatedAddon.setDescription("new addon description"); + updatedAddon.setDescription(LocalizedValueDto.of("new addon description")); addonFacade.updateAddon(addonDto.getName(), updatedAddon, "*"); AddonDto actual = addonFacade.getAddon(addonDto.getName()); var expected = createAddonWithRoleLimitsDto("1"); - expected.setDescription("new addon description"); + expected.setDescription(LocalizedValueDto.of("new addon description")); assertAddon(actual, expected); } @@ -125,14 +126,14 @@ public void shouldSuccessfullyUpdateAddon() { AddonDto addonDto = createAddonWithRoleLimitsDto("1"); addonFacade.createAddon(addonDto); AddonDto updatedAddon = createAddonWithRoleLimitsDto("1"); - updatedAddon.setDescription("new addon description"); + updatedAddon.setDescription(LocalizedValueDto.of("new addon description")); addonFacade.updateAddon(addonDto.getName(), updatedAddon, "*"); AddonDto actual = addonFacade.getAddon(updatedAddon.getName()); var expected = createAddonWithRoleLimitsDto("1"); - expected.setDescription("new addon description"); + expected.setDescription(LocalizedValueDto.of("new addon description")); assertAddon(actual, expected); } @@ -158,7 +159,7 @@ public void shouldSuccessfullyUpdateAddonWithCorrectHash() { AddonDto addonDto = createAddonWithRoleLimitsDto("1"); addonFacade.createAddon(addonDto); AddonDto updatedAddon = createAddonWithRoleLimitsDto("1"); - updatedAddon.setDescription("new addon description"); + updatedAddon.setDescription(LocalizedValueDto.of("new addon description")); var hash = addonFacade.getAddonWithHash(addonDto.getName()).hash(); @@ -167,7 +168,7 @@ public void shouldSuccessfullyUpdateAddonWithCorrectHash() { AddonDto actual = addonFacade.getAddon(updatedAddon.getName()); var expected = createAddonWithRoleLimitsDto("1"); - expected.setDescription("new addon description"); + expected.setDescription(LocalizedValueDto.of("new addon description")); assertAddon(actual, expected); } @@ -177,7 +178,7 @@ public void shouldThrowWhenUpdateAddonWithIncorrectHash() { AddonDto addonDto = createAddonWithRoleLimitsDto("1"); addonFacade.createAddon(addonDto); AddonDto updatedAddon = createAddonWithRoleLimitsDto("1"); - updatedAddon.setDescription("new addon description"); + updatedAddon.setDescription(LocalizedValueDto.of("new addon description")); Assertions.assertThrows(OptimisticLockConflictException.class, () -> addonFacade.updateAddon(addonDto.getName(), updatedAddon, "test")); diff --git a/src/test/java/com/epam/aidial/cfg/functional/tests/ApplicationFunctionalTest.java b/src/test/java/com/epam/aidial/cfg/functional/tests/ApplicationFunctionalTest.java index 6e9327bd6..04bc0407b 100644 --- a/src/test/java/com/epam/aidial/cfg/functional/tests/ApplicationFunctionalTest.java +++ b/src/test/java/com/epam/aidial/cfg/functional/tests/ApplicationFunctionalTest.java @@ -14,6 +14,7 @@ import com.epam.aidial.cfg.dto.EntitySyncStateStatusDto; import com.epam.aidial.cfg.dto.ExternalServiceDto; import com.epam.aidial.cfg.dto.InterceptorDto; +import com.epam.aidial.cfg.dto.LocalizedValueDto; import com.epam.aidial.cfg.dto.McpDto; import com.epam.aidial.cfg.dto.ResourceAuthSettingsDto; import com.epam.aidial.cfg.dto.source.ApplicationContainerSourceDto; @@ -53,6 +54,7 @@ import static com.epam.aidial.cfg.functional.utils.FunctionalTestHelper.createInterceptorDto; import static com.epam.aidial.cfg.functional.utils.FunctionalTestHelper.createRoleDto; import static com.epam.aidial.cfg.functional.utils.FunctionalTestHelper.defaultCoreFeatures; +import static com.epam.aidial.cfg.functional.utils.FunctionalTestHelper.toCoreLocalizedValue; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.isNull; @@ -158,13 +160,13 @@ public void shouldSuccessfullyCreateAndUpdateApplication() { ApplicationDto applicationDto = createApplicationDtoWithEndpointAndLimits("1"); applicationFacade.createApplication(applicationDto); ApplicationDto updatedApplication = createApplicationDtoWithEndpointAndLimits("1"); - updatedApplication.setDescription("new application description"); + updatedApplication.setDescription(LocalizedValueDto.of(Map.of("en", "new application description"))); applicationFacade.updateApplication(applicationDto.getName(), updatedApplication, "*"); ApplicationDto actual = applicationFacade.getApplication(applicationDto.getName()); var expected = createApplicationDtoWithEndpointAndLimits("1"); - expected.setDescription("new application description"); + expected.setDescription(LocalizedValueDto.of(Map.of("en", "new application description"))); assertApplication(actual, expected); } @@ -179,7 +181,7 @@ public void shouldSuccessfullyCreateAndAddInterceptor() { applicationFacade.createApplication(applicationDto); ApplicationDto updatedApplication = createApplicationDtoWithEndpointAndLimits("1"); - updatedApplication.setDescription("new model description"); + updatedApplication.setDescription(LocalizedValueDto.of("New ToolSet description")); updatedApplication.setDefaults(Map.of()); updatedApplication.setInterceptors(List.of("interceptor1")); @@ -196,7 +198,7 @@ public void shouldThrowExceptionWhenRenameApplication() { ApplicationDto applicationDto = createApplicationDtoWithEndpointAndLimits("1"); applicationFacade.createApplication(applicationDto); ApplicationDto updatedApplication = createApplicationDtoWithEndpointAndLimits("2"); - updatedApplication.setDescription("new application description"); + updatedApplication.setDescription(LocalizedValueDto.of("New ToolSet description")); IllegalArgumentException exception = Assertions.assertThrows( IllegalArgumentException.class, @@ -240,7 +242,7 @@ public void shouldSuccessfullyUpdateApplicationWithCorrectHash() { ApplicationDto applicationDto = createApplicationDtoWithEndpointAndLimits("1"); applicationFacade.createApplication(applicationDto); ApplicationDto updatedApplication = createApplicationDtoWithEndpointAndLimits("1"); - updatedApplication.setDescription("new application description"); + updatedApplication.setDescription(LocalizedValueDto.of("New ToolSet description")); var hash = applicationFacade.getApplicationWithHash(applicationDto.getName()).hash(); @@ -248,7 +250,7 @@ public void shouldSuccessfullyUpdateApplicationWithCorrectHash() { var actual = applicationFacade.getApplication(applicationDto.getName()); var expected = createApplicationDtoWithEndpointAndLimits("1"); - expected.setDescription("new application description"); + expected.setDescription(LocalizedValueDto.of("New ToolSet description")); assertApplication(actual, expected); } @@ -268,7 +270,7 @@ public void shouldSuccessfullyCreateWithInterceptor() { InterceptorDto interceptorDto = createInterceptorDto("1"); interceptorDto.setName("int1"); - interceptorDto.setDescription("int1_dsc"); + interceptorDto.setDescription(LocalizedValueDto.of("int1_dsc")); interceptorDto.setEndpoint("https://endpoint.test.com/interceptor"); interceptorFacade.createInterceptor(interceptorDto); @@ -406,12 +408,12 @@ public void shouldThrowExceptionWhenCreateApplicationWithExistingDisplayNameAndD initRoles(); ApplicationDto applicationDto = createApplicationDtoWithEndpointAndLimits("1"); - applicationDto.setDisplayName("display_name"); + applicationDto.setDisplayName(LocalizedValueDto.of("display_name")); applicationDto.setDisplayVersion("1.0"); applicationFacade.createApplication(applicationDto); ApplicationDto applicationDto2 = createApplicationDtoWithEndpointAndLimits("2"); - applicationDto2.setDisplayName("display_name"); + applicationDto2.setDisplayName(LocalizedValueDto.of("display_name")); applicationDto2.setDisplayVersion("1.0"); EntityAlreadyExistsException exception = Assertions.assertThrows( @@ -426,14 +428,14 @@ public void shouldThrowExceptionWhenUpdateApplicationWithExistingDisplayNameAndD initRoles(); ApplicationDto applicationDto = createApplicationDtoWithEndpointAndLimits("1"); - applicationDto.setDisplayName("display_name"); + applicationDto.setDisplayName(LocalizedValueDto.of("display_name")); applicationFacade.createApplication(applicationDto); ApplicationDto applicationDto2 = createApplicationDtoWithEndpointAndLimits("2"); - applicationDto2.setDisplayName("display_name_2"); + applicationDto2.setDisplayName(LocalizedValueDto.of("display_name_2")); applicationFacade.createApplication(applicationDto2); - applicationDto.setDisplayName("display_name_2"); + applicationDto.setDisplayName(LocalizedValueDto.of("display_name_2")); EntityAlreadyExistsException exception = Assertions.assertThrows( EntityAlreadyExistsException.class, @@ -463,9 +465,9 @@ public void shouldSuccessfullyGetCoreApplication() { CoreApplication expected = new CoreApplication(); expected.setName(applicationDto.getName()); - expected.setDisplayName(applicationDto.getDisplayName()); - expected.setDescription(applicationDto.getDescription()); - expected.setIntro(applicationDto.getIntro()); + expected.setDisplayName(toCoreLocalizedValue(applicationDto.getDisplayName())); + expected.setDescription(toCoreLocalizedValue(applicationDto.getDescription())); + expected.setIntro(toCoreLocalizedValue(applicationDto.getIntro())); expected.setEndpoint(applicationDto.getEndpoint()); expected.setDefaults(applicationDto.getDefaults()); expected.setApplicationProperties(applicationDto.getApplicationProperties()); @@ -517,7 +519,7 @@ public void shouldSuccessfullyGetFullySyncedEntitySyncStateWhenApplicationIsEqua public void shouldSuccessfullyGetInProgressTooLongEntitySyncStateWhenApplicationIsNotEqualToConfigApplicationAndUpdatedLongAgo() throws JsonProcessingException { doReturn(1000L).when(transactionTimestampContext).getTimestamp(); ApplicationDto applicationDto = createApplicationDtoWithEndpoint("1"); - applicationDto.setDescription("description OLD"); + applicationDto.setDescription(LocalizedValueDto.of("description OLD")); applicationFacade.createApplication(applicationDto); JsonNode config = coreConfig(); diff --git a/src/test/java/com/epam/aidial/cfg/functional/tests/ApplicationTypeSchemaFunctionalTest.java b/src/test/java/com/epam/aidial/cfg/functional/tests/ApplicationTypeSchemaFunctionalTest.java index 8a89eb3f7..fe6cd75f0 100644 --- a/src/test/java/com/epam/aidial/cfg/functional/tests/ApplicationTypeSchemaFunctionalTest.java +++ b/src/test/java/com/epam/aidial/cfg/functional/tests/ApplicationTypeSchemaFunctionalTest.java @@ -9,6 +9,7 @@ import com.epam.aidial.cfg.dto.EntitySyncStateDto; import com.epam.aidial.cfg.dto.EntitySyncStateStatusDto; import com.epam.aidial.cfg.dto.InterceptorDto; +import com.epam.aidial.cfg.dto.LocalizedValueDto; import com.epam.aidial.cfg.dto.source.ApplicationEndpointsSourceDto; import com.epam.aidial.cfg.dto.source.ApplicationSchemaSourceDto; import com.epam.aidial.cfg.exception.EntityAlreadyExistsException; @@ -295,7 +296,7 @@ public void shouldSuccessfullyCreateAndDeleteApplicationTypeSchemaAndApplication ApplicationDto applicationDto = new ApplicationDto(); applicationDto.setName("application"); - applicationDto.setDisplayName("application"); + applicationDto.setDisplayName(LocalizedValueDto.of("application")); applicationDto.setSource(new ApplicationSchemaSourceDto(new URI("https://test-schema.example"))); applicationFacade.createApplication(applicationDto); // when diff --git a/src/test/java/com/epam/aidial/cfg/functional/tests/AssistantFunctionalTest.java b/src/test/java/com/epam/aidial/cfg/functional/tests/AssistantFunctionalTest.java index 4fb867050..baa82242f 100644 --- a/src/test/java/com/epam/aidial/cfg/functional/tests/AssistantFunctionalTest.java +++ b/src/test/java/com/epam/aidial/cfg/functional/tests/AssistantFunctionalTest.java @@ -1,6 +1,7 @@ package com.epam.aidial.cfg.functional.tests; import com.epam.aidial.cfg.dto.AssistantDto; +import com.epam.aidial.cfg.dto.LocalizedValueDto; import com.epam.aidial.cfg.exception.EntityNotFoundException; import com.epam.aidial.cfg.features.flag.aspect.FeatureFlagGateEvaluationAspect; import com.epam.aidial.cfg.web.facade.AssistantFacade; @@ -106,13 +107,13 @@ public void shouldSuccessfullyCreateAndUpdateAssistant() { AssistantDto assistantDto = createAssistantDto("1"); assistantFacade.createAssistant(assistantDto); AssistantDto updatedAssistant = createAssistantDto("1"); - updatedAssistant.setDescription("new assistant description"); + updatedAssistant.setDescription(LocalizedValueDto.of("new assistant description")); assistantFacade.updateAssistant(assistantDto.getName(), updatedAssistant); AssistantDto actual = assistantFacade.getAssistant(assistantDto.getName()); var expected = createAssistantDto("1"); - expected.setDescription("new assistant description"); + expected.setDescription(LocalizedValueDto.of("new assistant description")); assertAssistant(actual, expected); } @@ -122,7 +123,7 @@ public void shouldThrowExceptionWhenRenameAssistant() { AssistantDto assistantDto = createAssistantDto("1"); assistantFacade.createAssistant(assistantDto); AssistantDto updatedAssistant = createAssistantDto("2"); - updatedAssistant.setDescription("new assistant description"); + updatedAssistant.setDescription(LocalizedValueDto.of("new assistant description")); IllegalArgumentException exception = Assertions.assertThrows( IllegalArgumentException.class, diff --git a/src/test/java/com/epam/aidial/cfg/functional/tests/ConfigTransferFunctionalTest.java b/src/test/java/com/epam/aidial/cfg/functional/tests/ConfigTransferFunctionalTest.java index 39bbc715d..ba0f9d0d9 100644 --- a/src/test/java/com/epam/aidial/cfg/functional/tests/ConfigTransferFunctionalTest.java +++ b/src/test/java/com/epam/aidial/cfg/functional/tests/ConfigTransferFunctionalTest.java @@ -11,6 +11,7 @@ import com.epam.aidial.cfg.domain.model.ExportFormat; import com.epam.aidial.cfg.domain.model.ExportKeyInfo; import com.epam.aidial.cfg.domain.model.ImportConfigPreview; +import com.epam.aidial.cfg.domain.model.LocalizedValue; import com.epam.aidial.cfg.domain.model.Model; import com.epam.aidial.cfg.domain.model.route.DependentRoute; import com.epam.aidial.cfg.domain.model.source.ApplicationEndpointsSource; @@ -33,6 +34,7 @@ import com.epam.aidial.cfg.dto.InterceptorRunnerDto; import com.epam.aidial.cfg.dto.KeyDto; import com.epam.aidial.cfg.dto.LimitDto; +import com.epam.aidial.cfg.dto.LocalizedValueDto; import com.epam.aidial.cfg.dto.ModelDto; import com.epam.aidial.cfg.dto.ResourceAuthSettingsDto; import com.epam.aidial.cfg.dto.ResourceTypeDto; @@ -285,7 +287,7 @@ void testImport_WithConflict() throws IOException { Map models = modelFacade.getAll().stream().collect(Collectors.toMap(ModelDto::getName, a -> a)); Assertions.assertThat(models).containsOnlyKeys("testModel1", "testModel2"); Assertions.assertThat(models.get("testModel1")).satisfies(model -> { - Assertions.assertThat(model.getDisplayName()).isEqualTo("Test Model1"); + Assertions.assertThat(model.getDisplayName()).isEqualTo(LocalizedValueDto.of("Test Model1")); Assertions.assertThat(model.getDisplayVersion()).isEqualTo("2.0.0"); Assertions.assertThat(model.getEndpoint()).isNull(); Assertions.assertThat(model.getSource() instanceof ModelAdapterSource); @@ -495,7 +497,7 @@ void testImport_TwoModelsAndRoleExist_ImportModelWithNewUserRolesAndExistingRole // given var dial22ModelDto = new ModelDto(); dial22ModelDto.setName("DIAL22"); - dial22ModelDto.setDisplayName("DIAL22"); + dial22ModelDto.setDisplayName(LocalizedValueDto.of("DIAL22")); dial22ModelDto.setIsPublic(true); modelFacade.createModel(dial22ModelDto); @@ -2230,7 +2232,7 @@ void testExportPreview_CoreFormatKeyWithAllDependencies_SelectedItemsExportReque Assertions.assertThat(preview.getRoles()).hasSize(1).first() .satisfies(role -> { Assertions.assertThat(role.getName()).isEqualTo("default"); - Assertions.assertThat(role.getDisplayName()).isEqualTo("default"); + Assertions.assertThat(role.getDisplayName()).isEqualTo(LocalizedValue.of("default")); }); Assertions.assertThat(preview.getModels()).hasSize(1).first() .satisfies(model -> { @@ -2240,17 +2242,17 @@ void testExportPreview_CoreFormatKeyWithAllDependencies_SelectedItemsExportReque Assertions.assertThat(preview.getApplications()).hasSize(1).first() .satisfies(app -> { Assertions.assertThat(app.getName()).isEqualTo("testApplication1"); - Assertions.assertThat(app.getDisplayName()).isEqualTo("Test Application1"); + Assertions.assertThat(app.getDisplayName()).isEqualTo(LocalizedValue.of("Test Application1")); }); Assertions.assertThat(preview.getRoutes()).hasSize(1).first() .satisfies(route -> { Assertions.assertThat(route.getName()).isEqualTo("test_route1"); - Assertions.assertThat(route.getDisplayName()).isEqualTo("test_route1"); + Assertions.assertThat(route.getDisplayName()).isEqualTo(LocalizedValue.of("test_route1")); }); Assertions.assertThat(preview.getInterceptors()).hasSize(1).first() .satisfies(interceptor -> { Assertions.assertThat(interceptor.getName()).isEqualTo("testInterceptor1"); - Assertions.assertThat(interceptor.getDisplayName()).isEqualTo("Test Interceptor1"); + Assertions.assertThat(interceptor.getDisplayName()).isEqualTo(LocalizedValue.of("Test Interceptor1")); }); Assertions.assertThat(preview.getApplicationRunners()).hasSize(1).first() .isInstanceOfSatisfying(ExportApplicationTypeSchemaInfo.class, @@ -2303,7 +2305,7 @@ void testExportPreview_AdminFormatKeyWithAllDependencies_SelectedItemsExportRequ Assertions.assertThat(preview.getRoles()).hasSize(1).first() .satisfies(role -> { Assertions.assertThat(role.getName()).isEqualTo("default"); - Assertions.assertThat(role.getDisplayName()).isEqualTo("default"); + Assertions.assertThat(role.getDisplayName()).isEqualTo(LocalizedValue.of("default")); }); Assertions.assertThat(preview.getModels()).hasSize(1).first() .satisfies(model -> Assertions.assertThat(model.getName()).isEqualTo("testModel1")); @@ -2312,7 +2314,7 @@ void testExportPreview_AdminFormatKeyWithAllDependencies_SelectedItemsExportRequ Assertions.assertThat(preview.getRoutes()).hasSize(1).first() .satisfies(route -> { Assertions.assertThat(route.getName()).isEqualTo("test_route1"); - Assertions.assertThat(route.getDisplayName()).isEqualTo("test_route1"); + Assertions.assertThat(route.getDisplayName()).isEqualTo(LocalizedValue.of("test_route1")); }); Assertions.assertThat(preview.getInterceptors()).hasSize(1).first() .satisfies(interceptor -> Assertions.assertThat(interceptor.getName()).isEqualTo("testInterceptor1")); @@ -2461,7 +2463,7 @@ void testImportZip() throws IOException { Assertions.assertThat(interceptors.get("testInterceptor1")).satisfies(i -> { Assertions.assertThat(i.getEntities()).containsExactlyInAnyOrder("testModel1", "testApplication1"); - Assertions.assertThat(i.getDisplayName()).isEqualTo("testInterceptor1"); + Assertions.assertThat(i.getDisplayName()).isEqualTo(LocalizedValueDto.of("testInterceptor1")); }); Assertions.assertThat(interceptors.get("testInterceptor2")).satisfies(i -> Assertions.assertThat(((InterceptorRunnerSourceDto) i.getSource()).runnerName()).isEqualTo("testRunner1") @@ -2560,7 +2562,7 @@ void testImportZip_WithConflict() throws IOException { Assertions.assertThat(interceptors.get("testInterceptor1")).satisfies(i -> { Assertions.assertThat(i.getEntities()).containsExactlyInAnyOrder("testModel1", "testApplication1"); - Assertions.assertThat(i.getDisplayName()).isEqualTo("testInterceptor1"); + Assertions.assertThat(i.getDisplayName()).isEqualTo(LocalizedValueDto.of("testInterceptor1")); }); Assertions.assertThat(interceptors.get("testInterceptor2")).satisfies(i -> Assertions.assertThat(((InterceptorRunnerSourceDto) i.getSource()).runnerName()).isEqualTo("testRunner1") @@ -3070,7 +3072,7 @@ void testExportCoreConfig_VersionFieldSerialization() throws IOException { var modelDto = new ModelDto(); modelDto.setName(modelName); - modelDto.setDisplayName(modelName); + modelDto.setDisplayName(LocalizedValueDto.of(modelName)); modelDto.setAuthor(author); modelDto.setCreatedAt(createdAt); modelDto.setUpdatedAt(updatedAt); diff --git a/src/test/java/com/epam/aidial/cfg/functional/tests/CoreConfigAutoImportOnBootstrapFunctionalTest.java b/src/test/java/com/epam/aidial/cfg/functional/tests/CoreConfigAutoImportOnBootstrapFunctionalTest.java index e373fc3ad..684d0b48b 100644 --- a/src/test/java/com/epam/aidial/cfg/functional/tests/CoreConfigAutoImportOnBootstrapFunctionalTest.java +++ b/src/test/java/com/epam/aidial/cfg/functional/tests/CoreConfigAutoImportOnBootstrapFunctionalTest.java @@ -1,6 +1,7 @@ package com.epam.aidial.cfg.functional.tests; import com.epam.aidial.cfg.dto.ConfigRevisionDto; +import com.epam.aidial.cfg.dto.LocalizedValueDto; import com.epam.aidial.cfg.dto.ModelDto; import com.epam.aidial.cfg.functional.tests.history.TestHistoryFacade; import com.epam.aidial.cfg.web.facade.ModelFacade; @@ -33,7 +34,7 @@ public void testCoreConfigAutoImportedSuccessfully() { assertThat(models).hasSize(1).first().satisfies(modelDto -> { assertThat(modelDto.getName()).isEqualTo("testModel"); - assertThat(modelDto.getDisplayName()).isEqualTo("testModel displayName"); + assertThat(modelDto.getDisplayName()).isEqualTo(LocalizedValueDto.of("testModel displayName")); }); ConfigRevisionDto latestRevision = CollectionUtils.lastElement(historyFacade.getRevisionsList()); @@ -60,7 +61,7 @@ public void testMergeJson_laterFileWins() { .containsExactlyInAnyOrder("model-a", "model-b", "model-shared"); assertThat(models).filteredOn(m -> "model-shared".equals(m.getName())) .first().extracting(ModelDto::getDisplayName) - .isEqualTo("Shared from B (wins)"); + .isEqualTo(LocalizedValueDto.of("Shared from B (wins)")); } } diff --git a/src/test/java/com/epam/aidial/cfg/functional/tests/InterceptorFunctionalTest.java b/src/test/java/com/epam/aidial/cfg/functional/tests/InterceptorFunctionalTest.java index d6428e299..7ebccff5e 100644 --- a/src/test/java/com/epam/aidial/cfg/functional/tests/InterceptorFunctionalTest.java +++ b/src/test/java/com/epam/aidial/cfg/functional/tests/InterceptorFunctionalTest.java @@ -13,6 +13,7 @@ import com.epam.aidial.cfg.dto.FeaturesDto; import com.epam.aidial.cfg.dto.InterceptorDto; import com.epam.aidial.cfg.dto.InterceptorRunnerDto; +import com.epam.aidial.cfg.dto.LocalizedValueDto; import com.epam.aidial.cfg.dto.ModelDto; import com.epam.aidial.cfg.dto.source.InterceptorContainerSourceDto; import com.epam.aidial.cfg.dto.source.InterceptorRunnerSourceDto; @@ -50,6 +51,7 @@ import static com.epam.aidial.cfg.functional.utils.FunctionalTestHelper.createInterceptorRunnerDto; import static com.epam.aidial.cfg.functional.utils.FunctionalTestHelper.createModelDtoWithEndpoint; import static com.epam.aidial.cfg.functional.utils.FunctionalTestHelper.defaultCoreFeatures; +import static com.epam.aidial.cfg.functional.utils.FunctionalTestHelper.toCoreLocalizedValue; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.when; @@ -472,8 +474,8 @@ public void shouldResolveEndpointsForContainerSource() { InterceptorDto interceptorDto = new InterceptorDto(); interceptorDto.setName("container-interceptor"); - interceptorDto.setDisplayName("container-interceptor"); - interceptorDto.setDescription("Container interceptor"); + interceptorDto.setDisplayName(LocalizedValueDto.of("container-interceptor")); + interceptorDto.setDescription(LocalizedValueDto.of("Container interceptor")); InterceptorContainerSourceDto sourceDto = new InterceptorContainerSourceDto( containerId, @@ -553,8 +555,8 @@ public void shouldSuccessfullyGetCoreInterceptor() { CoreInterceptor expected = new CoreInterceptor(); expected.setName(interceptorDto.getName()); - expected.setDisplayName(interceptorDto.getDisplayName()); - expected.setDescription(interceptorDto.getDescription()); + expected.setDisplayName(toCoreLocalizedValue(interceptorDto.getDisplayName())); + expected.setDescription(toCoreLocalizedValue(interceptorDto.getDescription())); expected.setEndpoint(interceptorDto.getEndpoint()); expected.setFeatures(defaultCoreFeatures()); expected.setFeatures(defaultCoreFeatures()); @@ -653,7 +655,7 @@ public void shouldSuccessfullyGetFullySyncedEntitySyncStateWhenInterceptorIsEqua public void shouldSuccessfullyGetInProgressTooLongEntitySyncStateWhenInterceptorIsNotEqualToConfigInterceptorAndUpdatedLongAgo() throws JsonProcessingException { doReturn(1000L).when(transactionTimestampContext).getTimestamp(); InterceptorDto interceptorDto = createInterceptorDto("1"); - interceptorDto.setDescription("description OLD"); + interceptorDto.setDescription(LocalizedValueDto.of("description OLD")); interceptorFacade.createInterceptor(interceptorDto); JsonNode config = coreConfig(); diff --git a/src/test/java/com/epam/aidial/cfg/functional/tests/InterceptorRunnerFunctionalTest.java b/src/test/java/com/epam/aidial/cfg/functional/tests/InterceptorRunnerFunctionalTest.java index 9a8452a8f..41b290674 100644 --- a/src/test/java/com/epam/aidial/cfg/functional/tests/InterceptorRunnerFunctionalTest.java +++ b/src/test/java/com/epam/aidial/cfg/functional/tests/InterceptorRunnerFunctionalTest.java @@ -2,6 +2,7 @@ import com.epam.aidial.cfg.dto.InterceptorDto; import com.epam.aidial.cfg.dto.InterceptorRunnerDto; +import com.epam.aidial.cfg.dto.LocalizedValueDto; import com.epam.aidial.cfg.dto.source.InterceptorEndpointsSourceDto; import com.epam.aidial.cfg.dto.source.InterceptorRunnerSourceDto; import com.epam.aidial.cfg.exception.EntityAlreadyExistsException; @@ -228,8 +229,8 @@ private InterceptorRunnerDto createDto(String suffix) { private InterceptorDto createInterceptorDto(String suffix, String runnerName) { InterceptorDto interceptorDto = new InterceptorDto(); interceptorDto.setName("interceptor" + suffix); - interceptorDto.setDisplayName("interceptor" + suffix); - interceptorDto.setDescription("description" + suffix); + interceptorDto.setDisplayName(LocalizedValueDto.of("interceptor" + suffix)); + interceptorDto.setDescription(LocalizedValueDto.of("description" + suffix)); interceptorDto.setSource(new InterceptorRunnerSourceDto(runnerName)); return interceptorDto; } diff --git a/src/test/java/com/epam/aidial/cfg/functional/tests/ModelFunctionalTest.java b/src/test/java/com/epam/aidial/cfg/functional/tests/ModelFunctionalTest.java index 2b2728eae..fad06b01b 100644 --- a/src/test/java/com/epam/aidial/cfg/functional/tests/ModelFunctionalTest.java +++ b/src/test/java/com/epam/aidial/cfg/functional/tests/ModelFunctionalTest.java @@ -10,6 +10,7 @@ import com.epam.aidial.cfg.dto.EntitySyncStateStatusDto; import com.epam.aidial.cfg.dto.InterceptorDto; import com.epam.aidial.cfg.dto.LimitDto; +import com.epam.aidial.cfg.dto.LocalizedValueDto; import com.epam.aidial.cfg.dto.ModelDto; import com.epam.aidial.cfg.dto.ShareResourceLimitDto; import com.epam.aidial.cfg.dto.UpstreamDto; @@ -25,6 +26,7 @@ import com.epam.aidial.cfg.web.facade.InterceptorFacade; import com.epam.aidial.cfg.web.facade.ModelFacade; import com.epam.aidial.cfg.web.facade.RoleFacade; +import com.epam.aidial.core.config.CoreLocalizedValue; import com.epam.aidial.core.config.CoreModel; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; @@ -49,6 +51,7 @@ import static com.epam.aidial.cfg.functional.utils.FunctionalTestHelper.createModelDtoWithLimitsAndEndpoint; import static com.epam.aidial.cfg.functional.utils.FunctionalTestHelper.createRoleDto; import static com.epam.aidial.cfg.functional.utils.FunctionalTestHelper.defaultCoreFeatures; +import static com.epam.aidial.cfg.functional.utils.FunctionalTestHelper.toCoreLocalizedValue; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.when; @@ -142,13 +145,13 @@ public void shouldSuccessfullyCreateAndUpdateModel() { ModelDto updatedModel = createModelDtoWithLimitsAndEndpoint("1"); updatedModel.setSource(new ModelAdapterSourceDto("adapter2", "/newEndpointDeploymentName/chat/completions")); - updatedModel.setDescription("new model description"); + updatedModel.setDescription(LocalizedValueDto.of("new model description")); updatedModel.setDefaults(Map.of()); modelFacade.updateModel(modelDto.getName(), updatedModel, "*"); ModelDto actual = modelFacade.getModel(modelDto.getName()); var expected = createModelDtoWithLimitsAndEndpoint("1"); - expected.setDescription("new model description"); + expected.setDescription(LocalizedValueDto.of("new model description")); expected.setDefaults(Map.of()); expected.setMaxRetryAttempts(1); expected.setDefaultRoleLimit(new LimitDto()); @@ -189,7 +192,7 @@ public void shouldThrowExceptionWhenRenameModel() { ModelDto modelDto = createModelDtoWithLimitsAndEndpoint("1"); modelFacade.createModel(modelDto); ModelDto updatedModel = createModelDtoWithLimitsAndEndpoint("2"); - updatedModel.setDescription("new model description"); + updatedModel.setDescription(LocalizedValueDto.of("New ToolSet description")); IllegalArgumentException exception = Assertions.assertThrows( IllegalArgumentException.class, @@ -209,7 +212,7 @@ public void shouldSuccessfullyCreateAndAddInterceptor() { ModelDto modelDto = createModelDtoWithLimitsAndEndpoint("1"); modelFacade.createModel(modelDto); ModelDto updatedModel = createModelDtoWithLimitsAndEndpoint("1"); - updatedModel.setDescription("new model description"); + updatedModel.setDescription(LocalizedValueDto.of("New ToolSet description")); updatedModel.setDefaults(Map.of()); updatedModel.setInterceptors(List.of("interceptor1")); @@ -380,12 +383,12 @@ public void shouldThrowExceptionWhenCreateModelWithExistingDisplayNameAndDisplay initRoles(); ModelDto modelDto = createModelDtoWithLimitsAndEndpoint("1"); - modelDto.setDisplayName("display_name"); + modelDto.setDisplayName(LocalizedValueDto.of("display_name")); modelDto.setDisplayVersion("1.0"); modelFacade.createModel(modelDto); ModelDto modelDto2 = createModelDtoWithLimitsAndEndpoint("2"); - modelDto2.setDisplayName("display_name"); + modelDto2.setDisplayName(LocalizedValueDto.of("display_name")); modelDto2.setDisplayVersion("1.0"); EntityAlreadyExistsException exception = Assertions.assertThrows( @@ -400,14 +403,14 @@ public void shouldThrowExceptionWhenUpdateModelWithExistingDisplayNameAndDisplay initRoles(); ModelDto modelDto = createModelDtoWithLimitsAndEndpoint("1"); - modelDto.setDisplayName("display_name"); + modelDto.setDisplayName(LocalizedValueDto.of("display_name")); modelFacade.createModel(modelDto); ModelDto modelDto2 = createModelDtoWithLimitsAndEndpoint("2"); - modelDto2.setDisplayName("display_name_2"); + modelDto2.setDisplayName(LocalizedValueDto.of("display_name_2")); modelFacade.createModel(modelDto2); - modelDto.setDisplayName("display_name_2"); + modelDto.setDisplayName(LocalizedValueDto.of("display_name_2")); EntityAlreadyExistsException exception = Assertions.assertThrows( EntityAlreadyExistsException.class, @@ -456,9 +459,9 @@ public void shouldSuccessfullyGetCoreModel() { CoreModel expected = new CoreModel(); expected.setTokenizerModel(modelDto.getTokenizerModel()); expected.setName(modelDto.getName()); - expected.setDisplayName(modelDto.getDisplayName()); - expected.setDescription(modelDto.getDescription()); - expected.setIntro(modelDto.getIntro()); + expected.setDisplayName(toCoreLocalizedValue(modelDto.getDisplayName())); + expected.setDescription(toCoreLocalizedValue(modelDto.getDescription())); + expected.setIntro(toCoreLocalizedValue(modelDto.getIntro())); expected.setEndpoint(modelDto.getEndpoint()); expected.setDefaults(modelDto.getDefaults()); expected.setFeatures(defaultCoreFeatures()); @@ -490,14 +493,14 @@ public void shouldSuccessfullyUpdateCoreModel() { modelFacade.createModel(modelDto); CoreModel coreModel = modelFacade.getCoreModelWithHash(modelDto.getName()).core(); - coreModel.setDescription("New description"); + coreModel.setDescription(CoreLocalizedValue.of("New description")); modelFacade.updateModel(modelDto.getName(), coreModel, "*"); ModelDto actual = modelFacade.getModel(modelDto.getName()); Assertions.assertEquals("model1", actual.getName()); Assertions.assertEquals(adapterSourceDto, actual.getSource()); - Assertions.assertEquals("New description", actual.getDescription()); + Assertions.assertEquals(LocalizedValueDto.of("New description"), actual.getDescription()); } @Test @@ -619,7 +622,7 @@ public void shouldSuccessfullyGetInProgressTooLongEntitySyncStateWhenModelIsNotE ModelDto modelDto = createModelDto("1"); modelDto.setUpstreams(List.of(secretUpstreamDto, publicUpstreamDto)); - modelDto.setDescription("description OLD"); + modelDto.setDescription(LocalizedValueDto.of("description OLD")); modelDto.setEmbeddingDimensions(1536); modelFacade.createModel(modelDto); @@ -668,8 +671,8 @@ private void assertModel(ModelDto actual, ModelDto expected) { private ModelDto expectedDto1() { ModelDto modelDto = new ModelDto(); modelDto.setName("model1"); - modelDto.setDisplayName("model1"); - modelDto.setDescription("description1"); + modelDto.setDisplayName(LocalizedValueDto.of("model1")); + modelDto.setDescription(LocalizedValueDto.of("description1")); modelDto.setRoleLimits(Map.of( "role1", new LimitDto() )); diff --git a/src/test/java/com/epam/aidial/cfg/functional/tests/ToolSetFunctionalTest.java b/src/test/java/com/epam/aidial/cfg/functional/tests/ToolSetFunctionalTest.java index 53613cc5d..f759e150c 100644 --- a/src/test/java/com/epam/aidial/cfg/functional/tests/ToolSetFunctionalTest.java +++ b/src/test/java/com/epam/aidial/cfg/functional/tests/ToolSetFunctionalTest.java @@ -11,6 +11,7 @@ import com.epam.aidial.cfg.dto.AuthenticationTypeDto; import com.epam.aidial.cfg.dto.EntitySyncStateDto; import com.epam.aidial.cfg.dto.EntitySyncStateStatusDto; +import com.epam.aidial.cfg.dto.LocalizedValueDto; import com.epam.aidial.cfg.dto.ResourceAuthSettingsDto; import com.epam.aidial.cfg.dto.ToolSetDto; import com.epam.aidial.cfg.dto.ToolSetDto.TransportDto; @@ -46,6 +47,7 @@ import static com.epam.aidial.cfg.functional.utils.FunctionalTestHelper.createRoleDto; import static com.epam.aidial.cfg.functional.utils.FunctionalTestHelper.createToolSetDto; import static com.epam.aidial.cfg.functional.utils.FunctionalTestHelper.createToolSetDtoWithoutRoleLimits; +import static com.epam.aidial.cfg.functional.utils.FunctionalTestHelper.toCoreLocalizedValue; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.isNull; @@ -209,14 +211,14 @@ public void shouldSuccessfullyCreateAndUpdateToolSet() { toolSetFacade.createToolSet(toolSetDto); ToolSetDto updatedToolSet = createToolSetDto("1"); - updatedToolSet.setDescription("New ToolSet description"); + updatedToolSet.setDescription(LocalizedValueDto.of("New ToolSet description")); toolSetFacade.updateToolSet(toolSetDto.getName(), updatedToolSet, "*"); ToolSetDto actual = toolSetFacade.getToolSet(toolSetDto.getName()); var expected = createToolSetDto("1"); - expected.setDescription("New ToolSet description"); + expected.setDescription(LocalizedValueDto.of("New ToolSet description")); assertToolSet(actual, expected); } @@ -257,14 +259,14 @@ public void shouldSuccessfullyUpdateToolSetWithCorrectHash() { var hash = toolSetFacade.getToolSetWithHash(toolSetDto.getName()).hash(); ToolSetDto updatedToolSet = createToolSetDto("1"); - updatedToolSet.setDescription("New ToolSet description"); + updatedToolSet.setDescription(LocalizedValueDto.of("New ToolSet description")); toolSetFacade.updateToolSet(toolSetDto.getName(), updatedToolSet, hash); ToolSetDto actual = toolSetFacade.getToolSet(toolSetDto.getName()); var expected = createToolSetDto("1"); - expected.setDescription("New ToolSet description"); + expected.setDescription(LocalizedValueDto.of("New ToolSet description")); assertToolSet(actual, expected); } @@ -275,7 +277,7 @@ public void shouldThrowWhenUpdateToolSetWithIncorrectHash() { toolSetFacade.createToolSet(toolSetDto); ToolSetDto updatedToolSet = createToolSetDto("1"); - updatedToolSet.setDescription("New ToolSet description"); + updatedToolSet.setDescription(LocalizedValueDto.of("New ToolSet description")); Assertions.assertThrows(OptimisticLockConflictException.class, () -> toolSetFacade.updateToolSet(toolSetDto.getName(), updatedToolSet, "test")); @@ -286,7 +288,7 @@ public void shouldThrowExceptionWhenRenameToolSet() { ToolSetDto toolSetDto = createToolSetDto("1"); toolSetFacade.createToolSet(toolSetDto); ToolSetDto updatedToolSet = createToolSetDto("2"); - updatedToolSet.setDescription("New ToolSet description"); + updatedToolSet.setDescription(LocalizedValueDto.of("New ToolSet description")); IllegalArgumentException exception = Assertions.assertThrows( IllegalArgumentException.class, @@ -385,7 +387,7 @@ public void shouldResolveEndpointsForContainerSource() { ToolSetDto toolSetDto = createToolSetDto("1"); toolSetDto.setName("container-toolset"); - toolSetDto.setDescription("Container toolset"); + toolSetDto.setDescription(LocalizedValueDto.of("Container toolset")); ToolSetContainerSourceDto sourceDto = new ToolSetContainerSourceDto( containerId, @@ -438,7 +440,7 @@ public void shouldRefreshEndpointsForContainerSource() { ToolSetDto toolSetDto = createToolSetDto("1"); toolSetDto.setName(refreshedToolSetName); - toolSetDto.setDescription("Refresh toolset"); + toolSetDto.setDescription(LocalizedValueDto.of("Refresh toolset")); ToolSetContainerSourceDto sourceDto = new ToolSetContainerSourceDto( containerId, @@ -483,8 +485,8 @@ public void shouldSuccessfullyGetCoreToolSet() { expected.setAuthSettings(null); expected.setEndpoint(toolSetDto.getEndpoint()); expected.setTransport(CoreToolSet.Transport.HTTP); - expected.setDisplayName(toolSetDto.getDisplayName()); - expected.setDescription(toolSetDto.getDescription()); + expected.setDisplayName(toCoreLocalizedValue(toolSetDto.getDisplayName())); + expected.setDescription(toCoreLocalizedValue(toolSetDto.getDescription())); expected.setMaxRetryAttempts(toolSetDto.getMaxRetryAttempts()); expected.setProvider(toolSetDto.getProvider()); expected.setUserRoles(toolSetDto.getRoleLimits().keySet()); @@ -520,7 +522,7 @@ public void shouldSuccessfullyGetFullySyncedEntitySyncStateWhenToolSetIsEqualToC public void shouldSuccessfullyGetInProgressTooLongEntitySyncStateWhenToolSetIsNotEqualToConfigToolSetAndUpdatedLongAgo() throws JsonProcessingException { doReturn(1000L).when(transactionTimestampContext).getTimestamp(); ToolSetDto toolSetDto = createToolSetDto("1"); - toolSetDto.setDescription("description OLD"); + toolSetDto.setDescription(LocalizedValueDto.of("description OLD")); toolSetFacade.createToolSet(toolSetDto); JsonNode config = coreConfig(); diff --git a/src/test/java/com/epam/aidial/cfg/functional/tests/history/ActivityAuditFunctionalTest.java b/src/test/java/com/epam/aidial/cfg/functional/tests/history/ActivityAuditFunctionalTest.java index 177120f3b..bed3dffd3 100644 --- a/src/test/java/com/epam/aidial/cfg/functional/tests/history/ActivityAuditFunctionalTest.java +++ b/src/test/java/com/epam/aidial/cfg/functional/tests/history/ActivityAuditFunctionalTest.java @@ -3,6 +3,7 @@ import com.epam.aidial.cfg.domain.model.page.SortDirection; import com.epam.aidial.cfg.dto.AuditActivityDto; import com.epam.aidial.cfg.dto.LimitDto; +import com.epam.aidial.cfg.dto.LocalizedValueDto; import com.epam.aidial.cfg.dto.ModelDto; import com.epam.aidial.cfg.dto.PageDto; import com.epam.aidial.cfg.dto.ToolSetDto; @@ -79,7 +80,7 @@ public void shouldSuccessfullyLogAuditActivities() { // update model1 description ModelDto updatedModel = createModelDtoWithLimitsAndEndpoint("ActivityAudit1"); - updatedModel.setDescription("new model description"); + updatedModel.setDescription(LocalizedValueDto.of("new model description")); updatedModel.setDefaults(Map.of()); modelFacade.updateModel(modelDto.getName(), updatedModel, "*"); diff --git a/src/test/java/com/epam/aidial/cfg/functional/tests/history/AddonHistoryFunctionalTest.java b/src/test/java/com/epam/aidial/cfg/functional/tests/history/AddonHistoryFunctionalTest.java index 71b214ff5..a69c35f4b 100644 --- a/src/test/java/com/epam/aidial/cfg/functional/tests/history/AddonHistoryFunctionalTest.java +++ b/src/test/java/com/epam/aidial/cfg/functional/tests/history/AddonHistoryFunctionalTest.java @@ -3,6 +3,7 @@ import com.epam.aidial.cfg.dto.AddonDto; import com.epam.aidial.cfg.dto.ConfigRevisionDto; import com.epam.aidial.cfg.dto.LimitDto; +import com.epam.aidial.cfg.dto.LocalizedValueDto; import com.epam.aidial.cfg.dto.ShareResourceLimitDto; import com.epam.aidial.cfg.exception.EntityNotFoundException; import com.epam.aidial.cfg.web.facade.AddonFacade; @@ -47,13 +48,13 @@ public void shouldSuccessfullyCreateAndUpdateAddon() { // 2 update addon1 description AddonDto updatedAddon = createAddonDtoWithAllLimits("1"); - updatedAddon.setDescription("new addon description"); + updatedAddon.setDescription(LocalizedValueDto.of("new addon description")); addonFacade.updateAddon(addonDto.getName(), updatedAddon, "*"); // verify addon1 AddonDto actual = addonFacade.getAddon(addonDto.getName()); var expected = createAddonDtoWithAllLimits("1"); - expected.setDescription("new addon description"); + expected.setDescription(LocalizedValueDto.of("new addon description")); expected.setDefaultRoleLimit(new LimitDto()); assertAddon(actual, expected); diff --git a/src/test/java/com/epam/aidial/cfg/functional/tests/history/ApplicationHistoryFunctionalTest.java b/src/test/java/com/epam/aidial/cfg/functional/tests/history/ApplicationHistoryFunctionalTest.java index ef188be5a..c92369d52 100644 --- a/src/test/java/com/epam/aidial/cfg/functional/tests/history/ApplicationHistoryFunctionalTest.java +++ b/src/test/java/com/epam/aidial/cfg/functional/tests/history/ApplicationHistoryFunctionalTest.java @@ -6,6 +6,7 @@ import com.epam.aidial.cfg.dto.ConfigRevisionDto; import com.epam.aidial.cfg.dto.InterceptorDto; import com.epam.aidial.cfg.dto.LimitDto; +import com.epam.aidial.cfg.dto.LocalizedValueDto; import com.epam.aidial.cfg.dto.ShareResourceLimitDto; import com.epam.aidial.cfg.dto.source.ApplicationEndpointsSourceDto; import com.epam.aidial.cfg.dto.source.ApplicationSchemaSourceDto; @@ -62,7 +63,7 @@ public void shouldSuccessfullyCreateAndUpdateApplication() { // 2 update application1 description ApplicationDto updatedApplication = createApplicationDtoWithEndpointAndLimits("1"); - updatedApplication.setDescription("new application description"); + updatedApplication.setDescription(LocalizedValueDto.of("new application description")); updatedApplication.setEndpoint("endpoint2"); applicationFacade.updateApplication(applicationDto.getName(), updatedApplication, "*"); @@ -71,7 +72,7 @@ public void shouldSuccessfullyCreateAndUpdateApplication() { var expected = createApplicationDtoWithEndpointAndLimits("1"); ShareResourceLimitDto defaultShareResourceLimitDto = new ShareResourceLimitDto(); defaultShareResourceLimitDto.setMaxAcceptedUsers(10); - expected.setDescription("new application description"); + expected.setDescription(LocalizedValueDto.of("new application description")); expected.setDefaultRoleLimit(new LimitDto()); expected.setInterceptors(List.of()); expected.setEndpoint("endpoint2"); diff --git a/src/test/java/com/epam/aidial/cfg/functional/tests/history/AssistantHistoryFunctionalTest.java b/src/test/java/com/epam/aidial/cfg/functional/tests/history/AssistantHistoryFunctionalTest.java index 46c8ce8b5..1aed2e81b 100644 --- a/src/test/java/com/epam/aidial/cfg/functional/tests/history/AssistantHistoryFunctionalTest.java +++ b/src/test/java/com/epam/aidial/cfg/functional/tests/history/AssistantHistoryFunctionalTest.java @@ -3,6 +3,7 @@ import com.epam.aidial.cfg.dto.AssistantDto; import com.epam.aidial.cfg.dto.ConfigRevisionDto; import com.epam.aidial.cfg.dto.LimitDto; +import com.epam.aidial.cfg.dto.LocalizedValueDto; import com.epam.aidial.cfg.dto.RoleDto; import com.epam.aidial.cfg.dto.ShareResourceLimitDto; import com.epam.aidial.cfg.exception.EntityNotFoundException; @@ -48,13 +49,13 @@ public void shouldSuccessfullyCreateAndUpdateAssistant() { // 2 update assistant1 description AssistantDto updatedAssistant = createAssistantDto("1"); - updatedAssistant.setDescription("new assistant description"); + updatedAssistant.setDescription(LocalizedValueDto.of("new assistant description")); assistantFacade.updateAssistant(assistantDto.getName(), updatedAssistant); // verify assistant1 AssistantDto actual = assistantFacade.getAssistant(assistantDto.getName()); var expected = createAssistantDto("1"); - expected.setDescription("new assistant description"); + expected.setDescription(LocalizedValueDto.of("new assistant description")); expected.setDefaultRoleLimit(new LimitDto()); assertAssistant(actual, expected); diff --git a/src/test/java/com/epam/aidial/cfg/functional/tests/history/InterceptorHistoryFunctionalTest.java b/src/test/java/com/epam/aidial/cfg/functional/tests/history/InterceptorHistoryFunctionalTest.java index 78357278c..451f1da95 100644 --- a/src/test/java/com/epam/aidial/cfg/functional/tests/history/InterceptorHistoryFunctionalTest.java +++ b/src/test/java/com/epam/aidial/cfg/functional/tests/history/InterceptorHistoryFunctionalTest.java @@ -5,6 +5,7 @@ import com.epam.aidial.cfg.dto.GlobalSettingsDto; import com.epam.aidial.cfg.dto.InterceptorDto; import com.epam.aidial.cfg.dto.InterceptorRunnerDto; +import com.epam.aidial.cfg.dto.LocalizedValueDto; import com.epam.aidial.cfg.dto.ModelDto; import com.epam.aidial.cfg.dto.source.InterceptorEndpointsSourceDto; import com.epam.aidial.cfg.dto.source.InterceptorRunnerSourceDto; @@ -55,13 +56,13 @@ public void shouldSuccessfullyCreateAndUpdateInterceptor() { // update interceptor1 description InterceptorDto updatedInterceptor = createInterceptorDto("1"); - updatedInterceptor.setDescription("new interceptor description"); + updatedInterceptor.setDescription(LocalizedValueDto.of("new interceptor description")); interceptorFacade.updateInterceptor(interceptorDto.getName(), updatedInterceptor, "*"); // verify interceptor1 InterceptorDto actual = interceptorFacade.getInterceptor(interceptorDto.getName()); var expected = createInterceptorDto("1"); - expected.setDescription("new interceptor description"); + expected.setDescription(LocalizedValueDto.of("new interceptor description")); expected.setSource(new InterceptorEndpointsSourceDto()); expected.setApplicationTypeSchemas(List.of()); assertInterceptor(actual, expected); @@ -69,7 +70,7 @@ public void shouldSuccessfullyCreateAndUpdateInterceptor() { var actualAtRevision = actual; final Integer revNumberToRollback = CollectionUtils.lastElement(historyFacade.getRevisionsList()).getId(); - updatedInterceptor.setDescription("new new interceptor description"); + updatedInterceptor.setDescription(LocalizedValueDto.of("new new interceptor description")); updatedInterceptor.setDefaults(Map.of("key1", "val1")); interceptorFacade.updateInterceptor(interceptorDto.getName(), updatedInterceptor, "*"); diff --git a/src/test/java/com/epam/aidial/cfg/functional/tests/history/ModelHistoryFunctionalTest.java b/src/test/java/com/epam/aidial/cfg/functional/tests/history/ModelHistoryFunctionalTest.java index ffc28fe23..e450770ea 100644 --- a/src/test/java/com/epam/aidial/cfg/functional/tests/history/ModelHistoryFunctionalTest.java +++ b/src/test/java/com/epam/aidial/cfg/functional/tests/history/ModelHistoryFunctionalTest.java @@ -4,6 +4,7 @@ import com.epam.aidial.cfg.dto.ConfigRevisionDto; import com.epam.aidial.cfg.dto.InterceptorDto; import com.epam.aidial.cfg.dto.LimitDto; +import com.epam.aidial.cfg.dto.LocalizedValueDto; import com.epam.aidial.cfg.dto.ModelDto; import com.epam.aidial.cfg.dto.RoleDto; import com.epam.aidial.cfg.dto.ShareResourceLimitDto; @@ -61,14 +62,14 @@ public void shouldSuccessfullyRollbackModels() { // update model1 description ModelDto updatedModel = createModelDtoWithLimitsAndEndpoint("1"); - updatedModel.setDescription("new model description"); + updatedModel.setDescription(LocalizedValueDto.of("new model description")); updatedModel.setDefaults(Map.of()); modelFacade.updateModel(modelDto.getName(), updatedModel, "*"); // verify model1 ModelDto actual = modelFacade.getModel(modelDto.getName()); var expected = createModelDtoWithLimitsAndEndpoint("1"); - expected.setDescription("new model description"); + expected.setDescription(LocalizedValueDto.of("new model description")); expected.setDefaults(Map.of()); expected.setDefaultRoleLimit(new LimitDto()); expected.setMaxRetryAttempts(1); @@ -174,7 +175,7 @@ public void shouldSuccessfullyRollbackModelsWithInterceptorsWhenInterceptorDelet final Integer revNumberToRollback = CollectionUtils.lastElement(historyFacade.getRevisionsList()).getId(); var actualAtRevision = modelFacade.getAll(); - modelDto.setDescription("new description"); + modelDto.setDescription(LocalizedValueDto.of("new description")); modelFacade.updateModel(modelDto.getName(), modelDto, "*"); interceptorFacade.deleteInterceptor(interceptor1.getName()); diff --git a/src/test/java/com/epam/aidial/cfg/functional/tests/history/ToolSetHistoryFunctionalTest.java b/src/test/java/com/epam/aidial/cfg/functional/tests/history/ToolSetHistoryFunctionalTest.java index a87bc8eae..aab212a46 100644 --- a/src/test/java/com/epam/aidial/cfg/functional/tests/history/ToolSetHistoryFunctionalTest.java +++ b/src/test/java/com/epam/aidial/cfg/functional/tests/history/ToolSetHistoryFunctionalTest.java @@ -5,6 +5,7 @@ import com.epam.aidial.cfg.dto.AuthenticationTypeDto; import com.epam.aidial.cfg.dto.ConfigRevisionDto; import com.epam.aidial.cfg.dto.LimitDto; +import com.epam.aidial.cfg.dto.LocalizedValueDto; import com.epam.aidial.cfg.dto.ResourceAuthSettingsDto; import com.epam.aidial.cfg.dto.RoleDto; import com.epam.aidial.cfg.dto.ToolSetDto; @@ -80,7 +81,7 @@ public void shouldSuccessfullyCreateAndUpdateToolSet() { // 2. Update ToolSet1 description ToolSetDto updatedToolSet = createToolSetDto("1"); - updatedToolSet.setDescription("New ToolSet description"); + updatedToolSet.setDescription(LocalizedValueDto.of("New ToolSet description")); updatedToolSet.setSource(containerSourceDto); updatedToolSet.setAuthSettings(authSettingsDto); toolSetFacade.updateToolSet(toolSetDto.getName(), updatedToolSet, "*"); @@ -88,7 +89,7 @@ public void shouldSuccessfullyCreateAndUpdateToolSet() { // 3. Verify ToolSet1 ToolSetDto actual = toolSetFacade.getToolSet(toolSetDto.getName()); var expected = createToolSetDto("1"); - expected.setDescription("New ToolSet description"); + expected.setDescription(LocalizedValueDto.of("New ToolSet description")); expected.setDefaultRoleLimit(new LimitDto()); expected.setEndpoint(containerUrl + endpointPath); expected.setSource(containerSourceDto); diff --git a/src/test/java/com/epam/aidial/cfg/functional/utils/FunctionalTestHelper.java b/src/test/java/com/epam/aidial/cfg/functional/utils/FunctionalTestHelper.java index ae8db9ead..9584cfb79 100644 --- a/src/test/java/com/epam/aidial/cfg/functional/utils/FunctionalTestHelper.java +++ b/src/test/java/com/epam/aidial/cfg/functional/utils/FunctionalTestHelper.java @@ -10,6 +10,7 @@ import com.epam.aidial.cfg.dto.InterceptorRunnerDto; import com.epam.aidial.cfg.dto.KeyDto; import com.epam.aidial.cfg.dto.LimitDto; +import com.epam.aidial.cfg.dto.LocalizedValueDto; import com.epam.aidial.cfg.dto.McpDto; import com.epam.aidial.cfg.dto.ModelDto; import com.epam.aidial.cfg.dto.RoleDto; @@ -22,6 +23,7 @@ import com.epam.aidial.cfg.dto.source.ModelAdapterSourceDto; import com.epam.aidial.cfg.dto.source.ModelEndpointsSourceDto; import com.epam.aidial.core.config.CoreFeatures; +import com.epam.aidial.core.config.CoreLocalizedValue; import java.time.Instant; import java.util.ArrayList; @@ -43,8 +45,8 @@ public static RoleDto createRoleDto(String suffix) { public static ModelDto createModelDto(String suffix) { ModelDto modelDto = new ModelDto(); modelDto.setName("model" + suffix); - modelDto.setDisplayName("model" + suffix); - modelDto.setDescription("description" + suffix); + modelDto.setDisplayName(LocalizedValueDto.of("model" + suffix)); + modelDto.setDescription(LocalizedValueDto.of("description" + suffix)); modelDto.setMaxRetryAttempts(1); return modelDto; } @@ -85,8 +87,8 @@ public static AdapterDto createAdapterDto(String suffix) { public static ToolSetDto createToolSetDtoWithoutRoleLimits(String suffix) { ToolSetDto toolSet = new ToolSetDto(); toolSet.setName("ToolSet" + suffix); - toolSet.setDisplayName("ToolSet" + suffix); - toolSet.setDescription("description" + suffix); + toolSet.setDisplayName(LocalizedValueDto.of("ToolSet" + suffix)); + toolSet.setDescription(LocalizedValueDto.of("description" + suffix)); toolSet.setEndpoint("https://endpoint.test.com/toolset" + suffix); toolSet.setTransport(ToolSetDto.TransportDto.HTTP); toolSet.setMaxRetryAttempts(1); @@ -124,8 +126,8 @@ public static RouteDto createRouteDtoWithLimits(String suffix) { public static AddonDto createAddonDto(String suffix) { AddonDto addonDto = new AddonDto(); addonDto.setName("addon" + suffix); - addonDto.setDisplayName("addon" + suffix); - addonDto.setDescription("description" + suffix); + addonDto.setDisplayName(LocalizedValueDto.of("addon" + suffix)); + addonDto.setDescription(LocalizedValueDto.of("description" + suffix)); return addonDto; } @@ -169,8 +171,8 @@ public static KeyDto createKeyDtoWithRole(String suffix) { public static InterceptorDto createInterceptorDto(String suffix) { InterceptorDto interceptorDto = new InterceptorDto(); interceptorDto.setName("interceptor" + suffix); - interceptorDto.setDescription("description" + suffix); - interceptorDto.setDisplayName("displayName" + suffix); + interceptorDto.setDescription(LocalizedValueDto.of("description" + suffix)); + interceptorDto.setDisplayName(LocalizedValueDto.of("displayName" + suffix)); interceptorDto.setEndpoint("https://endpoint.test.com/interceptor" + suffix); interceptorDto.setEntities(List.of()); interceptorDto.setTopics(new TreeSet<>(Set.of("topic1", "topic2"))); @@ -187,8 +189,8 @@ public static InterceptorDto createInterceptorDtoWithEntities(String suffix) { public static ApplicationDto createBaseApplicationDto(String suffix) { ApplicationDto applicationDto = new ApplicationDto(); applicationDto.setName("application" + suffix); - applicationDto.setDisplayName("application" + suffix); - applicationDto.setDescription("description" + suffix); + applicationDto.setDisplayName(LocalizedValueDto.of("application" + suffix)); + applicationDto.setDescription(LocalizedValueDto.of("description" + suffix)); applicationDto.setSource(new ApplicationEndpointsSourceDto()); return applicationDto; } @@ -221,8 +223,8 @@ public static ApplicationDto createApplicationDtoWithEndpointAndLimits(String su public static AssistantDto createAssistantDto(String suffix) { AssistantDto assistantDto = new AssistantDto(); assistantDto.setName("assistant" + suffix); - assistantDto.setDisplayName("assistant" + suffix); - assistantDto.setDescription("description" + suffix); + assistantDto.setDisplayName(LocalizedValueDto.of("assistant" + suffix)); + assistantDto.setDescription(LocalizedValueDto.of("description" + suffix)); assistantDto.setRoleLimits(Map.of( "role" + suffix, new LimitDto() )); @@ -300,4 +302,11 @@ public static AuditActivityDto createAuditActivityDto(String activityType, Strin return auditActivityDto; } + + public static CoreLocalizedValue toCoreLocalizedValue(LocalizedValueDto dto) { + if (dto == null) { + return null; + } + return dto.isPlain() ? CoreLocalizedValue.of(dto.getPlainValue()) : CoreLocalizedValue.of(dto.getLocaleMap()); + } } \ No newline at end of file diff --git a/src/test/java/com/epam/aidial/cfg/service/ApplicationEximServiceTest.java b/src/test/java/com/epam/aidial/cfg/service/ApplicationEximServiceTest.java index d724c181f..fb62077de 100644 --- a/src/test/java/com/epam/aidial/cfg/service/ApplicationEximServiceTest.java +++ b/src/test/java/com/epam/aidial/cfg/service/ApplicationEximServiceTest.java @@ -1,17 +1,21 @@ package com.epam.aidial.cfg.service; import com.epam.aidial.cfg.client.mapper.ApplicationClientMapperImpl; +import com.epam.aidial.cfg.client.mapper.LocalizedValueClientMapper; import com.epam.aidial.cfg.client.mapper.RouteMapperImpl; import com.epam.aidial.cfg.configuration.JsonMapperConfiguration; import com.epam.aidial.cfg.dto.ApplicationEximDto; import com.epam.aidial.cfg.dto.ApplicationsEximDto; +import com.epam.aidial.cfg.dto.LocalizedValueDto; import com.epam.aidial.cfg.dto.McpResourceDto; +import com.epam.aidial.cfg.mapper.LocalizedValueMapper; import com.epam.aidial.cfg.model.ApplicationResource; import com.epam.aidial.cfg.model.ApplicationResourceNodeInfo; import com.epam.aidial.cfg.model.CreateApplicationResource; import com.epam.aidial.cfg.model.ImportConflictResolutionStrategy; import com.epam.aidial.cfg.model.ImportResources; import com.epam.aidial.cfg.model.ImportResourcesStatus; +import com.epam.aidial.cfg.model.LocalizedValue; import com.epam.aidial.cfg.model.McpResource; import com.epam.aidial.cfg.model.NodeType; import com.epam.aidial.cfg.model.Rule; @@ -30,6 +34,7 @@ import java.util.Collections; import java.util.List; +import java.util.Map; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -48,7 +53,9 @@ ApplicationClientMapperImpl.class, ApplicationEximService.class, ResourceImportValidator.class, - RouteMapperImpl.class + RouteMapperImpl.class, + LocalizedValueClientMapper.class, + LocalizedValueMapper.class }) @TestPropertySource(properties = { "applications.import.consecutiveErrorsThreshold=2" @@ -81,9 +88,9 @@ void exportApplications_SinglePath() { var applicationExim = result.getApplications().get(0); assertThat(applicationExim.getApplicationTypeSchemaId()).isEqualTo("https://test1.epam.com"); - assertThat(applicationExim.getDisplayName()).isEqualTo("application1"); + assertThat(applicationExim.getDisplayName()).isEqualTo(LocalizedValue.of("application1")); assertThat(applicationExim.getFolderId()).isEqualTo("public/"); - assertThat(applicationExim.getDescription()).isEqualTo("application description 1"); + assertThat(applicationExim.getDescription()).isEqualTo(LocalizedValue.of("application description 1")); assertThat(applicationExim.getMcp().getEndpoint()).isEqualTo("http://localhost:9876/1/mcp"); } @@ -92,7 +99,10 @@ void exportApplications_SinglePath() { void exportApplications_MultiplePaths() { // given var application1 = getApplicationResource("1"); + application1.setDisplayName(LocalizedValue.of(Map.of("en", "application description en", "de", + "application description de"))); var application2 = getApplicationResource("2"); + application2.setDisplayName(LocalizedValue.of(Map.of("en", "application description en"))); var path1 = application1.getPath(); var path2 = application2.getPath(); @@ -112,7 +122,9 @@ void exportApplications_MultiplePaths() { assertThat(application1Exim1.getApplicationTypeSchemaId()).isEqualTo("https://test1.epam.com"); assertThat(application1Exim1.getName()).isEqualTo("application1"); assertThat(application1Exim1.getFolderId()).isEqualTo("public/"); - assertThat(application1Exim1.getDescription()).isEqualTo("application description 1"); + assertThat(application1Exim1.getDescription()).isEqualTo(LocalizedValue.of("application description 1")); + assertThat(application1Exim1.getDisplayName()).isEqualTo(LocalizedValue.of(Map.of("en", "application description en", "de", + "application description de"))); assertThat(application1Exim1.getMcp().getEndpoint()).isEqualTo("http://localhost:9876/1/mcp"); // Verify second application @@ -120,7 +132,8 @@ void exportApplications_MultiplePaths() { assertThat(application1Exim2.getApplicationTypeSchemaId()).isEqualTo("https://test2.epam.com"); assertThat(application1Exim2.getName()).isEqualTo("application2"); assertThat(application1Exim2.getFolderId()).isEqualTo("public/"); - assertThat(application1Exim2.getDescription()).isEqualTo("application description 2"); + assertThat(application1Exim2.getDescription()).isEqualTo(LocalizedValue.of("application description 2")); + assertThat(application1Exim2.getDisplayName()).isEqualTo(LocalizedValue.of(Map.of("en", "application description en"))); assertThat(application1Exim2.getMcp().getEndpoint()).isEqualTo("http://localhost:9876/2/mcp"); } @@ -193,7 +206,7 @@ void exportApplications_FolderPath_excludesTechnicalFile() { application.setName("test"); application.setVersion("0.0.1"); application.setFolderId("public/folder1/folder2/"); - application.setDescription("d"); + application.setDescription(LocalizedValue.of("d")); application.setMcp(mcp); application.setApplicationTypeSchemaId("https://test.epam.com"); @@ -284,7 +297,7 @@ void importApplications_NotFlatImport() { assertThat(applicationResource.getName()).isEqualTo("application1"); assertThat(applicationResource.getVersion()).isEqualTo("0.0.1"); assertThat(applicationResource.getFolderId()).isEqualTo("public/to/folder1/"); - assertThat(applicationResource.getDescription()).isEqualTo("application description 1"); + assertThat(applicationResource.getDescription()).isEqualTo(LocalizedValue.of("application description 1")); assertThat(applicationResource.getMcp().getEndpoint()).isEqualTo("http://localhost:9876/1/mcp"); } @@ -335,7 +348,7 @@ void importApplications_FlatImport() { assertThat(application.getName()).isEqualTo("application1"); assertThat(application.getVersion()).isEqualTo("0.0.1"); assertThat(application.getFolderId()).isEqualTo("public/to/"); - assertThat(application.getDescription()).isEqualTo("application description 1"); + assertThat(application.getDescription()).isEqualTo(LocalizedValue.of("application description 1")); assertThat(application.getMcp().getEndpoint()).isEqualTo("http://localhost:9876/1/mcp"); } @@ -388,11 +401,11 @@ private ApplicationResource getApplicationResource(String suffix) { var application = new ApplicationResource(); application.setApplicationTypeSchemaId(String.format("https://test%s.epam.com", suffix)); application.setName("application" + suffix); - application.setDisplayName("application" + suffix); + application.setDisplayName(LocalizedValue.of("application" + suffix)); application.setVersion(String.format("0.0.%s", suffix)); application.setFolderId(String.format("public/folder%s/", suffix)); application.setPath(String.format("%s%s__%s", application.getFolderId(), application.getName(), application.getVersion())); - application.setDescription(String.format("application description %s", suffix)); + application.setDescription(LocalizedValue.of(String.format("application description %s", suffix))); application.setMcp(mcp); return application; } @@ -405,9 +418,9 @@ private ApplicationEximDto getApplicationEximDto(String suffix) { .applicationTypeSchemaId(String.format("https://test%s.epam.com", suffix)) .name("application" + suffix) .version(String.format("0.0.%s", suffix)) - .displayName("application" + suffix) + .displayName(LocalizedValueDto.of("application" + suffix)) .folderId(String.format("public/folder%s/", suffix)) - .description(String.format("application description %s", suffix)) + .description(LocalizedValueDto.of(String.format("application description %s", suffix))) .mcp(mcp) .build(); } diff --git a/src/test/java/com/epam/aidial/cfg/service/AssertionsTest.java b/src/test/java/com/epam/aidial/cfg/service/AssertionsTest.java deleted file mode 100644 index bae7b7d38..000000000 --- a/src/test/java/com/epam/aidial/cfg/service/AssertionsTest.java +++ /dev/null @@ -1,133 +0,0 @@ -package com.epam.aidial.cfg.service; - -import com.epam.aidial.cfg.exception.ValidationException; -import com.epam.aidial.core.config.CoreModel; -import org.junit.jupiter.api.Test; - -import java.util.Map; - -import static com.epam.aidial.cfg.service.Assertions.assertUniqueDisplayName; -import static org.assertj.core.api.Assertions.assertThatThrownBy; - -class AssertionsTest { - - @Test - void testAssertUniqueDisplayName_DisplayNameEmpty() { - // given - CoreModel model = new CoreModel(); - model.setDisplayName("testModel"); - // when - assertUniqueDisplayName(Map.of("testModel", model), null); - // then - } - - @Test - void testAssertUniqueDisplayName_NewDisplayName() { - // given - CoreModel model = new CoreModel(); - model.setDisplayName("testModel"); - // when - assertUniqueDisplayName(Map.of("testModel", model), "newDisplayName"); - // then - } - - @Test - void testAssertUniqueDisplayName_DisplayNameIsNotUnique() { - // given - CoreModel model = new CoreModel(); - model.setDisplayName("testModel"); - Map models = Map.of("testModel", model); - // when - assertThatThrownBy(() -> assertUniqueDisplayName(models, "testModel")) - // then - .isInstanceOf(ValidationException.class) - .hasMessageContaining("displayName is not unique"); - } - - @Test - void testAssertUniqueDisplayNameAndVersion_DisplayNameEmptyAndDisplayVersionEmpty() { - // given - CoreModel model = new CoreModel(); - model.setDisplayName("testModel"); - model.setDisplayVersion("1.0.0"); - Map models = Map.of("testModel", model); - // when - Assertions.assertUniqueDisplayNameAndVersion(models, null, null); - // then - } - - @Test - void testAssertUniqueDisplayNameAndVersion_DisplayNameEmptyAndDisplayVersionNotEmpty_Exception() { - // given - CoreModel model = new CoreModel(); - model.setDisplayVersion("1.0.0"); - Map models = Map.of("testModel", model); - // when - assertThatThrownBy(() -> Assertions.assertUniqueDisplayNameAndVersion(models, null, "1.0.0")) - // then - .isInstanceOf(ValidationException.class) - .hasMessageContaining("displayVersion is not unique"); - } - - @Test - void testAssertUniqueDisplayNameAndVersion_DisplayNameEmptyAndDisplayVersionNotEmpty_Success() { - // given - CoreModel model = new CoreModel(); - model.setDisplayVersion("1.0.0"); - Map models = Map.of("testModel", model); - // when - Assertions.assertUniqueDisplayNameAndVersion(models, null, "2.0.0"); - // then - } - - @Test - void testAssertUniqueDisplayNameAndVersion_DisplayNameNotEmptyAndDisplayVersionEmpty_Exception() { - // given - CoreModel model = new CoreModel(); - model.setDisplayName("testModel"); - Map models = Map.of("testModel", model); - // when - assertThatThrownBy(() -> Assertions.assertUniqueDisplayNameAndVersion(models, "testModel", null)) - // then - .isInstanceOf(ValidationException.class) - .hasMessageContaining("displayName is not unique"); - } - - @Test - void testAssertUniqueDisplayNameAndVersion_DisplayNameNotEmptyAndDisplayVersionEmpty_Success() { - // given - CoreModel model = new CoreModel(); - model.setDisplayName("testModel"); - model.setDisplayVersion("1.0.0"); - Map models = Map.of("testModel", model); - // when - Assertions.assertUniqueDisplayNameAndVersion(models, "testModel", null); - // then - } - - @Test - void testAssertUniqueDisplayNameAndVersion_DisplayNameNotEmptyAndDisplayVersionNotEmpty_Exception() { - // given - CoreModel model = new CoreModel(); - model.setDisplayName("testModel"); - model.setDisplayVersion("1.0.0"); - Map models = Map.of("testModel", model); - // when - assertThatThrownBy(() -> Assertions.assertUniqueDisplayNameAndVersion(models, "testModel", "1.0.0")) - // then - .isInstanceOf(ValidationException.class) - .hasMessageContaining("displayName and displayVersion are not unique"); - } - - @Test - void testAssertUniqueDisplayNameAndVersion_DisplayNameNotEmptyAndDisplayVersionNotEmpty_Success() { - // given - CoreModel model = new CoreModel(); - model.setDisplayName("testModel"); - model.setDisplayVersion("1.0.0"); - Map models = Map.of("testModel", model); - // when - Assertions.assertUniqueDisplayNameAndVersion(models, "testModel", "2.0.0"); - // then - } -} \ No newline at end of file diff --git a/src/test/java/com/epam/aidial/cfg/service/CoreDeploymentServiceTest.java b/src/test/java/com/epam/aidial/cfg/service/CoreDeploymentServiceTest.java index 4cef4f1ec..211459ed5 100644 --- a/src/test/java/com/epam/aidial/cfg/service/CoreDeploymentServiceTest.java +++ b/src/test/java/com/epam/aidial/cfg/service/CoreDeploymentServiceTest.java @@ -3,6 +3,7 @@ import com.epam.aidial.cfg.client.DeploymentClient; import com.epam.aidial.cfg.client.dto.ApplicationDataDto; import com.epam.aidial.cfg.client.dto.DeploymentDataDto; +import com.epam.aidial.cfg.client.dto.LocalizedValueDto; import com.epam.aidial.cfg.client.dto.ModelCapabilitiesDataDto; import com.epam.aidial.cfg.client.dto.ModelDataDto; import com.epam.aidial.cfg.client.dto.ModelLimitsDataDto; @@ -105,7 +106,7 @@ void listDeploymentsShouldMapMixedDeploymentTypesFromCore() { var modelDto = ModelDataDto.builder() .id("gpt-4") .object("dial-model") - .displayName("GPT-4") + .displayName(LocalizedValueDto.of("GPT-4")) .capabilities(ModelCapabilitiesDataDto.builder().chatCompletion(true).build()) .limits(ModelLimitsDataDto.builder().maxPromptTokens(128000).build()) .pricing(ModelPricingDataDto.builder().unit("token").prompt("0.1").build()) @@ -113,14 +114,14 @@ void listDeploymentsShouldMapMixedDeploymentTypesFromCore() { var applicationDto = ApplicationDataDto.builder() .id("my-app") .object("dial-application") - .displayName("My App") + .displayName(LocalizedValueDto.of("My App")) .applicationTypeSchemaId("schema-1") .applicationProperties(Map.of("key", "value")) .build(); var toolsetDto = ToolSetDataDto.builder() .id("my-toolset") .object("dial-toolset") - .displayName("My Toolset") + .displayName(LocalizedValueDto.of("My Toolset")) .transport("streamable-http") .allowedTools(List.of("tool-a")) .build(); diff --git a/src/test/java/com/epam/aidial/cfg/service/ToolSetEximServiceTest.java b/src/test/java/com/epam/aidial/cfg/service/ToolSetEximServiceTest.java index d30919c0e..6766e0ab4 100644 --- a/src/test/java/com/epam/aidial/cfg/service/ToolSetEximServiceTest.java +++ b/src/test/java/com/epam/aidial/cfg/service/ToolSetEximServiceTest.java @@ -1,14 +1,18 @@ package com.epam.aidial.cfg.service; +import com.epam.aidial.cfg.client.mapper.LocalizedValueClientMapper; import com.epam.aidial.cfg.client.mapper.RouteMapperImpl; import com.epam.aidial.cfg.client.mapper.ToolSetClientMapperImpl; import com.epam.aidial.cfg.configuration.JsonMapperConfiguration; +import com.epam.aidial.cfg.dto.LocalizedValueDto; import com.epam.aidial.cfg.dto.ToolSetEximDto; import com.epam.aidial.cfg.dto.ToolSetsEximDto; +import com.epam.aidial.cfg.mapper.LocalizedValueMapper; import com.epam.aidial.cfg.model.CreateToolSetResource; import com.epam.aidial.cfg.model.ImportConflictResolutionStrategy; import com.epam.aidial.cfg.model.ImportResources; import com.epam.aidial.cfg.model.ImportResourcesStatus; +import com.epam.aidial.cfg.model.LocalizedValue; import com.epam.aidial.cfg.model.NodeType; import com.epam.aidial.cfg.model.Rule; import com.epam.aidial.cfg.model.RuleFunction; @@ -28,6 +32,7 @@ import java.util.Collections; import java.util.List; +import java.util.Map; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -46,7 +51,9 @@ ToolSetClientMapperImpl.class, ToolSetEximService.class, ResourceImportValidator.class, - RouteMapperImpl.class + RouteMapperImpl.class, + LocalizedValueClientMapper.class, + LocalizedValueMapper.class }) @TestPropertySource(properties = { "toolsets.import.consecutiveErrorsThreshold=2" @@ -78,9 +85,9 @@ void exportToolSets_SinglePath() { assertThat(result.getToolSets()).hasSize(1); var toolSetExim = result.getToolSets().get(0); - assertThat(toolSetExim.getDisplayName()).isEqualTo("toolSet1"); + assertThat(toolSetExim.getDisplayName()).isEqualTo(LocalizedValue.of("toolSet1")); assertThat(toolSetExim.getFolderId()).isEqualTo("public/"); - assertThat(toolSetExim.getDescription()).isEqualTo("toolSet description 1"); + assertThat(toolSetExim.getDescription()).isEqualTo(LocalizedValue.of("toolSet description 1")); assertThat(toolSetExim.isForwardPerRequestKey()).isTrue(); } @@ -108,14 +115,14 @@ void exportToolSets_MultiplePaths() { var toolSetExim1 = result.getToolSets().get(0); assertThat(toolSetExim1.getName()).isEqualTo("toolSet1"); assertThat(toolSetExim1.getFolderId()).isEqualTo("public/"); - assertThat(toolSetExim1.getDescription()).isEqualTo("toolSet description 1"); + assertThat(toolSetExim1.getDescription()).isEqualTo(LocalizedValue.of("toolSet description 1")); assertThat(toolSetExim1.isForwardPerRequestKey()).isTrue(); // Verify second toolSet1Exim1 var toolSetExim2 = result.getToolSets().get(1); assertThat(toolSetExim2.getName()).isEqualTo("toolSet2"); assertThat(toolSetExim2.getFolderId()).isEqualTo("public/"); - assertThat(toolSetExim2.getDescription()).isEqualTo("toolSet description 2"); + assertThat(toolSetExim2.getDescription()).isEqualTo(LocalizedValue.of("toolSet description 2")); assertThat(toolSetExim2.isForwardPerRequestKey()).isTrue(); } @@ -244,6 +251,7 @@ void importToolSets_NotFlatImport() { .build(); var toolSetExim = getToolSetEximDto("1"); + toolSetExim.setIntro(LocalizedValueDto.of(Map.of("en", "intro en", "de", "intro en"))); var toolSetsExim = new ToolSetsEximDto(); toolSetsExim.setToolSets(List.of(toolSetExim)); @@ -272,7 +280,8 @@ void importToolSets_NotFlatImport() { assertThat(toolSetEximResource.getName()).isEqualTo("toolSet1"); assertThat(toolSetEximResource.getVersion()).isEqualTo("0.0.1"); assertThat(toolSetEximResource.getFolderId()).isEqualTo("public/to/folder1/"); - assertThat(toolSetEximResource.getDescription()).isEqualTo("toolSet description 1"); + assertThat(toolSetEximResource.getDescription()).isEqualTo(LocalizedValue.of("toolSet description 1")); + assertThat(toolSetEximResource.getIntro()).isEqualTo(LocalizedValue.of(Map.of("en", "intro en", "de", "intro en"))); } @Test @@ -322,7 +331,7 @@ void importToolSets_FlatImport() { assertThat(toolSet.getName()).isEqualTo("toolSet1"); assertThat(toolSet.getVersion()).isEqualTo("0.0.1"); assertThat(toolSet.getFolderId()).isEqualTo("public/to/"); - assertThat(toolSet.getDescription()).isEqualTo("toolSet description 1"); + assertThat(toolSet.getDescription()).isEqualTo(LocalizedValue.of("toolSet description 1")); assertThat(toolSet.isForwardPerRequestKey()).isFalse(); } @@ -372,11 +381,11 @@ void importToolSets_UniquenessConflict_ImportsNonDuplicateItems() { private ToolSetResource getToolSetResource(String suffix) { var toolSet = new ToolSetResource(); toolSet.setName("toolSet" + suffix); - toolSet.setDisplayName("toolSet" + suffix); + toolSet.setDisplayName(LocalizedValue.of("toolSet" + suffix)); toolSet.setVersion(String.format("0.0.%s", suffix)); toolSet.setFolderId(String.format("public/folder%s/", suffix)); toolSet.setPath(String.format("%s%s__%s", toolSet.getFolderId(), toolSet.getName(), toolSet.getVersion())); - toolSet.setDescription(String.format("toolSet description %s", suffix)); + toolSet.setDescription(LocalizedValue.of(String.format("toolSet description %s", suffix))); toolSet.setForwardPerRequestKey(true); return toolSet; } @@ -385,9 +394,9 @@ private ToolSetEximDto getToolSetEximDto(String suffix) { return ToolSetEximDto.builder() .name("toolSet" + suffix) .version(String.format("0.0.%s", suffix)) - .displayName("toolSet" + suffix) + .displayName(LocalizedValueDto.of("toolSet" + suffix)) .folderId(String.format("public/folder%s/", suffix)) - .description(String.format("toolSet description %s", suffix)) + .description(LocalizedValueDto.of(String.format("toolSet description %s", suffix))) .forwardPerRequestKey(false) .build(); } diff --git a/src/test/java/com/epam/aidial/cfg/service/ZipApplicationEximServiceTest.java b/src/test/java/com/epam/aidial/cfg/service/ZipApplicationEximServiceTest.java index 9fbcc09a3..5fc73ddd2 100644 --- a/src/test/java/com/epam/aidial/cfg/service/ZipApplicationEximServiceTest.java +++ b/src/test/java/com/epam/aidial/cfg/service/ZipApplicationEximServiceTest.java @@ -1,10 +1,13 @@ package com.epam.aidial.cfg.service; import com.epam.aidial.cfg.client.mapper.ApplicationClientMapperImpl; +import com.epam.aidial.cfg.client.mapper.LocalizedValueClientMapper; import com.epam.aidial.cfg.client.mapper.RouteMapperImpl; import com.epam.aidial.cfg.configuration.JsonMapperConfiguration; import com.epam.aidial.cfg.dto.ApplicationEximDto; import com.epam.aidial.cfg.dto.ApplicationsEximDto; +import com.epam.aidial.cfg.dto.LocalizedValueDto; +import com.epam.aidial.cfg.mapper.LocalizedValueMapper; import com.epam.aidial.cfg.model.ApplicationResource; import com.epam.aidial.cfg.model.ImportConflictResolutionStrategy; import com.epam.aidial.cfg.model.ImportResourcePreview; @@ -13,6 +16,7 @@ import com.epam.aidial.cfg.model.ImportResourcesPreview; import com.epam.aidial.cfg.model.ImportResourcesResult; import com.epam.aidial.cfg.model.ImportResourcesStatus; +import com.epam.aidial.cfg.model.LocalizedValue; import com.fasterxml.jackson.databind.ObjectMapper; import lombok.SneakyThrows; import org.apache.commons.lang3.tuple.Pair; @@ -43,7 +47,9 @@ ApplicationClientMapperImpl.class, ZipApplicationEximService.class, ResourceImportValidator.class, - RouteMapperImpl.class + RouteMapperImpl.class, + LocalizedValueClientMapper.class, + LocalizedValueMapper.class }) class ZipApplicationEximServiceTest { @@ -174,7 +180,7 @@ void importZipApplications_ZipContainConflictingApplicationsAcrossFiles_ThrowErr var application1 = getApplicationEximDto("1"); var application2 = getApplicationEximDto("1"); - application2.setDescription("changed description"); + application2.setDescription(LocalizedValueDto.of("changed description")); var inputStream = getZipInputStream(List.of( Pair.of("applications/application1.json", ApplicationsEximDto.builder() @@ -213,8 +219,8 @@ void importZipApplications_ZipContainConflictingApplicationsWithinAndAcrossFiles var application1 = getApplicationEximDto("1"); var application2 = getApplicationEximDto("1"); var application3 = getApplicationEximDto("1"); - application2.setDescription("changed description 2"); - application3.setDescription("changed description 3"); + application2.setDescription(LocalizedValueDto.of("changed description 2")); + application3.setDescription(LocalizedValueDto.of("changed description 3")); var inputStream = getZipInputStream(List.of( Pair.of("applications/application1.json", ApplicationsEximDto.builder() @@ -384,11 +390,11 @@ private ApplicationResource getApplicationResource(String suffix) { var application = new ApplicationResource(); application.setApplicationTypeSchemaId(String.format("https://test%s.epam.com", suffix)); application.setName("application" + suffix); - application.setDisplayName("application" + suffix); + application.setDisplayName(LocalizedValue.of("application" + suffix)); application.setVersion(String.format("0.0.%s", suffix)); application.setFolderId(String.format("public/folder%s/", suffix)); application.setPath(String.format("%s/%s__%s", application.getFolderId(), application.getName(), application.getVersion())); - application.setDescription(String.format("application description %s", suffix)); + application.setDescription(LocalizedValue.of(String.format("application description %s", suffix))); return application; } @@ -397,9 +403,9 @@ private ApplicationEximDto getApplicationEximDto(String suffix) { .applicationTypeSchemaId(String.format("https://test%s.epam.com", suffix)) .name("application" + suffix) .version(String.format("0.0.%s", suffix)) - .displayName("application" + suffix) + .displayName(LocalizedValueDto.of("application" + suffix)) .folderId(String.format("public/folder%s/", suffix)) - .description(String.format("application description %s", suffix)) + .description(LocalizedValueDto.of(String.format("application description %s", suffix))) .build(); } diff --git a/src/test/java/com/epam/aidial/cfg/service/ZipToolSetEximServiceTest.java b/src/test/java/com/epam/aidial/cfg/service/ZipToolSetEximServiceTest.java index c5961803d..32116efe3 100644 --- a/src/test/java/com/epam/aidial/cfg/service/ZipToolSetEximServiceTest.java +++ b/src/test/java/com/epam/aidial/cfg/service/ZipToolSetEximServiceTest.java @@ -1,10 +1,13 @@ package com.epam.aidial.cfg.service; +import com.epam.aidial.cfg.client.mapper.LocalizedValueClientMapper; import com.epam.aidial.cfg.client.mapper.RouteMapperImpl; import com.epam.aidial.cfg.client.mapper.ToolSetClientMapperImpl; import com.epam.aidial.cfg.configuration.JsonMapperConfiguration; +import com.epam.aidial.cfg.dto.LocalizedValueDto; import com.epam.aidial.cfg.dto.ToolSetEximDto; import com.epam.aidial.cfg.dto.ToolSetsEximDto; +import com.epam.aidial.cfg.mapper.LocalizedValueMapper; import com.epam.aidial.cfg.model.ImportConflictResolutionStrategy; import com.epam.aidial.cfg.model.ImportResourcePreview; import com.epam.aidial.cfg.model.ImportResources; @@ -12,6 +15,7 @@ import com.epam.aidial.cfg.model.ImportResourcesPreview; import com.epam.aidial.cfg.model.ImportResourcesResult; import com.epam.aidial.cfg.model.ImportResourcesStatus; +import com.epam.aidial.cfg.model.LocalizedValue; import com.epam.aidial.cfg.model.ToolSetResource; import com.fasterxml.jackson.databind.ObjectMapper; import lombok.SneakyThrows; @@ -43,7 +47,9 @@ ToolSetClientMapperImpl.class, ZipToolSetEximService.class, ResourceImportValidator.class, - RouteMapperImpl.class + RouteMapperImpl.class, + LocalizedValueClientMapper.class, + LocalizedValueMapper.class }) class ZipToolSetEximServiceTest { @@ -174,7 +180,7 @@ void importZipToolSets_ZipContainConflictingToolSetsAcrossFiles_ThrowError() { var toolSet1 = getToolSetEximDto("1"); var toolSet2 = getToolSetEximDto("1"); - toolSet2.setDescription("changed description"); + toolSet2.setDescription(LocalizedValueDto.of("changed description")); var inputStream = getZipInputStream(List.of( Pair.of("toolSets/toolSet1.json", ToolSetsEximDto.builder() @@ -213,8 +219,8 @@ void importZipToolSets_ZipContainConflictingToolSetsWithinAndAcrossFiles_ThrowEr var toolSet1 = getToolSetEximDto("1"); var toolSet2 = getToolSetEximDto("1"); var toolSet3 = getToolSetEximDto("1"); - toolSet2.setDescription("changed description 2"); - toolSet3.setDescription("changed description 3"); + toolSet2.setDescription(LocalizedValueDto.of("changed description 2")); + toolSet3.setDescription(LocalizedValueDto.of("changed description 3")); var inputStream = getZipInputStream(List.of( Pair.of("toolSets/toolSet1.json", ToolSetsEximDto.builder() @@ -383,11 +389,11 @@ private InputStream getZipWithRawEntry(String entryPath, byte[] rawContent) { private ToolSetResource getToolSetResource(String suffix) { var toolSet = new ToolSetResource(); toolSet.setName("toolSet" + suffix); - toolSet.setDisplayName("toolSet" + suffix); + toolSet.setDisplayName(LocalizedValue.of("toolSet" + suffix)); toolSet.setVersion(String.format("0.0.%s", suffix)); toolSet.setFolderId(String.format("public/folder%s/", suffix)); toolSet.setPath(String.format("%s/%s__%s", toolSet.getFolderId(), toolSet.getName(), toolSet.getVersion())); - toolSet.setDescription(String.format("toolSet description %s", suffix)); + toolSet.setDescription(LocalizedValue.of(String.format("toolSet description %s", suffix))); return toolSet; } @@ -395,9 +401,9 @@ private ToolSetEximDto getToolSetEximDto(String suffix) { return ToolSetEximDto.builder() .name("toolSet" + suffix) .version(String.format("0.0.%s", suffix)) - .displayName("toolSet" + suffix) + .displayName(LocalizedValueDto.of("toolSet" + suffix)) .folderId(String.format("public/folder%s/", suffix)) - .description(String.format("toolSet description %s", suffix)) + .description(LocalizedValueDto.of(String.format("toolSet description %s", suffix))) .build(); } diff --git a/src/test/java/com/epam/aidial/cfg/service/config/export/ConfigExportServiceSecuredImplTest.java b/src/test/java/com/epam/aidial/cfg/service/config/export/ConfigExportServiceSecuredImplTest.java index b34e3fcf6..31be1ebcf 100644 --- a/src/test/java/com/epam/aidial/cfg/service/config/export/ConfigExportServiceSecuredImplTest.java +++ b/src/test/java/com/epam/aidial/cfg/service/config/export/ConfigExportServiceSecuredImplTest.java @@ -7,6 +7,7 @@ import com.epam.aidial.core.config.CoreAuthenticationType; import com.epam.aidial.core.config.CoreExternalService; import com.epam.aidial.core.config.CoreKey; +import com.epam.aidial.core.config.CoreLocalizedValue; import com.epam.aidial.core.config.CoreModel; import com.epam.aidial.core.config.CoreResourceAuthSettings; import com.epam.aidial.core.config.CoreRole; @@ -322,7 +323,7 @@ private Config createTestConfig() { Map models = new HashMap<>(); CoreModel model = new CoreModel(); model.setName("model1"); - model.setDisplayName("Test Model"); + model.setDisplayName(CoreLocalizedValue.of("Test Model")); CoreUpstream upstream = new CoreUpstream(); upstream.setEndpoint("https://api.example.com"); @@ -336,7 +337,7 @@ private Config createTestConfig() { Map toolsets = new HashMap<>(); CoreToolSet toolSet = new CoreToolSet(); toolSet.setName("toolset1"); - toolSet.setDisplayName("Test ToolSet"); + toolSet.setDisplayName(CoreLocalizedValue.of("Test ToolSet")); CoreResourceAuthSettings authSettings = new CoreResourceAuthSettings(); authSettings.setClientSecret("client-secret-1"); @@ -354,8 +355,8 @@ private Config createMixedSecretsConfig() { // Add a second model with both secret and non-secret data CoreModel model2 = new CoreModel(); model2.setName("model2"); - model2.setDisplayName("Test Model 2"); - + model2.setDisplayName(CoreLocalizedValue.of("Test Model 2")); + CoreUpstream upstream2 = new CoreUpstream(); upstream2.setEndpoint("https://api2.example.com"); model2.setUpstreams(List.of(upstream2)); @@ -365,7 +366,7 @@ private Config createMixedSecretsConfig() { // Add a toolset without secrets CoreToolSet toolSet2 = new CoreToolSet(); toolSet2.setName("toolset2"); - toolSet2.setDisplayName("Test ToolSet 2"); + toolSet2.setDisplayName(CoreLocalizedValue.of("Test ToolSet 2")); toolSet2.setAuthSettings(new CoreResourceAuthSettings()); config.getToolsets().put("toolset2", toolSet2); diff --git a/src/test/java/com/epam/aidial/cfg/service/config/export/ConfigServiceTest.java b/src/test/java/com/epam/aidial/cfg/service/config/export/ConfigServiceTest.java index 1b9072170..778385b75 100644 --- a/src/test/java/com/epam/aidial/cfg/service/config/export/ConfigServiceTest.java +++ b/src/test/java/com/epam/aidial/cfg/service/config/export/ConfigServiceTest.java @@ -34,6 +34,7 @@ import com.epam.aidial.cfg.domain.service.ToolSetService; import com.epam.aidial.cfg.domain.utils.ModelEndpointUtils; import com.epam.aidial.cfg.utils.ResourceUtils; +import com.epam.aidial.cfg.web.facade.mapper.LocalizedValueDtoMapper; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; @@ -64,7 +65,8 @@ DeploymentEntityMapperImpl.class, UpstreamEntityMapperImpl.class, LimitEntityMapperImpl.class, - ResourceAuthSettingsEntityMapperImpl.class + ResourceAuthSettingsEntityMapperImpl.class, + LocalizedValueDtoMapper.class }) class ConfigServiceTest { diff --git a/src/test/java/com/epam/aidial/cfg/service/config/transfer/JsonConfigMergerTest.java b/src/test/java/com/epam/aidial/cfg/service/config/transfer/JsonConfigMergerTest.java index 5a977d09d..69ee3bb61 100644 --- a/src/test/java/com/epam/aidial/cfg/service/config/transfer/JsonConfigMergerTest.java +++ b/src/test/java/com/epam/aidial/cfg/service/config/transfer/JsonConfigMergerTest.java @@ -29,7 +29,7 @@ void laterFileWins_onSameModelKey() throws Exception { Path file1 = tempFile("{\"models\":{\"gpt-4\":{\"displayName\":\"old\",\"endpoint\":\"https://old\"}}}"); Path file2 = tempFile("{\"models\":{\"gpt-4\":{\"displayName\":\"new\",\"endpoint\":\"https://new\"}}}"); Config result = merger.merge(List.of(file1.toString(), file2.toString())); - assertThat(result.getModels().get("gpt-4").getDisplayName()).isEqualTo("new"); + assertThat(result.getModels().get("gpt-4").getDisplayName().getPlainValue()).isEqualTo("new"); } @Test diff --git a/src/test/java/com/epam/aidial/cfg/web/controller/none/ApplicationResourceControllerTest.java b/src/test/java/com/epam/aidial/cfg/web/controller/none/ApplicationResourceControllerTest.java index 87a151f83..608b18c58 100644 --- a/src/test/java/com/epam/aidial/cfg/web/controller/none/ApplicationResourceControllerTest.java +++ b/src/test/java/com/epam/aidial/cfg/web/controller/none/ApplicationResourceControllerTest.java @@ -10,6 +10,7 @@ import com.epam.aidial.cfg.exception.ResourceNotFoundException; import com.epam.aidial.cfg.exception.ResourcePreconditionFailedException; import com.epam.aidial.cfg.mapper.ApplicationResourceMapperImpl; +import com.epam.aidial.cfg.mapper.LocalizedValueMapper; import com.epam.aidial.cfg.mapper.ResourceMapperImpl; import com.epam.aidial.cfg.model.ApplicationResource; import com.epam.aidial.cfg.model.ApplicationResourceNodeInfo; @@ -53,7 +54,8 @@ JsonMapperConfiguration.class, ApplicationResourceMapperImpl.class, RouteMapperImpl.class, - ResourceMapperImpl.class + ResourceMapperImpl.class, + LocalizedValueMapper.class }) class ApplicationResourceControllerTest extends AbstractControllerNoneSecureTest { private static final String DTO_JSON_BASE_PATH = "/application-resources/"; diff --git a/src/test/java/com/epam/aidial/cfg/web/controller/none/ConfigControllerTest.java b/src/test/java/com/epam/aidial/cfg/web/controller/none/ConfigControllerTest.java index 18cd0c687..5440394b5 100644 --- a/src/test/java/com/epam/aidial/cfg/web/controller/none/ConfigControllerTest.java +++ b/src/test/java/com/epam/aidial/cfg/web/controller/none/ConfigControllerTest.java @@ -11,6 +11,7 @@ import com.epam.aidial.cfg.domain.model.ExportKeyInfo; import com.epam.aidial.cfg.domain.model.ImportComponent; import com.epam.aidial.cfg.domain.model.ImportConfigPreview; +import com.epam.aidial.cfg.domain.model.LocalizedValue; import com.epam.aidial.cfg.domain.model.Model; import com.epam.aidial.cfg.domain.utils.ModelEndpointUtils; import com.epam.aidial.cfg.dto.ExportConfigComponentTypeDto; @@ -41,6 +42,7 @@ import com.epam.aidial.cfg.web.facade.mapper.InterceptorSourceDtoMapperImpl; import com.epam.aidial.cfg.web.facade.mapper.KeyDtoMapperImpl; import com.epam.aidial.cfg.web.facade.mapper.LimitDtoMapperImpl; +import com.epam.aidial.cfg.web.facade.mapper.LocalizedValueDtoMapper; import com.epam.aidial.cfg.web.facade.mapper.ModelDtoMapperImpl; import com.epam.aidial.cfg.web.facade.mapper.ModelSourceDtoMapperImpl; import com.epam.aidial.cfg.web.facade.mapper.ResourceAuthSettingsDtoMapperImpl; @@ -92,7 +94,8 @@ InterceptorSourceDtoMapperImpl.class, InstantMapperImpl.class, FeaturesDtoMapperImpl.class, AttachmentPathDtoMapperImpl.class, DeploymentInterfaceDtoMapperImpl.class, ToolSetDtoMapperImpl.class, ModelSourceDtoMapperImpl.class, ResourceAuthSettingsDtoMapperImpl.class, CostLimitDtoMapperImpl.class, - ToolSetSourceDtoMapperImpl.class, ValidityStateDtoMapperImpl.class, AdapterSourceDtoMapperImpl.class, ApplicationSourceDtoMapperImpl.class + ToolSetSourceDtoMapperImpl.class, ValidityStateDtoMapperImpl.class, AdapterSourceDtoMapperImpl.class, ApplicationSourceDtoMapperImpl.class, + LocalizedValueDtoMapper.class }) class ConfigControllerTest extends AbstractControllerNoneSecureTest { @@ -179,7 +182,7 @@ void testImportPreview() throws Exception { var model = new Model(); var deployment = new Deployment("testModel1"); model.setDeployment(deployment); - model.setDisplayName("testModel1"); + model.setDisplayName(LocalizedValue.of("testModel1")); model.setDisplayVersion("1.0.0"); model.setInterceptors(List.of("testInterceptor1")); model.setAuthor("test-author"); @@ -218,7 +221,7 @@ void testImportPreviewZip() throws Exception { var model = new Model(); var deployment = new Deployment("testModel1"); model.setDeployment(deployment); - model.setDisplayName("testModel1"); + model.setDisplayName(LocalizedValue.of("testModel1")); model.setDisplayVersion("1.0.0"); model.setInterceptors(List.of("testInterceptor1")); model.setAuthor("test-author"); @@ -296,8 +299,8 @@ void testExportPreview_Key_CoreFormat() throws Exception { ExportKeyInfo componentInfo = ExportKeyInfo.builder() .type(ExportConfigComponentType.KEY) .name("keyName") - .displayName("displayName") - .description("key description") + .displayName(LocalizedValue.of("displayName")) + .description(LocalizedValue.of("key description")) .roles(List.of("default")) .build(); @@ -324,8 +327,8 @@ void testExportPreview_ApplicationTypeSchema_CoreFormat() throws Exception { ExportApplicationTypeSchemaInfo componentInfo = ExportApplicationTypeSchemaInfo.builder() .type(ExportConfigComponentType.APPLICATION_TYPE_SCHEMA) .id("id") - .description("description") - .displayName("displayName") + .description(LocalizedValue.of("description")) + .displayName(LocalizedValue.of("displayName")) .build(); ExportConfigPreview exportConfigPreview = new ExportConfigPreview(); @@ -353,8 +356,8 @@ void testExportPreview_ToolSets_CoreFormat() throws Exception { ExportComponentInfo componentInfo = ExportComponentInfo.builder() .type(ExportConfigComponentType.TOOL_SET) .name("name1") - .description("description1") - .displayName("displayName1") + .description(LocalizedValue.of("description1")) + .displayName(LocalizedValue.of("displayName1")) .build(); ExportConfigPreview exportConfigPreview = new ExportConfigPreview(); @@ -387,8 +390,8 @@ void testExportPreview_InterceptorRunners_AdminFormat() throws Exception { ExportComponentInfo componentInfo = ExportComponentInfo.builder() .type(ExportConfigComponentType.INTERCEPTOR_RUNNER) .name("name1") - .description("description1") - .displayName("displayName1") + .description(LocalizedValue.of("description1")) + .displayName(LocalizedValue.of("displayName1")) .build(); ExportConfigPreview exportConfigPreview = new ExportConfigPreview(); diff --git a/src/test/java/com/epam/aidial/cfg/web/controller/none/ConversationControllerTest.java b/src/test/java/com/epam/aidial/cfg/web/controller/none/ConversationControllerTest.java index 7a4114645..bae136868 100644 --- a/src/test/java/com/epam/aidial/cfg/web/controller/none/ConversationControllerTest.java +++ b/src/test/java/com/epam/aidial/cfg/web/controller/none/ConversationControllerTest.java @@ -9,6 +9,7 @@ import com.epam.aidial.cfg.exception.ResourcePreconditionFailedException; import com.epam.aidial.cfg.mapper.ApplicationResourceMapperImpl; import com.epam.aidial.cfg.mapper.ConversationMapperImpl; +import com.epam.aidial.cfg.mapper.LocalizedValueMapper; import com.epam.aidial.cfg.mapper.PublicationMapperImpl; import com.epam.aidial.cfg.mapper.ResourceMapperImpl; import com.epam.aidial.cfg.model.Conversation; @@ -52,7 +53,8 @@ PublicationMapperImpl.class, RouteMapperImpl.class, ResourceMapperImpl.class, - ApplicationResourceMapperImpl.class + ApplicationResourceMapperImpl.class, + LocalizedValueMapper.class }) class ConversationControllerTest extends AbstractControllerNoneSecureTest { diff --git a/src/test/java/com/epam/aidial/cfg/web/controller/none/DeploymentControllerTest.java b/src/test/java/com/epam/aidial/cfg/web/controller/none/DeploymentControllerTest.java index 01756eab5..ecbf6e60f 100644 --- a/src/test/java/com/epam/aidial/cfg/web/controller/none/DeploymentControllerTest.java +++ b/src/test/java/com/epam/aidial/cfg/web/controller/none/DeploymentControllerTest.java @@ -1,6 +1,7 @@ package com.epam.aidial.cfg.web.controller.none; import com.epam.aidial.cfg.dto.ApplicationDataDto; +import com.epam.aidial.cfg.dto.LocalizedValueDto; import com.epam.aidial.cfg.dto.ModelCapabilitiesDataDto; import com.epam.aidial.cfg.dto.ModelDataDto; import com.epam.aidial.cfg.dto.ModelLimitsDataDto; @@ -12,6 +13,7 @@ import com.epam.aidial.cfg.model.ApplicationData; import com.epam.aidial.cfg.model.DeploymentType; import com.epam.aidial.cfg.model.InterfaceType; +import com.epam.aidial.cfg.model.LocalizedValue; import com.epam.aidial.cfg.model.ModelCapabilitiesData; import com.epam.aidial.cfg.model.ModelData; import com.epam.aidial.cfg.model.ModelLimitsData; @@ -83,12 +85,12 @@ void testListDeploymentsShouldReturnAllWhenNoFilter() throws Exception { var model = ModelData.builder() .id("gpt-4") .object("model") - .displayName("GPT-4") + .displayName(LocalizedValue.of("GPT-4")) .build(); var deployment = new ModelDataDto(); deployment.setId("gpt-4"); deployment.setObject("model"); - deployment.setDisplayName("GPT-4"); + deployment.setDisplayName(LocalizedValueDto.of("GPT-4")); when(coreDeploymentService.listDeployments(null, null)).thenReturn(List.of(model)); when(deploymentMapper.toDeploymentDataDtoList(List.of(model))).thenReturn(List.of(deployment)); diff --git a/src/test/java/com/epam/aidial/cfg/web/controller/none/PublicationControllerTest.java b/src/test/java/com/epam/aidial/cfg/web/controller/none/PublicationControllerTest.java index b07266234..98cd4f49a 100644 --- a/src/test/java/com/epam/aidial/cfg/web/controller/none/PublicationControllerTest.java +++ b/src/test/java/com/epam/aidial/cfg/web/controller/none/PublicationControllerTest.java @@ -7,6 +7,7 @@ import com.epam.aidial.cfg.exception.EntityNotFoundException; import com.epam.aidial.cfg.mapper.ApplicationResourceMapperImpl; import com.epam.aidial.cfg.mapper.FileMapperImpl; +import com.epam.aidial.cfg.mapper.LocalizedValueMapper; import com.epam.aidial.cfg.mapper.PublicationMapperImpl; import com.epam.aidial.cfg.model.ApplicationPublication; import com.epam.aidial.cfg.model.ConversationPublication; @@ -57,6 +58,7 @@ ApplicationResourceMapperImpl.class, RouteMapperImpl.class, FileMapperImpl.class, + LocalizedValueMapper.class, }) class PublicationControllerTest extends AbstractControllerNoneSecureTest { diff --git a/src/test/java/com/epam/aidial/cfg/web/controller/none/ToolSetResourceControllerTest.java b/src/test/java/com/epam/aidial/cfg/web/controller/none/ToolSetResourceControllerTest.java index 05919d1a1..b05508a71 100644 --- a/src/test/java/com/epam/aidial/cfg/web/controller/none/ToolSetResourceControllerTest.java +++ b/src/test/java/com/epam/aidial/cfg/web/controller/none/ToolSetResourceControllerTest.java @@ -5,6 +5,7 @@ import com.epam.aidial.cfg.dto.CallToolResourceRequestDto; import com.epam.aidial.cfg.dto.CreateToolSetResourceDto; import com.epam.aidial.cfg.dto.CredentialsLevelDto; +import com.epam.aidial.cfg.dto.LocalizedValueDto; import com.epam.aidial.cfg.dto.ResourcePathDto; import com.epam.aidial.cfg.dto.ResourcePathsDto; import com.epam.aidial.cfg.dto.ResourceSignInRequestDto; @@ -13,6 +14,7 @@ import com.epam.aidial.cfg.exception.NotModifiedException; import com.epam.aidial.cfg.exception.ResourceNotFoundException; import com.epam.aidial.cfg.exception.ResourcePreconditionFailedException; +import com.epam.aidial.cfg.mapper.LocalizedValueMapper; import com.epam.aidial.cfg.mapper.ResourceCredentialMapper; import com.epam.aidial.cfg.mapper.ResourceMapperImpl; import com.epam.aidial.cfg.mapper.ToolSetResourceMapperImpl; @@ -59,7 +61,8 @@ @Import({ JsonMapperConfiguration.class, ToolSetResourceMapperImpl.class, - ResourceMapperImpl.class + ResourceMapperImpl.class, + LocalizedValueMapper.class }) public class ToolSetResourceControllerTest extends AbstractControllerNoneSecureTest { @@ -263,7 +266,7 @@ void testCreateToolSetResourceWithEmptyDisplayName() throws Exception { // Test with empty displayName var dtoEmptyDisplayName = objectMapper.readValue(createToolSetDtoJson, CreateToolSetResourceDto.class); - dtoEmptyDisplayName.setDisplayName(""); + dtoEmptyDisplayName.setDisplayName(LocalizedValueDto.of("")); var jsonEmptyDisplayName = objectMapper.writeValueAsString(dtoEmptyDisplayName); mockMvc.perform(post(CREATE_API_PATH) diff --git a/src/test/resources/import/import_preview.json b/src/test/resources/import/import_preview.json index 7082de56d..e6efdf6ab 100644 --- a/src/test/resources/import/import_preview.json +++ b/src/test/resources/import/import_preview.json @@ -650,7 +650,7 @@ "interceptors": [ "testInterceptor1" ], - "topics": ["test1", "test2"], + "topics": ["test2", "test1"], "maxRetryAttempts": 5, "type": "embedding", "upstreams": [],