Skip to content

feat|fix: adding attribute to skip null properties #329

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

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
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
35 changes: 26 additions & 9 deletions src/Octokit.GraphQL.Core/Core/Serializers/QuerySerializer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,13 @@ namespace Octokit.GraphQL.Core.Serializers
{
public class QuerySerializer
{
private static readonly ConcurrentDictionary<Type, Tuple<string, MethodInfo>[]> typeCache = new ConcurrentDictionary<Type, Tuple<string, MethodInfo>[]>();
/// <summary>
/// A cache of previously reflected types where:
/// Item1: The name of the type
/// Item2: The method info object to retrieve the value of the property.
/// Item3: A boolean indicating whether the value should always be serialized (true) or only serialized when not null (false)
/// </summary>
private static readonly ConcurrentDictionary<Type, Tuple<string, MethodInfo, bool>[]> typeCache = new ConcurrentDictionary<Type, Tuple<string, MethodInfo, bool>[]>();

private readonly int indentation;
private readonly string comma = ",";
Expand Down Expand Up @@ -232,12 +238,12 @@ private void SerializeValue(StringBuilder builder, object value)
{
var objectType = value.GetType();

Tuple<string, MethodInfo>[] properties;
Tuple<string, MethodInfo, bool>[] properties;
if (!typeCache.TryGetValue(objectType, out properties))
{
properties = objectType.GetRuntimeProperties()
.Where(info => info.GetMethod.IsPublic)
.Select(info => new Tuple<string, MethodInfo>(info.Name.LowerFirstCharacter(), info.GetMethod))
.Select(info => new Tuple<string, MethodInfo, bool>(info.Name.LowerFirstCharacter(), info.GetMethod, null == info.GetCustomAttribute<SerializeIfNotNull>()))
.ToArray();

typeCache.TryAdd(objectType, properties);
Expand All @@ -247,12 +253,23 @@ private void SerializeValue(StringBuilder builder, object value)
//Cache Hit
}

var hasOpenBrace = false;
for (var index = 0; index < properties.Length; index++)
{
var property = properties[index];
var valueObj = property.Item2.Invoke(value, null);

if (index == 0)
// Property 3 indicates if we should write the property when the value of the property is null.
// True: always write the value
// False: only write the value if it isn't null
if (property.Item3 == false && null == valueObj)
{
continue;
}

if (!hasOpenBrace)
{
hasOpenBrace = true;
OpenBrace(builder);
}
else
Expand All @@ -261,12 +278,12 @@ private void SerializeValue(StringBuilder builder, object value)
}

builder.Append(property.Item1.LowerFirstCharacter()).Append(colon);
SerializeValue(builder, property.Item2.Invoke(value, null));
SerializeValue(builder, valueObj);
}

if (index + 1 == properties.Length)
{
CloseBrace(builder);
}
if (hasOpenBrace)
{
CloseBrace(builder);
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using System;

namespace Octokit.GraphQL.Core.Serializers
{
[AttributeUsage(AttributeTargets.Property)]
public class SerializeIfNotNull : Attribute
{
}
}
95 changes: 95 additions & 0 deletions src/Octokit.GraphQL.UnitTests/QueryBuilderTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -394,5 +394,100 @@ ... on RepositoryOwner {


}

/// <summary>
/// Tests that the <see cref="Mutation.CreateRepositoryRuleset"/> mutation skips serializing null values in the
/// <see cref="RuleParametersInput"/> class. When the null values in this class are sent to GitHub the call
/// fails.
/// </summary>
[Fact]
public void Repository_CreateRepositoryRuleset()
{
var expected = @"mutation {
createRepositoryRuleset(input: {
clientMutationId: null,sourceId: ""hello world"",name: ""main"",target: BRANCH,rules: [ {
id: null,type: DELETION,parameters: null
}, {
id: null,type: PULL_REQUEST,parameters: {
pullRequest: {
dismissStaleReviewsOnPush: true,requireCodeOwnerReview: true,requireLastPushApproval: false,requiredApprovingReviewCount: 0,requiredReviewThreadResolution: false
}
}
}, {
id: null,type: REQUIRED_STATUS_CHECKS,parameters: {
requiredStatusChecks: {
requiredStatusChecks: [ {
context: ""ng test"",integrationId: null
}, {
context: ""ng lint"",integrationId: null
}],strictRequiredStatusChecksPolicy: true
}
}
}],conditions: {
refName: {
exclude: [],include: [""~DEFAULT_BRANCH""]
},repositoryName: null,repositoryId: null,repositoryProperty: null
},enforcement: ACTIVE,bypassActors: null
}) {
ruleset {
id
}
}
}";

var expression = new Mutation()
.CreateRepositoryRuleset(new Arg<CreateRepositoryRulesetInput>(new CreateRepositoryRulesetInput
{
SourceId = new ID("hello world"),
Name = "main",
Enforcement = RuleEnforcement.Active,
Target = RepositoryRulesetTarget.Branch,
Rules = new[]
{
new RepositoryRuleInput {
Type = RepositoryRuleType.Deletion
},
new RepositoryRuleInput {
Type = RepositoryRuleType.PullRequest,
Parameters = new RuleParametersInput {
PullRequest = new PullRequestParametersInput {
DismissStaleReviewsOnPush = true,
RequireCodeOwnerReview = true
}
}
},
new RepositoryRuleInput {
Type = RepositoryRuleType.RequiredStatusChecks,
Parameters = new RuleParametersInput {
RequiredStatusChecks = new RequiredStatusChecksParametersInput {
RequiredStatusChecks = new[]
{
new StatusCheckConfigurationInput {
Context = "ng test"
},
new StatusCheckConfigurationInput {
Context = "ng lint"
}
},
StrictRequiredStatusChecksPolicy = true
}
}
}
},
Conditions = new RepositoryRuleConditionsInput
{
RefName = new RefNameConditionTargetInput
{
Include = new[] { "~DEFAULT_BRANCH" },
Exclude = new string[] { }
}
}
}))
.Select(x => x.Ruleset.Id);

var query = expression.Compile();

Assert.Equal(expected, query.ToString(2), ignoreLineEndingDifferences: true);
}
}
}
13 changes: 11 additions & 2 deletions src/Octokit.GraphQL/Model/RuleParametersInput.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
namespace Octokit.GraphQL.Model
{
using System;
using System.Collections.Generic;
using Core.Serializers;

/// <summary>
/// Specifies the parameters for a `RepositoryRule` object. Only one of the fields should be specified.
Expand All @@ -11,51 +10,61 @@ public class RuleParametersInput
/// <summary>
/// Parameters used for the `update` rule type
/// </summary>
[SerializeIfNotNull]
public UpdateParametersInput Update { get; set; }

/// <summary>
/// Parameters used for the `required_deployments` rule type
/// </summary>
[SerializeIfNotNull]
public RequiredDeploymentsParametersInput RequiredDeployments { get; set; }

/// <summary>
/// Parameters used for the `pull_request` rule type
/// </summary>
[SerializeIfNotNull]
public PullRequestParametersInput PullRequest { get; set; }

/// <summary>
/// Parameters used for the `required_status_checks` rule type
/// </summary>
[SerializeIfNotNull]
public RequiredStatusChecksParametersInput RequiredStatusChecks { get; set; }

/// <summary>
/// Parameters used for the `commit_message_pattern` rule type
/// </summary>
[SerializeIfNotNull]
public CommitMessagePatternParametersInput CommitMessagePattern { get; set; }

/// <summary>
/// Parameters used for the `commit_author_email_pattern` rule type
/// </summary>
[SerializeIfNotNull]
public CommitAuthorEmailPatternParametersInput CommitAuthorEmailPattern { get; set; }

/// <summary>
/// Parameters used for the `committer_email_pattern` rule type
/// </summary>
[SerializeIfNotNull]
public CommitterEmailPatternParametersInput CommitterEmailPattern { get; set; }

/// <summary>
/// Parameters used for the `branch_name_pattern` rule type
/// </summary>
[SerializeIfNotNull]
public BranchNamePatternParametersInput BranchNamePattern { get; set; }

/// <summary>
/// Parameters used for the `tag_name_pattern` rule type
/// </summary>
[SerializeIfNotNull]
public TagNamePatternParametersInput TagNamePattern { get; set; }

/// <summary>
/// Parameters used for the `workflows` rule type
/// </summary>
[SerializeIfNotNull]
public WorkflowsParametersInput Workflows { get; set; }
}
}