diff --git a/JsonApiNet.Tests/Conversion/ComplexArticleTests.cs b/JsonApiNet.Tests/Conversion/ComplexArticleTests.cs deleted file mode 100644 index a704a5b..0000000 --- a/JsonApiNet.Tests/Conversion/ComplexArticleTests.cs +++ /dev/null @@ -1,32 +0,0 @@ -using System; -using JsonApiNet.Components; -using JsonApiNet.Tests.Data; -using JsonApiNet.Tests.Models; -using Microsoft.VisualStudio.TestTools.UnitTesting; -using Newtonsoft.Json; - -namespace JsonApiNet.Tests.Conversion -{ - [TestClass] - public class ComplexArticleTests - { - private JsonApiDocument _document; - - [TestInitialize] - public void TestInitialize() - { - _document = JsonConvert.DeserializeObject>(TestData.ValidDocumentComplexTypesJson()); - } - - [TestMethod] - public void ConversionTest() - { - var article = _document.Resource; - Assert.AreEqual(Guid.Parse("93efd274-72b7-4738-8446-7263a5d54d05"), article.Identifier); - Assert.AreEqual("articles", article.ResourceType); - Assert.AreEqual("JSON API paints my bikeshed!", article.ArticleTitle); - Assert.AreEqual(512, article.Tidbits.IsbnNumber); - Assert.AreEqual("News", article.Tidbits.Genre); - } - } -} diff --git a/JsonApiNet.Tests/Conversion/CompoundResourceTests.cs b/JsonApiNet.Tests/Conversion/CompoundResourceTests.cs deleted file mode 100644 index be43c18..0000000 --- a/JsonApiNet.Tests/Conversion/CompoundResourceTests.cs +++ /dev/null @@ -1,45 +0,0 @@ -using System.Collections.Generic; -using JsonApiNet.Components; -using JsonApiNet.Tests.Data; -using JsonApiNet.Tests.Models; -using Microsoft.VisualStudio.TestTools.UnitTesting; -using Newtonsoft.Json; - -namespace JsonApiNet.Tests.Conversion -{ - [TestClass] - public class CompoundResourceTests - { - private JsonApiDocument> _document; - - [TestInitialize] - public void TestInitialize() - { - _document = JsonConvert.DeserializeObject>>(TestData.ValidDocumentCompoundJson()); - } - - [TestMethod] - public void ConversionTest() - { - var articles = _document.Resource; - - Assert.AreEqual(1, articles.Count); - - var article = articles[0]; - Assert.AreEqual("JSON API paints my bikeshed!", article.Title); - - Assert.IsNotNull(article.WrittenBy); - - var author = article.WrittenBy; - Assert.AreEqual("Dan", author.FirstName); - Assert.AreEqual("Gebhardt", author.LastName); - - Assert.IsNotNull(article.Comments); - - var comments = article.Comments; - Assert.AreEqual(2, comments.Count); - Assert.AreEqual("First!", comments[0].Body); - Assert.AreEqual("I like XML better", comments[1].Body); - } - } -} \ No newline at end of file diff --git a/JsonApiNet.Tests/Conversion/SingularResourceTests.cs b/JsonApiNet.Tests/Conversion/SingularResourceTests.cs deleted file mode 100644 index 0105a98..0000000 --- a/JsonApiNet.Tests/Conversion/SingularResourceTests.cs +++ /dev/null @@ -1,28 +0,0 @@ -using JsonApiNet.Components; -using JsonApiNet.Tests.Data; -using JsonApiNet.Tests.Models; -using Microsoft.VisualStudio.TestTools.UnitTesting; -using Newtonsoft.Json; - -namespace JsonApiNet.Tests.Conversion -{ - [TestClass] - public class SingularResourceTests - { - private JsonApiDocument _document; - - [TestInitialize] - public void TestInitialize() - { - _document = JsonConvert.DeserializeObject>(TestData.ValidDocumentSimpleJson()); - } - - [TestMethod] - public void ConversionTest() - { - var article = _document.Resource; - Assert.AreEqual("42", article.Id); - Assert.AreEqual("JSON API paints my bikeshed!", article.Title); - } - } -} diff --git a/JsonApiNet.Tests/Data/ReadmeAttributeTypeResolution.json b/JsonApiNet.Tests/Data/ReadmeAttributeTypeResolution.json new file mode 100644 index 0000000..46d7469 --- /dev/null +++ b/JsonApiNet.Tests/Data/ReadmeAttributeTypeResolution.json @@ -0,0 +1,11 @@ +{ + "data": { + "type": "ghost_busters", + "id": "Egon Spengler", + "attributes": { + "quotes": [ + "I collect spores, molds, and fungus." + ], + } + } +} \ No newline at end of file diff --git a/JsonApiNet.Tests/Data/ReadmeCompoundDocument.json b/JsonApiNet.Tests/Data/ReadmeCompoundDocument.json new file mode 100644 index 0000000..e00167e --- /dev/null +++ b/JsonApiNet.Tests/Data/ReadmeCompoundDocument.json @@ -0,0 +1,61 @@ +{ + "data": [{ + "type": "articles", + "id": "30cd428f-1a3b-459b-a9a8-0ca87c14dd31", + "attributes": { + "title": "JSON API paints my bikeshed!" + }, + "links": { + "self": "http://example.com/articles/1" + }, + "relationships": { + "author": { + "links": { + "self": "http://example.com/articles/1/relationships/author", + "related": "http://example.com/articles/1/author" + }, + "data": { "type": "people", "id": "9" } + }, + "comments": { + "links": { + "self": "http://example.com/articles/1/relationships/comments", + "related": "http://example.com/articles/1/comments" + }, + "data": [ + { "type": "comments", "id": "5" }, + { "type": "comments", "id": "12" } + ] + } + } + }], + "included": [{ + "type": "people", + "id": "9", + "attributes": { + "first-name": "Dan", + "last-name": "Gebhardt", + "twitter": "dgeb" + }, + "links": { + "self": "http://example.com/people/9" + } + }, { + "type": "comments", + "id": "5", + "attributes": { + "body": "First!" + }, + "links": { + "self": "http://example.com/comments/5" + } + }, { + "type": "comments", + "id": "12", + "attributes": { + "body": "I like XML better" + }, + "links": { + "self": "http://example.com/comments/12" + } + }] +} \ No newline at end of file diff --git a/JsonApiNet.Tests/Data/ReadmeMixedResources.json b/JsonApiNet.Tests/Data/ReadmeMixedResources.json new file mode 100644 index 0000000..bea31aa --- /dev/null +++ b/JsonApiNet.Tests/Data/ReadmeMixedResources.json @@ -0,0 +1,21 @@ +{ + "data": [{ + "type": "articles", + "id": "30cd428f-1a3b-459b-a9a8-0ca87c14dd31", + "attributes": { + "title": "JSON API paints my bikeshed!" + } + },{ + "type": "books", + "id": "4062e824-544c-41c9-9ef0-f05d03476d1e", + "attributes": { + "title": "and I wrote a book about it..." + } + },{ + "type": "magazines", + "id": "85b03dc1-8f43-4660-809d-b3869ca0935a", + "attributes": { + "title": "which was featured in a magazine!" + } + }] +} \ No newline at end of file diff --git a/JsonApiNet.Tests/Data/ReadmeMultipleResources.json b/JsonApiNet.Tests/Data/ReadmeMultipleResources.json new file mode 100644 index 0000000..a5399bd --- /dev/null +++ b/JsonApiNet.Tests/Data/ReadmeMultipleResources.json @@ -0,0 +1,9 @@ +{ + "data": [{ + "type": "articles", + "id": "30cd428f-1a3b-459b-a9a8-0ca87c14dd31", + "attributes": { + "title": "JSON API paints my bikeshed!" + } + }] +} \ No newline at end of file diff --git a/JsonApiNet.Tests/Data/ReadmeResourceTypeResolution.json b/JsonApiNet.Tests/Data/ReadmeResourceTypeResolution.json new file mode 100644 index 0000000..a1b7304 --- /dev/null +++ b/JsonApiNet.Tests/Data/ReadmeResourceTypeResolution.json @@ -0,0 +1,9 @@ +{ + "data": { + "type": "rain_drops", + "id": "1234", + "attributes": { + "splatter": true + } + } +} \ No newline at end of file diff --git a/JsonApiNet.Tests/Data/ReadmeSingleResource.json b/JsonApiNet.Tests/Data/ReadmeSingleResource.json new file mode 100644 index 0000000..1a30593 --- /dev/null +++ b/JsonApiNet.Tests/Data/ReadmeSingleResource.json @@ -0,0 +1,9 @@ +{ + "data": { + "type": "articles", + "id": "30cd428f-1a3b-459b-a9a8-0ca87c14dd31", + "attributes": { + "title": "JSON API paints my bikeshed!" + } + } +} \ No newline at end of file diff --git a/JsonApiNet.Tests/Data/TestData.cs b/JsonApiNet.Tests/Data/TestData.cs index 649ebcf..74d5846 100644 --- a/JsonApiNet.Tests/Data/TestData.cs +++ b/JsonApiNet.Tests/Data/TestData.cs @@ -8,20 +8,55 @@ public static class TestData { public static string ValidDocumentErrorsJson() { - return ReadExample("ValidDocumentErrors.json"); + return ReadEmbeddedResource("ValidDocumentErrors.json"); } public static string ValidDocumentSimpleJson() { - return ReadExample("ValidDocumentSimple.json"); + return ReadEmbeddedResource("ValidDocumentSimple.json"); } public static string ValidDocumentCompoundJson() { - return ReadExample("ValidDocumentCompound.json"); + return ReadEmbeddedResource("ValidDocumentCompound.json"); } - private static string ReadExample(string key) + public static string ValidDocumentComplexTypesJson() + { + return ReadEmbeddedResource("ValidDocumentComplexTypes.json"); + } + + public static string ReadmeSingleResourceJson() + { + return ReadEmbeddedResource("ReadmeSingleResource.json"); + } + + public static string ReadmeMultipleResourcesJson() + { + return ReadEmbeddedResource("ReadmeMultipleResources.json"); + } + + public static string ReadmeCompoundDocumentJson() + { + return ReadEmbeddedResource("ReadmeCompoundDocument.json"); + } + + public static string ReadmeMixedResourcesJson() + { + return ReadEmbeddedResource("ReadmeMixedResources.json"); + } + + public static string ReadmeAttributeTypeResolutionJson() + { + return ReadEmbeddedResource("ReadmeAttributeTypeResolution.json"); + } + + public static string ReadmeResourceTypeResolutionJson() + { + return ReadEmbeddedResource("ReadmeResourceTypeResolution.json"); + } + + private static string ReadEmbeddedResource(string key) { var assembly = Assembly.GetExecutingAssembly(); @@ -40,10 +75,5 @@ private static string ReadExample(string key) } } } - - public static string ValidDocumentComplexTypesJson() - { - return ReadExample("ValidDocumentComplexTypes.json"); - } } } \ No newline at end of file diff --git a/JsonApiNet.Tests/JsonApiNet.Tests.csproj b/JsonApiNet.Tests/JsonApiNet.Tests.csproj index 9575dc4..8ac28a9 100644 --- a/JsonApiNet.Tests/JsonApiNet.Tests.csproj +++ b/JsonApiNet.Tests/JsonApiNet.Tests.csproj @@ -58,19 +58,21 @@ - - - - - - - - + + + + + + + + + + @@ -80,9 +82,15 @@ - + + + + + + + diff --git a/JsonApiNet.Tests/Models/Comment.cs b/JsonApiNet.Tests/Models/Comment.cs deleted file mode 100644 index 3d683fb..0000000 --- a/JsonApiNet.Tests/Models/Comment.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace JsonApiNet.Tests.Models -{ - public class Comment - { - public int Id { get; set; } - public string Body { get; set; } - } -} diff --git a/JsonApiNet.Tests/Models/ComplexArticle.cs b/JsonApiNet.Tests/Models/ComplexArticle.cs deleted file mode 100644 index ce3e435..0000000 --- a/JsonApiNet.Tests/Models/ComplexArticle.cs +++ /dev/null @@ -1,29 +0,0 @@ -using System; -using JsonApiNet.Attributes; -using Newtonsoft.Json; - -namespace JsonApiNet.Tests.Models -{ - public class ComplexArticle - { - [JsonApiId] - public Guid Identifier { get; set; } - - [JsonApiType] - public string ResourceType { get; set; } - - [JsonApiAttribute("title")] - public string ArticleTitle { get; set; } - - // implicitly-defined JsonApiAttribute - public Tidbits Tidbits { get; set; } - } - - public class Tidbits - { - [JsonProperty("isbn")] - public long IsbnNumber { get; set; } - - public string Genre { get; set; } - } -} \ No newline at end of file diff --git a/JsonApiNet.Tests/Models/CompoundArticle.cs b/JsonApiNet.Tests/Models/CompoundArticle.cs deleted file mode 100644 index 318890c..0000000 --- a/JsonApiNet.Tests/Models/CompoundArticle.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System.Collections.Generic; -using JsonApiNet.Attributes; - -namespace JsonApiNet.Tests.Models -{ - public class CompoundArticle - { - public int Id { get; set; } - - public string Title { get; set; } - - [JsonApiRelationship("author")] - public Person WrittenBy { get; set; } - - // implicitly-defined JsonApiRelationship - public List Comments { get; set; } - } -} \ No newline at end of file diff --git a/JsonApiNet.Tests/Models/Person.cs b/JsonApiNet.Tests/Models/Person.cs deleted file mode 100644 index b3dd519..0000000 --- a/JsonApiNet.Tests/Models/Person.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace JsonApiNet.Tests.Models -{ - public class Person - { - public int Id { get; set; } - public string FirstName { get; set; } - public string LastName { get; set; } - public string Twitter { get; set; } - } -} diff --git a/JsonApiNet.Tests/Models/SimpleArticle.cs b/JsonApiNet.Tests/Models/SimpleArticle.cs deleted file mode 100644 index 7d3d95b..0000000 --- a/JsonApiNet.Tests/Models/SimpleArticle.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace JsonApiNet.Tests.Models -{ - public class SimpleArticle - { - public string Id { get; set; } - - public string Title { get; set; } - } -} \ No newline at end of file diff --git a/JsonApiNet.Tests/Parsing/CompoundDocumentTests.cs b/JsonApiNet.Tests/Parsing/CompoundDocumentTests.cs index 00700f1..5c6e2f5 100644 --- a/JsonApiNet.Tests/Parsing/CompoundDocumentTests.cs +++ b/JsonApiNet.Tests/Parsing/CompoundDocumentTests.cs @@ -2,7 +2,6 @@ using JsonApiNet.Components; using JsonApiNet.Tests.Data; using Microsoft.VisualStudio.TestTools.UnitTesting; -using Newtonsoft.Json; namespace JsonApiNet.Tests.Parsing { @@ -14,30 +13,31 @@ public class CompoundDocumentTests [TestInitialize] public void TestInitialize() { - _document = JsonConvert.DeserializeObject(TestData.ValidDocumentCompoundJson()); + var json = TestData.ValidDocumentCompoundJson(); + _document = JsonApi.Document(json); } [TestMethod] - public void DeserializObjectReturnsAnObject() + public void DeserializObjectReturnsAnObjectTest() { Assert.IsNotNull(_document); } [TestMethod] - public void DataIsList() + public void DataIsListTest() { Assert.IsInstanceOfType(_document.Data, typeof(List)); Assert.AreEqual(1, _document.Data.Count); } [TestMethod] - public void Meta() + public void MetaTest() { Assert.AreEqual("api", _document.Meta["json"]); } [TestMethod] - public void JsonApi() + public void JsonApiTest() { Assert.AreEqual("1.0", _document.JsonApi["version"]); } diff --git a/JsonApiNet.Tests/Parsing/ErrorsDocumentTests.cs b/JsonApiNet.Tests/Parsing/ErrorsDocumentTests.cs index 525ed87..3280134 100644 --- a/JsonApiNet.Tests/Parsing/ErrorsDocumentTests.cs +++ b/JsonApiNet.Tests/Parsing/ErrorsDocumentTests.cs @@ -1,5 +1,7 @@ using JsonApiNet.Components; +using JsonApiNet.Exceptions; using JsonApiNet.Tests.Data; +using JsonApiNet.Tests.Readme.AttributePropertyResolution; using Microsoft.VisualStudio.TestTools.UnitTesting; using Newtonsoft.Json; @@ -13,7 +15,8 @@ public class ErrorsDocumentTests [TestInitialize] public void TestInitialize() { - _document = JsonConvert.DeserializeObject(TestData.ValidDocumentErrorsJson()); + var json = TestData.ValidDocumentErrorsJson(); + _document = JsonApi.Document(json); } [TestMethod] @@ -22,6 +25,21 @@ public void DeserializObjectReturnsAnObject() Assert.IsNotNull(_document); } + [TestMethod] + public void ResourceFromDocumentThrows() + { + var json = TestData.ValidDocumentErrorsJson(); + try + { + var res = JsonApi.ResourceFromDocument
(json); + Assert.Fail("Did not throw JsonApiErrorsException!"); + } + catch (JsonApiErrorsException e) + { + Assert.AreEqual("Error Title: Error details go here.", e.Message); + } + } + [TestMethod] public void Errors() { diff --git a/JsonApiNet.Tests/Parsing/SimpleDocumentTests.cs b/JsonApiNet.Tests/Parsing/SimpleDocumentTests.cs index fe22e48..2e12382 100644 --- a/JsonApiNet.Tests/Parsing/SimpleDocumentTests.cs +++ b/JsonApiNet.Tests/Parsing/SimpleDocumentTests.cs @@ -1,48 +1,48 @@ -using JsonApiNet.Components; -using JsonApiNet.Tests.Data; -using Microsoft.VisualStudio.TestTools.UnitTesting; -using Newtonsoft.Json; +//using JsonApiNet.Components; +//using JsonApiNet.Tests.Data; +//using Microsoft.VisualStudio.TestTools.UnitTesting; +//using Newtonsoft.Json; -namespace JsonApiNet.Tests.Parsing -{ - [TestClass] - public class SimpleDocumentTests - { - private JsonApiDocument _document; +//namespace JsonApiNet.Tests.Parsing +//{ +// [TestClass] +// public class SimpleDocumentTests +// { +// private JsonApiDocument _document; - [TestInitialize] - public void TestInitialize() - { - _document = JsonConvert.DeserializeObject(TestData.ValidDocumentSimpleJson()); - Assert.IsFalse(_document.HasErrors); - } +// [TestInitialize] +// public void TestInitialize() +// { +// _document = JsonConvert.DeserializeObject(TestData.ValidDocumentSimpleJson()); +// Assert.IsFalse(_document.HasErrors); +// } - [TestMethod] - public void DeserializObjectReturnsAnObject() - { - Assert.IsNotNull(_document); - } +// [TestMethod] +// public void DeserializObjectReturnsAnObject() +// { +// Assert.IsNotNull(_document); +// } - [TestMethod] - public void NonDataAttributesAreNull() - { - Assert.IsNull(_document.Errors); - Assert.IsNull(_document.Meta); - Assert.IsNull(_document.JsonApi); - Assert.IsNull(_document.Links); - Assert.IsNull(_document.Included); - } +// [TestMethod] +// public void NonDataAttributesAreNull() +// { +// Assert.IsNull(_document.Errors); +// Assert.IsNull(_document.Meta); +// Assert.IsNull(_document.JsonApi); +// Assert.IsNull(_document.Links); +// Assert.IsNull(_document.Included); +// } - [TestMethod] - public void DataIsParsed() - { - var data = _document.Data[0]; +// [TestMethod] +// public void DataIsParsed() +// { +// var data = _document.Data[0]; - Assert.IsNotNull(data); - Assert.AreEqual("42", data.Id); - Assert.AreEqual("articles", data.Type); - Assert.AreEqual("JSON API paints my bikeshed!", data.Attributes["title"]); - Assert.AreEqual("http://example.com/articles/11", data.Links["self"].Href); - } - } -} \ No newline at end of file +// Assert.IsNotNull(data); +// Assert.AreEqual("42", data.Id); +// Assert.AreEqual("articles", data.Type); +// Assert.AreEqual("JSON API paints my bikeshed!", data.Attributes["title"]); +// Assert.AreEqual("http://example.com/articles/11", data.Links["self"].Href); +// } +// } +//} \ No newline at end of file diff --git a/JsonApiNet.Tests/Readme/AttributePropertyResolution/ReadmeAttributePropertyResolutionTests.cs b/JsonApiNet.Tests/Readme/AttributePropertyResolution/ReadmeAttributePropertyResolutionTests.cs new file mode 100644 index 0000000..cd2135f --- /dev/null +++ b/JsonApiNet.Tests/Readme/AttributePropertyResolution/ReadmeAttributePropertyResolutionTests.cs @@ -0,0 +1,24 @@ +using JsonApiNet.Attributes; +using JsonApiNet.Tests.Data; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace JsonApiNet.Tests.Readme.AttributePropertyResolution +{ + [TestClass] + public class ReadmeAttributePropertyResolutionTests + { + [TestMethod] + public void MappedTitleToSubjectTest() + { + var json = TestData.ReadmeSingleResourceJson(); + var article = JsonApi.ResourceFromDocument
(json); + Assert.AreEqual("JSON API paints my bikeshed!", article.Subject); + } + } + + public class Article + { + [JsonApiAttribute("title")] + public string Subject { get; set; } + } +} \ No newline at end of file diff --git a/JsonApiNet.Tests/Readme/AttributeTypeResolution/ReadmeAttributeTypeResolutionTests.cs b/JsonApiNet.Tests/Readme/AttributeTypeResolution/ReadmeAttributeTypeResolutionTests.cs new file mode 100644 index 0000000..87cad9c --- /dev/null +++ b/JsonApiNet.Tests/Readme/AttributeTypeResolution/ReadmeAttributeTypeResolutionTests.cs @@ -0,0 +1,25 @@ +using System.Collections.Generic; +using JsonApiNet.Tests.Data; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace JsonApiNet.Tests.Readme.AttributeTypeResolution +{ + [TestClass] + public class ReadmeAttributeTypeResolutionTests + { + [TestMethod] + public void ArticleFromDocumentTest() + { + var json = TestData.ReadmeAttributeTypeResolutionJson(); + var ghostBuster = JsonApi.ResourceFromDocument(json); + Assert.AreEqual("I collect spores, molds, and fungus.", ghostBuster.Quotes[0]); + } + } + + public class GhostBuster + { + public string Id { get; set; } + + public List Quotes { get; set; } + } +} \ No newline at end of file diff --git a/JsonApiNet.Tests/Readme/CompoundDocument/ReadmeCompoundDocumentTests.cs b/JsonApiNet.Tests/Readme/CompoundDocument/ReadmeCompoundDocumentTests.cs new file mode 100644 index 0000000..4f18dd6 --- /dev/null +++ b/JsonApiNet.Tests/Readme/CompoundDocument/ReadmeCompoundDocumentTests.cs @@ -0,0 +1,50 @@ +using System.Collections.Generic; +using JsonApiNet.Tests.Data; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace JsonApiNet.Tests.Readme.CompoundDocument +{ + [TestClass] + public class ReadmeCompoundDocumentTests + { + [TestMethod] + public void CompoundDocumentTest() + { + var json = TestData.ReadmeCompoundDocumentJson(); + var articles = JsonApi.ResourceFromDocument>(json); + + Assert.IsNotNull(articles); + Assert.AreEqual(1, articles.Count); + + var article = articles[0]; + Assert.AreEqual("JSON API paints my bikeshed!", article.Title); + + var author = articles[0].Author; + Assert.AreEqual("Gebhardt", author.LastName); + + var comments = articles[0].Comments; + Assert.AreEqual("I like XML better", comments[1].Body); + } + } + + public class Article + { + public string Title { get; set; } + + public Person Author { get; set; } + + public List Comments { get; set; } + } + + public class Person + { + public string FirstName { get; set; } + + public string LastName { get; set; } + } + + public class Comment + { + public string Body { get; set; } + } +} \ No newline at end of file diff --git a/JsonApiNet.Tests/Readme/CustomPropertyResolver/ReadmeCustomPropertyResolverTests.cs b/JsonApiNet.Tests/Readme/CustomPropertyResolver/ReadmeCustomPropertyResolverTests.cs new file mode 100644 index 0000000..b27170a --- /dev/null +++ b/JsonApiNet.Tests/Readme/CustomPropertyResolver/ReadmeCustomPropertyResolverTests.cs @@ -0,0 +1,38 @@ +using System; +using System.Reflection; +using JsonApiNet.Resolvers; +using JsonApiNet.Tests.Data; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace JsonApiNet.Tests.Readme.CustomPropertyResolver +{ + [TestClass] + public class ReadmeCustomPropertyResolverTests + { + [TestMethod] + public void CustomPropertyResolverTest() + { + var json = TestData.ReadmeSingleResourceJson(); + var article = JsonApi.ResourceFromDocument
(json, null, new NamingThingsIsHardResolver()); + Assert.AreEqual("JSON API paints my bikeshed!", article.ThingYouCallIt); + } + } + + public class Article + { + public string ThingYouCallIt { get; set; } + } + + public class NamingThingsIsHardResolver : JsonApiPropertyResolver + { + public override PropertyInfo ResolveJsonApiAttribute(Type type, string attributeName) + { + if (type == typeof(Article) && attributeName == "title") + { + return type.GetProperty("ThingYouCallIt"); + } + + return base.ResolveJsonApiAttribute(type, attributeName); + } + } +} \ No newline at end of file diff --git a/JsonApiNet.Tests/Readme/IdTypePropertyResolution/ReadmeIdTypePropertyResolutionTests.cs b/JsonApiNet.Tests/Readme/IdTypePropertyResolution/ReadmeIdTypePropertyResolutionTests.cs new file mode 100644 index 0000000..60bd985 --- /dev/null +++ b/JsonApiNet.Tests/Readme/IdTypePropertyResolution/ReadmeIdTypePropertyResolutionTests.cs @@ -0,0 +1,28 @@ +using System; +using JsonApiNet.Attributes; +using JsonApiNet.Tests.Data; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace JsonApiNet.Tests.Readme.IdTypePropertyResolution +{ + [TestClass] + public class ReadmeIdTypePropertyResolutionTests + { + [TestMethod] + public void MappedIdAndTypeTest() + { + var json = TestData.ReadmeSingleResourceJson(); + var article = JsonApi.ResourceFromDocument
(json); + Assert.AreEqual(Guid.Parse("30cd428f-1a3b-459b-a9a8-0ca87c14dd31"), article.Identifier); + Assert.AreEqual("articles", article.ResourceType); + } + } + + public class Article { + [JsonApiId] + public Guid Identifier { get; set; } + + [JsonApiType] + public string ResourceType { get; set; } + } +} \ No newline at end of file diff --git a/JsonApiNet.Tests/Readme/MixedResources/ReadmeMixedResourceTests.cs b/JsonApiNet.Tests/Readme/MixedResources/ReadmeMixedResourceTests.cs new file mode 100644 index 0000000..7c93f45 --- /dev/null +++ b/JsonApiNet.Tests/Readme/MixedResources/ReadmeMixedResourceTests.cs @@ -0,0 +1,47 @@ +using System; +using System.Collections.Generic; +using JsonApiNet.Tests.Data; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace JsonApiNet.Tests.Readme.MixedResources +{ + [TestClass] + public class ReadmeMixedResourcesTests + { + [TestMethod] + public void MixedResourcesWithCommonInterfaceTest() + { + var json = TestData.ReadmeMixedResourcesJson(); + var titled = JsonApi.ResourceFromDocument>(json); + + Assert.IsNotNull(titled); + Assert.AreEqual(3, titled.Count); + + Assert.AreEqual("JSON API paints my bikeshed!", titled[0].Title); + Assert.AreEqual("and I wrote a book about it...", titled[1].Title); + Assert.AreEqual("which was featured in a magazine!", titled[2].Title); + } + } + + public interface ITitled + { + string Title { get; set; } + } + + public class Article : ITitled + { + public Guid Id { get; set; } + + public string Title { get; set; } + } + + public class Book : ITitled + { + public string Title { get; set; } + } + + public class Magazine : ITitled + { + public string Title { get; set; } + } +} \ No newline at end of file diff --git a/JsonApiNet.Tests/Readme/MultipleResources/ReadmeMultipleResourcesTests.cs b/JsonApiNet.Tests/Readme/MultipleResources/ReadmeMultipleResourcesTests.cs new file mode 100644 index 0000000..0422c6c --- /dev/null +++ b/JsonApiNet.Tests/Readme/MultipleResources/ReadmeMultipleResourcesTests.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; +using JsonApiNet.Tests.Data; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace JsonApiNet.Tests.Readme.MultipleResources +{ + [TestClass] + public class ReadmeMultipleResourcesTests + { + [TestMethod] + public void ArticlesFromDocumentTest() + { + var json = TestData.ReadmeMultipleResourcesJson(); + var articles = JsonApi.ResourceFromDocument>(json); + + Assert.IsNotNull(articles); + Assert.AreEqual(1, articles.Count); + + var article = articles[0]; + Assert.AreEqual("JSON API paints my bikeshed!", article.Title); + } + } + + public class Article + { + public Guid Id { get; set; } + + public string Title { get; set; } + } +} \ No newline at end of file diff --git a/JsonApiNet.Tests/Readme/RelationshipPropertyResolution/ReadmeRelationshipPropertyResolutionTests.cs b/JsonApiNet.Tests/Readme/RelationshipPropertyResolution/ReadmeRelationshipPropertyResolutionTests.cs new file mode 100644 index 0000000..df942e6 --- /dev/null +++ b/JsonApiNet.Tests/Readme/RelationshipPropertyResolution/ReadmeRelationshipPropertyResolutionTests.cs @@ -0,0 +1,52 @@ +using System.Collections.Generic; +using JsonApiNet.Attributes; +using JsonApiNet.Tests.Data; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace JsonApiNet.Tests.Readme.RelationshipPropertyResolution +{ + [TestClass] + public class ReadmeRelationshipPropertyResolutionTests + { + [TestMethod] + public void RemappedRelationshipTest() + { + var json = TestData.ReadmeCompoundDocumentJson(); + var articles = JsonApi.ResourceFromDocument>(json); + + Assert.IsNotNull(articles); + Assert.AreEqual(1, articles.Count); + + var article = articles[0]; + Assert.AreEqual("JSON API paints my bikeshed!", article.Title); + + var author = articles[0].WrittenBy; + Assert.AreEqual("Gebhardt", author.LastName); + Assert.AreEqual("Gebhardt", articles[0].WrittenBy.LastName); // this is the actual README line + + var comments = articles[0].Comments; + Assert.AreEqual("I like XML better", comments[1].Body); + } + } + + public class Article + { + public string Title { get; set; } + + [JsonApiRelationship("author")] + public Person WrittenBy { get; set; } + + public List Comments { get; set; } + } + + public class Person + { + public string FirstName { get; set; } + public string LastName { get; set; } + } + + public class Comment + { + public string Body { get; set; } + } +} \ No newline at end of file diff --git a/JsonApiNet.Tests/Readme/ResourceTypeResolution/ReadmeResourceTypeResolutionTests.cs b/JsonApiNet.Tests/Readme/ResourceTypeResolution/ReadmeResourceTypeResolutionTests.cs new file mode 100644 index 0000000..c7193f8 --- /dev/null +++ b/JsonApiNet.Tests/Readme/ResourceTypeResolution/ReadmeResourceTypeResolutionTests.cs @@ -0,0 +1,47 @@ +using System; +using JsonApiNet.Resolvers; +using JsonApiNet.Tests.Data; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace JsonApiNet.Tests.Readme.ResourceTypeResolution +{ + [TestClass] + public class ReadmeResourceTypeReolutionTests + { + [TestMethod] + public void ContainerVsResourceTypeTest() + { + var json = TestData.ReadmeResourceTypeResolutionJson(); + + var rainDrop = JsonApi.ResourceFromDocument(json); + Assert.IsTrue(((RainDrop)rainDrop).Splatter); + } + + [TestMethod] + public void CustomResourceTypeResolverTest() + { + var json = TestData.ReadmeResourceTypeResolutionJson(); + + var lemonDrop = JsonApi.ResourceFromDocument(json, new BarneyTypeResolver()); + Assert.IsTrue(lemonDrop.Splatter); + } + } + + public class RainDrop + { + public bool Splatter { get; set; } + } + + public class BarneyTypeResolver : IJsonApiTypeResolver + { + public Type ResolveType(string typeName) + { + return typeName == "rain_drops" ? typeof(LemonDrop) : null; + } + } + + public class LemonDrop + { + public bool Splatter { get; set; } + } +} \ No newline at end of file diff --git a/JsonApiNet.Tests/Readme/SingleResource/ReadmeSingleResourceTests.cs b/JsonApiNet.Tests/Readme/SingleResource/ReadmeSingleResourceTests.cs new file mode 100644 index 0000000..fcf060b --- /dev/null +++ b/JsonApiNet.Tests/Readme/SingleResource/ReadmeSingleResourceTests.cs @@ -0,0 +1,34 @@ +using System; +using JsonApiNet.Tests.Data; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace JsonApiNet.Tests.Readme.SingleResource +{ + [TestClass] + public class ReadmeSingleResourceTests + { + [TestMethod] + public void ArticleFromDocumentTest() + { + var json = TestData.ReadmeSingleResourceJson(); + var article = JsonApi.ResourceFromDocument
(json); + Assert.AreEqual("JSON API paints my bikeshed!", article.Title); + } + + [TestMethod] + public void ArticleFromJsonApiDocumentResourceTest() + { + var json = TestData.ReadmeSingleResourceJson(); + var document = JsonApi.Document
(json); + var article = document.Resource; + Assert.AreEqual("JSON API paints my bikeshed!", article.Title); + } + } + + public class Article + { + public Guid Id { get; set; } + + public string Title { get; set; } + } +} \ No newline at end of file diff --git a/JsonApiNet.sln b/JsonApiNet.sln index 88fa38c..477eb1a 100644 --- a/JsonApiNet.sln +++ b/JsonApiNet.sln @@ -10,6 +10,7 @@ EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{A7167AEE-B69A-4F96-B345-3E9DD138A926}" ProjectSection(SolutionItems) = preProject .gitignore = .gitignore + JsonApiNet\JsonApiNet.nuspec = JsonApiNet\JsonApiNet.nuspec README.md = README.md EndProjectSection EndProject diff --git a/JsonApiNet.sln.DotSettings b/JsonApiNet.sln.DotSettings new file mode 100644 index 0000000..a5884b2 --- /dev/null +++ b/JsonApiNet.sln.DotSettings @@ -0,0 +1,2 @@ + + <data><IncludeFilters /><ExcludeFilters><Filter ModuleMask="JsonApiNet" ModuleVersionMask="*" ClassMask="JsonApiNet.Exceptions.*" FunctionMask="*" IsEnabled="True" /><Filter ModuleMask="JsonApiNet" ModuleVersionMask="*" ClassMask="JsonApiNet.Components.JsonApiResourceLinkage" FunctionMask="*" IsEnabled="True" /><Filter ModuleMask="JsonApiNet" ModuleVersionMask="*" ClassMask="JsonApiNet.Components.JsonApiLink" FunctionMask="*" IsEnabled="True" /><Filter ModuleMask="JsonApiNet" ModuleVersionMask="*" ClassMask="JsonApiNet.Components.JsonApiResourceIdentifier" FunctionMask="*" IsEnabled="True" /><Filter ModuleMask="JsonApiNet" ModuleVersionMask="*" ClassMask="JsonApiNet.Components.JsonApiDocument" FunctionMask="*_HasMultipleResources" IsEnabled="True" /><Filter ModuleMask="JsonApiNet" ModuleVersionMask="*" ClassMask="JsonApiNet.Attributes.*" FunctionMask="*" IsEnabled="True" /><Filter ModuleMask="JsonApiNet" ModuleVersionMask="*" ClassMask="JsonApiNet.JsonConverters.*" FunctionMask="*" IsEnabled="True" /></ExcludeFilters></data> \ No newline at end of file diff --git a/JsonApiNet/Attributes/JsonApiAttributeAttribute.cs b/JsonApiNet/Attributes/JsonApiAttributeAttribute.cs index b6ba90c..a9618fd 100644 --- a/JsonApiNet/Attributes/JsonApiAttributeAttribute.cs +++ b/JsonApiNet/Attributes/JsonApiAttributeAttribute.cs @@ -1,6 +1,6 @@ namespace JsonApiNet.Attributes { - public class JsonApiAttributeAttribute : JsonApiBaseAttribute + public class JsonApiAttributeAttribute : JsonApiPropertyAttribute { public JsonApiAttributeAttribute(string attributeName) { diff --git a/JsonApiNet/Attributes/JsonApiIdAttribute.cs b/JsonApiNet/Attributes/JsonApiIdAttribute.cs index 2c32091..adff819 100644 --- a/JsonApiNet/Attributes/JsonApiIdAttribute.cs +++ b/JsonApiNet/Attributes/JsonApiIdAttribute.cs @@ -1,6 +1,6 @@ namespace JsonApiNet.Attributes { - public class JsonApiIdAttribute : JsonApiBaseAttribute + public class JsonApiIdAttribute : JsonApiPropertyAttribute { } } \ No newline at end of file diff --git a/JsonApiNet/Attributes/JsonApiBaseAttribute.cs b/JsonApiNet/Attributes/JsonApiPropertyAttribute.cs similarity index 68% rename from JsonApiNet/Attributes/JsonApiBaseAttribute.cs rename to JsonApiNet/Attributes/JsonApiPropertyAttribute.cs index 5894623..5e62f97 100644 --- a/JsonApiNet/Attributes/JsonApiBaseAttribute.cs +++ b/JsonApiNet/Attributes/JsonApiPropertyAttribute.cs @@ -3,7 +3,7 @@ namespace JsonApiNet.Attributes { [AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)] - public abstract class JsonApiBaseAttribute : Attribute + public abstract class JsonApiPropertyAttribute : Attribute { } } \ No newline at end of file diff --git a/JsonApiNet/Attributes/JsonApiRelationshipAttribute.cs b/JsonApiNet/Attributes/JsonApiRelationshipAttribute.cs index a7399a3..736e51e 100644 --- a/JsonApiNet/Attributes/JsonApiRelationshipAttribute.cs +++ b/JsonApiNet/Attributes/JsonApiRelationshipAttribute.cs @@ -1,6 +1,6 @@ namespace JsonApiNet.Attributes { - public class JsonApiRelationshipAttribute : JsonApiBaseAttribute + public class JsonApiRelationshipAttribute : JsonApiPropertyAttribute { public JsonApiRelationshipAttribute(string relationshipName) { diff --git a/JsonApiNet/Attributes/JsonApiResourceTypeAttribute.cs b/JsonApiNet/Attributes/JsonApiResourceTypeAttribute.cs new file mode 100644 index 0000000..53b97a4 --- /dev/null +++ b/JsonApiNet/Attributes/JsonApiResourceTypeAttribute.cs @@ -0,0 +1,15 @@ +using System; + +namespace JsonApiNet.Attributes +{ + [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)] + public class JsonApiResourceTypeAttribute : Attribute + { + public JsonApiResourceTypeAttribute(string resourceTypeName) + { + JsonApiResourceTypeName = resourceTypeName; + } + + public string JsonApiResourceTypeName { get; set; } + } +} \ No newline at end of file diff --git a/JsonApiNet/Attributes/JsonApiTypeAttribute.cs b/JsonApiNet/Attributes/JsonApiTypeAttribute.cs index 9fce6a4..7da141f 100644 --- a/JsonApiNet/Attributes/JsonApiTypeAttribute.cs +++ b/JsonApiNet/Attributes/JsonApiTypeAttribute.cs @@ -1,6 +1,6 @@ namespace JsonApiNet.Attributes { - public class JsonApiTypeAttribute : JsonApiBaseAttribute + public class JsonApiTypeAttribute : JsonApiPropertyAttribute { } } \ No newline at end of file diff --git a/JsonApiNet/Components/JsonApiDocument.cs b/JsonApiNet/Components/JsonApiDocument.cs index 70f6874..a3f3104 100644 --- a/JsonApiNet/Components/JsonApiDocument.cs +++ b/JsonApiNet/Components/JsonApiDocument.cs @@ -1,11 +1,8 @@ using System.Collections.Generic; using System.Linq; -using JsonApiNet.JsonConverters; -using Newtonsoft.Json; namespace JsonApiNet.Components { - [JsonConverter(typeof(DocumentJsonConverter))] public class JsonApiDocument { public bool HasSingleResource { get; set; } @@ -34,18 +31,12 @@ public bool HasErrors // the HasSingleResource or HasMultipleResources helpers to determine behavior public List Data { get; set; } + public dynamic Resource { get; set; } + // note this glosses over some format issues, like if there are multiple included resources with the same identifier public JsonApiResource GetIncludedResourceByIdentifier(JsonApiResourceIdentifier id) { return Included.FirstOrDefault(jsonApiResource => jsonApiResource.ResourceIdentifier.Equals(id)); } } - - [JsonConverter(typeof(DocumentJsonConverter))] - public class JsonApiDocument : JsonApiDocument - where T : new() - { - // resource(s) represented by this document - public T Resource { get; set; } - } } \ No newline at end of file diff --git a/JsonApiNet/Components/JsonApiResource.cs b/JsonApiNet/Components/JsonApiResource.cs index 22775b4..ab36aaf 100644 --- a/JsonApiNet/Components/JsonApiResource.cs +++ b/JsonApiNet/Components/JsonApiResource.cs @@ -4,37 +4,18 @@ namespace JsonApiNet.Components { public class JsonApiResource { - private string _id; - private string _type; - - public JsonApiResource() + public JsonApiResource(string type, string id) { - ResourceIdentifier = new JsonApiResourceIdentifier(null, null); + Id = id; + Type = type; + ResourceIdentifier = new JsonApiResourceIdentifier(type, id); } [JsonProperty("id")] - public string Id - { - get { return _id; } - - set - { - _id = value; - ResourceIdentifier.Id = _id; - } - } + public string Id { get; private set; } [JsonProperty("type")] - public string Type - { - get { return _type; } - - set - { - _type = value; - ResourceIdentifier.Type = _type; - } - } + public string Type { get; private set; } [JsonProperty("attributes")] public JsonApiAttributes Attributes { get; set; } diff --git a/JsonApiNet/Components/JsonApiResourceLinkage.cs b/JsonApiNet/Components/JsonApiResourceLinkage.cs index dde272e..f850534 100644 --- a/JsonApiNet/Components/JsonApiResourceLinkage.cs +++ b/JsonApiNet/Components/JsonApiResourceLinkage.cs @@ -1,6 +1,4 @@ using System.Collections.Generic; -using JsonApiNet.JsonConverters; -using Newtonsoft.Json; namespace JsonApiNet.Components { @@ -17,7 +15,6 @@ namespace JsonApiNet.Components * * The boolean properties are set by the ResourceLinkageJsonConverter */ - [JsonConverter(typeof(ResourceLinkageJsonConverter))] public class JsonApiResourceLinkage { public List ResourceIdentifiers { get; set; } diff --git a/JsonApiNet/Exceptions/JsonApiErrorsException.cs b/JsonApiNet/Exceptions/JsonApiErrorsException.cs new file mode 100644 index 0000000..d4ef6fa --- /dev/null +++ b/JsonApiNet/Exceptions/JsonApiErrorsException.cs @@ -0,0 +1,22 @@ +using System; +using JsonApiNet.Components; + +namespace JsonApiNet.Exceptions +{ + public class JsonApiErrorsException : Exception + { + public JsonApiErrorsException(JsonApiErrors errors) + : base(errors.Message) + { + Errors = errors; + } + + public JsonApiErrorsException(JsonApiErrors errors, Exception innerException) + : base(errors.Message, innerException) + { + Errors = errors; + } + + private JsonApiErrors Errors { get; set; } + } +} \ No newline at end of file diff --git a/JsonApiNet/Exceptions/JsonApiTypeNotFoundException.cs b/JsonApiNet/Exceptions/JsonApiTypeNotFoundException.cs new file mode 100644 index 0000000..f29f8aa --- /dev/null +++ b/JsonApiNet/Exceptions/JsonApiTypeNotFoundException.cs @@ -0,0 +1,12 @@ +using System; + +namespace JsonApiNet.Exceptions +{ + public class JsonApiTypeNotFoundException : Exception + { + public JsonApiTypeNotFoundException(string message) + : base(message) + { + } + } +} \ No newline at end of file diff --git a/JsonApiNet/Helpers/JsonApiResourceMapper.cs b/JsonApiNet/Helpers/JsonApiResourceMapper.cs deleted file mode 100644 index 17c53b9..0000000 --- a/JsonApiNet/Helpers/JsonApiResourceMapper.cs +++ /dev/null @@ -1,187 +0,0 @@ -using System; -using System.Collections; -using System.Reflection; -using JsonApiNet.Attributes; -using JsonApiNet.Components; -using JsonApiNet.Exceptions; - -namespace JsonApiNet.Helpers -{ - internal class JsonApiResourceMapper - { - private readonly JsonApiDocument _document; - private readonly ResourcePropertyResolver _resolver; - - public JsonApiResourceMapper(JsonApiDocument document, ResourcePropertyResolver resolver) - { - _document = document; - _resolver = resolver; - } - - public object MapJsonApiResource(JsonApiResource jsonApiResource) - { - var resource = (dynamic)Activator.CreateInstance(_resolver.ResourceType); - - var idProperty = _resolver.PropertyInfoForAttributeType(typeof(JsonApiIdAttribute)); - SetParseableStringProperty(resource, idProperty, jsonApiResource.Id); - - var typeProperty = _resolver.PropertyInfoForAttributeType(typeof(JsonApiTypeAttribute)); - SetProperty(resource, typeProperty, jsonApiResource.Type); - - MapAttributes(resource, jsonApiResource); - MapRelationships(resource, jsonApiResource); - - return resource; - } - - private static void SetParseableStringProperty(object resource, PropertyInfo property, object value) - { - if (property == null) - { - return; - } - - var propertyType = property.PropertyType; - - if (propertyType == typeof(String)) - { - SetProperty(resource, property, value); - return; - } - - // attempt to call "T.Parse(Id)" where T is the property's Type - // assign the parsed result to the mapped property - var parseMethod = propertyType.GetMethod( - "Parse", - BindingFlags.Public | BindingFlags.Static, - null, - new[] { typeof(string) }, - null); - - if (parseMethod == null || parseMethod.ReturnType != propertyType) - { - return; - } - - dynamic result = parseMethod.Invoke(null, new[] { value }); - SetProperty(resource, property, result); - } - - private static void SetProperty(object resource, PropertyInfo property, object value) - { - if (property == null) - { - return; - } - - property.SetMethod.Invoke(resource, new[] { value }); - } - - // be refactored to avoid repeating some of the class/list initialization work - private void MapRelationships(object resource, JsonApiResource jsonApiResource) - { - if (jsonApiResource.Relationships == null) - { - return; - } - - foreach (var kvp in jsonApiResource.Relationships) - { - MapRelationship(resource, kvp.Key, kvp.Value); - } - } - - // TODO: this code is pretty similar to what lives in DocumentJsonConverter, can probably DRY it up - // it's basically "instantiate a property resolver, determine if list or single element, map instances" - private void MapRelationship(object resource, string relationshipName, JsonApiRelationship relationship) - { - // if it's an empty to-one relationship, we're done because there's nothing to map into it - if (relationship.IsEmptyToOneRelationship) - { - return; - } - - var relProperty = _resolver.PropertyInfoForJsonApiRelationshipName(relationshipName); - - // if the relationship is not mapped into a property on the resource then we're done - if (relProperty == null) - { - return; - } - - var relPropertyType = relProperty.PropertyType; - var relResourceType = relPropertyType.GetSingleElementType(); - var relPropertyResolver = new ResourcePropertyResolver(relResourceType); - var relResourceMapper = new JsonApiResourceMapper(_document, relPropertyResolver); - - if (relPropertyType.IsListType()) - { - if (relationship.IsIncludedToOneRelationship) - { - throw new JsonApiUsageException("Unable to map to-one relationship into a collection"); - } - - // instantiate relPropertyType, which is an IList - var constructorInfo = relPropertyType.GetConstructor(Type.EmptyTypes); - - if (constructorInfo == null) - { - throw new JsonApiUsageException(string.Format("No default constructor found for {0}", relPropertyType.Name)); - } - - var list = (IList)constructorInfo.Invoke(null); - - foreach (var resourceIdentifier in relationship.Data.ResourceIdentifiers) - { - var includedResource = GetIncludedResourceByIdentifier(resourceIdentifier); - var mappedEntity = relResourceMapper.MapJsonApiResource(includedResource); - list.Add(mappedEntity); - } - - SetProperty(resource, relProperty, list); - } - else - { - if (relationship.IsEmptyToManyRelationship || relationship.IsIncludedToManyRelationship) - { - throw new JsonApiUsageException("Unable to map to-many relationship into a single object"); - } - - var resourceIdentifier = relationship.Data.ResourceIdentifiers[0]; // because IsSingleRelationship is true here - var includedResource = GetIncludedResourceByIdentifier(resourceIdentifier); - var mappedEntity = relResourceMapper.MapJsonApiResource(includedResource); - - SetProperty(resource, relProperty, mappedEntity); - } - } - - private JsonApiResource GetIncludedResourceByIdentifier(JsonApiResourceIdentifier resourceIdentifier) - { - var includedResource = _document.GetIncludedResourceByIdentifier(resourceIdentifier); - - if (includedResource == null) - { - throw new JsonApiFormatException( - string.Format( - "No included resource found for identifier: {0}", - resourceIdentifier)); - } - - return includedResource; - } - - private void MapAttributes(object resource, JsonApiResource jsonApiResource) - { - if (jsonApiResource.Attributes == null) - { - return; - } - - foreach (var kvp in jsonApiResource.Attributes) - { - var attrProperty = _resolver.PropertyInfoForJsonApiAttributeName(kvp.Key); - SetProperty(resource, attrProperty, kvp.Value); - } - } - } -} \ No newline at end of file diff --git a/JsonApiNet/Helpers/JsonApiSettings.cs b/JsonApiNet/Helpers/JsonApiSettings.cs new file mode 100644 index 0000000..0512512 --- /dev/null +++ b/JsonApiNet/Helpers/JsonApiSettings.cs @@ -0,0 +1,16 @@ +using System; +using JsonApiNet.Resolvers; + +namespace JsonApiNet.Helpers +{ + public class JsonApiSettings + { + public IJsonApiTypeResolver TypeResolver { get; set; } + + public IJsonApiPropertyResolver PropertyResolver { get; set; } + + public Type ResultType { get; set; } + + public bool? CreateResource { get; set; } + } +} \ No newline at end of file diff --git a/JsonApiNet/Helpers/PropertyLookupCache.cs b/JsonApiNet/Helpers/PropertyLookupCache.cs new file mode 100644 index 0000000..91c8060 --- /dev/null +++ b/JsonApiNet/Helpers/PropertyLookupCache.cs @@ -0,0 +1,76 @@ +using System; +using System.Collections.Generic; +using System.Reflection; +using JsonApiNet.Attributes; +using JsonApiNet.Exceptions; + +namespace JsonApiNet.Helpers +{ + public class PropertyLookupCache + { + // look up properties with [JsonApiId] or [JsonApiType] attributes + public Dictionary PropertyInfoForAttributeType; + + // look up properties by "name" that have [JsonApiAttribute("name")] + public Dictionary PropertyInfoForJsonApiAttributeName; + + // look up properties by "name" that have [JsonApiRelationship("name")] + public Dictionary PropertyInfoForJsonApiRelationshipName; + + // look up properties by name + public Dictionary PropertyInfoForPropertyName; + + public PropertyLookupCache(Type type) + { + PropertyInfoForPropertyName = new Dictionary(); + PropertyInfoForJsonApiAttributeName = new Dictionary(); + PropertyInfoForJsonApiRelationshipName = new Dictionary(); + PropertyInfoForAttributeType = new Dictionary(); + + InitializeLookupTables(type); + } + + private static void InsertLookupTableEntry(IDictionary dictionary, T key, T1 value) + { + if (dictionary.ContainsKey(key)) + { + throw new JsonApiUsageException(string.Format("The \"{0}\" attribute was specified more than once", key)); + } + + dictionary[key] = value; + } + + private void InitializeLookupTables(Type type) + { + var objectTypeProperties = type.GetProperties(); + + foreach (var propertyInfo in objectTypeProperties) + { + var propertyAttrs = propertyInfo.GetCustomAttributes(typeof(JsonApiPropertyAttribute), true); + + // this will be a list of 1 because AllowMultiple = false on the BaseAttribute class + foreach (var propertyAttribute in propertyAttrs) + { + if (propertyAttribute.GetType() == typeof(JsonApiAttributeAttribute)) + { + // The "AttributeAttribute" stores the attribute name to map into the property + var jsonApiAttributeName = ((JsonApiAttributeAttribute)propertyAttribute).JsonApiAttributeName; + InsertLookupTableEntry(PropertyInfoForJsonApiAttributeName, jsonApiAttributeName, propertyInfo); + } + else if (propertyAttribute.GetType() == typeof(JsonApiRelationshipAttribute)) + { + // The "RelationshipAttribute" stores the relationship name to map into the property + var jsonApiRelationshipName = ((JsonApiRelationshipAttribute)propertyAttribute).JsonApiRelationshipName; + InsertLookupTableEntry(PropertyInfoForJsonApiRelationshipName, jsonApiRelationshipName, propertyInfo); + } + else + { + InsertLookupTableEntry(PropertyInfoForAttributeType, propertyAttribute.GetType(), propertyInfo); + } + } + + InsertLookupTableEntry(PropertyInfoForPropertyName, propertyInfo.Name, propertyInfo); + } + } + } +} \ No newline at end of file diff --git a/JsonApiNet/Helpers/PropertyLookupCacheManager.cs b/JsonApiNet/Helpers/PropertyLookupCacheManager.cs new file mode 100644 index 0000000..229c40f --- /dev/null +++ b/JsonApiNet/Helpers/PropertyLookupCacheManager.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; + +namespace JsonApiNet.Helpers +{ + public class PropertyLookupCacheManager + { + private Dictionary LookupCacheByType { get; set; } + + public PropertyLookupCache ForType(Type type) + { + if (LookupCacheByType == null) + { + LookupCacheByType = new Dictionary(); + } + + if (!LookupCacheByType.ContainsKey(type)) + { + LookupCacheByType[type] = new PropertyLookupCache(type); + } + + return LookupCacheByType[type]; + } + } +} \ No newline at end of file diff --git a/JsonApiNet/Helpers/ResourceMapper.cs b/JsonApiNet/Helpers/ResourceMapper.cs new file mode 100644 index 0000000..8c31ef5 --- /dev/null +++ b/JsonApiNet/Helpers/ResourceMapper.cs @@ -0,0 +1,173 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using JsonApiNet.Components; +using JsonApiNet.Exceptions; + +namespace JsonApiNet.Helpers +{ + internal class ResourceMapper + { + private readonly JsonApiDocument _document; + private readonly JsonApiSettings _settings; + + public ResourceMapper(JsonApiDocument document, JsonApiSettings settings) + { + _document = document; + _settings = settings; + } + + public dynamic ToObject(JsonApiResource jsonApiResource) + { + var typeResolver = _settings.TypeResolver; + var resolvedType = typeResolver.ResolveType(jsonApiResource.Type); + + if (resolvedType == null) + { + throw new JsonApiTypeNotFoundException(string.Format("No type found for {0}", jsonApiResource.Type)); + } + + var resource = (dynamic)Activator.CreateInstance(resolvedType); + + var propResolver = _settings.PropertyResolver; + + var idProperty = propResolver.ResolveJsonApiId(resolvedType); + SetParseableStringProperty(resource, idProperty, jsonApiResource.Id); + + var typeProperty = propResolver.ResolveJsonApiType(resolvedType); + SetProperty(resource, typeProperty, jsonApiResource.Type); + + MapAttributes(resource, jsonApiResource); + MapRelationships(resource, jsonApiResource); + + return resource; + } + + private static void SetParseableStringProperty(dynamic resource, PropertyInfo property, object value) + { + if (property == null) + { + return; + } + + var propertyType = property.PropertyType; + + if (propertyType == typeof(String)) + { + SetProperty(resource, property, value); + return; + } + + // attempt to call "T.Parse(Id)" where T is the property's Type + // assign the parsed result to the mapped property + var parseMethod = propertyType.GetMethod( + "Parse", + BindingFlags.Public | BindingFlags.Static, + null, + new[] { typeof(string) }, + null); + + if (parseMethod == null || parseMethod.ReturnType != propertyType) + { + return; + } + + dynamic result = parseMethod.Invoke(null, new[] { value }); + SetProperty(resource, property, result); + } + + private static void SetProperty(dynamic resource, PropertyInfo property, object value) + { + if (property == null) + { + return; + } + + property.SetMethod.Invoke(resource, new[] { value }); + } + + private void MapRelationships(dynamic resource, JsonApiResource jsonApiResource) + { + if (jsonApiResource.Relationships == null) + { + return; + } + + foreach (var kvp in jsonApiResource.Relationships) + { + MapRelationship(resource, kvp.Key, kvp.Value); + } + } + + private void MapRelationship(dynamic resource, string relationshipName, JsonApiRelationship relationship) + { + // if it's an empty to-one relationship, we're done because there's nothing to map into it + if (relationship.IsEmptyToOneRelationship) + { + return; + } + + var propResolver = _settings.PropertyResolver; + var relProperty = propResolver.ResolveJsonApiRelationship(((object)resource).GetType(), relationshipName); + + // if the relationship is not mapped into a property on the resource then we're done + if (relProperty == null) + { + return; + } + + if (relationship.IsIncludedToOneRelationship) + { + var resourceIdentifier = relationship.Data.ResourceIdentifiers[0]; // because IsSingleRelationship is true here + var includedResource = GetIncludedResourceByIdentifier(resourceIdentifier); + var mappedEntity = ToObject(includedResource); + + SetProperty(resource, relProperty, mappedEntity); + } + else + { + // we'll make a container of the mapped property type + var elementType = relProperty.PropertyType.GetSingleElementType(); + var listType = typeof(List<>).MakeGenericType(new Type[] { elementType }); + var list = (IList)Activator.CreateInstance(listType); + + var jsonApiResources = relationship.Data.ResourceIdentifiers.Select(GetIncludedResourceByIdentifier); + + foreach (var jsonApiResource in jsonApiResources) + { + list.Add(ToObject(jsonApiResource)); + } + + SetProperty(resource, relProperty, list); + } + } + + private JsonApiResource GetIncludedResourceByIdentifier(JsonApiResourceIdentifier resourceIdentifier) + { + var includedResource = _document.GetIncludedResourceByIdentifier(resourceIdentifier); + + if (includedResource == null) + { + throw new JsonApiFormatException(string.Format("No included resource found for identifier: {0}", resourceIdentifier)); + } + + return includedResource; + } + + private void MapAttributes(dynamic resource, JsonApiResource jsonApiResource) + { + if (jsonApiResource.Attributes == null) + { + return; + } + + foreach (var kvp in jsonApiResource.Attributes) + { + var attrProperty = _settings.PropertyResolver.ResolveJsonApiAttribute(((object)resource).GetType(), kvp.Key); + SetProperty(resource, attrProperty, kvp.Value); + } + } + } +} \ No newline at end of file diff --git a/JsonApiNet/Helpers/ResourcePropertyResolver.cs b/JsonApiNet/Helpers/ResourcePropertyResolver.cs deleted file mode 100644 index 56d840b..0000000 --- a/JsonApiNet/Helpers/ResourcePropertyResolver.cs +++ /dev/null @@ -1,158 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Reflection; -using Humanizer; -using JsonApiNet.Attributes; -using JsonApiNet.Exceptions; - -namespace JsonApiNet.Helpers -{ - internal class ResourcePropertyResolver - { - private readonly Dictionary _propertyInfoForAttributeType; - private readonly Dictionary _propertyInfoForJsonApiAttributeName; - private readonly Dictionary _propertyInfoForJsonApiRelationshipName; - private readonly Dictionary _propertyInfoForPropertyName; - - public Type ResourceType { get; private set; } - - public ResourcePropertyResolver(Type resourceType) - { - ResourceType = resourceType; - - _propertyInfoForPropertyName = new Dictionary(); - _propertyInfoForJsonApiAttributeName = new Dictionary(); - _propertyInfoForJsonApiRelationshipName = new Dictionary(); - _propertyInfoForAttributeType = new Dictionary(); - - - InitializeLookupTables(); - } - - public PropertyInfo PropertyInfoForAttributeType(Type attributeType) - { - // Look for a property set via the [JsonApiId], etc. type - if (_propertyInfoForAttributeType.ContainsKey(attributeType)) - { - return _propertyInfoForAttributeType[attributeType]; - } - - var implicitName = ImplicitPropertyNameForAttributeType(attributeType); - - if (_propertyInfoForPropertyName.ContainsKey(implicitName)) - { - return _propertyInfoForPropertyName[implicitName]; - } - - return null; - } - - public PropertyInfo PropertyInfoForJsonApiAttributeName(string name) - { - // Look for a property set via the [JsonApiAttribute("name")] attribute - if (_propertyInfoForJsonApiAttributeName.ContainsKey(name)) - { - return _propertyInfoForJsonApiAttributeName[name]; - } - - var implicitName = ImplicitPropertyNameForJsonApiAttributeName(name); - - if (_propertyInfoForPropertyName.ContainsKey(implicitName)) - { - return _propertyInfoForPropertyName[implicitName]; - } - - return null; - } - - public PropertyInfo PropertyInfoForJsonApiRelationshipName(string name) - { - // Look for a property set via the [JsonApiAttribute("name")] attribute - if (_propertyInfoForJsonApiRelationshipName.ContainsKey(name)) - { - return _propertyInfoForJsonApiRelationshipName[name]; - } - - var implicitName = ImplicitPropertyNameForJsonApiRelationshipName(name); - - if (_propertyInfoForPropertyName.ContainsKey(implicitName)) - { - return _propertyInfoForPropertyName[implicitName]; - } - - return null; - } - - private static string ImplicitPropertyNameForJsonApiAttributeName(string name) - { - return name.Underscore().Pascalize(); - } - - private static string ImplicitPropertyNameForJsonApiRelationshipName(string name) - { - return name.Underscore().Pascalize(); - } - - private static string ImplicitPropertyNameForAttributeType(Type attributeType) - { - if (typeof(JsonApiIdAttribute) == attributeType) - { - return @"Id"; - } - - if (typeof(JsonApiTypeAttribute) == attributeType) - { - return @"Type"; - } - - throw new Exception(string.Format("No implicit property name found for [{0}]", attributeType.Name)); - } - - private static void InsertLookupTableEntry(IDictionary dictionary, T key, T1 value) - { - if (dictionary.ContainsKey(key)) - { - throw new JsonApiUsageException(string.Format("The \"{0}\" attribute was specified more than once", key)); - } - - dictionary[key] = value; - } - - // Reflect all properties of the _resourceType, building lookup tables for: - // 1. Get the Property for a JsonApiAttribute named 'title' - // 2. Get the Property with the [JsonApiId] attribute set - // 3. Get the Property with name "Title" - private void InitializeLookupTables() - { - var objectTypeProperties = ResourceType.GetProperties(); - - foreach (var propertyInfo in objectTypeProperties) - { - var propertyAttrs = propertyInfo.GetCustomAttributes(typeof(JsonApiBaseAttribute), true); - - // this will be a list of 1 because AllowMultiple = false on the BaseAttribute class - foreach (var propertyAttribute in propertyAttrs) - { - if (propertyAttribute.GetType() == typeof(JsonApiAttributeAttribute)) - { - // The "AttributeAttribute" stores the attribute name to map into the property - var jsonApiAttributeName = ((JsonApiAttributeAttribute)propertyAttribute).JsonApiAttributeName; - InsertLookupTableEntry(_propertyInfoForJsonApiAttributeName, jsonApiAttributeName, propertyInfo); - } - else if (propertyAttribute.GetType() == typeof(JsonApiRelationshipAttribute)) - { - // The "RelationshipAttribute" stores the relationship name to map into the property - var jsonApiRelationshipName = ((JsonApiRelationshipAttribute)propertyAttribute).JsonApiRelationshipName; - InsertLookupTableEntry(_propertyInfoForJsonApiRelationshipName, jsonApiRelationshipName, propertyInfo); - } - else - { - InsertLookupTableEntry(_propertyInfoForAttributeType, propertyAttribute.GetType(), propertyInfo); - } - } - - InsertLookupTableEntry(_propertyInfoForPropertyName, propertyInfo.Name, propertyInfo); - } - } - } -} \ No newline at end of file diff --git a/JsonApiNet/Helpers/TypeExtensions.cs b/JsonApiNet/Helpers/TypeExtensions.cs index 10593da..004457d 100644 --- a/JsonApiNet/Helpers/TypeExtensions.cs +++ b/JsonApiNet/Helpers/TypeExtensions.cs @@ -6,22 +6,30 @@ namespace JsonApiNet.Helpers { public static class TypeExtensions { - // type inherits from IList, return true - public static bool IsListType(this Type type) - { - return type.GetInterfaces().Any(x => x.IsGenericType && x.GetGenericTypeDefinition() == typeof(IList<>)); - } - - // type is T or List, return T + // given some sort of IEnumerable, return T + // given any other Type, return it public static Type GetSingleElementType(this Type type) { - return type.IsListType() ? type.GetGenericArguments()[0] : type; + return type.IsAssignableToGenericType(typeof(IEnumerable<>)) ? type.GetGenericArguments()[0] : type; } - // type is Foo, return T - public static Type GetSingleGenericType(this Type type) + // http://stackoverflow.com/questions/74616/how-to-detect-if-type-is-another-generic-type/1075059#1075059 + public static bool IsAssignableToGenericType(this Type givenType, Type genericType) { - return type.IsGenericType ? type.GetGenericArguments()[0] : null; + var interfaceTypes = givenType.GetInterfaces(); + + if (interfaceTypes.Any(it => it.IsGenericType && it.GetGenericTypeDefinition() == genericType)) + { + return true; + } + + if (givenType.IsGenericType && givenType.GetGenericTypeDefinition() == genericType) + { + return true; + } + + var baseType = givenType.BaseType; + return baseType != null && IsAssignableToGenericType(baseType, genericType); } } } \ No newline at end of file diff --git a/JsonApiNet/IJsonApiNetSerializer.cs b/JsonApiNet/IJsonApiNetSerializer.cs new file mode 100644 index 0000000..6f31b64 --- /dev/null +++ b/JsonApiNet/IJsonApiNetSerializer.cs @@ -0,0 +1,7 @@ +namespace JsonApiNet +{ + public interface IJsonApiNetSerializer + { + dynamic ResourceFromDocument(string json); + } +} \ No newline at end of file diff --git a/JsonApiNet/JsonApi.cs b/JsonApiNet/JsonApi.cs new file mode 100644 index 0000000..ca6509b --- /dev/null +++ b/JsonApiNet/JsonApi.cs @@ -0,0 +1,81 @@ +using System; +using JsonApiNet.Components; +using JsonApiNet.Helpers; +using JsonApiNet.Resolvers; + +namespace JsonApiNet +{ + public static class JsonApi + { + public static JsonApiDocument Document(string json, Type resultType = null, JsonApiSettings settings = null) + { + var serializer = MakeSerializer(resultType, settings); + + // default is to skip resource creation + if (serializer.Settings.CreateResource == null) + { + serializer.Settings.CreateResource = false; + } + + return serializer.Document(json); + } + + public static dynamic ResourceFromDocument(string json, Type resultType = null, JsonApiSettings settings = null) + { + var serializer = MakeSerializer(resultType, settings); + serializer.Settings.CreateResource = true; + + return serializer.ResourceFromDocument(json); + } + + public static JsonApiDocument Document( + string json, + IJsonApiTypeResolver typeResolver = null, + IJsonApiPropertyResolver propertyResolver = null) + { + var settings = new JsonApiSettings + { + CreateResource = true, + PropertyResolver = propertyResolver, + TypeResolver = typeResolver + }; + + var singleElementType = typeof(T).GetSingleElementType(); + + return Document(json, singleElementType, settings); + } + + public static T ResourceFromDocument( + string json, + IJsonApiTypeResolver typeResolver = null, + IJsonApiPropertyResolver propertyResolver = null) + { + var settings = new JsonApiSettings + { + CreateResource = true, + PropertyResolver = propertyResolver, + TypeResolver = typeResolver, + }; + + var singleElementType = typeof(T).GetSingleElementType(); + + return ResourceFromDocument(json, singleElementType, settings); + } + + private static JsonApiNetSerializer MakeSerializer(Type resultType, JsonApiSettings settings) + { + settings = settings ?? new JsonApiSettings(); + + settings.ResultType = settings.ResultType ?? resultType; + + if (settings.TypeResolver == null) + { + settings.TypeResolver = new ReflectingTypeResolver(resultType); + } + + settings.PropertyResolver = settings.PropertyResolver ?? new JsonApiPropertyResolver(); + + return new JsonApiNetSerializer(settings); + } + } +} \ No newline at end of file diff --git a/JsonApiNet/JsonApiNet.csproj b/JsonApiNet/JsonApiNet.csproj index 80083c8..f47de61 100644 --- a/JsonApiNet/JsonApiNet.csproj +++ b/JsonApiNet/JsonApiNet.csproj @@ -47,24 +47,35 @@ + - - - + + + + + + + + + + + + + + - + - - + @@ -74,8 +85,10 @@ - + + + diff --git a/JsonApiNet/JsonApiNet.nuspec b/JsonApiNet/JsonApiNet.nuspec index 2a1c397..269f816 100644 --- a/JsonApiNet/JsonApiNet.nuspec +++ b/JsonApiNet/JsonApiNet.nuspec @@ -2,19 +2,17 @@ JsonApiNet - 1.0.0 - l8nite - l8nite + 2.0.0 + Shaun Guth http://www.gnu.org/licenses/gpl-2.0.txt https://github.com/l8nite/JsonApiNet false Deserialize JSON API documents with ease - [v1.0.0] - - Parse JsonApiDocument structure - - Mapping Id, Type, and Attributes into Resource class - - Supports complex types for [JsonApiAttribute("name")] properties - - Map relationships with data links to included resources + [v2.0.0] + - A more usable interface + - Support for heterogenous collections + - Smarter type inference Copyright 2015 JSON JSONAPI diff --git a/JsonApiNet/JsonApiNetSerializer.cs b/JsonApiNet/JsonApiNetSerializer.cs new file mode 100644 index 0000000..9b0be92 --- /dev/null +++ b/JsonApiNet/JsonApiNetSerializer.cs @@ -0,0 +1,33 @@ +using JsonApiNet.Components; +using JsonApiNet.Helpers; +using JsonApiNet.JsonConverters; +using Newtonsoft.Json; + +namespace JsonApiNet +{ + public class JsonApiNetSerializer : IJsonApiNetSerializer + { + public JsonApiSettings Settings { get; set; } + + public JsonApiNetSerializer(JsonApiSettings settings) + { + Settings = settings; + } + + public dynamic ResourceFromDocument(string json) + { + var document = Document(json); + return document.Resource; + } + + public JsonApiDocument Document(string json) + { + return JsonConvert.DeserializeObject( + json, + new JsonSerializerSettings + { + ContractResolver = new ContractResolver(Settings) + }); + } + } +} \ No newline at end of file diff --git a/JsonApiNet/JsonConverters/AttributesJsonConverter.cs b/JsonApiNet/JsonConverters/AttributesJsonConverter.cs deleted file mode 100644 index 60b98c9..0000000 --- a/JsonApiNet/JsonConverters/AttributesJsonConverter.cs +++ /dev/null @@ -1,69 +0,0 @@ -using System; -using JsonApiNet.Components; -using JsonApiNet.Helpers; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; - -namespace JsonApiNet.JsonConverters -{ - internal class AttributesJsonConverter : JsonConverter - { - private readonly ResourcePropertyResolver _resolver; - - public AttributesJsonConverter(ResourcePropertyResolver resolver) - { - _resolver = resolver; - } - - public override bool CanRead - { - get { return true; } - } - - public override bool CanWrite - { - get { return false; } - } - - public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) - { - throw new NotImplementedException(); - } - - // This allows us to deserialize values in the 'attributes' for a JsonApiResource into complex types - // using the standard Json.NET deserialization properties/etc. - // This Converter is only used when it is registered, which only happens as part of the DocumentJsonConverter - // when deserializing a JSON API document into a JsonApiDocument. It reflects on T's properties to find - // the appropriate object types to convert the JSON into. - public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) - { - var attributesToken = JToken.Load(reader); - - if (attributesToken == null) - { - return null; - } - - var attributesObject = (JObject)attributesToken; - - var obj = new JsonApiAttributes(); - - foreach (var attributeProperty in attributesObject.Properties()) - { - var propertyInfo = _resolver.PropertyInfoForJsonApiAttributeName(attributeProperty.Name); - - if (propertyInfo != null) - { - obj[attributeProperty.Name] = attributeProperty.Value.ToObject(propertyInfo.PropertyType); - } - } - - return obj; - } - - public override bool CanConvert(Type objectType) - { - return objectType == typeof(JsonApiAttributes); - } - } -} \ No newline at end of file diff --git a/JsonApiNet/JsonConverters/ContractResolver.cs b/JsonApiNet/JsonConverters/ContractResolver.cs new file mode 100644 index 0000000..2324523 --- /dev/null +++ b/JsonApiNet/JsonConverters/ContractResolver.cs @@ -0,0 +1,43 @@ +using System; +using JsonApiNet.Components; +using JsonApiNet.Helpers; +using Newtonsoft.Json; +using Newtonsoft.Json.Serialization; + +namespace JsonApiNet.JsonConverters +{ + internal class ContractResolver : DefaultContractResolver + { + private readonly JsonApiSettings _jsonApiSettings; + + public ContractResolver(JsonApiSettings jsonApiSettings) + { + _jsonApiSettings = jsonApiSettings; + } + + protected override JsonConverter ResolveContractConverter(Type objectType) + { + if (objectType == typeof(JsonApiDocument)) + { + return new DocumentJsonConverter(_jsonApiSettings); + } + + if (objectType == typeof(JsonApiResource)) + { + return new ResourceJsonConverter(_jsonApiSettings); + } + + if (objectType == typeof(JsonApiLink)) + { + return new LinkJsonConverter(); + } + + if (objectType == typeof(JsonApiResourceLinkage)) + { + return new ResourceLinkageJsonConverter(); + } + + return base.ResolveContractConverter(objectType); + } + } +} \ No newline at end of file diff --git a/JsonApiNet/JsonConverters/DocumentJsonConverter.cs b/JsonApiNet/JsonConverters/DocumentJsonConverter.cs index f40be5e..d25bd33 100644 --- a/JsonApiNet/JsonConverters/DocumentJsonConverter.cs +++ b/JsonApiNet/JsonConverters/DocumentJsonConverter.cs @@ -1,6 +1,7 @@ using System; using System.Collections; using System.Collections.Generic; +using System.Configuration; using JsonApiNet.Components; using JsonApiNet.Exceptions; using JsonApiNet.Helpers; @@ -11,6 +12,13 @@ namespace JsonApiNet.JsonConverters { internal class DocumentJsonConverter : JsonConverter { + private readonly JsonApiSettings _settings; + + public DocumentJsonConverter(JsonApiSettings settings) + { + _settings = settings; + } + public override bool CanRead { get { return true; } @@ -26,63 +34,54 @@ public override void WriteJson(JsonWriter writer, object value, JsonSerializer s throw new NotImplementedException(); } - // objectType is JsonApiDocument where T is the desired Resource class - public override object ReadJson(JsonReader reader, Type jsonApiDocumentType, object existingValue, JsonSerializer serializer) + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) { var token = JToken.Load(reader); - var document = (JsonApiDocument)Activator.CreateInstance(jsonApiDocumentType); - MapTopLevelElements(document, token); + var document = new JsonApiDocument + { + Errors = ReadProperty(token, "errors", serializer), + Meta = ReadProperty(token, "meta", serializer), + JsonApi = ReadProperty(token, "jsonapi", serializer), + Links = ReadProperty(token, "links", serializer), + Included = ReadProperty>(token, "included", serializer) + }; - var dataToken = token["data"]; - - if (dataToken == null) + if (document.HasErrors && _settings.CreateResource == true) { - return document; + throw new JsonApiErrorsException(document.Errors); } - var resourceType = GetIndividualResourceTypeForJsonApiDocument(jsonApiDocumentType); - var hasResourceType = resourceType != null; - - JsonSerializer cerealizer; - ResourcePropertyResolver resolver = null; + var dataToken = token["data"]; - if (hasResourceType) - { - // sneaky, we're passing an attribute property resolver for the generic type T from JsonApiDocument - // into the contract resolver, which passes it to the AttributesJsonConverter, which can deserialize - // JsonApiAttributes objects using the complex objects found in "T"'s [JsonApiAttribute] mappings. - resolver = new ResourcePropertyResolver(resourceType); - cerealizer = new JsonSerializer - { - ContractResolver = new JsonApiAttributesContractResolver(resolver) - }; - } - else + if (dataToken == null) { - cerealizer = serializer; + return document; } - // the "data" member can be an array or a single resource, we'll store it as a List - // and set some boolean flags appropriately so that callers can do the right thing - if (dataToken.Type == JTokenType.Array) + switch (dataToken.Type) { - document.HasSingleResource = false; - document.Data = dataToken.ToObject>(cerealizer); - } - else - { - document.HasSingleResource = true; - document.Data = new List - { - dataToken.ToObject(cerealizer) - }; + // this is a multiple-resource document + case JTokenType.Array: + document.HasSingleResource = false; + document.Data = dataToken.ToObject>(serializer); + break; + + // this is a single-resource document + case JTokenType.Object: + document.HasSingleResource = true; + document.Data = new List + { + dataToken.ToObject(serializer) + }; + break; + default: + throw new JsonApiFormatException("The 'data' member was not an array or object"); } - // if they're trying to DeserializeObject>, we now have everything we need to build Resources of type T - if (hasResourceType) + if (_settings.CreateResource == true) { - ((dynamic)document).Resource = MapDocumentToGenericType(document, jsonApiDocumentType, resolver); + document.Resource = CreateResource(document); } return document; @@ -90,71 +89,43 @@ public override object ReadJson(JsonReader reader, Type jsonApiDocumentType, obj public override bool CanConvert(Type objectType) { - return objectType == typeof(JsonApiDocument<>) || objectType == typeof(JsonApiDocument); + return objectType == typeof(JsonApiDocument) || objectType == typeof(JsonApiDocument); } - // At the point of this method being called, the PropertyResolver should be instantiated for - // the specific individual resource type of the document - public dynamic MapDocumentToGenericType(JsonApiDocument document, Type jsonApiDocumentType, ResourcePropertyResolver resolver) + private static T ReadProperty(JToken token, string name, JsonSerializer serializer) where T : class { - var genericType = jsonApiDocumentType.GetSingleGenericType(); - var isListType = genericType.IsListType(); - - if ((document.HasSingleResource && isListType) || (document.HasMultipleResources && !isListType)) - { - throw new JsonApiUsageException( - "T from JsonApiDocument is an IList; however, this document represents a single resource"); - } + return (token == null || token[name] == null) ? null : token[name].ToObject(serializer); + } + private dynamic CreateResource(JsonApiDocument document) + { if (document.HasSingleResource) { - return MapJsonApiResource(document, document.Data[0], resolver); + return CreateResource(document, document.Data[0]); } - // instantiate genericType, which should be an IList - var constructorInfo = genericType.GetConstructor(Type.EmptyTypes); + // else if (document.HasMultipleResources) + var list = (IList)new List(); - if (constructorInfo == null) + if (_settings.ResultType != null) { - throw new JsonApiUsageException(string.Format("No default constructor found for {0}", genericType.Name)); + // if ResourceFromDocument> was used, ResultType will be T + var listType = typeof(List<>).MakeGenericType(new[] { _settings.ResultType }); + list = (IList)Activator.CreateInstance(listType); } - var list = (IList)constructorInfo.Invoke(null); - - // map each individual JsonApiResource from document.Data - foreach (var resource in document.Data) + foreach (var jsonApiResource in document.Data) { - list.Add(MapJsonApiResource(document, resource, resolver)); + list.Add(CreateResource(document, jsonApiResource)); } return list; } - private static void MapTopLevelElements(JsonApiDocument document, JToken token) - { - // use the standard Json.NET converters to handle all of these - document.Errors = token["errors"] != null ? token["errors"].ToObject() : null; - document.Meta = token["meta"] != null ? token["meta"].ToObject() : null; - document.JsonApi = token["jsonapi"] != null ? token["jsonapi"].ToObject() : null; - document.Links = token["links"] != null ? token["links"].ToObject() : null; - document.Included = token["included"] != null ? token["included"].ToObject>() : null; - } - - // jsonApiDocumentType is "JsonApiDocument" - // if T is an ICollection, return T1, otherwise return T - private static Type GetIndividualResourceTypeForJsonApiDocument(Type jsonApiDocumentType) - { - var typeOfT = jsonApiDocumentType.GetSingleGenericType(); - return typeOfT == null ? null : typeOfT.GetSingleElementType(); - } - - private static object MapJsonApiResource( - JsonApiDocument document, - JsonApiResource jsonApiResource, - ResourcePropertyResolver resolver) + private dynamic CreateResource(JsonApiDocument document, JsonApiResource jsonApiResource) { - var x = new JsonApiResourceMapper(document, resolver); - return x.MapJsonApiResource(jsonApiResource); + var mapper = new ResourceMapper(document, _settings); + return mapper.ToObject(jsonApiResource); } } } \ No newline at end of file diff --git a/JsonApiNet/JsonConverters/JsonApiAttributesContractResolver.cs b/JsonApiNet/JsonConverters/JsonApiAttributesContractResolver.cs deleted file mode 100644 index f1d1529..0000000 --- a/JsonApiNet/JsonConverters/JsonApiAttributesContractResolver.cs +++ /dev/null @@ -1,29 +0,0 @@ -using System; -using JsonApiNet.Components; -using JsonApiNet.Helpers; -using Newtonsoft.Json; -using Newtonsoft.Json.Serialization; - -namespace JsonApiNet.JsonConverters -{ - internal class JsonApiAttributesContractResolver : DefaultContractResolver - { - private readonly ResourcePropertyResolver _resolver; - - public JsonApiAttributesContractResolver(ResourcePropertyResolver resolver) - { - _resolver = resolver; - } - - protected override JsonConverter ResolveContractConverter(Type objectType) - { - if (objectType == null || !typeof(JsonApiAttributes).IsAssignableFrom(objectType)) - { - // use the standard converter for everything except JsonApiAttributes - return base.ResolveContractConverter(objectType); - } - - return new AttributesJsonConverter(_resolver); - } - } -} \ No newline at end of file diff --git a/JsonApiNet/JsonConverters/ResourceJsonConverter.cs b/JsonApiNet/JsonConverters/ResourceJsonConverter.cs new file mode 100644 index 0000000..7b6c29d --- /dev/null +++ b/JsonApiNet/JsonConverters/ResourceJsonConverter.cs @@ -0,0 +1,107 @@ +using System; +using JsonApiNet.Components; +using JsonApiNet.Exceptions; +using JsonApiNet.Helpers; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace JsonApiNet.JsonConverters +{ + internal class ResourceJsonConverter : JsonConverter + { + private readonly JsonApiSettings _settings; + + public ResourceJsonConverter(JsonApiSettings settings) + { + _settings = settings; + } + + public override bool CanRead + { + get { return true; } + } + + public override bool CanWrite + { + get { return false; } + } + + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + throw new NotImplementedException(); + } + + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + var resourceToken = JToken.Load(reader); + + if (resourceToken == null) + { + return null; + } + + if (resourceToken.Type != JTokenType.Object) + { + throw new JsonApiFormatException("Individual resource element must be an Object"); + } + + var resourceObj = (JObject)resourceToken; + + var jsonApiResource = new JsonApiResource((string)resourceObj["type"], (string)resourceObj["id"]); + + jsonApiResource.Attributes = ReadAttributes(resourceObj["attributes"], jsonApiResource, serializer); + jsonApiResource.Relationships = ReadProperty(resourceObj, "relationships", serializer); + jsonApiResource.Links = ReadProperty(resourceObj, "links", serializer); + jsonApiResource.Meta = ReadProperty(resourceObj, "meta", serializer); + + return jsonApiResource; + } + + public override bool CanConvert(Type objectType) + { + return objectType == typeof(JsonApiResource); + } + + private static T ReadProperty(JToken token, string name, JsonSerializer serializer) where T : class + { + return (token == null || token[name] == null) ? null : token[name].ToObject(serializer); + } + + private JsonApiAttributes ReadAttributes(JToken attributesToken, JsonApiResource resource, JsonSerializer serializer) + { + if (attributesToken == null) + { + return null; + } + + var resolvedType = _settings.TypeResolver.ResolveType(resource.Type); + + var attributesObject = (JObject)attributesToken; + + var obj = new JsonApiAttributes(); + + foreach (var attributeProperty in attributesObject.Properties()) + { + if (resolvedType != null) + { + var propertyInfo = _settings.PropertyResolver.ResolveJsonApiAttribute(resolvedType, attributeProperty.Name); + + if (propertyInfo != null) + { + obj[attributeProperty.Name] = attributeProperty.Value.ToObject(propertyInfo.PropertyType, serializer); + } + else + { + obj[attributeProperty.Name] = attributeProperty.Value.ToObject(typeof(object)); + } + } + else + { + obj[attributeProperty.Name] = attributeProperty.Value.ToObject(typeof(object)); + } + } + + return obj; + } + } +} \ No newline at end of file diff --git a/JsonApiNet/Properties/AssemblyInfo.cs b/JsonApiNet/Properties/AssemblyInfo.cs index 86f3bf6..5c36ec4 100644 --- a/JsonApiNet/Properties/AssemblyInfo.cs +++ b/JsonApiNet/Properties/AssemblyInfo.cs @@ -29,5 +29,5 @@ // You can specify all the values or you can default the Build and Revision Numbers // by using the '*' as shown below: // [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("1.0.0.0")] -[assembly: AssemblyFileVersion("1.0.0.0")] \ No newline at end of file +[assembly: AssemblyVersion("2.0.0.0")] +[assembly: AssemblyFileVersion("2.0.0.0")] \ No newline at end of file diff --git a/JsonApiNet/Resolvers/IJsonApiPropertyResolver.cs b/JsonApiNet/Resolvers/IJsonApiPropertyResolver.cs new file mode 100644 index 0000000..dda908d --- /dev/null +++ b/JsonApiNet/Resolvers/IJsonApiPropertyResolver.cs @@ -0,0 +1,16 @@ +using System; +using System.Reflection; + +namespace JsonApiNet.Resolvers +{ + public interface IJsonApiPropertyResolver + { + PropertyInfo ResolveJsonApiAttribute(Type type, string attributeName); + + PropertyInfo ResolveJsonApiRelationship(Type type, string relationshipName); + + PropertyInfo ResolveJsonApiType(Type type); + + PropertyInfo ResolveJsonApiId(Type type); + } +} \ No newline at end of file diff --git a/JsonApiNet/Resolvers/IJsonApiTypeResolver.cs b/JsonApiNet/Resolvers/IJsonApiTypeResolver.cs new file mode 100644 index 0000000..5c5ace3 --- /dev/null +++ b/JsonApiNet/Resolvers/IJsonApiTypeResolver.cs @@ -0,0 +1,9 @@ +using System; + +namespace JsonApiNet.Resolvers +{ + public interface IJsonApiTypeResolver + { + Type ResolveType(string typeName); + } +} \ No newline at end of file diff --git a/JsonApiNet/Resolvers/JsonApiPropertyResolver.cs b/JsonApiNet/Resolvers/JsonApiPropertyResolver.cs new file mode 100644 index 0000000..b337c29 --- /dev/null +++ b/JsonApiNet/Resolvers/JsonApiPropertyResolver.cs @@ -0,0 +1,88 @@ +using System; +using System.Reflection; +using Humanizer; +using JsonApiNet.Attributes; +using JsonApiNet.Helpers; + +namespace JsonApiNet.Resolvers +{ + public class JsonApiPropertyResolver : IJsonApiPropertyResolver + { + public PropertyLookupCacheManager PropertyLookupCacheManager { get; private set; } + + public JsonApiPropertyResolver() + { + PropertyLookupCacheManager = new PropertyLookupCacheManager(); + } + + public virtual PropertyInfo ResolveJsonApiAttribute(Type type, string attributeName) + { + var propertyLookupCache = PropertyLookupCacheManager.ForType(type); + var cache = propertyLookupCache.PropertyInfoForJsonApiAttributeName; + + // Look for a property set via the [JsonApiAttribute("name")] attribute + if (cache.ContainsKey(attributeName)) + { + return cache[attributeName]; + } + + // Look for a property name with the normalized form of the attributeName + return PropertyInfoByName(propertyLookupCache, attributeName.Underscore().Pascalize()); + } + + public virtual PropertyInfo ResolveJsonApiRelationship(Type type, string relationshipName) + { + var propertyLookupCache = PropertyLookupCacheManager.ForType(type); + var cache = propertyLookupCache.PropertyInfoForJsonApiRelationshipName; + + // Look for a property set via the [JsonApiRelationship("name")] attribute + if (cache.ContainsKey(relationshipName)) + { + return cache[relationshipName]; + } + + // Look for a property name with the normalized form of the relationshipName + return PropertyInfoByName(propertyLookupCache, relationshipName.Underscore().Pascalize()); + } + + public virtual PropertyInfo ResolveJsonApiType(Type type) + { + var propertyLookupCache = PropertyLookupCacheManager.ForType(type); + var cache = propertyLookupCache.PropertyInfoForAttributeType; + + var jsonApiTypeAttributeType = typeof(JsonApiTypeAttribute); + + if (cache.ContainsKey(jsonApiTypeAttributeType)) + { + return cache[jsonApiTypeAttributeType]; + } + + return PropertyInfoByName(propertyLookupCache, @"Type"); + } + + public virtual PropertyInfo ResolveJsonApiId(Type type) + { + var propertyLookupCache = PropertyLookupCacheManager.ForType(type); + var cache = propertyLookupCache.PropertyInfoForAttributeType; + + var jsonApiIdAttribute = typeof(JsonApiIdAttribute); + + if (cache.ContainsKey(jsonApiIdAttribute)) + { + return cache[jsonApiIdAttribute]; + } + + return PropertyInfoByName(propertyLookupCache, @"Id"); + } + + protected static PropertyInfo PropertyInfoByName(PropertyLookupCache propertyLookupCache, string propertyName) + { + if (propertyLookupCache.PropertyInfoForPropertyName.ContainsKey(propertyName)) + { + return propertyLookupCache.PropertyInfoForPropertyName[propertyName]; + } + + return null; + } + } +} \ No newline at end of file diff --git a/JsonApiNet/Resolvers/ReflectingTypeResolver.cs b/JsonApiNet/Resolvers/ReflectingTypeResolver.cs new file mode 100644 index 0000000..6207693 --- /dev/null +++ b/JsonApiNet/Resolvers/ReflectingTypeResolver.cs @@ -0,0 +1,117 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Humanizer; +using JsonApiNet.Attributes; +using JsonApiNet.Exceptions; +using JsonApiNet.Helpers; + +namespace JsonApiNet.Resolvers +{ + internal class ReflectingTypeResolver : IJsonApiTypeResolver + { + private readonly Type _protoType; + private readonly Dictionary _typeByJsonApiResourceTypeName; + private readonly Dictionary _typeByName; + + public ReflectingTypeResolver() + : this(null) + { + } + + public ReflectingTypeResolver(Type protoType) + { + _protoType = protoType; + + _typeByJsonApiResourceTypeName = new Dictionary(); + _typeByName = new Dictionary(); + + InitializeLookupTables(); + } + + public Type ResolveType(string typeName) + { + if (_typeByJsonApiResourceTypeName.ContainsKey(typeName)) + { + return _typeByJsonApiResourceTypeName[typeName]; + } + + var classifiedName = Classify(typeName); + + if (_typeByName.ContainsKey(classifiedName)) + { + return _typeByName[classifiedName]; + } + + throw new JsonApiTypeNotFoundException(string.Format("Could not ResolveType for '{0}'", typeName)); + } + + private static string Classify(string typeName) + { + return typeName.Underscore().Singularize().Pascalize(); + } + + private void InitializeLookupTables() + { + // these are first-one-wins into the _typeByName cache, so they are resolved in top-down priority order + if (_protoType != null) + { + AddTypeToLookupTables(_protoType); + + // add the rest of the types in the _protoType's namespace, then everything else in its Assembly + var leftovers = new List(); + foreach (var type in _protoType.Assembly.GetTypes()) + { + if (type.Namespace == _protoType.Namespace) + { + AddTypeToLookupTables(type); + } + else + { + leftovers.Add(type); + } + } + + foreach (var type in leftovers) + { + AddTypeToLookupTables(type); + } + } + + foreach (var type in AppDomain.CurrentDomain.GetAssemblies().SelectMany(a => a.GetTypes())) + { + AddTypeToLookupTables(type); + } + } + + private void AddTypeToLookupTables(Type type) + { + if (type == null) + { + return; + } + + // we've seen it already + if (_typeByName.ContainsKey(type.Name)) + { + return; + } + + // look this Type up by name + _typeByName[type.Name] = type; + + if (!type.IsDefined(typeof(JsonApiResourceTypeAttribute), true)) + { + return; + } + + var attribute = + type.GetCustomAttributes(typeof(JsonApiResourceTypeAttribute), true).FirstOrDefault() as JsonApiResourceTypeAttribute; + + if (attribute != null) + { + _typeByJsonApiResourceTypeName[attribute.JsonApiResourceTypeName] = type; + } + } + } +} \ No newline at end of file diff --git a/JsonApiNet/Resolvers/StringToTypeResolver.cs b/JsonApiNet/Resolvers/StringToTypeResolver.cs new file mode 100644 index 0000000..713d975 --- /dev/null +++ b/JsonApiNet/Resolvers/StringToTypeResolver.cs @@ -0,0 +1,44 @@ +using System; +using System.Collections.Generic; +using JsonApiNet.Exceptions; + +namespace JsonApiNet.Resolvers +{ + public class StringToTypeResolver : IJsonApiTypeResolver + { + private readonly Dictionary _stringToType; + + public StringToTypeResolver() + : this(null) + { + } + + public StringToTypeResolver(Dictionary stringToType) + { + _stringToType = stringToType ?? new Dictionary(); + } + + public void RegisterType(string typeName, Type type) + { + _stringToType[typeName] = type; + } + + public void DeregisterType(string typeName) + { + if (_stringToType.ContainsKey(typeName)) + { + _stringToType.Remove(typeName); + } + } + + public Type ResolveType(string typeName) + { + if (_stringToType.ContainsKey(typeName)) + { + return _stringToType[typeName]; + } + + throw new JsonApiTypeNotFoundException(string.Format("No Type was registered for {0}", typeName)); + } + } +} \ No newline at end of file diff --git a/README.md b/README.md index bee6793..ca330d2 100644 --- a/README.md +++ b/README.md @@ -1,19 +1,12 @@ JsonApiNet --- ---- -### This is an in-progress branch for `v2.0` of the `JsonApiNet` library, which is under active development and subject to change. See the `v1.0` branch for the initial library implementation and README. ### - -### Major changes in `v2.0` will include support for custom Type and Property resolvers (and a more consistent implementation), a less annoying interface than `JsonConvert.DeserializeObject>>`), support for heterogenous resource collections and support for dependency injection. - ---- - -An easy-to-use, extensible C# library for JSON API documents. +An easy-to-use, extensible C# library for JSON API documents. - Deserialization of complex attributes -- Compound document support for included resources -- Automatic `Type` and `Property` resolution -- Attribute and relationship property re-mapping +- Compound document support for included resources +- Extensible `Type` and `Property` resolution with intelligent defaults +- Attribute and relationship mapping - Full object graph for the JSON API document - And more! @@ -23,6 +16,13 @@ Built on top of [Json.NET](https://github.com/JamesNK/Newtonsoft.Json) and also > Note: This library doesn't provide a serializer (yet). There are a couple alternative serializers for JSON API out there, such as the [JsonApiMediaTypeFormatter](https://github.com/rmichela/JsonApiMediaTypeFormatter). I haven't done a ton of exploration in this area yet, but I would eventually like to extend this library to support both serialization and deserialization. If you're the author of one of these libraries and think it'd be a good fit to merge with this project, let me know. +Getting Started +--- + +`JsonApiNet` is available on `NuGet` ([See package details on nuget.org](https://www.nuget.org/packages/JsonApiNet/2.0.0)) + + Install-Package JsonApiNet + Single Resource --- @@ -48,13 +48,13 @@ And we want to parse this response and into an `Article` class: We do so by calling `ResourceFromDocument`: - var article = JsonApiNet.ResourceFromDocument
(json); + var article = JsonApi.ResourceFromDocument
(json); Assert.AreEqual("JSON API paints my bikeshed!", article.Title); Multiple Resources --- -From the example above, changing `"data"` into an array gives us a collection with 1 resource in it: +Consider this collection (it's the same as the previous example, except `"data"` is an array now). { "data": [{ @@ -66,12 +66,73 @@ From the example above, changing `"data"` into an array gives us a collection wi }] } -This time, we want to parse this response into a `List
`: +We can get a `List
` from it: - var articles = JsonApiNet.ResourceFromDocument>(json); + var articles = JsonApi.ResourceFromDocument>(json); Assert.AreEqual("JSON API paints my bikeshed!", articles[0].Title); -If your document's collection contains heterogenous resource types, you must ensure that the container given to `ResourceFromDocument` can store them or else you will get exceptions at run-time. + +Mixed Resource Types +--- +If your document's collection has heterogenous resource types, you must ensure that the container given to `ResourceFromDocument` can store them or else you will get exceptions at run-time. + +For example, consider the following document: + + { + "data": [{ + "type": "articles", + "id": "30cd428f-1a3b-459b-a9a8-0ca87c14dd31", + "attributes": { + "title": "JSON API paints my bikeshed!" + } + },{ + "type": "books", + "id": "4062e824-544c-41c9-9ef0-f05d03476d1e", + "attributes": { + "title": "and I wrote a book about it..." + } + },{ + "type": "magazines", + "id": "85b03dc1-8f43-4660-809d-b3869ca0935a", + "attributes": { + "title": "which was featured in a magazine!" + } + }] + } + +Let's say we wrote a common interface `ITitled`: + + public interface ITitled { + string Title { get; set; } + } + +And classes to hold the `Article`, `Book` and `Magazine` types, all of which implement `ITitled` + + public class Article : ITitled { + public Guid Id { get; set; } + public string Title { get; set; } + } + + public class Book : ITitled { + public string Title { get; set; } + } + + public class Magazine : ITitled { + public string Title { get; set; } + } + +Then we can parse this into a `List`: + + var titled = JsonApi.ResourceFromDocument>(json); + Assert.AreEqual("JSON API paints my bikeshed!", titled[0].Title); + Assert.AreEqual("and I wrote a book about it...", titled[1].Title); + Assert.AreEqual("which was featured in a magazine!", titled[2].Title); + +> If you forgot to make `Article` implement `ITitled`, you would see an exception like this: +> +> System.ArgumentException: The value "JsonApiNet.Tests.Readme.MixedResources.Article" is +> not of type "JsonApiNet.Tests.Readme.MixedResources.ITitled" and cannot be used in this +> generic collection. Compound Documents @@ -163,7 +224,7 @@ And a `Comment` class for the comments: Nothing changes in how we retrieve the `List
`: - var articles = JsonApiNet.ResourceFromDocument>(json); + var articles = JsonApi.ResourceFromDocument>(json); Assert.AreEqual("JSON API paints my bikeshed!", articles[0].Title); The author and comments are available too! @@ -181,11 +242,11 @@ In the examples, you might have noticed that there is a case mismatch between th The `JsonApiPropertyResolver` is what's responsible for finding the correct property on your `Type`. -> The default resolution process is: -> +> The default resolution process is: +> > 1. First look in the `Type` for properties with the `[JsonApiAttribute("name")]` attribute, where `"name"` matches the the key from the `"attributes"` object in the document. If a property is found, use that. -> -> 2. Next, look in the `Type` for a name that matches a normalized version of the key from the `"attributes"` object in the document. The normalization applies `Underscore()` and `PascalCase()` to the key, e.g. `"first-name"` becomes `FirstName`. +> +> 2. Next, look in the `Type` for a name that matches a normalized version of the key from the `"attributes"` object in the document. The normalization applies `Underscore()` and `PascalCase()` to the key, e.g. `"first-name"` becomes `FirstName`. For example, we could map the `"title"` attribute into a property named `Subject`: @@ -193,29 +254,42 @@ For example, we could map the `"title"` attribute into a property named `Subject [JsonApiAttribute("title")] public string Subject { get; set; } } + +Then fetch it back out: + + var article = JsonApi.ResourceFromDocument
(json); + Assert.AreEqual("JSON API paints my bikeshed!", article.Subject); + + +Customizing Property Resolution +--- +If you have complex rules, or you can't annotate your mapped classes, then you will need to write a custom `IJsonApiPropertyResolver` and give it to the serializer when converting your document. -If for some reason you can't annotate your classes with the `[JsonApiAttribute("name")]` attribute, then you will need to write a custom `IJsonApiPropertyResolver` and hand it off to the serializer when converting your document. For example: +For example, here is an `Article` with a property named `ThingYouCallit` - public class MonopolyResolver : IJsonApiPropertyResolver { - public PropertyInfo ResolveAttribute(Type type, string attributeName) { - if (attributeName == "monopoly") { - return type.GetProperty("boardwalk"); + public class Article { + public string ThingYouCallIt { get; set; } + } + +We'll write a custom `NamingThingsIsHardResolver` that can map the `title` attribute from our JSON API document into the `ThingYouCallIt` property: + + public class NamingThingsIsHardResolver : JsonApiPropertyResolver { + public override PropertyInfo ResolveJsonApiAttribute(Type type, string attributeName) { + if (type == typeof(Article) && attributeName == "title") { + return type.GetProperty("ThingYouCallIt"); } - - return null; + + return base.ResolveJsonApiAttribute(type, attributeName); } } You can use it like this: - var boardGame = JsonApiNet.ResourceFromDocument( - json, - new SerializerSettings { - PropertyResolver = new MonopolyResolver() - } - ); + var article = JsonApi.ResourceFromDocument
(json, null, new NamingThingsIsHardResolver()); + Assert.AreEqual("JSON API paints my bikeshed!", article.ThingYouCallIt); + -`"attributes"` object keys which do not resolve to a valid property are skipped. +> Note: Returning `null` for the `ResolveFoo` calls will skip assignment of that property. Type Resolution for Attributes @@ -245,7 +319,10 @@ We can map it into a `GhostBuster` class like this: In this example, the `JsonApiPropertyResolver` will resolve the `"quotes"` attribute into the `Quotes` property and then proceed to deserialize the quotes into a `List`. -> Note: This is using `Json.NET` under the hood, so you can map complex objects from your `"attributes"` values (or apply a custom `JsonConverter` to them, etc.)! + var ghostBuster = JsonApi.ResourceFromDocument(json); + Assert.AreEqual("I collect spores, molds, and fungus.", ghostBuster.Quotes[0]); + +> Note: This is using `Json.NET` under the hood, so you can map complex objects from your `"attributes"` values using `[JsonProperty]`, specify a custom `JsonConverter` on them, etc.! Property Resolution for Relationships @@ -259,7 +336,12 @@ For example, let's say we want to map the `"author"` relationship into a propert [JsonApiRelationship("author")] public Person WrittenBy { get; set; } } - + +And then we can use it like this: + + var articles = JsonApi.ResourceFromDocument>(json); + Assert.AreEqual("Gebhardt", articles[0].WrittenBy.LastName); + Type Resolution for Relationships --- @@ -271,24 +353,30 @@ Using the _Compound Document_ example from above, this **will not work**: [JsonApiRelationship("author")] public Comment Author { get; set; } } - -This doesn't work because the `"type"` for the `"author"` relationship is `"people"`, and the `JsonApiTypeResolver` will resolve this to the `Person` class. The new `Person` instance will then be assigned to the `Author` property (which we've purposefully typed as a `Comment`), resulting in a run-time exception. + +This doesn't work because the `"type"` for the `"author"` relationship is `"people"`, and the `JsonApiTypeResolver` will resolve this to the `Person` class. The new `Person` instance will then be assigned to the `Author` property (which in this example we've incorrectly specified is a `Comment`), resulting in a run-time exception. + +> You'll get an exception like: +> +> System.ArgumentException: Object of type +> 'JsonApiNet.Tests.Readme.RelationshipPropertyResolution.Person' +> cannot be converted to type +> 'JsonApiNet.Tests.Readme.RelationshipPropertyResolution.Comment' Property Resolution for Id and Type --- If you want to map the `"id"` and `"type"` fields of the resource into your class, you can use the `[JsonApiId]` or `[JsonApiType]` attributes respectively: - public class Article - { + public class Article { [JsonApiId] public Guid Identifier { get; set; } [JsonApiType] public string ResourceType { get; set; } } - - + + For the `"id"` field, `JsonApiNet` supports any `Type` that implements a static `Parse(string)` method and will use it to coerce the value found in the document to the appropriate type. @@ -296,23 +384,30 @@ Type Resolution for Resources --- How does `JsonApiNet` know which class to instantiate for a given resource `"type"`? Enter the `JsonApiTypeResolver`. -When you call `ResourceFromDocument`, you get back an instance of type `T`; however, that is *not* the `Type` used to instantiate your object. +When you call `ResourceFromDocument`, you get back an instance of type `T`, but that doesn't mean `JsonApiNet` is instantiating an object of type `T` for your resource types. For example, this works: - var article = JsonApiNet.ResourceFromDocument(json); + var article = JsonApi.ResourceFromDocument(json); Assert.AreEqual("JSON API paints my bikeshed!", ((Article)article).Title); - -Notice that the object `Type` is still `Article` (it casts successfully); however, I gave `ResourceFromDocument` the `object` type. + +Notice that it still got an `Article` (it casts successfully) even though we gave `ResourceFromDocument` a generic type of `object`. The `JsonApiTypeResolver` is responsible for determining the correct class to instantiate based on the `"type"` attribute associated with the resource in the JSON API document. -> The default resolution process is: -> -> 1. First look in the caller's assembly for classes with the `[JsonApiResourceType("name")]` attribute, where `"name"` matches the `"type"` from the document. If one is found, use that class. -> -> 2. Next, look in the caller's assembly for classes with a name that matches a normalized version of the `"type"` from the document. The normalization applies `Underscore()`, `Singularize()`, and `PascalCase()` to the `"type"`, e.g. `"crazy-cats"` becomes `CrazyCat` +The default `JsonApiTypeResolver` is the `ReflectingTypeResolver` and it works by interrogating all of the `Types` defined in all assemblies within the current `AppDomain` for classes with the `[JsonApiResourceType("name")]` attribute set. + +If a `Type` with a `[JsonApiResourceType("name")]` attribute is found, where `"name"` matches the `typeName` then that type is used. + +Otherwise, the first `Type` whose name matches the normalized form of the `typeName` is used. The normalization applies `Underscore()`, `Singularize()`, and `PascalCase()` to the `"type"`, e.g. `"crazy-cats"` becomes `CrazyCat`. + +It turns out this is pretty useful for most common cases; however, if you want to explicitly specify the `Type` chosen for a given resource `"type"` in your document, then you can implement _Custom Type Resolution_ as described below. + +> Note: If the `JsonApiTypeResolver` can't find a resource type for any reason, it will throw a `JsonApiTypeNotFoundException` + +Custom Type Resolution +--- If for some reason you can't apply the `[JsonApiResourceType]` attribute to your classes, you can also write a custom `IJsonApiTypeResolver` that implements whatever mapping you deem fit: public class BarneyTypeResolver : IJsonApiTypeResolver { @@ -320,27 +415,32 @@ If for some reason you can't apply the `[JsonApiResourceType]` attribute to your if (typeName == "rain_drops") { return typeof(LemonDrop); } - + return null; } } Then you can use it like so: - var lemonDrop = JsonApiNet.ResourceFromDocument( - json, - new SerializerSettings { - TypeResolver = new BarneyTypeResolver() + var lemonDrop = JsonApi.ResourceFromDocument(json, new BarneyTypeResolver()) + +This particular use-case is so common, that `JsonApiNet` provides a `StringToTypeResolver` which you can initialize with a `Dictionary` where the keys are the `"type"` members in your document and the values are the corresponding `Type` to instantiate: + + var resolver = new StringToTypeResolver( + new Dictionary { + { "rain_drops", typeof(LemonDrop) } } ); -If the `JsonApiTypeResolver` can't find a resource type, it will throw a `JsonApiTypeNotFoundException` + var lemonDrop = JsonApi.ResourceFromDocument(json, resolver); JsonApiDocument --- In addition to providing lots of awesome ways to get concrete resource classes out of your JSON API documents, `JsonApiNet` can also get you an object graph that represents the entire JSON API document. + var document = JsonApi.Document(json); + This is an instance of `JsonApiDocument` and has all the good stuff like `Meta`, `Links`, `Errors`, etc parsed into it. For example, if there were `'links'` at the top-level of a document: { @@ -350,23 +450,24 @@ This is an instance of `JsonApiDocument` and has all the good stuff like `Meta`, "data": { ... } } -We can deserialize into a `JsonApiDocument` and fetch the `'admin'` link: +For example, we could fetch the `"admin"` link from the top-level document: - var document = JsonApiNet.DeseralizeDocument(json); var links = document.Links; var adminUrl = links["admin"].Uri; - -In this case, `links` would be a `JsonApiLinks` container instance, holding `JsonApiLink` members. - + +In this case, `links` would be a `JsonApiLinks` container instance, holding `JsonApiLink` members. + > Note: Each `JsonApiLink` stored in the `JsonApiLinks` container has `Href` and `Meta` properties for fetching the values parsed out of the document and an additional `Uri` helper that converts the `Href` value into a `System.Uri`. In the case of a "simple link" (i.e., a string containing the link's URL), the `Meta` property will be null. You can get the parsed representation of the resource (a `JsonApiResource` instance) by calling `document.Data`. This instance will let you fetch the `Attributes`, `Relationships`, `Links`, `Id`, `Type`, and `Meta` that were parsed from the document. All of the properties are named consistently with the specification, so it shouldn't be too hard to discover what you need to drill down to a specific part of the document. -There is a generic from of `DeserializeDocument` that returns a `JsonApiDocument` instance, which you can use to access the `Resource` property and get what you would have normally received calling `ResourceFromDocument`. For example: +There is a generic from of `Document` that returns a `JsonApiDocument` instance which you can use to access the `Resource` property and get what you would have normally received calling `ResourceFromDocument`. + +For example: - var document = JsonApiNet.DeserializeDocument
(json); + var document = JsonApi.Document
(json); var article = document.Resource; Assert.AreEqual("JSON API paints my bikeshed!", article.Title); @@ -400,17 +501,17 @@ If you were calling `ResourceFromDocument` you might handle the error like th Article article; try { - article = JsonApiNet.ResourceFromDocument
(json); + article = JsonApi.ResourceFromDocument
(json); } catch (JsonApiErrorsException e) { Console.Error.WriteLine("Wat?! {0}", e.JsonApiErrors.Message); throw; } - + Alternatively, if you were using `DeserializeDocument`, you might handle it like this: - var document = JsonApiNet.DeserializeDocument
(json); - - if (document.HasErrors) { + var document = JsonApi.DeserializeDocument
(json); + + if (document.HasErrors) { Console.Error.WriteLine("Wat?! {0}", document.Errors.Message); return; } @@ -424,12 +525,13 @@ Errata --- ### Deserializing into a Type only known at run-time ### -If you want to deserialize into a container `Type` that is only known at run-time, you can use the non-generic form of `ResourceFromDocument` or `DeserializeDocument` and pass a `ResourceContainerType` value into the `SerializerSettings`: +If you want to deserialize into a container `Type` that is only known at run-time, you can use the non-generic form of `ResourceFromDocument` or `Document` and pass the `ResultType` to the `SerializerSettings` - var article = JsonApiNet.ResourceFromDocument( + var article = JsonApi.ResourceFromDocument( json, new SerializerSettings { - ResourceContainerType = typeof(Article) + CreateResource = true, + ResultType = typeof(Article) } ); @@ -442,7 +544,7 @@ The static methods provided on `JsonApiNet` are convenience methods. You can als var serializer = new JsonApiNetSerializer(); var article = serializer.ResourceFromDocument
(json); Assert.AreEqual("JSON API paints my bikeshed!", article.Title); - + The `JsonApinetSerializer` inherits from `IJsonApiNetSerializer` which can inject or mock as you see fit.