diff --git a/cmd/followdata/root.go b/cmd/followdata/root.go index f51a911..3b3c07f 100644 --- a/cmd/followdata/root.go +++ b/cmd/followdata/root.go @@ -28,6 +28,7 @@ func NewRootCommand() *cobra.Command { } func addCommonFlags(cmd *cobra.Command) { + cmd.Flags().Int(instagram.FlagLimit, instagram.Unlimited, `maximum results to display, leave empty for unlimited`) cmd.Flags().String(instagram.FlagOrder, instagram.OrderDesc, `order direction ("asc", "desc")`) cmd.Flags().String(instagram.FlagOutput, instagram.OutputTable, `output format ("json", "table", "yaml")`) cmd.Flags().String(instagram.FlagSortBy, instagram.FieldTimestamp, `sort by field ("timestamp", "username")`) diff --git a/cmd/root/root.go b/cmd/root/root.go index d6852e1..43bda0f 100644 --- a/cmd/root/root.go +++ b/cmd/root/root.go @@ -2,6 +2,7 @@ package root import ( "fmt" + "os" "github.com/cecobask/instagram-insights/cmd/followdata" "github.com/cecobask/instagram-insights/cmd/information" @@ -24,6 +25,8 @@ func NewRootCommand() *cobra.Command { SilenceUsage: true, DisableAutoGenTag: true, } + cmd.SetOut(os.Stdout) + cmd.SetErr(os.Stderr) cmd.AddCommand( information.NewRootCommand(), followdata.NewRootCommand(), diff --git a/pkg/instagram/constants.go b/pkg/instagram/constants.go index a01f26e..c2beccd 100644 --- a/pkg/instagram/constants.go +++ b/pkg/instagram/constants.go @@ -3,6 +3,7 @@ package instagram const ( FieldTimestamp = "timestamp" FieldUsername = "username" + Unlimited = 0 OrderAsc = "asc" OrderDesc = "desc" OutputJson = "json" @@ -12,6 +13,7 @@ const ( ) const ( + FlagLimit = "limit" FlagOrder = "order" FlagOutput = "output" FlagSortBy = "sort-by" diff --git a/pkg/instagram/followdata/followdata.go b/pkg/instagram/followdata/followdata.go index 261cd1e..582a3db 100644 --- a/pkg/instagram/followdata/followdata.go +++ b/pkg/instagram/followdata/followdata.go @@ -47,6 +47,7 @@ func (h *handler) Followers(opts *instagram.Options) (*string, error) { } } h.followData.Followers.Sort(opts.SortBy, opts.Order) + h.followData.Followers.Limit(opts.Limit) return h.followData.Followers.output(opts.Output) } @@ -59,6 +60,7 @@ func (h *handler) Following(opts *instagram.Options) (*string, error) { return nil, err } h.followData.Following.Sort(opts.SortBy, opts.Order) + h.followData.Following.Limit(opts.Limit) return h.followData.Following.output(opts.Output) } @@ -72,6 +74,7 @@ func (h *handler) Unfollowers(opts *instagram.Options) (*string, error) { } h.followData.hydrateUnfollowers() h.followData.Unfollowers.Sort(opts.SortBy, opts.Order) + h.followData.Unfollowers.Limit(opts.Limit) return h.followData.Unfollowers.output(opts.Output) } @@ -283,3 +286,9 @@ func (ul *userList) Sort(field string, order string) { slices.Reverse(ul.users) } } + +func (ul *userList) Limit(limit int) { + if limit > 0 && limit < len(ul.users) { + ul.users = ul.users[:limit] + } +} diff --git a/pkg/instagram/followdata/followdata_test.go b/pkg/instagram/followdata/followdata_test.go index ccec9b0..ba347c7 100644 --- a/pkg/instagram/followdata/followdata_test.go +++ b/pkg/instagram/followdata/followdata_test.go @@ -462,3 +462,64 @@ func Test_userList_Sort(t *testing.T) { }) } } + +func Test_userList_Limit(t *testing.T) { + type fields struct { + users []user + } + type args struct { + limit int + } + testUsers := []user{ + { + ProfileUrl: "https://www.instagram.com/username1", + Username: "username1", + Timestamp: ×tamp{}, + }, + { + ProfileUrl: "https://www.instagram.com/username2", + Username: "username2", + Timestamp: ×tamp{}, + }, + } + tests := []struct { + name string + fields fields + args args + assertions func(t *testing.T, ul *userList) + }{ + { + name: "succeeds to limit users", + fields: fields{ + users: testUsers, + }, + args: args{ + limit: 1, + }, + assertions: func(t *testing.T, ul *userList) { + assert.Equal(t, 1, len(ul.users)) + }, + }, + { + name: "avoids to limit users when limit is unlimited", + fields: fields{ + users: testUsers, + }, + args: args{ + limit: instagram.Unlimited, + }, + assertions: func(t *testing.T, ul *userList) { + assert.Equal(t, 2, len(ul.users)) + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ul := &userList{ + users: tt.fields.users, + } + ul.Limit(tt.args.limit) + tt.assertions(t, ul) + }) + } +} diff --git a/pkg/instagram/options.go b/pkg/instagram/options.go index 3bf05c0..3a7cc5b 100644 --- a/pkg/instagram/options.go +++ b/pkg/instagram/options.go @@ -7,12 +7,17 @@ import ( ) type Options struct { + Limit int Order string Output string SortBy string } func NewOptions(flags *pflag.FlagSet) (*Options, error) { + limit, err := flags.GetInt(FlagLimit) + if err != nil { + return nil, err + } order, err := flags.GetString(FlagOrder) if err != nil { return nil, err @@ -26,6 +31,7 @@ func NewOptions(flags *pflag.FlagSet) (*Options, error) { return nil, err } return &Options{ + Limit: limit, Order: order, Output: output, SortBy: sortBy, @@ -39,6 +45,9 @@ func NewEmptyOptions() *Options { } func (o *Options) Validate() error { + if err := validateLimit(o.Limit); err != nil { + return err + } if err := validateOrder(o.Order); err != nil { return err } @@ -51,6 +60,14 @@ func (o *Options) Validate() error { return nil } +func validateLimit(value int) error { + if value < 0 { + return fmt.Errorf("invalid limit: %d", value) + } + return nil + +} + func validateOrder(value string) error { switch value { case OrderAsc, OrderDesc: diff --git a/pkg/instagram/options_test.go b/pkg/instagram/options_test.go index e4875c8..a878eb9 100644 --- a/pkg/instagram/options_test.go +++ b/pkg/instagram/options_test.go @@ -42,6 +42,7 @@ func Test_validateOutput(t *testing.T) { func TestOptions_Validate(t *testing.T) { type fields struct { + Limit int Order string Output string SortBy string @@ -54,12 +55,20 @@ func TestOptions_Validate(t *testing.T) { { name: "succeeds to validate all options", fields: fields{ + Limit: Unlimited, Order: OrderAsc, Output: OutputTable, SortBy: FieldTimestamp, }, wantErr: false, }, + { + name: "fails to validate limit", + fields: fields{ + Limit: -1, + }, + wantErr: true, + }, { name: "fails to validate order", fields: fields{ @@ -88,6 +97,7 @@ func TestOptions_Validate(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { o := Options{ + Limit: tt.fields.Limit, Order: tt.fields.Order, Output: tt.fields.Output, SortBy: tt.fields.SortBy, @@ -114,6 +124,7 @@ func TestNewOptions(t *testing.T) { args: args{ flags: func() *pflag.FlagSet { flags := pflag.NewFlagSet("", pflag.ExitOnError) + flags.Int(FlagLimit, 1000, "") flags.String(FlagOrder, OrderAsc, "") flags.String(FlagOutput, OutputTable, "") flags.String(FlagSortBy, FieldTimestamp, "") @@ -121,6 +132,7 @@ func TestNewOptions(t *testing.T) { }(), }, want: &Options{ + Limit: 1000, Order: OrderAsc, Output: OutputTable, SortBy: FieldTimestamp, @@ -128,18 +140,31 @@ func TestNewOptions(t *testing.T) { wantErr: false, }, { - name: "fails to find flag order", + name: "fails to find flag limit", args: args{ flags: pflag.NewFlagSet("", pflag.ExitOnError), }, want: nil, wantErr: true, }, + { + name: "fails to find flag order", + args: args{ + flags: func() *pflag.FlagSet { + flags := pflag.NewFlagSet("", pflag.ExitOnError) + flags.Int(FlagLimit, Unlimited, "") + return flags + }(), + }, + want: nil, + wantErr: true, + }, { name: "fails to find flag output", args: args{ flags: func() *pflag.FlagSet { flags := pflag.NewFlagSet("", pflag.ExitOnError) + flags.Int(FlagLimit, Unlimited, "") flags.String(FlagOrder, OrderAsc, "") return flags }(), @@ -152,6 +177,7 @@ func TestNewOptions(t *testing.T) { args: args{ flags: func() *pflag.FlagSet { flags := pflag.NewFlagSet("", pflag.ExitOnError) + flags.Int(FlagLimit, Unlimited, "") flags.String(FlagOrder, OrderAsc, "") flags.String(FlagOutput, OutputTable, "") return flags