Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add ascii and utf8 interpolators #381

Merged
merged 3 commits into from
Jul 28, 2022
Merged
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
54 changes: 54 additions & 0 deletions core/shared/src/main/scala-2/scodec/bits/LiteralSyntaxMacros.scala
Original file line number Diff line number Diff line change
Expand Up @@ -81,4 +81,58 @@ object LiteralSyntaxMacros {
reify(ByteVector.fromValidHex(stringBuilder.splice.toString))
}

def asciiStringInterpolator(c: blackbox.Context)(args: c.Expr[String]*): c.Expr[ByteVector] = {
import c.universe._

val Apply(_, List(Apply(_, parts))) = c.prefix.tree
val partLiterals: List[String] = parts.map { case Literal(Constant(part: String)) =>
ByteVector.encodeAscii(part) match {
case Left(ex) =>
c.error(
c.enclosingPosition,
s"ascii string literal may only contain ascii characters: $ex"
)
case Right(_) => // do nothing
}
part
}

val headPart = c.Expr[String](Literal(Constant(partLiterals.head)))
val initialStringBuilder = reify(new StringBuilder().append(headPart.splice))
val stringBuilder =
args.zip(partLiterals.tail).foldLeft(initialStringBuilder) { case (sb, (arg, part)) =>
val partExpr = c.Expr[String](Literal(Constant(part)))
reify(sb.splice.append(arg.splice).append(partExpr.splice))
}

reify(ByteVector.encodeAscii(stringBuilder.splice.toString).fold(throw _, identity))
}

def utf8StringInterpolator(c: blackbox.Context)(args: c.Expr[String]*): c.Expr[ByteVector] = {
import c.universe._

val Apply(_, List(Apply(_, parts))) = c.prefix.tree
val partLiterals: List[String] = parts.map { case Literal(Constant(part: String)) =>
ByteVector.encodeUtf8(part) match {
case Left(ex) =>
c.error(
c.enclosingPosition,
s"utf8 string literal may only contain UTF8 characters: $ex"
)
case Right(_) => // do nothing
}
part
}

val headPart = c.Expr[String](Literal(Constant(partLiterals.head)))
val initialStringBuilder = reify(new StringBuilder().append(headPart.splice))
val stringBuilder =
args.zip(partLiterals.tail).foldLeft(initialStringBuilder) { case (sb, (arg, part)) =>
val partExpr = c.Expr[String](Literal(Constant(part)))
reify(sb.splice.append(arg.splice).append(partExpr.splice))
}

reify(ByteVector.encodeUtf8(stringBuilder.splice.toString).fold(throw _, identity))
}

}
24 changes: 24 additions & 0 deletions core/shared/src/main/scala-2/scodec/bits/package.scala
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,30 @@ package object bits extends ScalaVersionSpecific {
def hex(args: ByteVector*): ByteVector = macro LiteralSyntaxMacros.hexStringInterpolator
}

/** Provides the `ascii` string interpolator, which returns `ByteVector` instances from ascii
* strings.
*/
final implicit class AsciiStringSyntax(val sc: StringContext) extends AnyVal {

/** Converts this ascii literal string to a `ByteVector`.
*
* Named arguments are supported in the same manner as the standard `s` interpolator.
*/
def asciiBytes(args: String*): ByteVector = macro LiteralSyntaxMacros.asciiStringInterpolator
}

/** Provides the `utf8` string interpolator, which returns `ByteVector` instances from UTF8
* strings.
*/
final implicit class Utf8StringSyntax(val sc: StringContext) extends AnyVal {

/** Converts this UTF8 literal string to a `ByteVector`.
*
* Named arguments are supported in the same manner as the standard `s` interpolator.
*/
def utf8Bytes(args: String*): ByteVector = macro LiteralSyntaxMacros.utf8StringInterpolator
}

private[bits] implicit class EitherOps[L, R](val self: Either[L, R]) extends AnyVal {
def map[R2](f: R => R2): Either[L, R2] =
self match {
Expand Down
34 changes: 34 additions & 0 deletions core/shared/src/main/scala-3/scodec/bits/Interpolators.scala
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,28 @@ extension (inline ctx: StringContext)
inline def bin(inline args: Any*): BitVector =
${ Literals.Bin('ctx, 'args) }

/** Provides the `ascii` string interpolator, which returns `ByteVector` instances from ascii strings.
*
* @example {{{
* scala> val b = ascii"deadbeef"
* val b: scodec.bits.ByteVector = ByteVector(8 bytes, 0x6465616462656566)
* }}}
*/
extension (inline ctx: StringContext)
inline def asciiBytes(inline args: Any*): ByteVector =
${ Literals.Ascii('ctx, 'args) }

/** Provides the `utf8` string interpolator, which returns `ByteVector` instances from utf8 strings.
*
* @example {{{
* scala> val b = utf8"ɟǝǝqpɐǝp"
* val b: scodec.bits.ByteVector = ByteVector(13 bytes, 0xc99fc79dc79d7170c990c79d70)
* }}}
*/
extension (inline ctx: StringContext)
inline def utf8Bytes(inline args: Any*): ByteVector =
${ Literals.Utf8('ctx, 'args) }

object Literals:

trait Validator[A]:
Expand Down Expand Up @@ -90,3 +112,15 @@ object Literals:
ByteVector.fromBin(s) match
case None => Left("binary string literal may only contain characters [0, 1]")
case Some(_) => Right('{ BitVector.fromValidBin(${ Expr(s) }) })

object Ascii extends Validator[ByteVector]:
def validate(s: String)(using Quotes): Either[String, Expr[ByteVector]] =
ByteVector.encodeAscii(s) match
case Left(ex) => Left(s"ascii string literal may only contain valid ascii: $ex")
case Right(_) => Right('{ ByteVector.encodeAscii(${ Expr(s) }).fold(throw _, identity) })

object Utf8 extends Validator[ByteVector]:
def validate(s: String)(using Quotes): Either[String, Expr[ByteVector]] =
ByteVector.encodeUtf8(s) match
case Left(ex) => Left(s"UTF8 string literal may only contain valid UTF8: $ex")
case Right(_) => Right('{ ByteVector.encodeUtf8(${ Expr(s) }).fold(throw _, identity) })
9 changes: 9 additions & 0 deletions core/shared/src/test/scala/scodec/bits/ByteVectorTest.scala
Original file line number Diff line number Diff line change
Expand Up @@ -773,4 +773,13 @@ class ByteVectorTest extends BitsSuite {
assert(is.available() == 0)
}
}

test("ascii interpolator") {
assertEquals(asciiBytes"deadbeef", ByteVector.encodeAscii(s"deadbeef").toOption.get)
assert(compileErrors("""asciiBytes"ɟǝǝqpɐǝp"""").contains("error"))
}

test("utf8 interpolator") {
assertEquals(utf8Bytes"ɟǝǝqpɐǝp", ByteVector.encodeUtf8(s"ɟǝǝqpɐǝp").toOption.get)
}
}