├── project ├── build.properties └── plugins.sbt ├── version.sbt ├── .gitignore ├── droste └── src │ ├── test │ └── scala │ │ └── io │ │ └── circe │ │ └── droste │ │ └── DrosteSuite.scala │ └── main │ └── scala │ └── io │ └── circe │ └── droste │ └── package.scala ├── pattern └── src │ ├── test │ └── scala │ │ └── io │ │ └── circe │ │ └── pattern │ │ ├── CirceSuite.scala │ │ ├── JsonFSuite.scala │ │ └── package.scala │ └── main │ └── scala │ └── io │ └── circe │ └── pattern │ └── JsonF.scala ├── .scalafmt.conf ├── .github └── workflows │ ├── ci.yml │ └── clean.yml ├── README.md └── scalastyle-config.xml /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=1.6.2 2 | -------------------------------------------------------------------------------- /version.sbt: -------------------------------------------------------------------------------- 1 | ThisBuild / version := "0.2.1-SNAPSHOT" 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | target/ 3 | .idea/ 4 | .idea_modules/ 5 | .bsp/ 6 | .DS_STORE 7 | .cache 8 | .settings 9 | .project 10 | .classpath 11 | tmp/ 12 | -------------------------------------------------------------------------------- /droste/src/test/scala/io/circe/droste/DrosteSuite.scala: -------------------------------------------------------------------------------- 1 | package io.circe.droste 2 | 3 | import higherkindness.droste.laws.BasisLaws 4 | import io.circe.Json 5 | import io.circe.pattern.JsonF 6 | import io.circe.testing.instances._ 7 | import org.scalacheck.Properties 8 | 9 | class DrosteSuite extends Properties("JsonF") { 10 | include(BasisLaws.props[JsonF, Json]("JsonF", "Json")) 11 | } 12 | -------------------------------------------------------------------------------- /pattern/src/test/scala/io/circe/pattern/CirceSuite.scala: -------------------------------------------------------------------------------- 1 | package io.circe.pattern 2 | 3 | import io.circe.testing.ArbitraryInstances 4 | import org.scalatest.flatspec.AnyFlatSpec 5 | import org.scalatestplus.scalacheck.ScalaCheckDrivenPropertyChecks 6 | import org.typelevel.discipline.scalatest.FlatSpecDiscipline 7 | 8 | trait CirceSuite extends AnyFlatSpec with FlatSpecDiscipline with ScalaCheckDrivenPropertyChecks with ArbitraryInstances 9 | -------------------------------------------------------------------------------- /.scalafmt.conf: -------------------------------------------------------------------------------- 1 | version = 3.5.0 2 | runner.dialect = scala213 3 | continuationIndent.defnSite = 2 4 | docstrings.style = Asterisk 5 | includeCurlyBraceInSelectChains = false 6 | maxColumn = 120 7 | newlines.alwaysBeforeElseAfterCurlyIf = false 8 | newlines.alwaysBeforeMultilineDef = false 9 | optIn.breakChainOnFirstMethodDot = false 10 | spaces.inImportCurlyBraces = true 11 | rewrite.rules = [ 12 | AvoidInfix, 13 | RedundantBraces, 14 | RedundantParens, 15 | AsciiSortImports, 16 | PreferCurlyFors 17 | ] 18 | -------------------------------------------------------------------------------- /droste/src/main/scala/io/circe/droste/package.scala: -------------------------------------------------------------------------------- 1 | package io.circe 2 | 3 | import cats.kernel.Eq 4 | import cats.~> 5 | import higherkindness.droste.{ Algebra, Basis, Coalgebra, Delay } 6 | import higherkindness.droste.syntax.compose.∘ 7 | import io.circe.pattern.JsonF 8 | 9 | package object droste { 10 | val jsonAlgebra: Algebra[JsonF, Json] = Algebra(JsonF.foldJson) 11 | val jsonCoalgebra: Coalgebra[JsonF, Json] = Coalgebra(JsonF.unfoldJson) 12 | 13 | implicit val jsonBasis: Basis[JsonF, Json] = Basis.Default(jsonAlgebra, jsonCoalgebra) 14 | 15 | implicit val jsonDelayedEq: Delay[Eq, JsonF] = 16 | λ[Eq ~> (Eq ∘ JsonF)#λ](eq => JsonF.jsonFEqInstance(eq)) 17 | } 18 | -------------------------------------------------------------------------------- /pattern/src/test/scala/io/circe/pattern/JsonFSuite.scala: -------------------------------------------------------------------------------- 1 | package io.circe.pattern 2 | 3 | import cats.instances.int._, cats.instances.option._, cats.instances.set._ 4 | import cats.laws.discipline.TraverseTests 5 | import io.circe.Json 6 | import io.circe.pattern.JsonF.{ foldJson, unfoldJson } 7 | 8 | class JsonFSuite extends CirceSuite { 9 | checkAll("Traverse[JsonF]", TraverseTests[JsonF].traverse[Int, Int, Int, Set[Int], Option, Option]) 10 | 11 | "fold then unfold" should "be identity " in forAll { jsonF: JsonF[Json] => 12 | assert(unfoldJson(foldJson(jsonF)) === jsonF) 13 | } 14 | 15 | "unfold then fold" should "be identity " in forAll { json: Json => assert(foldJson(unfoldJson(json)) === json) } 16 | } 17 | -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | addSbtPlugin("com.codecommit" % "sbt-github-actions" % "0.13.0") 2 | addSbtPlugin("com.github.sbt" % "sbt-release" % "1.1.0") 3 | addSbtPlugin("com.github.sbt" % "sbt-pgp" % "2.1.2") 4 | addSbtPlugin("com.typesafe" % "sbt-mima-plugin" % "1.0.1") 5 | addSbtPlugin("com.typesafe.sbt" % "sbt-ghpages" % "0.6.3") 6 | addSbtPlugin("com.typesafe.sbt" % "sbt-git" % "1.0.2") 7 | addSbtPlugin("com.typesafe.sbt" % "sbt-site" % "1.4.1") 8 | addSbtPlugin("org.portable-scala" % "sbt-scalajs-crossproject" % "1.1.0") 9 | addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.7.1") 10 | addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.4.6") 11 | addSbtPlugin("org.scalastyle" %% "scalastyle-sbt-plugin" % "1.0.0") 12 | addSbtPlugin("org.scoverage" % "sbt-scoverage" % "1.9.3") 13 | -------------------------------------------------------------------------------- /pattern/src/test/scala/io/circe/pattern/package.scala: -------------------------------------------------------------------------------- 1 | package io.circe 2 | 3 | import io.circe.pattern.JsonF.{ ArrayF, BooleanF, NullF, NumberF, ObjectF, StringF } 4 | import io.circe.testing.instances._ 5 | import org.scalacheck.{ Arbitrary, Gen, Shrink } 6 | 7 | package object pattern { 8 | implicit def arbitraryJsonF[A: Arbitrary]: Arbitrary[JsonF[A]] = 9 | Arbitrary( 10 | Gen.oneOf[JsonF[A]]( 11 | Arbitrary.arbitrary[Boolean].map(BooleanF), 12 | Arbitrary.arbitrary[JsonNumber].map(NumberF), 13 | Gen.const(NullF), 14 | Arbitrary.arbitrary[String].map(StringF), 15 | Arbitrary.arbitrary[Vector[(String, A)]].map(_.groupBy(_._1).mapValues(_.head._2).toVector).map(ObjectF.apply), 16 | Arbitrary.arbitrary[Vector[A]].map(ArrayF.apply) 17 | ) 18 | ) 19 | 20 | implicit def shrinkJsonF[A](implicit A: Shrink[A]): Shrink[JsonF[A]] = Shrink { 21 | case JsonF.NullF => Stream.empty 22 | case JsonF.BooleanF(_) => Stream.empty 23 | case JsonF.NumberF(n) => shrinkJsonNumber.shrink(n).map(JsonF.NumberF(_)) 24 | case JsonF.StringF(s) => Shrink.shrinkString.shrink(s).map(JsonF.StringF(_)) 25 | case JsonF.ArrayF(values) => 26 | Shrink.shrinkContainer[Vector, A].shrink(values).map(JsonF.ArrayF(_)) 27 | case JsonF.ObjectF(fields) => 28 | Shrink.shrinkContainer[Vector, (String, A)].shrink(fields).map(JsonF.ObjectF(_)) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /.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: ['**'] 13 | push: 14 | branches: ['**'] 15 | 16 | env: 17 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 18 | 19 | jobs: 20 | build: 21 | name: Build and Test 22 | strategy: 23 | matrix: 24 | os: [ubuntu-latest] 25 | scala: [2.12.15, 2.13.7] 26 | java: [adopt@1.8] 27 | runs-on: ${{ matrix.os }} 28 | steps: 29 | - name: Checkout current branch (full) 30 | uses: actions/checkout@v2 31 | with: 32 | fetch-depth: 0 33 | 34 | - name: Setup Java and Scala 35 | uses: olafurpg/setup-scala@v13 36 | with: 37 | java-version: ${{ matrix.java }} 38 | 39 | - name: Cache sbt 40 | uses: actions/cache@v2 41 | with: 42 | path: | 43 | ~/.sbt 44 | ~/.ivy2/cache 45 | ~/.coursier/cache/v1 46 | ~/.cache/coursier/v1 47 | ~/AppData/Local/Coursier/Cache/v1 48 | ~/Library/Caches/Coursier/v1 49 | key: ${{ runner.os }}-sbt-cache-v2-${{ hashFiles('**/*.sbt') }}-${{ hashFiles('project/build.properties') }} 50 | 51 | - name: Check that workflows are up to date 52 | run: sbt ++${{ matrix.scala }} githubWorkflowCheck 53 | 54 | - name: Test 55 | run: sbt ++${{ matrix.scala }} clean coverage scalastyle scalafmtCheckAll scalafmtSbtCheck test coverageReport 56 | 57 | - uses: codecov/codecov-action@v1 58 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # circe-droste 2 | 3 | [![Build status](https://img.shields.io/github/workflow/status/circe/circe-droste/Continuous%20Integration.svg)](https://github.com/circe/circe-droste/actions) 4 | [![Coverage status](https://img.shields.io/codecov/c/github/circe/circe-droste/master.svg)](https://codecov.io/github/circe/circe-droste) 5 | [![Gitter](https://img.shields.io/badge/gitter-join%20chat-green.svg)](https://gitter.im/circe/circe) 6 | [![Maven Central](https://img.shields.io/maven-central/v/io.circe/circe-droste_2.13.svg)](https://maven-badges.herokuapp.com/maven-central/io.circe/circe-droste_2.13) 7 | 8 | This project includes some tools for working with [Circe][circe]'s representation of JSON documents using recursion 9 | schemes. It currently includes a pattern functor for `io.circe.Json` and some basic integration with [Droste][droste]. 10 | 11 | ## Usage 12 | 13 | Count all the nulls anywhere in a document! 14 | 15 | ```scala 16 | import higherkindness.droste.Algebra, higherkindness.droste.scheme.cata 17 | import io.circe.pattern.JsonF, io.circe.droste._, io.circe.literal._ 18 | 19 | val nullCounter: Algebra[JsonF, Int] = Algebra { 20 | case JsonF.NullF => 1 21 | case JsonF.ArrayF(xs) => xs.sum 22 | case JsonF.ObjectF(fs) => fs.map(_._2).sum 23 | case _ => 0 24 | } 25 | 26 | val doc = json"""{"x":[null,{"y":[1,null,true,[null,null]]}]}""" 27 | 28 | val result = cata(nullCounter).apply(doc) // result: Int = 4 29 | ``` 30 | 31 | Or you can use Droste's `foldMap`: 32 | 33 | ```scala 34 | import cats.instances.int._ 35 | import higherkindness.droste.syntax.project._ 36 | import io.circe.droste._, io.circe.literal._ 37 | 38 | val doc = json"""{"x":[null,{"y":[1,null,true,[null,null]]}]}""" 39 | 40 | val result = doc.foldMap(j => if (j.isNull) 1 else 0) // result: Int = 4 41 | ``` 42 | 43 | ## Contributors and participation 44 | 45 | This project supports the Scala [code of conduct][code-of-conduct] and we want 46 | all of its channels (Gitter, GitHub, etc.) to be welcoming environments for everyone. 47 | 48 | Please see the [Circe contributors' guide][contributing] for details on how to submit a pull 49 | request. 50 | 51 | ## License 52 | 53 | circe-droste is licensed under the **[Apache License, Version 2.0][apache]** 54 | (the "License"); you may not use this software except in compliance with the 55 | License. 56 | 57 | Unless required by applicable law or agreed to in writing, software 58 | distributed under the License is distributed on an "AS IS" BASIS, 59 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 60 | See the License for the specific language governing permissions and 61 | limitations under the License. 62 | 63 | [apache]: http://www.apache.org/licenses/LICENSE-2.0 64 | [api-docs]: https://circe.github.io/circe-droste/api/io/circe/ 65 | [circe]: https://github.com/circe/circe 66 | [code-of-conduct]: https://www.scala-lang.org/conduct.html 67 | [contributing]: https://circe.github.io/circe/contributing.html 68 | [droste]: https://github.com/higherkindness/droste 69 | -------------------------------------------------------------------------------- /scalastyle-config.xml: -------------------------------------------------------------------------------- 1 | 2 | Circe Configuration 3 | 4 | 5 | FOR 6 | IF 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | true 77 | 78 | 79 | 80 | 81 | all 82 | .+ 83 | 84 | 85 | 86 | -------------------------------------------------------------------------------- /pattern/src/main/scala/io/circe/pattern/JsonF.scala: -------------------------------------------------------------------------------- 1 | package io.circe.pattern 2 | 3 | import cats.{ Applicative, Eval, Traverse } 4 | import cats.instances.tuple._ 5 | import cats.instances.vector._ 6 | import cats.kernel.Eq 7 | import cats.kernel.instances.string._ 8 | import io.circe.{ Json, JsonNumber, JsonObject } 9 | 10 | /** 11 | * A pattern-functor reflecting the JSON datatype structure in a non-recursive way. 12 | */ 13 | sealed trait JsonF[+A] { 14 | def map[B](f: A => B): JsonF[B] 15 | } 16 | 17 | object JsonF { 18 | final case object NullF extends JsonF[Nothing] { 19 | def map[B](f: Nothing => B): JsonF[B] = this 20 | } 21 | final case class BooleanF(b: Boolean) extends JsonF[Nothing] { 22 | def map[B](f: Nothing => B): JsonF[B] = this 23 | } 24 | final case class NumberF(n: JsonNumber) extends JsonF[Nothing] { 25 | def map[B](f: Nothing => B): JsonF[B] = this 26 | } 27 | final case class StringF(s: String) extends JsonF[Nothing] { 28 | def map[B](f: Nothing => B): JsonF[B] = this 29 | } 30 | final case class ArrayF[A](value: Vector[A]) extends JsonF[A] { 31 | def map[B](f: A => B): JsonF[B] = ArrayF(value.map(f)) 32 | } 33 | final case class ObjectF[A](fields: Vector[(String, A)]) extends JsonF[A] { 34 | def map[B](f: A => B): JsonF[B] = ObjectF(fields.map { case (k, v) => (k, f(v)) }) 35 | } 36 | 37 | /** 38 | * An co-algebraic function that unfolds one layer of json into the pattern functor. Can be used for anamorphisms. 39 | */ 40 | def unfoldJson(json: Json): JsonF[Json] = json.foldWith(unfolder) 41 | 42 | /** 43 | * An algebraic function that collapses one layer of pattern-functor into Json. Can be used for catamorphisms. 44 | */ 45 | def foldJson(jsonF: JsonF[Json]): Json = jsonF match { 46 | case NullF => Json.Null 47 | case BooleanF(bool) => Json.fromBoolean(bool) 48 | case StringF(string) => Json.fromString(string) 49 | case NumberF(value) => Json.fromJsonNumber(value) 50 | case ArrayF(vec) => Json.fromValues(vec) 51 | case ObjectF(fields) => Json.obj(fields: _*) 52 | } 53 | 54 | private[this] type Field[A] = (String, A) 55 | private[this] type Fields[A] = Vector[(String, A)] 56 | private[this] val fieldInstance: Traverse[Fields] = catsStdInstancesForVector.compose[Field] 57 | 58 | implicit val jsonFTraverseInstance: Traverse[JsonF] = new Traverse[JsonF] { 59 | override def map[A, B](fa: JsonF[A])(f: A => B): JsonF[B] = fa.map(f) 60 | 61 | override def traverse[G[_], A, B](fa: JsonF[A])(f: A => G[B])(implicit G: Applicative[G]): G[JsonF[B]] = fa match { 62 | case NullF => G.pure(NullF) 63 | case x @ BooleanF(_) => G.pure(x) 64 | case x @ StringF(_) => G.pure(x) 65 | case x @ NumberF(_) => G.pure(x) 66 | case ArrayF(vecA) => G.map(catsStdInstancesForVector.traverse(vecA)(f))(vecB => ArrayF(vecB)) 67 | case ObjectF(fieldsA) => 68 | G.map(fieldInstance.traverse(fieldsA)(f))(fieldsB => ObjectF(fieldsB)) 69 | } 70 | 71 | override def foldLeft[A, B](fa: JsonF[A], b: B)(f: (B, A) => B): B = 72 | fa match { 73 | case NullF => b 74 | case BooleanF(_) => b 75 | case StringF(_) => b 76 | case NumberF(_) => b 77 | case ArrayF(vecA) => vecA.foldLeft(b)(f) 78 | case ObjectF(fieldsA) => 79 | fieldInstance.foldLeft(fieldsA, b)(f) 80 | } 81 | 82 | override def foldRight[A, B](fa: JsonF[A], lb: Eval[B])(f: (A, Eval[B]) => Eval[B]): Eval[B] = 83 | fa match { 84 | case NullF => lb 85 | case BooleanF(_) => lb 86 | case StringF(_) => lb 87 | case NumberF(_) => lb 88 | case ArrayF(vecA) => catsStdInstancesForVector.foldRight(vecA, lb)(f) 89 | case ObjectF(fieldsA) => 90 | fieldInstance.foldRight(fieldsA, lb)(f) 91 | } 92 | } 93 | 94 | implicit def jsonFEqInstance[A: Eq]: Eq[JsonF[A]] = Eq.instance { 95 | case (NullF, NullF) => true 96 | case (BooleanF(b1), BooleanF(b2)) => b1 == b2 97 | case (StringF(s1), StringF(s2)) => s1 == s2 98 | case (NumberF(jn1), NumberF(jn2)) => jn1 == jn2 99 | case (ArrayF(values1), ArrayF(values2)) => Eq[Vector[A]].eqv(values1, values2) 100 | case (ObjectF(values1), ObjectF(values2)) => Eq[Vector[(String, A)]].eqv(values1, values2) 101 | case _ => false 102 | } 103 | 104 | private[this] val unfolder: Json.Folder[JsonF[Json]] = 105 | new Json.Folder[JsonF[Json]] { 106 | def onNull = NullF 107 | def onBoolean(value: Boolean) = BooleanF(value) 108 | def onNumber(value: JsonNumber) = NumberF(value) 109 | def onString(value: String) = StringF(value) 110 | def onArray(value: Vector[Json]) = ArrayF(value) 111 | def onObject(value: JsonObject) = 112 | ObjectF(value.toVector) 113 | } 114 | } 115 | --------------------------------------------------------------------------------