diff --git a/shared/src/main/scala/com/comcast/ip4s/Cidr.scala b/shared/src/main/scala/com/comcast/ip4s/Cidr.scala index cec97c8c..367d637e 100644 --- a/shared/src/main/scala/com/comcast/ip4s/Cidr.scala +++ b/shared/src/main/scala/com/comcast/ip4s/Cidr.scala @@ -20,6 +20,7 @@ import scala.util.Try import scala.util.hashing.MurmurHash3 import cats.{Order, Show} +import com.comcast.ip4s.Cidr.Strict /** Classless Inter-Domain Routing address, which represents an IP address and its routing prefix. * @@ -28,10 +29,24 @@ import cats.{Order, Show} * @param prefixBits * number of leading 1s in the routing mask */ -final class Cidr[+A <: IpAddress] private (val address: A, val prefixBits: Int) extends Product with Serializable { +sealed class Cidr[+A <: IpAddress] protected (val address: A, val prefixBits: Int) extends Product with Serializable { def copy[AA >: A <: IpAddress](address: AA = this.address, prefixBits: Int = this.prefixBits): Cidr[AA] = Cidr[AA](address, prefixBits) + /** Returns a normalized cidr range, where the address is truncated to the prefix, so that the returned range + * is (and prints as) a spec-valid cidr range, with no bits outside the routing mask set. + * + * @return a normalized cidr range + * + * @example {{{ + * scala> val raw = Cidr(ipv4"10.11.12.13", 8) + * raw: Cidr[Ipv4Address] = 10.11.12.13/8 + * scala> raw.normalized + * res0: Cidr.Strict[Ipv4Address] = 10.0.0.0/8 + * }}} + */ + def normalized: Strict[A] = Cidr.Strict(this) + /** Returns the routing mask. * * @example {{{ @@ -118,6 +133,23 @@ final class Cidr[+A <: IpAddress] private (val address: A, val prefixBits: Int) object Cidr { + /** A normalized cidr range, of which the address is identical to the prefix. + * + * This means the address will never have any bits set outside the prefix. For example, a range + * such as 192.168.0.1/31 is not allowed. + */ + final class Strict[+A <: IpAddress] private (override val prefix: A, prefixBits: Int) + extends Cidr[A](prefix, prefixBits) { + override def normalized: this.type = this + } + + object Strict { + def apply[A <: IpAddress](cidr: Cidr[A]): Cidr.Strict[A] = cidr match { + case already: Strict[_] => already.asInstanceOf[Strict[A]] + case _ => new Cidr.Strict(cidr.prefix, cidr.prefixBits) + } + } + /** Constructs a CIDR from the supplied IP address and prefix bit count. Note if `prefixBits` is less than 0, the * built `Cidr` will have `prefixBits` set to 0. Similarly, if `prefixBits` is greater than the bit length of the * address, it will be set to the bit length of the address. diff --git a/test-kit/shared/src/main/scala/com/comcast/ip4s/Arbitraries.scala b/test-kit/shared/src/main/scala/com/comcast/ip4s/Arbitraries.scala index 796399b2..bbd203cf 100644 --- a/test-kit/shared/src/main/scala/com/comcast/ip4s/Arbitraries.scala +++ b/test-kit/shared/src/main/scala/com/comcast/ip4s/Arbitraries.scala @@ -45,6 +45,10 @@ object Arbitraries { implicit def cidrArbitrary[A <: IpAddress](implicit arbIp: Arbitrary[A]): Arbitrary[Cidr[A]] = Arbitrary(cidrGenerator(arbIp.arbitrary)) + implicit def cidrStrictArbitrary[A <: IpAddress](implicit arbIp: Arbitrary[A]): Arbitrary[Cidr.Strict[A]] = Arbitrary( + cidrGenerator(arbIp.arbitrary).map(_.normalized) + ) + val portGenerator: Gen[Port] = Gen.chooseNum(0, 65535).map(Port.fromInt(_).get) implicit val portArbitrary: Arbitrary[Port] = Arbitrary(portGenerator) diff --git a/test-kit/shared/src/test/scala/com/comcast/ip4s/CidrStrictTest.scala b/test-kit/shared/src/test/scala/com/comcast/ip4s/CidrStrictTest.scala new file mode 100644 index 00000000..92f4ccf3 --- /dev/null +++ b/test-kit/shared/src/test/scala/com/comcast/ip4s/CidrStrictTest.scala @@ -0,0 +1,26 @@ +/* + * Copyright 2018 Comcast Cable Communications Management, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.comcast.ip4s + +import org.scalacheck.Prop.forAll +import Arbitraries._ + +class CidrStrictTest extends BaseTestSuite { + property("prefix and address are identical") { + forAll { (cidr: Cidr.Strict[IpAddress]) => assertEquals(cidr.address, cidr.prefix) } + } +}