Skip to content
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

Link header #1687

Expand Up @@ -6,15 +6,15 @@ import cats.effect._
import cats.implicits._
import java.nio.ByteBuffer
import java.nio.charset.StandardCharsets
import org.http4s.{headers => H, _}
import org.http4s.blaze._
import org.http4s.blaze.pipeline.Command.Connected
import org.http4s.blazecore.{ResponseParser, SeqTestHead}
import org.http4s.dsl.io._
import org.http4s.headers.{Date, `Content-Length`, `Transfer-Encoding`}
import org.http4s.{headers => H, _}
import org.specs2.specification.core.Fragment
import scala.concurrent.Await
import scala.concurrent.duration._
import scala.concurrent.Await

class Http1ServerStageSpec extends Http4sSpec {
def makeString(b: ByteBuffer): String = {
Expand Down
32 changes: 32 additions & 0 deletions core/src/main/scala/org/http4s/headers/Link.scala
@@ -0,0 +1,32 @@
package org.http4s
package headers

import org.http4s.parser.HttpHeaderParser
import org.http4s.util.Writer

object Link extends HeaderKey.Internal[Link] {
override def parse(s: String): ParseResult[Link] =
HttpHeaderParser.LINK(s)
}

final case class Link(
uri: Uri,
rel: Option[String] = None,
rev: Option[String] = None,
title: Option[String] = None,
`type`: Option[MediaRange] = None)
extends Header.Parsed {
def key: Link.type = Link
override lazy val value = super.value
override def renderValue(writer: Writer): writer.type = {
writer << "<" << uri.toString << ">"
rel.foreach(writer.append("; rel=").append(_))
rev.foreach(writer.append("; rev=").append(_))
title.foreach(writer.append("; title=").append(_))
`type`.foreach { m =>
writer.append("; type=")
HttpCodec[MediaRange].render(writer, m)
}
writer
}
}
16 changes: 8 additions & 8 deletions core/src/main/scala/org/http4s/parser/HttpHeaderParser.scala
Expand Up @@ -26,23 +26,23 @@ import org.http4s.syntax.string._

object HttpHeaderParser
extends SimpleHeaders
with AcceptCharsetHeader
with AcceptEncodingHeader
with AcceptHeader
with AcceptLanguageHeader
with AuthorizationHeader
with CacheControlHeader
with ContentTypeHeader
with CookieHeader
with AcceptCharsetHeader
with AcceptEncodingHeader
with AuthorizationHeader
with RangeParser
with LinkHeader
with LocationHeader
with OriginHeader
with ProxyAuthenticateHeader
with RangeParser
with RefererHeader
with StrictTransportSecurityHeader
with ProxyAuthenticateHeader
with WwwAuthenticateHeader
with ZipkinHeader
with OriginHeader {

with ZipkinHeader {
type HeaderParser = String => ParseResult[Parsed]

private val allParsers =
Expand Down
37 changes: 37 additions & 0 deletions core/src/main/scala/org/http4s/parser/LinkHeader.scala
@@ -0,0 +1,37 @@
package org.http4s
package parser

import org.http4s.headers.Link
import org.http4s.internal.parboiled2.support.{::, HNil}
import org.http4s.internal.parboiled2.{Rule, Rule1}

trait LinkHeader {
def LINK(value: String): ParseResult[Link] = new LinkParser(value).parse

private class LinkParser(value: String)
extends UriHeaderParser[Link](value)
with MediaRange.MediaRangeParser {

override def fromUri(uri: Uri): Link = Link(uri)

override def entry: Rule1[Link] = rule {
"<" ~ super[UriHeaderParser].entry ~ ">" ~ zeroOrMore(";" ~ OptWS ~ LinkAttr)
}

def LinkAttr: Rule[Link :: HNil, Link :: HNil] = rule {
"rel=" ~ (Token | QuotedString) ~> { (link: Link, rel: String) =>
link.copy(rel = Some(rel))
} |
"rev=" ~ (Token | QuotedString) ~> { (link: Link, rev: String) =>
link.copy(rev = Some(rev))
} |
"title=" ~ (Token | QuotedString) ~> { (link: Link, title: String) =>
link.copy(title = Some(title))
} |
"type=" ~ (MediaRangeDef | ("\"" ~ MediaRangeDef ~ "\"")) ~> {
(link: Link, `type`: MediaRange) =>
link.copy(`type` = Some(`type`))
}
}
}
}
Expand Up @@ -702,6 +702,12 @@ trait ArbitraryInstances {
} yield Uri(scheme, authority, path, query, fragment)
}

implicit val http4sTestingArbitraryForLink: Arbitrary[Link] = Arbitrary {
for {
uri <- http4sTestingArbitraryForUri.arbitrary
} yield Link(uri)
}

implicit val http4sTestingCogenForUri: Cogen[Uri] =
Cogen[String].contramap(_.renderString)

Expand Down
31 changes: 31 additions & 0 deletions tests/src/test/scala/org/http4s/headers/LinkSpec.scala
@@ -0,0 +1,31 @@
package org.http4s
package headers

class LinkSpec extends HeaderLaws {
// FIXME Uri does not round trip properly: https://github.com/http4s/http4s/issues/1651
// checkAll(name = "Link", headerLaws(Link))

val link = """</feed>; rel="alternate"; type="text/*"; title="main"; rev="previous""""

"parse" should {
"accept format RFC 5988" in {
val parsedLink = Link.parse(link).right
parsedLink.map(_.uri) must beRight(Uri.uri("/feed"))
parsedLink.map(_.rel) must beRight(Option("alternate"))
parsedLink.map(_.title) must beRight(Option("main"))
parsedLink.map(_.`type`) must beRight(Option(MediaRange.`text/*`))
parsedLink.map(_.rev) must beRight(Option("previous"))
}
}

"render" should {
"properly format link according to RFC 5988" in {
Link(
Uri.uri("/feed"),
rel = Some("alternate"),
title = Some("main"),
`type` = Some(MediaRange.`text/*`)).renderString must_==
"Link: </feed>; rel=alternate; title=main; type=text/*"
}
}
}