├── samples └── helloedn │ ├── project │ ├── build.properties │ └── plugins.sbt │ ├── build.sbt │ └── src │ └── main │ └── scala │ └── main.scala ├── project ├── build.properties ├── site.sbt └── plugins.sbt ├── .gitignore ├── macros └── src │ ├── main │ └── scala │ │ ├── package.scala │ │ └── macros.scala │ └── test │ └── scala │ └── MacrosSpec.scala ├── common └── src │ └── main │ └── scala │ ├── package.scala │ └── types.scala ├── stream-parser └── src │ ├── test │ └── scala │ │ └── ParserSpec.scala │ └── main │ └── scala │ ├── parser.scala │ └── package.scala ├── parser └── src │ ├── main │ └── scala │ │ ├── package.scala │ │ └── parser.scala │ └── test │ └── scala │ └── ParserSpec.scala ├── validation └── src │ ├── test │ └── scala │ │ ├── WriteSpec.scala │ │ └── ValidateSpec.scala │ └── main │ └── scala │ ├── package.scala │ ├── write.scala │ └── validate.scala ├── LICENSE └── README.md /samples/helloedn/project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=0.13.7 2 | -------------------------------------------------------------------------------- /project/build.properties: -------------------------------------------------------------------------------- 1 | <<<<<<< HEAD 2 | sbt.version=0.13.7 3 | ======= 4 | sbt.version=0.13.6 5 | >>>>>>> topic/unidoc 6 | -------------------------------------------------------------------------------- /project/site.sbt: -------------------------------------------------------------------------------- 1 | resolvers += "jgit-repo" at "http://download.eclipse.org/jgit/maven" 2 | 3 | addSbtPlugin("com.typesafe.sbt" % "sbt-site" % "0.8.1") 4 | 5 | addSbtPlugin("com.typesafe.sbt" % "sbt-ghpages" % "0.5.3") 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | logs 2 | project/project 3 | project/target 4 | target 5 | tmp 6 | .history 7 | dist 8 | /.idea 9 | /*.iml 10 | /out 11 | /.idea_modules 12 | /.classpath 13 | /.project 14 | /RUNNING_PID 15 | /.settings 16 | -------------------------------------------------------------------------------- /samples/helloedn/project/plugins.sbt: -------------------------------------------------------------------------------- 1 | resolvers ++= Seq( 2 | Resolver.url( 3 | "bintray-sbt-plugin-releases", 4 | url("http://dl.bintray.com/content/sbt/sbt-plugin-releases") 5 | )(Resolver.ivyStylePatterns) 6 | ) 7 | 8 | addSbtPlugin("me.lessis" % "bintray-sbt" % "0.1.2") 9 | -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | resolvers ++= Seq( 2 | Resolver.url( 3 | "bintray-sbt-plugin-releases", 4 | url("http://dl.bintray.com/content/sbt/sbt-plugin-releases") 5 | )(Resolver.ivyStylePatterns) 6 | , "jgit-repo" at "http://download.eclipse.org/jgit/maven" 7 | ) 8 | 9 | addSbtPlugin("me.lessis" % "bintray-sbt" % "0.1.2") 10 | 11 | addSbtPlugin("com.typesafe.sbt" % "sbt-git" % "0.6.4") 12 | 13 | addSbtPlugin("com.eed3si9n" % "sbt-unidoc" % "0.3.1") 14 | -------------------------------------------------------------------------------- /samples/helloedn/build.sbt: -------------------------------------------------------------------------------- 1 | name := "helloedn" 2 | 3 | resolvers += bintray.Opts.resolver.mavenRepo("mandubian") 4 | 5 | val scalednVersion = "1.0.0-e8180d08620a607ec47613f8c2585f7784e86625" 6 | 7 | libraryDependencies ++= Seq( 8 | // only need scaledn parser? 9 | "com.mandubian" %% "scaledn-parser" % scalednVersion 10 | // only need scaledn validation/serialization? 11 | , "com.mandubian" %% "scaledn-validation" % scalednVersion 12 | // only need scaledn macros? 13 | , "com.mandubian" %% "scaledn-macros" % scalednVersion 14 | ) 15 | -------------------------------------------------------------------------------- /samples/helloedn/src/main/scala/main.scala: -------------------------------------------------------------------------------- 1 | 2 | import shapeless._ 3 | 4 | import scaledn._ 5 | import parser._ 6 | 7 | import macros._ 8 | import validate._ 9 | import write._ 10 | 11 | 12 | object HelloEDN { 13 | def main(args: Array[String]) { 14 | val edn = EDN("""{"foo" 1, "bar" true, "baz" (1.2 2.3 3.4)}""") 15 | println("EDN:"+edn) 16 | assert(toEDNString(edn) == """{"foo" 1, "bar" true, "baz" (1.2 2.3 3.4)}""") 17 | } 18 | } 19 | 20 | 21 | // object HelloEDN extends App { 22 | // case class Address(lat:Double, lon:Double) 23 | // case class Person (name:String, addr:Address) 24 | 25 | // val data = """#yo.helloedn/Person {:name "yo", 26 | // :addr #yo.helloedn/Address {:lat 0.0, :lon 0.0}}""" 27 | 28 | // println(parseEDN(data).map(validateEDN[Person])) 29 | 30 | // } -------------------------------------------------------------------------------- /macros/src/main/scala/package.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2014 Pascal Voitot 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package scaledn 17 | 18 | 19 | /** EDN macro parsing EDN strings into Scala/Shapeless types at compile-time 20 | * 21 | * {{{ 22 | * import scaledn._ 23 | * import macros._ 24 | * 25 | * val s = EDNs("""(1 2 3) "toto" [true false] :foo/bar""") 26 | * }}} 27 | */ 28 | package object macros extends EDNMacros -------------------------------------------------------------------------------- /common/src/main/scala/package.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2014 Pascal Voitot 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package object scaledn { 17 | 18 | /** This is a dummy alias to make you believe you manipulate 19 | * a very special EDN type whereas it is just an Any restricted 20 | * by the EDN specified parsing to those types: 21 | * 22 | * - Long (64bits) 12345 23 | * - Double (64 bits) 123.45 24 | * - BigInt 12345M 25 | * - BigDecimal 123.45N 26 | * - String "foobar" 27 | * - Characters \c \newline \return \space \tab \\ \u0308 etc... 28 | * - EDN Symbol foo/bar 29 | * - EDN Keyword :foo/bar 30 | * - EDN Nil nil 31 | * - Tagged vaues #foo/bar value 32 | * - heterogenous list (1 true "toto") 33 | * - heterogenous vector [1 true "toto"] 34 | * - heterogenous set #{1 true "toto"} 35 | * - heterogenous map {1 "toto", 1.234 "toto"} 36 | * 37 | * For more info, go to the [[https://github.com/edn-format/edn EDN format site]] 38 | */ 39 | type EDN = Any 40 | 41 | /** Another alias to be used with Path writes (is it useful?) 42 | */ 43 | type EDNMap = Map[String, EDN] 44 | } -------------------------------------------------------------------------------- /stream-parser/src/test/scala/ParserSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2014 Pascal Voitot 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package scaledn 17 | package stream 18 | package parser 19 | 20 | import org.scalatest._ 21 | import scala.util.{Try, Success, Failure} 22 | 23 | import scalaz.stream.Process 24 | import scalaz.stream.parsers._ 25 | 26 | import EDNToken._ 27 | 28 | class StreamParserSpec extends FlatSpec with Matchers with TryValues { 29 | "EDN lexing" should "lex a string" in { 30 | val output = Process("\"toto\"": _*).toSource pipe tokenize(EDNToken.Rules) stripW 31 | 32 | output.runLog.run should equal (Seq(EDNStr("toto"))) 33 | } 34 | 35 | it should "lex a long" in { 36 | val output = Process("123": _*).toSource pipe tokenize(EDNToken.Rules) stripW 37 | 38 | output.runLog.run should equal (Seq(EDNLong(123))) 39 | } 40 | 41 | it should "lex a double" in { 42 | val output = Process("-123.345e-4": _*).toSource pipe tokenize(EDNToken.Rules, 2) stripW 43 | 44 | output.runLog.run should equal (Seq(EDNDouble(-123.345e-4))) 45 | } 46 | 47 | "EDN parsing" should "parse the fundamental values" in { 48 | parseEDN(Process("\"toto\"": _*).toSource).runLog.run should equal (Seq("toto")) 49 | parseEDN(Process("123": _*).toSource).runLog.run should equal (Seq(123L)) 50 | parseEDN(Process("-123.345e-4": _*).toSource).runLog.run should equal (Seq(-123.345e-4)) 51 | } 52 | } -------------------------------------------------------------------------------- /parser/src/main/scala/package.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2014 Pascal Voitot 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package scaledn 17 | 18 | 19 | package object parser { 20 | import org.parboiled2.ParserInput 21 | import scala.util.{Try, Success, Failure} 22 | 23 | /** parses a string input to EDN but keep only first element 24 | * 25 | * {{{ 26 | * import scaledn.parser._ 27 | * 28 | * parseEDN("""(1, "foo", :foo/bar)""") match { 29 | * case Success(t) => \/-(t) 30 | * case Failure(f : org.parboiled2.ParseError) => -\/(parser.formatError(f)) 31 | * } 32 | * }}} 33 | * 34 | * @param in the string input 35 | * @return a single EDN or the parsing error 36 | */ 37 | def parseEDN(in: ParserInput): Try[EDN] = EDNParser(in).Root.run().map(_.head) 38 | 39 | /** parses a string input to EDN and keep all elements 40 | * 41 | * {{{ 42 | * import scaledn.parser._ 43 | * 44 | * parseEDNs("""(1, "foo", :foo/bar) [1 2 3]""") match { 45 | * case Success(t) => \/-(t) 46 | * case Failure(f : org.parboiled2.ParseError) => -\/(parser.formatError(f)) 47 | * }}} 48 | * 49 | * [[https://github.com/sirthias/parboiled2/blob/master/parboiled-core/src/main/scala/org/parboiled2/ParserInput.scala org.parboiled2.ParserInput]] provides implicit conversions from [[java.lang.String]] or 50 | * [[http://docs.oracle.com/javase/6/docs/api/java/lang/String.html java.lang.Array[Byte]]] 51 | * 52 | * @param in the string input 53 | * @return a Seq[EDN] or the parsing error 54 | */ 55 | def parseEDNs(in: ParserInput): Try[Seq[EDN]] = EDNParser(in).Root.run() 56 | 57 | } -------------------------------------------------------------------------------- /common/src/main/scala/types.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2014 Pascal Voitot 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package scaledn 17 | 18 | 19 | /** AST root type 20 | * The types represented by the AST are: 21 | * 22 | * - EDN Symbol foo/bar 23 | * - EDN Keyword :foo/bar 24 | * - EDN Nil nil 25 | * - EDN Tagged values #foo/bar value 26 | * 27 | * 28 | * The following types aren't represented in the AST because the types 29 | * described in EDN specs are isomorphic/bijective with Scala types : 30 | * 31 | * - Long (64bits) 12345 32 | * - Double (64 bits) 123.45 33 | * - BigInt 12345M 34 | * - BigDecimal 123.45N 35 | * - String "foobar" 36 | * - Characters \c \newline \return \space \tab \\ \u0308 etc... 37 | * - heterogenous list (1 true "toto") 38 | * - heterogenous vector [1 true "toto"] 39 | * - heterogenous set #{1 true "toto"} 40 | * - heterogenous map {1 "toto", 1.234 "toto"} 41 | * 42 | * For more info, go to the [[https://github.com/edn-format/edn EDN format site]] 43 | */ 44 | sealed trait EDNValue 45 | 46 | /** EDN Symbol representing an generic identifier with a name and potentially a namespace 47 | * like: 48 | * {{{ 49 | * foo.bar/toto 50 | * }}} 51 | */ 52 | case class EDNSymbol(value: String, namespace: Option[String] = None) extends EDNValue { 53 | override def toString = value 54 | } 55 | 56 | /** EDN keyword representing a unique identifier with a name and potentially a namespace 57 | * like: 58 | * {{{ 59 | * :foo.bar/toto` 60 | * }}} 61 | */ 62 | case class EDNKeyword(value: EDNSymbol) extends EDNValue { 63 | override def toString = s":$value" 64 | } 65 | 66 | /** EDN tagged values look like `#foo.bar/toto 123L` and correspond to the extension 67 | * mechanism provided by EDN. The tag implies a semantic that should be managed by 68 | * a handler. By default, EDN provides 2 default handlers: 69 | * - #inst "1985-04-12T23:20:50.52Z" for RFC-3339 instants 70 | * - #uuid "f81d4fae-7dec-11d0-a765-00a0c91e6bf6" for UUID 71 | */ 72 | case class EDNTagged[A](tag: EDNSymbol, value: A) extends EDNValue { 73 | override def toString = s"#$tag ${value.toString}" 74 | } 75 | 76 | /** The EDN Nil value that can represent anything null/nil/nothing you need */ 77 | case object EDNNil extends EDNValue { 78 | override def toString = "nil" 79 | } 80 | 81 | /** Unneeded types 82 | 83 | case class EDNBoolean(value: Boolean) extends EDNValue 84 | case class EDNString(value: String) extends EDNValue 85 | case class EDNChar(value: Char) extends EDNValue 86 | 87 | case class EDNLong(value: Long) extends EDNValue 88 | case class EDNBigInt(value: BigInt) extends EDNValue 89 | 90 | case class EDNDouble(value: Double) extends EDNValue 91 | case class EDNBigDec(value: BigDecimal) extends EDNValue 92 | */ 93 | -------------------------------------------------------------------------------- /stream-parser/src/main/scala/parser.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2014 Pascal Voitot 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package scaledn 17 | package stream 18 | package parser 19 | 20 | import scalaz._ 21 | import scalaz.syntax.equal._ 22 | import scalaz.syntax.monad._ 23 | import scalaz.syntax.show._ 24 | import scalaz.std.anyVal._ 25 | 26 | import scalaz.stream._ 27 | import scalaz.stream.parsers._ 28 | 29 | import scala.util.matching.Regex 30 | 31 | import Parser._ 32 | 33 | 34 | /** copied from D.Spiewak Json sample */ 35 | sealed trait EDNToken { 36 | final val as: EDNToken = this // lightweight ascription 37 | } 38 | 39 | object EDNToken { 40 | case object LBrace extends EDNToken // { 41 | case object RBrace extends EDNToken // } 42 | 43 | case object LBracket extends EDNToken // [ 44 | case object RBracket extends EDNToken // ] 45 | 46 | case object Comma extends EDNToken // , 47 | 48 | final case class EDNStr(str: String) extends EDNToken // "foo" 49 | final case class EDNLong(value: Long) extends EDNToken // 3 50 | final case class EDNDouble(value: Double) extends EDNToken // 3.14 51 | final case class EDNExp(value: String) extends EDNToken // 3.14 52 | 53 | case object True extends EDNToken // true 54 | case object False extends EDNToken // false 55 | 56 | val stringLiteral = ("\""+"""(([^"\p{Cntrl}\\]|\\[\\'"bfnrt]|\\u[a-fA-F0-9]{4})*)"""+"\"").r 57 | 58 | val floatingPointNumber = """(-?(\d+(\.\d*)?|\d*\.\d+)([eE][+-]?\d+)?)""".r 59 | 60 | val decimalNumber = """(\d+(\.\d*)?|\d*\.\d+)""".r 61 | 62 | val naturalNumber = """(\d+)""".r 63 | 64 | val exponential = """([eE][+-]?\d+)""".r 65 | 66 | val Rules: Map[Regex, PartialFunction[List[String], EDNToken]] = Map( 67 | stringLiteral -> { case body :: _ => EDNStr(canonicalizeStr(body)) }, 68 | naturalNumber -> { case body :: _ => EDNLong(body.toLong) }, 69 | floatingPointNumber -> { case body :: _ => EDNDouble(body.toDouble) }, 70 | exponential -> { case body :: _ => EDNExp(body) } 71 | // """\{""".r -> { case Nil => LBrace }, 72 | // """\}""".r -> { case Nil => RBrace }, 73 | 74 | // """\[""".r -> { case Nil => LBracket }, 75 | // """\]""".r -> { case Nil => RBracket }, 76 | 77 | // """,""".r -> { case Nil => Comma }, 78 | 79 | 80 | // "true".r -> { case Nil => True }, 81 | // "false".r -> { case Nil => False } 82 | ) 83 | 84 | private def canonicalizeStr(body: String): String = { 85 | val (_, back) = body.foldLeft((false, "")) { 86 | case ((true, acc), c) => (false, acc + c) 87 | case ((false, acc), '\\') => (true, acc) 88 | case ((false, acc), c) => (false, acc + c) 89 | } 90 | 91 | back 92 | } 93 | 94 | implicit val eq: Equal[EDNToken] = Equal.equalA[EDNToken] 95 | implicit val show: Show[EDNToken] = Show.showA[EDNToken] 96 | 97 | } 98 | 99 | 100 | object SParser { 101 | import EDNToken._ 102 | 103 | lazy val root: Parser[EDNToken, EDN] = ( 104 | strValue 105 | | longValue 106 | | doubleValue 107 | ) 108 | 109 | lazy val strValue: Parser[EDNToken, EDN] = 110 | pattern { case EDNStr(body) => body } 111 | 112 | lazy val longValue: Parser[EDNToken, EDN] = 113 | pattern { case EDNLong(body) => body } 114 | 115 | lazy val doubleValue: Parser[EDNToken, EDN] = 116 | pattern { case EDNDouble(body) => body } 117 | } 118 | 119 | -------------------------------------------------------------------------------- /stream-parser/src/main/scala/package.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2014 Pascal Voitot 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package scaledn 17 | package stream 18 | 19 | import scalaz._ 20 | import scalaz.syntax.equal._ 21 | import scalaz.syntax.monad._ 22 | import scalaz.syntax.show._ 23 | import scalaz.std.anyVal._ 24 | import scalaz.concurrent.Task 25 | 26 | import scalaz.stream._ 27 | import scalaz.stream.parsers._ 28 | 29 | import scala.util.matching.Regex 30 | 31 | import Parser._ 32 | 33 | package object parser { 34 | 35 | def parseEDN(in: Process[Task, Char]): Process[Task, EDN] = 36 | in.pipe(tokenize(EDNToken.Rules, 2)) 37 | .stripW 38 | .pipe(parse(SParser.root)) 39 | .stripW 40 | 41 | /** 42 | * Somewhat-inefficiently (but usefully!) tokenizes an input stream of characers into 43 | * a stream of tokens given a set of regular expressions and mapping functions. Note 44 | * that the resulting process will have memory usage which is linearly proportional to 45 | * the longest *invalid* substring, so…be careful. There are better ways to implement 46 | * this function. MUCH better ways. 47 | */ 48 | def tokenize[T]( 49 | rules: Map[Regex, PartialFunction[List[String], T]], 50 | maxMatchDepth: Int = 1, 51 | whitespace: Option[Regex] = Some("""\s+""".r) 52 | ): Process1[Char, Char \/ T] = { 53 | import Process._ 54 | 55 | def iseqAsCharSeq(seq: IndexedSeq[Char]): CharSequence = new CharSequence { 56 | def charAt(i: Int) = seq(i) 57 | def length = seq.length 58 | def subSequence(start: Int, end: Int) = iseqAsCharSeq(seq.slice(start, end)) 59 | override def toString = seq.mkString 60 | } 61 | 62 | def attempt(buffer: CharSequence, requireIncomplete: Boolean): Option[T] = { 63 | println(s"buffer:$buffer") 64 | 65 | // glory to ugly mutable code ;) 66 | var opt: Option[T] = None 67 | val it = rules.iterator 68 | 69 | while(it.hasNext && opt.isEmpty) { 70 | val (pattern, pf) = it.next 71 | pattern 72 | .findPrefixMatchOf(buffer) 73 | .filter(!requireIncomplete || _.matched.length < buffer.length) 74 | .foreach { m => 75 | val subs = m.subgroups 76 | println(s"subs:$subs") 77 | if(pf isDefinedAt subs) opt = Some(pf(subs)) 78 | } 79 | } 80 | 81 | opt 82 | } 83 | 84 | /** 85 | * Buffer up characters until we get a prefix match PLUS one character that doesn't match (this 86 | * is to defeat early-completion of greedy matchers). Once we get a prefix match that satisfies 87 | * a rule in the map, emit the resulting token and flush the buffer. Any characters that aren't 88 | * matched by any rule are emitted. 89 | * 90 | * Note that the `tokenize` function should probably have a maxBuffer: Int parameter, or similar, 91 | * since it would be possible to DoS this function by simply feeding an extremely long unmatched 92 | * prefix. Better yet, we should just knuckle-down and write a DFA compiler. Would be a lot 93 | * simpler. 94 | */ 95 | def inner(buffer: Vector[Char], depth: Int): Process1[Char, Char \/ T] = { 96 | receive1Or[Char, Char \/ T]( 97 | attempt(iseqAsCharSeq(buffer), false) 98 | .map { \/-(_) } 99 | .map (emit) 100 | .getOrElse (emitAll(buffer map { -\/(_) })) 101 | ){ c => 102 | val buffer2 = buffer :+ c 103 | val csBuffer = iseqAsCharSeq(buffer2) 104 | 105 | val wsMatch = whitespace flatMap { _ findPrefixOf csBuffer } 106 | 107 | wsMatch match { 108 | case Some(prefix) => 109 | // if we matched prefix whitespace, move on with a clean buffer 110 | inner(buffer2 drop prefix.length, depth) 111 | 112 | case None => 113 | attempt(csBuffer, true) map (\/-(_)) match { 114 | case Some(t) => 115 | // it matched but maybe it will match on next one too 116 | // if (depth > 0), try on next input 117 | if(depth > 0) inner(buffer2, depth - 1) 118 | // if (depth == 0), accept this match & reset buffer 119 | else emit(t) ++ inner(Vector(c), maxMatchDepth) 120 | case None => 121 | inner(buffer2, depth) 122 | } 123 | } 124 | } 125 | } 126 | 127 | inner(Vector.empty, maxMatchDepth) 128 | 129 | } 130 | } -------------------------------------------------------------------------------- /macros/src/test/scala/MacrosSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2014 Pascal Voitot 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package scaledn 17 | package macros 18 | 19 | import org.scalatest._ 20 | 21 | import scala.util.{Try, Success, Failure} 22 | 23 | import scaledn._ 24 | import scaledn.macros._ 25 | 26 | 27 | class MacrosSpec extends FlatSpec with Matchers with TryValues { 28 | 29 | "EDN Macros" should "parse basic types" in { 30 | val e: String = EDN("\"toto\"") 31 | e should equal ("toto") 32 | 33 | val bt: Boolean = EDN("true") 34 | bt should equal (true) 35 | 36 | val bf: Boolean = EDN("false") 37 | bf should equal (false) 38 | 39 | val l: Long = EDN("123") 40 | l should equal (123L) 41 | 42 | val d: Double = EDN("123.456") 43 | d should equal (123.456) 44 | 45 | val bi: BigInt = EDN("123M") 46 | bi should equal (BigInt("123")) 47 | 48 | val bd: BigDecimal = EDN("123.456N") 49 | bd should equal (BigDecimal("123.456")) 50 | 51 | val s: EDNSymbol = EDN("foo/bar") 52 | s should equal (EDNSymbol("foo/bar", Some("foo"))) 53 | 54 | val kw: EDNKeyword = EDN(":foo/bar") 55 | kw should equal (EDNKeyword(EDNSymbol("foo/bar", Some("foo")))) 56 | 57 | val nil: EDNNil.type = EDN("nil") 58 | nil should equal (EDNNil) 59 | 60 | val list: List[Long] = EDN("(1 2 3)") 61 | list should equal (List(1L, 2L, 3L)) 62 | 63 | val list2: List[Any] = EDN("""(1 "toto" 3)""") 64 | list2 should equal (List(1L, "toto", 3L)) 65 | 66 | val vector: Vector[String] = EDN("""["tata" "toto" "tutu"]""") 67 | vector should equal (Vector("tata", "toto", "tutu")) 68 | 69 | val vector2: Vector[Any] = EDN("""[1 "toto" 3]""") 70 | vector2 should equal (Vector(1L, "toto", 3L)) 71 | 72 | val set: Set[Double] = EDN("#{1.23 2.45 3.23}") 73 | set should equal (Set(1.23, 2.45, 3.23)) 74 | 75 | val set2: Set[Any] = EDN("""#{1 "toto" 3}""") 76 | set2 should equal (Set(1L, "toto", 3L)) 77 | 78 | val map: Map[Long, String] = EDN("""{ 1 "toto", 2 "tata", 3 "tutu" }""") 79 | map should equal (Map(1 -> "toto", 2 -> "tata", 3 -> "tutu")) 80 | 81 | val map2: Map[Any, Any] = EDN("""{ 1 "toto", "tata" 2, 3 "tutu" }""") 82 | map2 should equal (Map(1 -> "toto", "tata" -> 2, 3 -> "tutu")) 83 | } 84 | 85 | it should "parse seq" in { 86 | val s: Seq[Any] = EDNs("""(1 2 3) "toto" [true false] :foo/bar""") 87 | s should equal (Seq( 88 | Seq(1L, 2L, 3L), 89 | "toto", 90 | Vector(true, false), 91 | EDNKeyword(EDNSymbol("foo/bar", Some("foo"))) 92 | )) 93 | } 94 | 95 | it should "parse single to hlist" in { 96 | import shapeless.{HNil, ::} 97 | import shapeless.record._ 98 | import shapeless.syntax.singleton._ 99 | 100 | val s: Long :: String :: Boolean :: HNil = EDNH("""(1 "toto" true)""") 101 | s should equal (1L :: "toto" :: true :: HNil) 102 | 103 | val s2: Long = EDNH("""1""") 104 | s2 should equal (1L) 105 | 106 | val s3 = EDNH("""{1 "toto" true 1.234 "foo" (1 2 3)}""") 107 | s3 should equal ( 108 | 1L ->> "toto" :: 109 | true ->> 1.234 :: 110 | "foo" ->> List(1L, 2L, 3L) :: 111 | HNil 112 | ) 113 | 114 | val s4 = EDNHR("""{1 "toto" true 1.234 "foo" (1 2 3)}""") 115 | s4 should equal ( 116 | 1L ->> "toto" :: 117 | true ->> 1.234 :: 118 | "foo" ->> (1L :: 2L :: 3L :: HNil) :: 119 | HNil 120 | ) 121 | } 122 | 123 | it should "parse multiple to hlist" in { 124 | import shapeless.{HNil, ::} 125 | val s: List[Long] :: String :: Vector[Boolean] :: EDNKeyword :: HNil = EDNHs("""(1 2 3) "toto" [true false] :foo/bar""") 126 | s should equal ( 127 | Seq(1L, 2L, 3L) :: 128 | "toto" :: 129 | Vector(true, false) :: 130 | EDNKeyword(EDNSymbol("foo/bar", Some("foo"))) :: 131 | HNil 132 | ) 133 | 134 | val s2 = EDNHRs("""(1 2 3) "toto" [true false] :foo/bar""") 135 | s2 should equal ( 136 | (1L :: 2L :: 3L :: HNil) :: 137 | "toto" :: 138 | (true :: false :: HNil) :: 139 | EDNKeyword(EDNSymbol("foo/bar", Some("foo"))) :: 140 | HNil 141 | ) 142 | } 143 | 144 | it should "use string interpolation" in { 145 | import shapeless.{HNil, ::} 146 | 147 | val l = 123L 148 | val s = List("foo", "bar") 149 | 150 | val r: Long = EDN(s"$l") 151 | 152 | val r1: Seq[Any] = EDN(s"($l $s)") 153 | val r2: Long :: List[String] :: HNil = EDNH(s"($l $s)") 154 | } 155 | 156 | } -------------------------------------------------------------------------------- /validation/src/test/scala/WriteSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2014 Pascal Voitot 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package scaledn 17 | package validate 18 | 19 | import org.scalatest._ 20 | 21 | import scala.util.{Try, Success, Failure} 22 | 23 | import play.api.data.mapping._ 24 | import play.api.libs.functional.syntax._ 25 | 26 | import scaledn._ 27 | import write._ 28 | import macros._ 29 | 30 | class WriteSpec extends FlatSpec with Matchers with TryValues { 31 | 32 | case class Address(street: String, cp: Option[Int]) 33 | case class Person(name: String, age: Int, addr: Address) 34 | 35 | case class Person2(name: String, age: Option[Int]) 36 | 37 | "EDN Write" should "write basic types" in { 38 | toEDNString("toto") should equal ("\"toto\"") 39 | toEDNString(true) should equal ("true") 40 | toEDNString(false) should equal ("false") 41 | toEDNString(123L) should equal ("123") 42 | toEDNString(123) should equal ("123") 43 | toEDNString(BigInt("123")) should equal ("123N") 44 | toEDNString(123.456) should equal ("123.456") 45 | toEDNString(BigDecimal("123.456")) should equal ("123.456M") 46 | toEDNString('c') should equal ("""\c""") 47 | toEDNString('\r') should equal ("""\return""") 48 | toEDNString('\n') should equal ("""\newline""") 49 | toEDNString('\u0308') should equal ("\\u0308") 50 | } 51 | 52 | it should "write collections" in { 53 | toEDNString(List(1, 2, 3)) should equal ("""(1 2 3)""") 54 | toEDNString(Vector(1, 2, 3)) should equal ("""[1 2 3]""") 55 | toEDNString(Set(1, 2, 3)) should equal ("""#{1 2 3}""") 56 | toEDNString(Map("toto" -> 1, "tata" -> 2, "tutu" -> 3)) should equal ("""{"toto" 1, "tata" 2, "tutu" 3}""") 57 | toEDNString(Seq(1, 2, 3)) should equal ("""(1 2 3)""") 58 | } 59 | 60 | it should "write to path" in { 61 | toEDNString((Path \ "foo" \ "bar").write[String, EDNMap].writes("toto")) should equal ("""{"foo" {"bar" "toto"}}""") 62 | } 63 | 64 | it should "write hlist" in { 65 | import shapeless.{::, HNil} 66 | // import Writes._ 67 | 68 | implicitly[Write[Long :: String :: HNil, String]] 69 | 70 | toEDNString(1 :: true :: List(1L, 2L, 3L) :: HNil) should equal ("""(1 true (1 2 3))""") 71 | } 72 | 73 | it should "write case class & tuple" in { 74 | import shapeless.{::, HNil, HasProductGeneric, IsTuple} 75 | 76 | toEDNString(Person("toto", 34, Address("chboing", Some(75009)))) should equal ( 77 | """{"name" "toto", "age" 34, "addr" {"street" "chboing", "cp" 75009}}""" 78 | ) 79 | 80 | toEDNString(Person("toto", 34, Address("chboing", None))) should equal ( 81 | """{"name" "toto", "age" 34, "addr" {"street" "chboing"}}""" 82 | ) 83 | 84 | toEDNString((23, true)) should equal ("""[23 true]""") 85 | toEDNString((23, Vector(1, 2, 3), "toto" :: 2 :: true :: HNil, Person("toto", 34, Address("chboing", Some(75009))))) should equal ( 86 | """[23 [1 2 3] ("toto" 2 true) {"name" "toto", "age" 34, "addr" {"street" "chboing", "cp" 75009}}]""" 87 | ) 88 | 89 | toEDNString(Person2("toto", Some(34))) should equal ( 90 | """{"name" "toto", "age" 34}""" 91 | ) 92 | 93 | toEDNString(Person2("toto", None)) should equal ( 94 | """{"name" "toto"}""" 95 | ) 96 | } 97 | 98 | it should "write tagged classes" in { 99 | 100 | implicit val taggedPerson = Write{ p: Person => 101 | EDN(s"#myns/person {:name ${p.name}, :age ${p.age}, :addr {:street ${p.addr.street}, :cp ${p.addr.cp}} }") 102 | } 103 | 104 | toEDNString(Person("toto", 34, Address("chboing", Some(75009)))) should equal ( 105 | """#myns/person {:name "toto", :age 34, :addr {:street "chboing", :cp 75009}}""" 106 | ) 107 | 108 | toEDNString(Person("toto", 34, Address("chboing", None))) should equal ( 109 | """#myns/person {:name "toto", :age 34, :addr {:street "chboing"}}""" 110 | ) 111 | } 112 | 113 | it should "write tagged classes embedded" in { 114 | 115 | implicit val taggedAddr = Write{ addr: Address => 116 | EDN(s"#myns/addr {:street ${addr.street}, :cp ${addr.cp}}") 117 | } 118 | 119 | implicit val taggedPerson = Write{ p: Person => 120 | EDN(s"#myns/person {:name ${p.name}, :age ${p.age}, :addr ${tagged(p.addr)} }") 121 | } 122 | 123 | toEDNString(Person("toto", 34, Address("chboing", Some(75009)))) should equal ( 124 | """#myns/person {:name "toto", :age 34, :addr #myns/addr {:street "chboing", :cp 75009}}""" 125 | ) 126 | 127 | toEDNString(Person("toto", 34, Address("chboing", None))) should equal ( 128 | """#myns/person {:name "toto", :age 34, :addr #myns/addr {:street "chboing"}}""" 129 | ) 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /validation/src/test/scala/ValidateSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2014 Pascal Voitot 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package scaledn 17 | package validate 18 | 19 | import org.scalatest._ 20 | 21 | import scala.util.{Try, Success, Failure} 22 | 23 | import play.api.data.mapping._ 24 | import play.api.data.mapping.{Success => VS} 25 | 26 | import scaledn._ 27 | import scaledn.parser._ 28 | import scaledn.validate._ 29 | 30 | 31 | class ValidateSpec extends FlatSpec with Matchers with TryValues { 32 | 33 | case class CP(cp: Int) 34 | case class Address(street: String, cp: CP) 35 | case class Person(name: String, age: Int, addr: Address) 36 | 37 | "EDN Validation" should "validate String" in { 38 | parseEDN("\"foobar\"").map(validateEDN[String]).success.value should be ( 39 | play.api.data.mapping.Success("foobar") 40 | ) 41 | 42 | parseEDN("\"foobar\"").map(Path.read[EDN, String].validate).success.value should be ( 43 | play.api.data.mapping.Success("foobar") 44 | ) 45 | 46 | parseEDN("\"foobar\"").map(From[EDN]{ __ => __.read[String] }.validate).success.value should be ( 47 | play.api.data.mapping.Success("foobar") 48 | ) 49 | } 50 | 51 | it should "validate Long" in { 52 | parseEDN("12345").map(validateEDN[Long]).success.value should be ( 53 | play.api.data.mapping.Success(12345L) 54 | ) 55 | } 56 | 57 | it should "validate Double" in { 58 | parseEDN("12345.123").map(validateEDN[Double]).success.value should be ( 59 | play.api.data.mapping.Success(12345.123) 60 | ) 61 | } 62 | 63 | it should "validate Symbol" in { 64 | parseEDN("foo.foo2/bar").map(validateEDN[EDNSymbol]).success.value should be ( 65 | play.api.data.mapping.Success(EDNSymbol("foo.foo2/bar", Some("foo.foo2"))) 66 | ) 67 | } 68 | 69 | it should "validate Keyword" in { 70 | parseEDN(":foo.foo2/bar").map(validateEDN[EDNKeyword]).success.value should be ( 71 | play.api.data.mapping.Success(EDNKeyword(EDNSymbol("foo.foo2/bar", Some("foo.foo2")))) 72 | ) 73 | } 74 | 75 | it should "validate List[Long]" in { 76 | parseEDN("""(1 2 3 4 5)""").map(validateEDN[List[Long]]).success.value should be ( 77 | play.api.data.mapping.Success(List(1L, 2L, 3L, 4L, 5L)) 78 | ) 79 | 80 | parseEDN("""(1 2 3 "toto" 5)""").map(validateEDN[List[Long]]).success.value should be ( 81 | play.api.data.mapping.Failure(Seq(Path \ 3 -> Seq(ValidationError("error.number", "Long")))) 82 | ) 83 | } 84 | 85 | it should "validate Seq[Long]" in { 86 | parseEDN("""(1 2 3 4 5)""").map(validateEDN[Seq[Long]]).success.value should be ( 87 | play.api.data.mapping.Success(List(1L, 2L, 3L, 4L, 5L)) 88 | ) 89 | } 90 | 91 | it should "validate Vector[Long]" in { 92 | parseEDN("""[1 2 3 4 5]""").map(validateEDN[Vector[Long]]).success.value should be ( 93 | play.api.data.mapping.Success(Vector(1L, 2L, 3L, 4L, 5L)) 94 | ) 95 | parseEDN("""[1 2 3 "toto" 5]""").map(validateEDN[Vector[Long]]).success.value should be ( 96 | play.api.data.mapping.Failure(Seq(Path \ 3 -> Seq(ValidationError("error.number", "Long")))) 97 | ) 98 | } 99 | 100 | it should "validate Set[Long]" in { 101 | parseEDN("""#{1 2 3 4 5}""").map(validateEDN[Set[Long]]).success.value should be ( 102 | play.api.data.mapping.Success(Set(1L, 2L, 3L, 4L, 5L)) 103 | ) 104 | parseEDN("""#{1 2 3 "toto" 5}""").map(validateEDN[Set[Long]]).success.value should be ( 105 | play.api.data.mapping.Failure(Seq(Path \ "toto" -> Seq(ValidationError("error.number", "Long")))) 106 | ) 107 | } 108 | 109 | it should "validate Map[String, Long]" in { 110 | parseEDN("""{ "toto" 1 "tata" 2 "tutu" 3 }""").map(validateEDN[Map[String, Long]]).success.value should be ( 111 | play.api.data.mapping.Success(Map( 112 | "toto" -> 1L, 113 | "tata" -> 2L, 114 | "tutu" -> 3L 115 | )) 116 | ) 117 | } 118 | 119 | it should "validate Map[Long, String]" in { 120 | parseEDN("""{ 1 "toto" 2 "tata" 3 "tutu" }""").map(validateEDN[Map[Long, String]]).success.value should be ( 121 | play.api.data.mapping.Success(Map( 122 | 1L -> "toto", 123 | 2L -> "tata", 124 | 3L -> "tutu" 125 | )) 126 | ) 127 | } 128 | 129 | it should "validate Option[Long]" in { 130 | parseEDN("""{ "toto" 1 "tata" 2 "tutu" 3 }""").map( 131 | (Path \ "tata").read[EDN, Option[Long]].validate 132 | ).success.value should be ( 133 | play.api.data.mapping.Success(Some(2)) 134 | ) 135 | } 136 | 137 | it should "validate deep path Double" in { 138 | parseEDN("""{ "toto" 1, "tata" { "foo" 2.123, "bar" 3 }, "tutu" 3 }""").map( 139 | (Path \ "tata" \ "foo").read[EDN, Double].validate 140 | ).success.value should be ( 141 | play.api.data.mapping.Success(2.123) 142 | ) 143 | } 144 | 145 | it should "validate hlist" in { 146 | import shapeless._ 147 | import scaledn.EDNNil 148 | 149 | parseEDN("""(1 "toto" true nil)""").map( 150 | validateEDN[Long :: String :: Boolean :: EDNNil.type :: HNil] 151 | ).success.value should be ( 152 | play.api.data.mapping.Success(1L :: "toto" :: true :: EDNNil :: HNil) 153 | ) 154 | 155 | parseEDN("""[1 "toto" true nil]""").map( 156 | validateEDN[Long :: String :: Boolean :: EDNNil.type :: HNil] 157 | ).success.value should be ( 158 | play.api.data.mapping.Success(1L :: "toto" :: true :: EDNNil :: HNil) 159 | ) 160 | } 161 | 162 | it should "validate case class / tuples" in { 163 | import shapeless.HasProductGeneric 164 | 165 | parseEDN("""("toto" 34 ("chboing" (75009)))""").map( 166 | validateEDN[Person] 167 | ).success.value should be ( 168 | play.api.data.mapping.Success(Person("toto", 34, Address("chboing", CP(75009)))) 169 | ) 170 | 171 | parseEDN("""{"name" "toto", "age" 34, "addr" {"street" "chboing", "cp" {"cp" 75009}}}""").map( 172 | validateEDN[Person] 173 | ).success.value should be ( 174 | play.api.data.mapping.Success(Person("toto", 34, Address("chboing", CP(75009)))) 175 | ) 176 | 177 | parseEDN("""("toto" 34 {"street" "chboing", "cp" {"cp" 75009}})""").map( 178 | validateEDN[Tuple3[String, Int, Address]] 179 | ).success.value should be ( 180 | play.api.data.mapping.Success(("toto", 34, Address("chboing", CP(75009)))) 181 | ) 182 | 183 | } 184 | } -------------------------------------------------------------------------------- /validation/src/main/scala/package.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2014 Pascal Voitot 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package scaledn 17 | 18 | /** 19 | * Write Rules to serialize Scala/Shapeless structures to EDN formatted String 20 | * 21 | * {{{ 22 | * import scaledn._ 23 | * import write._ 24 | * 25 | * // All primitives types are managed 26 | * toEDNString("toto") should equal ("\"toto\"") 27 | * toEDNString(123) should equal ("123") 28 | * toEDNString(BigInt("123")) should equal ("123M") 29 | * toEDNString(123.456) should equal ("123.456") 30 | * toEDNString(BigDecimal("123.456")) should equal ("123.456N") 31 | * toEDNString('\r') should equal ("""\return""") 32 | * toEDNString('\u0308') should equal ("\\u0308") 33 | * 34 | * // Scala collections too 35 | * toEDNString(List(1, 2, 3)) should equal ("""(1 2 3)""") 36 | * toEDNString(Vector(1, 2, 3)) should equal ("""[1 2 3]""") 37 | * toEDNString(Set(1, 2, 3)) should equal ("""#{1 2 3}""") 38 | * toEDNString(Map("toto" -> 1, "tata" -> 2, "tutu" -> 3)) should equal ("""{"toto" 1,"tata" 2,"tutu" 3}""") 39 | * 40 | * // Shapeless HList 41 | * toEDNString(1 :: true :: List(1L, 2L, 3L) :: HNil) should equal ("""(1 true (1 2 3))""") 42 | * 43 | * // Tuples 44 | * toEDNString( 45 | * (23, Vector(1, 2, 3), "toto" :: 2 :: true :: HNil, Person("toto", 34, Address("chboing", 75009))) 46 | * ) should equal ( 47 | * """(23 [1 2 3] ("toto" 2 true) {"name" "toto", "age" 34, "addr" {"street" "chboing", "cp" 75009}})""" 48 | * ) 49 | * 50 | * // Case classes (without) 51 | * case class Address(street: String, cp: Int) 52 | * case class Person(name: String, age: Int, addr: Address) 53 | * 54 | * toEDNString(Person("toto", 34, Address("chboing", 75009))) should equal ( 55 | * """{"name" "toto", "age" 34, "addr" {"street" "chboing", "cp" 75009}}""" 56 | * ) 57 | * }}} 58 | */ 59 | package object write extends Writes { 60 | import play.api.data.mapping.WriteLike 61 | 62 | /** 63 | * Serializes Scala/Shapeless structures to EDN formatted String 64 | * 65 | * {{{ 66 | * import scaledn._ 67 | * import write._ 68 | * 69 | * // All primitives types are managed 70 | * toEDNString("toto") should equal ("\"toto\"") 71 | * }}} 72 | */ 73 | def toEDNString[I](i: I)(implicit w: WriteLike[I, String]): String = w.writes(i) 74 | 75 | /** 76 | * Serializes Scala/Shapeless structures to a EDN Tagged value 77 | * 78 | * {{{ 79 | * import scaledn._ 80 | * import write._ 81 | * 82 | * implicit val taggedAddr = Write{ addr: Address => 83 | * EDN(s"#myns/addr {:street ${addr.street}, :cp ${addr.cp}}") 84 | * } 85 | * 86 | * tagged(Addr("Paris", CP(75009))) should equal (EDN("#myns/addr {:street "Paris", :cp 75009}")) 87 | * }}} 88 | */ 89 | def tagged[I, J](i: I)(implicit w: WriteLike[I, EDNTagged[J]]): EDNTagged[J] = w.writes(i) 90 | } 91 | 92 | /** 93 | * Validation Rules to validate EDN types to Scala/Shapeless structures 94 | * 95 | * As explained in common package doc, all primitive EDN types are isomorphic/bijective 96 | * to Scala types so the validation is often a simple runtime cast except for 97 | * heterogenous collection where using HList becomes really interesting. 98 | * 99 | * Validation is also interesting for case classes. 100 | * 101 | * It is based on generic [validation API](https://github.com/jto/validation) developed 102 | * by Julien Tournay. 103 | * 104 | * {{{ 105 | * import scaledn._ 106 | * import parser._ 107 | * import validate._ 108 | * 109 | * // basic types 110 | * parseEDN("\"foobar\"").map(validateEDN[String]).success.value should be ( 111 | * play.api.data.mapping.Success("foobar") 112 | * ) 113 | * parseEDN("12345").map(validateEDN[Long]).success.value should be ( 114 | * play.api.data.mapping.Success(12345L) 115 | * ) 116 | * parseEDN("12345.123").map(validateEDN[Double]).success.value should be ( 117 | * play.api.data.mapping.Success(12345.123) 118 | * ) 119 | * 120 | * // Scala collections 121 | * parseEDN("""(1 2 3 4 5)""").map(validateEDN[List[Long]]).success.value should be ( 122 | * play.api.data.mapping.Success(List(1L, 2L, 3L, 4L, 5L)) 123 | * ) 124 | * parseEDN("""(1 2 3 "toto" 5)""").map(validateEDN[List[Long]]).success.value should be ( 125 | * play.api.data.mapping.Failure(Seq(Path \ 3 -> Seq(ValidationError("error.number", "Long")))) 126 | * ) 127 | * parseEDN("""[1 2 3 4 5]""").map(validateEDN[Vector[Long]]).success.value should be ( 128 | * play.api.data.mapping.Success(Vector(1L, 2L, 3L, 4L, 5L)) 129 | * ) 130 | * parseEDN("""#{1 2 3 4 5}""").map(validateEDN[Set[Long]]).success.value should be ( 131 | * play.api.data.mapping.Success(Set(1L, 2L, 3L, 4L, 5L)) 132 | * ) 133 | * parseEDN("""{ 1 "toto" 2 "tata" 3 "tutu" }""").map(validateEDN[Map[Long, String]]).success.value should be ( 134 | * play.api.data.mapping.Success(Map( 135 | * 1L -> "toto", 136 | * 2L -> "tata", 137 | * 3L -> "tutu" 138 | * )) 139 | * ) 140 | * 141 | * // Shapeless heterogenous lists 142 | * parseEDN("""(1 "toto" true nil)""").map( 143 | * validateEDN[Long :: String :: Boolean :: EDNNil.type :: HNil] 144 | * ).success.value should be ( 145 | * play.api.data.mapping.Success(1L :: "toto" :: true :: EDNNil :: HNil) 146 | * ) 147 | * 148 | * // Path validation in a map only 149 | * parseEDN("""{ "toto" 1, "tata" { "foo" 2.123, "bar" 3 }, "tutu" 3 }""").map( 150 | * (Path \ "tata" \ "foo").read[EDN, Double].validate 151 | * ).success.value should be ( 152 | * play.api.data.mapping.Success(2.123) 153 | * ) 154 | * 155 | * // case class validation from heteregenous collections 156 | * parseEDN("""("toto" 34 ("chboing" (75009)))""").map( 157 | * validateEDN[Person] 158 | * ).success.value should be ( 159 | * play.api.data.mapping.Success(Person("toto", 34, Address("chboing", CP(75009)))) 160 | * ) 161 | * 162 | * // case class validation from heteregenous maps 163 | * parseEDN("""{"name" "toto", "age" 34, "addr" {"street" "chboing", "cp" {"cp" 75009}}}""").map( 164 | * validateEDN[Person] 165 | * ).success.value should be ( 166 | * play.api.data.mapping.Success(Person("toto", 34, Address("chboing", CP(75009)))) 167 | * ) 168 | * 169 | * // tuple validation from heteregenous maps 170 | * parseEDN("""("toto" 34 {"street" "chboing", "cp" {"cp" 75009}})""").map( 171 | * validateEDN[Tuple3[String, Int, Address]] 172 | * ).success.value should be ( 173 | * play.api.data.mapping.Success(("toto", 34, Address("chboing", CP(75009)))) 174 | * ) 175 | * 176 | * }}} 177 | * 178 | */ 179 | package object validate extends Rules { 180 | import play.api.data.mapping.{RuleLike, Validation} 181 | 182 | /** 183 | * Validate an EDN type to a Scala type 184 | * 185 | * It is based on generic [[https://github.com/jto/validation Validation API]] developed 186 | * by Julien Tournay. 187 | * 188 | * {{{ 189 | * import scaledn._ 190 | * import validate._ 191 | * 192 | * // validation 193 | * // Path validation in a map only 194 | * parseEDN("""{ "toto" 1, "tata" { "foo" 2.123, "bar" 3 }, "tutu" 3 }""").map( 195 | * (Path \ "tata" \ "foo").read[EDN, Double].validate 196 | * ).success.value should be ( 197 | * play.api.data.mapping.Success(2.123) 198 | * ) 199 | * 200 | * // case class validation from heteregenous collections 201 | * parseEDN("""("toto" 34 ("chboing" (75009)))""").map( 202 | * validateEDN[Person] 203 | * ).success.value should be ( 204 | * play.api.data.mapping.Success(Person("toto", 34, Address("chboing", CP(75009)))) 205 | * ) 206 | * 207 | * // case class validation from heteregenous maps 208 | * parseEDN("""{"name" "toto", "age" 34, "addr" {"street" "chboing", "cp" {"cp" 75009}}}""").map( 209 | * validateEDN[Person] 210 | * ).success.value should be ( 211 | * play.api.data.mapping.Success(Person("toto", 34, Address("chboing", CP(75009)))) 212 | * ) 213 | * 214 | * // tuple validation from heteregenous maps 215 | * parseEDN("""("toto" 34 {"street" "chboing", "cp" {"cp" 75009}})""").map( 216 | * validateEDN[Tuple3[String, Int, Address]] 217 | * ).success.value should be ( 218 | * play.api.data.mapping.Success(("toto", 34, Address("chboing", CP(75009)))) 219 | * ) 220 | * }}} 221 | */ 222 | def validateEDN[T](edn: EDN)(implicit r: RuleLike[EDN, T]): Validation[EDN, T] = r.validate(edn) 223 | } -------------------------------------------------------------------------------- /validation/src/main/scala/write.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2014 Pascal Voitot 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package scaledn 17 | package write 18 | 19 | import play.api.data.mapping._ 20 | 21 | import scaledn._ 22 | 23 | 24 | object Writes extends Writes 25 | 26 | trait Writes extends LowWrites { 27 | 28 | import play.api.libs.functional.Monoid 29 | 30 | implicit def jsonMonoid = new Monoid[EDNMap] { 31 | def append(a1: EDNMap, a2: EDNMap) = a1 ++ a2 32 | def identity = Map.empty[String, EDN] 33 | } 34 | 35 | implicit val stringW = Write[String, String]( str => "\"" + str + "\"" ) 36 | implicit val booleanW = Write[Boolean, String]( l => if(l) "true" else "false" ) 37 | implicit val longW = Write[Long, String]( l => l.toString ) 38 | implicit val shortW = Write[Short, String]( l => l.toString ) 39 | implicit val intW = Write[Int, String]( l => l.toString ) 40 | implicit val floatW = Write[Float, String]( l => l.toString ) 41 | implicit val doubleW = Write[Double, String]( l => l.toString ) 42 | implicit val bigIntW = Write[BigInt, String]( l => l.toString + "N" ) 43 | implicit val bigDecimalW = Write[BigDecimal, String]( l => l.toString + "M" ) 44 | 45 | implicit val charW = Write[Char, String]{ 46 | case '\n' => """\newline""" 47 | case '\r' => """\return""" 48 | case ' ' => """\space""" 49 | case '\t' => """\tab""" 50 | case '\\' => "\\" 51 | case c if c.isLetterOrDigit => "\\"+c 52 | case unicode => "\\u%04X".format(unicode.toInt) 53 | } 54 | 55 | implicit val symbolW = Write[EDNSymbol, String]( s => s.toString ) 56 | implicit val nilW = Write[EDNNil.type, String]( s => "nil" ) 57 | implicit val keywordW = Write[EDNKeyword, String]( s => ":" + symbolW.writes(s.value) ) 58 | 59 | implicit def taggedW[A](implicit w: Write[A, String]) = 60 | Write[EDNTagged[A], String]( t => s"#${t.tag} ${w.writes(t.value)}" ) 61 | 62 | implicit def seqSW[A](implicit w: Write[A, String]) = Write[Seq[A], String]( s => s.map(w.writes).mkString("(", " ", ")") ) 63 | implicit def listSW[A](implicit w: Write[A, String]) = Write[List[A], String]( s => s.map(w.writes).mkString("(", " ", ")") ) 64 | implicit def vectorSW[A](implicit w: Write[A, String]) = Write[Vector[A], String]( s => s.map(w.writes).mkString("[", " ", "]") ) 65 | implicit def setSW[A](implicit w: Write[A, String]) = Write[Set[A], String]( s => s.map(w.writes).mkString("#{", " ", "}") ) 66 | implicit def mapSW[K, V](implicit wk: Write[K, String], wv: Write[V, String]) = 67 | Write[Map[K, V], String]{ s => s.map{ case (k,v) => 68 | s"${wk.writes(k)} ${wv.writes(v)}" }.mkString("{", ", ", "}") 69 | } 70 | 71 | val mapSWOpt = Write[Map[EDN, EDN], String]{ s => 72 | s.map{ case (k,v) => 73 | v match { 74 | case o: Option[EDN] => o match { 75 | case None => "" 76 | case Some(v) => s"${ednW.writes(k)} ${ednW.writes(v)}" 77 | } 78 | case v => s"${ednW.writes(k)} ${ednW.writes(v)}" 79 | } 80 | }.filterNot(_.isEmpty).mkString("{", ", ", "}") 81 | } 82 | 83 | def optW[A](implicit w: Write[A, String]) = Write[Option[A], String]{ 84 | case None => "" 85 | case Some(a) => w.writes(a) 86 | } 87 | 88 | implicit def ednW: Write[EDN, String] = Write[EDN, String]{ 89 | case s: String => stringW.writes(s) 90 | case b: Boolean => booleanW.writes(b) 91 | case l: Long => longW.writes(l) 92 | case s: Short => shortW.writes(s) 93 | case i: Int => intW.writes(i) 94 | case f: Float => floatW.writes(f) 95 | case d: Double => doubleW.writes(d) 96 | case b: BigInt => bigIntW.writes(b) 97 | case b: BigDecimal => bigDecimalW.writes(b) 98 | case c: Char => charW.writes(c) 99 | case s: EDNSymbol => symbolW.writes(s) 100 | case k: EDNKeyword => keywordW.writes(k) 101 | case t: EDNTagged[EDN @unchecked] => taggedW(ednW).writes(t) 102 | case EDNNil => "nil" 103 | case s: List[EDN] => listSW(ednW).writes(s) 104 | case s: Vector[EDN] => vectorSW(ednW).writes(s) 105 | case s: Set[EDN @unchecked] => setSW(ednW).writes(s) 106 | case s: Seq[EDN] => seqSW(ednW).writes(s) 107 | case s: Map[EDN @ unchecked, EDN @ unchecked] => mapSWOpt.writes(s) 108 | case s: Option[EDN] => optW[EDN].writes(s) 109 | 110 | case s => throw new RuntimeException(s"$s unsupported EDN type") 111 | } 112 | 113 | implicit def write2Path[I](path: Path): Write[I, Map[String, Any]] = 114 | Write { i => 115 | path match { 116 | case Path(KeyPathNode(x) :: _) \: _ => 117 | val ps = path.path.reverse 118 | val KeyPathNode(k) = ps.head 119 | val o = Map[String, EDN](k -> i) 120 | ps.tail.foldLeft(o){ 121 | case (os, KeyPathNode(k)) => Map[String, EDN](k -> os) 122 | case _ => throw new RuntimeException(s"path $path is not a path to a Map") 123 | } 124 | case _ => throw new RuntimeException(s"path $path is not a path to a Map") // XXX: should be a compile time error 125 | } 126 | } 127 | 128 | 129 | implicit def customTagged[I, J](implicit wi:Write[I, EDNTagged[J]], wj:Write[J, String]): Write[I, String] = 130 | Write[I, String]{ i => 131 | taggedW(wj).writes(wi.writes(i)) 132 | } 133 | 134 | 135 | } 136 | 137 | trait LowWrites extends SuperLowWrites { 138 | 139 | import shapeless._ 140 | import shapeless.labelled.FieldType 141 | import syntax.singleton._ 142 | import tag.@@ 143 | import shapeless.labelled.FieldType 144 | import shapeless.ops.hlist.IsHCons 145 | import syntax.singleton._ 146 | import shapeless.ops.record.Selector 147 | import record._ 148 | 149 | // import shapelessext._ 150 | 151 | 152 | implicit def writeHNil: Write[HNil, String] = Write { _ => "()" } 153 | 154 | implicit def writeHList[H, HT <: HList]( 155 | implicit 156 | wh: Write[H, String], 157 | wt: SeqWrite[HT, String] 158 | ): Write[H :: HT, String] = 159 | Write { case h :: t => 160 | (wh.writes(h) +: wt.writes(t)).filterNot(_.isEmpty).mkString("(", " ", ")") 161 | } 162 | 163 | implicit def genWriteTuple[P, HL <: HList, HH , HT <: HList]( 164 | implicit 165 | tuple: IsTuple[P], 166 | gen: Generic.Aux[P, HL], 167 | c: IsHCons.Aux[HL, HH, HT], 168 | wh: Write[HH, String], 169 | wt: SeqWrite[HT, String] 170 | ): Write[P, String] = 171 | Write{ p => 172 | val t = gen.to(p) 173 | (wh.writes(t.head) +: wt.writes(t.tail)).filterNot(_.isEmpty).mkString("[", " ", "]") 174 | } 175 | 176 | implicit def subGenWrite[H, HT <: HList, K, V]( 177 | implicit 178 | un: Unpack2[H, FieldType, K, V], 179 | wh: Write[FieldType[K, V], String], 180 | wt: SeqWrite[HT, String] 181 | ): SeqWrite[H :: HT, String] = 182 | SeqWrite{ case h :: t => 183 | wh.writes(h.asInstanceOf[FieldType[K, V]]) +: wt.writes(t) 184 | } 185 | 186 | implicit def fieldTypeWO[K <: Symbol, V](implicit witness: Witness.Aux[K], wv: Write[V, String]) = 187 | Write[FieldType[K, Option[V]], String] { f => 188 | f map { v => "\"" + witness.value.name + "\"" + " " + wv.writes(v) } getOrElse "" 189 | } 190 | 191 | } 192 | 193 | trait SuperLowWrites extends play.api.data.mapping.DefaultWrites { 194 | import shapeless._ 195 | import shapeless.labelled.FieldType 196 | import shapeless.ops.hlist.IsHCons 197 | import syntax.singleton._ 198 | import shapeless.ops.record.Selector 199 | import record._ 200 | 201 | implicit def genWriteCaseClass[P, K, V, F, HL <: HList, HT <: HList]( 202 | implicit 203 | cc: HasProductGeneric[P], 204 | not: P <:!< EDNValue, 205 | gen: LabelledGeneric.Aux[P, HL], 206 | c: IsHCons.Aux[HL, F, HT], 207 | un: Unpack2[F, FieldType, K, V], 208 | wh: Write[FieldType[K, V], String], 209 | wt: SeqWrite[HT, String], 210 | witness: Witness.Aux[K], 211 | selector : Selector.Aux[HL, K, V] 212 | ): Write[P, String] = 213 | Write{ p => 214 | val t = gen.to(p) 215 | //(wh.writes(t.fieldAt(witness)(selector)) +: wt.writes(t.tail)).filterNot(_.isEmpty).mkString("{", ", ", "}") 216 | (wh.writes(labelled.field[witness.T](selector(t))) +: wt.writes(t.tail)).filterNot(_.isEmpty).mkString("{", ", ", "}") 217 | } 218 | 219 | implicit def fieldTypeW[K <: Symbol, V](implicit witness: Witness.Aux[K], wv: Write[V, String]) = 220 | Write[FieldType[K, V], String] { f => 221 | "\"" + witness.value.name + "\"" + " " + wv.writes(f) 222 | } 223 | 224 | trait SeqWrite[I, O] { 225 | def writes(i: I): Seq[O] 226 | } 227 | object SeqWrite{ 228 | def apply[I, O](w: I => Seq[O]): SeqWrite[I, O] = new SeqWrite[I, O] { 229 | def writes(i: I) = w(i) 230 | } 231 | } 232 | 233 | // implicit def seqWrite[Tmplicit w: Write[T, String]): SeqWrite[T, String] = SeqWrite[T, String]{ s => Seq(w.writes(s)) } 234 | // implicit def scalaSymbolW[K <: Symbol] = SeqWrite[K, String]{ s => Seq("\"" + s.name + "\"") } 235 | // implicit def scalaSymbolTaggedW[T] = SeqWrite[Symbol @@ T, String]{ s => scalaSymbolW.writes(s) } 236 | 237 | implicit def subHNilW: SeqWrite[HNil, String] = SeqWrite { _ => Seq() } 238 | 239 | implicit def subHListW[H, HT <: HList]( 240 | implicit 241 | wh: Write[H, String], 242 | wt: SeqWrite[HT, String] 243 | ): SeqWrite[H :: HT, String] = 244 | SeqWrite{ case h :: t => 245 | wh.writes(h) +: wt.writes(t) 246 | } 247 | 248 | } 249 | 250 | 251 | 252 | -------------------------------------------------------------------------------- /parser/src/main/scala/parser.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2014 Pascal Voitot 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package scaledn 17 | package parser 18 | 19 | import org.parboiled2._ 20 | import scala.annotation.switch 21 | import org.joda.time.{DateTime, DateTimeZone} 22 | 23 | 24 | /** The parboiled2 EDN Parser 25 | * 26 | * {{{ 27 | * val parser = EDNParser("""{1 "foo", "bar" 1.234M, :foo/bar [1,2,3]} #_foo/bar :bar/foo""") 28 | * parser.Root.run() match { 29 | * case Success(t) => \/-(t) 30 | * case Failure(f : org.parboiled2.ParseError) => -\/(parser.formatError(f)) 31 | * } 32 | * }}} 33 | * 34 | * The parsed types are the following: 35 | * 36 | * - Long (64bits) 12345 37 | * - Double (64 bits) 123.45 38 | * - BigInt 12345N 39 | * - BigDecimal 123.45M 40 | * - String "foobar" 41 | * - EDN Symbol foo/bar 42 | * - EDN Keyword :foo/bar 43 | * - EDN Nil nil 44 | * - heterogenous list (1 true "toto") 45 | * - heterogenous vector [1 true "toto"] 46 | * - heterogenous set #{1 true "toto"} 47 | * - heterogenous map {1 "toto", 1.234 "toto"} 48 | * 49 | * There are special syntaxes: 50 | * 51 | * - comments are lines starting with `;` 52 | * - values starting with `#_` are parsed but discarded 53 | * 54 | * EDN is an extensible format using tags starting with `#` such as: 55 | * 56 | * {{{ 57 | * #foo/bar value 58 | * }}} 59 | * 60 | * The parser can provide tag handlers that can be applied when a tag is parsed. 61 | * EDN specifies 2 tag handlers: 62 | * 63 | * - #inst "1985-04-12T23:20:50.52Z" for RFC-3339 instants 64 | * - #uuid "f81d4fae-7dec-11d0-a765-00a0c91e6bf6" for UUID 65 | * 66 | * 67 | * The parser can also be extended with your own specific handlers: 68 | * 69 | * 70 | * {{{ 71 | * val parser = new EDNParser("""#foo bar""") { 72 | * // defines your own handler as a parboiled2 rule 73 | * val fooTag = rule("foo" ~ WS ~ "bar" ~ push("toto")) 74 | * 75 | * // override tags keeping default tags if you need them 76 | * override def tags = rule(fooTag | super.tags) 77 | * } 78 | * 79 | * parser.Root.run().success.value should be ( 80 | * Vector("toto") 81 | * ) 82 | * }}} 83 | * 84 | */ 85 | object EDNParser { 86 | def apply(input: ParserInput) = new EDNParser(input) 87 | } 88 | 89 | class EDNParser(val input: ParserInput) extends Parser with StringBuilding { 90 | import CharPredicate.{Digit, Digit19, HexDigit, Alpha, AlphaNum} 91 | 92 | /** automatically consumes whitespaces */ 93 | implicit def wspStr(s: String): Rule0 = rule( str(s) ~ WS ) 94 | 95 | /** the main rule to be called when parsing all EDN */ 96 | def Root: Rule1[Seq[Any]] = rule( oneOrMore(Elem) ~ EOI ) 97 | 98 | /** as shown in parboiled2 samples, here is a (premature) optimization 99 | * that checks first character before dispatching to the right rule 100 | */ 101 | def Elem: Rule1[Any] = rule ( 102 | SkipWS ~ run ( 103 | (cursorChar: @switch) match { 104 | case '"' => String 105 | case '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' | '-' | '+' => Double | Long | SimpleSymbol 106 | case ':' => Keyword 107 | case '(' => List 108 | case '{' => Map 109 | case '[' => Vector 110 | case '#' => Set | Tagged 111 | case 't' => True | Symbol 112 | case 'f' => False | Symbol 113 | case 'n' => Nil | Symbol 114 | case '\\' => Char 115 | case _ => Symbol | SimpleSymbol 116 | } 117 | ) ~ SkipWS 118 | ) 119 | 120 | /** 121 | * NIL 122 | */ 123 | def Nil = rule { "nil" ~ push(EDNNil)} 124 | 125 | /** 126 | * BOOLEAN 127 | */ 128 | def True = rule { "true" ~ push(true) } 129 | def False = rule { "false" ~ push(false) } 130 | 131 | def Boolean = rule { True | False } 132 | 133 | /** 134 | * LONG 12345 / 12345M 135 | */ 136 | def Long = rule( 137 | capture(SignedNumber) ~ LongExact 138 | ) 139 | def LongExact = rule( 140 | ch('N') ~> ( (s:String) => BigInt(s) ) 141 | | run( (s:String) => java.lang.Long.parseLong(s) ) 142 | ) 143 | def SignedNumber = rule( optional(anyOf("+-")) ~ Number ) 144 | 145 | def Number = rule( 146 | Digit19 ~ Digits | Digit 147 | ) 148 | def Digits = rule( oneOrMore(Digit) ) 149 | 150 | /** 151 | * DOUBLE 123.45e+9 / 1.23456789N 152 | */ 153 | def Double = rule( 154 | capture(SignedNumber ~ FracExp) ~ DoubleExact 155 | ) 156 | def DoubleExact = rule( 157 | ch('M') ~> ( (s:String) => scala.BigDecimal(s) ) 158 | | run( (s:String) => java.lang.Double.parseDouble(s) ) 159 | ) 160 | def FracExp = rule( 161 | Frac ~ Exp | 162 | Frac | 163 | Exp 164 | ) 165 | def Frac = rule( ch('.') ~ Digits ) 166 | def Exp = rule( Ex ~ Digits ) 167 | def Ex = rule( ignoreCase('e') ~ optional(anyOf("+-")) ) 168 | 169 | /** 170 | * STRING "foobar" 171 | */ 172 | def String = rule ( '"' ~ clearSB() ~ Characters ~ '"' ~ push(sb.toString) ) 173 | 174 | def Characters = rule ( zeroOrMore(NormalChar | '\\' ~ EscapedChar) ) 175 | def NormalChar = rule ( !QuoteBackSlash ~ ANY ~ appendSB() ) 176 | 177 | def EscapedChar = rule ( 178 | 'n' ~ appendSB('\n') 179 | | 'r' ~ appendSB('\r') 180 | | 't' ~ appendSB('\t') 181 | | '"' ~ appendSB('\"') 182 | | '\'' ~ appendSB('\'') 183 | | '\\' ~ appendSB('\\') 184 | | 'b' ~ appendSB('\b') 185 | | 'f' ~ appendSB('\f') 186 | | Unicode ~> { code => sb.append(code.asInstanceOf[Char]); () } 187 | ) 188 | 189 | /** 190 | * CHARACTER \c \newline ... 191 | */ 192 | def Char = rule { '\\' ~ Chars } 193 | 194 | def Chars = rule ( 195 | "newline" ~ push('\n') 196 | | "return" ~ push('\r') 197 | | "space" ~ push(' ') 198 | | "tab" ~ push('\t') 199 | | "\\" ~ push('\\') 200 | | Unicode ~> { code => push(code.asInstanceOf[Char]) } 201 | | AlphaNum ~ push(lastChar) 202 | ) 203 | 204 | /** 205 | * KEYWORD 206 | */ 207 | def Keyword = rule( ':' ~ !CharPredicate(":/") ~ Symbol ~> (EDNKeyword(_)) ) 208 | 209 | /** 210 | * SYMBOL 211 | */ 212 | def Symbol = rule ( 213 | !CharPredicate(":#;") ~ 214 | clearSB() ~ 215 | optional(SymbolNameSpace ~ push(sb.toString)) ~ clearSB() ~ 216 | SymbolString ~ push(sb.toString) ~> { (ns, value) => 217 | EDNSymbol(ns.map(_+"/").getOrElse("") + value, ns) 218 | } 219 | ) 220 | 221 | def SimpleSymbol = rule ( 222 | capture(CharPredicate("*+!-_?$%&=<>/")) ~> (EDNSymbol(_)) 223 | ) 224 | 225 | def SymbolNameSpace = rule(SymbolString ~ "/") 226 | def SymbolString = rule( 227 | SymbolSpecial 228 | | SymFirstChars ~ appendSB() ~ zeroOrMore(SymChars ~ appendSB()) 229 | ) 230 | def SymbolSpecial = rule( 231 | SymbolSpecialStart ~ zeroOrMore(SymChars ~ appendSB()) 232 | ) 233 | def SymbolSpecialStart = rule( 234 | capture(SymSpecialFirstChars ~ !Digit) ~> ((s:String) => appendSB(s)) 235 | ) 236 | def SymFirstChars = rule(Alpha | SymNonAlphaChars) 237 | def SymChars = rule(AlphaNum | SymNonAlphaChars | SymSpecialFirstChars) 238 | // non alpha chars without +-. 239 | val SymNonAlphaChars = CharPredicate("*!_?$%&=<>:#") 240 | val SymSpecialFirstChars = CharPredicate("+-.") 241 | 242 | def Unicode = rule ( 'u' ~ capture(HexDigit ~ HexDigit ~ HexDigit ~ HexDigit) ~> 243 | (java.lang.Integer.parseInt(_, 16)) 244 | ) 245 | 246 | /** 247 | * TAGGED 248 | */ 249 | def Tagged = rule( 250 | ch('#') ~ tags 251 | ) 252 | 253 | // the rule to override if you want more handlers 254 | def tags = rule( defaultTags | unknownTags ) 255 | 256 | // default tag handlers 257 | def defaultTags = rule(uuid | instant) 258 | 259 | // This rule can consume any tag even if there is no handler for it 260 | def unknownTags = rule(Symbol ~ WS ~ Elem ~> ( EDNTagged(_, _) )) 261 | 262 | def uuid = rule("uuid" ~ WS ~ String ~> (java.util.UUID.fromString(_))) 263 | def instant = rule("inst" ~ WS ~ String ~> (new DateTime(_, DateTimeZone.UTC))) 264 | 265 | /** 266 | * DISCARD 267 | */ 268 | def Discard = rule( 269 | str("#_") ~ WS ~ Elem ~> (_ => ()) 270 | ) 271 | 272 | /** 273 | * LIST 274 | */ 275 | def List = rule( 276 | ch('(') ~ zeroOrMore(Elem) ~ ch(')') ~> ( scala.collection.immutable.List(_:_*) ) 277 | ) 278 | 279 | /** 280 | * VECTOR 281 | */ 282 | def Vector = rule( 283 | ch('[') ~ zeroOrMore(Elem) ~ ch(']') ~> ( scala.collection.immutable.Vector(_:_*) ) 284 | ) 285 | 286 | /** 287 | * SET 288 | */ 289 | def Set = rule( 290 | str("#{") ~ zeroOrMore(Elem) ~ ch('}') ~ run { 291 | elements: Seq[Any] => test(elements.distinct.size == elements.size) ~ push(scala.collection.immutable.Set(elements:_*)) 292 | } 293 | ) 294 | 295 | 296 | /** 297 | * MAP 298 | */ 299 | def Map = rule( 300 | ch('{') ~ zeroOrMore(Pair) ~ ch('}') ~> ( scala.collection.immutable.Map(_:_*) ) 301 | ) 302 | def Pair = rule( 303 | Elem ~ Elem ~> ((_, _)) 304 | ) 305 | 306 | /** 307 | * COMMENT 308 | */ 309 | def Comment = rule( 310 | ch(';') ~ zeroOrMore(!Newline ~ ANY) ~ (Newline | EOI) 311 | ) 312 | 313 | 314 | /** 315 | * All Whitespaces 316 | */ 317 | def SkipWS = rule( zeroOrMore(WS_D_C_NL) ) 318 | 319 | def WS_D_C_NL = rule( WS_NL_CommaChar | Discard | Comment ) 320 | 321 | def WS = rule( zeroOrMore(WSChar) ) 322 | def Newline = rule { optional('\r') ~ '\n' } 323 | 324 | def ws(c: Char) = rule { c ~ WSChar } 325 | 326 | val QuoteBackSlash = CharPredicate("\"\\") 327 | 328 | val WSChar = CharPredicate(" \t") 329 | 330 | val WS_NL_CommaChar = CharPredicate(" \n\r\t,") 331 | 332 | } 333 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | 203 | -------------------------------------------------------------------------------- /parser/src/test/scala/ParserSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2014 Pascal Voitot 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package scaledn 17 | 18 | package parser 19 | 20 | import org.scalatest._ 21 | import scala.util.{Try, Success, Failure} 22 | 23 | 24 | class ParserSpec extends FlatSpec with Matchers with TryValues { 25 | 26 | "EDNParser" should "parse Nil" in { 27 | EDNParser("""nil""").Nil.run().success.value should be (EDNNil) 28 | EDNParser("""nil 29 | \n""").Nil.run().success.value should be (EDNNil) 30 | EDNParser("""Nil""").Nil.run() should be ('failure) 31 | } 32 | 33 | it should "parse Boolean" in { 34 | EDNParser("""true""").Boolean.run().success.value should be (true) 35 | EDNParser("""True""").Boolean.run() should be ('failure) 36 | EDNParser("""true 37 | \n""").Boolean.run().success.value should be (true) 38 | EDNParser("""false 39 | \t""").Boolean.run().success.value should be (false) 40 | } 41 | 42 | it should "parse String" in { 43 | EDNParser("\"coucou\"").String.run().success.value should be ("coucou") 44 | EDNParser("\"coucou\n\b\t\f\b\\\"foo\\\" 'bar' \"").String.run().success.value should be ("coucou\n\b\t\f\b\"foo\" 'bar' ") 45 | } 46 | 47 | it should "parse Character" in { 48 | EDNParser("\\newline").Char.run().success.value should be ('\n') 49 | EDNParser("\\return").Char.run().success.value should be ('\r') 50 | EDNParser("\\space").Char.run().success.value should be (' ') 51 | EDNParser("\\tab").Char.run().success.value should be ('\t') 52 | EDNParser("\\\\").Char.run().success.value should be ('\\') 53 | EDNParser("\\u00D5").Char.run().success.value should be ('\u00D5') 54 | EDNParser("\\u00D5 ").Char.run().success.value should be ('\u00D5') 55 | EDNParser("\\t ").Char.run().success.value should be ('t') 56 | EDNParser("\\r").Char.run().success.value should be ('r') 57 | EDNParser("\\9 ").Char.run().success.value should be ('9') 58 | } 59 | 60 | it should "parse Symbol" in { 61 | EDNParser("""toto""").Symbol.run().success.value should be (EDNSymbol("toto")) 62 | EDNParser("""foo/bar""").Symbol.run().success.value should be (EDNSymbol("foo/bar", Some("foo"))) 63 | EDNParser("""1foo""").Symbol.run() should be ('failure) 64 | EDNParser("""-1foo""").Symbol.run() should be ('failure) 65 | EDNParser("""+1foo""").Symbol.run() should be ('failure) 66 | EDNParser(""".1foo""").Symbol.run() should be ('failure) 67 | EDNParser("""foo&>-<.bar:#$%""").Symbol.run().success.value should be (EDNSymbol("foo&>-<.bar:#$%")) 68 | EDNParser("""foo&>""").SimpleSymbol.run().success.value should be (EDNSymbol(">")) 118 | } 119 | 120 | it should "parse simple symbol list" in { 121 | EDNParser("""(/ 1 2)""").List.run().success.value should be (List(EDNSymbol("/"), 1L, 2L)) 122 | EDNParser("""(+ 1 2)""").List.run().success.value should be (List(EDNSymbol("+"), 1L, 2L)) 123 | } 124 | 125 | 126 | it should "parse Vector" in { 127 | EDNParser("""[1, "foo", :foo/bar]""").Vector.run().success.value should be (Vector(1L, "foo", EDNKeyword(EDNSymbol("foo/bar", Some("foo"))))) 128 | EDNParser("""[1 "foo" :foo/bar]""").Vector.run().success.value should be (Vector(1L, "foo", EDNKeyword(EDNSymbol("foo/bar", Some("foo"))))) 129 | 130 | EDNParser("""[db.part/db]""").Vector.run().success.value should be (Vector(EDNSymbol("db.part/db", Some("db.part")))) 131 | } 132 | 133 | it should "parse Set" in { 134 | EDNParser("""#{1, "foo", :foo/bar}""").Set.run().success.value should be (Set(1L, "foo", EDNKeyword(EDNSymbol("foo/bar", Some("foo"))))) 135 | EDNParser("""#{1 "foo" :foo/bar}""").Set.run().success.value should be (Set(1L, "foo", EDNKeyword(EDNSymbol("foo/bar", Some("foo"))))) 136 | EDNParser("""#{1 1 "foo" :foo/bar}""").Set.run() should be ('failure) 137 | } 138 | 139 | it should "parse Map" in { 140 | EDNParser("""{1 "foo", "bar" 1.234M, :foo/bar [1,2,3]}""").Map.run().success.value should be ( 141 | Map( 142 | 1L -> "foo", 143 | "bar" -> BigDecimal("1.234"), 144 | EDNKeyword(EDNSymbol("foo/bar", Some("foo"))) -> Vector(1, 2, 3) 145 | ) 146 | ) 147 | 148 | EDNParser("""{1 "foo" "bar" 1.234M :foo/bar [1,2,3]}""").Map.run().success.value should be ( 149 | Map( 150 | 1L -> "foo", 151 | "bar" -> BigDecimal("1.234"), 152 | EDNKeyword(EDNSymbol("foo/bar", Some("foo"))) -> Vector(1, 2, 3) 153 | ) 154 | ) 155 | } 156 | 157 | it should "parse Tags" in { 158 | EDNParser("#uuid \"f81d4fae-7dec-11d0-a765-00a0c91e6bf6\"").Tagged.run().success.value should be ( 159 | java.util.UUID.fromString("f81d4fae-7dec-11d0-a765-00a0c91e6bf6") 160 | ) 161 | 162 | EDNParser("#inst \"1985-04-12T23:20:50.52Z\"").Tagged.run().success.value should be ( 163 | new org.joda.time.DateTime("1985-04-12T23:20:50.52Z", org.joda.time.DateTimeZone.UTC) 164 | ) 165 | 166 | EDNParser("#custom :blabla/bar").Tagged.run().success.value should be ( 167 | EDNTagged(EDNSymbol("custom"), EDNKeyword(EDNSymbol("blabla/bar", Some("blabla")))) 168 | ) 169 | } 170 | 171 | it should "parse Multiple tags" in { 172 | EDNParser("#custom0 #custom1 1").Tagged.run().success.value should be ( 173 | EDNTagged(EDNSymbol("custom0"), EDNTagged(EDNSymbol("custom1"), 1L)) 174 | ) 175 | } 176 | 177 | 178 | it should "parse Discard" in { 179 | EDNParser("""#_foo/bar""").Discard.run() should be ('success) 180 | EDNParser("""#_ :foo/bar""").Discard.run() should be ('success) 181 | 182 | //discard doesn't need a whitespace 183 | EDNParser("""[#_1 2]""").Vector.run().success.value should be (Vector(2L)) 184 | } 185 | 186 | it should "parse mutiple Discards" in { 187 | EDNParser("""[#_ #_ 1 2 3]""").Vector.run().success.value should be (Vector(3L)) 188 | EDNParser("""[#_#_1 2 3]""").Vector.run().success.value should be (Vector(3L)) 189 | } 190 | 191 | it should "parse Comment" in { 192 | EDNParser("""; _foo/bar""").Comment.run() should be ('success) 193 | EDNParser(""";_sqdjlkj foo/bar""").Comment.run() should be ('success) 194 | EDNParser(""";_sqdjlkj foo/bar sdfsdfs 195 | """).Comment.run() should be ('success) 196 | } 197 | 198 | it should "skip comments" in { 199 | val parser = EDNParser(""" 200 | ; 1 balbal dsdkfjsdlfj sdfkjds lfdsjlkf 201 | {1 "foo" "bar" 1.234M :foo/bar [1,2,3]} ; 2 lfkjlkfjdskfjd 202 | ;3 SDFLKJ aejlkj-'"(-' 203 | """).Root.run().success.value should be ( 204 | Vector( 205 | Map( 206 | 1L -> "foo", 207 | "bar" -> BigDecimal("1.234"), 208 | EDNKeyword(EDNSymbol("foo/bar", Some("foo"))) -> Vector(1, 2, 3) 209 | ) 210 | ) 211 | ) 212 | 213 | } 214 | 215 | it should "parse full" in { 216 | EDNParser("""{1 "foo", "bar" 1.234M, :foo/bar [1,2,3] :bar/foo""").Root.run() should be ('failure) 217 | EDNParser("""{1 "foo", "bar" 1.234M, :foo/bar [1,2,3]} :bar/foo""").Root.run().success.value should be ( 218 | Vector( 219 | Map( 220 | 1L -> "foo", 221 | "bar" -> BigDecimal("1.234"), 222 | EDNKeyword(EDNSymbol("foo/bar", Some("foo"))) -> Vector(1, 2, 3) 223 | ), 224 | 225 | EDNKeyword(EDNSymbol("bar/foo", Some("bar"))) 226 | ) 227 | ) 228 | } 229 | 230 | it should "parse full with discard" in { 231 | EDNParser("""{1 "foo", "bar" 1.234M, :foo/bar [1,2,3]} #_foo/bar :bar/foo""").Root.run().success.value should be ( 232 | Vector( 233 | Map( 234 | 1L -> "foo", 235 | "bar" -> BigDecimal("1.234"), 236 | EDNKeyword(EDNSymbol("foo/bar", Some("foo"))) -> Vector(1, 2, 3) 237 | ), 238 | 239 | EDNKeyword(EDNSymbol("bar/foo", Some("bar"))) 240 | ) 241 | ) 242 | 243 | // match { 244 | // case Success(t) => println("SUCCESS:"+t) 245 | // case Failure(f : org.parboiled2.ParseError) => println("PARSE:"+parser.formatError(f)) 246 | // } 247 | } 248 | 249 | it should "be extendable" in { 250 | val parser = new EDNParser("""#foo bar""") { 251 | def fooTag = rule("foo" ~ WS ~ "bar" ~ push("toto")) 252 | 253 | override def tags = rule(fooTag | super.tags) 254 | } 255 | 256 | parser.Root.run().success.value should be ( 257 | Vector("toto") 258 | ) 259 | } 260 | 261 | it should "parse bigger" in { 262 | val str = """ 263 | [{:db/id #db/id [db.part/db] 264 | :db/ident :object/name 265 | :db/doc "Name of a Solar System object." 266 | :db/valueType :db.type/string 267 | :db/index true 268 | :db/cardinality :db.cardinality/one 269 | :db.install/_attribute :db.part/db} 270 | {:db/id #db/id [db.part/db] 271 | :db/ident :object/meanRadius 272 | :db/doc "Mean radius of an object." 273 | :db/index true 274 | :db/valueType :db.type/double 275 | :db/cardinality :db.cardinality/one 276 | :db.install/_attribute :db.part/db} 277 | {:db/id #db/id [db.part/db] 278 | :db/ident :data/source 279 | :db/doc "Source of the data in a transaction." 280 | :db/valueType :db.type/string 281 | :db/index true 282 | :db/cardinality :db.cardinality/one 283 | :db.install/_attribute :db.part/db}] 284 | [{:db/id #db/id [db.part/tx] 285 | :db/doc "Solar system objects bigger than Pluto."} 286 | {:db/id #db/id [db.part/tx] 287 | :data/source "http://en.wikipedia.org/wiki/List_of_Solar_System_objects_by_size"} 288 | {:db/id #db/id [db.part/user] 289 | :object/name "Sun" 290 | :object/meanRadius 696000.0} 291 | {:db/id #db/id [db.part/user] 292 | :object/name "Jupiter" 293 | :object/meanRadius 69911.0} 294 | {:db/id #db/id [db.part/user] 295 | :object/name "Saturn" 296 | :object/meanRadius 58232.0} 297 | {:db/id #db/id [db.part/user] 298 | :object/name "Uranus" 299 | :object/meanRadius 25362.0} 300 | {:db/id #db/id [db.part/user] 301 | :object/name "Neptune" 302 | :object/meanRadius 24622.0} 303 | {:db/id #db/id [db.part/user] 304 | :object/name "Earth" 305 | :object/meanRadius 6371.0} 306 | {:db/id #db/id [db.part/user] 307 | :object/name "Venus" 308 | :object/meanRadius 6051.8} 309 | {:db/id #db/id [db.part/user] 310 | :object/name "Mars" 311 | :object/meanRadius 3390.0} 312 | {:db/id #db/id [db.part/user] 313 | :object/name "Ganymede" 314 | :object/meanRadius 2631.2} 315 | {:db/id #db/id [db.part/user] 316 | :object/name "Titan" 317 | :object/meanRadius 2576.0} 318 | {:db/id #db/id [db.part/user] 319 | :object/name "Mercury" 320 | :object/meanRadius 2439.7} 321 | {:db/id #db/id [db.part/user] 322 | :object/name "Callisto" 323 | :object/meanRadius 2410.3} 324 | {:db/id #db/id [db.part/user] 325 | :object/name "Io" 326 | :object/meanRadius 1821.5} 327 | {:db/id #db/id [db.part/user] 328 | :object/name "Moon" 329 | :object/meanRadius 1737.1} 330 | {:db/id #db/id [db.part/user] 331 | :object/name "Europa" 332 | :object/meanRadius 1561.0} 333 | {:db/id #db/id [db.part/user] 334 | :object/name "Triton" 335 | :object/meanRadius 1353.4} 336 | {:db/id #db/id [db.part/user] 337 | :object/name "Eris" 338 | :object/meanRadius 1163.0}] 339 | """ 340 | 341 | EDNParser(str).Root.run() should be ('success) 342 | // val parser = EDNParser(str) 343 | // parser.Root.run() match { 344 | // case Success(t) => println("SUCCESS:"+t) 345 | // case Failure(f : org.parboiled2.ParseError) => println("PARSE:"+parser.formatError(f)) 346 | // } 347 | } 348 | 349 | } -------------------------------------------------------------------------------- /macros/src/main/scala/macros.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2014 Pascal Voitot 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package scaledn 17 | package macros 18 | 19 | import scala.language.experimental.macros 20 | import scala.reflect.macros.whitebox.Context 21 | 22 | import parser._ 23 | import scala.util.{Try, Success => TrySuccess, Failure => TryFailure} 24 | import shapeless.{HList, HNil} 25 | 26 | 27 | trait EDNMacros { 28 | 29 | /** 30 | * Macro parsing '''Single''' EDN value mapping collections to scala collection 31 | * 32 | * So, heterogenous collection will use `Any` 33 | * 34 | * {{{ 35 | * // the type is just for info as it is inferred by scalac macro 36 | * val list: List[Long] = EDN("""(1 2 3)""") 37 | * list should equal (List(1L, 2L, 3L)) 38 | * 39 | * val map = EDN("""{ 1 "toto", 2 "tata", 3 "tutu" }""") 40 | * map should equal (Map(1L -> "toto", 2L -> "tata", 3L -> "tutu")) 41 | * }}} 42 | * 43 | * You can also use String interpolation mixed with this macro 44 | * {{{ 45 | * // the types are just for info as it is inferred by scalac macro 46 | * val l = 123L 47 | * val s = List("foo", "bar") 48 | * 49 | * val r: Long = EDN(s"$$l") 50 | * 51 | * val r1: Seq[Any] = EDN(s"($$l $$s)") 52 | * }}} 53 | */ 54 | def EDN(edn: String): Any = macro MacroImpl.ednImpl 55 | 56 | /** 57 | * Macro parsing '''Multiple''' EDN value mapping collections to scala collection 58 | * 59 | * So, heterogenous collection will use `Any` 60 | * 61 | * {{{ 62 | * // the types are just for info as it is inferred by scalac macro 63 | * val s: Seq[Any] = EDNs("""(1 2 3) "toto" [true false] :foo/bar""") 64 | * s should equal (Seq( 65 | * Seq(1L, 2L, 3L), 66 | * "toto", 67 | * Vector(true, false), 68 | * EDNKeyword(EDNSymbol("foo/bar", Some("foo"))) 69 | * )) 70 | * }}} 71 | * 72 | * You can also use String interpolation mixed with this macro 73 | * 74 | */ 75 | def EDNs(edn: String): Any = macro MacroImpl.ednsImpl 76 | 77 | /** 78 | * Macro parsing '''Single''' EDN Value mapping collections to heterogenous shapeless HList 79 | * 80 | * The conversion of collections to HList is applied at first level only 81 | * To recursively convert to HList, use recursive macros 82 | * 83 | * {{{ 84 | * // the types are just for info as it is inferred by scalac macro 85 | * val s: Long :: String :: Boolean :: HNil = EDNH("""(1 "toto" true)""") 86 | * s should equal (1L :: "toto" :: true :: HNil) 87 | * 88 | * val s3 = EDNH("""{1 "toto" true 1.234 "foo" (1 2 3)}""") 89 | * s3 should equal ( 90 | * 1L ->> "toto" :: 91 | * true ->> 1.234 :: 92 | * "foo" ->> List(1L, 2L, 3L) :: 93 | * HNil 94 | * ) 95 | * }}} 96 | * 97 | * You can also use String interpolation mixed with this macro 98 | * {{{ 99 | * // the types are just for info as it is inferred by scalac macro 100 | * val l = 123L 101 | * val s = List("foo", "bar") 102 | * 103 | * val r2: Long :: List[String] :: HNil = EDNH(s"($$l $$s)") 104 | * }}} 105 | */ 106 | def EDNH(edn: String): Any = macro MacroImpl.ednhImpl 107 | 108 | /** 109 | * Macro parsing '''Multiple''' EDN Value mapping collections to heterogenous shapeless HList 110 | * 111 | * The conversion of collections to HList is applied at first level only 112 | * To recursively convert to HList, use recursive macros 113 | * 114 | * {{{ 115 | * // the type is just for info as it is inferred by scalac macro 116 | * val s: List[Long] :: String :: Vector[Boolean] :: EDNKeyword. 117 | * }}} 118 | */ 119 | def EDNHs(edn: String): Any = macro MacroImpl.ednhsImpl 120 | 121 | /** 122 | * Macro parsing '''Single''' EDN Value mapping '''recursively''' collections 123 | * to heterogenous shapeless HList 124 | * 125 | * {{{ 126 | * // the type is just for info as it is inferred by scalac macro 127 | * val s: List[Long] :: String :: Vector[Boolean] :: EDNKeyword :: HNil = EDNHs("""(1 2 3) "toto" [true false] :foo/bar""") 128 | * s should equal ( 129 | * Seq(1L, 2L, 3L) :: 130 | * "toto" :: 131 | * Vector(true, false) :: 132 | * EDNKeyword(EDNSymbol("foo/bar", Some("foo"))) :: 133 | * HNil 134 | * ) 135 | * }}} 136 | * 137 | * You can also use String interpolation mixed with this macro. 138 | * 139 | */ 140 | def EDNHR(edn: String): Any = macro MacroImpl.ednhrImpl 141 | 142 | /** 143 | * Macro parsing '''Multiple''' EDN Value mapping '''recursively''' collections 144 | * to heterogenous shapeless HList 145 | * 146 | * {{{ 147 | * // the type is just for info as it is inferred by scalac macro 148 | * val s2 = EDNHRs("""(1 2 3) "toto" [true false] :foo/bar""") 149 | * s2 should equal ( 150 | * (1L :: 2L :: 3L :: HNil) :: 151 | * "toto" :: 152 | * (true :: false :: HNil) :: 153 | * EDNKeyword(EDNSymbol("foo/bar", Some("foo"))) :: 154 | * HNil 155 | * ) 156 | * }}} 157 | * 158 | * You can also use String interpolation mixed with this macro. 159 | * 160 | */ 161 | def EDNHRs(edn: String): Any = macro MacroImpl.ednhrsImpl 162 | } 163 | 164 | object MacroImpl { 165 | import scala.collection.mutable 166 | 167 | private def abortWithMessage(c: Context, message: String) = 168 | c.abort(c.enclosingPosition, message) 169 | 170 | private def abortWithThrowable(c: Context, throwable: Throwable) = 171 | c.abort(c.enclosingPosition, throwable.getMessage) 172 | 173 | def genericMacro[T](c: Context)(edn: c.Expr[String]) 174 | (parse: EDNParser => Try[T]) 175 | (literal: (T, Helper[c.type], mutable.Stack[c.Tree]) => c.Tree): c.Expr[Any] = 176 | { 177 | import c.universe._ 178 | 179 | val helper = new Helper[c.type](c) 180 | 181 | edn.tree match { 182 | case Literal(Constant(s: String)) => 183 | val parser = EDNParser(s) 184 | parse(parser) match { 185 | case TrySuccess(s) => c.Expr(literal(s, helper, mutable.Stack.empty[c.Tree])) 186 | case TryFailure(f : org.parboiled2.ParseError) => abortWithMessage(c, parser.formatError(f)) 187 | case TryFailure(e) => abortWithMessage(c, "Unexpected failure: " + e.getMessage) 188 | } 189 | 190 | case s@q"scala.StringContext.apply(..$parts).s(..$args)" => 191 | val partsWithPlaceholders = q"""Seq(..$parts).mkString(" scaledn/! ")""" 192 | val strWithPlaceHolders = c.eval(c.Expr[String](c.untypecheck(partsWithPlaceholders.duplicate))) 193 | val parser = EDNParser(strWithPlaceHolders) 194 | val argsStack = mutable.Stack.concat(args) 195 | parse(parser) match { 196 | case TrySuccess(s) => c.Expr(literal(s, helper, argsStack)) 197 | case TryFailure(f : org.parboiled2.ParseError) => abortWithMessage(c, parser.formatError(f)) 198 | case TryFailure(e) => abortWithMessage(c, "Unexpected failure: " + e.getMessage) 199 | } 200 | 201 | case _ => 202 | abortWithMessage(c, "Expected a string literal") 203 | } 204 | } 205 | 206 | def ednImpl(c: Context)(edn: c.Expr[String]): c.Expr[Any] = 207 | genericMacro[EDN](c)(edn){ 208 | parser => parser.Root.run().map(_.head) 209 | }{ 210 | (t, helper, stack) => helper.literalEDN(t, stack) 211 | } 212 | 213 | 214 | def ednsImpl(c: Context)(edn: c.Expr[String]): c.Expr[Any] = 215 | genericMacro[Seq[EDN]](c)(edn){ 216 | parser => parser.Root.run() 217 | }{ 218 | (t, helper, stack) => helper.literalEDN(t, stack) 219 | } 220 | 221 | 222 | def ednhImpl(c: Context)(edn: c.Expr[String]): c.Expr[Any] = 223 | genericMacro[EDN](c)(edn){ 224 | parser => parser.Root.run().map(_.head) 225 | }{ 226 | (t, helper, stack) => helper.literalEDNH(t, stack) 227 | } 228 | 229 | 230 | def ednhsImpl(c: Context)(edn: c.Expr[String]): c.Expr[Any] = 231 | genericMacro[Seq[EDN]](c)(edn){ 232 | parser => parser.Root.run() 233 | }{ 234 | (t, helper, stack) => helper.literalEDNHS(t, stack) 235 | } 236 | 237 | 238 | def ednhrImpl(c: Context)(edn: c.Expr[String]): c.Expr[Any] = 239 | genericMacro[EDN](c)(edn){ 240 | parser => parser.Root.run().map(_.head) 241 | }{ 242 | (t, helper, stack) => helper.literalEDNHR(t, stack) 243 | } 244 | 245 | 246 | def ednhrsImpl(c: Context)(edn: c.Expr[String]): c.Expr[Any] = 247 | genericMacro[Seq[EDN]](c)(edn){ 248 | parser => parser.Root.run() 249 | }{ 250 | (t, helper, stack) => helper.literalEDNHRS(t, stack) 251 | } 252 | 253 | } 254 | 255 | class Helper[C <: Context](val c: C) { 256 | import c.universe._ 257 | import scala.collection.mutable 258 | 259 | private def abortWithMessage(message: String) = 260 | c.abort(c.enclosingPosition, message) 261 | 262 | implicit val bigDecimalLiftable = new Liftable[BigDecimal] { 263 | def apply(n: BigDecimal) = 264 | c.Expr[BigDecimal]( 265 | Apply( 266 | q"scala.math.BigDecimal.apply", 267 | List(Literal(Constant(n.toString))))).tree 268 | } 269 | 270 | implicit val bigIntLiftable = new Liftable[BigInt] { 271 | def apply(n: BigInt) = 272 | c.Expr[BigInt]( 273 | Apply( 274 | q"scala.math.BigInt.apply", 275 | List(Literal(Constant(n.toString))))).tree 276 | } 277 | 278 | def literalEDN(edn: Any, stk: mutable.Stack[c.Tree]): c.Tree = 279 | edn match { 280 | case s: String => q"$s" 281 | case b: Boolean => q"$b" 282 | case l: Long => q"$l" 283 | case d: Double => q"$d" 284 | case bi: BigInt => q"$bi" 285 | case bd: BigDecimal => q"$bd" 286 | case s: EDNSymbol => literalEDNSymbol(s, stk) //q"_root_.scaledn.EDNSymbol(${s.value}, ${s.namespace})" 287 | case kw: EDNKeyword => literalEDNKeyword(kw, stk) 288 | case EDNNil => q"_root_.scaledn.EDNNil" 289 | case list: List[EDN] => 290 | val args = list.map(literalEDN(_, stk)) 291 | q"_root_.scala.collection.immutable.List(..$args)" 292 | case vector: Vector[EDN] => 293 | val args = vector.map(literalEDN(_, stk)) 294 | q"_root_.scala.collection.immutable.Vector(..$args)" 295 | case set: Set[EDN @unchecked] => 296 | val args = set.map(literalEDN(_, stk)) 297 | q"_root_.scala.collection.immutable.Set(..$args)" 298 | case map: Map[EDN @unchecked, EDN @unchecked] => 299 | val args = map.map{ case(k, v) => literalEDN(k, stk) -> literalEDN(v, stk) } 300 | q"_root_.scala.collection.immutable.Map(..$args)" 301 | case seq: Seq[EDN] => 302 | val args = seq.map(literalEDN(_, stk)) 303 | q"_root_.scala.collection.immutable.Seq(..$args)" 304 | case t: EDNTagged[EDN @unchecked] => 305 | val tag = literalEDNSymbol(t.tag, stk) 306 | val value = literalEDN(t.value, stk) 307 | q"_root_.scaledn.EDNTagged($tag, $value)" 308 | case x => 309 | // TODO Add search implicit for this element 310 | if (x == null) 311 | abortWithMessage("null is not supported") 312 | else 313 | abortWithMessage(s"unexpected value $x with ${x.getClass}") 314 | } 315 | 316 | def literalEDNR(edn: Any, stk: mutable.Stack[c.Tree]): c.Tree = 317 | edn match { 318 | case s: String => q"$s" 319 | case b: Boolean => q"$b" 320 | case l: Long => q"$l" 321 | case d: Double => q"$d" 322 | case bi: BigInt => q"$bi" 323 | case bd: BigDecimal => q"$bd" 324 | case s: EDNSymbol => literalEDNSymbol(s, stk) //q"_root_.scaledn.EDNSymbol(${s.value}, ${s.namespace})" 325 | case kw: EDNKeyword => literalEDNKeyword(kw, stk) 326 | case EDNNil => q"_root_.scaledn.EDNNil" 327 | case list: List[EDN] => 328 | literalEDNHS(list, stk) 329 | case vector: Vector[EDN] => 330 | literalEDNHS(vector, stk) 331 | case set: Set[EDN @unchecked] => 332 | literalEDNHS(set.toSeq, stk) 333 | case map: Map[EDN @unchecked, EDN @unchecked] => 334 | literalRecords(map.toSeq, stk) 335 | case seq: Seq[EDN] => 336 | literalEDNHS(seq, stk) 337 | case t: EDNTagged[EDN @unchecked] => 338 | val tag = literalEDNSymbol(t.tag, stk) 339 | val value = literalEDNR(t.value, stk) 340 | println("VALUE:"+value) 341 | q"_root_.scaledn.EDNTagged($tag, $value)" 342 | case x => 343 | if (x == null) 344 | abortWithMessage("nil is not supported") 345 | else 346 | abortWithMessage(s"unexpected value $x with ${x.getClass}") 347 | } 348 | 349 | def literalEDNKeyword(kw: EDNKeyword, stk: mutable.Stack[c.Tree]): c.Tree = 350 | q"_root_.scaledn.EDNKeyword(${literalEDNSymbol(kw.value, stk)})" 351 | 352 | def literalEDNSymbol(s: EDNSymbol, stk: mutable.Stack[c.Tree]): c.Tree = { 353 | if (s.value == "scaledn/!") 354 | try { 355 | val t = stk.pop() 356 | q"""$t""" 357 | } catch { 358 | case ex: NoSuchElementException => 359 | abortWithMessage("The symbol 'scaledn/!' is reserved by Scaledn") 360 | } 361 | else 362 | q"_root_.scaledn.EDNSymbol(${s.value}, ${s.namespace})" 363 | 364 | } 365 | 366 | def literalEDNH(edn: EDN, stk: mutable.Stack[c.Tree]): c.Tree = { 367 | edn match { 368 | case list: List[EDN] => literalEDNHS(list, stk) 369 | case vector: Vector[EDN] => literalEDNHS(vector, stk) 370 | case set: Set[EDN @unchecked] => literalEDNHS(set.toSeq, stk) 371 | case map: Map[EDN @unchecked, EDN @unchecked] => literalRecords(map.toSeq, stk) 372 | case seq: Seq[EDN] => literalEDNHS(seq, stk) 373 | case x => literalEDN(x, stk) 374 | } 375 | } 376 | 377 | def literalEDNHR(edn: EDN, stk: mutable.Stack[c.Tree]): c.Tree = { 378 | edn match { 379 | case list: List[EDN] => literalEDNHRS(list, stk) 380 | case vector: Vector[EDN] => literalEDNHRS(vector, stk) 381 | case set: Set[EDN @unchecked] => literalEDNHRS(set.toSeq, stk) 382 | case map: Map[EDN @unchecked, EDN @unchecked] => literalRecordsR(map.toSeq, stk) 383 | case seq: Seq[EDN] => literalEDNHRS(seq, stk) 384 | case x => literalEDNR(x, stk) 385 | } 386 | } 387 | 388 | def literalEDNHS(edns: Seq[EDN], stk: mutable.Stack[c.Tree]): c.Tree = { 389 | edns match { 390 | case Seq() => literalHNil 391 | case head +: tail => literalHL(literalEDN(head, stk), literalEDNHS(tail, stk)) 392 | } 393 | } 394 | 395 | def literalEDNHRS(edns: Seq[EDN], stk: mutable.Stack[c.Tree]): c.Tree = { 396 | edns match { 397 | case Seq() => literalHNil 398 | case head +: tail => literalHL(literalEDNHR(head, stk), literalEDNHRS(tail, stk)) 399 | } 400 | } 401 | 402 | def literalRecords(edns: Seq[(EDN, EDN)], stk: mutable.Stack[c.Tree]): c.Tree = { 403 | edns match { 404 | case Seq() => literalHNil 405 | case (k, v) +: tail => literalHL(literalRecord(literalEDN(k, stk), literalEDN(v, stk)), literalRecords(tail, stk)) 406 | } 407 | } 408 | 409 | def literalRecordsR(edns: Seq[(EDN, EDN)], stk: mutable.Stack[c.Tree]): c.Tree = { 410 | edns match { 411 | case Seq() => literalHNil 412 | case (k, v) +: tail => literalHL(literalRecord(literalEDNHR(k, stk), literalEDNHR(v, stk)), literalRecordsR(tail, stk)) 413 | } 414 | } 415 | 416 | def literalHNil: c.Tree = q"_root_.shapeless.HNil" 417 | 418 | def literalHL(head: c.Tree, tail: c.Tree): c.Tree = q"_root_.shapeless.::($head, $tail)" 419 | 420 | def literalRecord(key: c.Tree, value: c.Tree): c.Tree = 421 | q"_root_.shapeless.syntax.singleton.mkSingletonOps($key).->>($value)" 422 | 423 | } 424 | 425 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | SCALEDN, [EDN](https://github.com/edn-format/edn) Scala API 2 | =============== 3 | 4 | A Scala [EDN](https://github.com/edn-format/edn) parser/serializer/validator based on : 5 | 6 | - [Parboiled2](https://github.com/sirthias/parboiled2), 7 | - [Shapeless](https://github.com/milessabin/shapeless), 8 | - [Generic Validation](https://github.com/jto/validation) 9 | - Scala Macros 10 | 11 | > It works only in Scala 2.11.x 12 | 13 | ## Using it in your project 14 | 15 | > For now, the deps are still snapshots as the API is being robustified according to comments of you, users. So **I deliver Jars on Bintray for now based on ugly Git hashes**. 16 | 17 | ### Add Bintray SBT to your project 18 | 19 | > Follow instructions on [bintray-sbt](https://github.com/softprops/bintray-sbt) 20 | 21 | #### Add sbt-bintray to your sbt `project/plugins.sbt` 22 | 23 | ```scala 24 | resolvers += Resolver.url( 25 | "bintray-sbt-plugin-releases", 26 | url("http://dl.bintray.com/content/sbt/sbt-plugin-releases"))( 27 | Resolver.ivyStylePatterns) 28 | 29 | addSbtPlugin("me.lessis" % "bintray-sbt" % "0.1.2") 30 | ``` 31 | 32 | #### Add sbt 0.13.x to your sbt `project/build.properties` 33 | 34 | > this plugin targets sbt 0.13. 35 | 36 | You will need to add the following to your `project/build.properties` file if you have multiple versions of sbt installed 37 | 38 | ``` 39 | sbt.version=0.13.7 40 | ``` 41 | 42 | #### Add Bintray resolver + deps to your `build.sbt` 43 | 44 | ```scala 45 | resolvers += bintray.Opts.resolver.mavenRepo("mandubian") 46 | 47 | val scalednVersion = "1.0.0-e8180d08620a607ec47613f8c2585f7784e86625" 48 | 49 | libraryDependencies ++= Seq( 50 | // only need scaledn parser? 51 | "com.mandubian" %% "scaledn-parser" % scalednVersion 52 | // only need scaledn validation/serialization? 53 | , "com.mandubian" %% "scaledn-validation" % scalednVersion 54 | // only need scaledn macros? 55 | , "com.mandubian" %% "scaledn-macros" % scalednVersion 56 | ) 57 | 58 | 59 | //or for custom subprojects 60 | 61 | scalednVersion := "1.0.0-f77f98cc305ce8a304d8941f800505c6b3d41d74" 62 | 63 | lazy val myproj = project 64 | .settings( 65 | resolvers += bintray.Opts.resolver.mavenRepo("mandubian") 66 | ) 67 | .settings( 68 | libraryDependencies ++= Seq( 69 | // only need scaledn parser? 70 | "com.mandubian" %% "scaledn-parser" % scalednVersion 71 | // only need scaledn validation/serialization? 72 | , "com.mandubian" %% "scaledn-validation" % scalednVersion 73 | // only need scaledn macros? 74 | , "com.mandubian" %% "scaledn-macros" % scalednVersion 75 | ) 76 | ) 77 | ``` 78 | 79 | > There is a [helloedn sample](samples/helloedn) 80 | 81 | ## Why EDN?... 82 | 83 | > Because Json is not enough & quite limitating 84 | 85 | EDN is described as an _extensible data notation_ specified (not really standardized) [there](https://github.com/edn-format/edn). Clojure & Datalog used in Datomic are supersets of EDN. 86 | 87 | EDN allows much more things than Json while keeping the same simplicity. 88 | 89 | Here are the main points making EDN great to represent & exchange Data 90 | 91 |
92 | ### EDN manages number types far better than Json 93 | 94 | For Json, all numbers (floating or integer, exponential or not) are all considered in the same way so numbers can only be mapped to the biggest number format: `BigDecimal`. It is really bad in terms of semantics and performance. 95 | 96 | In EDN, numbers can be : 97 | 98 | - 64bits integer aka `Long` in Scala: 99 | 100 | ``` 101 | 12345 102 | ``` 103 | - 64bits floating point numbers & exponentials aka `Double` in Scala: 104 | 105 | ``` 106 | 123.45e-9 107 | ``` 108 | - Natural Integers aka `BigInt` in Scala: 109 | 110 | ``` 111 | 1234567891234N 112 | ``` 113 | - Exact Floating Number aka `BigDecimal` in Scala: 114 | 115 | ``` 116 | 123.4578972345M 117 | ``` 118 | 119 | 120 |
121 | ### EDN knows much more about collections 122 | 123 | Collections in Json are just: 124 | 125 | - lists of heterogenous json values 126 | - maps of key strings and json values. 127 | 128 | In EDN, you can have: 129 | 130 | - heterogenous lists 131 | 132 | ``` 133 | (1 true "toto) 134 | ``` 135 | - heterogenous vectors/arrays 136 | ``` 137 | [1 true "toto] 138 | ``` 139 | - heterogenous sets 140 | ``` 141 | #{1 true "toto} 142 | ``` 143 | - heterogenous maps with heterogenous keys & values 144 | ``` 145 | {1 "toto", "foo" 2} 146 | ``` 147 | 148 |
149 | ### EDN accepts characters & unicode 150 | 151 | Json doesn't know about characters outside strings. 152 | 153 | EDN can manage chars: 154 | 155 | ``` 156 | // simple char 157 | \c 158 | 159 | // special chars 160 | \newline 161 | \return 162 | \space 163 | \tag 164 | \\ 165 | 166 | // unicode 167 | \u0308 168 | ``` 169 | 170 |
171 | ### EDN accepts comments & discarded values 172 | 173 | There are special syntaxes: 174 | 175 | - comments are lines starting with `;` 176 | - values starting with `#_` are parsed but discarded 177 | 178 |
179 | ### EDN knows about symbols & keywords 180 | 181 | These are notions that don't exist in Json. 182 | 183 | Symbols can reference anything external or internal that you want to identify. A `Symbol` can have a namespace such as `foo/bar`. 184 | 185 | Keywords are unique identifiers or enumerated values that can be reused in your data structure. A `Keyword` is just a symbol preceded by a `:` such as :foo/bar. 186 | 187 |
188 | ### EDN is extensible using tags 189 | 190 | EDN is an extensible format using tags starting with `#` such as: 191 | 192 | ``` 193 | #foo/bar value 194 | ``` 195 | 196 | When parsing EDN format, the parser should provide tag handlers that can be applied when a tag is discovered. In this way, you can extend default format with your own formats. 197 | 198 | EDN specifies 2 tag handlers by default: 199 | 200 | - `#inst "1985-04-12T23:20:50.52Z"` for RFC-3339 instants 201 | - `#uuid "f81d4fae-7dec-11d0-a765-00a0c91e6bf6"` for UUID 202 | 203 |
204 | ### EDN has no root node & can be streamed 205 | 206 | Json is defined to have a root `map` node: `{ key : value }` or `[ ... ]`. 207 | 208 | Json can't accept single values outside of this. So Json isn't really meant to be streamed as you need to find closing tags to finish parsing a value. 209 | 210 | EDN doesn't require this and can consist in multiple heterogenous values: 211 | 212 | ```1 123.45 "toto" true nil (1 2 3)``` 213 | 214 | As a consequence, EDN can be used to stream your data structures. 215 | 216 |
217 | ### Conclusion: EDN should be preferred to Json 218 | 219 | All of these points make EDN a far better & stricter & more evolutive notation to represent data structures than Json. It can be used in the same way as Json but could make a far better RPC string format than Json. 220 | 221 | I still wonder why Json has become the de-facto standard except for the reason that the _not so serious_ Javascript language parses it natively and because people were so sick about XML that they would have accepted anything changing their daily life. 222 | 223 | But JS could also parse EDN without any problem and all more robust & typed backend languages would earn a lot from using EDN instead of JSON for their interfaces. 224 | 225 | EDN could be used in REST API & also for streaming API. 226 | That's exactly why, I wanted to provide a complete Scala API for EDN to test this idea a bit further. 227 | 228 |
229 |
230 | ## Scaledn insight 231 | 232 |
233 | ### Runtime Parsing 234 | 235 | Scaledn can be used to parse the EDN string or arrays of chars received by your API. 236 | 237 | All types described in EDN format are isomorphic to Scala types so I've decided to skip the complete AST wrapping those types and directly parse to Scala types. 238 | 239 | - `"foobar"` is parsed to `String` 240 | - `123` is parsed to `Long` 241 | - `(1 2 3)` is parsed to `List[Long]` 242 | - `(1 "toto" 3)` is parsed to `List[Any]` 243 | - `{"toto" 1 "tata" 2}` is parsed to `Map[String, Long]` 244 | - `{1 "toto" 2 "tata"}` is parsed to `Map[Long, String]` 245 | - `{1 "toto" true 3}` is parsed to `Map[Any, Any]` 246 | - etc... 247 | 248 | The parser (based on [Parboiled2](https://github.com/sirthias/parboiled2)) provides 2 main functions: 249 | 250 | ```scala 251 | import scaledn._ 252 | import parser._ 253 | 254 | // parses only the first EDN value discovered in the String input 255 | def parseEDN(in: ParserInput): Try[EDN] = ... 256 | 257 | // parses all EDN values discovered in the String input 258 | def parseEDNs(in: ParserInput): Try[Seq[EDN]] = ... 259 | ``` 260 | 261 | If you look in common package, you'll see that `EDN` is just an alias for `Any` ;) 262 | 263 | 264 | Here is how you can use it: 265 | 266 | ```scala 267 | import scaledn._ 268 | import parser._ 269 | 270 | // Single Value 271 | parseEDN("""{1 "foo", "bar" 1.234M, :foo/bar [1,2,3]} #_foo/bar :bar/foo""") match { 272 | case Success(t) => \/-(t) 273 | case Failure(f : org.parboiled2.ParseError) => -\/(parser.formatError(f)) 274 | } 275 | 276 | // Multiple Value 277 | parseEDNs("""{1 "foo", "bar" 1.234M, :foo/bar [1,2,3]} :bar/foo""").success.value should be ( 278 | Vector( 279 | Map( 280 | 1L -> "foo", 281 | "bar" -> BigDecimal("1.234"), 282 | EDNKeyword(EDNSymbol("foo/bar", Some("foo"))) -> Vector(1, 2, 3) 283 | ), 284 | EDNKeyword(EDNSymbol("bar/foo", Some("bar"))) 285 | ) 286 | )) 287 | ``` 288 | 289 | > Some people will think `Any` is a bit too large and I agree but it's quite practical to use. Moreover, using validation explained a bit later, you can parse your EDN and then map it to a stronger typed scala structure and then `Any` disappears. 290 | 291 |
292 | ## Compile-time parsing with Macros 293 | 294 | When you use static EDN structures in your Scala code, you can write them in their string format and _scaledn_ can parse them at compile-time using Scala macros and thus prevent a lot of errors you can encounter in dynamic languages. 295 | 296 | The macro mechanism is based on quasiquotes & whitebox macro contexts which allow to infer types of your parsed EDN structures at compile-time. For example: 297 | 298 | ```scala 299 | > val s:Long = EDN("\"toto\"") 300 | 301 | [error] found : String("toto") 302 | [error] required: Long 303 | [error] val e: Long = EDN("\"toto\"") 304 | 305 | ``` 306 | 307 | Whooohooo magic :) 308 | 309 | 310 |
311 | ### Classic Scala types 312 | 313 | Here is how you can use it: 314 | 315 | ```scala 316 | import scaledn._ 317 | import macros._ 318 | 319 | // All types are just for info and can be omitted below, the macro infers them quite well 320 | val e: String = EDN("\"toto\"") 321 | 322 | val bt: Boolean = EDN("true") 323 | 324 | val bf: Boolean = EDN("false") 325 | 326 | val l: Long = EDN("123") 327 | 328 | val d: Double = EDN("123.456") 329 | 330 | val bi: BigInt = EDN("123M") 331 | 332 | val bd: BigDecimal = EDN("123.456N") 333 | 334 | val s: EDNSymbol = EDN("foo/bar") 335 | 336 | val kw: EDNKeyword = EDN(":foo/bar") 337 | 338 | // Homogenous collection inferred as Vecto[String] 339 | val vector: Vector[String] = EDN("""["tata" "toto" "tutu"]""") 340 | 341 | // multiple heterogenous values inferred as Seq[Any] 342 | val s = EDNs("""(1 2 3) "toto" [true false] :foo/bar""") 343 | // note the small s at the end of EDN to inform the macro there are several values 344 | ``` 345 | 346 | 347 | ### Shapeless heterogenous collections 348 | 349 | EDN allows to manipulate heterogenous collections. In Scala, when one thinks _heterogenous collection_, one thinks [Shapeless](https://github.com/milessabin/shapeless). Scaledn macros can parse & map your EDN stringified structures to Scala strongly typed structures. 350 | 351 | 352 | ```scala 353 | import scaledn._ 354 | import macros._ 355 | 356 | import shapeless.{HNil, ::} 357 | import shapeless.record._ 358 | import shapeless.syntax.singleton._ 359 | 360 | // Heterogenous list 361 | val s = EDNH("""(1 "toto" true)""") 362 | s should equal (1L :: "toto" :: true :: HNil) 363 | 364 | // Heterogenous Map/Record 365 | val s3 = EDNH("""{1 "toto" true 1.234 "foo" (1 2 3)}""") 366 | s3 should equal ( 367 | 1L ->> "toto" :: 368 | true ->> 1.234 :: 369 | "foo" ->> List(1L, 2L, 3L) :: 370 | HNil 371 | ) 372 | ``` 373 | 374 | > please note the `H` in `EDNH` for heterogenous 375 | 376 | > I must say using these macros, it might be even simpler to write Shapeless hlists or records than using scala API ;) 377 | 378 |
379 | ### Macro API 380 | 381 | Scaledn provides different macros depending on the depth of introspection you require in your collection with respect to heterogeneity. 382 | 383 | Have a look directly at [Macro API](https://github.com/mandubian/scaledn/blob/master/macros/src/main/scala/macros.scala) 384 | 385 |
386 | ### Mixing macro with Scala string interpolation 387 | 388 | Following ideas implemented by Daniel James in [Datomisca](http://pellucidanalytics.github.io/datomisca/), scaledn proposes to use String interpolation mixed with parsing macro such as: 389 | 390 | ```scala 391 | import scaledn._ 392 | import macros._ 393 | 394 | import shapeless.{HNil, ::} 395 | 396 | val l = 123L 397 | val s = List("foo", "bar") 398 | 399 | val r: Long = EDN(s"$l") 400 | 401 | val r1: Seq[Any] = EDN(s"($l $s)") 402 | val r2: Long :: List[String] :: HNil = EDNH(s"($l $s)") 403 | ``` 404 | 405 | Nothing to add, macros are cool sometimes :) 406 | 407 |
408 |
409 | ## Runtime validation of EDN to Scala 410 | 411 | When writing REST or external API, the received data can never be trusted before being validated. So, you generally try to validate what is received and map it to a strong-typed structures. For example: 412 | 413 | ```scala 414 | // parse the received string input 415 | parseEDN("""{ 1 "toto" 2 "tata" 3 "tutu" }""") 416 | // then validate it to a Scala type 417 | .map(validateEDN[Map[Long, String]]) 418 | .success.value should be ( 419 | play.api.data.mapping.Success(Map( 420 | 1L -> "toto", 421 | 2L -> "tata", 422 | 3L -> "tutu" 423 | )) 424 | ) 425 | ``` 426 | 427 | The validation API is the following: 428 | 429 | ```scala 430 | import scaledn._ 431 | import validate._ 432 | 433 | def validateEDN[T](edn: EDN)(implicit r: RuleLike[EDN, T]): Validation[EDN, T] = r.validate(edn) 434 | ``` 435 | 436 | Scaledn validation is based on [Generic Validation API](https://github.com/jto/validation) developed by my [MFGLabs](http://www.mfglabs.com)'s colleague & friend [Julien Tournay](https://github.com/jto). This API was developed for Play Framework & Typesafe last year to generalize Json validation API to all data formats. But it will never be integrated in Play as Typesafe considers it to be too pure Scala & pure FP-oriented. Yet, we use this API in production at [MFGLabs](http://www.mfglabs.com) and maintain/extend it ourselves. 437 | 438 | As explained before, Scaledn parser parses EDN values directly to Scala types as they are bijective so validation is often just a runtime cast and not very interesting in general. 439 | 440 | What's much more interesting is to validate to Shapeless HList, Records and even more interesting to CaseClasses & Tuples based on Shapeless fantastic auto-generated Generic macros. 441 | 442 | Let's take a few examples to show the power of this feature: 443 | 444 | ```scala 445 | import scaledn._ 446 | import validate._ 447 | 448 | import play.api.data.mapping._ 449 | import shapeless.{HNil, ::} 450 | 451 | case class CP(cp: Int) 452 | case class Address(street: String, cp: CP) 453 | case class Person(name: String, age: Int, addr: Address) 454 | // Remark that NO implicits must be declared on our case classes 455 | 456 | // HLISTS 457 | parseEDN("""(1 "toto" true nil)""").map( 458 | validateEDN[Long :: String :: Boolean :: EDNNil.type :: HNil] 459 | ).success.value should be ( 460 | Success(1L :: "toto" :: true :: EDNNil :: HNil) 461 | ) 462 | 463 | // TUPLES 464 | parseEDN("""("toto" 34 {"street" "chboing", "cp" {"cp" 75009}})""").map( 465 | validateEDN[Tuple3[String, Int, Address]] 466 | ).success.value should be ( 467 | Success(("toto", 34, Address("chboing", CP(75009)))) 468 | ) 469 | 470 | // CASECLASSES 471 | parseEDN("""("toto" 34 ("chboing" (75009)))""").map( 472 | validateEDN[Person] 473 | ).success.value should be ( 474 | Success(Person("toto", 34, Address("chboing", CP(75009)))) 475 | ) 476 | 477 | parseEDN("""{"name" "toto", "age" 34, "addr" {"street" "chboing", "cp" {"cp" 75009}}}""").map( 478 | validateEDN[Person] 479 | ).success.value should be ( 480 | Success(Person("toto", 34, Address("chboing", CP(75009)))) 481 | ) 482 | 483 | ``` 484 | 485 | > I think here you can see the power of this validation feature without writing any boilerplate... 486 | 487 | 488 |
489 |
490 | ## Serializing Scala to EDN 491 | 492 | Using [Generic Validation API](https://github.com/jto/validation), you can also write scala structures to any other data format. 493 | 494 | ```scala 495 | import scaledn._ 496 | import write._ 497 | 498 | toEDNString("toto") should equal ("\"toto\"") 499 | toEDNString(List(1, 2, 3)) should equal ("""(1 2 3)""") 500 | ``` 501 | 502 | The write API is the following: 503 | 504 | ```scala 505 | import scaledn._ 506 | import write._ 507 | 508 | def toEDNString[I](i: I)(implicit w: WriteLike[I, String]): String = w.writes(i) 509 | ``` 510 | 511 | Once again, what's more interesting is using shapeless & caseclasses & tuples. 512 | 513 | ```scala 514 | import scaledn._ 515 | import write._ 516 | 517 | import shapeless.{HNil, ::} 518 | 519 | // HLIST 520 | toEDNString(1 :: true :: List(1L, 2L, 3L) :: HNil) should equal ("""(1 true (1 2 3))""") 521 | 522 | // TUPLE 523 | toEDNString((23, true)) should equal ("""(23 true)""") 524 | 525 | // CASE CLASS 526 | case class Address(street: String, cp: Int) 527 | case class Person(name: String, age: Int, addr: Address) 528 | // Remark that NO implicits must be declared on our case classes 529 | 530 | toEDNString(Person("toto", 34, Address("chboing", 75009))) should equal ( 531 | """{"name" "toto", "age" 34, "addr" {"street" "chboing", "cp" 75009}}""" 532 | ) 533 | ``` 534 | 535 | 536 | ## TODO 537 | 538 | This project is a first draft so it requires a bit more work. 539 | 540 | Here are a few points to work on: 541 | 542 | - patch remaining glitches/bugs 543 | - write more tests for all cases 544 | - study streamed parser asap 545 | - write sample apps 546 | 547 | Don't hesitate to test, find bugs, contribute, give remarks, ideas... 548 | 549 | Have fun in EDN world.. 550 | 551 | -------------------------------------------------------------------------------- /validation/src/main/scala/validate.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2014 Pascal Voitot 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package scaledn 17 | package validate 18 | 19 | import play.api.libs.functional._ 20 | import play.api.libs.functional.syntax._ 21 | import play.api.data.mapping._ 22 | 23 | import scaledn._ 24 | 25 | 26 | object Rules extends Rules 27 | 28 | 29 | /** TRICKKKKKKK 30 | * scalac fails is if looking for recursive Rule[EDN, VT] so the trick is to go to SubRule 31 | */ 32 | trait SubRule[I, O] { 33 | def validate(data: I): VA[O] 34 | } 35 | 36 | object SubRule { 37 | def apply[I, O](f: I => VA[O]) = new SubRule[I, O]{ 38 | def validate(data: I): VA[O] = f(data) 39 | } 40 | 41 | def fromMapping[I, O](f: Mapping[ValidationError, I, O]) = 42 | SubRule[I, O](f(_: I).fail.map(errs => Seq(play.api.data.mapping.Path -> errs))) 43 | 44 | def fromRule[I, O](rule: Rule[I, O]) = SubRule[I, O] { i => rule.validate(i) } 45 | 46 | } 47 | 48 | trait SubRule2[I, O] { 49 | def validate(data: I): VA[O] 50 | } 51 | 52 | object SubRule2 { 53 | def apply[I, O](f: I => VA[O]) = new SubRule2[I, O]{ 54 | def validate(data: I): VA[O] = f(data) 55 | } 56 | 57 | def fromMapping[I, O](f: Mapping[ValidationError, I, O]) = 58 | SubRule2[I, O](f(_: I).fail.map(errs => Seq(play.api.data.mapping.Path -> errs))) 59 | 60 | def fromRule[I, O](rule: Rule[I, O]) = SubRule2[I, O] { i => rule.validate(i) } 61 | 62 | } 63 | 64 | trait Rules extends ValidationUtils with ShapelessRules { 65 | 66 | private def ednAs[T](f: PartialFunction[EDN, Validation[ValidationError, T]])(msg: String, args: Any*) = 67 | Rule.fromMapping[EDN, T]( 68 | f.orElse { 69 | case j => Failure(Seq(ValidationError(msg, args: _*))) 70 | } 71 | ) 72 | 73 | implicit def stringR = ednAs[String] { 74 | case s: String => Success(s) 75 | }("error.invalid", "String") 76 | 77 | implicit def booleanR = ednAs[Boolean] { 78 | case b: Boolean => Success(b) 79 | }("error.invalid", "Boolean") 80 | 81 | implicit def longR = ednAs[Long] { 82 | case l: Long => Success(l) 83 | }("error.number", "Long") 84 | 85 | implicit def bigIntR = ednAs[BigInt] { 86 | case b: BigInt => Success(b) 87 | }("error.number", "BigInt") 88 | 89 | implicit def intR = ednAs[Int] { 90 | case l: Long => Success(l.toInt) 91 | case b: BigInt if b.isValidInt => Success(b.toInt) 92 | }("error.number", "Int") 93 | 94 | implicit def shortR = ednAs[Short] { 95 | case l: Long => Success(l.toShort) 96 | case b: BigInt if b.isValidShort => Success(b.toShort) 97 | }("error.number", "Short") 98 | 99 | implicit def doubleR = ednAs[Double] { 100 | case d: Double => Success(d) 101 | }("error.number", "Double") 102 | 103 | implicit def BigDecimalR = ednAs[BigDecimal] { 104 | case b: BigDecimal => Success(b) 105 | }("error.number", "BigDecimal") 106 | 107 | // BigDecimal.isValidFloat is buggy, see [SI-6699] 108 | import java.{ lang => jl } 109 | private def isValidFloat(bd: BigDecimal) = { 110 | val d = bd.toFloat 111 | !d.isInfinity && bd.bigDecimal.compareTo(new java.math.BigDecimal(jl.Float.toString(d), bd.mc)) == 0 112 | } 113 | 114 | implicit def floatR = ednAs[Float] { 115 | case d: Double => Success(d.toFloat) 116 | case b: BigDecimal if(isValidFloat(b)) => Success(b.toFloat) 117 | }("error.number", "Float") 118 | 119 | implicit def symbolR = ednAs[EDNSymbol] { 120 | case s: EDNSymbol => Success(s) 121 | }("error.invalid", "Symbol") 122 | 123 | implicit def keywordR = ednAs[EDNKeyword] { 124 | case k: EDNKeyword => Success(k) 125 | }("error.invalid", "Keyword") 126 | 127 | implicit def listR = ednAs[List[EDN]] { 128 | case s: List[EDN] => Success(s) 129 | }("error.invalid", "List") 130 | 131 | implicit def setR = ednAs[Set[EDN]] { 132 | case s: Set[EDN @unchecked] => Success(s) 133 | }("error.invalid", "Set") 134 | 135 | implicit def vectorR = ednAs[Vector[EDN]] { 136 | case v: Vector[EDN] => Success(v) 137 | }("error.invalid", "Vector") 138 | 139 | implicit def mapR = ednAs[Map[EDN, EDN]] { 140 | case m: Map[EDN @unchecked, EDN @unchecked] => Success(m) 141 | }("error.invalid", "Map") 142 | 143 | implicit def mapKVR[K, V](implicit rk: RuleLike[EDN, K], rv: RuleLike[EDN, V]): Rule[EDN, Map[K, V]] = { 144 | mapR.compose(Path)( 145 | Rule { kvs => 146 | val rkr = Rule.toRule(rk) 147 | val rvr = Rule.toRule(rv) 148 | val validations = kvs.toSeq.map { case kv => 149 | val vk = rkr.repath((Path \ (kv._1.toString + "-key")) ++ _).validate(kv._1) 150 | val vv = rvr.repath((Path \ (kv._1.toString + "-value")) ++ _).validate(kv._2) 151 | tupled2(vk, vv) 152 | } 153 | Validation.sequence(validations).map(_.toMap) 154 | } 155 | ) 156 | } 157 | 158 | implicit def pickInEdn[II <: EDN, O](p: Path)(implicit r: RuleLike[EDN, O]): Rule[II, O] = { 159 | 160 | def search(path: Path, edn: EDN): Option[EDN] = path.path match { 161 | case KeyPathNode(k) :: t => 162 | edn match { 163 | case m: Map[EDN @unchecked, EDN @unchecked] => 164 | m.find(_._1.toString == k).flatMap(kv => search(Path(t), kv._2)) 165 | case _ => None 166 | } 167 | case IdxPathNode(i) :: t => 168 | edn match { 169 | case l: List[EDN] => l.lift(i).flatMap(j => search(Path(t), j)) 170 | case _ => None 171 | } 172 | case Nil => Some(edn) 173 | } 174 | 175 | Rule[II, EDN] { edn => 176 | search(p, edn) match { 177 | case None => Failure(Seq(Path -> Seq(ValidationError("error.required")))) 178 | case Some(edn) => Success(edn) 179 | } 180 | }.compose(r) 181 | } 182 | 183 | def seqR[I]: Rule[EDN, Seq[EDN]] = listR.fmap(_.toSeq) orElse vectorR.fmap(_.toSeq) 184 | 185 | def _seqR[I, O](implicit r: RuleLike[I, O]): Rule[Seq[I], Seq[O]] = 186 | Rule { is: Seq[I] => 187 | val rr = Rule.toRule(r) 188 | val withI = is.zipWithIndex.map { 189 | case (v, i) => 190 | rr.repath((Path \ i) ++ _).validate(v) 191 | } 192 | traverse(withI) 193 | } 194 | 195 | implicit def pickSeq[O](implicit r: RuleLike[EDN, O]): Rule[EDN, Seq[O]] = seqR compose _seqR[EDN, O] 196 | 197 | def _listR[I, O](implicit r: RuleLike[I, O]): Rule[List[I], List[O]] = 198 | Rule { is: List[I] => 199 | val rr = Rule.toRule(r) 200 | val withI = is.zipWithIndex.map { 201 | case (v, i) => 202 | rr.repath((Path \ i) ++ _).validate(v) 203 | } 204 | traverse(withI) 205 | } 206 | 207 | implicit def pickList[O](implicit r: RuleLike[EDN, O]): Rule[EDN, List[O]] = listR compose _listR[EDN, O] 208 | 209 | def _setR[I, O](implicit r: RuleLike[I, O]): Rule[Set[I], Set[O]] = 210 | Rule { is: Set[I] => 211 | val rr = Rule.toRule(r) 212 | val withI = is.zipWithIndex.map { 213 | case (v, i) => 214 | rr.repath((Path \ v.toString) ++ _).validate(v) 215 | } 216 | traverse(withI) 217 | } 218 | 219 | implicit def pickSet[O](implicit r: RuleLike[EDN, O]): Rule[EDN, Set[O]] = setR compose _setR[EDN, O] 220 | 221 | def _vectorR[I, O](implicit r: RuleLike[I, O]): Rule[Vector[I], Vector[O]] = 222 | Rule { is: Vector[I] => 223 | val rr = Rule.toRule(r) 224 | val withI = is.zipWithIndex.map { 225 | case (v, i) => 226 | rr.repath((Path \ i) ++ _).validate(v) 227 | } 228 | traverse(withI) 229 | } 230 | 231 | implicit def pickVector[O](implicit r: RuleLike[EDN, O]): Rule[EDN, Vector[O]] = vectorR compose _vectorR[EDN, O] 232 | 233 | implicit val nilR = ednAs[EDNNil.type] { 234 | case EDNNil => Success(EDNNil) 235 | }("error.invalid", "nil") 236 | 237 | implicit def ooo[O](p: Path)(implicit pick: Path => RuleLike[EDN, EDN], coerce: RuleLike[EDN, O]): Rule[EDN, Option[O]] = 238 | optionR(Rule.zero[O])(pick, coerce)(p) 239 | 240 | def optionR[J, O](r: => RuleLike[J, O], noneValues: RuleLike[EDN, EDN]*)( 241 | implicit pick: Path => RuleLike[EDN, EDN], coerce: RuleLike[EDN, J] 242 | ): Path => Rule[EDN, Option[O]] = 243 | super.opt[J, O](r, (nilR.fmap(n => n: EDN) +: noneValues): _*) 244 | 245 | } 246 | 247 | 248 | trait ShapelessRules extends ValidationUtils with LowerRules { 249 | import shapeless.{HList, ::, HNil, Unpack2, Witness, LabelledGeneric, Generic, IsTuple} 250 | import shapeless.labelled.FieldType 251 | import shapeless.ops.hlist.IsHCons 252 | 253 | // import shapelessext._ 254 | 255 | 256 | def ap2[HH, HT <: HList](head: VA[HH], tail: VA[HT])(implicit applicative: Applicative[VA]): VA[HH::HT] = 257 | applicative.apply( 258 | applicative.map( 259 | head, 260 | (h: HH) => (t: HT) => h :: t 261 | ), 262 | tail 263 | ) 264 | 265 | 266 | implicit val hnilR: Rule[EDN, HNil] = Rule.fromMapping[EDN, HNil] { 267 | case l: List[EDN] if l.isEmpty => Success(HNil) 268 | case s: Set[EDN @ unchecked] if s.isEmpty => Success(HNil) 269 | case v: Vector[EDN] if v.isEmpty => Success(HNil) 270 | case m: Map[EDN @unchecked, EDN @unchecked] /* map doesn't use head/tail if m.isEmpty*/ => Success(HNil) 271 | case a => Failure(Seq(ValidationError("error.invalid", "HNil"))) 272 | } 273 | 274 | 275 | /** TRICKKKKKKK 276 | * scalac fails is if looking for recursive Rule[EDN, VT] so the trick is to go to SubRule 277 | */ 278 | implicit def hlistR[HH, HT <: HList, K, V]( 279 | implicit 280 | hr: Rule[EDN, HH], 281 | ht: SubRule[EDN, HT], 282 | applicative: Applicative[VA] 283 | ): Rule[EDN, HH :: HT] = Rule[EDN, HH :: HT]{ 284 | case head :: tail => 285 | ap2(hr.validate(head), ht.validate(tail)) 286 | case l: List[EDN] if !l.isEmpty => 287 | ap2(hr.validate(l.head), ht.validate(l.tail)) 288 | case v: Vector[EDN] if !v.isEmpty => 289 | ap2(hr.validate(v.head), ht.validate(v.tail)) 290 | case m: Map[EDN @unchecked, EDN @unchecked] if !m.isEmpty => 291 | ap2(hr.validate(m), ht.validate(m)) 292 | case a => 293 | Failure(Seq(play.api.data.mapping.Path -> Seq(ValidationError("error.invalid", "HList Rule (only supports non empty HList, List & Vector and any Map)")))) 294 | } 295 | 296 | /** TRICKKKKKKK 297 | * scalac fails is if looking for recursive Rule[EDN, VT] so the trick is to go to SubRule 298 | */ 299 | implicit def tupleR[P, VS <: HList, VH, VT <: HList]( 300 | implicit 301 | cc: IsTuple[P], 302 | genValues: Generic.Aux[P, VS], 303 | c2: IsHCons.Aux[VS, VH, VT], 304 | vhr: Rule[EDN, VH], 305 | vtr: SubRule[EDN, VT] 306 | ): Rule[EDN, P] = Rule[EDN, P]{ edn => edn match { 307 | case head :: tail => 308 | ap2(vhr.validate(head), vtr.validate(tail)).map{ l => genValues.from(l.asInstanceOf[VS]) } 309 | case l: List[EDN] if !l.isEmpty => 310 | ap2(vhr.validate(l.head), vtr.validate(l.tail)).map{ l => genValues.from(l.asInstanceOf[VS]) } 311 | case v: Vector[EDN] if !v.isEmpty => 312 | ap2(vhr.validate(v.head), vtr.validate(v.tail)).map{ l => genValues.from(l.asInstanceOf[VS]) } 313 | case a => 314 | Failure(Seq(play.api.data.mapping.Path -> Seq(ValidationError("error.invalid", "Tuple Rule (only supports non empty HList, List, Vector)")))) 315 | }} 316 | 317 | 318 | implicit def fieldTypeR[K <: Symbol, V]( 319 | implicit witness: Witness.Aux[K], wv: RuleLike[EDN, V] 320 | ): Rule[EDN, FieldType[K, V]] = { 321 | import shapeless.labelled._ 322 | import play.api.data.mapping.Path 323 | (play.api.data.mapping.Path \ witness.value.name).read[EDN, V].fmap { v => field[K](v) } 324 | } 325 | 326 | 327 | implicit val hnilSR: SubRule[EDN, HNil] = SubRule.fromMapping[EDN, HNil] { 328 | case l: List[EDN] if l.isEmpty => Success(HNil) 329 | case s: Set[EDN @ unchecked] if s.isEmpty => Success(HNil) 330 | case v: Vector[EDN] if v.isEmpty => Success(HNil) 331 | case m: Map[EDN @unchecked, EDN @unchecked] /* map doesn't use head/tail if m.isEmpty*/ => Success(HNil) 332 | case a => Failure(Seq(ValidationError("error.invalid", "HNil"))) 333 | } 334 | 335 | // SubRule higher level for Hlist of FieldTypes 336 | implicit def hlistSRF[HH, HT <: HList, K, V]( 337 | implicit 338 | un: Unpack2[HH, FieldType, K, V], 339 | hr: Rule[EDN, FieldType[K, V]], 340 | ht: SubRule[EDN, HT], 341 | applicative: Applicative[VA] 342 | ): SubRule[EDN, HH :: HT] = SubRule[EDN, HH :: HT]{ 343 | case scala.::(head, tail) => 344 | ap2(hr.validate(head).map(l => l.asInstanceOf[HH]), ht.validate(tail)) 345 | case l: List[EDN] if !l.isEmpty => 346 | ap2(hr.validate(l.head).map(l => l.asInstanceOf[HH]), ht.validate(l.tail)) 347 | case v: Vector[EDN] if !v.isEmpty => 348 | ap2(hr.validate(v.head).map(l => l.asInstanceOf[HH]), ht.validate(v.tail)) 349 | case m: Map[EDN @unchecked, EDN @unchecked] if !m.isEmpty => 350 | ap2(hr.validate(m).map(l => l.asInstanceOf[HH]), ht.validate(m)) 351 | case a => 352 | Failure(Seq(play.api.data.mapping.Path -> Seq(ValidationError("error.invalid", "HList Rule (only supports non empty HList, List & Vector and any Map)")))) 353 | } 354 | 355 | } 356 | 357 | 358 | 359 | trait LowerRules extends play.api.data.mapping.DefaultRules[EDN] { 360 | import shapeless.{HList, ::, HNil, Unpack2, Witness, LabelledGeneric, Generic, HasProductGeneric, <:!<} 361 | import shapeless.labelled.FieldType 362 | import shapeless.ops.hlist.IsHCons 363 | 364 | 365 | /** TRICKKKKKKK 366 | * scalac fails is if looking for recursive Rule[EDN, VT] so the trick is to go to SubRule 367 | */ 368 | implicit def caseClassR[P, HL <: HList, HH, HT <: HList, K, V, VS <: HList, VH, VT <: HList]( 369 | implicit 370 | cc: HasProductGeneric[P], 371 | not: P <:!< EDNValue, 372 | genFields: LabelledGeneric.Aux[P, HL], 373 | c1: IsHCons.Aux[HL, HH, HT], 374 | un1: Unpack2[HH, FieldType, K, V], 375 | hhr: Rule[EDN, FieldType[K, V]], 376 | htr: SubRule[EDN, HT], 377 | genValues: Generic.Aux[P, VS], 378 | c2: IsHCons.Aux[VS, VH, VT], 379 | vhr: Rule[EDN, VH], 380 | vtr: SubRule2[EDN, VT] 381 | ): Rule[EDN, P] = Rule[EDN, P]{ edn => edn match { 382 | case head :: tail => 383 | ap2(vhr.validate(head), vtr.validate(tail)).map{ l => genValues.from(l.asInstanceOf[VS]) } 384 | case l: List[EDN] if !l.isEmpty => 385 | ap2(vhr.validate(l.head), vtr.validate(l.tail)).map{ l => genValues.from(l.asInstanceOf[VS]) } 386 | case v: Vector[EDN] if !v.isEmpty => 387 | ap2(vhr.validate(v.head), vtr.validate(v.tail)).map{ l => genValues.from(l.asInstanceOf[VS]) } 388 | case m: Map[EDN @unchecked, EDN @unchecked] if !m.isEmpty => 389 | ap2(hhr.validate(m), htr.validate(m)).map{ l => genFields.from(l.asInstanceOf[HL]) } 390 | case a => 391 | Failure(Seq(play.api.data.mapping.Path -> Seq(ValidationError("error.invalid", "CaseClass Rule (only supports non empty HList, List, Vector & Map)")))) 392 | }} 393 | 394 | implicit val hnilSR2: SubRule2[EDN, HNil] = SubRule2.fromMapping[EDN, HNil] { 395 | case l: List[EDN] if l.isEmpty => Success(HNil) 396 | case s: Set[EDN @ unchecked] if s.isEmpty => Success(HNil) 397 | case v: Vector[EDN] if v.isEmpty => Success(HNil) 398 | case m: Map[EDN @unchecked, EDN @unchecked] /* map doesn't use head/tail if m.isEmpty*/ => Success(HNil) 399 | case a => Failure(Seq(ValidationError("error.invalid", "HNil"))) 400 | } 401 | 402 | implicit def hlistSR2[HH, HT <: HList, K, V]( 403 | implicit 404 | hr: Rule[EDN, HH], 405 | ht: SubRule2[EDN, HT], 406 | applicative: Applicative[VA] 407 | ): SubRule2[EDN, HH :: HT] = SubRule2[EDN, HH :: HT]{ 408 | case head :: tail => 409 | ap2(hr.validate(head), ht.validate(tail)) 410 | case l: List[EDN] if !l.isEmpty => 411 | ap2(hr.validate(l.head), ht.validate(l.tail)) 412 | case v: Vector[EDN] if !v.isEmpty => 413 | ap2(hr.validate(v.head), ht.validate(v.tail)) 414 | case m: Map[EDN @unchecked, EDN @unchecked] if !m.isEmpty => 415 | ap2(hr.validate(m), ht.validate(m)) 416 | case a => 417 | Failure(Seq(play.api.data.mapping.Path -> Seq(ValidationError("error.invalid", "HList Rule (only supports non empty HList, List & Vector and any Map)")))) 418 | } 419 | 420 | // SubRule lower level for Hlist 421 | implicit def hlistSR[HH, HT <: HList, K, V]( 422 | implicit 423 | hr: Rule[EDN, HH], 424 | ht: SubRule2[EDN, HT], 425 | applicative: Applicative[VA] 426 | ): SubRule[EDN, HH :: HT] = SubRule[EDN, HH :: HT]{ 427 | case head :: tail => 428 | ap2(hr.validate(head), ht.validate(tail)) 429 | case l: List[EDN] if !l.isEmpty => 430 | ap2(hr.validate(l.head), ht.validate(l.tail)) 431 | case v: Vector[EDN] if !v.isEmpty => 432 | ap2(hr.validate(v.head), ht.validate(v.tail)) 433 | case m: Map[EDN @unchecked, EDN @unchecked] if !m.isEmpty => 434 | ap2(hr.validate(m), ht.validate(m)) 435 | case a => 436 | Failure(Seq(play.api.data.mapping.Path -> Seq(ValidationError("error.invalid", "HList Rule (only supports non empty HList, List & Vector and any Map)")))) 437 | } 438 | 439 | } 440 | 441 | trait ValidationUtils { 442 | import scala.collection.generic.CanBuildFrom 443 | 444 | def traverse[E, A, B, M[X] <: TraversableOnce[X], N[X] <: TraversableOnce[X]](vs: M[Validation[E, A]])( 445 | implicit cbf: CanBuildFrom[M[A], A, N[A]] 446 | ): Validation[E, N[A]] = { 447 | vs.foldLeft[Validation[E, N[A]]](Success(cbf().result)) { 448 | case (Success(as), Success(a)) => Success((cbf() ++= as += a).result) 449 | case (Success(_), Failure(e)) => Failure(e) 450 | case (Failure(e), Success(_)) => Failure(e) 451 | case (Failure(e1), Failure(e2)) => Failure(e1 ++ e2) 452 | } 453 | } 454 | 455 | def tupled2[E, A, B](vs: (Validation[E, A], Validation[E, B])): Validation[E, (A, B)] = { 456 | vs match { 457 | case (Success(a), Success(b)) => Success((a, b)) 458 | case (Success(_), Failure(e)) => Failure(e) 459 | case (Failure(e), Success(_)) => Failure(e) 460 | case (Failure(e1), Failure(e2)) => Failure(e1 ++ e2) 461 | } 462 | } 463 | } --------------------------------------------------------------------------------