Skip to content

Commit

Permalink
Add C# project data support
Browse files Browse the repository at this point in the history
  • Loading branch information
lyonsil committed Sep 18, 2023
1 parent 5709ed3 commit 8841c45
Show file tree
Hide file tree
Showing 29 changed files with 1,529 additions and 237 deletions.
27 changes: 27 additions & 0 deletions c-sharp-tests/Projects/RawDirectoryProjectStreamManagerTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Paranext.DataProvider.Projects;

namespace TestParanextDataProvider.Projects
{
internal class RawDirectoryProjectStorageInterpreterTests
{
[Test]
public void RawDirPSI_GetExistingDataStreamNames_NotEmpty()
{
var metadata = new ProjectMetadata(
new Guid(),
"test",
ProjectStorageType.ParatextFolders,
"test"
);
// For now we are just reusing the "assets" directory to verify this works when reading from a directory
var projectDetails = new ProjectDetails(metadata, "assets");
var psi = new RawDirectoryProjectStreamManager(projectDetails);
Assert.That(psi.GetExistingDataStreamNames(), Has.Length.GreaterThan(80));
}
}
}
14 changes: 10 additions & 4 deletions c-sharp-tests/c-sharp-tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,17 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.3.2" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.7.2" />
<PackageReference Include="NUnit" Version="3.13.3" />
<PackageReference Include="NUnit3TestAdapter" Version="4.3.0" />
<PackageReference Include="NUnit.Analyzers" Version="3.5.0" />
<PackageReference Include="coverlet.collector" Version="3.1.2" />
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
<PackageReference Include="NUnit.Analyzers" Version="3.6.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="coverlet.collector" Version="6.0.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="ParatextData" Version="9.4.0-beta" />
</ItemGroup>

Expand Down
63 changes: 63 additions & 0 deletions c-sharp/JsonUtils/ProjectDataScopeConverter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
using Newtonsoft.Json.Linq;
using Paranext.DataProvider.Projects;

namespace Paranext.DataProvider.JsonUtils;

internal static class ProjectDataScopeConverter
{
private const string PROJECT_ID = "projectId";
private const string PROJECT_NAME = "projectName";
private const string EXTENSION_NAME = "extensionName";
private const string DATA_TYPE = "dataType";
private const string DATA_QUALIFIER = "dataQualifier";

public static bool TryGetProjectDataScope(
string jsonString,
out ProjectDataScope? dataScope,
out string errorMessage
)
{
try
{
JObject parsedArgs = JObject.Parse(jsonString);
dataScope = new ProjectDataScope()
{
ProjectID = Guid.Parse(Get(parsedArgs, PROJECT_ID)!),
ProjectName = Get(parsedArgs, PROJECT_NAME),
ExtensionName = Get(parsedArgs, EXTENSION_NAME),
DataType = Get(parsedArgs, DATA_TYPE),
DataQualifier = Get(parsedArgs, DATA_QUALIFIER)
};
if (
(dataScope.ProjectID == null)
&& (dataScope.ProjectName == null)
&& (dataScope.ExtensionName == null)
&& (dataScope.DataType == null)
&& (dataScope.DataQualifier == null)
)
{
throw new Exception("Data scope cannot be empty");
}
}
catch (Exception ex)
{
dataScope = null;
errorMessage = ex.ToString();
return false;
}

errorMessage = "";
return true;
}

private static string? Get(JObject jObject, string propertyName)
{
if (
!jObject.TryGetValue(propertyName, out var property)
|| (property.Value<string>() == null)
)
return null;

return property.Value<string>()!;
}
}
61 changes: 61 additions & 0 deletions c-sharp/JsonUtils/ProjectMetadataConverter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
using Newtonsoft.Json.Linq;
using Paranext.DataProvider.Projects;

namespace Paranext.DataProvider.JsonUtils
{
internal static class ProjectMetadataConverter
{
private const string ID = "id";
private const string NAME = "name";
private const string STORAGE_TYPE = "storageType";
private const string PROJECT_TYPE = "projectType";

public static bool TryGetMetadata(
string jsonString,
out ProjectMetadata? projectMetadata,
out string errorMessage
)
{
try
{
JObject parsedArgs = JObject.Parse(jsonString);
Guid id = Guid.Parse(Get(parsedArgs, ID));
string name = Get(parsedArgs, NAME);
Get(parsedArgs, STORAGE_TYPE).FromSerializedString(out ProjectStorageType pst);
string projectType = Get(parsedArgs, PROJECT_TYPE);
projectMetadata = new ProjectMetadata(id, name, pst, projectType);
}
catch (Exception ex)
{
projectMetadata = null;
errorMessage = ex.ToString();
return false;
}

errorMessage = "";
return true;
}

private static string Get(JObject jObject, string propertyName)
{
if (
!jObject.TryGetValue(propertyName, out var property)
|| (property.Value<string>() == null)
)
throw new ArgumentException($"Missing \"{propertyName}\" property in JSON");

return property.Value<string>()!;
}

public static string ToJsonString(this ProjectMetadata projectMetadata)
{
return new JObject
{
[ID] = projectMetadata.ID.ToString(),
[NAME] = projectMetadata.Name,
[STORAGE_TYPE] = projectMetadata.ProjectStorageType.ToSerializedString(),
[PROJECT_TYPE] = projectMetadata.ProjectType
}.ToString();
}
}
}
1 change: 1 addition & 0 deletions c-sharp/JsonUtils/VerseRefConverter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ out string errorMessage
versification
);
}

return true;
}
}
Expand Down
157 changes: 77 additions & 80 deletions c-sharp/NetworkObjects/DataProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,100 +6,97 @@
using System.Text.Json;
using System.Text.Json.Nodes;

namespace Paranext.DataProvider.NetworkObjects
namespace Paranext.DataProvider.NetworkObjects;

internal abstract class DataProvider : NetworkObject
{
internal abstract class DataProvider : NetworkObject
// This is an internal class because nothing else should be instantiating it directly
private class MessageEventDataUpdated : MessageEventGeneric<string>
{
// This is an internal class because nothing else should be instantiating it directly
private class MessageEventDataUpdated : MessageEventGeneric<string>
{
// A parameterless constructor is required for serialization to work
// ReSharper disable once UnusedMember.Local
public MessageEventDataUpdated()
: base(Enum<EventType>.Null) { }
// A parameterless constructor is required for serialization to work
// ReSharper disable once UnusedMember.Local
public MessageEventDataUpdated()
: base(Enum<EventType>.Null) { }

public MessageEventDataUpdated(Enum<EventType> eventType, string dataScope)
: base(eventType, dataScope) { }
}
public MessageEventDataUpdated(Enum<EventType> eventType, string dataScope)
: base(eventType, dataScope) { }
}

private readonly Enum<EventType> _eventType;
private readonly ConcurrentDictionary<
string,
MessageEventDataUpdated
> _updateEventsByScope = new();
private readonly Enum<EventType> _eventType;
private readonly ConcurrentDictionary<string, MessageEventDataUpdated> _updateEventsByScope =
new();

protected DataProvider(string name, PapiClient papiClient)
: base(papiClient)
{
// "-data" is the suffix used by PAPI for data provider names
DataProviderName = name + "-data";
protected DataProvider(string name, PapiClient papiClient)
: base(papiClient)
{
// ReSharper disable once VirtualMemberCallInConstructor
DataProviderName = name + "-data";

// "onDidUpdate" is the event name used by PAPI for data providers to notify consumers of updates
_eventType = new Enum<EventType>($"{DataProviderName}:onDidUpdate");
}
// "onDidUpdate" is the event name used by PAPI for data providers to notify consumers of updates
_eventType = new Enum<EventType>($"{DataProviderName}:onDidUpdate");
}

protected string DataProviderName { get; }
public string DataProviderName { get; }

/// <summary>
/// Register this data provider on the network so that other services can use it
/// </summary>
public async Task RegisterDataProvider()
{
await RegisterNetworkObject(DataProviderName, FunctionHandler);
await StartDataProvider();
}
/// <summary>
/// Register this data provider on the network so that other services can use it
/// </summary>
public async Task RegisterDataProvider()
{
await RegisterNetworkObject(DataProviderName, FunctionHandler);
await StartDataProvider();
}

// An array of strings serialized as JSON will be sent here.
// The first item in the array is the name of the function to call.
// All remaining items are arguments to pass to the function.
// Data providers must provide "get" and "set" functions.
private ResponseToRequest FunctionHandler(dynamic? request)
// An array of strings serialized as JSON will be sent here.
// The first item in the array is the name of the function to call.
// All remaining items are arguments to pass to the function.
// Data providers must provide "get" and "set" functions.
private ResponseToRequest FunctionHandler(dynamic? request)
{
string functionName;
JsonArray jsonArray;
try
{
string functionName;
JsonArray jsonArray;
try
{
jsonArray = ((JsonElement)request!).Deserialize<JsonNode>()!.AsArray();
if (jsonArray.Count == 0)
return ResponseToRequest.Failed(
$"No function name provided when calling data provider {DataProviderName}"
);
functionName = (string)jsonArray[0]!;
jsonArray.RemoveAt(0);
}
catch (Exception e)
{
Console.Error.WriteLine(e.ToString());
return ResponseToRequest.Failed("Invalid function call data");
}

return HandleRequest(functionName, jsonArray);
jsonArray = ((JsonElement)request!).Deserialize<JsonNode>()!.AsArray();
if (jsonArray.Count == 0)
return ResponseToRequest.Failed(
$"No function name provided when calling data provider {DataProviderName}"
);
functionName = (string)jsonArray[0]!;
jsonArray.RemoveAt(0);
}

/// <summary>
/// Notify all processes on the network that this data provider has new data
/// </summary>
/// <param name="dataScope">Indicator of what data changed in the provider</param>
protected void SendDataUpdateEvent(string dataScope)
catch (Exception e)
{
var dataUpdateEventMessage = _updateEventsByScope.GetOrAdd(
dataScope,
(scope) => new MessageEventDataUpdated(_eventType, scope)
);
PapiClient.SendEvent(dataUpdateEventMessage);
Console.Error.WriteLine(e.ToString());
return ResponseToRequest.Failed("Invalid function call data");
}

/// <summary>
/// Once a data provider has started, it should send out update events whenever its data changes.
/// </summary>
protected abstract Task StartDataProvider();
return HandleRequest(functionName, jsonArray);
}

/// <summary>
/// Handle a request from a service using this data provider
/// </summary>
/// <param name="functionName">This would typically be "getXYZ" or "setXYZ", where "XYZ" is a type of data handled by this provider</param>
/// <param name="args">Optional arguments provided by the requester for the function indicated</param>
/// <returns>ResponseToRequest value that either contains a response for the function or an error message</returns>
protected abstract ResponseToRequest HandleRequest(string functionName, JsonArray args);
/// <summary>
/// Notify all processes on the network that this data provider has new data
/// </summary>
/// <param name="dataScope">Indicator of what data changed in the provider</param>
protected void SendDataUpdateEvent(string dataScope)
{
var dataUpdateEventMessage = _updateEventsByScope.GetOrAdd(
dataScope,
(scope) => new MessageEventDataUpdated(_eventType, scope)
);
PapiClient.SendEvent(dataUpdateEventMessage);
}

/// <summary>
/// Once a data provider has started, it should send out update events whenever its data changes.
/// </summary>
protected abstract Task StartDataProvider();

/// <summary>
/// Handle a request from a service using this data provider
/// </summary>
/// <param name="functionName">This would typically be "getXYZ" or "setXYZ", where "XYZ" is a type of data handled by this provider</param>
/// <param name="args">Optional arguments provided by the requester for the function indicated</param>
/// <returns>ResponseToRequest value that either contains a response for the function or an error message</returns>
protected abstract ResponseToRequest HandleRequest(string functionName, JsonArray args);
}
Loading

0 comments on commit 8841c45

Please sign in to comment.