Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 70 additions & 0 deletions cli/cmd_list.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package cli

import "github.com/spf13/cobra"

// listCmd is an alias for subjects.
func (a *App) listCmd() *cobra.Command {
return &cobra.Command{
Use: "list",
Short: "List all CS subject guides (alias for subjects)",
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
subjects, err := a.client.Subjects(cmd.Context())
if err != nil {
return mapFetchErr(err)
}
if a.limit > 0 && len(subjects) > a.limit {
subjects = subjects[:a.limit]
}
return a.renderOrEmpty(subjects, len(subjects))
},
}
}

// topicCmd fetches one subject by slug.
func (a *App) topicCmd() *cobra.Command {
return &cobra.Command{
Use: "topic <slug>",
Short: "Show details for a CS topic by slug (e.g. programming)",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
subjects, err := a.client.Subjects(cmd.Context())
if err != nil {
return mapFetchErr(err)
}
for _, s := range subjects {
if s.Slug == args[0] {
return a.render([]*subjectSlice{
{Rank: s.Rank, Slug: s.Slug, Title: s.Title, URL: s.URL, BookURL: s.BookURL},
})
}
}
return codeError(exitNoData, nil)
},
}
}

// subjectSlice is render-friendly (same fields as Subject).
type subjectSlice struct {
Rank int `json:"rank" csv:"rank" tsv:"rank"`
Slug string `json:"slug" csv:"slug" tsv:"slug"`
Title string `json:"title" csv:"title" tsv:"title"`
URL string `json:"url" csv:"url" tsv:"url"`
BookURL string `json:"book_url" csv:"book_url" tsv:"book_url"`
}

// infoCmd prints site stats.
func (a *App) infoCmd() *cobra.Command {
return &cobra.Command{
Use: "info",
Short: "Print site stats (subject count)",
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
info, err := a.client.SiteInfo(cmd.Context())
if err != nil {
return mapFetchErr(err)
}
return a.render(info)
},
}
}
3 changes: 3 additions & 0 deletions cli/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,10 @@ func Root() *cobra.Command {
pf.IntVar(&app.cfg.Retries, "retries", app.cfg.Retries, "retry attempts on 429/5xx")

root.AddCommand(
app.listCmd(),
app.subjectsCmd(),
app.topicCmd(),
app.infoCmd(),
newVersionCmd(),
)
return root
Expand Down
37 changes: 31 additions & 6 deletions tycs/domain.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,17 +39,30 @@ tools. No API key, nothing to run alongside it.`,
func (Domain) Register(app *kit.App) {
app.SetClient(newClient)

// Resolver op: look up a single subject by slug. Seeds the mint index so
// ant can address subject records as tycs://subject/<slug>.
// list: all subjects.
kit.Handle(app, kit.OpMeta{Name: "list", Group: "read", List: true,
Summary: "List all CS subject guides",
URIType: "subject"}, listSubjects)

// subjects: alias for list.
kit.Handle(app, kit.OpMeta{Name: "subjects", Group: "read", List: true,
Summary: "List all CS subject guides from Teach Yourself CS",
URIType: "subject"}, listSubjects)

// topic: fetch one subject by slug.
kit.Handle(app, kit.OpMeta{Name: "topic", Group: "read", Single: true,
Summary: "Fetch a CS subject guide by slug", URIType: "subject",
Args: []kit.Arg{{Name: "slug", Help: "subject slug (e.g. programming)"}}}, getSubject)

// subject: resolver op.
kit.Handle(app, kit.OpMeta{Name: "subject", Group: "read", Single: true,
Summary: "Fetch a CS subject guide by slug", URIType: "subject",
Resolver: true,
Args: []kit.Arg{{Name: "slug", Help: "subject slug (e.g. programming)"}}}, getSubject)

// List op: all subjects.
kit.Handle(app, kit.OpMeta{Name: "subjects", Group: "read", List: true,
Summary: "List all CS subject guides from Teach Yourself CS",
URIType: "subject"}, listSubjects)
// info: site stats.
kit.Handle(app, kit.OpMeta{Name: "info", Group: "read", Single: true,
Summary: "Print site stats (subject count)"}, getSiteInfo)
}

func newClient(_ context.Context, cfg kit.Config) (any, error) {
Expand Down Expand Up @@ -78,6 +91,10 @@ type subjectsIn struct {
Client *Client `kit:"inject"`
}

type infoIn struct {
Client *Client `kit:"inject"`
}

func getSubject(ctx context.Context, in subjectRef, emit func(*Subject) error) error {
subjects, err := in.Client.Subjects(ctx)
if err != nil {
Expand All @@ -104,6 +121,14 @@ func listSubjects(ctx context.Context, in subjectsIn, emit func(*Subject) error)
return nil
}

func getSiteInfo(ctx context.Context, in infoIn, emit func(*Info) error) error {
info, err := in.Client.SiteInfo(ctx)
if err != nil {
return err
}
return emit(info)
}

func (Domain) Classify(input string) (uriType, id string, err error) {
input = strings.TrimSpace(input)
if input == "" {
Expand Down
28 changes: 28 additions & 0 deletions tycs/info_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package tycs_test

import (
"context"
"fmt"
"net/http"
"net/http/httptest"
"testing"
)

func TestSiteInfo(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, fakeHTML)

Check failure on line 13 in tycs/info_test.go

View workflow job for this annotation

GitHub Actions / lint

Error return value of `fmt.Fprint` is not checked (errcheck)
}))
defer ts.Close()

c := newTestClient(ts)
info, err := c.SiteInfo(context.Background())
if err != nil {
t.Fatal(err)
}
if info.Subjects != 2 {
t.Errorf("Subjects = %d, want 2", info.Subjects)
}
if info.Site == "" {
t.Error("Site should not be empty")
}
}
13 changes: 13 additions & 0 deletions tycs/tycs.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,19 @@ func (c *Client) Subjects(ctx context.Context) ([]*Subject, error) {
return subjects, nil
}

// SiteInfo returns site-level stats.
func (c *Client) SiteInfo(ctx context.Context) (*Info, error) {
subjects, err := c.Subjects(ctx)
if err != nil {
return nil, err
}
return &Info{
Site: Host,
Subjects: len(subjects),
Source: c.cfg.BaseURL,
}, nil
}

func (c *Client) get(ctx context.Context, url string) ([]byte, error) {
var lastErr error
for attempt := 0; attempt <= c.cfg.Retries; attempt++ {
Expand Down
7 changes: 7 additions & 0 deletions tycs/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,10 @@ type Subject struct {
URL string `json:"url" csv:"url" tsv:"url"`
BookURL string `json:"book_url" csv:"book_url" tsv:"book_url"`
}

// Info is site-level stats.
type Info struct {
Site string `json:"site" csv:"site" tsv:"site"`
Subjects int `json:"subjects" csv:"subjects" tsv:"subjects"`
Source string `json:"source" csv:"source" tsv:"source"`
}
Loading