Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ private[lepus] object DecodeEndpoint:
val endpoints = endpoint.asVector()

val endpointPaths = endpoints.filter(_.isPath)
val endpointQueryParams = endpoints.filter(_.isQueryParam)
val endpointQueryParams = endpoints.filter(_.isQuery)

decodingConvolution(
tailrecMatchPath(DecodePathRequest(request), endpointPaths, _, _),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import Endpoint.*
sealed trait Endpoint[T]:
private[lepus] type TypeParam = T
private[lepus] type ThisType <: Endpoint[T]

@targetName("and") def ++[N, TN](other: Endpoint[N])(using ParamConcat.Aux[T, N, TN]): Endpoint[TN] =
Pair[T, N, TN](this, other)

Expand All @@ -21,6 +22,66 @@ sealed trait Endpoint[T]:
@targetName("queryQ") def +?[N, TN](query: Query[N])(using ParamConcat.Aux[T, N, TN]): Endpoint[TN] = this ++ query
@targetName("query&") def +&[N, TN](query: Query[N])(using ParamConcat.Aux[T, N, TN]): Endpoint[TN] = this ++ query

/** Method for storing nested Endpoints in Vector. */
def asVector(): Vector[Endpoint[?]] =
this match
case Endpoint.Pair(left, right) => left.asVector() ++ right.asVector()
case r: Endpoint[?] => Vector(r)

/** A method to generate a Path string using only the Endpoint's Path information.
*
* @param format
* Argument to specify the format of the path parameter string.
* @return
* A Path String
*/
def toPath(format: String => String = (s: String) => s"{$s}"): String = "/" + asVector()
.map {
case e: Endpoint.FixedPath[?] => e.name
case e: Endpoint.Path[?] => format(e.name)
case _ => ""
}
.filter(_.nonEmpty)
.mkString("/")

/** A method to generate a string of paths for use with Scala's format method. */
def toFormatPath: String = toPath(_ => "%s")

/** A method to generate a Query string using only the Endpoint's Query information.
*
* @param format
* Argument to specify the format of the query parameter string.
* @return
* A Query String
*/
def toQuery(format: String => String = (s: String) => s"{$s}"): String = asVector()
.map {
case e: Endpoint.Query[?] => format(e.key)
case _ => ""
}
.filter(_.nonEmpty)
.mkString("&")

/** A method to generate a string of queries for use with Scala's format method. */
def toFormatQuery: String = toQuery(s => s"$s=%s")

/** Method to determine if the Endpoint is a Path. */
def isPath: Boolean = this match
case _: Endpoint.Path[?] => true
case _ => false

/** Method to determine if the Endpoint is a Query. */
def isQuery: Boolean = this match
case _: Endpoint.Query[?] => true
case _ => false

/** Value for generating a string of paths using the format method. */
def formatString: String =
val query: String = if toFormatQuery.nonEmpty then "?" + toFormatQuery else ""
toFormatPath + query

override def toString: String = formatString

object Endpoint:

/** Model for representing endpoint parameters
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,36 +8,6 @@ import lepus.router.http.*

trait ExtensionMethods extends lepus.core.internal.ExtensionMethods:

extension (endpoint: Endpoint[?])
def recursiveEndpoints[T](pf: PartialFunction[Endpoint[?], Vector[T]]): Vector[T] =
endpoint match
case Endpoint.Pair(left, right) => left.recursiveEndpoints(pf) ++ right.recursiveEndpoints(pf)
case r: Endpoint[?] if pf.isDefinedAt(r) => pf(r)
case _ => Vector.empty

def asVector(): Vector[Endpoint[?]] =
recursiveEndpoints {
case e: Endpoint[?] => Vector(e)
}

def toPath: String = "/" + asVector()
.map {
case e: Endpoint.FixedPath[?] => e.name
case e: Endpoint.Path[?] => s"{${ e.name }}"
case _ => ""
}
.mkString("/")

def isPath: Boolean =
endpoint match
case _: Endpoint.Path[?] => true
case _ => false

def isQueryParam: Boolean =
endpoint match
case _: Endpoint.Query[?] => true
case _ => false

// see https://github.com/scala/bug/issues/12186
extension [T](v: Vector[T])
def headAndTail: Option[(T, Vector[T])] = if v.isEmpty then None else Some((v.head, v.tail))
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
/** This file is part of the Lepus Framework. For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

package lepus.router.http

import org.scalatest.flatspec.AnyFlatSpec

import lepus.core.generic.{ SchemaType, Schema }
import lepus.core.generic.semiauto.*

import lepus.router.{ *, given }
import lepus.router.http.Endpoint.*

class EndpointTest extends AnyFlatSpec:

it should "generate endpoint" in {
assertCompiles("""
import lepus.router.{ *, given }
import lepus.router.http.Endpoint.*

val endpoint1: Endpoint[String] = "test1" / bindPath[String]("p1")
val endpoint2: Endpoint[String] = bindPath[String]("p1") / "test2"
val endpoint3: Endpoint[(String, String)] = endpoint1 ++ endpoint2
val endpoint4: Endpoint[(String, String, Long)] = endpoint1 ++ endpoint2 / bindPath[Long]("p1")
""".stripMargin)
}

it should "generate endpoint failure" in {
assertDoesNotCompile("""
import lepus.router.{ *, given }
import lepus.router.http.Endpoint.*

val endpoint1: Endpoint[Long] = "test1" / bindPath[String]("p1")
val endpoint2: Endpoint[Long] = bindPath[String]("p1") / "test2"
val endpoint3: Endpoint[(Long, Long)] = endpoint1 and endpoint2
val endpoint4: Endpoint[(Long, Long, String)] = endpoint1 and endpoint2 / bindPath[Long]("p1")
""".stripMargin)
}

it should "compile" in {
assertCompiles("""
import cats.effect.IO
import org.http4s.dsl.io.*
import lepus.router.{ *, given }

bindPath[Long]("p1") / bindPath[String]("p2") ->> RouterConstructor.of {
case GET => Ok("Hello")
}
""".stripMargin)
}

it should "compile failure" in {
assertDoesNotCompile("""
import cats.effect.IO
import org.http4s.dsl.io.*
import lepus.router.{ *, given }

bindPath[Long]("p1") / bindPath[String]("p2") ->> RouterConstructor.of[IO, (String, String)] {
case GET => Ok("Hello")
}
""".stripMargin)
}

it should "The Endpoint string will be the same as the specified string." in {
val endpoint1: Endpoint[String] = "test1" / bindPath[String]("p1")
endpoint1.toFormatPath === "test1/%s"
}

it should "If multiple Endpoints are combined, the string will be the same as the specified value." in {
val endpoint1: Endpoint[String] = "test1" / bindPath[String]("p1")
val endpoint2: Endpoint[String] = bindPath[String]("p1") / "test2"
val endpoint3: Endpoint[(String, String)] = endpoint1 ++ endpoint2

endpoint3.toFormatPath === "test1/%s/%s/test2"
}

it should "Matches the string specified when the path is generated from Endpoint." in {
val endpoint: Endpoint[String] = "test1" / bindPath[String]("p1")
endpoint.toFormatPath.format("hoge") === "test1/hoge"
}

it should "Matches the string specified when a path is generated from multiple composited Endpoints." in {
val endpoint1: Endpoint[String] = "test1" / bindPath[String]("p1")
val endpoint2: Endpoint[Long] = bindPath[Long]("p1") / "test2"
val endpoint3: Endpoint[(String, Long)] = endpoint1 ++ endpoint2

endpoint3.toFormatPath.format("hoge", 1L) === "test1/hoge/1/test2"
}

it should "The Endpoint string in the Query parameter will be the same as the specified string." in {
val endpoint: Endpoint[(Int, Long)] = bindQuery[Int]("page") +& bindQuery[Long]("limit")
endpoint.toFormatQuery.format(1, 10L) === "page=1&limit=10"
}

it should "The Endpoint string consisting of multiple Query parameters will be the same as the specified string." in {
val endpoint1: Endpoint[List[String]] = bindQuery[List[String]]("area")
val endpoint2: Endpoint[(Int, Long)] = bindQuery[Int]("page") +& bindQuery[Long]("limit")
val endpoint3: Endpoint[(List[String], Int, Long)] = endpoint1 ++ endpoint2
endpoint3.toFormatQuery.format("tokyo,kanagawa", 1, 10L) === "area=tokyo,kanagawa&page=1&limit=10"
}

it should "The string of the Endpoint from which the Path and Query are composited will be the same as the specified value." in {
val endpoint1: Endpoint[String] = "hello" / bindPath[String]("p1")
val endpoint2: Endpoint[(Int, Long)] = bindQuery[Int]("page") +& bindQuery[Long]("limit")
val endpoint3: Endpoint[(String, Int, Long)] = endpoint1 ++ endpoint2

endpoint3.formatString.format("world", 1, 10L) === "hello/world?page=1&limit=10"
}

it should "Value object using opaque type can also generate Endpoint." in {
object ValueObject:
opaque type Id = String

object Id:
def apply(id: String): Id = id

given EndpointConverter[String, ValueObject.Id] =
EndpointConverter.convertT[String, ValueObject.Id](str => str)(using Schema.given_Schema_String)

val endpoint: Endpoint[ValueObject.Id] = "hello" / bindPath[ValueObject.Id]("p1")

endpoint.toFormatPath === "hello/%s" & endpoint.toFormatPath.format("12345678") === "hello/12345678"
}

it should "class can also be used to generate Endpoints." in {
case class Id(long: Long)
object Id:

given Schema[Id] = deriveSchemer[Id]
given EndpointConverter[String, Id] =
EndpointConverter.convertT[String, Id](str => Id(str.toLong))

val endpoint: Endpoint[Id] = "hello" / bindPath[Id]("p1")

endpoint.toFormatPath === "hello/%s"
}

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ private[lepus] object RouterToOpenAPI:

val component = schemaTuple.map(v => Component(v.map(x => x._1.shortName -> schemaToOpenApiSchema(x._2))))
val endpoints = groupEndpoint.map {
case (endpoint, route) => endpoint.toPath -> routerToPath(endpoint, route, schemaToOpenApiSchema)
case (endpoint, route) => endpoint.toPath() -> routerToPath(endpoint, route, schemaToOpenApiSchema)
}
val tags = router.routes.toList
.flatMap(_._2 match
Expand Down