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 }