From 45c71cbc6a3bf695203a1cdb176005f3d7bd1456 Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Fri, 27 Jan 2023 06:37:39 +0000 Subject: [PATCH 1/3] `Modifier` is `Contravariant` --- calico/src/main/scala/calico/html/Modifier.scala | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/calico/src/main/scala/calico/html/Modifier.scala b/calico/src/main/scala/calico/html/Modifier.scala index e4693a85..387cc164 100644 --- a/calico/src/main/scala/calico/html/Modifier.scala +++ b/calico/src/main/scala/calico/html/Modifier.scala @@ -18,7 +18,9 @@ package calico package html import calico.syntax.* +import cats.Contravariant import cats.Foldable +import cats.Id import cats.effect.kernel.Async import cats.effect.kernel.Resource import cats.effect.syntax.all.* @@ -34,8 +36,14 @@ trait Modifier[F[_], E, A]: inline final def contramap[B](inline f: B => A): Modifier[F, E, B] = (b: B, e: E) => outer.modify(f(b), e) -private object Modifier: - def forSignal[F[_]: Async, E, M, V](signal: M => Signal[F, V])( +object Modifier: + inline given [F[_], E]: Contravariant[Modifier[F, E, _]] = + _contravariant.asInstanceOf[Contravariant[Modifier[F, E, _]]] + private val _contravariant: Contravariant[Modifier[Id, Any, _]] = new: + def contramap[A, B](fa: Modifier[Id, Any, A])(f: B => A) = + fa.contramap(f) + + private[html] def forSignal[F[_]: Async, E, M, V](signal: M => Signal[F, V])( mkModify: (M, E) => V => F[Unit]): Modifier[F, E, M] = (m, e) => signal(m).getAndUpdates.flatMap { (head, tail) => val modify = mkModify(m, e) @@ -43,7 +51,8 @@ private object Modifier: tail.foreach(modify(_)).compile.drain.cedeBackground.void } - def forSignalResource[F[_]: Async, E, M, V](signal: M => Resource[F, Signal[F, V]])( + private[html] def forSignalResource[F[_]: Async, E, M, V]( + signal: M => Resource[F, Signal[F, V]])( mkModify: (M, E) => V => F[Unit]): Modifier[F, E, M] = (m, e) => signal(m).flatMap { sig => sig.getAndUpdates.flatMap { (head, tail) => From a0fa919e28ed944c281c9375a96c8970c384ef68 Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Fri, 27 Jan 2023 07:03:12 +0000 Subject: [PATCH 2/3] Replace `Codec` with `encoders` --- calico/src/main/scala/calico/html/Codec.scala | 96 ------------------- calico/src/main/scala/calico/html/Html.scala | 6 +- .../src/main/scala/calico/html/HtmlAttr.scala | 37 +++---- calico/src/main/scala/calico/html/Prop.scala | 42 ++++---- .../src/main/scala/calico/html/encoders.scala | 48 ++++++++++ .../calico/html/codegen/CalicoGenerator.scala | 10 +- 6 files changed, 96 insertions(+), 143 deletions(-) delete mode 100644 calico/src/main/scala/calico/html/Codec.scala create mode 100644 calico/src/main/scala/calico/html/encoders.scala diff --git a/calico/src/main/scala/calico/html/Codec.scala b/calico/src/main/scala/calico/html/Codec.scala deleted file mode 100644 index 0608231b..00000000 --- a/calico/src/main/scala/calico/html/Codec.scala +++ /dev/null @@ -1,96 +0,0 @@ -/* - * Copyright 2022 Arman Bilge - * - * 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 calico.html - -import scala.scalajs.js - -/** - * This trait represents a way to encode and decode HTML attribute or DOM property values. - * - * It is needed because attributes encode all values as strings regardless of their type, and - * then there are also multiple ways to encode e.g. boolean values. Some attributes encode those - * as "true" / "false" strings, others as presence or absence of the element, and yet others use - * "yes" / "no" or "on" / "off" strings, and properties encode booleans as actual booleans. - * - * Scala DOM Types hides all this mess from you using codecs. All those pseudo-boolean - * attributes would be simply `Attr[Boolean](name, codec)` in your code. - */ -private sealed abstract class Codec[ScalaType, DomType]: - - /** - * Convert the result of a `dom.Node.getAttribute` call to appropriate Scala type. - * - * Note: HTML Attributes are generally optional, and `dom.Node.getAttribute` will return - * `null` if an attribute is not defined on a given DOM node. However, this decoder is only - * intended for cases when the attribute is defined. - */ - def decode(domValue: DomType): ScalaType - - /** - * Convert desired attribute value to appropriate DOM type. The resulting value should be - * passed to `dom.Node.setAttribute` call, EXCEPT when resulting value is a `null`. In that - * case you should call `dom.Node.removeAttribute` instead. - * - * We use `null` instead of [[Option]] here to reduce overhead in JS land. This method should - * not be called by end users anyway, it's the consuming library's job to call this method - * under the hood. - */ - def encode(scalaValue: ScalaType): DomType - -private object Codec: - - inline def identity[A]: Codec[A, A] = identityInstance.asInstanceOf[Codec[A, A]] - private val identityInstance: Codec[Any, Any] = new: - def decode(domValue: Any): Any = domValue - def encode(scalaValue: Any): Any = scalaValue - - val whitespaceSeparatedStrings: Codec[List[String], String] = new: - def decode(domValue: String) = domValue.split(" ").toList - - def encode(scalaValue: List[String]) = - if scalaValue.isEmpty then "" - else - var acc = scalaValue.head - var tail = scalaValue.tail - while tail.nonEmpty do - acc += " " + tail.head - tail = tail.tail - acc - - val booleanAsAttrPresence: Codec[Boolean, String] = new: - def decode(domValue: String): Boolean = domValue ne null - def encode(scalaValue: Boolean): String = if scalaValue then "" else null - - val booleanAsTrueFalseString: Codec[Boolean, String] = new: - def decode(domValue: String): Boolean = domValue == "true" - def encode(scalaValue: Boolean): String = if scalaValue then "true" else "false" - - val booleanAsYesNoString: Codec[Boolean, String] = new: - def decode(domValue: String): Boolean = domValue == "yes" - def encode(scalaValue: Boolean): String = if scalaValue then "yes" else "no" - - val booleanAsOnOffString: Codec[Boolean, String] = new: - def decode(domValue: String): Boolean = domValue == "on" - def encode(scalaValue: Boolean): String = if scalaValue then "on" else "off" - - inline def doubleAsString: Codec[Double, String] = new: - def decode(domValue: String): Double = domValue.toDouble - def encode(scalaValue: Double): String = scalaValue.toString - - inline def intAsString: Codec[Int, String] = new: - def decode(domValue: String): Int = domValue.toInt - def encode(scalaValue: Int): String = scalaValue.toString diff --git a/calico/src/main/scala/calico/html/Html.scala b/calico/src/main/scala/calico/html/Html.scala index 46a6d28e..9cc67f15 100644 --- a/calico/src/main/scala/calico/html/Html.scala +++ b/calico/src/main/scala/calico/html/Html.scala @@ -45,10 +45,10 @@ sealed trait Html[F[_]](using F: Async[F]) def cls: ClassProp[F] = ClassProp[F] - def role: HtmlAttr[F, List[String]] = HtmlAttr("role", Codec.whitespaceSeparatedStrings) + def role: HtmlAttr[F, List[String]] = HtmlAttr("role", encoders.whitespaceSeparatedStrings) def dataAttr(suffix: String): HtmlAttr[F, String] = - HtmlAttr("data-" + suffix, Codec.identity) + HtmlAttr("data-" + suffix, encoders.identity) def children: Children[F] = Children[F] @@ -56,4 +56,4 @@ sealed trait Html[F[_]](using F: Async[F]) KeyedChildren[F, K](f) def styleAttr: HtmlAttr[F, String] = - HtmlAttr("style", Codec.identity) + HtmlAttr("style", encoders.identity) diff --git a/calico/src/main/scala/calico/html/HtmlAttr.scala b/calico/src/main/scala/calico/html/HtmlAttr.scala index cd1a6918..e00401e0 100644 --- a/calico/src/main/scala/calico/html/HtmlAttr.scala +++ b/calico/src/main/scala/calico/html/HtmlAttr.scala @@ -16,57 +16,58 @@ package calico.html +import cats.Contravariant import cats.effect.kernel.Async import cats.effect.kernel.Resource import fs2.concurrent.Signal import org.scalajs.dom -sealed class HtmlAttr[F[_], V] private[calico] (key: String, codec: Codec[V, String]): +sealed class HtmlAttr[F[_], V] private[calico] (key: String, encode: V => String): import HtmlAttr.* @inline def :=(v: V): ConstantModifier[V] = - ConstantModifier(key, codec, v) + ConstantModifier(key, encode, v) @inline def <--(vs: Signal[F, V]): SignalModifier[F, V] = - SignalModifier(key, codec, vs) + SignalModifier(key, encode, vs) @inline def <--(vs: Resource[F, Signal[F, V]]): SignalResourceModifier[F, V] = - SignalResourceModifier(key, codec, vs) + SignalResourceModifier(key, encode, vs) @inline def <--(vs: Signal[F, Option[V]]): OptionSignalModifier[F, V] = - OptionSignalModifier(key, codec, vs) + OptionSignalModifier(key, encode, vs) @inline def <--(vs: Resource[F, Signal[F, Option[V]]]): OptionSignalResourceModifier[F, V] = - OptionSignalResourceModifier(key, codec, vs) + OptionSignalResourceModifier(key, encode, vs) object HtmlAttr: final class ConstantModifier[V] private[calico] ( private[calico] val key: String, - private[calico] val codec: Codec[V, String], + private[calico] val encode: V => String, private[calico] val value: V ) final class SignalModifier[F[_], V] private[calico] ( private[calico] val key: String, - private[calico] val codec: Codec[V, String], + private[calico] val encode: V => String, private[calico] val values: Signal[F, V] ) final class SignalResourceModifier[F[_], V] private[calico] ( private[calico] val key: String, - private[calico] val codec: Codec[V, String], + private[calico] val encode: V => String, private[calico] val values: Resource[F, Signal[F, V]] ) final class OptionSignalModifier[F[_], V] private[calico] ( private[calico] val key: String, - private[calico] val codec: Codec[V, String], + private[calico] val encode: V => String, private[calico] val values: Signal[F, Option[V]] ) final class OptionSignalResourceModifier[F[_], V] private[calico] ( private[calico] val key: String, - private[calico] val codec: Codec[V, String], + private[calico] val encode: V => String, private[calico] val values: Resource[F, Signal[F, Option[V]]] ) @@ -78,7 +79,7 @@ private trait HtmlAttrModifiers[F[_]](using F: Async[F]): _forConstantHtmlAttr.asInstanceOf[Modifier[F, E, ConstantModifier[V]]] private val _forConstantHtmlAttr: Modifier[F, dom.Element, ConstantModifier[Any]] = - (m, e) => Resource.eval(F.delay(e.setAttribute(m.key, m.codec.encode(m.value)))) + (m, e) => Resource.eval(F.delay(e.setAttribute(m.key, m.encode(m.value)))) inline given forSignalHtmlAttr[E <: fs2.dom.Element[F], V] : Modifier[F, E, SignalModifier[F, V]] = @@ -86,7 +87,7 @@ private trait HtmlAttrModifiers[F[_]](using F: Async[F]): private val _forSignalHtmlAttr = Modifier.forSignal[F, dom.Element, SignalModifier[F, Any], Any](_.values) { (m, e) => v => - F.delay(e.setAttribute(m.key, m.codec.encode(v))) + F.delay(e.setAttribute(m.key, m.encode(v))) } inline given forSignalResourceHtmlAttr[E <: fs2.dom.Element[F], V] @@ -95,7 +96,7 @@ private trait HtmlAttrModifiers[F[_]](using F: Async[F]): private val _forSignalResourceHtmlAttr = Modifier.forSignalResource[F, dom.Element, SignalResourceModifier[F, Any], Any](_.values) { - (m, e) => v => F.delay(e.setAttribute(m.key, m.codec.encode(v))) + (m, e) => v => F.delay(e.setAttribute(m.key, m.encode(v))) } inline given forOptionSignalHtmlAttr[E <: fs2.dom.Element[F], V] @@ -105,7 +106,7 @@ private trait HtmlAttrModifiers[F[_]](using F: Async[F]): private val _forOptionSignalHtmlAttr = Modifier.forSignal[F, dom.Element, OptionSignalModifier[F, Any], Option[Any]](_.values) { (m, e) => v => - F.delay(v.fold(e.removeAttribute(m.key))(v => e.setAttribute(m.key, m.codec.encode(v)))) + F.delay(v.fold(e.removeAttribute(m.key))(v => e.setAttribute(m.key, m.encode(v)))) } inline given forOptionSignalResourceHtmlAttr[E <: fs2.dom.Element[F], V] @@ -116,7 +117,7 @@ private trait HtmlAttrModifiers[F[_]](using F: Async[F]): Modifier .forSignalResource[F, dom.Element, OptionSignalResourceModifier[F, Any], Option[Any]]( _.values) { (m, e) => v => - F.delay(v.fold(e.removeAttribute(m.key))(v => e.setAttribute(m.key, m.codec.encode(v)))) + F.delay(v.fold(e.removeAttribute(m.key))(v => e.setAttribute(m.key, m.encode(v)))) } final class Aria[F[_]] private extends AriaAttrs[F] @@ -125,5 +126,5 @@ private object Aria: inline def apply[F[_]]: Aria[F] = instance.asInstanceOf[Aria[F]] private val instance: Aria[cats.Id] = new Aria[cats.Id] -final class AriaAttr[F[_], V] private[calico] (suffix: String, codec: Codec[V, String]) - extends HtmlAttr[F, V]("aria-" + suffix, codec) +final class AriaAttr[F[_], V] private[calico] (suffix: String, encode: V => String) + extends HtmlAttr[F, V]("aria-" + suffix, encode) diff --git a/calico/src/main/scala/calico/html/Prop.scala b/calico/src/main/scala/calico/html/Prop.scala index a1988c8c..04ae12dd 100644 --- a/calico/src/main/scala/calico/html/Prop.scala +++ b/calico/src/main/scala/calico/html/Prop.scala @@ -27,71 +27,71 @@ import org.scalajs.dom import scala.scalajs.js -sealed class Prop[F[_], V, J] private[calico] (name: String, codec: Codec[V, J]): +sealed class Prop[F[_], V, J] private[calico] (name: String, encode: V => J): import Prop.* @inline def :=(v: V): ConstantModifier[V, J] = - ConstantModifier(name, codec, v) + ConstantModifier(name, encode, v) @inline def <--(vs: Signal[F, V]): SignalModifier[F, V, J] = - SignalModifier(name, codec, vs) + SignalModifier(name, encode, vs) @inline def <--(vs: Resource[F, Signal[F, V]]): SignalResourceModifier[F, V, J] = - SignalResourceModifier(name, codec, vs) + SignalResourceModifier(name, encode, vs) @inline def <--(vs: Signal[F, Option[V]]): OptionSignalModifier[F, V, J] = - OptionSignalModifier(name, codec, vs) + OptionSignalModifier(name, encode, vs) @inline def <--( vs: Resource[F, Signal[F, Option[V]]]): OptionSignalResourceModifier[F, V, J] = - OptionSignalResourceModifier(name, codec, vs) + OptionSignalResourceModifier(name, encode, vs) object Prop: final class ConstantModifier[V, J] private[calico] ( private[calico] val name: String, - private[calico] val codec: Codec[V, J], + private[calico] val encode: V => J, private[calico] val value: V ) final class SignalModifier[F[_], V, J] private[calico] ( private[calico] val name: String, - private[calico] val codec: Codec[V, J], + private[calico] val encode: V => J, private[calico] val values: Signal[F, V] ) final class SignalResourceModifier[F[_], V, J] private[calico] ( private[calico] val name: String, - private[calico] val codec: Codec[V, J], + private[calico] val encode: V => J, private[calico] val values: Resource[F, Signal[F, V]] ) final class OptionSignalModifier[F[_], V, J] private[calico] ( private[calico] val name: String, - private[calico] val codec: Codec[V, J], + private[calico] val encode: V => J, private[calico] val values: Signal[F, Option[V]] ) final class OptionSignalResourceModifier[F[_], V, J] private[calico] ( private[calico] val name: String, - private[calico] val codec: Codec[V, J], + private[calico] val encode: V => J, private[calico] val values: Resource[F, Signal[F, Option[V]]] ) private trait PropModifiers[F[_]](using F: Async[F]): import Prop.* - private inline def setProp[N, V, J](node: N, name: String, codec: Codec[V, J]) = + private inline def setProp[N, V, J](node: N, name: String, encode: V => J) = (value: V) => F.delay { - node.asInstanceOf[js.Dictionary[J]](name) = codec.encode(value) + node.asInstanceOf[js.Dictionary[J]](name) = encode(value) () } - private inline def setPropOption[N, V, J](node: N, name: String, codec: Codec[V, J]) = + private inline def setPropOption[N, V, J](node: N, name: String, encode: V => J) = (value: Option[V]) => F.delay { val dict = node.asInstanceOf[js.Dictionary[Any]] - value.fold(dict -= name)(v => dict(name) = codec.encode(v)) + value.fold(dict -= name)(v => dict(name) = encode(v)) () } @@ -99,14 +99,14 @@ private trait PropModifiers[F[_]](using F: Async[F]): _forConstantProp.asInstanceOf[Modifier[F, N, ConstantModifier[V, J]]] private val _forConstantProp: Modifier[F, Any, ConstantModifier[Any, Any]] = - (m, n) => Resource.eval(setProp(n, m.name, m.codec).apply(m.value)) + (m, n) => Resource.eval(setProp(n, m.name, m.encode).apply(m.value)) inline given forSignalProp[N, V, J]: Modifier[F, N, SignalModifier[F, V, J]] = _forSignalProp.asInstanceOf[Modifier[F, N, SignalModifier[F, V, J]]] private val _forSignalProp = Modifier.forSignal[F, Any, SignalModifier[F, Any, Any], Any](_.values) { (m, n) => - setProp(n, m.name, m.codec) + setProp(n, m.name, m.encode) } inline given forSignalResourceProp[N, V, J]: Modifier[F, N, SignalResourceModifier[F, V, J]] = @@ -114,7 +114,7 @@ private trait PropModifiers[F[_]](using F: Async[F]): private val _forSignalResourceProp = Modifier.forSignalResource[F, Any, SignalResourceModifier[F, Any, Any], Any](_.values) { - (m, n) => setProp(n, m.name, m.codec) + (m, n) => setProp(n, m.name, m.encode) } inline given forOptionSignalProp[N, V, J]: Modifier[F, N, OptionSignalModifier[F, V, J]] = @@ -122,7 +122,7 @@ private trait PropModifiers[F[_]](using F: Async[F]): private val _forOptionSignalProp = Modifier.forSignal[F, Any, OptionSignalModifier[F, Any, Any], Option[Any]](_.values) { - (m, n) => setPropOption(n, m.name, m.codec) + (m, n) => setPropOption(n, m.name, m.encode) } inline given forOptionSignalResourceProp[N, V, J] @@ -131,7 +131,7 @@ private trait PropModifiers[F[_]](using F: Async[F]): private val _forOptionSignalResourceProp = Modifier.forSignalResource[F, Any, OptionSignalResourceModifier[F, Any, Any], Option[Any]]( - _.values) { (m, n) => setPropOption(n, m.name, m.codec) } + _.values) { (m, n) => setPropOption(n, m.name, m.encode) } final class EventProp[F[_], E] private[calico] (key: String): import EventProp.* @@ -152,7 +152,7 @@ private trait EventPropModifiers[F[_]](using F: Async[F]): final class ClassProp[F[_]] private[calico] extends Prop[F, List[String], String]( "className", - Codec.whitespaceSeparatedStrings + encoders.whitespaceSeparatedStrings ): import ClassProp.* diff --git a/calico/src/main/scala/calico/html/encoders.scala b/calico/src/main/scala/calico/html/encoders.scala new file mode 100644 index 00000000..bd87938e --- /dev/null +++ b/calico/src/main/scala/calico/html/encoders.scala @@ -0,0 +1,48 @@ +/* + * Copyright 2022 Arman Bilge + * + * 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 calico +package html + +private object encoders: + inline def identity[A]: A => A = _identity.asInstanceOf[A => A] + private val _identity: Any => Any = x => x + + val whitespaceSeparatedStrings: List[String] => String = strings => + if strings.isEmpty then "" + else + var acc = strings.head + var tail = strings.tail + while tail.nonEmpty do + acc += " " + tail.head + tail = tail.tail + acc + + val booleanAsAttrPresence: Boolean => String = + if _ then "" else null + + val booleanAsTrueFalseString: Boolean => String = + _.toString + + val booleanAsYesNoString: Boolean => String = + if _ then "yes" else "no" + + val booleanAsOnOffString: Boolean => String = + if _ then "on" else "off" + + val doubleAsString: Double => String = _.toString + + val intAsString: Int => String = _.toString diff --git a/project/src/main/scala/calico/html/codegen/CalicoGenerator.scala b/project/src/main/scala/calico/html/codegen/CalicoGenerator.scala index f9e0a6af..1a39f8a0 100644 --- a/project/src/main/scala/calico/html/codegen/CalicoGenerator.scala +++ b/project/src/main/scala/calico/html/codegen/CalicoGenerator.scala @@ -67,8 +67,8 @@ private[codegen] class CalicoGenerator(srcManaged: File) override val codecsImport: String = "" private def transformCodecName(codec: String) = codec match { - case c if c.endsWith("AsIs") => s"Codec.identity[${c.dropRight(4)}]" - case c => s"Codec.${c(0).toLower}${c.substring(1)}" + case c if c.endsWith("AsIs") => s"encoders.identity[${c.dropRight(4)}]" + case c => s"encoders.${c(0).toLower}${c.substring(1)}" } override def generateTagsTrait( @@ -146,11 +146,11 @@ private[codegen] class CalicoGenerator(srcManaged: File) val baseImplDef = if (tagType == SvgTagType) { List( - s"@inline private[calico] def ${baseImplName}[V](key: String, codec: Codec[V, String], namespace: Option[String]): ${keyKind}[V] = ${keyKindConstructor(keyKind)}(key, codec, namespace)" + s"@inline private[calico] def ${baseImplName}[V](key: String, encode: V => String, namespace: Option[String]): ${keyKind}[V] = ${keyKindConstructor(keyKind)}(key, encode, namespace)" ) } else { List( - s"@inline private[calico] def ${baseImplName}[V](key: String, codec: Codec[V, String]): ${keyKind}[F, V] = ${keyKindConstructor(keyKind)}(key, codec)" + s"@inline private[calico] def ${baseImplName}[V](key: String, encode: V => String): ${keyKind}[F, V] = ${keyKindConstructor(keyKind)}(key, encode)" ) } @@ -198,7 +198,7 @@ private[codegen] class CalicoGenerator(srcManaged: File) val (defs, defGroupComments) = defsAndGroupComments(defGroups, printDefGroupComments) val baseImplDef = List( - s"@inline private[calico] def ${baseImplName}[V, DomV](key: String, codec: Codec[V, DomV]): ${keyKind}[F, V, DomV] = ${keyKindConstructor(keyKind)}(key, codec)" + s"@inline private[calico] def ${baseImplName}[V, DomV](key: String, encode: V => DomV): ${keyKind}[F, V, DomV] = ${keyKindConstructor(keyKind)}(key, encode)" ) val headerLines = List( From 8ce27561a4c150c6b32cde5e0365e895137a2b91 Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Fri, 27 Jan 2023 07:16:21 +0000 Subject: [PATCH 3/3] `Prop` and `HtmlAttr` are `Contravariant` --- calico/src/main/scala/calico/html/HtmlAttr.scala | 10 ++++++++++ calico/src/main/scala/calico/html/Prop.scala | 11 +++++++++++ 2 files changed, 21 insertions(+) diff --git a/calico/src/main/scala/calico/html/HtmlAttr.scala b/calico/src/main/scala/calico/html/HtmlAttr.scala index e00401e0..d7d27bdb 100644 --- a/calico/src/main/scala/calico/html/HtmlAttr.scala +++ b/calico/src/main/scala/calico/html/HtmlAttr.scala @@ -17,6 +17,7 @@ package calico.html import cats.Contravariant +import cats.Id import cats.effect.kernel.Async import cats.effect.kernel.Resource import fs2.concurrent.Signal @@ -40,7 +41,16 @@ sealed class HtmlAttr[F[_], V] private[calico] (key: String, encode: V => String @inline def <--(vs: Resource[F, Signal[F, Option[V]]]): OptionSignalResourceModifier[F, V] = OptionSignalResourceModifier(key, encode, vs) + @inline def contramap[U](f: U => V): HtmlAttr[F, U] = + new HtmlAttr(key, f.andThen(encode)) + object HtmlAttr: + inline given [F[_]]: Contravariant[HtmlAttr[F, _]] = + _contravariant.asInstanceOf[Contravariant[HtmlAttr[F, _]]] + private val _contravariant: Contravariant[HtmlAttr[Id, _]] = new: + def contramap[A, B](fa: HtmlAttr[Id, A])(f: B => A): HtmlAttr[Id, B] = + fa.contramap(f) + final class ConstantModifier[V] private[calico] ( private[calico] val key: String, private[calico] val encode: V => String, diff --git a/calico/src/main/scala/calico/html/Prop.scala b/calico/src/main/scala/calico/html/Prop.scala index 04ae12dd..fe49cf01 100644 --- a/calico/src/main/scala/calico/html/Prop.scala +++ b/calico/src/main/scala/calico/html/Prop.scala @@ -18,6 +18,8 @@ package calico package html import calico.syntax.* +import cats.Contravariant +import cats.Id import cats.effect.kernel.Async import cats.effect.kernel.Resource import cats.syntax.all.* @@ -46,7 +48,16 @@ sealed class Prop[F[_], V, J] private[calico] (name: String, encode: V => J): vs: Resource[F, Signal[F, Option[V]]]): OptionSignalResourceModifier[F, V, J] = OptionSignalResourceModifier(name, encode, vs) + @inline def contramap[U](f: U => V): Prop[F, U, J] = + new Prop(name, f.andThen(encode)) + object Prop: + inline given [F[_], J]: Contravariant[Prop[F, _, J]] = + _contravariant.asInstanceOf[Contravariant[Prop[F, _, J]]] + private val _contravariant: Contravariant[Prop[Id, _, Any]] = new: + def contramap[A, B](fa: Prop[Id, A, Any])(f: B => A): Prop[Id, B, Any] = + fa.contramap(f) + final class ConstantModifier[V, J] private[calico] ( private[calico] val name: String, private[calico] val encode: V => J,