Skip to content
This repository was archived by the owner on Aug 12, 2022. It is now read-only.

Commit 477ed61

Browse files
authored
add contest FetchIncoming command (#78)
1 parent 948a860 commit 477ed61

File tree

11 files changed

+225
-87
lines changed

11 files changed

+225
-87
lines changed

Dockerfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ WORKDIR /src
33
COPY go.mod go.sum Makefile .git ./
44
COPY cmd cmd
55
COPY internal internal
6+
COPY pkg pkg
67
RUN go mod download
78
RUN make install
89

docs/cmd/ach_contest.md

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

docs/cmd/ach_contest_incoming.md

Lines changed: 29 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

integration/integration.sh

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,14 @@ set -eux
44
mkdir work
55
cd work
66

7+
# show incoming contests
8+
ach contest incoming
9+
10+
# create a contest working directory
711
ach contest create --default-template contestFoo
12+
13+
# in a contest directory, ...
14+
15+
# test command should succeeds
816
cd contestFoo/A
917
ach test

internal/cmd/ach/contest/contest.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ package contest
33
import (
44
"github.com/spf13/cobra"
55
"github.com/yuchiki/atcoderHelper/internal/cmd/ach/contest/create"
6+
"github.com/yuchiki/atcoderHelper/internal/cmd/ach/contest/incoming"
7+
"github.com/yuchiki/atcoderHelper/internal/repository"
68
)
79

810
func NewContestCmd() *cobra.Command {
@@ -19,4 +21,5 @@ func NewContestCmd() *cobra.Command {
1921

2022
func registerSubcommands(cmd *cobra.Command) {
2123
cmd.AddCommand(create.NewContestCreateCmd())
24+
cmd.AddCommand(incoming.NewContestIncomingCmd(repository.FetchIncoming))
2225
}

internal/cmd/ach/contest/contest_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ func TestAch_Execute(t *testing.T) {
1010
testutil.TestCaseTemplates{
1111
testutil.HasName("contest"),
1212
testutil.HasSubcommands("create"),
13+
testutil.HasSubcommands("incoming"),
1314
}.
1415
Build(NewContestCmd).
1516
Run(t)
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package incoming
2+
3+
import (
4+
"github.com/spf13/cobra"
5+
"github.com/yuchiki/atcoderHelper/internal/repository"
6+
)
7+
8+
func NewContestIncomingCmd(fetcher func() ([]repository.ContestInfo, error)) *cobra.Command {
9+
cmd := &cobra.Command{
10+
Use: "incoming",
11+
Short: "show incoming contests",
12+
Long: "show incoming contests.",
13+
Args: cobra.ExactArgs(0),
14+
RunE: func(cmd *cobra.Command, args []string) error {
15+
contests, err := fetcher()
16+
if err != nil {
17+
return err
18+
}
19+
20+
for _, contest := range contests {
21+
cmd.Printf("%v: %v\n", contest.ID, contest.Name)
22+
}
23+
24+
return nil
25+
},
26+
}
27+
28+
return cmd
29+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package incoming
2+
3+
import (
4+
"errors"
5+
"testing"
6+
7+
"github.com/spf13/cobra"
8+
"github.com/yuchiki/atcoderHelper/internal/repository"
9+
"github.com/yuchiki/atcoderHelper/internal/testutil"
10+
)
11+
12+
var errUnknownInRepositoryLayer = errors.New("an error thrown in fetcher function")
13+
14+
func TestIncoming_Execute(t *testing.T) {
15+
generateBuilder := func(infos []repository.ContestInfo, err error) func() *cobra.Command {
16+
return func() *cobra.Command {
17+
return NewContestIncomingCmd(func() ([]repository.ContestInfo, error) {
18+
return infos, err
19+
})
20+
}
21+
}
22+
23+
testutil.TestCaseTemplates{
24+
testutil.HasName("incoming"),
25+
}.Build(generateBuilder(nil, nil)).Run(t)
26+
27+
testutil.TestCases{
28+
{
29+
Name: "OK",
30+
CmdBuilder: generateBuilder([]repository.ContestInfo{
31+
{ID: "id1", Name: "name1"},
32+
{ID: "id2", Name: "name2"},
33+
{ID: "id3", Name: "name3"},
34+
}, nil),
35+
OutputCheck: testutil.OutputShouldBe("id1: name1\nid2: name2\nid3: name3\n"),
36+
},
37+
{
38+
Name: "returns an error when the repository layer fails",
39+
CmdBuilder: generateBuilder(nil, errUnknownInRepositoryLayer),
40+
ErrorCheck: testutil.AnyError(),
41+
},
42+
}.Run(t)
43+
}

internal/reposiotory/incoming.go

Lines changed: 0 additions & 81 deletions
This file was deleted.

internal/repository/incoming.go

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
package repository
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
"strings"
7+
8+
"github.com/yuchiki/atcoderHelper/pkg/queryablehtml"
9+
)
10+
11+
const (
12+
AtCoderURL = "https://atcoder.jp"
13+
IncomingPath = "/contests"
14+
)
15+
16+
var errContestPathCannotBeParsed = errors.New("contest path cannot be parsed")
17+
18+
type ContestInfo struct {
19+
ID string
20+
Name string
21+
}
22+
23+
// FetchIncoming fetches information of incoming contests.
24+
func FetchIncoming() ([]ContestInfo, error) {
25+
node, err := queryablehtml.Fetch(AtCoderURL + IncomingPath)
26+
if err != nil {
27+
return nil, err
28+
}
29+
30+
contestTRs, err := node.
31+
GetNodeByID("contest-table-upcoming").
32+
GetChildByTag("div").
33+
GetChildByTag("div").
34+
GetChildByTag("table").
35+
GetChildByTag("tbody").
36+
GetChildrenByTag("tr")
37+
if err != nil {
38+
return nil, err
39+
}
40+
41+
contestInfos := []ContestInfo{}
42+
43+
for _, tr := range contestTRs {
44+
contestInfo, err := trToContestInfo(tr)
45+
if err != nil {
46+
return nil, err
47+
}
48+
49+
contestInfos = append(contestInfos, contestInfo)
50+
}
51+
52+
return contestInfos, nil
53+
}
54+
55+
func trToContestInfo(tr queryablehtml.QueryableNode) (ContestInfo, error) {
56+
tds, err := tr.GetChildrenByTag("td")
57+
if err != nil {
58+
return ContestInfo{}, err
59+
}
60+
61+
//nolint: gomnd
62+
if len(tds) < 2 {
63+
return ContestInfo{}, fmt.Errorf("second td does not exist: %w", queryablehtml.ErrNodeNotFound)
64+
}
65+
66+
link := tds[1].GetChildByTag("a")
67+
if link.Err != nil {
68+
return ContestInfo{}, err
69+
}
70+
71+
url, err := link.GetAttr("href")
72+
if err != nil {
73+
return ContestInfo{}, err
74+
}
75+
76+
id, err := getContestID(url)
77+
if err != nil {
78+
return ContestInfo{}, err
79+
}
80+
81+
name, err := link.GetText()
82+
if err != nil {
83+
return ContestInfo{}, err
84+
}
85+
86+
return ContestInfo{
87+
ID: id,
88+
Name: name,
89+
}, nil
90+
}
91+
92+
func getContestID(contestRelativePath string) (string, error) {
93+
each := strings.Split(contestRelativePath, "/")
94+
95+
//nolint:gomnd
96+
if len(each) != 3 {
97+
return "", fmt.Errorf(
98+
"path '%s' cannot be parsed: %w",
99+
contestRelativePath,
100+
errContestPathCannotBeParsed)
101+
}
102+
103+
return each[2], nil
104+
}

0 commit comments

Comments
 (0)