diff --git a/src/SIL.XForge.Scripture/Controllers/MachineApiController.cs b/src/SIL.XForge.Scripture/Controllers/MachineApiController.cs index 9d3448f3c6..cf5682a522 100644 --- a/src/SIL.XForge.Scripture/Controllers/MachineApiController.cs +++ b/src/SIL.XForge.Scripture/Controllers/MachineApiController.cs @@ -7,6 +7,7 @@ using Microsoft.AspNetCore.Mvc; using Polly.CircuitBreaker; using Serval.Client; +using SIL.Converters.Usj; using SIL.XForge.Models; using SIL.XForge.Realtime; using SIL.XForge.Scripture.Models; @@ -427,6 +428,61 @@ CancellationToken cancellationToken } } + /// + /// Gets the pre-translations for the specified chapter as USJ. + /// + /// The Scripture Forge project identifier. + /// The book number. + /// The chapter number. If zero, the entire book is returned. + /// The cancellation token. + /// The pre-translations were successfully queried for. + /// You do not have permission to retrieve the pre-translations for this project. + /// The project does not exist or is not configured on the ML server. + /// Retrieving the pre-translations in this format is not supported. + /// The engine has not been built on the ML server. + /// The ML server is temporarily unavailable or unresponsive. + [HttpGet(MachineApi.GetPreTranslationUsj)] + public async Task> GetPreTranslationUsjAsync( + string sfProjectId, + int bookNum, + int chapterNum, + CancellationToken cancellationToken + ) + { + try + { + Usj usj = await _machineApiService.GetPreTranslationUsjAsync( + _userAccessor.UserId, + sfProjectId, + bookNum, + chapterNum, + cancellationToken + ); + return Ok(usj); + } + catch (BrokenCircuitException e) + { + _exceptionHandler.ReportException(e); + return StatusCode(StatusCodes.Status503ServiceUnavailable, MachineApiUnavailable); + } + catch (DataNotFoundException) + { + return NotFound(); + } + catch (ForbiddenException) + { + return Forbid(); + } + catch (InvalidOperationException) + { + return Conflict(); + } + catch (NotSupportedException) + { + return new StatusCodeResult(StatusCodes.Status405MethodNotAllowed); + } + } + /// /// Gets the pre-translations for the specified chapter as USX. /// diff --git a/src/SIL.XForge.Scripture/Models/MachineApi.cs b/src/SIL.XForge.Scripture/Models/MachineApi.cs index cc76baa421..26f171363d 100644 --- a/src/SIL.XForge.Scripture/Models/MachineApi.cs +++ b/src/SIL.XForge.Scripture/Models/MachineApi.cs @@ -27,6 +27,8 @@ public static class MachineApi "translation/engines/project:{sfProjectId}/actions/preTranslate/{bookNum}_{chapterNum}/delta"; public const string GetPreTranslationUsfm = "translation/engines/project:{sfProjectId}/actions/preTranslate/{bookNum}_{chapterNum}/usfm"; + public const string GetPreTranslationUsj = + "translation/engines/project:{sfProjectId}/actions/preTranslate/{bookNum}_{chapterNum}/usj"; public const string GetPreTranslationUsx = "translation/engines/project:{sfProjectId}/actions/preTranslate/{bookNum}_{chapterNum}/usx"; public const string GetLastCompletedPreTranslationBuild = diff --git a/src/SIL.XForge.Scripture/SIL.XForge.Scripture.csproj b/src/SIL.XForge.Scripture/SIL.XForge.Scripture.csproj index f50af68bbc..73b7c8be61 100644 --- a/src/SIL.XForge.Scripture/SIL.XForge.Scripture.csproj +++ b/src/SIL.XForge.Scripture/SIL.XForge.Scripture.csproj @@ -62,6 +62,7 @@ + diff --git a/src/SIL.XForge.Scripture/Services/IMachineApiService.cs b/src/SIL.XForge.Scripture/Services/IMachineApiService.cs index 7572ab8846..77a0b906ec 100644 --- a/src/SIL.XForge.Scripture/Services/IMachineApiService.cs +++ b/src/SIL.XForge.Scripture/Services/IMachineApiService.cs @@ -2,6 +2,7 @@ using System.Threading.Tasks; using Autofac.Extras.DynamicProxy; using Serval.Client; +using SIL.Converters.Usj; using SIL.XForge.EventMetrics; using SIL.XForge.Realtime; using SIL.XForge.Scripture.Models; @@ -67,6 +68,13 @@ Task GetPreTranslationUsfmAsync( bool isServalAdmin, CancellationToken cancellationToken ); + Task GetPreTranslationUsjAsync( + string curUserId, + string sfProjectId, + int bookNum, + int chapterNum, + CancellationToken cancellationToken + ); Task GetPreTranslationUsxAsync( string curUserId, string sfProjectId, diff --git a/src/SIL.XForge.Scripture/Services/MachineApiService.cs b/src/SIL.XForge.Scripture/Services/MachineApiService.cs index e95f21282d..53e33531e8 100644 --- a/src/SIL.XForge.Scripture/Services/MachineApiService.cs +++ b/src/SIL.XForge.Scripture/Services/MachineApiService.cs @@ -11,6 +11,7 @@ using Microsoft.Extensions.Options; using Newtonsoft.Json; using Serval.Client; +using SIL.Converters.Usj; using SIL.ObjectModel; using SIL.XForge.Configuration; using SIL.XForge.DataAccess; @@ -558,6 +559,42 @@ CancellationToken cancellationToken } } + public async Task GetPreTranslationUsjAsync( + string curUserId, + string sfProjectId, + int bookNum, + int chapterNum, + CancellationToken cancellationToken + ) + { + // Ensure that the user has permission + SFProject project = await EnsureProjectPermissionAsync(curUserId, sfProjectId); + + // Retrieve the user secret + Attempt attempt = await userSecrets.TryGetAsync(curUserId); + if (!attempt.TryResult(out UserSecret userSecret)) + { + throw new DataNotFoundException("The user does not exist."); + } + + try + { + string usfm = await preTranslationService.GetPreTranslationUsfmAsync( + sfProjectId, + bookNum, + chapterNum, + cancellationToken + ); + string usx = paratextService.GetBookText(userSecret, project.ParatextId, bookNum, usfm); + return UsxToUsj.UsxStringToUsj(usx); + } + catch (ServalApiException e) + { + ProcessServalApiException(e); + throw; + } + } + public async Task GetPreTranslationUsxAsync( string curUserId, string sfProjectId, diff --git a/test/SIL.XForge.Scripture.Tests/Controllers/MachineApiControllerTests.cs b/test/SIL.XForge.Scripture.Tests/Controllers/MachineApiControllerTests.cs index 03de4cc6d5..63f780e51a 100644 --- a/test/SIL.XForge.Scripture.Tests/Controllers/MachineApiControllerTests.cs +++ b/test/SIL.XForge.Scripture.Tests/Controllers/MachineApiControllerTests.cs @@ -9,6 +9,7 @@ using NUnit.Framework; using Polly.CircuitBreaker; using Serval.Client; +using SIL.Converters.Usj; using SIL.XForge.Models; using SIL.XForge.Realtime; using SIL.XForge.Scripture.Models; @@ -1045,6 +1046,123 @@ await env .GetPreTranslationUsfmAsync(User01, Project01, 40, 1, false, CancellationToken.None); } + [Test] + public async Task GetPreTranslationUsjAsync_MachineApiDown() + { + // Set up test environment + var env = new TestEnvironment(); + env.MachineApiService.GetPreTranslationUsjAsync(User01, Project01, 40, 1, CancellationToken.None) + .Throws(new BrokenCircuitException()); + + // SUT + ActionResult actual = await env.Controller.GetPreTranslationUsjAsync( + Project01, + 40, + 1, + CancellationToken.None + ); + + env.ExceptionHandler.Received(1).ReportException(Arg.Any()); + Assert.IsInstanceOf(actual.Result); + Assert.AreEqual(StatusCodes.Status503ServiceUnavailable, (actual.Result as ObjectResult)?.StatusCode); + } + + [Test] + public async Task GetPreTranslationUsjAsync_NoPermission() + { + // Set up test environment + var env = new TestEnvironment(); + env.MachineApiService.GetPreTranslationUsjAsync(User01, Project01, 40, 1, CancellationToken.None) + .Throws(new ForbiddenException()); + + // SUT + ActionResult actual = await env.Controller.GetPreTranslationUsjAsync( + Project01, + 40, + 1, + CancellationToken.None + ); + + Assert.IsInstanceOf(actual.Result); + } + + [Test] + public async Task GetPreTranslationUsjAsync_NoProject() + { + // Set up test environment + var env = new TestEnvironment(); + env.MachineApiService.GetPreTranslationUsjAsync(User01, Project01, 40, 1, CancellationToken.None) + .Throws(new DataNotFoundException(string.Empty)); + + // SUT + ActionResult actual = await env.Controller.GetPreTranslationUsjAsync( + Project01, + 40, + 1, + CancellationToken.None + ); + + Assert.IsInstanceOf(actual.Result); + } + + [Test] + public async Task GetPreTranslationUsjAsync_NotBuilt() + { + // Set up test environment + var env = new TestEnvironment(); + env.MachineApiService.GetPreTranslationUsjAsync(User01, Project01, 40, 1, CancellationToken.None) + .Throws(new InvalidOperationException()); + + // SUT + ActionResult actual = await env.Controller.GetPreTranslationUsjAsync( + Project01, + 40, + 1, + CancellationToken.None + ); + + Assert.IsInstanceOf(actual.Result); + } + + [Test] + public async Task GetPreTranslationUsjAsync_NotSupported() + { + // Set up test environment + var env = new TestEnvironment(); + env.MachineApiService.GetPreTranslationUsjAsync(User01, Project01, 40, 1, CancellationToken.None) + .Throws(new NotSupportedException()); + + // SUT + ActionResult actual = await env.Controller.GetPreTranslationUsjAsync( + Project01, + 40, + 1, + CancellationToken.None + ); + + Assert.IsInstanceOf(actual.Result); + Assert.AreEqual(StatusCodes.Status405MethodNotAllowed, (actual.Result as IStatusCodeActionResult)?.StatusCode); + } + + [Test] + public async Task GetPreTranslationUsjAsync_Success() + { + // Set up test environment + var env = new TestEnvironment(); + env.MachineApiService.GetPreTranslationUsjAsync(User01, Project01, 40, 1, CancellationToken.None) + .Returns(Task.FromResult(new Usj())); + + // SUT + ActionResult actual = await env.Controller.GetPreTranslationUsjAsync( + Project01, + 40, + 1, + CancellationToken.None + ); + + Assert.IsInstanceOf(actual.Result); + } + [Test] public async Task GetPreTranslationUsxAsync_MachineApiDown() { diff --git a/test/SIL.XForge.Scripture.Tests/Services/MachineApiServiceTests.cs b/test/SIL.XForge.Scripture.Tests/Services/MachineApiServiceTests.cs index f022dd2a23..e2d09a3eac 100644 --- a/test/SIL.XForge.Scripture.Tests/Services/MachineApiServiceTests.cs +++ b/test/SIL.XForge.Scripture.Tests/Services/MachineApiServiceTests.cs @@ -14,6 +14,7 @@ using NUnit.Framework; using Polly.CircuitBreaker; using Serval.Client; +using SIL.Converters.Usj; using SIL.XForge.DataAccess; using SIL.XForge.Models; using SIL.XForge.Realtime; @@ -1263,6 +1264,78 @@ public async Task GetPreTranslationUsfmAsync_Success() Assert.AreEqual(expected, usfm); } + [Test] + public void GetPreTranslationUsjAsync_CorpusDoesNotSupportUsfm() + { + // Set up test environment + var env = new TestEnvironment(); + env.PreTranslationService.GetPreTranslationUsfmAsync(Project01, 40, 1, CancellationToken.None) + .Throws(ServalApiExceptions.InvalidCorpus); + + // SUT + Assert.ThrowsAsync( + () => env.Service.GetPreTranslationUsjAsync(User01, Project01, 40, 1, CancellationToken.None) + ); + } + + [Test] + public async Task GetPreTranslationUsjAsync_MissingUserSecret() + { + // Set up test environment + var env = new TestEnvironment(); + await env.UserSecrets.DeleteAllAsync(_ => true); + + // SUT + Assert.ThrowsAsync( + () => env.Service.GetPreTranslationUsjAsync(User01, Project01, 40, 1, CancellationToken.None) + ); + } + + [Test] + public async Task GetPreTranslationUsjAsync_Success() + { + // Set up test environment + var env = new TestEnvironment(); + const string usfm = "\\c 1 \\v1 Verse 1"; + const string usx = + "" + + "Verse 1"; + Usj expected = new Usj + { + Type = Usj.UsjType, + Version = Usj.UsjVersion, + Content = + [ + new UsjMarker + { + Type = "book", + Marker = "id", + Code = "MAT", + }, + new UsjMarker + { + Type = "chapter", + Marker = "c", + Number = "1", + }, + new UsjMarker + { + Type = "verse", + Marker = "v", + Number = "1", + }, + "Verse 1", + ], + }; + env.PreTranslationService.GetPreTranslationUsfmAsync(Project01, 40, 1, CancellationToken.None) + .Returns(Task.FromResult(usfm)); + env.ParatextService.GetBookText(Arg.Any(), Arg.Any(), 40, usfm).Returns(usx); + + // SUT + Usj actual = await env.Service.GetPreTranslationUsjAsync(User01, Project01, 40, 1, CancellationToken.None); + Assert.That(actual, Is.EqualTo(expected).UsingPropertiesComparer()); + } + [Test] public void GetPreTranslationUsxAsync_CorpusDoesNotSupportUsfm() {