Skip to content
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

Parameterised deep links #4

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
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
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import DeepLink
/// Open the details screen displaying the media with the provieded TMDB
/// identifier and media type.
@DeepLink(generateInitWithURL: true)
public struct MediaDetailsCallsheetDeepLink: CallsheetDeepLink {
public struct MediaDetailsCallsheetDeepLink: CallsheetDeepLink, ParameterisedDeepLink {
@Host
private let host = "open"

Expand All @@ -15,6 +15,7 @@ public struct MediaDetailsCallsheetDeepLink: CallsheetDeepLink {

/// - parameter tmdbId: The TMDB identifier of the media to open.
/// - parameter mediaType: The type of media the item with ``tmdbId`` is.
@ParametersInitialiser(nameMap: ["mediaType": "Media Type", "tmdbId": "TMDB ID"])
public init(mediaType: CallsheetMediaType, tmdbId: Int) {
self.mediaType = mediaType
self.tmdbId = tmdbId
Expand Down
3 changes: 2 additions & 1 deletion Sources/CallsheetDeepLink/SearchCallsheetDeepLink.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import DeepLink

/// A deep link to the the search UI with a prefilled query.
@DeepLink(generateInitWithURL: true)
public struct SearchCallsheetDeepLink: CallsheetDeepLink {
public struct SearchCallsheetDeepLink: CallsheetDeepLink, ParameterisedDeepLink {
@Host
private let host = "search"

Expand All @@ -18,6 +18,7 @@ public struct SearchCallsheetDeepLink: CallsheetDeepLink {
/// - parameter mediaType: The type of media to filter by. As version 2023.3
/// this is ignored. See https://mastodon.social/@caseyliss/111028913064766244
/// - parameter query: The query to prefill.
@ParametersInitialiser(nameMap: ["mediaType": "Media Type", "query": "Query"])
public init(mediaType: CallsheetMediaType, query: String) {
self.mediaType = mediaType
self.query = query
Expand Down
3 changes: 2 additions & 1 deletion Sources/CallsheetDeepLink/TVEpisodeCallsheetDeepLink.swift
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import DeepLink

@DeepLink(generateInitWithURL: true)
public struct TVEpisodeCallsheetDeepLink: CallsheetDeepLink {
public struct TVEpisodeCallsheetDeepLink: CallsheetDeepLink, ParameterisedDeepLink {
@Host
private let host = "open"

Expand All @@ -23,6 +23,7 @@ public struct TVEpisodeCallsheetDeepLink: CallsheetDeepLink {
@PathItem
public var episode: Int

@ParametersInitialiser(nameMap: ["tmdbId": "TMDB ID", "season": "Season", "episode": "Episode"])
public init(tmdbId: Int, season: Int, episode: Int) {
self.tmdbId = tmdbId
self.season = season
Expand Down
3 changes: 2 additions & 1 deletion Sources/CallsheetDeepLink/TVSeasonCallsheetDeepLink.swift
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import DeepLink

@DeepLink(generateInitWithURL: true, trailingSlash: true)
public struct TVSeasonCallsheetDeepLink: CallsheetDeepLink {
public struct TVSeasonCallsheetDeepLink: CallsheetDeepLink, ParameterisedDeepLink {
@Host
private let host = "open"

Expand All @@ -17,6 +17,7 @@ public struct TVSeasonCallsheetDeepLink: CallsheetDeepLink {
@PathItem
public var season: Int

@ParametersInitialiser(nameMap: ["tmdbId": "TMDB ID", "season": "Season"])
public init(tmdbId: Int, season: Int) {
self.tmdbId = tmdbId
self.season = season
Expand Down
70 changes: 70 additions & 0 deletions Sources/DeepLink/DeepLink.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,73 @@ public protocol DeepLink: Hashable, Sendable {
/// Check ``DeepLink.canOpen`` to know if this URL can be opened.
var url: URL { get }
}

public protocol ParameterisedDeepLink: DeepLink {
static var deepLinkParameters: [DeepLinkParameter] { get }

static func makeWithParameters(_ parameters: [String]) throws -> Self
}

public struct DeepLinkParameter: Sendable {
public let name: String

public let type: Any.Type

public init(name: String, type: Any.Type) {
self.name = name
self.type = type
}
}

public struct IncorrectParameterCountError: LocalizedError {
public let errorDescription: String?

public init(errorDescription: String?) {
self.errorDescription = errorDescription
}
}

public enum DeepLinkParameterFactory<Value: LosslessStringConvertible> {
public static func makeValue(
string: String,
parameterIndex: Int,
parameterName: String
) throws -> Value {
guard let value = Value(string) else {
throw DeepLinkParameterError(
errorDescription: "“\(string)” is not a valid \(valueTypeDescription)",
string: string,
parameterIndex: parameterIndex,
parameterName: parameterName
)
}
return value
}

private static var valueTypeDescription: String {
if Value.self == Int.self {
"number"
} else {
"\(Value.self)"
}
}
}

import Foundation

public struct DeepLinkParameterError: LocalizedError {
public let errorDescription: String?

public let string: String

public let parameterIndex: Int

public let parameterName: String

public init(errorDescription: String?, string: String, parameterIndex: Int, parameterName: String) {
self.errorDescription = errorDescription
self.string = string
self.parameterIndex = parameterIndex
self.parameterName = parameterName
}
}
3 changes: 3 additions & 0 deletions Sources/DeepLink/Macros.swift
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,6 @@ public macro User() = #externalMacro(module: "DeepLinkPlugin", type: "User")
@attached(member, names: named(description), named(init))
@attached(extension, conformances: TypedStringURLComponent)
public macro TypedStringURLComponent() = #externalMacro(module: "DeepLinkPlugin", type: "TypedStringURLComponent")

@attached(peer, names: named(deepLinkParameters), named(makeWithParameters))
public macro ParametersInitialiser(nameMap: [String: String] = [:]) = #externalMacro(module: "DeepLinkPlugin", type: "ParametersInitialiser")
13 changes: 0 additions & 13 deletions Sources/DeepLinkPlugin/DeepLink.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import Foundation
@preconcurrency import SwiftDiagnostics
import SwiftSyntax
import SwiftSyntaxMacros

Expand Down Expand Up @@ -560,15 +559,3 @@ private struct QueryItemVariable {

let includeWhenNil: Bool
}

private struct ErrorDiagnosticMessage: DiagnosticMessage, Error {
let message: String
let diagnosticID: MessageID
let severity: DiagnosticSeverity

init(id: String, message: String) {
self.message = message
diagnosticID = MessageID(domain: "uk.josephduffy.DeepLink", id: id)
severity = .error
}
}
1 change: 1 addition & 0 deletions Sources/DeepLinkPlugin/DeepLinkPlugin.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ struct DeepLinkPlugin: CompilerPlugin {
let providingMacros: [Macro.Type] = [
DeepLink.self,
Host.self,
ParametersInitialiser.self,
PathItem.self,
QueryItem.self,
QueryItems.self,
Expand Down
13 changes: 13 additions & 0 deletions Sources/DeepLinkPlugin/ErrorDiagnosticMessage.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
@preconcurrency import SwiftDiagnostics

struct ErrorDiagnosticMessage: DiagnosticMessage, Error {
let message: String
let diagnosticID: MessageID
let severity: DiagnosticSeverity

init(id: String, message: String) {
self.message = message
diagnosticID = MessageID(domain: "uk.josephduffy.DeepLink", id: id)
severity = .error
}
}
97 changes: 97 additions & 0 deletions Sources/DeepLinkPlugin/ParametersInitialiser.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import Foundation
import SwiftSyntax
import SwiftSyntaxMacros

public struct ParametersInitialiser: PeerMacro {
public static func expansion(
of node: AttributeSyntax,
providingPeersOf declaration: some DeclSyntaxProtocol,
in context: some MacroExpansionContext
) throws -> [DeclSyntax] {
guard let initialiser = declaration.as(InitializerDeclSyntax.self) else {
throw ErrorDiagnosticMessage(id: "unsupported-declaration-type", message: "@ParametersInitialiser can only be applied to type initialisers.")
}

let parameters = initialiser.signature.parameterClause.parameters

guard !parameters.isEmpty else {
throw ErrorDiagnosticMessage(id: "empty-parameters", message: "@ParametersInitialiser can only be applied to initialisers with at least 1 parameter.")
}


let labeledArguments = node.arguments?.as(LabeledExprListSyntax.self) ?? []

var nameMap: [String: String] = [:]

let nameMapArgument = labeledArguments
.first(where: { $0.label?.trimmed.text == "nameMap" })

if let nameMapArgument {
if let dictionary = nameMapArgument.expression.as(DictionaryExprSyntax.self) {
switch dictionary.content {
case .elements(let elements):
for element in elements {
guard let keyLiteral = element.key.as(StringLiteralExprSyntax.self) else { continue }
if keyLiteral.segments.count != 1 {
fatalError()
}
let key = keyLiteral.segments.first!.as(StringSegmentSyntax.self)!.content.text

guard let valueLiteral = element.value.as(StringLiteralExprSyntax.self) else { continue }
if valueLiteral.segments.count != 1 {
fatalError()
}
let value = valueLiteral.segments.first!.as(StringSegmentSyntax.self)!.content.text

nameMap[key] = value
}
case .colon:
break
}
}
}

var deepLinkParameters = "public static let deepLinkParameters: [DeepLinkParameter] = ["
var makeWithParameters = #"""
public static func makeWithParameters(_ parameters: [String]) throws -> Self {
guard parameters.count == deepLinkParameters.count else {
throw IncorrectParameterCountError(errorDescription: "Incorrect number of parameter provided. Requires \(deepLinkParameters.count) but \(parameters.count) were provided.")
}
return Self(
"""#

let enumeratedParameters = parameters.enumerated()

for (index, parameter) in enumeratedParameters {
let name: String = {
if let secondName = parameter.secondName {
return nameMap[secondName.trimmed.text] ?? secondName.trimmed.text
} else {
return nameMap[parameter.firstName.trimmed.text] ?? parameter.firstName.trimmed.text
}
}()
deepLinkParameters += "\n"
deepLinkParameters += #"DeepLinkParameter(name: "\#(name)", type: \#(parameter.type.trimmed).self),"#

if index != 0 {
makeWithParameters += ","
}

makeWithParameters += "\n"
makeWithParameters += #"\#(parameter.firstName.trimmed): try DeepLinkParameterFactory.makeValue(string: parameters[\#(index)], parameterIndex: \#(index), parameterName: "\#(name)")"#
}

deepLinkParameters += "\n]"
makeWithParameters += """
)
}
"""

return [
"\(raw: deepLinkParameters)",
"\(raw: makeWithParameters)",
]
}
}