Skip to content

Commit b9b1d39

Browse files
authored
Datastar extension methods for Endpoint (#3736) (#3745)
1 parent bcb0140 commit b9b1d39

File tree

11 files changed

+204
-132
lines changed

11 files changed

+204
-132
lines changed

.github/workflows/pr-automation.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
name: PR Automation
22

33
on:
4-
pull_request:
4+
pull_request_target:
55
types: [opened, synchronize, reopened, edited]
66
schedule:
77
# Run daily at 00:00 UTC to check for stale PRs

zio-http-datastar-sdk/src/main/scala/zio/http/datastar/Attributes.scala

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import scala.language.implicitConversions
66

77
import zio.schema._
88

9+
import zio.http.template2.Dom.AttributeValue
910
import zio.http.template2._
1011

1112
trait Attributes {
@@ -165,14 +166,14 @@ trait Attributes {
165166
* [[https://data-star.dev/reference/attributes#data-preserve-attr]]
166167
*/
167168
final def dataPreserveAttr(attribute: Dom.Attribute, attributes: Dom.Attribute*): Dom.Attribute =
168-
Dom.attr(s"$prefix-preserve-attr", (attribute +: attributes).map(_.name).mkString(" "))
169+
Dom.attr(s"$prefix-preserve-attr", AttributeValue.StringValue((attribute +: attributes).map(_.name).mkString(" ")))
169170

170171
/**
171172
* data-preserve-attr – Preserve attributes during morphing. Doc:
172173
* [[https://data-star.dev/reference/attributes#data-preserve-attr]]
173174
*/
174175
final def dataPreserveAttr(attribute: String, attributes: String*): Dom.Attribute =
175-
Dom.attr(s"$prefix-preserve-attr", (attribute +: attributes).mkString(" "))
176+
Dom.attr(s"$prefix-preserve-attr", AttributeValue.StringValue((attribute +: attributes).mkString(" ")))
176177

177178
/**
178179
* data-ref – Assign a local reference. Doc:
@@ -237,7 +238,7 @@ object Attributes {
237238

238239
final case class Single(prefix: String, className: String, caseModifier: CaseModifier = defaultCaseModifier)
239240
extends DataClass {
240-
assert(className.nonEmpty, "Class name cannot be empty")
241+
assert(!className.isBlank, "Class name cannot be empty")
241242
private[datastar] val full = s"$prefix-class-$className${caseModifier.suffix(defaultCaseModifier)}"
242243

243244
def :=(signal: Signal[_]): CompleteAttribute = Dom.attr(full) := signal.ref

zio-http-datastar-sdk/src/main/scala/zio/http/datastar/DatastarEvent.scala

Lines changed: 24 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import zio._
44

55
import zio.http.ServerSentEvent
66
import zio.http.datastar.ServerSentEventGenerator.DefaultRetryDelay
7+
import zio.http.template2.Dom.AttributeValue
78
import zio.http.template2._
89

910
sealed trait DatastarEvent {
@@ -15,13 +16,17 @@ sealed trait DatastarEvent {
1516
object DatastarEvent {
1617

1718
final case class PatchElements(
18-
elements: Iterable[Dom],
19+
elements: Dom,
1920
selector: Option[CssSelector] = None,
2021
mode: ElementPatchMode = ElementPatchMode.Outer,
2122
useViewTransition: Boolean = false,
2223
eventId: Option[String] = None,
2324
retryDuration: Duration = 1000.millis,
2425
) extends DatastarEvent {
26+
assert(
27+
mode != ElementPatchMode.Remove || (selector.nonEmpty && elements.isEmpty),
28+
"When using mode 'remove', 'selector' must be defined and 'elements' must be empty",
29+
)
2530
override val eventType: EventType = EventType.PatchElements
2631
override def toServerSentEvent: ServerSentEvent[String] = {
2732
val sb = new StringBuilder()
@@ -36,13 +41,7 @@ object DatastarEvent {
3641
sb.append("useViewTransition true\n")
3742
}
3843

39-
elements.foreach { d =>
40-
val rendered = d.render
41-
if (rendered.contains('\n'))
42-
rendered.split('\n').foreach(line => sb.append("elements ").append(line).append('\n'))
43-
else
44-
sb.append("elements ").append(rendered).append('\n')
45-
}
44+
sb.append("elements ").append(elements.renderMinified).append('\n')
4645

4746
val retry = if (retryDuration != DefaultRetryDelay) Some(retryDuration) else None
4847
ServerSentEvent(sb.toString(), Some(eventType.render), eventId, retry)
@@ -159,8 +158,10 @@ object DatastarEvent {
159158
script0: Dom.Element.Script,
160159
options: ExecuteScriptOptions,
161160
): ExecuteScript = {
162-
val removeAttr = if (options.autoRemove) Dom.attr("data-effect", "el.remove") else Dom.empty
163-
val scriptWithAttrs = script0(removeAttr)(options.attributes.map(a => Dom.attr(a._1, a._2)))
161+
val removeAttr =
162+
if (options.autoRemove) Dom.attr("data-effect", AttributeValue.StringValue("el.remove")) else Dom.empty
163+
val scriptWithAttrs =
164+
script0(removeAttr)(options.attributes.map(a => Dom.attr(a._1, AttributeValue.StringValue(a._2))))
164165

165166
ExecuteScript(
166167
script = scriptWithAttrs,
@@ -200,7 +201,7 @@ object DatastarEvent {
200201
patchElements(elements, PatchElementOptions.default)
201202

202203
def patchElements(elements: String, options: PatchElementOptions): PatchElements =
203-
patchElements(elements.split('\n').map(Dom.raw).toList, options)
204+
patchElements(Dom.raw(elements), options)
204205

205206
def patchElements(elements: String, selector: Option[CssSelector]): PatchElements =
206207
patchElements(elements, PatchElementOptions(selector = selector))
@@ -239,10 +240,17 @@ object DatastarEvent {
239240
patchElements(elements, PatchElementOptions(selector, mode, useViewTransition, eventId, retryDuration))
240241

241242
def patchElements(element: Dom): PatchElements =
242-
patchElements(List(element), PatchElementOptions.default)
243+
patchElements(element, PatchElementOptions.default)
243244

244245
def patchElements(element: Dom, options: PatchElementOptions): PatchElements =
245-
patchElements(List(element), options)
246+
patchElements(
247+
element,
248+
options.selector,
249+
options.mode,
250+
options.useViewTransition,
251+
options.eventId,
252+
options.retryDuration,
253+
)
246254

247255
def patchElements(element: Dom, selector: Option[CssSelector]): PatchElements =
248256
patchElements(element, PatchElementOptions(selector = selector))
@@ -275,63 +283,13 @@ object DatastarEvent {
275283
eventId: Option[String],
276284
retryDuration: Duration,
277285
): PatchElements =
278-
patchElements(element, PatchElementOptions(selector, mode, useViewTransition, eventId, retryDuration))
279-
280-
def patchElements(elements: Iterable[Dom]): PatchElements =
281-
patchElements(elements, PatchElementOptions.default)
282-
283-
def patchElements(elements: Iterable[Dom], options: PatchElementOptions): PatchElements = {
284-
PatchElements(
285-
elements = elements,
286-
selector = options.selector,
287-
mode = options.mode,
288-
useViewTransition = options.useViewTransition,
289-
eventId = options.eventId,
290-
retryDuration = options.retryDuration,
291-
)
292-
}
293-
294-
def patchElements(elements: Iterable[Dom], selector: Option[CssSelector]): PatchElements =
295-
patchElements(elements, PatchElementOptions(selector = selector))
296-
297-
def patchElements(elements: Iterable[Dom], selector: Option[CssSelector], mode: ElementPatchMode): PatchElements =
298-
patchElements(elements, PatchElementOptions(selector = selector, mode = mode))
299-
300-
def patchElements(
301-
elements: Iterable[Dom],
302-
selector: Option[CssSelector],
303-
mode: ElementPatchMode,
304-
useViewTransition: Boolean,
305-
): PatchElements =
306-
patchElements(
307-
elements,
308-
PatchElementOptions(selector = selector, mode = mode, useViewTransition = useViewTransition),
309-
)
310-
311-
def patchElements(
312-
elements: Iterable[Dom],
313-
selector: Option[CssSelector],
314-
mode: ElementPatchMode,
315-
useViewTransition: Boolean,
316-
eventId: Option[String],
317-
): PatchElements =
318-
patchElements(elements, PatchElementOptions(selector, mode, useViewTransition, eventId))
319-
320-
def patchElements(
321-
elements: Iterable[Dom],
322-
selector: Option[CssSelector],
323-
mode: ElementPatchMode,
324-
useViewTransition: Boolean,
325-
eventId: Option[String],
326-
retryDuration: Duration,
327-
): PatchElements =
328-
patchElements(elements, PatchElementOptions(selector, mode, useViewTransition, eventId, retryDuration))
286+
PatchElements(element, selector, mode, useViewTransition, eventId, retryDuration)
329287

330288
def patchSignals(signal: String): PatchSignals =
331-
patchSignals(Iterable(signal), PatchSignalOptions.default)
289+
patchSignals(List(signal), PatchSignalOptions.default)
332290

333291
def patchSignals(signal: String, options: PatchSignalOptions): PatchSignals =
334-
patchSignals(Iterable(signal), options)
292+
patchSignals(List(signal), options)
335293

336294
def patchSignals(signal: String, onlyIfMissing: Boolean): PatchSignals =
337295
patchSignals(signal, PatchSignalOptions(onlyIfMissing = onlyIfMissing))

zio-http-datastar-sdk/src/main/scala/zio/http/datastar/DatastarPackageBase.scala

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ import zio._
77
import zio.stream.ZStream
88

99
import zio.http._
10-
import zio.http.codec.HttpCodec
10+
import zio.http.codec.{Doc, HttpCodec, HttpContentCodec}
11+
import zio.http.endpoint.{AuthType, Endpoint}
1112
import zio.http.template2._
1213

1314
trait DatastarPackageBase extends Attributes {
@@ -32,6 +33,64 @@ trait DatastarPackageBase extends Attributes {
3233
HttpCodec.header(Header.CacheControl).const(Header.CacheControl.NoCache) ++
3334
HttpCodec.header(Header.Connection).const(Header.Connection.KeepAlive)
3435

36+
val datastarEventCodec =
37+
HttpCodec.content[Dom] ++
38+
HttpCodec
39+
.header(Header.ContentType)
40+
.const(Header.ContentType(MediaType.text.`html`)) ++
41+
HttpCodec.header(Header.CacheControl).const(Header.CacheControl.NoCache) ++
42+
HttpCodec.headerAs[CssSelector]("datastar-selector").optional ++
43+
HttpCodec.headerAs[ElementPatchMode]("datastar-mode").optional ++
44+
HttpCodec.headerAs[Boolean]("datastar-use-view-transition").optional
45+
46+
implicit class EndpointExtensions(endpoint: Endpoint.type) {
47+
def datastarEvents[Input](
48+
route: RoutePattern[Input],
49+
): Endpoint[Input, Input, ZNothing, ZStream[Any, Nothing, DatastarEvent], AuthType.None.type] =
50+
Endpoint(
51+
route,
52+
route.toHttpCodec,
53+
datastarCodec.transformOrFailLeft(_ => Left("Not implemented"))(_.map(_.toServerSentEvent)),
54+
HttpCodec.unused,
55+
HttpContentCodec.responseErrorCodec,
56+
Doc.empty,
57+
AuthType.None,
58+
)
59+
60+
def datastarEvent[Input](
61+
route: RoutePattern[Input],
62+
): Endpoint[Input, Input, ZNothing, DatastarEvent.PatchElements, AuthType.None.type] =
63+
Endpoint(
64+
route,
65+
route.toHttpCodec,
66+
datastarEventCodec.transformOrFailLeft[DatastarEvent.PatchElements](_ => Left("Not implemented"))(event =>
67+
(
68+
event.elements,
69+
event.selector,
70+
if (event.mode == ElementPatchMode.Outer) None else Some(event.mode),
71+
if (event.useViewTransition) Some(true) else None,
72+
),
73+
),
74+
HttpCodec.unused,
75+
HttpContentCodec.responseErrorCodec,
76+
Doc.empty,
77+
AuthType.None,
78+
)
79+
80+
def datastar[Input](
81+
route: RoutePattern[Input],
82+
): Endpoint[Input, Input, ZNothing, ZStream[Any, Nothing, ServerSentEvent[String]], AuthType.None.type] =
83+
Endpoint(
84+
route,
85+
route.toHttpCodec,
86+
datastarCodec,
87+
HttpCodec.unused,
88+
HttpContentCodec.responseErrorCodec,
89+
Doc.empty,
90+
AuthType.None,
91+
)
92+
}
93+
3594
implicit def signalUpdateToModifier[A](signalUpdate: SignalUpdate[A]): Modifier =
3695
dataSignals(signalUpdate.signal)(signalUpdate.signal.schema) := signalUpdate.toExpression
3796

0 commit comments

Comments
 (0)