Skip to content

Resource inheritance #1142

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 19 commits into from
Apr 4, 2022
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Resource inheritance: sparse fieldsets
  • Loading branch information
Bart Koelman committed Mar 21, 2022
commit da7024c957585bdee5e8897acf56fafb07e3cadc
75 changes: 75 additions & 0 deletions src/JsonApiDotNetCore/Queries/FieldSelection.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
using System.Text;
using JetBrains.Annotations;
using JsonApiDotNetCore.Configuration;
using JsonApiDotNetCore.Resources.Annotations;

namespace JsonApiDotNetCore.Queries;

/// <summary>
/// Provides access to sparse fieldsets, per resource type. There's usually just a single resource type, but there can be multiple in case an endpoint
/// for an abstract resource type returns derived types.
/// </summary>
[PublicAPI]
public sealed class FieldSelection : Dictionary<ResourceType, FieldSelectors>
{
public bool IsEmpty => Values.All(selectors => selectors.IsEmpty);

public ISet<ResourceType> GetResourceTypes()
{
return Keys.ToHashSet();
}

#pragma warning disable AV1130 // Return type in method signature should be a collection interface instead of a concrete type
public FieldSelectors GetOrCreateSelectors(ResourceType resourceType)
#pragma warning restore AV1130 // Return type in method signature should be a collection interface instead of a concrete type
{
ArgumentGuard.NotNull(resourceType, nameof(resourceType));

if (!ContainsKey(resourceType))
{
this[resourceType] = new FieldSelectors();
}

return this[resourceType];
}

public override string ToString()
{
var builder = new StringBuilder();

var writer = new IndentingStringWriter(builder);
WriteSelection(writer);

return builder.ToString();
}

internal void WriteSelection(IndentingStringWriter writer)
{
using (writer.Indent())
{
foreach (ResourceType type in GetResourceTypes())
{
writer.WriteLine($"{nameof(FieldSelectors)}<{type.ClrType.Name}>");
WriterSelectors(writer, type);
}
}
}

private void WriterSelectors(IndentingStringWriter writer, ResourceType type)
{
using (writer.Indent())
{
foreach ((ResourceFieldAttribute field, QueryLayer? nextLayer) in GetOrCreateSelectors(type))
{
if (nextLayer == null)
{
writer.WriteLine(field.ToString());
}
else
{
nextLayer.WriteLayer(writer, $"{field.PublicName}: ");
}
}
}
}
}
70 changes: 70 additions & 0 deletions src/JsonApiDotNetCore/Queries/FieldSelectors.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
using JetBrains.Annotations;
using JsonApiDotNetCore.Resources.Annotations;

namespace JsonApiDotNetCore.Queries;

/// <summary>
/// A data structure that contains which fields (attributes and relationships) to retrieve, or empty to retrieve all. In the case of a relationship, it
/// contains the nested query constraints.
/// </summary>
[PublicAPI]
public sealed class FieldSelectors : Dictionary<ResourceFieldAttribute, QueryLayer?>
{
public bool IsEmpty => !this.Any();

public bool ContainsReadOnlyAttribute
{
get
{
return this.Any(selector => selector.Key is AttrAttribute attribute && attribute.Property.SetMethod == null);
}
}

public bool ContainsOnlyRelationships
{
get
{
return this.All(selector => selector.Key is RelationshipAttribute);
}
}

public bool ContainsField(ResourceFieldAttribute field)
{
ArgumentGuard.NotNull(field, nameof(field));

return ContainsKey(field);
}

public void IncludeAttribute(AttrAttribute attribute)
{
ArgumentGuard.NotNull(attribute, nameof(attribute));

this[attribute] = null;
}

public void IncludeAttributes(IEnumerable<AttrAttribute> attributes)
{
ArgumentGuard.NotNull(attributes, nameof(attributes));

foreach (AttrAttribute attribute in attributes)
{
this[attribute] = null;
}
}

public void IncludeRelationship(RelationshipAttribute relationship, QueryLayer? queryLayer)
{
ArgumentGuard.NotNull(relationship, nameof(relationship));

this[relationship] = queryLayer;
}

public void RemoveAttributes()
{
while (this.Any(pair => pair.Key is AttrAttribute))
{
ResourceFieldAttribute field = this.First(pair => pair.Key is AttrAttribute).Key;
Remove(field);
}
}
}
41 changes: 41 additions & 0 deletions src/JsonApiDotNetCore/Queries/IndentingStringWriter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
using System.Text;

namespace JsonApiDotNetCore.Queries;

internal sealed class IndentingStringWriter : IDisposable
{
private readonly StringBuilder _builder;

private int _indentDepth;

public IndentingStringWriter(StringBuilder builder)
{
_builder = builder;
}

public void WriteLine(string? line)
{
if (_indentDepth > 0)
{
_builder.Append(new string(' ', _indentDepth * 2));
}

_builder.AppendLine(line);
}

public IndentingStringWriter Indent()
{
WriteLine("{");
_indentDepth++;
return this;
}

public void Dispose()
{
if (_indentDepth > 0)
{
_indentDepth--;
WriteLine("}");
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,14 +37,14 @@ private ResourceType ParseSparseFieldTarget()

EatSingleCharacterToken(TokenKind.OpenBracket);

ResourceType resourceType = ParseResourceName();
ResourceType resourceType = ParseResourceType();

EatSingleCharacterToken(TokenKind.CloseBracket);

return resourceType;
}

private ResourceType ParseResourceName()
private ResourceType ParseResourceType()
{
if (TokenStack.TryPop(out Token? token) && token.Kind == TokenKind.Text)
{
Expand Down
Loading