diff --git a/Bonsai.Editor/DocumentationException.cs b/Bonsai.Editor/DocumentationException.cs new file mode 100644 index 00000000..4223ef8f --- /dev/null +++ b/Bonsai.Editor/DocumentationException.cs @@ -0,0 +1,19 @@ +using System; + +namespace Bonsai.Editor +{ + internal sealed class DocumentationException : Exception + { + public DocumentationException() + : base() + { } + + public DocumentationException(string message) + : base(message) + { } + + public DocumentationException(string message, Exception innerException) + : base(message, innerException) + { } + } +} diff --git a/Bonsai.Editor/DocumentationHelper.cs b/Bonsai.Editor/DocumentationHelper.cs index 9de19bc7..92d34bad 100644 --- a/Bonsai.Editor/DocumentationHelper.cs +++ b/Bonsai.Editor/DocumentationHelper.cs @@ -5,6 +5,7 @@ using System.Net; using System.Net.Cache; using System.Threading.Tasks; +using YamlDotNet.Core; using YamlDotNet.Serialization; using YamlDotNet.Serialization.NamingConventions; @@ -36,16 +37,14 @@ static async Task> GetXRefMapAsync(string baseUrl, pa throw new ArgumentException("No downstream URLs have been specified.", nameof(hrefs)); } - WebException lastException = default; + Exception lastException = default; for (int i = 0; i < hrefs.Length; i++) { try { return await GetXRefMapAsync($"{baseUrl}{hrefs[i]}"); } - catch (WebException ex) when (ex.Response is HttpWebResponse httpResponse && - httpResponse.StatusCode is HttpStatusCode.NotFound) - { - lastException = ex; - continue; - } + catch (WebException ex) when (ex.Response is HttpWebResponse { StatusCode: HttpStatusCode.NotFound }) + { lastException ??= ex; } // Always prefer a DocumentationException as it'll be more specific + catch (DocumentationException ex) + { lastException = ex; } } throw lastException; @@ -61,11 +60,25 @@ static async Task> GetXRefMapAsync(string baseUrl) using var response = await request.GetResponseAsync(); var stream = response.GetResponseStream(); using var reader = new StreamReader(stream); + + if (reader.ReadLine().Trim() != "### YamlMime:XRefMap") + throw new DocumentationException("The documentation server did not respond with a cross-reference map."); + var deserializer = new DeserializerBuilder() .WithNamingConvention(CamelCaseNamingConvention.Instance) .IgnoreUnmatchedProperties() .Build(); - var xrefmap = deserializer.Deserialize(reader); + + XRefMap xrefmap; + try { xrefmap = deserializer.Deserialize(reader); } + catch (YamlException ex) + { + throw new DocumentationException("The cross-reference map returned by the documentation server is malformed.", ex); + } + + if (xrefmap.References is null) + throw new DocumentationException("The cross-reference map returned by the documentation server is malformed."); + return xrefmap.References.ToDictionary( reference => reference.Uid, reference => $"{baseUrl}/{reference.Href}"); diff --git a/Bonsai.Editor/EditorForm.cs b/Bonsai.Editor/EditorForm.cs index 06f5fa39..bf12a9e4 100644 --- a/Bonsai.Editor/EditorForm.cs +++ b/Bonsai.Editor/EditorForm.cs @@ -2366,12 +2366,12 @@ private async Task OpenDocumentationAsync(string assemblyName, string uid) var message = $"The specified operator {uid} was not found in the documentation for {assemblyName}."; editorSite.ShowError(ex, message); } - catch (SystemException ex) when (ex is WebException || ex is NotSupportedException) + catch (Exception ex) when (ex is WebException or NotSupportedException or DocumentationException) { var message = $"The documentation for the module {assemblyName} is not available. {{0}}"; editorSite.ShowError(ex, message); } - catch (SystemException ex) + catch (Exception ex) { editorSite.ShowError(ex); }