1 /**
2  * Parsing contents of ini-like files via range-based interface.
3  * Authors: 
4  *  $(LINK2 https://github.com/MyLittleRobo, 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 && is(ElementType!Range : const(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 byFirstLines()
35     {
36         return _range.until!(isGroupHeader);
37     }
38     
39     /**
40      * Iterate thorugh groups of ini-like file.
41      */
42     auto byGroup()
43     {   
44         static struct ByGroup
45         {
46             static struct Group
47             {
48                 this(Range range, ElementType!Range originalLine)
49                 {
50                     _range = range;
51                     _originalLine = originalLine;
52                 }
53                 
54                 auto name() {
55                     return parseGroupHeader(_originalLine);
56                 }
57                 
58                 auto originalLine() {
59                     return _originalLine;
60                 }
61                 
62                 auto byEntry()
63                 {
64                     return _range.until!(isGroupHeader);
65                 }
66                 
67             private:
68                 ElementType!Range _originalLine;
69                 Range _range;
70             }
71             
72             this(Range range)
73             {
74                 _range = range.find!(isGroupHeader);
75                 ElementType!Range line;
76                 if (!_range.empty) {
77                     line = _range.front;
78                     _range.popFront();
79                 }
80                 _currentGroup = Group(_range, line);
81             }
82             
83             auto front()
84             {
85                 return _currentGroup;
86             }
87             
88             bool empty()
89             {
90                 return _currentGroup.name.empty;
91             }
92             
93             void popFront()
94             {
95                 _range = _range.find!(isGroupHeader);
96                 ElementType!Range line;
97                 if (!_range.empty) {
98                     line = _range.front;
99                     _range.popFront();
100                 }
101                 _currentGroup = Group(_range, line);
102             }
103         private:
104             Group _currentGroup;
105             Range _range;
106         }
107         
108         return ByGroup(_range.find!(isGroupHeader));
109     }
110     
111 private:
112     Range _range;
113 }
114 
115 /**
116  * Convenient function for creation of IniLikeReader instance.
117  * Params:
118  *  range = input range of strings (strings must be without trailing new line characters)
119  * Returns: IniLikeReader for given range.
120  * See_Also: iniLikeFileReader, iniLikeStringReader
121  */
122 auto iniLikeRangeReader(Range)(Range range)
123 {
124     return IniLikeReader!Range(range);
125 }
126 
127 ///
128 unittest
129 {
130     string contents = 
131 `First comment
132 Second comment
133 [First group]
134 KeyValue1
135 KeyValue2
136 [Second group]
137 KeyValue3
138 KeyValue4
139 [Empty group]
140 [Third group]
141 KeyValue5
142 KeyValue6`;
143     auto r = iniLikeRangeReader(contents.splitLines());
144     
145     auto byFirstLines = r.byFirstLines;
146     
147     assert(byFirstLines.front == "First comment");
148     assert(byFirstLines.equal(["First comment", "Second comment"]));
149     
150     auto byGroup = r.byGroup;
151     
152     assert(byGroup.front.name == "First group");
153     assert(byGroup.front.originalLine == "[First group]");
154     //assert(byGroup.map!(g => g.name).equal(["First group", "Second group", "Empty group", "Third group"]));
155     
156     
157     assert(byGroup.front.byEntry.front == "KeyValue1");
158     assert(byGroup.front.byEntry.equal(["KeyValue1", "KeyValue2"]));
159     byGroup.popFront();
160     assert(byGroup.front.name == "Second group");
161     byGroup.popFront();
162     assert(byGroup.front.name == "Empty group");
163     assert(byGroup.front.byEntry.empty);
164     byGroup.popFront();
165     assert(byGroup.front.name == "Third group");
166     byGroup.popFront();
167     assert(byGroup.empty);
168 }
169 
170 /**
171  * Convenient function for reading ini-like contents from the file.
172  * Throws: $(B ErrnoException) if file could not be opened.
173  * Note: This function uses byLineCopy internally. Fallbacks to byLine on older compilers.
174  * See_Also: iniLikeRangeReader, iniLikeStringReader
175  */
176 @trusted auto iniLikeFileReader(string fileName)
177 {
178     import std.stdio : File;
179     static if( __VERSION__ < 2067 ) {
180         return iniLikeRangeReader(File(fileName, "r").byLine().map!(s => s.idup));
181     } else {
182         return iniLikeRangeReader(File(fileName, "r").byLineCopy());
183     }
184 }
185 
186 /**
187  * Convenient function for reading ini-like contents from string.
188  * Note: on frontends < 2.067 it uses splitLines thereby allocates strings.
189  * See_Also: iniLikeRangeReader, iniLikeFileReader
190  */
191 @trusted auto iniLikeStringReader(String)(String contents) if (is(String : const(char)[]))
192 {
193     static if( __VERSION__ < 2067 ) {
194         return iniLikeRangeReader(contents.splitLines());
195     } else {
196         return iniLikeRangeReader(contents.lineSplitter());
197     }
198 }