issue.go 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272
  1. // The package provides a series of data structures for interaction with and
  2. // interpolation of issues.
  3. //
  4. // At its most basic level, an issue is a simple collection of strings of text,
  5. // with no specific implementation requirements, aside from their existance.
  6. // As per spec, issues are simply folders with specific named plain text files.
  7. // Two of those files require the files to have data, while the rest do not require
  8. // associated data.
  9. //
  10. // The only two mandatory files to pass spec are the "description" file
  11. // ($ISSUE_PATH/description) and the "status" file ($ISSUE_PATH/status):
  12. //
  13. // - the description file:
  14. //
  15. // An issue's description should have, at the least, a descriptive title.
  16. // The authors recommend following the Git Commit basic style guidelines.
  17. //
  18. // - the status file:
  19. //
  20. // An issue's status file should contain at minimum a human readable one
  21. // line description of the status' current state. For example, "open" or
  22. // "closed." The authors recommend using a semicolon to separate assignments
  23. // or related goals. For example, "assigned:arianagiroux" or "bug:critical".
  24. //
  25. // These couplings of files and subsequent file data are represented via the Field
  26. // struct. Each Field struct has the following fields:
  27. //
  28. // - The file's path (Field.Path), relative to the issue's root path, and
  29. //
  30. // - The file's contents (Field.Data)
  31. //
  32. // Example:
  33. //
  34. // ...
  35. // // define a new Field
  36. // description := Field.New(data_from_file, path_relative_to_root)
  37. // // OR
  38. // description := Field.NewFromPath(path_to_file) // NOT IMPLEMENTED AS OF 0.0.3
  39. //
  40. // The spec additionally outlines optional implementations of "tags" and
  41. // "blockers". What the developer uses these for is up to them. Both are treated
  42. // similarly, where tags are located in a directory named "tags", and blockers
  43. // are located in a directory named "blockedby". The spec does not outline any
  44. // use for the content of these plain files, and (as of 0.0.3) issues does not
  45. // load the data from these files.
  46. //
  47. // Note: the files within the "blockedby" folder are referred to as both "Blockers"
  48. // and "Blockedby" interchangeably throughout the package, where applicable.
  49. //
  50. // The package defines the struct "VariadicField" to facilitate both "tags" and
  51. // "blockers". Theoretically, it can facilitate any folder that matches the
  52. // restraints as outlined by the spec. A VariadicField is, at its core, a slice
  53. // of Field objects with an associated path. This is ensure that it's underlying
  54. // []Field object is associated with a path on disk.
  55. //
  56. // Example:
  57. //
  58. // ...
  59. // // define some Fields
  60. // fields := []Field{{path:"tag1"}, {path:"tag2"}}
  61. // // define a new VariadicField
  62. // tags := VariadicField.New(fields, "/tags")
  63. // // OR
  64. // tags := VariadicField{Fields:fields, Path:"/tags"}
  65. // // OR
  66. // tags := VariadicField{path:"/tags"}.NewFromPath("an_issue/tags")
  67. //
  68. // The Issue struct utilizes both Field and VariadicField structs to represent
  69. // a full spec compliant issue. While it is possible to instantiate the struct
  70. // directly, it is not recommended. The struct provides two constructor functions,
  71. // which handles parsing spec confoming issue "titles" (read: root issue path)
  72. // as human readable strings.
  73. //
  74. // Example:
  75. //
  76. // ...
  77. // // data prep
  78. // description := Field{Path:"/description", Data:"a descriptive blurb"}
  79. // status := Field{Path:"/status", Data:"open"}
  80. // tags := VariadicField{Path:"/tags"}
  81. // blockers := VariadicField{Path:"/blockedby"}
  82. // path := "issues/an_issue"
  83. // ...
  84. // // new Issue with custom title
  85. // customTitleIssue := Issue.New(Issue{}, "A Custom Title",
  86. // description, status, tags, blockers, path)
  87. // // new Issue, automatic title
  88. // autoTitleIssue := Issue.New(Issue{}, "",
  89. // description, status, tags, blockers, path)
  90. // // new Issue from path on disk
  91. // issueFromDisk := Issue.NewFroMPath(Issue{}, "issues/an_issue")
  92. //
  93. // Finally, the package defines a simple data structure for a collection of
  94. // issues: the "IssueCollection". This is a simple slice of Issues ([]Issue).
  95. package issues
  96. import (
  97. "os"
  98. "path/filepath"
  99. "strings"
  100. )
  101. // Issue Definitions ------------------------------------------------------------
  102. // ----------------------------------------------------------------------------
  103. type Issue struct {
  104. Title string // The title of the bug in human readable format
  105. Description Field // The description of the bug
  106. Status Field // The status of the bug
  107. Tags VariadicField // A slice of VariadicFields
  108. Blockedby VariadicField // A slice of VariadicFields
  109. Path string // The path to the bug
  110. // machineTitle string // The machine parseable bug title
  111. }
  112. // Constrcutor for Issues
  113. func (i Issue) New(title string, description Field, status Field, tags VariadicField, blockedby VariadicField, path string) (issue Issue, err error) {
  114. if len(title) == 0 {
  115. title = parsePathToHuman(path)
  116. }
  117. return Issue{
  118. Title: title,
  119. Description: description,
  120. Status: status,
  121. Tags: tags,
  122. Blockedby: blockedby,
  123. Path: path,
  124. }, err
  125. }
  126. // Constrcutor for Issues that loads relevant data from disk
  127. func (i Issue) NewFromPath(path string) (bug Issue, err error) {
  128. // Required Fields
  129. description := &Field{Path: "/description"}
  130. status := &Field{Path: "/status"}
  131. requiredFields := []*Field{description, status}
  132. for _, field := range requiredFields {
  133. data, err := readPath(path + field.Path)
  134. if err != nil {
  135. return Issue{}, err
  136. }
  137. field.Data = strings.TrimRight(data, "\n")
  138. }
  139. // Variadic Fields
  140. tags := VariadicField{Path: "/tags"}
  141. blockers := VariadicField{Path: "/blockedby"}
  142. tags, _ = VariadicField.NewFromPath(tags, path)
  143. blockers, _ = VariadicField.NewFromPath(blockers, path)
  144. // we can ignore the errors, as loadVariadicField already gracefully handles
  145. //them.
  146. // title from path
  147. title := parsePathToHuman(path)
  148. return Issue{
  149. Title: title,
  150. Description: *description,
  151. Status: *status,
  152. Tags: tags,
  153. Blockedby: blockers,
  154. Path: path,
  155. }, err
  156. }
  157. // Field Definitions ----------------------------------------------------------
  158. // ----------------------------------------------------------------------------
  159. // A struct representing data that is tied to data on disk
  160. type Field struct {
  161. Path string
  162. Data string
  163. }
  164. // Constructor for Fields
  165. func (f Field) New(data string, path string) Field { return Field{Data: data, Path: path} }
  166. // VariadicField Definitions --------------------------------------------------
  167. // ----------------------------------------------------------------------------
  168. // VariadicFields hold lists of Field objects.
  169. type VariadicField struct {
  170. Path string // The associated path on disk of Fields represented by Variadic Field
  171. Fields []Field // The underlying slice of Field objects
  172. }
  173. // Constructor for VariadicFields
  174. func (vf VariadicField) New(fields []Field, path string) VariadicField {
  175. return VariadicField{Fields: fields, Path: path}
  176. }
  177. // Constructor for VariadicFields that loads relevant data from disk
  178. func (vf VariadicField) NewFromPath(pathOnDisk string) (v VariadicField, err error) {
  179. rootPath := pathOnDisk + "/" + vf.Path
  180. files, err := os.ReadDir(rootPath)
  181. if err != nil {
  182. return vf, err
  183. }
  184. for _, file := range files {
  185. data, err := readPath(rootPath + "/" + file.Name())
  186. if err != nil {
  187. return vf, err
  188. }
  189. if file.Name()[0:1] == "." {
  190. continue
  191. }
  192. vf.Fields = append(vf.Fields, Field{Data: strings.TrimRight(data, "\n"), Path: file.Name()})
  193. }
  194. return vf, err
  195. }
  196. // IssueCollection Definitions ------------------------------------------------
  197. // ----------------------------------------------------------------------------
  198. type IssueCollection struct {
  199. Collection []Issue
  200. Path string
  201. selection int
  202. }
  203. func (ic IssueCollection) NewFromPath(path string) (collection IssueCollection, err error) {
  204. ic.Path = path
  205. files, err := os.ReadDir(path)
  206. if err != nil {
  207. return IssueCollection{}, err
  208. }
  209. for _, file := range files {
  210. issuePath := path + "/" + file.Name()
  211. if IsIssue(issuePath) {
  212. issue, err := Issue.NewFromPath(Issue{}, issuePath)
  213. if err != nil {
  214. continue
  215. }
  216. ic.Collection = append(ic.Collection, issue)
  217. }
  218. }
  219. return ic, nil
  220. }
  221. // Util Definitions -----------------------------------------------------------
  222. // ----------------------------------------------------------------------------
  223. // Parses human readable strings as path strings
  224. func parseHumanToPath(humanReadable string) string {
  225. var out string
  226. out = strings.ReplaceAll(humanReadable, "-", "\\replace/")
  227. out = strings.ReplaceAll(out, " ", "-")
  228. out = strings.ReplaceAll(out, "\\replace/", "--")
  229. return out
  230. }
  231. // Parses machine parseable paths as human readable strings
  232. func parsePathToHuman(path string) string {
  233. _, last := filepath.Split(filepath.Clean(path))
  234. last = strings.ReplaceAll(last, "--", "\\replace/")
  235. last = strings.ReplaceAll(last, "-", " ")
  236. last = strings.ReplaceAll(last, "\\replace/", "-")
  237. return last
  238. }