package datamaps
import (
"log"
"os"
"path/filepath"
)
const (
configDirName = "datamaps"
dbName = "datamaps.db"
)
// Help text.
const Usage = `
usage: datamaps COMMAND [OPTIONS]
datamaps will NOT create configuration files automatically. Ensure this is handled by
calling: "datamaps setup". This will create the database file required to run the
application.
-Managing datamap files-
Command: datamap
All datamap related actions. Before datamaps can do much, it must know about a datamap.
A datamap starts life as a CSV file with the format:
cell_key,template_sheet,cell_reference,type
key1,sheet1,A10,TEXT
key2,sheet1,A11,NUMBER
...
Options:
--import PATH Import a datamap the csv datamap at PATH
--datamapname NAME Name for imported datamap
`
// mocking funcs in go https://stackoverflow.com/questions/19167970/mock-functions-in-go
// we only need the func signature to create the type. This is pretty weird, we're want to mock
// os.UserHomeDir so that we can set it to something like /tmp in our tests. Here we are creating
// two types: getUserConfigDir to represent the os.UserConfigDir function, dbPathChecker as a wrapper
// which which we can assign methods to that holds the value of the func os.UserConfigDir and the
// method, check(), which does the work, using the passed in func to determine the user $HOME/.config
// directory.
// Which is a lot of work for what it is, but it does make this testable and serves as an example
// of how things could be done in Go.
// getUserConfigDir allows replaces os.UserConfigDir
// for testing purposes.
type getUserConfigDir func() (string, error)
// DBPathChecker contains the func used to create the user config dir.
type DBPathChecker struct {
userConfig getUserConfigDir
}
// NewDBPathChecker creates a DBPathChecker using whatever
// func you want as the argument, as long as it matches the
// type os.UserConfigDir. This makes it convenient for testing
// and was done as an experiment here to practice mocking in Go.
func NewDBPathChecker(h getUserConfigDir) *DBPathChecker {
return &DBPathChecker{userConfig: h}
}
// Check returns true if the necessary config files (including
// the database) are in place - false if not
func (db *DBPathChecker) Check() bool {
userConfig, err := db.userConfig()
if err != nil {
log.Fatal(err)
}
dbPath := filepath.Join(userConfig, "datamaps", "datamaps.db")
if _, err := os.Stat(dbPath); os.IsNotExist(err) {
println("Seems that " + dbPath + " not there")
return false
}
return true
}
// SetUp creates the config directory and requisite files
func SetUp() (string, error) {
dir, err := os.UserConfigDir()
if err != nil {
return "", err
}
// check if config folder exists
configPath := filepath.Join(dir, configDirName)
dbPath := filepath.Join(configPath, dbName)
if _, err := os.Stat(configPath); os.IsNotExist(err) {
log.Println("Config directory does not exist.")
log.Printf("Creating config directory %s.\n", configPath)
if err := os.Mkdir(filepath.Join(dir, "datamaps"), 0700); err != nil {
return "", err
}
} else {
log.Println("Config directory found.")
}
if _, err := os.Stat(dbPath); os.IsNotExist(err) {
log.Println("Database does not exist.")
_, err = os.Create(dbPath)
if err != nil {
return "", err
}
log.Printf("Creating database file at %s.\n", dbPath)
_, err := setupDB(dbPath)
if err != nil {
return "", err
}
} else {
log.Println("Database file found.")
}
return dir, nil
}
func userConfigDir() (string, error) {
dir, err := os.UserConfigDir()
if err != nil {
return "", err
}
configPath := filepath.Join(dir, configDirName)
return configPath, nil
}
func defaultDMPath() (string, error) {
dir, err := os.UserHomeDir()
if err != nil {
return "", err
}
return filepath.Join(dir, "Documents", "datamaps"), nil
}
func defaultXLSXPath() (string, error) {
dir, err := os.UserHomeDir()
if err != nil {
return "", err
}
return filepath.Join(dir, "Documents", "datamaps", "import"), nil
}
// Options for the whole CLI application.
type Options struct {
// Command is the main CLI sub-command (e.g. "datamap" handles all datamap
// operations and the flags that follow pertain only to that operation.
Command string
// DBPath is the path to the database file.
DBPath string
// DMPath is the path to a datamap file.
DMPath string
// DMNane is the name of a datamap, whether setting or querying.
DMName string
// XLSXPath is the path to a directory containing ".xlsx" files for
// importing.
XLSXPath string
// ReturnName is the name of a Return, whether setting or querying.
ReturnName string
// DMOverwrite is currently not used.
DMOverwrite bool
// DMInitial is currently not used.
DMInitial bool
// MasterOutPutPath is where the master.xlsx file is to be saved
MasterOutPutPath string
}
func defaultOptions() *Options {
dbpath, err := userConfigDir()
if err != nil {
log.Fatalf("Unable to get user config directory %v", err)
}
dmPath, err := defaultDMPath()
if err != nil {
log.Fatalf("unable to get default datamaps directory path: %v", err)
}
xlsxPath, err := defaultXLSXPath()
if err != nil {
log.Fatalf("unable to get default XLSX directory path: %v", err)
}
homeDir, err := os.UserHomeDir()
if err != nil {
log.Fatal("unable to get user home directory")
}
return &Options{
Command: "help",
DBPath: filepath.Join(dbpath, dbName),
DMPath: dmPath,
DMName: "Unnamed Datamap",
XLSXPath: xlsxPath,
ReturnName: "Unnamed Return",
DMOverwrite: false,
DMInitial: false,
MasterOutPutPath: filepath.Join(homeDir, "Desktop"),
}
}
// nextString get the next string in a slice.
func nextString(args []string, i *int, message string) string {
if len(args) > *i+1 {
*i++
} else {
log.Fatal(message)
}
return args[*i]
}
func processOptions(opts *Options, allArgs []string) {
if len(allArgs) == 0 {
allArgs = append(allArgs, "help")
}
switch allArgs[0] {
case "checkdb":
opts.Command = "checkdb"
case "import":
opts.Command = "import"
case "help":
opts.Command = "help"
case "datamap":
opts.Command = "datamap"
case "setup":
opts.Command = "setup"
case "server":
opts.Command = "server"
case "createmaster":
opts.Command = "createmaster"
default:
log.Fatal("No relevant command provided.")
}
restArgs := allArgs[1:]
for i := 0; i < len(allArgs[1:]); i++ {
arg := restArgs[i]
switch arg {
case "--xlsxpath":
opts.XLSXPath = nextString(restArgs, &i, "xlsx directory path required")
case "--returnname":
opts.ReturnName = nextString(restArgs, &i, "return name required")
case "--import":
opts.DMPath = nextString(restArgs, &i, "import path required")
case "--datamapname":
opts.DMName = nextString(restArgs, &i, "datamap name required")
case "--overwrite":
opts.DMOverwrite = true
case "--initial":
opts.DMInitial = true
case "--masteroutputdir":
opts.MasterOutPutPath = nextString(restArgs, &i, "master output directory required")
}
}
}
//ParseOptions for CLI.
func ParseOptions() *Options {
opts := defaultOptions()
processOptions(opts, os.Args[1:])
return opts
}