Repository: http4s/http4s (scala)

Issue:
Wrong correspondence between query param syntax methods
Initially discussed in [discord](https://discord.com/channels/632277896739946517/632286375311573032/1101695422499672147).

Looks like this docs for the QueryParam syntax is misleading:
https://github.com/http4s/http4s/blob/841ca8c5c6116f2065b941ea5a9e111faad2c8a2/core/shared/src/main/scala/org/http4s/QueryOps.scala#L40-L42
it states that it is an alias for `withQueryParam`, but in fact, there's no `withQueryParam` version that would correspond to that `+*?` method.

The only `withQueryParam` method that would take a single argument is this one:
https://github.com/http4s/http4s/blob/841ca8c5c6116f2065b941ea5a9e111faad2c8a2/core/shared/src/main/scala/org/http4s/QueryOps.scala#L143-L144
But it is not the same as the syntax from the above that refers to it.

The actual problem is that there's could be a code that defines, let's say, `class Foo`:
```scala
case class Foo(value: Int)

object Foo {
  implicit val fooQueryParam: QueryParam[Foo] = QueryParam.fromKey("foo")
  implicit val fooQueryParamEncoder: QueryParamEncoder[Foo] =  QueryParamEncoder[Int].contramap(_.value)
}
```
Therefore, simple replacing `+*?` with `withQueryParam` leads to completely different results:
 - `uri"foo/bar" +*? Foo(123)            ` ==> `foo/bar?foo=123`
 - `uri"foo/bar".withQueryParam(Foo(123))` ==> `foo/bar?foo`

Pull request description:
Also updates the implementation and scaladoc for QueryOps.+*? to delegate to withQueryParamValue

Fixes http4s#7095

Patch:
diff --git a/core/shared/src/main/scala/org/http4s/QueryOps.scala b/core/shared/src/main/scala/org/http4s/QueryOps.scala
index 699e0f732ad..01d8ed9c55c 100644
--- a/core/shared/src/main/scala/org/http4s/QueryOps.scala
+++ b/core/shared/src/main/scala/org/http4s/QueryOps.scala
@@ -37,9 +37,9 @@ trait QueryOps {
   def +?[T: QueryParam]: Self =
     _withQueryParam(QueryParam[T].key, Nil)
 
-  /** alias for withQueryParam */
+  /** alias for withQueryParamValue */
   def +*?[T: QueryParamKeyLike: QueryParamEncoder](value: T): Self =
-    _withQueryParam(QueryParamKeyLike[T].getKey(value), QueryParamEncoder[T].encode(value) :: Nil)
+    withQueryParamValue(value)
 
   /** alias for withQueryParam */
   def +*?[T: QueryParam: QueryParamEncoder](values: collection.Seq[T]): Self =
@@ -161,6 +161,13 @@ trait QueryOps {
   ): Self =
     _withQueryParam(QueryParamKeyLike[K].getKey(key), values.map(QueryParamEncoder[T].encode))
 
+  /** Creates a new `Self` with the specified parameter in the [[Query]].
+    * If a parameter with the given `QueryParam.key` already exists the value(s) will be
+    * replaced.
+    */
+  def withQueryParamValue[T: QueryParamKeyLike: QueryParamEncoder](value: T): Self =
+    _withQueryParam(QueryParamKeyLike[T].getKey(value), QueryParamEncoder[T].encode(value) :: Nil)
+
   /** Creates maybe a new `Self` with all the specified parameters in the
     * [[Query]]. If any of the given parameters' keys already exists, the
     * value(s) will be replaced. Parameters from the input map are added


Test patch:
diff --git a/tests/shared/src/test/scala/org/http4s/UriSuite.scala b/tests/shared/src/test/scala/org/http4s/UriSuite.scala
index a82e2b391b6..a130f20dea6 100644
--- a/tests/shared/src/test/scala/org/http4s/UriSuite.scala
+++ b/tests/shared/src/test/scala/org/http4s/UriSuite.scala
@@ -37,8 +37,6 @@ import org.scalacheck.Gen
 import org.scalacheck.Prop._
 import org.typelevel.ci._
 
-import scala.collection.immutable.Seq
-
 // TODO: this needs some more filling out
 class UriSuite extends Http4sSuite {
   sealed case class Ttl(seconds: Int)
@@ -573,10 +571,16 @@ class UriSuite extends Http4sSuite {
   }
 
   test("Uri parameters should parse empty query string") {
-    assertEquals(Uri(query = Query.unsafeFromString("")).multiParams, Map("" -> Nil))
+    assertEquals(
+      Uri(query = Query.unsafeFromString("")).multiParams,
+      Map[String, Seq[String]]("" -> Nil),
+    )
   }
   test("Uri parameters should parse parameter without key but with empty value") {
-    assertEquals(Uri(query = Query.unsafeFromString("=")).multiParams, Map("" -> List("")))
+    assertEquals(
+      Uri(query = Query.unsafeFromString("=")).multiParams,
+      Map[String, Seq[String]]("" -> List("")),
+    )
   }
   test("Uri parameters should parse parameter without key but with value") {
     assertEquals(
@@ -716,9 +720,9 @@ class UriSuite extends Http4sSuite {
     )
   }
   test("Uri.multiParams should find parameter with empty key and without value") {
-    assertEquals(Uri(query = Query.unsafeFromString("&")).multiParams, Map("" -> Seq()))
-    assertEquals(Uri(query = Query.unsafeFromString("&&")).multiParams, Map("" -> Seq()))
-    assertEquals(Uri(query = Query.unsafeFromString("&&&")).multiParams, Map("" -> Seq()))
+    assertEquals(Uri(query = Query.unsafeFromString("&")).multiParams, Map("" -> Seq[String]()))
+    assertEquals(Uri(query = Query.unsafeFromString("&&")).multiParams, Map("" -> Seq[String]()))
+    assertEquals(Uri(query = Query.unsafeFromString("&&&")).multiParams, Map("" -> Seq[String]()))
   }
   test("Uri.multiParams should find parameter with an empty value") {
     assertEquals(
@@ -739,7 +743,7 @@ class UriSuite extends Http4sSuite {
   test("Uri.multiParams should find parameter without value") {
     assertEquals(
       Uri(query = Query.unsafeFromString("param1&param2&param3")).multiParams,
-      Map("param1" -> Seq(), "param2" -> Seq(), "param3" -> Seq()),
+      Map[String, Seq[String]]("param1" -> Seq(), "param2" -> Seq(), "param3" -> Seq()),
     )
   }
 
@@ -838,8 +842,10 @@ class UriSuite extends Http4sSuite {
   test(
     "Uri parameter convenience methods should add a query parameter with a QueryParamEncoder and an implicit key"
   ) {
-    val u = Uri().+*?(Ttl(2))
-    assertEquals(u, Uri(query = Query.unsafeFromString(s"ttl=2")))
+    val u0 = Uri().+*?(Ttl(2))
+    assertEquals(u0, Uri(query = Query.unsafeFromString(s"ttl=2")))
+    val u1 = Uri().withQueryParamValue(Ttl(2))
+    assertEquals(u1, Uri(query = Query.unsafeFromString(s"ttl=2")))
   }
   test("Uri parameter convenience methods should add a QueryParam instance") {
     val u = Uri().withQueryParam[Ttl]