Skip to content

Commit bab795a

Browse files
author
Chris Santero
committed
support serializing complex attributes to types other than string
1 parent 202b522 commit bab795a

6 files changed

Lines changed: 154 additions & 14 deletions

File tree

JSONAPI.Tests/Core/ResourceTypeRegistrarTests.cs

Lines changed: 87 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -187,25 +187,38 @@ private AttributeGrabBag InitializeGrabBag()
187187

188188
private void AssertAttribute<TPropertyType, TTokenType>(IResourceTypeRegistration reg, string attributeName,
189189
JToken tokenToSet, TPropertyType expectedPropertyValue, TTokenType expectedTokenAfterSet, Func<AttributeGrabBag, TPropertyType> getPropertyFunc)
190+
{
191+
AssertAttributeHelper(reg, attributeName, tokenToSet, grabBag =>
192+
{
193+
var propertyValueAfterSet = getPropertyFunc(grabBag);
194+
propertyValueAfterSet.Should().Be(expectedPropertyValue);
195+
}, token =>
196+
{
197+
if (expectedTokenAfterSet == null)
198+
token.Should().BeNull();
199+
else
200+
{
201+
var convertedTokenValue = token.Value<TTokenType>();
202+
convertedTokenValue.Should().Be(expectedTokenAfterSet);
203+
}
204+
});
205+
}
206+
207+
private void AssertAttributeHelper(IResourceTypeRegistration reg, string attributeName,
208+
JToken tokenToSet, Action<AttributeGrabBag> testPropertyValueAfterSet,
209+
Action<JToken> testTokenAfterSetAndGet)
190210
{
191211
var grabBag = InitializeGrabBag();
192212

193213
var field = reg.GetFieldByName(attributeName);
194-
var attribute = (ResourceTypeAttribute) field;
214+
var attribute = (ResourceTypeAttribute)field;
195215
attribute.JsonKey.Should().Be(attributeName);
196216

197217
attribute.SetValue(grabBag, tokenToSet);
198-
var propertyValueAfterSet = getPropertyFunc(grabBag);
199-
propertyValueAfterSet.Should().Be(expectedPropertyValue);
200-
218+
testPropertyValueAfterSet(grabBag);
219+
201220
var convertedToken = attribute.GetValue(grabBag);
202-
if (expectedTokenAfterSet == null)
203-
convertedToken.Should().BeNull();
204-
else
205-
{
206-
var convertedTokenValue = convertedToken.Value<TTokenType>();
207-
convertedTokenValue.Should().Be(expectedTokenAfterSet);
208-
}
221+
testTokenAfterSetAndGet(convertedToken);
209222
}
210223

211224
[TestMethod]
@@ -733,5 +746,68 @@ public void BuildRegistration_sets_up_correct_attribute_for_nullable_enum_field(
733746
AssertAttribute(reg, "nullable-enum-field", (int)SampleEnum.Value1, SampleEnum.Value1, (int)SampleEnum.Value1, g => g.NullableEnumField);
734747
AssertAttribute(reg, "nullable-enum-field", null, null, (SampleEnum?)null, g => g.NullableEnumField);
735748
}
749+
750+
[TestMethod]
751+
public void BuildRegistration_sets_up_correct_attribute_for_to_one_complex_field()
752+
{
753+
// Arrange
754+
var registrar = new ResourceTypeRegistrar(new DefaultNamingConventions(new PluralizationService()));
755+
756+
// Act
757+
var reg = registrar.BuildRegistration(typeof(AttributeGrabBag));
758+
759+
// Assert
760+
AssertAttributeHelper(reg, "to-one-complex-type-field",
761+
new JObject { { "intProp", 32 }, { "StringProp", "qux" } },
762+
grabBag =>
763+
{
764+
grabBag.ToOneComplexTypeField.Should().NotBeNull();
765+
grabBag.ToOneComplexTypeField.IntProp.Should().Be(32);
766+
grabBag.ToOneComplexTypeField.StringProp.Should().Be("qux");
767+
},
768+
token =>
769+
{
770+
((int)token["intProp"]).Should().Be(32);
771+
((string)token["StringProp"]).Should().Be("qux");
772+
});
773+
AssertAttribute(reg, "to-one-complex-type-field", null, null, (SampleComplexType)null, g => g.ToOneComplexTypeField);
774+
}
775+
776+
[TestMethod]
777+
public void BuildRegistration_sets_up_correct_attribute_for_to_many_complex_field()
778+
{
779+
// Arrange
780+
var registrar = new ResourceTypeRegistrar(new DefaultNamingConventions(new PluralizationService()));
781+
782+
// Act
783+
var reg = registrar.BuildRegistration(typeof(AttributeGrabBag));
784+
785+
// Assert
786+
AssertAttributeHelper(reg, "to-many-complex-type-field",
787+
new JArray
788+
{
789+
new JObject { { "intProp", 49 }, { "StringProp", "blue" } },
790+
new JObject { { "intProp", 67 }, { "StringProp", "orange" } }
791+
},
792+
grabBag =>
793+
{
794+
var result = grabBag.ToManyComplexTypeField.ToArray();
795+
result.Length.Should().Be(2);
796+
result[0].IntProp.Should().Be(49);
797+
result[0].StringProp.Should().Be("blue");
798+
result[1].IntProp.Should().Be(67);
799+
result[1].StringProp.Should().Be("orange");
800+
},
801+
token =>
802+
{
803+
var jarray = (JArray) token;
804+
jarray.Count.Should().Be(2);
805+
((int)jarray[0]["intProp"]).Should().Be(49);
806+
((string)jarray[0]["StringProp"]).Should().Be("blue");
807+
((int)jarray[1]["intProp"]).Should().Be(67);
808+
((string)jarray[1]["StringProp"]).Should().Be("orange");
809+
});
810+
AssertAttribute(reg, "to-many-complex-type-field", null, null, (SampleComplexType[])null, g => g.ToManyComplexTypeField);
811+
}
736812
}
737813
}

JSONAPI.Tests/Models/AttributeGrabBag.cs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
using System;
2+
using System.Collections.Generic;
23
using JSONAPI.Attributes;
4+
using Newtonsoft.Json;
5+
using Newtonsoft.Json.Serialization;
36

47
namespace JSONAPI.Tests.Models
58
{
@@ -9,6 +12,13 @@ public enum SampleEnum
912
Value2 = 2
1013
}
1114

15+
public class SampleComplexType
16+
{
17+
[JsonProperty("intProp")]
18+
public Int32 IntProp { get; set; }
19+
public string StringProp { get; set; }
20+
}
21+
1222
public class AttributeGrabBag
1323
{
1424
public string Id { get; set; }
@@ -48,5 +58,11 @@ public class AttributeGrabBag
4858

4959
[SerializeAsComplex]
5060
public string ComplexAttributeField { get; set; }
61+
62+
[SerializeAsComplex]
63+
public SampleComplexType ToOneComplexTypeField { get; set; }
64+
65+
[SerializeAsComplex]
66+
public virtual ICollection<SampleComplexType> ToManyComplexTypeField { get; set; }
5167
}
5268
}

JSONAPI/Core/ComplexAttributeValueConverter.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ namespace JSONAPI.Core
55
{
66
/// <summary>
77
/// Implementation of <see cref="IAttributeValueConverter" /> suitable for
8-
/// use with complex attributes.
8+
/// use with complex attributes that deserialize to strings.
99
/// </summary>
1010
public class ComplexAttributeValueConverter : IAttributeValueConverter
1111
{
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
using System.Reflection;
2+
using Newtonsoft.Json.Linq;
3+
4+
namespace JSONAPI.Core
5+
{
6+
/// <summary>
7+
/// Implementation of <see cref="IAttributeValueConverter" /> suitable for
8+
/// use with complex attributes that deserialize to custom types.
9+
/// </summary>
10+
public class ObjectComplexAttributeValueConverter : IAttributeValueConverter
11+
{
12+
private readonly PropertyInfo _property;
13+
private readonly bool _isToMany;
14+
15+
/// <summary>
16+
/// Creates a new ComplexAttributeValueConverter
17+
/// </summary>
18+
/// <param name="property"></param>
19+
/// <param name="isToMany"></param>
20+
public ObjectComplexAttributeValueConverter(PropertyInfo property, bool isToMany)
21+
{
22+
_property = property;
23+
_isToMany = isToMany;
24+
}
25+
26+
public JToken GetValue(object resource)
27+
{
28+
var value = _property.GetValue(resource);
29+
if (value == null) return null;
30+
return _isToMany ? (JToken)JArray.FromObject(value) : JObject.FromObject(value);
31+
}
32+
33+
public void SetValue(object resource, JToken value)
34+
{
35+
var deserialized = value?.ToObject(_property.PropertyType);
36+
_property.SetValue(resource, deserialized);
37+
}
38+
}
39+
}

JSONAPI/Core/ResourceTypeRegistrar.cs

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,15 @@ protected virtual IAttributeValueConverter GetValueConverterForProperty(Property
101101
{
102102
var serializeAsComplexAttribute = prop.GetCustomAttribute<SerializeAsComplexAttribute>();
103103
if (serializeAsComplexAttribute != null)
104-
return new ComplexAttributeValueConverter(prop);
104+
{
105+
if (prop.PropertyType == typeof (string))
106+
return new ComplexAttributeValueConverter(prop);
107+
108+
var isToMany =
109+
prop.PropertyType.IsArray ||
110+
(prop.PropertyType.GetInterfaces().Contains(typeof(System.Collections.IEnumerable)) && prop.PropertyType.IsGenericType);
111+
return new ObjectComplexAttributeValueConverter(prop, isToMany);
112+
}
105113

106114
if (prop.PropertyType == typeof(DateTime))
107115
return new DateTimeAttributeValueConverter(prop, false);
@@ -150,7 +158,7 @@ protected virtual ResourceTypeField CreateResourceTypeField(PropertyInfo prop)
150158

151159
var type = prop.PropertyType;
152160

153-
if (prop.PropertyType.CanWriteAsJsonApiAttribute())
161+
if (prop.PropertyType.CanWriteAsJsonApiAttribute() || prop.GetCustomAttributes<SerializeAsComplexAttribute>().Any())
154162
{
155163
var converter = GetValueConverterForProperty(prop);
156164
return new ResourceTypeAttribute(converter, prop, jsonKey);

JSONAPI/JSONAPI.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@
8181
<Compile Include="Core\IEphemeralRelatedResourceReader.cs" />
8282
<Compile Include="Core\INamingConventions.cs" />
8383
<Compile Include="Core\IResourceTypeRegistrar.cs" />
84+
<Compile Include="Core\ObjectComplexAttributeValueConverter.cs" />
8485
<Compile Include="Core\PathVisitor.cs" />
8586
<Compile Include="Core\ResourceTypeAttribute.cs" />
8687
<Compile Include="Core\ResourceTypeRegistrar.cs" />

0 commit comments

Comments
 (0)