From c96d56d86b788a0dbbc50fd7cc1c9ee84155fc86 Mon Sep 17 00:00:00 2001 From: "Brian P. Holt" Date: Mon, 10 Feb 2020 10:21:23 -0600 Subject: [PATCH 1/2] enforce NonEmpty property on Spreadsheet ID --- .../gsheets4s/algebras/SpreadsheetsValues.scala | 6 ++++-- .../interpreters/RestSpreadsheetsValues.scala | 5 +++-- src/main/scala/gsheets4s/model.scala | 3 +++ .../integration/SpreadsheetsValuesSpec.scala | 7 ++++--- .../interpreters/TestSpreadsheetsValues.scala | 13 ++++++++----- src/test/scala/gsheets4s/programs/TestProgram.scala | 3 ++- .../gsheets4s/unit/SpreadsheetsValuesSpec.scala | 6 ++++-- 7 files changed, 28 insertions(+), 15 deletions(-) diff --git a/src/main/scala/gsheets4s/algebras/SpreadsheetsValues.scala b/src/main/scala/gsheets4s/algebras/SpreadsheetsValues.scala index 34e797a..42d42e3 100644 --- a/src/main/scala/gsheets4s/algebras/SpreadsheetsValues.scala +++ b/src/main/scala/gsheets4s/algebras/SpreadsheetsValues.scala @@ -1,12 +1,14 @@ package gsheets4s package algebras +import eu.timepit.refined.types.string.NonEmptyString + import model._ trait SpreadsheetsValues[F[_]] { - def get(spreadsheetID: String, range: A1Notation): F[Either[GsheetsError, ValueRange]] + def get(spreadsheetID: NonEmptyString, range: A1Notation): F[Either[GsheetsError, ValueRange]] def update( - spreadsheetID: String, + spreadsheetID: NonEmptyString, range: A1Notation, updates: ValueRange, valueInputOption: ValueInputOption diff --git a/src/main/scala/gsheets4s/interpreters/RestSpreadsheetsValues.scala b/src/main/scala/gsheets4s/interpreters/RestSpreadsheetsValues.scala index b102b9f..6f0d7b1 100644 --- a/src/main/scala/gsheets4s/interpreters/RestSpreadsheetsValues.scala +++ b/src/main/scala/gsheets4s/interpreters/RestSpreadsheetsValues.scala @@ -3,17 +3,18 @@ package interpreters import io.circe.generic.auto._ import io.lemonlabs.uri.typesafe.dsl._ +import eu.timepit.refined.types.string.NonEmptyString import algebras.SpreadsheetsValues import http._ import model._ class RestSpreadsheetsValues[F[_]](client: HttpClient[F]) extends SpreadsheetsValues[F] { - def get(spreadsheetID: String, range: A1Notation): F[Either[GsheetsError, ValueRange]] = + def get(spreadsheetID: NonEmptyString, range: A1Notation): F[Either[GsheetsError, ValueRange]] = client.get(spreadsheetID / "values" / range) def update( - spreadsheetID: String, + spreadsheetID: NonEmptyString, range: A1Notation, updates: ValueRange, valueInputOption: ValueInputOption diff --git a/src/main/scala/gsheets4s/model.scala b/src/main/scala/gsheets4s/model.scala index 450a399..7a7ea89 100644 --- a/src/main/scala/gsheets4s/model.scala +++ b/src/main/scala/gsheets4s/model.scala @@ -12,6 +12,7 @@ import eu.timepit.refined.boolean._ import eu.timepit.refined.char._ import eu.timepit.refined.collection._ import eu.timepit.refined.numeric._ +import eu.timepit.refined.types.string.NonEmptyString import gsheets4s.model.A1Notation import io.circe.{Decoder, DecodingFailure, Encoder, HCursor} import io.circe.generic.semiauto._ @@ -149,6 +150,8 @@ object model extends A1NotationLiteralSyntax { implicit def eitherDecoder[L, R](implicit l: Decoder[L], r: Decoder[R]): Decoder[Either[L, R]] = r.map(Right(_): Either[L, R]).or(l.map(Left(_): Either[L, R])) + + implicit val pathPartNonEmptyString: PathPart[NonEmptyString] = _.value } trait A1NotationLiteralSyntax { diff --git a/src/test/scala/gsheets4s/integration/SpreadsheetsValuesSpec.scala b/src/test/scala/gsheets4s/integration/SpreadsheetsValuesSpec.scala index 0de12fe..c238a3c 100644 --- a/src/test/scala/gsheets4s/integration/SpreadsheetsValuesSpec.scala +++ b/src/test/scala/gsheets4s/integration/SpreadsheetsValuesSpec.scala @@ -4,6 +4,7 @@ package integration import cats.effect.IO import cats.effect.concurrent.Ref import eu.timepit.refined.auto._ +import eu.timepit.refined.types.string.NonEmptyString import org.scalatest._ import model._ @@ -21,7 +22,7 @@ class SpreadsheetsValuesSpec extends AnyFlatSpec { clientSecret <- sys.env.get("GSHEETS4S_CLIENT_SECRET") } yield Credentials(accessToken, refreshToken, clientId, clientSecret) - val spreadsheetID = "1tk2S_A4LZfeZjoMskbfFXO42_b75A7UkSdhKaQZlDmA" + val spreadsheetID: NonEmptyString = "1tk2S_A4LZfeZjoMskbfFXO42_b75A7UkSdhKaQZlDmA" val not = SheetNameRangeNotation("Sheet1", Range(ColRowPosition("A", 1), ColRowPosition("B", 2))) val vr = ValueRange(not, Rows, List(List("1", "2"), List("3", "4"))) @@ -36,7 +37,7 @@ class SpreadsheetsValuesSpec extends AnyFlatSpec { } yield prog).unsafeRunSync() assert(res.isRight) val Right((uvr, vr2)) = res - assert(uvr.spreadsheetId == spreadsheetID) + assert(uvr.spreadsheetId == spreadsheetID.value) assert(uvr.updatedRange == vr.range) assert(vr.range == vr2.range) assert(vr.values == vr2.values) @@ -65,7 +66,7 @@ class SpreadsheetsValuesSpec extends AnyFlatSpec { } yield prog).unsafeRunSync() assert(res.isRight) val Right((uvr, vr2)) = res - assert(uvr.spreadsheetId == spreadsheetID) + assert(uvr.spreadsheetId == spreadsheetID.value) assert(uvr.updatedRange == vr.range) assert(vr.range == vr2.range) assert(vr.values == vr2.values) diff --git a/src/test/scala/gsheets4s/interpreters/TestSpreadsheetsValues.scala b/src/test/scala/gsheets4s/interpreters/TestSpreadsheetsValues.scala index 70b5be9..ab17a92 100644 --- a/src/test/scala/gsheets4s/interpreters/TestSpreadsheetsValues.scala +++ b/src/test/scala/gsheets4s/interpreters/TestSpreadsheetsValues.scala @@ -3,20 +3,23 @@ package interpreters import cats.Id import cats.syntax.foldable._ +import cats.instances.int._ +import cats.instances.option._ +import eu.timepit.refined.types.string.NonEmptyString import algebras._ import model._ object TestSpreadsheetsValues extends SpreadsheetsValues[Id] { - private var data: Map[String, List[List[String]]] = Map.empty + private var data: Map[NonEmptyString, List[List[String]]] = Map.empty - def get(spreadsheetID: String, range: A1Notation): Id[Either[GsheetsError, ValueRange]] = { - val values = data.get(spreadsheetID).getOrElse(List.empty) + def get(spreadsheetID: NonEmptyString, range: A1Notation): Id[Either[GsheetsError, ValueRange]] = { + val values = data.getOrElse(spreadsheetID, List.empty) Right(ValueRange(range, Rows, values)) } def update( - spreadsheetID: String, + spreadsheetID: NonEmptyString, range: A1Notation, updates: ValueRange, valueInputOption: ValueInputOption @@ -24,6 +27,6 @@ object TestSpreadsheetsValues extends SpreadsheetsValues[Id] { data = data + (spreadsheetID -> updates.values) val numRows = updates.values.size val numCols = updates.values.headOption.foldMap(_.size) - Right(UpdateValuesResponse(spreadsheetID, range, numRows, numCols, numRows * numCols)) + Right(UpdateValuesResponse(spreadsheetID.value, range, numRows, numCols, numRows * numCols)) } } diff --git a/src/test/scala/gsheets4s/programs/TestProgram.scala b/src/test/scala/gsheets4s/programs/TestProgram.scala index b7c7806..64944e8 100644 --- a/src/test/scala/gsheets4s/programs/TestProgram.scala +++ b/src/test/scala/gsheets4s/programs/TestProgram.scala @@ -2,6 +2,7 @@ package gsheets4s import cats.Monad import cats.data.EitherT +import eu.timepit.refined.types.string.NonEmptyString import algebras._ import model._ @@ -10,7 +11,7 @@ class TestPrograms[F[_]: Monad](alg: SpreadsheetsValues[F]) { import alg._ def updateAndGet( - spreadsheetId: String, + spreadsheetId: NonEmptyString, vr: ValueRange, vio: ValueInputOption ): F[Either[GsheetsError, (UpdateValuesResponse, ValueRange)]] = diff --git a/src/test/scala/gsheets4s/unit/SpreadsheetsValuesSpec.scala b/src/test/scala/gsheets4s/unit/SpreadsheetsValuesSpec.scala index c59ae19..750e570 100644 --- a/src/test/scala/gsheets4s/unit/SpreadsheetsValuesSpec.scala +++ b/src/test/scala/gsheets4s/unit/SpreadsheetsValuesSpec.scala @@ -2,15 +2,17 @@ package gsheets4s package unit import org.scalacheck._ +import eu.timepit.refined.types.string.NonEmptyString +import eu.timepit.refined.scalacheck.string._ import arbitraries._ import interpreters._ import model._ object SpreadsheetsValuesSpec extends Properties("SpreadsheetsValues unit") { - property("update/get program") = Prop.forAll { (id: String, vr: ValueRange, vio: ValueInputOption) => + property("update/get program") = Prop.forAll { (id: NonEmptyString, vr: ValueRange, vio: ValueInputOption) => new TestPrograms(TestSpreadsheetsValues).updateAndGet(id, vr, vio).map { case (uvr, vr2) => - uvr.spreadsheetId == id && + uvr.spreadsheetId == id.value && uvr.updatedRange == vr.range && vr.range == vr2.range && vr.values == vr2.values From 0bcdddc652f019b2a459bf22d797618146d407b6 Mon Sep 17 00:00:00 2001 From: "Brian P. Holt" Date: Mon, 10 Feb 2020 10:26:11 -0600 Subject: [PATCH 2/2] support spreadsheets.values.append method --- README.md | 2 +- .../gsheets4s/algebras/SpreadsheetsValues.scala | 8 ++++++++ .../interpreters/RestSpreadsheetsValues.scala | 13 +++++++++++++ src/main/scala/gsheets4s/model.scala | 9 +++++++++ .../interpreters/TestSpreadsheetsValues.scala | 14 +++++++++++++- .../scala/gsheets4s/programs/TestProgram.scala | 10 ++++++++++ .../gsheets4s/unit/SpreadsheetsValuesSpec.scala | 11 ++++++++++- 7 files changed, 64 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 6f6262d..05454f1 100644 --- a/README.md +++ b/README.md @@ -122,7 +122,7 @@ Here's the list of currently supported endpoints: - [ ] `copyTo` - [spreadsheets.values](https://developers.google.com/sheets/api/reference/rest/#rest-resource-v4spreadsheetsvalues) - - [ ] `append` + - [x] `append` - [ ] `batchClear` - [ ] `batchClearByDataFilter` - [ ] `batchGet` diff --git a/src/main/scala/gsheets4s/algebras/SpreadsheetsValues.scala b/src/main/scala/gsheets4s/algebras/SpreadsheetsValues.scala index 42d42e3..d62d459 100644 --- a/src/main/scala/gsheets4s/algebras/SpreadsheetsValues.scala +++ b/src/main/scala/gsheets4s/algebras/SpreadsheetsValues.scala @@ -13,4 +13,12 @@ trait SpreadsheetsValues[F[_]] { updates: ValueRange, valueInputOption: ValueInputOption ): F[Either[GsheetsError, UpdateValuesResponse]] + + def append(spreadsheetID: NonEmptyString, + range: A1Notation, + values: List[List[String]], + majorDimension: Dimension = Rows, + valueInputOption: ValueInputOption = Raw, + insertDataOption: InsertDataOption = InsertRows, + ): F[Either[GsheetsError, AppendValuesResponse]] } diff --git a/src/main/scala/gsheets4s/interpreters/RestSpreadsheetsValues.scala b/src/main/scala/gsheets4s/interpreters/RestSpreadsheetsValues.scala index 6f0d7b1..233ddaa 100644 --- a/src/main/scala/gsheets4s/interpreters/RestSpreadsheetsValues.scala +++ b/src/main/scala/gsheets4s/interpreters/RestSpreadsheetsValues.scala @@ -4,6 +4,7 @@ package interpreters import io.circe.generic.auto._ import io.lemonlabs.uri.typesafe.dsl._ import eu.timepit.refined.types.string.NonEmptyString +import shapeless._ import algebras.SpreadsheetsValues import http._ @@ -21,4 +22,16 @@ class RestSpreadsheetsValues[F[_]](client: HttpClient[F]) extends SpreadsheetsVa ): F[Either[GsheetsError, UpdateValuesResponse]] = client.put(spreadsheetID / "values" / range, updates, List(("valueInputOption", valueInputOption.value))) + + override def append(spreadsheetID: NonEmptyString, + range: A1Notation, + values: List[List[String]], + majorDimension: Dimension, + valueInputOption: ValueInputOption, + insertDataOption: InsertDataOption): F[Either[GsheetsError, AppendValuesResponse]] = + client.post(spreadsheetID / "values" / (range :: ":append" :: HNil), ValueRange(range, majorDimension, values), + List( + "valueInputOption" -> valueInputOption.value, + "insertDataOption" -> insertDataOption.value, + )) } diff --git a/src/main/scala/gsheets4s/model.scala b/src/main/scala/gsheets4s/model.scala index 7a7ea89..4704819 100644 --- a/src/main/scala/gsheets4s/model.scala +++ b/src/main/scala/gsheets4s/model.scala @@ -119,6 +119,10 @@ object model extends A1NotationLiteralSyntax { case object Raw extends ValueInputOption("RAW") case object UserEntered extends ValueInputOption("USER_ENTERED") + sealed abstract class InsertDataOption(val value: String) + case object Overwrite extends InsertDataOption("OVERWRITE") + case object InsertRows extends InsertDataOption("INSERT_ROWS") + final case class ValueRange( range: A1Notation, majorDimension: Dimension, @@ -133,6 +137,11 @@ object model extends A1NotationLiteralSyntax { updatedCells: Int ) + final case class AppendValuesResponse(spreadsheetId: String, + tableRange: A1Notation, + updates: UpdateValuesResponse, + ) + final case class GsheetsError( code: Int, message: String, diff --git a/src/test/scala/gsheets4s/interpreters/TestSpreadsheetsValues.scala b/src/test/scala/gsheets4s/interpreters/TestSpreadsheetsValues.scala index ab17a92..b9a68f0 100644 --- a/src/test/scala/gsheets4s/interpreters/TestSpreadsheetsValues.scala +++ b/src/test/scala/gsheets4s/interpreters/TestSpreadsheetsValues.scala @@ -10,7 +10,7 @@ import eu.timepit.refined.types.string.NonEmptyString import algebras._ import model._ -object TestSpreadsheetsValues extends SpreadsheetsValues[Id] { +class TestSpreadsheetsValues extends SpreadsheetsValues[Id] { private var data: Map[NonEmptyString, List[List[String]]] = Map.empty def get(spreadsheetID: NonEmptyString, range: A1Notation): Id[Either[GsheetsError, ValueRange]] = { @@ -29,4 +29,16 @@ object TestSpreadsheetsValues extends SpreadsheetsValues[Id] { val numCols = updates.values.headOption.foldMap(_.size) Right(UpdateValuesResponse(spreadsheetID.value, range, numRows, numCols, numRows * numCols)) } + + override def append(spreadsheetID: NonEmptyString, + range: A1Notation, + values: List[List[String]], + majorDimension: Dimension, + valueInputOption: ValueInputOption, + insertDataOption: InsertDataOption): Id[Either[GsheetsError, AppendValuesResponse]] = { + data = data + (spreadsheetID -> values) + val numRows = values.size + val numCols = values.headOption.foldMap(_.size) + Right(AppendValuesResponse(spreadsheetID.value, range, UpdateValuesResponse(spreadsheetID.value, range, numRows, numCols, numRows * numCols))) + } } diff --git a/src/test/scala/gsheets4s/programs/TestProgram.scala b/src/test/scala/gsheets4s/programs/TestProgram.scala index 64944e8..8c753c2 100644 --- a/src/test/scala/gsheets4s/programs/TestProgram.scala +++ b/src/test/scala/gsheets4s/programs/TestProgram.scala @@ -19,4 +19,14 @@ class TestPrograms[F[_]: Monad](alg: SpreadsheetsValues[F]) { updateValuesResponse <- EitherT(update(spreadsheetId, vr.range, vr, vio)) valueRange <- EitherT(get(spreadsheetId, vr.range)) } yield (updateValuesResponse, valueRange)).value + + def appendAndGet( + spreadsheetId: NonEmptyString, + vr: ValueRange, + vio: ValueInputOption + ): F[Either[GsheetsError, (AppendValuesResponse, ValueRange)]] = + (for { + updateValuesResponse <- EitherT(append(spreadsheetId, vr.range, vr.values, vr.majorDimension, vio)) + valueRange <- EitherT(get(spreadsheetId, vr.range)) + } yield (updateValuesResponse, valueRange)).value } diff --git a/src/test/scala/gsheets4s/unit/SpreadsheetsValuesSpec.scala b/src/test/scala/gsheets4s/unit/SpreadsheetsValuesSpec.scala index 750e570..383d446 100644 --- a/src/test/scala/gsheets4s/unit/SpreadsheetsValuesSpec.scala +++ b/src/test/scala/gsheets4s/unit/SpreadsheetsValuesSpec.scala @@ -11,11 +11,20 @@ import model._ object SpreadsheetsValuesSpec extends Properties("SpreadsheetsValues unit") { property("update/get program") = Prop.forAll { (id: NonEmptyString, vr: ValueRange, vio: ValueInputOption) => - new TestPrograms(TestSpreadsheetsValues).updateAndGet(id, vr, vio).map { case (uvr, vr2) => + new TestPrograms(new TestSpreadsheetsValues).updateAndGet(id, vr, vio).map { case (uvr, vr2) => uvr.spreadsheetId == id.value && uvr.updatedRange == vr.range && vr.range == vr2.range && vr.values == vr2.values }.getOrElse(false) } + + property("append/get program") = Prop.forAll { (id: NonEmptyString, vr: ValueRange, vio: ValueInputOption) => + new TestPrograms(new TestSpreadsheetsValues).appendAndGet(id, vr, vio).map { case (avr, vr2) => + avr.spreadsheetId == id.value && + avr.updates.updatedRange == vr.range && + vr.range == vr2.range && + vr.values == vr2.values + }.getOrElse(false) + } }