// parseConf parses a requested file and returns // it in a form of conf structure. func parseConf(file string) *conf { var err error c := &conf{ watch: map[string]func(){}, } // Parse the configuration file.. c.m, err = parseFile(file) log.AssertNil(err) // Extract init tasks. init, err := parseSlice(c.m, initSection) log.AssertNil(err) c.init, err = c.processTasksFn(init, initSection) log.AssertNil(err) // Extract patterns and tasks from watch section of config file. watch, err := parseMap(c.m, watchSection) log.AssertNil(err) for pattern, tasks := range watch { section := watchSection + ":" + pattern // It is used for debug messages. c.watch[pattern], err = c.processTasksFn(tasks, section) log.AssertNil(err) } log.Trace.Printf(`Config file "%s" has been parsed.`, file) return c }
// Main is an entry point of the subcommand (tool). func main(hs []tool.Handler, i int, args tool.Data) { // The first argument in the list is a path. // If it's missing use an empty string instead. p := args.GetDefault(0, "") // Prepare source and destination directory paths. src, err := path.ImportToAbsolute("github.com/colegion/goal/internal/skeleton") log.AssertNil(err) destImp, err := path.CleanImport(p) log.AssertNil(err) dest, err := path.ImportToAbsolute(destImp) log.AssertNil(err) // Make sure the requested import path (dest) does not exist yet. if _, err := os.Stat(dest); !os.IsNotExist(err) { log.Error.Panicf(`Cannot use "%s", such import path already exists.`, destImp) } // Scan the skeleton directory and get a list of directories / files // to be copied / processed. res, err := walk(src) log.AssertNil(err) // Create the directories in destination path. for i := 0; i < len(res.dirs); i++ { err = os.MkdirAll(filepath.Join(dest, res.dirs[i]), 0755) log.AssertNil(err) } // Copy static files to the destination directories. for i := 0; i < len(res.files); i++ { copyFile(res.files[i].absolute, filepath.Join(dest, res.files[i].relative)) } // Process source files and copy to the destination directories. for i := 0; i < len(res.srcs); i++ { copyModifiedFile( res.srcs[i].absolute, filepath.Join(dest, res.srcs[i].relative), [][][]byte{ { []byte("github.com/colegion/goal/internal/skeleton"), []byte(destImp), }, }, ) } log.Info.Printf(info, destImp) }
// main is an entry point of the "run" subcommand (tool). func main(hs []tool.Handler, i int, args tool.Data) { // The first argument in the list is a path. // If it's missing use an empty string instead. p := args.GetDefault(0, "") // Determine import path and absolute path of the project to run. imp, err := path.CleanImport(p) log.AssertNil(err) dir, err := path.ImportToAbsolute(imp) log.AssertNil(err) // Prepare a path of configuration file. cf := filepath.Join(dir, ConfigFile) // Start a user tasks runner and instances controller. go instanceController() // Start a configuration file watcher. go configDaemon(imp, cf) // Show user friendly errors and terminate subprograms // in case of panics. defer func() { channel <- message{ action: "exit", } <-stopped log.Trace.Panicln("Application has been terminated.") }() // Execute all commands from the requested directory. curr, _ := os.Getwd() os.Chdir(dir) // pushd defer func() { // Going back to the initial directory. os.Chdir(curr) // popd }() // Load the configuration. reloadConfig() // Cleaning up after we are done. signal.Notify(notify, os.Interrupt, syscall.SIGTERM) <-notify }
// NewType reads the requested template and returns an output.Type // with initialized Template field. << and >> are used as delimiters. // "\" + "\n" sequences are removed from the template so newline // elision is supported. Moreover, ":" + "\t" are removed too for // a possibility of a better code formatting. func NewType(pkg, templatePath string) Type { // Read the template file, cut all "\" + line break. f, err := ioutil.ReadFile(templatePath) log.AssertNil(err) s := strings.Replace(string(f), "\\\r\n", "", -1) s = strings.Replace(s, "\\\n", "", -1) s = strings.Replace(s, "\\\r", "", -1) s = strings.Replace(s, ":\t", "", -1) // Allocate a new type, initialize template, then return. // Use <@ and > as delimiters, add template helper functions. n := filepath.Base(templatePath) t, err := template.New(n).Delims("<@", ">").Funcs(funcs).Parse(s) log.AssertNil(err) return Type{ Package: pkg, TemplateName: n, Template: t, } }
// processPackage gets an import path of a package, processes it, and // extracts controllers + actions. func (ps packages) processPackage(importPath string) { log.Trace.Printf(`Parsing "%s"...`, importPath) dir, err := path.ImportToAbsolute(importPath) log.AssertNil(err) p := reflect.ParseDir(dir, false) cs := ps.extractControllers(p) if len(cs.data) > 0 { ps[importPath] = controllers{ data: cs.data, init: ps.extractInitFunc(p), } } }
// copyFile reads a source file and copies it to the destination. // It doesn't check whether input source parameter is a regular file // rather than a directory. func copyFile(src, dst string) { // Open source file. sf, err := os.Open(src) log.AssertNil(err) defer sf.Close() // Make sure the file will be closed. // Get the meta info of the source file. info, err := sf.Stat() log.AssertNil(err) // Create a destination file. df, err := os.Create(dst) log.AssertNil(err) defer df.Close() // Make sure it will be closed at the end. // Copy the content of source to destination. _, err = io.Copy(df, sf) log.AssertNil(err) // Set the chmod of source to destination file. err = os.Chmod(dst, info.Mode()) log.AssertNil(err) }
// copyModifiedFile is similar to copyFile except it takes changes of type // [][][]byte as the third argument. // Example: // [][][]byte{ // [][]byte{ // []byte("key"), []byte("value"), // } // } // Keys are what should be replaced and their values are the replacements. func copyModifiedFile(src, dst string, changes [][][]byte) { // Open source file. sf, err := os.Open(src) log.AssertNil(err) defer sf.Close() // Make sure the file will be closed. // Get the meta info of the source file. info, err := sf.Stat() log.AssertNil(err) // Read its content. d := make([]byte, info.Size()) _, err = sf.Read(d) log.AssertNil(err) // Make the required changes. for i := 0; i < len(changes); i++ { d = bytes.Replace(d, changes[i][0], changes[i][1], -1) } // Write the content to the destination file. err = ioutil.WriteFile(dst, d, info.Mode()) log.AssertNil(err) }
// ListenFile is equivalent of Listen but for files. // If file is added using ListenFile and the same file // is within a pattern of Listen, only the first one // will trigger restarts. // I.e. we have the following calls: // w.Listen("./", fn1) // w.ListenFile("./goal.yml", fn2) // If "goal.yml" file is modified fn2 will be triggered. // fn1 may be triggered by changes in any file inside // "./" directory except "goal.yml". func (t *Type) ListenFile(path string, fn func()) *fsnotify.Watcher { // Create a new watcher. w, err := fsnotify.NewWatcher() log.AssertNil(err) // Watch a directory instead of file. // See issue #17 of fsnotify to find out more // why we do this. dir := filepath.Dir(path) w.Add(dir) // Clean path and replace back slashes // to the normal ones. path = filepath.ToSlash(path) // Start watching process. t.files[path] = true go t.NotifyOnUpdate(path, w, fn) return w }
// Listen gets a pattern and a function. The function will be executed // when files matching the pattern will be modified. func (t *Type) Listen(pattern string, fn func()) *fsnotify.Watcher { // Create a new watcher. w, err := fsnotify.NewWatcher() log.AssertNil(err) // Find directories matching the pattern. ds := glob(pattern) // Add the files to the watcher. for i := range ds { log.Trace.Printf(`Adding "%s" to the list of watched directories...`, ds[i]) err := w.Add(ds[i]) if err != nil { log.Warn.Println(err) } } // Start watching process. go t.NotifyOnUpdate(filepath.ToSlash(pattern), w, fn) return w }
// start is an entry point of the generate handlers command. func start() { // Clean the out directory. log.Trace.Printf(`Removing "%s" directory if already exists...`, *output) err := os.RemoveAll(*output) log.AssertNil(err) // Start processing of controllers. ps := packages{} absInput, err := path.ImportToAbsolute(*input) log.AssertNil(err) absImport, err := path.AbsoluteToImport(absInput) log.AssertNil(err) absOutput, err := path.ImportToAbsolute(*output) log.AssertNil(err) absImportOut, err := path.AbsoluteToImport(absOutput) log.AssertNil(err) log.Trace.Printf(`Processing "%s" package...`, absImport) ps.processPackage(absImport) // Start generation of handler packages. tpl, err := path.ImportToAbsolute("github.com/colegion/goal/tools/generate/handlers/handlers.go.template") log.AssertNil(err) t := generation.NewType("", tpl) t.Extension = ".go" // Save generated files as a .go source. // Iterate through all available packages and generate handlers for them. log.Trace.Printf(`Starting generation of "%s" package...`, *pkg) for imp := range ps { // Check whether current package is the main one // and should be stored at the root directory or it is a subpackage. // // I.e. if --input is "./controllers" and --output is "./assets/handlers", // we are saving processed "./controllers" package to "./assets/handlers" // and some "github.com/colegion/smth" to "./assets/handlers/github.com/colegion/smth". out := *output if imp != absImport { out = filepath.Join(out, imp) } t.CreateDir(out) // Iterate over all available controllers, generate handlers package on // every of them. n := 0 for name := range ps[imp].data { // Find parent controllers of this controller. cs := []parent{} for i, p := range ps[imp].data[name].Parents { // Make sure it is a controller rather than just some embedded struct. check := p.Import if check == "" { // Embedded parent is a local structure. check = absImport } if _, ok := ps[check]; !ok { // Such package is not in the list of scanned ones. continue } if _, ok := ps[check].data[p.Name]; !ok { // There is no such controller. continue } // It is a valid parent controller, add it to the list. cs = append(cs, parent{ ID: i, Import: p.Import, Name: p.Name, }) } // Initialize parameters and generate a package. t.Package = strings.ToLower(name) t.Context = map[string]interface{}{ "after": action.MethodAfter, "before": action.MethodBefore, "initially": method.InitiallyName, "finally": method.FinallyName, "controller": ps[imp].data[name], "controllers": ps[imp].data, "import": imp, "input": input, "name": name, "outputImport": absImportOut, "output": output, "package": pkg, "parents": cs, "initFunc": ps[imp].init, "num": n, "actionImport": action.InterfaceImport, "actionInterface": action.Interface, "strconv": action.StrconvContext, } t.Generate() n++ } } }