[Accepted] SF-0020: URI Templating

Hello community,

The review of URI Templating begins now and runs through March 21, 2025.

Reviews are an important part of the Swift-Foundation evolution process. All review feedback should be either on this forum thread or, if you would like to keep your feedback private, directly to me as the review manager by email or DM. When contacting the review manager directly, please include proposal name in the subject line.

What goes into a review?

The goal of the review process is to improve the proposal under review
through constructive criticism and, eventually, determine the direction of
Swift-Foundation. When writing your review, here are some questions you might want to
answer in your review:

  • What is your evaluation of the proposal?
  • Does this proposal fit well with the feel and direction of Swift-Foundation?
  • If you have used other languages or libraries with a similar
    feature, how do you feel that this proposal compares to those?
  • How much effort did you put into your review? A glance, a quick
    reading, or an in-depth study?

More information about Swift-Foundation review process is available here.

Thank you,

Tina L
Review Manager

8 Likes

The URL.init?(template:variables:) initializer looks misplaced to me. I think this operation would be more naturally expressed as something like a render method on URI.Template.

Proposed (using ! for brevity):

let template = URL.Template("http://www.example.com/foo{?query,number}")!
let url = URL(template: template, variables: ["a": "b"])!

Suggestion:

let template = URL.Template("http://www.example.com/foo{?query,number}")!
let url = template.render(with: ["a": "b"])!

According to the proposal this was mentioned as a considered alternative, and in my opinion the initializer is the better choice given the context.

I didn't think so. Like the proposal says, this seems consistent with other APIs, e.g.: String(format:). At least to my mind, this makes more sense, for in general templates don't really render or do anything; they're something you render or act with. In this case, the URL initialises itself with a template and variables.

Oh sorry, I totally forgot to check the alternatives section.

String(format:) seems like an odd comparison. It's quite a niche API in Swift (unlike its counterparts in C and ObjC, where format-based string creation is the norm).

More generally, initializer overloads feel overused and hard to discover. URL already has 18 documented initializers, and even more privateish undocumented ones (go in a repl, type URL.init and press tab).

They also compose poorly in fluent-style method chains, though I guess that's less relevant here, where a template is less likely to be used in that style.

+1, looks great, thanks @deggert!

Generally a +1.

Any reason why URL.Template itself isn't ExpressibleByStringLiteral, so we could directly use:

let url = URL(template: "http://www.example.com/foo{?query,number}", variables: ["a": "b"])!

Any reason why URL.Template itself isn't ExpressibleByStringLiteral , so we could directly use […]

That’s an interesting thought.

The general idea with URL.Template is that a server sends you a template. You fill in the variables and use that to send requests to the server.

In your example

let url = URL(template: "http://www.example.com/foo{?query,number}", variables: ["a": "b"])!

you could have done

let url = URL(string: "http://www.example.com/foo")!

and gotten the same result. ;)

Maybe the documentation needs to be updated to more clearly state what this is intended to be used for?

But I’m not opposed to adding ExpressibleByStringLiteral. But it might be promoting usage other than what’s (originally) intended. The question is thus: should we discourage such usage or encourage it.

There’s also the question of: Why isn’t URL itself ExpressibleByStringLiteral?

The review period has finished. I'd like to accept this proposal as-is since there's no other pending issues.

For the future, the JavaScript ecosystem is in the process of standardizing on the URL Pattern Standard as part of the Minimum Common Web Platform API. This standard is useful mainly for URL pattern matching, but also going the other directionsand encoding URLs from components based on the pattern. This is a far more popular standard and would make a good counterpart to this feature.

2 Likes

Both make sense. They are just two different standards.

When dealing with web tech, which is a far bigger pool than native apps, it seems like the best solution would be to standardise around the path the JS ecosystem is following.

Can we implement both standards or do we have to choose? If we have to choose then it might be best to consider that.

1 Like

That standard is based upon the WHATWG URL standard. So you would either have to choose possible inconsistencies with the URL API or matching behaviour inconsistent with the web.

I thought about implementing it for WebURL. Ultimately it wasn't a huge priority, for 2 main reasons:

  1. Very web-centric. From the standard:

    Finally, the URLPattern constructor string parser does not handle some parts of the basic URL parser state machine. For example, it does not treat backslashes specially as they would all be treated as pattern characters and would require excessive escaping. In addition, this parser might not handle some more esoteric parts of the URL parsing algorithm like file URLs with a hostname. The goal with this parser was to handle the most common URLs while allowing any niche case to be handled instead via the URLPatternInit constructor.

    I'm sorry, but file URLs with a hostname are "esoteric" now? As a URL library developer, this upsets me. It shows that the standard is really very much based around HTTP URLs, which... is fine for the web I guess, but I feel that a good URL library would have something more widely compatible.

  2. Limitations of string interpolations

    I think a good API for custom patterns in Swift would build up a generic type similar to a result builder, so that it can preserve type information about pattern expressions (e.g. capture this URL parameter as an Int). But, it should also support a lightweight string-interpolation syntax:

    let pattern = "https://example.com/products/\(Int.self)/availability"
    

    With result builders, you can do this kind of thing - you can have something like:

    Pattern {
      "https://example.com/products/"
       Capture(Int.self)
       "/availability"
    }
    

    In some ways, you could see ExpressibleByStringInterpolation as an early form of transformations in Swift, that would later be greatly expanded by result builders. But result builders allow you to build up a generic type, which is much more expressive than ExpressibleByStringInterpolation allows.

    If I could create a result-builder-like type out of a string interpolation, I'd be more interested in developing a pattern matching library for this stringy data format.

3 Likes

I really appreciate it, thanks for this!

Currently, I’m in the process of utilising URL.Template in a network client lib (yeah, yet another one). So far, I can really appreciate the current work.

However, I’m missing one feature: URL Template composition

Much like composing a URI from a given URI-Reference and a given base URI (see RFC 3986), we could imagine to provide the same functionality for URL.Templates.

I would appreciate it, if this could be implemented ;) - even though, there's no RFC for this.


This feature would make sense in the library I’m currently developing. It’s kind of a DSL to define endpoints:

Endpoint declaration:

// 1. Session with template variable in base URL
enum MultiTenantAPI: Session {
    static let baseURL = "https://{tenant}.api.example.com" // a BaseURLTemplateString
}

// 2. Group with template variable in path
enum APIVersion: Group {
    typealias Base = MultiTenantAPI
    static let path = "api/{version}" // a ReferenceURLTemplateString
    // Composed result: URL.Template("https://{tenant}.api.example.com/api/{version}")
}

// 3. The hypothetical composeURL that would enable this:
func composeURL(
    urlReference: ReferenceURLTemplateString,    // "/api/{version}"
    relativeTo baseURL: URL.Template?   // URL.Template("https://{tenant}.api.example.com")
) -> URL.Template  // URL.Template("https://{tenant}.api.example.com/api/{version}")

An endpoint might then be declared like this:

enum GetUser: Endpoint {
    typealias Session = UsersGroup
    static let path = "{userId}"
    // Final template: https://{tenant}.api.example.com/api/{version}/users/{userId}
    
    typealias Output = User
    struct URLParams: Encodable {
        let tenant: String      // From base Session
        let version: String     // From Group
        let userId: String      // From Endpoint
    }
}

I guess, you get the point.

Without going into details, how the library works exactly, in addition to make this work, the library has a URLTemplateVariablesEncoder - which encodes types conforming to Encodable into a URL.Template variables dictionary.

This encoder basically is akin to a JSONDecoder, i.e. with strategies to encode keys and values. This is the "missing component" to make URL.Template universally usable in applications ;)

Now, the missing feature in URL.Template would be composition, much like specified in RFC 3986 for regular URIs.

What do you think?

Expanding values into strings may require a more flexible design. Consider having Date values. There's a multitude of options to represent them as strings.

Currently, I'm using URL.Templates as is, but I implemented a URLTemplateVariablesEncoder which encodes a value conforming to Encodable to a URL.Template variables dictionary.
This encoder is akin to a JSONEncoder, that is, you can specify the format with strategies.

I'm aware, this is just one possible solution. Which one makes more sense, IMHO really depends on how you are intending to use it in an application. Providing basic building blocks, like URL.Template makes absolute sense. It's important that we can add more features on top of it, such my URLTemplateVariablesEncoder - which makes sense in my intended usage scenario.