| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267 |
- // 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
- }
- // IssueCollection Definitions ------------------------------------------------
- // ----------------------------------------------------------------------------
- type IssueCollection []Issue
- func (ic IssueCollection) NewFromPath(path string) (collection IssueCollection, err error) {
- 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
- }
- collection = append(collection, issue)
- }
- }
- return collection, 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
- }
|