feat: implemented many commands

- added register command
- added reset command
- added users command
- added agg command
- added feeds command
This commit is contained in:
Stevan Freeborn
2026-08-01 14:26:19 -05:00
parent fb53b052b0
commit c012f6008c
14 changed files with 619 additions and 6 deletions
+193 -1
View File
@@ -1,9 +1,18 @@
package command
import (
"cmp"
"context"
"fmt"
"net/url"
"slices"
"strings"
"time"
"github.com/StevanFreeborn/gator/internal/database"
"github.com/StevanFreeborn/gator/internal/rss"
"github.com/StevanFreeborn/gator/internal/state"
"github.com/google/uuid"
)
type Command struct {
@@ -36,6 +45,12 @@ func NewRegistry() *CommandRegistry {
commands := []*Command{
loginCommand(),
registerCommand(),
resetCommand(),
usersCommand(),
aggCommand(),
addFeedCommand(),
feedsCommand(),
}
for _, cmd := range commands {
@@ -63,7 +78,13 @@ func loginCommand() *Command {
username := s.Arguments[0]
err := s.Config.SetUser(username)
_, err := s.Database.GetUserByName(context.Background(), username)
if err != nil {
return fmt.Errorf("Failed to login")
}
err = s.Config.SetUser(username)
if err != nil {
return err
@@ -74,3 +95,174 @@ func loginCommand() *Command {
return nil
})
}
func registerCommand() *Command {
return newCommand("register", func(s *state.State) error {
if len(s.Arguments) == 0 {
return fmt.Errorf("Did not receive expected username argument")
}
createUserParams := database.CreateUserParams{
ID: uuid.New(),
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
Name: s.Arguments[0],
}
createdUser, err := s.Database.CreateUser(context.Background(), createUserParams)
if err != nil {
return err
}
s.Config.SetUser(createdUser.Name)
fmt.Printf("Successfully registered user '%s':\n", createdUser.Name)
fmt.Printf(" Id => %s\n", createdUser.ID)
fmt.Printf(" CreatedAt => %s\n", createdUser.CreatedAt)
fmt.Printf(" UpdatedAt => %s\n", createdUser.UpdatedAt)
fmt.Printf("Current user set to user '%s':\n", createdUser.Name)
return nil
})
}
func resetCommand() *Command {
return newCommand("reset", func(s *state.State) error {
err := s.Database.DeleteAllUsers(context.Background())
if err != nil {
return err
}
fmt.Println("Successfully delete all users")
return nil
})
}
func usersCommand() *Command {
return newCommand("users", func(s *state.State) error {
users, err := s.Database.GetAllUsers(context.Background())
if err != nil {
return err
}
slices.SortFunc(users, func(a, b database.User) int {
return cmp.Compare(a.Name, b.Name)
})
for _, user := range users {
msg := "* %s"
if user.Name == s.Config.CurrentUserName {
msg += " (current)"
}
fmt.Printf(msg+"\n", user.Name)
}
return nil
})
}
func aggCommand() *Command {
return newCommand("agg", func(s *state.State) error {
// if len(s.Arguments) == 0 {
// return fmt.Errorf("Did not receive expected feed argument")
// }
// feed := s.Arguments[0]
//
// validUrl, err := url.Parse(feed)
//
// if err != nil {
// return fmt.Errorf("Feed '%s' is not a valid url", feed)
// }
rssFeed, err := rss.FetchFeed(context.Background(), "https://www.wagslane.dev/index.xml")
if err != nil {
return err
}
fmt.Printf("%v\n", rssFeed)
return nil
})
}
func addFeedCommand() *Command {
return newCommand("addfeed", func(s *state.State) error {
if len(s.Arguments) < 2 {
return fmt.Errorf("Did not receive expected feed name and url")
}
feedName := s.Arguments[0]
feedUrl := s.Arguments[1]
validatedFeedUrl, err := url.Parse(feedUrl)
if strings.TrimSpace(feedName) == "" {
return fmt.Errorf("Feed name cannot be empty")
}
if err != nil {
return fmt.Errorf("Feed url '%s' is not a valid url", feedUrl)
}
currentUser, err := s.GetCurrentUser(context.Background())
if err != nil {
return fmt.Errorf("Currently logged in user does not exist. Cannot add feed for non-existent user.")
}
createFeedParams := database.CreateFeedParams{
ID: uuid.New(),
UserID: currentUser.ID,
Name: feedName,
Url: validatedFeedUrl.String(),
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
createdFeed, err := s.Database.CreateFeed(context.Background(), createFeedParams)
if err != nil {
return err
}
fmt.Printf("Successfully added feed '%s' with url '%s'\n", createdFeed.Name, createdFeed.Url)
fmt.Printf(" Id => %s\n", createdFeed.ID)
fmt.Printf(" UserId => %s\n", createdFeed.UserID)
fmt.Printf(" CreatedAt => %s\n", createdFeed.CreatedAt)
fmt.Printf(" UpdatedAt => %s\n", createdFeed.UpdatedAt)
return nil
})
}
func feedsCommand() *Command {
return newCommand("feeds", func(s *state.State) error {
feeds, err := s.Database.GetAllFeeds(context.Background())
if err != nil {
return err
}
for _, feed := range feeds {
feedsUserName := "Unknown"
user, err := s.Database.GetUserById(context.Background(), feed.UserID)
if err == nil {
feedsUserName = user.Name
}
fmt.Printf("* %s [%s] (%s)\n", feed.Name, feed.Url, feedsUserName)
}
return nil
})
}
+31
View File
@@ -0,0 +1,31 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.31.1
package database
import (
"context"
"database/sql"
)
type DBTX interface {
ExecContext(context.Context, string, ...interface{}) (sql.Result, error)
PrepareContext(context.Context, string) (*sql.Stmt, error)
QueryContext(context.Context, string, ...interface{}) (*sql.Rows, error)
QueryRowContext(context.Context, string, ...interface{}) *sql.Row
}
func New(db DBTX) *Queries {
return &Queries{db: db}
}
type Queries struct {
db DBTX
}
func (q *Queries) WithTx(tx *sql.Tx) *Queries {
return &Queries{
db: tx,
}
}
+90
View File
@@ -0,0 +1,90 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.31.1
// source: feeds.sql
package database
import (
"context"
"time"
"github.com/google/uuid"
)
const createFeed = `-- name: CreateFeed :one
INSERT INTO feeds (id, created_at, updated_at, name, url, user_id)
VALUES (
$1,
$2,
$3,
$4,
$5,
$6
)
RETURNING id, created_at, updated_at, name, url, user_id
`
type CreateFeedParams struct {
ID uuid.UUID
CreatedAt time.Time
UpdatedAt time.Time
Name string
Url string
UserID uuid.UUID
}
func (q *Queries) CreateFeed(ctx context.Context, arg CreateFeedParams) (Feed, error) {
row := q.db.QueryRowContext(ctx, createFeed,
arg.ID,
arg.CreatedAt,
arg.UpdatedAt,
arg.Name,
arg.Url,
arg.UserID,
)
var i Feed
err := row.Scan(
&i.ID,
&i.CreatedAt,
&i.UpdatedAt,
&i.Name,
&i.Url,
&i.UserID,
)
return i, err
}
const getAllFeeds = `-- name: GetAllFeeds :many
SELECT id, created_at, updated_at, name, url, user_id FROM feeds
`
func (q *Queries) GetAllFeeds(ctx context.Context) ([]Feed, error) {
rows, err := q.db.QueryContext(ctx, getAllFeeds)
if err != nil {
return nil, err
}
defer rows.Close()
var items []Feed
for rows.Next() {
var i Feed
if err := rows.Scan(
&i.ID,
&i.CreatedAt,
&i.UpdatedAt,
&i.Name,
&i.Url,
&i.UserID,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
+27
View File
@@ -0,0 +1,27 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.31.1
package database
import (
"time"
"github.com/google/uuid"
)
type Feed struct {
ID uuid.UUID
CreatedAt time.Time
UpdatedAt time.Time
Name string
Url string
UserID uuid.UUID
}
type User struct {
ID uuid.UUID
CreatedAt time.Time
UpdatedAt time.Time
Name string
}
+123
View File
@@ -0,0 +1,123 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.31.1
// source: users.sql
package database
import (
"context"
"time"
"github.com/google/uuid"
)
const createUser = `-- name: CreateUser :one
INSERT INTO users (id, created_at, updated_at, name)
VALUES (
$1,
$2,
$3,
$4
)
RETURNING id, created_at, updated_at, name
`
type CreateUserParams struct {
ID uuid.UUID
CreatedAt time.Time
UpdatedAt time.Time
Name string
}
func (q *Queries) CreateUser(ctx context.Context, arg CreateUserParams) (User, error) {
row := q.db.QueryRowContext(ctx, createUser,
arg.ID,
arg.CreatedAt,
arg.UpdatedAt,
arg.Name,
)
var i User
err := row.Scan(
&i.ID,
&i.CreatedAt,
&i.UpdatedAt,
&i.Name,
)
return i, err
}
const deleteAllUsers = `-- name: DeleteAllUsers :exec
DELETE FROM users
`
func (q *Queries) DeleteAllUsers(ctx context.Context) error {
_, err := q.db.ExecContext(ctx, deleteAllUsers)
return err
}
const getAllUsers = `-- name: GetAllUsers :many
SELECT id, created_at, updated_at, name FROM users
`
func (q *Queries) GetAllUsers(ctx context.Context) ([]User, error) {
rows, err := q.db.QueryContext(ctx, getAllUsers)
if err != nil {
return nil, err
}
defer rows.Close()
var items []User
for rows.Next() {
var i User
if err := rows.Scan(
&i.ID,
&i.CreatedAt,
&i.UpdatedAt,
&i.Name,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getUserById = `-- name: GetUserById :one
SELECT id, created_at, updated_at, name FROM users
WHERE id = $1
`
func (q *Queries) GetUserById(ctx context.Context, id uuid.UUID) (User, error) {
row := q.db.QueryRowContext(ctx, getUserById, id)
var i User
err := row.Scan(
&i.ID,
&i.CreatedAt,
&i.UpdatedAt,
&i.Name,
)
return i, err
}
const getUserByName = `-- name: GetUserByName :one
SELECT id, created_at, updated_at, name FROM users
WHERE name = $1
`
func (q *Queries) GetUserByName(ctx context.Context, name string) (User, error) {
row := q.db.QueryRowContext(ctx, getUserByName, name)
var i User
err := row.Scan(
&i.ID,
&i.CreatedAt,
&i.UpdatedAt,
&i.Name,
)
return i, err
}
+61
View File
@@ -0,0 +1,61 @@
package rss
import (
"context"
"encoding/xml"
"html"
"net/http"
)
type RSSItem struct {
Title string `xml:"title"`
Link string `xml:"link"`
Description string `xml:"description"`
PubDate string `xml:"pubDate"`
}
type RSSChannel struct {
Title string `xml:"title"`
Link string `xml:"link"`
Description string `xml:"description"`
Item []RSSItem `xml:"item"`
}
type RSSFeed struct {
Channel RSSChannel `xml:"channel"`
}
func FetchFeed(ctx context.Context, feedURL string) (*RSSFeed, error) {
var rssFeed RSSFeed
req, err := http.NewRequestWithContext(ctx, http.MethodGet, feedURL, nil)
req.Header.Set("User-Agent", "gator")
if err != nil {
return nil, err
}
res, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer res.Body.Close()
err = xml.NewDecoder(res.Body).Decode(&rssFeed)
if err != nil {
return nil, err
}
rssFeed.Channel.Title = html.UnescapeString(rssFeed.Channel.Title)
rssFeed.Channel.Description = html.UnescapeString(rssFeed.Channel.Description)
for _, item := range rssFeed.Channel.Item {
item.Title = html.UnescapeString(item.Title)
item.Description = html.UnescapeString(item.Description)
}
return &rssFeed, nil
}
+13 -3
View File
@@ -1,12 +1,22 @@
package state
import "github.com/StevanFreeborn/gator/internal/config"
import (
"context"
"github.com/StevanFreeborn/gator/internal/config"
"github.com/StevanFreeborn/gator/internal/database"
)
type State struct {
Database *database.Queries
Config *config.Config
Arguments []string
}
func NewState(c *config.Config, args []string) *State {
return &State{Config: c, Arguments: args}
func NewState(d *database.Queries, c *config.Config, args []string) *State {
return &State{Database: d, Config: c, Arguments: args}
}
func (s *State) GetCurrentUser(ctx context.Context) (database.User, error) {
return s.Database.GetUserByName(ctx, s.Config.CurrentUserName)
}