Skip to content

Commit

Permalink
feat(jwt): add support for if and optionals unwrapping on dsl claims
Browse files Browse the repository at this point in the history
  • Loading branch information
beatt83 committed Sep 14, 2024
1 parent 4cc79fe commit 3aa354f
Show file tree
Hide file tree
Showing 4 changed files with 197 additions and 73 deletions.
81 changes: 75 additions & 6 deletions Sources/JSONWebToken/Claims/ArrayClaim.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,25 +23,94 @@ public struct ArrayClaim: Claim {
/// A result builder for constructing array claims.
@resultBuilder
public struct ArrayClaimBuilder {
public typealias ClaimPartialResult = [any Claim]
public typealias ArrayClaimPartialResult = [ArrayElementClaim]
public typealias StringClaimPartialResult = [StringClaim]
/// Builds an array of `ArrayElementClaim` from the provided components.
/// - Parameter components: The array element claims to include in the array.
/// - Returns: An array of `ArrayElementClaim`.
public static func buildBlock(_ components: ArrayElementClaim...) -> [ArrayElementClaim] {
components
public static func buildBlock(_ components: ArrayClaimPartialResult...) -> ArrayClaimPartialResult {
components.flatMap { $0 }
}

/// Builds an array of `ArrayElementClaim` from the provided components.
/// - Parameter components: The array element claims to include in the array.
/// - Returns: An array of `ArrayElementClaim`.
public static func buildBlock(_ components: Claim...) -> [Claim] {
components
public static func buildBlock(_ components: ClaimPartialResult...) -> ClaimPartialResult {
components.flatMap { $0 }
}

/// Builds an array of `StringClaim` from the provided components.
/// - Parameter components: The string claims to include in the array.
/// - Returns: An array of `StringClaim`.
public static func buildBlock(_ components: StringClaim...) -> [StringClaim] {
components
public static func buildBlock(_ components: StringClaimPartialResult...) -> StringClaimPartialResult {
components.flatMap { $0 }
}

public static func buildExpression(_ expression: any Claim) -> ClaimPartialResult {
[expression]
}

public static func buildExpression(_ expression: ArrayElementClaim) -> ArrayClaimPartialResult {
[expression]
}

public static func buildExpression(_ expression: StringClaim) -> StringClaimPartialResult {
[expression]
}

/// Adds support for optionals
public static func buildOptional(_ component: ClaimPartialResult?) -> ClaimPartialResult {
guard let component else {
return []
}
return component
}


/// Adds support for if statements in build block
public static func buildEither(first component: ClaimPartialResult) -> ClaimPartialResult {
component
}

public static func buildEither(second component: ClaimPartialResult) -> ClaimPartialResult {
component
}

/// Adds support for optionals
public static func buildOptional(_ component: ArrayClaimPartialResult?) -> ArrayClaimPartialResult {
guard let component else {
return []
}
return component
}


/// Adds support for if statements in build block
public static func buildEither(first component: ArrayClaimPartialResult) -> ArrayClaimPartialResult {
component
}

public static func buildEither(second component: ArrayClaimPartialResult) -> ArrayClaimPartialResult {
component
}

/// Adds support for optionals
public static func buildOptional(_ component: StringClaimPartialResult?) -> StringClaimPartialResult {
guard let component else {
return []
}
return component
}


/// Adds support for if statements in build block
public static func buildEither(first component: StringClaimPartialResult) -> StringClaimPartialResult {
component
}

public static func buildEither(second component: StringClaimPartialResult) -> StringClaimPartialResult {
component
}
}

Expand Down
46 changes: 35 additions & 11 deletions Sources/JSONWebToken/Claims/JWTClaimsBuilder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,22 +19,46 @@ import Foundation
/// A result builder for constructing JWT claims.
@resultBuilder
public struct JWTClaimsBuilder {
/// Builds a claim from the provided components.
/// - Parameter components: The claims to include.
/// - Returns: An `ObjectClaim` containing the provided claims.
public static func buildBlock(_ components: Claim...) -> Claim {
ObjectClaim(root: true, claims: components.map(\.value))
public typealias PartialResult = [any Claim]

public static func buildExpression(_ expression: any Claim) -> PartialResult {
[expression]
}

public static func buildExpression(_ expression: PartialResult) -> PartialResult {
expression
}

public static func buildBlock(_ components: PartialResult...) -> PartialResult {
components.flatMap { $0 }
}

public static func buildBlock(_ component: PartialResult) -> PartialResult {
component
}

public static func buildOptional(_ component: PartialResult?) -> PartialResult {
component ?? []
}

public static func buildEither(first component: PartialResult) -> PartialResult {
component
}

public static func buildEither(second component: PartialResult) -> PartialResult {
component
}

/// Builds a claim using a closure with the result builder.
/// - Parameter builder: A closure that returns a claim.
/// - Returns: A claim built by the closure.
/// - Throws: Rethrows any error thrown within the builder closure.
public static func build(@JWTClaimsBuilder builder: () throws -> Claim) rethrows -> Claim {
try builder()
public static func buildEmpty() -> PartialResult {
[]
}

public static func buildFinalResult(_ components: PartialResult) -> ObjectClaim {
ObjectClaim(root: true, claims: components.map(\.value))
}
}


extension Value {
func getValue<T>() -> T? {
switch self {
Expand Down
37 changes: 32 additions & 5 deletions Sources/JSONWebToken/Claims/ObjectClaim.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,38 @@ public struct ObjectClaim: Claim {
/// A result builder for constructing object claims.
@resultBuilder
public struct ObjectClaimBuilder {
/// Builds an array of `Claim` from the provided components.
/// - Parameter components: The claims to include in the object.
/// - Returns: An array of `Claim`.
public static func buildBlock(_ components: Claim...) -> [Claim] {
components
public typealias PartialResult = [any Claim]

public static func buildExpression(_ expression: any Claim) -> PartialResult {
[expression]
}

public static func buildExpression(_ expression: PartialResult) -> PartialResult {
expression
}

public static func buildBlock(_ components: PartialResult...) -> PartialResult {
components.flatMap { $0 }
}

public static func buildBlock(_ component: PartialResult) -> PartialResult {
component
}

public static func buildOptional(_ component: PartialResult?) -> PartialResult {
component ?? []
}

public static func buildEither(first component: PartialResult) -> PartialResult {
component
}

public static func buildEither(second component: PartialResult) -> PartialResult {
component
}

public static func buildEmpty() -> PartialResult {
[]
}
}

Expand Down
106 changes: 55 additions & 51 deletions Tests/JWTTests/JWTTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -171,40 +171,47 @@ final class JWTTests: XCTestCase {
))
}

func testClaims() throws {
let result = JWTClaimsBuilder.build {
IssuerClaim(value: "testIssuer")
SubjectClaim(value: "testSubject")
ExpirationTimeClaim(value: Date(timeIntervalSince1970: 1609459200)) // Fixed date for testing
IssuedAtClaim(value: Date(timeIntervalSince1970: 1609459200))
NotBeforeClaim(value: Date(timeIntervalSince1970: 1609459200))
JWTIdentifierClaim(value: "ThisIdentifier")
AudienceClaim(value: "testAud")
StringClaim(key: "testStr1", value: "value1")
NumberClaim(key: "testN1", value: 0)
NumberClaim(key: "testN2", value: 1.1)
NumberClaim(key: "testN3", value: Double(1.233232))
BoolClaim(key: "testBool1", value: true)
ArrayClaim(key: "testArray") {
ArrayElementClaim.string("valueArray1")
ArrayElementClaim.string("valueArray2")
ArrayElementClaim.bool(true)
ArrayElementClaim.array {
ArrayElementClaim.string("nestedNestedArray1")
}
ArrayElementClaim.object {
StringClaim(key: "nestedNestedObject", value: "nestedNestedValue")
}
@JWTClaimsBuilder var resultTestClaims: ObjectClaim {
IssuerClaim(value: "testIssuer")
SubjectClaim(value: "testSubject")
ExpirationTimeClaim(value: Date(timeIntervalSince1970: 1609459200)) // Fixed date for testing
IssuedAtClaim(value: Date(timeIntervalSince1970: 1609459200))
NotBeforeClaim(value: Date(timeIntervalSince1970: 1609459200))
JWTIdentifierClaim(value: "ThisIdentifier")
AudienceClaim(value: "testAud")
StringClaim(key: "testStr1", value: "value1")
NumberClaim(key: "testN1", value: 0)
NumberClaim(key: "testN2", value: 1.1)
NumberClaim(key: "testN3", value: Double(1.233232))
BoolClaim(key: "testBool1", value: true)

if true {
StringClaim(key: "willShow", value: "testValue")
StringClaim(key: "willShow2", value: "testValue")
} else {
StringClaim(key: "dontShow", value: "testValue")
}
ArrayClaim(key: "testArray") {
ArrayElementClaim.string("valueArray1")
ArrayElementClaim.string("valueArray2")
ArrayElementClaim.bool(true)
ArrayElementClaim.array {
ArrayElementClaim.string("nestedNestedArray1")
}
ObjectClaim(key: "testObject") {
StringClaim(key: "testDicStr1", value: "valueDic1")
ArrayElementClaim.object {
StringClaim(key: "nestedNestedObject", value: "nestedNestedValue")
}
}

ObjectClaim(key: "testObject") {
StringClaim(key: "testDicStr1", value: "valueDic1")
}
}

func testClaims() throws {
let encoder = JSONEncoder()
encoder.outputFormatting = [.sortedKeys, .withoutEscapingSlashes]
encoder.dateEncodingStrategy = .secondsSince1970
let coded = try encoder.encode(result.value)
let coded = try encoder.encode(resultTestClaims.value)

let jsonString = try XCTUnwrap(String(data: coded, encoding: .utf8))
print(jsonString)
Expand All @@ -231,40 +238,33 @@ final class JWTTests: XCTestCase {
"testN2":1.1,
"testN3":1.233232,
"testObject":{"testDicStr1":"valueDic1"},
"testStr1":"value1"
"testStr1":"value1",
"willShow":"testValue",
"willShow2":"testValue"
}
"""

XCTAssertFalse(jsonString.contains("dontShow"))
XCTAssertTrue(areJSONStringsEqual(jsonString, expectedJSON))
}

func testEmptyClaims() throws {
let result = JWTClaimsBuilder.build {
}

let encoder = JSONEncoder()
encoder.outputFormatting = [.sortedKeys, .withoutEscapingSlashes]
encoder.dateEncodingStrategy = .secondsSince1970
let coded = try encoder.encode(result.value)

let jsonString = try XCTUnwrap(String(data: coded, encoding: .utf8))
print(jsonString)

// Verify the structure of the resulting JSON
let expectedJSON = "{}"

XCTAssertTrue(areJSONStringsEqual(jsonString, expectedJSON))
func testJWTClaims() throws {
try JWT.signed(
claims: {
},
protectedHeader: DefaultJWSHeaderImpl(keyID: ""),
key: nil as JWK?
)
}

@JWTClaimsBuilder var resultTestSingleClaim: ObjectClaim {
IssuerClaim(value: "singleIssuer")
}

func testSingleClaim() throws {
let result = JWTClaimsBuilder.build {
IssuerClaim(value: "singleIssuer")
}

let encoder = JSONEncoder()
encoder.outputFormatting = [.sortedKeys, .withoutEscapingSlashes]
encoder.dateEncodingStrategy = .secondsSince1970
let coded = try encoder.encode(result.value)
let coded = try encoder.encode(resultTestSingleClaim.value)

let jsonString = try XCTUnwrap(String(data: coded, encoding: .utf8))
print(jsonString)
Expand Down Expand Up @@ -293,6 +293,10 @@ final class JWTTests: XCTestCase {
}
}

func build(@JWTClaimsBuilder builder: () throws -> [Claim]) rethrows -> ObjectClaim {
JWTClaimsBuilder.buildFinalResult(try builder())
}

extension String {
/// Returns a new string with all whitespace and newline characters removed.
///
Expand Down

0 comments on commit 3aa354f

Please sign in to comment.