Skip to content

Commit

Permalink
Added fields as query parameter (#199)
Browse files Browse the repository at this point in the history
* Added fields as query parameter

* Added xml comments

* Also fixed tabs

* Added a correct ToString method

* Fixed to string method of FieldsetProperty

* Added test for fields not known to type
  • Loading branch information
PhyberApex authored and joukevandermaas committed Sep 10, 2018
1 parent 952e792 commit 3fd7e12
Show file tree
Hide file tree
Showing 14 changed files with 215 additions and 67 deletions.
1 change: 1 addition & 0 deletions Saule/Constants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ public static class QueryNames
public const string Sorting = "sort";
public const string Filtering = "filter";
public const string Including = "include";
public const string Fieldset = "fields";
}

public static class QueryValues
Expand Down
2 changes: 2 additions & 0 deletions Saule/Http/AllowsQueryAttribute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using System.Net.Http;
using System.Web.Http.Controllers;
using System.Web.Http.Filters;
using Saule.Queries.Fieldset;
using Saule.Queries.Filtering;
using Saule.Queries.Including;
using Saule.Queries.Sorting;
Expand All @@ -28,6 +29,7 @@ public override void OnActionExecuting(HttpActionContext actionContext)

queryContext.Sort = new SortContext(queryParams);
queryContext.Filter = new FilterContext(queryParams);
queryContext.Fieldset = new FieldsetContext(queryParams);

if (queryContext.Include == null)
{
Expand Down
3 changes: 2 additions & 1 deletion Saule/JsonApiSerializer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,8 @@ public PreprocessResult PreprocessContent(object @object, ApiResource resource,
propertyNameConverter: config.PropertyNameConverter,
urlBuilder: UrlPathBuilder,
paginationContext: QueryContext?.Pagination,
includeContext: QueryContext?.Include);
includeContext: QueryContext?.Include,
fieldsetContext: QueryContext?.Fieldset);
}
catch (Exception ex)
{
Expand Down
2 changes: 2 additions & 0 deletions Saule/JsonApiSerializerOfT.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using Newtonsoft.Json.Linq;
using Saule.Http;
using Saule.Queries;
using Saule.Queries.Fieldset;
using Saule.Queries.Filtering;
using Saule.Queries.Including;
using Saule.Queries.Pagination;
Expand Down Expand Up @@ -130,6 +131,7 @@ private QueryContext GetQueryContext(IEnumerable<KeyValuePair<string, string>> f
context.Sort = new SortContext(keyValuePairs);
context.Filter = new FilterContext(keyValuePairs) { QueryFilters = QueryFilterExpressions };
context.Include = new IncludeContext(keyValuePairs);
context.Fieldset = new FieldsetContext(keyValuePairs);
}

return context;
Expand Down
36 changes: 36 additions & 0 deletions Saule/Queries/Fieldset/FieldsetContext.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
using System.Collections.Generic;
using System.Linq;

namespace Saule.Queries.Fieldset
{
/// <summary>
/// Context for fieldset operations
/// </summary>
public class FieldsetContext
{
/// <summary>
/// Initializes a new instance of the <see cref="FieldsetContext"/> class.
/// </summary>
/// <param name="queryParams">query string that might contain Fieldset keyword</param>
public FieldsetContext(IEnumerable<KeyValuePair<string, string>> queryParams)
{
Properties =
from query in queryParams
where query.Key.StartsWith(Constants.QueryNames.Fieldset)
let type = query.Key.Substring(Constants.QueryNames.Fieldset.Length + 1)
let fields = query.Value.Split(',')
select new FieldsetProperty(type, fields);
}

/// <summary>
/// Gets including properties
/// </summary>
public IEnumerable<FieldsetProperty> Properties { get; internal set; }

/// <inheritdoc/>
public override string ToString()
{
return Properties != null && Properties.Any() ? string.Join("&", Properties.Select(p => p.ToString())) : string.Empty;
}
}
}
35 changes: 35 additions & 0 deletions Saule/Queries/Fieldset/FieldsetProperty.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
namespace Saule.Queries.Fieldset
{
/// <summary>
/// Property for fieldset
/// </summary>
public class FieldsetProperty
{
/// <summary>
/// Initializes a new instance of the <see cref="FieldsetProperty"/> class.
/// </summary>
/// <param name="type">type for field filter</param>
/// <param name="fields">fields to serialize filter</param>
public FieldsetProperty(string type, string[] fields)
{
Type = type;
Fields = fields;
}

/// <summary>
/// Gets property type
/// </summary>
public string Type { get; }

/// <summary>
/// Gets property fields
/// </summary>
public string[] Fields { get; }

/// <inheritdoc/>
public override string ToString()
{
return $"fields[{Type}]={string.Join(",", Fields)}";
}
}
}
9 changes: 8 additions & 1 deletion Saule/Queries/QueryContext.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Saule.Queries.Fieldset;
using Saule.Queries.Filtering;
using Saule.Queries.Including;
using Saule.Queries.Pagination;
Expand Down Expand Up @@ -33,6 +34,11 @@ public class QueryContext
/// </summary>
public IncludeContext Include { get; internal set; }

/// <summary>
/// Gets include context
/// </summary>
public FieldsetContext Fieldset { get; internal set; }

/// <summary>
/// Gets or sets a value indicating whether that query parameters
/// will be handled by action itself or Saule should handle them
Expand All @@ -47,7 +53,8 @@ public override string ToString()
Pagination?.ToString(),
Sort?.ToString(),
Filter?.ToString(),
Include?.ToString()
Include?.ToString(),
Fieldset?.ToString()
};

return string.Join("&", result.Where(c => !string.IsNullOrEmpty(c)));
Expand Down
2 changes: 2 additions & 0 deletions Saule/Saule.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,8 @@
<Compile Include="Http\JsonApiQueryValueProvider.cs" />
<Compile Include="Http\JsonApiQueryValueProviderFactory.cs" />
<Compile Include="JsonApiSerializerOfT.cs" />
<Compile Include="Queries\Fieldset\FieldsetContext.cs" />
<Compile Include="Queries\Fieldset\FieldsetProperty.cs" />
<Compile Include="Serialization\CamelCasePropertyNameConverter.cs" />
<Compile Include="Serialization\DefaultPropertyNameConverter.cs" />
<Compile Include="Serialization\IPropertyNameConverter.cs" />
Expand Down
34 changes: 33 additions & 1 deletion Saule/Serialization/ResourceSerializer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System.Linq;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Saule.Queries.Fieldset;
using Saule.Queries.Including;
using Saule.Queries.Pagination;

Expand All @@ -14,6 +15,7 @@ internal class ResourceSerializer
private readonly Uri _baseUrl;
private readonly PaginationContext _paginationContext;
private readonly IncludeContext _includeContext;
private readonly FieldsetContext _fieldsetContext;
private readonly ApiResource _resource;
private readonly object _value;
private readonly IPropertyNameConverter _propertyNameConverter;
Expand All @@ -28,6 +30,7 @@ public ResourceSerializer(
IUrlPathBuilder urlBuilder,
PaginationContext paginationContext,
IncludeContext includeContext,
FieldsetContext fieldsetContext,
IPropertyNameConverter propertyNameConverter = null)
{
_propertyNameConverter = propertyNameConverter ?? new DefaultPropertyNameConverter();
Expand All @@ -37,6 +40,7 @@ public ResourceSerializer(
_baseUrl = baseUrl;
_paginationContext = paginationContext;
_includeContext = includeContext;
_fieldsetContext = fieldsetContext;
_includedGraphPaths = IncludedGraphPathsFromContext(includeContext);
}

Expand Down Expand Up @@ -248,7 +252,17 @@ private JObject SerializeNode(ResourceGraphNode node, bool isCollection)
}
}

var attributes = SerializeAttributes(node);
JObject attributes = null;
if (_fieldsetContext != null && _fieldsetContext.Properties.Count(property => property.Type == node.Key.Type) > 0)
{
FieldsetProperty fieldset = _fieldsetContext.Properties.Where(property => property.Type == node.Key.Type).First();
attributes = SerializeAttributes(node, fieldset);
}
else
{
attributes = SerializeAttributes(node);
}

if (attributes != null)
{
response["attributes"] = attributes;
Expand Down Expand Up @@ -281,6 +295,24 @@ private JObject SerializeAttributes(ResourceGraphNode node)
return JObject.FromObject(attributeHash, _serializer);
}

private JObject SerializeAttributes(ResourceGraphNode node, FieldsetProperty fieldset)
{
var attributeHash = node.Resource.Attributes
.Where(a =>
node.SourceObject.IncludesProperty(_propertyNameConverter.ToModelPropertyName(a.InternalName)) && fieldset.Fields.Contains(_propertyNameConverter.ToModelPropertyName(a.InternalName)))
.Select(a =>
new
{
Key = _propertyNameConverter.ToJsonPropertyName(a.InternalName),
Value = node.SourceObject.GetValueOfProperty(_propertyNameConverter.ToModelPropertyName(a.InternalName))
})
.ToDictionary(
kvp => kvp.Key,
kvp => kvp.Value);

return JObject.FromObject(attributeHash, _serializer);
}

private JObject SerializeRelationships(ResourceGraphNode node)
{
if (node.Relationships.Count == 0)
Expand Down
30 changes: 30 additions & 0 deletions Tests/JsonApiSerializerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,36 @@ public void ConditionallyAppliesFilters()

}

[Fact(DisplayName = "Uses query fieldset expressions if specified")]
public void UsesQueryFieldsetExpressions()
{
var target = new JsonApiSerializer<CompanyResource>
{
AllowQuery = true
};
var companies = Get.Companies(1).ToList();
var result = target.Serialize(companies, new Uri(DefaultUrl, "?fields[corporation]=Name,Location"));
_output.WriteLine(result.ToString());

Assert.NotNull(result["data"][0]["attributes"]["name"]);
Assert.NotNull(result["data"][0]["attributes"]["location"]);
Assert.Null(result["data"][0]["attributes"]["number-of-employees"]);
}

[Fact(DisplayName = "Returns no fields if requested field is not part of that model")]
public void ReturnEmptyModelForUnknownFieldsetExpressions()
{
var target = new JsonApiSerializer<CompanyResource>
{
AllowQuery = true
};
var companies = Get.Companies(1).ToList();
var result = target.Serialize(companies, new Uri(DefaultUrl, "?fields[corporation]=Notafield"));
_output.WriteLine(result.ToString());

Assert.False(((JToken)result["data"][0]["attributes"]).HasValues);
}

[Fact(DisplayName = "Does not allow null Uri")]
public void HasAContract()
{
Expand Down
14 changes: 7 additions & 7 deletions Tests/Serialization/MetadataTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ public void SerializesObjectsCorrectly()
new Uri("http://localhost/people/123"),
new DefaultUrlPathBuilder(),
null,
null);
null, null);

var result = target.Serialize();
_output.WriteLine(result.ToString());
Expand Down Expand Up @@ -67,7 +67,7 @@ public void SerializesDictionariesCorrectly()
new Uri("http://localhost/people/123"),
new DefaultUrlPathBuilder(),
null,
null);
null, null);

var result = target.Serialize();
_output.WriteLine(result.ToString());
Expand Down Expand Up @@ -95,7 +95,7 @@ public void PassesThroughJTokens()
new Uri("http://localhost/people/123"),
new DefaultUrlPathBuilder(),
null,
null);
null, null);

var result = target.Serialize();
_output.WriteLine(result.ToString());
Expand All @@ -121,7 +121,7 @@ public void SerializesCollectionsCorrectly()
new Uri("http://localhost/people/123"),
new DefaultUrlPathBuilder(),
null,
null);
null, null);

var result = target.Serialize();
_output.WriteLine(result.ToString());
Expand All @@ -146,7 +146,7 @@ public void CallsGetMetadataForCorrectly()
new Uri("http://localhost/people/123"),
new DefaultUrlPathBuilder(),
null,
null);
null, null);

target.Serialize();

Expand All @@ -160,7 +160,7 @@ public void CallsGetMetadataForCorrectly()
new Uri("http://localhost/people/123"),
new DefaultUrlPathBuilder(),
null,
null);
null, null);

target.Serialize();

Expand All @@ -178,7 +178,7 @@ public void EmptyForNull()
new Uri("http://localhost/people/123"),
new DefaultUrlPathBuilder(),
null,
null);
null, null);

var result = target.Serialize();
_output.WriteLine(result.ToString());
Expand Down
6 changes: 3 additions & 3 deletions Tests/Serialization/ResourceDeserializerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,10 @@ public ResourceDeserializerTests()
_people = Get.People(5).ToArray();
var singleSerializer = new ResourceSerializer(
_person, new PersonResource(), new Uri("http://example.com/people/1"),
new DefaultUrlPathBuilder(), null, null);
new DefaultUrlPathBuilder(), null, null, null);
var multiSerializer = new ResourceSerializer(
_people, new PersonResource(), new Uri("http://example.com/people/"),
new DefaultUrlPathBuilder(), null, null);
new DefaultUrlPathBuilder(), null, null, null);

_singleJson = JToken.Parse(singleSerializer.Serialize().ToString());
_collectionJson = JToken.Parse(multiSerializer.Serialize().ToString());
Expand All @@ -53,7 +53,7 @@ public void DeserializesCamelCase()

var singleSerializer = new ResourceSerializer(
_person, new PersonResource(), new Uri("http://example.com/people/1"),
new DefaultUrlPathBuilder(), null, null, camelCasePropertyNameConverter);
new DefaultUrlPathBuilder(), null, null, null, camelCasePropertyNameConverter);

var singleJson = JToken.Parse(singleSerializer.Serialize().ToString());

Expand Down
Loading

0 comments on commit 3fd7e12

Please sign in to comment.