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