From 2ee90f9288d2a2fa1859055a2f782d22525c348a Mon Sep 17 00:00:00 2001 From: Dragisa Krsmanovic Date: Sat, 30 Jun 2018 08:45:05 -0700 Subject: [PATCH 01/19] Add support for isInEuropeanUnion flag (closes #87) --- README.md | 3 ++- .../com.snowplowanalytics.maxmind.iplookups/model.scala | 6 ++++-- .../IpLookupsTest.scala | 9 ++++++--- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index cb432dc..654c12f 100644 --- a/README.md +++ b/README.md @@ -153,7 +153,8 @@ case class IpLocation( timezone: Option[String], postalCode: Option[String], metroCode: Option[Int], - regionName: Option[String] + regionName: Option[String], + isInEuropeanUnion: Boolean ) ``` diff --git a/src/main/scala/com.snowplowanalytics.maxmind.iplookups/model.scala b/src/main/scala/com.snowplowanalytics.maxmind.iplookups/model.scala index 22376b7..2efdae9 100644 --- a/src/main/scala/com.snowplowanalytics.maxmind.iplookups/model.scala +++ b/src/main/scala/com.snowplowanalytics.maxmind.iplookups/model.scala @@ -27,7 +27,8 @@ object model { timezone: Option[String], postalCode: Option[String], metroCode: Option[Int], - regionName: Option[String] + regionName: Option[String], + isInEuropeanUnion: Boolean ) /** Companion class contains a constructor which takes a MaxMind CityResponse. */ @@ -49,7 +50,8 @@ object model { timezone = Option(cityResponse.getLocation.getTimeZone), postalCode = Option(cityResponse.getPostal.getCode), metroCode = Option(cityResponse.getLocation.getMetroCode).map(_.toInt), - regionName = Option(cityResponse.getMostSpecificSubdivision.getName) + regionName = Option(cityResponse.getMostSpecificSubdivision.getName), + isInEuropeanUnion = cityResponse.getCountry.isInEuropeanUnion ) } diff --git a/src/test/scala/com.snowplowanalytics.maxmind.iplookups/IpLookupsTest.scala b/src/test/scala/com.snowplowanalytics.maxmind.iplookups/IpLookupsTest.scala index 0f61592..8860a9e 100644 --- a/src/test/scala/com.snowplowanalytics.maxmind.iplookups/IpLookupsTest.scala +++ b/src/test/scala/com.snowplowanalytics.maxmind.iplookups/IpLookupsTest.scala @@ -55,7 +55,8 @@ object IpLookupsTest { timezone = Some("Asia/Harbin"), postalCode = None, metroCode = None, - regionName = Some("Jilin Sheng") + regionName = Some("Jilin Sheng"), + isInEuropeanUnion = false ).asRight.some, new AddressNotFoundException("The address 175.16.199.0 is not in the database.").asLeft.some, new AddressNotFoundException("The address 175.16.199.0 is not in the database.").asLeft.some, @@ -73,7 +74,8 @@ object IpLookupsTest { timezone = Some("America/Los_Angeles"), postalCode = Some("98354"), metroCode = Some(819), - regionName = Some("Washington") + regionName = Some("Washington"), + isInEuropeanUnion = false ).asRight.some, "Century Link".asRight.some, "Lariat Software".asRight.some, @@ -91,7 +93,8 @@ object IpLookupsTest { timezone = Some("Asia/Thimphu"), postalCode = None, metroCode = None, - regionName = None + regionName = None, + isInEuropeanUnion = false ).asRight.some, "Loud Packet".asRight.some, "zudoarichikito_".asRight.some, From d54c9d4932e8f7c15bed6e0b6542de6ba25be3f6 Mon Sep 17 00:00:00 2001 From: Mukund Ananthu Date: Tue, 2 Oct 2018 15:07:35 +0530 Subject: [PATCH 02/19] Bump geoip2 to 2.12.0 (closes #93) --- project/Dependencies.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project/Dependencies.scala b/project/Dependencies.scala index d7c3f2f..1beee71 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -13,7 +13,7 @@ import sbt._ object Dependencies { - val maxmind = "com.maxmind.geoip2" % "geoip2" % "2.11.0" + val maxmind = "com.maxmind.geoip2" % "geoip2" % "2.12.0" val catsEffect = "org.typelevel" %% "cats-effect" % "0.10.1" val cats = "org.typelevel" %% "cats-core" % "1.1.0" val lruMap = "com.snowplowanalytics" %% "scala-lru-map" % "0.1.0" From a901b38f23d3ce9750d02fdf6fd0710a26c771fc Mon Sep 17 00:00:00 2001 From: Erik Lilja Date: Tue, 2 Oct 2018 12:14:22 +0200 Subject: [PATCH 03/19] Bump cats-core to 1.6.0 (closes #91) --- project/Dependencies.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project/Dependencies.scala b/project/Dependencies.scala index 1beee71..416ded1 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -15,7 +15,7 @@ import sbt._ object Dependencies { val maxmind = "com.maxmind.geoip2" % "geoip2" % "2.12.0" val catsEffect = "org.typelevel" %% "cats-effect" % "0.10.1" - val cats = "org.typelevel" %% "cats-core" % "1.1.0" + val cats = "org.typelevel" %% "cats-core" % "1.6.0" val lruMap = "com.snowplowanalytics" %% "scala-lru-map" % "0.1.0" val scalaCheck = "org.scalacheck" %% "scalacheck" % "1.14.0" % "test" val specs2 = "org.specs2" %% "specs2-core" % "4.0.3" % "test" From 3146e9947e5562f3b0ba87a46815acc5fc7fd7b1 Mon Sep 17 00:00:00 2001 From: Erik Lilja Date: Tue, 2 Oct 2018 12:15:25 +0200 Subject: [PATCH 04/19] Bump cats-effect to 1.2.0 (closes #90) --- project/Dependencies.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project/Dependencies.scala b/project/Dependencies.scala index 416ded1..966d94f 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -14,7 +14,7 @@ import sbt._ object Dependencies { val maxmind = "com.maxmind.geoip2" % "geoip2" % "2.12.0" - val catsEffect = "org.typelevel" %% "cats-effect" % "0.10.1" + val catsEffect = "org.typelevel" %% "cats-effect" % "1.2.0" val cats = "org.typelevel" %% "cats-core" % "1.6.0" val lruMap = "com.snowplowanalytics" %% "scala-lru-map" % "0.1.0" val scalaCheck = "org.scalacheck" %% "scalacheck" % "1.14.0" % "test" From 4f9a33f923e29e08c5376da551ce23534d65ecd1 Mon Sep 17 00:00:00 2001 From: dorsev Date: Mon, 8 Oct 2018 12:46:05 +0300 Subject: [PATCH 05/19] Bump Scala to 2.12.8 and remove Scala 2.11 support (closes #89) --- .travis.yml | 5 ++--- README.md | 3 +-- build.sbt | 3 +-- 3 files changed, 4 insertions(+), 7 deletions(-) diff --git a/.travis.yml b/.travis.yml index 553dd67..f95ab9f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,6 @@ language: scala scala: -- 2.11.12 -- 2.12.5 +- 2.12.8 jdk: - oraclejdk8 script: @@ -11,7 +10,7 @@ deploy: provider: script script: "./.travis/deploy.sh $TRAVIS_TAG" on: - condition: '"${TRAVIS_SCALA_VERSION}" == "2.12.5"' + condition: '"${TRAVIS_SCALA_VERSION}" == "2.12.8"' tags: true after_success: - bash <(curl -s https://codecov.io/bash) diff --git a/README.md b/README.md index 654c12f..a86d59e 100644 --- a/README.md +++ b/README.md @@ -19,8 +19,7 @@ can also configure an LRU (Least Recently Used) cache of variable size ## Installation -The latest version of scala-maxmind-iplookups is **0.5.0** and is compatible with Scala 2.11 and -2.12. +The latest version of scala-maxmind-iplookups is **0.5.0** and is compatible with Scala 2.12. Add this to your SBT config: diff --git a/build.sbt b/build.sbt index c7bd38e..b8a22a1 100644 --- a/build.sbt +++ b/build.sbt @@ -19,8 +19,7 @@ lazy val root = project name := "scala-maxmind-iplookups", version := "0.5.0", description := "Scala wrapper for MaxMind GeoIP2 library", - scalaVersion := "2.11.12", - crossScalaVersions := Seq("2.11.12", "2.12.5"), + scalaVersion := "2.12.8", scalacOptions := BuildSettings.compilerOptions, javacOptions := BuildSettings.javaCompilerOptions, scalafmtOnCompile := true From efe7828a106291b370454c03090cd41076375552 Mon Sep 17 00:00:00 2001 From: Nitay Kufert Date: Thu, 13 Dec 2018 12:05:41 +0200 Subject: [PATCH 06/19] Remove thread safety warning regarding LRU cache from readme file (closes #99) --- README.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/README.md b/README.md index a86d59e..4d9970d 100644 --- a/README.md +++ b/README.md @@ -196,10 +196,6 @@ println(lookupResult.connectionType) // => Some(Right("Dialup")) We recommend trying different LRU cache sizes to see what works best for you. -Please note that the LRU cache is **not** thread-safe ([see this note][twitter-lru-cache]). Switch -it off if you are thinking about performing ip lookups with the same `IpLookups` instance across -threads. - ## Building etc Assuming you already have SBT installed: From 63e0876d23b2f34c98a7390cd8137945d68b17f4 Mon Sep 17 00:00:00 2001 From: Oskar Kruschitz Date: Fri, 1 Mar 2019 08:32:25 +0100 Subject: [PATCH 07/19] Add support for continent and accuracyRadius to the model (closes #101) --- README.md | 4 +++- .../model.scala | 8 ++++++-- .../IpLookupsTest.scala | 12 +++++++++--- 3 files changed, 18 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 4d9970d..231b299 100644 --- a/README.md +++ b/README.md @@ -153,7 +153,9 @@ case class IpLocation( postalCode: Option[String], metroCode: Option[Int], regionName: Option[String], - isInEuropeanUnion: Boolean + isInEuropeanUnion: Boolean, + continent: String, + accuracyRadius: Int ) ``` diff --git a/src/main/scala/com.snowplowanalytics.maxmind.iplookups/model.scala b/src/main/scala/com.snowplowanalytics.maxmind.iplookups/model.scala index 2efdae9..f1ec3be 100644 --- a/src/main/scala/com.snowplowanalytics.maxmind.iplookups/model.scala +++ b/src/main/scala/com.snowplowanalytics.maxmind.iplookups/model.scala @@ -28,7 +28,9 @@ object model { postalCode: Option[String], metroCode: Option[Int], regionName: Option[String], - isInEuropeanUnion: Boolean + isInEuropeanUnion: Boolean, + continent: String, + accuracyRadius: Int ) /** Companion class contains a constructor which takes a MaxMind CityResponse. */ @@ -51,7 +53,9 @@ object model { postalCode = Option(cityResponse.getPostal.getCode), metroCode = Option(cityResponse.getLocation.getMetroCode).map(_.toInt), regionName = Option(cityResponse.getMostSpecificSubdivision.getName), - isInEuropeanUnion = cityResponse.getCountry.isInEuropeanUnion + isInEuropeanUnion = cityResponse.getCountry.isInEuropeanUnion, + continent = cityResponse.getContinent.getName, + accuracyRadius = cityResponse.getLocation.getAccuracyRadius ) } diff --git a/src/test/scala/com.snowplowanalytics.maxmind.iplookups/IpLookupsTest.scala b/src/test/scala/com.snowplowanalytics.maxmind.iplookups/IpLookupsTest.scala index 8860a9e..36252c0 100644 --- a/src/test/scala/com.snowplowanalytics.maxmind.iplookups/IpLookupsTest.scala +++ b/src/test/scala/com.snowplowanalytics.maxmind.iplookups/IpLookupsTest.scala @@ -56,7 +56,9 @@ object IpLookupsTest { postalCode = None, metroCode = None, regionName = Some("Jilin Sheng"), - isInEuropeanUnion = false + isInEuropeanUnion = false, + continent = "Asia", + accuracyRadius = 100 ).asRight.some, new AddressNotFoundException("The address 175.16.199.0 is not in the database.").asLeft.some, new AddressNotFoundException("The address 175.16.199.0 is not in the database.").asLeft.some, @@ -75,7 +77,9 @@ object IpLookupsTest { postalCode = Some("98354"), metroCode = Some(819), regionName = Some("Washington"), - isInEuropeanUnion = false + isInEuropeanUnion = false, + continent = "North America", + accuracyRadius = 22 ).asRight.some, "Century Link".asRight.some, "Lariat Software".asRight.some, @@ -94,7 +98,9 @@ object IpLookupsTest { postalCode = None, metroCode = None, regionName = None, - isInEuropeanUnion = false + isInEuropeanUnion = false, + continent = "Asia", + accuracyRadius = 534 ).asRight.some, "Loud Packet".asRight.some, "zudoarichikito_".asRight.some, From c1eedc1adfabc3eaf6d20b7105bffd0a5ab57e3d Mon Sep 17 00:00:00 2001 From: Ben Fradet Date: Wed, 6 Mar 2019 10:33:34 +0100 Subject: [PATCH 08/19] Use sbt-tpolecat (closes #102) --- build.sbt | 1 - project/BuildSettings.scala | 16 ---------------- project/plugins.sbt | 3 ++- .../IpLookups.scala | 1 - 4 files changed, 2 insertions(+), 19 deletions(-) diff --git a/build.sbt b/build.sbt index b8a22a1..72ecd2f 100644 --- a/build.sbt +++ b/build.sbt @@ -20,7 +20,6 @@ lazy val root = project version := "0.5.0", description := "Scala wrapper for MaxMind GeoIP2 library", scalaVersion := "2.12.8", - scalacOptions := BuildSettings.compilerOptions, javacOptions := BuildSettings.javaCompilerOptions, scalafmtOnCompile := true ) diff --git a/project/BuildSettings.scala b/project/BuildSettings.scala index 8335e13..188264f 100644 --- a/project/BuildSettings.scala +++ b/project/BuildSettings.scala @@ -27,22 +27,6 @@ import scoverage.ScoverageKeys._ object BuildSettings { - lazy val compilerOptions = Seq( - "-deprecation", - "-encoding", "UTF-8", - "-feature", - "-language:existentials", - "-language:higherKinds", - "-language:implicitConversions", - "-unchecked", - "-Yno-adapted-args", - "-Ywarn-dead-code", - "-Ywarn-numeric-widen", - "-Xfuture", - "-Xlint", - "-Ypartial-unification" - ) - lazy val javaCompilerOptions = Seq( "-source", "1.8", "-target", "1.8" diff --git a/project/plugins.sbt b/project/plugins.sbt index ce57c3d..cdc9271 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -3,4 +3,5 @@ addSbtPlugin("com.lucidchart" % "sbt-scalafmt" % "1.15") addSbtPlugin("com.eed3si9n" % "sbt-unidoc" % "0.4.1") addSbtPlugin("com.typesafe.sbt" % "sbt-site" % "1.3.2") addSbtPlugin("com.typesafe.sbt" % "sbt-ghpages" % "0.6.2") -addSbtPlugin("org.scoverage" % "sbt-scoverage" % "1.5.1") \ No newline at end of file +addSbtPlugin("org.scoverage" % "sbt-scoverage" % "1.5.1") +addSbtPlugin("io.github.davidgregory084" % "sbt-tpolecat" % "0.1.5") diff --git a/src/main/scala/com.snowplowanalytics.maxmind.iplookups/IpLookups.scala b/src/main/scala/com.snowplowanalytics.maxmind.iplookups/IpLookups.scala index d27017f..2103358 100644 --- a/src/main/scala/com.snowplowanalytics.maxmind.iplookups/IpLookups.scala +++ b/src/main/scala/com.snowplowanalytics.maxmind.iplookups/IpLookups.scala @@ -14,7 +14,6 @@ package com.snowplowanalytics.maxmind.iplookups import java.io.File import java.net.InetAddress -import java.util.{Collections, Map} import com.maxmind.db.CHMCache import com.maxmind.geoip2.model.CityResponse From 52aa30a384ed733eab8d0391af36396f3ab70d90 Mon Sep 17 00:00:00 2001 From: Erik Lilja Date: Mon, 1 Oct 2018 20:20:53 +0200 Subject: [PATCH 09/19] Bump scala-lru-map to 0.3.0 (closes #105) --- project/Dependencies.scala | 2 +- .../IpLookups.scala | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/project/Dependencies.scala b/project/Dependencies.scala index 966d94f..7483366 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -16,7 +16,7 @@ object Dependencies { val maxmind = "com.maxmind.geoip2" % "geoip2" % "2.12.0" val catsEffect = "org.typelevel" %% "cats-effect" % "1.2.0" val cats = "org.typelevel" %% "cats-core" % "1.6.0" - val lruMap = "com.snowplowanalytics" %% "scala-lru-map" % "0.1.0" + val lruMap = "com.snowplowanalytics" %% "scala-lru-map" % "0.3.0" val scalaCheck = "org.scalacheck" %% "scalacheck" % "1.14.0" % "test" val specs2 = "org.specs2" %% "specs2-core" % "4.0.3" % "test" } diff --git a/src/main/scala/com.snowplowanalytics.maxmind.iplookups/IpLookups.scala b/src/main/scala/com.snowplowanalytics.maxmind.iplookups/IpLookups.scala index 2103358..0634836 100644 --- a/src/main/scala/com.snowplowanalytics.maxmind.iplookups/IpLookups.scala +++ b/src/main/scala/com.snowplowanalytics.maxmind.iplookups/IpLookups.scala @@ -18,11 +18,12 @@ import java.net.InetAddress import com.maxmind.db.CHMCache import com.maxmind.geoip2.model.CityResponse import com.maxmind.geoip2.DatabaseReader -import com.snowplowanalytics.lrumap.LruMap +import com.snowplowanalytics.lrumap.{CreateLruMap, LruMap} import cats.effect.Sync import cats.syntax.either._ import cats.syntax.flatMap._ import cats.syntax.functor._ +import cats.syntax.option._ import model._ @@ -46,10 +47,10 @@ object IpLookups { connectionTypeFile: Option[File] = None, memCache: Boolean = true, lruCacheSize: Int = 10000 - ): F[IpLookups[F]] = + )(implicit CLM: CreateLruMap[F, String, IpLookupResult]): F[IpLookups[F]] = ( if (lruCacheSize > 0) - Sync[F].map(LruMap.create[F, String, IpLookupResult](lruCacheSize))(Some(_)) + CLM.create(lruCacheSize).map(_.some) else Sync[F].pure(None) ).flatMap((lruCache) => Sync[F].delay { From a6d8cda82a3fa12626f90bed6ca979079d475a5b Mon Sep 17 00:00:00 2001 From: Ben Fradet Date: Wed, 6 Mar 2019 14:11:36 +0100 Subject: [PATCH 10/19] Introduce tagless final encoding (closes #108) --- .../IpAddressResolver.scala | 39 +++++ .../IpLookups.scala | 140 ++++++++++++------ .../SpecializedReader.scala | 58 ++++++-- .../model.scala | 4 + .../IpLookupsTest.scala | 70 ++++++--- 5 files changed, 237 insertions(+), 74 deletions(-) create mode 100644 src/main/scala/com.snowplowanalytics.maxmind.iplookups/IpAddressResolver.scala diff --git a/src/main/scala/com.snowplowanalytics.maxmind.iplookups/IpAddressResolver.scala b/src/main/scala/com.snowplowanalytics.maxmind.iplookups/IpAddressResolver.scala new file mode 100644 index 0000000..16ac732 --- /dev/null +++ b/src/main/scala/com.snowplowanalytics.maxmind.iplookups/IpAddressResolver.scala @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2012-2018 Snowplow Analytics Ltd. All rights reserved. + * + * This program is licensed to you under the Apache License Version 2.0, + * and you may not use this file except in compliance with the Apache License Version 2.0. + * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Apache License Version 2.0 is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + */ +package com.snowplowanalytics.maxmind.iplookups + +import java.net.InetAddress + +import cats.Eval +import cats.effect.Sync +import cats.syntax.either._ + +/** Data type letting you resolve IP address */ +sealed trait IpAddressResolver[F[_]] { + def resolve(ip: String): F[Either[Throwable, InetAddress]] + + protected def getIpAddress(ip: String): Either[Throwable, InetAddress] = + Either.catchNonFatal(InetAddress.getByName(ip)) +} + +object IpAddressResolver { + implicit def ipAddressResolver[F[_]: Sync]: IpAddressResolver[F] = new IpAddressResolver[F] { + def resolve(ip: String): F[Either[Throwable, InetAddress]] = + Sync[F].delay { getIpAddress(ip) } + } + + implicit def evalIpAddressResolver: IpAddressResolver[Eval] = new IpAddressResolver[Eval] { + def resolve(ip: String): Eval[Either[Throwable, InetAddress]] = + Eval.later { getIpAddress(ip) } + } +} diff --git a/src/main/scala/com.snowplowanalytics.maxmind.iplookups/IpLookups.scala b/src/main/scala/com.snowplowanalytics.maxmind.iplookups/IpLookups.scala index 0634836..07b4a93 100644 --- a/src/main/scala/com.snowplowanalytics.maxmind.iplookups/IpLookups.scala +++ b/src/main/scala/com.snowplowanalytics.maxmind.iplookups/IpLookups.scala @@ -15,15 +15,14 @@ package com.snowplowanalytics.maxmind.iplookups import java.io.File import java.net.InetAddress -import com.maxmind.db.CHMCache -import com.maxmind.geoip2.model.CityResponse -import com.maxmind.geoip2.DatabaseReader -import com.snowplowanalytics.lrumap.{CreateLruMap, LruMap} +import cats.{Eval, Monad} import cats.effect.Sync -import cats.syntax.either._ import cats.syntax.flatMap._ import cats.syntax.functor._ import cats.syntax.option._ +import com.maxmind.db.CHMCache +import com.maxmind.geoip2.DatabaseReader +import com.snowplowanalytics.lrumap.{CreateLruMap, LruMap} import model._ @@ -32,7 +31,6 @@ object IpLookups { /** * Create an IpLookups from Files - * * @param geoFile Geographic lookup database file * @param ispFile ISP lookup database file * @param domainFile Domain lookup database file @@ -49,10 +47,12 @@ object IpLookups { lruCacheSize: Int = 10000 )(implicit CLM: CreateLruMap[F, String, IpLookupResult]): F[IpLookups[F]] = ( - if (lruCacheSize > 0) + if (lruCacheSize > 0) { CLM.create(lruCacheSize).map(_.some) - else Sync[F].pure(None) - ).flatMap((lruCache) => + } else { + Sync[F].pure(None) + } + ).flatMap { lruCache => Sync[F].delay { new IpLookups( geoFile, @@ -62,11 +62,47 @@ object IpLookups { memCache, lruCache ) - }) + } + } /** - * Alternative constructor taking Strings rather than Files - * + * Create an unsafe IpLookups from Files + * @param geoFile Geographic lookup database file + * @param ispFile ISP lookup database file + * @param domainFile Domain lookup database file + * @param connectionTypeFile Connection type lookup database file + * @param memCache Whether to use MaxMind's CHMCache + * @param lruCacheSize Maximum size of LruMap cache + */ + def unsafeCreateFromFiles( + geoFile: Option[File] = None, + ispFile: Option[File] = None, + domainFile: Option[File] = None, + connectionTypeFile: Option[File] = None, + memCache: Boolean = true, + lruCacheSize: Int = 10000 + )(implicit CLM: CreateLruMap[Eval, String, IpLookupResult]): Eval[IpLookups[Eval]] = + ( + if (lruCacheSize > 0) { + CLM.create(lruCacheSize).map(_.some) + } else { + Eval.now(None) + } + ).flatMap { lruCache => + Eval.later { + new IpLookups( + geoFile, + ispFile, + domainFile, + connectionTypeFile, + memCache, + lruCache + ) + } + } + + /** + * Alternative constructor taking filenames rather than Files * @param geoFile Geographic lookup database filepath * @param ispFile ISP lookup database filepath * @param domainFile Domain lookup database filepath @@ -90,37 +126,62 @@ object IpLookups { memCache, lruCacheSize ) + + /** + * Alternative unsafe constructor taking filenames rather than Files + * @param geoFile Geographic lookup database filepath + * @param ispFile ISP lookup database filepath + * @param domainFile Domain lookup database filepath + * @param connectionTypeFile Connection type lookup database filepath + * @param memCache Whether to use MaxMind's CHMCache + * @param lruCacheSize Maximum size of LruMap cache + */ + def unsafeCreateFromFilenames( + geoFile: Option[String] = None, + ispFile: Option[String] = None, + domainFile: Option[String] = None, + connectionTypeFile: Option[String] = None, + memCache: Boolean = true, + lruCacheSize: Int = 10000 + ): Eval[IpLookups[Eval]] = + IpLookups.unsafeCreateFromFiles( + geoFile.map(new File(_)), + ispFile.map(new File(_)), + domainFile.map(new File(_)), + connectionTypeFile.map(new File(_)), + memCache, + lruCacheSize + ) } /** * IpLookups is a Scala wrapper around MaxMind's own DatabaseReader Java class. - * * Two main differences: - * * 1. getLocation(ipS: String) now returns an IpLocation * case class, not a raw MaxMind Location * 2. IpLookups introduces an LRU cache to improve * lookup performance - * * Inspired by: * https://github.com/jt6211/hadoop-dns-mining/blob/master/src/main/java/io/covert/dns/geo/IpLookups.java */ -class IpLookups[F[_]: Sync] private ( +class IpLookups[F[_]: Monad] private ( geoFile: Option[File], ispFile: Option[File], domainFile: Option[File], connectionTypeFile: Option[File], memCache: Boolean, lru: Option[LruMap[F, String, IpLookupResult]] -) { +)( + implicit + SR: SpecializedReader[F], + IAR: IpAddressResolver[F]) { // Configure the lookup services - private val geoService = getService(geoFile) - private val ispService = getService(ispFile).map(SpecializedReader(_, ReaderFunctions.isp)) - private val orgService = getService(ispFile).map(SpecializedReader(_, ReaderFunctions.org)) - private val domainService = - getService(domainFile).map(SpecializedReader(_, ReaderFunctions.domain)) + private val geoService = getService(geoFile) + private val ispService = getService(ispFile).map((_, ReaderFunctions.isp)) + private val orgService = getService(ispFile).map((_, ReaderFunctions.org)) + private val domainService = getService(domainFile).map((_, ReaderFunctions.domain)) private val connectionTypeService = - getService(connectionTypeFile).map(SpecializedReader(_, ReaderFunctions.connectionType)) + getService(connectionTypeFile).map((_, ReaderFunctions.connectionType)) /** * Get a LookupService from a database file @@ -144,15 +205,15 @@ class IpLookups[F[_]: Sync] private ( */ private def getLookup( ipAddress: Either[Throwable, InetAddress], - service: Option[SpecializedReader] + service: Option[(DatabaseReader, ReaderFunction)] ): F[Option[Either[Throwable, String]]] = (ipAddress, service) match { - case (Right(ipA), Some(svc)) => - Sync[F].map(svc.getValue(ipA))(Some(_)) + case (Right(ipA), Some((db, f))) => + SR.getValue(f, db, ipA).map(_.some) case (Left(f), _) => - Sync[F].pure(Some(Left(f))) + Monad[F].pure(Some(Left(f))) case _ => - Sync[F].pure(None) + Monad[F].pure(None) } /** @@ -169,11 +230,10 @@ class IpLookups[F[_]: Sync] private ( ipAddress: Either[Throwable, InetAddress] ): F[Option[Either[Throwable, IpLocation]]] = (ipAddress, geoService) match { case (Right(ipA), Some(gs)) => - Sync[F].map(getCityResponse(gs, ipA))( - (loc) => Some(loc.map(IpLocation(_))) - ) - case (Left(f), _) => Sync[F].pure(Some(Left(f))) - case _ => Sync[F].pure(None) + SR.getCityValue(gs, ipA) + .map(loc => loc.map(IpLocation(_)).some) + case (Left(f), _) => Monad[F].pure(Some(Left(f))) + case _ => Monad[F].pure(None) } /** @@ -188,7 +248,7 @@ class IpLookups[F[_]: Sync] private ( */ private def performLookupsWithoutLruCache(ip: String): F[IpLookupResult] = for { - ipAddress <- getIpAddress(ip) + ipAddress <- IAR.resolve(ip) ipLocation <- getLocationLookup(ipAddress) isp <- getLookup(ipAddress, ispService) @@ -219,17 +279,7 @@ class IpLookups[F[_]: Sync] private ( lru .get(ip) - .map(_.map(Sync[F].pure(_))) + .map(_.map(Monad[F].pure(_))) .flatMap(_.getOrElse(lookupAndCache)) } - - /** Transforms a String into an Either[Throwable, InetAddress] */ - private def getIpAddress(ip: String): F[Either[Throwable, InetAddress]] = - Sync[F].delay { Either.catchNonFatal(InetAddress.getByName(ip)) } - - private def getCityResponse( - gs: DatabaseReader, - ipAddress: InetAddress - ): F[Either[Throwable, CityResponse]] = - Sync[F].delay { Either.catchNonFatal(gs.city(ipAddress)) } } diff --git a/src/main/scala/com.snowplowanalytics.maxmind.iplookups/SpecializedReader.scala b/src/main/scala/com.snowplowanalytics.maxmind.iplookups/SpecializedReader.scala index 7255de6..aa3ec3f 100644 --- a/src/main/scala/com.snowplowanalytics.maxmind.iplookups/SpecializedReader.scala +++ b/src/main/scala/com.snowplowanalytics.maxmind.iplookups/SpecializedReader.scala @@ -14,21 +14,61 @@ package com.snowplowanalytics.maxmind.iplookups import java.net.InetAddress -import com.maxmind.geoip2.DatabaseReader -import cats.syntax.either._ - -import com.snowplowanalytics.maxmind.iplookups.ReaderFunctions.ReaderFunction +import cats.Eval import cats.effect.Sync import cats.syntax.either._ +import com.maxmind.geoip2.DatabaseReader +import com.maxmind.geoip2.model.CityResponse + +import model._ + +/** Data type letting you read data in maxmind's DatabaseReader. */ +sealed trait SpecializedReader[F[_]] { + def getValue( + f: ReaderFunction, + db: DatabaseReader, + ip: InetAddress + ): F[Either[Throwable, String]] -final case class SpecializedReader(db: DatabaseReader, f: ReaderFunction) { - def getValue[F[_]: Sync](ip: InetAddress): F[Either[Throwable, String]] = - Sync[F].delay { Either.catchNonFatal(f(db, ip)) } + def getCityValue( + db: DatabaseReader, + ip: InetAddress + ): F[Either[Throwable, CityResponse]] } -object ReaderFunctions { - type ReaderFunction = (DatabaseReader, InetAddress) => String +object SpecializedReader { + implicit def specializedReader[F[_]: Sync]: SpecializedReader[F] = new SpecializedReader[F] { + def getValue( + f: ReaderFunction, + db: DatabaseReader, + ip: InetAddress + ): F[Either[Throwable, String]] = + Sync[F].delay { Either.catchNonFatal(f(db, ip)) } + + def getCityValue( + db: DatabaseReader, + ip: InetAddress + ): F[Either[Throwable, CityResponse]] = + Sync[F].delay { Either.catchNonFatal(db.city(ip)) } + } + + implicit def evalSpecializedReader: SpecializedReader[Eval] = new SpecializedReader[Eval] { + def getValue( + f: ReaderFunction, + db: DatabaseReader, + ip: InetAddress + ): Eval[Either[Throwable, String]] = + Eval.later { Either.catchNonFatal(f(db, ip)) } + def getCityValue( + db: DatabaseReader, + ip: InetAddress + ): Eval[Either[Throwable, CityResponse]] = + Eval.later { Either.catchNonFatal(db.city(ip)) } + } +} + +object ReaderFunctions { val isp = (db: DatabaseReader, ip: InetAddress) => db.isp(ip).getIsp val org = (db: DatabaseReader, ip: InetAddress) => db.isp(ip).getOrganization val domain = (db: DatabaseReader, ip: InetAddress) => db.domain(ip).getDomain diff --git a/src/main/scala/com.snowplowanalytics.maxmind.iplookups/model.scala b/src/main/scala/com.snowplowanalytics.maxmind.iplookups/model.scala index f1ec3be..6a62df8 100644 --- a/src/main/scala/com.snowplowanalytics.maxmind.iplookups/model.scala +++ b/src/main/scala/com.snowplowanalytics.maxmind.iplookups/model.scala @@ -12,9 +12,13 @@ */ package com.snowplowanalytics.maxmind.iplookups +import java.net.InetAddress + +import com.maxmind.geoip2.DatabaseReader import com.maxmind.geoip2.model.CityResponse object model { + type ReaderFunction = (DatabaseReader, InetAddress) => String /** A case class wrapper around the MaxMind CityResponse class. */ final case class IpLocation( diff --git a/src/test/scala/com.snowplowanalytics.maxmind.iplookups/IpLookupsTest.scala b/src/test/scala/com.snowplowanalytics.maxmind.iplookups/IpLookupsTest.scala index 36252c0..6d292e1 100644 --- a/src/test/scala/com.snowplowanalytics.maxmind.iplookups/IpLookupsTest.scala +++ b/src/test/scala/com.snowplowanalytics.maxmind.iplookups/IpLookupsTest.scala @@ -14,23 +14,23 @@ package com.snowplowanalytics.maxmind.iplookups import java.net.UnknownHostException +import cats.Eval +import cats.effect.IO +import cats.syntax.either._ +import cats.syntax.option._ import com.maxmind.geoip2.exception.AddressNotFoundException import org.specs2.mutable.Specification import org.specs2.specification.Tables -import cats.syntax.either._ -import cats.syntax.option._ -import cats.effect.IO import model._ object IpLookupsTest { + val geoFile = getClass.getResource("GeoIP2-City-Test.mmdb").getFile + val ispFile = getClass.getResource("GeoIP2-ISP-Test.mmdb").getFile + val domainFile = getClass.getResource("GeoIP2-Domain-Test.mmdb").getFile + val connectionTypeFile = getClass.getResource("GeoIP2-Connection-Type-Test.mmdb").getFile - def ipLookupsFromFiles(memCache: Boolean, lruCache: Int): IpLookups[IO] = { - val geoFile = getClass.getResource("GeoIP2-City-Test.mmdb").getFile - val ispFile = getClass.getResource("GeoIP2-ISP-Test.mmdb").getFile - val domainFile = getClass.getResource("GeoIP2-Domain-Test.mmdb").getFile - val connectionTypeFile = getClass.getResource("GeoIP2-Connection-Type-Test.mmdb").getFile - + def ioIpLookupsFromFiles(memCache: Boolean, lruCache: Int): IpLookups[IO] = IpLookups .createFromFilenames[IO]( Some(geoFile), @@ -38,9 +38,21 @@ object IpLookupsTest { Some(domainFile), Some(connectionTypeFile), memCache, - lruCache) + lruCache + ) .unsafeRunSync - } + + def evalIpLookupsFromFiles(memCache: Boolean, lruCache: Int): IpLookups[Eval] = + IpLookups + .unsafeCreateFromFilenames( + Some(geoFile), + Some(ispFile), + Some(domainFile), + Some(connectionTypeFile), + memCache, + lruCache + ) + .value // Databases and test data taken from https://github.com/maxmind/MaxMind-DB/tree/master/test-data val testData: Map[String, IpLookupResult] = Map( @@ -140,37 +152,55 @@ class IpLookupsTest extends Specification with Tables { lruCache <- Seq(0, 1000, 10000) } { - val ipLookups = ipLookupsFromFiles(memCache, lruCache) + val ioIpLookups = ioIpLookupsFromFiles(memCache, lruCache) + val evalIpLookups = evalIpLookupsFromFiles(memCache, lruCache) testData foreach { case (ip, expected) => formatter(ip, memCache, lruCache) should { - val actual = ipLookups.performLookups(ip).unsafeRunSync - matchIpLookupResult(actual, expected) + val ioActual = ioIpLookups.performLookups(ip).unsafeRunSync + val evalActual = evalIpLookups.performLookups(ip).value + matchIpLookupResult(ioActual, expected) + matchIpLookupResult(evalActual, expected) } } } "providing an invalid ip should fail" in { - val ipLookups = ipLookupsFromFiles(true, 0) - val expected = IpLookupResult( + val ioIpLookups = ioIpLookupsFromFiles(true, 0) + val evalIpLookups = evalIpLookupsFromFiles(true, 0) + val ioExpected = IpLookupResult( new UnknownHostException("not: Name or service not known").asLeft.some, new UnknownHostException("not: Name or service not known").asLeft.some, new UnknownHostException("not: Name or service not known").asLeft.some, new UnknownHostException("not: Name or service not known").asLeft.some, new UnknownHostException("not: Name or service not known").asLeft.some ) - val actual = ipLookups.performLookups("not").unsafeRunSync - matchIpLookupResult(actual, expected) + val evalExpected = IpLookupResult( + new UnknownHostException("not").asLeft.some, + new UnknownHostException("not").asLeft.some, + new UnknownHostException("not").asLeft.some, + new UnknownHostException("not").asLeft.some, + new UnknownHostException("not").asLeft.some + ) + val ioActual = ioIpLookups.performLookups("not").unsafeRunSync + val evalActual = evalIpLookups.performLookups("not").value + matchIpLookupResult(ioActual, ioExpected) + matchIpLookupResult(evalActual, evalExpected) } "providing no files should return Nones" in { - val actual = (for { + val ioActual = (for { ipLookups <- IpLookups.createFromFiles[IO](None, None, None, None, true, 0) res <- ipLookups.performLookups("67.43.156.0") } yield res).unsafeRunSync + val evalActual = (for { + ipLookups <- IpLookups.unsafeCreateFromFiles(None, None, None, None, true, 0) + res <- ipLookups.performLookups("67.43.156.0") + } yield res).value val expected = IpLookupResult(None, None, None, None, None) - matchIpLookupResult(actual, expected) + matchIpLookupResult(ioActual, expected) + matchIpLookupResult(evalActual, expected) } } From 154dbb7b1a5e1d3dd3e9a5a35fcbe9518890c112 Mon Sep 17 00:00:00 2001 From: Dilyan Damyanov Date: Tue, 19 Nov 2019 12:02:24 +0100 Subject: [PATCH 11/19] Bump SBT to 1.2.8 (closes #109) Co-authored-by: Ben Fradet --- project/Dependencies.scala | 4 ++-- project/build.properties | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/project/Dependencies.scala b/project/Dependencies.scala index 7483366..976b7c5 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -17,6 +17,6 @@ object Dependencies { val catsEffect = "org.typelevel" %% "cats-effect" % "1.2.0" val cats = "org.typelevel" %% "cats-core" % "1.6.0" val lruMap = "com.snowplowanalytics" %% "scala-lru-map" % "0.3.0" - val scalaCheck = "org.scalacheck" %% "scalacheck" % "1.14.0" % "test" - val specs2 = "org.specs2" %% "specs2-core" % "4.0.3" % "test" + val scalaCheck = "org.scalacheck" %% "scalacheck" % "1.14.0" % Test + val specs2 = "org.specs2" %% "specs2-core" % "4.0.3" % Test } diff --git a/project/build.properties b/project/build.properties index 31334bb..c0bab04 100644 --- a/project/build.properties +++ b/project/build.properties @@ -1 +1 @@ -sbt.version=1.1.1 +sbt.version=1.2.8 From 03da53e286df85921bda881bd6f16e2767610037 Mon Sep 17 00:00:00 2001 From: Ben Fradet Date: Wed, 6 Mar 2019 14:21:23 +0100 Subject: [PATCH 12/19] Bump specs2-core to 4.4.1 (closes #110) --- project/Dependencies.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project/Dependencies.scala b/project/Dependencies.scala index 976b7c5..2e2c3a6 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -18,5 +18,5 @@ object Dependencies { val cats = "org.typelevel" %% "cats-core" % "1.6.0" val lruMap = "com.snowplowanalytics" %% "scala-lru-map" % "0.3.0" val scalaCheck = "org.scalacheck" %% "scalacheck" % "1.14.0" % Test - val specs2 = "org.specs2" %% "specs2-core" % "4.0.3" % Test + val specs2 = "org.specs2" %% "specs2-core" % "4.4.1" % Test } From 75593c2aa8e9db0658255431af7567e487c7ce97 Mon Sep 17 00:00:00 2001 From: Ben Fradet Date: Wed, 6 Mar 2019 14:23:28 +0100 Subject: [PATCH 13/19] Extend copyright to 2019 (closes #111) --- LICENSE-2.0.txt | 2 +- README.md | 2 +- build.sbt | 2 +- project/BuildSettings.scala | 2 +- project/Dependencies.scala | 2 +- .../IpAddressResolver.scala | 2 +- .../com.snowplowanalytics.maxmind.iplookups/IpLookups.scala | 2 +- .../SpecializedReader.scala | 2 +- .../scala/com.snowplowanalytics.maxmind.iplookups/model.scala | 2 +- .../com.snowplowanalytics.maxmind.iplookups/IpLookupsTest.scala | 2 +- 10 files changed, 10 insertions(+), 10 deletions(-) diff --git a/LICENSE-2.0.txt b/LICENSE-2.0.txt index abf5f15..78b4d79 100644 --- a/LICENSE-2.0.txt +++ b/LICENSE-2.0.txt @@ -187,7 +187,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright 2012 Snowplow Analytics Ltd. + Copyright 2012-2019 Snowplow Analytics Ltd. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/README.md b/README.md index 231b299..453f5df 100644 --- a/README.md +++ b/README.md @@ -223,7 +223,7 @@ As such we recommend upgrading to version 0.4.0 as soon as possible ## Copyright and license -Copyright 2012-2018 Snowplow Analytics Ltd. +Copyright 2012-2019 Snowplow Analytics Ltd. Licensed under the [Apache License, Version 2.0][license] (the "License"); you may not use this software except in compliance with the License. diff --git a/build.sbt b/build.sbt index 72ecd2f..11bc94c 100644 --- a/build.sbt +++ b/build.sbt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012-2018 Snowplow Analytics Ltd. All rights reserved. + * Copyright (c) 2012-2019 Snowplow Analytics Ltd. All rights reserved. * * This program is licensed to you under the Apache License Version 2.0, * and you may not use this file except in compliance with the Apache License Version 2.0. diff --git a/project/BuildSettings.scala b/project/BuildSettings.scala index 188264f..42e61d7 100644 --- a/project/BuildSettings.scala +++ b/project/BuildSettings.scala @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012-2018 Snowplow Analytics Ltd. All rights reserved. + * Copyright (c) 2012-2019 Snowplow Analytics Ltd. All rights reserved. * * This program is licensed to you under the Apache License Version 2.0, * and you may not use this file except in compliance with the Apache License Version 2.0. diff --git a/project/Dependencies.scala b/project/Dependencies.scala index 2e2c3a6..8cc1f56 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012-2018 Snowplow Analytics Ltd. All rights reserved. + * Copyright (c) 2012-2019 Snowplow Analytics Ltd. All rights reserved. * * This program is licensed to you under the Apache License Version 2.0, * and you may not use this file except in compliance with the Apache License Version 2.0. diff --git a/src/main/scala/com.snowplowanalytics.maxmind.iplookups/IpAddressResolver.scala b/src/main/scala/com.snowplowanalytics.maxmind.iplookups/IpAddressResolver.scala index 16ac732..6e46183 100644 --- a/src/main/scala/com.snowplowanalytics.maxmind.iplookups/IpAddressResolver.scala +++ b/src/main/scala/com.snowplowanalytics.maxmind.iplookups/IpAddressResolver.scala @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012-2018 Snowplow Analytics Ltd. All rights reserved. + * Copyright (c) 2012-2019 Snowplow Analytics Ltd. All rights reserved. * * This program is licensed to you under the Apache License Version 2.0, * and you may not use this file except in compliance with the Apache License Version 2.0. diff --git a/src/main/scala/com.snowplowanalytics.maxmind.iplookups/IpLookups.scala b/src/main/scala/com.snowplowanalytics.maxmind.iplookups/IpLookups.scala index 07b4a93..70b6912 100644 --- a/src/main/scala/com.snowplowanalytics.maxmind.iplookups/IpLookups.scala +++ b/src/main/scala/com.snowplowanalytics.maxmind.iplookups/IpLookups.scala @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012-2018 Snowplow Analytics Ltd. All rights reserved. + * Copyright (c) 2012-2019 Snowplow Analytics Ltd. All rights reserved. * * This program is licensed to you under the Apache License Version 2.0, * and you may not use this file except in compliance with the Apache License Version 2.0. diff --git a/src/main/scala/com.snowplowanalytics.maxmind.iplookups/SpecializedReader.scala b/src/main/scala/com.snowplowanalytics.maxmind.iplookups/SpecializedReader.scala index aa3ec3f..ef6d27b 100644 --- a/src/main/scala/com.snowplowanalytics.maxmind.iplookups/SpecializedReader.scala +++ b/src/main/scala/com.snowplowanalytics.maxmind.iplookups/SpecializedReader.scala @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012-2018 Snowplow Analytics Ltd. All rights reserved. + * Copyright (c) 2012-2019 Snowplow Analytics Ltd. All rights reserved. * * This program is licensed to you under the Apache License Version 2.0, * and you may not use this file except in compliance with the Apache License Version 2.0. diff --git a/src/main/scala/com.snowplowanalytics.maxmind.iplookups/model.scala b/src/main/scala/com.snowplowanalytics.maxmind.iplookups/model.scala index 6a62df8..0576ac7 100644 --- a/src/main/scala/com.snowplowanalytics.maxmind.iplookups/model.scala +++ b/src/main/scala/com.snowplowanalytics.maxmind.iplookups/model.scala @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012-2018 Snowplow Analytics Ltd. All rights reserved. + * Copyright (c) 2012-2019 Snowplow Analytics Ltd. All rights reserved. * * This program is licensed to you under the Apache License Version 2.0, * and you may not use this file except in compliance with the Apache License Version 2.0. diff --git a/src/test/scala/com.snowplowanalytics.maxmind.iplookups/IpLookupsTest.scala b/src/test/scala/com.snowplowanalytics.maxmind.iplookups/IpLookupsTest.scala index 6d292e1..2281750 100644 --- a/src/test/scala/com.snowplowanalytics.maxmind.iplookups/IpLookupsTest.scala +++ b/src/test/scala/com.snowplowanalytics.maxmind.iplookups/IpLookupsTest.scala @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012-2018 Snowplow Analytics Ltd. All rights reserved. + * Copyright (c) 2012-2019 Snowplow Analytics Ltd. All rights reserved. * * This program is licensed to you under the Apache License Version 2.0, * and you may not use this file except in compliance with the Apache License Version 2.0. From 446423db2d297c5a4e9955920deafae61e461e62 Mon Sep 17 00:00:00 2001 From: Ben Fradet Date: Mon, 22 Apr 2019 15:39:01 +0200 Subject: [PATCH 14/19] Abstract over IpLookups creation (closes #113) --- .../IpLookups.scala | 174 ++++++++---------- .../IpLookupsTest.scala | 12 +- 2 files changed, 85 insertions(+), 101 deletions(-) diff --git a/src/main/scala/com.snowplowanalytics.maxmind.iplookups/IpLookups.scala b/src/main/scala/com.snowplowanalytics.maxmind.iplookups/IpLookups.scala index 70b6912..954a81e 100644 --- a/src/main/scala/com.snowplowanalytics.maxmind.iplookups/IpLookups.scala +++ b/src/main/scala/com.snowplowanalytics.maxmind.iplookups/IpLookups.scala @@ -26,8 +26,7 @@ import com.snowplowanalytics.lrumap.{CreateLruMap, LruMap} import model._ -/** Companion object to hold alternative constructors. */ -object IpLookups { +trait CreateIpLookups[F[_]] { /** * Create an IpLookups from Files @@ -38,71 +37,17 @@ object IpLookups { * @param memCache Whether to use MaxMind's CHMCache * @param lruCacheSize Maximum size of LruMap cache */ - def createFromFiles[F[_]: Sync]( + def createFromFiles( geoFile: Option[File] = None, ispFile: Option[File] = None, domainFile: Option[File] = None, connectionTypeFile: Option[File] = None, memCache: Boolean = true, lruCacheSize: Int = 10000 - )(implicit CLM: CreateLruMap[F, String, IpLookupResult]): F[IpLookups[F]] = - ( - if (lruCacheSize > 0) { - CLM.create(lruCacheSize).map(_.some) - } else { - Sync[F].pure(None) - } - ).flatMap { lruCache => - Sync[F].delay { - new IpLookups( - geoFile, - ispFile, - domainFile, - connectionTypeFile, - memCache, - lruCache - ) - } - } - - /** - * Create an unsafe IpLookups from Files - * @param geoFile Geographic lookup database file - * @param ispFile ISP lookup database file - * @param domainFile Domain lookup database file - * @param connectionTypeFile Connection type lookup database file - * @param memCache Whether to use MaxMind's CHMCache - * @param lruCacheSize Maximum size of LruMap cache - */ - def unsafeCreateFromFiles( - geoFile: Option[File] = None, - ispFile: Option[File] = None, - domainFile: Option[File] = None, - connectionTypeFile: Option[File] = None, - memCache: Boolean = true, - lruCacheSize: Int = 10000 - )(implicit CLM: CreateLruMap[Eval, String, IpLookupResult]): Eval[IpLookups[Eval]] = - ( - if (lruCacheSize > 0) { - CLM.create(lruCacheSize).map(_.some) - } else { - Eval.now(None) - } - ).flatMap { lruCache => - Eval.later { - new IpLookups( - geoFile, - ispFile, - domainFile, - connectionTypeFile, - memCache, - lruCache - ) - } - } + ): F[IpLookups[F]] /** - * Alternative constructor taking filenames rather than Files + * Alternative constructor taking filenames rather than files * @param geoFile Geographic lookup database filepath * @param ispFile ISP lookup database filepath * @param domainFile Domain lookup database filepath @@ -110,48 +55,87 @@ object IpLookups { * @param memCache Whether to use MaxMind's CHMCache * @param lruCacheSize Maximum size of LruMap cache */ - def createFromFilenames[F[_]: Sync]( + def createFromFilenames( geoFile: Option[String] = None, ispFile: Option[String] = None, domainFile: Option[String] = None, connectionTypeFile: Option[String] = None, memCache: Boolean = true, lruCacheSize: Int = 10000 - ): F[IpLookups[F]] = - IpLookups.createFromFiles( - geoFile.map(new File(_)), - ispFile.map(new File(_)), - domainFile.map(new File(_)), - connectionTypeFile.map(new File(_)), - memCache, - lruCacheSize - ) + ): F[IpLookups[F]] = createFromFiles( + geoFile.map(new File(_)), + ispFile.map(new File(_)), + domainFile.map(new File(_)), + connectionTypeFile.map(new File(_)), + memCache, + lruCacheSize + ) +} - /** - * Alternative unsafe constructor taking filenames rather than Files - * @param geoFile Geographic lookup database filepath - * @param ispFile ISP lookup database filepath - * @param domainFile Domain lookup database filepath - * @param connectionTypeFile Connection type lookup database filepath - * @param memCache Whether to use MaxMind's CHMCache - * @param lruCacheSize Maximum size of LruMap cache - */ - def unsafeCreateFromFilenames( - geoFile: Option[String] = None, - ispFile: Option[String] = None, - domainFile: Option[String] = None, - connectionTypeFile: Option[String] = None, - memCache: Boolean = true, - lruCacheSize: Int = 10000 - ): Eval[IpLookups[Eval]] = - IpLookups.unsafeCreateFromFiles( - geoFile.map(new File(_)), - ispFile.map(new File(_)), - domainFile.map(new File(_)), - connectionTypeFile.map(new File(_)), - memCache, - lruCacheSize - ) +object CreateIpLookups { + def apply[F[_]](implicit ev: CreateIpLookups[F]): CreateIpLookups[F] = ev + + implicit def syncCreateIpLookups[F[_]: Sync]( + implicit CLM: CreateLruMap[F, String, IpLookupResult] + ): CreateIpLookups[F] = new CreateIpLookups[F] { + override def createFromFiles( + geoFile: Option[File] = None, + ispFile: Option[File] = None, + domainFile: Option[File] = None, + connectionTypeFile: Option[File] = None, + memCache: Boolean = true, + lruCacheSize: Int = 10000 + ): F[IpLookups[F]] = + ( + if (lruCacheSize > 0) { + CLM.create(lruCacheSize).map(_.some) + } else { + Sync[F].pure(None) + } + ).flatMap { lruCache => + Sync[F].delay { + new IpLookups( + geoFile, + ispFile, + domainFile, + connectionTypeFile, + memCache, + lruCache + ) + } + } + } + + implicit def evalCreateIpLookups( + implicit CLM: CreateLruMap[Eval, String, IpLookupResult] + ): CreateIpLookups[Eval] = new CreateIpLookups[Eval] { + override def createFromFiles( + geoFile: Option[File] = None, + ispFile: Option[File] = None, + domainFile: Option[File] = None, + connectionTypeFile: Option[File] = None, + memCache: Boolean = true, + lruCacheSize: Int = 10000 + ): Eval[IpLookups[Eval]] = + ( + if (lruCacheSize > 0) { + CLM.create(lruCacheSize).map(_.some) + } else { + Eval.now(None) + } + ).flatMap { lruCache => + Eval.later { + new IpLookups( + geoFile, + ispFile, + domainFile, + connectionTypeFile, + memCache, + lruCache + ) + } + } + } } /** @@ -164,7 +148,7 @@ object IpLookups { * Inspired by: * https://github.com/jt6211/hadoop-dns-mining/blob/master/src/main/java/io/covert/dns/geo/IpLookups.java */ -class IpLookups[F[_]: Monad] private ( +class IpLookups[F[_]: Monad] private[iplookups] ( geoFile: Option[File], ispFile: Option[File], domainFile: Option[File], diff --git a/src/test/scala/com.snowplowanalytics.maxmind.iplookups/IpLookupsTest.scala b/src/test/scala/com.snowplowanalytics.maxmind.iplookups/IpLookupsTest.scala index 2281750..c585fc2 100644 --- a/src/test/scala/com.snowplowanalytics.maxmind.iplookups/IpLookupsTest.scala +++ b/src/test/scala/com.snowplowanalytics.maxmind.iplookups/IpLookupsTest.scala @@ -31,8 +31,8 @@ object IpLookupsTest { val connectionTypeFile = getClass.getResource("GeoIP2-Connection-Type-Test.mmdb").getFile def ioIpLookupsFromFiles(memCache: Boolean, lruCache: Int): IpLookups[IO] = - IpLookups - .createFromFilenames[IO]( + CreateIpLookups[IO] + .createFromFilenames( Some(geoFile), Some(ispFile), Some(domainFile), @@ -43,8 +43,8 @@ object IpLookupsTest { .unsafeRunSync def evalIpLookupsFromFiles(memCache: Boolean, lruCache: Int): IpLookups[Eval] = - IpLookups - .unsafeCreateFromFilenames( + CreateIpLookups[Eval] + .createFromFilenames( Some(geoFile), Some(ispFile), Some(domainFile), @@ -191,11 +191,11 @@ class IpLookupsTest extends Specification with Tables { "providing no files should return Nones" in { val ioActual = (for { - ipLookups <- IpLookups.createFromFiles[IO](None, None, None, None, true, 0) + ipLookups <- CreateIpLookups[IO].createFromFiles(None, None, None, None, true, 0) res <- ipLookups.performLookups("67.43.156.0") } yield res).unsafeRunSync val evalActual = (for { - ipLookups <- IpLookups.unsafeCreateFromFiles(None, None, None, None, true, 0) + ipLookups <- CreateIpLookups[Eval].createFromFiles(None, None, None, None, true, 0) res <- ipLookups.performLookups("67.43.156.0") } yield res).value val expected = IpLookupResult(None, None, None, None, None) From a59a4e1723cb99db77d8a3ba8013af5b589cc336 Mon Sep 17 00:00:00 2001 From: Dilyan Damyanov Date: Tue, 19 Nov 2019 12:03:23 +0100 Subject: [PATCH 15/19] Support cats.Id (closes #112) Co-authored-by: Ben Fradet --- .../IpAddressResolver.scala | 9 ++++-- .../IpLookups.scala | 32 +++++++++++++++++-- .../SpecializedReader.scala | 19 +++++++++-- .../IpLookupsTest.scala | 30 ++++++++++++++++- 4 files changed, 83 insertions(+), 7 deletions(-) diff --git a/src/main/scala/com.snowplowanalytics.maxmind.iplookups/IpAddressResolver.scala b/src/main/scala/com.snowplowanalytics.maxmind.iplookups/IpAddressResolver.scala index 6e46183..7c34dc4 100644 --- a/src/main/scala/com.snowplowanalytics.maxmind.iplookups/IpAddressResolver.scala +++ b/src/main/scala/com.snowplowanalytics.maxmind.iplookups/IpAddressResolver.scala @@ -14,7 +14,7 @@ package com.snowplowanalytics.maxmind.iplookups import java.net.InetAddress -import cats.Eval +import cats.{Eval, Id} import cats.effect.Sync import cats.syntax.either._ @@ -27,7 +27,7 @@ sealed trait IpAddressResolver[F[_]] { } object IpAddressResolver { - implicit def ipAddressResolver[F[_]: Sync]: IpAddressResolver[F] = new IpAddressResolver[F] { + implicit def syncIpAddressResolver[F[_]: Sync]: IpAddressResolver[F] = new IpAddressResolver[F] { def resolve(ip: String): F[Either[Throwable, InetAddress]] = Sync[F].delay { getIpAddress(ip) } } @@ -36,4 +36,9 @@ object IpAddressResolver { def resolve(ip: String): Eval[Either[Throwable, InetAddress]] = Eval.later { getIpAddress(ip) } } + + implicit def idIpAddressResolver: IpAddressResolver[Id] = new IpAddressResolver[Id] { + def resolve(ip: String): Id[Either[Throwable, InetAddress]] = + getIpAddress(ip) + } } diff --git a/src/main/scala/com.snowplowanalytics.maxmind.iplookups/IpLookups.scala b/src/main/scala/com.snowplowanalytics.maxmind.iplookups/IpLookups.scala index 954a81e..623446e 100644 --- a/src/main/scala/com.snowplowanalytics.maxmind.iplookups/IpLookups.scala +++ b/src/main/scala/com.snowplowanalytics.maxmind.iplookups/IpLookups.scala @@ -15,7 +15,7 @@ package com.snowplowanalytics.maxmind.iplookups import java.io.File import java.net.InetAddress -import cats.{Eval, Monad} +import cats.{Eval, Id, Monad} import cats.effect.Sync import cats.syntax.flatMap._ import cats.syntax.functor._ @@ -26,7 +26,7 @@ import com.snowplowanalytics.lrumap.{CreateLruMap, LruMap} import model._ -trait CreateIpLookups[F[_]] { +sealed trait CreateIpLookups[F[_]] { /** * Create an IpLookups from Files @@ -136,6 +136,34 @@ object CreateIpLookups { } } } + + implicit def idCreateIpLookups( + implicit CLM: CreateLruMap[Id, String, IpLookupResult] + ): CreateIpLookups[Id] = new CreateIpLookups[Id] { + override def createFromFiles( + geoFile: Option[File] = None, + ispFile: Option[File] = None, + domainFile: Option[File] = None, + connectionTypeFile: Option[File] = None, + memCache: Boolean = true, + lruCacheSize: Int = 10000 + ): Id[IpLookups[Id]] = { + val lruCache: Option[LruMap[Id, String, IpLookupResult]] = + if (lruCacheSize > 0) { + CLM.create(lruCacheSize).map(_.some) + } else { + None + } + new IpLookups( + geoFile, + ispFile, + domainFile, + connectionTypeFile, + memCache, + lruCache + ) + } + } } /** diff --git a/src/main/scala/com.snowplowanalytics.maxmind.iplookups/SpecializedReader.scala b/src/main/scala/com.snowplowanalytics.maxmind.iplookups/SpecializedReader.scala index ef6d27b..65340b6 100644 --- a/src/main/scala/com.snowplowanalytics.maxmind.iplookups/SpecializedReader.scala +++ b/src/main/scala/com.snowplowanalytics.maxmind.iplookups/SpecializedReader.scala @@ -14,7 +14,7 @@ package com.snowplowanalytics.maxmind.iplookups import java.net.InetAddress -import cats.Eval +import cats.{Eval, Id} import cats.effect.Sync import cats.syntax.either._ import com.maxmind.geoip2.DatabaseReader @@ -37,7 +37,7 @@ sealed trait SpecializedReader[F[_]] { } object SpecializedReader { - implicit def specializedReader[F[_]: Sync]: SpecializedReader[F] = new SpecializedReader[F] { + implicit def syncSpecializedReader[F[_]: Sync]: SpecializedReader[F] = new SpecializedReader[F] { def getValue( f: ReaderFunction, db: DatabaseReader, @@ -66,6 +66,21 @@ object SpecializedReader { ): Eval[Either[Throwable, CityResponse]] = Eval.later { Either.catchNonFatal(db.city(ip)) } } + + implicit def idSpecializedReader: SpecializedReader[Id] = new SpecializedReader[Id] { + def getValue( + f: ReaderFunction, + db: DatabaseReader, + ip: InetAddress + ): Id[Either[Throwable, String]] = + Either.catchNonFatal(f(db, ip)) + + def getCityValue( + db: DatabaseReader, + ip: InetAddress + ): Id[Either[Throwable, CityResponse]] = + Either.catchNonFatal(db.city(ip)) + } } object ReaderFunctions { diff --git a/src/test/scala/com.snowplowanalytics.maxmind.iplookups/IpLookupsTest.scala b/src/test/scala/com.snowplowanalytics.maxmind.iplookups/IpLookupsTest.scala index c585fc2..586876f 100644 --- a/src/test/scala/com.snowplowanalytics.maxmind.iplookups/IpLookupsTest.scala +++ b/src/test/scala/com.snowplowanalytics.maxmind.iplookups/IpLookupsTest.scala @@ -14,7 +14,7 @@ package com.snowplowanalytics.maxmind.iplookups import java.net.UnknownHostException -import cats.Eval +import cats.{Eval, Id} import cats.effect.IO import cats.syntax.either._ import cats.syntax.option._ @@ -54,6 +54,17 @@ object IpLookupsTest { ) .value + def idIpLookupsFromFiles(memCache: Boolean, lruCache: Int): IpLookups[Id] = + CreateIpLookups[Id] + .createFromFilenames( + Some(geoFile), + Some(ispFile), + Some(domainFile), + Some(connectionTypeFile), + memCache, + lruCache + ) + // Databases and test data taken from https://github.com/maxmind/MaxMind-DB/tree/master/test-data val testData: Map[String, IpLookupResult] = Map( "175.16.199.0" -> IpLookupResult( @@ -154,14 +165,17 @@ class IpLookupsTest extends Specification with Tables { val ioIpLookups = ioIpLookupsFromFiles(memCache, lruCache) val evalIpLookups = evalIpLookupsFromFiles(memCache, lruCache) + val idIpLookups = idIpLookupsFromFiles(memCache, lruCache) testData foreach { case (ip, expected) => formatter(ip, memCache, lruCache) should { val ioActual = ioIpLookups.performLookups(ip).unsafeRunSync val evalActual = evalIpLookups.performLookups(ip).value + val idActual = idIpLookups.performLookups(ip) matchIpLookupResult(ioActual, expected) matchIpLookupResult(evalActual, expected) + matchIpLookupResult(idActual, expected) } } } @@ -169,6 +183,7 @@ class IpLookupsTest extends Specification with Tables { "providing an invalid ip should fail" in { val ioIpLookups = ioIpLookupsFromFiles(true, 0) val evalIpLookups = evalIpLookupsFromFiles(true, 0) + val idIpLookups = idIpLookupsFromFiles(true, 0) val ioExpected = IpLookupResult( new UnknownHostException("not: Name or service not known").asLeft.some, new UnknownHostException("not: Name or service not known").asLeft.some, @@ -183,10 +198,19 @@ class IpLookupsTest extends Specification with Tables { new UnknownHostException("not").asLeft.some, new UnknownHostException("not").asLeft.some ) + val idExpected = IpLookupResult( + new UnknownHostException("not").asLeft.some, + new UnknownHostException("not").asLeft.some, + new UnknownHostException("not").asLeft.some, + new UnknownHostException("not").asLeft.some, + new UnknownHostException("not").asLeft.some + ) val ioActual = ioIpLookups.performLookups("not").unsafeRunSync val evalActual = evalIpLookups.performLookups("not").value + val idActual = idIpLookups.performLookups("not") matchIpLookupResult(ioActual, ioExpected) matchIpLookupResult(evalActual, evalExpected) + matchIpLookupResult(idActual, idExpected) } "providing no files should return Nones" in { @@ -198,9 +222,13 @@ class IpLookupsTest extends Specification with Tables { ipLookups <- CreateIpLookups[Eval].createFromFiles(None, None, None, None, true, 0) res <- ipLookups.performLookups("67.43.156.0") } yield res).value + val idActual = CreateIpLookups[Id] + .createFromFiles(None, None, None, None, true, 0) + .performLookups("67.43.156.0") val expected = IpLookupResult(None, None, None, None, None) matchIpLookupResult(ioActual, expected) matchIpLookupResult(evalActual, expected) + matchIpLookupResult(idActual, expected) } } From 8e3469530f11bc69bf2b08ca80c082857cc29860 Mon Sep 17 00:00:00 2001 From: Ben Fradet Date: Mon, 22 Apr 2019 16:11:18 +0200 Subject: [PATCH 16/19] Add readme examples (closes #79) --- README.md | 70 ++++++++++++++++++++++++++++++++++++++----------------- 1 file changed, 49 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index 453f5df..0852858 100644 --- a/README.md +++ b/README.md @@ -44,7 +44,7 @@ import cats.effect.IO import com.snowplowanalytics.maxmind.iplookups.IpLookups val result = (for { - ipLookups <- IpLookups.createFromFilenames[IO]( + ipLookups <- CreateIpLookups[IO].createFromFilenames( geoFile = Some("/opt/maxmind/GeoLite2-City.mmdb") ispFile = None, domainFile = None, @@ -52,8 +52,7 @@ val result = (for { memCache = false, lruCacheSize = 20000 ) - - lookup <- ipLookups.performLookups[IO]("175.16.199.0") + lookup <- ipLookups.performLookups("175.16.199.0") } yield lookup).unsafeRunSync() result.ipLocation match { @@ -65,7 +64,37 @@ result.ipLocation match { } ``` -Note that `GeoLite2-City.mmdb` is updated by MaxMind each month.. +`cats.Eval` and `cats.Id` are also supported: + +```scala +import cats.{Eval, Id} + +val evalResult: Eval[IpLookupResult] = for { + ipLookups <- CreateIpLookups[Eval].createFromFilenames( + geoFile = Some("/opt/maxmind/GeoLite2-City.mmdb") + ispFile = None, + domainFile = None, + connectionTypeFile = None, + memCache = false, + lruCacheSize = 20000 + ) + lookup <- ipLookups.performLookups("175.16.199.0") +} yield lookup + +val idResult: IpLookupResult = { + val ipLookups = CreateIpLookups[Id].createFromFilenames( + geoFile = Some("/opt/maxmind/GeoLite2-City.mmdb") + ispFile = None, + domainFile = None, + connectionTypeFile = None, + memCache = false, + lruCacheSize = 20000 + ) + ipLookups.performLookups("175.16.199.0") +} +``` + +Note that `GeoLite2-City.mmdb` is updated by MaxMind each month. For further usage examples for Scala MaxMind IP Lookups, please see the tests in [`IpLookupsTest.scala`][iplookupstest-scala]. The test suite uses test databases provided by @@ -78,7 +107,7 @@ MaxMind. The signature is as follows: ```scala -case class IpLookups( +final case class IpLookups( geoFile: Option[File], ispFile: Option[File], domainFile: Option[File], @@ -88,11 +117,11 @@ case class IpLookups( ) ``` -In the `IpLookups` companion object there is an alternative constructor which takes `Option[String]` +`CreateIpLookups` proposes an alternative constructor which takes `Option[String]` as file paths to the databases instead: ```scala -def apply( +def createFromFilenames( geoFile: Option[String], ispFile: Option[String], domainFile: Option[String], @@ -120,7 +149,7 @@ cache, set its size to zero, i.e. `lruCache = 0`. The `performLookups(ip)` method returns a: ```scala -case class IpLookupResult( +final case class IpLookupResult( ipLocation: Option[Either[Throwable, IpLocation]], isp: Option[Either[Throwable, String]], organization: Option[Either[Throwable, String]], @@ -142,7 +171,7 @@ Note that enabling providing an ISP database will return an `organization` in ad The geographic lookup returns an `IpLocation` case class instance with the following structure: ```scala -case class IpLocation( +final case class IpLocation( countryCode: String, countryName: String, region: Option[String], @@ -166,16 +195,17 @@ This example shows how to do a lookup using all four databases. ```scala import com.snowplowanalytics.maxmind.iplookups.IpLookups -val ipLookups = IpLookups( - geoFile = Some("/opt/maxmind/GeoLite2-City.mmdb"), - ispFile = Some("/opt/maxmind/GeoIP2-ISP.mmdb"), - domainFile = Some("/opt/maxmind/GeoIP2-Domain.mmdb"), - connectionType = Some("/opt/maxmind/GeoIP2-Connection-Type.mmdb"), - memCache = false, - lruCache = 10000 -) - -val lookupResult = ipLookups.performLookups("70.46.123.145") +val lookupResult = (for { + ipLookups <- CreateIpLookups[IO].createFromFilenames( + geoFile = Some("/opt/maxmind/GeoLite2-City.mmdb"), + ispFile = Some("/opt/maxmind/GeoIP2-ISP.mmdb"), + domainFile = Some("/opt/maxmind/GeoIP2-Domain.mmdb"), + connectionType = Some("/opt/maxmind/GeoIP2-Connection-Type.mmdb"), + memCache = false, + lruCache = 10000 + ) + lookupResult <- ipLookups.performLookups("70.46.123.145") +} yield lookupResult).unsafeRunSync() // Geographic lookup println(lookupResult.ipLocation).map(_.countryName) // => Some(Right("United States")) @@ -238,8 +268,6 @@ limitations under the License. [iplookupstest-scala]: https://github.com/snowplow/scala-maxmind-iplookups/blob/master/src/test/scala/com.snowplowanalytics.maxmind.iplookups/IpLookupsTest.scala -[twitter-lru-cache]: https://twitter.github.com/commons/apidocs/com/twitter/common/util/caching/LRUCache.html - [maxmind-downloads]: https://dev.maxmind.com/geoip/geoip2/downloadable/#MaxMind_APIs [maxmind-isp]: https://www.maxmind.com/en/geoip2-isp-database [maxmind-domain]: https://www.maxmind.com/en/geoip2-domain-name-database From 2fac76343fb0c10d5069056c4f2087e8920bb5eb Mon Sep 17 00:00:00 2001 From: Dilyan Damyanov Date: Tue, 12 Nov 2019 11:27:41 +0000 Subject: [PATCH 17/19] Change Travis distribution to Trusty (closes #117) --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index f95ab9f..035ffb2 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,4 +1,5 @@ language: scala +dist: trusty scala: - 2.12.8 jdk: From 3e1550e468f1635ab883e34074d06cf4ec8c73f8 Mon Sep 17 00:00:00 2001 From: Anton Parkhomenko Date: Tue, 19 Nov 2019 18:56:15 +0300 Subject: [PATCH 18/19] Add function to combine errors raised when processing a CityResponse (closes #120) --- .../model.scala | 24 ++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/src/main/scala/com.snowplowanalytics.maxmind.iplookups/model.scala b/src/main/scala/com.snowplowanalytics.maxmind.iplookups/model.scala index 0576ac7..c22635f 100644 --- a/src/main/scala/com.snowplowanalytics.maxmind.iplookups/model.scala +++ b/src/main/scala/com.snowplowanalytics.maxmind.iplookups/model.scala @@ -14,12 +14,21 @@ package com.snowplowanalytics.maxmind.iplookups import java.net.InetAddress +import cats.data.ValidatedNel +import cats.syntax.traverse._ +import cats.syntax.either._ +import cats.syntax.apply._ +import cats.instances.option._ +import cats.instances.either._ + import com.maxmind.geoip2.DatabaseReader import com.maxmind.geoip2.model.CityResponse object model { type ReaderFunction = (DatabaseReader, InetAddress) => String + type Error[A] = Either[Throwable, A] + /** A case class wrapper around the MaxMind CityResponse class. */ final case class IpLocation( countryCode: String, @@ -70,5 +79,18 @@ object model { organization: Option[Either[Throwable, String]], domain: Option[Either[Throwable, String]], connectionType: Option[Either[Throwable, String]] - ) + ) { + // Combine all errors if any + def results: ValidatedNel[ + Throwable, + (Option[IpLocation], Option[String], Option[String], Option[String], Option[String])] = { + val location = ipLocation.sequence[Error, IpLocation].toValidatedNel + val provider = isp.sequence[Error, String].toValidatedNel + val org = organization.sequence[Error, String].toValidatedNel + val dom = domain.sequence[Error, String].toValidatedNel + val connection = connectionType.sequence[Error, String].toValidatedNel + + (location, provider, org, dom, connection).tupled + } + } } From c3c47e631765d72f79a0cd018cae9b8e392b4cb0 Mon Sep 17 00:00:00 2001 From: Ben Fradet Date: Wed, 6 Mar 2019 14:28:59 +0100 Subject: [PATCH 19/19] Prepare for release --- CHANGELOG | 21 +++++++++++++++++++++ README.md | 4 ++-- build.sbt | 2 +- 3 files changed, 24 insertions(+), 3 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 37cc523..419bcd0 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,24 @@ +Version 0.6.0 (2019-11-21) +-------------------------- +Add support for isInEuropeanUnion flag (#87) +Add support for continent and accuracyRadius to the model (#101) +Introduce tagless final encoding (#108) +Support cats.Id (#112) +Abstract over IpLookups creation (#113) +Add function to combine errors raised when processing a CityResponse (#120) +Add readme examples (#79) +Bump geoip2 to 2.12.0 (#93) +Bump cats-effect to 1.2.0 (#90) +Bump cats-core to 1.6.0 (#91) +Bump scala-lru-map to 0.3.0 (#105) +Bump specs2-core to 4.4.1 (#110) +Bump Scala to 2.12.8 and remove Scala 2.11 support (#89) +Bump SBT to 1.2.8 (#109) +Use sbt-tpolecat (#102) +Change Travis distribution to Trusty (#117) +Remove thread safety warning regarding LRU cache from readme file (#99) +Extend copyright to 2019 (#111) + Version 0.5.0 (2018-06-29) -------------------------- Add Gitter badge (#57) diff --git a/README.md b/README.md index 0852858..140df93 100644 --- a/README.md +++ b/README.md @@ -19,12 +19,12 @@ can also configure an LRU (Least Recently Used) cache of variable size ## Installation -The latest version of scala-maxmind-iplookups is **0.5.0** and is compatible with Scala 2.12. +The latest version of scala-maxmind-iplookups is **0.6.0** and is compatible with Scala 2.12. Add this to your SBT config: ```scala -val maxmindIpLookups = "com.snowplowanalytics" %% "scala-maxmind-iplookups" % "0.5.0" +val maxmindIpLookups = "com.snowplowanalytics" %% "scala-maxmind-iplookups" % "0.6.0" ``` Retrieve the `GeoLite2-City.mmdb` file from the [MaxMind downloads page][maxmind-downloads] diff --git a/build.sbt b/build.sbt index 11bc94c..b1ea156 100644 --- a/build.sbt +++ b/build.sbt @@ -17,7 +17,7 @@ lazy val root = project .settings( organization := "com.snowplowanalytics", name := "scala-maxmind-iplookups", - version := "0.5.0", + version := "0.6.0", description := "Scala wrapper for MaxMind GeoIP2 library", scalaVersion := "2.12.8", javacOptions := BuildSettings.javaCompilerOptions,