All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.twitter.finagle.toggle.JsonToggleMap.scala Maven / Gradle / Ivy

The newest version!
package com.twitter.finagle.toggle

import com.fasterxml.jackson.annotation.JsonProperty
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.module.scala.DefaultScalaModule
import com.twitter.util.Try
import java.net.URL
import scala.collection.{breakOut, immutable}

/**
 * [[ToggleMap ToggleMaps]] in JSON format.
 *
 * @define jsonschema The [[http://json-schema.org/ JSON Schema]] used is:
 * {{{
 * {
 *   "\$schema": "http://json-schema.org/draft-04/schema#",
 *   "type": "object",
 *   "required": [
 *     "toggles"
 *   ],
 *   "properties": {
 *     "toggles": {
 *       "type": "array",
 *       "items": {
 *         "type": "object",
 *         "properties": {
 *           "id": { "type": "string" },
 *           "description": { "type": "string" },
 *           "fraction": {
 *             "type": "number",
 *             "minimum": 0.0,
 *             "maximum": 1.0,
 *             "exclusiveMinimum": false,
 *             "exclusiveMaxmimum": false
 *           },
 *           "comment": { "type": "string" }
 *         },
 *         "required": [
 *           "id",
 *           "fraction"
 *         ]
 *       }
 *     }
 *   }
 * }
 * }}}
 *
 * @define example Here is an example of a JSON [[ToggleMap]] input:
 * {{{
 * {
 *   "toggles": [
 *     {
 *       "id": "com.game.of.thrones.WargingEnabled",
 *       "description": "Use warging for computing the response.",
 *       "fraction": 0.1
 *     },
 *     {
 *       "id": "com.game.of.thrones.IsWinterComing",
 *       "description": "Controls whether or not winter is coming.",
 *       "fraction": 1.0,
 *       "comment": "We've seen the white walkers, we know that winter is coming."
 *     }
 *   ]
 * }
 * }}}
 *
 * With the exception of "comment", the properties correspond to the various
 * fields on [[Toggle.Metadata]].
 *  - '''name''': Corresponds to `Toggle.Metadata.id`.
 *  - '''fraction''': Corresponds to `Toggle.Metadata.fraction` and values must be
 *    between `0.0` and `1.0`, inclusive.
 *  - '''description''': Corresponds to `Toggle.Metadata.description`.
 *  - '''comment''': For documentation purposes only and is not used in the
 *    creation of the [[ToggleMap]].
 */
object JsonToggleMap {

  private[this] val mapper: ObjectMapper =
    new ObjectMapper().registerModule(DefaultScalaModule)

  /**
   * How to treat the "description" field on a toggle.
   *
   * @see [[DescriptionIgnored]] and [[DescriptionRequired]].
   */
  sealed abstract class DescriptionMode

  /**
   * Requires toggles to have a "description" field.
   *
   * This is useful for the library owner's base definitions of [[Toggle]].
   */
  object DescriptionRequired extends DescriptionMode

  /**
   * Transforms the Toggle's "description" field into being empty.
   *
   * This is useful for service owner overrides of a toggle where
   * the developer making modifications is not the one who has defined
   * the toggle itself.
   */
  object DescriptionIgnored extends DescriptionMode

  private[this] case class JsonToggle(
      @JsonProperty(required = true) id: String,
      @JsonProperty(required = true) fraction: Double,
      description: Option[String],
      comment: Option[String])

  private[this] case class JsonToggles(
      @JsonProperty(required = true) toggles: Seq[JsonToggle]) {

    def toToggleMap(
      source: String,
      descriptionMode: DescriptionMode
    ): ToggleMap = {
      val invalid = toggles.find { md =>
        descriptionMode match {
          case DescriptionRequired => md.description.isEmpty
          case DescriptionIgnored => false
        }
      }
      invalid match {
        case None => ()
        case Some(md) =>
          throw new IllegalArgumentException(s"Mandatory description is missing for: $md")
      }

      val metadata: immutable.Seq[Toggle.Metadata] =
        toggles.map { jsonToggle =>
          val description = descriptionMode match {
            case DescriptionRequired => jsonToggle.description
            case DescriptionIgnored => None
          }
          Toggle.Metadata(
            jsonToggle.id,
            jsonToggle.fraction,
            description,
            source)
        }(breakOut)

      val ids = metadata.map(_.id)
      val uniqueIds = ids.distinct
      if (ids.size != uniqueIds.size) {
        throw new IllegalArgumentException(
          s"Duplicate Toggle ids found: ${ids.mkString(",")}")
      }
      new ToggleMap.Immutable(metadata)
    }
  }

  /**
   * Attempts to parse the given JSON `String` into a [[ToggleMap]].
   *
   * $jsonschema
   *
   * $example
   *
   * @param descriptionMode how to treat the "description" field for a toggle.
   */
  def parse(json: String, descriptionMode: DescriptionMode): Try[ToggleMap] = Try {
    val jsonToggles = mapper.readValue(json, classOf[JsonToggles])
    jsonToggles.toToggleMap("JSON String", descriptionMode)
  }

  /**
   * Attempts to parse the given JSON `URL` into a [[ToggleMap]].
   *
   * Useful for loading resource files via [[StandardToggleMap]].
   *
   * $jsonschema
   *
   * $example
   *
   * @param descriptionMode how to treat the "description" field for a toggle.
   */
  def parse(url: URL, descriptionMode: DescriptionMode): Try[ToggleMap] = Try {
    val jsonToggles = mapper.readValue(url, classOf[JsonToggles])
    jsonToggles.toToggleMap(url.toString, descriptionMode)
  }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy