// The package provides a series of data structures for interaction with and // interpolation of issues. // // At its most basic level, an issue is a simple collection of strings of text, // with no specific implementation requirements, aside from their existance. // As per spec, issues are simply folders with specific named plain text files. // Two of those files require the files to have data, while the rest do not require // associated data. // // The only two mandatory files to pass spec are the "description" file // ($ISSUE_PATH/description) and the "status" file ($ISSUE_PATH/status): // // - the description file: // // An issue's description should have, at the least, a descriptive title. // The authors recommend following the Git Commit basic style guidelines. // // - the status file: // // An issue's status file should contain at minimum a human readable one // line description of the status' current state. For example, "open" or // "closed." The authors recommend using a semicolon to separate assignments // or related goals. For example, "assigned:arianagiroux" or "bug:critical". // // These couplings of files and subsequent file data are represented via the Field // struct. Each Field struct has the following fields: // // - The file's path (Field.Path), relative to the issue's root path, and // // - The file's contents (Field.Data) // // Example: // // ... // // define a new Field // description := Field.New(data_from_file, path_relative_to_root) // // OR // description := Field.NewFromPath(path_to_file) // NOT IMPLEMENTED AS OF 0.0.3 // // The spec additionally outlines optional implementations of "tags" and // "blockers". What the developer uses these for is up to them. Both are treated // similarly, where tags are located in a directory named "tags", and blockers // are located in a directory named "blockedby". The spec does not outline any // use for the content of these plain files, and (as of 0.0.3) issues does not // load the data from these files. // // Note: the files within the "blockedby" folder are referred to as both "Blockers" // and "Blockedby" interchangeably throughout the package, where applicable. // // The package defines the struct "VariadicField" to facilitate both "tags" and // "blockers". Theoretically, it can facilitate any folder that matches the // restraints as outlined by the spec. A VariadicField is, at its core, a slice // of Field objects with an associated path. This is ensure that it's underlying // []Field object is associated with a path on disk. // // Example: // // ... // // define some Fields // fields := []Field{{path:"tag1"}, {path:"tag2"}} // // define a new VariadicField // tags := VariadicField.New(fields, "/tags") // // OR // tags := VariadicField{Fields:fields, Path:"/tags"} // // OR // tags := VariadicField{path:"/tags"}.NewFromPath("an_issue/tags") // // The Issue struct utilizes both Field and VariadicField structs to represent // a full spec compliant issue. While it is possible to instantiate the struct // directly, it is not recommended. The struct provides two constructor functions, // which handles parsing spec confoming issue "titles" (read: root issue path) // as human readable strings. // // Example: // // ... // // data prep // description := Field{Path:"/description", Data:"a descriptive blurb"} // status := Field{Path:"/status", Data:"open"} // tags := VariadicField{Path:"/tags"} // blockers := VariadicField{Path:"/blockedby"} // path := "issues/an_issue" // ... // // new Issue with custom title // customTitleIssue := Issue.New(Issue{}, "A Custom Title", // description, status, tags, blockers, path) // // new Issue, automatic title // autoTitleIssue := Issue.New(Issue{}, "", // description, status, tags, blockers, path) // // new Issue from path on disk // issueFromDisk := Issue.NewFroMPath(Issue{}, "issues/an_issue") // // Finally, the package defines a simple data structure for a collection of // issues: the "IssueCollection". This is a simple slice of Issues ([]Issue). package issues import ( "os" "path/filepath" "strings" ) // Issue Definitions ------------------------------------------------------------ // ---------------------------------------------------------------------------- type Issue struct { Title string // The title of the bug in human readable format Description Field // The description of the bug Status Field // The status of the bug Tags VariadicField // A slice of VariadicFields Blockedby VariadicField // A slice of VariadicFields Path string // The path to the bug // machineTitle string // The machine parseable bug title } // Constrcutor for Issues func (i Issue) New(title string, description Field, status Field, tags VariadicField, blockedby VariadicField, path string) (issue Issue, err error) { if len(title) == 0 { title = parsePathToHuman(path) } return Issue{ Title: title, Description: description, Status: status, Tags: tags, Blockedby: blockedby, Path: path, }, err } // Constrcutor for Issues that loads relevant data from disk func (i Issue) NewFromPath(path string) (bug Issue, err error) { // Required Fields description := &Field{Path: "/description"} status := &Field{Path: "/status"} requiredFields := []*Field{description, status} for _, field := range requiredFields { data, err := readPath(path + field.Path) if err != nil { return Issue{}, err } field.Data = strings.TrimRight(data, "\n") } // Variadic Fields tags := VariadicField{Path: "/tags"} blockers := VariadicField{Path: "/blockedby"} tags, _ = VariadicField.NewFromPath(tags, path) blockers, _ = VariadicField.NewFromPath(blockers, path) // we can ignore the errors, as loadVariadicField already gracefully handles //them. // title from path title := parsePathToHuman(path) return Issue{ Title: title, Description: *description, Status: *status, Tags: tags, Blockedby: blockers, Path: path, }, err } // Field Definitions ---------------------------------------------------------- // ---------------------------------------------------------------------------- // A struct representing data that is tied to data on disk type Field struct { Path string Data string } // Constructor for Fields func (f Field) New(data string, path string) Field { return Field{Data: data, Path: path} } // VariadicField Definitions -------------------------------------------------- // ---------------------------------------------------------------------------- // VariadicFields hold lists of Field objects. type VariadicField struct { Path string // The associated path on disk of Fields represented by Variadic Field Fields []Field // The underlying slice of Field objects } // Constructor for VariadicFields func (vf VariadicField) New(fields []Field, path string) VariadicField { return VariadicField{Fields: fields, Path: path} } // Constructor for VariadicFields that loads relevant data from disk func (vf VariadicField) NewFromPath(pathOnDisk string) (v VariadicField, err error) { rootPath := pathOnDisk + "/" + vf.Path files, err := os.ReadDir(rootPath) if err != nil { return vf, err } for _, file := range files { data, err := readPath(rootPath + "/" + file.Name()) if err != nil { return vf, err } if file.Name()[0:1] == "." { continue } vf.Fields = append(vf.Fields, Field{Data: strings.TrimRight(data, "\n"), Path: file.Name()}) } return vf, err } func (vf VariadicField) AsString() string { var output string for _, field := range vf.Fields { output = output + strings.TrimRight(field.Path, " \t\n") + ", " } output = strings.TrimRight(output, ", ") return output } // IssueCollection Definitions ------------------------------------------------ // ---------------------------------------------------------------------------- type IssueCollection struct { Collection []Issue Path string selection int } func (ic IssueCollection) NewFromPath(path string) (collection IssueCollection, err error) { ic.Path = path files, err := os.ReadDir(path) if err != nil { return IssueCollection{}, err } for _, file := range files { issuePath := path + "/" + file.Name() if IsIssue(issuePath) { issue, err := Issue.NewFromPath(Issue{}, issuePath) if err != nil { continue } ic.Collection = append(ic.Collection, issue) } } return ic, nil } // Util Definitions ----------------------------------------------------------- // ---------------------------------------------------------------------------- // Parses human readable strings as path strings func parseHumanToPath(humanReadable string) string { var out string out = strings.ReplaceAll(humanReadable, "-", "\\replace/") out = strings.ReplaceAll(out, " ", "-") out = strings.ReplaceAll(out, "\\replace/", "--") return out } // Parses machine parseable paths as human readable strings func parsePathToHuman(path string) string { _, last := filepath.Split(filepath.Clean(path)) last = strings.ReplaceAll(last, "--", "\\replace/") last = strings.ReplaceAll(last, "-", " ") last = strings.ReplaceAll(last, "\\replace/", "-") return last }