Skip to content

Commit

Permalink
Merge pull request #95 from FirelyTeam/feature/parse-complex-author-o…
Browse files Browse the repository at this point in the history
…bject

Support complex Author objects in manifest.json
  • Loading branch information
mmsmits authored Feb 13, 2024
2 parents 7473968 + ed56aa2 commit 6b3d7fa
Show file tree
Hide file tree
Showing 5 changed files with 271 additions and 2 deletions.
76 changes: 76 additions & 0 deletions Firely.Fhir.Packages.Tests/ManifestSerializationTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,82 @@ public void RoundtripsAllCommonProperties()
manif2.Should().BeEquivalentTo(manif);
}

[TestMethod]
public void TestAuthorSerialization()
{
PackageManifest manif = new(name: "author.test", version: "1.0.1")
{
Author = "Marten"
};

var json = PackageParser.SerializeManifest(manif);

json.Should().Contain("\"author\": \"Marten\"");
var manif2 = PackageParser.ParseManifest(json);
manif2.Should().BeEquivalentTo(manif);
manif2?.AuthorInformation?.Name.Should().Be("Marten");

//Test the other way around

manif = new(name: "author.test", version: "1.0.1")
{
AuthorInformation = new() { Name = "Marten", Url = "https://foo.bar", Email = "[email protected]" }
};

json = PackageParser.SerializeManifest(manif);

json.Should().Contain(" \"author\": {\r\n \"name\": \"Marten\",\r\n \"email\": \"[email protected]\",\r\n \"url\": \"https://foo.bar\"\r\n }");
manif2 = PackageParser.ParseManifest(json);
manif2.Should().BeEquivalentTo(manif);
manif2?.Author?.Should().Be("Marten <[email protected]> (https://foo.bar)");
}

[TestMethod]
public void TestReadingAComplexAuthorProperty()
{
var json = File.ReadAllText($"TestData/unknown-properties-package.json");
var manifest = PackageParser.ParseManifest(json);
//manifest.AuthorInformation()
manifest.Should().NotBeNull();
}

[DataRow("foo <foo@bar> (http://foo.bar)", "foo", "foo@bar", "http://foo.bar")]
[DataRow("foo (http://foo.bar)", "foo", null, "http://foo.bar")]
[DataRow("foo <foo@bar>", "foo", "foo@bar", null)]
[DataRow("<foo@bar> (http://foo.bar)", null, "foo@bar", "http://foo.bar")]
[DataTestMethod]
public void TestAuthorStringParsing(string? fullAuthor, string? name, string? email, string? url)
{
PackageManifest manif = new(name: "authot.test", version: "1.0.1")
{
Author = fullAuthor
};

manif.AuthorInformation?.Name.Should().Be(name);
manif.AuthorInformation?.Email.Should().Be(email);
manif.AuthorInformation?.Url.Should().Be(url);
}

[DataRow("foo <foo@bar> (http://foo.bar)", "foo", "foo@bar", "http://foo.bar")]
[DataRow("foo (http://foo.bar)", "foo", null, "http://foo.bar")]
[DataRow("foo <foo@bar>", "foo", "foo@bar", null)]
[DataRow("<foo@bar> (http://foo.bar)", null, "foo@bar", "http://foo.bar")]
[DataTestMethod]
public void TestAuthorStringSerializing(string? fullAuthor, string? name, string? email, string? url)
{
PackageManifest manif = new(name: "authot.test", version: "1.0.1")
{
AuthorInformation = new()
{
Name = name,
Email = email,
Url = url
}
};

manif.Author.Should().Be(fullAuthor);
}

[TestMethod]
public void DoesNotWriteNulls()
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"dependencies": {
"hl7.fhir.r4.core": "4.0.1"
},
"fhirVersions": [
"4.0.1"
],
"author": {
"name": "Barney Rubble",
"email": "[email protected]",
"url": "http://barnyrubble.tumblr.com/"
}
}
57 changes: 57 additions & 0 deletions Firely.Fhir.Packages/Core/AuthorJsonConverter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
using Newtonsoft.Json;
using System;

namespace Firely.Fhir.Packages
{
/// <summary>
/// Only does something custom for the author element, otherwise just do the regular serialization.
/// </summary>
internal class AuthorJsonConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
{
return objectType == typeof(AuthorInfo) || objectType == typeof(string);
}

public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
{

if (reader.TokenType == JsonToken.StartObject)
{

if (objectType == typeof(AuthorInfo))
{
// Use DummyDictionary to fool JsonSerializer into not using this converter recursively
var author = serializer.Deserialize<DummyDictionary>(reader);
return author;
}
}
else if (reader.TokenType == JsonToken.String && reader.Path == "author")
{
if (reader.Value?.ToString() is { } value)
{
var author = AuthorSerializer.Deserialize(value);
return author;
}
}
return serializer.Deserialize(reader);
}

public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
{
if (value is AuthorInfo author)
{
serializer.Serialize(writer, author.ParsedFromString ? AuthorSerializer.Serialize(author) : value);
}
else
{
serializer.Serialize(writer, value);
}
}

/// <summary>
/// Dummy to fool JsonSerializer into not using this converter recursively
/// </summary>
private class DummyDictionary : AuthorInfo { }
}
}
3 changes: 2 additions & 1 deletion Firely.Fhir.Packages/Core/PackageParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@ public static class PackageParser

public static PackageManifest? ParseManifest(string content)
{
return JsonConvert.DeserializeObject<PackageManifest>(content);
var authorConverter = new AuthorJsonConverter();
return JsonConvert.DeserializeObject<PackageManifest>(content, authorConverter);
}

public static PackageManifest? ParseManifest(byte[] buffer)
Expand Down
124 changes: 123 additions & 1 deletion Firely.Fhir.Packages/Models/JsonModels.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;

namespace Firely.Fhir.Packages
{
Expand Down Expand Up @@ -144,8 +145,21 @@ public PackageManifest(string name, string version)
/// <summary>
/// Author of the package.
/// </summary>
[JsonIgnore]
public string? Author
{
get => (AuthorInformation != null ? AuthorSerializer.Serialize(AuthorInformation) : null);
set
{
if (value is not null)
AuthorInformation = AuthorSerializer.Deserialize(value);

}
}

[JsonConverter(typeof(AuthorJsonConverter))]
[JsonProperty(PropertyName = "author")]
public string? Author;
public AuthorInfo? AuthorInformation;

/// <summary>
/// Other packages that the contents of this packages depend on.
Expand Down Expand Up @@ -684,6 +698,114 @@ internal static void SetFhirVersion(this PackageManifest manifest, string versio
manifest.FhirVersions = new List<string> { version };
}
}



public class AuthorInfo
{
[JsonProperty(PropertyName = "name")]
public string? Name;

[JsonProperty(PropertyName = "email")]
public string? Email;

[JsonProperty(PropertyName = "url")]
public string? Url;

/// <summary>
/// The npm specification allows author information to be serialized in json as a single string, or as a complext object.
/// This boolean keeps track of it was parsed from either one, so it can be serialized to the same output again.
/// </summary>
/// See issue: https://github.com/FirelyTeam/Firely.Fhir.Packages/issues/94
[JsonIgnore]
internal bool ParsedFromString = false;
}

/// <summary>
/// Parse AuthorInfo object based on the following pattern "name <email> (url)"
/// </summary>
internal static class AuthorSerializer
{

private const char EMAIL_START_CHAR = '<';
private const char EMAIL_END_CHAR = '>';
private const char URL_START_CHAR = '(';
private const char URL_END_CHAR = ')';

internal static AuthorInfo Deserialize(string authorString)
{
var authorInfo = new AuthorInfo();

// Extract name
authorInfo.Name = getName(authorString);

// Extract email
authorInfo.Email = getStringBetweenCharacters(EMAIL_START_CHAR, EMAIL_END_CHAR, authorString);

// Extract Url
authorInfo.Url = getStringBetweenCharacters(URL_START_CHAR, URL_END_CHAR, authorString);

//If author was set using parsing of a string, we will think it should be deserialized as a string too.
authorInfo.ParsedFromString = true;

return authorInfo;
}

internal static string Serialize(AuthorInfo authorInfo)
{
var builder = new StringBuilder();
if (authorInfo.Name != null)
{
builder.Append(authorInfo.Name);
}
if (authorInfo.Email != null)
{
builder.Append($" {EMAIL_START_CHAR}{authorInfo.Email}{EMAIL_END_CHAR}");
}
if (authorInfo.Url != null)
{
builder.Append($" {URL_START_CHAR}{authorInfo.Url}{URL_END_CHAR}");
}
return builder.ToString().TrimStart();
}

private static string? getStringBetweenCharacters(char start, char end, string input)
{
// Extract email
var urlStartIndex = input.IndexOf(start);
if (urlStartIndex != -1)
{
var urlEndIndex = input.IndexOf(end, urlStartIndex);
if (urlEndIndex != -1)
{
return input.Substring(urlStartIndex + 1, urlEndIndex - urlStartIndex - 1).Trim();
}
}
return null;
}

private static string? getName(string input)
{
if (input[0] == EMAIL_START_CHAR || input[0] == URL_START_CHAR)
return null;

var nameStartIndex = 0;

var nameEndIndex = input.IndexOf(EMAIL_START_CHAR, nameStartIndex);
if (nameEndIndex != -1)
{
return input.Substring(nameStartIndex, nameEndIndex - nameStartIndex).Trim();
}
else
{
nameEndIndex = input.IndexOf(URL_START_CHAR, nameStartIndex);
return nameEndIndex != -1
? input.Substring(nameStartIndex, nameEndIndex - nameStartIndex).Trim()
: input.Substring(nameStartIndex).Trim();
}
}

}
}

#nullable restore

0 comments on commit 6b3d7fa

Please sign in to comment.