├── .gitignore ├── LICENSE.txt ├── README.md ├── project ├── Publishing.scala └── build.scala └── src ├── main └── scala │ └── jsentric │ ├── AndMatcher.scala │ ├── Codecs.scala │ ├── Composite.scala │ ├── Contract.scala │ ├── Extractors.scala │ ├── Functions.scala │ ├── Jsentric.scala │ ├── Lens.scala │ ├── Matcher.scala │ ├── MaybeStrictness.scala │ ├── Path.scala │ ├── Projection.scala │ ├── Query.scala │ ├── QueryJsonb.scala │ ├── Validation.scala │ ├── Validator.scala │ ├── package.scala │ └── queryTree │ ├── QueryTree.scala │ └── Tree.scala └── test └── scala └── jsentric ├── ContractTests.scala ├── ExtractorCompositorTests.scala ├── FunctionsTests.scala ├── LensTests.scala ├── ProjectionTests.scala ├── QueryJsonbTests.scala ├── QueryTests.scala ├── Readme.scala ├── ValidatorTests.scala └── queryTree └── QueryTreeTests.scala /.gitignore: -------------------------------------------------------------------------------- 1 | # use glob syntax. 2 | syntax: glob 3 | *.ser 4 | *.class 5 | *~ 6 | *.bak 7 | *.off 8 | *.old 9 | .DS_Store 10 | 11 | # building 12 | lib_managed 13 | project/boot 14 | project/target 15 | target 16 | logs 17 | 18 | # IDEA 19 | *.iml 20 | *.ipr 21 | *.iws 22 | /.idea/ 23 | /.idea_modules/ 24 | 25 | # ECLIPSE 26 | .classpath 27 | .project 28 | .settings 29 | 30 | # SBT 31 | /.history 32 | publish.sbt -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | 179 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # jsentric 2 | Json contract patterns, validation, lenses and query 3 | 4 | "io.higherstate" %% "jsentric" % "1.0.0" 5 | 6 | resolvers ++= Seq( 7 | "Sonatype releases" at "http://oss.sonatype.org/content/repositories/releases/", 8 | ) 9 | 10 | jsentric is built upon [argonaut][] and is designed to facilitate the use of the basic json datatypes in cases where we have partially dynamic data or are regularly moving through bounded context and may not wish to constantly serialize/deserialize from class objects. 11 | 12 | jsentric works by describing a singleton contract which represents data we might wish to extract from the json data structure. By doing so, we get easy validation, lenses and even a type safe mongo db query generator. 13 | 14 | ```scala 15 | /*define a contract, 16 | \ \? \! expected, optional, default properties 17 | \: \:? \:! expected, optional, default array properties 18 | \\ \\? expected, option object properties 19 | */ 20 | object Order extends Contract { 21 | val firstName = \[String]("firstName", nonEmptyOrWhiteSpace) 22 | val lastName = \[String]("lastName", nonEmptyOrWhiteSpace) 23 | val orderId = \?[Int]("orderId", reserved && immutable) 24 | 25 | val email = new \\("email") { 26 | val friendlyName = \?[String]("friendlyName") 27 | val address = \[String]("address") 28 | } 29 | val status = \?[String]("status", in("pending", "processing", "sent") && reserved) 30 | val notes = \?[String]("notes", internal) 31 | 32 | val orderLines = \:[(String, Int)]("orderLines", forall(custom[(String, Int)](ol => ol._2 >= 0, "Cannot order negative items"))) 33 | 34 | import Composite._ 35 | //Combine properties to make a composite pattern matcher 36 | lazy val fullName = firstName @: lastName 37 | } 38 | 39 | import argonaut._ 40 | 41 | //Create a new Json object 42 | val newOrder = Order.$create{o => 43 | o.firstName.$set("John") ~ 44 | o.lastName.$set("Smith") ~ 45 | o.email.address.$set("johnSmith@test.com") ~ 46 | o.orderLines.$append("Socks" -> 3) 47 | } 48 | 49 | //validate a new json object 50 | val validated = Order.$validate(newOrder) 51 | 52 | //pattern match property values 53 | newOrder match { 54 | case Order.email.address(email) && Order.email.friendlyName(Some(name)) => 55 | println(s"$email <$name>") 56 | case Order.email.address(email) && Order.fullName(firstName, lastName) => 57 | println(s"$email <$firstName $lastName>") 58 | } 59 | 60 | //make changes to the json object. 61 | val pending = 62 | Order{o => 63 | o.orderId.$set(123) ~ 64 | o.status.$set("pending") ~ 65 | o.notes.$modify(maybe => Some(maybe.foldLeft("Order now pending.")(_ + _))) 66 | }(newOrder) 67 | 68 | //strip out any properties marked internal 69 | val sendToClient = Order.$sanitize(pending) 70 | 71 | //generate query json 72 | val relatedOrdersQuery = Order.orderId.$gt(56) && Order.status.$in("processing", "sent") 73 | //experimental convert to postgres jsonb clause 74 | val postgresQuery = QueryJsonb("data", relatedOrdersQuery) 75 | 76 | import scalaz.{\/, \/-} 77 | //create a dynamic property 78 | val dynamic = Order.$dynamic[\/[String, Int]]("age") 79 | 80 | sendToClient match { 81 | case dynamic(Some(\/-(ageInt))) => 82 | println(ageInt) 83 | case _ => 84 | } 85 | 86 | val statusDelta = Order.$create(_.status.$set("processing")) 87 | //validate against current state 88 | Order.$validate(statusDelta, pending) 89 | //apply delta to current state 90 | val processing = pending.delta(statusDelta) 91 | 92 | //Define subcontract for reusable or recursive structures 93 | trait UserTimestamp extends SubContract { 94 | val account = \[String]("account") 95 | val timestamp = \[Long]("timestamp") 96 | } 97 | object Element extends Contract { 98 | val created = new \\("created", immutable) with UserTimestamp 99 | val modified = new \\("modified") with UserTimestamp 100 | } 101 | 102 | //try to force a match even if wrong type 103 | import LooseCodecs._ 104 | Json("orderId" := "23628") match { 105 | case Order.orderId(Some(id)) => id 106 | } 107 | ``` 108 | 109 | *Auto generation of schema information is still a work in progress 110 | 111 | *mongo query is not a full feature set. 112 | 113 | [argonaut]: http://argonaut.io/ 114 | -------------------------------------------------------------------------------- /project/Publishing.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2011 Alois Cochard 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import sbt._ 18 | import Keys._ 19 | 20 | object Publishing extends Sonatype(build) { 21 | def projectUrl = "https://github.com/higherstate/jsentric" 22 | def developerId = "JamiePullar" 23 | def developerName = "Jamie Pullar" 24 | def licenseName = "Apache License" 25 | def licenseUrl = "http://www.apache.org/licenses/LICENSE-2.0.txt" 26 | } 27 | 28 | /* Sonatype Publishing */ 29 | 30 | // Please note it's necessary to: 31 | // 32 | // 1. PGP sign artifacts using the following plugin: 33 | // http://www.scala-sbt.org/xsbt-gpg-plugin/ 34 | // 35 | // 2. Add your sonatype credentials in '~/.ivy2/.credentials' using the following format: 36 | // realm=Sonatype Nexus Repository Manager 37 | // host=oss.sonatype.org 38 | // user= 39 | // password= 40 | 41 | abstract class Sonatype(build: Build) { 42 | import build._ 43 | 44 | val ossSnapshots = "Sonatype OSS Snapshots" at "https://oss.sonatype.org/content/repositories/snapshots/" 45 | val ossStaging = "Sonatype OSS Staging" at "https://oss.sonatype.org/service/local/staging/deploy/maven2/" 46 | 47 | def projectUrl: String 48 | def developerId: String 49 | def developerName: String 50 | 51 | def licenseName: String 52 | def licenseUrl: String 53 | def licenseDistribution = "repo" 54 | def scmUrl = projectUrl 55 | def scmConnection = "scm:git:" + scmUrl 56 | 57 | def generatePomExtra(scalaVersion: String): xml.NodeSeq = { 58 | { projectUrl } 59 | 60 | 61 | { licenseName } 62 | { licenseUrl } 63 | { licenseDistribution } 64 | 65 | 66 | 67 | { scmUrl } 68 | { scmConnection } 69 | 70 | 71 | 72 | { developerId } 73 | { developerName } 74 | 75 | 76 | } 77 | 78 | def settings: Seq[Setting[_]] = Seq( 79 | credentialsSetting, 80 | publishMavenStyle := true, 81 | publishTo <<= version((v: String) => Some( if (v.trim endsWith "SNAPSHOT") ossSnapshots else ossStaging)), 82 | publishArtifact in Test := false, 83 | pomIncludeRepository := (_ => false), 84 | pomExtra <<= (scalaVersion)(generatePomExtra) 85 | ) 86 | 87 | lazy val credentialsSetting = credentials += { 88 | Seq("SONATYPE_USER", "SONATYPE_PASS").map(k => sys.env.get(k)) match { 89 | case Seq(Some(user), Some(pass)) => 90 | Credentials("Sonatype Nexus Repository Manager", "oss.sonatype.org", user, pass) 91 | case _ => 92 | Credentials(Path.userHome / ".ivy2" / ".credentials") 93 | } 94 | } 95 | } -------------------------------------------------------------------------------- /project/build.scala: -------------------------------------------------------------------------------- 1 | import sbt._ 2 | import Keys._ 3 | 4 | object build extends Build { 5 | 6 | val scala = "2.11.8" 7 | 8 | lazy val jsentric = Project( 9 | id = "jsentric", 10 | base = file("."), 11 | settings = Defaults.defaultSettings ++ commonSettings ++ Publishing.settings 12 | ) 13 | 14 | lazy val commonSettings = Seq( 15 | organization := "io.higherstate", 16 | version := "1.0.2", 17 | scalaVersion := scala, 18 | scalacOptions ++= Seq( 19 | "-deprecation", 20 | "-encoding", "UTF-8", 21 | "-feature", 22 | "-language:implicitConversions", "-language:higherKinds", "-language:postfixOps", "-language:reflectiveCalls", 23 | "-unchecked", 24 | "-Xfatal-warnings", 25 | "-Yinline-warnings", 26 | "-Yno-adapted-args", 27 | "-Ywarn-dead-code", 28 | "-Ywarn-value-discard", 29 | "-Xfuture" 30 | ), 31 | javacOptions ++= Seq("-target", "1.8", "-source", "1.8", "-Xlint:deprecation"), 32 | libraryDependencies ++= Seq( 33 | "org.scala-lang" % "scala-reflect" % "2.11.8", 34 | "org.scala-lang.modules" %% "scala-xml" % "1.0.5", 35 | "org.scalaz" %% "scalaz-core" % "7.1.7", 36 | "io.argonaut" %% "argonaut" % "6.1", 37 | "com.chuusai" %% "shapeless" % "2.3.0", 38 | "joda-time" % "joda-time" % "2.9.2", 39 | "org.joda" % "joda-convert" % "1.8", 40 | "org.scalatest" %% "scalatest" % "2.2.6" % "test" 41 | ), 42 | resolvers ++= Seq( 43 | "Maven Central Server" at "http://repo1.maven.org/maven2", 44 | "Sonatype releases" at "http://oss.sonatype.org/content/repositories/releases/", 45 | "Sonatype snapshots" at "http://oss.sonatype.org/content/repositories/snapshots/" 46 | ) 47 | ) 48 | } -------------------------------------------------------------------------------- /src/main/scala/jsentric/AndMatcher.scala: -------------------------------------------------------------------------------- 1 | package jsentric 2 | 3 | import argonaut.{CodecJson, Json} 4 | 5 | trait AndMatcher { 6 | object && { 7 | def unapply[A](a: A) = Some((a, a)) 8 | } 9 | } 10 | trait CodecMatcher { 11 | def Codec[T](implicit codec:CodecJson[T]) = 12 | new CodecWrapper[T](codec) 13 | } 14 | 15 | class CodecWrapper[T](val codec:CodecJson[T]) extends AnyVal { 16 | def unapply(j: Json): Option[T] = 17 | codec.decodeJson(j).toOption 18 | 19 | def apply(t:T):Json = 20 | codec.encode(t) 21 | } -------------------------------------------------------------------------------- /src/main/scala/jsentric/Codecs.scala: -------------------------------------------------------------------------------- 1 | package jsentric 2 | 3 | import argonaut._ 4 | import Argonaut._ 5 | import scalaz.{\/-, -\/, \/} 6 | 7 | object JsonSchema { 8 | val TYPE = "type" 9 | val ITEMS = "items" 10 | val PROPERTIES = "properties" 11 | val REQUIRED = "required" 12 | val DEFAULT = "default" 13 | } 14 | 15 | trait SeqExtractor[T] { 16 | def unapply(s:Seq[Json]):Option[Seq[T]] 17 | } 18 | 19 | trait Codecs extends EncodeJsons with DecodeJsons { 20 | 21 | implicit lazy val booleanCodec = 22 | argonaut.CodecJson.derived[Boolean] 23 | 24 | implicit lazy val stringCodec = 25 | argonaut.CodecJson.derived[String] 26 | 27 | implicit lazy val longCodec = 28 | argonaut.CodecJson.derived[Long] 29 | 30 | implicit lazy val intCodec = 31 | argonaut.CodecJson.derived[Int] 32 | 33 | implicit lazy val doubleCodec = 34 | argonaut.CodecJson.derived[Double] 35 | 36 | implicit lazy val floatCodec = 37 | argonaut.CodecJson.derived[Float] 38 | 39 | implicit lazy val jsonObjectEncoder = new EncodeJson[JsonObject] { 40 | def encode(a: JsonObject): Json = 41 | jObject(a) 42 | } 43 | implicit lazy val jsonObjectDecoder = 44 | optionDecoder(_.obj, "object") 45 | 46 | implicit lazy val jsonObjectCodec = 47 | argonaut.CodecJson.derived[JsonObject] 48 | 49 | implicit lazy val jsonCodec = 50 | argonaut.CodecJson.derived[Json] 51 | 52 | implicit lazy val jsonArrayCodec = 53 | argonaut.CodecJson.derived[JsonArray] 54 | 55 | implicit def optionCodec[T](implicit codec:CodecJson[T]) = 56 | argonaut.CodecJson.derived(OptionEncodeJson(codec.Encoder), OptionDecodeJson(codec.Decoder)) 57 | 58 | 59 | implicit def tupleCodec[T1,T2](implicit codec1:CodecJson[T1], codec2:CodecJson[T2]) = 60 | argonaut.CodecJson.derived(Tuple2EncodeJson(codec1.Encoder, codec2.Encoder), Tuple2DecodeJson(codec1.Decoder, codec2.Decoder)) 61 | 62 | implicit def eitherCodec[L, R](implicit left:CodecJson[L], right:CodecJson[R]) = 63 | argonaut.CodecJson.derived( 64 | new EncodeJson[Either[L,R]]{ 65 | def encode(a: Either[L, R]): Json = { 66 | a match { 67 | case Left(l) => left.encode(l) 68 | case Right(r) => right.encode(r) 69 | } 70 | } 71 | }, 72 | optionDecoder[Either[L,R]](e => left.decodeJson(e).toOption.map(Left(_)).orElse(right.decodeJson(e).toOption.map(Right(_))), "either") 73 | ) 74 | 75 | implicit def disruptCodec[L, R](implicit left:CodecJson[L], right:CodecJson[R]) = 76 | argonaut.CodecJson.derived( 77 | new EncodeJson[\/[L,R]]{ 78 | def encode(a: \/[L, R]): Json = { 79 | a match { 80 | case -\/(l) => left.encode(l) 81 | case \/-(r) => right.encode(r) 82 | } 83 | } 84 | }, 85 | optionDecoder[\/[L,R]](e => left.decodeJson(e).toOption.map(-\/(_)).orElse(right.decodeJson(e).toOption.map(\/-(_))), "either") 86 | ) 87 | } 88 | 89 | trait OptimisticCodecs extends Codecs { 90 | 91 | implicit val maybeStrictness:MaybeStrictness = MaybeOptimistic 92 | 93 | override implicit def BooleanDecodeJson: DecodeJson[Boolean] = { 94 | optionDecoder( x => 95 | x.bool.orElse(x.string.collect { 96 | case t if t.equalsIgnoreCase("true") => true 97 | case f if f.equalsIgnoreCase("false") => false 98 | }) 99 | , "Boolean") 100 | } 101 | 102 | implicit lazy val jSeqCodec:CodecJson[Seq[Json]] = 103 | seqCodec(jsonCodec) 104 | 105 | implicit lazy val jSetCodec:CodecJson[Set[Json]] = 106 | setCodec(jsonCodec) 107 | 108 | implicit lazy val jVectorCodec:CodecJson[Vector[Json]] = 109 | vectorCodec(jsonCodec) 110 | 111 | implicit def seqCodec[T](implicit codec:CodecJson[T]) = 112 | argonaut.CodecJson.derived[Seq[T]]( 113 | new EncodeJson[Seq[T]]{ 114 | def encode(a: Seq[T]): Json = 115 | jArray(a.map(codec.encode).toList) 116 | }, 117 | optionDecoder[Seq[T]](_.array.map(_.flatMap(t => codec.decodeJson(t).toOption)), "array") 118 | ) 119 | 120 | implicit def setCodec[T](implicit codec:CodecJson[T]) = 121 | argonaut.CodecJson.derived[Set[T]]( 122 | new EncodeJson[Set[T]]{ 123 | def encode(a: Set[T]): Json = 124 | jArray(a.map(codec.encode).toList) 125 | }, 126 | optionDecoder[Set[T]](_.array.map(_.flatMap(t => codec.decodeJson(t).toOption).toSet), "array") 127 | ) 128 | 129 | implicit def vectorCodec[T](implicit codec:CodecJson[T]) = 130 | argonaut.CodecJson.derived[Vector[T]]( 131 | new EncodeJson[Vector[T]]{ 132 | def encode(a: Vector[T]): Json = 133 | jArray(a.map(codec.encode).toList) 134 | }, 135 | optionDecoder[Vector[T]](_.array.map(_.flatMap(t => codec.decodeJson(t).toOption).toVector), "array") 136 | ) 137 | 138 | implicit def mapCodec[T](implicit codec:CodecJson[T]) = 139 | argonaut.CodecJson.derived[Map[String, T]]( 140 | new EncodeJson[Map[String, T]]{ 141 | def encode(a: Map[String, T]): Json = 142 | a.mapValues(codec.encode).asJson 143 | }, 144 | optionDecoder[Map[String, T]](_.obj.map(_.toList.flatMap(t => codec.decodeJson(t._2).toOption.map(t._1 -> _)).toMap), "object") 145 | ) 146 | } 147 | 148 | trait PessimisticCodecs extends Codecs { 149 | import scalaz._ 150 | import Scalaz._ 151 | 152 | implicit val maybeStrictness:MaybeStrictness = MaybeNull 153 | 154 | override implicit def BooleanDecodeJson:DecodeJson[Boolean] = 155 | optionDecoder(x => x.bool, "Boolean") 156 | 157 | override implicit def DoubleDecodeJson: DecodeJson[Double] = 158 | optionDecoder(x => x.number.map(_.toDouble), "Double") 159 | 160 | override implicit def FloatDecodeJson: DecodeJson[Float] = 161 | optionDecoder(x => x.number.map(_.toDouble).collect{ case f if f >= Float.MinValue && f <= Float.MaxValue => f.toFloat}, "Float") 162 | 163 | override implicit def IntDecodeJson: DecodeJson[Int] = 164 | optionDecoder(x => x.number.flatMap { 165 | case JsonLong(l) => 166 | if (l >= Int.MinValue && l <= Int.MaxValue) Some(l.toInt) 167 | else None 168 | case n => Option(n.toDouble).collect { case f if f >= Int.MinValue && f <= Int.MaxValue && f % 1 == 0 => f.toInt } 169 | }, "Int") 170 | 171 | override implicit def LongDecodeJson: DecodeJson[Long] = 172 | optionDecoder(x => x.number.flatMap { 173 | case JsonLong(l) => Some(l) 174 | case n => Option(n.toDouble).collect { case f if f >= Long.MinValue && f <= Long.MaxValue && f % 1 == 0 => f.toLong } 175 | }, "Long") 176 | 177 | override implicit def ShortDecodeJson: DecodeJson[Short] = 178 | optionDecoder(x => x.number.flatMap { 179 | case JsonLong(l) => 180 | if (l >= Short.MinValue && l <= Short.MaxValue) Some(l.toShort) 181 | else None 182 | case n => Option(n.toDouble).collect { case f if f >= Short.MinValue && f <= Short.MaxValue && f % 1 == 0 => f.toShort } 183 | }, "Short") 184 | 185 | override implicit def OptionDecodeJson[A](implicit e: DecodeJson[A]): DecodeJson[Option[A]] = 186 | DecodeJson {r => 187 | if (r.focus.isNull) DecodeResult.ok(None) 188 | else e.decodeJson(r.focus).map(Some(_)) 189 | } 190 | 191 | implicit def jSeqCodec:CodecJson[Seq[Json]] = 192 | seqCodec(jsonCodec) 193 | 194 | implicit def jSetCodec:CodecJson[Set[Json]] = 195 | setCodec(jsonCodec) 196 | 197 | implicit def jVectorCodec:CodecJson[Vector[Json]] = 198 | vectorCodec(jsonCodec) 199 | 200 | implicit def seqCodec[T](implicit codec:CodecJson[T]) = 201 | argonaut.CodecJson.derived[Seq[T]]( 202 | new EncodeJson[Seq[T]]{ 203 | def encode(a: Seq[T]): Json = 204 | jArray(a.map(codec.encode).toList) 205 | }, 206 | optionDecoder[Seq[T]](_.array.flatMap(_.map(t => codec.decodeJson(t).toOption).sequence[Option, T]), "array") 207 | ) 208 | 209 | implicit def setCodec[T](implicit codec:CodecJson[T]) = 210 | argonaut.CodecJson.derived[Set[T]]( 211 | new EncodeJson[Set[T]]{ 212 | def encode(a: Set[T]): Json = 213 | jArray(a.map(codec.encode).toList) 214 | }, 215 | optionDecoder[Set[T]](_.array.flatMap(_.map(t => codec.decodeJson(t).toOption).sequence[Option, T].map(_.toSet)), "array") 216 | ) 217 | 218 | implicit def vectorCodec[T](implicit codec:CodecJson[T]) = 219 | argonaut.CodecJson.derived[Vector[T]]( 220 | new EncodeJson[Vector[T]]{ 221 | def encode(a: Vector[T]): Json = 222 | jArray(a.map(codec.encode).toList) 223 | }, 224 | optionDecoder[Vector[T]](_.array.flatMap(_.map(t => codec.decodeJson(t).toOption).sequence[Option, T].map(_.toVector)), "array") 225 | ) 226 | 227 | implicit def mapCodec[T](implicit codec:CodecJson[T]) = 228 | argonaut.CodecJson.derived[Map[String, T]]( 229 | new EncodeJson[Map[String, T]]{ 230 | def encode(a: Map[String, T]): Json = 231 | a.mapValues(codec.encode).asJson 232 | }, 233 | optionDecoder[Map[String, T]](_.obj.flatMap(_.toList.map(t => codec.decodeJson(t._2).toOption.map(t._1 -> _)).sequence[Option, (String, T)].map(_.toMap)), "object") 234 | ) 235 | } 236 | 237 | 238 | object PessimisticCodecs extends PessimisticCodecs 239 | object OptimisticCodecs extends OptimisticCodecs -------------------------------------------------------------------------------- /src/main/scala/jsentric/Composite.scala: -------------------------------------------------------------------------------- 1 | package jsentric 2 | 3 | import argonaut.Json 4 | import shapeless.ops.hlist.Tupler 5 | import shapeless.{::, HNil, HList} 6 | 7 | trait Evaluator[L <: HList] { 8 | type Out <: HList 9 | 10 | def apply(json:Json, l:L):Option[Out] 11 | } 12 | 13 | object Composite { 14 | 15 | type Aux[T <: HList, O <: HList] = Evaluator[T]{ type Out = O } 16 | 17 | type E[T] = Unapplicable[T] 18 | 19 | implicit val evaluatorHNil:Aux[HNil, HNil] = new Evaluator[HNil] { 20 | type Out = HNil 21 | 22 | def apply(json:Json, l:HNil) = Some(HNil) 23 | } 24 | 25 | implicit def evalHCons[H, T <: HList](implicit evalT: Evaluator[T]): Aux[E[H] :: T, H :: evalT.Out] = 26 | new Evaluator[E[H] :: T] { 27 | type Out = H :: evalT.Out 28 | 29 | def apply(json:Json, l: E[H] :: T):Option[Out] = 30 | for { 31 | h <- l.head.unapply(json) 32 | t <- evalT(json, l.tail) 33 | } yield h :: t 34 | } 35 | 36 | implicit class HListExt[T <: HList](val t:T) extends AnyVal { 37 | def @:[S](prev:Unapplicable[S]):Unapplicable[S] :: T = 38 | prev :: t 39 | } 40 | } 41 | 42 | case class JsonList[L <: HList, O <: HList, T](maybes: L, ev: Composite.Aux[L, O], tpl:Tupler.Aux[O, T]) { 43 | 44 | def unapply(json:Json):Option[T] = { 45 | ev.apply(json, maybes).map{hl => 46 | tpl.apply(hl) 47 | } 48 | } 49 | 50 | //TODO: add $set method, look at FnFromProduct 51 | 52 | def @:[S, T2](prev:Unapplicable[S])(implicit tpl2:Tupler.Aux[S :: O, T2]) = 53 | JsonList(prev :: maybes, Composite.evalHCons[S, L](ev), tpl2) 54 | } -------------------------------------------------------------------------------- /src/main/scala/jsentric/Contract.scala: -------------------------------------------------------------------------------- 1 | package jsentric 2 | 3 | import argonaut._ 4 | import Argonaut._ 5 | import scalaz.Scalaz._ 6 | import shapeless._ 7 | import shapeless.ops.hlist._ 8 | 9 | trait SelfApply { 10 | def apply[R](f:this.type => R):R = f(this) 11 | } 12 | 13 | trait BaseContract extends SelfApply { 14 | implicit protected def absolutePath:Path 15 | 16 | def $dynamic[T](path:String)(implicit codec:CodecJson[T], strictness:MaybeStrictness) = 17 | new Maybe[T](path, absolutePath \ path, EmptyValidator)(codec, strictness) 18 | } 19 | 20 | trait Contract extends BaseContract { 21 | implicit protected def absolutePath:Path = Path.empty 22 | 23 | def unapply(j:Json):Option[Json] = 24 | Some(j) 25 | } 26 | 27 | abstract class ContractType(val $key:String, val matcher:Matcher = DefaultMatcher) extends BaseContract with Unapplicable[Json] { 28 | implicit protected def absolutePath: Path = Path.empty 29 | def unapply(j:Json):Option[Json] = 30 | j.obj.exists(_($key).exists(matcher.isMatch)).option(j) 31 | 32 | } 33 | 34 | trait SubContract { 35 | implicit protected def absolutePath:Path 36 | } 37 | 38 | trait Unapplicable[T] { 39 | def unapply(j:Json):Option[T] 40 | } 41 | 42 | trait Property[T <: Any] extends SelfApply { 43 | def codec: CodecJson[T] 44 | def absolutePath:Path 45 | def relativePath:Path 46 | def validator:Validator[_] 47 | def isValidType(j:Json):Boolean 48 | } 49 | 50 | //implicit codecs private so properties dont get implicit clashes 51 | class Expected[T](val relativePath:Path, implicit val absolutePath:Path, val validator:Validator[T])(implicit private val _codec: CodecJson[T]) 52 | extends Property[T] with Functions with Unapplicable[T] { 53 | def codec = _codec 54 | 55 | def unapply(j:Json):Option[T] = 56 | getValue(j, absolutePath.segments).flatMap(v => codec.decodeJson(v).toOption) 57 | 58 | def isValidType(j:Json) = !codec.decodeJson(j).isError 59 | } 60 | 61 | class Maybe[T](val relativePath:Path, implicit val absolutePath:Path, val validator:Validator[Option[T]])(implicit private val _codec: CodecJson[T], strictness:MaybeStrictness) 62 | extends Property[T] with Functions with Unapplicable[Option[T]] { 63 | def codec = _codec 64 | 65 | def unapply(j:Json):Option[Option[T]] = 66 | getValue(j, absolutePath.segments).fold[Option[Option[T]]](Some(None)) { v => 67 | strictness(v, codec) 68 | } 69 | 70 | def isValidType(j:Json) = 71 | strictness(j, codec).nonEmpty 72 | } 73 | 74 | class Default[T](val relativePath:Path, implicit val absolutePath:Path, val default:T, val validator:Validator[Option[T]])(implicit private val _codec: CodecJson[T], strictness:MaybeStrictness) 75 | extends Property[T] with Functions with Unapplicable[T] { 76 | def codec = _codec 77 | 78 | def unapply(j:Json):Option[T] = 79 | getValue(j, absolutePath.segments).fold[Option[T]](Some(default)) { v => 80 | strictness(v, codec).map(_.getOrElse(default)) 81 | } 82 | 83 | def isValidType(j:Json) = strictness(j, codec).nonEmpty 84 | } 85 | 86 | abstract class ValueContract[T](val validator: Validator[T] = EmptyValidator)(implicit _codec:CodecJson[T]) extends BaseContract with Property[T] { 87 | implicit val absolutePath: Path = Path.empty 88 | val relativePath: Path = Path.empty 89 | def codec: CodecJson[T] = _codec 90 | def unapply(j:Json):Option[T] = 91 | codec.decodeJson(j).toOption 92 | 93 | override def isValidType(j: Json): JsonBoolean = 94 | !codec.decodeJson(j).isError 95 | } 96 | 97 | class EmptyProperty[T](implicit val codec: CodecJson[T]) extends Property[T] { 98 | def absolutePath: Path = Path.empty 99 | def relativePath: Path = Path.empty 100 | def validator: Validator[T] = ??? 101 | def isValidType(j:Json) = false 102 | } 103 | 104 | object \ { 105 | def apply[T](path:Path, validator:Validator[T] = EmptyValidator)(implicit parentPath:Path, codec: CodecJson[T]) = 106 | new Expected[T](path, parentPath ++ path, validator)(codec) 107 | } 108 | 109 | object \? { 110 | def apply[T](path:Path, validator:Validator[Option[T]] = EmptyValidator)(implicit parentPath:Path, codec: CodecJson[T], strictness:MaybeStrictness) = 111 | new Maybe[T](path, parentPath ++ path, validator)(codec, strictness) 112 | } 113 | 114 | object \! { 115 | def apply[T](path:Path, default:T, validator:Validator[Option[T]] = EmptyValidator)(implicit parentPath:Path, codec: CodecJson[T], strictness:MaybeStrictness) = 116 | new Default[T](path, parentPath ++ path, default, validator)(codec, strictness) 117 | } 118 | 119 | abstract class \\(path:Path, validator:Validator[Json] = EmptyValidator)(implicit parentPath:Path) 120 | extends Expected[Json](path, parentPath ++ path, validator)(OptimisticCodecs.jsonCodec) with BaseContract 121 | 122 | abstract class \\?(path:Path, validator:Validator[Option[Json]] = EmptyValidator)(implicit parentPath:Path, strictness:MaybeStrictness) 123 | extends Maybe[Json](path, parentPath ++ path, validator)(OptimisticCodecs.jsonCodec, strictness) with BaseContract 124 | 125 | case class \:[T](path:Path, override val validator:Validator[Seq[T]] = EmptyValidator)( 126 | implicit parentPath:Path, 127 | private val _codec: CodecJson[Seq[T]], 128 | private val _seqCodec: CodecJson[Seq[Json]], 129 | private val _elementCodec: CodecJson[T], 130 | private val _strictness:MaybeStrictness) 131 | extends Expected[Seq[T]](path, parentPath ++ path, validator)(_codec) { 132 | def elementCodec = _elementCodec 133 | def seqCodec = _seqCodec 134 | def strictness = _strictness 135 | } 136 | 137 | case class \:?[T](path:Path, override val validator:Validator[Option[Seq[T]]] = EmptyValidator)( 138 | implicit parentPath:Path, 139 | private val _codec: CodecJson[Seq[T]], 140 | private val _optionCodec: CodecJson[Option[Seq[T]]], 141 | private val _seqCodec: CodecJson[Seq[Json]], 142 | private val _elementCodec: CodecJson[T], 143 | private val _strictness:MaybeStrictness 144 | ) 145 | extends Maybe[Seq[T]](path, parentPath ++ path, validator)(_codec, _strictness) { 146 | def elementCodec = _elementCodec 147 | def seqCodec = _seqCodec 148 | def strictness = _strictness 149 | } -------------------------------------------------------------------------------- /src/main/scala/jsentric/Extractors.scala: -------------------------------------------------------------------------------- 1 | package jsentric 2 | 3 | import argonaut._ 4 | import Argonaut._ 5 | 6 | object JArray { 7 | def unapply(json:Json):Option[JsonArray] = 8 | json.array 9 | } 10 | object JObject { 11 | def unapply(json:Json):Option[JsonObject] = 12 | json.obj 13 | } 14 | object JDouble { 15 | def unapply(json:Json):Option[Double] = 16 | json.number.map(_.toDouble) 17 | } 18 | object JLong { 19 | def unapply(json:Json):Option[Long] = 20 | json.number.collect { 21 | case JsonLong(l) => l 22 | } 23 | } 24 | object JString { 25 | def unapply(json:Json):Option[String] = 26 | json.string 27 | } 28 | object JBool { 29 | def unapply(json:Json):Option[Boolean] = 30 | json.bool 31 | } 32 | object JMap { 33 | def unapply(json:Json):Option[Map[JsonField, Json]] = 34 | json.obj.map(_.toMap) 35 | } -------------------------------------------------------------------------------- /src/main/scala/jsentric/Functions.scala: -------------------------------------------------------------------------------- 1 | package jsentric 2 | 3 | import argonaut._ 4 | import Argonaut._ 5 | import scalaz.Scalaz._ 6 | 7 | //Migrated to Argonaut as is, might be possible to replace with Argonaut functionality 8 | trait Functions extends Any { 9 | /** 10 | * Concatenate and replace json structure with delta, where a JNull or returning an Empty JObject value will clear the key value pair 11 | * @param delta 12 | * @return 13 | */ 14 | def applyDelta(target:Json, delta:Json):Json = 15 | (target.obj, delta.obj) match { 16 | case (Some(ot), Some(od)) => 17 | jObject(od.toList.foldLeft(ot)({ 18 | case (acc, (k, j)) if j.isNull => 19 | acc - k 20 | case (acc, (k, v)) => acc(k) match { 21 | case None => acc + (k, v) 22 | case Some(l) => 23 | val d = applyDelta(l, v) 24 | if (d == jEmptyObject) 25 | acc - k 26 | else 27 | acc + (k, d) 28 | } 29 | })) 30 | case _ => 31 | delta 32 | } 33 | 34 | def select(target:Json, projection:Json):Json = 35 | (target.obj, projection.obj) match { 36 | case (Some(ot), Some(od)) => 37 | jObject(od.toList.foldLeft(JsonObject.empty)({ 38 | case (acc, (k, JLong(1))) => 39 | ot(k).fold(acc){v => 40 | acc.+(k,v) 41 | } 42 | case (acc, (k, j)) => 43 | ot(k).fold(acc){v => 44 | val result = select(v, j) 45 | if (result.obj.exists(_.isEmpty)) acc 46 | else acc.+(k, result) 47 | } 48 | })) 49 | case _ => 50 | jEmptyObject 51 | } 52 | 53 | def difference(delta:Json, source:Json):Option[Json] = 54 | (delta, source) match { 55 | case (d, s) if d == s => 56 | None 57 | case (JObject(d), JObject(j)) => 58 | val s = j.toMap 59 | val o = d.toList.flatMap { kvp => 60 | s.get(kvp._1).fold(Option(kvp)){ v => 61 | difference(kvp._2, v).map(kvp._1 -> _) 62 | } 63 | } 64 | o.nonEmpty.option(Json(o:_*)) 65 | case (d, _) => 66 | Some(d) 67 | } 68 | 69 | def mergeDelta(target:Json, delta:Json):Json = 70 | target.deepmerge(delta) 71 | 72 | 73 | def getValue(target:Json, segments:Segments):Option[Json] = 74 | (segments, target) match { 75 | case (Vector(), _) => 76 | Some(target) 77 | case (Left(head) +: tail, j) => 78 | j.field(head).flatMap(getValue(_, tail)) 79 | case (Right(head) +: tail, j) => 80 | j.array.flatMap(_.lift(head)).flatMap(getValue(_, tail)) 81 | case _ => None 82 | } 83 | 84 | def setValue(target:Option[Json], segments:Segments, value:Json):Json = 85 | (segments, target) match { 86 | case (Left(key) +: tail, Some(j)) => 87 | (key -> setValue(j.field(key), tail, value)) ->: j 88 | case (Left(key) +: tail, _) => 89 | Json(key -> setValue(None, tail, value)) 90 | case (Right(index) +: tail, Some(j)) => 91 | val array = j.arrayOrEmpty 92 | val (left, right) = array.splitAt(index) 93 | if (left.size < index) 94 | jArray(left.padTo(index, jNull) :+ setValue(None, tail, value)) 95 | else 96 | jArray((left :+ setValue(right.headOption, tail, value)) ++ right.tail) 97 | case (Right(index) +: tail, _) => 98 | jArray(List.fill(index)(jNull) :+ setValue(None, tail, value)) 99 | case _ => 100 | value 101 | } 102 | 103 | /** 104 | * Like set value except for in array, final value will be inserted in location rather than replaced 105 | * @param target 106 | * @param segments 107 | * @param value 108 | * @return 109 | */ 110 | def insertValue(target:Option[Json], segments:Segments, value:Json):Json = 111 | (segments, target) match { 112 | case (Left(key) +: tail, Some(j)) => 113 | (key -> setValue(j.field(key), tail, value)) ->: j 114 | case (Left(key) +: tail, _) => 115 | Json(key -> setValue(None, tail, value)) 116 | case (Right(index) +: tail, Some(j)) => 117 | val array = j.arrayOrEmpty 118 | val (left, right) = array.splitAt(index) 119 | if (left.size < index) 120 | jArray(left.padTo(index, jNull) :+ setValue(None, tail, value)) 121 | else if (tail.isEmpty) 122 | jArray(left ++ (value :: right)) 123 | else 124 | jArray((left :+ setValue(right.headOption, tail, value)) ++ right.tail) 125 | case (Right(index) +: tail, _) => 126 | jArray(List.fill(index)(jNull) :+ setValue(None, tail, value)) 127 | case _ => 128 | value 129 | } 130 | 131 | def dropValue(target:Json, segments:Segments):Json = 132 | (segments, target) match { 133 | case (Left(key) +: Vector(), j) => 134 | j.withObject{obj => 135 | obj - key 136 | } 137 | case (Left(key) +: tail, j) => 138 | j.withObject{obj => 139 | obj(key).fold(obj){v => obj + (key, dropValue(v, tail))} 140 | } 141 | case (Right(index) +: Vector(), j) => 142 | j.withArray{arr => 143 | val (left, right) = arr.splitAt(index) 144 | left ++ right.drop(1) 145 | } 146 | case (Right(index) +: tail, j) => 147 | j.withArray {arr => 148 | val (left, right) = arr.splitAt(index) 149 | left ++ (dropValue(right.head, tail) :: right.drop(1)) 150 | } 151 | case _ => 152 | target 153 | } 154 | } 155 | 156 | object Functions extends Functions 157 | -------------------------------------------------------------------------------- /src/main/scala/jsentric/Jsentric.scala: -------------------------------------------------------------------------------- 1 | package jsentric 2 | 3 | import argonaut._ 4 | 5 | object Jsentric extends 6 | PessimisticCodecs with 7 | Validation with 8 | Lens with 9 | Query with 10 | Projection with 11 | ACursors with 12 | Contexts with 13 | Cursors with 14 | CursorHistorys with 15 | CursorOps with 16 | CursorOpElements with 17 | DecodeResults with 18 | HCursors with 19 | Jsons with 20 | JsonIdentitys with 21 | JsonObjects with 22 | PrettyParamss with 23 | AndMatcher with 24 | CodecMatcher with 25 | Validators with 26 | StringWraps -------------------------------------------------------------------------------- /src/main/scala/jsentric/Lens.scala: -------------------------------------------------------------------------------- 1 | package jsentric 2 | 3 | import argonaut._ 4 | import Argonaut._ 5 | 6 | trait Lens extends Functions { 7 | 8 | implicit def lensCombinator(f: Json => Json):LensCombinator = 9 | new LensCombinator(f) 10 | 11 | implicit def valueContractExt[T](c:ValueContract[T]):ValueContractExt[T] = 12 | new ValueContractExt[T](c) 13 | 14 | implicit def contractExt[T <: BaseContract](c:T):ContractExt[T] = 15 | new ContractExt(c) 16 | 17 | implicit def contractTypeExt[T <: ContractType](c:T):ContractTypeExt[T] = 18 | new ContractTypeExt(c) 19 | 20 | implicit def expectedLens[T](prop: Expected[T]):ExpectedLens[T] = 21 | new ExpectedLens(prop) 22 | 23 | implicit def maybeLens[T](prop: Maybe[T]):MaybeLens[T] = 24 | new MaybeLens(prop) 25 | 26 | implicit def defaultLens[T](prop: Default[T]):DefaultLens[T] = 27 | new DefaultLens(prop) 28 | 29 | implicit def jsonLens[T](json:Json):JsonLens[T] = 30 | new JsonLens(json) 31 | 32 | implicit def arrayLens[T](prop: \:[T]):ArrayLens[T] = 33 | new ArrayLens(prop) 34 | 35 | implicit def maybeArrayLens[T](prop: \:?[T]):MaybeArrayLens[T] = 36 | new MaybeArrayLens(prop) 37 | } 38 | 39 | object Lens extends Lens 40 | 41 | class LensCombinator(val f: Json => Json) extends AnyVal { 42 | def ~(f2: Json => Json): Json => Json = 43 | (j: Json) => f2(f(j)) 44 | } 45 | 46 | class ValueContractExt[T](val c:ValueContract[T]) extends AnyVal { 47 | def $create(value:T):Json = 48 | c.codec(value) 49 | } 50 | 51 | class ContractExt[T <: BaseContract](val c:T) extends AnyVal { 52 | def $create(f:c.type => Json => Json):Json = 53 | f(c)(jEmptyObject) 54 | } 55 | 56 | class ContractTypeExt[T <: ContractType](val c:T) extends AnyVal { 57 | def $create(f:c.type => Json => Json):Json = 58 | f(c)(Json(c.$key -> c.matcher.default)) 59 | def $create() = 60 | Json(c.$key -> c.matcher.default) 61 | } 62 | 63 | sealed trait PropertyLens[T] extends Any with Functions { 64 | def prop:Property[T] 65 | 66 | def $get(j:Json):Option[T] = 67 | getValue(j, prop.absolutePath.segments).flatMap(j => prop.codec.decodeJson(j).toOption) 68 | 69 | def $set = 70 | (value:T) => (j:Json) => setValue(Some(j), prop.absolutePath.segments, prop.codec.encode(value)) 71 | def $maybeSet = 72 | (value:Option[T]) => (j:Json) => 73 | value.fold(j) { v => 74 | setValue(Some(j), prop.absolutePath.segments, prop.codec.encode(v)) 75 | } 76 | 77 | 78 | } 79 | 80 | class ExpectedLens[T](val prop: Expected[T]) extends AnyVal with PropertyLens[T] { 81 | //applies set if value is nonEmpty, does not drop on empty 82 | def $modify = 83 | (func:T => T) => (j:Json) => 84 | $get(j).fold[Json](j)(v => setValue(Some(j), prop.absolutePath.segments, prop.codec.encode(func(v)))) 85 | def $copy = 86 | (p:Property[T]) => (j:Json) => { 87 | getValue(j, prop.absolutePath.segments) match { 88 | case None => j 89 | case Some(value) => 90 | insertValue(Some(j), p.absolutePath.segments, value) 91 | } 92 | } 93 | 94 | def $forceDrop = 95 | (j:Json) => dropValue(j, prop.absolutePath.segments) 96 | } 97 | 98 | class MaybeLens[T](val prop: Maybe[T]) extends AnyVal with PropertyLens[T] { 99 | def $drop = 100 | (j:Json) => dropValue(j, prop.absolutePath.segments) 101 | def $setOrDrop = 102 | (value:Option[T]) => (j:Json) => value.fold(dropValue(j, prop.absolutePath.segments)){v => 103 | setValue(Some(j), prop.absolutePath.segments, prop.codec.encode(v)) 104 | } 105 | def $modify = 106 | (func:Option[T] => Option[T]) => (j:Json) => 107 | $setOrDrop(func($get(j)))(j) 108 | def $copy = 109 | (p:Property[T]) => (j:Json) => { 110 | getValue(j, prop.absolutePath.segments) match { 111 | case None => 112 | j 113 | case Some(value) => 114 | insertValue(Some(j), p.absolutePath.segments, value) 115 | } 116 | } 117 | 118 | def $nullify = 119 | (j:Json) => setValue(Some(j), prop.absolutePath.segments, jNull) 120 | } 121 | 122 | class DefaultLens[T](val prop: Default[T]) extends AnyVal with Functions { 123 | def $get(j:Json):T = 124 | getValue(j, prop.absolutePath.segments).flatMap(js => prop.codec.decodeJson(js).toOption).getOrElse(prop.default) 125 | def $set = 126 | (value:T) => (j:Json) => setValue(Some(j), prop.absolutePath.segments, prop.codec.encode(value)) 127 | def $maybeSet = 128 | (value:Option[T]) => (j:Json) => 129 | value.map { v => 130 | setValue(Some(j), prop.absolutePath.segments, prop.codec.encode(v)) 131 | } 132 | def $reset = 133 | (j:Json) => dropValue(j, prop.absolutePath.segments) 134 | def $setOrReset = 135 | (value:Option[T]) => (j:Json) => value.fold(dropValue(j, prop.absolutePath.segments)){v => 136 | setValue(Some(j), prop.absolutePath.segments, prop.codec.encode(v)) 137 | } 138 | def $modify = 139 | (func:T => T) => (j:Json) => 140 | setValue(Some(j), prop.absolutePath.segments, prop.codec.encode(func($get(j)))) 141 | def $copy = 142 | (p:Property[T]) => (j:Json) => { 143 | getValue(j, prop.absolutePath.segments) match { 144 | case None => 145 | insertValue(Some(j), p.absolutePath.segments, prop.codec.encode(prop.default)) 146 | case Some(value) => 147 | insertValue(Some(j), p.absolutePath.segments, value) 148 | } 149 | } 150 | def $nullify = 151 | (j:Json) => setValue(Some(j), prop.absolutePath.segments, jNull) 152 | } 153 | 154 | class JsonLens[T](val json:Json) extends AnyVal with Functions { 155 | def select(properties:Property[_]*):Json = { 156 | properties.foldLeft(jEmptyObject) { (j, p) => 157 | getValue(json, p.absolutePath.segments).fold(j){v => 158 | setValue(Some(j), p.absolutePath.segments, v) 159 | } 160 | } 161 | } 162 | def exclude(properties:Property[_]*):Json = { 163 | properties.foldLeft(json){ (j, p) => 164 | dropValue(j, p.absolutePath.segments) 165 | } 166 | } 167 | def append(params:(String, Json)*):Json = 168 | json.withObject(params.foldLeft(_)((o, p) => p +: o)) 169 | 170 | def concat(value:Json):Json = 171 | json.arrayOrObject[Json]( 172 | json -->>: value -->>: jEmptyArray, 173 | arr => jArray(value.array.fold(arr :+ value)(a => arr ++ a)), 174 | obj => value.obj.fold(jArray(List(jObject(obj), value)))(o => jObject(o.toList.foldLeft(obj)((t, p) => p +: t))) 175 | ) 176 | 177 | def delta(delta:Json):Json = 178 | applyDelta(json, delta) 179 | 180 | def diff(source:Json):Option[Json] = 181 | difference(json, source) 182 | } 183 | 184 | class ArrayLens[T](val prop: \:[T]) extends AnyVal with Functions { 185 | def $at(index:Int) = 186 | new Maybe[T](Path(index), prop.absolutePath \ index, EmptyValidator)(prop.elementCodec, prop.strictness) 187 | 188 | def $head = $at(0) 189 | 190 | def $append = 191 | (value:T) => (j:Json) => 192 | setValue(Some(j), prop.absolutePath.segments, prop.seqCodec(current(j) :+ prop.elementCodec.encode(value))) 193 | 194 | def $prepend = 195 | (value:T) => (j:Json) => 196 | setValue(Some(j), prop.absolutePath.segments, prop.seqCodec(prop.elementCodec(value) +: current(j))) 197 | 198 | protected def current(j:Json) = 199 | getValue(j, prop.absolutePath.segments).flatMap(js => prop.seqCodec.decodeJson(js).toOption).getOrElse(Seq.empty) 200 | } 201 | 202 | class MaybeArrayLens[T](val prop: \:?[T]) extends AnyVal with Functions { 203 | def $at(index:Int) = 204 | new Maybe[T](Path(index), prop.absolutePath \ index, EmptyValidator)(prop.elementCodec, prop.strictness) 205 | 206 | def $head = $at(0) 207 | 208 | def $append = 209 | (value:T) => (j:Json) => 210 | setValue(Some(j), prop.absolutePath.segments, prop.seqCodec(current(j) :+ prop.elementCodec.encode(value))) 211 | 212 | def $prepend = 213 | (value:T) => (j:Json) => 214 | setValue(Some(j), prop.absolutePath.segments, prop.seqCodec(prop.elementCodec(value) +: current(j))) 215 | 216 | protected def current(j:Json) = 217 | getValue(j, prop.absolutePath.segments).flatMap(js => prop.seqCodec.decodeJson(js).toOption).getOrElse(Seq.empty) 218 | } -------------------------------------------------------------------------------- /src/main/scala/jsentric/Matcher.scala: -------------------------------------------------------------------------------- 1 | package jsentric 2 | 3 | import argonaut.Argonaut._ 4 | import argonaut.{CodecJson, Json} 5 | 6 | 7 | trait Matcher { 8 | def isMatch(j:Json):Boolean 9 | def default:Json 10 | } 11 | 12 | object DefaultMatcher extends Matcher { 13 | def isMatch(j:Json):Boolean = true 14 | def default:Json = jNull 15 | } 16 | 17 | object JsonMatchers { 18 | implicit def valueMatcher[T](value:T)(implicit _codec:CodecJson[T]) = new Matcher { 19 | val default: Json = _codec.encode(value) 20 | def isMatch(j: Json): Boolean = j == default 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/main/scala/jsentric/MaybeStrictness.scala: -------------------------------------------------------------------------------- 1 | package jsentric 2 | 3 | import argonaut.{CodecJson, Json} 4 | 5 | trait MaybeStrictness { 6 | def apply[T](value:Json, codec:CodecJson[T]):Option[Option[T]] 7 | } 8 | object MaybeOptimistic extends MaybeStrictness { 9 | override def apply[T](value: Json, codec: CodecJson[T]): Option[Option[T]] = 10 | Some(codec.decodeJson(value).toOption) 11 | } 12 | object MaybeNull extends MaybeStrictness { 13 | override def apply[T](value: Json, codec: CodecJson[T]): Option[Option[T]] = 14 | if (value.isNull) Some(None) 15 | else codec.decodeJson(value).toOption.map(Some(_)) 16 | } 17 | object MaybeStrict extends MaybeStrictness { 18 | override def apply[T](value: Json, codec: CodecJson[T]): Option[Option[T]] = 19 | if (value.isNull) Some(None) 20 | else codec.decodeJson(value).toOption.map(Some(_)) 21 | } -------------------------------------------------------------------------------- /src/main/scala/jsentric/Path.scala: -------------------------------------------------------------------------------- 1 | package jsentric 2 | 3 | import argonaut._ 4 | import scala.util.Try 5 | 6 | case class Path(segments:Segments) extends AnyVal { 7 | def \(part:String) = Path(segments :+ Left(part)) 8 | def \(part:Int) = Path(segments :+ Right(part)) 9 | def ++(path:Path) = Path(segments ++ path.segments) 10 | 11 | def hasSubPath(path:Path) = 12 | path.segments.zip(segments).foldLeft(true) { 13 | case (a, (s, p)) => a && s == p 14 | } 15 | 16 | //TODO handle chars \ " etc 17 | override def toString = 18 | segments.map(_.merge).mkString("\\") 19 | 20 | } 21 | 22 | object Path extends PathExt { 23 | type Mix = Int with String 24 | val empty = Path(Vector.empty) 25 | 26 | def apply[T >: Mix](s:T*):Path = 27 | Path( 28 | s.collect { 29 | case i:Int => Right(i) 30 | case s:String => Left(s) 31 | }.toVector 32 | ) 33 | 34 | //TODO handle chars \ " etc 35 | def fromString(s:String):Path = 36 | Path(s.split('\\').map{s => 37 | Try(s.toInt).map(Right(_)).getOrElse(Left(s)) 38 | }.toVector) 39 | } 40 | trait PathExt extends Functions { 41 | 42 | implicit def jPath(json:Json) = 43 | new JPath(json) 44 | 45 | implicit def JMaybePath(json:Option[Json]) = 46 | new JMaybePath(json) 47 | } 48 | 49 | class JPath(val json:Json) extends AnyVal with Functions { 50 | def \(key:String):Option[Json] = 51 | json.field(key) 52 | 53 | def \(key:Int):Option[Json] = 54 | json.array.flatMap(_.lift(key)) 55 | 56 | def \(path:Path):Option[Json] = 57 | getValue(json, path.segments) 58 | } 59 | 60 | class JMaybePath(val json:Option[Json]) extends AnyVal with Functions { 61 | def \(key:String):Option[Json] = 62 | json.flatMap(_.field(key)) 63 | def \(key:Int):Option[Json] = 64 | json.flatMap(_.array.flatMap(_.lift(key))) 65 | def \(path:Path):Option[Json] = 66 | json.flatMap(j => getValue(j, path.segments)) 67 | } 68 | -------------------------------------------------------------------------------- /src/main/scala/jsentric/Projection.scala: -------------------------------------------------------------------------------- 1 | package jsentric 2 | 3 | import argonaut.Argonaut._ 4 | import argonaut.Json 5 | 6 | trait Projection { 7 | 8 | implicit def jsonProjectionExt(json:Json):JsonProjectionExt = 9 | new JsonProjectionExt(json) 10 | 11 | implicit def projectionExt[T](prop:Property[T]):ProjectionQuery[T] = 12 | new ProjectionQuery(prop) 13 | } 14 | 15 | object Projection extends Projection { 16 | def paths(projection:Json):Option[Set[Path]] = 17 | paths(projection, Vector.empty) 18 | private def paths(projection:Json, segments:Segments):Option[Set[Path]] = { 19 | projection.obj.flatMap{ o => 20 | val pairs = o.toList.flatMap { 21 | case (key, JLong(1) | JDouble(1)) => 22 | Some(Set(Path(segments :+ Left(key)))) 23 | case (key, j) if j.isObject => 24 | paths(j, segments :+ Left(key)) 25 | case _ => 26 | None 27 | } 28 | if (pairs.length < o.size) None 29 | else Some(pairs.reduce(_ ++ _)) 30 | } 31 | } 32 | } 33 | 34 | 35 | class JsonProjectionExt(val json:Json) extends AnyVal with Functions { 36 | def $select(value:Json) = 37 | select(json, value) 38 | 39 | def &(d:Json):Json = 40 | applyDelta(json, d) 41 | } 42 | 43 | class ProjectionQuery[T](val prop:Property[T]) extends AnyVal { 44 | 45 | def $:Json = 46 | nest(jNumber(1)) 47 | 48 | private def nest(obj:Json) = 49 | Query.pathToObject(prop.absolutePath.segments, obj) 50 | } -------------------------------------------------------------------------------- /src/main/scala/jsentric/Query.scala: -------------------------------------------------------------------------------- 1 | package jsentric 2 | 3 | import scala.util.matching.Regex 4 | import scalaz._ 5 | import Scalaz._ 6 | import argonaut._ 7 | import Argonaut._ 8 | import Lens._ 9 | import queryTree._ 10 | import queryTree.Tree 11 | 12 | trait Query extends Functions with Lens { 13 | 14 | implicit def jsonQueryExt(json:Json):JsonQueryExt = 15 | new JsonQueryExt(json) 16 | 17 | implicit def valueQuery[T](prop:Property[T]):ValueQuery[T] = 18 | new ValueQuery(prop) 19 | 20 | implicit def maybeQuery[T](prop:Maybe[T]):MaybeQuery[T] = 21 | new MaybeQuery(prop) 22 | 23 | implicit def numericQuery[T >: JNumeric](prop:Property[T]):NumericQuery[T] = 24 | new NumericQuery(prop) 25 | 26 | implicit def stringQuery[T >: JOptionable[String]](prop:Property[T]):StringQuery[T] = 27 | new StringQuery(prop) 28 | 29 | implicit class ArrayQuery[T](val prop: Expected[Seq[T]])(implicit codec: CodecJson[T]) { 30 | 31 | def $elemMatch(f:Property[T] => Json):Json = 32 | nest(("$elemMatch" -> f(new EmptyProperty[T])) ->: jEmptyObject) 33 | 34 | private def nest(obj:Json) = 35 | Query.pathToObject(prop.absolutePath.segments, obj) 36 | } 37 | 38 | implicit class MaybeArrayQuery[T](val prop: Maybe[Seq[T]])(implicit codec: CodecJson[T]) { 39 | 40 | def $elemMatch(f:Property[T] => Json):Json = 41 | nest(("$elemMatch" -> f(new EmptyProperty[T])) ->: jEmptyObject) 42 | 43 | 44 | private def nest(obj:Json) = 45 | Query.pathToObject(prop.absolutePath.segments, obj) 46 | } 47 | 48 | implicit class SetQuery[T](val prop: Expected[Set[T]])(implicit codec: CodecJson[T]) { 49 | 50 | def $elemMatch(f:Property[T] => Json):Json = 51 | nest(("$elemMatch" -> f(new EmptyProperty[T])) ->: jEmptyObject) 52 | 53 | 54 | private def nest(obj:Json) = 55 | Query.pathToObject(prop.absolutePath.segments, obj) 56 | } 57 | 58 | implicit class MaybeSetQuery[T](val prop: Maybe[Set[T]])(implicit codec: CodecJson[T]) { 59 | 60 | def $elemMatch(f:Property[T] => Json):Json = 61 | nest(("$elemMatch" -> f(new EmptyProperty[T])) ->: jEmptyObject) 62 | 63 | private def nest(obj:Json) = 64 | Query.pathToObject(prop.absolutePath.segments, obj) 65 | } 66 | 67 | def not(json:Json) = 68 | Json("$not" -> json) 69 | 70 | } 71 | 72 | object Query { 73 | private[jsentric] def apply(value:Option[Json], query:JsonObject):Boolean = { 74 | query.toList.forall { 75 | case ("$and", JArray(values)) => 76 | values.flatMap(_.obj).forall(apply(value, _)) 77 | case ("$or", JArray(values)) => 78 | values.flatMap(_.obj).exists(apply(value, _)) 79 | case ("$eq", v) => 80 | value.exists(x => order.lift(x -> v).contains(Ordering.EQ)) 81 | case ("$ne", v) => 82 | !value.exists(x => order.lift(x -> v).contains(Ordering.EQ)) 83 | case ("$regex" | "$options", _) => 84 | query("$regex").collect{ 85 | case JString(v) => 86 | val options = query("$options").collect{ case JString(o) => s"(?$o)"}.getOrElse("") 87 | value.collect { case JString(s) => (options + v).r.pattern.matcher(s).matches }.getOrElse(false) 88 | }.getOrElse(false) 89 | 90 | case ("$like", JString(v)) => 91 | value.collect { 92 | case JString(s) => 93 | ("(?i)" + v.replace("%", ".*")).r.pattern.matcher(s).matches 94 | }.getOrElse(false) 95 | case ("$lt", v) => 96 | value.exists(x => order.lift(x -> v).contains(Ordering.LT)) 97 | case ("$gt", v) => 98 | value.exists(x => order.lift(x -> v).contains(Ordering.GT)) 99 | case ("$lte", v) => 100 | value.exists(x => order.lift(x -> v).exists(r => r == Ordering.LT || r == Ordering.EQ)) 101 | case ("$gte", v) => 102 | value.exists(x => order.lift(x -> v).exists(r => r == Ordering.GT || r == Ordering.EQ)) 103 | case ("$in", JArray(values)) => 104 | value.exists(j => values.exists(x => order.lift(x -> j).contains(Ordering.EQ))) 105 | case ("$nin", JArray(values)) => 106 | !value.exists(j => values.exists(x => order.lift(x -> j).contains(Ordering.EQ))) //nin doesnt require existence, as per mongodb 107 | case ("$exists", JBool(v)) => 108 | value.isDefined == v 109 | case ("$not", v) => 110 | v.obj.exists(o => !apply(value, o)) 111 | case ("$elemMatch", JObject(j)) => 112 | value.collect { case JArray(seq) => seq.exists(s => apply(Some(s), j)) }.getOrElse(false) 113 | case ("$elemMatch", v) => 114 | value.collect { case JArray(seq) => seq.contains(v) }.getOrElse(false) 115 | case (key, JObject(obj)) => 116 | apply(value.flatMap(_.field(key)), obj) 117 | case (key, v) => 118 | value.flatMap(_.obj).fold(false) { l => 119 | l(key).contains(v) 120 | } 121 | } 122 | } 123 | 124 | private[jsentric] def apply(value:Json, query:Tree):Boolean = { 125 | query match { 126 | case &(trees) => 127 | trees.forall(t => this(value, t)) 128 | case |(trees) => 129 | trees.exists(t => this(value, t)) 130 | case !!(tree) => 131 | !this(value, tree) 132 | case ?(Path(segments), "$eq", v) => 133 | getValue(value, segments).exists(x => order.lift(x -> v).contains(Ordering.EQ)) 134 | case ?(Path(segments), "$ne", v) => 135 | !getValue(value, segments).exists(x => order.lift(x -> v).contains(Ordering.EQ)) 136 | case /(Path(segments), regex) => 137 | getValue(value, segments).flatMap(_.string).exists(s => regex.pattern.matcher(s).matches) 138 | case %(Path(segments), _, regex) => 139 | getValue(value, segments).flatMap(_.string).exists(s => regex.pattern.matcher(s).matches) 140 | case ?(Path(segments), "$lt", v) => 141 | getValue(value, segments).exists(x => order.lift(x -> v).contains(Ordering.LT)) 142 | case ?(Path(segments), "$gt", v) => 143 | getValue(value, segments).exists(x => order.lift(x -> v).contains(Ordering.GT)) 144 | case ?(Path(segments), "$lte", v) => 145 | getValue(value, segments).exists(x => order.lift(x -> v).exists(r => r == Ordering.LT || r == Ordering.EQ)) 146 | case ?(Path(segments), "$gte", v) => 147 | getValue(value, segments).exists(x => order.lift(x -> v).exists(r => r == Ordering.GT || r == Ordering.EQ)) 148 | case ?(Path(segments), "$in", JArray(values)) => 149 | getValue(value, segments).exists(j => values.exists(x => order.lift(x -> j).contains(Ordering.EQ))) 150 | case ?(Path(segments), "$nin", JArray(values)) => 151 | !getValue(value, segments).exists(j => values.exists(x => order.lift(x -> j).contains(Ordering.EQ))) 152 | case ?(Path(segments), "$exists", JBool(v)) => 153 | getValue(value, segments).nonEmpty == v 154 | case ∃(Path(segments), subQuery) => 155 | getValue(value, segments).collect { case JArray(seq) => seq.exists(s => apply(s, subQuery)) }.getOrElse(false) 156 | } 157 | } 158 | 159 | private[jsentric] def pathToObject(path:Segments,obj:Json):Json= { 160 | path match { 161 | case Seq() => obj 162 | case head :+ Left(tail) => pathToObject(head, Json(tail -> obj)) 163 | } 164 | } 165 | 166 | def order:PartialFunction[(Json, Json), Ordering] = { 167 | case (JDouble(x), JDouble(y)) => x ?|? y 168 | case (JLong(x), JLong(y)) => x ?|? y 169 | case (JString(x), JString(y)) => x ?|? y 170 | case (JBool(x), JBool(y)) => x ?|? y 171 | case (JDouble(x), JLong(y)) => x ?|? y 172 | case (JLong(x), JDouble(y)) => x.toDouble ?|? y 173 | } 174 | 175 | 176 | } 177 | 178 | class JsonQueryExt(val json:Json) extends AnyVal with Functions { 179 | def $isMatch(value:Json) = 180 | json.obj.fold(json == value){o => Query.apply(Some(value), o)} 181 | 182 | def &&(d:Json):Json = 183 | json.obj.collect{ 184 | case obj if (obj ?? "$or") && d.objectFields.exists(_.contains("$or")) => 185 | Json("$and" -> jArray(List(json, d))) 186 | case obj if obj ?? "$and" => 187 | d.obj.collect { 188 | case obj2 if obj ?? "$and" => 189 | Json("$and" -> obj("$and").get.concat(obj2("$and").get)) 190 | }.getOrElse(Json("$and" -> obj("$and").get.concat(d))) 191 | }.getOrElse(applyDelta(json, d)) 192 | 193 | def ||(d:Json):Json = 194 | json.obj.flatMap{o => 195 | o("$or").flatMap{j => 196 | j.array.map(a => Json("$or" -> jArray(a :+ d))) 197 | } 198 | }.getOrElse(Json("$or" -> jArray(List(json, d)))) 199 | } 200 | //Handle default? 201 | class ValueQuery[T](val prop: Property[T]) extends AnyVal { 202 | 203 | def $eq(value:T) = nest(prop.codec(value)) 204 | //Currently not supporting chaining of $ne in an && for the same field 205 | def $ne(value:T) = nest(Json("$ne" -> prop.codec(value))) 206 | def $in(values:T*) = nest(Json("$in" -> jArray(values.toList.map(prop.codec.apply)))) 207 | def $nin(values:T*) = nest(Json("$nin" -> jArray(values.toList.map(prop.codec.apply)))) 208 | 209 | private def nest(obj:Json) = 210 | Query.pathToObject(prop.absolutePath.segments, obj) 211 | } 212 | 213 | class MaybeQuery[T](val prop:Maybe[T]) extends AnyVal { 214 | def $exists(value:Boolean) = nest(Json("$exists" := value)) 215 | private def nest(obj:Json) = 216 | Query.pathToObject(prop.absolutePath.segments, obj) 217 | } 218 | 219 | class NumericQuery[T >: JNumeric](val prop: Property[T]) extends AnyVal { 220 | 221 | def $lt(value:Double) = nest(Json("$lt" -> jNumberOrString(value))) 222 | def $lt(value:Long) = nest(Json("$lt" -> jNumber(value))) 223 | 224 | def $gt(value:Double) = nest(Json("$gt" -> jNumberOrString(value))) 225 | def $gt(value:Long) = nest(Json("$gt" -> jNumber(value))) 226 | 227 | def $lte(value:Double) = nest(Json("$lt" -> jNumberOrString(value))) 228 | def $lte(value:Long) = nest(Json("$lt" -> jNumber(value))) 229 | 230 | def $gte(value:Double) = nest(Json("$gt" -> jNumberOrString(value))) 231 | def $gte(value:Long) = nest(Json("$gt" -> jNumber(value))) 232 | 233 | private def nest(obj:Json) = 234 | Query.pathToObject(prop.absolutePath.segments, obj) 235 | } 236 | 237 | class StringQuery[T >: JOptionable[String]](val prop:Property[T]) extends AnyVal { 238 | 239 | def $regex(value:String) = nest(Json("$regex" := value)) 240 | def $regex(value:String, options:String) = nest(Json("$regex" := value, "$options" := options)) 241 | def $regex(r:Regex) = nest(Json("$regex" := r.regex)) 242 | def $like(value:String) = nest(Json("$like" := value)) 243 | 244 | private def nest(obj:Json) = 245 | Query.pathToObject(prop.absolutePath.segments, obj) 246 | } -------------------------------------------------------------------------------- /src/main/scala/jsentric/QueryJsonb.scala: -------------------------------------------------------------------------------- 1 | package jsentric 2 | 3 | import argonaut._ 4 | import Argonaut._ 5 | import queryTree._ 6 | import queryTree.Tree 7 | import scalaz._ 8 | import Scalaz._ 9 | 10 | /* 11 | Experimental feature for converting from mongo db style query to a PostGres jsonb query 12 | Uses the jdbc ?? escape for ? 13 | */ 14 | object QueryJsonb { 15 | 16 | type JbValid = NonEmptyList[(String, Path)] \/ String 17 | 18 | def apply(field:String, query:Json): JbValid = 19 | query.obj.fold[JbValid](\/-(s"Value = ${escape(query.toString())}::jsonb")){ j => 20 | treeToPostgres(field)(QueryTree(j) -> false).map(_.mkString) 21 | } 22 | 23 | def apply(field:String, query:Tree): JbValid = 24 | treeToPostgres(field)(query -> false).map(_.mkString) 25 | 26 | 27 | private def treeToPostgres(field:String):Function[(Tree, Boolean), NonEmptyList[(String, Path)] \/ Vector[String]] = { 28 | case (&(Seq(value)), g) => 29 | treeToPostgres(field)(value -> false).map(_ ++ g.option(")")) 30 | case (|(Seq(value)), g) => 31 | treeToPostgres(field)(value -> false).map(_ ++ g.option(")")) 32 | case (&(head +: tail), g) => 33 | builder(treeToPostgres(field)(head -> false), treeToPostgres(field)(&(tail) -> true)) { (h, t) => 34 | ((!g).option("(") ++: h :+ " AND ") ++ t 35 | } 36 | case (|(head +: tail), g) => 37 | builder(treeToPostgres(field)(head -> false), treeToPostgres(field)(&(tail) -> true)) { (h, t) => 38 | ((!g).option("(") ++: h :+ " OR ") ++ t 39 | } 40 | case (!!(tree), g) => 41 | treeToPostgres(field)(tree -> false).map(v => "NOT (" +: v :+ ")") 42 | //TODO empty Path 43 | case (/(path, regex), _) => 44 | \/-("(" +: field +: " #>> '" +: toPath(path) +: "') ~ '" +: regex.toString +: Vector("'")) 45 | case (%(path, like, _), _) => 46 | \/-("(" +: field +: " #>> '" +: toPath(path) +: "') ILIKE '" +: like +: Vector("'")) 47 | case (?(path, "$eq", value), _) => 48 | \/-(field +: " @> '" +: toObject(path.segments, value) :+ "'::jsonb") 49 | case (?(path, "$ne", value), _) => 50 | \/-("NOT " +: field +: " @> '" +: toObject(path.segments, value) :+ "'::jsonb") 51 | case (?(path, "$in", value), _) if value.isArray => 52 | \/-(Vector(field, " #> '", toPath(path), "' <@ '", escape(value.toString()), "'::jsonb")) 53 | case (?(path, "$nin", value), _) if value.isArray => 54 | \/-(Vector("NOT ", field, " #> '", toPath(path), "' <@ '", escape(value.toString()), "'::jsonb")) 55 | case (?(path, o@("$nin" | "$in"), _), _) => 56 | -\/(NonEmptyList(s"Operation $o expected an array" -> path)) 57 | case (?(path, "$exists", JBool(true)), _) => 58 | \/-(field +: toSearch(path.segments)) 59 | case (?(path, "$exists", JBool(false)), _) => 60 | \/-("NOT " +: field +: toSearch(path.segments)) 61 | case (?(path, "$exists", _), _) => 62 | -\/(NonEmptyList("Operation $exists requires a boolean" -> path)) 63 | //TODO resolve duplicate jsonb_typeOfs which can occur in ands 64 | case (?(path, Op(op), value), _) => 65 | val p = toPath(path) 66 | //TODO: use applicative builder.. 67 | for { 68 | v <- serialize(value -> path) 69 | c <- getCast(value -> path) 70 | t <- getType(value -> path) 71 | } yield Vector( 72 | "(", 73 | s"jsonb_typeof($field #> '$p') = '$t'", 74 | " AND ", 75 | s"($field #>> '$p') :: $c $op $v", 76 | s")") 77 | 78 | case (∃(path, ?(Path(Seq()), "$eq", value)), _) => 79 | \/-(field +: " @> '" +: toObject(path.segments, jArrayElements(value)) :+ "'::jsonb") 80 | case (∃(path, &(Seq(?(subPath, "$eq", value)))), _) => 81 | \/-(field +: toElement(path) +: " @> " +: "'[" +: toObject(subPath.segments, value) :+ "]'") 82 | case (∃(path, _), _) => 83 | -\/(NonEmptyList("Currently only equality is supported in element match." -> path)) 84 | case (?(path, op, _), _) => 85 | -\/(NonEmptyList(s"Unable to parse query operation $op." -> path)) 86 | } 87 | 88 | private def toObject(segments:Segments, value:Json):Vector[String] = 89 | segments match { 90 | case head +: tail => 91 | "{\"" +: escape(head) +: "\":" +: toObject(tail, value) :+ "}" 92 | case _ => Vector(escape(value.toString())) 93 | } 94 | 95 | private def serialize:Function[(Json, Path), JbValid] = { 96 | case (JString(s), _) => \/-(escape(s)) 97 | case (JBool(true), _) => \/-("true") 98 | case (JBool(false), _) => \/-("false") 99 | case (JLong(l), _) => \/-(l.toString) 100 | case (JDouble(d), _) => \/-(d.toString) 101 | case (j, _) if j.isNull => \/-("null") 102 | case (_, path) => 103 | -\/(NonEmptyList("Unsupported type" -> path)) 104 | } 105 | 106 | private def escape(s:Either[String, Int]):String = 107 | escape(s.merge.toString) 108 | private def escape(s:String):String = 109 | s.replace("'","''") 110 | private def toPath(path:Path) = 111 | path.segments.map(escape).mkString("{", ",", "}") 112 | private def toSearch:Function[Segments, Vector[String]] = { 113 | case tail +: Seq() => 114 | Vector(" ?? '", escape(tail), "'") 115 | case head +: tail => 116 | " -> '" +: escape(head) +: "'" +: toSearch(tail) 117 | } 118 | 119 | private def toElement(path:Path):String = 120 | path.segments.map(escape).map(s => s" -> '$s'").mkString("") 121 | 122 | private def getType:Function[(Json,Path), JbValid] = { 123 | case (j,_) if j.isNumber => \/-("number") 124 | case (j,_) if j.isString => \/-("string") 125 | case (j,_) if j.isBool => \/-("boolean") 126 | case (j,_) if j.isObject => \/-("object") 127 | case (j,_) if j.isArray => \/-("array") 128 | case (j,_) if j.isNull => \/-("null") 129 | case (_, path) => 130 | -\/(NonEmptyList("Unsupported type" -> path)) 131 | } 132 | 133 | private def getCast:Function[(Json, Path), JbValid] = { 134 | case (j,_) if j.isNumber => \/-("NUMERIC") 135 | case (j,_) if j.isString => \/-("TEXT") 136 | case (j,_) if j.isBool => \/-("BOOLEAN") 137 | case (_, path) => 138 | -\/(NonEmptyList("Unsupported type" -> path)) 139 | } 140 | 141 | object Op { 142 | def unapply(op: String): Option[String] = 143 | op match { 144 | case "$lt" => Some("<") 145 | case "$lte" => Some("<=") 146 | case "$gt" => Some(">") 147 | case "$gte" => Some(">=") 148 | case _ => None 149 | } 150 | } 151 | 152 | private def builder[T]( 153 | left:NonEmptyList[(String, Path)] \/ Vector[String], 154 | right:NonEmptyList[(String, Path)] \/ Vector[String]) 155 | (f:(Vector[String], Vector[String]) => T):NonEmptyList[(String, Path)] \/ T = { 156 | (left, right) match { 157 | case (\/-(l), \/-(r)) => \/-(f(l,r)) 158 | case (-\/(l), -\/(r)) => -\/(l.append(r)) 159 | case (_, -\/(r)) => -\/(r) 160 | case (-\/(l), _) => -\/(l) 161 | } 162 | } 163 | 164 | 165 | } 166 | 167 | 168 | -------------------------------------------------------------------------------- /src/main/scala/jsentric/Validation.scala: -------------------------------------------------------------------------------- 1 | package jsentric 2 | 3 | import argonaut._ 4 | import scalaz._ 5 | 6 | trait Validation extends Functions { 7 | implicit def valueContractValidation[T](contract:ValueContract[T]) = 8 | new ValueContractValidation[T](contract) 9 | 10 | implicit def baseContractValidation(contract:BaseContract) = 11 | new BaseContractValidation(contract) 12 | 13 | implicit def propertyValidation[T](prop:Property[T]) = 14 | new PropertyValidation(prop) 15 | 16 | } 17 | 18 | object Validation extends Validation { 19 | def seqToJValid(seq:Seq[(String, Path)], json:Json) = 20 | seq match { 21 | case Nil => \/-(json) 22 | case failure +: failures => -\/(NonEmptyList(failure, failures:_*)) 23 | } 24 | } 25 | 26 | class ValueContractValidation[T](val contract:ValueContract[T]) extends AnyVal { 27 | import Validation._ 28 | def $validate(newContent:Json):JValid = 29 | seqToJValid($validate(newContent, None, Path.empty), newContent) 30 | 31 | def $validate(deltaContent:Json, currentState:Json):JValid = 32 | seqToJValid($validate(deltaContent, Some(currentState), Path.empty), deltaContent) 33 | //TODO better approach here 34 | def $validate(deltaContent:Json, currentState:Option[Json]):JValid = 35 | seqToJValid($validate(deltaContent, currentState, Path.empty), deltaContent) 36 | 37 | def $validate(value: Json, currentState: Option[Json], path:Path): Seq[(String, Path)] = 38 | contract.validator.validate(Some(value), currentState, path) 39 | 40 | def $sanitize(json:Json):Json = ??? 41 | } 42 | 43 | class BaseContractValidation(val contract:BaseContract) extends AnyVal with Functions { 44 | import Validation._ 45 | def $validate(newContent:Json):JValid = 46 | seqToJValid($validate(newContent, None, Path.empty), newContent) 47 | 48 | def $validate(deltaContent:Json, currentState:Json):JValid = 49 | seqToJValid($validate(deltaContent, Some(currentState), Path.empty), deltaContent) 50 | 51 | def $validate(deltaContent:Json, currentState:Option[Json]):JValid = 52 | seqToJValid($validate(deltaContent, currentState, Path.empty), deltaContent) 53 | //TODO better approach here 54 | def $validate(value: Json, currentState: Option[Json], path:Path): Seq[(String, Path)] = 55 | ValidationPropertyCache.getProperties(contract).flatMap{p => 56 | val v = getValue(value, p.relativePath.segments) 57 | val c = currentState.flatMap(getValue(_, p.relativePath.segments)) 58 | new PropertyValidation(p).$validate(v, c, path ++ p.relativePath) 59 | } 60 | 61 | def $sanitize(json:Json):Json = { 62 | ValidationPropertyCache.getInternal(contract).foldLeft(json){ (j, p) => 63 | dropValue(j, p.absolutePath.segments) 64 | } 65 | } 66 | } 67 | 68 | class PropertyValidation[T](val prop:Property[T]) extends AnyVal { 69 | def $validate(value: Option[Json], currentState: Option[Json], path:Path): Seq[(String, Path)] = 70 | ((value, currentState, prop) match { 71 | case (None, None, p: Expected[_]) => 72 | Seq("Value required." -> path) 73 | case (Some(v), c, p) if !p.isValidType(v) => 74 | Seq(s"Unexpected type '${v.getClass.getSimpleName}'." -> path) 75 | case (Some(v), c, b:BaseContract) => 76 | new BaseContractValidation(b).$validate(v, c, path) 77 | case _ => 78 | Seq.empty 79 | }) ++ prop.validator.validate(value, currentState, path) 80 | } 81 | 82 | private object ValidationPropertyCache { 83 | private var properties:Map[Class[_], Seq[Property[_]]] = Map.empty 84 | private var internal:Map[Class[_], Seq[Property[_]]] = Map.empty 85 | def getProperties(contract:BaseContract):Seq[Property[_]] = 86 | properties.get(contract.getClass) match { 87 | case Some(p) => 88 | p 89 | case None => 90 | val vp = 91 | contract.getClass.getMethods 92 | .filter(m => m.getParameterTypes.isEmpty && classOf[Property[_]].isAssignableFrom(m.getReturnType)) 93 | .map(_.invoke(contract).asInstanceOf[Property[_]]) 94 | .toSeq 95 | this.synchronized { 96 | properties = properties + (contract.getClass -> vp) 97 | } 98 | vp 99 | } 100 | 101 | def getInternal(contract:BaseContract):Seq[Property[_]] = { 102 | internal.get(contract.getClass) match { 103 | case Some(p) => 104 | p 105 | case None => 106 | val ip = getProperties(contract).collect { 107 | case p if isInternal(p.validator) => Seq(p) 108 | case p: BaseContract => 109 | getInternal(p) 110 | }.flatten 111 | this.synchronized { 112 | internal = internal + (contract.getClass -> ip) 113 | } 114 | ip 115 | } 116 | } 117 | 118 | private val isInternal:Function[Validator[_], Boolean] = { 119 | case AndValidator(l,r) => 120 | isInternal(l) || isInternal(r) 121 | case OrValidator(l, r) => // bit odd through excpetion for now 122 | throw new Exception("Internal json validator shouldn't be in an 'or' conditional") 123 | case v => v == Internal 124 | } 125 | } 126 | 127 | -------------------------------------------------------------------------------- /src/main/scala/jsentric/Validator.scala: -------------------------------------------------------------------------------- 1 | package jsentric 2 | 3 | import argonaut._ 4 | import Argonaut._ 5 | import scalaz.Scalaz._ 6 | 7 | trait Validator[+T] { 8 | 9 | def validate(value:Option[Json], currentState:Option[Json], path:Path):Seq[(String, Path)] 10 | 11 | def &&[S >: T] (v:Validator[S]):Validator[S] = AndValidator(this, v) 12 | 13 | def ||[S >: T] (v:Validator[S]):Validator[S] = OrValidator(this, v) 14 | } 15 | 16 | case class AndValidator[T, A >: T, B >: T](left:Validator[A], right:Validator[B]) extends Validator[T] { 17 | def validate(value:Option[Json], currentState:Option[Json], path:Path):Seq[(String, Path)] = 18 | left.validate(value, currentState, path:Path) ++ right.validate(value, currentState, path:Path) 19 | } 20 | 21 | case class OrValidator[T, A >: T, B >: T](left:Validator[A], right:Validator[B]) extends Validator[T] { 22 | def validate(value:Option[Json], currentState:Option[Json], path:Path):Seq[(String, Path)] ={ 23 | left.validate(value, currentState, path:Path) match { 24 | case Seq() => Seq() 25 | case list => right.validate(value, currentState, path:Path) match { 26 | case Seq() => Seq() 27 | case list2 if list2.size < list.size => list2 28 | case _ => list 29 | } 30 | } 31 | } 32 | } 33 | 34 | case object EmptyValidator extends Validator[Nothing] { 35 | def validate(value: Option[Json], currentState: Option[Json], path: Path): Seq[(String, Path)] = 36 | Nil 37 | } 38 | 39 | case object Internal extends SimpleValidator[Option[Nothing]] { 40 | def maybeValid(path:Path) = { 41 | case (Some(_), _) => 42 | "Value is reserved and cannot be provided." -> path 43 | } 44 | } 45 | 46 | trait SimpleValidator[+T] extends Validator[T] { 47 | def maybeValid(path:Path):PartialFunction[(Option[Json],Option[Json]), (String, Path)] 48 | 49 | def validate(value: Option[Json], currentState: Option[Json], path:Path): Seq[(String, Path)] = 50 | maybeValid(path).lift(value -> currentState).toSeq 51 | } 52 | 53 | trait Validators { 54 | import Path._ 55 | 56 | val immutable = new SimpleValidator[Nothing] { 57 | def maybeValid(path:Path) = { 58 | case (Some(a), Some(b)) if a != b => 59 | "Value is immutable and cannot be changed." -> path 60 | } 61 | } 62 | 63 | val notNull = new SimpleValidator[Option[Nothing]] { 64 | def maybeValid(path: Path) = { 65 | case (Some(j), _) if j.isNull => 66 | "Value cannot be null." -> path 67 | } 68 | } 69 | 70 | val reserved = new SimpleValidator[Option[Nothing]] { 71 | def maybeValid(path:Path) = { 72 | case (Some(_), _) => 73 | "Value is reserved and cannot be provided." -> path 74 | } 75 | } 76 | 77 | val internal = Internal 78 | 79 | sealed trait BoundedValidator extends SimpleValidator[JNumeric] { 80 | def doubleFail(n: Double): Boolean 81 | 82 | def message(n: Number): String 83 | 84 | def maybeValid(path: Path) = { 85 | case (Some(JDouble(n)), _) if doubleFail(n) => 86 | message(n) -> path 87 | } 88 | } 89 | 90 | def >[T](value: Long) = new BoundedValidator { 91 | def doubleFail(n: Double): Boolean = n <= value 92 | 93 | def message(n: Number): String = s"Value $n is not greater than $value" 94 | } 95 | 96 | def >[T](value: Double):Validator[JNumeric] = new BoundedValidator { 97 | def doubleFail(n: Double): Boolean = n <= value 98 | 99 | def message(n: Number): String = s"Value $n is not greater than $value" 100 | } 101 | 102 | def >=[T](value: Long):Validator[JNumeric] = new BoundedValidator { 103 | def doubleFail(n: Double): Boolean = n < value 104 | 105 | def message(n: Number): String = s"Value $n is not greater than or equal to $value" 106 | } 107 | 108 | def >=[T](value: Double) = new BoundedValidator { 109 | def doubleFail(n: Double): Boolean = n < value 110 | 111 | def message(n: Number): String = s"Value $n is not greater than or equal to $value" 112 | } 113 | 114 | def <[T](value: Long) = new BoundedValidator { 115 | def doubleFail(n: Double): Boolean = n >= value 116 | 117 | def message(n: Number): String = s"Value $n is not less than $value" 118 | } 119 | 120 | def <[T](value: Double) = new BoundedValidator { 121 | def doubleFail(n: Double): Boolean = n <= value 122 | 123 | def message(n: Number): String = s"Value $n is not less than $value" 124 | } 125 | 126 | def <=[T](value: Long) = new BoundedValidator { 127 | def doubleFail(n: Double): Boolean = n > value 128 | 129 | def message(n: Number): String = s"Value $n is not less than or equal to $value" 130 | } 131 | 132 | def <=[T](value: Double) = new BoundedValidator { 133 | def doubleFail(n: Double): Boolean = n > value 134 | 135 | def message(n: Number): String = s"Value $n is not greater than or equal to $value" 136 | } 137 | 138 | def in[T](values:T*)(implicit codec: CodecJson[T]) = new SimpleValidator[JOptionable[T]] { 139 | def maybeValid(path: Path): PartialFunction[(Option[Json], Option[Json]), (String, Path)] = { 140 | case (Some(j), _) if !codec.decodeJson(j).toOption.exists(values.contains) => 141 | "Value outside of allowed values." -> path 142 | } 143 | } 144 | 145 | def nin[T](values:T*)(implicit codec: CodecJson[T]) = new SimpleValidator[JOptionable[T]] { 146 | def maybeValid(path: Path): PartialFunction[(Option[Json], Option[Json]), (String, Path)] = { 147 | case (Some(j), _) if codec.decodeJson(j).toOption.exists(values.contains) => 148 | "Value not allowed." -> path 149 | } 150 | } 151 | 152 | def inCaseInsensitive(values:String*) = new SimpleValidator[JOptionable[String]] { 153 | def maybeValid(path: Path): PartialFunction[(Option[Json], Option[Json]), (String, Path)] = { 154 | case (Some(JString(s)), _) if !values.exists(_.equalsIgnoreCase(s)) => 155 | "Value outside of allowed values." -> path 156 | } 157 | } 158 | 159 | def ninCaseInsensitive(values:String*) = new SimpleValidator[JOptionable[String]] { 160 | def maybeValid(path: Path): PartialFunction[(Option[Json], Option[Json]), (String, Path)] = { 161 | case (Some(JString(s)), _) if values.exists(_.equalsIgnoreCase(s)) => 162 | "Value outside of allowed values." -> path 163 | } 164 | } 165 | 166 | def minLength(value: Int) = new SimpleValidator[JOptionable[JLength]] { 167 | def maybeValid(path: Path) = { 168 | case (Some(JArray(seq)), _) if seq.length < value => 169 | s"Array must have length of at least $value" -> path 170 | case (Some(JString(s)), _) if s.length < value => 171 | s"String must have length of at least $value" -> path 172 | } 173 | } 174 | 175 | def maxLength(value: Int) = new SimpleValidator[JOptionable[JLength]] { 176 | def maybeValid(path: Path) = { 177 | case (Some(JArray(seq)), _) if seq.length > value => 178 | s"Array must have length of no greater than $value" -> path 179 | case (Some(JString(s)), _) if s.length > value => 180 | s"String must have length of no greater than $value" -> path 181 | } 182 | } 183 | 184 | val nonEmpty = new SimpleValidator[JOptionable[JLength]] { 185 | def maybeValid(path: Path) = { 186 | case (Some(JArray(seq)), _) if seq.isEmpty => 187 | s"Array must not be empty" -> path 188 | case (Some(JString(s)), _) if s.isEmpty => 189 | s"String must not be empty" -> path 190 | } 191 | } 192 | 193 | val nonEmptyOrWhiteSpace:Validator[String] = new SimpleValidator[JLength] { 194 | def maybeValid(path: Path) = { 195 | case (Some(JString(text)), _) if text.trim().isEmpty => 196 | s"Text must not be all empty or whitespace" -> path 197 | } 198 | } 199 | 200 | def forall[T](validator: Validator[T]) = new Validator[JOptionable[Seq[Nothing]]] { 201 | def validate(value: Option[Json], currentState: Option[Json], pathContext: Path): Seq[(String, Path)] = 202 | value collect { 203 | case JArray(seq) => 204 | for { 205 | (e, i) <- seq.zipWithIndex 206 | v <- validator.validate(Some(e), None, pathContext \ i) 207 | } yield v 208 | } getOrElse Seq.empty 209 | } 210 | 211 | //TODO Forall doesnt validate agains current state, bit of an odd one.. 212 | def forall(contract: BaseContract) = new Validator[JOptionable[Seq[Nothing]]] { 213 | def validate(value: Option[Json], currentState: Option[Json], pathContext: Path): Seq[(String, Path)] = 214 | value collect { 215 | case JArray(seq) => 216 | for { 217 | (e, i) <- seq.zipWithIndex 218 | v <- new BaseContractValidation(contract).$validate(e, None, pathContext \ i) 219 | } yield v 220 | } getOrElse Seq.empty 221 | } 222 | 223 | def values(contract:BaseContract) = new Validator[JOptionable[Map[String,Nothing]]] { 224 | def validate(value: Option[Json], currentState: Option[Json], path: Path): Seq[(String, Path)] = 225 | value collect { 226 | case JObject(map) => 227 | map.toMap.flatMap{ kv => 228 | val current = currentState \ kv._1 229 | new BaseContractValidation(contract).$validate(kv._2, current, path \ kv._1) 230 | }.toSeq 231 | } getOrElse Seq.empty[(String, Path)] 232 | } 233 | 234 | def custom[T](t: T => Boolean, message:String)(implicit codec:CodecJson[T]) = new Validator[JOptionable[T]] { 235 | def validate(value: Option[Json], currentState: Option[Json], path: Path): Seq[(String, Path)] = 236 | value.flatMap(j => codec.decodeJson(j).toOption) 237 | .filterNot(t) 238 | .fold(Seq.empty[(String, Path)]){_ => Seq(message -> path)} 239 | 240 | } 241 | 242 | def customCompare[T](deltaCurrent:(T, T) => Boolean, message:String)(implicit codec:CodecJson[T]) = new Validator[JOptionable[T]] { 243 | def validate(value: Option[Json], currentState: Option[Json], path: Path): Seq[(String, Path)] = 244 | (for { 245 | d <- value 246 | c <- currentState 247 | dt <- codec.decodeJson(d).toOption 248 | ct <- codec.decodeJson(c).toOption 249 | f <- (!deltaCurrent(dt, ct)).option(Seq(message -> path)) 250 | } yield f).getOrElse(Nil) 251 | } 252 | } 253 | 254 | object Validators extends Validators 255 | -------------------------------------------------------------------------------- /src/main/scala/jsentric/package.scala: -------------------------------------------------------------------------------- 1 | import scalaz.{NonEmptyList, \/} 2 | import argonaut._ 3 | import Argonaut._ 4 | 5 | package object jsentric { 6 | 7 | type Segments = Vector[Either[String, Int]] 8 | type JValid = \/[NonEmptyList[(String, Path)], Json] 9 | 10 | type JNumeric = Long with Int with Float with Double with Option[Long] with Option[Int] with Option[Float] with Option[Double] 11 | type JLength = String with Seq[Nothing] 12 | type JOptionable[T] = T with Option[T] 13 | 14 | implicit def stringToPath(s:String) = Path(s) 15 | } 16 | 17 | -------------------------------------------------------------------------------- /src/main/scala/jsentric/queryTree/QueryTree.scala: -------------------------------------------------------------------------------- 1 | package jsentric.queryTree 2 | 3 | import argonaut.Argonaut._ 4 | import argonaut.JsonObject 5 | import jsentric.{JArray, JObject, JString, Path} 6 | import scalaz.Scalaz._ 7 | 8 | object QueryTree { 9 | 10 | def apply(query:JsonObject) = { 11 | buildTree(query, Path.empty) 12 | } 13 | private def buildTree(query:JsonObject, path:Path):Tree = { 14 | &(query.toList.flatMap{ 15 | case ("$and", JArray(values)) => 16 | Some(&(values.flatMap(_.obj.map(buildTree(_, path))))) 17 | case ("$or", JArray(values)) => 18 | Some(|(values.flatMap(_.obj.map(buildTree(_, path))))) 19 | case ("$not", JObject(value)) => 20 | Some(!!(buildTree(value, path))) 21 | case ("$elemMatch", JObject(value)) => 22 | Some(∃(path, buildTree(value, Path.empty))) 23 | case ("$elemMatch", j) => 24 | Some(∃(path, ?(Path.empty, "$eq", j))) 25 | case (o@("$eq" | "$ne" | "$lt" | "$gt" | "$lte" | "$gte" | "$in" | "$nin" | "$exists"), v) => 26 | Some(?(path, o, v)) 27 | case ("$regex", JString(s)) => 28 | val options = query("$options").collect{ case JString(o) => s"(?$o)"}.getOrElse("") 29 | Some(/(path, (options + s).r)) 30 | case ("$like", JString(s)) => 31 | Some(%(path, s, ("(?i)" + s.replace("%", ".*")).r)) 32 | case ("$options", _) => 33 | None 34 | case (key, JObject(v)) => 35 | Some(buildTree(v, path \ key)) 36 | case (key, j) => 37 | Some(?(path \ key, "$eq", j)) 38 | }) 39 | } 40 | 41 | def partition(tree:Tree, paths:Set[Path]):(Option[Tree], Option[Tree]) = { 42 | tree match { 43 | case |(trees) => 44 | val (l,r) = trees.map(partition(_, paths)).unzip 45 | if (l.count(_.nonEmpty) < trees.length) //not all elements present in query 46 | None -> Some(tree) 47 | else { 48 | val lm = Some(|(l.flatten)) 49 | if (r.forall(_.isEmpty)) 50 | lm -> None 51 | else 52 | lm -> Some(tree) 53 | } 54 | 55 | case &(trees) => 56 | val (l,r) = trees.map(partition(_, paths)).unzip 57 | val lm = l.flatten 58 | val rm = r.flatten 59 | lm.nonEmpty.option(&(lm)) -> rm.nonEmpty.option(&(rm)) 60 | 61 | case ?(path, _, _) => 62 | if (paths.exists(path.hasSubPath)) Some(tree) -> None 63 | else None -> Some(tree) 64 | 65 | case /(path, _) => 66 | if (paths.exists(path.hasSubPath)) Some(tree) -> None 67 | else None -> Some(tree) 68 | 69 | case %(path, _, _) => 70 | if (paths.exists(path.hasSubPath)) Some(tree) -> None 71 | else None -> Some(tree) 72 | 73 | case ∃(path, _) => 74 | if (paths.exists(path.hasSubPath)) Some(tree) -> None 75 | else None -> Some(tree) 76 | 77 | case !!(t) => 78 | val (l, r) = negPartition(t, paths) 79 | l.map(!!) -> r.map(!!) 80 | } 81 | } 82 | 83 | private def negPartition(tree:Tree, paths:Set[Path]):(Option[Tree], Option[Tree]) = { 84 | tree match { 85 | case |(trees) => 86 | val (l,r) = trees.map(negPartition(_, paths)).unzip 87 | val lm = l.flatten 88 | val rm = r.flatten 89 | lm.nonEmpty.option(|(lm)) -> rm.nonEmpty.option(|(rm)) 90 | 91 | case &(trees) => 92 | val (l,r) = trees.map(negPartition(_, paths)).unzip 93 | if (l.count(_.nonEmpty) < trees.length) //not all elements present in query 94 | None -> Some(tree) 95 | else { 96 | val lm = Some(&(l.flatten)) 97 | if (r.forall(_.isEmpty)) 98 | lm -> None 99 | else 100 | lm -> Some(tree) 101 | } 102 | 103 | case ?(path, _, _) => 104 | if (paths.exists(path.hasSubPath)) Some(tree) -> None 105 | else None -> Some(tree) 106 | 107 | case /(path, _) => 108 | if (paths.exists(path.hasSubPath)) Some(tree) -> None 109 | else None -> Some(tree) 110 | 111 | case %(path, _, _) => 112 | if (paths.exists(path.hasSubPath)) Some(tree) -> None 113 | else None -> Some(tree) 114 | 115 | case ∃(path, _) => 116 | if (paths.exists(path.hasSubPath)) Some(tree) -> None 117 | else None -> Some(tree) 118 | 119 | case !!(t) => 120 | val (l, r) = partition(t, paths) 121 | l.map(!!) -> r.map(!!) 122 | } 123 | } 124 | } 125 | 126 | 127 | -------------------------------------------------------------------------------- /src/main/scala/jsentric/queryTree/Tree.scala: -------------------------------------------------------------------------------- 1 | package jsentric.queryTree 2 | 3 | import argonaut.Json 4 | import jsentric.{Query, Path} 5 | 6 | import scala.util.matching.Regex 7 | 8 | sealed trait Tree { 9 | def isMatch(json:Json) = 10 | Query.apply(json, this) 11 | } 12 | final case class ?(path:Path, op:String, value:Json) extends Tree 13 | final case class ∃(path:Path, tree:Tree) extends Tree 14 | final case class /(path:Path, regex:Regex) extends Tree 15 | final case class %(path:Path, like:String, regex:Regex) extends Tree 16 | final case class &(seq:Seq[Tree]) extends Tree 17 | final case class |(seq:Seq[Tree]) extends Tree 18 | final case class !!(tree:Tree) extends Tree 19 | -------------------------------------------------------------------------------- /src/test/scala/jsentric/ContractTests.scala: -------------------------------------------------------------------------------- 1 | package jsentric 2 | 3 | import argonaut._ 4 | import org.scalatest.{Matchers, FunSuite} 5 | import scalaz.{\/-, \/} 6 | 7 | class ContractTests extends FunSuite with Matchers { 8 | import Jsentric._ 9 | 10 | test("Contract pattern matching") { 11 | object Test extends Contract { 12 | val one = \[String]("one") 13 | val two = \?[Boolean]("two") 14 | val three = \![Int]("three", 3) 15 | val four = \[Long]("four") 16 | } 17 | 18 | (Json("one" := "string", "two" := false) match { 19 | case Test.one(s) && Test.two(Some(v)) => s -> v 20 | }) should equal ("string" -> false) 21 | 22 | (Json("one" := "string") match { 23 | case Test.one(s) && Test.two(None) && Test.three(int) => s -> int 24 | }) should equal ("string" -> 3) 25 | 26 | (Json("one" := 123) match { 27 | case Test.one(s) && Test.two(None) => s 28 | case _ => "wrong type" 29 | }) should equal ("wrong type") 30 | 31 | (Json("two" := false) match { 32 | case Test.one(s) && Test.two(None) => s 33 | case Test.two(Some(false)) => "two match" 34 | }) should equal ("two match") 35 | 36 | (Json("three" := 4) match { 37 | case Test.three(i) => i 38 | }) should equal (4) 39 | 40 | (Json("three" := "4") match { 41 | case Test.three(i) => true 42 | case _ => false 43 | }) should equal (false) 44 | 45 | (Json("two" := "String") match { 46 | case Test.two(None) => true 47 | case _ => false 48 | }) should equal (false) 49 | 50 | (Json("two" := jNull) match { 51 | case Test.two(None) => true 52 | case _ => false 53 | }) should be (true) 54 | 55 | (Json("two" := "false") match { 56 | case Test.two(Some(i)) => true 57 | case _ => false 58 | }) should equal (false) 59 | 60 | (Json("three" := "not a number") match { 61 | case Test.three(i) => i 62 | case _ => "wrong type" 63 | }) should equal ("wrong type") 64 | 65 | (jEmptyObject match { 66 | case Test.two(None) => true 67 | case _ => false 68 | }) should be (true) 69 | 70 | val temp = Json("four" := 9223372036854775807L) 71 | (Json("four" := 9223372036854775807L) match { 72 | case Test.four(l) => true 73 | case _ => false 74 | }) should be (true) 75 | 76 | (Json("four" := -9223372036854775808L) match { 77 | case Test.four(l) => true 78 | case _ => false 79 | }) should be (true) 80 | } 81 | 82 | test("Optimistic codecs") { 83 | import OptimisticCodecs._ 84 | 85 | object Test extends Contract { 86 | val one = \[String]("one") 87 | val two = \?[Boolean]("two") 88 | val three = \![Int]("three", 3) 89 | } 90 | (Json("three" := "4") match { 91 | case Test.three(i) => i 92 | case _ => false 93 | }) should equal (4) 94 | 95 | (Json("two" := "false") match { 96 | case Test.two(Some(i)) => true 97 | case _ => false 98 | }) should equal (true) 99 | 100 | (Json("two" := jNull) match { 101 | case Test.two(None) => true 102 | case _ => false 103 | }) should be (true) 104 | 105 | (Json("two" := "text") match { 106 | case Test.two(None) => true 107 | case _ => false 108 | }) should be (true) 109 | } 110 | 111 | test("Nested pattern matching") { 112 | object Test1 extends Contract { 113 | val nested = new \\("nested") { 114 | val one = \[Int]("one") 115 | val level2 = new \\("level2") { 116 | val two = \[Int]("two") 117 | } 118 | } 119 | } 120 | 121 | (Json("nested" -> Json("one" := 1)) match { 122 | case Test1.nested.one(v) => v 123 | }) should equal (1) 124 | (Json("nested" -> Json("level2" -> Json("two" := 34))) match { 125 | case Test1.nested.level2.two(v) => v 126 | }) should equal (34) 127 | } 128 | 129 | test("Default value contract") { 130 | object Test extends Contract { 131 | val one = \![Boolean]("one", false) 132 | } 133 | 134 | (Json("one" := true) match { 135 | case Test.one(b) => b 136 | }) should equal (true) 137 | 138 | (Json() match { 139 | case Test.one(b) => b 140 | }) should equal (false) 141 | } 142 | 143 | test("Advanced patterns") { 144 | object Adv extends Contract { 145 | val tuple = \[(Int, String)]("tuple") 146 | val disrupt = \[\/[String, (Float, Boolean)]] ("disrupt") 147 | val option = \[Option[Seq[Int]]]("option") 148 | } 149 | val obj = Json("tuple" -> jArrayElements(jNumberOrString(45), jString("test")), "disrupt" -> jArrayElements(jNumberOrString(4.56), jFalse), "option" := List(1,2,3,4)) 150 | (obj match { 151 | case Adv.tuple((l,r)) && Adv.disrupt(\/-((f, b))) => 152 | (l,r, f, b) 153 | case Adv.tuple((l,r)) => 154 | (l,r) 155 | }) should equal ((45, "test", 4.56F, false)) 156 | } 157 | 158 | test("Array pattern matching") { 159 | object Arr extends Contract { 160 | val exp = \:[String]("exp") 161 | val maybe = \:?[Int]("maybe") 162 | } 163 | 164 | val obj1 = Json("exp" -> jArrayElements(jString("one"), jString("two"))) 165 | (obj1 match { 166 | case Arr.exp(seq) => seq 167 | }) should equal (Seq("one", "two")) 168 | 169 | val obj2 = Json("exp" -> jArrayElements(jString("one"), jTrue)) 170 | (obj2 match { 171 | case Arr.exp(seq) => seq 172 | case _ => "wrong type" 173 | }) should equal ("wrong type") 174 | 175 | val obj3 = Json("maybe" := List(jNumber(1), jNumber(2))) 176 | (obj3 match { 177 | case Arr.maybe(Some(seq)) => seq 178 | }) should equal (Seq(1, 2)) 179 | } 180 | 181 | test("Optimistic array pattern") { 182 | import OptimisticCodecs._ 183 | 184 | object OptimisticArr extends Contract { 185 | val exp = \:[String]("exp") 186 | val maybe = \:?[Int]("maybe") 187 | } 188 | 189 | (Json("exp" -> jArrayElements(jString("one"), jTrue, jString("three"))) match { 190 | case OptimisticArr.exp(seq) => seq 191 | case _ => "wrong type" 192 | }) should equal (Seq("one", "three")) 193 | 194 | (Json("maybe" := "value") match { 195 | case OptimisticArr.maybe(None) => true 196 | }) should be (true) 197 | 198 | } 199 | 200 | test("Dynamic property") { 201 | object Dyn extends Contract { 202 | val nest = new \\("nest") {} 203 | } 204 | 205 | val dynamic1 = Dyn.$dynamic[Int]("int") 206 | (jEmptyObject match { 207 | case dynamic1(i) => i 208 | }) should be (None) 209 | 210 | (Json("int" := 4) match { 211 | case dynamic1(i) => i 212 | }) should be (Some(4)) 213 | 214 | val dynamic2 = Dyn.nest.$dynamic[Boolean]("bool") 215 | 216 | (Json("nest" -> Json("bool" := true)) match { 217 | case dynamic2(b) => b 218 | }) should be (Some(true)) 219 | } 220 | 221 | test("Recursive contract") { 222 | trait Recursive extends SubContract { 223 | val level = \[Int]("level") 224 | lazy val child = new \\?("child") with Recursive 225 | } 226 | object Recursive extends Contract with Recursive 227 | 228 | (Json("level" := 0, "child" -> Json("level" := 1, "child" -> Json("level" := 2))) match { 229 | case Recursive.child.level(l1) && Recursive.child.child.level(l2) => l1 -> l2 230 | }) should equal (1 -> 2) 231 | } 232 | 233 | test("implicit codec test") { 234 | implicit val D = Codec[Double] 235 | 236 | (jNumberOrString(3) match { 237 | case D(double) => double 238 | }) should equal (3.0) 239 | } 240 | 241 | test("nested codec") { 242 | object T extends Contract { 243 | val t1 = new \\?("t1") { 244 | val t2 = \:?[Json]("t2") 245 | } 246 | } 247 | } 248 | 249 | test("Type contracts") { 250 | object Existence extends ContractType("req") { 251 | val req = \[String]("req") 252 | val value = \[Boolean]("value") 253 | } 254 | (Json("req" := "test") match { 255 | case Existence(_) => true 256 | }) should be (true) 257 | 258 | (Json("value" := "test") match { 259 | case Existence(_) => true 260 | case _ => false 261 | }) should be (false) 262 | } 263 | 264 | test("Value contract") { 265 | object MapContract extends ValueContract[Map[String, Boolean]]() 266 | 267 | (Json("value1" := true, "value2" := false) match { 268 | case MapContract(m) => m.size 269 | }) should be (2) 270 | 271 | 272 | } 273 | } -------------------------------------------------------------------------------- /src/test/scala/jsentric/ExtractorCompositorTests.scala: -------------------------------------------------------------------------------- 1 | package jsentric 2 | 3 | import argonaut._ 4 | import org.scalatest.{FunSuite, Matchers} 5 | import shapeless._ 6 | 7 | class ExtractorCompositorTests extends FunSuite with Matchers { 8 | import Jsentric._ 9 | import ApplicativeLens._ 10 | import ops.hlist.Tupler._ 11 | 12 | object TestObj extends Contract { 13 | val int = \[Int]("int") 14 | val bool = \[Boolean]("bool") 15 | val string = \[String]("string") 16 | 17 | lazy val composite = TestObj.string @: TestObj.int @: TestObj.bool 18 | } 19 | 20 | test("Messing about") { 21 | val json = Json("int" := 1, "bool" := false, "string" := "Test") 22 | 23 | json match { 24 | case TestObj.composite((s, i, b)) => 25 | s should equal ("Test") 26 | i should equal (1) 27 | b should equal (false) 28 | } 29 | } 30 | 31 | } -------------------------------------------------------------------------------- /src/test/scala/jsentric/FunctionsTests.scala: -------------------------------------------------------------------------------- 1 | package jsentric 2 | 3 | import argonaut._ 4 | import Argonaut._ 5 | import org.scalatest.{Matchers, FunSuite} 6 | 7 | class FunctionsTests extends FunSuite with Matchers with Functions { 8 | 9 | test("Applying svalue delta on obj") { 10 | val obj = Json("one" := 1, "two" := "two") 11 | applyDelta(obj, jString("value")) should equal (jString("value")) 12 | applyDelta(obj, jNull) should equal (jNull) 13 | } 14 | 15 | test("Applying single key delta object value on obj") { 16 | val obj = Json("one" := 1, "two" := "two") 17 | applyDelta(obj, jEmptyObject) should equal (obj) 18 | applyDelta(obj, Json("three" := 3)) should equal (("three" := 3) ->: obj) 19 | applyDelta(obj, Json("two" := "three")) should equal (Json("one" := 1, "two" := "three")) 20 | applyDelta(obj, Json("one" -> jNull)) should equal (Json("two" := "two")) 21 | applyDelta(obj, Json("two" -> jEmptyObject)) should equal (Json("one" := 1)) 22 | } 23 | 24 | test("Applying nested delta object") { 25 | val obj = Json("one" := 1, "obj" -> Json("two" := false, "three" := List(1,2,3,4), "four" -> Json("five" := 5))) 26 | applyDelta(obj, Json("obj" -> jEmptyObject)) should equal (obj) 27 | applyDelta(obj, Json("obj" -> jNull)) should equal (Json("one" := 1)) 28 | applyDelta(obj, Json("obj" -> Json("two" := true))) should equal (Json("one" := 1, "obj" -> Json("two" := true, "three" := List(1,2,3,4), "four" -> Json("five" := 5)))) 29 | applyDelta(obj, Json("obj" -> Json("three" -> jNull))) should equal (Json("one" := 1, "obj" -> Json("two" := false, "four" -> Json("five" := 5)))) 30 | applyDelta(obj, Json("obj" -> Json("two" -> jNull, "three" -> jNull, "four" -> jNull))) should equal (Json("one" := 1)) 31 | applyDelta(obj, Json("obj" -> Json("six" := "vi"))) should equal (Json("one" := 1, "obj" -> Json("two" := false, "three" := List(1,2,3,4), "four" -> Json("five" := 5), "six" := "vi"))) 32 | } 33 | 34 | test("Get value") { 35 | val obj = Json("one" := 1, "obj" -> Json("two" := false, "three" := List(1,2,3,4), "four" -> Json("five" := 5))) 36 | getValue(obj, Vector.empty) should be (Some(obj)) 37 | getValue(obj, Path("two").segments) should be (None) 38 | getValue(obj, Path("one").segments) should be (Some(jNumber(1))) 39 | getValue(obj, Path("obj", "two").segments) should be (Some(jFalse)) 40 | getValue(obj, Path("obj", "one").segments) should be (None) 41 | getValue(obj, Path("obj", "four").segments) should be (Some(Json("five" := 5))) 42 | getValue(obj, Path("obj", "four", "five").segments) should be (Some(jNumber(5))) 43 | getValue(obj, Path("obj", "three", 3).segments) should be (Some(jNumber(4))) 44 | getValue(obj, Path("obj", "three", 4).segments) should be (None) 45 | } 46 | 47 | test("Set value") { 48 | val obj = Json("one" := 1, "obj" -> Json("two" := false, "three" := List(1,2,3,4), "four" -> Json("five" := 5))) 49 | setValue(Some(obj), Vector.empty, jString("replace")) should be (jString("replace")) 50 | setValue(Some(obj), Path("two").segments, jString("set")) should be (Json("one" := 1, "two" := "set", "obj" -> Json("two" := false, "three" := List(1,2,3,4), "four" -> Json("five" := 5)))) 51 | setValue(Some(obj), Path("one").segments, jString("replace")) should be (Json("one" := "replace", "obj" -> Json("two" := false, "three" := List(1,2,3,4), "four" -> Json("five" := 5)))) 52 | setValue(Some(obj), Path("obj", "two").segments, jString("replace")) should be (Json("one" := 1, "obj" -> Json("two" := "replace", "three" := List(1,2,3,4), "four" -> Json("five" := 5)))) 53 | setValue(Some(obj), Path("obj", "four", "five").segments, jString("replace")) should be (Json("one" := 1, "obj" -> Json("two" := false, "three" := List(1,2,3,4), "four" -> Json("five" := "replace")))) 54 | setValue(Some(obj), Path("obj", "three", 3).segments, jNumberOrString(5)) should be (Json("one" := 1, "obj" -> Json("two" := false, "three" := List(1,2,3,5), "four" -> Json("five" := 5)))) 55 | setValue(Some(obj), Path("obj", "three", 5).segments, jNumberOrString(5)) should be (Json("one" := 1, "obj" -> Json("two" := false, "three" := List(jNumberOrString(1),jNumberOrString(2),jNumberOrString(3),jNumberOrString(4),jNull,jNumberOrString(5)), "four" -> Json("five" := 5)))) 56 | setValue(Some(obj), Path("two", "three").segments, jString("set")) should be (Json("one" := 1, "two" -> Json("three" := "set"), "obj" -> Json("two" := false, "three" := List(1,2,3,4), "four" -> Json("five" := 5)))) 57 | setValue(Some(obj), Path("two", 2).segments, jTrue) should be (Json("one" := 1, "two" := List(jNull, jNull, jTrue), "obj" -> Json("two" := false, "three" := List(1,2,3,4), "four" -> Json("five" := 5)))) 58 | } 59 | 60 | test("Get difference") { 61 | val obj = Json("one" := 1, "obj" -> Json("two" := false, "three" := List(1,2,3,4), "four" -> Json("five" := 5))) 62 | difference(obj, obj) should be (None) 63 | difference(jEmptyObject, obj) should be (None) 64 | difference(Json("one" := 1), obj) should be (None) 65 | difference(Json("one" := 1, "obj" -> Json("two" := false)), obj) should be (None) 66 | difference(Json("obj" -> Json("four" -> jEmptyObject)), obj) should be (None) 67 | difference(Json("obj" -> Json("four" -> Json("five" := 5))), obj) should be (None) 68 | 69 | difference(Json("one" := 2), obj) should be (Some(Json("one" := 2))) 70 | difference(Json("six" := 6), obj) should be (Some(Json("six" := 6))) 71 | difference(Json("obj" -> Json("four" -> Json("six" := 34.56))), obj) should be (Some(Json("obj" -> Json("four" -> Json("six" := 34.56))))) 72 | difference(Json("obj" -> Json("two" := true, "three" := List(1,2,3,4))), obj) should be (Some(Json("obj" -> Json("two" := true)))) 73 | difference(Json("obj" -> Json("three" := List(1,2,3,4,5,6))), obj) should be (Some(Json("obj" -> Json("three" := List(1,2,3,4,5,6))))) 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/test/scala/jsentric/LensTests.scala: -------------------------------------------------------------------------------- 1 | package jsentric 2 | 3 | import argonaut._ 4 | import org.scalatest.{Matchers, FunSuite} 5 | 6 | class LensTests extends FunSuite with Matchers { 7 | import Jsentric._ 8 | 9 | test("Expected property lens test") { 10 | object ExpTest extends Contract { 11 | val value = \[Double]("value") 12 | val option = \[Option[String]]("option") 13 | val value2 = \[Double]("value2") 14 | } 15 | val withSome = Json("value" := 3.45, "option" := "some", "value2" := -1) 16 | 17 | ExpTest.value.$set(42.5)(withSome) should be (Json("value" := 42.5, "option" := "some", "value2" := -1)) 18 | ExpTest.value.$get(withSome) should be (Some(3.45)) 19 | ExpTest.value.$modify(_ * 2)(withSome) should be (Json("value" := 6.9, "option" := "some", "value2" := -1)) 20 | ExpTest.value.$maybeSet(None)(withSome) should be (withSome) 21 | ExpTest.value.$maybeSet(Some(-1))(withSome) should be (Json("value" := -1, "option" := "some", "value2" := -1)) 22 | ExpTest.value.$copy(ExpTest.value2)(withSome) should be (Json("value" := 3.45, "option" := "some", "value2" := 3.45)) 23 | 24 | ExpTest.option.$set(None)(withSome) should be (Json("value" := 3.45, "option" -> jNull, "value2" := -1)) 25 | ExpTest.option.$set(Some("new"))(withSome) should be (Json("value" := 3.45, "option" := "new", "value2" := -1)) 26 | ExpTest.option.$get(withSome) should be (Some(Some("some"))) 27 | } 28 | 29 | test("Maybe property lens test") { 30 | object ExpTest extends Contract { 31 | val value = \?[Boolean]("value") 32 | val value2 = \[Boolean]("value2") 33 | } 34 | val withSome = Json("value" := true, "value2" := false) 35 | val withNone = Json("value2" := false) 36 | 37 | ExpTest.value.$get(withSome) should be (Some(true)) 38 | ExpTest.value.$get(withNone) should be (None) 39 | 40 | ExpTest.value.$set(false)(withSome) should be (Json("value" := false, "value2" := false)) 41 | ExpTest.value.$set(true)(withSome) should be (withSome) 42 | ExpTest.value.$set(false)(withNone) should be (Json("value" := false, "value2" := false)) 43 | ExpTest.value.$set(true)(withNone) should be (withSome) 44 | 45 | ExpTest.value.$drop(withSome) should be (withNone) 46 | ExpTest.value.$drop(withNone) should be (withNone) 47 | 48 | ExpTest.value.$setOrDrop(Some(false))(withSome) should be (Json("value" := false, "value2" := false)) 49 | ExpTest.value.$setOrDrop(None)(withSome) should be (withNone) 50 | 51 | ExpTest.value.$modify(_ => None)(withSome) should be (withNone) 52 | ExpTest.value.$modify(o => o.map(!_))(withSome) should be (Json("value" := false, "value2" := false)) 53 | ExpTest.value.$modify(o => Some(o.isEmpty))(withNone) should be (withSome) 54 | 55 | ExpTest.value.$copy(ExpTest.value2)(withSome) should be (Json("value" := true, "value2" := true)) 56 | ExpTest.value.$copy(ExpTest.value2)(withNone) should be (withNone) 57 | } 58 | 59 | test("Default property lens test") { 60 | object ExpTest extends Contract { 61 | val value = \![String]("value", "default") 62 | val value2 = \[String]("value2") 63 | } 64 | val withSome = Json("value" := "set", "value2" := "add") 65 | val withNone = Json("value2" := "add") 66 | 67 | ExpTest.value.$get(withSome) should be ("set") 68 | ExpTest.value.$get(withNone) should be ("default") 69 | 70 | ExpTest.value.$set("new")(withSome) should be (Json("value" := "new", "value2" := "add")) 71 | ExpTest.value.$set("new")(withNone) should be (Json("value" := "new", "value2" := "add")) 72 | ExpTest.value.$set("default")(withSome) should be (Json("value" := "default", "value2" := "add")) 73 | ExpTest.value.$set("default")(withNone) should be (Json("value" := "default", "value2" := "add")) 74 | 75 | ExpTest.value.$reset(withSome) should be (withNone) 76 | ExpTest.value.$reset(withNone) should be (withNone) 77 | 78 | ExpTest.value.$modify(_ + "-")(withSome) should be (Json("value" := "set-", "value2" := "add")) 79 | ExpTest.value.$modify(_ + "-")(withNone) should be (Json("value" := "default-", "value2" := "add")) 80 | 81 | ExpTest.value.$setOrReset(None)(withSome) should be (withNone) 82 | ExpTest.value.$setOrReset(Some("set"))(withNone) should be (withSome) 83 | ExpTest.value.$copy(ExpTest.value2)(withSome) should be (Json("value" := "set", "value2" := "set")) 84 | ExpTest.value.$copy(ExpTest.value2)(withNone) should be (Json("value2" := "default")) 85 | } 86 | 87 | test("Expected array lens test") { 88 | object ExpTest extends Contract { 89 | val value = \:[Int]("value") 90 | val value2 = \:[Int]("value2") 91 | } 92 | val withSome = Json("value" := List(jNumberOrNull(1),jNumberOrNull(2),jNumberOrNull(3)), "value2" := List(jNumberOrNull(2))) 93 | val withNone = Json("value2" := List(jNumberOrNull(2))) 94 | 95 | ExpTest.value.$at(2).$get(withSome) should be (Some(3)) 96 | ExpTest.value.$at(3).$get(withSome) should be (None) 97 | ExpTest.value.$at(3).$get(withNone) should be (None) 98 | 99 | ExpTest.value.$at(2).$set(5)(withSome) should be (Json("value" := List(jNumberOrNull(1),jNumberOrNull(2),jNumberOrNull(5)), "value2" := List(jNumberOrNull(2)))) 100 | ExpTest.value.$at(6).$set(5)(withSome) should be (Json("value" := List(jNumberOrNull(1),jNumberOrNull(2),jNumberOrNull(3), jNull, jNull, jNull, jNumberOrNull(5)), "value2" := List(jNumberOrNull(2)))) 101 | 102 | ExpTest.value.$append(4)(withSome) should be (Json("value" := List(jNumberOrNull(1),jNumberOrNull(2),jNumberOrNull(3),jNumberOrNull(4)), "value2" := List(jNumberOrNull(2)))) 103 | ExpTest.value.$append(4)(withNone) should be (Json("value" := List(jNumberOrNull(4)), "value2" -> jArrayElements(jNumberOrNull(2)))) 104 | 105 | ExpTest.value.$prepend(0)(withSome) should be (Json("value" := List(jNumberOrNull(0),jNumberOrNull(1),jNumberOrNull(2),jNumberOrNull(3)), "value2" := List(jNumberOrNull(2)))) 106 | ExpTest.value.$prepend(0)(withNone) should be (Json("value" := List(jNumberOrNull(0)), "value2" -> jArrayElements(jNumberOrNull(2)))) 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/test/scala/jsentric/ProjectionTests.scala: -------------------------------------------------------------------------------- 1 | package jsentric 2 | 3 | import argonaut.Json 4 | import org.scalatest.{FunSuite, Matchers} 5 | 6 | class ProjectionTests extends FunSuite with Matchers { 7 | import jsentric.Jsentric._ 8 | 9 | test("Projection selection") { 10 | object Query1 extends Contract { 11 | val field = \?[String]("field") 12 | val nested = new \\("nested") { 13 | val field2 = \[String]("field2") 14 | val field3 = \?[Int]("field3") 15 | } 16 | } 17 | val projection = Query1.field.$ & Query1.nested.field2.$ 18 | val result = Query1.$create(c => c.field.$set("one") ~ c.nested.field2.$set("two")) 19 | result.$select(projection) should equal (Json("field" := "one", "nested" -> Json("field2" := "two"))) 20 | 21 | val noMatch = Json("value" := 1, "nested" -> Json("value2" := "test")) 22 | noMatch.$select(projection) should equal (jEmptyObject) 23 | } 24 | } -------------------------------------------------------------------------------- /src/test/scala/jsentric/QueryJsonbTests.scala: -------------------------------------------------------------------------------- 1 | package jsentric 2 | 3 | import org.scalatest.{FunSuite, Matchers} 4 | import scalaz.\/- 5 | 6 | 7 | class QueryJsonbTests extends FunSuite with Matchers { 8 | import Jsentric._ 9 | 10 | test("simple operations") { 11 | object SimpleObject extends Contract { 12 | val int = \?[Int]("int") 13 | val string = \[String]("string") 14 | val array = \:[Int]("array") 15 | val bool = \[Boolean]("bool") 16 | val nested = new \\("nested") { 17 | val double = \[Double]("double") 18 | val string = \?[String]("string") 19 | } 20 | } 21 | val exists = SimpleObject.int.$exists(true) 22 | val notExists = SimpleObject.int.$exists(false) 23 | val equals = SimpleObject.string.$eq("value") 24 | val nequals = SimpleObject.bool.$ne(false) 25 | val compare = SimpleObject.int.$gte(45) 26 | val contains = SimpleObject.array.$elemMatch(_.$eq(4)) 27 | val composite = SimpleObject.int.$gte(45) && SimpleObject.int.$lt(500) 28 | val in = SimpleObject.int.$in(4,7,9,10) 29 | val nin = SimpleObject.string.$nin("value", "value2") 30 | val nt = Jsentric.not(SimpleObject.string.$eq("value")) 31 | val nested = SimpleObject.nested.double.$lte(34) 32 | val like = SimpleObject.nested.string.$like("%tr%") 33 | val regex = SimpleObject.nested.string.$regex(".ES.*", "i") 34 | 35 | 36 | 37 | QueryJsonb("content", exists) should be (\/-("content ?? 'int'")) 38 | QueryJsonb("content", notExists) should be (\/-("NOT content ?? 'int'")) 39 | QueryJsonb("content", equals) should be (\/-("content @> '{\"string\":\"value\"}'::jsonb")) 40 | QueryJsonb("content", nequals) should be (\/-("NOT content @> '{\"bool\":false}'::jsonb")) 41 | QueryJsonb("content", compare) should be (\/-("(jsonb_typeof(content #> '{int}') = 'number' AND (content #>> '{int}') :: NUMERIC > 45)")) 42 | QueryJsonb("content", contains) should be (\/-("content @> '{\"array\":[4]}'::jsonb")) 43 | QueryJsonb("content", composite) should be (\/-("((jsonb_typeof(content #> '{int}') = 'number' AND (content #>> '{int}') :: NUMERIC > 45) AND (jsonb_typeof(content #> '{int}') = 'number' AND (content #>> '{int}') :: NUMERIC < 500))")) 44 | QueryJsonb("content", in) should be (\/-("content #> '{int}' <@ '[4,7,9,10]'::jsonb")) 45 | QueryJsonb("content", nin) should be (\/-("NOT content #> '{string}' <@ '[\"value\",\"value2\"]'::jsonb")) 46 | QueryJsonb("content", nt) should be (\/-("NOT (content @> '{\"string\":\"value\"}'::jsonb)")) 47 | QueryJsonb("content", nested) should be (\/-("(jsonb_typeof(content #> '{nested,double}') = 'number' AND (content #>> '{nested,double}') :: NUMERIC < 34)")) 48 | QueryJsonb("content", like) should be (\/-("(content #>> '{nested,string}') ILIKE '%tr%'")) 49 | QueryJsonb("content", regex) should be (\/-("(content #>> '{nested,string}') ~ '(?i).ES.*'")) 50 | } 51 | 52 | 53 | } -------------------------------------------------------------------------------- /src/test/scala/jsentric/QueryTests.scala: -------------------------------------------------------------------------------- 1 | package jsentric 2 | 3 | import jsentric.queryTree.QueryTree 4 | import org.scalatest.{FunSuite, Matchers} 5 | import argonaut._ 6 | import Argonaut._ 7 | 8 | class QueryTests extends FunSuite with Matchers { 9 | import Jsentric._ 10 | 11 | test("Existance/nonexistance of field") { 12 | object Query1 extends Contract { 13 | val field = \?[String]("field") 14 | val nested = new \\("nested") { 15 | val field2 = \?[String]("field2") 16 | } 17 | } 18 | val query = Query1.field.$exists(true) 19 | val tree = QueryTree(query.obj.get) 20 | 21 | query.$isMatch(Json("field" := "value")) should be (true) 22 | query.$isMatch(Json("field2" := "value")) should be (false) 23 | tree.isMatch(Json("field" := "value")) should be (true) 24 | tree.isMatch(Json("field2" := "value")) should be (false) 25 | 26 | val query2 = Query1.field.$exists(false) && Query1.nested.field2.$exists(true) 27 | val tree2 = QueryTree(query2.obj.get) 28 | 29 | query2.$isMatch(Json("nested" -> Json("field2" := "value"))) should be (true) 30 | query2.$isMatch(Json("field" := "value", "nested" -> Json("field2" := "value"))) should be (false) 31 | tree2.isMatch(Json("nested" -> Json("field2" := "value"))) should be (true) 32 | tree2.isMatch(Json("field" := "value", "nested" -> Json("field2" := "value"))) should be (false) 33 | 34 | } 35 | 36 | test("Equality") { 37 | object Query2 extends Contract { 38 | val field = \?[String]("field") 39 | val nested = new \\("nested") { 40 | val field2 = \[Int]("field2") 41 | } 42 | } 43 | val query1 = Query2.field.$eq("TEST") || Query2.nested.field2.$eq(45) 44 | val tree1 = QueryTree(query1.obj.get) 45 | query1.$isMatch(Json("field" := "TEST")) should be (true) 46 | query1.$isMatch(jEmptyObject) should be (false) 47 | query1.$isMatch(Json("field" := "TEST2")) should be (false) 48 | query1.$isMatch(Json("nested" -> Json("field2" := 45))) should be (true) 49 | query1.$isMatch(Json("field" := "TEST", "nested" -> Json("field2" := 45))) should be (true) 50 | tree1.isMatch(Json("field" := "TEST")) should be (true) 51 | tree1.isMatch(jEmptyObject) should be (false) 52 | tree1.isMatch(Json("field" := "TEST2")) should be (false) 53 | tree1.isMatch(Json("nested" -> Json("field2" := 45))) should be (true) 54 | tree1.isMatch(Json("field" := "TEST", "nested" -> Json("field2" := 45))) should be (true) 55 | 56 | val query2 = Query2.field.$ne("TEST") || Query2.nested(n => n.field2.$gte(45) && n.field2.$lt(52)) 57 | val tree2 = QueryTree(query2.obj.get) 58 | 59 | query2.$isMatch(Json("field" := "TEST")) should be (false) 60 | query2.$isMatch(Json("field" := "TEST", "nested" -> Json("field2" := 44))) should be (false) 61 | query2.$isMatch(Json("field" := "TEST", "nested" -> Json("field2" := 52))) should be (false) 62 | query2.$isMatch(Json("field" := "TEST2", "nested" -> Json("field2" := 45))) should be (true) 63 | query2.$isMatch(Json("nested" -> Json("field2" := 44))) should be (true) 64 | tree2.isMatch(Json("field" := "TEST")) should be (false) 65 | tree2.isMatch(Json("field" := "TEST", "nested" -> Json("field2" := 44))) should be (false) 66 | tree2.isMatch(Json("field" := "TEST", "nested" -> Json("field2" := 52))) should be (false) 67 | tree2.isMatch(Json("field" := "TEST2", "nested" -> Json("field2" := 45))) should be (true) 68 | tree2.isMatch(Json("nested" -> Json("field2" := 44))) should be (true) 69 | 70 | val query3 = Query2(q => q.field.$in("TEST", "TEST2") && q.nested.field2.$nin(4,5,6)) 71 | val tree3 = QueryTree(query3.obj.get) 72 | query3.$isMatch(Json("field" := "TEST")) should be (true) 73 | query3.$isMatch(Json("field" := "TEST", "nested" -> Json("field2" := 3))) should be (true) 74 | query3.$isMatch(Json("field" := "TEST", "nested" -> Json("field2" := 4))) should be (false) 75 | query3.$isMatch(Json("field" := "TEST3")) should be (false) 76 | query3.$isMatch(Json("field" := "TEST3", "nested" -> Json("field2" := 3))) should be (false) 77 | query3.$isMatch(Json("nested" -> Json("field2" := 3))) should be (false) 78 | tree3.isMatch(Json("field" := "TEST")) should be (true) 79 | tree3.isMatch(Json("field" := "TEST", "nested" -> Json("field2" := 3))) should be (true) 80 | tree3.isMatch(Json("field" := "TEST", "nested" -> Json("field2" := 4))) should be (false) 81 | tree3.isMatch(Json("field" := "TEST3")) should be (false) 82 | tree3.isMatch(Json("field" := "TEST3", "nested" -> Json("field2" := 3))) should be (false) 83 | tree3.isMatch(Json("nested" -> Json("field2" := 3))) should be (false) 84 | 85 | //TODO not a generalised solution 86 | val query4 = Query2.field.$like("value") 87 | val tree4 = QueryTree(query4.obj.get) 88 | query4.$isMatch(Json("field" := "Value")) should be (true) 89 | query4.$isMatch(jEmptyObject) should be (false) 90 | query4.$isMatch(Json("field" := "Values")) should be (false) 91 | tree4.isMatch(Json("field" := "Value")) should be (true) 92 | tree4.isMatch(jEmptyObject) should be (false) 93 | tree4.isMatch(Json("field" := "Values")) should be (false) 94 | 95 | val query5 = Query2.field.$like("%lue") 96 | val tree5 = QueryTree(query5.obj.get) 97 | query5.$isMatch(Json("field" := "ValuE")) should be (true) 98 | query5.$isMatch(jEmptyObject) should be (false) 99 | query5.$isMatch(Json("field" := "Values")) should be (false) 100 | tree5.isMatch(Json("field" := "ValuE")) should be (true) 101 | tree5.isMatch(jEmptyObject) should be (false) 102 | tree5.isMatch(Json("field" := "Values")) should be (false) 103 | 104 | val query6 = Query2.field.$regex("vaLUe", "i") 105 | val tree6 = QueryTree(query6.obj.get) 106 | query6.$isMatch(Json("field" := "Value")) should be (true) 107 | query6.$isMatch(jEmptyObject) should be (false) 108 | query6.$isMatch(Json("field" := "Values")) should be (false) 109 | tree6.isMatch(Json("field" := "Value")) should be (true) 110 | tree6.isMatch(jEmptyObject) should be (false) 111 | tree6.isMatch(Json("field" := "Values")) should be (false) 112 | } 113 | 114 | test("Long double equality") { 115 | Json("field" := 1L).$isMatch(Json("field" := 1.00D)) should be (true) 116 | QueryTree(Json("field" := 1L).obj.get).isMatch(Json("field" := 1.00D)) should be (true) 117 | } 118 | 119 | test("element match") { 120 | object Query3 extends Contract { 121 | val doubles = \:[Long]("doubles") 122 | val nested = new \\("nested") { 123 | val strings = \:?[String]("strings") 124 | } 125 | } 126 | 127 | val query1 = Query3.doubles.$elemMatch(_.$gt(4)) 128 | 129 | query1.$isMatch(Json("doubles" := (3.asJson -->>: 5.asJson -->>: jEmptyArray))) should be (true) 130 | query1.$isMatch(Json("doubles" := (2.asJson -->>: 4.asJson -->>: jEmptyArray))) should be (false) 131 | query1.$isMatch(Json("doubles" := jEmptyArray)) should be (false) 132 | } 133 | 134 | test("boolean operators") { 135 | object Query4 extends Contract { 136 | val value = \[Double]("value") 137 | } 138 | 139 | val query1 = Query4.value.$gt(0) || Query4.value.$lt(-10) 140 | val tree1 = QueryTree(query1.obj.get) 141 | query1.$isMatch(Json("value" := 2)) should be (true) 142 | query1.$isMatch(Json("value" := -3)) should be (false) 143 | query1.$isMatch(Json("value" := -15)) should be (true) 144 | tree1.isMatch(Json("value" := 2)) should be (true) 145 | tree1.isMatch(Json("value" := -3)) should be (false) 146 | tree1.isMatch(Json("value" := -15)) should be (true) 147 | 148 | val query2 = Jsentric.not(query1) 149 | val tree2 = QueryTree(query2.obj.get) 150 | query2.$isMatch(Json("value" := 2)) should be (false) 151 | query2.$isMatch(Json("value" := -3)) should be (true) 152 | query2.$isMatch(Json("value" := -15)) should be (false) 153 | tree2.isMatch(Json("value" := 2)) should be (false) 154 | tree2.isMatch(Json("value" := -3)) should be (true) 155 | tree2.isMatch(Json("value" := -15)) should be (false) 156 | 157 | val query3 = Query4.value.$gte(0) && Query4.value.$lt(50) 158 | val tree3 = QueryTree(query3.obj.get) 159 | query3.$isMatch(Json("value" := 12)) should be (true) 160 | query3.$isMatch(Json("value" := -3)) should be (false) 161 | query3.$isMatch(Json("value" := 50)) should be (false) 162 | tree3.isMatch(Json("value" := 12)) should be (true) 163 | tree3.isMatch(Json("value" := -3)) should be (false) 164 | tree3.isMatch(Json("value" := 50)) should be (false) 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /src/test/scala/jsentric/Readme.scala: -------------------------------------------------------------------------------- 1 | package jsentric 2 | 3 | class Readme { 4 | import jsentric._ 5 | import Jsentric._ 6 | 7 | /*define a contract, 8 | / /? /! expected, optional, default properties 9 | /: /:? /:! expected, optional, default array properties 10 | // //? expected, option object properties 11 | */ 12 | object Order extends Contract { 13 | val firstName = \[String]("firstName", nonEmptyOrWhiteSpace) 14 | val lastName = \[String]("lastName", nonEmptyOrWhiteSpace) 15 | val orderId = \?[Int]("orderId", reserved && immutable) 16 | 17 | val email = new \\("email") { 18 | val friendlyName = \?[String]("friendlyName") 19 | val address = \[String]("address") 20 | } 21 | val status = \?[String]("status", in("pending", "processing", "sent") && reserved) 22 | val notes = \?[String]("notes", internal) 23 | 24 | val orderLines = \:[(String, Int)]("orderLines", forall(custom[(String, Int)](ol => ol._2 >= 0, "Cannot order negative items"))) 25 | 26 | import Composite._ 27 | //Combine properties to make a composite pattern matcher 28 | lazy val fullName = firstName @: lastName 29 | } 30 | 31 | import argonaut._ 32 | 33 | //Create a new Json object 34 | val newOrder = Order.$create{o => 35 | o.firstName.$set("John") ~ 36 | o.lastName.$set("Smith") ~ 37 | o.email.address.$set("johnSmith@test.com") ~ 38 | o.orderLines.$append("Socks" -> 3) 39 | } 40 | 41 | //validate a new json object 42 | val validated = Order.$validate(newOrder) 43 | 44 | //pattern match property values 45 | newOrder match { 46 | case Order.email.address(email) && Order.email.friendlyName(Some(name)) => 47 | println(s"$email <$name>") 48 | case Order.email.address(email) && Order.fullName(firstName, lastName) => 49 | println(s"$email <$firstName $lastName>") 50 | } 51 | 52 | //make changes to the json object. 53 | val pending = 54 | Order{o => 55 | o.orderId.$set(123) ~ 56 | o.status.$set("pending") ~ 57 | o.notes.$modify(maybe => Some(maybe.foldLeft("Order now pending.")(_ + _))) 58 | }(newOrder) 59 | 60 | //strip out any properties marked internal 61 | val sendToClient = Order.$sanitize(pending) 62 | 63 | //generate query json 64 | val relatedOrdersQuery = Order.orderId.$gt(56) && Order.status.$in("processing", "sent") 65 | //experimental convert to postgres jsonb clause 66 | val postgresQuery = QueryJsonb("data", relatedOrdersQuery) 67 | 68 | import scalaz.{\/, \/-} 69 | //create a dynamic property 70 | val dynamic = Order.$dynamic[\/[String, Int]]("age") 71 | 72 | sendToClient match { 73 | case dynamic(Some(\/-(ageInt))) => 74 | println(ageInt) 75 | case _ => 76 | } 77 | 78 | val statusDelta = Order.$create(_.status.$set("processing")) 79 | //validate against current state 80 | Order.$validate(statusDelta, pending) 81 | //apply delta to current state 82 | val processing = pending.delta(statusDelta) 83 | 84 | //Define subcontract for reusable or recursive structures 85 | trait UserTimestamp extends SubContract { 86 | val account = \[String]("account") 87 | val timestamp = \[Long]("timestamp") 88 | } 89 | object Element extends Contract { 90 | val created = new \\("created", immutable) with UserTimestamp 91 | val modified = new \\("modified") with UserTimestamp 92 | } 93 | 94 | //try to force a match even if wrong type 95 | import OptimisticCodecs._ 96 | Json("orderId" := "23628") match { 97 | case Order.orderId(Some(id)) => id 98 | } 99 | 100 | } -------------------------------------------------------------------------------- /src/test/scala/jsentric/ValidatorTests.scala: -------------------------------------------------------------------------------- 1 | package jsentric 2 | 3 | import org.scalatest.{FunSuite, Matchers} 4 | import argonaut._ 5 | import Argonaut._ 6 | 7 | import scalaz._ 8 | 9 | class ValidatorTests extends FunSuite with Matchers { 10 | import Jsentric._ 11 | 12 | private def toSet(dis: \/[NonEmptyList[(String, Path)], Json]): \/[Set[(String, Path)], Json] = 13 | dis.leftMap(_.list.toList.toSet) 14 | 15 | test("Property validation") { 16 | 17 | object StrValid extends Contract { 18 | val expected = \[String]("expected") 19 | val maybe = \?[Int]("maybe") 20 | val default = \![Boolean]("default", false) 21 | val option = \?[Option[String]]("option") 22 | } 23 | 24 | StrValid.$validate(jEmptyObject) should be (-\/(NonEmptyList("Value required." -> Path("expected")))) 25 | val json1 = Json("expected" := "value") 26 | StrValid.$validate(json1) should be (\/-(json1)) 27 | StrValid.$validate(Json("expected" := 3)) should be (-\/(NonEmptyList("Unexpected type 'JNumber'." -> Path("expected")))) 28 | 29 | val json2 = Json("expected" := "value", "maybe" := 4) 30 | StrValid.$validate(json2) should be (\/-(json2)) 31 | StrValid.$validate(Json("expected" := "value", "maybe" := 4.6)) should be (-\/(NonEmptyList("Unexpected type 'JNumber'." -> Path("maybe")))) 32 | 33 | val json3 = Json("expected" := "value", "default" := true) 34 | StrValid.$validate(json3) should be (\/-(json3)) 35 | StrValid.$validate(Json("expected" := "value", "default" := 4.6)) should be (-\/(NonEmptyList("Unexpected type 'JNumber'." -> Path("default")))) 36 | 37 | val json4 = Json("expected" := "value", "option" := "value") 38 | StrValid.$validate(json4) should be (\/-(json4)) 39 | val json5 = Json("expected" := "value", "maybe" := jNull, "option" := jNull) 40 | StrValid.$validate(json5) should be (\/-(json5)) 41 | StrValid.$validate(Json("expected" := "value", "option" := false)) should be (-\/(NonEmptyList("Unexpected type 'JBool'." -> Path("option")))) 42 | } 43 | 44 | test("Nested validation") { 45 | 46 | object NestValid extends Contract { 47 | val value1 = \[String]("value1") 48 | val nest1 = new \\("nest1") { 49 | val value2 = \[String]("value2") 50 | val value3 = \[String]("value3") 51 | } 52 | val nest2 = new \\?("nest2") { 53 | val nest3 = new \\("nest3") { 54 | val value4 = \[String]("value4") 55 | } 56 | val value5 = \[String]("value5") 57 | } 58 | } 59 | 60 | val json1 = Json("value1" := "V", "nest1" := Json("value2" := "V", "value3" := "V")) 61 | NestValid.$validate(json1) should be (\/-(json1)) 62 | val json2 = Json("value1" := "V", "nest1" := Json("value2" := "V", "value3" := "V"), "nest2" -> Json("nest3" -> Json("value4" := "V"), "value5" := "V")) 63 | NestValid.$validate(json2) should be (\/-(json2)) 64 | 65 | toSet(NestValid.$validate(Json("value1" := "V", "nest1" := Json("value3" := 3)))) should 66 | be (-\/(Set("Value required." -> "nest1"\"value2", "Unexpected type 'JNumber'." -> "nest1"\"value3"))) 67 | 68 | toSet(NestValid.$validate(Json("value1" := "V", "nest2" := jEmptyObject))) should 69 | be (-\/(Set("Value required." -> Path("nest1"), "Value required." -> "nest2"\"nest3", "Value required." -> "nest2"\"value5"))) 70 | } 71 | 72 | test("Internal and reserved validators") { 73 | 74 | object IRValid extends Contract { 75 | val reserve = \?[String]("reserve", reserved) 76 | val defaultReserve = \![Int]("defaultReserve", 0, reserved) 77 | 78 | val intern = \?[Boolean]("intern", internal) 79 | val internReserve = \![Boolean]("internReserve", false, internal) 80 | } 81 | 82 | IRValid.$validate(jEmptyObject) should be (\/-(jEmptyObject)) 83 | IRValid.$validate(Json("reserve" := "check")) should be (-\/(NonEmptyList("Value is reserved and cannot be provided." -> Path("reserve")))) 84 | } 85 | 86 | test("Custom validator") { 87 | 88 | object Custom extends Contract { 89 | val values = \:?[(String, Int)]("values" , forall(custom((t:(String, Int)) => t._2 > 0, "Int must be greater than zero"))) 90 | val compare = \?[Int]("compare", customCompare[Int]((d,c) => math.abs(d - c) < 3, "Cannot change by more than three")) 91 | } 92 | 93 | val success = Json("values" -> jArrayElements(jArrayElements("one".asJson, 1.asJson))) 94 | val failure = Json("values" -> jArrayElements(jArrayElements("one".asJson, 1.asJson), jArrayElements("negative".asJson, (-1).asJson))) 95 | 96 | Custom.$validate(success) should be (\/-(success)) 97 | Custom.$validate(failure) should be (-\/(NonEmptyList("Int must be greater than zero" -> "values"\1))) 98 | 99 | val compare = Json("compare" := 5) 100 | Custom.$validate(compare, Json("compare" := 7)) should be (\/-(compare)) 101 | Custom.$validate(compare, Json("compare" := 0)) should be (-\/(NonEmptyList("Cannot change by more than three" -> Path("compare")))) 102 | } 103 | 104 | test("Delta validation") { 105 | 106 | object Delta extends Contract { 107 | val expected = \[String]("expected") 108 | val immute = \[Boolean]("immute", immutable) 109 | val maybe = \?[Int]("maybe") 110 | val reserve = \?[Float]("reserve", reserved) 111 | } 112 | 113 | val replaceExpected = Json("expected" := "replace") 114 | val replaceImmute = Json("immute" := true) 115 | val replaceMaybe = Json("maybe" := 123) 116 | val clearMaybe = Json("maybe" -> jNull) 117 | val replaceReserve = Json("reserve" := 12.3) 118 | Delta.$validate(replaceExpected, Json("expected" := "original", "immute" := false)) should be (\/-(replaceExpected)) 119 | Delta.$validate(replaceImmute, Json("expected" := "original", "immute" := false)) should be (-\/(NonEmptyList("Value is immutable and cannot be changed." -> Path("immute")))) 120 | Delta.$validate(replaceImmute, Json("expected" := "original", "immute" := true)) should be (\/-(replaceImmute)) 121 | 122 | Delta.$validate(replaceMaybe, Json("expected" := "original", "immute" := false)) should be (\/-(replaceMaybe)) 123 | Delta.$validate(replaceMaybe, Json("expected" := "original", "immute" := false, "maybe" := 1)) should be (\/-(replaceMaybe)) 124 | Delta.$validate(clearMaybe, Json("expected" := "original", "immute" := false)) should be (\/-(clearMaybe)) 125 | Delta.$validate(clearMaybe, Json("expected" := "original", "immute" := false, "maybe" := 1)) should be (\/-(clearMaybe)) 126 | Delta.$validate(replaceReserve, Json("expected" := "original", "immute" := false, "maybe" := 1)) should be (-\/(NonEmptyList("Value is reserved and cannot be provided." -> Path("reserve")))) 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /src/test/scala/jsentric/queryTree/QueryTreeTests.scala: -------------------------------------------------------------------------------- 1 | package jsentric.queryTree 2 | 3 | import jsentric.Path 4 | import org.scalatest.{FunSuite, Matchers} 5 | 6 | class QueryTreeTests extends FunSuite with Matchers { 7 | 8 | def op(fields:String*) = 9 | ?(Path(fields:_*), "", argonaut.Argonaut.jTrue) 10 | 11 | test("Query partition") { 12 | 13 | val and = &(Seq(op("and1"), op("and2"))) 14 | QueryTree.partition(and, Set()) should equal (None -> Some(and)) 15 | QueryTree.partition(and, Set(Path("and1"))) should equal (Some(&(Seq(op("and1")))) -> Some(&(Seq(op("and2"))))) 16 | QueryTree.partition(and, Set(Path("and1"), Path("and2"))) should equal (Some(and) -> None) 17 | 18 | val or = |(Seq(op("or1"), op("or2"))) 19 | QueryTree.partition(or, Set()) should equal (None -> Some(or)) 20 | QueryTree.partition(or, Set(Path("or1"))) should equal (None -> Some(or)) 21 | QueryTree.partition(or, Set(Path("or1"), Path("or2"))) should equal (Some(or) -> None) 22 | 23 | val or2 = |(Seq(op("or3"), op("or4"), op("or5"))) 24 | 25 | val andOr = &(Seq(or, or2)) 26 | QueryTree.partition(andOr, Set()) should equal (None -> Some(andOr)) 27 | QueryTree.partition(andOr, Set("or1", "or4")) should equal (None -> Some(andOr)) 28 | QueryTree.partition(andOr, Set("or1", "or2")) should equal (Some(&(Seq(or))) -> Some(&(Seq(or2)))) 29 | QueryTree.partition(andOr, Set("or1", "or2", "or4")) should equal (Some(&(Seq(or))) -> Some(&(Seq(or2)))) 30 | QueryTree.partition(andOr, Set("or3", "or4", "or5")) should equal (Some(&(Seq(or2))) -> Some(&(Seq(or)))) 31 | QueryTree.partition(andOr, Set("or1", "or2", "or3", "or4", "or5")) should equal (Some(andOr) -> None) 32 | 33 | val and2 = &(Seq(op("and3"), op("and4"))) 34 | val orAnd = |(Seq(and, and2)) 35 | QueryTree.partition(orAnd, Set()) should equal (None -> Some(orAnd)) 36 | QueryTree.partition(orAnd, Set("and1", "and2")) should equal (None -> Some(orAnd)) 37 | QueryTree.partition(orAnd, Set("and1", "and3")) should equal ( 38 | Some(|(Seq(&(Seq(op("and1"))), &(Seq(op("and3")))))) -> Some(orAnd) 39 | ) 40 | QueryTree.partition(orAnd, Set("and1", "and2", "and3")) should equal ( 41 | Some(|(Seq(&(Seq(op("and1"), op("and2"))), &(Seq(op("and3")))))) -> Some(orAnd) 42 | ) 43 | 44 | val notAnd = !!(and) 45 | QueryTree.partition(notAnd, Set()) should equal (None -> Some(notAnd)) 46 | QueryTree.partition(notAnd, Set("and2")) should equal (None -> Some(notAnd)) 47 | QueryTree.partition(notAnd, Set("and1", "and2")) should equal (Some(notAnd) -> None) 48 | 49 | val notOr = !!(or) 50 | QueryTree.partition(notOr, Set()) should equal (None -> Some(notOr)) 51 | QueryTree.partition(notOr, Set("or1")) should equal (Some(!!(|(Seq(op("or1"))))) -> Some(!!(|(Seq(op("or2"))))) ) 52 | 53 | val notAndSeq = &(Seq(op("a"), !!(&(Seq(op("b"), !!(&(Seq(op("c"), op("d"))))))))) 54 | QueryTree.partition(notAndSeq, Set("a", "b", "c", "d")) should equal (Some(notAndSeq) -> None) 55 | QueryTree.partition(notAndSeq, Set("a", "c", "d")) should equal ( 56 | Some(&(List(op("a")))) -> Some(&(List(!!(&(List(op("b"), !!(&(List(op("c"), op("d")))))))))) 57 | ) 58 | QueryTree.partition(notAndSeq, Set("b", "c")) should equal ( 59 | Some(&(Seq(!!(&(Seq(op("b"), !!(&(Seq(op("c")))))))))) -> Some(notAndSeq) 60 | ) 61 | } 62 | } 63 | --------------------------------------------------------------------------------