├── README.md ├── bench └── src │ └── main │ ├── resources │ ├── bar.json │ ├── dkw-sample.json │ └── foo.json │ └── scala │ └── cats │ └── parse │ └── bench │ ├── parsley.scala │ ├── atto.scala │ ├── self.scala │ ├── fastparse.scala │ ├── StringInBench.scala │ ├── JsonBench.scala │ └── parboiled2.scala ├── project ├── build.properties ├── plugins.sbt ├── Dependencies.scala └── MimaExclusionRules.scala ├── PULL_REQUEST_TEMPLATE.md ├── .git-blame-ignore-revs ├── .scalafmt.conf ├── .github ├── workflows │ ├── release-drafter.yml │ ├── clean.yml │ └── ci.yml └── release-drafter.yml ├── Release.md ├── .gitignore ├── LICENSE ├── core ├── js │ └── src │ │ └── main │ │ └── scala │ │ └── cats │ │ └── parse │ │ └── BitSet.scala ├── jvm │ └── src │ │ ├── main │ │ └── scala │ │ │ └── cats │ │ │ └── parse │ │ │ └── BitSet.scala │ │ └── test │ │ └── scala │ │ └── cats │ │ └── parse │ │ ├── JvmNumbersTests.scala │ │ └── JvmStringsTests.scala ├── native │ └── src │ │ └── main │ │ └── scala │ │ └── cats │ │ └── parse │ │ └── BitSet.scala └── shared │ └── src │ ├── main │ └── scala │ │ └── cats │ │ └── parse │ │ ├── StringCodec.scala │ │ ├── Caret.scala │ │ ├── DefaultParser.scala │ │ ├── DefaultParser0.scala │ │ ├── BitSetCompat.scala │ │ ├── SemVer.scala │ │ ├── Numbers.scala │ │ ├── Rfc5234.scala │ │ ├── strings │ │ └── Json.scala │ │ ├── LocationMap.scala │ │ ├── Accumulator.scala │ │ └── RadixNode.scala │ └── test │ └── scala │ └── cats │ └── parse │ ├── SemVerTest.scala │ ├── StringsTest.scala │ ├── AccumulatorTest.scala │ ├── BitSetTest.scala │ ├── NumbersTest.scala │ ├── Rfc5234Test.scala │ ├── ErrorShowTest.scala │ ├── RepParserConstructionTest.scala │ ├── RadixNodeTest.scala │ └── LocationMapTest.scala └── docs └── index.md /README.md: -------------------------------------------------------------------------------- 1 | docs/index.md -------------------------------------------------------------------------------- /bench/src/main/resources/bar.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=1.11.7 2 | -------------------------------------------------------------------------------- /PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | Thank you for contributing to `cats-parse`! 2 | 3 | This is a kind reminder to run `sbt prePR` and commit the changed files, if any, before submitting. 4 | 5 | 6 | -------------------------------------------------------------------------------- /.git-blame-ignore-revs: -------------------------------------------------------------------------------- 1 | # Scala Steward: Reformat with scalafmt 3.7.5 2 | f09f531b864e0dec446f423247b63ecda0235f0a 3 | 4 | # Scala Steward: Reformat with scalafmt 3.8.6 5 | 608a97a3081b4d2b4b71addf68bd56506d13ce01 6 | -------------------------------------------------------------------------------- /.scalafmt.conf: -------------------------------------------------------------------------------- 1 | version = "3.10.2" 2 | 3 | runner.dialect = scala213 4 | 5 | style = default 6 | 7 | maxColumn = 100 8 | 9 | align.preset = none 10 | 11 | fileOverride { 12 | "glob:**/scala-3/**" { 13 | runner.dialect = scala3 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /.github/workflows/release-drafter.yml: -------------------------------------------------------------------------------- 1 | name: Release Drafter 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | update_release_draft: 10 | runs-on: ubuntu-latest 11 | steps: 12 | # Drafts your next Release notes as Pull Requests are merged into "master" 13 | - uses: release-drafter/release-drafter@v5 14 | env: 15 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | addSbtPlugin("org.portable-scala" % "sbt-scala-native-crossproject" % "1.3.2") 2 | addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.5.9") 3 | addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.20.1") 4 | addSbtPlugin("org.scoverage" % "sbt-scoverage" % "2.4.2") 5 | addSbtPlugin("pl.project13.scala" % "sbt-jmh" % "0.4.8") 6 | addSbtPlugin("org.typelevel" % "sbt-typelevel" % "0.8.2") 7 | addSbtPlugin("org.typelevel" % "sbt-typelevel-site" % "0.8.2") 8 | -------------------------------------------------------------------------------- /Release.md: -------------------------------------------------------------------------------- 1 | # How To Release 2 | 1. Create a git tag using `git tag -a vX.Y.Z -m "vX.Y.Z"` 3 | 2. Push the tag using `git push origin vX.Y.Z` 4 | - Doing so will create a Github Action build for the `X.Y.Z` release. 5 | - This Github Action builds the release, checks mima compatibility using the [strictSemVer](https://github.com/djspiewak/sbt-spiewak/blob/709fc19b389394777e61206e4d1b6df69e039e24/core/src/main/scala/sbtspiewak/SpiewakPlugin.scala#L400-L444) option, and then releases to Sonatype. -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.class 2 | *.log 3 | 4 | # sbt specific 5 | .cache 6 | .history 7 | .lib/ 8 | dist/* 9 | target/ 10 | lib_managed/ 11 | src_managed/ 12 | project/boot/ 13 | project/target/ 14 | project/plugins/project/ 15 | 16 | # Scala-IDE specific 17 | .scala_dependencies 18 | .worksheet 19 | .vscode 20 | 21 | # Sublime 22 | *.sublime-project 23 | *.sublime-workspace 24 | 25 | # emacs 26 | TAGS 27 | 28 | # vim 29 | *.swp 30 | 31 | # vscode wants to put metals directories here 32 | .metals 33 | .bloop 34 | .bsp 35 | project/metals.sbt 36 | project/project/metals.sbt 37 | project/project/project/metals.sbt 38 | -------------------------------------------------------------------------------- /project/Dependencies.scala: -------------------------------------------------------------------------------- 1 | import sbt._ 2 | import org.portablescala.sbtplatformdeps.PlatformDepsPlugin.autoImport._ 3 | 4 | object Dependencies { 5 | lazy val cats = Def.setting("org.typelevel" %%% "cats-core" % "2.13.0") 6 | lazy val munit = Def.setting("org.scalameta" %%% "munit" % "1.1.1") 7 | lazy val munitScalacheck = Def.setting("org.scalameta" %%% "munit-scalacheck" % "1.1.0") 8 | lazy val fastParse = "com.lihaoyi" %% "fastparse" % "3.1.1" 9 | lazy val parsley = "org.http4s" %% "parsley" % "1.5.0-M3" 10 | lazy val jawnAst = Def.setting("org.typelevel" %% "jawn-ast" % "1.6.0") 11 | lazy val parboiled = "org.parboiled" %% "parboiled" % "2.5.1" 12 | lazy val attoCore = "org.tpolecat" %% "atto-core" % "0.9.5" 13 | } 14 | -------------------------------------------------------------------------------- /.github/release-drafter.yml: -------------------------------------------------------------------------------- 1 | categories: 2 | - title: '🚀 Enhancements' 3 | label: 'enhancement' 4 | - title: '🐛 Bug Fixes' 5 | label: 'bug' 6 | - title: '📜 Scalafix Migrations' 7 | label: 'scalafix-migration' 8 | - title: '📗 Documentation' 9 | label: 'documentation' 10 | - title: '🧪 Test Improvements' 11 | label: 'testing' 12 | - title: '🏗️ Build Improvements' 13 | label: 'build' 14 | - title: '🔧 Refactorings' 15 | label: 'refactoring' 16 | - title: '🌱 Dependency Updates' 17 | label: 'dependency-update' 18 | exclude-labels: 19 | - 'administration' 20 | - 'repos-update' 21 | template: | 22 | ## What's changed 23 | 24 | $CHANGES 25 | 26 | ## Contributors to this release 27 | 28 | $CONTRIBUTORS -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 typelevel.scala 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /core/js/src/main/scala/cats/parse/BitSet.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021 Typelevel 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy of 5 | * this software and associated documentation files (the "Software"), to deal in 6 | * the Software without restriction, including without limitation the rights to 7 | * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 8 | * the Software, and to permit persons to whom the Software is furnished to do so, 9 | * subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in all 12 | * copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 16 | * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 17 | * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 18 | * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 19 | * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | */ 21 | 22 | package cats.parse 23 | 24 | object BitSetUtil extends BitSetUtilCompat(true, false) 25 | -------------------------------------------------------------------------------- /core/jvm/src/main/scala/cats/parse/BitSet.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021 Typelevel 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy of 5 | * this software and associated documentation files (the "Software"), to deal in 6 | * the Software without restriction, including without limitation the rights to 7 | * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 8 | * the Software, and to permit persons to whom the Software is furnished to do so, 9 | * subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in all 12 | * copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 16 | * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 17 | * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 18 | * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 19 | * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | */ 21 | 22 | package cats.parse 23 | 24 | object BitSetUtil extends BitSetUtilCompat(false, true) 25 | -------------------------------------------------------------------------------- /core/native/src/main/scala/cats/parse/BitSet.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021 Typelevel 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy of 5 | * this software and associated documentation files (the "Software"), to deal in 6 | * the Software without restriction, including without limitation the rights to 7 | * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 8 | * the Software, and to permit persons to whom the Software is furnished to do so, 9 | * subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in all 12 | * copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 16 | * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 17 | * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 18 | * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 19 | * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | */ 21 | 22 | package cats.parse 23 | 24 | object BitSetUtil extends BitSetUtilCompat(false, false) 25 | -------------------------------------------------------------------------------- /core/shared/src/main/scala/cats/parse/StringCodec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021 Typelevel 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy of 5 | * this software and associated documentation files (the "Software"), to deal in 6 | * the Software without restriction, including without limitation the rights to 7 | * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 8 | * the Software, and to permit persons to whom the Software is furnished to do so, 9 | * subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in all 12 | * copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 16 | * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 17 | * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 18 | * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 19 | * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | */ 21 | 22 | package cats.parse 23 | 24 | trait StringCodec[+P[_] <: Parser0[_], A] { 25 | def parser: P[A] 26 | def encode(e: A): String 27 | } 28 | -------------------------------------------------------------------------------- /core/shared/src/test/scala/cats/parse/SemVerTest.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021 Typelevel 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy of 5 | * this software and associated documentation files (the "Software"), to deal in 6 | * the Software without restriction, including without limitation the rights to 7 | * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 8 | * the Software, and to permit persons to whom the Software is furnished to do so, 9 | * subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in all 12 | * copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 16 | * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 17 | * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 18 | * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 19 | * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | */ 21 | 22 | package cats.parse 23 | 24 | import org.scalacheck.Prop.forAll 25 | import org.scalacheck.Gen 26 | 27 | class SemVerTest extends munit.ScalaCheckSuite { 28 | 29 | val genSemVer: Gen[String] = for { 30 | major <- Gen.choose(0, 10) 31 | minor <- Gen.choose(0, 10) 32 | patch <- Gen.choose(0, 100) 33 | preRelease <- Gen.oneOf("", "-alpha", "-beta", "-alpha.1") 34 | buildMetadata <- Gen.oneOf("", "+001", "+20130313144700", "+exp.sha.5114f85", "+21AF26D3") 35 | } yield s"$major.$minor.$patch$preRelease$buildMetadata" 36 | 37 | property("semver parses SemVer") { 38 | forAll(genSemVer) { (sv: String) => 39 | assertEquals(SemVer.semverString.parseAll(sv), Right(sv)) 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /core/shared/src/test/scala/cats/parse/StringsTest.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021 Typelevel 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy of 5 | * this software and associated documentation files (the "Software"), to deal in 6 | * the Software without restriction, including without limitation the rights to 7 | * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 8 | * the Software, and to permit persons to whom the Software is furnished to do so, 9 | * subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in all 12 | * copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 16 | * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 17 | * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 18 | * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 19 | * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | */ 21 | 22 | package cats.parse 23 | 24 | import org.scalacheck.Prop.forAll 25 | 26 | class StringsTest extends munit.ScalaCheckSuite { 27 | val tests: Int = if (BitSetUtil.isScalaJvm) 20000 else 50 28 | 29 | override def scalaCheckTestParameters = 30 | super.scalaCheckTestParameters 31 | .withMinSuccessfulTests(tests) 32 | .withMaxDiscardRatio(10) 33 | 34 | def law[A](sc: StringCodec[Parser0, A], item: A) = { 35 | val str = sc.encode(item) 36 | assertEquals(sc.parser.parseAll(str), Right(item)) 37 | } 38 | 39 | property("json strings round trip") { 40 | forAll { (str: String) => 41 | law(strings.Json.delimited, str) 42 | law(strings.Json.undelimited, str) 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /core/shared/src/test/scala/cats/parse/AccumulatorTest.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021 Typelevel 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy of 5 | * this software and associated documentation files (the "Software"), to deal in 6 | * the Software without restriction, including without limitation the rights to 7 | * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 8 | * the Software, and to permit persons to whom the Software is furnished to do so, 9 | * subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in all 12 | * copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 16 | * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 17 | * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 18 | * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 19 | * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | */ 21 | 22 | package cats.parse 23 | 24 | import org.scalacheck.Gen 25 | import org.scalacheck.Prop.forAll 26 | 27 | class AccumulatorTest extends munit.ScalaCheckSuite { 28 | property("intCounter counts how many times it's invoked") { 29 | forAll(Gen.choose(0, 1000)) { (n: Int) => 30 | { 31 | val intAppender = implicitly[Accumulator0[Any, Int]].newAppender() 32 | for (_ <- 1 to n) intAppender.append(()) 33 | assertEquals(intAppender.finish(), n) 34 | } 35 | } 36 | } 37 | 38 | test("intCounters newAppender returns a new appender") { 39 | val counter = implicitly[Accumulator0[Any, Int]] 40 | val intAppender1 = counter.newAppender() 41 | val intAppender2 = counter.newAppender() 42 | val n = 100 43 | for (_ <- 1 to n) { 44 | intAppender1.append(()) 45 | intAppender2.append(()) 46 | } 47 | assert(intAppender1.finish() == n) 48 | assert(intAppender2.finish() == n) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /core/shared/src/main/scala/cats/parse/Caret.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021 Typelevel 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy of 5 | * this software and associated documentation files (the "Software"), to deal in 6 | * the Software without restriction, including without limitation the rights to 7 | * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 8 | * the Software, and to permit persons to whom the Software is furnished to do so, 9 | * subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in all 12 | * copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 16 | * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 17 | * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 18 | * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 19 | * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | */ 21 | 22 | package cats.parse 23 | 24 | import cats.Order 25 | 26 | /** This is a pointer to a zero based line, column, and total offset. 27 | */ 28 | case class Caret(line: Int, col: Int, offset: Int) 29 | 30 | object Caret { 31 | val Start: Caret = Caret(0, 0, 0) 32 | 33 | /** This order is the same as offset order if the Carets are both from the same input string, but 34 | * if from different ones will compare by line then by column and finally by offset. 35 | */ 36 | implicit val caretOrder: Order[Caret] = 37 | new Order[Caret] { 38 | def compare(left: Caret, right: Caret): Int = { 39 | val c0 = Integer.compare(left.line, right.line) 40 | if (c0 != 0) c0 41 | else { 42 | val c1 = Integer.compare(left.col, right.col) 43 | if (c1 != 0) c1 44 | else Integer.compare(left.offset, right.offset) 45 | } 46 | } 47 | } 48 | 49 | implicit val caretOrdering: Ordering[Caret] = 50 | caretOrder.toOrdering 51 | } 52 | -------------------------------------------------------------------------------- /project/MimaExclusionRules.scala: -------------------------------------------------------------------------------- 1 | import com.typesafe.tools.mima.core.{ 2 | ProblemFilters, 3 | IncompatibleMethTypeProblem, 4 | IncompatibleResultTypeProblem, 5 | DirectMissingMethodProblem, 6 | MissingClassProblem 7 | } 8 | 9 | object MimaExclusionRules { 10 | 11 | private val parserImplIncompatibleMethTypeProblem = 12 | Seq( 13 | "cats.parse.Parser#Impl.mergeCharIn", 14 | "cats.parse.Parser#Impl.mergeStrIn", 15 | "cats.parse.Parser#Impl#CharIn.copy", 16 | "cats.parse.Parser#Impl#CharIn.apply", 17 | "cats.parse.Parser#Impl#CharIn.this" 18 | ).map(ProblemFilters.exclude[IncompatibleMethTypeProblem](_)) 19 | 20 | private val parserImplIncompatibleResultTypeProblem = 21 | Seq( 22 | "cats.parse.Parser#Impl#StringIn.parseMut", 23 | "cats.parse.Parser#Impl.stringIn", 24 | "cats.parse.Parser#Impl#CharIn.copy$default$2", 25 | "cats.parse.Parser#Impl#CharIn.bitSet", 26 | "cats.parse.Parser#Impl#CharIn._2" 27 | ).map(ProblemFilters.exclude[IncompatibleResultTypeProblem](_)) 28 | 29 | private val parserImplDirectMissingMethodProblem = 30 | Seq( 31 | "cats.parse.Parser#Impl.mergeCharIn", 32 | "cats.parse.Parser#Impl.mergeStrIn" 33 | ).map(ProblemFilters.exclude[DirectMissingMethodProblem](_)) 34 | 35 | private val parserImplMissingClassProblem = 36 | Seq( 37 | "cats.parse.Parser$Impl$Rep0", 38 | "cats.parse.Parser$Impl$Rep0$" 39 | ).map(ProblemFilters.exclude[MissingClassProblem](_)) 40 | 41 | val parserImpl = 42 | parserImplIncompatibleMethTypeProblem ++ 43 | parserImplIncompatibleResultTypeProblem ++ 44 | parserImplDirectMissingMethodProblem ++ 45 | parserImplMissingClassProblem 46 | 47 | // TODO: Remove these rules in future release. 48 | private val bitSetUtilIncompatibleMethType = 49 | Seq( 50 | "cats.parse.BitSetUtil.isSingleton", 51 | "cats.parse.BitSetUtil.isSet" 52 | ).map(ProblemFilters.exclude[IncompatibleMethTypeProblem](_)) 53 | 54 | private val bitSetUtilIncompatibleResultType = 55 | Seq( 56 | "cats.parse.BitSetUtil.bitSetFor" 57 | ).map(ProblemFilters.exclude[IncompatibleResultTypeProblem](_)) 58 | 59 | val bitSetUtil = 60 | bitSetUtilIncompatibleMethType ++ 61 | bitSetUtilIncompatibleResultType 62 | } 63 | -------------------------------------------------------------------------------- /core/shared/src/test/scala/cats/parse/BitSetTest.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021 Typelevel 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy of 5 | * this software and associated documentation files (the "Software"), to deal in 6 | * the Software without restriction, including without limitation the rights to 7 | * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 8 | * the Software, and to permit persons to whom the Software is furnished to do so, 9 | * subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in all 12 | * copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 16 | * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 17 | * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 18 | * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 19 | * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | */ 21 | 22 | package cats.parse 23 | 24 | import org.scalacheck.Prop.forAll 25 | 26 | class BitSetTest extends munit.ScalaCheckSuite { 27 | // TODO: Remove isScalaJs/isScalaJvm in next minor version update. See https://github.com/typelevel/cats-parse/issues/391. 28 | test("isScalaJs/isScalaJvm is consistent") { 29 | if (BitSetUtil.isScalaJs || BitSetUtil.isScalaJvm) { 30 | assert(!(BitSetUtil.isScalaJs && BitSetUtil.isScalaJvm)) 31 | assert(BitSetUtil.isScalaJs ^ BitSetUtil.isScalaJvm) 32 | } 33 | } 34 | 35 | property("BitSetUtil union works") { 36 | forAll { (cs: List[List[Char]]) => 37 | val arys = cs.iterator.filter(_.nonEmpty).map(_.toArray.sorted) 38 | val bs = arys.map { ary => (ary(0).toInt, BitSetUtil.bitSetFor(ary)) } 39 | val sortedFlat = BitSetUtil.union(bs) 40 | assertEquals(sortedFlat.toSet, cs.flatten.toSet) 41 | } 42 | } 43 | 44 | property("BitSet.isSingleton is correct") { 45 | forAll { (c0: Char, cs: Set[Char]) => 46 | val set = cs + c0 47 | val bs = BitSetUtil.bitSetFor(set.toArray.sorted) 48 | 49 | assertEquals(BitSetUtil.isSingleton(bs), set.size == 1) 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /.github/workflows/clean.yml: -------------------------------------------------------------------------------- 1 | # This file was automatically generated by sbt-github-actions using the 2 | # githubWorkflowGenerate task. You should add and commit this file to 3 | # your git repository. It goes without saying that you shouldn't edit 4 | # this file by hand! Instead, if you wish to make changes, you should 5 | # change your sbt build configuration to revise the workflow description 6 | # to meet your needs, then regenerate this file. 7 | 8 | name: Clean 9 | 10 | on: push 11 | 12 | jobs: 13 | delete-artifacts: 14 | name: Delete Artifacts 15 | runs-on: ubuntu-latest 16 | env: 17 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 18 | steps: 19 | - name: Delete artifacts 20 | run: | 21 | # Customize those three lines with your repository and credentials: 22 | REPO=${GITHUB_API_URL}/repos/${{ github.repository }} 23 | 24 | # A shortcut to call GitHub API. 25 | ghapi() { curl --silent --location --user _:$GITHUB_TOKEN "$@"; } 26 | 27 | # A temporary file which receives HTTP response headers. 28 | TMPFILE=/tmp/tmp.$$ 29 | 30 | # An associative array, key: artifact name, value: number of artifacts of that name. 31 | declare -A ARTCOUNT 32 | 33 | # Process all artifacts on this repository, loop on returned "pages". 34 | URL=$REPO/actions/artifacts 35 | while [[ -n "$URL" ]]; do 36 | 37 | # Get current page, get response headers in a temporary file. 38 | JSON=$(ghapi --dump-header $TMPFILE "$URL") 39 | 40 | # Get URL of next page. Will be empty if we are at the last page. 41 | URL=$(grep '^Link:' "$TMPFILE" | tr ',' '\n' | grep 'rel="next"' | head -1 | sed -e 's/.*.*//') 42 | rm -f $TMPFILE 43 | 44 | # Number of artifacts on this page: 45 | COUNT=$(( $(jq <<<$JSON -r '.artifacts | length') )) 46 | 47 | # Loop on all artifacts on this page. 48 | for ((i=0; $i < $COUNT; i++)); do 49 | 50 | # Get name of artifact and count instances of this name. 51 | name=$(jq <<<$JSON -r ".artifacts[$i].name?") 52 | ARTCOUNT[$name]=$(( $(( ${ARTCOUNT[$name]} )) + 1)) 53 | 54 | id=$(jq <<<$JSON -r ".artifacts[$i].id?") 55 | size=$(( $(jq <<<$JSON -r ".artifacts[$i].size_in_bytes?") )) 56 | printf "Deleting '%s' #%d, %'d bytes\n" $name ${ARTCOUNT[$name]} $size 57 | ghapi -X DELETE $REPO/actions/artifacts/$id 58 | done 59 | done 60 | -------------------------------------------------------------------------------- /core/shared/src/main/scala/cats/parse/DefaultParser.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021 Typelevel 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy of 5 | * this software and associated documentation files (the "Software"), to deal in 6 | * the Software without restriction, including without limitation the rights to 7 | * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 8 | * the Software, and to permit persons to whom the Software is furnished to do so, 9 | * subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in all 12 | * copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 16 | * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 17 | * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 18 | * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 19 | * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | */ 21 | 22 | package cats.parse 23 | 24 | /** Typeclass for "has a Parser" 25 | * 26 | * This is primarily provided to help keep track of `Parser` instances, and as such the omission of 27 | * a `cats.Functor` instance is intentional. 28 | * @tparam A 29 | */ 30 | trait DefaultParser[+A] { 31 | def parser: Parser[A] 32 | 33 | /** Pass through to equivalent method on `Parser` 34 | * @see 35 | * [[Parser.parse]] 36 | */ 37 | def parse(string: String): Either[Parser.Error, (String, A)] = parser.parse(string) 38 | 39 | /** Pass through to equivalent method on `Parser` 40 | * @see 41 | * [[Parser.parseAll]] 42 | */ 43 | def parseAll(string: String): Either[Parser.Error, A] = parser.parseAll(string) 44 | } 45 | object DefaultParser { 46 | def apply[A](implicit P: DefaultParser[A]): P.type = P 47 | 48 | def instance[A](p: Parser[A]): DefaultParser[A] = new Impl[A](p) 49 | 50 | private final class Impl[+A](override val parser: Parser[A]) 51 | extends DefaultParser[A] 52 | with Serializable 53 | 54 | object syntax { 55 | implicit final class DefaultParserOps(private val raw: String) extends AnyVal { 56 | def parse[A: DefaultParser]: Either[Parser.Error, (String, A)] = 57 | DefaultParser[A].parser.parse(raw) 58 | 59 | def parseAll[A: DefaultParser]: Either[Parser.Error, A] = 60 | DefaultParser[A].parser.parseAll(raw) 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /bench/src/main/scala/cats/parse/bench/parsley.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021 Typelevel 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy of 5 | * this software and associated documentation files (the "Software"), to deal in 6 | * the Software without restriction, including without limitation the rights to 7 | * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 8 | * the Software, and to permit persons to whom the Software is furnished to do so, 9 | * subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in all 12 | * copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 16 | * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 17 | * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 18 | * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 19 | * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | */ 21 | 22 | /* Based on https://github.com/J-mie6/Parsley/blob/e5947782ee007ee9e5fc37eb9a668055e93c7f62/src/parsley/Benchmark.scala */ 23 | package cats.parse.bench.parsley 24 | 25 | import org.http4s.parsley._ 26 | import org.http4s.parsley.Parsley._ 27 | import org.http4s.parsley.Combinator._ 28 | import org.typelevel.jawn.ast._ 29 | 30 | object ParsleyJson { 31 | 32 | def json: Parsley[JValue] = { 33 | val jsontoks = LanguageDef( 34 | "", 35 | "", 36 | "", 37 | false, 38 | NotRequired, 39 | NotRequired, 40 | NotRequired, 41 | NotRequired, 42 | Set.empty, 43 | Set.empty, 44 | true, 45 | Predicate(Char.isWhitespace) 46 | ) 47 | val tok = new TokenParser(jsontoks) 48 | lazy val obj: Parsley[JValue] = tok.braces( 49 | tok.commaSep(+(tok.stringLiteral <~> tok.colon *> value)).map(pairs => JObject.fromSeq(pairs)) 50 | ) 51 | lazy val array: Parsley[JValue] = 52 | tok.brackets(tok.commaSep(value)).map(list => JArray.fromSeq(list)) 53 | lazy val value: Parsley[JValue] = 54 | (tok.stringLiteral.map(JString.apply) 55 | <|> tok.symbol("true") *> Parsley.pure(JTrue) 56 | <|> tok.symbol("false") *> Parsley.pure(JFalse) 57 | <|> tok.symbol("null") *> Parsley.pure(JNull) 58 | <|> array 59 | <|> attempt(tok.float).map(JNum.apply) 60 | <|> tok.integer.map(i => JNum.apply(i.toLong)) 61 | <|> obj) 62 | 63 | tok.whiteSpace *> (obj <|> array) <* eof 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /core/shared/src/main/scala/cats/parse/DefaultParser0.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021 Typelevel 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy of 5 | * this software and associated documentation files (the "Software"), to deal in 6 | * the Software without restriction, including without limitation the rights to 7 | * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 8 | * the Software, and to permit persons to whom the Software is furnished to do so, 9 | * subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in all 12 | * copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 16 | * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 17 | * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 18 | * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 19 | * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | */ 21 | 22 | package cats.parse 23 | 24 | /** Typeclass for "has a Parser0" 25 | * 26 | * This is primarily provided to help keep track of `Parser0` instances, and as such the omission 27 | * of a `cats.Functor` instance is intentional. 28 | * @tparam A 29 | */ 30 | trait DefaultParser0[+A] { 31 | def parser0: Parser0[A] 32 | 33 | /** Pass through to equivalent method on `Parser0` 34 | * @see 35 | * [[Parser0.parse]] 36 | */ 37 | def parse(string: String): Either[Parser.Error, (String, A)] = parser0.parse(string) 38 | 39 | /** Pass through to equivalent method on `Parser0` 40 | * @see 41 | * [[Parser0.parseAll]] 42 | */ 43 | def parseAll(string: String): Either[Parser.Error, A] = parser0.parseAll(string) 44 | } 45 | object DefaultParser0 { 46 | def apply[A](implicit P: DefaultParser0[A]): P.type = P 47 | 48 | def instance[A](p: Parser0[A]): DefaultParser0[A] = new Impl[A](p) 49 | 50 | private final class Impl[+A](override val parser0: Parser0[A]) 51 | extends DefaultParser0[A] 52 | with Serializable 53 | 54 | object syntax { 55 | implicit final class DefaultParser0Ops(private val raw: String) extends AnyVal { 56 | def parse[A: DefaultParser0]: Either[Parser.Error, (String, A)] = 57 | DefaultParser0[A].parser0.parse(raw) 58 | 59 | def parseAll[A: DefaultParser0]: Either[Parser.Error, A] = 60 | DefaultParser0[A].parser0.parseAll(raw) 61 | } 62 | } 63 | 64 | implicit def defaultParserIsDefaultParser0[A: DefaultParser]: DefaultParser0[A] = 65 | DefaultParser0.instance(DefaultParser[A].parser) 66 | } 67 | -------------------------------------------------------------------------------- /core/shared/src/main/scala/cats/parse/BitSetCompat.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021 Typelevel 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy of 5 | * this software and associated documentation files (the "Software"), to deal in 6 | * the Software without restriction, including without limitation the rights to 7 | * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 8 | * the Software, and to permit persons to whom the Software is furnished to do so, 9 | * subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in all 12 | * copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 16 | * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 17 | * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 18 | * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 19 | * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | */ 21 | 22 | package cats.parse 23 | 24 | import java.util.BitSet 25 | private[parse] abstract class BitSetUtilCompat( 26 | // TODO: Remove isScalaJs/isScalaJvm in next minor version update. See https://github.com/typelevel/cats-parse/issues/391. 27 | @inline final val isScalaJs: Boolean, 28 | @inline final val isScalaJvm: Boolean 29 | ) { 30 | type Tpe = BitSet 31 | 32 | @inline final def isSet(b: BitSet, idx: Int): Boolean = 33 | // BitSet can't deal with negatives, so mask those out 34 | b.get(idx & Int.MaxValue) 35 | 36 | // we require a sorted, nonEmpty, charArray 37 | def bitSetFor(charArray: Array[Char]): BitSet = { 38 | val min = charArray(0).toInt 39 | val bs = new BitSet(charArray(charArray.length - 1).toInt + 1 - min) 40 | var idx = 0 41 | while (idx < charArray.length) { 42 | bs.set(charArray(idx).toInt - min) 43 | idx += 1 44 | } 45 | 46 | bs 47 | } 48 | 49 | def isSingleton(t: Tpe): Boolean = t.cardinality() == 1 50 | 51 | // what are all the Chars in these bitsets 52 | def union(bs: List[(Int, BitSet)]): Iterable[Char] = 53 | union(bs.iterator) 54 | 55 | def union(bs: Iterator[(Int, BitSet)]): Iterable[Char] = { 56 | def toIter(m: Int, bs: BitSet): Iterator[Char] = 57 | Iterator 58 | .iterate(0) { m => bs.nextSetBit(m + 1) } 59 | .takeWhile(_ >= 0) 60 | .map { i => (m + i).toChar } 61 | 62 | bs.flatMap { case (m, bs) => toIter(m, bs) }.toSet 63 | } 64 | 65 | def bitSetForRange(count: Int): BitSet = { 66 | val bs = new BitSet(count) 67 | bs.flip(0, count) 68 | bs 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /core/jvm/src/test/scala/cats/parse/JvmNumbersTests.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021 Typelevel 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy of 5 | * this software and associated documentation files (the "Software"), to deal in 6 | * the Software without restriction, including without limitation the rights to 7 | * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 8 | * the Software, and to permit persons to whom the Software is furnished to do so, 9 | * subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in all 12 | * copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 16 | * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 17 | * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 18 | * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 19 | * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | */ 21 | 22 | package cats.parse 23 | 24 | import org.scalacheck.Prop.forAll 25 | import org.typelevel.jawn.ast.{JNum, JParser} 26 | 27 | /** Jawn doesn't publish artifacts for all the versions we support we use jawn to test JSON parsing 28 | * methods 29 | */ 30 | class JvmNumbersTest extends munit.ScalaCheckSuite { 31 | val tests: Int = 20000 32 | 33 | override def scalaCheckTestParameters = 34 | super.scalaCheckTestParameters 35 | .withMinSuccessfulTests(tests) 36 | .withMaxDiscardRatio(10) 37 | 38 | def jawnLaw(a: String) = { 39 | // 2.11 doesn't have toOption 40 | val jn = Numbers.jsonNumber.parseAll(a) match { 41 | case Left(_) => None 42 | case Right(a) => Some(a) 43 | } 44 | val jawn = JParser.parseFromString(a).toOption.collect { case jnum: JNum => jnum } 45 | 46 | assertEquals(jn.isDefined, jawn.isDefined) 47 | 48 | if (jn.isDefined) { 49 | assertEquals(jn.get, a) 50 | assertEquals(BigDecimal(a), jawn.get.asBigDecimal) 51 | } 52 | } 53 | property("jsonNumber parses if and only if Jawn would parse it as a number") { 54 | forAll { (a: String) => jawnLaw(a) } 55 | } 56 | 57 | property("jsonNumber parses if and only if Jawn would parse it as a number (valid Double)") { 58 | forAll { (a: Double) => 59 | if (a.isNaN || a.isInfinite) () 60 | else jawnLaw(a.toString) 61 | } 62 | } 63 | 64 | property("jsonNumber parses if and only if Jawn would parse it as a number (valid BigDecimal)") { 65 | forAll { (a: BigDecimal) => jawnLaw(a.toString) } 66 | } 67 | 68 | property("jsonNumber parses if and only if Jawn would parse it as a number (valid Int)") { 69 | forAll { (a: Int) => jawnLaw(a.toString) } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /bench/src/main/scala/cats/parse/bench/atto.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021 Typelevel 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy of 5 | * this software and associated documentation files (the "Software"), to deal in 6 | * the Software without restriction, including without limitation the rights to 7 | * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 8 | * the Software, and to permit persons to whom the Software is furnished to do so, 9 | * subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in all 12 | * copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 16 | * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 17 | * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 18 | * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 19 | * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | */ 21 | 22 | /* Based on https://github.com/tpolecat/atto/blob/v0.8.0/modules/docs/src/main/scala/json.scala */ 23 | package cats.parse.bench.atto 24 | 25 | import _root_.atto._, Atto._ 26 | import cats.implicits._ 27 | import java.lang.String 28 | import org.typelevel.jawn.ast._ 29 | 30 | object JsonExample extends Whitespace { 31 | // Bracketed, comma-separated sequence, internal whitespace allowed 32 | def seq[A](open: Char, p: Parser[A], close: Char): Parser[List[A]] = 33 | char(open).t ~> sepByT(p, char(',')) <~ char(close) 34 | 35 | // Colon-separated pair, internal whitespace allowed 36 | lazy val pair: Parser[(String, JValue)] = 37 | pairByT(stringLiteral, char(':'), jexpr) 38 | 39 | // Json Expression 40 | lazy val jexpr: Parser[JValue] = delay { 41 | stringLiteral -| JString.apply | 42 | seq('{', pair, '}') -| JObject.fromSeq | 43 | seq('[', jexpr, ']') -| JArray.fromSeq | 44 | double -| JNum.apply | 45 | string("null") >| JNull | 46 | string("true") >| JTrue | 47 | string("false") >| JFalse 48 | } 49 | 50 | } 51 | 52 | // Some extre combinators and syntax for coping with whitespace. Something like this might be 53 | // useful in core but it needs some thought. 54 | trait Whitespace { 55 | 56 | // Syntax for turning a parser into one that consumes trailing whitespace 57 | implicit class TokenOps[A](self: Parser[A]) { 58 | def t: Parser[A] = 59 | self <~ takeWhile(c => c.isSpaceChar || c === '\n') 60 | } 61 | 62 | // Delimited list 63 | def sepByT[A](a: Parser[A], b: Parser[_]): Parser[List[A]] = 64 | sepBy(a.t, b.t) 65 | 66 | // Delimited pair, internal whitespace allowed 67 | def pairByT[A, B](a: Parser[A], delim: Parser[_], b: Parser[B]): Parser[(A, B)] = 68 | pairBy(a.t, delim.t, b) 69 | 70 | } 71 | -------------------------------------------------------------------------------- /core/jvm/src/test/scala/cats/parse/JvmStringsTests.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021 Typelevel 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy of 5 | * this software and associated documentation files (the "Software"), to deal in 6 | * the Software without restriction, including without limitation the rights to 7 | * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 8 | * the Software, and to permit persons to whom the Software is furnished to do so, 9 | * subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in all 12 | * copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 16 | * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 17 | * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 18 | * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 19 | * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | */ 21 | 22 | package cats.parse 23 | 24 | import org.scalacheck.Prop.forAll 25 | import org.scalacheck.Gen 26 | import org.typelevel.jawn.ast.{JString, JParser} 27 | import scala.util.Success 28 | import org.scalacheck.Arbitrary 29 | 30 | /** Jawn doesn't publish artifacts for all the versions we support we use jawn to test JSON parsing 31 | * methods 32 | */ 33 | class JvmStringsTest extends munit.ScalaCheckSuite { 34 | val tests: Int = 200000 35 | 36 | override def scalaCheckTestParameters = 37 | super.scalaCheckTestParameters 38 | .withMinSuccessfulTests(tests) 39 | .withMaxDiscardRatio(10) 40 | 41 | property("JString(str).render parses") { 42 | forAll { (a: String) => 43 | val res = strings.Json.delimited.parser.parseAll(JString(a).render()) 44 | assertEquals(res, Right(a)) 45 | } 46 | } 47 | 48 | property("Strings.jsonEscape(str) parses in Jawn") { 49 | forAll { (a: String) => 50 | val json = strings.Json.delimited.encode(a) 51 | val res = JParser.parseFromString(json).get 52 | assertEquals(res, JString(a)) 53 | } 54 | } 55 | 56 | property("jsonString parses in exactly the same cases as Jawn") { 57 | val genBase = Gen.oneOf(Arbitrary.arbitrary[String], Gen.identifier) 58 | val maybeEscaped = Gen.oneOf(genBase, genBase.map(strings.Json.delimited.encode(_))) 59 | 60 | forAll(maybeEscaped) { (raw: String) => 61 | val resJawn = JParser 62 | .parseFromString(raw) match { 63 | case Success(JString(str)) => Some(str) 64 | case _ => None 65 | } 66 | 67 | val resThis = strings.Json.delimited.parser.parseAll(raw) match { 68 | case Right(r) => Some(r) 69 | case Left(_) => None 70 | } 71 | assertEquals(resJawn, resThis) 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /core/shared/src/test/scala/cats/parse/NumbersTest.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021 Typelevel 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy of 5 | * this software and associated documentation files (the "Software"), to deal in 6 | * the Software without restriction, including without limitation the rights to 7 | * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 8 | * the Software, and to permit persons to whom the Software is furnished to do so, 9 | * subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in all 12 | * copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 16 | * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 17 | * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 18 | * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 19 | * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | */ 21 | 22 | package cats.parse 23 | 24 | import org.scalacheck.Prop.forAll 25 | 26 | class NumbersTest extends munit.ScalaCheckSuite { 27 | val tests: Int = if (BitSetUtil.isScalaJvm) 20000 else 50 28 | 29 | override def scalaCheckTestParameters = 30 | super.scalaCheckTestParameters 31 | .withMinSuccessfulTests(tests) 32 | .withMaxDiscardRatio(10) 33 | 34 | property("bigInt round trips") { 35 | forAll { (bi: BigInt) => 36 | assertEquals(Numbers.bigInt.parseAll(bi.toString), Right(bi)) 37 | } 38 | } 39 | 40 | property("jsonNumber parses Int") { 41 | forAll { (a: Int) => 42 | assertEquals(Numbers.jsonNumber.void.parseAll(a.toString), Right(())) 43 | } 44 | } 45 | 46 | property("jsonNumber parses Long") { 47 | forAll { (a: Long) => 48 | assertEquals(Numbers.jsonNumber.void.parseAll(a.toString), Right(())) 49 | } 50 | } 51 | 52 | property("jsonNumber parses Float") { 53 | forAll { (a: Float) => 54 | if (a.isNaN || a.isInfinite) () 55 | else assertEquals(Numbers.jsonNumber.void.parseAll(a.toString), Right(())) 56 | } 57 | } 58 | 59 | property("jsonNumber parses Double") { 60 | forAll { (a: Double) => 61 | if (a.isNaN || a.isInfinite) () 62 | else assertEquals(Numbers.jsonNumber.void.parseAll(a.toString), Right(())) 63 | } 64 | } 65 | 66 | property("jsonNumber parses BigDecimal") { 67 | forAll { (a: BigDecimal) => 68 | assertEquals(Numbers.jsonNumber.void.parseAll(a.toString), Right(())) 69 | } 70 | } 71 | 72 | property("If jsonNumber parses, then BigDecimal would parse") { 73 | forAll { (a: String) => 74 | Numbers.jsonNumber.void.parseAll(a) match { 75 | case Left(_) => () 76 | case Right(_) => 77 | assertEquals(BigDecimal(a).toString, a) 78 | } 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /bench/src/main/scala/cats/parse/bench/self.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021 Typelevel 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy of 5 | * this software and associated documentation files (the "Software"), to deal in 6 | * the Software without restriction, including without limitation the rights to 7 | * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 8 | * the Software, and to permit persons to whom the Software is furnished to do so, 9 | * subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in all 12 | * copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 16 | * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 17 | * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 18 | * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 19 | * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | */ 21 | 22 | package cats.parse.bench.self 23 | 24 | import cats.parse.{Parser0 => P0, Parser => P, Numbers, strings} 25 | import org.typelevel.jawn.ast._ 26 | 27 | /* Based on https://github.com/johnynek/bosatsu/blob/7f4b75356c207b0e0eb2ab7d39f646e04b4004ca/core/src/main/scala/org/bykn/bosatsu/Json.scala */ 28 | object Json { 29 | private[this] val whitespace: P[Unit] = P.charIn(" \t\r\n").void 30 | private[this] val whitespaces0: P0[Unit] = whitespace.rep0.void 31 | 32 | /** This doesn't have to be super fast (but is fairly fast) since we use it in places where speed 33 | * won't matter: feeding it into a program that will convert it to bosatsu structured data 34 | */ 35 | val parser: P[JValue] = P.recursive[JValue] { recurse => 36 | val pnull = P.string("null").as(JNull) 37 | val bool = P.string("true").as(JBool.True).orElse(P.string("false").as(JBool.False)) 38 | val justStr = strings.Json.delimited.parser 39 | val str = justStr.map(JString(_)) 40 | val num = Numbers.jsonNumber.map(JNum(_)) 41 | 42 | val listSep: P[Unit] = 43 | P.char(',').soft.surroundedBy(whitespaces0).void 44 | 45 | def rep0[A](pa: P[A]): P0[List[A]] = 46 | pa.repSep0(listSep).surroundedBy(whitespaces0) 47 | 48 | val list = rep0(recurse).with1 49 | .between(P.char('['), P.char(']')) 50 | .map { vs => JArray.fromSeq(vs) } 51 | 52 | val kv: P[(String, JValue)] = 53 | justStr ~ (P.char(':').surroundedBy(whitespaces0) *> recurse) 54 | 55 | val obj = rep0(kv).with1 56 | .between(P.char('{'), P.char('}')) 57 | .map { vs => JObject.fromSeq(vs) } 58 | 59 | P.oneOf(str :: num :: list :: obj :: bool :: pnull :: Nil) 60 | } 61 | 62 | // any whitespace followed by json followed by whitespace followed by end 63 | val parserFile: P[JValue] = parser.between(whitespaces0, whitespaces0 ~ P.end) 64 | } 65 | -------------------------------------------------------------------------------- /bench/src/main/scala/cats/parse/bench/fastparse.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021 Typelevel 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy of 5 | * this software and associated documentation files (the "Software"), to deal in 6 | * the Software without restriction, including without limitation the rights to 7 | * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 8 | * the Software, and to permit persons to whom the Software is furnished to do so, 9 | * subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in all 12 | * copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 16 | * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 17 | * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 18 | * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 19 | * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | */ 21 | 22 | /* Based on http://www.lihaoyi.com/fastparse/#Json */ 23 | package cats.parse.bench.fastparse 24 | 25 | import org.typelevel.jawn.ast._ 26 | import _root_.fastparse._, NoWhitespace._ 27 | 28 | object JsonParse { 29 | def stringChars(c: Char) = c != '\"' && c != '\\' 30 | 31 | def space(implicit ctx: P[_]) = P(CharsWhileIn(" \r\n", 0)) 32 | def digits(implicit ctx: P[_]) = P(CharsWhileIn("0-9")) 33 | def exponent(implicit ctx: P[_]) = P(CharIn("eE") ~ CharIn("+\\-").? ~ digits) 34 | def fractional(implicit ctx: P[_]) = P("." ~ digits) 35 | def integral(implicit ctx: P[_]) = P("0" | CharIn("1-9") ~ digits.?) 36 | 37 | def number(implicit ctx: P[_]) = 38 | P(CharIn("+\\-").? ~ integral ~ fractional.? ~ exponent.?).!.map(x => JNum(x.toDouble)) 39 | 40 | def `null`(implicit ctx: P[_]) = P("null").map(_ => JNull) 41 | def `false`(implicit ctx: P[_]) = P("false").map(_ => JFalse) 42 | def `true`(implicit ctx: P[_]) = P("true").map(_ => JTrue) 43 | 44 | def hexDigit(implicit ctx: P[_]) = P(CharIn("0-9a-fA-F")) 45 | def unicodeEscape(implicit ctx: P[_]) = P("u" ~ hexDigit ~ hexDigit ~ hexDigit ~ hexDigit) 46 | def escape(implicit ctx: P[_]) = P("\\" ~ (CharIn("\"/\\\\bfnrt") | unicodeEscape)) 47 | 48 | def strChars(implicit ctx: P[_]) = P(CharsWhile(stringChars)) 49 | def string(implicit ctx: P[_]) = 50 | P(space ~ "\"" ~/ (strChars | escape).rep.! ~ "\"").map(JString.apply) 51 | 52 | def array(implicit ctx: P[_]) = 53 | P("[" ~/ jsonExpr.rep(sep = ","./) ~ space ~ "]").map(JArray.fromSeq) 54 | 55 | def pair(implicit ctx: P[_]) = P(string.map(_.asString) ~/ ":" ~/ jsonExpr) 56 | 57 | def obj(implicit ctx: P[_]) = 58 | P("{" ~/ pair.rep(sep = ","./) ~ space ~ "}").map(JObject.fromSeq) 59 | 60 | def jsonExpr(implicit ctx: P[_]): P[JValue] = P( 61 | space ~ (obj | array | string | `true` | `false` | `null` | number) ~ space 62 | ) 63 | } 64 | -------------------------------------------------------------------------------- /core/shared/src/main/scala/cats/parse/SemVer.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021 Typelevel 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy of 5 | * this software and associated documentation files (the "Software"), to deal in 6 | * the Software without restriction, including without limitation the rights to 7 | * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 8 | * the Software, and to permit persons to whom the Software is furnished to do so, 9 | * subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in all 12 | * copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 16 | * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 17 | * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 18 | * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 19 | * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | */ 21 | 22 | package cats.parse 23 | 24 | import cats.implicits._ 25 | 26 | /** SemVer 2.0.0 Parser based on https://semver.org */ 27 | object SemVer { 28 | 29 | case class Core(major: String, minor: String, patch: String) 30 | 31 | case class SemVer(core: Core, preRelease: Option[String], buildMetadata: Option[String]) 32 | 33 | val dot: Parser[Char] = Parser.charIn('.') 34 | val hyphen: Parser[Char] = Parser.charIn('-') 35 | val plus: Parser[Char] = Parser.charIn('+') 36 | 37 | val letter: Parser[Char] = Parser.ignoreCaseCharIn('a' to 'z') 38 | 39 | val nonDigit: Parser[Char] = letter | hyphen 40 | 41 | val identifierChar: Parser[Char] = Numbers.digit | nonDigit 42 | 43 | val identifierChars: Parser[String] = identifierChar.rep.string 44 | 45 | def numericIdentifier: Parser[String] = Numbers.nonNegativeIntString 46 | 47 | val alphanumericIdentifier: Parser[String] = identifierChars 48 | 49 | val buildIdentifier: Parser[String] = alphanumericIdentifier 50 | 51 | val preReleaseIdentifier: Parser[String] = alphanumericIdentifier 52 | 53 | val dotSeparatedBuildIdentifiers: Parser[String] = buildIdentifier.repSep(dot).string 54 | 55 | val build: Parser[String] = dotSeparatedBuildIdentifiers 56 | 57 | val dotSeparatedPreReleaseIdentifiers: Parser[String] = 58 | preReleaseIdentifier.repSep(dot).string 59 | 60 | val preRelease: Parser[String] = dotSeparatedPreReleaseIdentifiers 61 | 62 | val patch: Parser[String] = numericIdentifier 63 | val minor: Parser[String] = numericIdentifier 64 | val major: Parser[String] = numericIdentifier 65 | 66 | val core: Parser[Core] = (major, dot *> minor, dot *> patch).mapN(Core.apply) 67 | val coreString: Parser[String] = core.string 68 | 69 | val semver: Parser[SemVer] = 70 | (core ~ (hyphen *> preRelease).? ~ (plus *> build).?).map { case ((c, p), b) => 71 | SemVer(c, p, b) 72 | } 73 | val semverString: Parser[String] = semver.string 74 | } 75 | -------------------------------------------------------------------------------- /core/shared/src/main/scala/cats/parse/Numbers.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021 Typelevel 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy of 5 | * this software and associated documentation files (the "Software"), to deal in 6 | * the Software without restriction, including without limitation the rights to 7 | * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 8 | * the Software, and to permit persons to whom the Software is furnished to do so, 9 | * subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in all 12 | * copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 16 | * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 17 | * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 18 | * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 19 | * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | */ 21 | 22 | package cats.parse 23 | 24 | object Numbers { 25 | 26 | /** a single base 10 digit 27 | */ 28 | val digit: Parser[Char] = 29 | Parser.charIn('0' to '9') 30 | 31 | /** zero or more digit chars 32 | */ 33 | val digits0: Parser0[String] = digit.repAs0 34 | 35 | /** one or more digit chars 36 | */ 37 | val digits: Parser[String] = digit.repAs 38 | 39 | /** a single base 10 digit excluding 0 40 | */ 41 | val nonZeroDigit: Parser[Char] = 42 | Parser.charIn('1' to '9') 43 | 44 | /** A String of either 1 '0' or 1 non-zero digit followed by zero or more digits 45 | */ 46 | val nonNegativeIntString: Parser[String] = 47 | (nonZeroDigit ~ digits0).void 48 | .orElse(Parser.char('0')) 49 | .string 50 | 51 | /** A nonNegativeIntString possibly preceded by '-' 52 | */ 53 | val signedIntString: Parser[String] = 54 | (Parser.char('-').?.with1 ~ nonNegativeIntString).string 55 | 56 | /** map a signedIntString into a BigInt 57 | */ 58 | val bigInt: Parser[BigInt] = 59 | signedIntString.map(BigInt(_)) 60 | 61 | /** A string matching the json specification for numbers. from: 62 | * https://tools.ietf.org/html/rfc4627 63 | */ 64 | val jsonNumber: Parser[String] = { 65 | /* 66 | * number = [ minus ] int [ frac ] [ exp ] 67 | * decimal-point = %x2E ; . 68 | * digit1-9 = %x31-39 ; 1-9 69 | * e = %x65 / %x45 ; e E 70 | * exp = e [ minus / plus ] 1*DIGIT 71 | * frac = decimal-point 1*DIGIT 72 | * int = zero / ( digit1-9 *DIGIT ) 73 | * minus = %x2D ; - 74 | * plus = %x2B ; + 75 | * zero = %x30 ; 0 76 | */ 77 | val frac: Parser[Any] = Parser.char('.') ~ digits 78 | val exp: Parser[Unit] = (Parser.charIn("eE") ~ Parser.charIn("+-").? ~ digits).void 79 | 80 | (signedIntString ~ frac.? ~ exp.?).string 81 | } 82 | 83 | } 84 | -------------------------------------------------------------------------------- /bench/src/main/scala/cats/parse/bench/StringInBench.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021 Typelevel 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy of 5 | * this software and associated documentation files (the "Software"), to deal in 6 | * the Software without restriction, including without limitation the rights to 7 | * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 8 | * the Software, and to permit persons to whom the Software is furnished to do so, 9 | * subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in all 12 | * copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 16 | * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 17 | * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 18 | * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 19 | * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | */ 21 | 22 | package cats.parse 23 | package bench 24 | 25 | import cats.parse.Parser 26 | import java.util.concurrent.TimeUnit 27 | import org.openjdk.jmh.annotations._ 28 | 29 | @State(Scope.Benchmark) 30 | @BenchmarkMode(Array(Mode.AverageTime)) 31 | @OutputTimeUnit(TimeUnit.NANOSECONDS) 32 | private[parse] class StringInBenchmarks { 33 | @Param(Array("foo", "broad")) 34 | var test: String = _ 35 | 36 | var inputs: List[String] = _ 37 | 38 | var stringsToMatch: List[String] = _ 39 | 40 | var radixNode: RadixNode = _ 41 | 42 | var stringInV: Parser[Unit] = _ 43 | 44 | var stringInS: Parser[String] = _ 45 | 46 | var oneOf: Parser[Unit] = _ 47 | 48 | @Setup(Level.Trial) 49 | def setup(): Unit = { 50 | if (test == "foo") { 51 | inputs = List("foofoo", "bar", "foobat", "foot", "foobar") 52 | stringsToMatch = List("foobar", "foofoo", "foobaz", "foo", "bar") 53 | } else if (test == "broad") { 54 | // test all lower ascii strings like aaaa, aaab, aaac, ... bbba, bbbb, bbbc, ... 55 | stringsToMatch = (for { 56 | h <- 'a' to 'z' 57 | t <- 'a' to 'z' 58 | } yield s"$h$h$h$t").toList 59 | 60 | // take 10% of the inputs 61 | inputs = stringsToMatch.filter(_.hashCode % 10 == 0) 62 | } 63 | 64 | radixNode = RadixNode.fromStrings(stringsToMatch) 65 | stringInS = Parser.stringIn(stringsToMatch) 66 | stringInV = stringInS.void 67 | oneOf = Parser.oneOf(stringsToMatch.map(Parser.string(_))) 68 | } 69 | 70 | @Benchmark 71 | def stringInVParse(): Unit = 72 | inputs.foreach(stringInV.parseAll(_)) 73 | 74 | @Benchmark 75 | def stringInSParse(): Unit = 76 | inputs.foreach(stringInS.parseAll(_)) 77 | 78 | @Benchmark 79 | def oneOfParse(): Unit = 80 | inputs.foreach(oneOf.parseAll(_)) 81 | 82 | @Benchmark 83 | def radixMatchIn(): Unit = 84 | inputs.foreach { s => radixNode.matchAt(s, 0) >= 0 } 85 | 86 | @Benchmark 87 | def linearMatchIn(): Unit = 88 | inputs.foreach { s => stringsToMatch.exists(s.startsWith(_)) } 89 | } 90 | -------------------------------------------------------------------------------- /bench/src/main/scala/cats/parse/bench/JsonBench.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021 Typelevel 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy of 5 | * this software and associated documentation files (the "Software"), to deal in 6 | * the Software without restriction, including without limitation the rights to 7 | * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 8 | * the Software, and to permit persons to whom the Software is furnished to do so, 9 | * subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in all 12 | * copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 16 | * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 17 | * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 18 | * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 19 | * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | */ 21 | 22 | package cats.parse.bench 23 | 24 | import java.util.concurrent.TimeUnit 25 | import org.openjdk.jmh.annotations._ 26 | import org.typelevel.jawn.ast.JValue 27 | import scala.io.Source 28 | 29 | /* Based on https://github.com/typelevel/jawn/blob/v1.0.0/benchmark/src/main/scala/jawn/JmhBenchmarks.scala */ 30 | @State(Scope.Benchmark) 31 | @BenchmarkMode(Array(Mode.AverageTime)) 32 | @OutputTimeUnit(TimeUnit.MILLISECONDS) 33 | abstract class JmhBenchmarks(name: String) { 34 | val text: String = 35 | Source.fromResource(name, getClass.getClassLoader).getLines().mkString("\n") 36 | 37 | // @Benchmark too slow 38 | def attoParse(): JValue = { 39 | import _root_.atto._, Atto._ 40 | cats.parse.bench.atto.JsonExample.jexpr.parseOnly(text).option.get 41 | } 42 | 43 | @Benchmark 44 | def catsParseParse(): JValue = 45 | self.Json.parser.parse(text) match { 46 | case Right((_, json)) => json 47 | case Left(e) => sys.error(e.toString) 48 | } 49 | 50 | @Benchmark 51 | def fastparseParse(): JValue = 52 | _root_.fastparse.parse(text, fastparse.JsonParse.jsonExpr(_)).get.value 53 | 54 | @Benchmark 55 | def jawnParse(): JValue = 56 | org.typelevel.jawn.ast.JParser.parseFromString(text).get 57 | 58 | @Benchmark 59 | def parboiled2Parse(): JValue = 60 | new parboiled2.JsonParser(text).Json.run().get 61 | 62 | @Benchmark 63 | def parsleyParseCold(): JValue = 64 | org.http4s.parsley.runParserThreadSafe(parsley.ParsleyJson.json, text).toOption.get 65 | 66 | // Stable instance to warm up 67 | val hotParsley = parsley.ParsleyJson.json 68 | 69 | // @Benchmark Failing when multithreaded 70 | def parsleyParseHot(): JValue = 71 | org.http4s.parsley.runParserThreadSafe(hotParsley, text).toOption.get 72 | } 73 | 74 | class BarBench extends JmhBenchmarks("bar.json") 75 | class Qux2Bench extends JmhBenchmarks("qux2.json") 76 | class Bla25Bench extends JmhBenchmarks("bla25.json") 77 | class CountriesBench extends JmhBenchmarks("countries.geo.json") 78 | class Ugh10kBench extends JmhBenchmarks("ugh10k.json") 79 | -------------------------------------------------------------------------------- /core/shared/src/main/scala/cats/parse/Rfc5234.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021 Typelevel 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy of 5 | * this software and associated documentation files (the "Software"), to deal in 6 | * the Software without restriction, including without limitation the rights to 7 | * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 8 | * the Software, and to permit persons to whom the Software is furnished to do so, 9 | * subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in all 12 | * copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 16 | * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 17 | * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 18 | * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 19 | * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | */ 21 | 22 | package cats.parse 23 | 24 | /** Parsers for the common rules of RFC5234. These rules are referenced by several RFCs. 25 | * 26 | * @see 27 | * [[https://tools.ietf.org/html/rfc5234]] 28 | */ 29 | object Rfc5234 { 30 | 31 | /** A-Z and a-z, without diacritics 32 | */ 33 | val alpha: Parser[Char] = 34 | Parser.charIn('A' to 'Z') | Parser.charIn('a' to 'z') 35 | 36 | /** `0` or `1` 37 | */ 38 | val bit: Parser[Char] = 39 | Parser.charIn('0' to '1') 40 | 41 | /** any 7-bit US-ASCII character, excluding NUL 42 | */ 43 | val char: Parser[Char] = 44 | Parser.charIn(0x01.toChar to 0x7f.toChar) 45 | 46 | /** carriage return 47 | */ 48 | val cr: Parser[Unit] = 49 | Parser.char('\r') 50 | 51 | /** linefeed 52 | */ 53 | val lf: Parser[Unit] = 54 | Parser.char('\n') 55 | 56 | /** Internet standard newline */ 57 | val crlf: Parser[Unit] = 58 | Parser.string("\r\n") 59 | 60 | /** controls */ 61 | val ctl: Parser[Char] = 62 | Parser.charIn(0x7f, (0x00.toChar to 0x1f.toChar): _*) 63 | 64 | /** `0` to `9` 65 | */ 66 | val digit: Parser[Char] = 67 | Numbers.digit 68 | 69 | /** double quote (`"`) 70 | */ 71 | val dquote: Parser[Unit] = 72 | Parser.char('"') 73 | 74 | /** hexadecimal digit, case insensitive 75 | */ 76 | val hexdig: Parser[Char] = 77 | digit | Parser.ignoreCaseCharIn('A' to 'F') 78 | 79 | /** horizontal tab 80 | */ 81 | val htab: Parser[Unit] = 82 | Parser.char('\t') 83 | 84 | /** space */ 85 | val sp: Parser[Unit] = 86 | Parser.char(' ') 87 | 88 | /** white space (space or horizontal tab) */ 89 | val wsp: Parser[Unit] = sp | htab 90 | 91 | /** linear white space. 92 | * 93 | * Use of this rule permits lines containing only white space that are no longer legal in mail 94 | * headers and have caused interoperability problems in other contexts. 95 | * 96 | * Do not use when defining mail headers and use with caution in other contexts. 97 | */ 98 | val lwsp: Parser0[Unit] = 99 | Parser.repAs0[Unit, List[Unit]](wsp.orElse(crlf *> wsp)).void 100 | 101 | /** 8 bits of data 102 | */ 103 | val octet: Parser[Char] = 104 | Parser.charIn(0x00.toChar to 0xff.toChar) 105 | 106 | /** visible (printing) characters 107 | */ 108 | val vchar: Parser[Char] = 109 | Parser.charIn(0x21.toChar to 0x7e.toChar) 110 | } 111 | -------------------------------------------------------------------------------- /bench/src/main/resources/dkw-sample.json: -------------------------------------------------------------------------------- 1 | { 2 | "webapp": { 3 | "servlet": [ 4 | { 5 | "servletname": "cofaxCDS", 6 | "servletclass": "org.cofax.cds.CDSServlet", 7 | "initparam": { 8 | "configGlossaryinstallationAt": "Philadelphia, PA", 9 | "configGlossaryadminEmail": "ksm@pobox.com", 10 | "configGlossarypoweredBy": "Cofax", 11 | "configGlossarypoweredByIcon": "/images/cofax.gif", 12 | "configGlossarystaticPath": "/content/static", 13 | "templateProcessorClass": "org.cofax.WysiwygTemplate", 14 | "templateLoaderClass": "org.cofax.FilesTemplateLoader", 15 | "templatePath": "templates", 16 | "templateOverridePath": "", 17 | "defaultListTemplate": "listTemplate.htm", 18 | "defaultFileTemplate": "articleTemplate.htm", 19 | "useJSP": false, 20 | "jspListTemplate": "listTemplate.jsp", 21 | "jspFileTemplate": "articleTemplate.jsp", 22 | "cachePackageTagsTrack": 200, 23 | "cachePackageTagsStore": 200, 24 | "cachePackageTagsRefresh": 60, 25 | "cacheTemplatesTrack": 100, 26 | "cacheTemplatesStore": 50, 27 | "cacheTemplatesRefresh": 15, 28 | "cachePagesTrack": 200, 29 | "cachePagesStore": 100, 30 | "cachePagesRefresh": 10, 31 | "cachePagesDirtyRead": 10, 32 | "searchEngineListTemplate": "forSearchEnginesList.htm", 33 | "searchEngineFileTemplate": "forSearchEngines.htm", 34 | "searchEngineRobotsDb": "WEB-INF/robots.db", 35 | "useDataStore": true, 36 | "dataStoreClass": "org.cofax.SqlDataStore", 37 | "redirectionClass": "org.cofax.SqlRedirection", 38 | "dataStoreName": "cofax", 39 | "dataStoreDriver": "com.microsoft.jdbc.sqlserver.SQLServerDriver", 40 | "dataStoreUrl": "jdbc:microsoft:sqlserver://LOCALHOST:1433;DatabaseName=goon", 41 | "dataStoreUser": "sa", 42 | "dataStorePassword": "dataStoreTestQuery", 43 | "dataStoreTestQuery": "SET NOCOUNT ON;select test='test';", 44 | "dataStoreLogFile": "/usr/local/tomcat/logs/datastore.log", 45 | "dataStoreInitConns": 10, 46 | "dataStoreMaxConns": 100, 47 | "dataStoreConnUsageLimit": 100, 48 | "dataStoreLogLevel": "debug", 49 | "maxUrlLength": 500 50 | } 51 | }, 52 | { 53 | "servletname": "cofaxEmail", 54 | "servletclass": "org.cofax.cds.EmailServlet", 55 | "initparam": { 56 | "mailHost": "mail1", 57 | "mailHostOverride": "mail2" 58 | } 59 | }, 60 | { 61 | "servletname": "cofaxAdmin", 62 | "servletclass": "org.cofax.cds.AdminServlet" 63 | }, 64 | { 65 | "servletname": "fileServlet", 66 | "servletclass": "org.cofax.cds.FileServlet" 67 | }, 68 | { 69 | "servletname": "cofaxTools", 70 | "servletclass": "org.cofax.cms.CofaxToolsServlet", 71 | "initparam": { 72 | "templatePath": "toolstemplates/", 73 | "log": 1, 74 | "logLocation": "/usr/local/tomcat/logs/CofaxTools.log", 75 | "logMaxSize": "", 76 | "dataLog": 1, 77 | "dataLogLocation": "/usr/local/tomcat/logs/dataLog.log", 78 | "dataLogMaxSize": "", 79 | "removePageCache": "/content/admin/remove?cache=pages&id=", 80 | "removeTemplateCache": "/content/admin/remove?cache=templates&id=", 81 | "fileTransferFolder": "/usr/local/tomcat/webapps/content/fileTransferFolder", 82 | "lookInContext": 1, 83 | "adminGroupID": 4, 84 | "betaServer": true 85 | } 86 | } 87 | ], 88 | "servletmapping": { 89 | "cofaxCDS": "/", 90 | "cofaxEmail": "/cofaxutil/aemail/*", 91 | "cofaxAdmin": "/admin/*", 92 | "fileServlet": "/static/*", 93 | "cofaxTools": "/tools/*" 94 | }, 95 | "taglib": { 96 | "tagliburi": "cofax.tld", 97 | "tagliblocation": "/WEB-INF/tlds/cofax.tld" 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /bench/src/main/scala/cats/parse/bench/parboiled2.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021 Typelevel 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy of 5 | * this software and associated documentation files (the "Software"), to deal in 6 | * the Software without restriction, including without limitation the rights to 7 | * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 8 | * the Software, and to permit persons to whom the Software is furnished to do so, 9 | * subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in all 12 | * copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 16 | * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 17 | * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 18 | * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 19 | * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | */ 21 | 22 | /* Based on https://github.com/sirthias/parboiled2/blob/v2.2.1/examples/src/main/scala/org/parboiled2/examples/JsonParser.scala */ 23 | package cats.parse.bench.parboiled2 24 | 25 | import scala.annotation.switch 26 | import org.parboiled2._ 27 | import org.typelevel.jawn.ast._ 28 | 29 | /** This is a feature-complete JSON parser implementation that almost directly models the JSON 30 | * grammar presented at http://www.json.org as a parboiled2 PEG parser. 31 | */ 32 | class JsonParser(val input: ParserInput) extends Parser with StringBuilding { 33 | import CharPredicate.{Digit, Digit19, HexDigit} 34 | import JsonParser._ 35 | 36 | // the root rule 37 | def Json = rule(WhiteSpace ~ Value ~ EOI) 38 | 39 | def JsonObject: Rule1[JObject] = 40 | rule { 41 | ws('{') ~ zeroOrMore(Pair).separatedBy(ws(',')) ~ ws('}') ~> ( 42 | (fields: Seq[(String, JValue)]) => JObject.fromSeq(fields) 43 | ) 44 | } 45 | 46 | def Pair = rule(JsonStringUnwrapped ~ ws(':') ~ Value ~> ((_, _))) 47 | 48 | def Value: Rule1[JValue] = 49 | rule { 50 | // as an optimization of the equivalent rule: 51 | // JsonString | JsonNumber | JsonObject | JsonArray | JsonTrue | JsonFalse | JsonNull 52 | // we make use of the fact that one-char lookahead is enough to discriminate the cases 53 | run { 54 | (cursorChar: @switch) match { 55 | case '"' => JsonString 56 | case '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' | '-' => JsonNumber 57 | case '{' => JsonObject 58 | case '[' => JsonArray 59 | case 't' => JsonTrue 60 | case 'f' => JsonFalse 61 | case 'n' => JsonNull 62 | case _ => MISMATCH 63 | } 64 | } 65 | } 66 | 67 | def JsonString = rule(JsonStringUnwrapped ~> (JString(_))) 68 | 69 | def JsonStringUnwrapped = rule('"' ~ clearSB() ~ Characters ~ ws('"') ~ push(sb.toString)) 70 | 71 | def JsonNumber = rule(capture(Integer ~ optional(Frac) ~ optional(Exp)) ~> (JNum(_)) ~ WhiteSpace) 72 | 73 | def JsonArray = rule( 74 | ws('[') ~ zeroOrMore(Value).separatedBy(ws(',')) ~ ws(']') ~> (JArray.fromSeq(_)) 75 | ) 76 | 77 | def Characters = rule(zeroOrMore(NormalChar | '\\' ~ EscapedChar)) 78 | 79 | def NormalChar = rule(!QuoteBackslash ~ ANY ~ appendSB()) 80 | 81 | def EscapedChar = 82 | rule( 83 | QuoteSlashBackSlash ~ appendSB() 84 | | 'b' ~ appendSB('\b') 85 | | 'f' ~ appendSB('\f') 86 | | 'n' ~ appendSB('\n') 87 | | 'r' ~ appendSB('\r') 88 | | 't' ~ appendSB('\t') 89 | | Unicode ~> { code => sb.append(code.asInstanceOf[Char]); () } 90 | ) 91 | 92 | def Unicode = rule( 93 | 'u' ~ capture(HexDigit ~ HexDigit ~ HexDigit ~ HexDigit) ~> (java.lang.Integer.parseInt(_, 16)) 94 | ) 95 | 96 | def Integer = rule(optional('-') ~ (Digit19 ~ Digits | Digit)) 97 | 98 | def Digits = rule(oneOrMore(Digit)) 99 | 100 | def Frac = rule("." ~ Digits) 101 | 102 | def Exp = rule(ignoreCase('e') ~ optional(anyOf("+-")) ~ Digits) 103 | 104 | def JsonTrue = rule("true" ~ WhiteSpace ~ push(JTrue)) 105 | 106 | def JsonFalse = rule("false" ~ WhiteSpace ~ push(JFalse)) 107 | 108 | def JsonNull = rule("null" ~ WhiteSpace ~ push(JNull)) 109 | 110 | def WhiteSpace = rule(zeroOrMore(WhiteSpaceChar)) 111 | 112 | def ws(c: Char) = rule(c ~ WhiteSpace) 113 | } 114 | 115 | object JsonParser { 116 | val WhiteSpaceChar = CharPredicate(" \n\r\t\f") 117 | val QuoteBackslash = CharPredicate("\"\\") 118 | val QuoteSlashBackSlash = QuoteBackslash ++ "/" 119 | } 120 | -------------------------------------------------------------------------------- /core/shared/src/main/scala/cats/parse/strings/Json.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021 Typelevel 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy of 5 | * this software and associated documentation files (the "Software"), to deal in 6 | * the Software without restriction, including without limitation the rights to 7 | * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 8 | * the Software, and to permit persons to whom the Software is furnished to do so, 9 | * subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in all 12 | * copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 16 | * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 17 | * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 18 | * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 19 | * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | */ 21 | 22 | package cats.parse.strings 23 | 24 | import cats.parse.{Parser, Parser0, StringCodec} 25 | 26 | /** Parsers for common string literals 27 | */ 28 | object Json { 29 | 30 | /** Parses a quoted json string */ 31 | val delimited: StringCodec[Parser, String] = 32 | new StringCodec[Parser, String] { 33 | private[this] val quote = "\"" 34 | def parser = Impl.JsonStringUtil.escapedString 35 | def encode(str: String): String = 36 | quote + Impl.JsonStringUtil.escape(str) + quote 37 | } 38 | 39 | val undelimited: StringCodec[Parser0, String] = 40 | new StringCodec[Parser0, String] { 41 | def parser = Impl.JsonStringUtil.undelimitedString1.orElse(Parser.pure("")) 42 | def encode(str: String): String = 43 | Impl.JsonStringUtil.escape(str) 44 | } 45 | 46 | private[this] object Impl { 47 | import cats.parse.{Parser => P} 48 | 49 | object JsonStringUtil { 50 | // Here are the rules for escaping in json 51 | val decodeTable: Map[Char, Char] = 52 | Map( 53 | ('\\', '\\'), 54 | ('/', '/'), 55 | ('\"', '\"'), 56 | ('b', 8.toChar), // backspace 57 | ('f', 12.toChar), // form-feed 58 | ('n', '\n'), 59 | ('r', '\r'), 60 | ('t', '\t') 61 | ) 62 | 63 | private val encodeTable = decodeTable.iterator.map { case (v, k) => (k, s"\\$v") }.toMap 64 | 65 | private val nonPrintEscape: Array[String] = 66 | (0 until 32).map { c => 67 | val strHex = c.toHexString 68 | val strPad = List.fill(4 - strHex.length)('0').mkString 69 | s"\\u$strPad$strHex" 70 | }.toArray 71 | 72 | val escapedToken: P[Char] = { 73 | def parseIntStr(p: P[String]): P[Char] = 74 | p.map(Integer.parseInt(_, 16).toChar) 75 | 76 | val escapes = P.fromCharMap(decodeTable) 77 | 78 | val hex4 = P.charIn(('0' to '9') ++ ('a' to 'f') ++ ('A' to 'F')).repExactlyAs[String](4) 79 | val u4 = P.char('u') *> parseIntStr(hex4) 80 | 81 | // do the oneOf in a guess order of likelihood 82 | val after = P.oneOf(escapes :: u4 :: Nil) 83 | P.char('\\') *> after 84 | } 85 | 86 | val notEscape: P[Char] = 87 | P.charIn((0x20.toChar to 0x10ffff.toChar).toSet - '\\' - '"') 88 | 89 | /** String content without the delimiter 90 | */ 91 | val undelimitedString1: P[String] = 92 | notEscape.orElse(escapedToken).repAs 93 | 94 | val escapedString: P[String] = { 95 | // .string is much faster than repAs since it can just do 96 | // an array copy vs building a string with a StringBuilder and Chars 97 | // but we can only copy if there are no escape values since copying 98 | // does not decode 99 | // but since escape strings are rare, this bet is generally worth it. 100 | val cheapAndCommon = notEscape.rep0.string 101 | 102 | P.char('\"') *> ( 103 | (cheapAndCommon <* P.char('\"')).backtrack | 104 | (undelimitedString1.orElse(P.pure("")) <* P.char('\"')) 105 | ) 106 | } 107 | 108 | def escape(str: String): String = { 109 | val lowest = 0x20.toChar 110 | val highest = 0x10ffff.toChar 111 | str.flatMap { c => 112 | encodeTable.get(c) match { 113 | case None => 114 | if ((c < lowest) || (c > highest)) nonPrintEscape(c.toInt) 115 | else c.toString 116 | case Some(esc) => esc 117 | } 118 | } 119 | } 120 | } 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /core/shared/src/test/scala/cats/parse/Rfc5234Test.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021 Typelevel 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy of 5 | * this software and associated documentation files (the "Software"), to deal in 6 | * the Software without restriction, including without limitation the rights to 7 | * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 8 | * the Software, and to permit persons to whom the Software is furnished to do so, 9 | * subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in all 12 | * copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 16 | * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 17 | * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 18 | * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 19 | * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | */ 21 | 22 | package cats.parse 23 | 24 | import org.scalacheck.Arbitrary.arbitrary 25 | import org.scalacheck.Gen 26 | import org.scalacheck.Prop.forAll 27 | 28 | class Rfc5234Test extends munit.ScalaCheckSuite { 29 | val allChars: Set[Char] = Set(Char.MinValue to Char.MaxValue: _*) 30 | 31 | def singleCharProperties[A](name: String, rule: Parser0[A], valid: Set[Char], f: Char => A) = { 32 | val genValid = Gen.oneOf(valid) 33 | // Bias toward the chars we tend to find in these parsers 34 | val genInvalid = Gen.frequency( 35 | List( 36 | 3 -> (Set(0x00.toChar to 0x7f.toChar: _*) -- valid), 37 | 1 -> (Set(0x80.toChar to 0xff.toChar: _*) -- valid), 38 | 1 -> (Set(0x100.toChar to Char.MaxValue: _*) -- valid) 39 | ).collect { 40 | case (freq, set) if set.nonEmpty => 41 | freq -> Gen.oneOf(set) 42 | }: _* 43 | ) 44 | property(s"${name} parses single valid char") { 45 | forAll(genValid) { c => 46 | assertEquals(rule.parseAll(c.toString), Right(f(c))) 47 | } 48 | } 49 | property(s"${name} rejects single invalid char") { 50 | forAll(genInvalid) { c => 51 | assert(rule.parseAll(c.toString).isLeft) 52 | } 53 | } 54 | property(s"${name} rejects all but single char") { 55 | forAll(Gen.stringOf(genValid).filter(_.size != 1)) { c => 56 | assert(rule.parseAll(c.toString).isLeft) 57 | } 58 | } 59 | } 60 | 61 | def singleConstCharProperties(name: String, rule: Parser0[Unit], valid: Char) = 62 | singleCharProperties(name, rule, Set(valid), _ => ()) 63 | 64 | def singleMultiCharProperties(name: String, rule: Parser0[Char], valid: Set[Char]) = 65 | singleCharProperties(name, rule, valid, identity) 66 | 67 | singleMultiCharProperties( 68 | "alpha", 69 | Rfc5234.alpha, 70 | Set((0x41.toChar to 0x5a.toChar) ++ (0x61.toChar to 0x7a.toChar): _*) 71 | ) 72 | singleMultiCharProperties("bit", Rfc5234.bit, Set('0', '1')) 73 | singleMultiCharProperties("char", Rfc5234.char, Set(0x01.toChar to 0x7f.toChar: _*)) 74 | singleConstCharProperties("cr", Rfc5234.cr, 0x0d.toChar) 75 | singleMultiCharProperties("ctl", Rfc5234.ctl, Set(0x00.toChar to 0x1f.toChar: _*) + 0x7f.toChar) 76 | singleMultiCharProperties("digit", Rfc5234.digit, Set(0x30.toChar to 0x39.toChar: _*)) 77 | singleConstCharProperties("dquote", Rfc5234.dquote, 0x22.toChar) 78 | singleMultiCharProperties( 79 | "hexdig", 80 | Rfc5234.hexdig, 81 | Set('0' to '9': _*) ++ Set('A' to 'F': _*) ++ Set('a' to 'f': _*) 82 | ) 83 | singleConstCharProperties("htab", Rfc5234.htab, 0x09.toChar) 84 | singleConstCharProperties("lf", Rfc5234.lf, 0x0a.toChar) 85 | singleMultiCharProperties("octet", Rfc5234.octet, Set(0x00.toChar to 0xff.toChar: _*)) 86 | singleConstCharProperties("sp", Rfc5234.sp, 0x20.toChar) 87 | singleMultiCharProperties("vchar", Rfc5234.vchar, Set(0x21.toChar to 0x7e.toChar: _*)) 88 | singleCharProperties("wsp", Rfc5234.wsp, Set(0x20.toChar, 0x09.toChar), _ => ()) 89 | 90 | test("crlf accepts \r\n") { 91 | assertEquals(Rfc5234.crlf.parseAll("\r\n"), Right(())) 92 | } 93 | property("crlf rejects all but \r\n") { 94 | forAll(arbitrary[String].filterNot(_ == "\r\n")) { (s: String) => 95 | assert(Rfc5234.crlf.parseAll(s).isLeft) 96 | } 97 | } 98 | 99 | property("lwsp accepts all linear white space") { 100 | val genWsp = Gen.oneOf(0x20.toChar, 0x09.toChar) 101 | val genLwsp = Gen 102 | .listOf( 103 | Gen.oneOf( 104 | genWsp.map(_.toString), 105 | genWsp.map("\r\n" + _) 106 | ) 107 | ) 108 | .map(_.mkString) 109 | forAll(genLwsp) { (s: String) => 110 | assertEquals(Rfc5234.lwsp.parseAll(s), Right(())) 111 | } 112 | } 113 | property("lwsp rejects crlf unless followed by wsp") { 114 | val gen = Gen 115 | .option(Gen.oneOf(allChars - 0x20.toChar - 0x09.toChar)) 116 | .map(opt => "\r\n" ++ opt.fold("")(_.toString)) 117 | forAll(gen) { (s: String) => 118 | assert(Rfc5234.lwsp.parseAll(s).isLeft) 119 | } 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /core/shared/src/main/scala/cats/parse/LocationMap.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021 Typelevel 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy of 5 | * this software and associated documentation files (the "Software"), to deal in 6 | * the Software without restriction, including without limitation the rights to 7 | * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 8 | * the Software, and to permit persons to whom the Software is furnished to do so, 9 | * subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in all 12 | * copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 16 | * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 17 | * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 18 | * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 19 | * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | */ 21 | 22 | package cats.parse 23 | 24 | import java.util.Arrays 25 | 26 | /** This is a class to convert linear offset in a string into lines, or the column and line numbers. 27 | * 28 | * This is useful for display to humans who in text editors think in terms of line and column 29 | * numbers 30 | */ 31 | class LocationMap(val input: String) { 32 | 33 | private[parse] val lines: Array[String] = 34 | input.split("\n", -1) 35 | 36 | private[this] val endsWithNewLine: Boolean = 37 | (input.length > 0) && (input.last == '\n') 38 | 39 | // The position of the first element of the ith line 40 | private[this] val firstPos: Array[Int] = { 41 | val it = lines.iterator.map(_.length) 42 | val it2 = new Iterator[(Int, Boolean)] { 43 | def hasNext = it.hasNext 44 | def next() = { 45 | val hn = hasNext 46 | val i = it.next() 47 | (i, hn) 48 | } 49 | } 50 | it2 51 | .map { 52 | case (i, true) => i + 1 // add 1 for the newline 53 | case (i, false) => i 54 | } 55 | .toArray 56 | .scanLeft(0)(_ + _) 57 | } 58 | 59 | /** How many lines are there 60 | */ 61 | def lineCount: Int = lines.length 62 | 63 | def isValidOffset(offset: Int): Boolean = 64 | (0 <= offset && offset <= input.length) 65 | 66 | /** Given a string offset return the line and column If input.length is given (EOF) we return the 67 | * same value as if the string were one character longer (i.e. if we have appended a non-newline 68 | * character at the EOF) 69 | */ 70 | def toLineCol(offset: Int): Option[(Int, Int)] = 71 | if (isValidOffset(offset)) { 72 | val Caret(line, col, _) = toCaretUnsafeImpl(offset) 73 | Some((line, col)) 74 | } else None 75 | 76 | // This does not do bounds checking because we 77 | // don't want to check twice. Callers to this need to 78 | // do bounds check 79 | private def toCaretUnsafeImpl(offset: Int): Caret = 80 | if (offset == input.length) { 81 | // this is end of line 82 | if (offset == 0) Caret.Start 83 | else { 84 | val Caret(line, col, _) = toCaretUnsafeImpl(offset - 1) 85 | if (endsWithNewLine) Caret(line = line + 1, col = 0, offset = offset) 86 | else Caret(line = line, col = col + 1, offset = offset) 87 | } 88 | } else { 89 | val idx = Arrays.binarySearch(firstPos, offset) 90 | if (idx < 0) { 91 | // idx = (~(insertion pos) - 1) 92 | // The insertion point is defined as the point at which the key would be 93 | // inserted into the array: the index of the first element greater than 94 | // the key, or a.length if all elements in the array are less than the specified key. 95 | // 96 | // so insertion pos = ~(idx + 1) 97 | val line = ~(idx + 1) 98 | // so we are pointing into a line 99 | val lineStart = firstPos(line) 100 | val col = offset - lineStart 101 | Caret(line = line, col = col, offset = offset) 102 | } else { 103 | // idx is exactly the right value because offset is beginning of a line 104 | Caret(line = idx, col = 0, offset = offset) 105 | } 106 | } 107 | 108 | /** Convert an offset to a Caret. throws IllegalArgumentException if offset is longer than input 109 | */ 110 | def toCaretUnsafe(offset: Int): Caret = 111 | if (isValidOffset(offset)) toCaretUnsafeImpl(offset) 112 | else throw new IllegalArgumentException(s"offset = $offset exceeds ${input.length}") 113 | 114 | def toCaret(offset: Int): Option[Caret] = 115 | if (isValidOffset(offset)) Some(toCaretUnsafeImpl(offset)) 116 | else None 117 | 118 | /** return the line without a newline 119 | */ 120 | def getLine(i: Int): Option[String] = 121 | if (i >= 0 && i < lines.length) Some(lines(i)) 122 | else None 123 | 124 | /** Return the offset for a given line/col. if we return Some(input.length) this means EOF if we 125 | * return Some(i) for 0 <= i < input.length it is a valid item else offset < 0 or offset > 126 | * input.length we return None 127 | */ 128 | def toOffset(line: Int, col: Int): Option[Int] = 129 | if ((line < 0) || (line > lines.length)) None 130 | else Some(firstPos(line) + col) 131 | } 132 | 133 | object LocationMap { 134 | def apply(str: String): LocationMap = new LocationMap(str) 135 | } 136 | -------------------------------------------------------------------------------- /core/shared/src/main/scala/cats/parse/Accumulator.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021 Typelevel 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy of 5 | * this software and associated documentation files (the "Software"), to deal in 6 | * the Software without restriction, including without limitation the rights to 7 | * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 8 | * the Software, and to permit persons to whom the Software is furnished to do so, 9 | * subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in all 12 | * copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 16 | * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 17 | * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 18 | * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 19 | * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | */ 21 | 22 | package cats.parse 23 | 24 | import cats.data.{NonEmptyList, NonEmptyVector} 25 | import scala.collection.mutable.Builder 26 | 27 | /** A limited Builder-like value we use for portability 28 | */ 29 | trait Appender[-A, +B] { 30 | def append(item: A): this.type 31 | def finish(): B 32 | } 33 | 34 | object Appender { 35 | def charStringAppender(): Appender[Char, String] = 36 | new Appender[Char, String] { 37 | val bldr = new java.lang.StringBuilder() 38 | 39 | def append(item: Char) = { 40 | bldr.append(item) 41 | this 42 | } 43 | 44 | def finish(): String = bldr.toString() 45 | } 46 | 47 | def stringAppender(): Appender[String, String] = 48 | new Appender[String, String] { 49 | val bldr = new java.lang.StringBuilder() 50 | 51 | def append(item: String) = { 52 | bldr.append(item) 53 | this 54 | } 55 | 56 | def finish(): String = bldr.toString() 57 | } 58 | 59 | def fromBuilder[A, B](bldr: Builder[A, B]): Appender[A, B] = 60 | new Appender[A, B] { 61 | def append(item: A) = { 62 | bldr += item 63 | this 64 | } 65 | 66 | def finish(): B = bldr.result() 67 | } 68 | 69 | val unitAppender: Appender[Any, Unit] = 70 | new Appender[Any, Unit] { 71 | def append(item: Any) = this 72 | def finish(): Unit = () 73 | } 74 | 75 | def intCounter(): Appender[Any, Int] = 76 | new Appender[Any, Int] { 77 | private[this] var n = 0 78 | def append(item: Any) = { 79 | n += 1 80 | this 81 | } 82 | def finish(): Int = n 83 | } 84 | } 85 | 86 | /** Creates an appender given the first item to be added This is used to build the result in 87 | * Parser.repAs 88 | */ 89 | trait Accumulator[-A, +B] { 90 | def newAppender(first: A): Appender[A, B] 91 | } 92 | 93 | /** Creates an appender This is used to build the result in Parser.repAs0 94 | */ 95 | trait Accumulator0[-A, +B] extends Accumulator[A, B] { 96 | def newAppender(): Appender[A, B] 97 | def newAppender(first: A): Appender[A, B] = 98 | newAppender().append(first) 99 | } 100 | 101 | object Accumulator0 { 102 | implicit val intCounter0: Accumulator0[Any, Int] = new Accumulator0[Any, Int] { 103 | override def newAppender(): Appender[Any, Int] = Appender.intCounter() 104 | } 105 | 106 | implicit val charStringAccumulator0: Accumulator0[Char, String] = 107 | new Accumulator0[Char, String] { 108 | def newAppender() = Appender.charStringAppender() 109 | } 110 | 111 | implicit val stringAccumulator0: Accumulator0[String, String] = 112 | new Accumulator0[String, String] { 113 | def newAppender() = Appender.stringAppender() 114 | } 115 | 116 | implicit def listAccumulator0[A]: Accumulator0[A, List[A]] = 117 | new Accumulator0[A, List[A]] { 118 | def newAppender() = Appender.fromBuilder(List.newBuilder[A]) 119 | } 120 | 121 | implicit def vectorAccumulator0[A]: Accumulator0[A, Vector[A]] = 122 | new Accumulator0[A, Vector[A]] { 123 | def newAppender() = Appender.fromBuilder(Vector.newBuilder[A]) 124 | } 125 | 126 | /** An accumulator that does nothing and returns Unit Note, this should not generally be used with 127 | * repAs0 because internal allocations still happen. Instead use .rep0.void 128 | */ 129 | val unitAccumulator0: Accumulator0[Any, Unit] = 130 | new Accumulator0[Any, Unit] { 131 | def newAppender() = Appender.unitAppender 132 | } 133 | } 134 | 135 | object Accumulator extends Priority0Accumulator { 136 | implicit def nonEmptyListAccumulator0[A]: Accumulator[A, NonEmptyList[A]] = 137 | new Accumulator[A, NonEmptyList[A]] { 138 | def newAppender(first: A): Appender[A, NonEmptyList[A]] = 139 | new Appender[A, NonEmptyList[A]] { 140 | val bldr = List.newBuilder[A] 141 | def append(item: A) = { 142 | bldr += item 143 | this 144 | } 145 | 146 | def finish() = NonEmptyList(first, bldr.result()) 147 | } 148 | } 149 | 150 | implicit def nonEmptyVectorAccumulator0[A]: Accumulator[A, NonEmptyVector[A]] = 151 | new Accumulator[A, NonEmptyVector[A]] { 152 | def newAppender(first: A): Appender[A, NonEmptyVector[A]] = 153 | new Appender[A, NonEmptyVector[A]] { 154 | val bldr = Vector.newBuilder[A] 155 | bldr += first 156 | 157 | def append(item: A) = { 158 | bldr += item 159 | this 160 | } 161 | 162 | def finish() = NonEmptyVector.fromVectorUnsafe(bldr.result()) 163 | } 164 | } 165 | } 166 | 167 | private[parse] sealed trait Priority0Accumulator { 168 | implicit def fromAccumulator0[A, B](implicit acc: Accumulator0[A, B]): Accumulator[A, B] = acc 169 | } 170 | -------------------------------------------------------------------------------- /core/shared/src/test/scala/cats/parse/ErrorShowTest.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021 Typelevel 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy of 5 | * this software and associated documentation files (the "Software"), to deal in 6 | * the Software without restriction, including without limitation the rights to 7 | * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 8 | * the Software, and to permit persons to whom the Software is furnished to do so, 9 | * subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in all 12 | * copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 16 | * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 17 | * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 18 | * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 19 | * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | */ 21 | 22 | package cats.parse 23 | 24 | import cats.implicits.toShow 25 | 26 | import org.scalacheck.Prop.forAll 27 | 28 | import Parser._ 29 | import Numbers.digits 30 | 31 | class ErrorShowTest extends munit.ScalaCheckSuite { 32 | 33 | def error(parser: Parser0[Any], input: String, expected: String)(implicit 34 | loc: munit.Location 35 | ): Unit = { 36 | test(input) { 37 | parser 38 | .parseAll(input) 39 | .fold( 40 | e => assertEquals(e.show, expected), 41 | _ => fail("should not parse") 42 | ) 43 | } 44 | } 45 | 46 | val ok = string("ok").void 47 | val nl = string("\n") 48 | val lx = (string("l") ~ digits).void 49 | val lxOk = ((lx | ok) ~ nl) 50 | 51 | // # Expectations: 52 | // OneOfStr 53 | error( 54 | string("foo") | string("bar") | string("baz"), 55 | "ko", 56 | s"""|ko 57 | |^ 58 | |expectation: 59 | |* must match one of the strings: {"bar", "baz", "foo"}""".stripMargin 60 | ) 61 | 62 | // InRange 63 | error( 64 | charIn(List('a', 'c', 'x', 'y')), 65 | "ko", 66 | """|ko 67 | |^ 68 | |expectations: 69 | |* must be char: 'a' 70 | |* must be char: 'c' 71 | |* must be a char within the range of: ['x', 'y']""".stripMargin 72 | ) 73 | 74 | // StartOfString 75 | error( 76 | ok ~ start, 77 | "ok", 78 | """|ok 79 | | ^ 80 | |expectation: 81 | |* must start the string""".stripMargin 82 | ) 83 | 84 | // EndOfString 85 | error( 86 | ok, 87 | "okmore".stripMargin, 88 | """|okmore 89 | | ^ 90 | |expectation: 91 | |* must end the string""".stripMargin 92 | ) 93 | 94 | // Length 95 | error( 96 | length(2), 97 | "a", 98 | """|a 99 | |^ 100 | |expectation: 101 | |* must have a length of 2 but got a length of 1""".stripMargin 102 | ) 103 | 104 | // ExpectedFailureAt 105 | error( 106 | not(ok), 107 | "okidou", 108 | """|okidou 109 | |^ 110 | |expectation: 111 | |* must fail but matched with ok""".stripMargin 112 | ) 113 | 114 | // Fail 115 | error( 116 | Fail, 117 | "ok", 118 | """|ok 119 | |^ 120 | |expectation: 121 | |* must fail""".stripMargin 122 | ) 123 | 124 | // FailWith 125 | error( 126 | failWith("error msg"), 127 | "ok", 128 | """|ok 129 | |^ 130 | |expectation: 131 | |* must fail: error msg""".stripMargin 132 | ) 133 | 134 | // WithContext 135 | error( 136 | withContext0(ok, "using ok") | withContext0(lx, "using lx"), 137 | "ko", 138 | """|ko 139 | |^ 140 | |expectations: 141 | |* context: using ok, must match string: "ok" 142 | |* context: using lx, must be char: 'l'""".stripMargin 143 | ) 144 | 145 | // Context 146 | error( 147 | lxOk.rep(9), 148 | """|l1 149 | |l2 150 | |l3 151 | |l4 152 | |ko 153 | |l6 154 | |l7 155 | |l8 156 | |l9 157 | |""".stripMargin, 158 | """|... 159 | |l3 160 | |l4 161 | |ko 162 | |^ 163 | |expectations: 164 | |* must match string: "ok" 165 | |* must be char: 'l' 166 | |l6 167 | |l7 168 | |...""".stripMargin 169 | ) 170 | 171 | error( 172 | lxOk.rep(3), 173 | """|l1 174 | |ko 175 | |l3""".stripMargin, 176 | """|l1 177 | |ko 178 | |^ 179 | |expectations: 180 | |* must match string: "ok" 181 | |* must be char: 'l' 182 | |l3""".stripMargin 183 | ) 184 | 185 | error( 186 | lxOk.rep(2), 187 | """|l1 188 | |ko""".stripMargin, 189 | """|l1 190 | |ko 191 | |^ 192 | |expectations: 193 | |* must match string: "ok" 194 | |* must be char: 'l'""".stripMargin 195 | ) 196 | 197 | error( 198 | lxOk.rep(2), 199 | """|ko 200 | |l2""".stripMargin, 201 | """|ko 202 | |^ 203 | |expectations: 204 | |* must match string: "ok" 205 | |* must be char: 'l' 206 | |l2""".stripMargin 207 | ) 208 | 209 | test("without input") { 210 | ok 211 | .parseAll("ko") 212 | .fold( 213 | e => { 214 | val expected = 215 | """|at offset 0 216 | |expectation: 217 | |* must match string: "ok"""".stripMargin 218 | assertEquals(Error(e.failedAtOffset, e.expected).show, expected) 219 | }, 220 | _ => fail("should not parse") 221 | ) 222 | } 223 | 224 | property("error show does not crash") { 225 | import cats.implicits._ 226 | import ParserGen.{arbParser, arbString} 227 | 228 | forAll { (p: Parser[Unit], in: String) => 229 | p.parseAll(in) match { 230 | case Right(_) => () 231 | case Left(err) => assert(err.show ne null) 232 | } 233 | } 234 | } 235 | } 236 | -------------------------------------------------------------------------------- /core/shared/src/test/scala/cats/parse/RepParserConstructionTest.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021 Typelevel 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy of 5 | * this software and associated documentation files (the "Software"), to deal in 6 | * the Software without restriction, including without limitation the rights to 7 | * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 8 | * the Software, and to permit persons to whom the Software is furnished to do so, 9 | * subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in all 12 | * copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 16 | * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 17 | * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 18 | * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 19 | * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | */ 21 | 22 | package cats.parse 23 | 24 | import org.scalacheck.Gen 25 | import org.scalacheck.Prop.forAll 26 | 27 | class RepParserConstructionTest extends munit.ScalaCheckSuite { 28 | import ParserGen.biasSmall 29 | 30 | override def scalaCheckTestParameters = 31 | super.scalaCheckTestParameters 32 | .withMinSuccessfulTests(1000) 33 | .withMaxDiscardRatio(10) 34 | 35 | val validMin = biasSmall(1) 36 | val validMin0 = biasSmall(0) 37 | val validMax = validMin 38 | 39 | val validMinMax = for { 40 | min <- validMin 41 | max <- biasSmall(min) 42 | } yield (min, max) 43 | 44 | val validMinMax0 = for { 45 | min <- validMin0 46 | max <- biasSmall(min) 47 | } yield (min, max) 48 | 49 | val invalidMin = Gen.choose(Int.MinValue, 0) 50 | val invalidMin0 = Gen.choose(Int.MinValue, -1) 51 | val invalidMax = Gen.choose(Int.MinValue, 0) 52 | 53 | val invalidMinMax = Gen.oneOf( 54 | for (min <- validMin; maxDiff <- biasSmall(1)) 55 | yield (min, Integer.min(min - maxDiff, Int.MinValue)), 56 | for (min <- invalidMin; max <- biasSmall(0)) yield (min, max), 57 | for (min <- invalidMin; max <- invalidMax) yield (min, max), 58 | for (min <- validMin; max <- invalidMax) yield (min, max) 59 | ) 60 | 61 | val invalidMinMax0 = Gen.oneOf( 62 | for (min <- validMin0; maxDiff <- biasSmall(1)) 63 | yield (min, Integer.min(min - maxDiff, Int.MinValue)), 64 | for (min <- invalidMin0; max <- biasSmall(0)) yield (min, max), 65 | for (min <- invalidMin0; max <- invalidMax) yield (min, max), 66 | for (min <- validMin0; max <- invalidMax) yield (min, max) 67 | ) 68 | 69 | property("rep constructs parser with min >= 1, min <= max") { 70 | forAll(validMinMax) { 71 | case (min: Int, max: Int) => { 72 | Parser.anyChar.rep(min = min, max = max) 73 | assert(true) 74 | } 75 | } 76 | } 77 | 78 | property("rep fails to construct parser without min >= 1, min <= max") { 79 | forAll(invalidMinMax) { 80 | case (min: Int, max: Int) => { 81 | intercept[IllegalArgumentException] { 82 | Parser.anyChar.rep(min = min, max = max) 83 | } 84 | intercept[IllegalArgumentException] { 85 | Parser.repSep(Parser.anyChar, min = min, max = max, Parser.pure("")) 86 | } 87 | assert(true) 88 | } 89 | } 90 | } 91 | 92 | property("rep constructs parser with min >= 1") { 93 | forAll(validMin) { (min: Int) => 94 | { 95 | Parser.anyChar.rep(min = min) 96 | assert(true) 97 | } 98 | } 99 | } 100 | 101 | property("rep fails to construct parser without min >= 1") { 102 | forAll(invalidMin) { (min: Int) => 103 | { 104 | intercept[IllegalArgumentException] { 105 | Parser.anyChar.rep(min = min) 106 | } 107 | assert(true) 108 | } 109 | } 110 | } 111 | 112 | property("rep0 constructs parser with min >= 0, (min, 1) <= max") { 113 | forAll(validMinMax0) { 114 | case (min: Int, max: Int) => { 115 | Parser.anyChar.rep0(min = min, max = max) 116 | assert(true) 117 | } 118 | } 119 | } 120 | 121 | property("rep0 fails to construct parser without min >= 0, (min, 1) <= max") { 122 | forAll(invalidMinMax0) { 123 | case (min: Int, max: Int) => { 124 | intercept[IllegalArgumentException] { 125 | Parser.anyChar.rep0(min = min, max = max) 126 | } 127 | intercept[IllegalArgumentException] { 128 | Parser.repSep0(Parser.anyChar, min = min, max = max, Parser.pure("")) 129 | } 130 | assert(true) 131 | } 132 | } 133 | } 134 | 135 | property("rep0 constructs parser with min >= 0") { 136 | forAll(validMin0) { (min: Int) => 137 | { 138 | Parser.anyChar.rep0(min = min) 139 | assert(true) 140 | } 141 | } 142 | } 143 | 144 | property("rep0 fails to construct parser without min >= 0") { 145 | forAll(invalidMin0) { (min: Int) => 146 | { 147 | intercept[IllegalArgumentException] { 148 | Parser.anyChar.rep0(min = min) 149 | } 150 | assert(true) 151 | } 152 | } 153 | } 154 | 155 | property("repAs constructs parser with min >= 1, min <= max") { 156 | forAll(validMinMax) { 157 | case (min: Int, max: Int) => { 158 | Parser.anyChar.repAs[String](min = min, max = max) 159 | assert(true) 160 | } 161 | } 162 | } 163 | 164 | property("repAs fails to construct parser without min >= 1, min <= max") { 165 | forAll(invalidMinMax) { 166 | case (min: Int, max: Int) => { 167 | intercept[IllegalArgumentException] { 168 | Parser.anyChar.repAs[String](min = min, max = max) 169 | } 170 | assert(true) 171 | } 172 | } 173 | } 174 | 175 | property("repAs constructs parser with min >= 1") { 176 | forAll(validMin) { (min: Int) => 177 | { 178 | Parser.anyChar.repAs[String](min = min) 179 | assert(true) 180 | } 181 | } 182 | } 183 | 184 | property("repAs fails to construct parser without min >= 1") { 185 | forAll(invalidMin) { (min: Int) => 186 | { 187 | intercept[IllegalArgumentException] { 188 | Parser.anyChar.repAs[String](min = min) 189 | } 190 | assert(true) 191 | } 192 | } 193 | } 194 | 195 | property("repAs0 constructs parser with max >= 1") { 196 | forAll(validMax) { (max: Int) => 197 | { 198 | Parser.anyChar.repAs0[String](max = max) 199 | assert(true) 200 | } 201 | } 202 | } 203 | 204 | property("repAs0 fails to construct parser without max >= 1") { 205 | forAll(invalidMax) { (max: Int) => 206 | { 207 | intercept[IllegalArgumentException] { 208 | Parser.anyChar.repAs0[String](max = max) 209 | } 210 | assert(true) 211 | } 212 | } 213 | } 214 | 215 | } 216 | -------------------------------------------------------------------------------- /core/shared/src/main/scala/cats/parse/RadixNode.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021 Typelevel 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy of 5 | * this software and associated documentation files (the "Software"), to deal in 6 | * the Software without restriction, including without limitation the rights to 7 | * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 8 | * the Software, and to permit persons to whom the Software is furnished to do so, 9 | * subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in all 12 | * copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 16 | * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 17 | * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 18 | * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 19 | * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | */ 21 | 22 | package cats.parse 23 | 24 | import cats.data.NonEmptyList 25 | import cats.kernel.Semilattice 26 | import scala.annotation.tailrec 27 | 28 | private[parse] final class RadixNode( 29 | matched: String, 30 | bitMask: Int, 31 | // the prefixes are the rest of the string after the fsts (not including the fsts Char) 32 | prefixes: Array[String], 33 | children: Array[RadixNode] 34 | ) { 35 | override def toString(): String = { 36 | def list[A](ary: Array[A]): String = ary.mkString("[", ", ", "]") 37 | val ps = list(prefixes) 38 | val cs = list(children) 39 | s"RadixNode($matched, $bitMask, $ps, $cs)" 40 | } 41 | 42 | /** @return 43 | * all strings that are in this RadixNode 44 | */ 45 | def allStrings: List[String] = { 46 | // matched may be null (meaning there is not yet a partial match) 47 | // or it may be non-null: there is a partial match 48 | // if it is non-null it may be duplicated in children 49 | val rest = children.iterator.flatMap { 50 | case null => Nil 51 | case c => c.allStrings 52 | } 53 | 54 | if (matched eq null) rest.toList 55 | else (matched :: rest.filterNot(_ == matched).toList) 56 | } 57 | 58 | /** If this matches, return the new offset, else return -1 59 | * 60 | * @param str 61 | * the string to match against this RadixNode 62 | * @param offset 63 | * the initial offset 64 | * @return 65 | * the new offset after a match, or -1 66 | */ 67 | def matchAt(str: String, off: Int): Int = 68 | matchAtOrNull(str, off) match { 69 | case null => -1 70 | case nonNull => off + nonNull.length 71 | } 72 | 73 | final def matchAtOrNull(str: String, offset: Int): String = 74 | if ((offset < 0) || (str.length < offset)) null 75 | else matchAtOrNullLoop(str, offset) 76 | 77 | // loop invariant: 0 <= offset <= str.length 78 | @tailrec 79 | final protected def matchAtOrNullLoop(str: String, offset: Int): String = 80 | if (offset < str.length) { 81 | val c = str.charAt(offset) 82 | // this is a hash of c 83 | val idx = c.toInt & bitMask 84 | val prefix = prefixes(idx) 85 | if (prefix ne null) { 86 | /* 87 | * this prefix *may* match here, but may not 88 | * note we only know that c & bitMask matches 89 | * what the prefix has to be, it could differ 90 | * on other bits. 91 | */ 92 | val plen = prefix.length 93 | if (str.regionMatches(offset, prefix, 0, plen)) { 94 | children(idx).matchAtOrNullLoop(str, offset + plen) 95 | } else { 96 | matched 97 | } 98 | } else { 99 | matched 100 | } 101 | } else { 102 | // this is only the case where offset == str.length 103 | // due to our invariant 104 | matched 105 | } 106 | } 107 | 108 | private[parse] object RadixNode { 109 | private val emptyStringArray = new Array[String](1) 110 | private val emptyChildrenArray = new Array[RadixNode](1) 111 | 112 | private def fromTree(prevMatch: String, prefix: String, rest: List[String]): RadixNode = { 113 | val (nonEmpties, empties) = rest.partition(_.nonEmpty) 114 | 115 | // If rest contains the empty string, we have a valid prefix 116 | val thisPrefix = if (empties.nonEmpty) prefix else prevMatch 117 | 118 | if (nonEmpties.isEmpty) { 119 | new RadixNode(thisPrefix, 0, emptyStringArray, emptyChildrenArray) 120 | } else { 121 | val headKeys = nonEmpties.iterator.map(_.head).toSet 122 | /* 123 | * The idea here is to use b lowest bits of the char 124 | * as an index into the array, with the smallest 125 | * number b such that all the keys are unique & b 126 | */ 127 | @tailrec 128 | def findBitMask(b: Int): Int = 129 | if (b == 0xffff) b // biggest it can be 130 | else { 131 | val hs = headKeys.size 132 | val allDistinct = 133 | // they can't all be distinct if the size isn't as big as the headKeys size 134 | ((b + 1) >= hs) && 135 | (headKeys.iterator.map { c => c.toInt & b }.toSet.size == hs) 136 | if (allDistinct) b 137 | else findBitMask((b << 1) | 1) 138 | } 139 | 140 | val bitMask = findBitMask(0) 141 | val branching = bitMask + 1 142 | val prefixes = new Array[String](branching) 143 | val children = new Array[RadixNode](branching) 144 | nonEmpties 145 | .groupBy { s => (s.head.toInt & bitMask) } 146 | .foreach { case (idx, strings) => 147 | // strings is a non-empty List[String] which all start with the same char 148 | val prefix1 = strings.reduce(commonPrefixSemilattice.combine(_, _)) 149 | // note prefix1.length >= 1 because they all match on the first character 150 | prefixes(idx) = prefix1 151 | children(idx) = 152 | fromTree(thisPrefix, prefix + prefix1, strings.map(_.drop(prefix1.length))) 153 | } 154 | 155 | new RadixNode(thisPrefix, bitMask, prefixes, children) 156 | } 157 | } 158 | 159 | /** This is identical to fromStrings and only here for binary compatibility 160 | */ 161 | def fromSortedStrings(strings: NonEmptyList[String]): RadixNode = 162 | fromTree(null, "", strings.toList.distinct) 163 | 164 | def fromStrings(strs: Iterable[String]): RadixNode = 165 | fromTree(null, "", strs.toList.distinct) 166 | 167 | final def commonPrefixLength(s1: String, s2: String): Int = { 168 | val len = Integer.min(s1.length, s2.length) 169 | var idx = 0 170 | while (idx < len) { 171 | if (s1.charAt(idx) != s2.charAt(idx)) { 172 | return idx 173 | } else { 174 | idx = idx + 1 175 | } 176 | } 177 | 178 | idx 179 | } 180 | 181 | val commonPrefixSemilattice: Semilattice[String] = 182 | new Semilattice[String] { 183 | def combine(x: String, y: String): String = { 184 | val l = commonPrefixLength(x, y) 185 | if (l == 0) "" 186 | else if (l == x.length) x 187 | else if (l == y.length) y 188 | else x.take(l) 189 | } 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /core/shared/src/test/scala/cats/parse/RadixNodeTest.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021 Typelevel 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy of 5 | * this software and associated documentation files (the "Software"), to deal in 6 | * the Software without restriction, including without limitation the rights to 7 | * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 8 | * the Software, and to permit persons to whom the Software is furnished to do so, 9 | * subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in all 12 | * copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 16 | * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 17 | * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 18 | * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 19 | * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | */ 21 | 22 | package cats.parse 23 | 24 | import org.scalacheck.{Gen, Prop} 25 | import org.scalacheck.Prop.forAll 26 | 27 | class RadixNodeTest extends munit.ScalaCheckSuite { 28 | val tests: Int = if (BitSetUtil.isScalaJvm) 20000 else 50 29 | 30 | override def scalaCheckTestParameters = 31 | super.scalaCheckTestParameters 32 | .withMinSuccessfulTests(tests) 33 | .withMaxDiscardRatio(10) 34 | 35 | property("commonPrefixLength is consistent") { 36 | forAll { (s1: String, s2: String) => 37 | val len = RadixNode.commonPrefixLength(s1, s2) 38 | val minLen = Integer.min(s1.length, s2.length) 39 | 40 | assert(len >= 0) 41 | assert(len <= minLen) 42 | assertEquals(s1.take(len), s2.take(len)) 43 | if (len < minLen) assertNotEquals(s1.charAt(len), s2.charAt(len)) 44 | } 45 | } 46 | 47 | property("commonPrefixLength is commutative") { 48 | forAll { (s1: String, s2: String) => 49 | assertEquals(RadixNode.commonPrefixLength(s1, s2), RadixNode.commonPrefixLength(s2, s1)) 50 | } 51 | } 52 | 53 | property("commonPrefixLength(s, s + r) == s.length") { 54 | forAll { (s: String, r: String) => 55 | assert(RadixNode.commonPrefixLength(s, s + r) == s.length) 56 | } 57 | } 58 | 59 | property("commonPrefixLength(s + r, s + t) >= s.length") { 60 | forAll { (s: String, r: String, t: String) => 61 | assert(RadixNode.commonPrefixLength(s + r, s + t) >= s.length) 62 | } 63 | } 64 | 65 | property("commonPrefixLength is commutative") { 66 | forAll { (s: String, r: String) => 67 | assertEquals(RadixNode.commonPrefixLength(s, r), RadixNode.commonPrefixLength(r, s)) 68 | } 69 | } 70 | 71 | property("If we match, then string is in the set") { 72 | def law(ss0: List[String], target: String): Prop = { 73 | val ss = ss0.filter(_.nonEmpty) 74 | val radix = RadixNode.fromStrings(ss) 75 | val matchLen = radix.matchAt(target, 0) 76 | assertEquals( 77 | matchLen >= 0, 78 | ss.exists(target.startsWith(_)), 79 | s"ss=$ss, ss.size = ${ss.size}, matchLen=$matchLen, radix=$radix" 80 | ) 81 | } 82 | 83 | val p1 = forAll { (ss: List[String], head: Char, tail: String) => 84 | val target = s"$head$tail" 85 | law(ss, target) 86 | } 87 | 88 | val regressions = 89 | (List("噈"), s"噈\u0000") :: 90 | Nil 91 | 92 | regressions.foldLeft(p1) { case (p, (ss, t)) => p && law(ss, t) } 93 | } 94 | 95 | property("we match everything in the set") { 96 | forAll { (ss0: List[String], head: Char, tail: String) => 97 | val s1 = s"$head$tail" 98 | val ss = s1 :: ss0 99 | val radix = RadixNode.fromStrings(ss) 100 | ss.foreach { target => 101 | assert((radix.matchAt(target, 0) >= 0) || target.isEmpty) 102 | } 103 | } 104 | } 105 | 106 | property("commonPrefix is associative") { 107 | val sl = RadixNode.commonPrefixSemilattice 108 | forAll { (s0: String, s1: String, s2: String) => 109 | val left = sl.combine(sl.combine(s0, s1), s2) 110 | val right = sl.combine(s0, sl.combine(s1, s2)) 111 | assertEquals(left, right) 112 | } 113 | } 114 | 115 | property("commonPrefix commutes") { 116 | val sl = RadixNode.commonPrefixSemilattice 117 | forAll { (s0: String, s1: String) => 118 | val left = sl.combine(s0, s1) 119 | val right = sl.combine(s1, s0) 120 | assertEquals(left, right) 121 | } 122 | } 123 | 124 | property("commonPrefix is finds prefix") { 125 | val sl = RadixNode.commonPrefixSemilattice 126 | forAll { (s0: String, suffix: String) => 127 | assertEquals(sl.combine(s0, s0 + suffix), s0) 128 | assertEquals(sl.combine(s0 + suffix, s0), s0) 129 | } 130 | } 131 | 132 | property("RadixNode.fromStrings(emptyString :: Nil) matches everything") { 133 | val nilRadix = RadixNode.fromStrings("" :: Nil) 134 | forAll { (targ: String) => 135 | forAll(Gen.choose(0, targ.length)) { off => 136 | assertEquals(nilRadix.matchAtOrNull(targ, off), "") 137 | } 138 | } 139 | } 140 | 141 | property("fromString(Nil) matches nothing") { 142 | forAll { (s: String) => 143 | forAll(Gen.choose(-1, s.length + 1)) { off => 144 | assert(RadixNode.fromStrings(Nil).matchAt(s, off) < 0) 145 | } 146 | } 147 | } 148 | 149 | property("RadixTree singleton") { 150 | forAll { (s: String, prefix: String, suffix: String) => 151 | val tree = RadixNode.fromStrings(s :: Nil) 152 | assertEquals(tree.matchAtOrNull(prefix + s + suffix, prefix.length), s) 153 | } 154 | } 155 | 156 | property("RadixTree union property") { 157 | forAll { (t1: List[String], t2: List[String], targ: String) => 158 | val tree1 = RadixNode.fromStrings(t1) 159 | val tree2 = RadixNode.fromStrings(t2) 160 | val tree3 = RadixNode.fromStrings(t1 ::: t2) 161 | 162 | forAll(Gen.choose(-1, targ.length + 1)) { off => 163 | val m1 = math.max(tree1.matchAt(targ, off), tree2.matchAt(targ, off)) 164 | assertEquals(m1, tree3.matchAt(targ, off)) 165 | } 166 | } 167 | } 168 | 169 | property("matchAtOrNull is consistent") { 170 | forAll { (args: List[String], targ: String) => 171 | val radix = RadixNode.fromStrings(args) 172 | forAll(Gen.choose(-1, targ.length + 1)) { off => 173 | radix.matchAt(targ, off) match { 174 | case x if x < 0 => 175 | assertEquals(radix.matchAtOrNull(targ, off), null) 176 | case off1 => 177 | val len = off1 - off 178 | val left = radix.matchAtOrNull(targ, off) 179 | assert(off + len <= targ.length, s"len = $len, off = $off") 180 | assertEquals(left, targ.substring(off, off + len), s"len = $len, left = $left") 181 | } 182 | } 183 | } 184 | } 185 | 186 | test("example from ParserTest") { 187 | val tree = RadixNode.fromStrings(List("foo", "foobar", "foofoo", "foobat")) 188 | assertEquals(tree.matchAtOrNull("foobal", 0), "foo") 189 | } 190 | 191 | property("RadixNode.allStrings roundTrips") { 192 | forAll { (ss: List[String]) => 193 | assertEquals(RadixNode.fromStrings(ss).allStrings.sorted, ss.distinct.sorted) 194 | } 195 | } 196 | } 197 | -------------------------------------------------------------------------------- /core/shared/src/test/scala/cats/parse/LocationMapTest.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021 Typelevel 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy of 5 | * this software and associated documentation files (the "Software"), to deal in 6 | * the Software without restriction, including without limitation the rights to 7 | * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 8 | * the Software, and to permit persons to whom the Software is furnished to do so, 9 | * subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in all 12 | * copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 16 | * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 17 | * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 18 | * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 19 | * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | */ 21 | 22 | package cats.parse 23 | 24 | import org.scalacheck.{Arbitrary, Gen, Prop} 25 | import Prop.forAll 26 | 27 | class LocationMapTest extends munit.ScalaCheckSuite { 28 | 29 | val tests: Int = if (BitSetUtil.isScalaJs) 50 else 20000 30 | 31 | override def scalaCheckTestParameters = 32 | super.scalaCheckTestParameters 33 | .withMinSuccessfulTests(tests) 34 | .withMaxDiscardRatio(10) 35 | 36 | property("single line locations") { 37 | val singleLine: Gen[String] = 38 | Arbitrary.arbitrary[String].map(_.filterNot(_ == '\n')) 39 | 40 | forAll(singleLine, Arbitrary.arbitrary[Int]) { (sline, offset) => 41 | val lm = LocationMap(sline) 42 | 43 | assert(lm.getLine(0) == Some(sline)) 44 | lm.toLineCol(offset) match { 45 | case None => 46 | assert(offset < 0 || offset >= sline.length) 47 | case Some((row, col)) => 48 | assert(row == 0) 49 | assert(col == offset) 50 | } 51 | } 52 | } 53 | 54 | property("position of end-of-line is the same as adding a constant") { 55 | forAll { (str: String) => 56 | val lm0 = LocationMap(str) 57 | val lm1 = LocationMap(str + "a") 58 | 59 | assert(lm0.toLineCol(str.length) == lm1.toLineCol(str.length)) 60 | } 61 | } 62 | 63 | property("adding more content never changes the Caret of an offset") { 64 | forAll { (s0: String, s1: String) => 65 | val lm0 = LocationMap(s0) 66 | val lm1 = LocationMap(s0 + s1) 67 | 68 | (0 to s0.length).foreach { idx => 69 | assertEquals(lm0.toCaretUnsafe(idx), lm1.toCaretUnsafe(idx)) 70 | } 71 | } 72 | } 73 | 74 | test("some specific examples") { 75 | val lm0 = LocationMap("\n") 76 | assert(lm0.toLineCol(0) == Some((0, 0))) 77 | assertEquals(lm0.toLineCol(1), Some((1, 0))) 78 | 79 | val lm1 = LocationMap("012\n345\n678") 80 | assert(lm1.toLineCol(-1) == None) 81 | assert(lm1.toLineCol(0) == Some((0, 0))) 82 | assert(lm1.toLineCol(1) == Some((0, 1))) 83 | assert(lm1.toLineCol(2) == Some((0, 2))) 84 | assert(lm1.toLineCol(3) == Some((0, 3))) 85 | assert(lm1.toLineCol(4) == Some((1, 0))) 86 | assert(lm1.toLineCol(5) == Some((1, 1))) 87 | assert(lm1.toLineCol(6) == Some((1, 2))) 88 | assert(lm1.toLineCol(7) == Some((1, 3))) 89 | assert(lm1.toLineCol(8) == Some((2, 0))) 90 | assert(lm1.toLineCol(9) == Some((2, 1))) 91 | assert(lm1.toLineCol(10) == Some((2, 2))) 92 | assert(lm1.toLineCol(11) == Some((2, 3))) 93 | } 94 | 95 | property("we can reassemble input with getLine") { 96 | forAll { (str: String) => 97 | val lm = LocationMap(str) 98 | 99 | val reconstruct = Iterator 100 | .iterate(0)(_ + 1) 101 | .map(lm.getLine _) 102 | .takeWhile(_.isDefined) 103 | .collect { case Some(l) => l } 104 | .mkString("\n") 105 | 106 | assertEquals(reconstruct, str) 107 | } 108 | } 109 | 110 | property("toLineCol is defined for all valid offsets, and getLine isDefined consistently") { 111 | 112 | forAll { (s: String, offset: Int) => 113 | val lm = LocationMap(s) 114 | 115 | def test(offset: Int) = 116 | lm.toLineCol(offset) match { 117 | case None => 118 | assert(offset < 0 || offset >= s.length) 119 | case Some((row, col)) => 120 | lm.getLine(row) match { 121 | case None => fail(s"offset = $offset, s = $s") 122 | case Some(line) => 123 | assert(line.length >= col) 124 | if (line.length == col) assert((offset == s.length) || s(offset) == '\n') 125 | else assert(line(col) == s(offset)) 126 | } 127 | } 128 | 129 | test(offset) 130 | if (s.nonEmpty) test(math.abs(offset % s.length)) 131 | } 132 | } 133 | 134 | property("if a string is not empty, 0 offset is (0, 0)") { 135 | forAll { (s: String) => 136 | LocationMap(s).toLineCol(0) match { 137 | case Some(r) => assertEquals(r, ((0, 0))) 138 | case None => fail("could not get the first item") 139 | } 140 | } 141 | } 142 | 143 | property("slow toLineCol matches") { 144 | 145 | def slow(str: String, offset: Int): Option[(Int, Int)] = { 146 | val split = str.split("\n", -1) 147 | def lineCol(off: Int, row: Int): (Int, Int) = 148 | if (row == split.length) (row, 0) 149 | else { 150 | val r = split(row) 151 | val extraNewLine = 152 | if (row < (split.length - 1)) 1 else 0 // all but the last have an extra newline 153 | val chars = r.length + extraNewLine 154 | 155 | if (off >= chars) lineCol(off - chars, row + 1) 156 | else (row, off) 157 | } 158 | 159 | if (offset < 0 || offset > str.length) None 160 | else if (offset == str.length) Some { 161 | if (offset == 0) (0, 0) 162 | else { 163 | val (l, c) = lineCol(offset - 1, 0) 164 | if (str.last == '\n') (l + 1, 0) 165 | else (l, c + 1) 166 | } 167 | } 168 | else Some(lineCol(offset, 0)) 169 | } 170 | 171 | assert(slow("\n", 0) == Some((0, 0))) 172 | assert(LocationMap("\n").toLineCol(0) == Some((0, 0))) 173 | 174 | assertEquals(slow("\n", 1), Some((1, 0))) 175 | assertEquals(LocationMap("\n").toLineCol(1), Some((1, 0))) 176 | 177 | assert(slow(" \n", 1) == Some((0, 1))) 178 | assert(LocationMap(" \n").toLineCol(1) == Some((0, 1))) 179 | 180 | assertEquals(slow(" \n", 2), Some((1, 0))) 181 | assertEquals(LocationMap(" \n").toLineCol(2), Some((1, 0))) 182 | 183 | assert(slow(" \n ", 1) == Some((0, 1))) 184 | assert(LocationMap(" \n ").toLineCol(1) == Some((0, 1))) 185 | 186 | assert(slow("\n ", 1) == Some((1, 0))) 187 | assert(LocationMap("\n ").toLineCol(1) == Some((1, 0))) 188 | 189 | forAll { (str: String, offset: Int) => 190 | val lm = LocationMap(str) 191 | assertEquals(lm.toLineCol(offset), slow(str, offset)) 192 | (0 to str.length).foreach { o => 193 | assertEquals(lm.toLineCol(o), slow(str, o)) 194 | } 195 | } 196 | } 197 | 198 | property("if x > y && toLineCol(x).isDefined, then toLineCol(x) > toLineCol(y)") { 199 | forAll { (s: String, x: Int, y: Int) => 200 | val lm = LocationMap(s) 201 | val lcx = lm.toLineCol(x) 202 | val lcy = lm.toLineCol(y) 203 | 204 | if (x > y && y >= 0 && lcx.isDefined) { 205 | (lcx, lcy) match { 206 | case (Some((lx, cx)), Some((ly, cy))) => 207 | assert(lx > ly || ((lx == ly) && (cx > cy))) 208 | case other => 209 | fail(other.toString) 210 | } 211 | } 212 | } 213 | } 214 | 215 | property("toLineCol toOffset round trips") { 216 | forAll { (s: String, offset: Int) => 217 | val offsets = (-s.length to 2 * s.length).toSet + offset 218 | 219 | val lm = LocationMap(s) 220 | offsets.foreach { o => 221 | lm.toLineCol(o) match { 222 | case Some((l, c)) => 223 | assertEquals(lm.toOffset(l, c), Some(o)) 224 | case None => 225 | assert(o < 0 || o > s.length) 226 | } 227 | } 228 | } 229 | } 230 | 231 | property("lineCount and getLine are consistent") { 232 | forAll { (s: String) => 233 | val lm = LocationMap(s) 234 | assert(s.endsWith(lm.getLine(lm.lineCount - 1).get)) 235 | } 236 | } 237 | 238 | property("toLineCol and toCaret are consistent") { 239 | forAll { (s: String, other: Int) => 240 | val lm = LocationMap(s) 241 | (0 to s.length).foreach { offset => 242 | val c = lm.toCaretUnsafe(offset) 243 | val oc = lm.toCaret(offset) 244 | val lc = lm.toLineCol(offset) 245 | 246 | assertEquals(oc, Some(c)) 247 | assertEquals(lc, oc.map { c => (c.line, c.col) }) 248 | assertEquals(c.offset, offset) 249 | val Caret(line, col, off1) = c 250 | assertEquals(line, c.line) 251 | assertEquals(col, c.col) 252 | assertEquals(off1, offset) 253 | } 254 | 255 | if (other < 0 || s.length < other) { 256 | assert(scala.util.Try(lm.toCaretUnsafe(other)).isFailure) 257 | assertEquals(lm.toCaret(other), None) 258 | assertEquals(lm.toLineCol(other), None) 259 | } 260 | } 261 | } 262 | 263 | property("Caret ordering matches offset ordering") { 264 | forAll { (s: String, o1: Int, o2: Int) => 265 | val lm = LocationMap(s) 266 | val c1 = lm.toCaret(o1) 267 | val c2 = lm.toCaret(o2) 268 | 269 | if (c1.isDefined && c2.isDefined) { 270 | assertEquals(Ordering[Option[Caret]].compare(c1, c2), Integer.compare(o1, o2)) 271 | } 272 | } 273 | } 274 | } 275 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | # This file was automatically generated by sbt-github-actions using the 2 | # githubWorkflowGenerate task. You should add and commit this file to 3 | # your git repository. It goes without saying that you shouldn't edit 4 | # this file by hand! Instead, if you wish to make changes, you should 5 | # change your sbt build configuration to revise the workflow description 6 | # to meet your needs, then regenerate this file. 7 | 8 | name: Continuous Integration 9 | 10 | on: 11 | pull_request: 12 | branches: ['**', '!update/**', '!pr/**'] 13 | push: 14 | branches: ['**', '!update/**', '!pr/**'] 15 | tags: [v*] 16 | 17 | env: 18 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 19 | 20 | 21 | concurrency: 22 | group: ${{ github.workflow }} @ ${{ github.ref }} 23 | cancel-in-progress: true 24 | 25 | jobs: 26 | build: 27 | name: Test 28 | strategy: 29 | matrix: 30 | os: [ubuntu-22.04] 31 | scala: [2.12, 2.13, 3] 32 | java: [temurin@8] 33 | project: [rootJS, rootJVM, rootNative] 34 | runs-on: ${{ matrix.os }} 35 | timeout-minutes: 60 36 | steps: 37 | - name: Checkout current branch (full) 38 | uses: actions/checkout@v5 39 | with: 40 | fetch-depth: 0 41 | 42 | - name: Setup sbt 43 | uses: sbt/setup-sbt@v1 44 | 45 | - name: Setup Java (temurin@8) 46 | id: setup-java-temurin-8 47 | if: matrix.java == 'temurin@8' 48 | uses: actions/setup-java@v5 49 | with: 50 | distribution: temurin 51 | java-version: 8 52 | cache: sbt 53 | 54 | - name: sbt update 55 | if: matrix.java == 'temurin@8' && steps.setup-java-temurin-8.outputs.cache-hit == 'false' 56 | run: sbt +update 57 | 58 | - name: Check that workflows are up to date 59 | run: sbt githubWorkflowCheck 60 | 61 | - name: Check headers and formatting 62 | if: matrix.java == 'temurin@8' && matrix.os == 'ubuntu-22.04' 63 | run: sbt 'project ${{ matrix.project }}' '++ ${{ matrix.scala }}' headerCheckAll scalafmtCheckAll 'project /' scalafmtSbtCheck 64 | 65 | - name: scalaJSLink 66 | if: matrix.project == 'rootJS' 67 | run: sbt 'project ${{ matrix.project }}' '++ ${{ matrix.scala }}' Test/scalaJSLinkerResult 68 | 69 | - name: nativeLink 70 | if: matrix.project == 'rootNative' 71 | run: sbt 'project ${{ matrix.project }}' '++ ${{ matrix.scala }}' Test/nativeLink 72 | 73 | - name: Test 74 | run: sbt 'project ${{ matrix.project }}' '++ ${{ matrix.scala }}' test 75 | 76 | - name: Check binary compatibility 77 | if: matrix.java == 'temurin@8' && matrix.os == 'ubuntu-22.04' 78 | run: sbt 'project ${{ matrix.project }}' '++ ${{ matrix.scala }}' mimaReportBinaryIssues 79 | 80 | - name: Generate API documentation 81 | if: matrix.java == 'temurin@8' && matrix.os == 'ubuntu-22.04' 82 | run: sbt 'project ${{ matrix.project }}' '++ ${{ matrix.scala }}' doc 83 | 84 | - name: Make target directories 85 | if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/main') 86 | run: mkdir -p core/native/target core/js/target core/jvm/target project/target 87 | 88 | - name: Compress target directories 89 | if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/main') 90 | run: tar cf targets.tar core/native/target core/js/target core/jvm/target project/target 91 | 92 | - name: Upload target directories 93 | if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/main') 94 | uses: actions/upload-artifact@v4 95 | with: 96 | name: target-${{ matrix.os }}-${{ matrix.java }}-${{ matrix.scala }}-${{ matrix.project }} 97 | path: targets.tar 98 | 99 | publish: 100 | name: Publish Artifacts 101 | needs: [build] 102 | if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/main') 103 | strategy: 104 | matrix: 105 | os: [ubuntu-22.04] 106 | java: [temurin@8] 107 | runs-on: ${{ matrix.os }} 108 | steps: 109 | - name: Checkout current branch (full) 110 | uses: actions/checkout@v5 111 | with: 112 | fetch-depth: 0 113 | 114 | - name: Setup sbt 115 | uses: sbt/setup-sbt@v1 116 | 117 | - name: Setup Java (temurin@8) 118 | id: setup-java-temurin-8 119 | if: matrix.java == 'temurin@8' 120 | uses: actions/setup-java@v5 121 | with: 122 | distribution: temurin 123 | java-version: 8 124 | cache: sbt 125 | 126 | - name: sbt update 127 | if: matrix.java == 'temurin@8' && steps.setup-java-temurin-8.outputs.cache-hit == 'false' 128 | run: sbt +update 129 | 130 | - name: Download target directories (2.12, rootJS) 131 | uses: actions/download-artifact@v4 132 | with: 133 | name: target-${{ matrix.os }}-${{ matrix.java }}-2.12-rootJS 134 | 135 | - name: Inflate target directories (2.12, rootJS) 136 | run: | 137 | tar xf targets.tar 138 | rm targets.tar 139 | 140 | - name: Download target directories (2.12, rootJVM) 141 | uses: actions/download-artifact@v4 142 | with: 143 | name: target-${{ matrix.os }}-${{ matrix.java }}-2.12-rootJVM 144 | 145 | - name: Inflate target directories (2.12, rootJVM) 146 | run: | 147 | tar xf targets.tar 148 | rm targets.tar 149 | 150 | - name: Download target directories (2.12, rootNative) 151 | uses: actions/download-artifact@v4 152 | with: 153 | name: target-${{ matrix.os }}-${{ matrix.java }}-2.12-rootNative 154 | 155 | - name: Inflate target directories (2.12, rootNative) 156 | run: | 157 | tar xf targets.tar 158 | rm targets.tar 159 | 160 | - name: Download target directories (2.13, rootJS) 161 | uses: actions/download-artifact@v4 162 | with: 163 | name: target-${{ matrix.os }}-${{ matrix.java }}-2.13-rootJS 164 | 165 | - name: Inflate target directories (2.13, rootJS) 166 | run: | 167 | tar xf targets.tar 168 | rm targets.tar 169 | 170 | - name: Download target directories (2.13, rootJVM) 171 | uses: actions/download-artifact@v4 172 | with: 173 | name: target-${{ matrix.os }}-${{ matrix.java }}-2.13-rootJVM 174 | 175 | - name: Inflate target directories (2.13, rootJVM) 176 | run: | 177 | tar xf targets.tar 178 | rm targets.tar 179 | 180 | - name: Download target directories (2.13, rootNative) 181 | uses: actions/download-artifact@v4 182 | with: 183 | name: target-${{ matrix.os }}-${{ matrix.java }}-2.13-rootNative 184 | 185 | - name: Inflate target directories (2.13, rootNative) 186 | run: | 187 | tar xf targets.tar 188 | rm targets.tar 189 | 190 | - name: Download target directories (3, rootJS) 191 | uses: actions/download-artifact@v4 192 | with: 193 | name: target-${{ matrix.os }}-${{ matrix.java }}-3-rootJS 194 | 195 | - name: Inflate target directories (3, rootJS) 196 | run: | 197 | tar xf targets.tar 198 | rm targets.tar 199 | 200 | - name: Download target directories (3, rootJVM) 201 | uses: actions/download-artifact@v4 202 | with: 203 | name: target-${{ matrix.os }}-${{ matrix.java }}-3-rootJVM 204 | 205 | - name: Inflate target directories (3, rootJVM) 206 | run: | 207 | tar xf targets.tar 208 | rm targets.tar 209 | 210 | - name: Download target directories (3, rootNative) 211 | uses: actions/download-artifact@v4 212 | with: 213 | name: target-${{ matrix.os }}-${{ matrix.java }}-3-rootNative 214 | 215 | - name: Inflate target directories (3, rootNative) 216 | run: | 217 | tar xf targets.tar 218 | rm targets.tar 219 | 220 | - name: Import signing key 221 | if: env.PGP_SECRET != '' && env.PGP_PASSPHRASE == '' 222 | env: 223 | PGP_SECRET: ${{ secrets.PGP_SECRET }} 224 | PGP_PASSPHRASE: ${{ secrets.PGP_PASSPHRASE }} 225 | run: echo $PGP_SECRET | base64 -d -i - | gpg --import 226 | 227 | - name: Import signing key and strip passphrase 228 | if: env.PGP_SECRET != '' && env.PGP_PASSPHRASE != '' 229 | env: 230 | PGP_SECRET: ${{ secrets.PGP_SECRET }} 231 | PGP_PASSPHRASE: ${{ secrets.PGP_PASSPHRASE }} 232 | run: | 233 | echo "$PGP_SECRET" | base64 -d -i - > /tmp/signing-key.gpg 234 | echo "$PGP_PASSPHRASE" | gpg --pinentry-mode loopback --passphrase-fd 0 --import /tmp/signing-key.gpg 235 | (echo "$PGP_PASSPHRASE"; echo; echo) | gpg --command-fd 0 --pinentry-mode loopback --change-passphrase $(gpg --list-secret-keys --with-colons 2> /dev/null | grep '^sec:' | cut --delimiter ':' --fields 5 | tail -n 1) 236 | 237 | - name: Publish 238 | env: 239 | SONATYPE_USERNAME: ${{ secrets.SONATYPE_USERNAME }} 240 | SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }} 241 | SONATYPE_CREDENTIAL_HOST: ${{ secrets.SONATYPE_CREDENTIAL_HOST }} 242 | run: sbt tlCiRelease 243 | 244 | dependency-submission: 245 | name: Submit Dependencies 246 | if: github.event.repository.fork == false && github.event_name != 'pull_request' 247 | strategy: 248 | matrix: 249 | os: [ubuntu-22.04] 250 | java: [temurin@8] 251 | runs-on: ${{ matrix.os }} 252 | steps: 253 | - name: Checkout current branch (full) 254 | uses: actions/checkout@v5 255 | with: 256 | fetch-depth: 0 257 | 258 | - name: Setup sbt 259 | uses: sbt/setup-sbt@v1 260 | 261 | - name: Setup Java (temurin@8) 262 | id: setup-java-temurin-8 263 | if: matrix.java == 'temurin@8' 264 | uses: actions/setup-java@v5 265 | with: 266 | distribution: temurin 267 | java-version: 8 268 | cache: sbt 269 | 270 | - name: sbt update 271 | if: matrix.java == 'temurin@8' && steps.setup-java-temurin-8.outputs.cache-hit == 'false' 272 | run: sbt +update 273 | 274 | - name: Submit Dependencies 275 | uses: scalacenter/sbt-dependency-submission@v2 276 | with: 277 | modules-ignore: rootjs_2.12 rootjs_2.13 rootjs_3 docs_2.12 docs_2.13 docs_3 rootjvm_2.12 rootjvm_2.13 rootjvm_3 rootnative_2.12 rootnative_2.13 rootnative_3 bench_2.12 bench_2.13 bench_3 278 | configs-ignore: test scala-tool scala-doc-tool test-internal 279 | 280 | coverage: 281 | name: Generate coverage report 282 | strategy: 283 | matrix: 284 | os: [ubuntu-22.04] 285 | java: [temurin@11] 286 | runs-on: ${{ matrix.os }} 287 | steps: 288 | - name: Checkout current branch (fast) 289 | uses: actions/checkout@v5 290 | 291 | - name: Setup Java (temurin@8) 292 | id: setup-java-temurin-8 293 | if: matrix.java == 'temurin@8' 294 | uses: actions/setup-java@v5 295 | with: 296 | distribution: temurin 297 | java-version: 8 298 | cache: sbt 299 | 300 | - name: sbt update 301 | if: matrix.java == 'temurin@8' && steps.setup-java-temurin-8.outputs.cache-hit == 'false' 302 | run: sbt +update 303 | 304 | - run: sbt coverage rootJVM/test coverageAggregate 305 | 306 | - uses: codecov/codecov-action@v3 307 | 308 | site: 309 | name: Generate Site 310 | strategy: 311 | matrix: 312 | os: [ubuntu-22.04] 313 | java: [temurin@11] 314 | runs-on: ${{ matrix.os }} 315 | steps: 316 | - name: Checkout current branch (full) 317 | uses: actions/checkout@v5 318 | with: 319 | fetch-depth: 0 320 | 321 | - name: Setup sbt 322 | uses: sbt/setup-sbt@v1 323 | 324 | - name: Setup Java (temurin@8) 325 | id: setup-java-temurin-8 326 | if: matrix.java == 'temurin@8' 327 | uses: actions/setup-java@v5 328 | with: 329 | distribution: temurin 330 | java-version: 8 331 | cache: sbt 332 | 333 | - name: sbt update 334 | if: matrix.java == 'temurin@8' && steps.setup-java-temurin-8.outputs.cache-hit == 'false' 335 | run: sbt +update 336 | 337 | - name: Setup Java (temurin@11) 338 | id: setup-java-temurin-11 339 | if: matrix.java == 'temurin@11' 340 | uses: actions/setup-java@v5 341 | with: 342 | distribution: temurin 343 | java-version: 11 344 | cache: sbt 345 | 346 | - name: sbt update 347 | if: matrix.java == 'temurin@11' && steps.setup-java-temurin-11.outputs.cache-hit == 'false' 348 | run: sbt +update 349 | 350 | - name: Generate site 351 | run: sbt docs/tlSite 352 | 353 | - name: Publish site 354 | if: github.event_name != 'pull_request' && github.ref == 'refs/heads/main' 355 | uses: peaceiris/actions-gh-pages@v4.0.0 356 | with: 357 | github_token: ${{ secrets.GITHUB_TOKEN }} 358 | publish_dir: site/target/docs/site 359 | keep_files: true 360 | -------------------------------------------------------------------------------- /bench/src/main/resources/foo.json: -------------------------------------------------------------------------------- 1 | [{"GdaUlDXsTuwYcdumocBZsXNwxQCytVLX": [true, false, "KYZEtTBTObOspTTgtc", 3.5196723581340683e+30, false, "etNjqFAYFEMGqVDxhSme", 0.06216879421353596, 360.7930019650112, "VqXRdpnSjkLJEbdVYH", null, "noSJKTQlFlgEkkFQpyMTEBjVK", "JjZBSOXgNZpKqhoPRXeVtdVWjYIJdnewvMGAr", 22.705327918266516, false, 2.432878944876615e+21, 1464275964.8445144, "vnJgMcAXZQnsGGMtouViteiLtX", 518.8851028291534, -9802.822925274302, "cclkrRUVaOBemjtnzR", 625.99964676936, 1030463758.8078852, 3014.2868184600125, null, false, 3.499001600620241e-22, "GivWlnHEaZFvfUMsXHwMRhIbTBebcWnhfHPLXYnjMVUTy", null, true, 306.47717690281956, false, "rXeYQiMuBMSoumYXEXJwwG", "MWWnReUGHGNeNUchYJpZxORBoMJSvguASNUloi", 671.9794228639812, 7.006989863838409, -757.2248279543983, 1734910.5592586847, "anciTCr", 2.57836148970514e+30, "ncgIQXRfbdFluDbPfUQRXSIaieIIMNB", 14636.201699306308, 6.7045688212229325e+34, -75721.69146006841, 2.3949073930184423e+33, "ZOnzBqZjw", -3305.346146630032, 7.531624724565006e+16, 9.210705157905434e+27, "fFChmqlavnZRKvPsLMydAScmPWED", "oVyMRmMXnuxfVGHzVmBqGzSQG", null, "MDpyNdUbTUjEyqPkOsIKDyHz", true, 0.10616287293480287, -946.3776307692416, true, 6109.514815293814, -409.8204048463292, -76510.36335865095, "gKCUzbAYrxPmxHxCjbcHnPuEQqQXNgXKi", "pHhsdFFDGViczWsvtMNONXTGDCVaNx", 387237232.6465047, 3.3842579076463993e+19, 123016703692.48529, "nuIWrsSjwKdoxPmOyWcfAMWObTFT", 170437.89480762804, -91459.60085462581, 207.73404736449572, 303.996246819226, "SEcFpcjjGhaLEDRjwRJrGWAVYKbHQttEzZy", "pQqCVtMGsXJbsUGeifLjSqvivdZd", "bYvGlpWwmNYfuvebBu", 9.982286006881393e+17, 2.400946968491445e-30, 12.46791361592061, 587000.2051367643, 7.71774340282823e+20, true, 3.870830141968803e-43, true, 3583008.996734635, 2.3312260484460783, 22965.025472389007, -807.7149739641076, 3.6750173465085565e+50, 2.334778061574644e-21, true, 380252062551090.6, false, "cWexmTWgQRhkQuJCytaBD", 2.6127204647094794e+32, false, 10167573062096.414, -193438.81737393895, 543123006.2367094, 1.345335989975779e-25, -173575925.08490053, "aGohypToeqUhKNfzUtNJahlPNUjoZGr", -276095.76271724695, 3.1946297796635483e-13, -1152.2122634113898, 4007.201892061793, null, 0.08612010831808821, 31241.604630179343, "mrcPqsXmBwOGcJHTVenUOwAsOynEIexiCjJsMhWLGzo", "VtozpCgBwfIRjCZTlwjEQCDqTEaO", true, 6.669323594774973e-23, 4.4609204136789704e+18, "YcVoqhpkiKFJeUUIrLBeVmlTNwZbdnevLSmyMqRsuBUF", -119280.75817579619, 271.04946946417004, -332.1610711658965, 30069113948252.37, true, 5.000481614043688e+46, 880974189.2944489, 7442516780.411504, "MPQerPpqchYCQIhVbNLUlTGoBdIOYTQei", 9.033968304134887e-09, null, 5.7096128195395495e-24, true, 859.0670636051982, -779.9999435968335, "thdCOHmViWfeJLLvijGcpygPKkzGNgrjIMx", null, true, null, true, "tfKfyVmnEkIRGRxe", -494.9564761488707, -852.2250194439819, 1662866.588593335, 87.20584464591616, "MNlbikOsAckEQVOcxhhIMYUotqLR", -821.7160773961375, true, 11115277970.954212], "QfkEGbUxBzEDrRyNktJODHTyDZknGvDvrEPOzIGVcz": [false, true, 1.730947523472292e+55, true, -189886658.91209581, "CXZl", 9.579438922121425e-18, false, 12862352134.242197, 2.977658375478196e+38, 8.243523447028408e+17, 10029749.988418005, null, 4.90133731278328e+26, 4.442315584308546e+38, 1828.1259271313495, 7.57960368870994e-10, 2.0893839950150994e+18, true, -379339.22540188296, "cCCzAToMcPdHlZAoWlEGBaNwLPpNtignD", null, 1.0835807605592079e+35, 2.51366278645446e+31, true, null, 4.902924498778203e-07, "FmEltgsXKkKtOdBwFDnefDdvHLmeaTvJUYsJvgHtm", 986.6094887778036, 2.5102203431580414, 4.156151042955725e+26, -7277.468717433106, 7.398140479519486e-12, 7.476091332973635e+19, -695.0018762699184, 64893.91667547837, 66066.63303514461, "uNNsMbKEOOyuZBxfPdiS", 64971810654112.46, true, "ngkweXVWQHqG", -965.0594187440844, null, "UEibAuMHMfgtD", true, 1.824064974505566e-29, 1.1650700429732269e-07, null, 3.53916389572903e+56, 2.7753471076497996e-28, 2510997.8711998546, "ADPswhVjSIqYBWRZOLocTMdtkQNh", -93219.17181122529, 4.237260115163709e+19, -53.65392118170018, -166838.5729252223, 19.54510521359619, 3.2026274732754506e-37], "mEGlGP": [6.828594299904194e-84, true, 1.9743248000416805e-28, "clvYYFfBDlUAArYYEeYFjIqLAxhG", 931.0546996384494, 1319324128.928022, true, -552.213137373891, "PvHDzgCjBpKbzuYtKtifRVHDtZgfx", 153.525547471895, 5.043849402199617e-53, -305262944.79021794, 1.4191277287861553e-16, 196786.4009762581, 1.3108552584408415e+63, "JruGsHIPjDQppzsivpnSshANEJaMtjlBXbd", true, 3678961.9242486977, -654.6395360260253, 465.48495074572594, "jUHICnboVvtmZoSamBUpy", 5.8248062613784015e-18, "zWUjmqBGDYDAcaZVtxwZbNUFEshOVGxxkPHoZWN", -343.08388427859, -133.69609881467073, false, -1306074.8948417804, "QCqXyvImPoBMekYFSaew", "afbqjPLnJ", true, null, 220.8365282989227, "TINeWNLlIOELyCGBVpxppwORGCJjh", 977.5458437563032, 1.0896003891959263e+31, 1.430081301934177e+70, 0.001233788661991793, -821.0019567561765, 358.3377103748316, 1.0749827208839229e-47, -472095.47209434275, null, true, "QSQIiDkreackPPdWVKPeNhedribURntlMNJpBWS", "gjhPuJjqzvRJubZhLvoekjSRSuYMEOsCMmZ", 6.942391356254295e-68, 2.8894635408749383e-34, "WHchoMakQvNBhQahnJiKaFLndLtEgl", false, 4.872073477519365e-31, "ddlEmiYpHUXGR", 237480259.17152292], "haKPoYpbuOBPqUzWgVXyUTpHfyS": {"MNSXmffGFOrph": -108.99997699345221, "PhbJaifrJuTeeeDjSdCSWfORGpoBfROXocdJaTXOS": "iLilkAmQGGnKPGuTZrv", "KXfuyQSuvCUzeVp": 3.853097797300774e-05, "MfRlvXZCQALchuApIlpwTnOcazYx": "hvRZBRJniqWYFvKvXCssgtaCgtJble", "TPpyzJIPmGdruYPiksqVtOFpbnHR": 443.20214173253544, "VFgMiscxCQUprBockHjpiucSMtXFZ": null, "BDoCTRpofjdcLne": 759.940612437452, "SQwINeMJURqqeHrdfseSOmMQiosGj": 4.919270065304941e-25, "jmMKaMy": -3536.7981246839076, "zEBDXNNYAeHZvzVJGSY": -8745670.659554193, "OqSemeXmjzOBATjvvXGGkpzssdLgLsosNy": 0.03919006508695574, "LxdcbJvnjiNIudYDZlMv": 1198.6090117519923, "MPjQGGrYwYYbaRANhDGwfhBwwGmDktbGaEuX": 2.8827629576775615e-25, "DmRvsGqzHQHiOLxqL": false, "QlyPoWUDTnPcjDccmzM": false, "bxukhmyyloAvdFXDyTnnFKwSVBU": "DWQadmkQvgzhtkoauCDLN", "HnfXMkesHfYNgRNsDukTEqTGvINvWuNYN": "QwTVHdzLbIsPAIChTfrr", "olVRmerjDq": 3.071980989567377e+23, "DniXeODDLqPwOObjwUjxiEEeRJeZ": 6.530729531629425e+25, "MXcqAjbXDgmkHxyUenLqvPoKsFOyeBzLP": 8.248319518227774e-65, "CdbfGhPhWDKYmJRhi": "YcppcReEvmxklLjUsfb", "YwuEVCsKnNjwniNSFZJMGPMLNNTf": "CdclkAvoavkIfnWtxJGOukFCicauhykbr", "wcnLFEtYLFanTavbbNnWFbAhjPRYhPGSgLqUAYY": 4.09262202052754e-18, "agOFUETaDPEQwvrBIZIqFb": 6.470179896113126e-26, "qcBQccNpoZmyGrecmUHIhWQjcQVFTHF": 1.8881322847217427e-32, "DnxEbmpqjl": 1.9410696820347994e+22, "CKlCQuKdvuxNLlLBmHLYcrIAdWvRf": -347.41582277895526, "MpbEUwjBPEfLbfdriKvRrkXe": 832.5139762982811, "JcqXMuQxzhzGDzHtJzbqZUTLZSno": 7.818748261329551e-46, "IifozCLyorfUgiwuY": "npSylBcmtlcreJelCzM", "ogJopKatjwUOQUMmdYIXEMgDiEPdCTRqsZEYgbFI": 3798736.122054874, "TbuMjvYCFqKDAzOhhsvejNzjPOptgotpPNw": 1129615595.5544176, "NcRvEoeOWMgnrSkbpcIPvjJIGMpJmXLmlU": "nXKMBENDfyqpUfAcoiceeb", "MhYWwskClwsRsSacNCnUzKTNpWJMMzNNOS": null, "JSIQHgdjidkFVHaUaazsLUWfNCWtFYepxs": -517.4572451435245, "BPdqNrNDrAEIBGTboFvExvGgE": false, "opdaQwvrOLbZEVoIRoWKnOeu": "AwlObEFypIWeFgiKMNUppeMqwtv", "QeTMwcSaNkWlBFYvObkgjRWJxItyaEUDJzFSfoMQn": -143.75320887455422, "qlWFKngyJWDocRmIHdQOd": 2.5773116980968874e-41, "fEswAHeCwqdSJNycFYjgfMMbksPzzRJBHi": 1.661959376266927e-14}, "CpObpTgrrltoGNQdeBbwlMTykP": {"sWSzOFoSQoPX": 4.833255627961974e+42, "ix": 727.4576340702018, "tbCaLlbDCUZeOtqFNDovETRRnSJCA": 1.8863713990788425e-44, "qMoRCVXmaMiKRADanqcnhfRFqDZQjEKqYiFiSQJljZGybZv": 0.007138529699633157, "zKMiVJjEsajxCSSYGEiihPyfVt": 1.5488954538756959e-40, "EUUTLcmyPusqLCkBqDVxskjnBoxe": false, "abWJIPpQXkLsYxabLiKPWouozE": "EXzmIGcATniZ", "MNtYkblWiqJADIDMRNBBkmHMWGvuKx": 6.858677814933545e+20, "qkbTLbnSiqRHEZuZYeScMbLiI": 1.6745051261408164e+24, "XwnhwGozbFzFylAGNZfYTArfQEkIwuRy": 15.731137507157433, "usRRSjrtqULlZXpt": 12.974282372939967, "YFdLpYrGoQFNB": 3.635757577648293e+31, "CXHaMNuQLTnlmYfQBpx": 3.362799872922862e-06, "gvckGHSPqwXukfdWgcDMoegC": "qjCxKzPCHnrMlOmiIUdEuWQGKjCPKh", "YuODjkgHcmRRnqsUfHvJClqKMMCoeSjNLZNBvcw": "WxchALcvQqBnIPyiNjRpbPtEvGFzrH", "WyTxtDZyTkXlDQeMxLS": "gxIEncOnJftTGsrLWxSmSwJnx", "yhEViPNOuywHCeSgyJMMG": 785.8803985346397, "xBaqprHNdZqIRbPquywAfDtnYGG": "sOwkhKQStBAtwkXyhX", "wIceczSaPezFOmxLUmgNNDZKSQjnYlcOzS": 4.76875421291897e+44, "fTJGwrtPENu": 6.615966372840822e-19, "tIiCisYmHhB": 6.361259618891217e-07, "vnFatsaMMnKcbhdbxyNDeUKuJJPKq": -366.9490172310019, "NdzVqwwOSgQZEVLerZL": 2.1007271091142541e-22, "GbuUrUQZSQ": false, "AXsdYs": true, "HeVofkYsFUYQFiyXsjCl": false, "sgueKbFWAjsWumQzdnhVFlOuWwarObPvkcOi": null, "GMsLeqzlhTSFpaemKOjc": "mLDDhLtiPTJigstxmBqlWUOzDbnQJy", "QNVZdidlPIXdrzdusYMvX": 2.721535941146409e-10, "IEjYHkePCSsIQmk": null, "mNQmtynmErWdoJaCrgkNdnvrQmtKNV": 2.2722715334662117e-17, "ytPtlmCcvjvWGMOxMRQjBTpIhfj": "VKSkkJY", "XwCCFEEFTNMJVbpyGVshfXiZrkdyokKWVJcRXGzBYPsearr": 1788430.7186662594, "dQjylxJrVCVhwLuhxDiAGQK": "MyAvmaXzsbwyratul", "BgAyyNcdaEztdlXpyCw": "tVfZWkQFRCoUJIdHzPHhrFdduOiL", "QydnCaAxCKYPUiJlfnibZh": null, "cEIlPWfTuVuQtUpWBQKfIxqCUiMKszFcGUOUuHpRaGSRxje": 2.0415146312444064e+24, "OZm": "LixZlLZtJynCe", "CaPeKRjiTSeiQwylzzFpB": 134.30702930663747, "JhJXffbYmcTVEOb": 683328489320.7626, "QKqBqFUDndZXyigPZxMJoYDFHbVTEaMOQ": -4441.29805934717, "kaVDFgADPMRPZFIVbwtaWYtOTwYBtgKz": "WsuisQmydYxXGKxrYdmhkN", "ByJCgSuYdvfDVrIMaFSvifNuKnugI": 1.2828110635585035e-42, "FnrWDZaaqrJcEsqgSPPsDpjHiowKSIvkqfeVVmHuf": true, "PofEEolzINVgIqcwdpjCWJwIoVuRMTCS": -3436.1910572396846, "drdzMCmvJFEfAnNtgz": 1.3706464043438796e-12, "dOlsRwvqpgvcsXqvJTkcNVFBaIGJEOkMNlbMLRsIhJytU": null, "mRAOCQswcuQhygyKOuhPhbyQwPXwtmggMK": -69.0000964256784, "JUPVOfjsuWvieiebbVhruuds": 22990383.139900506, "diGfjYBOCpgaoOnFjDhjvj": 420733746.5473564, "NAEWxKoWTqHZCewPCqs": 3.0188681072888575e-20, "YnUJsFmIfHgPtZwFRSHkniyjFjOTrAqpy": 773.0000081196735, "fRXiKgzAZxKm": 1775908.0932019257, "bqTSoPGwjVMCZdgviNQUVXLFhJOUBMBis": "ASiChqhlBNFbrHorBS", "FJPuckcgAqcYwsErmMGJB": null, "wsLvUJFkfGfkTrKtBDTejqAAjCCnkltzcY": null, "eKovSsKAuHCWaCvFIuWyOAjAugdr": 6.529567418177419e-19, "g": "xCSGHnbAvjFBpoyDocTUPQYotyc", "FgRlEDqAQbhauNkOlhctxlpxIaeYCuf": false, "bFOaVWfXQvCzbbZxOlRX": 6.727324490614409e-11, "sYnSAHgCgjgaXHJWDbpwJO": -980.000000005631, "HTxnIDNzlmDBxMLkgKl": "UzRYAtHMdTD", "BxMvgviBNaKbyHgmo": true, "bBKgZFwUDJeLQigVWfzdXLqwiJWpWrLvr": true, "PUkvtYxLASSnVNvUCOVPbEOtOp": -2.687536587009542, "BlHolZanFxAPVgMHwYynvWPjkICAwxFAIg": -678.7817807774779, "pCzMGHWxATOkjrqnWmklMQHw": 1.2633487590805248e+17, "bVnYleutRpJtSnvnGxaKkMIALE": 195.92701947252016, "VLqbHGbAfqwCuSGIytKkiomvvcypLhC": null, "iJCLsHdQFtKfxTxZHsGnk": "skZkuuhQccXruykNhNfGFPwtwgiyOGxdNTaGEUsBTGdN", "urTgWwbbqemiM": 434.04440794141135, "KpUmdFXkBjl": 3.028662862312299e+21, "jomPkuYlNkHViKjMAGvlTktuQeVmgCBJ": true, "hRBQNbaNbBBbknDPHFIoVjSrcciu": -732.1074910337082, "jwvCSrvtfYlxDNOHwJTIeZExMrwEaWFrhd": false, "hjEJJUZCKWYPmXOPRBMDxXEasqwTlcfMVUNBUsqgQ": 2.5566411073063377e-52}, "AuCbmKrXKZKwiqUYgwfxfCNgE": {"KjYfwIeYkcnINwGsbhjbRBrXPeSYbVJggL": true, "TqyEZoIaoGaSdaTuBEpqRJaMgnyVgQGTjqRb": 6097516135853.049, "OgjxwtJYXjkWbsgWVLWxehnBuWLC": true, "tWZmgYD": 44090.99435044499, "lBFyxKNCFqxWoWmXEbocYIrDGvqLufhA": 9.744283990717285e-06, "FEooxEtffQbvRmNjEGJsmJNKIpvPGYaOedrFJOwrxZpc": 43022156.95305111, "ETDdQuFwcniIJgrTbPrpNknyr": "OQEkppqruMhAnuzgIpzwYNjmHmhAQP", "CgFPfrOSqnKVoAucT": "IGlJcBgiypmBOcTQvCsJcuXrsV", "qZVBjfGpqDbWdLPReyrOzLQygYHKammXze": 359473.884839006, "bFzzDHndUxgPQShAegtnyNjWNnhvrXQ": 3.0151248875570626e-68, "UMcezNCBuIvVWJKlY": 471.8428928917785, "UDKriGIkxVAVgUIxkoN": "vrxXF", "dabpnzqzjSUXOHCCqjrvJttzHOKq": 3.610221060893495e+39, "MzJWKZtElgZRvlZGpcdpakqPXXruQiVvPDjOa": 1510431.4526725914, "gOqLZyQmNUzwXXshUmivLUHE": 394.7120418211913, "QCkdaMyeoJlhsYNWenPboDTKwiIihrYt": true, "GFjVkflyEpwVFIOtBDXWrWcVmxMzYexRXJuMPkJkzJ": 744.3151026353033, "bfvoeBYAPgfeoftddgotg": 3.395795593508771e+35, "NhCHzsWkezVwCMNtAeWxdMghwayuErPOoAbpc": false, "EfyqswWcuXhuNjrsbCzYKptmPLmh": -2344972566.03224, "QDfBKRHaRmkNenLyCONULtxqf": -53185.56962995247, "LNXVmFFJmfYtPITIHoRpKPlGtRtASxRpjyIFH": -302.8793927507829, "dqetmVBLyHKLoaTrwmbOINQfU": 3.404329412681278e-20, "BljMfEIFOEskSq": "hTxtmbHv", "TMxntdwkFVqhvflBgVTnwXj": 3.658786497054809e-25, "FKBSJHUEajFeuuxpGQlDopxrKk": null, "HcpaoJOlYKLtmzo": "EUkhFbsmRuCvimGtcR", "Ab": "WtgighWntibRCZUqSSG", "Gr": 8.3339541923873655e-22, "QUZEonacGIpQWwbuHBdwiYNvIHO": 47.40198316262668, "FFjjjVBjjIwUEYHsrZjIeBoYyhgeycdIFgmJhkXmpEJNId": -96016.10671836963, "UesFGzannehEPWUyyvLmhRqQMKJJbuHJVVZneyabp": -211227035215.59213, "YYFNsjJSnpJuiMBrSHLKgOLjEOmFXqQBAsMD": 258692050719.094, "jeYJaqbaAGkCCOPYwPdjsytQWnzrJtIncghvOpnzUsSM": 1.5448721761462635e-38, "taZXvHsnaWGMmJHSAfaMoLuxDJOzz": 3.4022828924774365e-12, "QlxmTVigDfiPbXTHQobWSRUUZ": 3.2610511778843966e-26, "WJAFfTvTwCCbXymnPFPsndBODdlj": 5.22700182845888e+64, "kCxXuhsXUFHioGaawaltI": 3.528742508172698e-30, "UwKovuPCiMCyTdkBdjcTRgoMwVrPyopHjJrTvVuT": "jmNqtWMkhGpzDCXFumMGBOUNiqE"}, "LvQMBVUVPBznWvx": {"QOagCZfJCBPKnwgRVvPtNTHxNXNeEBTJw": "lZTzxCgCgz", "yzRkpFIFoZVfnrwaWZlIHwnveYnkJWXqgjBKqF": "QybEPZGBTnBC", "TydjryzgPDBpymlamSaICqNdlAHBvueUugC": 3.4803130223280207e+20, "tPUrHDscLQFJsncKkhXhnJiQQwpx": -240.00219705692015, "ongJQlyaNxIiYLmWtZCwORDzBE": 388.66251896167205, "bKJhSOPGWbXUKsVGhEN": 1.3929238930086614e+42, "mHlMWIL": true, "HDUkHGrNrgtCgWScXzPbUDxWqWKEDp": 1778006362.2243018, "RvhlcIo": 1.7797399139561276e-08, "LsouglKyQSPlhqHYgD": false, "uwwDwqkjzuBzDYRqPEAnfNFkhCvqkwXEAjU": 87452.08001366118, "vaRENwS": 9.498501566288149e-05, "hXtVGPuAqkLTIWvOVawJmRqzYyKfpFlhzxPxQBNvxUjb": "F", "wehsMCowquTtyGFfj": -414.89328967140915, "HCFutJaPhhgxmtcmwhVghFyANnBATm": true, "UZelXJCcsGNMsixlyEhX": 1.5182531964779307e-15, "TYBVibsYcvQdOCSpKHSkWab": false, "INeoZZUAYtWGa": 4.595861569398779e-21, "dWgQQIFALupeIZJnU": -58.554907011912874}, "vglbnEPxvzUWsjGFszE": {"mFPwsrkjPihaTBTuQGMxkUrxUTHSNeTGSt": "RMDopaNOYmtpHjqKCxvo", "ECpHpCyCQwwAdcHaVnxygf": null, "ukCjKKXE": 5.675495860087726e-09, "f": "YTvBVUqnmFkMMUdd", "JRhnfAdmV": "wLWnPRatFLYSdDukshCCyWVcDbVfvfHS", "uYOuHjfSChOamtGEwMMldpAPSSFlQrJesKskGnMwXMYEmwoaafzWtpfaoTGv": 1.7684048784938347e-40, "uitCAnEYeKCONUSaWkWEZa": "FOzGVQwRbPexfwJYNyXfQbthgTjIEBbLFGJSdlAvjX", "QNXFEqnXDedEETKoqUmFYdOaFwWDAjKJpH": -400.9999869138578, "ADeCKywDBeo": false, "UUTMCGcpLDvyICnBxNUxHNXjQoaXmGmzchT": 9.657614456060255e+33, "JixtRZJeHWoleIPmOBQDdjPrNqCLsu": 7.865277570302374e-29, "SnaGkoNQXkREHOfoIjLeYLsu": true, "tyEgjkwYmMaXQaWCz": true}, "jb": {"DpXWaommWxkcezxiSdVvkET": 1.0640412879482271e-05, "uHYvVzgmuIDkEWKmMRjzWvhEU": 3.4653104715687e+16, "edgerOaquGoIsvPqbEIJqwVVjDgXtFjQTeWCDuaLNrl": 4.432525100738127e+47, "yAyVIOwotObSWOsjVcHADhqXhqjmuQgHHC": 4260131.035371279, "dddieVqcIHXWNPBrzcqubHRmnyDGnGnLahvXH": "bCXOtexKBQpWHyuKXTsjcjKKBbPM", "XxrdNZpPRBMtB": null, "EqgTkpRxyGiYdbQvUmGpPRn": 7.42572547238298e-105, "NyQcGgfKqTdjRVMVdmKnk": "nlCObcsLWAsnTbjsTNDjfaG", "BNEkLdISBcLVcYlDTDFQNLvKNpPrMLThNsqsDe": "qdLBKVowLNQtCtlcklTT", "tdpHZidinSQbuxkkNJnOqBRXE": false, "tyvWuUwmdesdUmFKwGblEKvPcFYkEUSnpdSSSIhUCuNhNX": 60631766328387.54, "XRmopXTMloGUWrXWwQExqG": "GMNJeqtBfwEygvmFooDVNeuVEpZxnJD", "rzxveHpDWiHNOHOQUvwHBD": 87.95017130572676, "aJhxuFfPzVaTIrzjkfeTmfeoNeCj": 46.99999978593087, "hBrhYFyqNqluPn": 267520.99701338453, "HCxkbLncJektoAAbVisaBoGeq": 9.56362638214255e-13, "UyxaSFgycZuglQrEMzpjCy": -184.35131635457608, "sDIRXpZDfEftUmPAdlJ": 25926.252202355598, "FIWnhWbEoKwWstZluvLP": 97.93622037584996, "UyLfuDBjSvpFHqdYeORUYh": -121025.1401011686, "YexrsOtqR": false, "lfWkPvpZJokUrkVNGhqiMwWdLmrdtTxF": "TPUenPqNpuRVfaaNBbZpwWv", "AfLdzGiekQuzEKuCsRbbrzigvRLPKVoCg": "BQxYHHrHRpoFAzWKJGNkxSZnDWCG", "nweBRdVICaawPIQURNYiHhNQIAFVljpsfiVXM": "NDpHiqXGdNIypxDAmXOAQHmgx", "YmeiXmUUPCTfKPhGYDayZvZpnKpfiwViOeQvuUqSFMaehgsS": false, "KvfPtQIdfwpOViKQGMADUa": -540071.989759356, "tpIjueqMnKhDxWDtusinguQIQkxidIAMaCtoqShG": -613.7701130685031, "mTUIJFadxNFhLIOFJVnRDj": "bFRAsbgyEtWfZrxdNPmWEAFChP", "zRUZTQOfjcEnlmCGBWSkm": "YHHTOzlrNiKZbWPhWPJatkUmIEjqHYKICniiuLp", "rBVfQvkVisivIlNoHaM": -134624601536.09836, "mpQDhDCrRYyAMKKDfp": 397.1972098623496, "MCpSUQAkNjuUnZNdpnqzVOBxqhrYPS": 1.431705159161263e+30, "CFleYVtsQOwvEgiVUcdmbNcLpQeOaNO": -2247.0745224970788, "EKSTkebJPVLjLzwtsbLWhOkcUU": -30690.407671662397, "MdTVtOVcJBVNUGSNsodITrgaJm": 425.03149345727695, "OLXLTByPITPtWcGNf": 193.99998774411603, "ERtbIINZHjTicWVW": 2428.5684626397697, "NSQUzZFkLDYHNbtvBzrKEnvlOcQa": "bNEkbRituBOdsWvmEwOIhYxXtjduAZHWmcN", "HHbsCOTaqvXaHDQo": 143080971996.47122, "OkYSKkHTYsNCNDdudrhFrPQWyzHBzS": false, "LkqoPYWnhBmNtidaALacgpCpkaYcEgtoJS": "RHRECfOkZPMlCnLDhSTovnMXMsxCGBIDBPVBaOpWutpkQ", "gHdCjGXvBqluaAljkhYetPozxhQntJgOrVLhSESUHSImqFvGj": "fBfecHEZvwMlrcAnxtYeFrRaohSQRi", "MsJHfIvdVoGVWvdfZeFnXOZE": 2.904653268270397e+39, "CjXLMkz": -69032.98907010234, "akKQpIqDFqwwaaWBoOAjsLiJNrFIoxJza": 643.6229498921875, "hKUSXAFkUXjlCIKnGHbSJbyXevQ": "SBcbtSNxKgcMYXRXJtjNgl", "YGdOHIBFluADWGmWjFMCcgbFxwtc": false, "ANsjUBMBvgaahbkeGHsHGECdlwlLABklU": 3.0083986503811824, "GmLwsQTSCcMOknYrYceb": 2.021029554634517e-31, "ndaLvxOwhfjjzImgdKjmdTwLIKZj": "BbgBBzfMYJnrjB", "vvjTKoryxAnvuvlSQC": null, "xwzaWGmysCpCMXdniFszkuEjoBtHXyZQiBn": "RRsOeZuKicXLQoQvVNpLMwTMtFQAkbOGZlgTGzMUKyt", "OmFjqWDusxYuOGVRBelwF": 2.0562788259752858e-39, "RhzCcvuKhrpatQQKzzuLBnIkdYrkbT": 40.00015413158343, "AUpeHNRzSsymzcpmQAydkzKDBmBluD": "NrruRyNcickFErgAwqyyBQKzANzEoexJGemlXVqLDGgW", "wFGlPaxHLIpOFvfrpsdUvSsopOic": -543.0008924519282}, "cvOfVLbUoZanJNlPw": {"": 5713.611887241794, "StDCONwnUPcorGwSqxKTPIPwKgTOGHLsMCLr": "XBOAifluSPxk", "qYgqFFzWKflQwuwVAsQryT": "RZOzxUfjAiHjK", "OXqcXKPYSqsMbqYIDDAUCh": -9404684759.890007, "hTlPLrdDBDXeJJlAfihHaUJMGSKoGFUR": 1.9544790012650856e-36, "kwvrKrnyrDlDfgPOnEAUCZQObcGGrOUcb": "hepYGIlsdgYzLEzYKXTiogAgJjDm", "KBfduYNECFLKSmANfHa": 811.4297291849001, "BeGLDUjXWpKJsCgWUje": "zKBZTHBJtcfZChNQeZlhPWUNDIIugPNI", "jrrodRvwyMOIDhUgJHqcj": false, "FupCUYkYlxaUdxHuFTOuidHXOPw": 1.7626978980891414e+55, "sFggVxNOzwUEyVItkSHvuaosQXfpbkMCBEnEmtytY": 1.032353856106203e-44, "ynGTzzpiWUFgVqMXUahibHJnaru": 640.85994718038, "cAtFaQpwcew": 17579073744.134026, "MmXNRDjdLXfvRpQZnraKbJevO": true, "jYOHhqypuvmqXxzfVEqAuKIccrBOjfGNv": 1.274680923843407e+17, "itvKPJunyPHOfKiBZdEeKnwyUyQMMC": 1.1488074253294929e+21, "ATuzqcYRatSGCPWoVSlGe": false, "PQKGQczKeVvw": -33.98321018455386, "jNlWMgSe": 556.9999997813125, "puvDPVY": 1.0118062625751008e+41}}] -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # cats-parse 2 | 3 | [![Continuous Integration](https://github.com/typelevel/cats-parse/workflows/Continuous%20Integration/badge.svg)](https://github.com/typelevel/cats-parse/actions?query=workflow%3A%22Continuous+Integration%22)[![codecov](https://codecov.io/gh/typelevel/cats-parse/branch/main/graph/badge.svg)](https://codecov.io/gh/typelevel/cats-parse) 4 | 5 | A parsing library for the cats ecosystem. 6 | 7 | To use in sbt add, the following to your `libraryDependencies`: 8 | 9 | ```scala 10 | // use this snippet for the JVM 11 | libraryDependencies += "org.typelevel" %% "cats-parse" % "1.0.0" 12 | 13 | // use this snippet for JS, or cross-building 14 | libraryDependencies += "org.typelevel" %%% "cats-parse" % "1.0.0" 15 | ``` 16 | 17 | The [API docs](https://javadoc.io/doc/org.typelevel/cats-parse_2.13/1.0.0/cats/parse/index.html) are published. 18 | 19 | Why another parsing library? See this [blog post detailing the 20 | design](https://posco.medium.com/designing-a-parsing-library-in-scala-d5076de52536). To reiterate, 21 | this library has a few goals: 22 | 23 | 1. Compatibility: should work on all scala platforms and recent versions. Currently it supports JVM, JS on versions 2.11, 2.12, 2.13, and 3. The core library should have minimal dependencies. Currently this library only depends on cats. 24 | 2. Excellent performance: should be as fast or faster than any parser combinator that has comparable scala version support. 25 | 3. Cats friendliness: method names match cats style, and out of the box support for cats typeclasses. 26 | 4. Precise errors: following the [Haskell Trifecta parsing library](https://hackage.haskell.org/package/trifecta), backtracking is opt-in vs opt-out. This design tends to make it easier to write parsers that point correctly to failure points. 27 | 5. Safety: by separating Parser0, a parser that may consume no input, from Parser, a parser must consume at least one character on success. Most combinators and methods can be made safer to use and less prone to runtime errors. 28 | 6. Stability: we are very reluctant to break compatibility between versions. We want to put a minimal tax on users to stay on the latest versions. 29 | 30 | # Tutorial 31 | 32 | ## Simple parser 33 | 34 | The library provides a set of simple parsers which might be combined to create any parsing logic. The simplest parser is `Parser.anyChar` which is successful where there is one char at the input. It has type `Parser[Char]` which means it returns one parsed char. 35 | 36 | To provide any input to parser one need to use `parse` method. 37 | 38 | ```scala mdoc:reset 39 | import cats.parse.Parser 40 | 41 | val p: Parser[Char] = Parser.anyChar 42 | 43 | p.parse("t") 44 | 45 | p.parse("") 46 | 47 | p.parse("two") 48 | ``` 49 | 50 | Notice the return type. `Tuple2[String, Char]` contains the rest of the input string and one parsed char if parsing was successful. It returns `Left` with error message if there was some parsing error. 51 | 52 | ## Mapping output 53 | 54 | The output of the parser might be processed with `map` method: 55 | 56 | ```scala mdoc:reset 57 | import cats.parse.Parser 58 | 59 | case class CharWrapper(value: Char) 60 | 61 | val p: Parser[CharWrapper] = Parser.anyChar.map(char => CharWrapper(char)) 62 | 63 | p.parse("t") 64 | ``` 65 | 66 | There are built-in methods for mapping the output to types `String` or `Unit`: 67 | 68 | ```scala mdoc:reset 69 | import cats.parse.Rfc5234.digit 70 | import cats.parse.Parser 71 | 72 | /* String */ 73 | 74 | val p2: Parser[String] = digit.map((c: Char) => c.toString) 75 | // is analog to 76 | val p3: Parser[String] = digit.string 77 | 78 | p3.parse("1") 79 | 80 | /* Unit */ 81 | 82 | val p4: Parser[Unit] = digit.map(_ => ()) 83 | // is analog to 84 | val p5: Parser[Unit] = digit.void 85 | 86 | p5.parse("1") 87 | ``` 88 | 89 | ## Combining parsers 90 | 91 | The parsers might be combined through operators: 92 | 93 | - `~` - product. Allows continuing parsing if the left side was successful; 94 | - `<*`, `*>` - productL and productR. Works just like product but drop part of result; 95 | - `surroundedBy` - identical to `border *> parsingResult <* border`; 96 | - `between` - identical to `border1 *> parsingResult <* border2`; 97 | - `|`, `orElse`. Parser will be successful if any of sides is successful. 98 | 99 | For this example we'll be using `cats.parse.Rfc5234` package which contains such parsers as `alpha` (Latin alphabet) and `sp` (whitespace). 100 | 101 | ```scala mdoc:reset 102 | import cats.parse.Rfc5234.{sp, alpha, digit} 103 | import cats.parse.Parser 104 | 105 | /* Product */ 106 | 107 | // the sp parser won't return the whitespace, it just returns Unit if it successful 108 | val p1: Parser[(Char, Unit)] = alpha ~ sp 109 | 110 | p1.parse("t") 111 | 112 | p1.parse("t ") 113 | 114 | 115 | /* productL, productR */ 116 | 117 | // The type is just Char because we dropping the space 118 | // to drop the alphabet change the arrow side: alpha *> sp 119 | val p2: Parser[Char] = alpha <* sp 120 | 121 | // still error since we need the space even if we drop it 122 | p2.parse("t") 123 | 124 | p2.parse("t ") 125 | 126 | 127 | /* surroundedBy */ 128 | 129 | val p4: Parser[Char] = sp *> alpha <* sp 130 | val p5: Parser[Char] = alpha.surroundedBy(sp) 131 | 132 | p4.parse(" a ") 133 | 134 | p5.parse(" a ") 135 | 136 | 137 | /* between */ 138 | 139 | val p6: Parser[Char] = sp *> alpha <* digit 140 | val p7: Parser[Char] = alpha.between(sp, digit) 141 | 142 | p6.parse(" a1") 143 | 144 | p7.parse(" a1") 145 | 146 | 147 | /* OrElse */ 148 | 149 | val p3: Parser[AnyVal] = alpha | sp 150 | 151 | p3.parse("t") 152 | 153 | p3.parse(" ") 154 | ``` 155 | 156 | ## Repeating parsers 157 | 158 | Sometimes we need something to repeat zero or more times. The cats-parse have `rep` and `rep0` methods for repeating values. `rep` means that the parser must be successful _at least one time_. `rep0` means that the parser output might be empty. 159 | 160 | ```scala mdoc:reset 161 | import cats.data.NonEmptyList 162 | import cats.parse.Rfc5234.alpha 163 | import cats.parse.{Parser, Parser0} 164 | 165 | val p1: Parser[NonEmptyList[Char]] = alpha.rep 166 | val p2: Parser0[List[Char]] = alpha.rep0 167 | 168 | p1.parse("") 169 | 170 | p2.parse("") 171 | 172 | p2.parse("something") 173 | ``` 174 | 175 | Notice the types of parsers. `Parser` type always means some non-empty output and the output of `Parser0` might be empty. 176 | 177 | One common task in this example is to parse a full line (or words) of text. In the example it is done by `rep`, and then it could be mapped to `String` in different ways: 178 | 179 | ```scala mdoc:reset 180 | import cats.data.NonEmptyList 181 | import cats.parse.Rfc5234.alpha 182 | import cats.parse.Parser 183 | 184 | val p: Parser[String] = alpha.rep.map((l: NonEmptyList[Char]) => l.toList.mkString) 185 | 186 | val p2: Parser[String] = alpha.rep.string 187 | val p3: Parser[String] = alpha.repAs[String] 188 | ``` 189 | 190 | All three parsers will be identical in parsing results, but `p2` and `p3` are using built-in methods which will not create intermediate list. `rep` + `map` creates intermediate list which is mapped to string in this example. 191 | 192 | ## Parsers with empty output 193 | 194 | Some parsers never return a value. They have a type `Parser0`. One might get this type of parser when using `rep0` or `.?` methods. 195 | 196 | ```scala mdoc:reset 197 | import cats.parse.Rfc5234.{alpha, sp} 198 | import cats.parse.Parser 199 | 200 | val p: Parser[String] = (alpha.rep <* sp.?).rep.string 201 | 202 | p.parse("hello world") 203 | ``` 204 | 205 | Notice the type we got - `Parser[String]`. That is because we have `rep` outside and our `alpha.rep` parser with `Parser` type is on the left side of the clause. But what if we want to parse strings with spaces at the beginning? 206 | 207 | ```scala:fail 208 | val p = (sp.? *> alpha.rep <* sp.?).rep.string 209 | ``` 210 | 211 | We will get an error `value rep is not a member of cats.parse.Parser0`. This happens since we have the left-side parser as optional in `sp.? *> alpha.rep <* sp.?` clause. This clause has a type `Parser0` which can't be repeated. 212 | 213 | But this parser can't be empty because of `alpha.rep` parser, and we know it. For these types of parsers we need to use `with1` wrapper method on the _left side_ of the clause: 214 | 215 | ```scala mdoc:reset 216 | import cats.parse.Rfc5234.{alpha, sp} 217 | import cats.parse.Parser 218 | 219 | 220 | val p: Parser[String] = (sp.?.with1 *> alpha.rep <* sp.?).rep.string 221 | 222 | p.parse("hello world") 223 | 224 | p.parse(" hello world") 225 | ``` 226 | 227 | If we have multiple `Parser0` parsers before the `Parser` - we'd need to use parenthesis like this: 228 | `(sp.? ~ sp.?).with1 *> alpha.rep`. 229 | 230 | ## Error handling 231 | 232 | Parser might be interrupted by parsing error. There are two kinds of errors: 233 | 234 | - an error that has consumed 0 characters (**epsilon failure**); 235 | - an error that has consumed 1 or more characters (**arresting failure**) (sometimes called halting failure). 236 | 237 | ```scala mdoc:reset 238 | import cats.parse.Rfc5234.{alpha, sp} 239 | import cats.parse.Parser 240 | 241 | val p1: Parser[Char] = alpha 242 | val p2: Parser[Char] = sp *> alpha 243 | 244 | // epsilon failure 245 | p1.parse("123") 246 | 247 | // arresting failure 248 | p2.parse(" 1") 249 | ``` 250 | 251 | We need to make this difference because the first type of error allows us to say that parser is not matching the input before we started to process it and the second error happens while parser processing the input. 252 | 253 | ### Backtrack 254 | 255 | Backtrack allows us to convert an _arresting failure_ to _epsilon failure_. It also rewinds the input to the offset to that used before parsing began. The resulting parser might still be combined with others. Let's look at the example: 256 | 257 | ```scala mdoc:reset 258 | import cats.parse.Rfc5234.{digit, sp} 259 | 260 | val p = sp *> digit <* sp 261 | 262 | p.parse(" 1") 263 | ``` 264 | 265 | `Parser.Error` contains two parameters: 266 | 267 | ```scala 268 | final case class Error(input: String, failedAtOffset: Int, expected: NonEmptyList[Expectation]) 269 | 270 | case class InRange(offset: Int, lower: Char, upper: Char) extends Expectation 271 | ``` 272 | 273 | In the error message we see the failed offset and the expected value. There is a lot of expected error types which can be found in source code. 274 | 275 | One thing we can do in this situation is providing a fallback parser which can be used in case of error. We can do this by using `backtrack` (which rewinds the input, so it will be passed to fallback parser as it was before the error) and combining it with `orElse` operator: 276 | 277 | ```scala mdoc:reset 278 | import cats.parse.Rfc5234.{digit, sp} 279 | 280 | val p1 = sp *> digit <* sp 281 | val p2 = sp *> digit 282 | 283 | p1.backtrack.orElse(p2).parse(" 1") 284 | 285 | (p1.backtrack | p2 ).parse(" 1") 286 | ``` 287 | 288 | Notice that `(p1.backtrack | p2)` clause is another parser by itself since we're still combining parsers by using `orElse`. 289 | 290 | But we've already used `orElse` in example before without any `backtrack` operator, and it worked just fine. Why do we need `backtrack` now? Let's look at this example: 291 | 292 | ```scala mdoc:reset 293 | import cats.parse.Rfc5234.{digit, sp} 294 | 295 | val p1 = sp *> digit <* sp 296 | val p2 = sp *> digit 297 | val p3 = digit 298 | 299 | (p1 | p2).parse(" 1") 300 | 301 | (p1 | p2 | p3).parse("1") 302 | ``` 303 | 304 | The first parser combination is interrupted by _arresting failures_ and the second parsing combination will only suffer from _epsilon failures_. The second parser works because `orElse` and `|` operators actually allows recovering from epsilon failures, but not from arresting failures. 305 | 306 | So the `backtrack` helps us where the _left side_ returns arresting failure. 307 | 308 | ### Soft 309 | 310 | This method might look similar to `backtrack`, but it allows us to _proceed_ the parsing when the _right side_ is returning an epsilon failure. It is really useful for ambiguous parsers when we can't really tell what exactly we are parsing before the end. Let's say we want to parse some input to the search engine which contains fields. This might look like "field:search_query". Let's try to write a parser for this: 311 | 312 | ```scala mdoc:reset 313 | import cats.parse.Rfc5234.{alpha, sp} 314 | import cats.parse.Parser 315 | import cats.parse.Parser.{char => pchar} 316 | 317 | val searchWord = alpha.rep.string 318 | 319 | val fieldValue = alpha.rep.string ~ pchar(':') 320 | 321 | val p1 = fieldValue.? ~ (searchWord ~ sp.?).rep.string 322 | 323 | 324 | p1.parse("title:The Wind Has Risen") 325 | 326 | p1.parse("The Wind Has Risen") 327 | ``` 328 | 329 | This error happens because we can't really tell if we are parsing the `fieldValue` before we met a `:` char. We might do this with by writing two parsers, converting the first one's failure to epsilon failure by `backtrack` and then providing fallback parser by `|` operator (which allows the epsilon failures): 330 | 331 | ```scala mdoc 332 | val p2 = fieldValue.? ~ (searchWord ~ sp.?).rep.string 333 | 334 | val p3 = (searchWord ~ sp.?).rep.string 335 | 336 | (p2.backtrack | p3).parse("title:The Wind Has Risen") 337 | 338 | (p2.backtrack | p3).parse("The Wind Has Risen") 339 | ``` 340 | 341 | But this problem might be resolved with `soft` method inside the first parser since the right side of it actually returns an epsilon failure itself: 342 | 343 | ```scala mdoc 344 | val fieldValueSoft = alpha.rep.string.soft ~ pchar(':') 345 | 346 | val p4 = fieldValueSoft.? ~ (searchWord ~ sp.?).rep.string 347 | 348 | p4.parse("title:The Wind Has Risen") 349 | 350 | p4.parse("The Wind Has Risen") 351 | ``` 352 | 353 | So when the _right side_ returns an epsilon failure the `soft` method allows us to rewind parsed input and try to proceed it's parsing with next parsers (without changing the parser itself!). 354 | 355 | # JSON parser example 356 | 357 | Below is most of a json parser (the string unescaping is elided). This example can give you a feel 358 | for what it is like to use this library. 359 | 360 | ```scala mdoc:invisible 361 | import cats.parse.strings.Json.delimited.{parser => jsonString} 362 | ``` 363 | 364 | ```scala mdoc 365 | import cats.parse.{Parser0, Parser => P, Numbers} 366 | import org.typelevel.jawn.ast._ 367 | 368 | object Json { 369 | private[this] val whitespace: P[Unit] = P.charIn(" \t\r\n").void 370 | private[this] val whitespaces0: Parser0[Unit] = whitespace.rep0.void 371 | 372 | val parser: P[JValue] = P.recursive[JValue] { recurse => 373 | val pnull = P.string("null").as(JNull) 374 | val bool = P.string("true").as(JBool.True).orElse(P.string("false").as(JBool.False)) 375 | val str = jsonString.map(JString(_)) 376 | val num = Numbers.jsonNumber.map(JNum(_)) 377 | 378 | val listSep: P[Unit] = 379 | P.char(',').soft.surroundedBy(whitespaces0).void 380 | 381 | def rep[A](pa: P[A]): Parser0[List[A]] = 382 | pa.repSep0(listSep).surroundedBy(whitespaces0) 383 | 384 | val list = rep(recurse).with1 385 | .between(P.char('['), P.char(']')) 386 | .map { vs => JArray.fromSeq(vs) } 387 | 388 | val kv: P[(String, JValue)] = 389 | jsonString ~ (P.char(':').surroundedBy(whitespaces0) *> recurse) 390 | 391 | val obj = rep(kv).with1 392 | .between(P.char('{'), P.char('}')) 393 | .map { vs => JObject.fromSeq(vs) } 394 | 395 | P.oneOf(str :: num :: list :: obj :: bool :: pnull :: Nil) 396 | } 397 | 398 | // any whitespace followed by json followed by whitespace followed by end 399 | val parserFile: P[JValue] = whitespaces0.with1 *> parser <* (whitespaces0 ~ P.end) 400 | } 401 | ``` 402 | 403 | # Performance 404 | 405 | We have a benchmark suite that compares JSON parsing across several commonly used libraries. A 406 | recent (2021/11/05) result is below: 407 | 408 | ``` 409 | [info] Benchmark Mode Cnt Score Error Units 410 | [info] BarBench.catsParseParse avgt 4 ≈ 10⁻⁴ ms/op 411 | [info] BarBench.fastparseParse avgt 4 ≈ 10⁻⁴ ms/op 412 | [info] BarBench.jawnParse avgt 4 ≈ 10⁻⁴ ms/op 413 | [info] BarBench.parboiled2Parse avgt 4 ≈ 10⁻⁴ ms/op 414 | [info] BarBench.parsleyParseCold avgt 4 0.064 ± 0.001 ms/op 415 | [info] Bla25Bench.catsParseParse avgt 4 23.095 ± 0.174 ms/op 416 | [info] Bla25Bench.fastparseParse avgt 4 15.622 ± 0.414 ms/op 417 | [info] Bla25Bench.jawnParse avgt 4 7.501 ± 0.143 ms/op 418 | [info] Bla25Bench.parboiled2Parse avgt 4 18.423 ± 6.094 ms/op 419 | [info] Bla25Bench.parsleyParseCold avgt 4 30.752 ± 0.279 ms/op 420 | [info] CountriesBench.catsParseParse avgt 4 7.169 ± 0.041 ms/op 421 | [info] CountriesBench.fastparseParse avgt 4 5.023 ± 0.023 ms/op 422 | [info] CountriesBench.jawnParse avgt 4 1.235 ± 0.011 ms/op 423 | [info] CountriesBench.parboiled2Parse avgt 4 2.936 ± 0.008 ms/op 424 | [info] CountriesBench.parsleyParseCold avgt 4 11.800 ± 0.162 ms/op 425 | [info] Qux2Bench.catsParseParse avgt 4 7.031 ± 0.599 ms/op 426 | [info] Qux2Bench.fastparseParse avgt 4 6.597 ± 0.031 ms/op 427 | [info] Qux2Bench.jawnParse avgt 4 2.227 ± 0.014 ms/op 428 | [info] Qux2Bench.parboiled2Parse avgt 4 5.514 ± 0.472 ms/op 429 | [info] Qux2Bench.parsleyParseCold avgt 4 10.327 ± 0.293 ms/op 430 | [info] StringInBenchmarks.oneOfParse avgt 4 88.105 ± 2.658 ns/op 431 | [info] StringInBenchmarks.stringInParse avgt 4 129.246 ± 1.820 ns/op 432 | [info] Ugh10kBench.catsParseParse avgt 4 53.679 ± 1.385 ms/op 433 | [info] Ugh10kBench.fastparseParse avgt 4 45.165 ± 0.356 ms/op 434 | [info] Ugh10kBench.jawnParse avgt 4 11.404 ± 0.068 ms/op 435 | [info] Ugh10kBench.parboiled2Parse avgt 4 31.984 ± 0.748 ms/op 436 | [info] Ugh10kBench.parsleyParseCold avgt 4 77.150 ± 1.093 ms/op 437 | ``` 438 | 439 | Note that parboiled and fastparse both use macros that make them very difficult to port to Dotty. 440 | Jawn is a specialized and optimized JSON parser, so that can be considered an upper bound on 441 | performance. 442 | Keep in mind that parser performance depends both on the parsing library but also how the parser 443 | is written, but these results suggest that this library is already quite competitive. 444 | 445 | # Migrating from Fastparse 446 | 447 | You should find all the Fastparse methods you are used to. If not, feel free to open an issue. 448 | There are a few things to keep in mind: 449 | 450 | 1. In fastparse, you wrap a parser in `P(...)` to make the interior lazy. Following cats, to get a lazily constructed parser use `Parser.defer` or `cats.Defer[Parser].defer`. 451 | 2. In fastparse the `~` operator does tuple concatenation. This can be nice, but also complex to see what the resulting type is. In cats-parse, `~` always returns a Tuple2 containing the parsed values from the left and right. To recover fastparse-like behavior, use cats syntax `(pa, pb, pc...).tupled`. 452 | 3. In fastparse, backtracking is opt-out by using cuts. In cats-parse, backtracking is opt-in using `.backtrack`. Put another way, normal product operations in cats-parse are like `~/` in fastparse. 453 | 4. In cats-parse, using `*>`, `<*`, and `.void` methods can be a significant optimization: if you don't need a result, communicate that to the library with those methods. 454 | 455 | # Getting and Giving Help 456 | 457 | We welcome new contributors and new maintainers. Please feel free to open issues and PRs. If you have any 458 | problem using the library, an issue is the best way to ask a question until we flush out more 459 | documentation. 460 | --------------------------------------------------------------------------------