-
Notifications
You must be signed in to change notification settings - Fork 20
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add ability to easily get the owner and default property definitions …
…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
1 parent
dda4145
commit d933daa
Showing
5 changed files
with
605 additions
and
0 deletions.
There are no files selected for viewing
263 changes: 263 additions & 0 deletions
263
MFiles.VAF.Extensions.Tests/Configuration/MetadataStructureValidatorTests.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
104 changes: 104 additions & 0 deletions
104
MFiles.VAF.Extensions/Configuration/MetadataStructureValidator.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} | ||
} |
Oops, something went wrong.