feat: implemented initial login command

This commit is contained in:
Stevan Freeborn
2026-07-31 08:06:29 -05:00
commit 55a4a1285d
8 changed files with 252 additions and 0 deletions
+19
View File
@@ -0,0 +1,19 @@
# Copyright (c) 2026 Stevan Freeborn
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+3
View File
@@ -0,0 +1,3 @@
# gator
This is a blog aggregator that was built as part of a [boot.dev](https://boot.dev) course.
+3
View File
@@ -0,0 +1,3 @@
module github.com/StevanFreeborn/gator
go 1.26.3
+76
View File
@@ -0,0 +1,76 @@
package command
import (
"fmt"
"github.com/StevanFreeborn/gator/internal/state"
)
type Command struct {
Name string
Handler CommandHandler
}
type CommandHandler func(s *state.State) error
func newCommand(name string, handler CommandHandler) *Command {
return &Command{
Name: name,
Handler: handler,
}
}
type CommandRegistry struct {
commands map[string]*Command
}
func (c *CommandRegistry) register(cmd *Command) error {
c.commands[cmd.Name] = cmd
return nil
}
func NewRegistry() *CommandRegistry {
cr := CommandRegistry{
commands: map[string]*Command{},
}
commands := []*Command{
loginCommand(),
}
for _, cmd := range commands {
cr.register(cmd)
}
return &cr
}
func (c *CommandRegistry) RunCommand(cmdName string, s *state.State) error {
cmd, found := c.commands[cmdName]
if !found {
return fmt.Errorf("No '%s' registered", cmdName)
}
return cmd.Handler(s)
}
func loginCommand() *Command {
return newCommand("login", func(s *state.State) error {
if len(s.Arguments) == 0 {
return fmt.Errorf("Did not receive expected username argument")
}
username := s.Arguments[0]
err := s.Config.SetUser(username)
if err != nil {
return err
}
fmt.Printf("Current user set to '%s'\n", username)
return nil
})
}
+92
View File
@@ -0,0 +1,92 @@
package config
import (
"encoding/json"
"fmt"
"os"
"path"
)
const GATOR_CONFIG_FILE = ".gatorconfig.json"
type Config struct {
DbUrl string `json:"db_url"`
CurrentUserName string `json:"current_user_name"`
}
func Read() (*Config, error) {
config := Config{}
configFilePath, err := getConfigPath()
if err != nil {
return nil, err
}
data, err := os.ReadFile(configFilePath)
if err != nil {
return nil, err
}
err = json.Unmarshal(data, &config)
if err != nil {
return nil, err
}
return &config, nil
}
// TODO: I don't think there should be
// hidden I/O in this method call
func (c *Config) SetUser(currentUserName string) error {
c.CurrentUserName = currentUserName
return writeFile(c)
}
func (c *Config) String() string {
return fmt.Sprintf("Config { DbUrl = %s, CurrentUserName = %s }", c.DbUrl, c.CurrentUserName)
}
func writeFile(config *Config) error {
data, err := json.Marshal(config)
if err != nil {
return err
}
configFilePath, err := getConfigPath()
if err != nil {
return err
}
file, err := os.Create(configFilePath)
if err != nil {
return err
}
defer file.Close()
_, err = file.Write(data)
if err != nil {
return err
}
return nil
}
func getConfigPath() (string, error) {
homeDir, err := os.UserHomeDir()
if err != nil {
return "", err
}
configFilePath := path.Join(homeDir, GATOR_CONFIG_FILE)
return configFilePath, nil
}
+12
View File
@@ -0,0 +1,12 @@
package state
import "github.com/StevanFreeborn/gator/internal/config"
type State struct {
Config *config.Config
Arguments []string
}
func NewState(c *config.Config, args []string) *State {
return &State{Config: c, Arguments: args}
}
+38
View File
@@ -0,0 +1,38 @@
package main
import (
"fmt"
"os"
"github.com/StevanFreeborn/gator/internal/command"
"github.com/StevanFreeborn/gator/internal/config"
"github.com/StevanFreeborn/gator/internal/state"
)
func main() {
if len(os.Args) < 2 {
fmt.Println("Not enough arguments provided. Usage: <command> <args>")
os.Exit(1)
}
c, err := config.Read()
if err != nil {
fmt.Printf("Error reading config file: %s", err)
}
cmd := os.Args[1]
args := os.Args[2:]
s := state.NewState(c, args)
registry := command.NewRegistry()
err = registry.RunCommand(cmd, s)
if err != nil {
fmt.Printf("Error running '%s' command: %s\n", cmd, err)
os.Exit(2)
}
os.Exit(0)
}
+9
View File
@@ -0,0 +1,9 @@
-- TODO: Finish writing SQL migration
-- for users table
-- +goose Up
CREATE TABLE users (
id UUID
);
-- +goose Down
DROP TABLE users;