Ops as Google Sheets API requests: the body of one spreadsheets.batchUpdate.
Google addresses sheets by a number, not a title, so encoding needs the ids
the spreadsheet already has: %{"Costs" => 0}, which a backend reads off the
metadata before it plans. A Sheetshow.Op.AddSheet picks an id for the sheet
it creates, so the ops after it in the same plan can write to a tab that does
not exist yet.
iex> plan = Sheetshow.row(["Rent", 1000], sheet: "Costs") |> Sheetshow.plan!()
iex> {:ok, body} = Sheetshow.Google.encode(plan, %{"Costs" => 0})
iex> [%{updateCells: update}] = body.requests
iex> update.start
%{sheetId: 0, rowIndex: 0, columnIndex: 0}A batch is all-or-nothing at Google, so an op naming a sheet that is not there
is an error here rather than a half-written spreadsheet, which is the answer
Sheetshow.Memory gives too.
Writing a cell replaces it: the field mask covers the value and the whole format, so a cell written without a style comes out with none, whatever was there before. That is what makes cells values rather than patches.
Summary
Functions
Cells from a spreadsheets.get answer.
Rows of values from a spreadsheets.values.get answer.
The scopes Sheetshow asks Google for when you name none, whichever kind of credential is asking.
A plan as a spreadsheets.batchUpdate body, given the sheet ids the
spreadsheet already has.
Same as encode/2, raising on failure.
The id a new sheet will claim, given the ids already in use.
Types
Functions
@spec decode(map()) :: [Sheetshow.Cell.t()]
Cells from a spreadsheets.get answer.
A cell's value is what was entered: a formula reads back as a formula, so
cells you read are cells you can write again. What Sheets worked it out to
goes in meta: :formatted is the text it displayed, :effective the value
behind that text, which is where a Sheetshow.CellError turns up. Empty
cells are not cells and are not returned.
iex> answer = %{"sheets" => [%{
...> "properties" => %{"title" => "Costs"},
...> "data" => [%{"rowData" => [%{"values" => [
...> %{"userEnteredValue" => %{"stringValue" => "Rent"}, "formattedValue" => "Rent"}
...> ]}]}]
...> }]}
iex> [cell] = Sheetshow.Google.decode(answer)
iex> {Sheetshow.Coord.to_a1(cell.coord), cell.value, cell.meta}
{"Costs!A1", "Rent", %{formatted: "Rent"}}Sheets keeps a date as a number and only its number format says so, so that
is what the decoding leans on: a serial number formatted as a date comes back
a Date, and one formatted as anything else stays a number.
Rows of values from a spreadsheets.values.get answer.
Google leaves trailing empty cells off the end of a row and trailing empty
rows off the end of the answer, so the rows come back ragged; they are padded
here to the width of the widest, and an empty cell is nil, which is the
shape Sheetshow.to_rows/1 gives.
iex> Sheetshow.Google.decode_values(%{"values" => [["Rent", 1000], ["Food"]]})
[["Rent", 1000], ["Food", nil]]What is lost against decode/1 is what a cell is: this endpoint renders a
formula's result and a formula's error as plain values, so #DIV/0! arrives
as the string "#DIV/0!" and is indistinguishable from someone typing it.
What is gained is the computed value and about a twelfth of the bytes,
which is the trade a database read wants and a cell read does not.
iex> Sheetshow.Google.decode_values(%{})
[]
@spec default_scopes() :: [String.t()]
The scopes Sheetshow asks Google for when you name none, whichever kind of credential is asking.
iex> Sheetshow.Google.default_scopes()
["https://www.googleapis.com/auth/spreadsheets"]drive.file is narrower but reaches only files the account itself created,
so it cannot see a spreadsheet someone shared with the account.
@spec encode(Sheetshow.Op.plan(), sheet_ids()) :: {:ok, map()} | {:error, Sheetshow.Error.t()}
A plan as a spreadsheets.batchUpdate body, given the sheet ids the
spreadsheet already has.
iex> {:error, %Sheetshow.Error{reason: :unknown_sheet}} =
...> Sheetshow.Google.encode(Sheetshow.plan!([Sheetshow.Cell.new("Costs!A1", 1)]), %{})
@spec encode!(Sheetshow.Op.plan(), sheet_ids()) :: map()
Same as encode/2, raising on failure.
@spec sheet_id(String.t(), sheet_ids()) :: non_neg_integer()
The id a new sheet will claim, given the ids already in use.
It comes from the title, so it is the same every time, which keeps a plan readable in a test, and two writers adding differently named tabs at the same time ask for different ids. A title that is already taken is the one real conflict, and Google refuses that on its own; picking, say, the lowest free id instead would invent a second conflict between writers who were not in each other's way at all.
iex> id = Sheetshow.Google.sheet_id("log")
iex> Sheetshow.Google.sheet_id("log") == id
true
iex> Sheetshow.Google.sheet_id("log", %{"Costs" => id}) == id + 1
true