Skip to content

Commit

Permalink
Merge pull request #3450 from cquiroz/accept-patchc
Browse files Browse the repository at this point in the history
Model Accept-Patch header
  • Loading branch information
cquiroz committed May 31, 2020
2 parents ae5377f + 1ed6e58 commit f965cf9
Show file tree
Hide file tree
Showing 9 changed files with 102 additions and 2 deletions.
11 changes: 11 additions & 0 deletions core/src/main/scala/org/http4s/Header.scala
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,17 @@ object Header {
}
}

/** Helper trait that provides a default way of rendering the value provided a Renderer */
trait RecurringRenderer extends Recurring {
type Value
implicit def renderer: Renderer[Value]
override def renderValue(writer: Writer): writer.type = {
renderer.render(writer, values.head)
values.tail.foreach(writer << ", " << Renderer.renderString(_))
writer
}
}

implicit val HeaderShow: Show[Header] = Show.show[Header] {
_.toString
}
Expand Down
23 changes: 22 additions & 1 deletion core/src/main/scala/org/http4s/headers/Accept-Patch.scala
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,27 @@
*/

package org.http4s

package headers

object `Accept-Patch` extends HeaderKey.Default
import org.http4s.parser.HttpHeaderParser
import org.http4s.util.Renderer
import cats.data.NonEmptyList

object `Accept-Patch` extends HeaderKey.Internal[`Accept-Patch`] with HeaderKey.Recurring {

override def parse(s: String): ParseResult[`Accept-Patch`] =
HttpHeaderParser.ACCEPT_PATCH(s)

}

// see https://tools.ietf.org/html/rfc5789#section-3.1
final case class `Accept-Patch` private (values: NonEmptyList[MediaType])
extends Header.RecurringRenderer {

type Value = MediaType
val renderer = Renderer[MediaType]

override def key: `Accept-Patch`.type = `Accept-Patch`

}
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ object HttpHeaderParser
}

private def gatherBuiltIn(): Unit = {
addParser_(CIString("ACCEPT-PATCH"), `ACCEPT_PATCH`)
addParser_(CIString("ACCEPT"), `ACCEPT`)
addParser_(CIString("ACCEPT-CHARSET"), `ACCEPT_CHARSET`)
addParser_(CIString("ACCEPT-ENCODING"), `ACCEPT_ENCODING`)
Expand Down
10 changes: 10 additions & 0 deletions core/src/main/scala/org/http4s/parser/SimpleHeaders.scala
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,16 @@ import org.typelevel.ci.CIString
* parser rules for all headers that can be parsed with one simple rule
*/
private[parser] trait SimpleHeaders {
def ACCEPT_PATCH(value: String): ParseResult[`Accept-Patch`] =
new Http4sHeaderParser[`Accept-Patch`](value) with MediaType.MediaTypeParser {
def entry =
rule {
oneOrMore(MediaTypeFull).separatedBy(ListSep) ~ EOL ~> { (medias: Seq[MediaType]) =>
`Accept-Patch`(NonEmptyList(medias.head, medias.tail.toList))
}
}
}.parse

def ALLOW(value: String): ParseResult[Allow] =
new Http4sHeaderParser[Allow](value) {
def entry =
Expand Down
2 changes: 2 additions & 0 deletions core/src/main/scala/org/http4s/util/Renderable.scala
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ trait Renderer[T] {
}

object Renderer {
@inline def apply[A](implicit ev: Renderer[A]): Renderer[A] = ev

def renderString[T: Renderer](t: T): String = new StringWriter().append(t).result

implicit val RFC7231InstantRenderer: Renderer[Instant] = new Renderer[Instant] {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -492,6 +492,13 @@ private[http4s] trait ArbitraryInstances {
)
}

implicit val http4sTestingArbitraryForAcceptPatchHeader: Arbitrary[headers.`Accept-Patch`] =
Arbitrary {
for {
media <- getArbitrary[NonEmptyList[MediaType]]
} yield headers.`Accept-Patch`(media)
}

implicit val http4sTestingArbitraryForAgeHeader: Arbitrary[headers.Age] =
Arbitrary {
for {
Expand Down
2 changes: 1 addition & 1 deletion tests/src/test/scala/org/http4s/HeadersSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ class HeadersSpec extends Http4sSpec {
Header("Accept-Patch", ""),
Header("Access-Control-Allow-Credentials", "")
)
headers.get(`Accept-Patch`).map(_.value) must beSome("")
headers.get(`Access-Control-Allow-Credentials`).map(_.value) must beSome("")
}

"Remove duplicate headers which are not of type Recurring on concatenation (++)" in {
Expand Down
33 changes: 33 additions & 0 deletions tests/src/test/scala/org/http4s/headers/AcceptPatchSpec.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/*
* Copyright 2013-2020 http4s.org
*
* SPDX-License-Identifier: Apache-2.0
*/

package org.http4s.headers

import cats.data.NonEmptyList
import org.http4s.MediaType

class AcceptPatchSpec extends HeaderLaws {
checkAll("AcceptPatch", headerLaws(`Accept-Patch`))

"render" should {
"media types" in {
`Accept-Patch`(
NonEmptyList.of(
new MediaType("text", "example"))).renderString must_== "Accept-Patch: text/example"
}
"mulitple media types" in {
`Accept-Patch`(
NonEmptyList.of(
new MediaType("application", "example"),
new MediaType(
"text",
"example",
extensions =
Map("charset" -> "utf-8")))).renderString must_== "Accept-Patch: application/example, text/example; charset=\"utf-8\""
}
}

}
15 changes: 15 additions & 0 deletions tests/src/test/scala/org/http4s/parser/SimpleHeadersSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,21 @@ import org.typelevel.ci.CIString

class SimpleHeadersSpec extends Http4sSpec {
"SimpleHeaders" should {
"parse Accept-Patch" in {
val header =
`Accept-Patch`(
NonEmptyList.of(new MediaType("text", "example", extensions = Map("charset" -> "utf-8"))))
HttpHeaderParser.parseHeader(header.toRaw) must beRight(header)
val multipleMediaTypes =
`Accept-Patch`(
NonEmptyList
.of(new MediaType("application", "example"), new MediaType("text", "example")))
HttpHeaderParser.parseHeader(multipleMediaTypes.toRaw) must beRight(multipleMediaTypes)

val bad = Header(header.name.toString, "foo; bar")
HttpHeaderParser.parseHeader(bad) must beLeft
}

"parse Connection" in {
val header = Connection(CIString("closed"))
HttpHeaderParser.parseHeader(header.toRaw) must beRight(header)
Expand Down

0 comments on commit f965cf9

Please sign in to comment.