Skip to content
10 changes: 9 additions & 1 deletion src/WalletFramework.Core/ClaimPaths/ClaimPathComponent.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,12 @@ public T Match<T>(
public string? AsKey() => _value.TryPickT0(out var key, out _) ? key : null;

public int? AsIndex() => _value.TryPickT1(out var idx, out _) ? idx : null;
}

public override string ToString()
{
return _value.Match(
key => key,
index => index.ToString(),
selectAll => selectAll.ToString());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
using WalletFramework.Core.Functional;

namespace WalletFramework.Core.Json.Errors;

public record JArrayIsNullOrEmptyError<T>()
: Error($"The JArray is null or empty for Type: `{nameof(T)}`");
59 changes: 59 additions & 0 deletions src/WalletFramework.Oid4Vc/Oid4Vp/Dcql/Models/ClaimIdentifier.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using WalletFramework.Core.Functional;
using WalletFramework.Core.Functional.Errors;

namespace WalletFramework.Oid4Vc.Oid4Vp.Dcql.Models;

/// <summary>
/// Represents a claim identifier used in DCQL.
/// </summary>
[JsonConverter(typeof(ClaimIdentifierJsonConverter))]
public record ClaimIdentifier
{
private string Value { get; }

[JsonConstructor]
private ClaimIdentifier(string value) => Value = value;

public string AsString() => Value;

public static Validation<ClaimIdentifier> Validate(string? value)
{
if (string.IsNullOrWhiteSpace(value))
return new StringIsNullOrWhitespaceError<ClaimIdentifier>();

return new ClaimIdentifier(value);
}
}

public class ClaimIdentifierJsonConverter : JsonConverter<ClaimIdentifier?>
{
public override ClaimIdentifier? ReadJson(JsonReader reader, Type objectType, ClaimIdentifier? existingValue, bool hasExistingValue, JsonSerializer serializer)
{
switch (reader.TokenType)
{
case JsonToken.String:
{
var value = (string)reader.Value!;
return ClaimIdentifier.Validate(value).ToOption().ToNullable();
}
case JsonToken.StartObject:
{
var obj = JObject.Load(reader);
var value = obj["value"]?.ToString();
return ClaimIdentifier.Validate(value).ToOption().ToNullable();
}
default:
return null;
}
}

public override void WriteJson(JsonWriter writer, ClaimIdentifier? value, JsonSerializer serializer)
{
writer.WriteStartObject();
writer.WritePropertyName("value");
writer.WriteValue(value?.AsString());
writer.WriteEndObject();
}
}
71 changes: 58 additions & 13 deletions src/WalletFramework.Oid4Vc/Oid4Vp/Dcql/Models/ClaimQuery.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
using WalletFramework.Oid4Vc.Oid4Vp.ClaimPaths;
using WalletFramework.Oid4Vc.RelyingPartyAuthentication.RegistrationCertificate;
using WalletFramework.SdJwtVc.Models.Records;
using static WalletFramework.Oid4Vc.Oid4Vp.Dcql.Models.CredentialClaimQueryConstants;
using static WalletFramework.Oid4Vc.Oid4Vp.Dcql.Models.ClaimQueryConstants;

namespace WalletFramework.Oid4Vc.Oid4Vp.Dcql.Models;

Expand All @@ -27,7 +27,7 @@ public class ClaimQuery
/// This MUST be a string identifying the particular claim.
/// </summary>
[JsonProperty(IdJsonKey)]
public string? Id { get; set; }
public ClaimIdentifier? Id { get; set; }

/// <summary>
/// A claims path pointer that specifies the path to a claim.
Expand Down Expand Up @@ -64,15 +64,7 @@ public static Validation<ClaimQuery> FromJObject(JObject json)
{
var id = json.GetByKey(IdJsonKey)
.OnSuccess(token => token.ToJValue())
.OnSuccess(value =>
{
if (string.IsNullOrWhiteSpace(value.Value?.ToString()))
{
return new StringIsNullOrWhitespaceError<CredentialQuery>();
}

return ValidationFun.Valid(value.Value.ToString());
})
.OnSuccess(value => ClaimIdentifier.Validate(value.Value?.ToString()))
.ToOption();

var path = json.GetByKey(PathJsonKey)
Expand Down Expand Up @@ -148,7 +140,7 @@ public static Validation<ClaimQuery> FromJObject(JObject json)
}

private static ClaimQuery Create(
Option<string> id,
Option<ClaimIdentifier> id,
Option<ClaimPath> path,
Option<IEnumerable<string>> values,
Option<IEnumerable<Purpose>> purpose,
Expand All @@ -164,7 +156,7 @@ private static ClaimQuery Create(
};
}

public static class CredentialClaimQueryConstants
public static class ClaimQueryConstants
{
public const string IdJsonKey = "id";

Expand Down Expand Up @@ -245,4 +237,57 @@ public static bool AreFulfilledBy(this IEnumerable<ClaimQuery>? claims, ICredent
return false;
}
}

/// <summary>
/// Returns the string representations of claim queries according to the credential format.
/// </summary>
public static IReadOnlyList<string> AsStrings(this IEnumerable<ClaimQuery> claims, string format)
{
switch (format)
{
case Constants.SdJwtVcFormat:
case Constants.SdJwtDcFormat:
var resultSdJwt =
from claim in claims
let path = claim.Path.GetPathComponents()
select string.Join('.', from c in path select c.AsKey() ?? c.AsIndex()?.ToString() ?? "*");
return [.. resultSdJwt];
case Constants.MdocFormat:
var resultMdoc =
from claim in claims
let components = claim.Path.GetPathComponents().ToArray()
let nameSpace = components.Length > 0 ? components[0].AsKey() : claim.Namespace
let claimName = components.Length > 1 ? components[1].AsKey() : claim.ClaimName
select $"['{nameSpace}']['{claimName}']";
return [.. resultMdoc];
default:
return [];
}
}

/// <summary>
/// Returns, for each ClaimSet, the set of ClaimQuery objects whose Id matches the ClaimIdentifiers in the set.
/// Only includes sets where all ClaimIdentifiers are matched by a ClaimQuery with a non-null Id.
/// If no ClaimQuery objects are matched, returns the original claimQueries.
/// </summary>
public static IEnumerable<IReadOnlyList<ClaimQuery>> ProcessSets(
this IEnumerable<ClaimQuery> claimQueries,
IEnumerable<ClaimSet> claimSets)
{
var queries = claimQueries as ClaimQuery[] ?? [.. claimQueries];
var result =
from set in claimSets
let matched =
(from id in set.Claims
join q in queries on id.AsString() equals q.Id?.AsString()
where q.Id != null
select q).ToArray()
where matched.Length == set.Claims.Count
select (IReadOnlyList<ClaimQuery>)matched;

var sets = result as IReadOnlyList<ClaimQuery>[] ?? [.. result];
return queries.Length == 0
? []
: sets.Any() ? sets : [[.. queries]];
}
}
50 changes: 50 additions & 0 deletions src/WalletFramework.Oid4Vc/Oid4Vp/Dcql/Models/ClaimSet.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
using Newtonsoft.Json.Linq;
using WalletFramework.Core.Functional;
using WalletFramework.Core.Json.Errors;

namespace WalletFramework.Oid4Vc.Oid4Vp.Dcql.Models;

/// <summary>
/// Represents a set of claim identifiers used in DCQL.
/// </summary>
public record ClaimSet
{
public IReadOnlyList<ClaimIdentifier> Claims { get; }

public ClaimSet(IReadOnlyList<ClaimIdentifier> claims) => Claims = claims;
}

public static class ClaimSetFun
{
public static Validation<ClaimSet> Validate(JArray array)
{
if (array.Count == 0)
return new JArrayIsNullOrEmptyError<ClaimSet>();

return
from ids in array.TraverseAll(token =>
{
var set = token.Type == JTokenType.String
? token.Value<string>()
: token.ToString();

return ClaimIdentifier.Validate(set);
})
select new ClaimSet([.. ids]);
}

public static Validation<IEnumerable<ClaimSet>> ValidateMany(JArray array)
{
if (array.Count == 0)
return new JArrayIsNullOrEmptyError<IEnumerable<ClaimSet>>();

return array.TraverseAll(token =>
{
var set = token.Type == JTokenType.Array
? (JArray)token
: new JArray(token);

return Validate(set);
});
}
}
Loading
Loading