1 module d_properties.properties;
2 
3 import object : Error;
4 import d_properties.reader;
5 import d_properties.writer;
6 
7 /** 
8  * The properties is a struct containing key-value pairs of strings, which can
9  * be easily and efficiently written to and read from a file.
10  */
11 public struct Properties {
12     /** 
13      * The internal values of this properties struct.
14      */
15     public string[string] values;
16 
17     /** 
18      * Constructs a Properties from the given values.
19      * Params:
20      *   valueMap = The associative array of properties.
21      */
22     public this(string[string] valueMap) {
23         foreach (key, value; valueMap) {
24             this.opIndexAssign(value, key);
25         }
26     }
27 
28     /** 
29      * Constructs a Properties by reading from each of the given files, in the
30      * order that they're provided. Note that properties in an earlier file
31      * will be overwritten by properties of the same key in later files.
32      * Params:
33      *   filenames = The list of filenames.
34      */
35     public this(string[] filenames ...) {
36         foreach (filename; filenames) {
37             Properties p = readFromFile(filename);
38             this.addAll(p);
39         }
40     }
41 
42     /** 
43      * Checks if the given property exists within this set.
44      * Params:
45      *   key = The property name.
46      * Returns: True if the property exists.
47      */
48     public bool has(string key) {
49         return cast(bool) (key in values);
50     }
51 
52     /** 
53      * Gets the value of a property, or returns the specified default value if
54      * the given property doesn't exist.
55      * Params:
56      *   key = The property name.
57      *   defaultValue = The default value to use, if no property exists.
58      * Returns: The value of the property, or the default value if the property
59      * doesn't exist.
60      */
61     public string get(string key, string defaultValue=null) const {
62         if (key !in values) return defaultValue;
63         return values[key];
64     }
65 
66     /** 
67      * Gets a property's value as a certain type. If the property does not
68      * exist, a `MissingPropertyException` is thrown. If the conversion could
69      * not be performed, a `std.conv.ConvException` is thrown.
70      * Params:
71      *   key = The property name.
72      * Returns: The value of the property.
73      */
74     public T get(T)(string key) const {
75         import std.conv : to;
76         if (key !in values) throw new MissingPropertyException(key);
77         return to!(T)(values[key]);
78     }
79 
80     /** 
81      * Gets a property's value as a certain type. If the property does not
82      * exist, a default value is returned. If the conversion could not be
83      * performed, a `std.conv.ConvException` is thrown.
84      * Params:
85      *   key = The property name.
86      *   defaultValue = The default value to use, if no property exists.
87      * Returns: The value of the property, or the default value if the property
88      * doesn't exist.
89      */
90     public T get(T)(string key, T defaultValue) const {
91         import std.conv : to;
92         if (key !in values) return defaultValue;
93         return to!(T)(values[key]);
94     }
95 
96     /** 
97      * Adds all properties from the given Properties to this one, overwriting
98      * any properties with the same name.
99      * Params:
100      *   properties = The properties to add to this one.
101      */
102     public void addAll(Properties[] properties ...) {
103         foreach (p; properties) {
104             foreach (key, value; p.values) {
105                 this.values[key] = value;
106             }
107         }
108     }
109 
110     /** 
111      * Adds all properties from the given files to this one.
112      * Params:
113      *   filenames = The names of files to read properties from.
114      */
115     public void addAll(string[] filenames ...) {
116         auto p = Properties(filenames);
117         this.addAll(p);
118     }
119 
120     /** 
121      * Gets a new set of properties containing only those whose names match
122      * the given prefix.
123      * Params:
124      *   prefix = The prefix to get properties for.
125      * Returns: A new properties containing any properties that match the given
126      * prefix.
127      */
128     public Properties getAll(string prefix) {
129         import std.algorithm : startsWith;
130         Properties p;
131         foreach (name, value; this.values) {
132             if (name.startsWith(prefix) && name.length > prefix.length) {
133                 size_t idx = prefix.length;
134                 if (name[idx] == '.' && name.length > prefix.length + 1) idx++;
135                 p[name[idx .. $]] = value;
136             }
137         }
138         return p;
139     }
140 
141     /** 
142      * Gets a set of properties whose names match the given prefix, and uses
143      * them to populate a struct of the given type.
144      * Params:
145      *   prefix = The prefix to get properties for.
146      * Returns: An instance of the given struct type.
147      */
148     public T getAll(T)(string prefix) {
149         static if (!__traits(isPOD, T)) {
150             assert(0, "Only Plain Old Data structs may be used to get all.");
151         }
152         import std.traits;
153         import std.conv : to;
154         auto props = getAll(prefix);
155         T t;
156         foreach (member; __traits(allMembers, T)) {
157             if (props.has(member)) {
158                 alias membertype = typeof(mixin("T()."~member));
159                 __traits(getMember, t, member) = to!(membertype)(props[member]);
160             }
161         }
162         return t;
163     }
164 
165     /** 
166      * Gets the value of a property, or throws a missing property exception.
167      * Params:
168      *   key = The property name.
169      * Returns: The value of the property.
170      */
171     string opIndex(string key) const {
172         if (key !in values) throw new MissingPropertyException(key);
173         return values[key];
174     }
175 
176     /** 
177      * Assigns the given value to a property.
178      * Params:
179      *   value = The value of the property.
180      *   key = The property name.
181      */
182     void opIndexAssign(string value, string key) {
183         values[key] = value;
184     }
185 
186     /** 
187      * Determines if this properties object is equal to the other.
188      * Params:
189      *   other = The other properties to check for equality with.
190      * Returns: True if these properties are the same as those given.
191      */
192     bool opEquals(const Properties other) const {
193         return this.values == other.values;
194     }
195 
196     /** 
197      * Implementation of the binary "in" operator to determine if a property is
198      * defined for this properties object.
199      * Params:
200      *   key = The name of a property.
201      * Returns: True if the property exists in this properties object.
202      */
203     bool opBinaryRight(string op)(string key) const if (op == "in") {
204         return (key in this.values) != null;
205     }
206 }
207 
208 /** 
209  * Exception that's thrown when attempting to index an unknown key.
210  */
211 class MissingPropertyException : Error {
212     this(string missingKey) {
213         super("Missing value for key \"" ~ missingKey ~ "\".");
214     }
215 }