Skip to content

URI Templating #1198

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 26 commits into from
Apr 21, 2025
Merged

Conversation

danieleggert
Copy link
Contributor

This adds RFC 6570 URI templates to the Swift URL type.

Evolution proposal: #1186

@danieleggert danieleggert marked this pull request as ready for review March 4, 2025 10:18
@jrflat
Copy link
Contributor

jrflat commented Mar 5, 2025

@swift-ci please test

@itingliu
Copy link
Contributor

@swift-ci please test

Copy link
Contributor

@parkera parkera left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you also add a benchmark suite for this functionality? You can pretty easily get a start by copying one of the existing formatting ones.

/// The template string needs to be a valid RFC 6570 template.
///
/// This will parse the template and throw an error if the template is invalid.
public init(_ template: String) throws {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to add availability annotations to this, too, even though the type it is an extension of is new?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IIRC, having @available on the type is sufficient.

}
}

private struct InvalidTemplateExpression: Swift.Error {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm, I wonder how we can make it easier to ensure this error isn't leaked into any public functions.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

None of the public functions are throwing. That should be enough, no?

// MARK: -

extension String {
public init(_ key: URL.Template.VariableName) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Was this part of the proposal? Adding a public, no-arg init to String seems very broad.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, I see it now. I'm not sure about it now that I see it in code. I know it is strongly typed, but it can cause confusion in documentation or at call sites. How critical is it to do this conversion in this way at a call site?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, these were part of the review.

Omitting the first argument label is what’s recommended by the API Design Guidelines for initializers that perform value preserving type conversions.

I’m not sure what alternative would be better.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@parkera would removing this from the implementation require another round of review/block this PR? Or could we maybe mark it internal for now?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@danieleggert does URL.Template.VariableName conform with ExpressibleByStringLiteral?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the right way forward is to remove ExpressibleByStringLiteral. This is only truly useful in small demo code. In “real” code, you’d usually have a fixed set of variable names, and would probably do something like:

extension URL.Template.VariableName {
    static var query: URL.Template.VariableName { URL.Template.VariableName("query") }
    static var number: URL.Template.VariableName { URL.Template.VariableName("number") }
}

and then use that when constructing the dictionary.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, or even

static var query: URL.Template.VariableName { .init("query") }

Copy link
Contributor

@jrflat jrflat left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for pitching and implementing this! I left some comments, some nits and some questions on whether we can merge percent-encoding functionality with implementations in URLParser.swift.

Another minor nit: would we want to rename the files to URLTemplate... given that the new type is based on URL? (I realize the RFC does name it "URI".)

I think we will also need to add the sources to CMakeLists in the URL folder.

@danieleggert
Copy link
Contributor Author

Could you also add a benchmark suite for this functionality? You can pretty easily get a start by copying one of the existing formatting ones.

The info about building and running Benchmarks is broken / missing / misleading.

SWIFTCI_USE_LOCAL_DEPS=.. swift package benchmark

doesn’t work, it needs to be ../.. — and then (because the top-level Package.swift also uses SWIFTCI_USE_LOCAL_DEPS, the top-level project also wants to use local dependencies.

I worked around it with local modificatoins to the Package.swift, but this could use some TLC.

@jrflat
Copy link
Contributor

jrflat commented Mar 26, 2025

@swift-ci please test

@jrflat
Copy link
Contributor

jrflat commented Mar 27, 2025

@swift-ci please test

@itingliu
Copy link
Contributor

itingliu commented Apr 1, 2025

@swift-ci please test

Copy link
Contributor

@parkera parkera left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good to me - @itingliu can you also review?

@itingliu
Copy link
Contributor

@swift-ci please test

Copy link
Contributor

@itingliu itingliu left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM in general. I left some comments, but feel free to address them in a follow up or dismiss them if not valid.

let escaped = String(literal).normalizedAddingPercentEncoding(
withAllowedCharacters: .unreservedReserved
)
elements.append(.literal(escaped))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a thought, not blocking: In copyLiteral and the regex matching function below, you're both working on Substring rather than String. And if I read it correctly, it looks like the associated String will later be used in other places that involves some Substring processing. That makes me wonder if it'd make sense to store the associated value of case literal() as Substring instead, so we can avoid Substring-String copying until the last minute, and if this would actually make a difference performance-wise

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point. My thinking was, that it would be better to store String because the case literal() will stay around as part of the URL.Template — and that instance is likely to remain in-memory for a long time. I don’t want that to keep the complete input String alive.

}

extension URL.Template.Expression {
init(_ input: String) throws {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As an enhancement: perhaps it's a good place to adopt typed throw?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This may also throw errors from RegEx parsing. I took a stab at using typed throws, but it ended up making the code more complex and more difficult to read.

// But any `%` that are not part of a valid escape sequence, need to be encoded.
guard next != UInt8(ascii: "%") || remainingInput.count < 2 else {
// Is this a valid escape sequence?
if remainingInput[remainingInput.startIndex].isValidHexDigit && remainingInput[remainingInput.startIndex + 1].isValidHexDigit {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If remainingInput.count == 1, is it still fine to do remainingInput[remainingInput.startIndex + 1]?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The remainingInput.count < 2 in the guard protects against that. The // Is this a valid escape sequence? code will only run if next == UInt8(ascii: "%") && remainingInput.count >= 2.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My bad. I kept reading it as remainingInput.count > 2 🤦‍♀️

index += 3
}
}
return String(decoding: outputBuffer[..<index], as: UTF8.self)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can't tell from the code, but is it guaranteed that index <= outputButter.count?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes.

@itingliu
Copy link
Contributor

@swift-ci please test

@danieleggert
Copy link
Contributor Author

@itingliu Thanks for the comments / feedback. I’ve pushed 2 additional commits.

@itingliu
Copy link
Contributor

@swift-ci please test

@itingliu itingliu merged commit a169ceb into swiftlang:main Apr 21, 2025
3 checks passed
itingliu added a commit to itingliu/swift-foundation that referenced this pull request Apr 21, 2025
`TestSupport` already defines a typealias for `Expression` as `public typealias Expression = Foundation.Expression`. Therefore the top level typealias `private typealias Expression = URL.Template.Expression` caused an error when `TestSupport` is imported.

Move the typealias declaration to inside the test suite.
bnbarham added a commit to bnbarham/swift-foundation that referenced this pull request Apr 22, 2025
swiftlang#1198 added most of
the new files, but missed `URLTemplate_Expression.swift`, add it.
bnbarham added a commit that referenced this pull request Apr 22, 2025
…1262)

#1198 added most of
the new files, but missed `URLTemplate_Expression.swift`, add it.
@danieleggert danieleggert deleted the uri-templating-implementation branch April 22, 2025 14:27
Steelskin added a commit to Steelskin/swift-foundation that referenced this pull request Apr 22, 2025
Following swiftlang#1198 and swiftlang#1262, this is needed to build on Windows.
itingliu added a commit that referenced this pull request Apr 22, 2025
`TestSupport` already defines a typealias for `Expression` as `public typealias Expression = Foundation.Expression`. Therefore the top level typealias `private typealias Expression = URL.Template.Expression` caused an error when `TestSupport` is imported.

Move the typealias declaration to inside the test suite.
itingliu added a commit to itingliu/swift-foundation that referenced this pull request Apr 22, 2025
itingliu added a commit to itingliu/swift-foundation that referenced this pull request Apr 22, 2025
@vanvoorden
Copy link
Contributor

vanvoorden commented Jun 27, 2025

I'm trying to run benchmarks locally from main and seeing a lot of errors like:

only available in macOS 26.0 or newer

AFAIK the benchmarks themselves do not look gated around that requirement? Was this going to be updated soon?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants