Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add C# project data support #427

Merged
merged 15 commits into from
Sep 26, 2023
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 = 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 = $"Failed to parse \"{jsonString}\" into ProjectDataScope: {ex}";
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);
string id = Get(parsedArgs, ID);
string name = Get(parsedArgs, NAME);
string projectStorageType = Get(parsedArgs, STORAGE_TYPE);
string projectType = Get(parsedArgs, PROJECT_TYPE);
projectMetadata = new ProjectMetadata(id, name, projectStorageType, 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(ProjectMetadata projectMetadata)
{
return new JObject
{
[ID] = projectMetadata.ID,
[NAME] = projectMetadata.Name,
[STORAGE_TYPE] = projectMetadata.ProjectStorageType,
[PROJECT_TYPE] = projectMetadata.ProjectType
}.ToString();
}
}
}
5 changes: 5 additions & 0 deletions c-sharp/JsonUtils/VerseRefConverter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ namespace Paranext.DataProvider.JsonUtils
{
internal class VerseRefConverter
{
/// <summary>
/// Attempts to convert a string containing JSON to a VerseRef object
/// </summary>
/// <returns>true if the conversion was successful, false otherwise</returns>
public static bool TryCreateVerseRef(
string jsonString,
out VerseRef verseRef,
Expand Down Expand Up @@ -83,6 +87,7 @@ out string errorMessage
versification
);
}

return true;
}
}
Expand Down
171 changes: 96 additions & 75 deletions c-sharp/NetworkObjects/DataProvider.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using Newtonsoft.Json;
using Paranext.DataProvider.MessageHandlers;
using Paranext.DataProvider.Messages;
using Paranext.DataProvider.MessageTransports;
Expand All @@ -6,100 +7,120 @@
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)
{
// "-data" is the suffix used by PAPI for data provider names
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");
}

public string DataProviderName { get; }

protected 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()
// 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
{
await RegisterNetworkObject(DataProviderName, FunctionHandler);
await StartDataProvider();
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);
}

// 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)
/// <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(dynamic? dataScope)
{
string scopeString;

if ((dataScope is string s) && !string.IsNullOrWhiteSpace(s))
{
// If we are returning "*", just pass it as a string. Otherwise we have to provide a JSON list of strings.
// Presumably this will change as part of https://github.com/paranext/paranext-core/issues/443
scopeString = (s == "*") ? s : JsonConvert.SerializeObject(new List<string> { s });
}
else if (dataScope != null)
{
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);
scopeString = JsonConvert.SerializeObject(dataScope);
}
catch (Exception e)
catch (Exception ex)
{
Console.Error.WriteLine(e.ToString());
return ResponseToRequest.Failed("Invalid function call data");
Console.WriteLine($"Unable to send data update event: {ex}");
return;
}

return HandleRequest(functionName, jsonArray);
}
else
return;

/// <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);
}
var dataUpdateEventMessage = _updateEventsByScope.GetOrAdd(
scopeString,
(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>
/// 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);
}
/// <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