1 /**
2  * Parsing contents of ini-like files via range-based interface.
3  * Authors:
4  *  $(LINK2 https://github.com/FreeSlave, Roman Chistokhodov)
5  * Copyright:
6  *  Roman Chistokhodov, 2015-2016
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.range;
14 
15 import inilike.common;
16 
17 
18 /**
19  * Object for iterating through ini-like file entries.
20  */
21 struct IniLikeReader(Range) if (isInputRange!Range && isSomeString!(ElementType!Range) && is(ElementEncodingType!(ElementType!Range) : char))
22 {
23     /**
24      * Construct from other range of strings.
25      */
26     this(Range range)
27     {
28         _range = range;
29     }
30 
31     /**
32      * Iterate through lines before any group header. It does not check if all lines are comments or empty lines.
33      */
34     auto byLeadingLines()
35     {
36         return _range.until!(isGroupHeader);
37     }
38 
39     /**
40      * Object representing single group (section) being parsed in .ini-like file.
41      */
42     static struct Group(Range)
43     {
44         private this(Range range, ElementType!Range originalLine)
45         {
46             _range = range;
47             _originalLine = originalLine;
48         }
49 
50         /**
51          * Name of group being parsed (without brackets).
52          * Note: This can become invalid during parsing the Input Range
53          * (e.g. if string buffer storing this value is reused in later reads).
54          */
55         auto groupName() {
56             return parseGroupHeader(_originalLine);
57         }
58 
59         /**
60          * Original line of group header (i.e. name with brackets).
61          * Note: This can become invalid during parsing the Input Range
62          * (e.g. if string buffer storing this value is reused in later reads).
63          */
64         auto originalLine() {
65             return _originalLine;
66         }
67 
68         /**
69          * Iterate over group entries - may be key-value pairs as well as comments or empty lines.
70          */
71         auto byEntry()
72         {
73             return _range.until!(isGroupHeader);
74         }
75 
76     private:
77         ElementType!Range _originalLine;
78         Range _range;
79     }
80 
81     /**
82      * Iterate thorugh groups of .ini-like file.
83      * Returns: Range of Group objects.
84      */
85     auto byGroup()
86     {
87         static struct ByGroup
88         {
89             this(Range range)
90             {
91                 _range = range.find!(isGroupHeader);
92                 ElementType!Range line;
93                 if (!_range.empty) {
94                     line = _range.front;
95                     _range.popFront();
96                 }
97                 _currentGroup = Group!Range(_range, line);
98             }
99 
100             auto front()
101             {
102                 return _currentGroup;
103             }
104 
105             bool empty()
106             {
107                 return _currentGroup.groupName.empty;
108             }
109 
110             void popFront()
111             {
112                 _range = _range.find!(isGroupHeader);
113                 ElementType!Range line;
114                 if (!_range.empty) {
115                     line = _range.front;
116                     _range.popFront();
117                 }
118                 _currentGroup = Group!Range(_range, line);
119             }
120         private:
121             Group!Range _currentGroup;
122             Range _range;
123         }
124 
125         return ByGroup(_range.find!(isGroupHeader));
126     }
127 private:
128     Range _range;
129 }
130 
131 /**
132  * Convenient function for creation of IniLikeReader instance.
133  * Params:
134  *  range = input range of strings (strings must be without trailing new line characters)
135  * Returns: IniLikeReader for given range.
136  * See_Also: $(D iniLikeFileReader), $(D iniLikeStringReader)
137  */
138 auto iniLikeRangeReader(Range)(Range range)
139 {
140     return IniLikeReader!Range(range);
141 }
142 
143 ///
144 unittest
145 {
146     string contents =
147 `First comment
148 Second comment
149 [First group]
150 KeyValue1
151 KeyValue2
152 [Second group]
153 KeyValue3
154 KeyValue4
155 [Empty group]
156 [Third group]
157 KeyValue5
158 KeyValue6`;
159     auto r = iniLikeRangeReader(contents.splitLines());
160 
161     auto byLeadingLines = r.byLeadingLines;
162 
163     assert(byLeadingLines.front == "First comment");
164     assert(byLeadingLines.equal(["First comment", "Second comment"]));
165 
166     auto byGroup = r.byGroup;
167 
168     assert(byGroup.front.groupName == "First group");
169     assert(byGroup.front.originalLine == "[First group]");
170 
171 
172     assert(byGroup.front.byEntry.front == "KeyValue1");
173     assert(byGroup.front.byEntry.equal(["KeyValue1", "KeyValue2"]));
174     byGroup.popFront();
175     assert(byGroup.front.groupName == "Second group");
176     byGroup.popFront();
177     assert(byGroup.front.groupName == "Empty group");
178     assert(byGroup.front.byEntry.empty);
179     byGroup.popFront();
180     assert(byGroup.front.groupName == "Third group");
181     byGroup.popFront();
182     assert(byGroup.empty);
183 }
184 
185 /**
186  * Convenient function for reading ini-like contents from the file.
187  * Throws: $(B ErrnoException) if file could not be opened.
188  * Note: This function uses byLineCopy internally. Fallbacks to byLine on older compilers.
189  * See_Also: $(D iniLikeRangeReader), $(D iniLikeStringReader)
190  */
191 @trusted auto iniLikeFileReader(string fileName)
192 {
193     import std.stdio : File;
194     static if( __VERSION__ < 2067 ) {
195         return iniLikeRangeReader(File(fileName, "r").byLine().map!(s => s.idup));
196     } else {
197         return iniLikeRangeReader(File(fileName, "r").byLineCopy());
198     }
199 }
200 
201 /**
202  * Convenient function for reading ini-like contents from string.
203  * Note: on frontends < 2.067 it uses splitLines thereby allocates strings.
204  * See_Also: $(D iniLikeRangeReader), $(D iniLikeFileReader)
205  */
206 @trusted auto iniLikeStringReader(String)(String contents) if (isSomeString!String && is(ElementEncodingType!String : char))
207 {
208     static if( __VERSION__ < 2067 ) {
209         return iniLikeRangeReader(contents.splitLines());
210     } else {
211         return iniLikeRangeReader(contents.lineSplitter());
212     }
213 }