From e4a919733e014fb24ce6c329e2644725af468957 Mon Sep 17 00:00:00 2001 From: aliraza556 Date: Thu, 16 Oct 2025 07:25:03 +0500 Subject: [PATCH 1/2] feat: add Datastar request generation from Endpoint definitions --- .../scala/zio/http/datastar/Attributes.scala | 36 ++ .../http/datastar/DatastarPackageBase.scala | 10 + .../zio/http/datastar/EndpointRequest.scala | 266 +++++++++++++++ .../http/datastar/EndpointRequestSpec.scala | 316 ++++++++++++++++++ .../DatastarEndpointRequestExample.scala | 109 ++++++ 5 files changed, 737 insertions(+) create mode 100644 zio-http-datastar-sdk/src/main/scala/zio/http/datastar/EndpointRequest.scala create mode 100644 zio-http-datastar-sdk/src/test/scala/zio/http/datastar/EndpointRequestSpec.scala create mode 100644 zio-http-example/src/main/scala/example/endpoint/DatastarEndpointRequestExample.scala diff --git a/zio-http-datastar-sdk/src/main/scala/zio/http/datastar/Attributes.scala b/zio-http-datastar-sdk/src/main/scala/zio/http/datastar/Attributes.scala index 34665a506..dd878fefb 100644 --- a/zio-http-datastar-sdk/src/main/scala/zio/http/datastar/Attributes.scala +++ b/zio-http-datastar-sdk/src/main/scala/zio/http/datastar/Attributes.scala @@ -6,6 +6,7 @@ import scala.language.implicitConversions import zio.schema._ +import zio.http.datastar.{EndpointRequest => ER} import zio.http.template2.Dom.AttributeValue import zio.http.template2._ @@ -217,6 +218,11 @@ trait Attributes { */ final def dataText: DatastarAttribute = DatastarAttribute(s"$prefix-text") + /** + * Helper for creating Datastar fetch actions from endpoint definitions. + */ + final def dataFetch: PartialDataFetch = PartialDataFetch(prefix) + } object Attributes { @@ -795,4 +801,34 @@ object Attributes { case object Snake extends CaseModifier case object Pascal extends CaseModifier } + + /** + * Helper for creating Datastar fetch actions. + */ + final case class PartialDataFetch(prefix: String) { + /** + * Creates a GET request action. + */ + def get(url: String): ER = ER.get(url) + + /** + * Creates a POST request action. + */ + def post(url: String): ER = ER.post(url) + + /** + * Creates a PUT request action. + */ + def put(url: String): ER = ER.put(url) + + /** + * Creates a PATCH request action. + */ + def patch(url: String): ER = ER.patch(url) + + /** + * Creates a DELETE request action. + */ + def delete(url: String): ER = ER.delete(url) + } } diff --git a/zio-http-datastar-sdk/src/main/scala/zio/http/datastar/DatastarPackageBase.scala b/zio-http-datastar-sdk/src/main/scala/zio/http/datastar/DatastarPackageBase.scala index 04bbab2ab..50f141f39 100644 --- a/zio-http-datastar-sdk/src/main/scala/zio/http/datastar/DatastarPackageBase.scala +++ b/zio-http-datastar-sdk/src/main/scala/zio/http/datastar/DatastarPackageBase.scala @@ -91,6 +91,16 @@ trait DatastarPackageBase extends Attributes { ) } + implicit class EndpointRequestExtensions[PathInput, Input, Err, Output, Auth <: AuthType]( + endpoint: Endpoint[PathInput, Input, Err, Output, Auth], + ) { + /** + * Creates a builder for Datastar request expressions from this endpoint. + */ + def toDatastarRequest: zio.http.datastar.EndpointRequest.EndpointRequestBuilder[PathInput, Input, Err, Output, Auth] = + zio.http.datastar.EndpointRequest.fromEndpoint(endpoint) + } + implicit def signalUpdateToModifier[A](signalUpdate: SignalUpdate[A]): Modifier = dataSignals(signalUpdate.signal)(signalUpdate.signal.schema) := signalUpdate.toExpression diff --git a/zio-http-datastar-sdk/src/main/scala/zio/http/datastar/EndpointRequest.scala b/zio-http-datastar-sdk/src/main/scala/zio/http/datastar/EndpointRequest.scala new file mode 100644 index 000000000..9a9c068b7 --- /dev/null +++ b/zio-http-datastar-sdk/src/main/scala/zio/http/datastar/EndpointRequest.scala @@ -0,0 +1,266 @@ +package zio.http.datastar + +import scala.language.implicitConversions + +import zio.Chunk + +import zio.http._ +import zio.http.codec._ +import zio.http.endpoint.{AuthType, Endpoint} +import zio.http.template2._ + +/** + * Represents a Datastar request action that can be used with data-on-* + * attributes to make HTTP requests to endpoints. + */ +sealed trait EndpointRequest { + def method: Method + def url: String + def headers: Map[String, String] + def includeHeaders: Boolean + def onlyIfMissing: Boolean + + /** + * Returns the Datastar action expression for this request. + * Example: @get('/api/users') + */ + def toActionExpression: Js + + /** + * Returns the full Datastar fetch expression with all options. + * Example: @get('/api/users', headers: {}) + */ + def toFetchExpression: Js = { + val parts = scala.collection.mutable.ListBuffer[String]() + + if (headers.nonEmpty) { + val headerStr = headers.map { case (k, v) => s"'$k': '$v'" }.mkString("{", ", ", "}") + parts += s"headers: $headerStr" + } + + if (includeHeaders) { + parts += "includeHeaders: true" + } + + if (onlyIfMissing) { + parts += "onlyIfMissing: true" + } + + val options = if (parts.nonEmpty) s", ${parts.mkString(", ")}" else "" + js"@${method.toString().toLowerCase}('$url'$options)" + } + + /** + * Sets whether to include request headers from the current page. + */ + def withIncludeHeaders(include: Boolean = true): EndpointRequest + + /** + * Sets whether to only fetch if the signal is missing. + */ + def withOnlyIfMissing(only: Boolean = true): EndpointRequest + + /** + * Adds a custom header to the request. + */ + def withHeader(key: String, value: String): EndpointRequest + + /** + * Adds multiple custom headers to the request. + */ + def withHeaders(newHeaders: Map[String, String]): EndpointRequest +} + +object EndpointRequest { + + private final case class EndpointRequestImpl( + method: Method, + url: String, + headers: Map[String, String] = Map.empty, + includeHeaders: Boolean = false, + onlyIfMissing: Boolean = false, + ) extends EndpointRequest { + + override def toActionExpression: Js = js"@${method.toString().toLowerCase}('$url')" + + override def withIncludeHeaders(include: Boolean): EndpointRequest = + copy(includeHeaders = include) + + override def withOnlyIfMissing(only: Boolean): EndpointRequest = + copy(onlyIfMissing = only) + + override def withHeader(key: String, value: String): EndpointRequest = + copy(headers = headers + (key -> value)) + + override def withHeaders(newHeaders: Map[String, String]): EndpointRequest = + copy(headers = headers ++ newHeaders) + } + + /** + * Creates a Datastar request action from an endpoint definition. + */ + def fromEndpoint[PathInput, Input, Err, Output, Auth <: AuthType]( + endpoint: Endpoint[PathInput, Input, Err, Output, Auth], + ): EndpointRequestBuilder[PathInput, Input, Err, Output, Auth] = + EndpointRequestBuilder(endpoint) + + /** + * Creates a GET request to the specified URL. + */ + def get(url: String): EndpointRequest = + EndpointRequestImpl(Method.GET, url) + + /** + * Creates a POST request to the specified URL. + */ + def post(url: String): EndpointRequest = + EndpointRequestImpl(Method.POST, url) + + /** + * Creates a PUT request to the specified URL. + */ + def put(url: String): EndpointRequest = + EndpointRequestImpl(Method.PUT, url) + + /** + * Creates a PATCH request to the specified URL. + */ + def patch(url: String): EndpointRequest = + EndpointRequestImpl(Method.PATCH, url) + + /** + * Creates a DELETE request to the specified URL. + */ + def delete(url: String): EndpointRequest = + EndpointRequestImpl(Method.DELETE, url) + + /** + * Builder for creating endpoint requests with parameter substitution. + */ + final case class EndpointRequestBuilder[PathInput, Input, Err, Output, Auth <: AuthType]( + endpoint: Endpoint[PathInput, Input, Err, Output, Auth], + headers: Map[String, String] = Map.empty, + includeHeaders: Boolean = false, + onlyIfMissing: Boolean = false, + ) { + + /** + * Builds the request with path and query parameters substituted. + * Path parameters are extracted from the endpoint's route pattern. + */ + def build(pathParams: PathInput = null.asInstanceOf[PathInput]): EndpointRequest = { + val method = extractMethod(endpoint.route) + val path = buildPath(endpoint.route, pathParams) + + EndpointRequestImpl( + method = method, + url = path, + headers = headers, + includeHeaders = includeHeaders, + onlyIfMissing = onlyIfMissing, + ) + } + + /** + * Builds the request with path parameters as signals (for dynamic substitution). + */ + def buildWithSignals(pathParams: String => String = identity): EndpointRequest = { + val method = extractMethod(endpoint.route) + val path = buildPathWithSignals(endpoint.route, pathParams) + + EndpointRequestImpl( + method = method, + url = path, + headers = headers, + includeHeaders = includeHeaders, + onlyIfMissing = onlyIfMissing, + ) + } + + /** + * Sets whether to include request headers from the current page. + */ + def withIncludeHeaders(include: Boolean = true): EndpointRequestBuilder[PathInput, Input, Err, Output, Auth] = + copy(includeHeaders = include) + + /** + * Sets whether to only fetch if the signal is missing. + */ + def withOnlyIfMissing(only: Boolean = true): EndpointRequestBuilder[PathInput, Input, Err, Output, Auth] = + copy(onlyIfMissing = only) + + /** + * Adds a custom header to the request. + */ + def withHeader(key: String, value: String): EndpointRequestBuilder[PathInput, Input, Err, Output, Auth] = + copy(headers = headers + (key -> value)) + + /** + * Adds multiple custom headers to the request. + */ + def withHeaders(newHeaders: Map[String, String]): EndpointRequestBuilder[PathInput, Input, Err, Output, Auth] = + copy(headers = headers ++ newHeaders) + + private def extractMethod(route: RoutePattern[_]): Method = { + route.method + } + + private def buildPath(route: RoutePattern[_], pathParams: PathInput): String = { + if (pathParams == null) { + // Build path without parameter substitution (will use placeholder format) + "/" + route.pathCodec.segments + .map { segment => + val segmentStr = segment.toString + if (segmentStr.startsWith("Literal(")) { + extractLiteralValue(segmentStr) + } else { + s"{${extractSegmentName(segmentStr)}}" + } + } + .mkString("/") + } else { + // Build path with actual parameter values + val formatted = endpoint.route.asInstanceOf[RoutePattern[PathInput]].format(pathParams) + formatted match { + case Right(path) => "/" + path.encode.dropWhile(_ == '/') + case Left(_) => "/" // fallback to root if formatting fails + } + } + } + + private def buildPathWithSignals(route: RoutePattern[_], signalMapper: String => String): String = { + "/" + route.pathCodec.segments + .map { segment => + val segmentStr = segment.toString + if (segmentStr.startsWith("Literal(")) { + extractLiteralValue(segmentStr) + } else { + val name = extractSegmentName(segmentStr) + s"$${${signalMapper(name)}}" + } + } + .mkString("/") + } + + private def extractLiteralValue(literalStr: String): String = { + // Extract the string value from Literal(...) + literalStr.stripPrefix("Literal(").stripSuffix(")").trim + } + + private def extractSegmentName(segmentStr: String): String = { + // Extract parameter name from segment description + // This is a simplified approach - may need adjustment based on actual PathCodec structure + val name = if (segmentStr.contains("(")) { + segmentStr.substring(segmentStr.indexOf("(") + 1, segmentStr.lastIndexOf(")")) + } else { + segmentStr + } + // Remove quotes if present + name.replaceAll("\"", "").replaceAll("'", "") + } + } + + implicit def endpointRequestToJs(request: EndpointRequest): Js = + request.toActionExpression +} + diff --git a/zio-http-datastar-sdk/src/test/scala/zio/http/datastar/EndpointRequestSpec.scala b/zio-http-datastar-sdk/src/test/scala/zio/http/datastar/EndpointRequestSpec.scala new file mode 100644 index 000000000..61fbd7efd --- /dev/null +++ b/zio-http-datastar-sdk/src/test/scala/zio/http/datastar/EndpointRequestSpec.scala @@ -0,0 +1,316 @@ +package zio.http.datastar + +import zio._ +import zio.test._ + +import zio.schema.{DeriveSchema, Schema} + +import zio.http._ +import zio.http.codec._ +import zio.http.endpoint._ +import zio.http.template2._ + +object EndpointRequestSpec extends ZIOSpecDefault { + + case class User(id: Int, name: String) + object User { + implicit val schema: Schema[User] = DeriveSchema.gen[User] + } + + case class CreateUserRequest(name: String, email: String) + object CreateUserRequest { + implicit val schema: Schema[CreateUserRequest] = DeriveSchema.gen[CreateUserRequest] + } + + override def spec = suite("EndpointRequestSpec")( + suite("basic request creation")( + test("should create GET request") { + val request = EndpointRequest.get("/api/users") + assertTrue( + request.method == Method.GET, + request.url == "/api/users", + request.toActionExpression.value == "@get('/api/users')", + ) + }, + test("should create POST request") { + val request = EndpointRequest.post("/api/users") + assertTrue( + request.method == Method.POST, + request.url == "/api/users", + request.toActionExpression.value == "@post('/api/users')", + ) + }, + test("should create PUT request") { + val request = EndpointRequest.put("/api/users/1") + assertTrue( + request.method == Method.PUT, + request.url == "/api/users/1", + request.toActionExpression.value == "@put('/api/users/1')", + ) + }, + test("should create PATCH request") { + val request = EndpointRequest.patch("/api/users/1") + assertTrue( + request.method == Method.PATCH, + request.url == "/api/users/1", + request.toActionExpression.value == "@patch('/api/users/1')", + ) + }, + test("should create DELETE request") { + val request = EndpointRequest.delete("/api/users/1") + assertTrue( + request.method == Method.DELETE, + request.url == "/api/users/1", + request.toActionExpression.value == "@delete('/api/users/1')", + ) + }, + ), + suite("request with headers")( + test("should add single header") { + val request = EndpointRequest + .get("/api/users") + .withHeader("Authorization", "Bearer token") + assertTrue( + request.headers.contains("Authorization"), + request.headers("Authorization") == "Bearer token", + request.toFetchExpression.value.contains("headers:"), + ) + }, + test("should add multiple headers") { + val request = EndpointRequest + .get("/api/users") + .withHeaders(Map("Authorization" -> "Bearer token", "X-Custom" -> "value")) + assertTrue( + request.headers.size == 2, + request.headers("Authorization") == "Bearer token", + request.headers("X-Custom") == "value", + ) + }, + test("should render fetch expression with headers") { + val request = EndpointRequest + .get("/api/users") + .withHeader("X-Custom", "test") + val expr = request.toFetchExpression.value + assertTrue( + expr.contains("@get('/api/users',"), + expr.contains("headers: {'X-Custom': 'test'}"), + ) + }, + ), + suite("request options")( + test("should set includeHeaders option") { + val request = EndpointRequest.get("/api/users").withIncludeHeaders(true) + assertTrue( + request.includeHeaders == true, + request.toFetchExpression.value.contains("includeHeaders: true"), + ) + }, + test("should set onlyIfMissing option") { + val request = EndpointRequest.get("/api/users").withOnlyIfMissing(true) + assertTrue( + request.onlyIfMissing == true, + request.toFetchExpression.value.contains("onlyIfMissing: true"), + ) + }, + test("should combine multiple options") { + val request = EndpointRequest + .get("/api/users") + .withIncludeHeaders(true) + .withOnlyIfMissing(true) + .withHeader("X-Custom", "value") + val expr = request.toFetchExpression.value + assertTrue( + expr.contains("headers:"), + expr.contains("includeHeaders: true"), + expr.contains("onlyIfMissing: true"), + ) + }, + ), + suite("endpoint request builder")( + test("should create request from simple GET endpoint") { + val endpoint = Endpoint(RoutePattern.GET / "api" / "users") + val request = endpoint.toDatastarRequest.build() + assertTrue( + request.method == Method.GET, + request.url.contains("api"), + request.url.contains("users"), + ) + }, + test("should create request from POST endpoint") { + val endpoint = Endpoint(RoutePattern.POST / "api" / "users") + val request = endpoint.toDatastarRequest.build() + assertTrue( + request.method == Method.POST, + request.toActionExpression.value.startsWith("@post("), + ) + }, + test("should create request from endpoint with path parameter") { + val endpoint = Endpoint(RoutePattern.GET / "api" / "users" / PathCodec.int("userId")) + val request = endpoint.toDatastarRequest.build(42) + assertTrue( + request.method == Method.GET, + request.url.contains("42"), + ) + }, + test("should create request with signals from endpoint") { + val endpoint = Endpoint(RoutePattern.GET / "api" / "users" / PathCodec.int("userId")) + val request = endpoint.toDatastarRequest.buildWithSignals() + assertTrue( + request.method == Method.GET, + request.url.contains("$"), + ) + }, + test("should support builder options on endpoint requests") { + val endpoint = Endpoint(RoutePattern.GET / "api" / "users") + val request = endpoint.toDatastarRequest + .withIncludeHeaders(true) + .withHeader("X-Custom", "test") + .build() + assertTrue( + request.includeHeaders == true, + request.headers.contains("X-Custom"), + ) + }, + ), + suite("integration with data-on attributes")( + test("should work with data-on-click") { + val request = EndpointRequest.get("/api/users") + val button = div(dataOn.click := request)("Load Users") + val html = button.render + assertTrue( + html.contains("data-on-click"), + html.contains("@get") && html.contains("/api/users"), + ) + }, + test("should work with data-on-click and endpoint") { + val endpoint = Endpoint(RoutePattern.GET / "api" / "users") + val request = endpoint.toDatastarRequest.build() + val button = div(dataOn.click := request)("Load Users") + val html = button.render + assertTrue( + html.contains("data-on-click"), + html.contains("@get("), + ) + }, + test("should work with data-on-submit") { + val request = EndpointRequest.post("/api/users") + val form = div(dataOn.submit.prevent := request) + val html = form.render + assertTrue( + html.contains("data-on-submit__prevent"), + html.contains("@post") && html.contains("/api/users"), + ) + }, + test("should work with event modifiers") { + val request = EndpointRequest.get("/api/users") + val button = div( + dataOn.click.debounce(300.millis).prevent := request, + )("Load") + val html = button.render + assertTrue( + html.contains("data-on-click__debounce.300ms__prevent"), + html.contains("@get") && html.contains("/api/users"), + ) + }, + ), + suite("dataFetch helper")( + test("should create GET request via dataFetch") { + val request = dataFetch.get("/api/users") + assertTrue( + request.method == Method.GET, + request.url == "/api/users", + ) + }, + test("should create POST request via dataFetch") { + val request = dataFetch.post("/api/users") + assertTrue( + request.method == Method.POST, + request.url == "/api/users", + ) + }, + test("should work with data-on-click") { + val button = div(dataOn.click := dataFetch.get("/api/users"))("Load") + val html = button.render + assertTrue( + html.contains("data-on-click"), + html.contains("@get") && html.contains("/api/users"), + ) + }, + ), + suite("complex endpoint scenarios")( + test("should handle endpoint with multiple path segments") { + val endpoint = Endpoint(RoutePattern.GET / "api" / "v1" / "users" / PathCodec.int("id")) + val request = endpoint.toDatastarRequest.build(123) + assertTrue( + request.url.contains("api"), + request.url.contains("v1"), + request.url.contains("users"), + request.url.contains("123"), + ) + }, + test("should handle endpoint with string path parameter") { + val endpoint = Endpoint(RoutePattern.GET / "api" / "users" / PathCodec.string("username")) + val request = endpoint.toDatastarRequest.build("john") + assertTrue( + request.url.contains("john"), + ) + }, + test("should handle PUT endpoint with path parameter") { + val endpoint = Endpoint(RoutePattern.PUT / "api" / "users" / PathCodec.int("id")) + val request = endpoint.toDatastarRequest.build(42) + assertTrue( + request.method == Method.PUT, + request.url.contains("42"), + request.toActionExpression.value.contains("@put("), + ) + }, + test("should handle DELETE endpoint with path parameter") { + val endpoint = Endpoint(RoutePattern.DELETE / "api" / "users" / PathCodec.int("id")) + val request = endpoint.toDatastarRequest.build(99) + assertTrue( + request.method == Method.DELETE, + request.url.contains("99"), + request.toActionExpression.value.contains("@delete("), + ) + }, + ), + suite("rendering in HTML")( + test("should render complete div with fetch") { + val button = div( + dataOn.click := dataFetch.get("/api/users"), + )("Load Users") + val html = button.render + assertTrue( + html.contains(""), + ) + }, + test("should render div with POST request") { + val form = div( + dataOn.submit.prevent := dataFetch.post("/api/users"), + )("Create") + val html = form.render + assertTrue( + html.contains(""), + ) + }, + test("should render div with DELETE request") { + val link = div( + dataOn.click.prevent := dataFetch.delete("/api/users/1"), + )("Delete") + val html = link.render + assertTrue( + html.contains(""), + ) + }, + ), + ) +} + diff --git a/zio-http-example/src/main/scala/example/endpoint/DatastarEndpointRequestExample.scala b/zio-http-example/src/main/scala/example/endpoint/DatastarEndpointRequestExample.scala new file mode 100644 index 000000000..c2389ede2 --- /dev/null +++ b/zio-http-example/src/main/scala/example/endpoint/DatastarEndpointRequestExample.scala @@ -0,0 +1,109 @@ +package example.endpoint + +import zio._ +import zio.http._ +import zio.http.codec._ +import zio.http.endpoint._ +import zio.http.codec.PathCodec + +/** + * This example demonstrates building Datastar fetch expressions from ZIO HTTP Endpoint definitions. + * + * Run this example from the root project: + * {{{ + * sbt "zioHttpExample/runMain example.endpoint.DatastarEndpointRequestExample" + * }}} + * + * Then visit: http://localhost:8080/ + */ +object DatastarEndpointRequestExample extends ZIOAppDefault { + + // Home page with examples + def renderHomePage(): String = { + s""" + | + | + | Datastar Endpoint Request Example + | + | + | + |

Datastar Endpoint Request Example

+ | + |

What this demonstrates:

+ |

This example shows how to generate Datastar fetch expressions from ZIO HTTP Endpoint definitions.

+ | + |

Example Code:

+ |
import zio.http._
+       |import zio.http.endpoint._
+       |import zio.http.codec.PathCodec
+       |
+       |// Define an endpoint
+       |val endpoint = Endpoint(RoutePattern.GET / "api" / "users")
+       |
+       |// Generate Datastar request expression using toDatastarRequest
+       |val request = endpoint.toDatastarRequest.build()
+       |// Result: @get('/api/users')
+       |
+       |// With path parameters - substitute with actual values
+       |val userEndpoint = Endpoint(RoutePattern.GET / "api" / "users" / PathCodec.int("id"))
+       |val userRequest = userEndpoint.toDatastarRequest.build(42)
+       |// Result: @get('/api/users/42')
+       |
+       |// With path parameters - use signals for dynamic values
+       |val deleteRequest = userEndpoint.toDatastarRequest.buildWithSignals()
+       |// Result: @get('/api/users/${'$'}{id}')
+       |
+       |// Using dataFetch helper (shorthand)
+       |import zio.http.datastar._
+       |val fetchRequest = dataFetch.get("/api/users")
+       |// Result: @get('/api/users')
+       |
+       |// With options
+       |val advancedRequest = endpoint.toDatastarRequest
+       |  .withIncludeHeaders(true)
+       |  .withHeader("X-Custom", "value")
+       |  .withOnlyIfMissing(true)
+       |  .build()
+       |// Result: @get('/api/users', {headers: {...}, onlyIfMissing: true})
+ | + |

Available Methods:

+ |
    + |
  • toDatastarRequest - Main method to convert Endpoint to Datastar request
  • + |
  • build() - Generate request without path parameters
  • + |
  • build(params) - Generate request with actual path parameter values
  • + |
  • buildWithSignals() - Generate request with signal placeholders for dynamic values
  • + |
  • withHeader(key, value) - Add custom headers
  • + |
  • withIncludeHeaders(true) - Include all headers in request
  • + |
  • with OnlyIfMissing(true) - Only fetch if data doesn't exist
  • + |
+ | + |

HTTP Methods Supported:

+ |
    + |
  • GET - generates @get(...)
  • + |
  • POST - generates @post(...)
  • + |
  • PUT - generates @put(...)
  • + |
  • PATCH - generates @patch(...)
  • + |
  • DELETE - generates @delete(...)
  • + |
+ | + |

Usage in HTML with Datastar:

+ |
<button data-on-click="@get('/api/users')">Load Users</button>
+       |<form data-on-submit.prevent="@post('/api/users')">...</form>
+       |<button data-on-click="@delete('/api/users/${'$'}{userId}')">Delete</button>
+ | + | + | + |""".stripMargin + } + + val homeRoute = Routes( + Method.GET / "" -> handler(Response(body = Body.fromString(renderHomePage()), headers = Headers(Header.ContentType(MediaType.text.html)))), + ) + + def run = Server.serve(homeRoute).provide(Server.default) +} From dee89b471463988b54095329355585bba8f74093 Mon Sep 17 00:00:00 2001 From: aliraza556 Date: Thu, 16 Oct 2025 14:10:26 +0500 Subject: [PATCH 2/2] fix format issue --- .../scala/zio/http/datastar/Attributes.scala | 1 + .../http/datastar/DatastarPackageBase.scala | 4 +- .../zio/http/datastar/EndpointRequest.scala | 53 +++++++++---------- .../http/datastar/EndpointRequestSpec.scala | 1 - .../DatastarEndpointRequestExample.scala | 21 ++++---- 5 files changed, 41 insertions(+), 39 deletions(-) diff --git a/zio-http-datastar-sdk/src/main/scala/zio/http/datastar/Attributes.scala b/zio-http-datastar-sdk/src/main/scala/zio/http/datastar/Attributes.scala index dd878fefb..c6dd3de3e 100644 --- a/zio-http-datastar-sdk/src/main/scala/zio/http/datastar/Attributes.scala +++ b/zio-http-datastar-sdk/src/main/scala/zio/http/datastar/Attributes.scala @@ -806,6 +806,7 @@ object Attributes { * Helper for creating Datastar fetch actions. */ final case class PartialDataFetch(prefix: String) { + /** * Creates a GET request action. */ diff --git a/zio-http-datastar-sdk/src/main/scala/zio/http/datastar/DatastarPackageBase.scala b/zio-http-datastar-sdk/src/main/scala/zio/http/datastar/DatastarPackageBase.scala index 50f141f39..2ac65f25c 100644 --- a/zio-http-datastar-sdk/src/main/scala/zio/http/datastar/DatastarPackageBase.scala +++ b/zio-http-datastar-sdk/src/main/scala/zio/http/datastar/DatastarPackageBase.scala @@ -94,10 +94,12 @@ trait DatastarPackageBase extends Attributes { implicit class EndpointRequestExtensions[PathInput, Input, Err, Output, Auth <: AuthType]( endpoint: Endpoint[PathInput, Input, Err, Output, Auth], ) { + /** * Creates a builder for Datastar request expressions from this endpoint. */ - def toDatastarRequest: zio.http.datastar.EndpointRequest.EndpointRequestBuilder[PathInput, Input, Err, Output, Auth] = + def toDatastarRequest + : zio.http.datastar.EndpointRequest.EndpointRequestBuilder[PathInput, Input, Err, Output, Auth] = zio.http.datastar.EndpointRequest.fromEndpoint(endpoint) } diff --git a/zio-http-datastar-sdk/src/main/scala/zio/http/datastar/EndpointRequest.scala b/zio-http-datastar-sdk/src/main/scala/zio/http/datastar/EndpointRequest.scala index 9a9c068b7..8f62b1d97 100644 --- a/zio-http-datastar-sdk/src/main/scala/zio/http/datastar/EndpointRequest.scala +++ b/zio-http-datastar-sdk/src/main/scala/zio/http/datastar/EndpointRequest.scala @@ -21,27 +21,26 @@ sealed trait EndpointRequest { def onlyIfMissing: Boolean /** - * Returns the Datastar action expression for this request. - * Example: @get('/api/users') + * Returns the Datastar action expression for this request. Example: @get('/api/users') */ def toActionExpression: Js /** - * Returns the full Datastar fetch expression with all options. - * Example: @get('/api/users', headers: {}) + * Returns the full Datastar fetch expression with all options. Example: @get('/api/users', + * headers: {}) */ def toFetchExpression: Js = { val parts = scala.collection.mutable.ListBuffer[String]() - + if (headers.nonEmpty) { val headerStr = headers.map { case (k, v) => s"'$k': '$v'" }.mkString("{", ", ", "}") parts += s"headers: $headerStr" } - + if (includeHeaders) { parts += "includeHeaders: true" } - + if (onlyIfMissing) { parts += "onlyIfMissing: true" } @@ -145,8 +144,8 @@ object EndpointRequest { ) { /** - * Builds the request with path and query parameters substituted. - * Path parameters are extracted from the endpoint's route pattern. + * Builds the request with path and query parameters substituted. Path + * parameters are extracted from the endpoint's route pattern. */ def build(pathParams: PathInput = null.asInstanceOf[PathInput]): EndpointRequest = { val method = extractMethod(endpoint.route) @@ -162,7 +161,8 @@ object EndpointRequest { } /** - * Builds the request with path parameters as signals (for dynamic substitution). + * Builds the request with path parameters as signals (for dynamic + * substitution). */ def buildWithSignals(pathParams: String => String = identity): EndpointRequest = { val method = extractMethod(endpoint.route) @@ -208,15 +208,14 @@ object EndpointRequest { private def buildPath(route: RoutePattern[_], pathParams: PathInput): String = { if (pathParams == null) { // Build path without parameter substitution (will use placeholder format) - "/" + route.pathCodec.segments - .map { segment => - val segmentStr = segment.toString - if (segmentStr.startsWith("Literal(")) { - extractLiteralValue(segmentStr) - } else { - s"{${extractSegmentName(segmentStr)}}" - } + "/" + route.pathCodec.segments.map { segment => + val segmentStr = segment.toString + if (segmentStr.startsWith("Literal(")) { + extractLiteralValue(segmentStr) + } else { + s"{${extractSegmentName(segmentStr)}}" } + } .mkString("/") } else { // Build path with actual parameter values @@ -229,16 +228,15 @@ object EndpointRequest { } private def buildPathWithSignals(route: RoutePattern[_], signalMapper: String => String): String = { - "/" + route.pathCodec.segments - .map { segment => - val segmentStr = segment.toString - if (segmentStr.startsWith("Literal(")) { - extractLiteralValue(segmentStr) - } else { - val name = extractSegmentName(segmentStr) - s"$${${signalMapper(name)}}" - } + "/" + route.pathCodec.segments.map { segment => + val segmentStr = segment.toString + if (segmentStr.startsWith("Literal(")) { + extractLiteralValue(segmentStr) + } else { + val name = extractSegmentName(segmentStr) + s"$${${signalMapper(name)}}" } + } .mkString("/") } @@ -263,4 +261,3 @@ object EndpointRequest { implicit def endpointRequestToJs(request: EndpointRequest): Js = request.toActionExpression } - diff --git a/zio-http-datastar-sdk/src/test/scala/zio/http/datastar/EndpointRequestSpec.scala b/zio-http-datastar-sdk/src/test/scala/zio/http/datastar/EndpointRequestSpec.scala index 61fbd7efd..79f642ba5 100644 --- a/zio-http-datastar-sdk/src/test/scala/zio/http/datastar/EndpointRequestSpec.scala +++ b/zio-http-datastar-sdk/src/test/scala/zio/http/datastar/EndpointRequestSpec.scala @@ -313,4 +313,3 @@ object EndpointRequestSpec extends ZIOSpecDefault { ), ) } - diff --git a/zio-http-example/src/main/scala/example/endpoint/DatastarEndpointRequestExample.scala b/zio-http-example/src/main/scala/example/endpoint/DatastarEndpointRequestExample.scala index c2389ede2..10fdc35c3 100644 --- a/zio-http-example/src/main/scala/example/endpoint/DatastarEndpointRequestExample.scala +++ b/zio-http-example/src/main/scala/example/endpoint/DatastarEndpointRequestExample.scala @@ -1,25 +1,24 @@ package example.endpoint import zio._ + import zio.http._ import zio.http.codec._ import zio.http.endpoint._ -import zio.http.codec.PathCodec /** - * This example demonstrates building Datastar fetch expressions from ZIO HTTP Endpoint definitions. + * This example demonstrates building Datastar fetch expressions from ZIO HTTP + * Endpoint definitions. * - * Run this example from the root project: - * {{{ - * sbt "zioHttpExample/runMain example.endpoint.DatastarEndpointRequestExample" - * }}} + * Run this example from the root project: {{{ sbt "zioHttpExample/runMain + * example.endpoint.DatastarEndpointRequestExample" }}} * * Then visit: http://localhost:8080/ */ object DatastarEndpointRequestExample extends ZIOAppDefault { // Home page with examples - def renderHomePage(): String = { + def renderHomePage(): String = s""" | | @@ -99,10 +98,14 @@ object DatastarEndpointRequestExample extends ZIOAppDefault { | | |""".stripMargin - } val homeRoute = Routes( - Method.GET / "" -> handler(Response(body = Body.fromString(renderHomePage()), headers = Headers(Header.ContentType(MediaType.text.html)))), + Method.GET / "" -> handler( + Response( + body = Body.fromString(renderHomePage()), + headers = Headers(Header.ContentType(MediaType.text.html)), + ), + ), ) def run = Server.serve(homeRoute).provide(Server.default)