Use OperationGoalState for intermediate steps in Install and GenerateGoalState
This commit is contained in:
Родитель
eb732f03b0
Коммит
4b79145702
|
@ -374,7 +374,13 @@ namespace Microsoft.Web.LibraryManager.Contracts
|
|||
&& normalizedFilePath.StartsWith(normalizedRootDirectory, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
internal static string NormalizePath(string path)
|
||||
/// <summary>
|
||||
/// Normalizes the path string so it can be easily compared.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Result will be lowercase and have any trailing slashes removed.
|
||||
/// </remarks>
|
||||
public static string NormalizePath(string path)
|
||||
{
|
||||
if (string.IsNullOrEmpty(path))
|
||||
{
|
||||
|
|
|
@ -17,11 +17,11 @@ namespace Microsoft.Web.LibraryManager.Contracts
|
|||
public static class PredefinedErrors
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents an unhandled exception that occured in the provider.
|
||||
/// Represents an unhandled exception that occurred in the provider.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// An <see cref="IProvider.InstallAsync"/> should never throw and this error
|
||||
/// should be used as when catching generic exeptions.
|
||||
/// should be used as when catching generic exceptions.
|
||||
/// </remarks>
|
||||
/// <returns>The error code LIB000</returns>
|
||||
public static IError UnknownException()
|
||||
|
@ -198,6 +198,12 @@ namespace Microsoft.Web.LibraryManager.Contracts
|
|||
public static IError FileNameMustNotBeEmpty(string libraryId)
|
||||
=> new Error("LIB020", string.Format(Text.ErrorFilePathIsEmpty, libraryId));
|
||||
|
||||
/// <summary>
|
||||
/// A library mapping does not have a destination specified
|
||||
/// </summary>
|
||||
public static IError DestinationNotSpecified(string libraryId)
|
||||
=> new Error("LIB021", string.Format(Text.ErrorDestinationNotSpecified, libraryId));
|
||||
|
||||
/// <summary>
|
||||
/// The manifest must specify a version
|
||||
/// </summary>
|
||||
|
|
|
@ -87,6 +87,15 @@ namespace Microsoft.Web.LibraryManager.Contracts.Resources {
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to The "{0}" library is missing a destination..
|
||||
/// </summary>
|
||||
internal static string ErrorDestinationNotSpecified {
|
||||
get {
|
||||
return ResourceManager.GetString("ErrorDestinationNotSpecified", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to The "{0}" destination file path has invalid characters.
|
||||
/// </summary>
|
||||
|
|
|
@ -1,17 +1,17 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<!--
|
||||
Microsoft ResX Schema
|
||||
|
||||
<!--
|
||||
Microsoft ResX Schema
|
||||
|
||||
Version 2.0
|
||||
|
||||
The primary goals of this format is to allow a simple XML format
|
||||
that is mostly human readable. The generation and parsing of the
|
||||
various data types are done through the TypeConverter classes
|
||||
|
||||
The primary goals of this format is to allow a simple XML format
|
||||
that is mostly human readable. The generation and parsing of the
|
||||
various data types are done through the TypeConverter classes
|
||||
associated with the data types.
|
||||
|
||||
|
||||
Example:
|
||||
|
||||
|
||||
... ado.net/XML headers & schema ...
|
||||
<resheader name="resmimetype">text/microsoft-resx</resheader>
|
||||
<resheader name="version">2.0</resheader>
|
||||
|
@ -26,36 +26,36 @@
|
|||
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
|
||||
<comment>This is a comment</comment>
|
||||
</data>
|
||||
|
||||
There are any number of "resheader" rows that contain simple
|
||||
|
||||
There are any number of "resheader" rows that contain simple
|
||||
name/value pairs.
|
||||
|
||||
Each data row contains a name, and value. The row also contains a
|
||||
type or mimetype. Type corresponds to a .NET class that support
|
||||
text/value conversion through the TypeConverter architecture.
|
||||
Classes that don't support this are serialized and stored with the
|
||||
|
||||
Each data row contains a name, and value. The row also contains a
|
||||
type or mimetype. Type corresponds to a .NET class that support
|
||||
text/value conversion through the TypeConverter architecture.
|
||||
Classes that don't support this are serialized and stored with the
|
||||
mimetype set.
|
||||
|
||||
The mimetype is used for serialized objects, and tells the
|
||||
ResXResourceReader how to depersist the object. This is currently not
|
||||
|
||||
The mimetype is used for serialized objects, and tells the
|
||||
ResXResourceReader how to depersist the object. This is currently not
|
||||
extensible. For a given mimetype the value must be set accordingly:
|
||||
|
||||
Note - application/x-microsoft.net.object.binary.base64 is the format
|
||||
that the ResXResourceWriter will generate, however the reader can
|
||||
|
||||
Note - application/x-microsoft.net.object.binary.base64 is the format
|
||||
that the ResXResourceWriter will generate, however the reader can
|
||||
read any of the formats listed below.
|
||||
|
||||
|
||||
mimetype: application/x-microsoft.net.object.binary.base64
|
||||
value : The object must be serialized with
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
|
||||
mimetype: application/x-microsoft.net.object.soap.base64
|
||||
value : The object must be serialized with
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.bytearray.base64
|
||||
value : The object must be serialized into a byte array
|
||||
value : The object must be serialized into a byte array
|
||||
: using a System.ComponentModel.TypeConverter
|
||||
: and then encoded with base64 encoding.
|
||||
-->
|
||||
|
@ -187,6 +187,9 @@ Valid files are {2}</value>
|
|||
<data name="ErrorFilePathIsEmpty" xml:space="preserve">
|
||||
<value>The library "{0}" cannot specify a file with an empty name</value>
|
||||
</data>
|
||||
<data name="ErrorDestinationNotSpecified" xml:space="preserve">
|
||||
<value>The "{0}" library is missing a destination.</value>
|
||||
</data>
|
||||
<data name="ErrorMissingManifestVersion" xml:space="preserve">
|
||||
<value>The Library Manager manifest must specify a version.</value>
|
||||
</data>
|
||||
|
|
|
@ -60,10 +60,25 @@ namespace Microsoft.Web.LibraryManager.Providers
|
|||
return LibraryOperationResult.FromCancelled(desiredState);
|
||||
}
|
||||
|
||||
ILibraryCatalog catalog = GetCatalog();
|
||||
ILibrary library = await catalog.GetLibraryAsync(desiredState.Name, desiredState.Version, cancellationToken).ConfigureAwait(false);
|
||||
OperationResult<ILibrary> getLibrary = await GetLibraryForInstallationState(desiredState, cancellationToken).ConfigureAwait(false);
|
||||
if (!getLibrary.Success)
|
||||
{
|
||||
return new LibraryOperationResult(desiredState, [.. getLibrary.Errors])
|
||||
{
|
||||
Cancelled = getLibrary.Cancelled,
|
||||
};
|
||||
}
|
||||
|
||||
LibraryInstallationGoalState goalState = GenerateGoalState(desiredState, library);
|
||||
OperationResult<LibraryInstallationGoalState> getGoalState = GenerateGoalState(desiredState, getLibrary.Result);
|
||||
if (!getGoalState.Success)
|
||||
{
|
||||
return new LibraryOperationResult(desiredState, [.. getGoalState.Errors])
|
||||
{
|
||||
Cancelled = getGoalState.Cancelled,
|
||||
};
|
||||
}
|
||||
|
||||
LibraryInstallationGoalState goalState = getGoalState.Result;
|
||||
|
||||
if (!IsSourceCacheReady(goalState))
|
||||
{
|
||||
|
@ -83,8 +98,30 @@ namespace Microsoft.Web.LibraryManager.Providers
|
|||
|
||||
}
|
||||
|
||||
private async Task<LibraryOperationResult> InstallFiles(LibraryInstallationGoalState goalState, CancellationToken cancellationToken)
|
||||
private async Task<OperationResult<ILibrary>> GetLibraryForInstallationState(ILibraryInstallationState desiredState, CancellationToken cancellationToken)
|
||||
{
|
||||
ILibrary library;
|
||||
try
|
||||
{
|
||||
ILibraryCatalog catalog = GetCatalog();
|
||||
library = await catalog.GetLibraryAsync(desiredState.Name, desiredState.Version, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (InvalidLibraryException)
|
||||
{
|
||||
string libraryId = LibraryNamingScheme.GetLibraryId(desiredState.Name, desiredState.Version);
|
||||
return OperationResult<ILibrary>.FromError(PredefinedErrors.UnableToResolveSource(libraryId, desiredState.ProviderId));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
HostInteraction.Logger.Log(ex.ToString(), LogLevel.Error);
|
||||
return OperationResult<ILibrary>.FromError(PredefinedErrors.UnknownException());
|
||||
}
|
||||
|
||||
return OperationResult<ILibrary>.FromSuccess(library);
|
||||
}
|
||||
|
||||
private async Task<LibraryOperationResult> InstallFiles(LibraryInstallationGoalState goalState, CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
foreach (KeyValuePair<string, string> kvp in goalState.InstalledFiles)
|
||||
|
@ -196,9 +233,16 @@ namespace Microsoft.Web.LibraryManager.Providers
|
|||
|
||||
#endregion
|
||||
|
||||
public LibraryInstallationGoalState GenerateGoalState(ILibraryInstallationState desiredState, ILibrary library)
|
||||
private OperationResult<LibraryInstallationGoalState> GenerateGoalState(ILibraryInstallationState desiredState, ILibrary library)
|
||||
{
|
||||
var goalState = new LibraryInstallationGoalState(desiredState);
|
||||
List<IError> errors = null;
|
||||
|
||||
if (string.IsNullOrEmpty(desiredState.DestinationPath))
|
||||
{
|
||||
return OperationResult<LibraryInstallationGoalState>.FromError(PredefinedErrors.DestinationNotSpecified(desiredState.Name));
|
||||
}
|
||||
|
||||
IEnumerable<string> outFiles;
|
||||
if (desiredState.Files == null || desiredState.Files.Count == 0)
|
||||
{
|
||||
|
@ -209,20 +253,39 @@ namespace Microsoft.Web.LibraryManager.Providers
|
|||
outFiles = FileGlobbingUtility.ExpandFileGlobs(desiredState.Files, library.Files.Keys);
|
||||
}
|
||||
|
||||
if (library.GetInvalidFiles(outFiles.ToList()) is IReadOnlyList<string> invalidFiles
|
||||
&& invalidFiles.Count > 0)
|
||||
{
|
||||
errors ??= [];
|
||||
errors.Add(PredefinedErrors.InvalidFilesInLibrary(desiredState.Name, invalidFiles, library.Files.Keys));
|
||||
}
|
||||
|
||||
foreach (string outFile in outFiles)
|
||||
{
|
||||
// strip the source prefix
|
||||
string destinationFile = Path.Combine(HostInteraction.WorkingDirectory, desiredState.DestinationPath, outFile);
|
||||
if (!FileHelpers.IsUnderRootDirectory(destinationFile, HostInteraction.WorkingDirectory))
|
||||
{
|
||||
errors ??= [];
|
||||
errors.Add(PredefinedErrors.PathOutsideWorkingDirectory());
|
||||
}
|
||||
destinationFile = FileHelpers.NormalizePath(destinationFile);
|
||||
|
||||
// don't forget to include the cache folder in the path
|
||||
string sourceFile = GetCachedFileLocalPath(desiredState, outFile);
|
||||
sourceFile = FileHelpers.NormalizePath(sourceFile);
|
||||
|
||||
// TODO: make goalState immutable
|
||||
// map destination back to the library-relative file it originated from
|
||||
goalState.InstalledFiles.Add(destinationFile, sourceFile);
|
||||
}
|
||||
|
||||
return goalState;
|
||||
if (errors is not null)
|
||||
{
|
||||
return OperationResult<LibraryInstallationGoalState>.FromErrors([.. errors]);
|
||||
}
|
||||
|
||||
return OperationResult<LibraryInstallationGoalState>.FromSuccess(goalState);
|
||||
}
|
||||
|
||||
public bool IsSourceCacheReady(LibraryInstallationGoalState goalState)
|
||||
|
|
|
@ -100,8 +100,7 @@ namespace Microsoft.Web.LibraryManager.Test.Providers.Cdnjs
|
|||
ILibraryOperationResult result = await _provider.InstallAsync(desiredState, CancellationToken.None).ConfigureAwait(false);
|
||||
Assert.IsFalse(result.Success);
|
||||
|
||||
// Unknown exception. We no longer validate ILibraryState at the provider level
|
||||
Assert.AreEqual("LIB000", result.Errors[0].Code);
|
||||
Assert.AreEqual("LIB021", result.Errors[0].Code);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
|
@ -148,11 +147,16 @@ namespace Microsoft.Web.LibraryManager.Test.Providers.Cdnjs
|
|||
Files = new[] { "*.js", "!*.min.js" },
|
||||
};
|
||||
|
||||
// Verify expansion of Files
|
||||
OperationResult<LibraryInstallationGoalState> getGoalState = await _provider.GetInstallationGoalStateAsync(desiredState, CancellationToken.None);
|
||||
Assert.IsTrue(getGoalState.Success);
|
||||
LibraryInstallationGoalState goalState = getGoalState.Result;
|
||||
Assert.AreEqual(1, goalState.InstalledFiles.Count);
|
||||
Assert.AreEqual("jquery.js", Path.GetFileName(goalState.InstalledFiles.Keys.First()));
|
||||
|
||||
// Install library
|
||||
ILibraryOperationResult result = await _provider.InstallAsync(desiredState, CancellationToken.None).ConfigureAwait(false);
|
||||
Assert.IsTrue(result.Success);
|
||||
Assert.IsTrue(result.InstallationState.Files.Count == 1); // jquery.min.js file was excluded
|
||||
Assert.AreEqual("jquery.js", result.InstallationState.Files.First());
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
|
|
|
@ -100,8 +100,7 @@ namespace Microsoft.Web.LibraryManager.Test.Providers.JsDelivr
|
|||
ILibraryOperationResult result = await _provider.InstallAsync(desiredState, CancellationToken.None).ConfigureAwait(false);
|
||||
Assert.IsFalse(result.Success);
|
||||
|
||||
// Unknown exception. We no longer validate ILibraryState at the provider level
|
||||
Assert.AreEqual("LIB000", result.Errors[0].Code);
|
||||
Assert.AreEqual("LIB021", result.Errors[0].Code);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
|
@ -148,10 +147,17 @@ namespace Microsoft.Web.LibraryManager.Test.Providers.JsDelivr
|
|||
Files = new[] { "dist/*.js", "!dist/*min*" },
|
||||
};
|
||||
|
||||
// Verify expansion of Files
|
||||
OperationResult<LibraryInstallationGoalState> getGoalState = await _provider.GetInstallationGoalStateAsync(desiredState, CancellationToken.None);
|
||||
Assert.IsTrue(getGoalState.Success);
|
||||
LibraryInstallationGoalState goalState = getGoalState.Result;
|
||||
// Remove the project folder and "/lib/" from the file paths
|
||||
List<string> installedFiles = goalState.InstalledFiles.Keys.Select(f => f.Substring(_projectFolder.Length + 5).Replace("\\", "/")).ToList();
|
||||
CollectionAssert.AreEquivalent(new[] { "dist/core.js", "dist/jquery.js", "dist/jquery.slim.js" }, installedFiles);
|
||||
|
||||
// Install library
|
||||
ILibraryOperationResult result = await _provider.InstallAsync(desiredState, CancellationToken.None).ConfigureAwait(false);
|
||||
Assert.IsTrue(result.Success);
|
||||
CollectionAssert.AreEquivalent(new[] { "dist/core.js", "dist/jquery.js", "dist/jquery.slim.js" }, result.InstallationState.Files.ToList());
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
|
|
|
@ -99,8 +99,7 @@ namespace Microsoft.Web.LibraryManager.Test.Providers.Unpkg
|
|||
ILibraryOperationResult result = await _provider.InstallAsync(desiredState, CancellationToken.None).ConfigureAwait(false);
|
||||
Assert.IsFalse(result.Success);
|
||||
|
||||
// Unknown exception. We no longer validate ILibraryState at the provider level
|
||||
Assert.AreEqual("LIB000", result.Errors[0].Code);
|
||||
Assert.AreEqual("LIB021", result.Errors[0].Code);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
|
@ -147,10 +146,17 @@ namespace Microsoft.Web.LibraryManager.Test.Providers.Unpkg
|
|||
Files = new[] { "dist/*.js", "!dist/*min*" },
|
||||
};
|
||||
|
||||
// Verify expansion of Files
|
||||
OperationResult<LibraryInstallationGoalState> getGoalState = await _provider.GetInstallationGoalStateAsync(desiredState, CancellationToken.None);
|
||||
Assert.IsTrue(getGoalState.Success);
|
||||
LibraryInstallationGoalState goalState = getGoalState.Result;
|
||||
// Remove the project folder and "/lib/" from the file paths
|
||||
List<string> installedFiles = goalState.InstalledFiles.Keys.Select(f => f.Substring(_projectFolder.Length + 5).Replace("\\", "/")).ToList();
|
||||
CollectionAssert.AreEquivalent(new[] { "dist/core.js", "dist/jquery.js", "dist/jquery.slim.js" }, installedFiles);
|
||||
|
||||
// Install library
|
||||
ILibraryOperationResult result = await _provider.InstallAsync(desiredState, CancellationToken.None).ConfigureAwait(false);
|
||||
Assert.IsTrue(result.Success);
|
||||
CollectionAssert.AreEquivalent(new[] { "dist/core.js", "dist/jquery.js", "dist/jquery.slim.js" }, result.InstallationState.Files.ToList());
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
|
|
Загрузка…
Ссылка в новой задаче