♻️ Reuse struct in cmd/activity

This commit is contained in:
OpenWaygate committed 2026-01-24 21:47:45 +08:00
1 parent 3ad66b296b
commit 5d9ff35799
6 files changed
+110 -228

No files matched your search

+1 -14
View File
@@ -1,4 +1,4 @@
load("@rules_go//go:def.bzl", "go_library", "go_test")
load("@rules_go//go:def.bzl", "go_library")
go_library(
name = "activity",
@@ -18,16 +18,3 @@ go_library(
"@com_github_spf13_cobra//:cobra",
],
)
go_test(
name = "activity_test",
srcs = ["list_test.go"],
embed = [":activity"],
deps = [
"//pkg/activity",
"@com_github_google_jsonschema_go//jsonschema",
"@com_github_stretchr_testify//assert",
"@org_golang_google_api//option",
"@org_golang_google_api//youtube/v3:youtube",
],
)
+21 -51
View File
@@ -7,7 +7,6 @@ import (
"bytes"
"context"
"encoding/json"
"io"
"log/slog"
"time"
@@ -23,37 +22,24 @@ const (
listTool = "activity-list"
)
type listIn struct {
ChannelId string `json:"channelId"`
Home *bool `json:"home,omitempty"`
MaxResults int64 `json:"maxResults"`
Mine *bool `json:"mine,omitempty"`
PublishedAfter string `json:"publishedAfter"`
PublishedBefore string `json:"publishedBefore"`
RegionCode string `json:"regionCode"`
Parts []string `json:"parts"`
Output string `json:"output"`
Jsonpath string `json:"jsonpath"`
}
var listInSchema = &jsonschema.Schema{
Type: "object",
Required: []string{},
Properties: map[string]*jsonschema.Schema{
"channelId": {Type: "string", Description: ciUsage},
"channel_id": {Type: "string", Description: ciUsage},
"home": {
Type: "boolean", Enum: []any{true, false}, Description: homeUsage,
},
"maxResults": {
"max_results": {
Type: "number", Description: pkg.MRUsage,
Default: json.RawMessage("5"), Minimum: jsonschema.Ptr(float64(0)),
},
"mine": {
Type: "boolean", Enum: []any{true, false}, Description: mineUsage,
},
"publishedAfter": {Type: "string", Description: paUsage},
"publishedBefore": {Type: "string", Description: pbUsage},
"regionCode": {Type: "string", Description: rcUsage},
"published_after": {Type: "string", Description: paUsage},
"published_before": {Type: "string", Description: pbUsage},
"region_code": {Type: "string", Description: rcUsage},
"parts": {
Type: "array", Description: pkg.PartsUsage,
Items: &jsonschema.Schema{Type: "string"},
@@ -104,19 +90,20 @@ var listCmd = &cobra.Command{
Short: short,
Long: long,
Run: func(cmd *cobra.Command, args []string) {
input := &listIn{
ChannelId: channelId,
Home: home,
MaxResults: maxResults,
Mine: mine,
PublishedAfter: publishedAfter,
PublishedBefore: publishedAfter,
RegionCode: regionCode,
Parts: parts,
Output: output,
Jsonpath: jsonpath,
}
if err := input.call(cmd.OutOrStdout()); err != nil {
input := activity.NewActivity(
activity.WithChannelId(channelId),
activity.WithHome(home),
activity.WithMaxResults(maxResults),
activity.WithMine(mine),
activity.WithPublishedAfter(publishedAfter),
activity.WithPublishedBefore(publishedBefore),
activity.WithRegionCode(regionCode),
activity.WithParts(parts),
activity.WithOutput(output),
activity.WithJsonpath(jsonpath),
activity.WithService(nil),
)
if err := input.List(cmd.OutOrStdout()); err != nil {
_ = cmd.Help()
cmd.PrintErrf("Error: %v\n", err)
}
@@ -124,7 +111,7 @@ var listCmd = &cobra.Command{
}
func listHandler(
ctx context.Context, req *mcp.CallToolRequest, input listIn,
ctx context.Context, req *mcp.CallToolRequest, input activity.Activity,
) (*mcp.CallToolResult, any, error) {
logger := slog.New(
mcp.NewLoggingHandler(
@@ -134,26 +121,9 @@ func listHandler(
)
var writer bytes.Buffer
if err := input.call(&writer); err != nil {
if err := input.List(&writer); err != nil {
logger.ErrorContext(ctx, err.Error(), "input", input)
return nil, nil, err
}
return &mcp.CallToolResult{Content: []mcp.Content{&mcp.TextContent{Text: writer.String()}}}, nil, nil
}
func (l *listIn) call(writer io.Writer, opts ...activity.Option) error {
defaultOpts := []activity.Option{
activity.WithChannelId(l.ChannelId),
activity.WithHome(l.Home),
activity.WithMaxResults(l.MaxResults),
activity.WithMine(l.Mine),
activity.WithPublishedAfter(l.PublishedAfter),
activity.WithPublishedBefore(l.PublishedBefore),
activity.WithRegionCode(l.RegionCode),
activity.WithService(nil),
}
defaultOpts = append(defaultOpts, opts...)
a := activity.NewActivity(defaultOpts...)
return a.List(l.Parts, l.Output, l.Jsonpath, writer)
}
-115
View File
@@ -1,115 +0,0 @@
// Copyright 2026 eat-pray-ai & OpenWaygate
// SPDX-License-Identifier: Apache-2.0
package activity
import (
"bytes"
"context"
"net/http"
"net/http/httptest"
"testing"
"github.com/eat-pray-ai/yutu/pkg/activity"
"github.com/google/jsonschema-go/jsonschema"
"github.com/stretchr/testify/assert"
"google.golang.org/api/option"
"google.golang.org/api/youtube/v3"
)
func TestList(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.Write([]byte(`{
"items": [
{
"id": "test-activity-id",
"snippet": {
"title": "Test Activity",
"type": "upload",
"publishedAt": "2024-01-01T00:00:00Z"
},
"contentDetails": {
"upload": {
"videoId": "test-video-id"
}
}
}
],
"nextPageToken": ""
}`))
}))
defer ts.Close()
svc, err := youtube.NewService(context.Background(), option.WithEndpoint(ts.URL), option.WithAPIKey("test-key"))
if err != nil {
t.Fatalf("failed to create service: %v", err)
}
tests := []struct {
name string
input *listIn
wantErr bool
verify func(t *testing.T, output string)
}{
{
name: "json output",
input: &listIn{
ChannelId: "test-channel-id",
Parts: []string{"id", "snippet", "contentDetails"},
Output: "json",
},
wantErr: false,
verify: func(t *testing.T, output string) {
assert.Contains(t, output, "test-activity-id")
assert.Contains(t, output, "Test Activity")
assert.Contains(t, output, "upload")
},
},
{
name: "table output",
input: &listIn{
Home: jsonschema.Ptr(true),
MaxResults: 10,
RegionCode: "US",
Output: "table",
},
wantErr: false,
verify: func(t *testing.T, output string) {
assert.Contains(t, output, "ID")
assert.Contains(t, output, "TITLE")
assert.Contains(t, output, "TYPE")
assert.Contains(t, output, "TIME")
assert.Contains(t, output, "test-activity-id")
assert.Contains(t, output, "Test Activity")
},
},
{
name: "yaml output",
input: &listIn{
Mine: jsonschema.Ptr(true),
PublishedAfter: "2024-01-01T00:00:00Z",
PublishedBefore: "2024-12-31T23:59:59Z",
Output: "yaml",
},
wantErr: false,
verify: func(t *testing.T, output string) {
assert.Contains(t, output, "id: test-activity-id")
assert.Contains(t, output, "title: Test Activity")
assert.Contains(t, output, "type: upload")
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var buf bytes.Buffer
err = tt.input.call(&buf, activity.WithService(svc))
if (err != nil) != tt.wantErr {
t.Errorf("listIn.call() error = %v, wantErr %v", err, tt.wantErr)
return
}
tt.verify(t, buf.String())
})
}
}
+17 -1
View File
@@ -1,4 +1,4 @@
load("@rules_go//go:def.bzl", "go_library")
load("@rules_go//go:def.bzl", "go_library", "go_test")
go_library(
name = "channel",
@@ -20,3 +20,19 @@ go_library(
"@com_github_spf13_pflag//:pflag",
],
)
go_test(
name = "channel_test",
srcs = [
"list_test.go",
"update_test.go",
],
embed = [":channel"],
deps = [
"//pkg/channel",
"@com_github_google_jsonschema_go//jsonschema",
"@com_github_stretchr_testify//assert",
"@org_golang_google_api//option",
"@org_golang_google_api//youtube/v3:youtube",
],
)
+55 -31
View File
@@ -19,7 +19,7 @@ var (
errGetActivity = errors.New("failed to get activity")
)
type activity struct {
type Activity struct {
service *youtube.Service
ChannelId string `yaml:"channel_id" json:"channel_id"`
Home *bool `yaml:"home" json:"home"`
@@ -28,34 +28,33 @@ type activity struct {
PublishedAfter string `yaml:"published_after" json:"published_after"`
PublishedBefore string `yaml:"published_before" json:"published_before"`
RegionCode string `yaml:"region_code" json:"region_code"`
Parts []string `yaml:"parts" json:"parts"`
Output string `yaml:"output" json:"output"`
Jsonpath string `yaml:"jsonpath" json:"jsonpath"`
}
type Activity[T any] interface {
List([]string, string, string, io.Writer) error
Get([]string) ([]*T, error)
type IActivity[T any] interface {
List(io.Writer) error
Get() ([]*T, error)
preRun()
}
type Option func(*activity)
type Option func(*Activity)
func NewActivity(opts ...Option) Activity[youtube.Activity] {
a := &activity{}
func NewActivity(opts ...Option) IActivity[youtube.Activity] {
a := &Activity{}
for _, opt := range opts {
opt(a)
}
if a.service == nil {
a.service = auth.NewY2BService(
auth.WithCredential("", pkg.Root.FS()),
auth.WithCacheToken("", pkg.Root.FS()),
).GetService()
}
return a
}
func (a *activity) Get(parts []string) ([]*youtube.Activity, error) {
call := a.service.Activities.List(parts)
func (a *Activity) Get() ([]*youtube.Activity, error) {
a.preRun()
call := a.service.Activities.List(a.Parts)
if a.ChannelId != "" {
call = call.ChannelId(a.ChannelId)
}
@@ -104,19 +103,17 @@ func (a *activity) Get(parts []string) ([]*youtube.Activity, error) {
return items, nil
}
func (a *activity) List(
parts []string, output string, jsonpath string, writer io.Writer,
) error {
activities, err := a.Get(parts)
func (a *Activity) List(writer io.Writer) error {
activities, err := a.Get()
if err != nil && activities == nil {
return err
}
switch output {
switch a.Output {
case "json":
utils.PrintJSON(activities, jsonpath, writer)
utils.PrintJSON(activities, a.Jsonpath, writer)
case "yaml":
utils.PrintYAML(activities, jsonpath, writer)
utils.PrintYAML(activities, a.Jsonpath, writer)
case "table":
tb := table.NewWriter()
defer tb.Render()
@@ -135,14 +132,23 @@ func (a *activity) List(
return err
}
func (a *Activity) preRun() {
if a.service == nil {
a.service = auth.NewY2BService(
auth.WithCredential("", pkg.Root.FS()),
auth.WithCacheToken("", pkg.Root.FS()),
).GetService()
}
}
func WithChannelId(channelId string) Option {
return func(a *activity) {
return func(a *Activity) {
a.ChannelId = channelId
}
}
func WithHome(home *bool) Option {
return func(a *activity) {
return func(a *Activity) {
if home != nil {
a.Home = home
}
@@ -150,7 +156,7 @@ func WithHome(home *bool) Option {
}
func WithMaxResults(maxResults int64) Option {
return func(a *activity) {
return func(a *Activity) {
if maxResults < 0 {
maxResults = 1
} else if maxResults == 0 {
@@ -161,7 +167,7 @@ func WithMaxResults(maxResults int64) Option {
}
func WithMine(mine *bool) Option {
return func(a *activity) {
return func(a *Activity) {
if mine != nil {
a.Mine = mine
}
@@ -169,25 +175,43 @@ func WithMine(mine *bool) Option {
}
func WithPublishedAfter(publishedAfter string) Option {
return func(a *activity) {
return func(a *Activity) {
a.PublishedAfter = publishedAfter
}
}
func WithPublishedBefore(publishedBefore string) Option {
return func(a *activity) {
return func(a *Activity) {
a.PublishedBefore = publishedBefore
}
}
func WithRegionCode(regionCode string) Option {
return func(a *activity) {
return func(a *Activity) {
a.RegionCode = regionCode
}
}
func WithParts(parts []string) Option {
return func(a *Activity) {
a.Parts = parts
}
}
func WithOutput(output string) Option {
return func(a *Activity) {
a.Output = output
}
}
func WithJsonpath(jsonpath string) Option {
return func(a *Activity) {
a.Jsonpath = jsonpath
}
}
func WithService(svc *youtube.Service) Option {
return func(c *activity) {
return func(c *Activity) {
c.service = svc
}
}
+16 -16
View File
@@ -24,7 +24,7 @@ func TestNewActivity(t *testing.T) {
tests := []struct {
name string
args args
want Activity[youtube.Activity]
want IActivity[youtube.Activity]
}{
{
name: "with all options",
@@ -40,7 +40,7 @@ func TestNewActivity(t *testing.T) {
WithService(&youtube.Service{}),
},
},
want: &activity{
want: &Activity{
ChannelId: "test-channel-123",
Home: &homeTrue,
MaxResults: 50,
@@ -57,7 +57,7 @@ func TestNewActivity(t *testing.T) {
WithService(&youtube.Service{}),
},
},
want: &activity{},
want: &Activity{},
},
{
name: "with nil boolean options",
@@ -68,7 +68,7 @@ func TestNewActivity(t *testing.T) {
WithService(&youtube.Service{}),
},
},
want: &activity{},
want: &Activity{},
},
{
name: "with false boolean options",
@@ -79,7 +79,7 @@ func TestNewActivity(t *testing.T) {
WithService(&youtube.Service{}),
},
},
want: &activity{
want: &Activity{
Home: &homeFalse,
Mine: &mineFalse,
},
@@ -92,7 +92,7 @@ func TestNewActivity(t *testing.T) {
WithService(&youtube.Service{}),
},
},
want: &activity{
want: &Activity{
MaxResults: math.MaxInt64,
},
},
@@ -104,7 +104,7 @@ func TestNewActivity(t *testing.T) {
WithService(&youtube.Service{}),
},
},
want: &activity{
want: &Activity{
MaxResults: 1,
},
},
@@ -119,7 +119,7 @@ func TestNewActivity(t *testing.T) {
WithService(&youtube.Service{}),
},
},
want: &activity{
want: &Activity{
ChannelId: "",
PublishedAfter: "",
PublishedBefore: "",
@@ -136,7 +136,7 @@ func TestNewActivity(t *testing.T) {
WithService(&youtube.Service{}),
},
},
want: &activity{
want: &Activity{
ChannelId: "partial-channel",
MaxResults: 25,
RegionCode: "UK",
@@ -147,13 +147,13 @@ func TestNewActivity(t *testing.T) {
for _, tt := range tests {
t.Run(
tt.name, func(t *testing.T) {
if got := NewActivity(tt.args.opts...); !reflect.DeepEqual(got.(*activity).ChannelId, tt.want.(*activity).ChannelId) ||
!reflect.DeepEqual(got.(*activity).Home, tt.want.(*activity).Home) ||
!reflect.DeepEqual(got.(*activity).MaxResults, tt.want.(*activity).MaxResults) ||
!reflect.DeepEqual(got.(*activity).Mine, tt.want.(*activity).Mine) ||
!reflect.DeepEqual(got.(*activity).PublishedAfter, tt.want.(*activity).PublishedAfter) ||
!reflect.DeepEqual(got.(*activity).PublishedBefore, tt.want.(*activity).PublishedBefore) ||
!reflect.DeepEqual(got.(*activity).RegionCode, tt.want.(*activity).RegionCode) {
if got := NewActivity(tt.args.opts...); !reflect.DeepEqual(got.(*Activity).ChannelId, tt.want.(*Activity).ChannelId) ||
!reflect.DeepEqual(got.(*Activity).Home, tt.want.(*Activity).Home) ||
!reflect.DeepEqual(got.(*Activity).MaxResults, tt.want.(*Activity).MaxResults) ||
!reflect.DeepEqual(got.(*Activity).Mine, tt.want.(*Activity).Mine) ||
!reflect.DeepEqual(got.(*Activity).PublishedAfter, tt.want.(*Activity).PublishedAfter) ||
!reflect.DeepEqual(got.(*Activity).PublishedBefore, tt.want.(*Activity).PublishedBefore) ||
!reflect.DeepEqual(got.(*Activity).RegionCode, tt.want.(*Activity).RegionCode) {
t.Errorf("NewActivity() = %v, want %v", got, tt.want)
}
},