Elastic.SemanticKernel.Connectors.Elasticsearch/Internal/Helpers/VectorStoreRecordPropertyVerification.cs (126 lines of code) (raw):
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Reflection;
namespace Microsoft.Extensions.VectorData;
/// <summary>
/// Contains helpers for verifying the types of vector store record properties.
/// </summary>
[ExcludeFromCodeCoverage]
internal static class VectorStoreRecordPropertyVerification
{
/// <summary>
/// Verify that the given properties are of the supported types.
/// </summary>
/// <param name="properties">The properties to check.</param>
/// <param name="supportedTypes">A set of supported types that the provided properties may have.</param>
/// <param name="propertyCategoryDescription">A description of the category of properties being checked. Used for error messaging.</param>
/// <param name="supportEnumerable">A value indicating whether <see cref="IEnumerable{T}"/> versions of all the types should also be supported.</param>
/// <exception cref="ArgumentException">Thrown if any of the properties are not in the given set of types.</exception>
public static void VerifyPropertyTypes(List<PropertyInfo> properties, HashSet<Type> supportedTypes, string propertyCategoryDescription, bool? supportEnumerable = false)
{
var supportedEnumerableElementTypes = supportEnumerable == true
? supportedTypes
: [];
VerifyPropertyTypes(properties, supportedTypes, supportedEnumerableElementTypes, propertyCategoryDescription);
}
/// <summary>
/// Verify that the given properties are of the supported types.
/// </summary>
/// <param name="properties">The properties to check.</param>
/// <param name="supportedTypes">A set of supported types that the provided properties may have.</param>
/// <param name="supportedEnumerableElementTypes">A set of supported types that the provided enumerable properties may use as their element type.</param>
/// <param name="propertyCategoryDescription">A description of the category of properties being checked. Used for error messaging.</param>
/// <exception cref="ArgumentException">Thrown if any of the properties are not in the given set of types.</exception>
public static void VerifyPropertyTypes(List<PropertyInfo> properties, HashSet<Type> supportedTypes, HashSet<Type> supportedEnumerableElementTypes, string propertyCategoryDescription)
{
foreach (var property in properties)
{
VerifyPropertyType(property.Name, property.PropertyType, supportedTypes, supportedEnumerableElementTypes, propertyCategoryDescription);
}
}
/// <summary>
/// Verify that the given properties are of the supported types.
/// </summary>
/// <param name="properties">The properties to check.</param>
/// <param name="supportedTypes">A set of supported types that the provided properties may have.</param>
/// <param name="propertyCategoryDescription">A description of the category of properties being checked. Used for error messaging.</param>
/// <param name="supportEnumerable">A value indicating whether <see cref="IEnumerable{T}"/> versions of all the types should also be supported.</param>
/// <exception cref="ArgumentException">Thrown if any of the properties are not in the given set of types.</exception>
public static void VerifyPropertyTypes(IEnumerable<VectorStoreRecordProperty> properties, HashSet<Type> supportedTypes, string propertyCategoryDescription, bool? supportEnumerable = false)
{
var supportedEnumerableElementTypes = supportEnumerable == true
? supportedTypes
: [];
VerifyPropertyTypes(properties, supportedTypes, supportedEnumerableElementTypes, propertyCategoryDescription);
}
/// <summary>
/// Verify that the given properties are of the supported types.
/// </summary>
/// <param name="properties">The properties to check.</param>
/// <param name="supportedTypes">A set of supported types that the provided properties may have.</param>
/// <param name="supportedEnumerableElementTypes">A set of supported types that the provided enumerable properties may use as their element type.</param>
/// <param name="propertyCategoryDescription">A description of the category of properties being checked. Used for error messaging.</param>
/// <exception cref="ArgumentException">Thrown if any of the properties are not in the given set of types.</exception>
public static void VerifyPropertyTypes(IEnumerable<VectorStoreRecordProperty> properties, HashSet<Type> supportedTypes, HashSet<Type> supportedEnumerableElementTypes, string propertyCategoryDescription)
{
foreach (var property in properties)
{
VerifyPropertyType(property.DataModelPropertyName, property.PropertyType, supportedTypes, supportedEnumerableElementTypes, propertyCategoryDescription);
}
}
/// <summary>
/// Verify that the given property is of the supported types.
/// </summary>
/// <param name="propertyName">The name of the property being checked. Used for error messaging.</param>
/// <param name="propertyType">The type of the property being checked.</param>
/// <param name="supportedTypes">A set of supported types that the provided property may have.</param>
/// <param name="supportedEnumerableElementTypes">A set of supported types that the provided property may use as its element type if it's enumerable.</param>
/// <param name="propertyCategoryDescription">A description of the category of property being checked. Used for error messaging.</param>
/// <exception cref="ArgumentException">Thrown if the property is not in the given set of types.</exception>
public static void VerifyPropertyType(string propertyName, Type propertyType, HashSet<Type> supportedTypes, HashSet<Type> supportedEnumerableElementTypes, string propertyCategoryDescription)
{
// Add shortcut before testing all the more expensive scenarios.
if (supportedTypes.Contains(propertyType))
{
return;
}
// Check all collection scenarios and get stored type.
if (supportedEnumerableElementTypes.Count > 0 && IsSupportedEnumerableType(propertyType))
{
var typeToCheck = GetCollectionElementType(propertyType);
if (!supportedEnumerableElementTypes.Contains(typeToCheck))
{
var supportedEnumerableElementTypesString = string.Join(", ", supportedEnumerableElementTypes!.Select(t => t.FullName));
throw new ArgumentException($"Enumerable {propertyCategoryDescription} properties must have one of the supported element types: {supportedEnumerableElementTypesString}. Element type of the property '{propertyName}' is {typeToCheck.FullName}.");
}
}
else
{
// if we got here, we know the type is not supported
var supportedTypesString = string.Join(", ", supportedTypes.Select(t => t.FullName));
throw new ArgumentException($"{propertyCategoryDescription} properties must be one of the supported types: {supportedTypesString}. Type of the property '{propertyName}' is {propertyType.FullName}.");
}
}
/// <summary>
/// Verify if the provided type is one of the supported Enumerable types.
/// </summary>
/// <param name="type">The type to check.</param>
/// <returns><see langword="true"/> if the type is a supported Enumerable, <see langword="false"/> otherwise.</returns>
public static bool IsSupportedEnumerableType(Type type)
{
if (type.IsArray || type == typeof(IEnumerable))
{
return true;
}
if (typeof(IList).IsAssignableFrom(type) && type.GetConstructor([]) != null)
{
return true;
}
if (type.IsGenericType)
{
var genericTypeDefinition = type.GetGenericTypeDefinition();
if (genericTypeDefinition == typeof(ICollection<>) ||
genericTypeDefinition == typeof(IEnumerable<>) ||
genericTypeDefinition == typeof(IList<>) ||
genericTypeDefinition == typeof(IReadOnlyCollection<>) ||
genericTypeDefinition == typeof(IReadOnlyList<>))
{
return true;
}
}
return false;
}
/// <summary>
/// Returns <see cref="Type"/> of collection elements.
/// </summary>
public static Type GetCollectionElementType(Type collectionType)
{
return collectionType switch
{
IEnumerable => typeof(object),
var enumerableType when enumerableType.IsGenericType && enumerableType.GetGenericTypeDefinition() == typeof(IEnumerable<>) => enumerableType.GetGenericArguments()[0],
var arrayType when arrayType.IsArray => arrayType.GetElementType()!,
var interfaceType when interfaceType.GetInterfaces().FirstOrDefault(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IEnumerable<>)) is Type enumerableInterface =>
enumerableInterface.GetGenericArguments()[0],
_ => collectionType
};
}
/// <summary>
/// Checks that if the provided <paramref name="recordType"/> is a <see cref="VectorStoreGenericDataModel{T}"/> that the key type is supported by the default mappers.
/// If not supported, a custom mapper must be supplied, otherwise an exception is thrown.
/// </summary>
/// <param name="recordType">The type of the record data model used by the connector.</param>
/// <param name="customMapperSupplied">A value indicating whether a custom mapper was supplied to the connector</param>
/// <param name="allowedKeyTypes">The list of key types supported by the default mappers.</param>
/// <exception cref="ArgumentException">Thrown if the key type of the <see cref="VectorStoreGenericDataModel{T}"/> is not supported by the default mappers and a custom mapper was not supplied.</exception>
public static void VerifyGenericDataModelKeyType(Type recordType, bool customMapperSupplied, IEnumerable<Type> allowedKeyTypes)
{
// If we are not dealing with a generic data model, no need to check anything else.
if (!recordType.IsGenericType || recordType.GetGenericTypeDefinition() != typeof(VectorStoreGenericDataModel<>))
{
return;
}
// If the key type is supported, we are good.
var keyType = recordType.GetGenericArguments()[0];
if (allowedKeyTypes.Contains(keyType))
{
return;
}
// If the key type is not supported out of the box, but a custom mapper was supplied, we are good.
if (customMapperSupplied)
{
return;
}
throw new ArgumentException($"The key type '{keyType.FullName}' of data model '{nameof(VectorStoreGenericDataModel<string>)}' is not supported by the default mappers. " +
$"Only the following key types are supported: {string.Join(", ", allowedKeyTypes)}. Please provide your own mapper to map to your chosen key type.");
}
/// <summary>
/// Checks that if the provided <paramref name="recordType"/> is a <see cref="VectorStoreGenericDataModel{T}"/> that a <see cref="VectorStoreRecordDefinition"/> is also provided.
/// </summary>
/// <param name="recordType">The type of the record data model used by the connector.</param>
/// <param name="recordDefinitionSupplied">A value indicating whether a record definition was supplied to the connector.</param>
/// <exception cref="ArgumentException">Thrown if a <see cref="VectorStoreRecordDefinition"/> is not provided when using <see cref="VectorStoreGenericDataModel{T}"/>.</exception>
public static void VerifyGenericDataModelDefinitionSupplied(Type recordType, bool recordDefinitionSupplied)
{
// If we are not dealing with a generic data model, no need to check anything else.
if (!recordType.IsGenericType || recordType.GetGenericTypeDefinition() != typeof(VectorStoreGenericDataModel<>))
{
return;
}
// If we are dealing with a generic data model, and a record definition was supplied, we are good.
if (recordDefinitionSupplied)
{
return;
}
throw new ArgumentException($"A {nameof(VectorStoreRecordDefinition)} must be provided when using '{nameof(VectorStoreGenericDataModel<string>)}'.");
}
}