issue.go 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283
  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. func (vf VariadicField) AsString() string {
  197. var output string
  198. for _, field := range vf.Fields {
  199. output = output + strings.TrimRight(field.Path, " \t\n") + ", "
  200. }
  201. output = strings.TrimRight(output, ", ")
  202. return output
  203. }
  204. // IssueCollection Definitions ------------------------------------------------
  205. // ----------------------------------------------------------------------------
  206. type IssueCollection struct {
  207. Collection []Issue
  208. Path string
  209. selection int
  210. }
  211. func (ic IssueCollection) NewFromPath(path string) (collection IssueCollection, err error) {
  212. ic.Path = path
  213. files, err := os.ReadDir(path)
  214. if err != nil {
  215. return IssueCollection{}, err
  216. }
  217. for _, file := range files {
  218. issuePath := path + "/" + file.Name()
  219. if IsIssue(issuePath) {
  220. issue, err := Issue.NewFromPath(Issue{}, issuePath)
  221. if err != nil {
  222. continue
  223. }
  224. ic.Collection = append(ic.Collection, issue)
  225. }
  226. }
  227. return ic, nil
  228. }
  229. // Util Definitions -----------------------------------------------------------
  230. // ----------------------------------------------------------------------------
  231. // Parses human readable strings as path strings
  232. func parseHumanToPath(humanReadable string) string {
  233. var out string
  234. out = strings.ReplaceAll(humanReadable, "-", "\\replace/")
  235. out = strings.ReplaceAll(out, " ", "-")
  236. out = strings.ReplaceAll(out, "\\replace/", "--")
  237. return out
  238. }
  239. // Parses machine parseable paths as human readable strings
  240. func parsePathToHuman(path string) string {
  241. _, last := filepath.Split(filepath.Clean(path))
  242. last = strings.ReplaceAll(last, "--", "\\replace/")
  243. last = strings.ReplaceAll(last, "-", " ")
  244. last = strings.ReplaceAll(last, "\\replace/", "-")
  245. return last
  246. }