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
1 change: 1 addition & 0 deletions internal/cmd/sheets.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ type SheetsCmd struct {
Get SheetsGetCmd `cmd:"" name:"get" help:"Get values from a range"`
Update SheetsUpdateCmd `cmd:"" name:"update" help:"Update values in a range"`
Append SheetsAppendCmd `cmd:"" name:"append" help:"Append values to a range"`
Insert SheetsInsertCmd `cmd:"" name:"insert" help:"Insert empty rows or columns into a sheet"`
Clear SheetsClearCmd `cmd:"" name:"clear" help:"Clear values in a range"`
Format SheetsFormatCmd `cmd:"" name:"format" help:"Apply cell formatting to a range"`
Metadata SheetsMetadataCmd `cmd:"" name:"metadata" help:"Get spreadsheet metadata"`
Expand Down
123 changes: 123 additions & 0 deletions internal/cmd/sheets_insert.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
package cmd

import (
"context"
"fmt"
"os"
"strings"

"google.golang.org/api/sheets/v4"

"github.com/steipete/gogcli/internal/outfmt"
"github.com/steipete/gogcli/internal/ui"
)

type SheetsInsertCmd struct {
SpreadsheetID string `arg:"" name:"spreadsheetId" help:"Spreadsheet ID"`
Sheet string `arg:"" name:"sheet" help:"Sheet name (eg. Sheet1)"`
Dimension string `arg:"" name:"dimension" help:"Dimension to insert: rows or cols"`
Start int64 `arg:"" name:"start" help:"Position before which to insert (1-based; for cols 1=A, 2=B)"`
Count int64 `name:"count" help:"Number of rows/columns to insert" default:"1"`
After bool `name:"after" help:"Insert after the position instead of before"`
}

func (c *SheetsInsertCmd) Run(ctx context.Context, flags *RootFlags) error {
u := ui.FromContext(ctx)
account, err := requireAccount(flags)
if err != nil {
return err
}

spreadsheetID := strings.TrimSpace(c.SpreadsheetID)
sheetName := strings.TrimSpace(c.Sheet)
if spreadsheetID == "" {
return usage("empty spreadsheetId")
}
if sheetName == "" {
return usage("empty sheet name")
}

dim := strings.ToLower(strings.TrimSpace(c.Dimension))
var apiDimension, dimLabel string
switch dim {
case "rows", "row":
apiDimension = "ROWS"
dimLabel = "row"
case "cols", "col", "columns", "column":
apiDimension = "COLUMNS"
dimLabel = "column"
default:
return fmt.Errorf("dimension must be rows or cols, got %q", c.Dimension)
}

if c.Start < 1 {
return fmt.Errorf("start must be >= 1")
}
if c.Count < 1 {
return fmt.Errorf("count must be >= 1")
}

svc, err := newSheetsService(ctx, account)
if err != nil {
return err
}

sheetIDs, err := fetchSheetIDMap(ctx, svc, spreadsheetID)
if err != nil {
return err
}
sheetID, ok := sheetIDs[sheetName]
if !ok {
return fmt.Errorf("unknown sheet %q", sheetName)
}

// Convert 1-based position to 0-based index for the API.
startIndex := c.Start - 1
if c.After {
startIndex = c.Start
}
endIndex := startIndex + c.Count

req := &sheets.BatchUpdateSpreadsheetRequest{
Requests: []*sheets.Request{
{
InsertDimension: &sheets.InsertDimensionRequest{
Range: &sheets.DimensionRange{
SheetId: sheetID,
Dimension: apiDimension,
StartIndex: startIndex,
EndIndex: endIndex,
},
InheritFromBefore: c.After,
},
},
},
}

if _, err := svc.Spreadsheets.BatchUpdate(spreadsheetID, req).Do(); err != nil {
return err
}

if outfmt.IsJSON(ctx) {
return outfmt.WriteJSON(os.Stdout, map[string]any{
"sheet": sheetName,
"dimension": apiDimension,
"start": c.Start,
"count": c.Count,
"after": c.After,
"startIndex": startIndex,
"endIndex": endIndex,
})
}

position := "before"
if c.After {
position = "after"
}
plural := dimLabel + "s"
if c.Count == 1 {
plural = dimLabel
}
u.Out().Printf("Inserted %d %s %s %s %d in %q", c.Count, plural, position, dimLabel, c.Start, sheetName)
return nil
}
163 changes: 163 additions & 0 deletions internal/cmd/sheets_insert_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
package cmd

import (
"context"
"encoding/json"
"io"
"net/http"
"net/http/httptest"
"strings"
"testing"

"google.golang.org/api/option"
"google.golang.org/api/sheets/v4"

"github.com/steipete/gogcli/internal/ui"
)

func TestSheetsInsertCmd(t *testing.T) {
origNew := newSheetsService
t.Cleanup(func() { newSheetsService = origNew })

var gotInsert *sheets.InsertDimensionRequest

srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
path := strings.TrimPrefix(r.URL.Path, "/sheets/v4")
path = strings.TrimPrefix(path, "/v4")
switch {
case strings.HasPrefix(path, "/spreadsheets/s1") && r.Method == http.MethodGet:
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(map[string]any{
"spreadsheetId": "s1",
"sheets": []map[string]any{
{"properties": map[string]any{"sheetId": 7, "title": "Data"}},
},
})
return
case strings.Contains(path, "/spreadsheets/s1:batchUpdate") && r.Method == http.MethodPost:
var req sheets.BatchUpdateSpreadsheetRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
t.Fatalf("decode batchUpdate: %v", err)
}
if len(req.Requests) != 1 || req.Requests[0].InsertDimension == nil {
t.Fatalf("expected insertDimension request, got %#v", req.Requests)
}
gotInsert = req.Requests[0].InsertDimension
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(map[string]any{})
return
default:
http.NotFound(w, r)
return
}
}))
defer srv.Close()

svc, err := sheets.NewService(context.Background(),
option.WithoutAuthentication(),
option.WithHTTPClient(srv.Client()),
option.WithEndpoint(srv.URL+"/"),
)
if err != nil {
t.Fatalf("NewService: %v", err)
}
newSheetsService = func(context.Context, string) (*sheets.Service, error) { return svc, nil }

flags := &RootFlags{Account: "a@b.com"}
u, uiErr := ui.New(ui.Options{Stdout: io.Discard, Stderr: io.Discard, Color: "never"})
if uiErr != nil {
t.Fatalf("ui.New: %v", uiErr)
}
ctx := ui.WithUI(context.Background(), u)

t.Run("insert rows before", func(t *testing.T) {
gotInsert = nil
cmd := &SheetsInsertCmd{}
if err := runKong(t, cmd, []string{
"s1", "Data", "rows", "2", "--count", "3",
}, ctx, flags); err != nil {
t.Fatalf("insert rows: %v", err)
}

if gotInsert == nil {
t.Fatal("expected insertDimension request")
}
if gotInsert.Range.SheetId != 7 {
t.Fatalf("unexpected sheet id: %d", gotInsert.Range.SheetId)
}
if gotInsert.Range.Dimension != "ROWS" {
t.Fatalf("unexpected dimension: %s", gotInsert.Range.Dimension)
}
// startRow=2 → startIndex=1, endIndex=1+3=4
if gotInsert.Range.StartIndex != 1 {
t.Fatalf("unexpected startIndex: %d, want 1", gotInsert.Range.StartIndex)
}
if gotInsert.Range.EndIndex != 4 {
t.Fatalf("unexpected endIndex: %d, want 4", gotInsert.Range.EndIndex)
}
if gotInsert.InheritFromBefore {
t.Fatal("expected inheritFromBefore=false")
}
})

t.Run("insert rows after", func(t *testing.T) {
gotInsert = nil
cmd := &SheetsInsertCmd{}
if err := runKong(t, cmd, []string{
"s1", "Data", "rows", "2", "--count", "1", "--after",
}, ctx, flags); err != nil {
t.Fatalf("insert rows: %v", err)
}

if gotInsert == nil {
t.Fatal("expected insertDimension request")
}
// startRow=2 --after → startIndex=2, endIndex=3
if gotInsert.Range.StartIndex != 2 {
t.Fatalf("unexpected startIndex: %d, want 2", gotInsert.Range.StartIndex)
}
if gotInsert.Range.EndIndex != 3 {
t.Fatalf("unexpected endIndex: %d, want 3", gotInsert.Range.EndIndex)
}
if !gotInsert.InheritFromBefore {
t.Fatal("expected inheritFromBefore=true")
}
})

t.Run("insert cols before", func(t *testing.T) {
gotInsert = nil
cmd := &SheetsInsertCmd{}
if err := runKong(t, cmd, []string{
"s1", "Data", "cols", "3", "--count", "2",
}, ctx, flags); err != nil {
t.Fatalf("insert cols: %v", err)
}

if gotInsert == nil {
t.Fatal("expected insertDimension request")
}
if gotInsert.Range.Dimension != "COLUMNS" {
t.Fatalf("unexpected dimension: %s", gotInsert.Range.Dimension)
}
// startCol=3 → startIndex=2, endIndex=2+2=4
if gotInsert.Range.StartIndex != 2 {
t.Fatalf("unexpected startIndex: %d, want 2", gotInsert.Range.StartIndex)
}
if gotInsert.Range.EndIndex != 4 {
t.Fatalf("unexpected endIndex: %d, want 4", gotInsert.Range.EndIndex)
}
})

t.Run("invalid dimension", func(t *testing.T) {
cmd := &SheetsInsertCmd{}
err := runKong(t, cmd, []string{
"s1", "Data", "sheets", "1",
}, ctx, flags)
if err == nil {
t.Fatal("expected error for invalid dimension")
}
if !strings.Contains(err.Error(), "dimension must be rows or cols") {
t.Fatalf("unexpected error: %v", err)
}
})
}