Skip to content

Conform Logger.Level to CustomStringConvertible and LosslessStringConvertible#395

Merged
czechboy0 merged 1 commit intoapple:mainfrom
fpseverino:logger-level-extensions
Dec 8, 2025
Merged

Conform Logger.Level to CustomStringConvertible and LosslessStringConvertible#395
czechboy0 merged 1 commit intoapple:mainfrom
fpseverino:logger-level-extensions

Conversation

@fpseverino
Copy link
Copy Markdown
Contributor

Conform Logger.Level to CustomStringConvertible and LosslessStringConvertible.

Motivation:

There are libraries, such as Vapor, that add these conformances privately and @retroactively. This would avoid having to implement these conformances manually in each library and would unify and standardise the implementation.

Modifications:

Make Logger.Level conform to CustomStringConvertible and LosslessStringConvertible, adding a public description computed property that returns the rawValue and a public initializer that takes a description string, lowercases it, and passes it to the rawValue initializer.

Result:

Logger.Level conforms to CustomStringConvertible and LosslessStringConvertible.

…ngConvertible`.

Motivation:

There are libraries, such as Vapor, that add these conformances privately and `@retroactive`ly. This would avoid having to implement these conformances manually in each library and would unify and standardise the implementation.

Modifications:

Make `Logger.Level` conform to `CustomStringConvertible` and `LosslessStringConvertible`, adding a public `description` computed property that returns the `rawValue` and a public initializer that takes a `description` string, lowercases it, and passes it to the `rawValue` initializer.

Result:

`Logger.Level` conforms to `CustomStringConvertible` and `LosslessStringConvertible`.
Copy link
Copy Markdown
Contributor

@czechboy0 czechboy0 left a comment

Choose a reason for hiding this comment

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

Oh I've wanted this for so long, just never got to open a PR - thank you @fpseverino 🙂

@fpseverino
Copy link
Copy Markdown
Contributor Author

I'm afraid I can't add the required label to the PR to make the CI pass

@czechboy0 czechboy0 added the 🆕 semver/minor Adds new public API. label Dec 7, 2025
@czechboy0 czechboy0 merged commit cdaa5ac into apple:main Dec 8, 2025
60 of 61 checks passed
@adam-fowler
Copy link
Copy Markdown
Contributor

For information this now breaks the following code.

import ArgumentParser
extension Logger.Level: @retroactive ExpressibleByArgument {}

As the argument parser cannot decide on whether to use which of the following

extension LosslessStringConvertible where Self: ExpressibleByArgument {
  public init?(argument: String) {
    self.init(argument)
  }
}
extension RawRepresentable
where Self: ExpressibleByArgument, RawValue: ExpressibleByArgument {
  public init?(argument: String) {
    if let value = RawValue(argument: argument) {
      self.init(rawValue: value)
    } else {
      return nil
    }
  }
}

@czechboy0
Copy link
Copy Markdown
Contributor

Hmm is this filed on the Argument Parser repo?

A library should always be allowed to conform its own type to a stdlib protocol. Retroactive conformances are always dangerous 🙂

@adam-fowler
Copy link
Copy Markdown
Contributor

Hmm is this filed on the Argument Parser repo?

A library should always be allowed to conform its own type to a stdlib protocol. Retroactive conformances are always dangerous 🙂

The retroactive conformance comes from the hummingbird template. Swift-log and swift-argument-parser are never going to depend on each other so this is the only way to implement this.

The argument parser issue is just a demonstration of a code pattern common across multiple libraries where two implementations of a function are provided one for RawRepresentable and one for LosslessStringConvertible. Conforming Logger.Level to both means the compiler can't decide on which to use.

@adam-fowler
Copy link
Copy Markdown
Contributor

There's nothing we can do about it now as it is in a release but it is worth noting for the future that conforming to both protocols can be problematic.

@czechboy0
Copy link
Copy Markdown
Contributor

czechboy0 commented Dec 13, 2025

Ah I see what you mean.

If a library provides two overloads, one for each protocol that can be reasonably used together, there are two solutions I can think of:

  1. The library provides a third overload that constraints to a type that conforms to both protocols - the Swift compiler will always choose that one when using a type that conforms to both, as it's more specific than the other two.
  2. Mark one of them with @_disfavoredOverload.

Either way, from the information above I'd say the action item is on Argument Parser.

@adam-fowler
Copy link
Copy Markdown
Contributor

Oh it broke swift-aws-lambda-runtime as well because they use something of the form env["LOG_LEVEL"].flatMap(Logger.Level.init) which can't tell the difference between the two init's that take a String.

@czechboy0
Copy link
Copy Markdown
Contributor

Yeah that's unfortunate.

I guess the lesson is that this conformance should have been introduced right when Logger.Level was created.

But otherwise I'm not sure how this could have been avoided from the Swift Log side.

@sebsto
Copy link
Copy Markdown

sebsto commented Dec 14, 2025

This should have been a major release as it changes the public API of the library.

Thank you @adam-fowler for having fixed the Lambda runtime.

@czechboy0
Copy link
Copy Markdown
Contributor

czechboy0 commented Dec 14, 2025

This should have been a major release as it changes the public API of the library.

SemVer 2.0, which this library follows, uses major versions for API-breaking changes.

Conforming a library's type to a standard library protocol is not an API break, it's a backwards compatible change, so it was released in a minor release.

@adam-fowler
Copy link
Copy Markdown
Contributor

Conforming a library's type to a standard library protocol is not an API break, it's a backwards compatible change, so it was released in a minor release.

This is a grey area, because in this case it created an additional init function that takes a String as a parameter. This causes any code reliant on passing the original init(rawValue: String) as a function parameter to break.

@czechboy0
Copy link
Copy Markdown
Contributor

That's right, however it's possible to construct code in such a way where any new API added can cause a build error. That'd mean that adding any new API requires a major version, which I don't believe is the intention or the lived experience of SemVer 2.0 and the Swift ecosystem.

This is one of the reasons I avoid capturing function signatures without spelling out all the parameter labels - it leads to potential ambiguity. Also, the @retroactive attribute was added precisely to force downstream package to notice places that can cause future issues, like this one.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

🆕 semver/minor Adds new public API.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants