Skip to content

Commit 4b79145

Browse files
committed
Use OperationGoalState for intermediate steps in Install and GenerateGoalState
1 parent eb732f0 commit 4b79145

File tree

8 files changed

+149
-46
lines changed

8 files changed

+149
-46
lines changed

src/LibraryManager.Contracts/FileHelpers.cs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -374,7 +374,13 @@ public static bool IsUnderRootDirectory(string filePath, string rootDirectory)
374374
&& normalizedFilePath.StartsWith(normalizedRootDirectory, StringComparison.OrdinalIgnoreCase);
375375
}
376376

377-
internal static string NormalizePath(string path)
377+
/// <summary>
378+
/// Normalizes the path string so it can be easily compared.
379+
/// </summary>
380+
/// <remarks>
381+
/// Result will be lowercase and have any trailing slashes removed.
382+
/// </remarks>
383+
public static string NormalizePath(string path)
378384
{
379385
if (string.IsNullOrEmpty(path))
380386
{

src/LibraryManager.Contracts/PredefinedErrors.cs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,11 @@ namespace Microsoft.Web.LibraryManager.Contracts
1717
public static class PredefinedErrors
1818
{
1919
/// <summary>
20-
/// Represents an unhandled exception that occured in the provider.
20+
/// Represents an unhandled exception that occurred in the provider.
2121
/// </summary>
2222
/// <remarks>
2323
/// An <see cref="IProvider.InstallAsync"/> should never throw and this error
24-
/// should be used as when catching generic exeptions.
24+
/// should be used as when catching generic exceptions.
2525
/// </remarks>
2626
/// <returns>The error code LIB000</returns>
2727
public static IError UnknownException()
@@ -198,6 +198,12 @@ public static IError DuplicateLibrariesInManifest(string duplicateLibrary)
198198
public static IError FileNameMustNotBeEmpty(string libraryId)
199199
=> new Error("LIB020", string.Format(Text.ErrorFilePathIsEmpty, libraryId));
200200

201+
/// <summary>
202+
/// A library mapping does not have a destination specified
203+
/// </summary>
204+
public static IError DestinationNotSpecified(string libraryId)
205+
=> new Error("LIB021", string.Format(Text.ErrorDestinationNotSpecified, libraryId));
206+
201207
/// <summary>
202208
/// The manifest must specify a version
203209
/// </summary>

src/LibraryManager.Contracts/Resources/Text.Designer.cs

Lines changed: 9 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/LibraryManager.Contracts/Resources/Text.resx

Lines changed: 30 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,17 @@
11
<?xml version="1.0" encoding="utf-8"?>
22
<root>
3-
<!--
4-
Microsoft ResX Schema
5-
3+
<!--
4+
Microsoft ResX Schema
5+
66
Version 2.0
7-
8-
The primary goals of this format is to allow a simple XML format
9-
that is mostly human readable. The generation and parsing of the
10-
various data types are done through the TypeConverter classes
7+
8+
The primary goals of this format is to allow a simple XML format
9+
that is mostly human readable. The generation and parsing of the
10+
various data types are done through the TypeConverter classes
1111
associated with the data types.
12-
12+
1313
Example:
14-
14+
1515
... ado.net/XML headers & schema ...
1616
<resheader name="resmimetype">text/microsoft-resx</resheader>
1717
<resheader name="version">2.0</resheader>
@@ -26,36 +26,36 @@
2626
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
2727
<comment>This is a comment</comment>
2828
</data>
29-
30-
There are any number of "resheader" rows that contain simple
29+
30+
There are any number of "resheader" rows that contain simple
3131
name/value pairs.
32-
33-
Each data row contains a name, and value. The row also contains a
34-
type or mimetype. Type corresponds to a .NET class that support
35-
text/value conversion through the TypeConverter architecture.
36-
Classes that don't support this are serialized and stored with the
32+
33+
Each data row contains a name, and value. The row also contains a
34+
type or mimetype. Type corresponds to a .NET class that support
35+
text/value conversion through the TypeConverter architecture.
36+
Classes that don't support this are serialized and stored with the
3737
mimetype set.
38-
39-
The mimetype is used for serialized objects, and tells the
40-
ResXResourceReader how to depersist the object. This is currently not
38+
39+
The mimetype is used for serialized objects, and tells the
40+
ResXResourceReader how to depersist the object. This is currently not
4141
extensible. For a given mimetype the value must be set accordingly:
42-
43-
Note - application/x-microsoft.net.object.binary.base64 is the format
44-
that the ResXResourceWriter will generate, however the reader can
42+
43+
Note - application/x-microsoft.net.object.binary.base64 is the format
44+
that the ResXResourceWriter will generate, however the reader can
4545
read any of the formats listed below.
46-
46+
4747
mimetype: application/x-microsoft.net.object.binary.base64
48-
value : The object must be serialized with
48+
value : The object must be serialized with
4949
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
5050
: and then encoded with base64 encoding.
51-
51+
5252
mimetype: application/x-microsoft.net.object.soap.base64
53-
value : The object must be serialized with
53+
value : The object must be serialized with
5454
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
5555
: and then encoded with base64 encoding.
5656
5757
mimetype: application/x-microsoft.net.object.bytearray.base64
58-
value : The object must be serialized into a byte array
58+
value : The object must be serialized into a byte array
5959
: using a System.ComponentModel.TypeConverter
6060
: and then encoded with base64 encoding.
6161
-->
@@ -187,6 +187,9 @@ Valid files are {2}</value>
187187
<data name="ErrorFilePathIsEmpty" xml:space="preserve">
188188
<value>The library "{0}" cannot specify a file with an empty name</value>
189189
</data>
190+
<data name="ErrorDestinationNotSpecified" xml:space="preserve">
191+
<value>The "{0}" library is missing a destination.</value>
192+
</data>
190193
<data name="ErrorMissingManifestVersion" xml:space="preserve">
191194
<value>The Library Manager manifest must specify a version.</value>
192195
</data>

src/LibraryManager/Providers/BaseProvider.cs

Lines changed: 69 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -60,10 +60,25 @@ public virtual async Task<ILibraryOperationResult> InstallAsync(ILibraryInstalla
6060
return LibraryOperationResult.FromCancelled(desiredState);
6161
}
6262

63-
ILibraryCatalog catalog = GetCatalog();
64-
ILibrary library = await catalog.GetLibraryAsync(desiredState.Name, desiredState.Version, cancellationToken).ConfigureAwait(false);
63+
OperationResult<ILibrary> getLibrary = await GetLibraryForInstallationState(desiredState, cancellationToken).ConfigureAwait(false);
64+
if (!getLibrary.Success)
65+
{
66+
return new LibraryOperationResult(desiredState, [.. getLibrary.Errors])
67+
{
68+
Cancelled = getLibrary.Cancelled,
69+
};
70+
}
71+
72+
OperationResult<LibraryInstallationGoalState> getGoalState = GenerateGoalState(desiredState, getLibrary.Result);
73+
if (!getGoalState.Success)
74+
{
75+
return new LibraryOperationResult(desiredState, [.. getGoalState.Errors])
76+
{
77+
Cancelled = getGoalState.Cancelled,
78+
};
79+
}
6580

66-
LibraryInstallationGoalState goalState = GenerateGoalState(desiredState, library);
81+
LibraryInstallationGoalState goalState = getGoalState.Result;
6782

6883
if (!IsSourceCacheReady(goalState))
6984
{
@@ -83,8 +98,30 @@ public virtual async Task<ILibraryOperationResult> InstallAsync(ILibraryInstalla
8398

8499
}
85100

86-
private async Task<LibraryOperationResult> InstallFiles(LibraryInstallationGoalState goalState, CancellationToken cancellationToken)
101+
private async Task<OperationResult<ILibrary>> GetLibraryForInstallationState(ILibraryInstallationState desiredState, CancellationToken cancellationToken)
102+
{
103+
ILibrary library;
104+
try
105+
{
106+
ILibraryCatalog catalog = GetCatalog();
107+
library = await catalog.GetLibraryAsync(desiredState.Name, desiredState.Version, cancellationToken).ConfigureAwait(false);
108+
}
109+
catch (InvalidLibraryException)
110+
{
111+
string libraryId = LibraryNamingScheme.GetLibraryId(desiredState.Name, desiredState.Version);
112+
return OperationResult<ILibrary>.FromError(PredefinedErrors.UnableToResolveSource(libraryId, desiredState.ProviderId));
113+
}
114+
catch (Exception ex)
87115
{
116+
HostInteraction.Logger.Log(ex.ToString(), LogLevel.Error);
117+
return OperationResult<ILibrary>.FromError(PredefinedErrors.UnknownException());
118+
}
119+
120+
return OperationResult<ILibrary>.FromSuccess(library);
121+
}
122+
123+
private async Task<LibraryOperationResult> InstallFiles(LibraryInstallationGoalState goalState, CancellationToken cancellationToken)
124+
{
88125
try
89126
{
90127
foreach (KeyValuePair<string, string> kvp in goalState.InstalledFiles)
@@ -196,9 +233,16 @@ public virtual async Task<ILibraryOperationResult> UpdateStateAsync(ILibraryInst
196233

197234
#endregion
198235

199-
public LibraryInstallationGoalState GenerateGoalState(ILibraryInstallationState desiredState, ILibrary library)
236+
private OperationResult<LibraryInstallationGoalState> GenerateGoalState(ILibraryInstallationState desiredState, ILibrary library)
200237
{
201238
var goalState = new LibraryInstallationGoalState(desiredState);
239+
List<IError> errors = null;
240+
241+
if (string.IsNullOrEmpty(desiredState.DestinationPath))
242+
{
243+
return OperationResult<LibraryInstallationGoalState>.FromError(PredefinedErrors.DestinationNotSpecified(desiredState.Name));
244+
}
245+
202246
IEnumerable<string> outFiles;
203247
if (desiredState.Files == null || desiredState.Files.Count == 0)
204248
{
@@ -209,20 +253,39 @@ public LibraryInstallationGoalState GenerateGoalState(ILibraryInstallationState
209253
outFiles = FileGlobbingUtility.ExpandFileGlobs(desiredState.Files, library.Files.Keys);
210254
}
211255

256+
if (library.GetInvalidFiles(outFiles.ToList()) is IReadOnlyList<string> invalidFiles
257+
&& invalidFiles.Count > 0)
258+
{
259+
errors ??= [];
260+
errors.Add(PredefinedErrors.InvalidFilesInLibrary(desiredState.Name, invalidFiles, library.Files.Keys));
261+
}
262+
212263
foreach (string outFile in outFiles)
213264
{
214265
// strip the source prefix
215266
string destinationFile = Path.Combine(HostInteraction.WorkingDirectory, desiredState.DestinationPath, outFile);
267+
if (!FileHelpers.IsUnderRootDirectory(destinationFile, HostInteraction.WorkingDirectory))
268+
{
269+
errors ??= [];
270+
errors.Add(PredefinedErrors.PathOutsideWorkingDirectory());
271+
}
272+
destinationFile = FileHelpers.NormalizePath(destinationFile);
216273

217274
// don't forget to include the cache folder in the path
218275
string sourceFile = GetCachedFileLocalPath(desiredState, outFile);
276+
sourceFile = FileHelpers.NormalizePath(sourceFile);
219277

220278
// TODO: make goalState immutable
221279
// map destination back to the library-relative file it originated from
222280
goalState.InstalledFiles.Add(destinationFile, sourceFile);
223281
}
224282

225-
return goalState;
283+
if (errors is not null)
284+
{
285+
return OperationResult<LibraryInstallationGoalState>.FromErrors([.. errors]);
286+
}
287+
288+
return OperationResult<LibraryInstallationGoalState>.FromSuccess(goalState);
226289
}
227290

228291
public bool IsSourceCacheReady(LibraryInstallationGoalState goalState)

test/LibraryManager.Test/Providers/Cdnjs/CdnjsProviderTest.cs

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -100,8 +100,7 @@ public async Task InstallAsync_NoPathDefined()
100100
ILibraryOperationResult result = await _provider.InstallAsync(desiredState, CancellationToken.None).ConfigureAwait(false);
101101
Assert.IsFalse(result.Success);
102102

103-
// Unknown exception. We no longer validate ILibraryState at the provider level
104-
Assert.AreEqual("LIB000", result.Errors[0].Code);
103+
Assert.AreEqual("LIB021", result.Errors[0].Code);
105104
}
106105

107106
[TestMethod]
@@ -148,11 +147,16 @@ public async Task InstallAsync_WithGlobPatterns_CorrectlyInstallsAllMatchingFile
148147
Files = new[] { "*.js", "!*.min.js" },
149148
};
150149

150+
// Verify expansion of Files
151+
OperationResult<LibraryInstallationGoalState> getGoalState = await _provider.GetInstallationGoalStateAsync(desiredState, CancellationToken.None);
152+
Assert.IsTrue(getGoalState.Success);
153+
LibraryInstallationGoalState goalState = getGoalState.Result;
154+
Assert.AreEqual(1, goalState.InstalledFiles.Count);
155+
Assert.AreEqual("jquery.js", Path.GetFileName(goalState.InstalledFiles.Keys.First()));
156+
151157
// Install library
152158
ILibraryOperationResult result = await _provider.InstallAsync(desiredState, CancellationToken.None).ConfigureAwait(false);
153159
Assert.IsTrue(result.Success);
154-
Assert.IsTrue(result.InstallationState.Files.Count == 1); // jquery.min.js file was excluded
155-
Assert.AreEqual("jquery.js", result.InstallationState.Files.First());
156160
}
157161

158162
[TestMethod]

test/LibraryManager.Test/Providers/JsDelivr/JsDelivrProviderTest.cs

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -100,8 +100,7 @@ public async Task InstallAsync_NoPathDefined()
100100
ILibraryOperationResult result = await _provider.InstallAsync(desiredState, CancellationToken.None).ConfigureAwait(false);
101101
Assert.IsFalse(result.Success);
102102

103-
// Unknown exception. We no longer validate ILibraryState at the provider level
104-
Assert.AreEqual("LIB000", result.Errors[0].Code);
103+
Assert.AreEqual("LIB021", result.Errors[0].Code);
105104
}
106105

107106
[TestMethod]
@@ -148,10 +147,17 @@ public async Task InstallAsync_WithGlobPatterns_CorrectlyInstallsAllMatchingFile
148147
Files = new[] { "dist/*.js", "!dist/*min*" },
149148
};
150149

150+
// Verify expansion of Files
151+
OperationResult<LibraryInstallationGoalState> getGoalState = await _provider.GetInstallationGoalStateAsync(desiredState, CancellationToken.None);
152+
Assert.IsTrue(getGoalState.Success);
153+
LibraryInstallationGoalState goalState = getGoalState.Result;
154+
// Remove the project folder and "/lib/" from the file paths
155+
List<string> installedFiles = goalState.InstalledFiles.Keys.Select(f => f.Substring(_projectFolder.Length + 5).Replace("\\", "/")).ToList();
156+
CollectionAssert.AreEquivalent(new[] { "dist/core.js", "dist/jquery.js", "dist/jquery.slim.js" }, installedFiles);
157+
151158
// Install library
152159
ILibraryOperationResult result = await _provider.InstallAsync(desiredState, CancellationToken.None).ConfigureAwait(false);
153160
Assert.IsTrue(result.Success);
154-
CollectionAssert.AreEquivalent(new[] { "dist/core.js", "dist/jquery.js", "dist/jquery.slim.js" }, result.InstallationState.Files.ToList());
155161
}
156162

157163
[TestMethod]

test/LibraryManager.Test/Providers/Unpkg/UnpkgProviderTest.cs

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -99,8 +99,7 @@ public async Task InstallAsync_NoPathDefined()
9999
ILibraryOperationResult result = await _provider.InstallAsync(desiredState, CancellationToken.None).ConfigureAwait(false);
100100
Assert.IsFalse(result.Success);
101101

102-
// Unknown exception. We no longer validate ILibraryState at the provider level
103-
Assert.AreEqual("LIB000", result.Errors[0].Code);
102+
Assert.AreEqual("LIB021", result.Errors[0].Code);
104103
}
105104

106105
[TestMethod]
@@ -147,10 +146,17 @@ public async Task InstallAsync_WithGlobPatterns_CorrectlyInstallsAllMatchingFile
147146
Files = new[] { "dist/*.js", "!dist/*min*" },
148147
};
149148

149+
// Verify expansion of Files
150+
OperationResult<LibraryInstallationGoalState> getGoalState = await _provider.GetInstallationGoalStateAsync(desiredState, CancellationToken.None);
151+
Assert.IsTrue(getGoalState.Success);
152+
LibraryInstallationGoalState goalState = getGoalState.Result;
153+
// Remove the project folder and "/lib/" from the file paths
154+
List<string> installedFiles = goalState.InstalledFiles.Keys.Select(f => f.Substring(_projectFolder.Length + 5).Replace("\\", "/")).ToList();
155+
CollectionAssert.AreEquivalent(new[] { "dist/core.js", "dist/jquery.js", "dist/jquery.slim.js" }, installedFiles);
156+
150157
// Install library
151158
ILibraryOperationResult result = await _provider.InstallAsync(desiredState, CancellationToken.None).ConfigureAwait(false);
152159
Assert.IsTrue(result.Success);
153-
CollectionAssert.AreEquivalent(new[] { "dist/core.js", "dist/jquery.js", "dist/jquery.slim.js" }, result.InstallationState.Files.ToList());
154160
}
155161

156162
[TestMethod]

0 commit comments

Comments
 (0)