New upstream version 0.11.1
Emmanuel Bourg
7 years ago
| 17 | 17 | |
| 18 | 18 | ### Overview |
| 19 | 19 | |
| 20 | Jawn consists of three parts: | |
| 21 | ||
| 22 | 1. A fast, generic JSON parser | |
| 23 | 2. A small, somewhat anemic AST | |
| 20 | Jawn consists of four parts: | |
| 21 | ||
| 22 | 1. A fast, generic JSON parser (`jawn-parser`) | |
| 23 | 2. A small, somewhat anemic AST (`jawn-ast`) | |
| 24 | 24 | 3. Support packages which parse to third-party ASTs |
| 25 | 4. A few helpful utilities (`jawn-util`) | |
| 25 | 26 | |
| 26 | 27 | Currently Jawn is competitive with the fastest Java JSON libraries |
| 27 | 28 | (GSON and Jackson) and in the author's benchmarks it often wins. It |
| 29 | 30 | 2014). |
| 30 | 31 | |
| 31 | 32 | Given the plethora of really nice JSON libraries for Scala, the |
| 32 | expectation is that you are here for (1) and (3) not (2). | |
| 33 | expectation is that you're probably here for `jawn-parser` or a | |
| 34 | support package. | |
| 33 | 35 | |
| 34 | 36 | ### Quick Start |
| 35 | 37 | |
| 42 | 44 | resolvers += Resolver.sonatypeRepo("releases") |
| 43 | 45 | |
| 44 | 46 | // use this if you just want jawn's parser, and will implement your own facade |
| 45 | libraryDependencies += "org.spire-math" %% "jawn-parser" % "0.10.3" | |
| 47 | libraryDependencies += "org.spire-math" %% "jawn-parser" % "0.11.0" | |
| 46 | 48 | |
| 47 | 49 | // use this if you want jawn's parser and also jawn's ast |
| 48 | libraryDependencies += "org.spire-math" %% "jawn-ast" % "0.10.3" | |
| 50 | libraryDependencies += "org.spire-math" %% "jawn-ast" % "0.11.0" | |
| 49 | 51 | ``` |
| 50 | 52 | |
| 51 | 53 | If you want to use Jawn's parser with another project's AST, see the |
| 53 | 55 | you would say: |
| 54 | 56 | |
| 55 | 57 | ```scala |
| 56 | libraryDependencies += "org.spire-math" %% "jawn-spray" % "0.10.3" | |
| 58 | libraryDependencies += "org.spire-math" %% "jawn-spray" % "0.11.0" | |
| 57 | 59 | ``` |
| 58 | 60 | |
| 59 | 61 | There are a few reasons you might want to do this: |
| 132 | 134 | |
| 133 | 135 | Jawn currently supports six external ASTs directly: |
| 134 | 136 | |
| 135 | | AST | 2.10 | 2.11 | 2.12 | | |
| 136 | |-----------|-------|-------|-------| | |
| 137 | | Argonaut | 6.1 | 6.1 | | | |
| 138 | | Json4s | 3.5.0 | 3.5.0 | 3.5.0 | | |
| 139 | | Play-json | 2.4.8 | 2.5.9 | | | |
| 140 | | Rojoma | 2.4.3 | 2.4.3 | 2.4.3 | | |
| 141 | | Rojoma-v3 | 3.7.0 | 3.7.0 | 3.7.0 | | |
| 142 | | Spray | 1.3.2 | 1.3.2 | 1.3.2 | | |
| 137 | | AST | 2.10 | 2.11 | 2.12 | | |
| 138 | |-----------|--------|--------|-------| | |
| 139 | | Argonaut | 6.2 | 6.2 | 6.2 | | |
| 140 | | Json4s | 3.5.2 | 3.5.2 | 3.5.2 | | |
| 141 | | Play-json | 2.4.11 | 2.5.15 | 2.6.0 | | |
| 142 | | Rojoma | 2.4.3 | 2.4.3 | 2.4.3 | | |
| 143 | | Rojoma-v3 | 3.7.2 | 3.7.2 | 3.7.2 | | |
| 144 | | Spray | 1.3.3 | 1.3.3 | 1.3.3 | | |
| 143 | 145 | |
| 144 | 146 | Each of these subprojects provides a `Parser` object (an instance of |
| 145 | 147 | `SupportParser[J]`) that is parameterized on the given project's |
| 164 | 166 | ```scala |
| 165 | 167 | resolvers += Resolver.sonatypeRepo("releases") |
| 166 | 168 | |
| 167 | libraryDependencies += "org.spire-math" %% jawn-"XYZ" % "0.10.3" | |
| 169 | libraryDependencies += "org.spire-math" %% jawn-"XYZ" % "0.11.0" | |
| 168 | 170 | ``` |
| 169 | 171 | |
| 170 | 172 | This is an example of how you might use the parser into your code: |
| 188 | 190 | ```scala |
| 189 | 191 | resolvers += Resolver.sonatypeRepo("releases") |
| 190 | 192 | |
| 191 | libraryDependencies += "org.spire-math" %% "jawn-parser" % "0.10.3" | |
| 193 | libraryDependencies += "org.spire-math" %% "jawn-parser" % "0.11.0" | |
| 192 | 194 | ``` |
| 193 | 195 | |
| 194 | 196 | To support your AST of choice, you'll want to define a `Facade[J]` |
| 421 | 423 | All code is available to you under the MIT license, available at |
| 422 | 424 | http://opensource.org/licenses/mit-license.php. |
| 423 | 425 | |
| 424 | Copyright Erik Osheim, 2012-2016. | |
| 426 | Copyright Erik Osheim, 2012-2017. | |
| 14 | 14 | def parseFromString(s: String): Try[JValue] = |
| 15 | 15 | Try(new StringParser[JValue](s).parse) |
| 16 | 16 | |
| 17 | def parseFromCharSequence(cs: CharSequence): Try[JValue] = | |
| 18 | Try(new CharSequenceParser[JValue](cs).parse) | |
| 19 | ||
| 17 | 20 | def parseFromPath(path: String): Try[JValue] = |
| 18 | 21 | parseFromFile(new File(path)) |
| 19 | 22 |
| 203 | 203 | |
| 204 | 204 | case class DeferLong(s: String) extends JNum { |
| 205 | 205 | |
| 206 | lazy val n: Long = java.lang.Long.parseLong(s) | |
| 206 | lazy val n: Long = util.parseLongUnsafe(s) | |
| 207 | 207 | |
| 208 | 208 | final override def getInt: Option[Int] = Some(n.toInt) |
| 209 | 209 | final override def getLong: Option[Long] = Some(n) |
| 234 | 234 | lazy val n: Double = java.lang.Double.parseDouble(s) |
| 235 | 235 | |
| 236 | 236 | final override def getInt: Option[Int] = Some(n.toInt) |
| 237 | final override def getLong: Option[Long] = Some(n.toLong) | |
| 237 | final override def getLong: Option[Long] = Some(util.parseLongUnsafe(s)) | |
| 238 | 238 | final override def getDouble: Option[Double] = Some(n) |
| 239 | 239 | final override def getBigInt: Option[BigInt] = Some(BigDecimal(s).toBigInt) |
| 240 | 240 | final override def getBigDecimal: Option[BigDecimal] = Some(BigDecimal(s)) |
| 241 | 241 | |
| 242 | 242 | final override def asInt: Int = n.toInt |
| 243 | final override def asLong: Long = n.toLong | |
| 243 | final override def asLong: Long = util.parseLongUnsafe(s) | |
| 244 | 244 | final override def asDouble: Double = n |
| 245 | 245 | final override def asBigInt: BigInt = BigDecimal(s).toBigInt |
| 246 | 246 | final override def asBigDecimal: BigDecimal = BigDecimal(s) |
| 7 | 7 | final val jnull = JNull |
| 8 | 8 | final val jfalse = JFalse |
| 9 | 9 | final val jtrue = JTrue |
| 10 | final def jnum(s: String) = DeferNum(s) | |
| 11 | final def jint(s: String) = DeferLong(s) | |
| 12 | final def jstring(s: String) = JString(s) | |
| 10 | ||
| 11 | final def jnum(s: CharSequence, decIndex: Int, expIndex: Int): JValue = | |
| 12 | if (decIndex == -1 && expIndex == -1) { | |
| 13 | DeferLong(s.toString) | |
| 14 | } else { | |
| 15 | DeferNum(s.toString) | |
| 16 | } | |
| 17 | ||
| 18 | final def jstring(s: CharSequence): JValue = | |
| 19 | JString(s.toString) | |
| 13 | 20 | |
| 14 | 21 | final def singleContext(): FContext[JValue] = |
| 15 | 22 | new FContext[JValue] { |
| 16 | 23 | var value: JValue = _ |
| 17 | def add(s: String) { value = JString(s) } | |
| 24 | def add(s: CharSequence) { value = JString(s.toString) } | |
| 18 | 25 | def add(v: JValue) { value = v } |
| 19 | 26 | def finish: JValue = value |
| 20 | 27 | def isObj: Boolean = false |
| 23 | 30 | final def arrayContext(): FContext[JValue] = |
| 24 | 31 | new FContext[JValue] { |
| 25 | 32 | val vs = mutable.ArrayBuffer.empty[JValue] |
| 26 | def add(s: String) { vs.append(JString(s)) } | |
| 33 | def add(s: CharSequence) { vs.append(JString(s.toString)) } | |
| 27 | 34 | def add(v: JValue) { vs.append(v) } |
| 28 | 35 | def finish: JValue = JArray(vs.toArray) |
| 29 | 36 | def isObj: Boolean = false |
| 33 | 40 | new FContext[JValue] { |
| 34 | 41 | var key: String = null |
| 35 | 42 | val vs = mutable.Map.empty[String, JValue] |
| 36 | def add(s: String): Unit = | |
| 37 | if (key == null) { key = s } else { vs(key) = JString(s); key = null } | |
| 43 | def add(s: CharSequence): Unit = | |
| 44 | if (key == null) { key = s.toString } else { vs(key.toString) = JString(s.toString); key = null } | |
| 38 | 45 | def add(v: JValue): Unit = |
| 39 | 46 | { vs(key) = v; key = null } |
| 40 | 47 | def finish = JObject(vs) |
| 9 | 9 | |
| 10 | 10 | import scala.collection.mutable |
| 11 | 11 | import scala.util.{Try, Success} |
| 12 | ||
| 13 | import jawn.parser.TestUtil | |
| 12 | 14 | |
| 13 | 15 | import ArbitraryUtil._ |
| 14 | 16 | |
| 32 | 34 | value1 shouldBe value2 |
| 33 | 35 | value1.## shouldBe value2.## |
| 34 | 36 | |
| 35 | parser.Util.withTemp(json1) { t => | |
| 37 | TestUtil.withTemp(json1) { t => | |
| 36 | 38 | JParser.parseFromFile(t).get shouldBe value2 |
| 37 | 39 | } |
| 38 | 40 | } |
| 47 | 49 | jstr2 shouldBe jstr1 |
| 48 | 50 | json2 shouldBe json1 |
| 49 | 51 | json2.## shouldBe json1.## |
| 52 | } | |
| 53 | } | |
| 54 | ||
| 55 | property("string/charSequence parsing") { | |
| 56 | forAll { value: JValue => | |
| 57 | val s = CanonicalRenderer.render(value) | |
| 58 | val j1 = JParser.parseFromString(s) | |
| 59 | val cs = java.nio.CharBuffer.wrap(s.toCharArray) | |
| 60 | val j2 = JParser.parseFromCharSequence(cs) | |
| 61 | j1 shouldBe j2 | |
| 62 | j1.## shouldBe j2.## | |
| 50 | 63 | } |
| 51 | 64 | } |
| 52 | 65 | |
| 75 | 88 | } ++ checkRight(p.finish()) |
| 76 | 89 | |
| 77 | 90 | import AsyncParser.{UnwrapArray, ValueStream, SingleValue} |
| 91 | ||
| 92 | property("async multi") { | |
| 93 | val data = "[1,2,3][4,5,6]" | |
| 94 | val p = AsyncParser[JValue](ValueStream) | |
| 95 | val res0 = p.absorb(data) | |
| 96 | val res1 = p.finish | |
| 97 | //println((res0, res1)) | |
| 98 | true | |
| 99 | } | |
| 78 | 100 | |
| 79 | 101 | property("async parsing") { |
| 80 | 102 | forAll { (v: JValue) => |
| 133 | 155 | |
| 134 | 156 | val s0 = ("x" * (40 * M)) |
| 135 | 157 | val e0 = q + s0 + q |
| 136 | parser.Util.withTemp(e0) { t => | |
| 158 | TestUtil.withTemp(e0) { t => | |
| 137 | 159 | JParser.parseFromFile(t).filter(_ == JString(s0)).isSuccess shouldBe true |
| 138 | 160 | } |
| 139 | 161 | |
| 140 | 162 | val s1 = "\\" * (20 * M) |
| 141 | 163 | val e1 = q + s1 + s1 + q |
| 142 | parser.Util.withTemp(e1) { t => | |
| 164 | TestUtil.withTemp(e1) { t => | |
| 143 | 165 | JParser.parseFromFile(t).filter(_ == JString(s1)).isSuccess shouldBe true |
| 144 | 166 | } |
| 145 | 167 | } |
| 2 | 2 | javaOptions in run += "-Xmx6G" |
| 3 | 3 | |
| 4 | 4 | libraryDependencies ++= Seq( |
| 5 | "io.argonaut" %% "argonaut" % "6.1-M6", | |
| 6 | "org.json4s" %% "json4s-native" % "3.2.11", | |
| 7 | "org.json4s" %% "json4s-jackson" % "3.2.10", | |
| 8 | "com.typesafe.play" %% "play-json" % "2.3.0", | |
| 5 | "io.argonaut" %% "argonaut" % "6.2", | |
| 6 | "org.json4s" %% "json4s-native" % "3.5.2", | |
| 7 | "org.json4s" %% "json4s-jackson" % "3.5.2", | |
| 8 | "com.typesafe.play" %% "play-json" % "2.5.15", | |
| 9 | 9 | "com.rojoma" %% "rojoma-json" % "2.4.3", |
| 10 | "com.rojoma" %% "rojoma-json-v3" % "3.3.0", | |
| 11 | "io.spray" %% "spray-json" % "1.3.2", | |
| 12 | "org.parboiled" %% "parboiled" % "2.1.0", | |
| 13 | "com.fasterxml.jackson.core" % "jackson-annotations" % "2.5.3", | |
| 14 | "com.fasterxml.jackson.core" % "jackson-core" % "2.5.3", | |
| 15 | "com.fasterxml.jackson.core" % "jackson-databind" % "2.5.3", | |
| 16 | "com.google.code.gson" % "gson" % "2.2.4" | |
| 10 | "com.rojoma" %% "rojoma-json-v3" % "3.7.2", | |
| 11 | "io.spray" %% "spray-json" % "1.3.3", | |
| 12 | "org.parboiled" %% "parboiled" % "2.1.4", | |
| 13 | "com.fasterxml.jackson.core" % "jackson-annotations" % "2.8.4", | |
| 14 | "com.fasterxml.jackson.core" % "jackson-core" % "2.8.4", | |
| 15 | "com.fasterxml.jackson.core" % "jackson-databind" % "2.8.4", | |
| 16 | "com.google.code.gson" % "gson" % "2.8.1" | |
| 17 | 17 | ) |
| 18 | 18 | |
| 19 | 19 | // enable forking in run |
| 9 | 9 | @BenchmarkMode(Array(Mode.AverageTime)) |
| 10 | 10 | @OutputTimeUnit(TimeUnit.MILLISECONDS) |
| 11 | 11 | abstract class JmhBenchmarks(name: String) { |
| 12 | ||
| 13 | 12 | val path: String = s"src/main/resources/$name" |
| 14 | 13 | |
| 15 | 14 | def load(path: String): String = { |
| 26 | 25 | def buffered(path: String): BufferedReader = |
| 27 | 26 | new BufferedReader(new FileReader(new File(path))) |
| 28 | 27 | |
| 28 | @Benchmark | |
| 29 | def jawnCheckSyntax() = | |
| 30 | jawn.Syntax.checkString(load(path)) | |
| 31 | ||
| 32 | @Benchmark | |
| 33 | def jawnParse() = | |
| 34 | jawn.ast.JParser.parseFromFile(new File(path)).get | |
| 35 | ||
| 36 | @Benchmark | |
| 37 | def jawnStringParse() = | |
| 38 | jawn.ast.JParser.parseFromString(load(path)).get | |
| 39 | } | |
| 40 | ||
| 41 | trait OtherBenchmarks { self: JmhBenchmarks => | |
| 29 | 42 | @Benchmark |
| 30 | 43 | def json4sJacksonParse() = { |
| 31 | 44 | import org.json4s._ |
| 64 | 77 | def gsonParse() = |
| 65 | 78 | new com.google.gson.JsonParser().parse(buffered(path)) |
| 66 | 79 | |
| 67 | @Benchmark | |
| 68 | def jawnCheckSyntax() = | |
| 69 | jawn.Syntax.checkString(load(path)) | |
| 70 | ||
| 71 | @Benchmark | |
| 72 | def jawnParse() = | |
| 73 | jawn.ast.JParser.parseFromFile(new File(path)).get | |
| 74 | ||
| 75 | @Benchmark | |
| 76 | def jawnStringParse() = | |
| 77 | jawn.ast.JParser.parseFromString(load(path)).get | |
| 78 | ||
| 79 | 80 | // don't bother benchmarking jawn + external asts by default |
| 80 | 81 | |
| 81 | 82 | // @Benchmark |
| 104 | 105 | // } |
| 105 | 106 | } |
| 106 | 107 | |
| 107 | class Qux2Bench extends JmhBenchmarks("qux2.json") | |
| 108 | class Bla25Bench extends JmhBenchmarks("bla25.json") | |
| 109 | class CountriesBench extends JmhBenchmarks("countries.geo.json") | |
| 110 | class Ugh10kBench extends JmhBenchmarks("ugh10k.json") | |
| 108 | class Qux2Bench extends JmhBenchmarks("qux2.json") with OtherBenchmarks | |
| 109 | class Bla25Bench extends JmhBenchmarks("bla25.json") with OtherBenchmarks | |
| 110 | class CountriesBench extends JmhBenchmarks("countries.geo.json") with OtherBenchmarks | |
| 111 | class Ugh10kBench extends JmhBenchmarks("ugh10k.json") with OtherBenchmarks | |
| 112 | ||
| 113 | class JawnOnlyQux2Bench extends JmhBenchmarks("qux2.json") | |
| 114 | class JawnOnlyBla25Bench extends JmhBenchmarks("bla25.json") | |
| 115 | class JawnOnlyCountriesBench extends JmhBenchmarks("countries.geo.json") | |
| 116 | class JawnOnlyUgh10kBench extends JmhBenchmarks("ugh10k.json") | |
| 111 | 117 | |
| 112 | 118 | // // from https://github.com/zemirco/sf-city-lots-json |
| 113 | 119 | // class CityLotsBench extends JmhBenchmarks("citylots.json") |
| 0 | package jawn | |
| 1 | package benchmark | |
| 2 | ||
| 3 | import java.io.{BufferedReader, File, FileInputStream, FileReader} | |
| 4 | import java.util.concurrent.TimeUnit | |
| 5 | import org.openjdk.jmh.annotations._ | |
| 6 | import scala.collection.mutable | |
| 7 | ||
| 8 | case class Slice(s: String, begin: Int, limit: Int) extends CharSequence { | |
| 9 | val length: Int = limit - begin | |
| 10 | def charAt(i: Int): Char = s.charAt(begin + i) | |
| 11 | def subSequence(start: Int, end: Int): Slice = | |
| 12 | Slice(s, begin + start, Math.min(end + begin, limit)) | |
| 13 | override def toString: String = | |
| 14 | s.substring(begin, limit) | |
| 15 | } | |
| 16 | ||
| 17 | @State(Scope.Benchmark) | |
| 18 | @OutputTimeUnit(TimeUnit.MILLISECONDS) | |
| 19 | class ParseLongBench { | |
| 20 | ||
| 21 | val longs: Array[Long] = Array( | |
| 22 | -1346837161442476189L, -4666345991836441070L, 4868830844043235709L, | |
| 23 | 2992690405064579158L, -2017521011608078634L, -3039682866169364757L, | |
| 24 | 8997687047891586260L, 5932727796276454607L, 4062739618560250554L, | |
| 25 | 8668950167358198490L, -8565613821858118870L, 8049785848575684314L, | |
| 26 | -580831266940599830L, -3593199367295538945L, 8374322595267797482L, | |
| 27 | 3088261552516619129L, -6879203747403593851L, -1842900848925949857L, | |
| 28 | 4484592876047641351L, 5182973278356955602L, -6840392853855436945L, | |
| 29 | -4176340556015032222L, -536379174926548619L, 6343722878919863216L, | |
| 30 | 1557757008211571405L, -334093799456298669L, 619602023052756397L, | |
| 31 | 6904874397154297343L, -4332034907782234995L, -8767842695446545180L, | |
| 32 | -6127250063205613011L, 6902212562850963795L, 4778607575334665692L, | |
| 33 | 7674074815344809639L, -3834944692798167050L, 7406081418831471202L, | |
| 34 | -9126886315356724563L, 8093878176633322645L, 2471547025788214028L, | |
| 35 | -5018828829942988155L, -6676531171364391367L, 8189793226936659851L, | |
| 36 | 7150026713387306746L, -6065566098373722052L, 3281133763697608570L, | |
| 37 | 957103694526079944L, -3009447279791131829L, -1995600795755716697L, | |
| 38 | 2361055030313262510L, -4312828282749171343L, 8836216125516165138L, | |
| 39 | 5548785979447786253L, 8567551485822958810L, 5931896003625723150L, | |
| 40 | 3472058092439106147L, 4363240277904515929L, -2999484068697753019L, | |
| 41 | -8285358702782547958L, -2407429647076308777L, 4411565001760018584L, | |
| 42 | 792384115860070648L, 3328145302561962294L, -2377559446421434356L, | |
| 43 | -7837698939558960516L, -565806101451282875L, -4792610084643070650L, | |
| 44 | 2713520205731589923L, -6521104721472605988L, 5037187811345411645L, | |
| 45 | 3866939564433764178L, -3851229228204678079L, -8171137274242372558L, | |
| 46 | -14713951794749384L, 2061783257002637655L, -7375571393873059570L, | |
| 47 | 7402007407273053723L, -5104318069025846447L, -8956415532448219980L, | |
| 48 | 4904595193891993401L, 5396360181536889307L, -8043917553767343384L, | |
| 49 | -3666269817017255250L, -6535587792359353103L, -4553034734642385706L, | |
| 50 | -7544140164897268962L, 2468330113904053484L, 5790319365381968237L, | |
| 51 | -2734383156062609640L, -4831208471935595172L, 4502079643250626043L, | |
| 52 | 4778622151522470246L, 7233054223498326990L, 5833883346008509644L, | |
| 53 | -8013495378054295093L, 2944606201054530456L, -8608231828651976245L, | |
| 54 | -6957117814546267426L, -4744827311133020624L, 2640030216500286789L, | |
| 55 | 8343959867315747844L) | |
| 56 | ||
| 57 | val strs: Array[CharSequence] = | |
| 58 | longs.map(_.toString) | |
| 59 | ||
| 60 | val seqs: Array[CharSequence] = | |
| 61 | longs.map { n => | |
| 62 | val prefix = "x" * (n & 63).toInt | |
| 63 | val suffix = "y" * ((n * 7) & 63).toInt | |
| 64 | val i = prefix.length | |
| 65 | val s = n.toString | |
| 66 | Slice(prefix + s + suffix, i, s.length + i) | |
| 67 | } | |
| 68 | ||
| 69 | val str: CharSequence = "23948271429443" | |
| 70 | ||
| 71 | val seq: CharSequence = Slice("weigjewigjwi23948271429443jgewigjweiwjegiwgjiewjgeiwjg", 12, 26) | |
| 72 | ||
| 73 | def sumJava(css: Array[CharSequence]): Long = { | |
| 74 | var sum: Long = 0 | |
| 75 | var i = 0 | |
| 76 | while (i < css.length) { | |
| 77 | sum += java.lang.Long.parseLong(css(i).toString) | |
| 78 | i += 1 | |
| 79 | } | |
| 80 | sum | |
| 81 | } | |
| 82 | ||
| 83 | def sumStd(css: Array[CharSequence]): Long = { | |
| 84 | var sum: Long = 0 | |
| 85 | var i = 0 | |
| 86 | while (i < css.length) { | |
| 87 | sum += css(i).toString.toLong | |
| 88 | i += 1 | |
| 89 | } | |
| 90 | sum | |
| 91 | } | |
| 92 | ||
| 93 | def sumSafe(css: Array[CharSequence]): Long = { | |
| 94 | var sum: Long = 0 | |
| 95 | var i = 0 | |
| 96 | while (i < css.length) { | |
| 97 | sum += Util.parseLong(css(i)) | |
| 98 | i += 1 | |
| 99 | } | |
| 100 | sum | |
| 101 | } | |
| 102 | ||
| 103 | def sumUnsafe(css: Array[CharSequence]): Long = { | |
| 104 | var sum: Long = 0 | |
| 105 | var i = 0 | |
| 106 | while (i < css.length) { | |
| 107 | sum += Util.parseLongUnsafe(css(i)) | |
| 108 | i += 1 | |
| 109 | } | |
| 110 | sum | |
| 111 | } | |
| 112 | ||
| 113 | @Benchmark def stringArrayJava(): Long = sumJava(strs) | |
| 114 | @Benchmark def seqArrayJava(): Long = sumJava(seqs) | |
| 115 | @Benchmark def stringValueJava(): Long = java.lang.Long.parseLong(str.toString) | |
| 116 | @Benchmark def seqValueJava(): Long = java.lang.Long.parseLong(seq.toString) | |
| 117 | ||
| 118 | @Benchmark def stringArrayStd(): Long = sumStd(strs) | |
| 119 | @Benchmark def seqArrayStd(): Long = sumStd(seqs) | |
| 120 | @Benchmark def stringValueStd(): Long = str.toString.toLong | |
| 121 | @Benchmark def seqValueStd(): Long = seq.toString.toLong | |
| 122 | ||
| 123 | @Benchmark def stringArraySafe(): Long = sumSafe(strs) | |
| 124 | @Benchmark def seqArraySafe(): Long = sumSafe(seqs) | |
| 125 | @Benchmark def stringValueSafe(): Long = Util.parseLong(str) | |
| 126 | @Benchmark def seqValueSafe(): Long = Util.parseLong(seq) | |
| 127 | ||
| 128 | @Benchmark def stringArrayUnsafe(): Long = sumUnsafe(strs) | |
| 129 | @Benchmark def seqArrayUnsafe(): Long = sumUnsafe(seqs) | |
| 130 | @Benchmark def stringValueUnsafe(): Long = Util.parseLongUnsafe(str) | |
| 131 | @Benchmark def seqValueUnsafe(): Long = Util.parseLongUnsafe(seq) | |
| 132 | } |
| 0 | 0 | import ReleaseTransformations._ |
| 1 | ||
| 2 | lazy val previousJawnVersion = "0.10.4" | |
| 3 | ||
| 4 | lazy val stableCrossVersions = | |
| 5 | Seq("2.10.6", "2.11.11", "2.12.2") | |
| 6 | ||
| 7 | // we'll support 2.13.0-M1 soon but not yet | |
| 8 | lazy val allCrossVersions = | |
| 9 | stableCrossVersions | |
| 10 | ||
| 11 | lazy val benchmarkVersion = | |
| 12 | "2.12.2" | |
| 1 | 13 | |
| 2 | 14 | lazy val jawnSettings = Seq( |
| 3 | 15 | organization := "org.spire-math", |
| 4 | scalaVersion := "2.11.8", | |
| 5 | crossScalaVersions := Seq("2.10.6", "2.11.8", "2.12.0"), | |
| 16 | scalaVersion := "2.12.2", | |
| 17 | crossScalaVersions := allCrossVersions, | |
| 18 | ||
| 19 | mimaPreviousArtifacts := Set(organization.value %% moduleName.value % previousJawnVersion), | |
| 6 | 20 | |
| 7 | 21 | resolvers += Resolver.sonatypeRepo("releases"), |
| 8 | libraryDependencies ++= Seq( | |
| 9 | "org.scalatest" %% "scalatest" % "3.0.0" % "test", | |
| 10 | "org.scalacheck" %% "scalacheck" % "1.13.4" % "test" | |
| 11 | ), | |
| 12 | scalacOptions ++= Seq( | |
| 13 | //"-Yinline-warnings", | |
| 14 | "-deprecation", | |
| 15 | "-optimize", | |
| 16 | "-unchecked" | |
| 17 | ), | |
| 22 | ||
| 23 | libraryDependencies ++= | |
| 24 | "org.scalatest" %% "scalatest" % "3.0.3" % Test :: | |
| 25 | "org.scalacheck" %% "scalacheck" % "1.13.5" % Test :: | |
| 26 | Nil, | |
| 27 | ||
| 28 | scalacOptions ++= | |
| 29 | "-deprecation" :: | |
| 30 | "-optimize" :: | |
| 31 | "-unchecked" :: | |
| 32 | Nil, | |
| 18 | 33 | |
| 19 | 34 | licenses += ("MIT", url("http://opensource.org/licenses/MIT")), |
| 20 | 35 | homepage := Some(url("http://github.com/non/jawn")), |
| 25 | 40 | publishArtifact in Test := false, |
| 26 | 41 | pomIncludeRepository := Function.const(false), |
| 27 | 42 | |
| 28 | publishTo <<= (version).apply { v => | |
| 43 | publishTo := { | |
| 29 | 44 | val nexus = "https://oss.sonatype.org/" |
| 30 | if (v.trim.endsWith("SNAPSHOT")) | |
| 45 | if (isSnapshot.value) { | |
| 31 | 46 | Some("Snapshots" at nexus + "content/repositories/snapshots") |
| 32 | else | |
| 47 | } else { | |
| 33 | 48 | Some("Releases" at nexus + "service/local/staging/deploy/maven2") |
| 49 | } | |
| 34 | 50 | }, |
| 35 | 51 | |
| 36 | pomExtra := ( | |
| 37 | <scm> | |
| 38 | <url>git@github.com:non/jawn.git</url> | |
| 39 | <connection>scm:git:git@github.com:non/jawn.git</connection> | |
| 40 | </scm> | |
| 41 | <developers> | |
| 42 | <developer> | |
| 43 | <id>d_m</id> | |
| 44 | <name>Erik Osheim</name> | |
| 45 | <url>http://github.com/non/</url> | |
| 46 | </developer> | |
| 47 | </developers> | |
| 52 | scmInfo := Some(ScmInfo( | |
| 53 | browseUrl = url("https://github.com/non/jawn"), | |
| 54 | connection = "scm:git:git@github.com:non/jawn.git" | |
| 55 | )), | |
| 56 | ||
| 57 | developers += Developer( | |
| 58 | name = "Erik Osheim", | |
| 59 | email = "erik@plastic-idolatry.com", | |
| 60 | id = "d_m", | |
| 61 | url = url("http://github.com/non/") | |
| 48 | 62 | ), |
| 49 | 63 | |
| 50 | 64 | releaseProcess := Seq[ReleaseStep]( |
| 64 | 78 | lazy val noPublish = Seq( |
| 65 | 79 | publish := {}, |
| 66 | 80 | publishLocal := {}, |
| 67 | publishArtifact := false) | |
| 81 | publishArtifact := false, | |
| 82 | mimaPreviousArtifacts := Set()) | |
| 68 | 83 | |
| 69 | 84 | lazy val root = project.in(file(".")) |
| 70 | 85 | .aggregate(all.map(Project.projectToRef): _*) |
| 80 | 95 | .settings(jawnSettings: _*) |
| 81 | 96 | .disablePlugins(JmhPlugin) |
| 82 | 97 | |
| 98 | lazy val util = project.in(file("util")) | |
| 99 | .dependsOn(parser % "compile->compile;test->test") | |
| 100 | .settings(name := "util") | |
| 101 | .settings(moduleName := "jawn-util") | |
| 102 | .settings(jawnSettings: _*) | |
| 103 | .disablePlugins(JmhPlugin) | |
| 104 | ||
| 83 | 105 | lazy val ast = project.in(file("ast")) |
| 84 | 106 | .dependsOn(parser % "compile->compile;test->test") |
| 107 | .dependsOn(util % "compile->compile;test->test") | |
| 85 | 108 | .settings(name := "ast") |
| 86 | 109 | .settings(moduleName := "jawn-ast") |
| 87 | 110 | .settings(jawnSettings: _*) |
| 96 | 119 | .disablePlugins(JmhPlugin) |
| 97 | 120 | |
| 98 | 121 | lazy val supportArgonaut = support("argonaut") |
| 99 | .settings(crossScalaVersions := Seq("2.10.6", "2.11.8")) | |
| 100 | .settings(libraryDependencies += "io.argonaut" %% "argonaut" % "6.1") | |
| 122 | .settings(crossScalaVersions := stableCrossVersions) | |
| 123 | .settings(libraryDependencies += "io.argonaut" %% "argonaut" % "6.2") | |
| 101 | 124 | |
| 102 | 125 | lazy val supportJson4s = support("json4s") |
| 103 | .settings(libraryDependencies += "org.json4s" %% "json4s-ast" % "3.5.0") | |
| 126 | .dependsOn(util) | |
| 127 | .settings(crossScalaVersions := stableCrossVersions) | |
| 128 | .settings(libraryDependencies += "org.json4s" %% "json4s-ast" % "3.5.2") | |
| 104 | 129 | |
| 105 | 130 | lazy val supportPlay = support("play") |
| 106 | .settings(crossScalaVersions := Seq("2.10.6", "2.11.8")) | |
| 131 | .settings(crossScalaVersions := stableCrossVersions) | |
| 107 | 132 | .settings(libraryDependencies += (scalaBinaryVersion.value match { |
| 108 | case "2.10" => "com.typesafe.play" %% "play-json" % "2.4.8" | |
| 109 | case _ => "com.typesafe.play" %% "play-json" % "2.5.9" | |
| 133 | case "2.10" => "com.typesafe.play" %% "play-json" % "2.4.11" | |
| 134 | case "2.11" => "com.typesafe.play" %% "play-json" % "2.5.15" | |
| 135 | case _ => "com.typesafe.play" %% "play-json" % "2.6.0" | |
| 110 | 136 | })) |
| 111 | 137 | |
| 112 | 138 | lazy val supportRojoma = support("rojoma") |
| 113 | .settings(crossScalaVersions := Seq("2.10.6", "2.11.8", "2.12.0")) | |
| 139 | .settings(crossScalaVersions := stableCrossVersions) | |
| 114 | 140 | .settings(libraryDependencies += "com.rojoma" %% "rojoma-json" % "2.4.3") |
| 115 | 141 | |
| 116 | 142 | lazy val supportRojomaV3 = support("rojoma-v3") |
| 117 | .settings(libraryDependencies += "com.rojoma" %% "rojoma-json-v3" % "3.7.0") | |
| 143 | .settings(crossScalaVersions := stableCrossVersions) | |
| 144 | .settings(libraryDependencies += "com.rojoma" %% "rojoma-json-v3" % "3.7.2") | |
| 118 | 145 | |
| 119 | 146 | lazy val supportSpray = support("spray") |
| 147 | .settings(crossScalaVersions := stableCrossVersions) | |
| 120 | 148 | .settings(resolvers += "spray" at "http://repo.spray.io/") |
| 121 | .settings(libraryDependencies += "io.spray" %% "spray-json" % "1.3.2") | |
| 149 | .settings(libraryDependencies += "io.spray" %% "spray-json" % "1.3.3") | |
| 122 | 150 | |
| 123 | 151 | lazy val benchmark = project.in(file("benchmark")) |
| 124 | 152 | .dependsOn(all.map(Project.classpathDependency[Project]): _*) |
| 125 | 153 | .settings(name := "jawn-benchmark") |
| 126 | 154 | .settings(jawnSettings: _*) |
| 127 | .settings(scalaVersion := "2.11.8") | |
| 155 | .settings(scalaVersion := benchmarkVersion) | |
| 156 | .settings(crossScalaVersions := Seq(benchmarkVersion)) | |
| 128 | 157 | .settings(noPublish: _*) |
| 129 | .settings(crossScalaVersions := Seq("2.11.8")) | |
| 130 | 158 | .enablePlugins(JmhPlugin) |
| 131 | 159 | |
| 132 | 160 | lazy val all = |
| 133 | Seq(parser, ast, supportArgonaut, supportJson4s, supportPlay, supportRojoma, supportRojomaV3, supportSpray) | |
| 161 | Seq(parser, util, ast, supportArgonaut, supportJson4s, supportPlay, supportRojoma, supportRojomaV3, supportSpray) | |
| 242 | 242 | } |
| 243 | 243 | } |
| 244 | 244 | |
| 245 | // every 1M we shift our array back by 1M. | |
| 245 | // every 1M we shift our array back to the beginning. | |
| 246 | 246 | protected[this] final def reset(i: Int): Int = { |
| 247 | 247 | if (offset >= 1048576) { |
| 248 | len -= 1048576 | |
| 249 | offset -= 1048576 | |
| 250 | pos -= 1048576 | |
| 251 | System.arraycopy(data, 1048576, data, 0, len) | |
| 252 | i - 1048576 | |
| 248 | val diff = offset | |
| 249 | curr -= diff | |
| 250 | len -= diff | |
| 251 | offset = 0 | |
| 252 | pos -= diff | |
| 253 | System.arraycopy(data, diff, data, 0, len) | |
| 254 | i - diff | |
| 253 | 255 | } else { |
| 254 | 256 | i |
| 255 | 257 | } |
| 288 | 290 | * boundaries. Also, the resulting String is not guaranteed to have length |
| 289 | 291 | * (k - i). |
| 290 | 292 | */ |
| 291 | protected[this] final def at(i: Int, k: Int): String = { | |
| 293 | protected[this] final def at(i: Int, k: Int): CharSequence = { | |
| 292 | 294 | if (k > len) throw new AsyncException |
| 293 | 295 | val size = k - i |
| 294 | 296 | val arr = new Array[Byte](size) |
| 13 | 13 | * update its own mutable position fields. |
| 14 | 14 | */ |
| 15 | 15 | final class ByteBufferParser[J](src: ByteBuffer) extends SyncParser[J] with ByteBasedParser[J] { |
| 16 | final val start = src.position | |
| 17 | final val limit = src.limit - start | |
| 16 | private[this] final val start = src.position | |
| 17 | private[this] final val limit = src.limit - start | |
| 18 | 18 | |
| 19 | var line = 0 | |
| 20 | protected[this] final def newline(i: Int) { line += 1 } | |
| 19 | private[this] var lineState = 0 | |
| 20 | protected[this] def line(): Int = lineState | |
| 21 | ||
| 22 | protected[this] final def newline(i: Int) { lineState += 1 } | |
| 21 | 23 | protected[this] final def column(i: Int) = i |
| 22 | 24 | |
| 23 | 25 | protected[this] final def close() { src.position(src.limit) } |
| 26 | 28 | protected[this] final def byte(i: Int): Byte = src.get(i + start) |
| 27 | 29 | protected[this] final def at(i: Int): Char = src.get(i + start).toChar |
| 28 | 30 | |
| 29 | protected[this] final def at(i: Int, k: Int): String = { | |
| 31 | protected[this] final def at(i: Int, k: Int): CharSequence = { | |
| 30 | 32 | val len = k - i |
| 31 | 33 | val arr = new Array[Byte](len) |
| 32 | 34 | src.position(i + start) |
| 139 | 139 | * on unicode boundaries. Also, the resulting String is not |
| 140 | 140 | * guaranteed to have length (k - i). |
| 141 | 141 | */ |
| 142 | protected[this] final def at(i: Int, k: Int): String = { | |
| 142 | protected[this] final def at(i: Int, k: Int): CharSequence = { | |
| 143 | 143 | val len = k - i |
| 144 | 144 | if (k > Allsize) { |
| 145 | 145 | grow() |
| 10 | 10 | * |
| 11 | 11 | * It is simpler than ByteBasedParser. |
| 12 | 12 | */ |
| 13 | private[jawn] trait CharBasedParser[J] extends Parser[J] { | |
| 13 | trait CharBasedParser[J] extends Parser[J] { | |
| 14 | ||
| 15 | private[this] final val charBuilder = new CharBuilder() | |
| 14 | 16 | |
| 15 | 17 | /** |
| 16 | 18 | * See if the string has any escape sequences. If not, return the |
| 33 | 33 | } |
| 34 | 34 | } |
| 35 | 35 | |
| 36 | def extend(s: String): Unit = { | |
| 36 | def extend(s: CharSequence): Unit = { | |
| 37 | 37 | val tlen = len + s.length |
| 38 | 38 | resizeIfNecessary(tlen) |
| 39 | 39 | var i = 0 |
| 0 | package jawn | |
| 1 | ||
| 2 | /** | |
| 3 | * Lazy character sequence parsing. | |
| 4 | * | |
| 5 | * This is similar to StringParser, but acts on character sequences. | |
| 6 | */ | |
| 7 | private[jawn] final class CharSequenceParser[J](cs: CharSequence) extends SyncParser[J] with CharBasedParser[J] { | |
| 8 | var line = 0 | |
| 9 | final def column(i: Int) = i | |
| 10 | final def newline(i: Int) { line += 1 } | |
| 11 | final def reset(i: Int): Int = i | |
| 12 | final def checkpoint(state: Int, i: Int, stack: List[FContext[J]]): Unit = () | |
| 13 | final def at(i: Int): Char = cs.charAt(i) | |
| 14 | final def at(i: Int, j: Int): CharSequence = cs.subSequence(i, j) | |
| 15 | final def atEof(i: Int) = i == cs.length | |
| 16 | final def close() = () | |
| 17 | } |
| 2 | 2 | /** |
| 3 | 3 | * Facade is a type class that describes how Jawn should construct |
| 4 | 4 | * JSON AST elements of type J. |
| 5 | * | |
| 5 | * | |
| 6 | 6 | * Facade[J] also uses FContext[J] instances, so implementors will |
| 7 | 7 | * usually want to define both. |
| 8 | 8 | */ |
| 14 | 14 | def jnull(): J |
| 15 | 15 | def jfalse(): J |
| 16 | 16 | def jtrue(): J |
| 17 | def jnum(s: String): J | |
| 18 | def jint(s: String): J | |
| 19 | def jstring(s: String): J | |
| 17 | def jnum(s: CharSequence, decIndex: Int, expIndex: Int): J | |
| 18 | def jstring(s: CharSequence): J | |
| 20 | 19 | } |
| 21 | 20 | |
| 22 | 21 | /** |
| 27 | 26 | * cases where the entire JSON document consists of "333.33". |
| 28 | 27 | */ |
| 29 | 28 | trait FContext[J] { |
| 30 | def add(s: String): Unit | |
| 29 | def add(s: CharSequence): Unit | |
| 31 | 30 | def add(v: J): Unit |
| 32 | 31 | def finish: J |
| 33 | 32 | def isObj: Boolean |
| 7 | 7 | |
| 8 | 8 | def singleContext() = new FContext[J] { |
| 9 | 9 | var value: J = _ |
| 10 | def add(s: String) { value = jstring(s) } | |
| 10 | def add(s: CharSequence) { value = jstring(s) } | |
| 11 | 11 | def add(v: J) { value = v } |
| 12 | 12 | def finish: J = value |
| 13 | 13 | def isObj: Boolean = false |
| 15 | 15 | |
| 16 | 16 | def arrayContext() = new FContext[J] { |
| 17 | 17 | val vs = mutable.ArrayBuffer.empty[J] |
| 18 | def add(s: String) { vs.append(jstring(s)) } | |
| 18 | def add(s: CharSequence) { vs.append(jstring(s)) } | |
| 19 | 19 | def add(v: J) { vs.append(v) } |
| 20 | 20 | def finish: J = jarray(vs) |
| 21 | 21 | def isObj: Boolean = false |
| 24 | 24 | def objectContext() = new FContext[J] { |
| 25 | 25 | var key: String = null |
| 26 | 26 | val vs = mutable.Map.empty[String, J] |
| 27 | def add(s: String): Unit = | |
| 28 | if (key == null) { key = s } else { vs(key) = jstring(s); key = null } | |
| 27 | def add(s: CharSequence): Unit = | |
| 28 | if (key == null) { key = s.toString } else { vs(key) = jstring(s); key = null } | |
| 29 | 29 | def add(v: J): Unit = |
| 30 | 30 | { vs(key) = v; key = null } |
| 31 | 31 | def finish = jobject(vs) |
| 12 | 12 | object NullFacade extends Facade[Unit] { |
| 13 | 13 | |
| 14 | 14 | case class NullContext(isObj: Boolean) extends FContext[Unit] { |
| 15 | def add(s: String): Unit = () | |
| 15 | def add(s: CharSequence): Unit = () | |
| 16 | 16 | def add(v: Unit): Unit = () |
| 17 | 17 | def finish: Unit = () |
| 18 | 18 | } |
| 24 | 24 | def jnull(): Unit = () |
| 25 | 25 | def jfalse(): Unit = () |
| 26 | 26 | def jtrue(): Unit = () |
| 27 | def jnum(s: String): Unit = () | |
| 28 | def jint(s: String): Unit = () | |
| 29 | def jstring(s: String): Unit = () | |
| 27 | def jnum(s: CharSequence, decIndex: Int, expIndex: Int): Unit = () | |
| 28 | def jstring(s: CharSequence): Unit = () | |
| 30 | 29 | } |
| 34 | 34 | |
| 35 | 35 | protected[this] final val utf8 = Charset.forName("UTF-8") |
| 36 | 36 | |
| 37 | protected[this] final val charBuilder = new CharBuilder() | |
| 38 | ||
| 39 | 37 | /** |
| 40 | 38 | * Read the byte/char at 'i' as a Char. |
| 41 | 39 | * |
| 47 | 45 | /** |
| 48 | 46 | * Read the bytes/chars from 'i' until 'j' as a String. |
| 49 | 47 | */ |
| 50 | protected[this] def at(i: Int, j: Int): String | |
| 48 | protected[this] def at(i: Int, j: Int): CharSequence | |
| 51 | 49 | |
| 52 | 50 | /** |
| 53 | 51 | * Return true iff 'i' is at or beyond the end of the input (EOF). |
| 54 | 52 | */ |
| 55 | 53 | protected[this] def atEof(i: Int): Boolean |
| 56 | ||
| 57 | /** | |
| 58 | * Return true iff the byte/char at 'i' is equal to 'c'. | |
| 59 | */ | |
| 60 | protected[this] final def is(i: Int, c: Char): Boolean = at(i) == c | |
| 61 | ||
| 62 | /** | |
| 63 | * Return true iff the bytes/chars from 'i' until 'j' are equal to 'str'. | |
| 64 | */ | |
| 65 | protected[this] final def is(i: Int, j: Int, str: String): Boolean = at(i, j) == str | |
| 66 | 54 | |
| 67 | 55 | /** |
| 68 | 56 | * The reset() method is used to signal that we're working from the |
| 140 | 128 | protected[this] final def parseNum(i: Int, ctxt: FContext[J])(implicit facade: Facade[J]): Int = { |
| 141 | 129 | var j = i |
| 142 | 130 | var c = at(j) |
| 143 | var dec = false | |
| 131 | var decIndex = -1 | |
| 132 | var expIndex = -1 | |
| 144 | 133 | |
| 145 | 134 | if (c == '-') { |
| 146 | 135 | j += 1 |
| 156 | 145 | } |
| 157 | 146 | |
| 158 | 147 | if (c == '.') { |
| 159 | dec = true | |
| 148 | decIndex = j - i | |
| 160 | 149 | j += 1 |
| 161 | 150 | c = at(j) |
| 162 | 151 | if ('0' <= c && c <= '9') { |
| 167 | 156 | } |
| 168 | 157 | |
| 169 | 158 | if (c == 'e' || c == 'E') { |
| 170 | dec = true | |
| 159 | expIndex = j - i | |
| 171 | 160 | j += 1 |
| 172 | 161 | c = at(j) |
| 173 | 162 | if (c == '+' || c == '-') { |
| 181 | 170 | } |
| 182 | 171 | } |
| 183 | 172 | |
| 184 | if (dec) | |
| 185 | ctxt.add(facade.jnum(at(i, j))) | |
| 186 | else | |
| 187 | ctxt.add(facade.jint(at(i, j))) | |
| 173 | ctxt.add(facade.jnum(at(i, j), decIndex, expIndex)) | |
| 188 | 174 | j |
| 189 | 175 | } |
| 190 | 176 | |
| 205 | 191 | protected[this] final def parseNumSlow(i: Int, ctxt: FContext[J])(implicit facade: Facade[J]): Int = { |
| 206 | 192 | var j = i |
| 207 | 193 | var c = at(j) |
| 208 | var dec = false | |
| 194 | var decIndex = -1 | |
| 195 | var expIndex = -1 | |
| 209 | 196 | |
| 210 | 197 | if (c == '-') { |
| 211 | 198 | // any valid input will require at least one digit after - |
| 215 | 202 | if (c == '0') { |
| 216 | 203 | j += 1 |
| 217 | 204 | if (atEof(j)) { |
| 218 | ctxt.add(facade.jint(at(i, j))) | |
| 205 | ctxt.add(facade.jnum(at(i, j), decIndex, expIndex)) | |
| 219 | 206 | return j |
| 220 | 207 | } |
| 221 | 208 | c = at(j) |
| 223 | 210 | while ('0' <= c && c <= '9') { |
| 224 | 211 | j += 1 |
| 225 | 212 | if (atEof(j)) { |
| 226 | ctxt.add(facade.jint(at(i, j))) | |
| 213 | ctxt.add(facade.jnum(at(i, j), decIndex, expIndex)) | |
| 227 | 214 | return j |
| 228 | 215 | } |
| 229 | 216 | c = at(j) |
| 234 | 221 | |
| 235 | 222 | if (c == '.') { |
| 236 | 223 | // any valid input will require at least one digit after . |
| 237 | dec = true | |
| 224 | decIndex = j - i | |
| 238 | 225 | j += 1 |
| 239 | 226 | c = at(j) |
| 240 | 227 | if ('0' <= c && c <= '9') { |
| 241 | 228 | while ('0' <= c && c <= '9') { |
| 242 | 229 | j += 1 |
| 243 | 230 | if (atEof(j)) { |
| 244 | ctxt.add(facade.jnum(at(i, j))) | |
| 231 | ctxt.add(facade.jnum(at(i, j), decIndex, expIndex)) | |
| 245 | 232 | return j |
| 246 | 233 | } |
| 247 | 234 | c = at(j) |
| 253 | 240 | |
| 254 | 241 | if (c == 'e' || c == 'E') { |
| 255 | 242 | // any valid input will require at least one digit after e, e+, etc |
| 256 | dec = true | |
| 243 | expIndex = j - i | |
| 257 | 244 | j += 1 |
| 258 | 245 | c = at(j) |
| 259 | 246 | if (c == '+' || c == '-') { |
| 264 | 251 | while ('0' <= c && c <= '9') { |
| 265 | 252 | j += 1 |
| 266 | 253 | if (atEof(j)) { |
| 267 | ctxt.add(facade.jnum(at(i, j))) | |
| 254 | ctxt.add(facade.jnum(at(i, j), decIndex, expIndex)) | |
| 268 | 255 | return j |
| 269 | 256 | } |
| 270 | 257 | c = at(j) |
| 273 | 260 | die(i, "expected digit") |
| 274 | 261 | } |
| 275 | 262 | } |
| 276 | if (dec) | |
| 277 | ctxt.add(facade.jnum(at(i, j))) | |
| 278 | else | |
| 279 | ctxt.add(facade.jint(at(i, j))) | |
| 263 | ||
| 264 | ctxt.add(facade.jnum(at(i, j), decIndex, expIndex)) | |
| 280 | 265 | j |
| 281 | 266 | } |
| 282 | 267 | |
| 286 | 271 | * NOTE: This is only capable of generating characters from the basic plane. |
| 287 | 272 | * This is why it can only return Char instead of Int. |
| 288 | 273 | */ |
| 289 | protected[this] final def descape(s: String): Char = { | |
| 274 | protected[this] final def descape(s: CharSequence): Char = { | |
| 290 | 275 | val hc = HexChars |
| 291 | 276 | var i = 0 |
| 292 | 277 | var x = 0 |
| 304 | 289 | |
| 305 | 290 | /** |
| 306 | 291 | * Parse the JSON constant "true". |
| 307 | */ | |
| 308 | protected[this] final def parseTrue(i: Int)(implicit facade: Facade[J]) = | |
| 309 | if (is(i, i + 4, "true")) facade.jtrue else die(i, "expected true") | |
| 292 | * | |
| 293 | * Note that this method assumes that the first character has already been checked. | |
| 294 | */ | |
| 295 | protected[this] final def parseTrue(i: Int)(implicit facade: Facade[J]): J = | |
| 296 | if (at(i + 1) == 'r' && at(i + 2) == 'u' && at(i + 3) == 'e') { | |
| 297 | facade.jtrue | |
| 298 | } else { | |
| 299 | die(i, "expected true") | |
| 300 | } | |
| 310 | 301 | |
| 311 | 302 | /** |
| 312 | 303 | * Parse the JSON constant "false". |
| 313 | */ | |
| 314 | protected[this] final def parseFalse(i: Int)(implicit facade: Facade[J]) = | |
| 315 | if (is(i, i + 5, "false")) facade.jfalse else die(i, "expected false") | |
| 304 | * | |
| 305 | * Note that this method assumes that the first character has already been checked. | |
| 306 | */ | |
| 307 | protected[this] final def parseFalse(i: Int)(implicit facade: Facade[J]): J = | |
| 308 | if (at(i + 1) == 'a' && at(i + 2) == 'l' && at(i + 3) == 's' && at(i + 4) == 'e') { | |
| 309 | facade.jfalse | |
| 310 | } else { | |
| 311 | die(i, "expected false") | |
| 312 | } | |
| 316 | 313 | |
| 317 | 314 | /** |
| 318 | 315 | * Parse the JSON constant "null". |
| 319 | */ | |
| 320 | protected[this] final def parseNull(i: Int)(implicit facade: Facade[J]) = | |
| 321 | if (is(i, i + 4, "null")) facade.jnull else die(i, "expected null") | |
| 316 | * | |
| 317 | * Note that this method assumes that the first character has already been checked. | |
| 318 | */ | |
| 319 | protected[this] final def parseNull(i: Int)(implicit facade: Facade[J]): J = | |
| 320 | if (at(i + 1) == 'u' && at(i + 2) == 'l' && at(i + 3) == 'l') { | |
| 321 | facade.jnull | |
| 322 | } else { | |
| 323 | die(i, "expected null") | |
| 324 | } | |
| 322 | 325 | |
| 323 | 326 | /** |
| 324 | 327 | * Parse and return the next JSON value and the position beyond it. |
| 379 | 382 | protected[this] final def rparse(state: Int, j: Int, stack: List[FContext[J]])(implicit facade: Facade[J]): (J, Int) = { |
| 380 | 383 | val i = reset(j) |
| 381 | 384 | checkpoint(state, i, stack) |
| 382 | (state: @switch) match { | |
| 385 | ||
| 386 | val c = at(i) | |
| 387 | ||
| 388 | if (c == '\n') { | |
| 389 | newline(i) | |
| 390 | rparse(state, i + 1, stack) | |
| 391 | } else if (c == ' ' || c == '\t' || c == '\r') { | |
| 392 | rparse(state, i + 1, stack) | |
| 393 | } else if (state == DATA) { | |
| 383 | 394 | // we are inside an object or array expecting to see data |
| 384 | case DATA => | |
| 385 | (at(i): @switch) match { | |
| 386 | case '[' => rparse(ARRBEG, i + 1, facade.arrayContext() :: stack) | |
| 387 | case '{' => rparse(OBJBEG, i + 1, facade.objectContext() :: stack) | |
| 388 | ||
| 389 | case '-' | '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' => | |
| 390 | val ctxt = stack.head | |
| 391 | val j = parseNum(i, ctxt) | |
| 392 | rparse(if (ctxt.isObj) OBJEND else ARREND, j, stack) | |
| 393 | ||
| 394 | case '"' => | |
| 395 | val ctxt = stack.head | |
| 396 | val j = parseString(i, ctxt) | |
| 397 | rparse(if (ctxt.isObj) OBJEND else ARREND, j, stack) | |
| 398 | ||
| 399 | case 't' => | |
| 400 | val ctxt = stack.head | |
| 401 | ctxt.add(parseTrue(i)) | |
| 402 | rparse(if (ctxt.isObj) OBJEND else ARREND, i + 4, stack) | |
| 403 | ||
| 404 | case 'f' => | |
| 405 | val ctxt = stack.head | |
| 406 | ctxt.add(parseFalse(i)) | |
| 407 | rparse(if (ctxt.isObj) OBJEND else ARREND, i + 5, stack) | |
| 408 | ||
| 409 | case 'n' => | |
| 410 | val ctxt = stack.head | |
| 411 | ctxt.add(parseNull(i)) | |
| 412 | rparse(if (ctxt.isObj) OBJEND else ARREND, i + 4, stack) | |
| 413 | ||
| 414 | case ' ' => rparse(state, i + 1, stack) | |
| 415 | case '\t' => rparse(state, i + 1, stack) | |
| 416 | case '\r' => rparse(state, i + 1, stack) | |
| 417 | case '\n' => newline(i); rparse(state, i + 1, stack) | |
| 418 | ||
| 419 | case _ => | |
| 420 | die(i, "expected json value") | |
| 395 | if (c == '[') { | |
| 396 | rparse(ARRBEG, i + 1, facade.arrayContext() :: stack) | |
| 397 | } else if (c == '{') { | |
| 398 | rparse(OBJBEG, i + 1, facade.objectContext() :: stack) | |
| 399 | } else { | |
| 400 | val ctxt = stack.head | |
| 401 | ||
| 402 | if ((c >= '0' && c <= '9') || c == '-') { | |
| 403 | val j = parseNum(i, ctxt) | |
| 404 | rparse(if (ctxt.isObj) OBJEND else ARREND, j, stack) | |
| 405 | } else if (c == '"') { | |
| 406 | val j = parseString(i, ctxt) | |
| 407 | rparse(if (ctxt.isObj) OBJEND else ARREND, j, stack) | |
| 408 | } else if (c == 't') { | |
| 409 | ctxt.add(parseTrue(i)) | |
| 410 | rparse(if (ctxt.isObj) OBJEND else ARREND, i + 4, stack) | |
| 411 | } else if (c == 'f') { | |
| 412 | ctxt.add(parseFalse(i)) | |
| 413 | rparse(if (ctxt.isObj) OBJEND else ARREND, i + 5, stack) | |
| 414 | } else if (c == 'n') { | |
| 415 | ctxt.add(parseNull(i)) | |
| 416 | rparse(if (ctxt.isObj) OBJEND else ARREND, i + 4, stack) | |
| 417 | } else { | |
| 418 | die(i, "expected json value") | |
| 421 | 419 | } |
| 422 | ||
| 423 | // we are in an object expecting to see a key | |
| 424 | case KEY => | |
| 425 | (at(i): @switch) match { | |
| 426 | case '"' => | |
| 427 | val j = parseString(i, stack.head) | |
| 428 | rparse(SEP, j, stack) | |
| 429 | ||
| 430 | case ' ' => rparse(state, i + 1, stack) | |
| 431 | case '\t' => rparse(state, i + 1, stack) | |
| 432 | case '\r' => rparse(state, i + 1, stack) | |
| 433 | case '\n' => newline(i); rparse(state, i + 1, stack) | |
| 434 | ||
| 435 | case _ => die(i, "expected \"") | |
| 420 | } | |
| 421 | } else if ( | |
| 422 | (c == ']' && (state == ARREND || state == ARRBEG)) || | |
| 423 | (c == '}' && (state == OBJEND || state == OBJBEG)) | |
| 424 | ) { | |
| 425 | // we are inside an array or object and have seen a key or a closing | |
| 426 | // brace, respectively. | |
| 427 | if (stack.isEmpty) { | |
| 428 | error("invalid stack") | |
| 429 | } else { | |
| 430 | val ctxt1 = stack.head | |
| 431 | val tail = stack.tail | |
| 432 | ||
| 433 | if (tail.isEmpty) { | |
| 434 | (ctxt1.finish, i + 1) | |
| 435 | } else { | |
| 436 | val ctxt2 = tail.head | |
| 437 | ctxt2.add(ctxt1.finish) | |
| 438 | rparse(if (ctxt2.isObj) OBJEND else ARREND, i + 1, tail) | |
| 436 | 439 | } |
| 437 | ||
| 438 | // we are starting an array, expecting to see data or a closing bracket | |
| 439 | case ARRBEG => | |
| 440 | (at(i): @switch) match { | |
| 441 | case ']' => stack match { | |
| 442 | case ctxt1 :: Nil => | |
| 443 | (ctxt1.finish, i + 1) | |
| 444 | case ctxt1 :: ctxt2 :: tail => | |
| 445 | ctxt2.add(ctxt1.finish) | |
| 446 | rparse(if (ctxt2.isObj) OBJEND else ARREND, i + 1, ctxt2 :: tail) | |
| 447 | case _ => | |
| 448 | error("invalid stack") | |
| 449 | } | |
| 450 | ||
| 451 | case ' ' => rparse(state, i + 1, stack) | |
| 452 | case '\t' => rparse(state, i + 1, stack) | |
| 453 | case '\r' => rparse(state, i + 1, stack) | |
| 454 | case '\n' => newline(i); rparse(state, i + 1, stack) | |
| 455 | ||
| 456 | case _ => rparse(DATA, i, stack) | |
| 457 | } | |
| 458 | ||
| 459 | // we are starting an object, expecting to see a key or a closing brace | |
| 460 | case OBJBEG => | |
| 461 | (at(i): @switch) match { | |
| 462 | case '}' => stack match { | |
| 463 | case ctxt1 :: Nil => | |
| 464 | (ctxt1.finish, i + 1) | |
| 465 | case ctxt1 :: ctxt2 :: tail => | |
| 466 | ctxt2.add(ctxt1.finish) | |
| 467 | rparse(if (ctxt2.isObj) OBJEND else ARREND, i + 1, ctxt2 :: tail) | |
| 468 | case _ => | |
| 469 | error("invalid stack") | |
| 470 | } | |
| 471 | ||
| 472 | case ' ' => rparse(state, i + 1, stack) | |
| 473 | case '\t' => rparse(state, i + 1, stack) | |
| 474 | case '\r' => rparse(state, i + 1, stack) | |
| 475 | case '\n' => newline(i); rparse(state, i + 1, stack) | |
| 476 | ||
| 477 | case _ => rparse(KEY, i, stack) | |
| 478 | } | |
| 479 | ||
| 480 | // we are in an object just after a key, expecting to see a colon | |
| 481 | case SEP => | |
| 482 | (at(i): @switch) match { | |
| 483 | case ':' => rparse(DATA, i + 1, stack) | |
| 484 | ||
| 485 | case ' ' => rparse(state, i + 1, stack) | |
| 486 | case '\t' => rparse(state, i + 1, stack) | |
| 487 | case '\r' => rparse(state, i + 1, stack) | |
| 488 | case '\n' => newline(i); rparse(state, i + 1, stack) | |
| 489 | ||
| 490 | case _ => die(i, "expected :") | |
| 491 | } | |
| 492 | ||
| 493 | // we are at a possible stopping point for an array, expecting to see | |
| 494 | // either a comma (before more data) or a closing bracket. | |
| 495 | case ARREND => | |
| 496 | (at(i): @switch) match { | |
| 497 | case ',' => rparse(DATA, i + 1, stack) | |
| 498 | ||
| 499 | case ']' => stack match { | |
| 500 | case ctxt1 :: Nil => | |
| 501 | (ctxt1.finish, i + 1) | |
| 502 | case ctxt1 :: ctxt2 :: tail => | |
| 503 | ctxt2.add(ctxt1.finish) | |
| 504 | rparse(if (ctxt2.isObj) OBJEND else ARREND, i + 1, ctxt2 :: tail) | |
| 505 | case _ => | |
| 506 | error("invalid stack") | |
| 507 | } | |
| 508 | ||
| 509 | case ' ' => rparse(state, i + 1, stack) | |
| 510 | case '\t' => rparse(state, i + 1, stack) | |
| 511 | case '\r' => rparse(state, i + 1, stack) | |
| 512 | case '\n' => newline(i); rparse(state, i + 1, stack) | |
| 513 | ||
| 514 | case _ => die(i, "expected ] or ,") | |
| 515 | } | |
| 516 | ||
| 517 | // we are at a possible stopping point for an object, expecting to see | |
| 518 | // either a comma (before more data) or a closing brace. | |
| 519 | case OBJEND => | |
| 520 | (at(i): @switch) match { | |
| 521 | case ',' => rparse(KEY, i + 1, stack) | |
| 522 | ||
| 523 | case '}' => stack match { | |
| 524 | case ctxt1 :: Nil => | |
| 525 | (ctxt1.finish, i + 1) | |
| 526 | case ctxt1 :: ctxt2 :: tail => | |
| 527 | ctxt2.add(ctxt1.finish) | |
| 528 | rparse(if (ctxt2.isObj) OBJEND else ARREND, i + 1, ctxt2 :: tail) | |
| 529 | case _ => | |
| 530 | error("invalid stack") | |
| 531 | } | |
| 532 | ||
| 533 | case ' ' => rparse(state, i + 1, stack) | |
| 534 | case '\t' => rparse(state, i + 1, stack) | |
| 535 | case '\r' => rparse(state, i + 1, stack) | |
| 536 | case '\n' => newline(i); rparse(state, i + 1, stack) | |
| 537 | ||
| 538 | case _ => die(i, "expected } or ,") | |
| 539 | } | |
| 440 | } | |
| 441 | } else if (state == KEY) { | |
| 442 | // we are in an object expecting to see a key. | |
| 443 | if (c == '"') { | |
| 444 | val j = parseString(i, stack.head) | |
| 445 | rparse(SEP, j, stack) | |
| 446 | } else { | |
| 447 | die(i, "expected \"") | |
| 448 | } | |
| 449 | } else if (state == SEP) { | |
| 450 | // we are in an object just after a key, expecting to see a colon. | |
| 451 | if (c == ':') { | |
| 452 | rparse(DATA, i + 1, stack) | |
| 453 | } else { | |
| 454 | die(i, "expected :") | |
| 455 | } | |
| 456 | } else if (state == ARREND) { | |
| 457 | // we are in an array, expecting to see a comma (before more data). | |
| 458 | if (c == ',') { | |
| 459 | rparse(DATA, i + 1, stack) | |
| 460 | } else { | |
| 461 | die(i, "expected ] or ,") | |
| 462 | } | |
| 463 | } else if (state == OBJEND) { | |
| 464 | // we are in an object, expecting to see a comma (before more data). | |
| 465 | if (c == ',') { | |
| 466 | rparse(KEY, i + 1, stack) | |
| 467 | } else { | |
| 468 | die(i, "expected } or ,") | |
| 469 | } | |
| 470 | } else if (state == ARRBEG) { | |
| 471 | // we are starting an array, expecting to see data or a closing bracket. | |
| 472 | rparse(DATA, i, stack) | |
| 473 | } else { | |
| 474 | // we are starting an object, expecting to see a key or a closing brace. | |
| 475 | rparse(KEY, i, stack) | |
| 540 | 476 | } |
| 541 | 477 | } |
| 542 | 478 | } |
| 549 | 485 | |
| 550 | 486 | def parseFromString[J](s: String)(implicit facade: Facade[J]): Try[J] = |
| 551 | 487 | Try(new StringParser[J](s).parse) |
| 488 | ||
| 489 | def parseFromCharSequence[J](cs: CharSequence)(implicit facade: Facade[J]): Try[J] = | |
| 490 | Try(new CharSequenceParser[J](cs).parse) | |
| 552 | 491 | |
| 553 | 492 | def parseFromPath[J](path: String)(implicit facade: Facade[J]): Try[J] = |
| 554 | 493 | Try(ChannelParser.fromFile[J](new File(path)).parse) |
| 4 | 4 | /** |
| 5 | 5 | * Facade is a type class that describes how Jawn should construct |
| 6 | 6 | * JSON AST elements of type J. |
| 7 | * | |
| 7 | * | |
| 8 | 8 | * Facade[J] also uses FContext[J] instances, so implementors will |
| 9 | 9 | * usually want to define both. |
| 10 | 10 | */ |
| 14 | 14 | |
| 15 | 15 | def singleContext() = new FContext[J] { |
| 16 | 16 | var value: J = _ |
| 17 | def add(s: String) { value = jstring(s) } | |
| 17 | def add(s: CharSequence) { value = jstring(s) } | |
| 18 | 18 | def add(v: J) { value = v } |
| 19 | 19 | def finish: J = value |
| 20 | 20 | def isObj: Boolean = false |
| 22 | 22 | |
| 23 | 23 | def arrayContext() = new FContext[J] { |
| 24 | 24 | val vs = mutable.ListBuffer.empty[J] |
| 25 | def add(s: String) { vs += jstring(s) } | |
| 25 | def add(s: CharSequence) { vs += jstring(s) } | |
| 26 | 26 | def add(v: J) { vs += v } |
| 27 | 27 | def finish: J = jarray(vs.toList) |
| 28 | 28 | def isObj: Boolean = false |
| 31 | 31 | def objectContext() = new FContext[J] { |
| 32 | 32 | var key: String = null |
| 33 | 33 | var vs = Map.empty[String, J] |
| 34 | def add(s: String): Unit = | |
| 35 | if (key == null) { key = s } else { vs = vs.updated(key, jstring(s)); key = null } | |
| 34 | def add(s: CharSequence): Unit = | |
| 35 | if (key == null) { key = s.toString } else { vs = vs.updated(key, jstring(s)); key = null } | |
| 36 | 36 | def add(v: J): Unit = |
| 37 | 37 | { vs = vs.updated(key, v); key = null } |
| 38 | 38 | def finish = jobject(vs) |
| 16 | 16 | final def column(i: Int) = i |
| 17 | 17 | final def newline(i: Int) { line += 1 } |
| 18 | 18 | final def reset(i: Int): Int = i |
| 19 | final def checkpoint(state: Int, i: Int, stack: List[FContext[J]]) {} | |
| 19 | final def checkpoint(state: Int, i: Int, stack: List[FContext[J]]): Unit = () | |
| 20 | 20 | final def at(i: Int): Char = s.charAt(i) |
| 21 | final def at(i: Int, j: Int): String = s.substring(i, j) | |
| 21 | final def at(i: Int, j: Int): CharSequence = s.substring(i, j) | |
| 22 | 22 | final def atEof(i: Int) = i == s.length |
| 23 | 23 | final def close() = () |
| 24 | 24 | } |
| 13 | 13 | val big = q + ("x" * (40 * M)) + q |
| 14 | 14 | val bigEscaped = q + ("\\\\" * (20 * M)) + q |
| 15 | 15 | |
| 16 | Util.withTemp(big) { t => | |
| 16 | TestUtil.withTemp(big) { t => | |
| 17 | 17 | Parser.parseFromFile(t)(NullFacade).isSuccess shouldBe true |
| 18 | 18 | } |
| 19 | 19 | |
| 20 | Util.withTemp(bigEscaped) { t => | |
| 20 | TestUtil.withTemp(bigEscaped) { t => | |
| 21 | 21 | Parser.parseFromFile(t)(NullFacade).isSuccess shouldBe true |
| 22 | 22 | } |
| 23 | 23 | } |
| 0 | package jawn | |
| 1 | package parser | |
| 2 | ||
| 3 | import java.nio.ByteBuffer | |
| 4 | import org.scalatest.{Matchers, PropSpec} | |
| 5 | import org.scalatest.prop.PropertyChecks | |
| 6 | import scala.util.Success | |
| 7 | ||
| 8 | class JNumIndexCheck extends PropSpec with Matchers with PropertyChecks { | |
| 9 | object JNumIndexCheckFacade extends Facade[Boolean] { | |
| 10 | class JNumIndexCheckContext(val isObj: Boolean) extends FContext[Boolean] { | |
| 11 | var failed = false | |
| 12 | def add(s: CharSequence): Unit = () | |
| 13 | def add(v: Boolean): Unit = { | |
| 14 | if (!v) failed = true | |
| 15 | } | |
| 16 | def finish: Boolean = !failed | |
| 17 | } | |
| 18 | ||
| 19 | val singleContext: FContext[Boolean] = new JNumIndexCheckContext(false) | |
| 20 | val arrayContext: FContext[Boolean] = new JNumIndexCheckContext(false) | |
| 21 | val objectContext: FContext[Boolean] = new JNumIndexCheckContext(true) | |
| 22 | ||
| 23 | def jnull(): Boolean = true | |
| 24 | def jfalse(): Boolean = true | |
| 25 | def jtrue(): Boolean = true | |
| 26 | def jnum(s: CharSequence, decIndex: Int, expIndex: Int): Boolean = { | |
| 27 | val input = s.toString | |
| 28 | val inputDecIndex = input.indexOf('.') | |
| 29 | val inputExpIndex = if (input.indexOf('e') == -1) input.indexOf("E") else input.indexOf('e') | |
| 30 | ||
| 31 | decIndex == inputDecIndex && expIndex == inputExpIndex | |
| 32 | } | |
| 33 | def jstring(s: CharSequence): Boolean = true | |
| 34 | } | |
| 35 | ||
| 36 | property("jnum provides the correct indices with parseFromString") { | |
| 37 | forAll { (value: BigDecimal) => | |
| 38 | val json = s"""{ "num": ${value.toString} }""" | |
| 39 | Parser.parseFromString(json)(JNumIndexCheckFacade) shouldBe Success(true) | |
| 40 | } | |
| 41 | } | |
| 42 | ||
| 43 | property("jnum provides the correct indices with parseFromByteBuffer") { | |
| 44 | forAll { (value: BigDecimal) => | |
| 45 | val json = s"""{ "num": ${value.toString} }""" | |
| 46 | val bb = ByteBuffer.wrap(json.getBytes("UTF-8")) | |
| 47 | Parser.parseFromByteBuffer(bb)(JNumIndexCheckFacade) shouldBe Success(true) | |
| 48 | } | |
| 49 | } | |
| 50 | ||
| 51 | property("jnum provides the correct indices with parseFromFile") { | |
| 52 | forAll { (value: BigDecimal) => | |
| 53 | val json = s"""{ "num": ${value.toString} }""" | |
| 54 | TestUtil.withTemp(json) { t => | |
| 55 | Parser.parseFromFile(t)(JNumIndexCheckFacade) shouldBe Success(true) | |
| 56 | } | |
| 57 | } | |
| 58 | } | |
| 59 | ||
| 60 | property("jnum provides the correct indices at the top level with parseFromString") { | |
| 61 | forAll { (value: BigDecimal) => | |
| 62 | Parser.parseFromString(value.toString)(JNumIndexCheckFacade) shouldBe Success(true) | |
| 63 | } | |
| 64 | } | |
| 65 | ||
| 66 | property("jnum provides the correct indices at the top level with parseFromByteBuffer") { | |
| 67 | forAll { (value: BigDecimal) => | |
| 68 | val bb = ByteBuffer.wrap(value.toString.getBytes("UTF-8")) | |
| 69 | Parser.parseFromByteBuffer(bb)(JNumIndexCheckFacade) shouldBe Success(true) | |
| 70 | } | |
| 71 | } | |
| 72 | ||
| 73 | property("jnum provides the correct indices at the top level with parseFromFile") { | |
| 74 | forAll { (value: BigDecimal) => | |
| 75 | TestUtil.withTemp(value.toString) { t => | |
| 76 | Parser.parseFromFile(t)(JNumIndexCheckFacade) shouldBe Success(true) | |
| 77 | } | |
| 78 | } | |
| 79 | } | |
| 80 | } |
| 61 | 61 | import java.nio.ByteBuffer |
| 62 | 62 | |
| 63 | 63 | def isValidSyntax(s: String): Boolean = { |
| 64 | val cs = java.nio.CharBuffer.wrap(s.toCharArray) | |
| 65 | val r0 = Parser.parseFromCharSequence(cs)(NullFacade).isSuccess | |
| 64 | 66 | val r1 = Parser.parseFromString(s)(NullFacade).isSuccess |
| 65 | 67 | val bb = ByteBuffer.wrap(s.getBytes("UTF-8")) |
| 66 | 68 | val r2 = Parser.parseFromByteBuffer(bb)(NullFacade).isSuccess |
| 69 | if (r0 == r1) r1 else sys.error(s"CharSequence/String parsing disagree($r0, $r1): $s") | |
| 67 | 70 | if (r1 == r2) r1 else sys.error(s"String/ByteBuffer parsing disagree($r1, $r2): $s") |
| 68 | 71 | |
| 69 | Util.withTemp(s) { t => | |
| 72 | TestUtil.withTemp(s) { t => | |
| 70 | 73 | Parser.parseFromFile(t)(NullFacade).isSuccess |
| 71 | 74 | } |
| 72 | 75 |
| 0 | package jawn | |
| 1 | package parser | |
| 2 | ||
| 3 | import java.io._ | |
| 4 | ||
| 5 | object TestUtil { | |
| 6 | def withTemp[A](s: String)(f: File => A): A = { | |
| 7 | val t = File.createTempFile("jawn-syntax", ".json") | |
| 8 | val pw = new PrintWriter(t) | |
| 9 | pw.println(s) | |
| 10 | pw.close() | |
| 11 | try { | |
| 12 | f(t) | |
| 13 | } finally { | |
| 14 | t.delete() | |
| 15 | } | |
| 16 | } | |
| 17 | } |
| 0 | package jawn | |
| 1 | package parser | |
| 2 | ||
| 3 | import java.io._ | |
| 4 | ||
| 5 | object Util { | |
| 6 | def withTemp[A](s: String)(f: File => A): A = { | |
| 7 | val t = File.createTempFile("jawn-syntax", ".json") | |
| 8 | val pw = new PrintWriter(t) | |
| 9 | pw.println(s) | |
| 10 | pw.close() | |
| 11 | try { | |
| 12 | f(t) | |
| 13 | } finally { | |
| 14 | t.delete() | |
| 15 | } | |
| 16 | } | |
| 17 | } |
| 0 | addSbtPlugin("com.eed3si9n" % "sbt-doge" % "0.1.5") | |
| 1 | addSbtPlugin("com.jsuereth" % "sbt-pgp" % "1.0.0") | |
| 2 | addSbtPlugin("com.github.gseitz" % "sbt-release" % "1.0.0") | |
| 3 | addSbtPlugin("org.xerial.sbt" % "sbt-sonatype" % "0.5.0") | |
| 4 | ||
| 5 | addSbtPlugin("pl.project13.scala" % "sbt-jmh" % "0.1.15") | |
| 0 | addSbtPlugin("com.eed3si9n" % "sbt-doge" % "0.1.5") | |
| 1 | addSbtPlugin("pl.project13.scala" % "sbt-jmh" % "0.2.25") | |
| 2 | addSbtPlugin("com.typesafe" % "sbt-mima-plugin" % "0.1.14") | |
| 3 | addSbtPlugin("com.jsuereth" % "sbt-pgp" % "1.0.1") | |
| 4 | addSbtPlugin("com.github.gseitz" % "sbt-release" % "1.0.5") | |
| 5 | addSbtPlugin("org.xerial.sbt" % "sbt-sonatype" % "1.1") |
| 9 | 9 | def jnull() = Json.jNull |
| 10 | 10 | def jfalse() = Json.jFalse |
| 11 | 11 | def jtrue() = Json.jTrue |
| 12 | def jnum(s: String) = Json.jNumber(JsonNumber.unsafeDecimal(s)) | |
| 13 | def jint(s: String) = Json.jNumber(JsonNumber.unsafeDecimal(s)) | |
| 14 | def jstring(s: String) = Json.jString(s) | |
| 12 | ||
| 13 | def jnum(s: CharSequence, decIndex: Int, expIndex: Int) = | |
| 14 | Json.jNumber(JsonNumber.unsafeDecimal(s.toString)) | |
| 15 | def jstring(s: CharSequence) = Json.jString(s.toString) | |
| 15 | 16 | |
| 16 | 17 | def singleContext() = new FContext[Json] { |
| 17 | 18 | var value: Json = null |
| 18 | def add(s: String) { value = jstring(s) } | |
| 19 | def add(s: CharSequence) { value = jstring(s) } | |
| 19 | 20 | def add(v: Json) { value = v } |
| 20 | 21 | def finish: Json = value |
| 21 | 22 | def isObj: Boolean = false |
| 23 | 24 | |
| 24 | 25 | def arrayContext() = new FContext[Json] { |
| 25 | 26 | val vs = mutable.ListBuffer.empty[Json] |
| 26 | def add(s: String) { vs += jstring(s) } | |
| 27 | def add(s: CharSequence) { vs += jstring(s) } | |
| 27 | 28 | def add(v: Json) { vs += v } |
| 28 | 29 | def finish: Json = Json.jArray(vs.toList) |
| 29 | 30 | def isObj: Boolean = false |
| 32 | 33 | def objectContext() = new FContext[Json] { |
| 33 | 34 | var key: String = null |
| 34 | 35 | var vs = JsonObject.empty |
| 35 | def add(s: String): Unit = | |
| 36 | if (key == null) { key = s } else { vs = vs + (key, jstring(s)); key = null } | |
| 36 | def add(s: CharSequence): Unit = | |
| 37 | if (key == null) { key = s.toString } else { vs = vs + (key, jstring(s)); key = null } | |
| 37 | 38 | def add(v: Json): Unit = |
| 38 | 39 | { vs = vs + (key, v); key = null } |
| 39 | 40 | def finish = Json.jObject(vs) |
| 3 | 3 | import scala.collection.mutable |
| 4 | 4 | import org.json4s.JsonAST._ |
| 5 | 5 | |
| 6 | object Parser extends SupportParser[JValue] { | |
| 6 | object Parser extends Parser(false, false) | |
| 7 | ||
| 8 | class Parser(useBigDecimalForDouble: Boolean, useBigIntForLong: Boolean) extends SupportParser[JValue] { | |
| 7 | 9 | |
| 8 | 10 | implicit val facade: Facade[JValue] = |
| 9 | 11 | new Facade[JValue] { |
| 10 | 12 | def jnull() = JNull |
| 11 | 13 | def jfalse() = JBool(false) |
| 12 | 14 | def jtrue() = JBool(true) |
| 13 | def jnum(s: String) = JDouble(java.lang.Double.parseDouble(s)) | |
| 14 | def jint(s: String) = JInt(java.lang.Integer.parseInt(s)) | |
| 15 | def jstring(s: String) = JString(s) | |
| 15 | ||
| 16 | def jnum(s: CharSequence, decIndex: Int, expIndex: Int) = | |
| 17 | if (decIndex == -1 && expIndex == -1) { | |
| 18 | if (useBigIntForLong) JInt(BigInt(s.toString)) | |
| 19 | else JLong(util.parseLongUnsafe(s)) | |
| 20 | } else { | |
| 21 | if (useBigDecimalForDouble) JDecimal(BigDecimal(s.toString)) | |
| 22 | else JDouble(s.toString.toDouble) | |
| 23 | } | |
| 24 | ||
| 25 | def jstring(s: CharSequence) = JString(s.toString) | |
| 16 | 26 | |
| 17 | 27 | def singleContext() = |
| 18 | 28 | new FContext[JValue] { |
| 19 | 29 | var value: JValue = null |
| 20 | def add(s: String) { value = jstring(s) } | |
| 30 | def add(s: CharSequence) { value = jstring(s) } | |
| 21 | 31 | def add(v: JValue) { value = v } |
| 22 | 32 | def finish: JValue = value |
| 23 | 33 | def isObj: Boolean = false |
| 26 | 36 | def arrayContext() = |
| 27 | 37 | new FContext[JValue] { |
| 28 | 38 | val vs = mutable.ListBuffer.empty[JValue] |
| 29 | def add(s: String) { vs += jstring(s) } | |
| 39 | def add(s: CharSequence) { vs += jstring(s) } | |
| 30 | 40 | def add(v: JValue) { vs += v } |
| 31 | 41 | def finish: JValue = JArray(vs.toList) |
| 32 | 42 | def isObj: Boolean = false |
| 36 | 46 | new FContext[JValue] { |
| 37 | 47 | var key: String = null |
| 38 | 48 | val vs = mutable.ListBuffer.empty[JField] |
| 39 | def add(s: String): Unit = | |
| 40 | if (key == null) key = s | |
| 49 | def add(s: CharSequence): Unit = | |
| 50 | if (key == null) key = s.toString | |
| 41 | 51 | else { vs += JField(key, jstring(s)); key = null } |
| 42 | 52 | def add(v: JValue): Unit = |
| 43 | 53 | { vs += JField(key, v); key = null } |
| 9 | 9 | def jnull() = JsNull |
| 10 | 10 | def jfalse() = JsBoolean(false) |
| 11 | 11 | def jtrue() = JsBoolean(true) |
| 12 | def jnum(s: String) = JsNumber(BigDecimal(s)) | |
| 13 | def jint(s: String) = JsNumber(BigDecimal(s)) | |
| 14 | def jstring(s: String) = JsString(s) | |
| 12 | ||
| 13 | def jnum(s: CharSequence, decIndex: Int, expIndex: Int) = JsNumber(BigDecimal(s.toString)) | |
| 14 | def jstring(s: CharSequence) = JsString(s.toString) | |
| 15 | ||
| 15 | 16 | def jarray(vs: List[JsValue]) = JsArray(vs) |
| 16 | 17 | def jobject(vs: Map[String, JsValue]) = JsObject(vs) |
| 17 | 18 | } |
| 9 | 9 | def jnull() = JNull |
| 10 | 10 | def jfalse() = JBoolean.canonicalFalse |
| 11 | 11 | def jtrue() = JBoolean.canonicalTrue |
| 12 | def jnum(s: String) = JNumber(BigDecimal(s)) | |
| 13 | def jint(s: String) = JNumber(BigDecimal(s)) | |
| 14 | def jstring(s: String) = JString(s) | |
| 12 | def jnum(s: CharSequence, decIndex: Int, expIndex: Int) = JNumber(BigDecimal(s.toString)) | |
| 13 | def jstring(s: CharSequence) = JString(s.toString) | |
| 15 | 14 | def jarray(vs: mutable.ArrayBuffer[JValue]) = JArray(vs) |
| 16 | 15 | def jobject(vs: mutable.Map[String, JValue]) = JObject(vs) |
| 17 | 16 | } |
| 9 | 9 | def jnull() = JNull |
| 10 | 10 | def jfalse() = JBoolean.canonicalFalse |
| 11 | 11 | def jtrue() = JBoolean.canonicalTrue |
| 12 | def jnum(s: String) = JNumber.unsafeFromString(s) | |
| 13 | def jint(s: String) = JNumber.unsafeFromString(s) | |
| 14 | def jstring(s: String) = JString(s) | |
| 12 | def jnum(s: CharSequence, decIndex: Int, expIndex: Int) = JNumber.unsafeFromString(s.toString) | |
| 13 | def jstring(s: CharSequence) = JString(s.toString) | |
| 15 | 14 | def jarray(vs: mutable.ArrayBuffer[JValue]) = JArray(vs) |
| 16 | 15 | def jobject(vs: mutable.Map[String, JValue]) = JObject(vs) |
| 17 | 16 | } |
| 8 | 8 | def jnull() = JsNull |
| 9 | 9 | def jfalse() = JsFalse |
| 10 | 10 | def jtrue() = JsTrue |
| 11 | def jnum(s: String) = JsNumber(s) | |
| 12 | def jint(s: String) = JsNumber(s) | |
| 13 | def jstring(s: String) = JsString(s) | |
| 11 | def jnum(s: CharSequence, decIndex: Int, expIndex: Int) = JsNumber(s.toString) | |
| 12 | def jstring(s: CharSequence) = JsString(s.toString) | |
| 14 | 13 | def jarray(vs: List[JsValue]) = JsArray(vs: _*) |
| 15 | 14 | def jobject(vs: Map[String, JsValue]) = JsObject(vs) |
| 16 | 15 | } |
| 0 | package jawn.util | |
| 1 | ||
| 2 | class InvalidLong(s: String) extends NumberFormatException(s"For input string '$s'") | |
| 3 | ||
| 4 | object InvalidLong { | |
| 5 | def apply(s: String): InvalidLong = new InvalidLong(s) | |
| 6 | } |
| 0 | package jawn.util | |
| 1 | ||
| 2 | /** | |
| 3 | * Character sequence representing a lazily-calculated substring. | |
| 4 | * | |
| 5 | * This class has three constructors: | |
| 6 | * | |
| 7 | * - Slice(s) wraps a string, ensuring that future operations | |
| 8 | * (e.g. subSequence) will construct slices instead of strings. | |
| 9 | * | |
| 10 | * - Slice(s, start, limit) is the default, and ensures that: | |
| 11 | * | |
| 12 | * 1. start >= 0 | |
| 13 | * 2. limit >= start | |
| 14 | * 3. limit <= s.length | |
| 15 | * | |
| 16 | * - Slice.unsafe(s, start, limit) is for situations where the above | |
| 17 | * bounds-checking has already occurred. Only use this if you are | |
| 18 | * absolutely sure your arguments satisfy the above invariants. | |
| 19 | * | |
| 20 | * Slice's subSequence returns another slice. This means that when | |
| 21 | * wrapping a very large string, garbage collection on the underlying | |
| 22 | * string will not occur until all slices are freed. | |
| 23 | * | |
| 24 | * Slice's universal equality is only defined with regard to other | |
| 25 | * slices. This means comparing a Slice with other CharSequence values | |
| 26 | * (including String) will always return false. | |
| 27 | * | |
| 28 | * Slices are serializable. However! They use the default Java | |
| 29 | * serialization layout, which is not that efficient, and could be a | |
| 30 | * disaster in cases where a large shared string might be serialized | |
| 31 | * many times in different slices. | |
| 32 | */ | |
| 33 | @SerialVersionUID(1L) | |
| 34 | final class Slice private[jawn] (s: String, start: Int, limit: Int) extends CharSequence with Serializable { | |
| 35 | ||
| 36 | final val length: Int = | |
| 37 | limit - start | |
| 38 | ||
| 39 | def charAt(i: Int): Char = | |
| 40 | if (i < 0 || length <= i) throw new StringIndexOutOfBoundsException(s"index out of range: $i") | |
| 41 | else s.charAt(start + i) | |
| 42 | ||
| 43 | def subSequence(i: Int, j: Int): Slice = | |
| 44 | Slice(s, start + i, start + j) | |
| 45 | ||
| 46 | override def toString: String = | |
| 47 | s.substring(start, limit) | |
| 48 | ||
| 49 | override def equals(that: Any): Boolean = | |
| 50 | that match { | |
| 51 | case t: AnyRef if this eq t => | |
| 52 | true | |
| 53 | case slice: Slice => | |
| 54 | if (length != slice.length) return false | |
| 55 | var i: Int = 0 | |
| 56 | while (i < length) { | |
| 57 | if (charAt(i) != slice.charAt(i)) return false | |
| 58 | i += 1 | |
| 59 | } | |
| 60 | true | |
| 61 | case _ => | |
| 62 | false | |
| 63 | } | |
| 64 | ||
| 65 | override def hashCode: Int = { | |
| 66 | var hash: Int = 0x90decade | |
| 67 | var i: Int = start | |
| 68 | while (i < limit) { | |
| 69 | hash = s.charAt(i) + (hash * 103696301) // prime | |
| 70 | i += 1 | |
| 71 | } | |
| 72 | hash | |
| 73 | } | |
| 74 | } | |
| 75 | ||
| 76 | object Slice { | |
| 77 | ||
| 78 | val Empty: Slice = Slice("", 0, 0) | |
| 79 | ||
| 80 | def empty: Slice = Empty | |
| 81 | ||
| 82 | def apply(s: String): Slice = | |
| 83 | new Slice(s, 0, s.length) | |
| 84 | ||
| 85 | def apply(s: String, start: Int, limit: Int): Slice = | |
| 86 | if (start < 0 || limit < start || s.length < limit) { | |
| 87 | throw new IndexOutOfBoundsException(s"invalid slice: start=$start, limit=$limit, length=${s.length}") | |
| 88 | } else { | |
| 89 | new Slice(s, start, limit) | |
| 90 | } | |
| 91 | ||
| 92 | def unsafe(s: String, start: Int, limit: Int): Slice = | |
| 93 | new Slice(s, start, limit) | |
| 94 | } |
| 0 | package jawn | |
| 1 | ||
| 2 | package object util { | |
| 3 | ||
| 4 | /** | |
| 5 | * Parse the given character sequence as a single Long value (64-bit | |
| 6 | * signed integer) in decimal (base-10). | |
| 7 | * | |
| 8 | * Other than "0", leading zeros are not allowed, nor are leading | |
| 9 | * plusses. At most one leading minus is allowed. The value "-0" is | |
| 10 | * allowed, and is interpreted as 0. | |
| 11 | * | |
| 12 | * Stated more precisely, accepted values: | |
| 13 | * | |
| 14 | * - conform to the pattern: -?(0|([1-9][0-9]*)) | |
| 15 | * - are within [-9223372036854775808, 9223372036854775807] | |
| 16 | * | |
| 17 | * This method will throw an `InvalidLong` exception on invalid | |
| 18 | * input. | |
| 19 | */ | |
| 20 | def parseLong(cs: CharSequence): Long = { | |
| 21 | ||
| 22 | // we store the inverse of the positive sum, to ensure we don't | |
| 23 | // incorrectly overflow on Long.MinValue. for positive numbers | |
| 24 | // this inverse sum will be inverted before being returned. | |
| 25 | var inverseSum: Long = 0L | |
| 26 | var inverseSign: Long = -1L | |
| 27 | var i: Int = 0 | |
| 28 | ||
| 29 | if (cs.charAt(0) == '-') { | |
| 30 | inverseSign = 1L | |
| 31 | i = 1 | |
| 32 | } | |
| 33 | ||
| 34 | val len = cs.length | |
| 35 | val size = len - i | |
| 36 | if (i >= len) throw InvalidLong(cs.toString) | |
| 37 | if (size > 19) throw InvalidLong(cs.toString) | |
| 38 | if (cs.charAt(i) == '0' && size > 1) throw InvalidLong(cs.toString) | |
| 39 | ||
| 40 | while (i < len) { | |
| 41 | val digit = cs.charAt(i).toInt - 48 | |
| 42 | if (digit < 0 || 9 < digit) throw InvalidLong(cs.toString) | |
| 43 | inverseSum = inverseSum * 10L - digit | |
| 44 | i += 1 | |
| 45 | } | |
| 46 | ||
| 47 | // detect and throw on overflow | |
| 48 | if (size == 19 && (inverseSum >= 0 || (inverseSum == Long.MinValue && inverseSign < 0))) { | |
| 49 | throw InvalidLong(cs.toString) | |
| 50 | } | |
| 51 | ||
| 52 | inverseSum * inverseSign | |
| 53 | } | |
| 54 | ||
| 55 | /** | |
| 56 | * Parse the given character sequence as a single Long value (64-bit | |
| 57 | * signed integer) in decimal (base-10). | |
| 58 | * | |
| 59 | * For valid inputs, this method produces the same values as | |
| 60 | * `parseLong`. However, by avoiding input validation it is up to | |
| 61 | * 50% faster. | |
| 62 | * | |
| 63 | * For inputs which `parseLong` throws an error on, | |
| 64 | * `parseLongUnsafe` may (or may not) throw an error, or return a | |
| 65 | * bogus value. This method makes no guarantees about how it handles | |
| 66 | * invalid input. | |
| 67 | * | |
| 68 | * This method should only be used on sequences which have already | |
| 69 | * been parsed (e.g. by a Jawn parser). When in doubt, use | |
| 70 | * `parseLong(cs)`, which is still significantly faster than | |
| 71 | * `java.lang.Long.parseLong(cs.toString)`. | |
| 72 | */ | |
| 73 | def parseLongUnsafe(cs: CharSequence): Long = { | |
| 74 | ||
| 75 | // we store the inverse of the positive sum, to ensure we don't | |
| 76 | // incorrectly overflow on Long.MinValue. for positive numbers | |
| 77 | // this inverse sum will be inverted before being returned. | |
| 78 | var inverseSum: Long = 0L | |
| 79 | var inverseSign: Long = -1L | |
| 80 | var i: Int = 0 | |
| 81 | ||
| 82 | if (cs.charAt(0) == '-') { | |
| 83 | inverseSign = 1L | |
| 84 | i = 1 | |
| 85 | } | |
| 86 | ||
| 87 | val len = cs.length | |
| 88 | while (i < len) { | |
| 89 | inverseSum = inverseSum * 10L - (cs.charAt(i).toInt - 48) | |
| 90 | i += 1 | |
| 91 | } | |
| 92 | ||
| 93 | inverseSum * inverseSign | |
| 94 | } | |
| 95 | } |
| 0 | package jawn | |
| 1 | package util | |
| 2 | ||
| 3 | import org.scalatest._ | |
| 4 | import prop._ | |
| 5 | import org.scalacheck._ | |
| 6 | ||
| 7 | import scala.util._ | |
| 8 | ||
| 9 | class ParseLongCheck extends PropSpec with Matchers with PropertyChecks { | |
| 10 | ||
| 11 | case class UniformLong(value: Long) | |
| 12 | ||
| 13 | object UniformLong { | |
| 14 | implicit val arbitraryUniformLong: Arbitrary[UniformLong] = | |
| 15 | Arbitrary(Gen.choose(Long.MinValue, Long.MaxValue).map(UniformLong(_))) | |
| 16 | } | |
| 17 | ||
| 18 | property("both parsers accept on valid input") { | |
| 19 | forAll { (n0: UniformLong, prefix: String, suffix: String) => | |
| 20 | val n = n0.value | |
| 21 | val payload = n.toString | |
| 22 | val s = prefix + payload + suffix | |
| 23 | val i = prefix.length | |
| 24 | val cs = s.subSequence(i, payload.length + i) | |
| 25 | cs.toString shouldBe payload | |
| 26 | parseLong(cs) shouldBe n | |
| 27 | parseLongUnsafe(cs) shouldBe n | |
| 28 | } | |
| 29 | ||
| 30 | forAll { (s: String) => | |
| 31 | Try(parseLong(s)) match { | |
| 32 | case Success(n) => parseLongUnsafe(s) shouldBe n | |
| 33 | case Failure(_) => succeed | |
| 34 | } | |
| 35 | } | |
| 36 | } | |
| 37 | ||
| 38 | property("safe parser fails on invalid input") { | |
| 39 | forAll { (n: Long, m: Long, suffix: String) => | |
| 40 | val s1 = n.toString + suffix | |
| 41 | Try(parseLong(s1)) match { | |
| 42 | case Success(n) => n shouldBe s1.toLong | |
| 43 | case Failure(_) => Try(s1.toLong).isFailure | |
| 44 | } | |
| 45 | ||
| 46 | val s2 = n.toString + (m & 0x7fffffffffffffffL).toString | |
| 47 | Try(parseLong(s2)) match { | |
| 48 | case Success(n) => n shouldBe s2.toLong | |
| 49 | case Failure(_) => Try(s2.toLong).isFailure | |
| 50 | } | |
| 51 | } | |
| 52 | ||
| 53 | Try(parseLong("9223372036854775807")) shouldBe Try(Long.MaxValue) | |
| 54 | Try(parseLong("-9223372036854775808")) shouldBe Try(Long.MinValue) | |
| 55 | Try(parseLong("-0")) shouldBe Try(0L) | |
| 56 | ||
| 57 | assert(Try(parseLong("")).isFailure) | |
| 58 | assert(Try(parseLong("+0")).isFailure) | |
| 59 | assert(Try(parseLong("00")).isFailure) | |
| 60 | assert(Try(parseLong("01")).isFailure) | |
| 61 | assert(Try(parseLong("+1")).isFailure) | |
| 62 | assert(Try(parseLong("-")).isFailure) | |
| 63 | assert(Try(parseLong("--1")).isFailure) | |
| 64 | assert(Try(parseLong("9223372036854775808")).isFailure) | |
| 65 | assert(Try(parseLong("-9223372036854775809")).isFailure) | |
| 66 | } | |
| 67 | ||
| 68 | // NOTE: parseLongUnsafe is not guaranteed to crash, or do anything | |
| 69 | // predictable, on invalid input, so we don't test this direction. | |
| 70 | // Its "unsafe" suffix is there for a reason. | |
| 71 | } |
| 0 | package jawn | |
| 1 | package util | |
| 2 | ||
| 3 | import org.scalatest._ | |
| 4 | import prop._ | |
| 5 | import org.scalacheck._ | |
| 6 | ||
| 7 | import Arbitrary.arbitrary | |
| 8 | ||
| 9 | import scala.util._ | |
| 10 | ||
| 11 | class SliceCheck extends PropSpec with Matchers with PropertyChecks { | |
| 12 | ||
| 13 | val genSlice: Gen[Slice] = { | |
| 14 | val g = arbitrary[String] | |
| 15 | def c(start: Int, end: Int): Gen[Int] = | |
| 16 | if (end <= start) Gen.const(start) | |
| 17 | else Gen.choose(start, end) | |
| 18 | Gen.oneOf( | |
| 19 | g.map(Slice(_)), | |
| 20 | for { s <- g; n = s.length; i <- c(0, n) } yield Slice(s, i, n), | |
| 21 | for { s <- g; n = s.length; j <- c(0, n) } yield Slice(s, 0, j), | |
| 22 | for { s <- g; n = s.length; i <- c(0, n); j <- c(i, n) } yield Slice(s, i, j)) | |
| 23 | } | |
| 24 | ||
| 25 | implicit val arbitrarySlice: Arbitrary[Slice] = | |
| 26 | Arbitrary(genSlice) | |
| 27 | ||
| 28 | def tryEqual[A](got0: => A, expected0: => A): Unit = { | |
| 29 | val got = Try(got0) | |
| 30 | val expected = Try(expected0) | |
| 31 | got match { | |
| 32 | case Success(_) => got shouldBe expected | |
| 33 | case Failure(_) => assert(expected.isFailure) | |
| 34 | } | |
| 35 | } | |
| 36 | ||
| 37 | property("Slice(s, i, j) ~ s.substring(i, j)") { | |
| 38 | forAll { (s: String, i: Int, j: Int) => | |
| 39 | tryEqual( | |
| 40 | Slice(s, i, j).toString, | |
| 41 | s.substring(i, j)) | |
| 42 | } | |
| 43 | } | |
| 44 | ||
| 45 | property("Slice(s, i, j).charAt(k) ~ s.substring(i, j).charAt(k)") { | |
| 46 | forAll { (s: String, i: Int, j: Int, k: Int) => | |
| 47 | tryEqual( | |
| 48 | Slice(s, i, j).charAt(k), | |
| 49 | s.substring(i, j).charAt(k)) | |
| 50 | } | |
| 51 | } | |
| 52 | ||
| 53 | property("slice.length >= 0") { | |
| 54 | forAll { (cs: Slice) => | |
| 55 | cs.length should be >= 0 | |
| 56 | } | |
| 57 | } | |
| 58 | ||
| 59 | property("slice.charAt(i) ~ slice.toString.charAt(i)") { | |
| 60 | forAll { (cs: Slice, i: Int) => | |
| 61 | tryEqual( | |
| 62 | cs.charAt(i), | |
| 63 | cs.toString.charAt(i)) | |
| 64 | } | |
| 65 | } | |
| 66 | ||
| 67 | property("Slice(s, i, j).subSequence(k, l) ~ s.substring(i, j).substring(k, l)") { | |
| 68 | forAll { (s: String, i: Int, j: Int, k: Int, l: Int) => | |
| 69 | tryEqual( | |
| 70 | Slice(s, i, j).subSequence(k, l).toString, | |
| 71 | s.substring(i, j).substring(k, l)) | |
| 72 | } | |
| 73 | } | |
| 74 | ||
| 75 | property("Slice(s) ~ Slice(s, 0, s.length)") { | |
| 76 | forAll { (s: String) => | |
| 77 | tryEqual( | |
| 78 | Slice(s).toString, | |
| 79 | Slice(s, 0, s.length).toString) | |
| 80 | } | |
| 81 | } | |
| 82 | ||
| 83 | property("Slice(s, i, j) => Slice.unsafe(s, i, j)") { | |
| 84 | forAll { (s: String, i: Int, j: Int) => | |
| 85 | Try(Slice(s, i, j).toString) match { | |
| 86 | case Success(r) => r shouldBe Slice.unsafe(s, i, j).toString | |
| 87 | case Failure(_) => succeed | |
| 88 | } | |
| 89 | } | |
| 90 | } | |
| 91 | ||
| 92 | property("x == x") { | |
| 93 | forAll { (x: Slice) => x shouldBe x } | |
| 94 | } | |
| 95 | ||
| 96 | property("(x == y) = (x.toString == y.toString)") { | |
| 97 | forAll { (x: Slice, y: Slice) => | |
| 98 | (x == y) shouldBe (x.toString == y.toString) | |
| 99 | } | |
| 100 | } | |
| 101 | ||
| 102 | property("(x == y) -> (x.## == y.##)") { | |
| 103 | forAll { (x: Slice, y: Slice) => | |
| 104 | if (x == y) x.## shouldBe y.## | |
| 105 | else (x.## == y.##) shouldBe false | |
| 106 | } | |
| 107 | } | |
| 108 | ||
| 109 | property("x == Slice(x.toString)") { | |
| 110 | forAll { (x: Slice) => | |
| 111 | Slice(x.toString) shouldBe x | |
| 112 | } | |
| 113 | } | |
| 114 | ||
| 115 | property("slice is serializable") { | |
| 116 | import java.io._ | |
| 117 | ||
| 118 | forAll { (x: Slice) => | |
| 119 | val baos = new ByteArrayOutputStream() | |
| 120 | val oos = new ObjectOutputStream(baos) | |
| 121 | oos.writeObject(x) | |
| 122 | oos.close() | |
| 123 | val bytes = baos.toByteArray | |
| 124 | val bais = new ByteArrayInputStream(bytes) | |
| 125 | val ois = new ObjectInputStream(bais) | |
| 126 | Try(ois.readObject()) shouldBe Try(x) | |
| 127 | ois.close() | |
| 128 | } | |
| 129 | } | |
| 130 | } |