1 /**
2  * Reading ini-like files without usage of $(D inilike.file.IniLikeFile) class.
3  * Authors:
4  *  $(LINK2 https://github.com/FreeSlave, Roman Chistokhodov)
5  * Copyright:
6  *  Roman Chistokhodov, 2017
7  * License:
8  *  $(LINK2 http://www.boost.org/LICENSE_1_0.txt, Boost License 1.0).
9  * See_Also:
10  *  $(LINK2 http://standards.freedesktop.org/desktop-entry-spec/latest/index.html, Desktop Entry Specification)
11  */
12 
13 module inilike.read;
14 public import inilike.range;
15 public import inilike.exception;
16 import inilike.common;
17 
18 /// What to do when encounter some group name in onGroup callback of $(D readIniLike).
19 enum ActionOnGroup {
20     skip, /// Skip this group entries, don't do any processing.
21     proceed, /// Process the group entries as usual.
22     stopAfter, /// Stop after processing this group (don't parse next groups)
23 }
24 
25 /**
26  * Read ini-like file entries via the set of callbacks. Callbacks can be null, but basic format validation is still run in this case.
27  * Params:
28  *  reader = $(D inilike.range.IniLikeReader) object as returned by $(D inilike.range.iniLikeRangeReader) or similar function.
29  *  onLeadingComment = Delegate to call after leading comment (i.e. the one before any group) is read. The parameter is either comment of empty line.
30  *  onGroup = Delegate to call after group header is read. The parameter is group name (without brackets). Must return $(D ActionOnGroup). Providing the null callback is equal to providing the callback that always returns $(D ActionOnGroup.skip).
31  *  onKeyValue = Delegate to call after key-value entry is read and parsed. Parameters are key, value and the current group name. It's recommended to throw $(D inilike.exceptions.IniLikeEntryException) from this function in case if the key-value pair is invalid.
32  *  onCommentInGroup = Delegate to call after comment or empty line is read inside group section. The first parameter is either comment or empty line. The second parameter is the current group name.
33  *  fileName = Optional file name parameter to use in thrown exceptions.
34  * Throws:
35  *  $(D inilike.exception.IniLikeReadException) if error occured while parsing. Any exception thrown by callbacks will be transformed to $(D inilike.exception.IniLikeReadException).
36  */
37 void readIniLike(IniLikeReader)(IniLikeReader reader, scope void delegate(string) onLeadingComment = null, scope ActionOnGroup delegate(string) onGroup = null,
38         scope void delegate(string, string, string) onKeyValue = null, scope void delegate(string, string) onCommentInGroup = null, string fileName = null
39 ) {
40     size_t lineNumber = 0;
41 
42     version(DigitalMars) {
43         static void foo(size_t ) {}
44     }
45 
46     try {
47         foreach(line; reader.byLeadingLines)
48         {
49             lineNumber++;
50             if (line.isComment || line.strip.empty) {
51                 if (onLeadingComment !is null)
52                     onLeadingComment(line);
53             } else {
54                 throw new IniLikeException("Expected comment or empty line before any group");
55             }
56         }
57 
58         foreach(g; reader.byGroup)
59         {
60             lineNumber++;
61             string groupName = g.groupName;
62 
63             version(DigitalMars) {
64                 foo(lineNumber); //fix dmd codgen bug with -O
65             }
66 
67             auto actionOnGroup = onGroup is null ? ActionOnGroup.skip : onGroup(groupName);
68             final switch(actionOnGroup)
69             {
70                 case ActionOnGroup.stopAfter:
71                 case ActionOnGroup.proceed:
72                 {
73                     foreach(line; g.byEntry)
74                     {
75                         lineNumber++;
76 
77                         if (line.isComment || line.strip.empty) {
78                             if (onCommentInGroup !is null)
79                                 onCommentInGroup(line, groupName);
80                         } else {
81                             const t = parseKeyValue(line);
82 
83                             string key = t.key.stripRight;
84                             string value = t.value.stripLeft;
85 
86                             if (key.length == 0 && value.length == 0) {
87                                 throw new IniLikeException("Expected comment, empty line or key value inside group");
88                             } else {
89                                 if (onKeyValue !is null)
90                                     onKeyValue(key, value, groupName);
91                             }
92                         }
93                     }
94                     if (actionOnGroup == ActionOnGroup.stopAfter) {
95                         return;
96                     }
97                 }
98                 break;
99                 case ActionOnGroup.skip:
100                 {
101                     foreach(line; g.byEntry) {}
102                 }
103                 break;
104             }
105         }
106     }
107     catch(IniLikeEntryException e) {
108         throw new IniLikeReadException(e.msg, lineNumber, fileName, e, e.file, e.line, e.next);
109     }
110     catch (Exception e) {
111         throw new IniLikeReadException(e.msg, lineNumber, fileName, null, e.file, e.line, e.next);
112     }
113 }
114 
115 ///
116 unittest
117 {
118     string contents =
119 `# Comment
120 [ToSkip]
121 KeyInSkippedGroup=Value
122 [ToProceed]
123 KeyInNormalGroup=Value2
124 # Comment2
125 [ToStopAfter]
126 KeyInStopAfterGroup=Value3
127 # Comment3
128 [NeverGetThere]
129 KeyNeverGetThere=Value4
130 # Comment4`;
131     auto onLeadingComment = delegate void(string line) {
132         assert(line == "# Comment");
133     };
134     auto onGroup = delegate ActionOnGroup(string groupName) {
135         if (groupName == "ToSkip") {
136             return ActionOnGroup.skip;
137         } else if (groupName == "ToStopAfter") {
138             return ActionOnGroup.stopAfter;
139         } else {
140             return ActionOnGroup.proceed;
141         }
142     };
143     auto onKeyValue = delegate void(string key, string value, string groupName) {
144         assert((groupName == "ToProceed" && key == "KeyInNormalGroup" && value == "Value2") ||
145             (groupName == "ToStopAfter" && key == "KeyInStopAfterGroup" && value == "Value3"));
146     };
147     auto onCommentInGroup = delegate void(string line, string groupName) {
148         assert((groupName == "ToProceed" && line == "# Comment2") || (groupName == "ToStopAfter" && line == "# Comment3"));
149     };
150     readIniLike(iniLikeStringReader(contents), onLeadingComment, onGroup, onKeyValue, onCommentInGroup);
151     readIniLike(iniLikeStringReader(contents));
152 
153     import std.exception : assertThrown;
154     contents =
155 `Not a comment
156 [Group name]
157 Key=Value`;
158     assertThrown!IniLikeReadException(readIniLike(iniLikeStringReader(contents)));
159 }