1 /* 2 ------------------------------------------------------------------- 3 4 Copyright (C) 2014, Edwin van Leeuwen 5 6 This file is part of todod todo list manager. 7 8 Todod is free software; you can redistribute it and/or modify 9 it under the terms of the GNU General Public License as published by 10 the Free Software Foundation; either version 3 of the License, or 11 (at your option) any later version. 12 13 Todod is distributed in the hope that it will be useful, 14 but WITHOUT ANY WARRANTY; without even the implied warranty of 15 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 GNU General Public License for more details. 17 18 You should have received a copy of the GNU General Public License 19 along with Todod. If not, see <http://www.gnu.org/licenses/>. 20 21 ------------------------------------------------------------------- 22 */ 23 24 module todod.tag; 25 26 import std.json; 27 import std.conv; 28 import std.uuid; 29 30 import std.range; 31 import std.array; 32 import std.algorithm; 33 34 import todod.set; 35 import todod.storage; 36 37 version (unittest) { 38 import std.stdio; 39 } 40 41 class Tag { 42 string name; 43 UUID id; /// id is mainly used for syncing with habitrpg 44 45 this( string tag_name ) { 46 name = tag_name; 47 } 48 49 unittest { 50 Tag tag = new Tag("tag1"); 51 tag.id = randomUUID; 52 assert( tag.name == "tag1" ); 53 assert( !tag.id.empty ); 54 } 55 56 void opAssign( string tag_name ) { 57 name = tag_name; 58 id = UUID(); 59 } 60 61 /// If either id is uninitialized (empty) compare on name, otherwise on id 62 override bool opEquals(Object t) const { 63 auto otherTag = cast(Tag)(t); 64 if (id.empty || otherTag.id.empty) 65 return name == otherTag.name; 66 else 67 return id == otherTag.id; 68 } 69 70 unittest { 71 Tag tag1 = new Tag( "tag1" ); 72 Tag tag2 = new Tag( "tag2" ); 73 74 assert( tag1 != tag2 ); 75 tag1.id = randomUUID; 76 assert( tag1 != tag2 ); 77 tag2.id = tag1.id; 78 assert( tag1 == tag2 ); 79 80 tag1 = new Tag( "tag1" ); 81 tag2 = new Tag( "tag1" ); 82 83 assert( tag1 == tag2 ); 84 tag1.id = randomUUID; 85 assert( tag1 == tag2 ); 86 tag2.id = randomUUID; 87 assert( tag1 != tag2 ); 88 } 89 90 override int opCmp(Object t) const { 91 auto otherTag = cast(Tag)(t); 92 if ( this == otherTag ) 93 return 0; 94 else if ( name < otherTag.name ) 95 return -1; 96 return 1; 97 } 98 99 unittest { 100 Tag tag1 = new Tag( "tag1" ); 101 Tag tag2 = new Tag( "tag2" ); 102 103 assert( tag1 < tag2 ); 104 105 tag1 = "tag1"; 106 tag2 = "tag1"; 107 108 assert( !(tag1 < tag2) ); 109 assert( !(tag1 > tag2) ); 110 assert( (tag1 <= tag2) ); 111 assert( (tag1 >= tag2) ); 112 } 113 114 115 /// Turn into hash used by associative arrays. 116 /// Note that in rare cases (i.e. where one tag is id is initialized 117 /// and the other isn't this can lead to different hashes even though 118 /// opEquals returns equal. 119 override const nothrow size_t toHash() 120 { 121 if ( id.empty ) { 122 size_t hash; 123 foreach (char c; name) 124 hash = (hash * 9) + c; 125 return hash; 126 } 127 else 128 return id.toHash; 129 } 130 131 JSONValue toJSON() const { 132 JSONValue[string] json; 133 json["name"] = name; 134 json["id"] = id.toString; 135 return JSONValue( json ); 136 } 137 138 unittest { 139 Tag tag1 = new Tag( "tag1" ); 140 tag1 = "tag1"; 141 assert( tag1.toJSON["name"].str == "tag1" ); 142 } 143 144 unittest { 145 Tag tag1 = new Tag( "tag1" ); 146 assert( parseJSON(tag1.toJSON()).name == "tag1" ); 147 } 148 149 static Tag parseJSON( in JSONValue json ) { 150 Tag tag = new Tag( json["name"].str ); 151 tag.id = parseUUID( json["id"].str ); 152 return tag; 153 } 154 155 unittest { 156 // Do uniq and sort work properly? 157 Tag tag1 = new Tag( "tag1" ); 158 Tag tag2 = new Tag( "tag2" ); 159 160 Tag[] ts = [ tag2, tag1 ]; 161 sort( ts ); 162 assert( equal( ts, [ tag1, tag2 ] ) ); 163 164 ts = [ tag1, tag2, tag1 ]; 165 sort( ts ); 166 assert( equal( ts, [ tag1, tag1, tag2 ] ) ); 167 assert( equal( uniq(ts).array, [ tag1, tag2 ] ) ); 168 169 tag2.id = randomUUID; 170 tag1.id = tag2.id; 171 172 ts = [ tag1, tag2, tag1 ]; 173 sort( ts ); 174 assert( equal( ts[1].name, "tag2" ) ); 175 assert( equal( uniq(ts).array, [ tag1 ] ) ); 176 } 177 178 } 179 180 struct TagDelta { 181 Tags add_tags; 182 Tags delete_tags; 183 } 184 185 /// A sorted, unique set implementation for Tags 186 /// Currently based on simple list, so not very efficient 187 alias Tags = Set!Tag; 188 189 unittest { // Test for doubles 190 Tags tgs; 191 Tag tag3 = new Tag("tag3"); 192 tgs.add( tag3 ); 193 Tag tag4 = new Tag("tag4"); 194 tgs.add( tag4 ); 195 assert( equal( tgs.array, [ tag3, tag4 ] ) ); 196 assert( tgs.length == 2 ); 197 198 // Doubles 199 tgs.add( tag3 ); 200 assert( equal( tgs.array, [ tag3, tag4 ] ) ); 201 assert( tgs.length == 2 ); 202 203 // Sorted 204 Tag tag2 = new Tag("tag2"); 205 tgs.add( tag2 ); 206 assert( equal( tgs.array, [ tag2, tag3, tag4 ] ) ); 207 assert( tgs.length == 3 ); 208 209 // Add tag with same name, but with id set 210 Tag tag5 = new Tag("tag2"); 211 tag5.id = randomUUID; 212 assert( tgs.array.front.id.empty ); 213 tgs.add( tag5 ); 214 assert( !tgs.array.front.id.empty ); 215 assert( tgs.length == 3 ); 216 } 217 218 unittest { 219 Tags tgs; 220 Tag tag3 = new Tag("tag3"); 221 tgs.add( tag3 ); 222 Tag tag4 = new Tag("tag4"); 223 tgs.add( tag4 ); 224 assert( equal( tgs.array, [ tag3, tag4 ] ) ); 225 assert( tgs.length == 2 ); 226 227 tgs.remove( tag4 ); 228 assert( equal( tgs.array, [ tag3 ] ) ); 229 assert( tgs.length == 1 ); 230 231 tgs.remove( tag4 ); 232 assert( equal( tgs.array, [ tag3 ] ) ); 233 assert( tgs.length == 1 ); 234 } 235 236 Tags loadTags( GitRepo gr ) { 237 Tags tags; 238 239 auto tagsFileName = "tags.json"; 240 auto content = readFile( gr.workPath, tagsFileName ); 241 if (content != "") 242 tags = jsonToSet!(Tag)( std.json.parseJSON( content ), 243 (js) => Tag.parseJSON(js) ); 244 return tags; 245 } 246 247 void writeTags( Tags tags, GitRepo gr ) { 248 auto tagsFileName = "tags.json"; 249 JSONValue json = setToJSON!Tag( tags, 250 delegate( in Tag t ) {return t.toJSON();} ); 251 writeToFile( gr.workPath, tagsFileName, json.toPrettyString ); 252 commitChanges( gr, tagsFileName, "Updating tags file" ); 253 }