Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down
14 changes: 12 additions & 2 deletions src/main/scala/gsheets4s/algebras/SpreadsheetsValues.scala
Original file line number Diff line number Diff line change
@@ -1,14 +1,24 @@
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
): 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]]
}
18 changes: 16 additions & 2 deletions src/main/scala/gsheets4s/interpreters/RestSpreadsheetsValues.scala
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,35 @@ 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._
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
): 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,
))
}
12 changes: 12 additions & 0 deletions src/main/scala/gsheets4s/model.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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._
Expand Down Expand Up @@ -118,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,
Expand All @@ -132,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,
Expand All @@ -149,6 +159,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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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._
Expand All @@ -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")))
Expand All @@ -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)
Expand Down Expand Up @@ -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)
Expand Down
27 changes: 21 additions & 6 deletions src/test/scala/gsheets4s/interpreters/TestSpreadsheetsValues.scala
Original file line number Diff line number Diff line change
Expand Up @@ -3,27 +3,42 @@ 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
class TestSpreadsheetsValues extends SpreadsheetsValues[Id] {
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
): Id[Either[GsheetsError, UpdateValuesResponse]] = {
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))
}

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)))
}
}
13 changes: 12 additions & 1 deletion src/test/scala/gsheets4s/programs/TestProgram.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package gsheets4s

import cats.Monad
import cats.data.EitherT
import eu.timepit.refined.types.string.NonEmptyString

import algebras._
import model._
Expand All @@ -10,12 +11,22 @@ class TestPrograms[F[_]: Monad](alg: SpreadsheetsValues[F]) {
import alg._

def updateAndGet(
spreadsheetId: String,
spreadsheetId: NonEmptyString,
vr: ValueRange,
vio: ValueInputOption
): F[Either[GsheetsError, (UpdateValuesResponse, ValueRange)]] =
(for {
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
}
17 changes: 14 additions & 3 deletions src/test/scala/gsheets4s/unit/SpreadsheetsValuesSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,29 @@ 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) =>
new TestPrograms(TestSpreadsheetsValues).updateAndGet(id, vr, vio).map { case (uvr, vr2) =>
uvr.spreadsheetId == id &&
property("update/get program") = Prop.forAll { (id: NonEmptyString, vr: ValueRange, vio: ValueInputOption) =>
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)
}
}