From c1f7e8ae9105b472ba28bb19dcae720d7bc21912 Mon Sep 17 00:00:00 2001 From: Brian Buller Date: Wed, 14 Oct 2020 16:59:41 -0500 Subject: [PATCH] Basically Functional --- .gitignore | 1 + build.sh | 5 +- cmd/ncpt/app.go | 96 ++++++++++--- cmd/ncpt/app_cmds.go | 1 + cmd/ncpt/app_command.go | 4 - cmd/ncpt/app_quest_cmds.go | 78 ++++++++++- cmd/ncpt/app_ship_cmds.go | 67 +++++++++ cmd/ncpt/main.go | 6 +- cmd/ncpt/quest.go | 7 - cmd/ncpt/ship.go | 5 - {cmd/ncpt => internal/cli}/helpers.go | 22 ++- internal/data/quest.go | 128 +++++++++++++++++ internal/data/ship.go | 189 ++++++++++++++++++++++++++ internal/web/helpers.go | 12 ++ 14 files changed, 571 insertions(+), 50 deletions(-) create mode 100644 cmd/ncpt/app_cmds.go delete mode 100644 cmd/ncpt/app_command.go create mode 100644 cmd/ncpt/app_ship_cmds.go delete mode 100644 cmd/ncpt/quest.go delete mode 100644 cmd/ncpt/ship.go rename {cmd/ncpt => internal/cli}/helpers.go (67%) create mode 100644 internal/data/quest.go create mode 100644 internal/data/ship.go create mode 100644 internal/web/helpers.go diff --git a/.gitignore b/.gitignore index 1c420ab..1c84a05 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ # Dev Testing /.ncpt # Build Artifacts +/build/ /ncpt diff --git a/build.sh b/build.sh index 8df5e59..c7dbd02 100755 --- a/build.sh +++ b/build.sh @@ -9,4 +9,7 @@ trap 'echo "\"${last_command}\" command finished with exit code $?."' EXIT # Build chroniclerd binary cd cmd/ncpt go build -o ncpt -mv ncpt ../../ +if [ ! -d ../../build ]; then + mkdir ../../build +fi +mv ncpt ../../build diff --git a/cmd/ncpt/app.go b/cmd/ncpt/app.go index 9c63600..521303d 100644 --- a/cmd/ncpt/app.go +++ b/cmd/ncpt/app.go @@ -6,11 +6,20 @@ import ( "fmt" "os" "strings" + + "git.bullercodeworks.com/brian/netcaptain/internal/cli" + "git.bullercodeworks.com/brian/netcaptain/internal/data" ) const ( MODE_RUN = iota + MODE_VALIDATE_QUEST + MODE_VALIDATE_SHIP MODE_EDIT_QUEST + MODE_EDIT_SHIP + MODE_EDIT_HOLD + MODE_LIST_QUESTS + MODE_LIST_SHIPS ) type AppState struct { @@ -18,14 +27,15 @@ type AppState struct { Version int ConfDir string Parms []string - Ships map[string]Ship - Quests map[string]Quest + Verbose bool Mode int - // CLI Variables - Environment string - Request string + QuestsDir string + Quest *data.Quest + ShipsDir string + Ship *data.Ship + HoldsDir string Error error } @@ -38,47 +48,91 @@ func NewApp() *AppState { func (a *AppState) initialize() error { flag.StringVar(&a.ConfDir, "d", ".ncpt", "The configuration directory to use") - editMode := flag.Bool("e", false, "Edit the named Quest") + flag.BoolVar(&a.Verbose, "v", false, "Run in Verbose mode") + validateQuest := flag.String("c", "", "Validate the given quest is _c_orrect") + validateShip := flag.String("C", "", "Validate the given ship is _C_orrect") + editQuest := flag.String("e", "", "Edit the given quest") + editShip := flag.String("E", "", "Edit the given ship") + listQuestsMode := flag.Bool("l", false, "List all available Quests") + listShipsMode := flag.Bool("L", false, "List all available Ships") + editHold := flag.String("H", "", "Edit the hold for the given ship") + flag.Parse() a.Parms = flag.Args() - // Figure out what 'mode' we're running in - if *editMode { - a.Mode = MODE_EDIT_QUEST - } else { - a.Mode = MODE_RUN - } - - a.Ships = make(map[string]Ship) - a.Quests = make(map[string]Quest) if err := a.validateConfDir(); err != nil { return err } + a.QuestsDir = a.ConfDir + "/quests" + a.ShipsDir = a.ConfDir + "/ships" + a.HoldsDir = a.ConfDir + "/holds" + + // Figure out what 'mode' we're running in + if *validateQuest != "" { + a.Mode = MODE_VALIDATE_QUEST + a.Quest = data.NewQuest(a.ConfDir, *validateQuest) + } else if *validateShip != "" { + a.Mode = MODE_VALIDATE_SHIP + a.Ship = data.NewShip(a.ConfDir, *validateShip) + } else if *editQuest != "" { + a.Mode = MODE_EDIT_QUEST + a.Quest = data.NewQuest(a.ConfDir, *editQuest) + } else if *editShip != "" { + a.Mode = MODE_EDIT_SHIP + a.Ship = data.NewShip(a.ConfDir, *editShip) + } else if *listQuestsMode { + a.Mode = MODE_LIST_QUESTS + } else if *listShipsMode { + a.Mode = MODE_LIST_SHIPS + } else if *editHold != "" { + a.Mode = MODE_EDIT_HOLD + a.Ship = data.NewShip(a.ConfDir, *editHold) + } else { + a.Mode = MODE_RUN + } // Load Global Configs return nil } func (a *AppState) run() int { - if len(a.Parms) == 0 { - fmt.Fprint(os.Stderr, "No request provided ('list' to list available requests)\n") - return 1 - } switch a.Mode { + case MODE_VALIDATE_QUEST: + return a.validateQuest(a.Parms) + case MODE_VALIDATE_SHIP: + return a.validateShip(a.Parms) case MODE_EDIT_QUEST: return a.editQuest(a.Parms) + case MODE_EDIT_SHIP: + return a.editShip(a.Parms) + case MODE_EDIT_HOLD: + return a.editHold(a.Parms) + case MODE_LIST_QUESTS: + return a.listQuests(a.Parms) + case MODE_LIST_SHIPS: + return a.listShips(a.Parms) + default: return a.runQuest(a.Parms) } } func (a *AppState) validateConfDir() error { - if !DirExists(a.ConfDir) { - ans := PromptWDefault(fmt.Sprintf("Create configuration directory (%s)? [n]", a.ConfDir), "n") + if !cli.DirExists(a.ConfDir) { + ans := cli.PromptWDefault(fmt.Sprintf("Create configuration directories (%s)? [n]", a.ConfDir), "n") if strings.ToLower(ans) == "y" { if err := os.Mkdir(a.ConfDir, 0700); err != nil { return err } + if err := os.Mkdir(a.QuestsDir, 0700); err != nil { + return err + } + if err := os.Mkdir(a.ShipsDir, 0700); err != nil { + return err + } + if err := os.Mkdir(a.HoldsDir, 0700); err != nil { + return err + } } else { return errors.New("Configuration directory doesn't exist") } diff --git a/cmd/ncpt/app_cmds.go b/cmd/ncpt/app_cmds.go new file mode 100644 index 0000000..06ab7d0 --- /dev/null +++ b/cmd/ncpt/app_cmds.go @@ -0,0 +1 @@ +package main diff --git a/cmd/ncpt/app_command.go b/cmd/ncpt/app_command.go deleted file mode 100644 index 08bd98e..0000000 --- a/cmd/ncpt/app_command.go +++ /dev/null @@ -1,4 +0,0 @@ -package main - -type Command interface { -} diff --git a/cmd/ncpt/app_quest_cmds.go b/cmd/ncpt/app_quest_cmds.go index 4fb2533..62202a3 100644 --- a/cmd/ncpt/app_quest_cmds.go +++ b/cmd/ncpt/app_quest_cmds.go @@ -4,23 +4,89 @@ import ( "fmt" "os" "os/exec" + + "git.bullercodeworks.com/brian/netcaptain/internal/cli" + "git.bullercodeworks.com/brian/netcaptain/internal/data" ) -func (a *AppState) runQuest(parms []string) int { +func (a *AppState) listQuests(parms []string) int { + files := cli.ListFiles(a.QuestsDir) + for _, v := range files { + fmt.Println(v) + } return 0 } -func (a *AppState) editQuest(parms []string) int { - if len(parms) == 0 { - fmt.Println("Quest name must be provided.") +func (a *AppState) validateQuest(parms []string) int { + if a.Quest == nil { + cli.PrintErr("No quest given") return 1 } - cmd := exec.Command(os.Getenv("EDITOR"), fmt.Sprintf("%s/%s", a.ConfDir, parms[0])) + a.Quest.LoadArgs(parms) + if err := a.Quest.Load(); err != nil { + cli.PrintErr(fmt.Sprintf("Error loading quest: %s", err.Error())) + return 1 + } + if !a.Quest.IsValid() { + cli.PrintErr("Quest is not valid") + return 1 + } + return 0 +} + +func (a *AppState) runQuest(parms []string) int { + if len(parms) == 0 { + cli.PrintErr("No Quest Given") + return 1 + } + a.Quest = data.NewQuest(a.ConfDir, parms[0]) + a.Quest.Verbose = a.Verbose + parms = parms[1:] + var ships []string + if len(parms) > 1 { + ships = append(ships, parms[0]) + } + ships = append(ships, "default") + for _, v := range ships { + s := data.NewShip(a.ConfDir, v) + if s.Exists() { + a.Ship = s + a.Ship.Verbose = a.Verbose + if len(parms) > 0 && parms[0] == v { + parms = parms[1:] + } + a.Ship.LoadArgs(parms) + err := a.Ship.Load() + if err != nil { + cli.PrintErr(err.Error()) + return 1 + } + break + } + } + if a.Ship != nil { + return a.Ship.Sail(a.Quest) + } + a.Quest.LoadArgs(parms) + err := a.Quest.Load() + if err != nil { + cli.PrintErr(err.Error()) + return 1 + } + return a.Quest.Complete() +} + +func (a *AppState) editQuest(parms []string) int { + if a.Quest == nil { + cli.PrintErr("Quest name must be provided.") + return 1 + } + cmd := exec.Command(os.Getenv("EDITOR"), a.Quest.FilePath()) cmd.Stdin = os.Stdin cmd.Stdout = os.Stdout err := cmd.Run() if err != nil { - fmt.Printf("Error editing Quest (%s)", parms[0]) + cli.PrintErr(fmt.Sprintf("Error editing Quest (%s)", a.Quest.Name)) } return 0 } diff --git a/cmd/ncpt/app_ship_cmds.go b/cmd/ncpt/app_ship_cmds.go new file mode 100644 index 0000000..2ebdfa3 --- /dev/null +++ b/cmd/ncpt/app_ship_cmds.go @@ -0,0 +1,67 @@ +package main + +import ( + "fmt" + "os" + "os/exec" + "strings" + + "git.bullercodeworks.com/brian/netcaptain/internal/cli" +) + +func (a *AppState) listShips(parms []string) int { + files := cli.ListFiles(a.ShipsDir) + for _, v := range files { + fmt.Println(strings.TrimSuffix(v, ".json")) + } + return 0 +} + +func (a *AppState) validateShip(parms []string) int { + if a.Ship == nil { + cli.PrintErr("No ship given") + return 1 + } + a.Ship.LoadArgs(parms) + if err := a.Ship.Load(); err != nil { + cli.PrintErr(fmt.Sprintf("Error loading ship: %s", err.Error())) + return 1 + } + if !a.Ship.IsValid() { + cli.PrintErr("Ship is not valid") + return 1 + } + return 0 +} + +func (a *AppState) editShip(parms []string) int { + if a.Ship == nil { + cli.PrintErr("Ship name must be provided.") + return 1 + } + cmd := exec.Command(os.Getenv("EDITOR"), a.Ship.FilePath()) + cmd.Stdin = os.Stdin + cmd.Stdout = os.Stdout + err := cmd.Run() + if err != nil { + cli.PrintErr(fmt.Sprintf("Error editing Ship (%s)", a.Ship.Name)) + return 1 + } + return 0 +} + +func (a *AppState) editHold(parms []string) int { + if a.Ship == nil { + cli.PrintErr("Ship name must be provided.") + return 1 + } + cmd := exec.Command(os.Getenv("EDITOR"), a.Ship.HoldFilePath()) + cmd.Stdin = os.Stdin + cmd.Stdout = os.Stdout + err := cmd.Run() + if err != nil { + cli.PrintErr(fmt.Sprintf("Error editing Hold (%s)", a.Ship.Name)) + return 1 + } + return 0 +} diff --git a/cmd/ncpt/main.go b/cmd/ncpt/main.go index 0ea7754..cc85ffb 100644 --- a/cmd/ncpt/main.go +++ b/cmd/ncpt/main.go @@ -1,8 +1,9 @@ package main import ( - "fmt" "os" + + "git.bullercodeworks.com/brian/netcaptain/internal/cli" ) const ( @@ -13,10 +14,9 @@ const ( var app *AppState func main() { - fmt.Println("ncpt - NetCaptain") app = NewApp() if app.Error != nil { - PrintErr(app.Error.Error()) + cli.PrintErr(app.Error.Error()) os.Exit(1) } os.Exit(app.run()) diff --git a/cmd/ncpt/quest.go b/cmd/ncpt/quest.go deleted file mode 100644 index d7897a4..0000000 --- a/cmd/ncpt/quest.go +++ /dev/null @@ -1,7 +0,0 @@ -package main - -type Quest struct { - Method string - Url string - Headers map[string]string -} diff --git a/cmd/ncpt/ship.go b/cmd/ncpt/ship.go deleted file mode 100644 index 417ba12..0000000 --- a/cmd/ncpt/ship.go +++ /dev/null @@ -1,5 +0,0 @@ -package main - -type Ship struct { - Name string -} diff --git a/cmd/ncpt/helpers.go b/internal/cli/helpers.go similarity index 67% rename from cmd/ncpt/helpers.go rename to internal/cli/helpers.go index 81bf40e..b1c98c0 100644 --- a/cmd/ncpt/helpers.go +++ b/internal/cli/helpers.go @@ -1,9 +1,10 @@ -package main +package cli import ( "bufio" "fmt" "os" + "path/filepath" "strings" ) @@ -44,6 +45,21 @@ func DirExists(path string) bool { return info.IsDir() } -func PrintErr(o string) { - fmt.Fprint(os.Stderr, o) +func ListFiles(path string) []string { + var files []string + err := filepath.Walk(path, func(p string, info os.FileInfo, err error) error { + p = strings.TrimPrefix(p, path) + if p != "" { + files = append(files, strings.TrimPrefix(p, "/")) + } + return nil + }) + if err != nil { + return []string{} + } + return files +} + +func PrintErr(o string) { + fmt.Fprintln(os.Stderr, o) } diff --git a/internal/data/quest.go b/internal/data/quest.go new file mode 100644 index 0000000..2541703 --- /dev/null +++ b/internal/data/quest.go @@ -0,0 +1,128 @@ +package data + +import ( + "bytes" + "errors" + "fmt" + "io/ioutil" + "net/http" + "strings" + "text/template" + + "git.bullercodeworks.com/brian/netcaptain/internal/cli" + "git.bullercodeworks.com/brian/netcaptain/internal/web" +) + +type Quest struct { + Name string + Dir string + Verbose bool + Headers map[string]string + Method string + Url string + Body string + Raw string + Processed string + Args map[string]string + loaded bool +} + +func NewQuest(dir, nm string) *Quest { + return &Quest{ + Name: nm, + Dir: dir, + Headers: make(map[string]string), + Args: make(map[string]string), + } +} + +func (q *Quest) Load() error { + if !cli.FileExists(q.FilePath()) { + return errors.New("File Not Found") + } + data, err := ioutil.ReadFile(q.FilePath()) + if err != nil { + return err + } + q.Raw = string(data) + t, err := template.New(q.Name).Parse(q.Raw) + if err != nil { + return errors.New(fmt.Sprintf("Error parsing Quest as template: %s", err.Error())) + } + var tpl bytes.Buffer + err = t.Execute(&tpl, q) + if err != nil { + return errors.New(fmt.Sprintf("Error executing Quest template: %s\n->%s", q.Name, err.Error())) + } + q.Processed = tpl.String() + q.loaded = true + return nil +} + +func (q *Quest) LoadArgs(parms []string) { + for k, v := range parms { + q.Args[fmt.Sprintf("Arg%d", k)] = v + } +} + +func (q *Quest) FilePath() string { + return fmt.Sprintf("%s/quests/%s", q.Dir, q.Name) +} + +func (q *Quest) IsValid() bool { + return q.loaded +} + +func (q *Quest) request() *http.Request { + headers := make(map[string][]string) + var method, url, body string + for _, v := range strings.Split(q.Processed, "\n") { + flds := strings.Fields(v) + if len(flds) > 0 { + if web.IsHttpMethod(flds[0]) { + // This is the request line + method = flds[0] + url = flds[1] + } else if method == "" && len(flds) > 1 { + // Headers + k := strings.TrimSuffix(flds[0], ":") + v := strings.Join(flds[1:], " ") + headers[k] = append(headers[k], v) + } else { + // Body + if body != "" { + body = body + "\n" + } + body = body + v + } + } + } + + request, _ := http.NewRequest(method, url, bytes.NewBuffer([]byte(body))) + for k, v := range headers { + request.Header[k] = v + } + return request +} + +func (q *Quest) Completer() (*http.Response, error) { + client := &http.Client{} + return client.Do(q.request()) +} + +func (q *Quest) Complete() int { + resp, _ := q.Completer() + if q.Verbose { + for k, v := range resp.Header { + fmt.Println(k, v) + } + } + defer resp.Body.Close() + rbody, err := ioutil.ReadAll(resp.Body) + if err != nil { + cli.PrintErr(err.Error()) + return 1 + } + fmt.Println(string(rbody)) + return 0 +} diff --git a/internal/data/ship.go b/internal/data/ship.go new file mode 100644 index 0000000..ded63fe --- /dev/null +++ b/internal/data/ship.go @@ -0,0 +1,189 @@ +package data + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "io/ioutil" + "os" + "text/template" + + "git.bullercodeworks.com/brian/netcaptain/internal/cli" +) + +type Ship struct { + Name string `json:"-"` + Dir string `json:"-"` + Verbose bool `json:"-"` + Crew map[string]string `json:"crew"` // Static Members of the Ship + PlunderRules map[string]map[string]string `json:"plunder"` // Definitions for storing dynamic values + Hold map[string]string `json:"-"` // Where dynamic values are stored + Args map[string]string `json:"-"` // Arguments passed in from the command line + Raw []byte `json:"-"` + loaded bool `json:"-"` +} + +func NewShip(dir, nm string) *Ship { + return &Ship{ + Name: nm, + Dir: dir, + Crew: make(map[string]string), + PlunderRules: make(map[string]map[string]string), + Args: make(map[string]string), + Hold: make(map[string]string), + } +} + +func (s *Ship) Load() error { + if !s.Exists() { + return errors.New("Ship File not Found") + } + var err error + s.Raw, err = ioutil.ReadFile(s.FilePath()) + if err != nil { + return err + } + // Parse 'Raw' into Crew and Plunder + json.Unmarshal(s.Raw, &s) + for quest, questRules := range s.PlunderRules { + for key, val := range questRules { + ruleName := quest + "." + key + t, err := template.New(ruleName).Parse(val) + if err != nil { + return errors.New(fmt.Sprintf("Error parsing plunder rule: %s", ruleName)) + } + var tpl bytes.Buffer + err = t.Execute(&tpl, s) + if err != nil { + return errors.New(fmt.Sprintf("Error executing plunder rule: %s\n->%s", ruleName, err.Error())) + } + } + } + err = s.LoadHold() + if err != nil { + return err + } + + s.loaded = true + return nil +} + +func (s *Ship) LoadArgs(parms []string) { + for k, v := range parms { + s.Args[fmt.Sprintf("Arg%d", k)] = v + } +} + +func (s *Ship) Exists() bool { + return cli.FileExists(s.FilePath()) +} + +func (s *Ship) HoldExists() bool { + return cli.FileExists(s.HoldFilePath()) +} + +func (s *Ship) FilePath() string { + return fmt.Sprintf("%s/ships/%s.json", s.Dir, s.Name) +} + +func (s *Ship) HoldFilePath() string { + return fmt.Sprintf("%s/holds/%s.json", s.Dir, s.Name) +} + +func (s *Ship) IsValid() bool { + return s.loaded +} + +func (s *Ship) Sail(q *Quest) int { + // Set up the Quest + if !cli.FileExists(q.FilePath()) { + cli.PrintErr(fmt.Sprintf("Quest file (%s) not found", q.Name)) + return 1 + } + data, err := ioutil.ReadFile(q.FilePath()) + if err != nil { + cli.PrintErr(fmt.Sprintf("Error reading quest file (%s)", q.Name)) + return 1 + } + q.Raw = string(data) + t, err := template.New(s.Name + "." + q.Name).Parse(q.Raw) + if err != nil { + cli.PrintErr(fmt.Sprintf("Error parsing Quest as template: %s", err.Error())) + return 1 + } + var tpl bytes.Buffer + err = t.Execute(&tpl, s) + if err != nil { + cli.PrintErr(fmt.Sprintf("Error executing Quest template: %s\n->%s", q.Name, err.Error())) + return 1 + } + q.Processed = tpl.String() + q.loaded = true + + resp, err := q.Completer() + if err != nil { + cli.PrintErr(err.Error()) + return 1 + } + defer resp.Body.Close() + rbody, err := ioutil.ReadAll(resp.Body) + if err != nil { + cli.PrintErr(err.Error()) + return 1 + } + // Check if we need to plunder from this quest + if err = s.Plunder(q, rbody); err != nil { + cli.PrintErr(err.Error()) + return 1 + } + fmt.Println(string(rbody)) + err = s.SaveHold() + if err != nil { + cli.PrintErr(err.Error()) + return 1 + } + + return 0 +} + +func (s *Ship) Plunder(q *Quest, body []byte) error { + plunder, ok := s.PlunderRules[q.Name] + if !ok { + return nil + } + for k, v := range plunder { + buff := bytes.NewBuffer(body) + var result map[string]interface{} + json.NewDecoder(buff).Decode(&result) + switch val := result[v].(type) { + case string: + s.Hold[k] = val + } + } + + return nil +} + +/* Hold Functions */ +func (s *Ship) LoadHold() error { + hold, err := ioutil.ReadFile(s.HoldFilePath()) + if err != nil { + f, err := os.OpenFile(s.HoldFilePath(), os.O_CREATE|os.O_WRONLY, 0644) + defer f.Close() + return err + } + json.NewDecoder(bytes.NewBuffer(hold)).Decode(&s.Hold) + return nil +} + +func (s *Ship) SaveHold() error { + f, err := os.OpenFile(s.HoldFilePath(), os.O_RDWR|os.O_TRUNC, 0644) + if err != nil { + return err + } + defer f.Close() + enc := json.NewEncoder(f) + enc.Encode(s.Hold) + return nil +} diff --git a/internal/web/helpers.go b/internal/web/helpers.go new file mode 100644 index 0000000..93bae40 --- /dev/null +++ b/internal/web/helpers.go @@ -0,0 +1,12 @@ +package web + +import "net/http" + +func IsHttpMethod(val string) bool { + switch val { + case http.MethodGet, http.MethodHead, http.MethodPost, http.MethodPut, http.MethodPatch, http.MethodDelete, http.MethodConnect, http.MethodOptions, http.MethodTrace: + return true + default: + return false + } +}