Skip to content

Commit

Permalink
Allow custom formatting of Links in JSON.NET
Browse files Browse the repository at this point in the history
  • Loading branch information
markrendle committed Nov 28, 2013
1 parent 7a4c185 commit ef16b59
Show file tree
Hide file tree
Showing 18 changed files with 323 additions and 182 deletions.
2 changes: 1 addition & 1 deletion VERSION.txt
@@ -1 +1 @@
BUILD_VERSION = "0.10.4"
BUILD_VERSION = "0.11.1"
2 changes: 1 addition & 1 deletion packaging/nuspec/Simple.Web.AspNet.nuspec
Expand Up @@ -14,7 +14,7 @@
<tags>simple.web</tags>
<dependencies>
<dependency id="Simple.Web" version="0.0.0.0" />
<dependency id="Fix.AspNet" version="0.4" />
<dependency id="Fix.AspNet" version="0.6" />
</dependencies>
</metadata>
<files>
Expand Down
28 changes: 0 additions & 28 deletions packaging/nuspec/Simple.Web.Hosting.Self.nuspec

This file was deleted.

Expand Up @@ -62,6 +62,7 @@
<Link>Properties\CommonAssemblyInfo.cs</Link>
</Compile>
<Compile Include="HalJsonMediaTypeHandlerTests.cs" />
<Compile Include="SimpleLinkFormatterTests.cs" />
</ItemGroup>
<ItemGroup>
<None Include="packages.config" />
Expand Down
65 changes: 65 additions & 0 deletions src/Simple.Web.JsonNet.Tests/SimpleLinkFormatterTests.cs
@@ -0,0 +1,65 @@
namespace Simple.Web.JsonNet.Tests
{
using System;
using System.Collections.Generic;
using MediaTypeHandling;
using Newtonsoft.Json.Linq;
using TestHelpers;
using TestHelpers.Sample;
using Xunit;

public class SimpleLinkFormatterTests
{
[Fact]
public void PicksUpOrdersLinkFromCustomer()
{
const string idProperty = @"""id"":42";
const string ordersLink =
@"{""title"":null,""href"":""/customer/42/orders"",""rel"":""customer.orders"",""type"":""application/vnd.list.order+json""}";
const string selfLink =
@"{""title"":null,""href"":""/customer/42"",""rel"":""self"",""type"":""application/vnd.customer+json""}";

var content = new Content(new Uri("http://test.com/customer/42"), new CustomerHandler(),
new Customer {Id = 42});
var target = new JsonMediaTypeHandler
{
JsonLinksFormatter = new SimpleJsonLinksFormatter()
};
string json;
using (var stream = new StringBuilderStream())
{
target.Write<Customer>(content, stream).Wait();
json = stream.StringValue;
}
var jobj = JObject.Parse(json);
var links = jobj["_links"] as JObject;
Assert.NotNull(links);
Assert.Equal(links["customer.orders"].ToString(), "/customer/42/orders");
Assert.Equal(links["self"].ToString(), "/customer/42");
}

[Fact]
public void PicksUpPathFromThing()
{
const string thingLink =
@"{""title"":null,""href"":""/things?path=%2Ffoo%2Fbar"",""rel"":""self"",""type"":""application/json""}";

var content = new Content(new Uri("http://test.com/foo/bar"), new ThingHandler(),
new Thing {Path = "/foo/bar"});
var target = new JsonMediaTypeHandler
{
JsonLinksFormatter = new SimpleJsonLinksFormatter()
};
string json;
using (var stream = new StringBuilderStream())
{
target.Write<Thing>(content, stream).Wait();
json = stream.StringValue;
}
var jobj = JObject.Parse(json);
var links = jobj["_links"] as JObject;
Assert.NotNull(links);
Assert.Equal(links["self"].ToString(), "/things?path=%2Ffoo%2Fbar");
}
}
}
28 changes: 28 additions & 0 deletions src/Simple.Web.JsonNet/DefaultJsonLinksFormatter.cs
@@ -0,0 +1,28 @@
namespace Simple.Web.JsonNet
{
using System.Collections.Generic;
using Links;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;

public class DefaultJsonLinksFormatter : IJsonLinksFormatter
{
private string _linksPropertyName = "links";

public string LinksPropertyName
{
get { return _linksPropertyName; }
set { _linksPropertyName = value; }
}

public void FormatLinks(JContainer container, IEnumerable<Link> links, JsonSerializer serializer)
{
var jLinks = new JArray();
foreach (var link in links)
{
jLinks.Add(JObject.FromObject(link, serializer));
}
container[_linksPropertyName] = jLinks;
}
}
}
26 changes: 26 additions & 0 deletions src/Simple.Web.JsonNet/IJsonLinksFormatter.cs
@@ -0,0 +1,26 @@
namespace Simple.Web.JsonNet
{
using System.Collections.Generic;
using Links;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;

/// <summary>
/// Interface for types that format <see cref="Link"/> collections for JSON.NET
/// </summary>
/// <remarks>
/// The implementing types do not write the actual JSON text, rather, they create a JSON.NET <see cref="JToken"/>
/// allowing them to specify the structure or layout of the object.
/// </remarks>
public interface IJsonLinksFormatter
{
/// <summary>
/// Formats the links for a JSON item.
/// </summary>
/// <param name="container">The object to which the links will be added</param>
/// <param name="links">The links. Guaranteed to be non-empty.</param>
/// <param name="serializer">The serializer.</param>
/// <returns></returns>
void FormatLinks(JContainer container, IEnumerable<Link> links, JsonSerializer serializer);
}
}
26 changes: 18 additions & 8 deletions src/Simple.Web.JsonNet/JsonMediaTypeHandler.cs
Expand Up @@ -11,6 +11,22 @@ namespace Simple.Web.JsonNet
[MediaTypes(MediaType.Json, "application/*+json")]
public class JsonMediaTypeHandler : JsonNetMediaTypeHandlerBase
{
private static IJsonLinksFormatter _defaultJsonLinksFormatter = new DefaultJsonLinksFormatter();

public static IJsonLinksFormatter DefaultJsonLinksFormatter
{
get { return _defaultJsonLinksFormatter; }
set { _defaultJsonLinksFormatter = value; }
}

private IJsonLinksFormatter _jsonLinksFormatter = _defaultJsonLinksFormatter;

public IJsonLinksFormatter JsonLinksFormatter
{
get { return _jsonLinksFormatter; }
set { _jsonLinksFormatter = value; }
}

static JsonMediaTypeHandler()
{
SetDefaultSettings(new JsonSerializerSettings
Expand All @@ -23,18 +39,12 @@ static JsonMediaTypeHandler()

protected override void AddWireFormattedLinks(JToken wireFormattedItem, IEnumerable<Link> itemLinks)
{
IList<Link> links = itemLinks as IList<Link> ?? itemLinks.ToList();
var links = itemLinks as IList<Link> ?? itemLinks.ToList();
if (links.Count == 0)
{
return;
}
var jLinks = new JArray();
foreach (Link link in links)
{
EnsureLinkTypeIsJson(link);
jLinks.Add(JObject.FromObject(link, Serializer));
}
wireFormattedItem["links"] = jLinks;
_jsonLinksFormatter.FormatLinks((JContainer)wireFormattedItem, links.Where(EnsureLinkTypeIsJson), Serializer);
}

protected override JToken WrapCollection(IList<JToken> collection, IEnumerable<Link> collectionLinks)
Expand Down
5 changes: 3 additions & 2 deletions src/Simple.Web.JsonNet/JsonNetMediaTypeHandlerBase.cs
Expand Up @@ -28,7 +28,7 @@ protected JsonSerializer Serializer
get { return _serializer ?? (_serializer = JsonSerializer.Create(Settings)); }
}

protected void EnsureLinkTypeIsJson(Link link)
protected bool EnsureLinkTypeIsJson(Link link)
{
if (String.IsNullOrWhiteSpace(link.Type))
{
Expand All @@ -37,7 +37,8 @@ protected void EnsureLinkTypeIsJson(Link link)
if (!link.Type.EndsWith("json"))
{
link.Type += "+json";
}
}
return true;
}

protected override Task<T> FromWireFormat<T>(JToken wireFormat)
Expand Down
3 changes: 3 additions & 0 deletions src/Simple.Web.JsonNet/Simple.Web.JsonNet.csproj
Expand Up @@ -45,6 +45,8 @@
<Reference Include="System.Xml" />
</ItemGroup>
<ItemGroup>
<Compile Include="DefaultJsonLinksFormatter.cs" />
<Compile Include="IJsonLinksFormatter.cs" />
<Compile Include="JsonMediaTypeHandlerWithDeepLinks.cs" />
<Compile Include="JsonNetMediaTypeHandlerBase.cs" />
<Compile Include="LinkConverter.cs" />
Expand All @@ -54,6 +56,7 @@
</Compile>
<Compile Include="HalJsonMediaTypeHandler.cs" />
<Compile Include="JsonMediaTypeHandler.cs" />
<Compile Include="SimpleJsonLinksFormatter.cs" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Simple.Web\Simple.Web.csproj">
Expand Down
36 changes: 36 additions & 0 deletions src/Simple.Web.JsonNet/SimpleJsonLinksFormatter.cs
@@ -0,0 +1,36 @@
namespace Simple.Web.JsonNet
{
using System.Collections.Generic;
using Links;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;

/// <summary>
/// Formats links in JSON using a light-weight object format.
/// </summary>
/// <example>
/// A customer object might be formatted like this:
///
/// {"name":"ACME","_links":{"self":"/customers/acme","orders":"customers/acme/orders"}}
/// </example>
public class SimpleJsonLinksFormatter : IJsonLinksFormatter
{
private string _linksPropertyName = "_links";

public string LinksPropertyName
{
get { return _linksPropertyName; }
set { _linksPropertyName = value; }
}

public void FormatLinks(JContainer container, IEnumerable<Link> links, JsonSerializer serializer)
{
var jLinks = new JObject();
foreach (var link in links)
{
jLinks[link.Rel] = JValue.CreateString(link.Href);
}
container[_linksPropertyName] = jLinks;
}
}
}
22 changes: 11 additions & 11 deletions src/Simple.Web.Tests/ApplicationTests/ContentTypeTests.cs
Expand Up @@ -7,82 +7,82 @@ public class ContentTypeTests
[Fact]
public void FileWithASingleAcceptTypeReturnsThatType()
{
var type = Application.GetContentType("any", new[] { "test/type" });
var type = StaticContent.GetContentType("any", new[] { "test/type" });

Assert.Equal("test/type", type);
}

[Fact]
public void FileWithMultipleAcceptTypeReturnsFirstType()
{
var type = Application.GetContentType("any", new[] { "type/one", "type/two", "*/*" });
var type = StaticContent.GetContentType("any", new[] { "type/one", "type/two", "*/*" });

Assert.Equal("type/one", type);
}

[Fact]
public void FileWithNullAcceptTypesReturnsPlainText()
{
var type = Application.GetContentType("any", null);
var type = StaticContent.GetContentType("any", null);

Assert.Equal("text/plain", type);
}

[Fact]
public void FileWithEmptyAcceptTypesReturnsPlainText()
{
var type = Application.GetContentType("any", new string[] { });
var type = StaticContent.GetContentType("any", new string[] { });

Assert.Equal("text/plain", type);
}

[Fact]
public void FileWithWildcardAcceptTypeAndNoKnownExtensionReturnsPlainText()
{
var type = Application.GetContentType("any", new[] { "*/*" });
var type = StaticContent.GetContentType("any", new[] { "*/*" });

Assert.Equal("text/plain", type);
}

[Fact]
public void FileWithWildcardAcceptType_Jpg_ExtensionReturns_Jpeg()
{
var type = Application.GetContentType("any.jpg", new[] { "*/*" });
var type = StaticContent.GetContentType("any.jpg", new[] { "*/*" });

Assert.Equal("image/jpeg", type);
}
[Fact]
public void FileWithWildcardAcceptType_Jpeg_ExtensionReturns_Jpeg()
{
var type = Application.GetContentType("any.jpeg", new[] { "*/*" });
var type = StaticContent.GetContentType("any.jpeg", new[] { "*/*" });

Assert.Equal("image/jpeg", type);
}
[Fact]
public void FileWithWildcardAcceptType_Png_ExtensionReturns_Png()
{
var type = Application.GetContentType("any.png", new[] { "*/*" });
var type = StaticContent.GetContentType("any.png", new[] { "*/*" });

Assert.Equal("image/png", type);
}
[Fact]
public void FileWithWildcardAcceptType_Gif_ExtensionReturns_gif()
{
var type = Application.GetContentType("any.gif", new[] { "*/*" });
var type = StaticContent.GetContentType("any.gif", new[] { "*/*" });

Assert.Equal("image/gif", type);
}
[Fact]
public void FileWithWildcardAcceptType_Js_ExtensionReturns_JavaScript()
{
var type = Application.GetContentType("any.js", new[] { "*/*" });
var type = StaticContent.GetContentType("any.js", new[] { "*/*" });

Assert.Equal("text/javascript", type);
}
[Fact]
public void FileWithWildcardAcceptType_Javascript_ExtensionReturns_JavaScript()
{
var type = Application.GetContentType("any.JavaScript", new[] { "*/*" });
var type = StaticContent.GetContentType("any.JavaScript", new[] { "*/*" });

Assert.Equal("text/javascript", type);
}
Expand Down

0 comments on commit ef16b59

Please sign in to comment.