1 // Copyright © 2013, Peter Wood.
2 
3 module properd;
4 
5 import std.conv, std.file, std..string;
6 import std.stdio : File;
7 import std.algorithm : map;
8 import std.array;
9 
10 /**
11  * An exception class for the errors generated by the proper-d code.
12  */
13 class PropertyException : Exception {
14    this(string message, Throwable thrown=null) {
15       super(message, thrown);
16    }
17 }
18 
19 /**
20  * This function reads in the contents of a file before passing it to the
21  * parseProperties() function for processing.
22  *
23  * Returns:  The associative array returned by parseProperties().
24  *
25  * Params:
26  *    path =  A string containing the path and name of the file to be read.
27  */
28 string[string] readProperties(string path) {
29    if(!exists(path)) {
30       throw(new PropertyException(text("The '", path, "' file does not exist.")));
31    }
32    return(parseProperties(readText(path)));
33 }
34 
35 /**
36  * This function reads in the contents of a file before passing it to the
37  * parseProperties() function for processing.
38  *
39  * Returns:  The associative array returned by parseProperties().
40  *
41  * Params:
42  *    path =  A string containing the path and name of the file to be read.
43  */
44 string[string] readProperties(File* file) {
45    return(readProperties(file.name));
46 }
47 
48 /**
49  * This function takes a string of text and attempts to interpret it using the
50  * following set of rules...
51  *
52  *   1. Blank lines are ignored.
53  *   2. Lines that start (after the removal of leading whitespace) with a '#'
54  *      are ignored.
55  *   3. All other lines will first be stripped of leading and trailing
56  *      whitespace then searched for a '=' and, if that is not found, a ':'.
57  *      If neither of these is found an exception is thrown. If one of them is
58  *      found then the line is split around the first occurrence with the part
59  *      of the left hand side becoming a key and the part on the right hand
60  *      side become a value in the associative array generated.
61  *
62  * Returns:  An associative array of strings indexed by strings.
63  *
64  * Params:
65  *    input =  The string containing the text to be parsed.
66  */
67 string[string] parseProperties(string input) {
68    string[string] output;
69 
70    if(input.length > 0) {
71       uint  count;
72 
73       foreach(line; input.splitLines()) {
74          line = line.strip();
75          count++;
76          if(line.length > 0) {
77             if(line[0..1] != "#") {
78                auto index = line.indexOf("=");
79 
80                if(index == -1) {
81                   index = line.indexOf(":");
82                }
83 
84                if(index == -1) {
85                   throw(new PropertyException(text("Syntax error on line ", count, " of property data.")));
86                }
87 
88                output[line[0..index].strip()] = line[(index + 1)..$].strip();
89             }
90          }
91       }
92    }
93 
94    return(output);
95 }
96 
97 /**
98  * Convenience function that attempts to extract a property value from a
99  * property list (an associative array of strings indexed by strings) and
100  * convert it to a specified type.
101  *
102  * Params:
103  *    properties =   The associative array to locate the property in.
104  *    name =         The name the value is currently keyed under.
105  *    alternative =  The value to return in the case that the properties list
106  *                   doesn't contain the named property. Defaults to whatever
107  *                   the default initialization value for the desired type
108  *                   is.
109  */
110 T as(T)(string[string] properties, string name, T alternative=T.init) {
111    T result = alternative;
112    string value  = (name in properties ? properties[name] : null);
113 
114    if(value !is null) {
115       try {
116          result = to!(T)(value);
117       } catch(Exception exception) {
118          throw(new PropertyException(text("Cannot convert the value of the '", name, "' property ('", value, "') to a ", typeid(value), ".")));
119       }
120    }
121 
122    return(result);
123 }
124 
125 /**
126  * Template specialization of the as() function for generating boolean values.
127  * This function will recognise "true", "1", "yes" and "on" as values that
128  * convert to a boolean true, with everything else converting to false.
129  */
130 bool as(T : bool)(string[string] properties, string name, T alternative=T.init) {
131    bool   result = alternative;
132    string value  = (name in properties ? properties[name] : null);
133 
134    if(value !is null) {
135       value  = value.toLower();
136       result = (value == "true" || value == "1" || value == "yes" || value == "on");
137    }
138 
139    return(result);
140 }
141 
142 /**
143  * Convenience function that attempts to extract a property value from a
144  * property list (an associative array of strings indexed by strings),
145  * split the value with separater, convert each value to a specified type
146  * and return as an array.
147  *
148  * Params:
149  *    properties =   The associative array to locate the property in.
150  *    name =         The name the value is currently keyed under.
151  *    sep =          value separater
152  */
153 T[] asArray(T)(string[string] properties, string name, string sep = ",") {
154    T[] result;
155    string value  = (name in properties ? properties[name] : null);
156 
157    if(value !is null) {
158       try {
159          result = value.split(sep).map!strip.map!(to!(T)).array;
160       } catch(Exception exception) {
161          throw(new PropertyException(text("Cannot convert the value of the '", name, "' property ('", value, "') to a ", typeid(value), ".")));
162       }
163    }
164 
165    return(result);
166 }
167