refactor(classroom): share list and service helpers

This commit is contained in:
Peter Steinberger 2026-03-09 03:50:11 +00:00
parent d4ffe4c0e0
commit 7b669f85ae
6 changed files with 151 additions and 292 deletions

View File

@ -3,6 +3,7 @@ package cmd
import (
"context"
"fmt"
"io"
"os"
"strings"
@ -32,17 +33,12 @@ type ClassroomAnnouncementsListCmd struct {
}
func (c *ClassroomAnnouncementsListCmd) Run(ctx context.Context, flags *RootFlags) error {
u := ui.FromContext(ctx)
account, err := requireAccount(flags)
if err != nil {
return err
}
courseID := strings.TrimSpace(c.CourseID)
if courseID == "" {
return usage("empty courseId")
}
svc, err := newClassroomService(ctx, account)
_, svc, err := requireClassroomService(ctx, flags)
if err != nil {
return wrapClassroomError(err)
}
@ -69,57 +65,26 @@ func (c *ClassroomAnnouncementsListCmd) Run(ctx context.Context, flags *RootFlag
return resp.Announcements, resp.NextPageToken, nil
}
var announcements []*classroom.Announcement
nextPageToken := ""
if c.All {
all, err := collectAllPages(c.Page, fetch)
if err != nil {
return err
}
announcements = all
} else {
var err error
announcements, nextPageToken, err = fetch(c.Page)
if err != nil {
return err
}
announcements, nextPageToken, err := fetchClassroomPagedList(c.All, c.Page, fetch)
if err != nil {
return err
}
if outfmt.IsJSON(ctx) {
if err := outfmt.WriteJSON(ctx, os.Stdout, map[string]any{
"announcements": announcements,
"nextPageToken": nextPageToken,
}); err != nil {
return err
return writeClassroomPagedList(ctx, "announcements", announcements, nextPageToken, "No announcements", c.FailEmpty, false, func(w io.Writer) {
fmt.Fprintln(w, "ID\tSTATE\tTEXT\tSCHEDULED\tUPDATED")
for _, ann := range announcements {
if ann == nil {
continue
}
fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\n",
sanitizeTab(ann.Id),
sanitizeTab(ann.State),
sanitizeTab(truncateClassroomText(ann.Text, 50)),
sanitizeTab(ann.ScheduledTime),
sanitizeTab(ann.UpdateTime),
)
}
if len(announcements) == 0 {
return failEmptyExit(c.FailEmpty)
}
return nil
}
if len(announcements) == 0 {
u.Err().Println("No announcements")
return failEmptyExit(c.FailEmpty)
}
w, flush := tableWriter(ctx)
defer flush()
fmt.Fprintln(w, "ID\tSTATE\tTEXT\tSCHEDULED\tUPDATED")
for _, ann := range announcements {
if ann == nil {
continue
}
fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\n",
sanitizeTab(ann.Id),
sanitizeTab(ann.State),
sanitizeTab(truncateClassroomText(ann.Text, 50)),
sanitizeTab(ann.ScheduledTime),
sanitizeTab(ann.UpdateTime),
)
}
printNextPageHint(u, nextPageToken)
return nil
})
}
type ClassroomAnnouncementsGetCmd struct {
@ -129,10 +94,6 @@ type ClassroomAnnouncementsGetCmd struct {
func (c *ClassroomAnnouncementsGetCmd) Run(ctx context.Context, flags *RootFlags) error {
u := ui.FromContext(ctx)
account, err := requireAccount(flags)
if err != nil {
return err
}
courseID := strings.TrimSpace(c.CourseID)
announcementID := strings.TrimSpace(c.AnnouncementID)
if courseID == "" {
@ -142,7 +103,7 @@ func (c *ClassroomAnnouncementsGetCmd) Run(ctx context.Context, flags *RootFlags
return usage("empty announcementId")
}
svc, err := newClassroomService(ctx, account)
_, svc, err := requireClassroomService(ctx, flags)
if err != nil {
return wrapClassroomError(err)
}
@ -202,12 +163,7 @@ func (c *ClassroomAnnouncementsCreateCmd) Run(ctx context.Context, flags *RootFl
return err
}
account, err := requireAccount(flags)
if err != nil {
return err
}
svc, err := newClassroomService(ctx, account)
_, svc, err := requireClassroomService(ctx, flags)
if err != nil {
return wrapClassroomError(err)
}
@ -272,12 +228,7 @@ func (c *ClassroomAnnouncementsUpdateCmd) Run(ctx context.Context, flags *RootFl
return err
}
account, err := requireAccount(flags)
if err != nil {
return err
}
svc, err := newClassroomService(ctx, account)
_, svc, err := requireClassroomService(ctx, flags)
if err != nil {
return wrapClassroomError(err)
}
@ -315,12 +266,7 @@ func (c *ClassroomAnnouncementsDeleteCmd) Run(ctx context.Context, flags *RootFl
return err
}
account, err := requireAccount(flags)
if err != nil {
return err
}
svc, err := newClassroomService(ctx, account)
_, svc, err := requireClassroomService(ctx, flags)
if err != nil {
return wrapClassroomError(err)
}
@ -375,12 +321,7 @@ func (c *ClassroomAnnouncementsAssigneesCmd) Run(ctx context.Context, flags *Roo
return dryRunErr
}
account, err := requireAccount(flags)
if err != nil {
return err
}
svc, err := newClassroomService(ctx, account)
_, svc, err := requireClassroomService(ctx, flags)
if err != nil {
return wrapClassroomError(err)
}

View File

@ -3,6 +3,7 @@ package cmd
import (
"context"
"fmt"
"io"
"os"
"strings"
@ -34,17 +35,12 @@ type ClassroomCourseworkListCmd struct {
}
func (c *ClassroomCourseworkListCmd) Run(ctx context.Context, flags *RootFlags) error {
u := ui.FromContext(ctx)
account, err := requireAccount(flags)
if err != nil {
return err
}
courseID := strings.TrimSpace(c.CourseID)
if courseID == "" {
return usage("empty courseId")
}
svc, err := newClassroomService(ctx, account)
_, svc, err := requireClassroomService(ctx, flags)
if err != nil {
return wrapClassroomError(err)
}
@ -111,43 +107,22 @@ func (c *ClassroomCourseworkListCmd) Run(ctx context.Context, flags *RootFlags)
}
}
if outfmt.IsJSON(ctx) {
if err := outfmt.WriteJSON(ctx, os.Stdout, map[string]any{
"coursework": coursework,
"nextPageToken": nextPageToken,
}); err != nil {
return err
return writeClassroomPagedList(ctx, "coursework", coursework, nextPageToken, "No coursework", c.FailEmpty, true, func(w io.Writer) {
fmt.Fprintln(w, "ID\tTITLE\tSTATE\tDUE\tTYPE\tMAX_POINTS")
for _, work := range coursework {
if work == nil {
continue
}
fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\t%s\n",
sanitizeTab(work.Id),
sanitizeTab(work.Title),
sanitizeTab(work.State),
sanitizeTab(formatClassroomDue(work.DueDate, work.DueTime)),
sanitizeTab(work.WorkType),
formatFloatValue(work.MaxPoints),
)
}
if len(coursework) == 0 {
return failEmptyExit(c.FailEmpty)
}
return nil
}
if len(coursework) == 0 {
u.Err().Println("No coursework")
printNextPageHint(u, nextPageToken)
return failEmptyExit(c.FailEmpty)
}
w, flush := tableWriter(ctx)
defer flush()
fmt.Fprintln(w, "ID\tTITLE\tSTATE\tDUE\tTYPE\tMAX_POINTS")
for _, work := range coursework {
if work == nil {
continue
}
fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\t%s\n",
sanitizeTab(work.Id),
sanitizeTab(work.Title),
sanitizeTab(work.State),
sanitizeTab(formatClassroomDue(work.DueDate, work.DueTime)),
sanitizeTab(work.WorkType),
formatFloatValue(work.MaxPoints),
)
}
printNextPageHint(u, nextPageToken)
return nil
})
}
type ClassroomCourseworkGetCmd struct {
@ -157,10 +132,6 @@ type ClassroomCourseworkGetCmd struct {
func (c *ClassroomCourseworkGetCmd) Run(ctx context.Context, flags *RootFlags) error {
u := ui.FromContext(ctx)
account, err := requireAccount(flags)
if err != nil {
return err
}
courseID := strings.TrimSpace(c.CourseID)
courseworkID := strings.TrimSpace(c.CourseworkID)
if courseID == "" {
@ -170,7 +141,7 @@ func (c *ClassroomCourseworkGetCmd) Run(ctx context.Context, flags *RootFlags) e
return usage("empty courseworkId")
}
svc, err := newClassroomService(ctx, account)
_, svc, err := requireClassroomService(ctx, flags)
if err != nil {
return wrapClassroomError(err)
}
@ -290,12 +261,7 @@ func (c *ClassroomCourseworkCreateCmd) Run(ctx context.Context, flags *RootFlags
return dryRunErr
}
account, err := requireAccount(flags)
if err != nil {
return err
}
svc, err := newClassroomService(ctx, account)
_, svc, err := requireClassroomService(ctx, flags)
if err != nil {
return wrapClassroomError(err)
}
@ -415,12 +381,7 @@ func (c *ClassroomCourseworkUpdateCmd) Run(ctx context.Context, flags *RootFlags
return dryRunErr
}
account, err := requireAccount(flags)
if err != nil {
return err
}
svc, err := newClassroomService(ctx, account)
_, svc, err := requireClassroomService(ctx, flags)
if err != nil {
return wrapClassroomError(err)
}
@ -459,12 +420,7 @@ func (c *ClassroomCourseworkDeleteCmd) Run(ctx context.Context, flags *RootFlags
return err
}
account, err := requireAccount(flags)
if err != nil {
return err
}
svc, err := newClassroomService(ctx, account)
_, svc, err := requireClassroomService(ctx, flags)
if err != nil {
return wrapClassroomError(err)
}
@ -519,12 +475,7 @@ func (c *ClassroomCourseworkAssigneesCmd) Run(ctx context.Context, flags *RootFl
return dryRunErr
}
account, err := requireAccount(flags)
if err != nil {
return err
}
svc, err := newClassroomService(ctx, account)
_, svc, err := requireClassroomService(ctx, flags)
if err != nil {
return wrapClassroomError(err)
}

View File

@ -0,0 +1,60 @@
package cmd
import (
"context"
"io"
"os"
"github.com/steipete/gogcli/internal/outfmt"
"github.com/steipete/gogcli/internal/ui"
)
func fetchClassroomPagedList[T any](all bool, page string, fetch func(string) ([]*T, string, error)) ([]*T, string, error) {
if all {
items, err := collectAllPages(page, fetch)
if err != nil {
return nil, "", err
}
return items, "", nil
}
return fetch(page)
}
func writeClassroomPagedList[T any](
ctx context.Context,
jsonKey string,
items []*T,
nextPageToken string,
emptyMessage string,
failEmpty bool,
hintOnEmpty bool,
printTable func(io.Writer),
) error {
if outfmt.IsJSON(ctx) {
if err := outfmt.WriteJSON(ctx, os.Stdout, map[string]any{
jsonKey: items,
"nextPageToken": nextPageToken,
}); err != nil {
return err
}
if len(items) == 0 {
return failEmptyExit(failEmpty)
}
return nil
}
u := ui.FromContext(ctx)
if len(items) == 0 {
u.Err().Println(emptyMessage)
if hintOnEmpty {
printNextPageHint(u, nextPageToken)
}
return failEmptyExit(failEmpty)
}
w, flush := tableWriter(ctx)
defer flush()
printTable(w)
printNextPageHint(u, nextPageToken)
return nil
}

View File

@ -3,6 +3,7 @@ package cmd
import (
"context"
"fmt"
"io"
"os"
"strings"
@ -33,17 +34,12 @@ type ClassroomMaterialsListCmd struct {
}
func (c *ClassroomMaterialsListCmd) Run(ctx context.Context, flags *RootFlags) error {
u := ui.FromContext(ctx)
account, err := requireAccount(flags)
if err != nil {
return err
}
courseID := strings.TrimSpace(c.CourseID)
if courseID == "" {
return usage("empty courseId")
}
svc, err := newClassroomService(ctx, account)
_, svc, err := requireClassroomService(ctx, flags)
if err != nil {
return wrapClassroomError(err)
}
@ -110,41 +106,20 @@ func (c *ClassroomMaterialsListCmd) Run(ctx context.Context, flags *RootFlags) e
}
}
if outfmt.IsJSON(ctx) {
if err := outfmt.WriteJSON(ctx, os.Stdout, map[string]any{
"materials": materials,
"nextPageToken": nextPageToken,
}); err != nil {
return err
return writeClassroomPagedList(ctx, "materials", materials, nextPageToken, "No materials", c.FailEmpty, true, func(w io.Writer) {
fmt.Fprintln(w, "ID\tTITLE\tSTATE\tUPDATED")
for _, material := range materials {
if material == nil {
continue
}
fmt.Fprintf(w, "%s\t%s\t%s\t%s\n",
sanitizeTab(material.Id),
sanitizeTab(material.Title),
sanitizeTab(material.State),
sanitizeTab(material.UpdateTime),
)
}
if len(materials) == 0 {
return failEmptyExit(c.FailEmpty)
}
return nil
}
if len(materials) == 0 {
u.Err().Println("No materials")
printNextPageHint(u, nextPageToken)
return failEmptyExit(c.FailEmpty)
}
w, flush := tableWriter(ctx)
defer flush()
fmt.Fprintln(w, "ID\tTITLE\tSTATE\tUPDATED")
for _, material := range materials {
if material == nil {
continue
}
fmt.Fprintf(w, "%s\t%s\t%s\t%s\n",
sanitizeTab(material.Id),
sanitizeTab(material.Title),
sanitizeTab(material.State),
sanitizeTab(material.UpdateTime),
)
}
printNextPageHint(u, nextPageToken)
return nil
})
}
type ClassroomMaterialsGetCmd struct {
@ -154,10 +129,6 @@ type ClassroomMaterialsGetCmd struct {
func (c *ClassroomMaterialsGetCmd) Run(ctx context.Context, flags *RootFlags) error {
u := ui.FromContext(ctx)
account, err := requireAccount(flags)
if err != nil {
return err
}
courseID := strings.TrimSpace(c.CourseID)
materialID := strings.TrimSpace(c.MaterialID)
if courseID == "" {
@ -167,7 +138,7 @@ func (c *ClassroomMaterialsGetCmd) Run(ctx context.Context, flags *RootFlags) er
return usage("empty materialId")
}
svc, err := newClassroomService(ctx, account)
_, svc, err := requireClassroomService(ctx, flags)
if err != nil {
return wrapClassroomError(err)
}
@ -236,12 +207,7 @@ func (c *ClassroomMaterialsCreateCmd) Run(ctx context.Context, flags *RootFlags)
return err
}
account, err := requireAccount(flags)
if err != nil {
return err
}
svc, err := newClassroomService(ctx, account)
_, svc, err := requireClassroomService(ctx, flags)
if err != nil {
return wrapClassroomError(err)
}
@ -317,12 +283,7 @@ func (c *ClassroomMaterialsUpdateCmd) Run(ctx context.Context, flags *RootFlags)
return err
}
account, err := requireAccount(flags)
if err != nil {
return err
}
svc, err := newClassroomService(ctx, account)
_, svc, err := requireClassroomService(ctx, flags)
if err != nil {
return wrapClassroomError(err)
}
@ -361,12 +322,7 @@ func (c *ClassroomMaterialsDeleteCmd) Run(ctx context.Context, flags *RootFlags)
return err
}
account, err := requireAccount(flags)
if err != nil {
return err
}
svc, err := newClassroomService(ctx, account)
_, svc, err := requireClassroomService(ctx, flags)
if err != nil {
return wrapClassroomError(err)
}

View File

@ -3,6 +3,7 @@ package cmd
import (
"context"
"fmt"
"io"
"os"
"strings"
@ -29,17 +30,12 @@ type ClassroomTopicsListCmd struct {
}
func (c *ClassroomTopicsListCmd) Run(ctx context.Context, flags *RootFlags) error {
u := ui.FromContext(ctx)
account, err := requireAccount(flags)
if err != nil {
return err
}
courseID := strings.TrimSpace(c.CourseID)
if courseID == "" {
return usage("empty courseId")
}
svc, err := newClassroomService(ctx, account)
_, svc, err := requireClassroomService(ctx, flags)
if err != nil {
return wrapClassroomError(err)
}
@ -56,55 +52,24 @@ func (c *ClassroomTopicsListCmd) Run(ctx context.Context, flags *RootFlags) erro
return resp.Topic, resp.NextPageToken, nil
}
var topics []*classroom.Topic
nextPageToken := ""
if c.All {
all, err := collectAllPages(c.Page, fetch)
if err != nil {
return err
}
topics = all
} else {
var err error
topics, nextPageToken, err = fetch(c.Page)
if err != nil {
return err
}
topics, nextPageToken, err := fetchClassroomPagedList(c.All, c.Page, fetch)
if err != nil {
return err
}
if outfmt.IsJSON(ctx) {
if err := outfmt.WriteJSON(ctx, os.Stdout, map[string]any{
"topics": topics,
"nextPageToken": nextPageToken,
}); err != nil {
return err
return writeClassroomPagedList(ctx, "topics", topics, nextPageToken, "No topics", c.FailEmpty, false, func(w io.Writer) {
fmt.Fprintln(w, "TOPIC_ID\tNAME\tUPDATED")
for _, topic := range topics {
if topic == nil {
continue
}
fmt.Fprintf(w, "%s\t%s\t%s\n",
sanitizeTab(topic.TopicId),
sanitizeTab(topic.Name),
sanitizeTab(topic.UpdateTime),
)
}
if len(topics) == 0 {
return failEmptyExit(c.FailEmpty)
}
return nil
}
if len(topics) == 0 {
u.Err().Println("No topics")
return failEmptyExit(c.FailEmpty)
}
w, flush := tableWriter(ctx)
defer flush()
fmt.Fprintln(w, "TOPIC_ID\tNAME\tUPDATED")
for _, topic := range topics {
if topic == nil {
continue
}
fmt.Fprintf(w, "%s\t%s\t%s\n",
sanitizeTab(topic.TopicId),
sanitizeTab(topic.Name),
sanitizeTab(topic.UpdateTime),
)
}
printNextPageHint(u, nextPageToken)
return nil
})
}
type ClassroomTopicsGetCmd struct {
@ -114,10 +79,6 @@ type ClassroomTopicsGetCmd struct {
func (c *ClassroomTopicsGetCmd) Run(ctx context.Context, flags *RootFlags) error {
u := ui.FromContext(ctx)
account, err := requireAccount(flags)
if err != nil {
return err
}
courseID := strings.TrimSpace(c.CourseID)
topicID := strings.TrimSpace(c.TopicID)
if courseID == "" {
@ -127,7 +88,7 @@ func (c *ClassroomTopicsGetCmd) Run(ctx context.Context, flags *RootFlags) error
return usage("empty topicId")
}
svc, err := newClassroomService(ctx, account)
_, svc, err := requireClassroomService(ctx, flags)
if err != nil {
return wrapClassroomError(err)
}
@ -173,12 +134,7 @@ func (c *ClassroomTopicsCreateCmd) Run(ctx context.Context, flags *RootFlags) er
return err
}
account, err := requireAccount(flags)
if err != nil {
return err
}
svc, err := newClassroomService(ctx, account)
_, svc, err := requireClassroomService(ctx, flags)
if err != nil {
return wrapClassroomError(err)
}
@ -227,12 +183,7 @@ func (c *ClassroomTopicsUpdateCmd) Run(ctx context.Context, flags *RootFlags) er
return err
}
account, err := requireAccount(flags)
if err != nil {
return err
}
svc, err := newClassroomService(ctx, account)
_, svc, err := requireClassroomService(ctx, flags)
if err != nil {
return wrapClassroomError(err)
}
@ -270,12 +221,7 @@ func (c *ClassroomTopicsDeleteCmd) Run(ctx context.Context, flags *RootFlags) er
return err
}
account, err := requireAccount(flags)
if err != nil {
return err
}
svc, err := newClassroomService(ctx, account)
_, svc, err := requireClassroomService(ctx, flags)
if err != nil {
return wrapClassroomError(err)
}

View File

@ -4,6 +4,7 @@ import (
"context"
"google.golang.org/api/calendar/v3"
"google.golang.org/api/classroom/v1"
"google.golang.org/api/docs/v1"
"google.golang.org/api/drive/v3"
"google.golang.org/api/gmail/v1"
@ -29,6 +30,10 @@ func requireGmailService(ctx context.Context, flags *RootFlags) (string, *gmail.
return requireGoogleService(ctx, flags, newGmailService)
}
func requireClassroomService(ctx context.Context, flags *RootFlags) (string, *classroom.Service, error) {
return requireGoogleService(ctx, flags, newClassroomService)
}
func requireGoogleService[T any](ctx context.Context, flags *RootFlags, newService func(context.Context, string) (*T, error)) (string, *T, error) {
account, err := requireAccount(flags)
if err != nil {