1 // This file is written in D programming language 2 /** 3 * Module defines facilities for custom styling of logger messages. Template mixin $(generateStyle) 4 * generates code for your logging styles (represented by an enum) that can be mixed into logger 5 * implementation. 6 * 7 * See $(B generateStyle) for detailed description. 8 * 9 * Example of default logger style: 10 * ------------------------------- 11 * import dlogg.log, dlogg.style; 12 * 13 * mixin generateStyle!(LoggingLevel 14 * , LoggingLevel.Debug, "Debug: %1$s", "[%2$s] Debug: %1$s" 15 * , LoggingLevel.Notice, "Notice: %1$s", "[%2$s] Notice: %1$s" 16 * , LoggingLevel.Warning, "Warning: %1$s", "[%2$s] Warning: %1$s" 17 * , LoggingLevel.Fatal, "Fatal: %1$s", "[%2$s] Fatal: %1$s" 18 * , LoggingLevel.Muted, "", "" 19 * ); 20 * ------------------------------- 21 * 22 * Copyright: © 2013-2014 Anton Gushcha 23 * License: Subject to the terms of the MIT license, as written in the included LICENSE file. 24 * Authors: NCrashed <ncrashed@gmail.com> 25 */ 26 module dlogg.style; 27 28 /** 29 * Enum type that is used in time formatting function in $(B generateStyle). 30 * 31 * Note: the type is exposed to allow user to implement its own time formatting 32 * functions. 33 * 34 * Example: 35 * -------- 36 * string myTimeFormatting(DistType t, SysTime time) 37 * { 38 * final switch(t) 39 * { 40 * case(DistType.Console): return time.toSimpleString(); 41 * case(DistType.File): return time.toISOExtString(); 42 * } 43 * } 44 * -------- 45 */ 46 enum DistType 47 { 48 Console, 49 File 50 } 51 52 /** 53 * Utility mixin template that generates facilities for output message formatting 54 * for console output and file output. 55 * 56 * $(B Style) parameter defines what type is used as logging level. It must be an enum. 57 * Order of values defines behavior of muting (styles that less current low border aren't 58 * printed to output). 59 * 60 * First value of $(B Args) could be a function/delegate for customizing time formatting. 61 * It should have return type of $(B string) and parameters of $(B (DistType t, SysTime time)). 62 * Where $(B t) tells when function/delegate is called to format output to file or console, 63 * $(B time) is a time that should be converted to string. 64 * 65 * Rest part of $(Args) has format of list of triples ($(B Style) value, string, string). Style 66 * value defines for which logging level following format strings are. First format string is used 67 * for console output, the second one is for file output. 68 * 69 * Format strings could use two arguments: '%1$s' is message that is passed to a logger and 70 * '%2$s' is current time string. Formatting is handled by $(B std.format) module. 71 */ 72 mixin template generateStyle(Style, Args...) 73 { 74 import std.array; 75 import std.traits; 76 import std.datetime; 77 import std.format; 78 import std.conv; 79 import dlogg.style; 80 81 static if(Args.length > 0 && isCallable!(Args[0])) 82 { 83 alias TS = Args[1 .. $]; 84 85 alias timeFormat = Args[0]; 86 alias Params = ParameterTypeTuple!timeFormat; 87 alias RetT = ReturnType!timeFormat; 88 static assert(is(RetT == string), text(&timeFormat, " should have return type of string but got ", RetT.stringof)); 89 static assert(Params.length == 2 && is(Params[0] == DistType) && is(Params[1] == SysTime), 90 text(&timeFormat, " should have two parameters of types (DistType, SysTime) but got ", Params.stringof)); 91 } else 92 { 93 alias TS = Args; 94 95 string timeFormat(DistType t, SysTime time) 96 { 97 return time.toISOExtString; 98 } 99 } 100 101 /// Could not see style symbol while using with external packages 102 mixin("import "~moduleName!Style~" : "~Style.stringof~";"); 103 104 static assert(is(Style == enum), "First parameter '"~Style.stringof~"' is expected to be an enum type!"); 105 static assert(checkTypes!TS, "logStyle expected triples of ('"~Style.stringof~"', string, string)"); 106 static assert(checkCoverage!TS, "logStyle triples doesn't cover all '"~Style.stringof~"' cases!"); 107 108 /// Checks types of US to be Style, string, string triples 109 private template checkTypes(US...) 110 { 111 static if(US.length == 0) 112 { 113 enum checkTypes = true; 114 } else static if(US.length < 3) 115 { 116 enum checkTypes = false; 117 } else 118 { 119 enum checkTypes = is(Unqual!(typeof(US[0])) == Style) && isSomeString!(typeof(US[1])) && isSomeString!(typeof(US[2])) 120 && checkTypes!(US[3..$]); 121 } 122 } 123 124 /// Checking that triples covers all Style members 125 private template checkCoverage(US...) 126 { 127 /// To be able to pass two expression tuples in one template 128 private template Wrapper(T...) 129 { 130 alias get = T; 131 } 132 133 /// Finding ZSS[0] in ZSS[1..$], false if not finded 134 private template findMember(ZSS...) 135 { 136 enum member = ZSS[0]; 137 alias ZS = ZSS[1..$]; 138 139 static if(ZS.length == 0) 140 { 141 enum findMember = false; 142 } else 143 { 144 static if(ZS[0] == member) 145 { 146 enum findMember = true; 147 } else 148 { 149 enum findMember = findMember!(member, ZS[1..$]); 150 } 151 } 152 } 153 154 /// Iterating over USS[0] to find each in USS[1] 155 /// Wrapper is used to wrap expression tuples in expression tuple 156 private template iterate(USS...) 157 { 158 alias EMembers = USS[0]; 159 alias SMembers = USS[1]; 160 161 static if(EMembers.get.length == 0) 162 { 163 enum iterate = true; 164 } else 165 { 166 enum iterate = findMember!(EMembers.get[0], SMembers.get) 167 && iterate!(Wrapper!(EMembers.get[1..$]), SMembers); 168 } 169 } 170 171 /// We interested in only each first value of each triple 172 /// creates new expression tuple from only first triple values of Style 173 private template filter(US...) 174 { 175 private template Tuple(E...) 176 { 177 alias Tuple = E; 178 } 179 180 static if(US.length == 0) 181 { 182 alias filter = Tuple!(); 183 } else static if(US.length < 3) 184 { 185 static assert(false, "US invalid size!"); 186 } else 187 { 188 alias filter = Tuple!(US[0], filter!(US[3..$])); 189 } 190 } 191 192 enum checkCoverage = iterate!(Wrapper!(EnumMembers!Style), Wrapper!(filter!US)); 193 } 194 195 private template genSwitch(USS...) 196 { 197 enum variable = USS[0]; 198 enum formatElemIndex = USS[1]; 199 enum messageVariable = USS[2]; 200 enum timeVariable = USS[3]; 201 alias US = USS[4..$]; 202 203 private template genCases(US...) 204 { 205 static if(US.length == 0) 206 { 207 enum genCases = ""; 208 } else static if(US.length < 3) 209 { 210 static assert(false, "US invalid size!"); 211 } else 212 { 213 enum genCases = "\tcase("~Style.stringof~"."~US[0].to!string~"):\n\t{\n\t\t" 214 ~ `writer.formattedWrite("`~US[formatElemIndex]~`", `~messageVariable~", "~timeVariable~");\n\t\t" 215 ~ "break;\n\t}\n" 216 ~ genCases!(US[3..$]); 217 } 218 } 219 220 enum genSwitch = "final switch("~variable~")\n{\n" 221 ~ genCases!(US) ~ "}\n"; 222 } 223 224 string formatConsoleOutput(string message, Style level) @trusted 225 { 226 auto timeString = timeFormat(DistType.Console, Clock.currTime); 227 auto writer = appender!string(); 228 229 //pragma(msg, genSwitch!("level", 1, "message", "timeString", TS)); 230 mixin(genSwitch!("level", 1, "message", "timeString", TS)); 231 232 return writer.data; 233 } 234 235 string formatFileOutput(string message, Style level) @trusted 236 { 237 auto timeString = timeFormat(DistType.File, Clock.currTime); 238 auto writer = appender!string(); 239 240 //pragma(msg, genSwitch!("level", 2, "message", "timeString", TS)); 241 mixin(genSwitch!("level", 2, "message", "timeString", TS)); 242 243 return writer.data; 244 } 245 } 246 /// Example of default style 247 unittest 248 { 249 import dlogg.log; 250 mixin generateStyle!(LoggingLevel 251 , LoggingLevel.Debug, "Debug: %1$s", "[%2$s] Debug: %1$s" 252 , LoggingLevel.Notice, "Notice: %1$s", "[%2$s] Notice: %1$s" 253 , LoggingLevel.Warning, "Warning: %1$s", "[%2$s] Warning: %1$s" 254 , LoggingLevel.Fatal, "Fatal: %1$s", "[%2$s] Fatal: %1$s" 255 , LoggingLevel.Muted, "", "" 256 ); 257 } 258 version(unittest) 259 { 260 enum MyLevel 261 { 262 Error, 263 Debug 264 } 265 } 266 /// Example of custom style 267 unittest 268 { 269 mixin generateStyle!(MyLevel 270 , MyLevel.Debug, "Debug: %1$s", "[%2$s] Debug: %1$s" 271 , MyLevel.Error, "Fatal: %1$s", "[%2$s] Fatal: %1$s" 272 ); 273 } 274 /// Example of custom time formatting 275 unittest 276 { 277 import std.datetime; 278 279 string myTimeFormatting(DistType t, SysTime time) 280 { 281 final switch(t) 282 { 283 case(DistType.Console): return time.toSimpleString(); 284 case(DistType.File): return time.toISOExtString(); 285 } 286 } 287 288 mixin generateStyle!(MyLevel, myTimeFormatting 289 , MyLevel.Debug, "Debug: %1$s", "[%2$s] Debug: %1$s" 290 , MyLevel.Error, "Fatal: %1$s", "[%2$s] Fatal: %1$s" 291 ); 292 }