diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 79a9cda93..1865a036d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -16,10 +16,6 @@ Building and testing Scanamo Scanamo uses a standard [SBT](https://www.scala-sbt.org/) build. If you have SBT installed, you should first run `startDynamodbLocal` task from the SBT prompt to start a local dynamodb instance and afterwards run the `test` command to compile Scanamo and run its tests. -Most, though not all of Scanamo's tests are from examples in the scaladoc, -or `README.md`, which are turned into tests by -[sbt-doctest](https://github.com/tkawachi/sbt-doctest). - Contributing documentation -------------------------- diff --git a/build.sbt b/build.sbt index 7eb33f3cb..f529b71bd 100644 --- a/build.sbt +++ b/build.sbt @@ -56,9 +56,7 @@ val commonSettings = Seq( javacOptions ++= Seq("-source", "1.8", "-target", "1.8", "-Xlint"), scalacOptions := stdOptions ++ extraOptions(scalaVersion.value), addCompilerPlugin("org.typelevel" %% "kind-projector" % "0.10.3"), - // sbt-doctest leaves some unused values - // see https://github.com/scala/bug/issues/10270 - scalacOptions in Test := { + Test / scalacOptions := { val mainScalacOptions = scalacOptions.value (if (CrossVersion.partialVersion(scalaVersion.value) == Some((2, 12))) mainScalacOptions.filter(!Seq("-Ywarn-value-discard", "-Xlint").contains(_)) :+ "-Xlint:-unused,_" diff --git a/project/plugins.sbt b/project/plugins.sbt index d3cbc188b..69e2edb8f 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -1,9 +1,8 @@ -addSbtPlugin("com.localytics" % "sbt-dynamodb" % "2.0.3") -addSbtPlugin("com.github.tkawachi" % "sbt-doctest" % "0.9.6") -addSbtPlugin("com.47deg" % "sbt-microsites" % "1.2.1") -addSbtPlugin("com.geirsson" % "sbt-ci-release" % "1.5.3") -addSbtPlugin("org.scoverage" % "sbt-scoverage" % "1.6.1") -addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.4.0") -addSbtPlugin("ch.epfl.scala" % "sbt-bloop" % "1.4.1") -addSbtPlugin("com.github.cb372" % "sbt-explicit-dependencies" % "0.2.13") -addSbtPlugin("de.heikoseeberger" % "sbt-header" % "5.6.0") +addSbtPlugin("com.localytics" % "sbt-dynamodb" % "2.0.3") +addSbtPlugin("com.47deg" % "sbt-microsites" % "1.2.1") +addSbtPlugin("com.geirsson" % "sbt-ci-release" % "1.5.3") +addSbtPlugin("org.scoverage" % "sbt-scoverage" % "1.6.1") +addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.4.0") +addSbtPlugin("ch.epfl.scala" % "sbt-bloop" % "1.4.1") +addSbtPlugin("com.github.cb372" % "sbt-explicit-dependencies" % "0.2.13") +addSbtPlugin("de.heikoseeberger" % "sbt-header" % "5.6.0") diff --git a/scanamo/src/main/scala/org/scanamo/ScanamoSync.scala b/scanamo/src/main/scala/org/scanamo/ScanamoSync.scala index b39ff6aa9..04f595084 100644 --- a/scanamo/src/main/scala/org/scanamo/ScanamoSync.scala +++ b/scanamo/src/main/scala/org/scanamo/ScanamoSync.scala @@ -25,40 +25,19 @@ import org.scanamo.ops._ * * To avoid blocking, use [[org.scanamo.ScanamoAsync]] */ -class Scanamo private (client: DynamoDbClient) { - final private val interpreter = new ScanamoSyncInterpreter(client) +final class Scanamo private (client: DynamoDbClient) { + private val interpreter = new ScanamoSyncInterpreter(client) /** - * Execute the operations built with [[org.scanamo.Table]], using the client - * provided synchronously - * - * {{{ - * >>> import org.scanamo.generic.auto._ - * - * >>> case class Transport(mode: String, line: String) - * >>> val transport = Table[Transport]("transport") - * - * >>> val client = LocalDynamoDB.syncClient() - * >>> val scanamo = Scanamo(client) - * >>> import software.amazon.awssdk.services.dynamodb.model.ScalarAttributeType._ - * - * >>> LocalDynamoDB.withTable(client)("transport")("mode" -> S, "line" -> S) { - * ... import org.scanamo.syntax._ - * ... val operations = for { - * ... _ <- transport.putAll(Set( - * ... Transport("Underground", "Circle"), - * ... Transport("Underground", "Metropolitan"), - * ... Transport("Underground", "Central"))) - * ... results <- transport.query("mode" -> "Underground" and ("line" beginsWith "C")) - * ... } yield results.toList - * ... scanamo.exec(operations) - * ... } - * List(Right(Transport(Underground,Central)), Right(Transport(Underground,Circle))) - * }}} + * Execute the operations built with [[org.scanamo.Table]] */ - final def exec[A](op: ScanamoOps[A]): A = op.foldMap(interpreter) + def exec[A](op: ScanamoOps[A]): A = op.foldMap(interpreter) - final def execT[M[_]: Monad, A](hoist: Id ~> M)(op: ScanamoOpsT[M, A]): M[A] = + /** + * Execute the operations built with [[org.scanamo.Table]] with + * effects in the monad `M` threaded in. + */ + def execT[M[_]: Monad, A](hoist: Id ~> M)(op: ScanamoOpsT[M, A]): M[A] = op.foldMap(interpreter andThen hoist) } diff --git a/scanamo/src/main/scala/org/scanamo/SecondaryIndex.scala b/scanamo/src/main/scala/org/scanamo/SecondaryIndex.scala index 0c19d934c..bf414097d 100644 --- a/scanamo/src/main/scala/org/scanamo/SecondaryIndex.scala +++ b/scanamo/src/main/scala/org/scanamo/SecondaryIndex.scala @@ -31,30 +31,6 @@ sealed abstract class SecondaryIndex[V] { /** * Scan a secondary index - * - * This will only return items with a value present in the secondary index - * - * {{{ - * >>> case class Bear(name: String, favouriteFood: String, antagonist: Option[String]) - * - * >>> val client = LocalDynamoDB.syncClient() - * >>> val scanamo = Scanamo(client) - * >>> import software.amazon.awssdk.services.dynamodb.model.ScalarAttributeType._ - * - * >>> import org.scanamo.generic.auto._ - * - * >>> LocalDynamoDB.withRandomTableWithSecondaryIndex(client)("name" -> S)("antagonist" -> S) { (t, i) => - * ... val table = Table[Bear](t) - * ... val ops = for { - * ... _ <- table.put(Bear("Pooh", "honey", None)) - * ... _ <- table.put(Bear("Yogi", "picnic baskets", Some("Ranger Smith"))) - * ... _ <- table.put(Bear("Paddington", "marmalade sandwiches", Some("Mr Curry"))) - * ... antagonisticBears <- table.index(i).scan() - * ... } yield antagonisticBears - * ... scanamo.exec(ops) - * ... } - * List(Right(Bear(Paddington,marmalade sandwiches,Some(Mr Curry))), Right(Bear(Yogi,picnic baskets,Some(Ranger Smith)))) - * }}} */ def scan(): ScanamoOps[List[Either[DynamoReadError, V]]] @@ -79,32 +55,6 @@ sealed abstract class SecondaryIndex[V] { /** * Run a query against keys in a secondary index - * - * {{{ - * >>> case class GithubProject(organisation: String, repository: String, language: String, license: String) - * - * >>> val client = LocalDynamoDB.syncClient() - * >>> val scanamo = Scanamo(client) - * >>> import software.amazon.awssdk.services.dynamodb.model.ScalarAttributeType._ - * - * >>> import org.scanamo.syntax._ - * >>> import org.scanamo.generic.auto._ - * - * >>> LocalDynamoDB.withRandomTableWithSecondaryIndex(client)("organisation" -> S, "repository" -> S)("language" -> S, "license" -> S) { (t, i) => - * ... val githubProjects = Table[GithubProject](t) - * ... val operations = for { - * ... _ <- githubProjects.putAll(Set( - * ... GithubProject("typelevel", "cats", "Scala", "MIT"), - * ... GithubProject("localytics", "sbt-dynamodb", "Scala", "MIT"), - * ... GithubProject("tpolecat", "tut", "Scala", "MIT"), - * ... GithubProject("guardian", "scanamo", "Scala", "Apache 2") - * ... )) - * ... scalaMIT <- githubProjects.index(i).query("language" -> "Scala" and ("license" -> "MIT")) - * ... } yield scalaMIT.toList - * ... scanamo.exec(operations) - * ... } - * List(Right(GithubProject(typelevel,cats,Scala,MIT)), Right(GithubProject(tpolecat,tut,Scala,MIT)), Right(GithubProject(localytics,sbt-dynamodb,Scala,MIT))) - * }}} */ def query(query: Query[_]): ScanamoOps[List[Either[DynamoReadError, V]]] @@ -132,35 +82,6 @@ sealed abstract class SecondaryIndex[V] { /** * Query or scan an index, limiting the number of items evaluated by Dynamo - * - * {{{ - * >>> case class Transport(mode: String, line: String, colour: String) - * - * >>> val client = LocalDynamoDB.syncClient() - * >>> val scanamo = Scanamo(client) - * >>> import software.amazon.awssdk.services.dynamodb.model.ScalarAttributeType._ - * >>> import org.scanamo.syntax._ - * >>> import org.scanamo.generic.auto._ - * - * >>> LocalDynamoDB.withRandomTableWithSecondaryIndex(client)( - * ... "mode" -> S, "line" -> S)("mode" -> S, "colour" -> S - * ... ) { (t, i) => - * ... val transport = Table[Transport](t) - * ... val operations = for { - * ... _ <- transport.putAll(Set( - * ... Transport("Underground", "Circle", "Yellow"), - * ... Transport("Underground", "Metropolitan", "Magenta"), - * ... Transport("Underground", "Central", "Red"), - * ... Transport("Underground", "Picadilly", "Blue"), - * ... Transport("Underground", "Northern", "Black"))) - * ... somethingBeginningWithBl <- transport.index(i).limit(1).descending.query( - * ... ("mode" -> "Underground" and ("colour" beginsWith "Bl")) - * ... ) - * ... } yield somethingBeginningWithBl.toList - * ... scanamo.exec(operations) - * ... } - * List(Right(Transport(Underground,Picadilly,Blue))) - * }}} */ def limit(n: Int): SecondaryIndex[V] @@ -168,34 +89,6 @@ sealed abstract class SecondaryIndex[V] { * Filter the results of `scan` or `query` within DynamoDB * * Note that rows filtered out still count towards your consumed capacity - * {{{ - * >>> case class Transport(mode: String, line: String, colour: String) - * - * >>> val client = LocalDynamoDB.syncClient() - * >>> val scanamo = Scanamo(client) - * >>> import software.amazon.awssdk.services.dynamodb.model.ScalarAttributeType._ - * >>> import org.scanamo.syntax._ - * >>> import org.scanamo.generic.auto._ - * - * >>> LocalDynamoDB.withRandomTableWithSecondaryIndex(client)( - * ... "mode" -> S, "line" -> S)("mode" -> S, "colour" -> S - * ... ) { (t, i) => - * ... val transport = Table[Transport](t) - * ... val operations = for { - * ... _ <- transport.putAll(Set( - * ... Transport("Underground", "Circle", "Yellow"), - * ... Transport("Underground", "Metropolitan", "Magenta"), - * ... Transport("Underground", "Central", "Red"), - * ... Transport("Underground", "Picadilly", "Blue"), - * ... Transport("Underground", "Northern", "Black"))) - * ... somethingBeginningWithC <- transport.index(i) - * ... .filter("line" beginsWith ("C")) - * ... .query("mode" -> "Underground") - * ... } yield somethingBeginningWithC.toList - * ... scanamo.exec(operations) - * ... } - * List(Right(Transport(Underground,Central,Red)), Right(Transport(Underground,Circle,Yellow))) - * }}} */ def filter[C: ConditionExpression](condition: C): SecondaryIndex[V] diff --git a/scanamo/src/main/scala/org/scanamo/Table.scala b/scanamo/src/main/scala/org/scanamo/Table.scala index 02daccf88..f2a2fdef2 100644 --- a/scanamo/src/main/scala/org/scanamo/Table.scala +++ b/scanamo/src/main/scala/org/scanamo/Table.scala @@ -26,29 +26,6 @@ import org.scanamo.update.UpdateExpression /** * Represents a DynamoDB table that operations can be performed against - * - * {{{ - * >>> case class Transport(mode: String, line: String) - * - * >>> val client = LocalDynamoDB.syncClient() - * >>> val scanamo = Scanamo(client) - * >>> import software.amazon.awssdk.services.dynamodb.model.ScalarAttributeType._ - * - * >>> LocalDynamoDB.withRandomTable(client)("mode" -> S, "line" -> S) { t => - * ... import org.scanamo.syntax._ - * ... import org.scanamo.generic.auto._ - * ... val transport = Table[Transport](t) - * ... val operations = for { - * ... _ <- transport.putAll(Set( - * ... Transport("Underground", "Circle"), - * ... Transport("Underground", "Metropolitan"), - * ... Transport("Underground", "Central"))) - * ... results <- transport.query("mode" -> "Underground" and ("line" beginsWith "C")) - * ... } yield results.toList - * ... scanamo.exec(operations) - * ... } - * List(Right(Transport(Underground,Central)), Right(Transport(Underground,Circle))) - * }}} */ case class Table[V: DynamoFormat](name: String) { @@ -71,270 +48,23 @@ case class Table[V: DynamoFormat](name: String) { /** * Deletes multiple items by a unique key - * - * {{{ - * >>> case class Farm(animals: List[String]) - * >>> case class Farmer(name: String, age: Long, farm: Farm) - * - * >>> import software.amazon.awssdk.services.dynamodb.model.ScalarAttributeType._ - * >>> import org.scanamo.syntax._ - * >>> import org.scanamo.generic.auto._ - * - * >>> val client = LocalDynamoDB.syncClient() - * >>> val scanamo = Scanamo(client) - * - * >>> val dataSet = Set( - * ... Farmer("Patty", 200L, Farm(List("unicorn"))), - * ... Farmer("Ted", 40L, Farm(List("T-Rex"))), - * ... Farmer("Jack", 2L, Farm(List("velociraptor")))) - * >>> LocalDynamoDB.withRandomTable(client)("name" -> S) { t => - * ... val farm = Table[Farmer](t) - * ... val operations = for { - * ... _ <- farm.putAll(dataSet) - * ... _ <- farm.deleteAll("name" -> dataSet.map(_.name)) - * ... scanned <- farm.scan - * ... } yield scanned.toList - * ... scanamo.exec(operations) - * ... } - * List() - * }}} */ def deleteAll(items: UniqueKeys[_]): ScanamoOps[Unit] = ScanamoFree.deleteAll(name)(items) /** * A secondary index on the table which can be scanned, or queried against - * - * {{{ - * >>> case class Transport(mode: String, line: String, colour: String) - * - * >>> val client = LocalDynamoDB.syncClient() - * >>> val scanamo = Scanamo(client) - * >>> import software.amazon.awssdk.services.dynamodb.model.ScalarAttributeType._ - * >>> import org.scanamo.syntax._ - * >>> import org.scanamo.generic.auto._ - * - * >>> LocalDynamoDB.withRandomTableWithSecondaryIndex(client)("mode" -> S, "line" -> S)("colour" -> S) { (t, i) => - * ... val transport = Table[Transport](t) - * ... val operations = for { - * ... _ <- transport.putAll(Set( - * ... Transport("Underground", "Circle", "Yellow"), - * ... Transport("Underground", "Metropolitan", "Magenta"), - * ... Transport("Underground", "Central", "Red"))) - * ... MagentaLine <- transport.index(i).query("colour" -> "Magenta") - * ... } yield MagentaLine.toList - * ... scanamo.exec(operations) - * ... } - * List(Right(Transport(Underground,Metropolitan,Magenta))) - * }}} - * - * {{{ - * >>> case class GithubProject(organisation: String, repository: String, language: String, license: String) - * - * >>> import org.scanamo.generic.auto._ - * - * >>> LocalDynamoDB.withRandomTableWithSecondaryIndex(client)("organisation" -> S, "repository" -> S)("language" -> S, "license" -> S) { (t, i) => - * ... val githubProjects = Table[GithubProject](t) - * ... val operations = for { - * ... _ <- githubProjects.putAll(Set( - * ... GithubProject("typelevel", "cats", "Scala", "MIT"), - * ... GithubProject("localytics", "sbt-dynamodb", "Scala", "MIT"), - * ... GithubProject("tpolecat", "tut", "Scala", "MIT"), - * ... GithubProject("guardian", "scanamo", "Scala", "Apache 2") - * ... )) - * ... scalaMIT <- githubProjects.index(i).query("language" -> "Scala" and ("license" -> "MIT")) - * ... } yield scalaMIT.toList - * ... scanamo.exec(operations) - * ... } - * List(Right(GithubProject(typelevel,cats,Scala,MIT)), Right(GithubProject(tpolecat,tut,Scala,MIT)), Right(GithubProject(localytics,sbt-dynamodb,Scala,MIT))) - * }}} */ def index(indexName: String): SecondaryIndex[V] = SecondaryIndexWithOptions[V](name, indexName, ScanamoQueryOptions.default) /** * Updates an attribute that is not part of the key and returns the updated row - * - * To set an attribute: - * - * {{{ - * >>> case class Forecast(location: String, weather: String) - * - * >>> val client = LocalDynamoDB.syncClient() - * >>> val scanamo = Scanamo(client) - * >>> import software.amazon.awssdk.services.dynamodb.model.ScalarAttributeType._ - * - * >>> LocalDynamoDB.withRandomTable(client)("location" -> S) { t => - * ... import org.scanamo.syntax._ - * ... import org.scanamo.generic.auto._ - * ... val forecast = Table[Forecast](t) - * ... val operations = for { - * ... _ <- forecast.put(Forecast("London", "Rain")) - * ... updated <- forecast.update("location" -> "London", set("weather" -> "Sun")) - * ... } yield updated - * ... scanamo.exec(operations) - * ... } - * Right(Forecast(London,Sun)) - * }}} - * - * List attributes can also be appended or prepended to: - * - * {{{ - * >>> case class Character(name: String, actors: List[String]) - * - * >>> LocalDynamoDB.withRandomTable(client)("name" -> S) { t => - * ... import org.scanamo.syntax._ - * ... import org.scanamo.generic.auto._ - * ... val characters = Table[Character](t) - * ... val operations = for { - * ... _ <- characters.put(Character("The Doctor", List("Ecclestone", "Tennant", "Smith"))) - * ... _ <- characters.update("name" -> "The Doctor", append("actors" -> "Capaldi")) - * ... _ <- characters.update("name" -> "The Doctor", prepend("actors" -> "McCoy")) - * ... results <- characters.scan() - * ... } yield results.toList - * ... scanamo.exec(operations) - * ... } - * List(Right(Character(The Doctor,List(McCoy, Ecclestone, Tennant, Smith, Capaldi)))) - * }}} - * - * Appending or prepending creates the list if it does not yet exist: - * - * {{{ - * >>> LocalDynamoDB.withRandomTable(client)("name" -> S) { t => - * ... import org.scanamo.syntax._ - * ... import org.scanamo.generic.auto._ - * ... val characters = Table[Character](t) - * ... val operations = for { - * ... _ <- characters.update("name" -> "James Bond", append("actors" -> "Craig")) - * ... results <- characters.query("name" -> "James Bond") - * ... } yield results.toList - * ... scanamo.exec(operations) - * ... } - * List(Right(Character(James Bond,List(Craig)))) - * }}} - * - * To concatenate a list to the front or end of an existing list, use appendAll/prependAll: - * - * {{{ - * >>> case class Fruit(kind: String, sources: List[String]) - * - * >>> LocalDynamoDB.withRandomTable(client)("kind" -> S) { t => - * ... import org.scanamo.syntax._ - * ... import org.scanamo.generic.auto._ - * ... val fruits = Table[Fruit](t) - * ... val operations = for { - * ... _ <- fruits.put(Fruit("watermelon", List("USA"))) - * ... _ <- fruits.update("kind" -> "watermelon", appendAll("sources" -> List("China", "Turkey"))) - * ... _ <- fruits.update("kind" -> "watermelon", prependAll("sources" -> List("Brazil"))) - * ... results <- fruits.query("kind" -> "watermelon") - * ... } yield results.toList - * ... scanamo.exec(operations) - * ... } - * List(Right(Fruit(watermelon,List(Brazil, USA, China, Turkey)))) - * }}} - * - * Multiple operations can also be performed in one call: - * {{{ - * >>> case class Foo(name: String, bar: Int, l: List[String]) - * - * >>> LocalDynamoDB.withRandomTable(client)("name" -> S) { t => - * ... import org.scanamo.syntax._ - * ... import org.scanamo.generic.auto._ - * ... val foos = Table[Foo](t) - * ... val operations = for { - * ... _ <- foos.put(Foo("x", 0, List("First"))) - * ... updated <- foos.update("name" -> "x", - * ... append("l" -> "Second") and set("bar" -> 1)) - * ... } yield updated - * ... scanamo.exec(operations) - * ... } - * Right(Foo(x,1,List(First, Second))) - * }}} - * - * It"s" also possible to perform `ADD` and `DELETE` updates - * {{{ - * >>> case class Bar(name: String, counter: Long, set: Set[String]) - * - * >>> LocalDynamoDB.withRandomTable(client)("name" -> S) { t => - * ... import org.scanamo.syntax._ - * ... import org.scanamo.generic.auto._ - * ... val bars = Table[Bar](t) - * ... val operations = for { - * ... _ <- bars.put(Bar("x", 1L, Set("First"))) - * ... _ <- bars.update("name" -> "x", - * ... add("counter" -> 10L) and add("set" -> Set("Second"))) - * ... updatedBar <- bars.update("name" -> "x", delete("set" -> Set("First"))) - * ... } yield updatedBar - * ... scanamo.exec(operations) - * ... } - * Right(Bar(x,11,Set(Second))) - * }}} - * - * Updates may occur on nested attributes - * {{{ - * >>> case class Inner(session: String) - * >>> case class Middle(name: String, counter: Long, inner: Inner, list: List[Int]) - * >>> case class Outer(id: java.util.UUID, middle: Middle) - * - * >>> LocalDynamoDB.withRandomTable(client)("id" -> S) { t => - * ... import org.scanamo.syntax._ - * ... import org.scanamo.generic.auto._ - * ... val outers = Table[Outer](t) - * ... val id = java.util.UUID.fromString("a8345373-9a93-43be-9bcd-e3682c9197f4") - * ... val operations = for { - * ... _ <- outers.put(Outer(id, Middle("x", 1L, Inner("alpha"), List(1, 2)))) - * ... updatedOuter <- outers.update("id" -> id, - * ... set("middle" \ "inner" \ "session" -> "beta") and add(("middle" \ "list")(1) -> 1) - * ... ) - * ... } yield updatedOuter - * ... scanamo.exec(operations) - * ... } - * Right(Outer(a8345373-9a93-43be-9bcd-e3682c9197f4,Middle(x,1,Inner(beta),List(1, 3)))) - * }}} - * - * It"s" possible to update one field to the value of another - * {{{ - * >>> case class Thing(id: String, mandatory: Int, optional: Option[Int]) - * - * >>> LocalDynamoDB.withRandomTable(client)("id" -> S) { t => - * ... import org.scanamo.syntax._ - * ... import org.scanamo.generic.auto._ - * ... val things = Table[Thing](t) - * ... val operations = for { - * ... _ <- things.put(Thing("a1", 3, None)) - * ... updated <- things.update("id" -> "a1", set("optional", "mandatory")) - * ... } yield updated - * ... scanamo.exec(operations) - * ... } - * Right(Thing(a1,3,Some(3))) - * }}} */ def update(key: UniqueKey[_], expression: UpdateExpression): ScanamoOps[Either[DynamoReadError, V]] = ScanamoFree.update[V](name)(key)(expression) /** * Query or scan a table, limiting the number of items evaluated by Dynamo - * {{{ - * >>> case class Transport(mode: String, line: String) - * - * >>> val client = LocalDynamoDB.syncClient() - * >>> val scanamo = Scanamo(client) - * >>> import software.amazon.awssdk.services.dynamodb.model.ScalarAttributeType._ - * - * >>> LocalDynamoDB.withRandomTable(client)("mode" -> S, "line" -> S) { t => - * ... import org.scanamo.syntax._ - * ... import org.scanamo.generic.auto._ - * ... val transport = Table[Transport](t) - * ... val operations = for { - * ... _ <- transport.putAll(Set( - * ... Transport("Underground", "Circle"), - * ... Transport("Underground", "Metropolitan"), - * ... Transport("Underground", "Central"))) - * ... results <- transport.limit(1).query("mode" -> "Underground" and ("line" beginsWith "C")) - * ... } yield results.toList - * ... scanamo.exec(operations) - * ... } - * List(Right(Transport(Underground,Central))) - * }}} */ def limit(n: Int) = TableWithOptions[V](name, ScanamoQueryOptions.default).limit(n) @@ -343,273 +73,21 @@ case class Table[V: DynamoFormat](name: String) { * read operations against this table. Note that there is no equivalent on * table indexes as consistent reads from secondary indexes are not * supported by DynamoDB - * - * {{{ - * >>> case class City(country: String, name: String) - * - * >>> import software.amazon.awssdk.services.dynamodb.model.ScalarAttributeType._ - * >>> val client = LocalDynamoDB.syncClient() - * >>> val scanamo = Scanamo(client) - * >>> val (get, scan, query) = LocalDynamoDB.withRandomTable(client)("country" -> S, "name" -> S) { t => - * ... import org.scanamo.syntax._ - * ... import org.scanamo.generic.auto._ - * ... val cityTable = Table[City](t) - * ... val ops = for { - * ... _ <- cityTable.putAll(Set( - * ... City("US", "Nashville"), City("IT", "Rome"), City("IT", "Siena"), City("TZ", "Dar es Salaam"))) - * ... get <- cityTable.consistently.get("country" -> "US" and "name" -> "Nashville") - * ... scan <- cityTable.consistently.scan() - * ... query <- cityTable.consistently.query("country" -> "IT") - * ... } yield (get, scan, query) - * ... scanamo.exec(ops) - * ... } - * >>> get - * Some(Right(City(US,Nashville))) - * - * >>> scan - * List(Right(City(US,Nashville)), Right(City(IT,Rome)), Right(City(IT,Siena)), Right(City(TZ,Dar es Salaam))) - * - * >>> query - * List(Right(City(IT,Rome)), Right(City(IT,Siena))) - * }}} */ def consistently = ConsistentlyReadTable(name) /** * Performs the chained operation, `put` if the condition is met - * - * {{{ - * >>> case class Farm(animals: List[String], hectares: Int) - * >>> case class Farmer(name: String, age: Long, farm: Farm) - * - * >>> import org.scanamo.syntax._ - * >>> import org.scanamo.generic.auto._ - * >>> import org.scanamo.query._ - * >>> import software.amazon.awssdk.services.dynamodb.model.ScalarAttributeType._ - * >>> val client = LocalDynamoDB.syncClient() - * >>> val scanamo = Scanamo(client) - * - * >>> LocalDynamoDB.withRandomTable(client)("name" -> S) { t => - * ... val farmersTable = Table[Farmer](t) - * ... val farmerOps = for { - * ... _ <- farmersTable.put(Farmer("McDonald", 156L, Farm(List("sheep", "cow"), 30))) - * ... _ <- farmersTable.given("age" -> 156L).put(Farmer("McDonald", 156L, Farm(List("sheep", "chicken"), 30))) - * ... _ <- farmersTable.given("age" -> 15L).put(Farmer("McDonald", 156L, Farm(List("gnu", "chicken"), 30))) - * ... farmerWithNewStock <- farmersTable.get("name" -> "McDonald") - * ... } yield farmerWithNewStock - * ... scanamo.exec(farmerOps) - * ... } - * Some(Right(Farmer(McDonald,156,Farm(List(sheep, chicken),30)))) - * - * >>> case class Letter(roman: String, greek: String) - * >>> LocalDynamoDB.withRandomTable(client)("roman" -> S) { t => - * ... val lettersTable = Table[Letter](t) - * ... val ops = for { - * ... _ <- lettersTable.putAll(Set(Letter("a", "alpha"), Letter("b", "beta"), Letter("c", "gammon"))) - * ... _ <- lettersTable.given("greek" beginsWith "ale").put(Letter("a", "aleph")) - * ... _ <- lettersTable.given("greek" beginsWith "gam").put(Letter("c", "gamma")) - * ... letters <- lettersTable.scan() - * ... } yield letters - * ... scanamo.exec(ops).toList - * ... } - * List(Right(Letter(b,beta)), Right(Letter(c,gamma)), Right(Letter(a,alpha))) - * - * >>> import cats.implicits._ - * >>> case class Turnip(size: Int, description: Option[String]) - * >>> LocalDynamoDB.withRandomTable(client)("size" -> N) { t => - * ... val turnipsTable = Table[Turnip](t) - * ... val ops = for { - * ... _ <- turnipsTable.putAll(Set(Turnip(1, None), Turnip(1000, None))) - * ... initialTurnips <- turnipsTable.scan() - * ... _ <- initialTurnips.flatMap(_.toOption).traverse(t => - * ... turnipsTable.given("size" > 500).put(t.copy(description = Some("Big turnip in the country.")))) - * ... turnips <- turnipsTable.scan() - * ... } yield turnips - * ... scanamo.exec(ops).toList - * ... } - * List(Right(Turnip(1,None)), Right(Turnip(1000,Some(Big turnip in the country.)))) - * }}} - * - * Conditions can also make use of negation via `not`: - * - * {{{ - * >>> case class Thing(a: String, maybe: Option[Int]) - * >>> LocalDynamoDB.withRandomTable(client)("a" -> S) { t => - * ... val thingTable = Table[Thing](t) - * ... val ops = for { - * ... _ <- thingTable.putAll(Set(Thing("a", None), Thing("b", Some(1)), Thing("c", None))) - * ... _ <- thingTable.given(attributeExists("maybe")).put(Thing("a", Some(2))) - * ... _ <- thingTable.given(attributeExists("maybe")).put(Thing("b", Some(3))) - * ... _ <- thingTable.given(Not(attributeExists("maybe"))).put(Thing("c", Some(42))) - * ... _ <- thingTable.given(Not(attributeExists("maybe"))).put(Thing("b", Some(42))) - * ... things <- thingTable.scan() - * ... } yield things - * ... scanamo.exec(ops).toList - * ... } - * List(Right(Thing(b,Some(3))), Right(Thing(c,Some(42))), Right(Thing(a,None))) - * }}} - * - * be combined with `and` - * - * {{{ - * >>> case class Compound(a: String, maybe: Option[Int]) - * >>> LocalDynamoDB.withRandomTable(client)("a" -> S) { t => - * ... val compoundTable = Table[Compound](t) - * ... val ops = for { - * ... _ <- compoundTable.putAll(Set(Compound("alpha", None), Compound("beta", Some(1)), Compound("gamma", None))) - * ... _ <- compoundTable.given(Condition(attributeExists("maybe")) and "a" -> "alpha").put(Compound("alpha", Some(2))) - * ... _ <- compoundTable.given(Condition(attributeExists("maybe")) and "a" -> "beta").put(Compound("beta", Some(3))) - * ... _ <- compoundTable.given(Condition("a" -> "gamma") and attributeExists("maybe")).put(Compound("gamma", Some(42))) - * ... compounds <- compoundTable.scan() - * ... } yield compounds - * ... scanamo.exec(ops).toList - * ... } - * List(Right(Compound(beta,Some(3))), Right(Compound(alpha,None)), Right(Compound(gamma,None))) - * }}} - * - * or with `or` - * - * {{{ - * >>> case class Choice(number: Int, description: String) - * >>> LocalDynamoDB.withRandomTable(client)("number" -> N) { t => - * ... val choicesTable = Table[Choice](t) - * ... val ops = for { - * ... _ <- choicesTable.putAll(Set(Choice(1, "cake"), Choice(2, "crumble"), Choice(3, "custard"))) - * ... _ <- choicesTable.given(Condition("description" -> "cake") or Condition("description" -> "death")).put(Choice(1, "victoria sponge")) - * ... _ <- choicesTable.given(Condition("description" -> "cake") or Condition("description" -> "death")).put(Choice(2, "victoria sponge")) - * ... choices <- choicesTable.scan() - * ... } yield choices - * ... scanamo.exec(ops).toList - * ... } - * List(Right(Choice(2,crumble)), Right(Choice(1,victoria sponge)), Right(Choice(3,custard))) - * }}} - * - * The same forms of condition can be applied to deletions - * - * {{{ - * >>> case class Gremlin(number: Int, wet: Boolean, friendly: Boolean) - * >>> LocalDynamoDB.withRandomTable(client)("number" -> N) { t => - * ... val gremlinsTable = Table[Gremlin](t) - * ... val ops = for { - * ... _ <- gremlinsTable.putAll(Set(Gremlin(1, false, true), Gremlin(2, true, false))) - * ... _ <- gremlinsTable.given("wet" -> true).delete("number" -> 1) - * ... _ <- gremlinsTable.given("wet" -> true).delete("number" -> 2) - * ... remainingGremlins <- gremlinsTable.scan() - * ... } yield remainingGremlins - * ... scanamo.exec(ops).toList - * ... } - * List(Right(Gremlin(1,false,true))) - * }}} - * - * and updates - * - * {{{ - * >>> LocalDynamoDB.withRandomTable(client)("number" -> N) { t => - * ... val gremlinsTable = Table[Gremlin](t) - * ... val ops = for { - * ... _ <- gremlinsTable.putAll(Set(Gremlin(1, false, true), Gremlin(2, true, true))) - * ... _ <- gremlinsTable.given("wet" -> true).update("number" -> 1, set("friendly" -> false)) - * ... _ <- gremlinsTable.given("wet" -> true).update("number" -> 2, set("friendly" -> false)) - * ... remainingGremlins <- gremlinsTable.scan() - * ... } yield remainingGremlins - * ... scanamo.exec(ops).toList - * ... } - * List(Right(Gremlin(2,true,false)), Right(Gremlin(1,false,true))) - * }}} - * - * Conditions can also be placed on nested attributes - * - * {{{ - * >>> LocalDynamoDB.withRandomTable(client)("name" -> S) { t => - * ... val smallscaleFarmersTable = Table[Farmer](t) - * ... val farmerOps = for { - * ... _ <- smallscaleFarmersTable.put(Farmer("McDonald", 156L, Farm(List("sheep", "cow"), 30))) - * ... _ <- smallscaleFarmersTable.given("farm" \ "hectares" < 40L).put(Farmer("McDonald", 156L, Farm(List("gerbil", "hamster"), 20))) - * ... _ <- smallscaleFarmersTable.given("farm" \ "hectares" > 40L).put(Farmer("McDonald", 156L, Farm(List("elephant"), 50))) - * ... _ <- smallscaleFarmersTable.given("farm" \ "hectares" -> 20L).update("name" -> "McDonald", append("farm" \ "animals" -> "squirrel")) - * ... farmerWithNewStock <- smallscaleFarmersTable.get("name" -> "McDonald") - * ... } yield farmerWithNewStock - * ... scanamo.exec(farmerOps) - * ... } - * Some(Right(Farmer(McDonald,156,Farm(List(gerbil, hamster, squirrel),20)))) - * }}} */ def given[T: ConditionExpression](condition: T) = ConditionalOperation[V, T](name, condition) /** * Primes a search request with a key to start from: - * - * {{{ - * >>> import org.scanamo.syntax._ - * >>> import org.scanamo.generic.auto._ - * >>> import software.amazon.awssdk.services.dynamodb.model.ScalarAttributeType._ - * >>> val client = LocalDynamoDB.syncClient() - * >>> val scanamo = Scanamo(client) - * - * >>> case class Bear(name: String, favouriteFood: String) - * - * >>> LocalDynamoDB.withRandomTable(client)("name" -> S) { t => - * ... val table = Table[Bear](t) - * ... val ops = for { - * ... _ <- table.put(Bear("Pooh", "honey")) - * ... _ <- table.put(Bear("Baloo", "ants")) - * ... _ <- table.put(Bear("Yogi", "picnic baskets")) - * ... bears <- table.from("name" -> "Baloo").scan() - * ... } yield bears - * ... scanamo.exec(ops) - * ... } - * List(Right(Bear(Pooh,honey)), Right(Bear(Yogi,picnic baskets))) - * }}} - * - * Of course it works with queries too: - * - * {{{ - * >>> case class Event(`type`: String, tag: String, count: Int) - * - * >>> LocalDynamoDB.withRandomTable(client)("type" -> S, "tag" -> S) { t => - * ... val table = Table[Event](t) - * ... val ops = for { - * ... _ <- table.putAll(Set( - * ... Event("click", "paid", 600), - * ... Event("play", "profile", 100), - * ... Event("play", "politics", 200), - * ... Event("click", "profile", 400), - * ... Event("play", "print", 600), - * ... Event("click", "print", 300), - * ... Event("play", "paid", 900) - * ... )) - * ... events <- table.from("type" -> "play" and "tag" -> "politics").query("type" -> "play" and ("tag" beginsWith "p")) - * ... } yield events - * ... scanamo.exec(ops) - * ... } - * List(Right(Event(play,print,600)), Right(Event(play,profile,100))) - * }}} */ def from[K: UniqueKeyCondition](key: UniqueKey[K]) = TableWithOptions(name, ScanamoQueryOptions.default).from(key) /** * Scans all elements of a table - * - * {{{ - * >>> case class Bear(name: String, favouriteFood: String) - * - * >>> val client = LocalDynamoDB.syncClient() - * >>> val scanamo = Scanamo(client) - * >>> import software.amazon.awssdk.services.dynamodb.model.ScalarAttributeType._ - * - * >>> LocalDynamoDB.withRandomTable(client)("name" -> S) { t => - * ... import org.scanamo._ - * ... import org.scanamo.generic.auto._ - * ... val table = Table[Bear](t) - * ... val ops = for { - * ... _ <- table.put(Bear("Pooh", "honey")) - * ... _ <- table.put(Bear("Yogi", "picnic baskets")) - * ... bears <- table.scan() - * ... } yield bears - * ... scanamo.exec(ops) - * ... } - * List(Right(Bear(Pooh,honey)), Right(Bear(Yogi,picnic baskets))) - * }}} */ def scan(): ScanamoOps[List[Either[DynamoReadError, V]]] = ScanamoFree.scan[V](name) @@ -640,67 +118,11 @@ case class Table[V: DynamoFormat](name: String) { * place for putting that information: this is where `scan0` comes in handy! * * A particular use case is when one wants to paginate through result sets, say: - * {{{ - * >>> case class Transport(mode: String, line: String) - * - * >>> val client = LocalDynamoDB.syncClient() - * >>> val scanamo = Scanamo(client) - * - * >>> import cats.implicits._ - * >>> import org.scanamo._ - * >>> import org.scanamo.ops._ - * >>> import org.scanamo.syntax._ - * >>> import org.scanamo.generic.auto._ - * >>> import org.scanamo.query._ - * >>> import software.amazon.awssdk.services.dynamodb.model.ScalarAttributeType._ - * - * >>> LocalDynamoDB.withRandomTable(client)("mode" -> S, "line" -> S) { t => - * ... val table = Table[Transport](t) - * ... val ops = for { - * ... _ <- table.putAll(Set( - * ... Transport("Underground", "Circle"), - * ... Transport("Underground", "Metropolitan"), - * ... Transport("Underground", "Central") - * ... )) - * ... res <- table.limit(1).scan0 - * ... uniqueKeyCondition = UniqueKeyCondition[AndEqualsCondition[KeyEquals[String], KeyEquals[String]], (AttributeName, AttributeName)] - * ... lastKey = uniqueKeyCondition.fromDynamoObject(("mode", "line"), DynamoObject(res.lastEvaluatedKey)) - * ... ts <- lastKey.fold(List.empty[Either[DynamoReadError, Transport]].pure[ScanamoOps])(table.from(_).scan()) - * ... } yield ts - * ... scanamo.exec(ops) - * ... } - * List(Right(Transport(Underground,Circle)), Right(Transport(Underground,Metropolitan))) - * }}} */ def scan0: ScanamoOps[ScanResponse] = ScanamoFree.scan0[V](name) /** * Query a table based on the hash key and optionally the range key - * - * {{{ - * >>> case class Transport(mode: String, line: String) - * - * >>> val client = LocalDynamoDB.syncClient() - * >>> val scanamo = Scanamo(client) - * - * >>> import org.scanamo.syntax._ - * >>> import org.scanamo.generic.auto._ - * >>> import software.amazon.awssdk.services.dynamodb.model.ScalarAttributeType._ - * - * >>> LocalDynamoDB.withRandomTable(client)("mode" -> S, "line" -> S) { t => - * ... val table = Table[Transport](t) - * ... val ops = for { - * ... _ <- table.putAll(Set( - * ... Transport("Underground", "Circle"), - * ... Transport("Underground", "Metropolitan"), - * ... Transport("Underground", "Central") - * ... )) - * ... linesBeginningWithC <- table.query("mode" -> "Underground" and ("line" beginsWith "C")) - * ... } yield linesBeginningWithC - * ... scanamo.exec(ops) - * ... } - * List(Right(Transport(Underground,Central)), Right(Transport(Underground,Circle))) - * }}} */ def query(query: Query[_]): ScanamoOps[List[Either[DynamoReadError, V]]] = ScanamoFree.query[V](name)(query) @@ -734,88 +156,11 @@ case class Table[V: DynamoFormat](name: String) { * place for putting that information: this is where `query0` comes in handy! * * A particular use case is when one wants to paginate through result sets, say: - * {{{ - * >>> case class Transport(mode: String, line: String) - * - * >>> val client = LocalDynamoDB.syncClient() - * >>> val scanamo = Scanamo(client) - * - * >>> import cats.implicits._ - * >>> import org.scanamo._ - * >>> import org.scanamo.ops._ - * >>> import org.scanamo.syntax._ - * >>> import org.scanamo.generic.auto._ - * >>> import org.scanamo.query._ - * >>> import software.amazon.awssdk.services.dynamodb.model.ScalarAttributeType._ - * - * >>> LocalDynamoDB.withRandomTable(client)("mode" -> S, "line" -> S) { t => - * ... val table = Table[Transport](t) - * ... val ops = for { - * ... _ <- table.putAll(Set( - * ... Transport("Underground", "Circle"), - * ... Transport("Underground", "Metropolitan"), - * ... Transport("Underground", "Central"), - * ... Transport("Bus", "390"), - * ... Transport("Bus", "143"), - * ... Transport("Bus", "234") - * ... )) - * ... res <- table.limit(1).query0("mode" -> "Bus" and "line" -> "234") - * ... uniqueKeyCondition = UniqueKeyCondition[AndEqualsCondition[KeyEquals[String], KeyEquals[String]], (AttributeName, AttributeName)] - * ... lastKey = uniqueKeyCondition.fromDynamoObject(("mode", "line"), DynamoObject(res.lastEvaluatedKey)) - * ... ts <- lastKey.fold(List.empty[Either[DynamoReadError, Transport]].pure[ScanamoOps])(table.from(_).scan()) - * ... } yield ts - * ... scanamo.exec(ops) - * ... } - * List(Right(Transport(Bus,390)), Right(Transport(Underground,Central)), Right(Transport(Underground,Circle)), Right(Transport(Underground,Metropolitan))) - * }}} */ def query0(query: Query[_]): ScanamoOps[QueryResponse] = ScanamoFree.query0[V](name)(query) /** * Filter the results of a Scan or Query - * - * {{{ - * >>> case class Bear(name: String, favouriteFood: String, antagonist: Option[String]) - * - * >>> val client = LocalDynamoDB.syncClient() - * >>> val scanamo = Scanamo(client) - * >>> import software.amazon.awssdk.services.dynamodb.model.ScalarAttributeType._ - * - * >>> import org.scanamo.syntax._ - * >>> import org.scanamo.generic.auto._ - * - * >>> LocalDynamoDB.withRandomTable(client)("name" -> S) { t => - * ... val table = Table[Bear](t) - * ... val ops = for { - * ... _ <- table.put(Bear("Pooh", "honey", None)) - * ... _ <- table.put(Bear("Yogi", "picnic baskets", Some("Ranger Smith"))) - * ... honeyBears <- table.filter("favouriteFood" -> "honey").scan() - * ... competitiveBears <- table.filter(attributeExists("antagonist")).scan() - * ... } yield (honeyBears, competitiveBears) - * ... scanamo.exec(ops) - * ... } - * (List(Right(Bear(Pooh,honey,None))),List(Right(Bear(Yogi,picnic baskets,Some(Ranger Smith))))) - * - * >>> case class Station(line: String, name: String, zone: Int) - * - * >>> import software.amazon.awssdk.services.dynamodb.model.ScalarAttributeType._ - * - * >>> LocalDynamoDB.withRandomTable(client)("line" -> S, "name" -> S) { t => - * ... val stationTable = Table[Station](t) - * ... val ops = for { - * ... _ <- stationTable.putAll(Set( - * ... Station("Metropolitan", "Chalfont & Latimer", 8), - * ... Station("Metropolitan", "Chorleywood", 7), - * ... Station("Metropolitan", "Rickmansworth", 7), - * ... Station("Metropolitan", "Croxley", 7), - * ... Station("Jubilee", "Canons Park", 5) - * ... )) - * ... filteredStations <- stationTable.filter("zone" -> Set(8, 7)).query("line" -> "Metropolitan" and ("name" beginsWith "C")) - * ... } yield filteredStations - * ... scanamo.exec(ops) - * ... } - * List(Right(Station(Metropolitan,Chalfont & Latimer,8)), Right(Station(Metropolitan,Chorleywood,7)), Right(Station(Metropolitan,Croxley,7))) - * }}} */ def filter[C: ConditionExpression](condition: C) = TableWithOptions(name, ScanamoQueryOptions.default).filter(Condition(condition))