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

Create a modeled Ipv4Address #2679

Merged
merged 3 commits into from
Jul 5, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions client/src/test/scala/org/http4s/client/PoolManagerSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ import scala.concurrent.duration._

class PoolManagerSpec(name: String) extends Http4sSpec {
val _ = name
val key = RequestKey(Uri.Scheme.http, Uri.Authority(host = Uri.IPv4("127.0.0.1")))
val otherKey = RequestKey(Uri.Scheme.http, Uri.Authority(host = Uri.IPv4("localhost")))
val key = RequestKey(Uri.Scheme.http, Uri.Authority(host = ipv4"127.0.0.1"))
val otherKey = RequestKey(Uri.Scheme.http, Uri.Authority(host = Uri.RegName("localhost")))

class TestConnection extends Connection[IO] {
def isClosed = false
Expand Down
7 changes: 7 additions & 0 deletions core/src/main/scala/org/http4s/LiteralSyntaxMacros.scala
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,13 @@ object LiteralSyntaxMacros {
Uri.Scheme.fromString(_).isRight,
s => c.universe.reify(Uri.Scheme.unsafeFromString(s.splice)))

def ipv4AddressInterpolator(c: blackbox.Context)(args: c.Expr[Any]*): c.Expr[Uri.Ipv4Address] =
singlePartInterpolator(c)(
args,
"Ipv4Address",
Uri.Ipv4Address.fromString(_).isRight,
s => c.universe.reify(Uri.Ipv4Address.unsafeFromString(s.splice)))

def mediaTypeInterpolator(c: blackbox.Context)(args: c.Expr[Any]*): c.Expr[MediaType] =
singlePartInterpolator(c)(
args,
Expand Down
106 changes: 100 additions & 6 deletions core/src/main/scala/org/http4s/Uri.scala
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
package org.http4s

import cats.{Eq, Order, Show}
import cats.{Eq, Hash, Order, Show}
import cats.implicits.{catsSyntaxEither => _, _}
import java.net.{Inet4Address, InetAddress}
import java.nio.charset.StandardCharsets
import org.http4s.Uri._
import org.http4s.internal.parboiled2.{Parser => PbParser, _}
import org.http4s.internal.parboiled2.CharPredicate.{Alpha, Digit}
import org.http4s.internal.parboiled2.{Parser => PbParser}
import org.http4s.parser._
import org.http4s.syntax.string._
import org.http4s.util._
Expand Down Expand Up @@ -247,24 +248,117 @@ object Uri {
sealed trait Host extends Renderable {
final def value: String = this match {
case RegName(h) => h.toString
case IPv4(a) => a.toString
case addr: Ipv4Address =>
new StringBuffer()
.append(addr.a & 0xff)
.append(".")
.append(addr.b & 0xff)
.append(".")
.append(addr.c & 0xff)
.append(".")
.append(addr.d & 0xff)
.toString
case IPv6(a) => a.toString
}

override def render(writer: Writer): writer.type = this match {
case RegName(n) => writer << n
case IPv4(a) => writer << a
case a: Ipv4Address => writer << a.value
case IPv6(a) => writer << '[' << a << ']'
case _ => writer
}
}

final case class RegName(host: CaseInsensitiveString) extends Host
final case class IPv4(address: CaseInsensitiveString) extends Host

@deprecated("Renamed to Ipv4Address, modeled as case class of bytes", "0.21.0-M2")
type IPv4 = Ipv4Address

@deprecated("Renamed to Ipv4Address, modeled as case class of bytes", "0.21.0-M2")
object IPv4 {
@deprecated("Use Ipv4Address.fromString(ciString.value)", "0.21.0-M2")
def apply(ciString: CaseInsensitiveString): ParseResult[Ipv4Address] =
Ipv4Address.fromString(ciString.value)
}

case class Ipv4Address(a: Byte, b: Byte, c: Byte, d: Byte)
extends Host
with Ordered[Ipv4Address]
with Serializable {
override def toString: String = s"Ipv4Address($value)"

override def compare(that: Ipv4Address): Int = {
var cmp = a.compareTo(that.a)
if (cmp == 0) cmp = b.compareTo(that.b)
if (cmp == 0) cmp = c.compareTo(that.c)
if (cmp == 0) cmp = d.compareTo(that.d)
cmp
}

def toByteArray: Array[Byte] =
Array(a, b, c, d)

def toInet4Address: Inet4Address =
InetAddress.getByAddress(toByteArray).asInstanceOf[Inet4Address]
}

object Ipv4Address {
def fromString(s: String): ParseResult[Ipv4Address] =
new Http4sParser[Ipv4Address](s, "Invalid scheme") with Parser with IpParser {
def main = ipv4Address
}.parse

/** Like `fromString`, but throws on invalid input */
def unsafeFromString(s: String): Ipv4Address =
fromString(s).fold(throw _, identity)

def fromByteArray(bytes: Array[Byte]): ParseResult[Ipv4Address] =
bytes match {
case Array(a, b, c, d) =>
Right(Ipv4Address(a, b, c, d))
case _ =>
Left(ParseFailure("Invalid Ipv4Address", s"Byte array not exactly four bytes: ${bytes}"))
}

def fromInet4Address(address: Inet4Address): Ipv4Address =
address.getAddress match {
case Array(a, b, c, d) =>
Ipv4Address(a, b, c, d)
case array =>
throw bug(s"Inet4Address.getAddress not exactly four bytes: ${array}")
}

private[http4s] trait Parser { self: PbParser with IpParser =>
def ipv4Address: Rule1[Ipv4Address] = rule {
// format: off
decOctet ~ "." ~ decOctet ~ "." ~ decOctet ~ "." ~ decOctet ~>
{ (a: Byte, b: Byte, c: Byte, d: Byte) => new Ipv4Address(a, b, c, d) }
// format:on
}

private def decOctet = rule { capture(DecOctet) ~> (_.toInt.toByte) }
}

implicit val http4sInstancesForIpv4Address
: HttpCodec[Ipv4Address] with Order[Ipv4Address] with Hash[Ipv4Address] with Show[Ipv4Address] =
new HttpCodec[Ipv4Address] with Order[Ipv4Address] with Hash[Ipv4Address] with Show[Ipv4Address] {
def parse(s: String): ParseResult[Ipv4Address] =
Ipv4Address.fromString(s)
def render(writer: Writer, ipv4: Ipv4Address): writer.type =
writer << ipv4.value

def compare(x: Ipv4Address, y: Ipv4Address): Int = x.compareTo(y)

def hash(x: Ipv4Address): Int = x.hashCode

def show(x: Ipv4Address): String = x.toString
}
}


final case class IPv6(address: CaseInsensitiveString) extends Host

object RegName { def apply(name: String): RegName = new RegName(name.ci) }
object IPv4 { def apply(address: String): IPv4 = new IPv4(address.ci) }
object IPv6 { def apply(address: String): IPv6 = new IPv6(address.ci) }

/**
Expand Down
2 changes: 1 addition & 1 deletion core/src/main/scala/org/http4s/UriTemplate.scala
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,7 @@ object UriTemplate {

protected def renderHost(h: Host): String = h match {
case RegName(n) => n.toString
case IPv4(a) => a.toString
case a: Ipv4Address => a.value
case IPv6(a) => "[" + a.toString + "]"
case _ => ""
}
Expand Down
17 changes: 9 additions & 8 deletions core/src/main/scala/org/http4s/parser/Rfc3986Parser.scala
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import org.http4s.syntax.string._
private[parser] trait Rfc3986Parser
extends Parser
with Uri.Scheme.Parser
with Uri.Ipv4Address.Parser
with IpParser
with StringBuilding {
// scalastyle:off public.methods.have.type
Expand Down Expand Up @@ -68,15 +69,15 @@ private[parser] trait Rfc3986Parser
}

def Host: Rule1[org.http4s.Uri.Host] = rule {
capture(IpV4Address) ~> { s: String =>
org.http4s.Uri.IPv4(s.ci)
// format: off
ipv4Address |
(IpLiteral | capture(IpV6Address)) ~> { s: String =>
org.http4s.Uri.IPv6(s.ci)
} |
(IpLiteral | capture(IpV6Address)) ~> { s: String =>
org.http4s.Uri.IPv6(s.ci)
} |
capture(RegName) ~> { s: String =>
org.http4s.Uri.RegName(decode(s).ci)
}
capture(RegName) ~> { s: String =>
org.http4s.Uri.RegName(decode(s).ci)
}
// format:on
}

def Port = rule {
Expand Down
1 change: 1 addition & 0 deletions core/src/main/scala/org/http4s/syntax/LiteralsSyntax.scala
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ trait LiteralsSyntax {
class LiteralsOps(val sc: StringContext) extends AnyVal {
def uri(args: Any*): Uri = macro LiteralSyntaxMacros.uriInterpolator
def scheme(args: Any*): Uri.Scheme = macro LiteralSyntaxMacros.schemeInterpolator
def ipv4(args: Any*): Uri.Ipv4Address = macro LiteralSyntaxMacros.ipv4AddressInterpolator
def mediaType(args: Any*): MediaType = macro LiteralSyntaxMacros.mediaTypeInterpolator
def qValue(args: Any*): QValue = macro LiteralSyntaxMacros.qValueInterpolator
}
Original file line number Diff line number Diff line change
Expand Up @@ -583,25 +583,22 @@ private[http4s] trait ArbitraryInstances {
oneOf(g, const(ev.empty))

// https://tools.ietf.org/html/rfc3986#appendix-A
implicit val http4sTestingArbitraryForIPv4: Arbitrary[Uri.IPv4] = Arbitrary {
val num = numChar.map(_.toString)
def range(min: Int, max: Int) = choose(min.toChar, max.toChar).map(_.toString)
val genDecOctet = oneOf(
num,
range(49, 57) |+| num,
const("1") |+| num |+| num,
const("2") |+| range(48, 52) |+| num,
const("25") |+| range(48, 51)
)
listOfN(4, genDecOctet).map(_.mkString(".")).map(Uri.IPv4.apply)
implicit val http4sTestingArbitraryForIpv4Address: Arbitrary[Uri.Ipv4Address] = Arbitrary {
for {
a <- getArbitrary[Byte]
b <- getArbitrary[Byte]
c <- getArbitrary[Byte]
d <- getArbitrary[Byte]
} yield Uri.Ipv4Address(a, b, c, d)
}

implicit val http4sTestingCogenForIpv4Address: Cogen[Uri.Ipv4Address] =
Cogen[(Byte, Byte, Byte, Byte)].contramap(ipv4 => (ipv4.a, ipv4.b, ipv4.c, ipv4.d))

// https://tools.ietf.org/html/rfc3986#appendix-A
implicit val http4sTestingArbitraryForIPv6: Arbitrary[Uri.IPv6] = Arbitrary {
val h16 = timesBetween(min = 1, max = 4, genHexDigit.map(_.toString))
val ls32 = oneOf(
h16 |+| const(":") |+| h16,
http4sTestingArbitraryForIPv4.arbitrary.map(_.address.value))
val ls32 = oneOf(h16 |+| const(":") |+| h16, getArbitrary[Uri.Ipv4Address].map(_.value))
val h16colon = h16 |+| const(":")
val :: = const("::")

Expand All @@ -621,10 +618,7 @@ private[http4s] trait ArbitraryInstances {
implicit val http4sTestingArbitraryForUriHost: Arbitrary[Uri.Host] = Arbitrary {
val genRegName =
listOf(oneOf(genUnreserved, genPctEncoded, genSubDelims)).map(rn => Uri.RegName(rn.mkString))
oneOf(
http4sTestingArbitraryForIPv4.arbitrary,
http4sTestingArbitraryForIPv6.arbitrary,
genRegName)
oneOf(getArbitrary[Uri.Ipv4Address], http4sTestingArbitraryForIPv6.arbitrary, genRegName)
}

implicit val http4sTestingArbitraryForAuthority: Arbitrary[Uri.Authority] = Arbitrary {
Expand Down
2 changes: 1 addition & 1 deletion server/src/main/scala/org/http4s/server/Server.scala
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ abstract class Server[F[_]] {
Uri.Authority(
host = address.getAddress match {
case ipv4: Inet4Address =>
Uri.IPv4(ipv4.getHostAddress)
Uri.Ipv4Address.fromInet4Address(ipv4)
case ipv6: Inet6Address =>
Uri.IPv6(ipv6.getHostAddress)
case weird =>
Expand Down
55 changes: 55 additions & 0 deletions tests/src/test/scala/org/http4s/Ipv4AddressSpec.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package org.http4s

import cats.implicits._
import cats.kernel.laws.discipline.{HashTests, OrderTests}
import org.http4s.Uri.Ipv4Address
import org.http4s.laws.discipline.HttpCodecTests
import org.http4s.util.Renderer.renderString
import org.specs2.execute._, Typecheck._
import org.specs2.matcher.TypecheckMatchers._

class Ipv4AddressSpec extends Http4sSpec {
checkAll("Order[Ipv4Address]", OrderTests[Ipv4Address].order)
checkAll("Hash[Ipv4Address]", HashTests[Ipv4Address].hash)
checkAll("HttpCodec[Ipv4Address]", HttpCodecTests[Ipv4Address].httpCodec)

"render" should {
"render all 4 octets" in {
renderString(ipv4"192.168.0.1") must_== "192.168.0.1"
}
}

"fromInet4Address" should {
"round trip with toInet4Address" in prop { ipv4: Ipv4Address =>
Ipv4Address.fromInet4Address(ipv4.toInet4Address) must_== ipv4
}
}

"fromByteArray" should {
"round trip with toByteArray" in prop { ipv4: Ipv4Address =>
Ipv4Address.fromByteArray(ipv4.toByteArray) must_== Right(ipv4)
}
}

"compare" should {
"be consistent with unsigned int" in prop { xs: List[Ipv4Address] =>
def tupled(a: Ipv4Address) = (a.a, a.b, a.c, a.d)
xs.sorted.map(tupled) must_== xs.map(tupled).sorted
}

"be consistent with Ordered" in prop { (a: Ipv4Address, b: Ipv4Address) =>
math.signum(a.compareTo(b)) must_== math.signum(a.compare(b))
}
}

"ipv4 interpolator" should {
"be consistent with fromString" in {
Right(ipv4"127.0.0.1") must_== Ipv4Address.fromString("127.0.0.1")
Right(ipv4"192.168.0.1") must_== Ipv4Address.fromString("192.168.0.1")
}

"reject invalid values" in {
typecheck("""ipv4"256.0.0.0"""") must not succeed
}
}
}
6 changes: 3 additions & 3 deletions tests/src/test/scala/org/http4s/UriSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -188,17 +188,17 @@ http://example.org/a file
"render IPv4 URL with parameters" in {
Uri(
Some(Scheme.http),
Some(Authority(host = IPv4("192.168.1.1".ci), port = Some(80))),
Some(Authority(host = ipv4"192.168.1.1", port = Some(80))),
"/c",
Query.fromPairs("GB" -> "object", "Class" -> "one")).toString must_== ("http://192.168.1.1:80/c?GB=object&Class=one")
}

"render IPv4 URL with port" in {
Uri(Some(Scheme.http), Some(Authority(host = IPv4("192.168.1.1".ci), port = Some(8080)))).toString must_== ("http://192.168.1.1:8080")
Uri(Some(Scheme.http), Some(Authority(host = ipv4"192.168.1.1", port = Some(8080)))).toString must_== ("http://192.168.1.1:8080")
}

"render IPv4 URL without port" in {
Uri(Some(Scheme.http), Some(Authority(host = IPv4("192.168.1.1".ci)))).toString must_== ("http://192.168.1.1")
Uri(Some(Scheme.http), Some(Authority(host = ipv4"192.168.1.1"))).toString must_== ("http://192.168.1.1")
}

"render IPv6 URL with parameters" in {
Expand Down
Loading