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 }