├── .gitignore ├── project ├── build.properties └── plugins.sbt ├── version.sbt ├── .travis.yml ├── core ├── shared │ └── src │ │ ├── main │ │ └── scala │ │ │ └── io │ │ │ └── expressier │ │ │ ├── utils │ │ │ ├── JavaReflectionUtils.scala │ │ │ ├── StringReplacements.scala │ │ │ └── Unlabelled.scala │ │ │ ├── package.scala │ │ │ ├── ExpressRecord.scala │ │ │ ├── PatternParser.scala │ │ │ ├── Express.scala │ │ │ ├── Macros.scala │ │ │ └── internals │ │ │ └── NosyPatternParser.scala │ │ └── test │ │ └── scala │ │ └── io │ │ └── expressier │ │ └── ExpressSuite.scala └── jvm │ └── src │ └── test │ └── scala │ └── io │ └── expressier │ └── ExpressJvmSuite.scala ├── scalastyle-config.xml ├── README.md └── LICENSE /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | -------------------------------------------------------------------------------- /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=0.13.9 2 | -------------------------------------------------------------------------------- /version.sbt: -------------------------------------------------------------------------------- 1 | version in ThisBuild := "0.1.0-SNAPSHOT" 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | 3 | language: scala 4 | 5 | scala: 6 | - 2.10.5 7 | - 2.11.7 8 | 9 | jdk: 10 | - oraclejdk7 11 | - oraclejdk8 12 | - openjdk7 13 | 14 | script: sbt ++$TRAVIS_SCALA_VERSION coverage core/test coverageReport 15 | 16 | after_success: bash <(curl -s https://codecov.io/bash) 17 | -------------------------------------------------------------------------------- /core/shared/src/main/scala/io/expressier/utils/JavaReflectionUtils.scala: -------------------------------------------------------------------------------- 1 | package io.expressier.utils 2 | 3 | trait JavaReflectionUtils { 4 | protected def getField(clazz: Class[_], name: String) = { 5 | val field = clazz.getDeclaredField(name) 6 | field.setAccessible(true) 7 | field 8 | } 9 | 10 | protected def getMethod(clazz: Class[_], name: String) = { 11 | val method = clazz.getDeclaredMethod(name) 12 | method.setAccessible(true) 13 | method 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /core/shared/src/main/scala/io/expressier/package.scala: -------------------------------------------------------------------------------- 1 | package io 2 | 3 | import scala.language.experimental.macros 4 | 5 | package object expressier { 6 | private[expressier] val patternParser: PatternParser = 7 | new io.expressier.internals.NosyPatternParser 8 | 9 | implicit class ExpressStringContext(sc: StringContext) { 10 | def x(): Any = macro WhiteboxMacros.stringContextImpl 11 | } 12 | 13 | implicit class ExpressString[S <: String](s: S) { 14 | def express[T]: Express.To[T] = macro BlackboxMacros.stringImpl[T] 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /core/shared/src/main/scala/io/expressier/ExpressRecord.scala: -------------------------------------------------------------------------------- 1 | package io.expressier 2 | 3 | import io.expressier.utils.Unlabelled 4 | import shapeless.HList, shapeless.ops.hlist.Tupler, shapeless.syntax.DynamicRecordOps 5 | 6 | class ExpressRecord[S <: String, Out <: HList, V <: HList, P](exp: Express.Aux[S, Out])(implicit 7 | unlabelled: Unlabelled.Aux[Out, V], 8 | tupler: Tupler.Aux[V, P] 9 | ) { 10 | def apply(input: String): Option[DynamicRecordOps[Out]] = exp(input).map(DynamicRecordOps(_)) 11 | def unapply(input: String): Option[P] = exp(input).map(res => tupler(unlabelled(res))) 12 | } 13 | -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | resolvers ++= Seq( 2 | Classpaths.typesafeReleases, 3 | Classpaths.sbtPluginReleases, 4 | "jgit-repo" at "http://download.eclipse.org/jgit/maven" 5 | ) 6 | 7 | addSbtPlugin("com.eed3si9n" % "sbt-unidoc" % "0.3.3") 8 | addSbtPlugin("com.github.gseitz" % "sbt-release" % "1.0.0") 9 | addSbtPlugin("com.jsuereth" % "sbt-pgp" % "1.0.0") 10 | addSbtPlugin("com.typesafe.sbt" % "sbt-ghpages" % "0.5.3") 11 | addSbtPlugin("com.typesafe.sbt" % "sbt-git" % "0.8.4") 12 | addSbtPlugin("com.typesafe.sbt" % "sbt-site" % "0.8.1") 13 | addSbtPlugin("org.scala-js" % "sbt-scalajs" % "0.6.5") 14 | addSbtPlugin("org.scalastyle" %% "scalastyle-sbt-plugin" % "0.6.0") 15 | addSbtPlugin("org.scoverage" % "sbt-scoverage" % "1.2.0") 16 | addSbtPlugin("org.brianmckenna" % "sbt-wartremover" % "0.13") 17 | addSbtPlugin("pl.project13.scala" % "sbt-jmh" % "0.2.2") 18 | -------------------------------------------------------------------------------- /core/shared/src/main/scala/io/expressier/PatternParser.scala: -------------------------------------------------------------------------------- 1 | package io.expressier 2 | 3 | import java.util.regex.Pattern 4 | import scala.reflect.api.Universe 5 | 6 | /** 7 | * Pattern parsing functionality. 8 | */ 9 | trait PatternParser { 10 | case class ResultItem[U <: Universe]( 11 | name: Option[String], 12 | tpe: U#Type, 13 | converter: U#Tree => U#Tree 14 | ) 15 | 16 | def parsePattern(u: Universe)(p: Pattern): Option[List[ResultItem[u.type]]] 17 | 18 | def stringResult(u: Universe)(name: Option[String]): ResultItem[u.type] = 19 | ResultItem[u.type](name, u.typeOf[String], tree => tree) 20 | 21 | def integerResult(u: Universe)(name: Option[String]): ResultItem[u.type] = { 22 | import u._ 23 | ResultItem[u.type](name, typeOf[Int], tree => q"$tree.toInt") 24 | } 25 | 26 | def characterResult(u: Universe)(name: Option[String]): ResultItem[u.type] = { 27 | import u._ 28 | ResultItem[u.type](name, typeOf[Char], tree => q"$tree.head") 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /core/shared/src/main/scala/io/expressier/utils/StringReplacements.scala: -------------------------------------------------------------------------------- 1 | package io.expressier.utils 2 | 3 | import shapeless._ 4 | 5 | /** 6 | * A type class witnessing that `B` corresponds to `A` with some (or all) of its 7 | * fields replaced with [[String]]. 8 | */ 9 | trait StringReplacements[A <: HList, B <: HList] { 10 | def apply(a: A): B 11 | } 12 | 13 | object StringReplacements extends LowStringReplacements { 14 | implicit val hnilStringReplacements: StringReplacements[HNil, HNil] = 15 | new StringReplacements[HNil, HNil] { 16 | def apply(a: HNil): HNil = a 17 | } 18 | 19 | implicit def hconsStringReplacements1[H, T <: HList, O <: HList](implicit 20 | sr: StringReplacements[T, O] 21 | ): StringReplacements[H :: T, String :: O] = new StringReplacements[H :: T, String :: O] { 22 | def apply(a: H :: T): String :: O = a.head.toString :: sr(a.tail) 23 | } 24 | } 25 | 26 | trait LowStringReplacements { 27 | implicit def hconsStringReplacements0[H, T <: HList, O <: HList](implicit 28 | sr: StringReplacements[T, O] 29 | ): StringReplacements[H :: T, H :: O] = new StringReplacements[H :: T, H :: O] { 30 | def apply(a: H :: T): H :: O = a.head :: sr(a.tail) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /core/shared/src/main/scala/io/expressier/utils/Unlabelled.scala: -------------------------------------------------------------------------------- 1 | package io.expressier.utils 2 | 3 | import shapeless._, shapeless.labelled.FieldType 4 | 5 | /** 6 | * A type class witnessing that `B` corresponds to `A` with any labels removed. 7 | */ 8 | trait Unlabelled[A <: HList] extends DepFn1[A] { 9 | type Out <: HList 10 | 11 | def apply(a: A): Out 12 | } 13 | 14 | object Unlabelled extends LowPriorityUnlabelled { 15 | implicit val hnilUnlabelled: Aux[HNil, HNil] = 16 | new Unlabelled[HNil] { 17 | type Out = HNil 18 | def apply(a: HNil): HNil = a 19 | } 20 | 21 | implicit def hconsUnlabelled1[K, V, T <: HList, O <: HList](implicit 22 | unlabelled: Aux[T, O] 23 | ): Aux[FieldType[K, V] :: T, V :: O] = 24 | new Unlabelled[FieldType[K, V] :: T] { 25 | type Out = V :: O 26 | def apply(a: FieldType[K, V] :: T): V :: O = a.head :: unlabelled(a.tail) 27 | } 28 | } 29 | 30 | trait LowPriorityUnlabelled { 31 | type Aux[A <: HList, Out0 <: HList] = Unlabelled[A] { type Out = Out0 } 32 | 33 | implicit def hconsUnlabelled0[H, T <: HList, O <: HList](implicit 34 | unlabelled: Aux[T, O] 35 | ): Aux[H :: T, H :: O] = new Unlabelled[H :: T] { 36 | type Out = H :: O 37 | def apply(a: H :: T): H :: O = a.head :: unlabelled(a.tail) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /core/shared/src/test/scala/io/expressier/ExpressSuite.scala: -------------------------------------------------------------------------------- 1 | package io.expressier 2 | 3 | import org.scalatest.FunSuite 4 | 5 | class ExpressSuite extends FunSuite { 6 | val rows = List( 7 | "en: 456, 123, 1203", 8 | "de: 12, 567, 200", 9 | "cz: 1, 32, 10" 10 | ) 11 | 12 | test("extractor") { 13 | val Row = x"""(\w\w)\s*:\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*""" 14 | 15 | val totals: Map[String, Int] = rows.flatMap { 16 | case Row(id, first, second, third) => (id, first + second + third) :: Nil 17 | case _ => Nil 18 | }.toMap 19 | 20 | assert(totals === Map("en" -> 1782, "de" -> 779, "cz" -> 43)) 21 | } 22 | 23 | test("tuple parser") { 24 | val Row = """(\w\w)\s*:\s*(\d{3})\s*,\s*(\d+)\s*,\s*(\d+)\s*""".express[(String, Int, Int, Int)] 25 | 26 | val result = Row("en: 456, 123, 1203").get 27 | assert((result._1: String) === "en") 28 | assert((result._2: Int) === 456) 29 | assert((result._3: Int) === 123) 30 | assert((result._4: Int) === 1203) 31 | } 32 | 33 | test("tuple extractor") { 34 | val Row = """(\w\w)\s*:\s*(\d{3})\s*,\s*(\d+)\s*,\s*(\d+)\s*""".express[(String, Int, Int, Int)] 35 | 36 | val Row(name, first, middle, last) = "en: 456, 123, 1203" 37 | assert((name: String) === "en") 38 | assert((first: Int) === 456) 39 | assert((middle: Int) === 123) 40 | assert((last: Int) === 1203) 41 | } 42 | 43 | test("tuple extractor with strings") { 44 | val Row = """(\w\w)\s*:\s*(\d{3})\s*,\s*(\d+)\s*,\s*(\d+)\s*""".express[(String, Int, String, String)] 45 | 46 | val Row(name, first, middle, last) = "en: 456, 123, 1203" 47 | assert((name: String) === "en") 48 | assert((first: Int) === 456) 49 | assert((middle: String) === "123") 50 | assert((last: String) === "1203") 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /core/jvm/src/test/scala/io/expressier/ExpressJvmSuite.scala: -------------------------------------------------------------------------------- 1 | package io.expressier 2 | 3 | import org.scalatest.FunSuite 4 | 5 | class ExpressJvmSuite extends FunSuite { 6 | test("extractor with names") { 7 | val Row = x"""(?\w\w)\s*:\s*(?\d{3})\s*,\s*(\d+)\s*,\s*(?.)\s*""" 8 | 9 | val Row(name, first, middle, last) = "en: 456, 123, x" 10 | 11 | assert((name: String) === "en") 12 | assert((first: Int) === 456) 13 | assert((middle: Int) === 123) 14 | assert((last: Char) === 'x') 15 | } 16 | 17 | test("group name members") { 18 | val Row = x"""(?\w\w)\s*:\s*(?\d{3})\s*,\s*(\d+)\s*,\s*(?.)\s*""" 19 | 20 | val result = Row("en: 456, 123, x").get 21 | 22 | assert((result.name: String) === "en") 23 | assert((result.first: Int) === 456) 24 | assert((result.last: Char) === 'x') 25 | } 26 | 27 | test("case class parser") { 28 | case class Row(name: String, first: Int, middle: Int, last: Char) 29 | val RowParser = 30 | """(?\w\w)\s*:\s*(?\d{3})\s*,\s*(?\d+)\s*,\s*(?.)\s*""".express[Row] 31 | 32 | val result = RowParser("en: 456, 123, x").get 33 | assert((result.name: String) === "en") 34 | assert((result.first: Int) === 456) 35 | assert((result.middle: Int) === 123) 36 | assert((result.last: Char) === 'x') 37 | } 38 | 39 | test("case class parser (rearranged)") { 40 | case class Row(name: String, first: Char, middle: Int, last: Int) 41 | val RowParser = 42 | """(?\w\w)\s*:\s*(?\d{3})\s*,\s*(?\d+)\s*,\s*(?.)\s*""".express[Row] 43 | 44 | val result = RowParser("en: 456, 123, x").get 45 | assert((result.name: String) === "en") 46 | assert((result.first: Char) === 'x') 47 | assert((result.middle: Int) === 123) 48 | assert((result.last: Int) === 456) 49 | } 50 | 51 | test("case class parser (with strings and rearranged)") { 52 | case class Row(name: String, first: Char, middle: Int, last: String) 53 | val RowParser = 54 | """(?\w\w)\s*:\s*(?\d{3})\s*,\s*(?\d+)\s*,\s*(?.)\s*""".express[Row] 55 | 56 | val result = RowParser("en: 456, 123, x").get 57 | assert((result.name: String) === "en") 58 | assert((result.first: Char) === 'x') 59 | assert((result.middle: Int) === 123) 60 | assert((result.last: String) === "456") 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /core/shared/src/main/scala/io/expressier/Express.scala: -------------------------------------------------------------------------------- 1 | package io.expressier 2 | 3 | import io.expressier.utils.StringReplacements 4 | import scala.language.experimental.macros 5 | import shapeless._ 6 | import shapeless.ops.hlist.ZipWithKeys 7 | import shapeless.ops.record.{ Keys, SelectAll, Values } 8 | 9 | /** 10 | * A type class witnessing that `S` is a regular expression that can be used to 11 | * parse strings into values of type `Out`. 12 | */ 13 | trait Express[S <: String] { 14 | type Out 15 | def apply(input: String): Option[Out] 16 | def unapply(input: String): Option[Out] = apply(input) 17 | } 18 | 19 | object Express extends MidPriorityExpress { 20 | type To[Out0] = Express[_] { type Out = Out0 } 21 | 22 | implicit def materializeExpress[S <: String, T <: HList]: Aux[S, T] = 23 | macro WhiteboxMacros.materializeExpressImpl[S, T] 24 | } 25 | 26 | trait MidPriorityExpress extends LowPriorityExpress { 27 | implicit def labelledGenericExpress[S <: String, C, R <: HList](implicit 28 | gen: LabelledGeneric.Aux[C, R], 29 | exp: Aux[S, R] 30 | ): Aux[S, C] = new Express[S] { 31 | type Out = C 32 | def apply(input: String): Option[C] = exp(input).map(gen.from) 33 | } 34 | 35 | implicit def genericExpress[S <: String, C, R <: HList](implicit 36 | gen: Generic.Aux[C, R], 37 | exp: Aux[S, R] 38 | ): Aux[S, C] = new Express[S] { 39 | type Out = C 40 | def apply(input: String): Option[C] = exp(input).map(gen.from) 41 | } 42 | } 43 | 44 | /** 45 | * More complex instances. 46 | */ 47 | trait LowPriorityExpress { 48 | type Aux[S <: String, Out0] = Express[S] { type Out = Out0 } 49 | 50 | implicit def stringReplacedLabelledGenericExpress[ 51 | S <: String, 52 | C, 53 | Repr <: HList, 54 | ReprK <: HList, 55 | ReprV <: HList, 56 | Rec <: HList, 57 | RecV <: HList 58 | ](implicit 59 | gen: LabelledGeneric.Aux[C, Repr], 60 | keys: Keys.Aux[Repr, ReprK], 61 | vals: Values.Aux[Repr, ReprV], 62 | exp: Aux[S, Rec], 63 | sel: SelectAll.Aux[Rec, ReprK, RecV], 64 | sr: StringReplacements[RecV, ReprV], 65 | zipper: ZipWithKeys.Aux[ReprK, ReprV, Repr] 66 | ): Aux[S, C] = new Express[S] { 67 | type Out = C 68 | def apply(input: String): Option[C] = 69 | exp(input).map(res => gen.from(zipper(sr(sel(res))))) 70 | } 71 | 72 | implicit def stringReplacedGenericExpress[S <: String, C, T <: HList, R <: HList](implicit 73 | gen: Generic.Aux[C, R], 74 | exp: Aux[S, T], 75 | sr: StringReplacements[T, R] 76 | ): Aux[S, C] = new Express[S] { 77 | type Out = C 78 | def apply(input: String): Option[C] = exp(input).map(res => gen.from(sr(res))) 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /core/shared/src/main/scala/io/expressier/Macros.scala: -------------------------------------------------------------------------------- 1 | package io.expressier 2 | 3 | import scala.language.experimental.macros 4 | import scala.reflect.macros.{ blackbox, whitebox } 5 | import shapeless.HList 6 | 7 | @macrocompat.bundle 8 | class WhiteboxMacros(val c: whitebox.Context) { 9 | import patternParser.ResultItem 10 | import c.universe._ 11 | 12 | private[this] val expressTC = c.typeOf[Express[_]].typeConstructor 13 | private[this] val symTpe = c.typeOf[scala.Symbol] 14 | private[this] val tagTC = c.typeOf[shapeless.tag.@@[_, _]].typeConstructor 15 | private[this] val taggedSym = typeOf[shapeless.tag.Tagged[_]].typeConstructor.typeSymbol 16 | 17 | private[this] def stringSingletonTpe(s: String): c.Type = 18 | internal.constantType(Constant(s)) 19 | private[this] def symbolSingletonTpe(s: String): c.Type = 20 | appliedType(tagTC, List(symTpe, stringSingletonTpe(s))) 21 | 22 | private[this] def mkSymbolSingleton(s: String): Tree = 23 | q"""_root_.scala.Symbol($s).asInstanceOf[${symbolSingletonTpe(s)}]""" 24 | 25 | def stringContextImpl(): c.Expr[Any] = { 26 | val patternString = c.prefix.tree match { 27 | case q"${_}(${_}(${s: _root_.java.lang.String}))" => s 28 | } 29 | 30 | val instance = c.inferImplicitValue(appliedType(expressTC, stringSingletonTpe(patternString))) 31 | 32 | c.Expr[Any](q"new _root_.io.expressier.ExpressRecord($instance)") 33 | } 34 | 35 | def materializeExpressImpl[S <: String, T <: HList](implicit 36 | S: c.WeakTypeTag[S] 37 | ): c.Expr[Express.Aux[S, T]] = { 38 | val patternString = S.tpe match { 39 | case ConstantType(Constant(s: String)) => s 40 | case _ => c.abort(c.enclosingPosition, "Not a string literal singleton") 41 | } 42 | 43 | scala.util.Try(patternString.r.pattern).toOption.flatMap(pattern => 44 | patternParser.parsePattern(c.universe)(pattern) 45 | ).fold( 46 | c.abort(c.enclosingPosition, "Can't parse this regular expression") 47 | ) { results => 48 | val hlistTpe = results.foldRight(tq"_root_.shapeless.HNil": c.Tree) { 49 | case (ResultItem(None, tpe, _), acc) => tq"_root_.shapeless.::[$tpe, $acc]" 50 | case (ResultItem(Some(name), tpe, _), acc) => 51 | val nameTpe = mkSymbolSingleton(name) 52 | tq"""_root_.shapeless.::[_root_.shapeless.labelled.FieldType[$nameTpe, $tpe], $acc]""" 53 | } 54 | 55 | val resultNames = List.fill(results.size)(TermName(c.freshName)) 56 | val resultPatterns = resultNames.map(n => Bind(n, Ident(termNames.WILDCARD))) 57 | 58 | val converted = results.zip(resultNames).foldRight(q"_root_.shapeless.HNil": c.Tree) { 59 | case ((ResultItem(None, _, converter), resultName), acc) => 60 | q"_root_.shapeless.::(${ converter(Ident(resultName)) }, $acc)" 61 | case ((ResultItem(Some(name), _, converter), resultName), acc) => 62 | val nameTpe = mkSymbolSingleton(name) 63 | q""" 64 | _root_.shapeless.::( 65 | _root_.shapeless.labelled.field[$nameTpe](${ converter(Ident(resultName)) }), 66 | $acc 67 | ) 68 | """ 69 | } 70 | 71 | c.Expr[Express.Aux[S, T]]( 72 | q""" 73 | new _root_.io.expressier.Express[${c.weakTypeOf[S]}] { 74 | type Out = $hlistTpe 75 | private[this] val pattern = $patternString.r 76 | def apply(input: _root_.java.lang.String): _root_.scala.Option[$hlistTpe] = 77 | pattern.unapplySeq(input).map { 78 | case _root_.scala.List(..$resultPatterns) => $converted 79 | } 80 | } 81 | """ 82 | ) 83 | } 84 | } 85 | } 86 | 87 | @macrocompat.bundle 88 | class BlackboxMacros(val c: blackbox.Context) { 89 | import c.universe._ 90 | 91 | private[this] def stringSingletonTpe(s: String): c.Type = 92 | internal.constantType(Constant(s)) 93 | 94 | def stringImpl[T](implicit T: c.WeakTypeTag[T]): c.Expr[Express.To[T]] = { 95 | val patternString = c.prefix.tree match { 96 | case q"${_}(${s: _root_.java.lang.String})" => s 97 | } 98 | 99 | c.Expr[Express.To[T]](q""" 100 | implicitly[ 101 | _root_.io.expressier.Express.Aux[${stringSingletonTpe(patternString)}, $T] 102 | ]: _root_.io.expressier.Express.To[$T] 103 | """) 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /scalastyle-config.xml: -------------------------------------------------------------------------------- 1 | 2 | Scalastyle standard configuration 3 | 4 | 5 | FOR 6 | IF 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | true 77 | 78 | 79 | 80 | -------------------------------------------------------------------------------- /core/shared/src/main/scala/io/expressier/internals/NosyPatternParser.scala: -------------------------------------------------------------------------------- 1 | package io.expressier.internals 2 | 3 | import io.expressier.PatternParser 4 | import io.expressier.utils.JavaReflectionUtils 5 | import java.util.{ Map => JMap } 6 | import java.util.regex.Pattern 7 | import scala.collection.JavaConverters._ 8 | import scala.reflect.api.Universe 9 | 10 | class NosyPatternParser extends PatternParser with JavaReflectionUtils { 11 | lazy val patternClass = classOf[Pattern] 12 | lazy val ctypeClass = Class.forName("java.util.regex.Pattern$Ctype") 13 | lazy val curlyClass = Class.forName("java.util.regex.Pattern$Curly") 14 | lazy val dotClass = Class.forName("java.util.regex.Pattern$Dot") 15 | lazy val singleClass = Class.forName("java.util.regex.Pattern$Single") 16 | lazy val groupCurlyClass = Class.forName("java.util.regex.Pattern$GroupCurly") 17 | lazy val groupHeadClass = Class.forName("java.util.regex.Pattern$GroupHead") 18 | lazy val groupTailClass = Class.forName("java.util.regex.Pattern$GroupTail") 19 | lazy val nodeClass = Class.forName("java.util.regex.Pattern$Node") 20 | lazy val asciiClass = Class.forName("java.util.regex.ASCII") 21 | 22 | lazy val patternRoot = getField(patternClass, "root") 23 | lazy val patternGroupCount = getField(patternClass, "capturingGroupCount") 24 | lazy val patternNamedGroups = getMethod(patternClass, "namedGroups") 25 | lazy val nodeNext = getField(nodeClass, "next") 26 | lazy val curlyAtom = getField(curlyClass, "atom") 27 | lazy val groupTailGroupIndex = getField(groupTailClass, "groupIndex") 28 | lazy val groupCurlyGroupIndex = getField(groupCurlyClass, "groupIndex") 29 | lazy val ctypeCtype = getField(ctypeClass, "ctype") 30 | 31 | def getGroupCount(pattern: Pattern): Int = 32 | patternGroupCount.get(pattern).asInstanceOf[Int] 33 | 34 | def getNames(pattern: Pattern): Map[Int, String] = 35 | patternNamedGroups.invoke(pattern).asInstanceOf[ 36 | JMap[String, Integer] 37 | ].asScala.map { 38 | case (k, v) => (v.toInt - 1, k) 39 | }.toMap 40 | 41 | def getCurlyAtomList(curly: Any): List[Any] = getRest(curlyAtom.get(curly)) 42 | 43 | def getGroupIndex(groupTail: Any) = 44 | groupTailGroupIndex.get(groupTail).asInstanceOf[Int] 45 | 46 | def getCurlyGroupIndex(groupCurly: Any) = 47 | groupCurlyGroupIndex.get(groupCurly).asInstanceOf[Int] 48 | 49 | def getCtype(ctype: Any): Int = ctypeCtype.get(ctype).asInstanceOf[Int] 50 | 51 | def getASCIICode(name: String): Int = { 52 | val field = asciiClass.getDeclaredField(name) 53 | field.setAccessible(true) 54 | field.get(()).asInstanceOf[Int] 55 | } 56 | 57 | lazy val digitCode = getASCIICode("DIGIT") 58 | 59 | def getRest(start: Any): List[Any] = { 60 | val nodes = scala.collection.mutable.Buffer.empty[Any] 61 | var current = start 62 | 63 | while (current != null) { 64 | nodes += current 65 | current = nodeNext.get(current) 66 | } 67 | 68 | nodes.to[List] 69 | } 70 | 71 | def getNodes(pattern: Pattern): List[Any] = 72 | getRest(patternRoot.get(pattern)) 73 | 74 | sealed trait Group 75 | case class CandidateGroup(contents: List[Any]) extends Group 76 | case object OpaqueGroup extends Group 77 | 78 | def capturedGroups(pattern: Pattern): List[Group] = { 79 | getNodes(pattern).foldLeft((List.empty[Any], List.empty[Group])) { 80 | case ((nodes, acc), current) if groupHeadClass.isInstance(current) => 81 | (Nil, acc) 82 | 83 | case ((nodes, acc), current) if 84 | groupTailClass.isInstance(current) && getGroupIndex(current) > 0 => 85 | (Nil, acc :+ CandidateGroup(nodes)) 86 | 87 | case ((nodes, acc), current) if 88 | groupCurlyClass.isInstance(current) && getCurlyGroupIndex(current) > 0 => 89 | (Nil, acc :+ OpaqueGroup) 90 | case ((nodes, acc), current) => (nodes :+ current, acc) 91 | }._2 92 | } 93 | 94 | def isIntegral(nodes: List[Any]) = nodes.forall(node => 95 | ctypeClass.isInstance(node) && getCtype(node) == digitCode 96 | ) 97 | 98 | def parsePattern(u: Universe)(pattern: Pattern): Option[List[ResultItem[u.type]]] = { 99 | val names = getNames(pattern) 100 | val groups = capturedGroups(pattern) 101 | 102 | if (groups.size != getGroupCount(pattern) - 1) None else Some( 103 | capturedGroups(pattern).zipWithIndex.map { 104 | case (OpaqueGroup, i) => stringResult(u)(names.get(i)) 105 | 106 | case (CandidateGroup(node :: Nil), i) if 107 | curlyClass.isInstance(node) && isIntegral(getCurlyAtomList(node).init) => 108 | integerResult(u)(names.get(i)) 109 | 110 | case (CandidateGroup(nodes), i) if isIntegral(nodes) => 111 | integerResult(u)(names.get(i)) 112 | 113 | case (CandidateGroup(node :: Nil), i) if 114 | ctypeClass.isInstance(node) || 115 | dotClass.isInstance(node) || 116 | singleClass.isInstance(node) => 117 | characterResult(u)(names.get(i)) 118 | 119 | case (CandidateGroup(_), i) => stringResult(u)(names.get(i)) 120 | } 121 | ) 122 | } 123 | } 124 | 125 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # expressier 2 | 3 | [![Build status](https://travis-ci.org/travisbrown/expressier.svg?branch=master)](https://travis-ci.org/travisbrown/expressier) 4 | [![Coverage status](http://codecov.io/github/travisbrown/expressier/coverage.svg?branch=master)](http://codecov.io/github/travisbrown/expressier?branch=master) 5 | 6 | ## tl;dr 7 | 8 | Get more useful stuff out of your regular expressions: 9 | 10 | ```scala 11 | scala> import io.expressier._ 12 | import io.expressier._ 13 | 14 | scala> case class Item(name: String, x: Int, c: Char, s: String) 15 | defined class Item 16 | 17 | scala> val parse = """(?\w+): (?\d+), (?.), (?[A-Z]*)""".express[Item] 18 | parse: io.expressier.Express.To[Item] = ... 19 | 20 | scala> val result: Option[Item] = parse("Foo: 1001, ?, BAR") 21 | result: Option[Item] = Some(Item(Foo,1001,?,BAR)) 22 | ``` 23 | 24 | Or don't even bother with the case class: 25 | 26 | ```scala 27 | scala> val Qux = x"""(?.+) (?\d+) (?.) (?\1)""" 28 | 29 | scala> Qux("foo 12345 - foo").map(_.first) 30 | res0: Option[String] = Some(foo) 31 | 32 | scala> Qux("foo 12345 - foo").map(_.second) 33 | res1: Option[Int] = Some(12345) 34 | ``` 35 | 36 | A group matching `\d+` will end up as an `Int`, a `.` will be a `Char`, etc. 37 | If the types don't line up or you try to refer to a named group that doesn't 38 | exist, the compiler will let you know. 39 | 40 | ## Overview 41 | 42 | This is a quick proof-of-concept demonstration of what a type provider for 43 | regular expressions might look like. There are currently lots of limitations 44 | and problems with the implementation, and there's essentially no documentation 45 | beyond this README. For a more detailed introduction to type providers in Scala, 46 | please see [this project](https://github.com/travisbrown/type-provider-examples) 47 | and the [associated slide deck][type-provider-slides]. 48 | 49 | expressier is cross-built for Scala 2.10 and 2.11 (from a single codebase, 50 | thanks to Miles Sabin's new [macro-compat][macro-compat] project), but it 51 | provides only partial support for [Scala.js][scala-js], since named groups in 52 | regular expressions currently do not work on that platform. 53 | 54 | ## Motivation 55 | 56 | Suppose I've got a file with some kind of simple data format that I've read 57 | into a list of strings. 58 | 59 | ``` scala 60 | val rows = List( 61 | "en: 456, 123, 1203", 62 | "de: 12, 567, 200", 63 | "cz: 1, 32, 10" 64 | ) 65 | ``` 66 | 67 | Now suppose I only care about totals, and want a map from the language identifier 68 | to the sum of the three values. Assuming the format is fairly simple, it's 69 | reasonable to try to solve this problem with a regular expression: 70 | 71 | ``` scala 72 | val Row = """(\w\w)\s*:\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*""".r 73 | ``` 74 | 75 | Now we can write something like the following: 76 | 77 | ``` scala 78 | rows.map { 79 | case Row(id, first, second, third) => (id, first + second + third) 80 | case _ => sys.error("Invalid input") 81 | }.toMap 82 | ``` 83 | 84 | Except of course that thanks to [the magic of `any2stringadd`](https://issues.scala-lang.org/browse/SI-194) 85 | this gives us something completely different from what we expected: 86 | 87 | ``` scala 88 | Map(en -> 4561231203, de -> 12567200, cz -> 13210) 89 | ``` 90 | 91 | Where the values are strings, not integers. It's easy to forget that even 92 | though we know that we should be able to convert `(\d+)` into an integer 93 | (assuming we'll never have more than nine or ten digits), the compiler doesn't, 94 | and it's just handing those matches back to us as strings. So we write the 95 | following: 96 | 97 | ``` scala 98 | rows.map { 99 | case Row(id, first, second, third) => 100 | (id, first.toInt + second.toInt + third.toInt) 101 | case _ => sys.error("Invalid input") 102 | }.toMap 103 | ``` 104 | 105 | This works, but it's more verbose, a little redundant, and it involves a 106 | partial function. We know `toInt` should never explode here, but the compiler 107 | doesn't. 108 | 109 | ## With our type provider 110 | 111 | The implementation provided here allows the following usage: 112 | 113 | ``` scala 114 | import io.expressier._ 115 | 116 | val Row = x"""(\w\w)\s*:\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*""" 117 | 118 | val totals: Map[String, Int] = rows.collect { 119 | case Row(id, first, second, third) => (id, first + second + third) 120 | }.toMap 121 | ``` 122 | 123 | Or something like this: 124 | 125 | ``` scala 126 | scala> val Row(id, first, second, third) = "en: 456, 123, 1203" 127 | id: String = en 128 | first: Int = 456 129 | second: Int = 123 130 | third: Int = 1203 131 | ``` 132 | 133 | The digit groups are now typed as integers, and as a bonus we get compile-time 134 | validation for our regular expression syntax. 135 | 136 | ## With named groups 137 | 138 | Now suppose we've got a regular expression with some named groups: 139 | 140 | ``` scala 141 | import io.expressier._ 142 | 143 | val Data = x"""(?\w+)-(?.):(?\d+)""" 144 | ``` 145 | 146 | Now we can access these names as methods on the match: 147 | 148 | ``` scala 149 | val result = Data("foobar-Z:12345").getOrElse(sys.error("Invalid input")) 150 | ``` 151 | 152 | And then: 153 | 154 | ``` scala 155 | scala> result.id 156 | res0: String = foobar 157 | 158 | scala> result.sub 159 | res1: Char = Z 160 | 161 | scala> result.value 162 | res2: Int = 12345 163 | ``` 164 | 165 | Note that the type provider has determined that the sub-identifier can be 166 | returned as a single character. 167 | 168 | ## Case classes 169 | 170 | It's also now possible to collect the results of a regular expression match into 171 | a case class (thanks to [Shapeless][shapeless]'s `LabelledGeneric`): 172 | 173 | ```scala 174 | scala> case class Foo(i: Int, s: String, c: Char) 175 | defined class Foo 176 | 177 | scala> val parse = """(?\d{4}): (?.)(?[a-z]+)""".express[Foo] 178 | parse: io.expressier.Express.To[Foo] = ... 179 | 180 | scala> val result: Option[Foo] = parse("1234: Foo") 181 | result: Option[Foo] = Some(Foo(1234,oo,F)) 182 | ``` 183 | 184 | Note that the orders of the fields and named groups don't have to match. 185 | 186 | ## Warnings 187 | 188 | 189 | The implementation is a mess. There's no obvious library for parsing regular 190 | expressions in Java (or at least I didn't turn one up this evening), so I've 191 | just cracked open `java.util.regex`, and my `internals` package does a lot 192 | of horrible work with reflection on private fields and non-public classes 193 | inside `Pattern`. This is a limitation of the ecosystem, though, not of the 194 | approach. 195 | 196 | ## License 197 | 198 | circe is licensed under the **[Apache License, Version 2.0][apache]** (the 199 | "License"); you may not use this software except in compliance with the License. 200 | 201 | Unless required by applicable law or agreed to in writing, software 202 | distributed under the License is distributed on an "AS IS" BASIS, 203 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 204 | See the License for the specific language governing permissions and 205 | limitations under the License. 206 | 207 | [apache]: http://www.apache.org/licenses/LICENSE-2.0 208 | [macro-compat]: https://github.com/milessabin/macro-compat 209 | [scala-js]: http://www.scala-js.org/ 210 | [shapeless]: https://github.com/milessabin/shapeless 211 | [type-provider-slides]: https://github.com/travisbrown/type-provider-examples/blob/master/docs/scalar-2014-slides.pdf 212 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | --------------------------------------------------------------------------------