fthomas / refined

Refinement types for Scala
MIT License
1.71k stars 154 forks source link

[pureconfig] Refined keys for Map[String, T] #443

Open NeQuissimus opened 6 years ago

NeQuissimus commented 6 years ago

PureConfig provides an instance of ConfigReader for maps. (see https://github.com/pureconfig/pureconfig/blob/v0.9.0/core/src/main/scala/pureconfig/DerivedReaders.scala#L198)

Unfortunately, it seems impossible to provide a refinement for the map key without writing a custom ConfigReader I think refined should provide a ConfigReader[Map[Refined[String, ?], T]] that would allow for map keys to be something like String Refined MaxSize[W.249.T].

I attempted to create such a ConfigReader but failed to provide a generic definition.

I did, however, manage to make a specific one for my use case (Kafka topics), so maybe that can help:

// https://github.com/apache/kafka/blob/1.0.0/clients/src/main/java/org/apache/kafka/common/internals/Topic.java
  type TopicSpec = NonEmpty And MaxSize[W.`249`.T] And MatchesRegex[W.`"[a-zA-Z0-9\\\\.\\\\-_]+"`.T] And Not[Equal[W.`"."`.T]] And Not[Equal[W.`".."`.T]]
  type Topic = String Refined TopicSpec

  implicit def deriveTopicMap[T](implicit reader: Derivation[Lazy[ConfigReader[T]]]): ConfigReader[Map[Topic, T]] = new ConfigReader[Map[Topic, T]] {
    override def from(cur: ConfigCursor): Either[ConfigReaderFailures, Map[Topic, T]] = {
      cur.asMap.right.flatMap { map =>
        map.foldLeft[Either[ConfigReaderFailures, Map[Topic, T]]](Right(Map())) {
          case (acc, (key, valueConf)) => {
              val topic = refineV[TopicSpec](key)
              topic.map { key =>
                combineResults(acc, reader.value.value.from(valueConf)) { (map, value) => map + (key -> value) }
              }.getOrElse(Right(Map.empty[Topic, T]))
            }
        }
      }
    }
  }
fthomas commented 6 years ago

That sounds good to me. Something similar was recently merged in circe: https://github.com/circe/circe/pull/833

Maybe a generic version of that would have a signature similar to

implicit def deriveRefinedMap[F[_, _], K, P, T](
    implicit reader: Derivation[Lazy[ConfigReader[T]]],
    rt: RefType[F],
    v: Validate[K, P]
): ConfigReader[Map[F[K, P], T]]

?

crypticmind commented 5 years ago

This looks like a good exercise to familiarize with refined.

The signature @fthomas proposes looks OK, however, I see cur.asMap returns a map of StringConfigCursor, which makes sense because by definition, keys in HOCON have to be strings. So, it looks like the signature should constrain the map to refined string keys instead. Something like:

implicit def deriveRefinedMap[F[String, _], P, T](
    implicit reader: Derivation[Lazy[ConfigReader[T]]],
    rt: RefType[F],
    v: Validate[String, P]
): ConfigReader[Map[F[String, P], T]]

Does this make sense?

fthomas commented 5 years ago

@crypticmind Yes, that makes sense. But note that F[String, _] in the type parameters list should still be F[_, _] since RefType[F] requires that F is a binary type constructor.