Skip to content

Commit

Permalink
SF-3184 Add USJ support to the get pre-translations API
Browse files Browse the repository at this point in the history
  • Loading branch information
pmachapman committed Feb 3, 2025
1 parent 69e80ba commit 69da47c
Show file tree
Hide file tree
Showing 7 changed files with 295 additions and 0 deletions.
56 changes: 56 additions & 0 deletions src/SIL.XForge.Scripture/Controllers/MachineApiController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -427,6 +428,61 @@ CancellationToken cancellationToken
}
}

/// <summary>
/// Gets the pre-translations for the specified chapter as USJ.
/// </summary>
/// <param name="sfProjectId">The Scripture Forge project identifier.</param>
/// <param name="bookNum">The book number.</param>
/// <param name="chapterNum">The chapter number. If zero, the entire book is returned.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <response code="200">The pre-translations were successfully queried for.</response>
/// <response code="403">You do not have permission to retrieve the pre-translations for this project.</response>
/// <response code="404">The project does not exist or is not configured on the ML server.</response>
/// <response code="405">Retrieving the pre-translations in this format is not supported.</response>
/// <response code="409">The engine has not been built on the ML server.</response>
/// <response code="503">The ML server is temporarily unavailable or unresponsive.</response>
[HttpGet(MachineApi.GetPreTranslationUsj)]
public async Task<ActionResult<Usj>> 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);
}
}

/// <summary>
/// Gets the pre-translations for the specified chapter as USX.
/// </summary>
Expand Down
2 changes: 2 additions & 0 deletions src/SIL.XForge.Scripture/Models/MachineApi.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand Down
1 change: 1 addition & 0 deletions src/SIL.XForge.Scripture/SIL.XForge.Scripture.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@
</Content>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\SIL.Converters.Usj\SIL.Converters.Usj.csproj" />
<ProjectReference Include="..\SIL.XForge\SIL.XForge.csproj" />
</ItemGroup>
<ItemGroup>
Expand Down
8 changes: 8 additions & 0 deletions src/SIL.XForge.Scripture/Services/IMachineApiService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -67,6 +68,13 @@ Task<string> GetPreTranslationUsfmAsync(
bool isServalAdmin,
CancellationToken cancellationToken
);
Task<Usj> GetPreTranslationUsjAsync(
string curUserId,
string sfProjectId,
int bookNum,
int chapterNum,
CancellationToken cancellationToken
);
Task<string> GetPreTranslationUsxAsync(
string curUserId,
string sfProjectId,
Expand Down
37 changes: 37 additions & 0 deletions src/SIL.XForge.Scripture/Services/MachineApiService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -558,6 +559,42 @@ CancellationToken cancellationToken
}
}

public async Task<Usj> 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<UserSecret> 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;

Check warning on line 594 in src/SIL.XForge.Scripture/Services/MachineApiService.cs

View check run for this annotation

Codecov / codecov/patch

src/SIL.XForge.Scripture/Services/MachineApiService.cs#L594

Added line #L594 was not covered by tests
}
}

public async Task<string> GetPreTranslationUsxAsync(
string curUserId,
string sfProjectId,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<Usj> actual = await env.Controller.GetPreTranslationUsjAsync(
Project01,
40,
1,
CancellationToken.None
);

env.ExceptionHandler.Received(1).ReportException(Arg.Any<BrokenCircuitException>());
Assert.IsInstanceOf<ObjectResult>(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<Usj> actual = await env.Controller.GetPreTranslationUsjAsync(
Project01,
40,
1,
CancellationToken.None
);

Assert.IsInstanceOf<ForbidResult>(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<Usj> actual = await env.Controller.GetPreTranslationUsjAsync(
Project01,
40,
1,
CancellationToken.None
);

Assert.IsInstanceOf<NotFoundResult>(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<Usj> actual = await env.Controller.GetPreTranslationUsjAsync(
Project01,
40,
1,
CancellationToken.None
);

Assert.IsInstanceOf<ConflictResult>(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<Usj> actual = await env.Controller.GetPreTranslationUsjAsync(
Project01,
40,
1,
CancellationToken.None
);

Assert.IsInstanceOf<IStatusCodeActionResult>(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<Usj> actual = await env.Controller.GetPreTranslationUsjAsync(
Project01,
40,
1,
CancellationToken.None
);

Assert.IsInstanceOf<OkObjectResult>(actual.Result);
}

[Test]
public async Task GetPreTranslationUsxAsync_MachineApiDown()
{
Expand Down
73 changes: 73 additions & 0 deletions test/SIL.XForge.Scripture.Tests/Services/MachineApiServiceTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<NotSupportedException>(
() => 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<DataNotFoundException>(
() => 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 =
"<usx version=\"3.0\"><book code=\"MAT\" style=\"id\"></book><chapter number=\"1\" style=\"c\" />"
+ "<verse number=\"1\" style=\"v\" />Verse 1</usx>";
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<UserSecret>(), Arg.Any<string>(), 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()
{
Expand Down

0 comments on commit 69da47c

Please sign in to comment.