♻️ Reuse struct in cmd/activity
This commit is contained in:
1 parent
3ad66b296b
commit
5d9ff35799
6 files changed
+110
-228
No files matched your search
@@ -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
@@ -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)
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
},
|
||||
|
||||
Reference in new issue
Block a user