Skip to content

Commit

Permalink
Add ability to easily get the owner and default property definitions …
Browse files Browse the repository at this point in the history
…from a configured object type (#106)

Example of how to use attributes in https://github.com/M-Files/VAF.Extensions.Community/pull/106/files#diff-323491481893748a795de66cee281136c36079db9a7697ae7231acd5d35ca617 (will be the main readme once merged.
  • Loading branch information
CraigHawker authored Jul 13, 2023
1 parent dda4145 commit d933daa
Show file tree
Hide file tree
Showing 5 changed files with 605 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,263 @@
using MFiles.VAF.Configuration;
using MFiles.VAF.Extensions.Configuration;
using MFilesAPI;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Runtime.Serialization;
using System.Text;
using System.Threading.Tasks;

namespace MFiles.VAF.Extensions.Tests.Configuration
{
[TestClass]
public class MetadataStructureValidatorTests
: TestBaseWithVaultMock
{

protected override Mock<Vault> GetVaultMock()
{
// Create the object type mock.
var objectTypeOperationsMock = new Mock<VaultObjectTypeOperations>();
objectTypeOperationsMock.Setup(m => m.GetObjectTypeIDByAlias(It.IsAny<string>()))
.Returns((string alias) =>
{
switch (alias?.ToLower()?.Trim())
{
case "hello_world":
return 1;
default:
return -1;
}
});
objectTypeOperationsMock.Setup(m => m.GetObjectType(It.IsAny<int>()))
.Returns((int id) =>
{
switch (id)
{
case 1:
{
var objTypeMock = new Mock<ObjType>();
objTypeMock.SetupAllProperties();
objTypeMock.Setup(m => m.ID).Returns(id);
objTypeMock.Setup(m => m.RealObjectType).Returns(true);
objTypeMock.Setup(m => m.DefaultPropertyDef).Returns(123);
objTypeMock.Setup(m => m.OwnerPropertyDef).Returns(321);
return objTypeMock.Object;
}
default:
throw new InvalidOperationException($"Object type with ID {id} was not mocked");
}
});

// Create the property def mock.
var propertyDefOperationsMock = new Mock<VaultPropertyDefOperations>();
propertyDefOperationsMock.Setup(m => m.GetPropertyDef(It.IsAny<int>()))
.Returns((int id) =>
{
switch (id)
{
case 123:
case 321:
{
var propertyDefMock = new Mock<PropertyDef>();
propertyDefMock.SetupAllProperties();
propertyDefMock.Setup(m => m.ID).Returns(id);
return propertyDefMock.Object;
}
default:
throw new InvalidOperationException($"Property def with ID {id} was not mocked");
}
});

// Return an updated vault mock.
var vaultMock = base.GetVaultMock();
vaultMock.SetupGet(m => m.ObjectTypeOperations).Returns(() => objectTypeOperationsMock.Object);
vaultMock.SetupGet(m => m.PropertyDefOperations).Returns(() => propertyDefOperationsMock.Object);
return vaultMock;
}

public virtual IMetadataStructureValidator GetMetadataStructureValidator()
{
return new MFiles.VAF.Extensions.Configuration.MetadataStructureValidator();
}

[DataContract]
class Configuration
{
[DataMember]
[MFObjType(AllowEmpty = true)]
public MFIdentifier ObjectType { get; set; }

[DefaultPropertyDef(nameof(ObjectType))]
public MFIdentifier DefaultPropertyDef { get; set; }

[OwnerPropertyDef(nameof(ObjectType))]
public MFIdentifier OwnerPropertyDef { get; set; }

[DataMember]
public Configuration SubConfiguration { get; set; }

}

[TestMethod]
public void HappyPath()
{
// The config should have a single valid object type defined.
// The default/owner properties will be driven from this.
var config = new Configuration()
{
ObjectType = "hello_world"
};
Assert.IsNull(config.DefaultPropertyDef);
Assert.IsNull(config.OwnerPropertyDef);

// Set up the required mocks and other constructs.
var vaultMock = this.GetVaultMock();
var validator = this.GetMetadataStructureValidator();
var validationResult = new ValidationResultForValidation();

// Check that the overall validation passed.
Assert.IsTrue
(
validator.ValidateItem
(
vaultMock.Object,
"MyConfigId",
config,
validationResult
)
);

// Check that we got our properties populated.
Assert.AreEqual(123, config.DefaultPropertyDef?.ID);
Assert.AreEqual(321, config.OwnerPropertyDef?.ID);
}

[TestMethod]
public void InvalidObjectTypeAlias()
{
// The config should have a single valid object type defined.
// The default/owner properties will be driven from this.
var config = new Configuration()
{
ObjectType = "invalidObjectTypeAlias"
};
Assert.IsNull(config.DefaultPropertyDef);
Assert.IsNull(config.OwnerPropertyDef);

// Set up the required mocks and other constructs.
var vaultMock = this.GetVaultMock();
var validator = this.GetMetadataStructureValidator();
var validationResult = new ValidationResultForValidation();

// Check that the overall validation failed due to the object type.
Assert.IsFalse
(
validator.ValidateItem
(
vaultMock.Object,
"MyConfigId",
config,
validationResult
)
);

// Check that our properties are empty.
Assert.IsNull(config.DefaultPropertyDef?.ID);
Assert.IsNull(config.OwnerPropertyDef?.ID);
}

[TestMethod]
public void SubConfiguration_HappyPath()
{
// The config should have a single valid object type defined.
// The default/owner properties will be driven from this.
var config = new Configuration()
{
SubConfiguration = new Configuration()
{
ObjectType = "hello_world"
}
};
Assert.IsNull(config.SubConfiguration.DefaultPropertyDef);
Assert.IsNull(config.SubConfiguration.OwnerPropertyDef);

// Set up the required mocks and other constructs.
var vaultMock = this.GetVaultMock();
var validator = this.GetMetadataStructureValidator();
var validationResult = new ValidationResultForValidation();

// Check that the overall validation passed.
Assert.IsTrue
(
validator.ValidateItem
(
vaultMock.Object,
"MyConfigId",
config,
validationResult
)
);

// Check that we got our properties populated.
Assert.AreEqual(123, config.SubConfiguration.DefaultPropertyDef?.ID);
Assert.AreEqual(321, config.SubConfiguration.OwnerPropertyDef?.ID);
}

[DataContract]
class ConfigurationWithField
{
[DataMember]
[MFObjType(AllowEmpty = true)]
public MFIdentifier ObjectType;

[DefaultPropertyDef(nameof(ObjectType))]
public MFIdentifier DefaultPropertyDef;

[OwnerPropertyDef(nameof(ObjectType))]
public MFIdentifier OwnerPropertyDef;

[DataMember]
public ConfigurationWithField SubConfiguration;

}

[TestMethod]
public void HappyPath_WithField()
{
// The config should have a single valid object type defined.
// The default/owner properties will be driven from this.
var config = new ConfigurationWithField()
{
ObjectType = "hello_world"
};
Assert.IsNull(config.DefaultPropertyDef);
Assert.IsNull(config.OwnerPropertyDef);

// Set up the required mocks and other constructs.
var vaultMock = this.GetVaultMock();
var validator = this.GetMetadataStructureValidator();
var validationResult = new ValidationResultForValidation();

// Check that the overall validation passed.
Assert.IsTrue
(
validator.ValidateItem
(
vaultMock.Object,
"MyConfigId",
config,
validationResult
)
);

// Check that we got our properties populated.
Assert.AreEqual(123, config.DefaultPropertyDef?.ID);
Assert.AreEqual(321, config.OwnerPropertyDef?.ID);
}
}
}
8 changes: 8 additions & 0 deletions MFiles.VAF.Extensions/ConfigurableVaultApplicationBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@
using MFiles.VAF.Extensions.Dashboards.DevelopmentDashboardContent;
using System.Threading.Tasks;
using MFiles.VAF.Extensions.Logging;
using MFiles.VAF.Configuration;
using MFiles.VAF.Extensions.Configuration;

namespace MFiles.VAF.Extensions
{
Expand Down Expand Up @@ -128,5 +130,11 @@ protected override void StartApplication()

base.StartApplication();
}

/// <inheritdoc />
protected override IMetadataStructureValidator CreateMetadataStructureValidator()
{
return new MFiles.VAF.Extensions.Configuration.MetadataStructureValidator();
}
}
}
104 changes: 104 additions & 0 deletions MFiles.VAF.Extensions/Configuration/MetadataStructureValidator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
using MFiles.VAF.Common;
using MFiles.VAF.Configuration;
using MFiles.VAF.Configuration.JsonAdaptor;
using MFiles.VAF.Configuration.Logging;
using MFiles.VAF.Configuration.Validation;
using MFilesAPI;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Resources;
using System.Runtime.Serialization;
using System.Text;
using System.Threading.Tasks;

namespace MFiles.VAF.Extensions.Configuration
{
internal class MetadataStructureValidator
: VAF.Configuration.MetadataStructureValidator
{
/// <summary>
/// The logger for the metadata structure validator.
/// </summary>
private ILogger Logger { get; } = LogManager.GetLogger(typeof(MetadataStructureValidator));

/// <inheritdoc />
public override bool ValidateItem(Vault vault, IConfiguration configuration, object item, ValidationResultBase validationResult, Assembly[] containingAssemblies = null, int level = 10)
{
// Suppress validation exceptions if the configuration is null
// (which can happen if it fails deserialization).
if (item == null)
{
validationResult.ReportCustomFailure
(
configuration,
MFMetadataStructureItem.MFMetadataStructureItemNone,
"",
"The configuration was not provided; possible deserialization error (check configuration class structure).",
true
);
this.Logger?.Warn($"The provided configuration was null; possible deserialization error (check configuration class structure).");
return true;
}
return base.ValidateItem(vault, configuration, item, validationResult, containingAssemblies, level);
}

/// <inheritdoc />
protected virtual void ResolveOwnerOrDefaultPropertyDefs(Vault vault, object item)
{
// Sanity.
if (item == null)
return;

// Find child properties/fields that might have the attributes we care about.
var children = this.GetChildren(item);
foreach ( var child in children )
{
// Sanity.
if (null == child)
continue;
this.Logger?.Trace($"Checking {child.DeclaringType?.FullName}.{child.Name} for OwnerOrDefaultPropertyDefAttribute attributes.");

// If it doesn't have the attribute we care about then skip.
if (!(child?.GetCustomAttribute(typeof(OwnerOrDefaultPropertyDefAttribute), true) is OwnerOrDefaultPropertyDefAttribute attr))
{
this.Logger?.Trace($"No attribute found; skipping");
continue;
}

// Try to resolve the value.
this.Logger?.Debug($"{attr.GetType().Name} attribute found on {child.DeclaringType?.FullName}.{child.Name}.");
var identifier = attr.Resolve(vault, item.GetType(), item);
if (null == identifier)
{
this.Logger?.Info($"Could not resolve the object type associated with {child.DeclaringType?.FullName}.{child.Name} (looked for populated configuration property named {attr.ObjectTypeReference}).");
continue;
}

// Set the value.
{
this.Logger?.Debug($"Setting {child.DeclaringType?.FullName}.{child.Name} to {identifier.ID}.");
if (child.MemberType == MemberTypes.Field)
((FieldInfo)child).SetValue(item, identifier);
if (child.MemberType == MemberTypes.Property)
((PropertyInfo)child).SetValue(item, identifier);
}
}
}

/// <inheritdoc />
protected override bool ValidateItemInternal(Vault vault, IConfiguration configuration, object item, ValidationResultBase validationResult, object parent, MemberInfo member, int level, Assembly[] containingAssemblies, HashSet<object> handledItems)
{
// Call the base implementation.
var retValue = base.ValidateItemInternal(vault, configuration, item, validationResult, parent, member, level, containingAssemblies, handledItems);

// Update the ones we care about.
this.ResolveOwnerOrDefaultPropertyDefs(vault, item);

// Return the base implementation return value.
return retValue;
}
}
}
Loading

0 comments on commit d933daa

Please sign in to comment.