kanban.go (7612B)
1 // Package kanban implements Kanban logic. 2 // 3 // Kanban is Project oriented, where a Project holds the context for given set 4 // of Stages and Tickets. 5 // 6 // Projects are independent of each other. 7 // 8 // Notes: 9 // 10 // Project 11 // - represents some project that can be broken down into to discrete tasks, described by a name 12 // - each project has it's own arbitrary pipeline of stages with which tickets move through left-to-right 13 // - contains an ordered list of stages 14 // - stages are re-orderable 15 // - can be renamed 16 // - can be deleted 17 // 18 // Stage 19 // - represents an important part in the lifecycle of a task, described by a name 20 // - contains an ordered list of tickets 21 // - tickets are re-orderable 22 // - tickets can advance back and forth between stages, typically linearly 23 // - can be renamed 24 // - can be deleted 25 // 26 // Ticket 27 // - contains information about a task for a project 28 // - is unique to a Project and sits within one of it's stages 29 // - cannot occupy more than one stage 30 // - can be edited 31 // - can be deleted 32 package kanban 33 34 import ( 35 "fmt" 36 "time" 37 38 "github.com/google/uuid" 39 ) 40 41 // Project is a context for a given set of tickets. 42 type Project struct { 43 ID uuid.UUID 44 // Name of the project. 45 Name string 46 // Stages is the list of stages owned by the project. 47 Stages Stages 48 // Finalized is a psuedo stage that contains all finalized tickets. 49 Finalized []Ticket 50 } 51 52 // MakeStage assigns a ticket to the given stage. 53 func (p *Project) MakeStage(name string) { 54 p.Stages = append(p.Stages, Stage{ 55 Name: name, 56 }) 57 } 58 59 func (p *Project) ListStages() []Stage { 60 return p.Stages 61 } 62 63 func (p *Project) MoveStage(name string, dir Direction) bool { 64 return p.Stages.Swap(name, dir) 65 } 66 67 // AssignTicket assigns a ticket to the given stage. 68 func (p *Project) AssignTicket(stage string, ticket Ticket) error { 69 return p.Stages.Find(stage).Assign(ticket) 70 } 71 72 // Update an existing ticket. 73 // It is an error to attempt to update a ticket that does not exist. 74 func (p *Project) UpdateTicket(ticket Ticket) error { 75 for _, s := range p.Stages { 76 if s.Update(ticket) { 77 return nil 78 } 79 } 80 return fmt.Errorf("ticket does not exist: %v", ticket) 81 } 82 83 // ProgressTicket moves a ticket to the "next" stage. 84 func (p *Project) ProgressTicket(ticket Ticket) { 85 for ii, s := range p.Stages { 86 if s.Contains(ticket) { 87 if ii < len(p.Stages)-1 { 88 _ = p.Stages[ii+1].Assign(p.Stages[ii].Take(ticket)) 89 } 90 break 91 } 92 } 93 } 94 95 // RegressTicket moves a ticket to the "previous" stage. 96 func (p *Project) RegressTicket(ticket Ticket) { 97 for ii, s := range p.Stages { 98 if s.Contains(ticket) { 99 if ii > 0 { 100 _ = p.Stages[ii-1].Assign(p.Stages[ii].Take(ticket)) 101 } 102 break 103 } 104 } 105 } 106 107 // MoveTicket within a stage. 108 func (p *Project) MoveTicket(ticket Ticket, dir Direction) bool { 109 // @implement 110 return false 111 } 112 113 func (p *Project) ListTickets(stage string) []Ticket { 114 return p.Stages.Find(stage).Tickets 115 } 116 117 // StageForTicket returns the stage containing the specified ticket. 118 func (p *Project) StageForTicket(ticket Ticket) *Stage { 119 for ii, s := range p.Stages { 120 if s.Contains(ticket) { 121 return &p.Stages[ii] 122 } 123 } 124 return &Stage{} 125 } 126 127 // FinalizeTicket renders the ticket "complete" and moves it into an archive. 128 func (p *Project) FinalizeTicket(t Ticket) { 129 for ii, s := range p.Stages { 130 if s.Contains(t) { 131 p.Stages[ii].UnAssign(t) 132 p.Finalized = append(p.Finalized, t) 133 break 134 } 135 } 136 } 137 138 // Stage in the kanban pipeline, can hold a number of tickets. 139 type Stage struct { 140 Name string 141 Tickets []Ticket 142 } 143 144 // Assign appends a ticket to the stage with a unique ID. 145 // Existing tickets will be duplicated, but with different IDs. 146 func (s *Stage) Assign(ticket Ticket) error { 147 if ticket.ID == uuid.Nil { 148 id, err := uuid.NewUUID() 149 if err != nil { 150 return fmt.Errorf("generating ID: %v", err) 151 } 152 ticket.ID = id 153 ticket.Created = time.Now() 154 } 155 s.Tickets = append(s.Tickets, ticket) 156 return nil 157 } 158 159 // UnAssign removes a ticket from the stage. 160 func (s *Stage) UnAssign(ticket Ticket) { 161 for ii, t := range s.Tickets { 162 if t == ticket { 163 if len(s.Tickets) == 1 { 164 s.Tickets = []Ticket{} 165 } else { 166 s.Tickets = append(s.Tickets[:ii], s.Tickets[ii+1:]...) 167 } 168 } 169 } 170 } 171 172 // Stages is a list of Stage. 173 type Stages []Stage 174 175 // Swap the specified stage in the given direction. 176 // Returns false when at a boundary, and therefore no swap can occur. 177 func (stages *Stages) Swap(stage string, dir Direction) bool { 178 ii, ok := stages.Index(stage) 179 if !ok { 180 return false 181 } 182 if bounds := ii + dir.Next(); bounds < 0 || bounds > len(*stages)-1 { 183 return false 184 } 185 (*stages)[ii], (*stages)[ii+dir.Next()] = (*stages)[ii+dir.Next()], (*stages)[ii] 186 return true 187 } 188 189 // Find stage by name. 190 func (stages *Stages) Find(name string) *Stage { 191 for ii, s := range *stages { 192 if s.Name == name { 193 return &(*stages)[ii] 194 } 195 } 196 return &Stage{} 197 } 198 199 // Index returns the index postition for the stage, false if no stage exists. 200 func (stages *Stages) Index(name string) (int, bool) { 201 for ii, s := range *stages { 202 if s.Name == name { 203 return ii, true 204 } 205 } 206 return 0, false 207 } 208 209 // Take the specified ticket, if it exists. 210 // Removes it from the stage. 211 func (s *Stage) Take(ticket Ticket) Ticket { 212 s.UnAssign(ticket) 213 return ticket 214 } 215 216 // Contains returns true if the specified ticket exists in the stage. 217 func (s *Stage) Contains(ticket Ticket) bool { 218 for _, t := range s.Tickets { 219 if t == ticket { 220 return true 221 } 222 } 223 return false 224 } 225 226 // Update a ticket, returning a bool to indicate success. 227 // False means ticket does not exist and therefore nothing was updated. 228 func (s *Stage) Update(ticket Ticket) bool { 229 for ii, t := range s.Tickets { 230 if t.ID == ticket.ID { 231 s.Tickets[ii] = ticket 232 return true 233 } 234 } 235 return false 236 } 237 238 // Ticket in a stage. 239 type Ticket struct { 240 ID uuid.UUID 241 // Title of the ticket. 242 Title string 243 // Summary contains short and concise overview of the ticket. 244 Summary string 245 // Details contains the full details of the ticket. 246 Details string 247 // Created when the ticket was created. 248 Created time.Time 249 } 250 251 // Direction encodes mutually exclusive directions. 252 type Direction int8 253 254 const ( 255 Forward Direction = iota 256 Backward 257 ) 258 259 // Next returns the direction as a signed integer, where positive is forward. 260 func (dir Direction) Next() int { 261 switch dir { 262 case Forward: 263 return 1 264 case Backward: 265 return -1 266 } 267 return 0 268 } 269 270 // Invert returns the inverse of dir. 271 func (dir Direction) Invert() Direction { 272 switch dir { 273 case Forward: 274 return Backward 275 case Backward: 276 return Forward 277 } 278 return dir 279 } 280 281 func (p *Project) String() string { 282 if p == nil { 283 return "<nil>" 284 } 285 return fmt.Sprintf("%v", *p) 286 } 287 288 // Clone a project ensuring all data is copied. 289 func (p Project) Clone() Project { 290 var ( 291 stages = make([]Stage, len(p.Stages)) 292 finalized = make([]Ticket, len(p.Finalized)) 293 ) 294 copy(finalized, p.Finalized) 295 for ii, s := range p.Stages { 296 tickets := make([]Ticket, len(s.Tickets)) 297 copy(tickets, s.Tickets) 298 stages[ii] = Stage{ 299 Name: s.Name, 300 Tickets: tickets, 301 } 302 } 303 return Project{ 304 ID: p.ID, 305 Name: p.Name, 306 Stages: stages, 307 Finalized: finalized, 308 } 309 } 310 311 func (p *Project) Eq(other *Project) bool { 312 return p.ID == other.ID && 313 p.Name == other.Name && 314 p.Stages.Eq(other.Stages) 315 } 316 317 func (s Stages) Eq(other Stages) bool { 318 for ii := range s { 319 if !s[ii].Eq(other[ii]) { 320 return false 321 } 322 } 323 return true 324 } 325 326 func (s Stage) Eq(other Stage) bool { 327 if len(s.Tickets) != len(other.Tickets) { 328 return false 329 } 330 for ii, t := range s.Tickets { 331 if t != other.Tickets[ii] { 332 return false 333 } 334 } 335 return s.Name == other.Name 336 }