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

org.mashupbots.socko.rest.RestEndPoint.scala Maven / Gradle / Ivy

The newest version!
//
// Copyright 2013 Vibul Imtarnasan, David Bolton and Socko contributors.
//
// 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 org.mashupbots.socko.rest

import org.mashupbots.socko.events.EndPoint

/**
 * The HTTP method and path to a REST operation
 *
 * @param method HTTP method. e.g. `GET`.
 * @param rootPath Root path of the REST service. e.g. `/api`.
 * @param relativePath Path relative to the `rootPath` for the REST operation
 */
case class RestEndPoint(
  method: String,
  rootPath: String,
  relativePath: String) {

  /**
   * Full path from combining the `rootPath` with `relativePath`.
   */
  val fullPath = if (rootPath == "/") relativePath else {
    rootPath + (if (relativePath.startsWith("/")) "" else "/") + relativePath
  }

  /**
   * The full path split into path segments for ease of matching
   *
   * ==Example Usage==
   * If path = `/user/{Id}` and rootUrl in the config is `/api`, the full path segment are
   *
   * {{{
   * List(
   *   PathSegment("api", false),
   *   PathSegment("user", false),
   *   PathSegment("Id", true)
   * )
   * }}}
   */
  val fullPathSegments: List[PathSegment] = {
    if (relativePath == null || relativePath.length == 0)
      throw new IllegalArgumentException("Declaration path cannot be null or empty")

    val s = if (fullPath.startsWith("/")) fullPath.substring(1) else fullPath
    val ss = s.split("/").toList
    val segments = ss.map(s => PathSegment(s))
    segments
  }

  /**
   * Number of static path segments. Used to determine best match
   */
  val staticFullPathSegementsCount: Int = fullPathSegments.count(ps => !ps.isVariable)

  /**
   * The relative URL template split into path segments for ease of matching
   *
   * ==Example Usage==
   * If path = `/user/{Id}` and rootUrl in the config is `/api`, the relative path segment are
   *
   * {{{
   * List(
   *   PathSegment("user", false),
   *   PathSegment("Id", true)
   * )
   * }}}
   */
  val relativePathSegments: List[PathSegment] = {
    if (relativePath == null || relativePath.length == 0)
      throw new IllegalArgumentException("URI cannot be null or empty")

    val s = if (relativePath.startsWith("/")) relativePath.substring(1) else relativePath
    val ss = s.split("/").toList
    val segments = ss.map(s => PathSegment(s))
    segments
  }

  /**
   * Compares the URL of this operation to another.
   *
   * Comparison is based on method and path segments.
   *
   * For example, `GET /pets/{id}` is the same as `GET /{type}/{id}` because `{type}` is a variable
   * and can contain `pets`.
   *
   * However, the following are different:
   *  - `GET /pets` is different to `GET /users` because the static paths are different
   *  - `DELETE /pets/{id}` is different to `PUT /pets/{id}` because methods are different
   *
   * @param op Another REST operation to compare against
   * @return `True` if the URI templates are ambiguous and 2 or more unique end points can resolve to
   *   either URI templates.  `False` otherwise..
   */
  def comparePath(restEndPoint: RestEndPoint): Boolean = {

    if (method != restEndPoint.method) {
      // If different methods, then cannot be the same
      false
    } else if (fullPathSegments.length != restEndPoint.fullPathSegments.length) {
      // If different number of segments, then cannot be the same
      return false
    } else {
      // Compare paths
      def comparePathSegment(segments: List[(PathSegment, PathSegment)]): Boolean = {
        if (segments.isEmpty) {
          // Must resolve to the same endpoint - same method and path segments
          true
        } else {
          val (l, r) = segments.head
          if (!l.isVariable && !r.isVariable && l.name != r.name) {
            // If static segments are different, then cannot be the same
            false
          } else if ((l.isVariable && !r.isVariable) || (!l.isVariable && r.isVariable)) {
            // If mix of static and variable, then assume different. The static will take precedence
            false
          } else {
            // If both variable, then assume same so go to the next level
            comparePathSegment(segments.tail)
          }
        }
      }
      comparePathSegment(fullPathSegments.zip(restEndPoint.fullPathSegments))
    }
  }

  /**
   * Compares the URL template with the specified end point.
   *
   * For example, `GET /pets/{id}` matches the end point `GET /pets/123`.
   *
   * @param endpoint End point to match
   * @return `True` if this is a match; `False` if not a match.
   */
  def matchEndPoint(endpoint: EndPoint): Boolean = {
    // Convert HEAD to GET
    val endpointMethod = if (endpoint.isHEAD) "GET" else endpoint.method

    if (method != endpointMethod) {
      false
    } else if (fullPathSegments.length != endpoint.pathSegments.length) {
      return false
    } else {
      // Compare paths
      def comparePathSegment(segments: List[(PathSegment, String)]): Boolean = {
        if (segments.isEmpty) {
          // Must resolve to the same endpoint - same method and path segments
          true
        } else {
          val (ps, endpoint) = segments.head
          if (!ps.isVariable && ps.name != endpoint) {
            // If static segments are different, then cannot be the same
            false
          } else {
            comparePathSegment(segments.tail)
          }
        }
      }
      comparePathSegment(fullPathSegments.zip(endpoint.pathSegments))
    }
  }

}

/**
 * Companion object
 */
object RestEndPoint {

  /**
   * Alternative constructor using configuration and registration
   *
   * @param config REST configuration
   * @param registration REST registration details
   */
  def apply(config: RestConfig, registration: RestRegistration): RestEndPoint =
    RestEndPoint(registration.method.toString, config.rootPath, registration.path)
}

/**
 * Encapsulates a path segment
 *
 * ==Example Usage==
 * {{{
 * // '{Id}'
 * PathSegment("Id", true)
 *
 * // 'user'
 * PathSegment("user", false)
 * }}}
 *
 * @param name Name of the variable or static segment
 * @param isVariable Flag to denote if this segment is variable and is intended to be bound to a variable or not.
 *   If not, it is a static segment
 */
case class PathSegment(
  name: String,
  isVariable: Boolean) {
}

/**
 * Factory to parse a string into a path segment
 */
object PathSegment {
  /**
   * Parses a string into a path segment
   *
   * A string is a variable if it is in the format: `{name}`.  The `name` part will be put in the
   * name field of the path segment
   *
   * @param s string to parse
   */
  def apply(s: String): PathSegment =
    if (s == null || s.length == 0) throw new IllegalArgumentException("Path segment cannot be null or empty")
    else if (s.startsWith("{") && s.endsWith("}")) PathSegment(s.substring(1, s.length - 1), true)
    else PathSegment(s, false)
}





© 2015 - 2025 Weber Informatics LLC | Privacy Policy