From 1a537147fce8ae58b2cb66dbe61d55d3a5458780 Mon Sep 17 00:00:00 2001 From: Matt Lyons Date: Thu, 18 Apr 2024 17:14:00 -0500 Subject: [PATCH] Allow read/write of project settings in C# --- c-sharp/JsonUtils/VerseRefConverter.cs | 1 - .../Projects/ParatextProjectDataProvider.cs | 62 ++++++++++-- .../ParatextProjectStorageInterpreter.cs | 83 ++++++++++++---- c-sharp/Projects/ProjectDataType.cs | 10 ++ c-sharp/Projects/ProjectSettings.cs | 53 +++++++++++ c-sharp/Projects/ProjectSettingsService.cs | 94 +++++++++++++++++++ extensions/src/usfm-data-provider/index.d.ts | 4 +- src/main/main.ts | 2 +- 8 files changed, 280 insertions(+), 29 deletions(-) create mode 100644 c-sharp/Projects/ProjectDataType.cs create mode 100644 c-sharp/Projects/ProjectSettings.cs create mode 100644 c-sharp/Projects/ProjectSettingsService.cs diff --git a/c-sharp/JsonUtils/VerseRefConverter.cs b/c-sharp/JsonUtils/VerseRefConverter.cs index 929b0eaba8..9d1b594cb9 100644 --- a/c-sharp/JsonUtils/VerseRefConverter.cs +++ b/c-sharp/JsonUtils/VerseRefConverter.cs @@ -22,7 +22,6 @@ out string errorMessage catch (Exception e) { verseRef = new VerseRef(); - Console.Error.Write(e.ToString()); errorMessage = $"Invalid VerseRef ({jsonString}): {e.Message}"; return false; } diff --git a/c-sharp/Projects/ParatextProjectDataProvider.cs b/c-sharp/Projects/ParatextProjectDataProvider.cs index 10f3ef7150..8e76c1c3fd 100644 --- a/c-sharp/Projects/ParatextProjectDataProvider.cs +++ b/c-sharp/Projects/ParatextProjectDataProvider.cs @@ -1,3 +1,5 @@ +using System.Text.Json.Nodes; +using Newtonsoft.Json.Linq; using Paranext.DataProvider.MessageHandlers; using Paranext.DataProvider.MessageTransports; @@ -23,6 +25,9 @@ ProjectDetails projectDetails Getters.Add("getChapterUSX", GetChapterUSX); Setters.Add("setChapterUSX", SetChapterUSX); + + Getters.Add("getSetting", GetProjectSetting); + Setters.Add("setSetting", SetProjectSetting); } protected override Task StartDataProvider() @@ -30,6 +35,23 @@ protected override Task StartDataProvider() return Task.CompletedTask; } + protected override ResponseToRequest HandleRequest(string functionName, JsonArray args) + { + try + { + return functionName switch + { + "resetSetting" => ResetProjectSetting(args[0]!.ToJsonString()), + _ => base.HandleRequest(functionName, args) + }; + } + catch (Exception e) + { + Console.Error.WriteLine(e.ToString()); + return ResponseToRequest.Failed(e.ToString()); + } + } + protected override string GetProjectStorageInterpreterId() { return _paratextPsi.DataProviderName; @@ -45,6 +67,34 @@ protected override ResponseToRequest SetExtensionData(ProjectDataScope dataScope return _paratextPsi.SetExtensionData(dataScope, data); } + private ResponseToRequest GetProjectSetting(string key) + { + return Get(ProjectDataType.SETTINGS, JToken.Parse(key).ToString()); + } + + private ResponseToRequest SetProjectSetting(string key, string value) + { + return Set(ProjectDataType.SETTINGS, JToken.Parse(key).ToString(), value); + } + + // Typically for "reset" we would want to erase the setting and then call "getDefault" if a + // setting is not present when "get" is called. Since we're using PT settings as the backing + // store here, though, we want to keep all properties filled in inside of Settings.xml files + private ResponseToRequest ResetProjectSetting(string key) + { + string settingName = JToken.Parse(key).ToString(); + string? defaultValue = ProjectSettingsService.GetDefault( + PapiClient, + settingName, + ProjectType.Paratext + ); + if (defaultValue == null) + return ResponseToRequest.Failed($"Default value for {settingName} was null"); + ResponseToRequest retVal = Set(ProjectDataType.SETTINGS, settingName, defaultValue); + SendDataUpdateEvent(retVal.Contents); + return retVal; + } + private ResponseToRequest Get(string dataType, string dataQualifier) { ProjectDataScope scope = @@ -72,34 +122,34 @@ private ResponseToRequest Set(string dataType, string dataQualifier, string data #region USFM handling methods private ResponseToRequest GetBookUSFM(string jsonString) { - return Get(ParatextProjectStorageInterpreter.BookUSFM, jsonString); + return Get(ProjectDataType.BOOK_USFM, jsonString); } private ResponseToRequest GetChapterUSFM(string jsonString) { - return Get(ParatextProjectStorageInterpreter.ChapterUSFM, jsonString); + return Get(ProjectDataType.CHAPTER_USFM, jsonString); } private ResponseToRequest GetVerseUSFM(string jsonString) { - return Get(ParatextProjectStorageInterpreter.VerseUSFM, jsonString); + return Get(ProjectDataType.VERSE_USFM, jsonString); } private ResponseToRequest SetChapterUSFM(string dataQualifier, string data) { - return Set(ParatextProjectStorageInterpreter.ChapterUSFM, dataQualifier, data); + return Set(ProjectDataType.CHAPTER_USFM, dataQualifier, data); } #endregion #region USX handling methods private ResponseToRequest GetChapterUSX(string jsonString) { - return Get(ParatextProjectStorageInterpreter.ChapterUSX, jsonString); + return Get(ProjectDataType.CHAPTER_USX, jsonString); } private ResponseToRequest SetChapterUSX(string dataQualifier, string data) { - return Set(ParatextProjectStorageInterpreter.ChapterUSX, dataQualifier, data); + return Set(ProjectDataType.CHAPTER_USX, dataQualifier, data); } #endregion } diff --git a/c-sharp/Projects/ParatextProjectStorageInterpreter.cs b/c-sharp/Projects/ParatextProjectStorageInterpreter.cs index ba402e56d2..e4ca97a6bc 100644 --- a/c-sharp/Projects/ParatextProjectStorageInterpreter.cs +++ b/c-sharp/Projects/ParatextProjectStorageInterpreter.cs @@ -15,20 +15,15 @@ namespace Paranext.DataProvider.Projects; internal class ParatextProjectStorageInterpreter : ProjectStorageInterpreter { #region Constants / Member variables - public const string BookUSFM = "BookUSFM"; - public const string ChapterUSFM = "ChapterUSFM"; - public const string VerseUSFM = "VerseUSFM"; - public const string ChapterUSX = "ChapterUSX"; - // All data types related to Scripture editing. Changes to any portion of Scripture should send // out updates to all these data types - public static readonly List AllScriptureDataTypes = new List - { - BookUSFM, - ChapterUSFM, - VerseUSFM, - ChapterUSX - }; + public static readonly List AllScriptureDataTypes = + [ + ProjectDataType.BOOK_USFM, + ProjectDataType.CHAPTER_USFM, + ProjectDataType.VERSE_USFM, + ProjectDataType.CHAPTER_USX + ]; private readonly LocalParatextProjects _paratextProjects; #endregion @@ -140,22 +135,25 @@ public override ResponseToRequest GetProjectData(ProjectDataScope scope) return scope.DataType switch { - BookUSFM + ProjectDataType.BOOK_USFM => string.IsNullOrEmpty(error) ? ResponseToRequest.Succeeded(scrText.GetText(verseRef, false, true)) : ResponseToRequest.Failed(error), - ChapterUSFM + ProjectDataType.CHAPTER_USFM => string.IsNullOrEmpty(error) ? ResponseToRequest.Succeeded(scrText.GetText(verseRef, true, true)) : ResponseToRequest.Failed(error), - VerseUSFM + ProjectDataType.VERSE_USFM => string.IsNullOrEmpty(error) ? ResponseToRequest.Succeeded(scrText.Parser.GetVerseUsfmText(verseRef)) : ResponseToRequest.Failed(error), - ChapterUSX + ProjectDataType.CHAPTER_USX => string.IsNullOrEmpty(error) ? ResponseToRequest.Succeeded(GetChapterUsx(scrText, verseRef)) : ResponseToRequest.Failed(error), + ProjectDataType.SETTINGS + => ResponseToRequest.Succeeded(scrText.Settings.ParametersDictionary[ + ProjectSettings.GetParatextSettingNameFromPlatformBibleSettingName(scope.DataQualifier) ?? scope.DataQualifier]), _ => ResponseToRequest.Failed($"Unknown data type: {scope.DataType}") }; } @@ -184,7 +182,7 @@ public override ResponseToRequest SetProjectData(ProjectDataScope scope, string switch (scope.DataType) { - case ChapterUSFM: + case ProjectDataType.CHAPTER_USFM: if (!string.IsNullOrEmpty(error)) return ResponseToRequest.Failed(error); RunWithinLock( @@ -200,9 +198,9 @@ public override ResponseToRequest SetProjectData(ProjectDataScope scope, string ); } ); - // The value of returned strings are case sensitive and cannot change unless data provider subscriptions change + // The value of returned strings are case-sensitive and cannot change unless data provider subscriptions change return ResponseToRequest.Succeeded(AllScriptureDataTypes); - case ChapterUSX: + case ProjectDataType.CHAPTER_USX: if (!string.IsNullOrEmpty(error)) return ResponseToRequest.Failed(error); ResponseToRequest? response = null; @@ -211,6 +209,51 @@ public override ResponseToRequest SetProjectData(ProjectDataScope scope, string writeLock => response = SetChapterUsx(scrText, verseRef, data, writeLock) ); return response ?? ResponseToRequest.Failed("Unknown error occurred"); + case ProjectDataType.SETTINGS: + // If there is no Paratext setting for the name given, we'll create one lower down + ResponseToRequest? currentValueResponse = ResponseToRequest.Failed(""); + try + { + currentValueResponse = GetProjectData(scope); + } + catch (KeyNotFoundException) {} + + // Make sure the value we're planning to set is valid + var currentValueJson = currentValueResponse.Success + ? JsonConvert.SerializeObject(currentValueResponse.Contents) + : ""; + if (!ProjectSettingsService.IsValid( + PapiClient, + data, + currentValueJson, + scope.DataQualifier, + "", + ProjectType.Paratext)) + return ResponseToRequest.Failed($"Validation failed for {scope.DataQualifier}"); + + // Figure out which setting name to use + var paratextSettingName = + ProjectSettings.GetParatextSettingNameFromPlatformBibleSettingName( + scope.DataQualifier) ?? scope.DataQualifier; + + // Now actually write the setting + string? errorMessage = null; + RunWithinLock( + WriteScope.AllSettingsFiles(), + _ => { + try + { + scrText.Settings.SetSetting(paratextSettingName, data); + scrText.Settings.Save(); + } + catch (Exception ex) + { + errorMessage = ex.Message; + } + }); + return (errorMessage != null) + ? ResponseToRequest.Failed(errorMessage) + : ResponseToRequest.Succeeded(ProjectDataType.SETTINGS); default: return ResponseToRequest.Failed($"Unknown data type: {scope.DataType}"); } @@ -301,7 +344,7 @@ public override ResponseToRequest SetExtensionData(ProjectDataScope scope, strin // ReSharper restore AccessToDisposedClosure } ); - // The value of returned string is case sensitive and cannot change unless data provider subscriptions change + // The value of returned string is case-sensitive and cannot change unless data provider subscriptions change return ResponseToRequest.Succeeded("ExtensionData"); } catch (Exception e) diff --git a/c-sharp/Projects/ProjectDataType.cs b/c-sharp/Projects/ProjectDataType.cs new file mode 100644 index 0000000000..3234a2adc4 --- /dev/null +++ b/c-sharp/Projects/ProjectDataType.cs @@ -0,0 +1,10 @@ +namespace Paranext.DataProvider.Projects; + +public sealed class ProjectDataType +{ + public const string BOOK_USFM = "BookUSFM"; + public const string CHAPTER_USFM = "ChapterUSFM"; + public const string CHAPTER_USX = "ChapterUSX"; + public const string SETTINGS = "Settings"; + public const string VERSE_USFM = "VerseUSFM"; +} diff --git a/c-sharp/Projects/ProjectSettings.cs b/c-sharp/Projects/ProjectSettings.cs new file mode 100644 index 0000000000..7061f71a1d --- /dev/null +++ b/c-sharp/Projects/ProjectSettings.cs @@ -0,0 +1,53 @@ +namespace Paranext.DataProvider.Projects; + +public sealed class ProjectSettings +{ + public const string PB_BOOKS_PRESENT = "platformScripture.booksPresent"; + public const string PT_BOOKS_PRESENT = "BooksPresent"; + + public const string PB_FULL_NAME = "platform.fullName"; + public const string PT_FULL_NAME = "FullName"; + + public const string PB_LANGUAGE = "platform.language"; + public const string PT_LANGUAGE = "Language"; + + public const string PB_VERSIFICATION = "platformScripture.versification"; + public const string PT_VERSIFICATION = "Versification"; + + // Make sure this dictionary gets updated whenever new settings are added + private static readonly Dictionary s_platformBibleToParatextSettingsNames = + new() + { + { PB_BOOKS_PRESENT, PT_BOOKS_PRESENT }, + { PB_FULL_NAME, PT_FULL_NAME }, + { PB_LANGUAGE, PT_LANGUAGE }, + { PB_VERSIFICATION, PT_VERSIFICATION }, + }; + + private static readonly Dictionary s_paratextToPlatformBibleSettingsNames = + s_platformBibleToParatextSettingsNames.ToDictionary((i) => i.Value, (i) => i.Key); + + /// + /// Convert project setting names from Platform.Bible terminology to Paratext terminology + /// + /// Setting name in Platform.Bible terminology + /// Setting name in Paratext terminology if a mapping exists + public static string? GetParatextSettingNameFromPlatformBibleSettingName(string pbSettingName) + { + return s_platformBibleToParatextSettingsNames.TryGetValue(pbSettingName, out string? retVal) + ? retVal + : null; + } + + /// + /// Convert project setting names from Paratext terminology to Platform.Bible terminology + /// + /// Setting name in Paratext terminology + /// Setting name in Platform.Bible terminology if a mapping exists + public static string? GetPlatformBibleSettingNameFromParatextSettingName(string ptSettingName) + { + return s_paratextToPlatformBibleSettingsNames.TryGetValue(ptSettingName, out string? retVal) + ? retVal + : null; + } +} diff --git a/c-sharp/Projects/ProjectSettingsService.cs b/c-sharp/Projects/ProjectSettingsService.cs new file mode 100644 index 0000000000..07c18746ac --- /dev/null +++ b/c-sharp/Projects/ProjectSettingsService.cs @@ -0,0 +1,94 @@ +using System.Text.Json; +using Paranext.DataProvider.MessageTransports; + +namespace Paranext.DataProvider.Projects +{ + internal static class ProjectSettingsService + { + private const string PROJECT_SETTINGS_SERVICE = "object:ProjectSettingsService.function"; + + public static bool IsValid( + PapiClient papiClient, + string newValueJson, + string currentValueJson, + string key, + string allChangesJson, + string projectType + ) + { + bool requestSucceeded = false; + TaskCompletionSource taskSource = new(); + using var validationTask = taskSource.Task; + + papiClient.SendRequest( + PROJECT_SETTINGS_SERVICE, + new[] + { + "isValid", + key, + newValueJson, + currentValueJson, + projectType, + allChangesJson, + }, + (bool success, object? returnValue) => + { + try + { + if (success) + { + var result = (JsonElement?)returnValue; + if (result.HasValue) + requestSucceeded = result.Value.GetBoolean(); + } + + taskSource.TrySetResult(); + } + catch (Exception ex) + { + requestSucceeded = false; + taskSource.TrySetException(ex); + } + } + ); + + using var cts = new CancellationTokenSource(); + validationTask.Wait(cts.Token); + return requestSucceeded; + } + + public static string? GetDefault(PapiClient papiClient, string key, string projectType) + { + string? defaultValue = null; + TaskCompletionSource taskSource = new(); + using var getDefaultTask = taskSource.Task; + + papiClient.SendRequest( + PROJECT_SETTINGS_SERVICE, + new[] { "getDefault", key, projectType }, + (bool success, object? returnValue) => + { + try + { + if (success) + { + var result = (JsonElement?)returnValue; + if (result.HasValue) + defaultValue = result.Value.ToString(); + } + + taskSource.TrySetResult(); + } + catch (Exception ex) + { + taskSource.TrySetException(ex); + } + } + ); + + using var cts = new CancellationTokenSource(); + getDefaultTask.Wait(cts.Token); + return defaultValue; + } + } +} diff --git a/extensions/src/usfm-data-provider/index.d.ts b/extensions/src/usfm-data-provider/index.d.ts index 324982296e..457db14629 100644 --- a/extensions/src/usfm-data-provider/index.d.ts +++ b/extensions/src/usfm-data-provider/index.d.ts @@ -8,6 +8,7 @@ declare module 'usfm-data-provider' { IDataProvider, MandatoryProjectDataTypes, } from '@papi/core'; + import type { IProjectDataProvider } from 'papi-shared-types'; import { UnsubscriberAsync } from 'platform-bible-utils'; export type UsfmProviderDataTypes = { @@ -359,7 +360,8 @@ declare module 'usfm-data-provider' { ): Promise; } & ParatextStandardProjectDataProvider; - export type ParatextStandardProjectDataProvider = IDataProvider; + export type ParatextStandardProjectDataProvider = + IProjectDataProvider; } declare module 'papi-shared-types' { diff --git a/src/main/main.ts b/src/main/main.ts index 3b630c2a09..727d4701da 100644 --- a/src/main/main.ts +++ b/src/main/main.ts @@ -347,7 +347,7 @@ async function main() { { extensionName: 'foo', dataQualifier: 'fooData' }, 'This is the data from extension foo', ); - }, 10000); + }, 20000); // #endregion }